此文已由作者徐铭阳授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
JavaCV 是GitHub上一个工具类库,它包装了一系列主流的计算机视觉领域的常用工具,包括OpenCV,FFmpeg,libdc1394,PGR FlyCapture,OpenKinect,librealsense,CL PS3 Eye Driverx,videoInput,ARToolKitPlus以及flandmark,并将这些开源库的Native接口进一步封装成简单可用的Java方法来使用。对于Android开发者而言不用花精力去阅读每个库(像OpenCV或者FFmpeg)那样复杂繁琐的说明文档,就可以简单使用一些主要的功能,大大减少开发成本。本篇文章会介绍一下在Android端使用这个库的一些注意点以及使用方法。
我们都知道在5.0及以上系统去实现录屏方式可以通过Android原生的MediaProjection
类去实现,但是在项目推进过程中发现有两个缺陷:
MediaRecoder
去实现录制视频功能,但仅限于相机输入的数据流,要是从其他地方(比如从Unity中)获取经过渲染后有模型的数据流的话,MediaRecorder
完全做不到录制的需求。对于缺陷2,如果在视觉上做一下trick还可以接受,但是对于中断用户操作来说,这样体验非常不好。另外对于开发来说,权限请求过程因为在单独的线程,所以对相机的生命周期管理起来也很麻烦,加上还有背景音乐的播放,整个功能开发起来要注意的地方很多,也很容易踩坑(实际上,因为渲染是在Unity完成,Unity对于多媒体文件的处理又和Android完全不一样,因此确实要填不少坑)。
于是,就有了以录像的方式去实现录制视频,即拿到的是处理后的相机每一帧的数据,并将其编码成视频文件,同时将音频也合成到视频文件当中的策略去解决需求。
虽说Android原生提供了MediaMuxer
的类去实现音视频流的合成和同步,但是在编码和格式方面给开发者的选择不多,又对于音视频流的处理,最主流的莫过于FFmpeg,同样也支持移动端。所以,为了以后的扩展,我选择使用FFmpeg库。但是该库太庞大了,除了需要本地编so以外,对功能函数的使用初学者也会被绕地云里雾里,所以就找到了JavaCV这个封装了一系列工具的工具库。
该库支持gradle依赖,在build.gradle
文件中添加依赖如下依赖就可以将库下载到本地使用:
compile group: 'org.bytedeco', name: 'javacv-platform', version: '1.3.3'
但是在build工程时会发现构建不成功:
看错误Log是说在copylibusb-1.0.dylib
库文件到apk时,发生了两次一模一样的copy,导致库文件冲突了,即引用libfreenect-0.5.3-1.3-macosx-x86_64.jar
和libdc1394-2.2.4-1.3-macosx-x86_64.jar
时同时将这两个jar包中的libusb-1.0.dylib
库copy进了SDK,导致了构建时的文件冲突,这时需要在gradle文件中去规定优先级,即:
android {
...
packagingOptions {
pickFirst 'org/bytedeco/javacpp/macosx-x86_64/libusb-1.0.dylib'
exclude 'META-INF/services/javax.annotation.processing.Processor'
pickFirst 'META-INF/maven/org.bytedeco.javacpp-presets/opencv/pom.properties'
pickFirst 'META-INF/maven/org.bytedeco.javacpp-presets/opencv/pom.xml'
pickFirst 'META-INF/maven/org.bytedeco.javacpp-presets/ffmpeg/pom.properties'
pickFirst 'META-INF/maven/org.bytedeco.javacpp-presets/ffmpeg/pom.xml'
}
...
}
在打包Options时用pickFirst
定义选择第一个copy进去的libusb-1.0.dylib
,而忽略第二次copy时的相同文件。这样就能构建成功。
然而这时候发现把JavaCV的库依赖进工程以后,debug的apk包的大小从50M飙到了220M,这对于一个项目来说肯定是无法接受的,前面也说到JavaCV包装了太多开源库,必然导致了包体积过大,因此,我采用了第二种麻烦一点的方式,手动去添加自己需要的依赖库,其他的库不再放到工程中去。
手动添加的好处前面也说了可以各取所需,去除了无用的库,减少了apk包体的大小。
同样,在该库的GitHub地址上下载javacv-platform-1.3.3-bin.zip
文件,发现有222MB,会大大增加包体积也不冤。解压到本地后,文件夹里面包含了各个平台(Android/MacOSX/Linux/Windows)主流CPU架构(X86/ARM)对应的Jar包。
库比较多,我们需要的只是和FFmpeg相关的,因此将ffmpeg-android-arm.jar
、ffmpeg-android-x86.jar
以及ffmpeg.jar
以及通用的javacpp.jar
和javacv.jar
添加到工程libs
文件夹下,并添加到本地gradle依赖中。工程跑起来以后发现,loadLibrary错误:
在本地工程中找不到libjniavutil.so的库。到这里,我一直很奇怪,从git上下载下来的zip包里面没有内置so文件,那这里报的缺少的so是从哪里来的?既然引用的只是jar包,那么这里面应该会有线索,于是解压了ffmpeg-android-arm.jar
文件,发现里面原来都是so文件,JavaCV是将各个工具库的so打包到了jar文件中,并在编译的时候复制到了apk文件中,那么上面报的错可能就与编译时copy不成功有关。这样的话,不如直接把这些so文件放到jniLibs文件夹下面,直接引用,跳过copy的过程试试,还真成功了。
总结一下就是,手动添加JavaCV时,需要引用必要的javacpp.jar
和javacv.jar
两个文件,这两个库是对一系列引用so的native方法的封装,必须要存在。其他的都通过解压jar包的方式拿到so,直接放到工程里。
通过这种手动添加的方式,包体大小从50M增加到200M缩短到了只增加到60M,还是比较可观的。
接下来以我个人要做的以及遇到的问题为主,对JavaCV里面的录制视频过程进行简述。简而言之,要做的是一个非常规的,将从Unity引擎传过来的视频流在Android端进行编码成视频文件的需求。
代码的逻辑很简单,如下图所示:
initRecording:初始化配置
startRecording:开始录制
onNewFrame:处理每一帧的数据
stopRecording:停止录制并保存
我们一步步来具体说明。
1 初始化配置,先贴代码:
Frame[] images = new Frame[RECORD_LENGTH * frameRate];
timestamps = new long[images.length];
for (int i = 0; i < images.length; i++)
{
images[i] = new Frame(imageWidth, imageHeight, Frame.DEPTH_UBYTE, 4);
timestamps[i] = -1;
}
FFmpegFrameRecorder mRecorder = new FFmpegFrameRecorder(path, imageWidth, imageHeight, 1);
mRecorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
mRecorder.setFormat("mp4");
mRecorder.setSampleRate(sampleAudioRateInHz);
mRecorder.setFrameRate(frameRate);
filterString = "rotate=PI,hflip";
FFmpegFrameFilter mFilter = new FFmpegFrameFilter(filterString, imageWidth, imageHeight);
mFilter.setPixelFormat(avutil.AV_PIX_FMT_RGBA);
AudioRecordRunnable mAudioRecordRunnable = new AudioRecordRunnable();
audioThread = new Thread(mAudioRecordRunnable);
这是初始化部分的代码,也是比较重要的,如果配置不对,会出各种问题。从代码里面可以看到初始化分为四块:
Frame
数组,为每一帧数据存放的容器,并对每一帧设立一个时间戳,用于后续的同步问题;FFmpegFrameRecoder
(核心类),配置编码格式,输出格式,采样率等等,基本和MediaRecoder的配置类似。这里需要特别注意imageWidth
和imageHeight
两个参数,即每一帧数据流图像的宽高,经过测试,如果每一帧图像的宽或者高有大于1000px的需要特别注意,分分钟OOM的节奏,这也算是这个库的缺陷之一,无法很好的满足android移动端对高画质的需求;FFmpegFrameFilter
,该类的主要功能是对输出视频进行后期处理,后续会详细分析;AudioRecord
类(顺带一提,Android系统不支持内录音频,因此需要使用设备的MIC去实现音频录制),这不是重点,会一带而过。2 开始录制部分的代码很简单,
mRecorder.start();
audioThread.start();
mFilter.start();
即,分别开始FFmpegFrameRecorder
、录制音频的Thread以及FFmpegFrameFilter
,值得注意的是,FFmpegFrameFilter
的过滤器需要全程参与录制过程,也就是说,如果需要加入额外的视频处理,FFmpegFrameFilter
需要参与每一帧数据的处理。
3 处理每一帧的数据,这一步是最关键的部分,代码如下
int i = imagesIndex++ % images.length;
Frame rgbaImage = images[i];
timestamps[i] = 1000 * (System.currentTimeMillis() - startTime);
// get video data
if (rgbaImage != null && recording) {
((ByteBuffer) rgbaImage.image[0].position(0)).put(frameStream);
try {
long t = 1000 * (System.currentTimeMillis() - startTime);
if (t > mRecorder.getTimestamp()) {
mRecorder.setTimestamp(t);
}
if (SWITCH_FILTER) {
mFilter.push(rgbaImage);
Frame frame2;
while ((frame2 = mFilter.pull()) != null) {
mRecorder.record(frame2, avutil.AV_PIX_FMT_RGBA);
}
} else {
mRecorder.record(rgbaImage, avutil.AV_PIX_FMT_RGBA);
}
} catch (FFmpegFrameRecorder.Exception | FrameFilter.Exception e) {
e.printStackTrace();
}
}
首先是对每一帧输入图像设置时间戳,并将格式为RGBA的视频流frameStream塞到rgbaImage
当中。如果该过程当中需要对每一帧图像进行额外处理,则需要将rgbaImage
数据push到FFmpegFrameFilter
中,再从FFmpegFrameFilter
中pull出来,调用record
方法进行录制。
mRecorder.record(rgbaImage, avutil.AV_PIX_FMT_RGBA);
这句代码就是录制视频流的地方,avutil.AV_PIX_FMT_RGBA
对应于输入视频流的格式,如果输入和输出不一样,录制后的视频就会不正常。比如说,下图是一帧RGBA图像格式的数据流,
如果我将record方法中代表输出格式的参数设置为YUV格式对应于这里的AV_PIX_FMT_NV21
(即系统相机默认输出流格式),则会导致输出的图像是这个样子的。
输出的就会有问题,所以保证输入和输出的数据流格式一致很重要。
4 结束录制,释放资源。
try {
mRecorder.stop();
mRecorder.release();
mFilter.stop();
mFilter.release();
}
catch (FFmpegFrameRecorder.Exception | FrameFilter.Exception e) {
e.printStackTrace();
}
当暂停录屏以后,需要对Recorder和Filter进行释放,这个过程比较耗时,一般会在1-2s之间,值得注意的是,一旦这个释放过程被打断,比如退出了Activity,则视频保存不会成功。在JavaCV源码中可以看到release的过程最后调用了releaseUnsafe()
方法,该方法内部涉及到了一系列native资源的释放以及输出流的释放,因为代码较长,所以这边就不再贴代码,有兴趣,你可以深入了解一下。
因为这个类的功能相当强大,所以单独拉出来说明。
该类最后调用的是avfilter
native类,该类存在于FFmpeg原生库当中,也就是说只要FFmpeg支持的Filter类型,该类都能支持。Filter可以认为是通过类积木的方式将多种已经预定义好的功能自由组合,并作用到视频数据当中,达到实现效果。简单的一个原理图如下:
如果不加filter效果,走一般流程就可以如前面3小节处理每一帧的数据的代码所示,通过SWITCH_FILTER
标志为去控制是否加入filter。如果加入filter效果,则需要对每一帧数据进行简单的filter叠加处理,然后才通过编码器将处理后的数据打包。同时简单的filter graph又可以组合成更加复杂的filter效果。
具体举几个简单常用的例子,FFmpeg的Filter音视频(Audio Filters and Video Filters)都可以支持,这篇文章只举例Video Filters部分。
简单的部分,Filter可以实现视频的裁剪(crop)、翻转(flip)、旋转(rotate)、转置(transpose)、边界填充(pad)等等,更进一步还可以实现添加水印(delogo)、绘制字幕(subtitles)、提取缩略图(thumbnail)等等,还有更高进阶型的操作,这里不再细述。
在JavaCV当中,通过定义一些Filter的命令字符串来使用FFmpegFrameFilter
,如前面初始化视频配置的时候,有这么一句代码
filterString = "rotate=PI";
看字面意思就是对视频进行旋转180度,这个时候的视频样子就会变成下图这样。
再比如,使用如下操作
String filterString ="rotate=0,pad=640:480:0:40:violet";
这里为Filter定义了旋转0度(即保持原样),然后通过逗号分隔下一个Filter,将视频的尺寸定义为640*480,并且对上下边缘增加紫罗兰色(violet)的边框,得到效果如下:
其他诸如缩放(可以调整视频分辨率),裁剪(返回视频某个部分)等等都可以尝试玩一玩,还有如添加白噪声,边缘检测等等复杂的操作因为没接触过所以就不再赘述,免得经验不足误导你。
上述就是JavaCV中对FFmpeg的应用部分。总结起来,就是通过封装了FFmpeg库的JavaCV就能实现简单的视频合成并处理效果。
我在文章最开头的部分也提到了JavaCV除了能实现视频编辑以外还有其他很多功能,这里再具体扩展一下,就目前针对Android平台而言,能应用的库并不多,具体有artoolkitplus
,ffmpeg
,flandmark
,opencv
四个。ffmpeg前面已经介绍过了,下面介绍一下其他三个库的应用场景。
ARToolKitPlus
,即ARToolKit开源库的改进版,一个开发AR(Augmented Reality)功能的库,和ARToolKit库相比,这个库提供了更简单的基于C++的API,支持RGB565以及灰度图,支持基于4096二进制的marker,以及比较稳定的跟踪算法等等,不过受众不多。OpenCV
,这个库不用过多介绍,强大的跨平台计算机视觉库,在JavaCV里面也通过Demo示例了如何进行简单的人脸识别或者在视频中识别人脸等,有兴趣可以去实现感受一下。FlandMark
,人脸的关键点检测库,一般和OpenCV搭配使用,可以检测到眼睛的左右角点、鼻子、嘴的左右角点,总的来说,返回的特征点有点少,只能用于demo性质的尝试或者简单的操作,诸如对人脸进行美颜操作,这些点还远远不够。以上就是对JavaCV在Android部分的一些库的介绍,并详细介绍了使用FFmpeg部分对自定义视频的录制,希望对你有所帮助,文章难免有所疏漏,敬请谅解,感谢。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。