本文分享自己在视频录制播放过程中遇到的一些问题,主要包括:
- 视频录制流程
- 视频预览及surfaceholder
- 视频清晰度及文件大小
- 视频文件旋转
一、视频录制流程
以微信为例,其录制触发为按下(住)录制按钮,结束录制的触发条件为松开录制按钮或录制时间结束,其流程大概可以用下图来描述。
1.1、开始录制
根据上述流程及项目的编程惯例,可在oncreate()定义如下函数来完成功能:
初始化过程主要包括view,data以及listener三部分。在初始化view时,添加摄像头预览,添加倒计时文本组件,设置初始状态ui组件的可见;初始化data时,从intent中获取初始数据;初始化listener时,分别对录制触发按钮,保存/取消视频录制按钮以及视频预览界面添加监听。
当系统初始化成功后,等待用户按下录制按钮,因此在录制按钮的监听中,需要完成以下功能:录制,计时,更新界面组件。
1
2
3
4
5
6
7
8
9
10
|
if (isrecording) { mmediarecorder.stop(); releasemediarecorder(); mcamera.lock(); isrecording = false ; } if (startrecordvideo()) { starttimevideorecord(); isrecording = true ; } |
首先判断当前录制状态,如果正在录制,则先停止录制,释放mediarecorder资源,锁定摄像头,置位录制状态;然后开始视频录制startrecordvideo,其boolean型返回值表征是否启动成功,启动成功后,开始视频录制计时,并且置位录制状态。startrecordvideo涉及mediarecorder的配置,准备以及启动。
翻译成代码如下:
1
2
3
4
5
6
7
8
|
private boolean startrecordvideo() { configuremediarecorder(); if (!prepareconfiguredmediarecorder()) { return false ; } mmediarecorder.start(); return true ; } |
1.2、结束录制
根据上述流程图可知,结束录制的触发条件为松开录制按钮或计时时间到。在结束录制方法中,需要释放mediarecorder,开始循环播放已录制视频,设置界面更新等。
翻译成代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private void stoprecordvideo() { releasemediarecorder(); // 录制视频文件处理 if (currentrecordprogress < min_record_time) { toast.maketext(videoinputactivity. this , "录制时间太短" , toast.length_short).show(); } else { startvideoplay(); isplaying = true ; setuidisplayaftervideorecordfinish(); } currentrecordprogress = 0 ; updateprogressbar(currentrecordprogress); releasetimer(); // 状态设置 isrecording = false ; } |
二、视频预览及surfaceholder
视频预览采用surfaceview,相比于普通的view,surfaceview在一个新起的单独线程中绘制画面,该实现的优点是更新画面不会阻塞ui主线程,缺点是会带来事件同步的问题。当然,这涉及到ui事件的传递以及线程同步。
在实现中,通过继承surfaceview组件来实现自定义预览控件。首先,surfaceview的getholder()方法会返回surfaceholder,需要为surfaceholder添加surfaceholder.callback回调;其次,重写surfacecreated、surfacechanged和surfacedestroyed实现。
2.1、构造器
构造器包含了初始化域以及添加上述回调的过程。
1
2
3
4
5
6
7
8
9
|
public camerapreview(context context, camera camera) { super (context); mcamera = camera; msupportedpreviewsizes = mcamera.getparameters().getsupportedpreviewsizes(); mholder = getholder(); mholder.addcallback( this ); mholder.settype(surfaceholder.surface_type_push_buffers); } |
这里需要说明msupportedpreviewsizes,由于摄像头支持的预览尺寸由camera本身的参数决定,因此需要首先获取其所支持的预览尺寸。
2.2、预览尺寸的设置
从google官方的camera示例程序中可以看出,选择预览尺寸的标准是(1)摄像头支持的预览尺寸的宽高比与surfaceview的宽高比的绝对差值小于0.1;(2)在(1)获得的尺寸中,选取与surfaceview的高的差值最小的。通过代码对这两个标准进行了实现,这里贴一下官方的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public camera.size getoptimalpreviewsize(list<camera.size> sizes, int w, int h) { final double aspect_tolerance = 0.1 ; double targetratio = ( double ) w / h; if (sizes == null ) { return null ; } camera.size optimalsize = null ; double mindiff = double .max_value; int targetheight = h; for (camera.size size : sizes) { double ratio = ( double ) size.width / size.height; if (math.abs(ratio - targetratio) > aspect_tolerance) continue ; if (math.abs(size.height - targetheight) < mindiff) { optimalsize = size; mindiff = math.abs(size.height - targetheight); } } if (optimalsize == null ) { mindiff = double .max_value; for (camera.size size : sizes) { if (math.abs(size.height - targetheight) < mindiff) { optimalsize = size; mindiff = math.abs(size.height - targetheight); } } } return optimalsize; } |
在加载预览画面时,需要考虑camera支持的尺寸(getsupportedpreviewsizes)和加载预览画面的surfaceview的尺寸(layout_width/layout_height),在预览阶段,两者之间的关系直接影响清晰度及图像拉伸。对于camera的尺寸,由于设备的硬件差异,不同设备支持的尺寸存在差异,但在默认情况(orientation=landscape)下,其width>height。以htc609d为例,camera支持的分辨率为1280*720(16:9)……640*480(4:3)……480*320(3:2)等十多种,而其屏幕的分辨率为960*540(16:9)。因此,很容易得到以下结论:(1)当camera预览尺寸小于surfaceview尺寸较多时,预览画面就不清晰;(2)camera预览尺寸宽高比与surfaceview宽高比相差较大时,预览画面就会拉伸。
上述代码在手机设置为横屏时并没有问题,在设置为竖屏时,为获得最优的预览尺寸,需要在调用此方法前比较surfaceview的宽高。
1
2
3
4
|
if (msupportedpreviewsizes != null ) { mpreviewsize = getoptimalpreviewsize(msupportedpreviewsizes, math.max(width, height), math.min(width, height)); } |
获得与当前surfaceview匹配的预览尺寸后,即可通过camera.parameters进行设置。
1
2
3
4
5
6
7
8
9
|
camera.parameters mparams = mcamera.getparameters(); mparams.setpreviewsize(mpreviewsize.width, mpreviewsize.height); mcamera.setdisplayorientation( 90 ); list<string> focusmodes = mparams.getsupportedfocusmodes(); if (focusmodes.contains( "continuous-video" )){ mparams.setfocusmode(camera.parameters.focus_mode_continuous_video); } mcamera.setparameters(mparams); |
三、视频清晰度及文件大小
在第一节中讲到startrecordvideo,包括配置mediarecorder,准备mediarecorder以及启动,其中配置mediarecorder是视频录制的重点,需要了解每项配置参数的作用,根据业务场景灵活配置。这里参考google官方的示例给出一个可行的配置方案,然后再对其进行解释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
private void configuremediarecorder() { // begin_include (configure_media_recorder) mmediarecorder = new mediarecorder(); // step 1: unlock and set camera to mediarecorder mcamera.unlock(); mmediarecorder.setcamera(mcamera); mmediarecorder.setorientationhint( 90 ); // step 2: set sources mmediarecorder.setaudiosource(mediarecorder.audiosource.voice_recognition); mmediarecorder.setvideosource(mediarecorder.videosource.camera); // step 3: set a camera parameters mmediarecorder.setoutputformat(mediarecorder.outputformat.mpeg_4); /* fixed video size: 640 * 480*/ mmediarecorder.setvideosize(640, 480); /* encoding bit rate: 1 * 1024 * 1024*/ mmediarecorder.setvideoencodingbitrate( 1 * 1024 * 1024 ); mmediarecorder.setvideoencoder(mediarecorder.videoencoder.h264); mmediarecorder.setaudioencoder(mediarecorder.audioencoder.aac); // step 4: set output file mmediarecorder.setmaxfilesize(maxfilesizeinbytes); mmediarecorder.setoutputfile(videofilepath); // end_include (configure_media_recorder) // set mediarecorder errorlistener mmediarecorder.setonerrorlistener( this ); } |
step 1:
1.setcamera参数能够使得在预览和录制中快速切换,避免camera对象的重新加载。在某些android手机自带的照相机程序中,切换预览与录制中的短暂卡顿,读者可自行体会。
2.mmediarecorder.setorientationhint(90)在录制方向为竖直(portrait)时使用,它能使视频文件的沿顺时针方向旋转90度,如果不设置此项,播放视频时,画面会发生90度的旋转。不过这里更重要的是,即使设置了此项,在某些播放器上,画面依然会有90度的旋转(比如将在手机上正常播放的视频导入到pc中进行播放,或者嵌入h5的video标签中),这可是为什么呢?注意setorientationhint的说明:note that some video players may choose to ignore the compostion matrix in a video during playback. 那么如何做到在所有播放器上都能以正常方向播放呢?稍等,后续专门对其进行说明。
step 2:
1.setaudiosource(mediarecorder.audiosource.voice_recognition),voice_recognition相比于mic会根据语音识别的需要做一些调谐,当然,这需要在系统支持的情况下。
2.setvideosource自然是videosource.camera,只是在此两项设置必须在设置编码器之前设置,这无需说明。
step 3:
1.setoutputformat需要在step 2之后,并且在prepare()之前。这里采用outputformat.mpeg_4格式。
2.setvideosize需要权衡的因素较多,主要包括三方面:mediarecorder支持的录制尺寸、视频文件的大小以及兼容不同android机型。这里采用640 * 480(微信小视频的尺寸是320*240),文件大小在500-1000kb之间,并且市面上99%以上机型支持此录制尺寸。
3.setvideoencodingbitrate与视频的清晰度有关,设置此参数需要权衡清晰度与文件大小的关系。太高,文件大不易传输;太低,文件清晰度低,识别率低。需要根据实际业务场景灵活调整。
4.setvideoencoder采用h264编码,mpeg4、h263、h264等不同编码的差别比较,实际使用中,h264的压缩率较高,推荐使用。
5.setaudioencoder采用audioencoder.aac,该设置主要是考虑其通用性、兼容性。
step 4:
setmaxfilesize指定录制文件的大小限制,当然还可以限制其最大录制时间。
setoutputfile指定输出视频的路径。
setonerrorlistener指定错误监听器。
在完成上述配置之后,即可准备mediarecorder,并在返回成功后开始视频录制。
1
2
3
4
5
6
7
8
9
10
|
private boolean prepareconfiguredmediarecorder() { // step 5: prepare configured mediarecorder try { mmediarecorder.prepare(); } catch (exception e) { releasemediarecorder(); return false ; } return true ; } |
四、视频文件旋转
第三节中step 1提到对视频文件的旋转,因为某些播放器会忽略录制视频时的配置参数,因此可尝试通过第三方库对视频文件进行旋转,例如:opencv,fastcv等,在camera对象的camera.previewcallback中截取每帧数据byte[] data,然后对其进行处理,然后输出。该方法需要考虑处理方法的高效性,在编程时一般采用ndk,在c++中完成关键的处理,这里贴出fastcv中该处理方法的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public void onpreviewframe( byte [] data, camera c ) { // increment fps counter for camera. util.cameraframetick(); // perform processing on the camera preview data. update( data, mdesiredwidth, mdesiredheight ); // simple iir filter on time. mprocesstime = util.getfastcvprocesstime(); if ( c != null ) { // with buffer requires addbuffer each callback frame. c.addcallbackbuffer( mpreviewbuffer ); c.setpreviewcallbackwithbuffer( this ); } // mark dirty for render. requestrender(); } }; |
其中,update为native方法,其实现由jni中对应的文件完成,其中调用了libfastcv.a中相应的api。这里涉及ndk编程的基本方法步骤:(1)开发环境;(2)编写java代码、c/c++代码;(3)编译c/c++文件生成.so库;(4)重新编译工程,生成apk。由于本章不重点讲述ndk,这里不再展开。
除上述方法以外,笔者采用了另外一种思路进行了探索,上述方法处理的数据为每帧图像数据,可以理解为在线处理,而如果在录制完成之后再处理,可以理解为离线处理。这里采用了第三方库mp4parser,mp4parser是一款支持在android中进行视频分割的库,这里通过其进行视频旋转。至于具体效果如何,读者有兴趣可自行尝试,这里留个悬念。
1
2
3
4
5
6
7
|
private boolean rotatevideofilewithclockwisedegree(string sourcefilepath, int degree) { if (!isfileanddegreevalid(sourcefilepath, degree)) { return false ; } rotatevideofile(sourcefilepath, degree); return true ; } |
对输入参数进行合法性检测之后,根据检测结果判断是否进行旋转。
1
2
3
4
5
6
7
8
9
10
|
private boolean isfileanddegreevalid(string sourcefilepath, int degree) { if (sourcefilepath == null || (!sourcefilepath.endswith( ".mp4" )) || (! new file(sourcefilepath).exists())) { return false ; } if (degree == 0 || (degree % 90 != 0 )) { return false ; } return true ; } |
1
2
3
4
5
|
private void rotatevideofile(string sourcefilepath, int degree) { list<trackbox> trackboxes = gettrackboxesofvideofilebypath(sourcefilepath); movie rotatedmovie = getrotatedmovieoftrackbox(trackboxes); writemovietomodifiedfile(rotatedmovie); } |
通过mp4parser旋转视频主要分为三步:
(1)获取视频文件对应的trackboxes;
(2)根据trackboxes获取旋转后的movie对象;
(3)将movie对象写入文件。
1
2
3
4
5
6
7
8
9
10
11
12
|
private list<trackbox> gettrackboxesofvideofilebypath(string sourcefilepath) { isofile isofile = null ; list<trackbox> trackboxes = null ; try { isofile = new isofile(sourcefilepath); trackboxes = isofile.getmoviebox().getboxes(trackbox. class ); isofile.close(); } catch (ioexception e) { e.printstacktrace(); } return trackboxes; } |
1
2
3
4
5
6
7
8
9
|
private movie getrotatedmovieoftrackbox(list<trackbox> trackboxes) { movie rotatedmovie = new movie(); // 旋转 for (trackbox trackbox : trackboxes) { trackbox.gettrackheaderbox().setmatrix(matrix.rotate_90); rotatedmovie.addtrack( new mp4trackimpl(trackbox)); } return rotatedmovie; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private void writemovietomodifiedfile(movie movie) { container container = new defaultmp4builder().build(movie); file modifiedvideofile = new file(videofilepath.replace( ".mp4" , "_mod.mp4" )); fileoutputstream fos; try { fos = new fileoutputstream(modifiedvideofile); writablebytechannel bb = channels.newchannel(fos); container.writecontainer(bb); // 关闭文件流 fos.close(); } catch (exception e) { e.printstacktrace(); } } |
本文对android视频录制中常见的问题进行了说明,希望对大家的学习有所帮助。