JavaCV在Android中的应用

勿忘初心2018-10-26 10:05

此文已由作者徐铭阳授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


JavaCV简介

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类去实现,但是在项目推进过程中发现有两个缺陷:

  1. 使用该类去实现录屏的时候会跳出系统权限的弹窗,打断原有的操作;
    如下图所示:
  2. 录屏不同于用摄像头录像,会把原有的界面上的一些UI也录制进去,比如说播放和暂停的控件,进度条的控件等;
  3. 虽然可以用Android的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.jarlibdc1394-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.jarffmpeg-android-x86.jar以及ffmpeg.jar以及通用的javacpp.jarjavacv.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.jarjavacv.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);

这是初始化部分的代码,也是比较重要的,如果配置不对,会出各种问题。从代码里面可以看到初始化分为四块:

  • new Frame数组,为每一帧数据存放的容器,并对每一帧设立一个时间戳,用于后续的同步问题;
  • 使用FFmpegFrameRecoder(核心类),配置编码格式,输出格式,采样率等等,基本和MediaRecoder的配置类似。这里需要特别注意imageWidthimageHeight两个参数,即每一帧数据流图像的宽高,经过测试,如果每一帧图像的宽或者高有大于1000px的需要特别注意,分分钟OOM的节奏,这也算是这个库的缺陷之一,无法很好的满足android移动端对高画质的需求
  • 设置FFmpegFrameFilter,该类的主要功能是对输出视频进行后期处理,后续会详细分析;
  • 最后就是录制音频,我们知道视频和音频都是分开处理的,只是在最后合成,这里就开了一条录制音频的线程去处理音频部分,用的是Android原生的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资源的释放以及输出流的释放,因为代码较长,所以这边就不再贴代码,有兴趣,你可以深入了解一下。

关于FFmpegFrameFilter

因为这个类的功能相当强大,所以单独拉出来说明。
该类最后调用的是avfilternative类,该类存在于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平台而言,能应用的库并不多,具体有artoolkitplusffmpegflandmarkopencv四个。ffmpeg前面已经介绍过了,下面介绍一下其他三个库的应用场景。

  • ARToolKitPlus,即ARToolKit开源库的改进版,一个开发AR(Augmented Reality)功能的库,和ARToolKit库相比,这个库提供了更简单的基于C++的API,支持RGB565以及灰度图,支持基于4096二进制的marker,以及比较稳定的跟踪算法等等,不过受众不多。
  • OpenCV,这个库不用过多介绍,强大的跨平台计算机视觉库,在JavaCV里面也通过Demo示例了如何进行简单的人脸识别或者在视频中识别人脸等,有兴趣可以去实现感受一下。
  • FlandMark,人脸的关键点检测库,一般和OpenCV搭配使用,可以检测到眼睛的左右角点、鼻子、嘴的左右角点,总的来说,返回的特征点有点少,只能用于demo性质的尝试或者简单的操作,诸如对人脸进行美颜操作,这些点还远远不够。

总结

以上就是对JavaCV在Android部分的一些库的介绍,并详细介绍了使用FFmpeg部分对自定义视频的录制,希望对你有所帮助,文章难免有所疏漏,敬请谅解,感谢。


网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击