阅读更多

0顶
0踩

移动开发
【编者按】Android 视频相关的开发,大概一直是整个 android 生态、以及 Android API 中,最为分裂以及兼容性问题最为突出的一部分,本文从视频编码器的选择和如何对摄像头输出的 YUV 帧进行快速预处理两方面,从实践角度解析笔者曾趟过 Android 视频编码的那些坑,希望对广大读者有所助益。

Google 针对摄像头以及视频编码相关的 API,控制力一直非常差,导致不同厂商对这两个 API 的实现有不少差异,而且从 API 的设计来看,一直以来优化也相当有限,甚至有人认为这是“Android 上最难用的 API 之一”。

以微信为例,在 Android 设备录制一个 540P 的 MP4 文件,大体上遵循以下流程:

图1 Android 视频流编码流程图

从摄像头输出的 YUV 帧经过预处理之后,送入编码器,获得编码好的 H264 视频流。

上面只是针对视频流的编码,另外还需要对音频流单独录制,最后再将视频流和音频流合成最终视频。

这篇文章主要会对视频流的编码中两个常见问题进行分析:
  • 视频编码器的选择:硬编 or 软编?
  • 如何对摄像头输出的 YUV 帧进行快速预处理:镜像、缩放、旋转?
视频编码器的选择

对于录制视频的需求,不少 App 都需要对每一帧数据进行单独处理,因此很少会直接用到 MediaRecorder 来录取视频,一般来说,会有两个选择:
  • MediaCodec
  • FFMpeg+x264/openh264
下面我们逐个进行解析。

MediaCodec

MediaCodec 是 API 16 之后 Google 推出的用于音视频编解码的一套偏底层的 API,可以直接利用硬件加速进行视频的编解码。调用的时候需要先初始化 MediaCodec 作为视频的编码器,然后只需要不停传入原始的 YUV 数据进入编码器就可以直接输出编码好的 H.264 流,整个 API 设计模型同时包含了输入端和输出端的两条队列。

因此,作为编码器,输入端队列存放的是原始 YUV 数据,输出端队列输出的是编码好的 H.264 流,作为解码器则对应相反。在调用的时候,MediaCodec 提供了同步和异步两种调用方式,但是异步使用 Callback 的方式是在 API 21 之后才加入的,以同步调用为例,一般来说调用方式大概是这样(摘自官方例子):
MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

简单解释一下,通过 getInputBuffers 获取输入队列,然后调用 dequeueInputBuffer 获取输入队列空闲数组下标,注意 dequeueOutputBuffer 会有几个特殊的返回值表示当前编解码状态的变化,然后再通过 queueInputBuffer 把原始 YUV 数据送入编码器,而在输出队列端同样通过 getOutputBuffers 和 dequeueOutputBuffer 获取输出的 H.264 流,处理完输出数据之后,需要通过 releaseOutputBuffer 把输出 buffer 还给系统,重新放到输出队列中。

关于 MediaCodec 更复杂的使用例子,可以参照 CTS 测试里面的使用方式:EncodeDecodeTest.java

从上面例子来看 MediaCodec 的确是非常原始的 API,由于 MediaCodec 底层直接调用了手机平台硬件的编解码能力,所以速度非常快,但是因为 Google 对整个 Android 硬件生态的掌控力非常弱,所以这个 API 有很多问题:

颜色格式问题

MediaCodec 在初始化的时候,configure 过程中需要传入一个 MediaFormat 对象,当作为编码器使用的时候,我们一般需要在 MediaFormat 中指定视频的宽高、帧率、码率、I 帧间隔等基本信息。除此之外,还有一个重要的信息就是,指定编码器接受的 YUV 帧的颜色格式,这是由于 YUV 根据其采样比例,UV 分量的排列顺序有很多种不同的颜色格式,而对于 Android 的摄像头在 onPreviewFrame 输出的 YUV 帧格式,没有配置任何参数的情况下,基本上都是 NV21 格式,但 Google 对 MediaCodec 的 API 在设计和规范的时候,显得很不厚道,过于贴近 Android 的 HAL 层了,导致了 NV21 格式并不是所有机器的 MediaCodec 都支持这种格式作为编码器的输入格式。 因此,在初始化 MediaCodec 的时候,我们需要通过 codecInfo.getCapabilitiesForType 来查询机器上的 MediaCodec 实现具体支持哪些 YUV 格式作为输入格式。一般来说,起码在 4.4+ 的系统上,这两种格式在大部分机器上都有支持:
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar

两种格式分别是 YUV420P 和 NV21,如果机器上只支持 YUV420P 格式,则需要先将摄像头输出的 NV21 格式先转换成 YUV420P,才能送入编码器进行编码,否则最终出来的视频就会花屏,或者颜色出现错乱。

这个算是一个不大不小的坑,基本上用 MediaCodec 进行视频编码都会遇上这个问题。

编码器支持特性相当有限

如果使用 MediaCodec 来编码 H.264 视频流,对于 H.264 格式来说,会有一些针对压缩率以及码率相关的视频质量设置,典型的诸如 Profile(baseline, main, hight)、Profile Level、Bitrate mode(CBR、CQ、VBR),合理配置这些参数可以让我们在同等的码率下,获得更高的压缩率,从而提升视频的质量,Android 也提供了对应的 API 进行设置,可以设置到 MediaFormat 中:
MediaFormat.KEY_BITRATE_MODE
MediaFormat.KEY_PROFILE
MediaFormat.KEY_LEVEL

但问题是,对于 Profile、Level、Bitrate mode 这些设置,在大部分手机上都是不支持的,即使是设置了最终也不会生效,例如设置了 Profile 为 high,最后出来的视频依然还会是 Baseline。

这个问题,在 7.0 以下的机器几乎是必现的,其中一个可能的原因是,Android 在源码层级 hardcode 了 Profile 的的设置:
// XXX
if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) {
 ALOGW("Use baseline profile instead of %d for AVC recording",
     h264type.eProfile);
 h264type.eProfile = OMX_VIDEO_AVCProfileBaseline;
    }

Android 直到 7.0 之后才取消了这段地方的 Hardcode。
if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) {
        ....
    } else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain ||
            h264type.eProfile == OMX_VIDEO_AVCProfileHigh) {
        .....
    }

这个问题可以说间接导致了 MediaCodec 编码出来的视频质量偏低,同等码率下,难以获得跟软编码甚至 iOS 那样的视频质量。

16 位对齐要求

前面说到,MediaCodec 这个 API 在设计的时候,过于贴近 HAL 层,这在很多 SoC 的实现上,是直接把传入 MediaCodec 的 buffer,在不经过任何前置处理的情况下就直接送入了 Soc 中。而在编码 H264 视频流的时候,由于 H264 的编码块大小一般是 16x16,于是在一开始设置视频宽高的时候,如果设置了一个没有对齐 16 的大小,例如 960x540,在某些 CPU 上,最终编码出来的视频就会直接花屏。

很明显这还是因为厂商在实现这个 API 的时候,对传入的数据缺少校验以及前置处理导致的。目前来看,华为、三星的 SoC 出现这个问题会比较频繁,其他厂商的一些早期 Soc 也有这种问题,一般来说解决方法还是在设置视频宽高的时候,统一设置成对齐 16 位就好了。

FFMpeg+x264/openh264

除了使用 MediaCodec 进行编码之外,另外一种比较流行的方案就是使用 FFmpeg + x264/OpenH264 进行软编码,FFmpeg 适用于一些视频帧的预处理。这里主要是使用 x264/OpenH264 作为视频的编码器。

x264 基本上被认为是当今市面上最快的商用视频编码器,而且基本上所有 H264 的特性都支持,通过合理配置各种参数还是能够得到较好的压缩率和编码速度的,限于篇幅,这里不再阐述 H.264 的参数配置。

OpenH264 则是由思科开源的另外一个 H264 编码器,项目在 2013 年开源,对比起 x264 来说略显年轻,不过由于思科支付买了 H.264 的年度专利费,所以对于外部用户来说,相当于可以直接免费使用了。另外,firefox 直接内置了 OpenH264,作为其在 WebRTC 中的视频编解码器使用。

但对比起 x264,OpenH264 在 H264 高级特性的支持比较差:
  • Profile 只支持到 baseline,level 5.2;
  • 多线程编码只支持 slice based,不支持 frame based 的多线程编码。
从编码效率上来看,OpenH264 的速度也并不会比 x264 快,不过其最大的好处,还是能够直接免费使用。

软硬编对比

从上面的分析来看,硬编的好处主要在于速度快,而且系统自带,不需要引入外部的库,但是特性支持有限,而且硬编的压缩率一般偏低。对于软编码来说,虽然速度较慢,但是压缩率比较高,而且支持的 H264 特性也会比硬编码多很多,相对来说比较可控。就可用性而言,在 4.4+的系统上,MediaCodec 的可用性是能够基本保证的,但是不同等级机器的编码器能力会有不少差别,建议可以根据机器的配置,选择不同的编码器配置。

YUV 帧的预处理

根据最开始给出的流程,在送入编码器之前,我们需要先对摄像头输出的 YUV 帧进行一些前置处理。

缩放

如果设置了 Camera 的预览大小为 1080P,在 onPreviewFrame 中输出的 YUV 帧直接就是 1920x1080 的大小,如果需要编码跟这个大小不一样的视频,我们就需要在录制的过程中,实时的对 YUV 帧进行缩放。

以微信为例,摄像头预览 1080P 的数据,需要编码 960x540 大小的视频。

最为常见的做法是使用 FFmpeg 的 swsscale 函数进行直接缩放,效果/性能比较好的一般是选择 SWSFAST_BILINEAR 算法:
mScaleYuvCtxPtr = sws_getContext(
                   srcWidth,
                   srcHeight,
                   AV_PIX_FMT_NV21,
                   dstWidth,
                   dstHeight,
                   AV_PIX_FMT_NV21,
                   SWS_FAST_BILINEAR, NULL, NULL, NULL);
sws_scale(mScaleYuvCtxPtr,
                    (const uint8_t* const *) srcAvPicture->data,
                    srcAvPicture->linesize, 0, srcHeight,
                    dstAvPicture->data, dstAvPicture->linesize);

在 Nexus 6P 上,直接使用 FFmpeg 来进行缩放的时间基本上都需要 40ms+,对于我们需要录制 30fps 的来说,每帧处理时间最多就 30ms,如果光是缩放就消耗了如此多的时间,基本上录制出来的视频只能在 15fps 上下了。

很明显,直接使用 FFmpeg 进行缩放实在太慢了,不得不说 swsscale 在 FFmpeg 里面不适用。经对比了几种业界常用的算法之后,我们最后考虑使用快速缩放的算法,如图 3 所示。

我们选择一种叫做局部均值的算法,前后两行四个临近点算出最终图片的四个像素点,对于源图片的每行像素,我们可以使用 Neon 直接实现,以缩放 Y 分量为例:
const uint8* src_next = src_ptr + src_stride;
    asm volatile (
      "1:                                          \n"    
        "vld4.8     {d0, d1, d2, d3}, [%0]!        \n"
        "vld4.8     {d4, d5, d6, d7}, [%1]!        \n"
        "subs       %3, %3, #16                    \n"  // 16 processed per loop
        "vrhadd.u8   d0, d0, d1                    \n"
        "vrhadd.u8   d4, d4, d5                    \n"
        "vrhadd.u8   d0, d0, d4                    \n"
        "vrhadd.u8   d2, d2, d3                    \n"
        "vrhadd.u8   d6, d6, d7                    \n"
        "vrhadd.u8   d2, d2, d6                    \n"
        "vst2.8     {d0, d2}, [%2]!                    \n"  // store odd pixels
        "bgt        1b                             \n"
      : "+r"(src_ptr),          // %0
        "+r"(src_next),         // %1
        "+r"(dst),              // %2
        "+r"(dst_width)         // %3
      :
      : "q0", "q1", "q2", "q3"              // Clobber List
);

上面使用的 Neon 指令每次只能读取和存储 8 或者 16 位的数据,对于多出来的数据,只需要用同样的算法改成用 C 语言实现即可。

在使用上述的算法优化之后,进行每帧缩放,在 Nexus 6P 上,只需要不到 5ms 就能完成了,而对于缩放质量来说,FFmpeg 的 SWSFASTBILINEAR 算法和上述算法缩放出来的图片进行对比,峰值信噪比(psnr)在大部分场景下大概在 38-40 左右,质量也足够好。

旋转

在 Android 机器上,由于摄像头安装角度不同,onPreviewFrame 出来的 YUV 帧一般都是旋转了 90 度或者 270 度,如果最终视频是要竖拍的,那一般来说需要把 YUV 帧进行旋转。

对于旋转的算法,如果是纯 C 实现的代码,一般来说是个 O(n2 )复杂度的算法,如果是旋转 960x540 的 YUV 帧数据,在 Nexus 6P 上,每帧旋转也需要 30ms+,这显然也是不能接受的。

在这里我们换个思路,能不能不对 YUV 帧进行旋转?显当然是可以的。

事实上在 MP4 文件格式的头部,我们可以指定一个旋转矩阵,具体来说是在 moov.trak.tkhd box 里面指定,视频播放器在播放视频的时候,会读取这里的矩阵信息,从而决定视频本身的旋转角度、位移、缩放等,具体可以参考苹果的文档

通过 FFmpeg,我们可以很轻松的给合成之后的 mp4 文件打上这个旋转角度:
char rotateStr[1024];
sprintf(rotateStr, "%d", rotate);
av_dict_set(&out_stream->metadata, "rotate", rotateStr, 0);

于是可以在录制的时候省下一大笔旋转的开销。

镜像

在使用前置摄像头拍摄的时候,如果不对 YUV 帧进行处理,那么直接拍出来的视频是会镜像翻转的,这里原理就跟照镜子一样,从前置摄像头方向拿出来的 YUV 帧刚好是反的,但有些时候拍出来的镜像视频可能不合我们的需求,因此这个时候我们就需要对 YUV 帧进行镜像翻转。

但由于摄像头安装角度一般是 90 度或者 270 度,所以实际上原生的 YUV 帧是水平翻转过来的,因此做镜像翻转的时候,只需要刚好以中间为中轴,分别上下交换每行数据即可,注意 Y 跟 UV 要分开处理,这种算法用 Neon 实现相当简单:
asm volatile (
      "1:                                          \n"
        "vld4.8     {d0, d1, d2, d3}, [%2]!        \n"  // load 32 from src
        "vld4.8     {d4, d5, d6, d7}, [%3]!        \n"  // load 32 from dst
        "subs       %4, %4, #32                    \n"  // 32 processed per loop
        "vst4.8     {d0, d1, d2, d3}, [%1]!        \n"  // store 32 to dst
        "vst4.8     {d4, d5, d6, d7}, [%0]!        \n"  // store 32 to src
        "bgt        1b                             \n"
      : "+r"(src),   // %0
        "+r"(dst),   // %1
        "+r"(srcdata), // %2
        "+r"(dstdata), // %3
        "+r"(count)  // %4  // Output registers
      :                     // Input registers
      : "cc", "memory", "q0", "q1", "q2", "q3"  // Clobber List
    );

同样,剩余的数据用纯 C 代码实现就好了, 在 Nexus 6P 上,这种镜像翻转一帧 1080x1920 YUV 数据大概只要不到 5ms。

在编码好 H.264 视频流之后,最终处理就是把音频流跟视频流合流然后包装到 mp4 文件,这部分我们可以通过系统的 MediaMuxer、mp4v2,或者 FFmpeg 来实现,这部分比较简单,在这里就不再阐述了。

参考文献
  • 雷霄骅(leixiaohua1020)的专栏 ,大名鼎鼎雷神的博客,里面有非常多关于音视频编码/FFmpeg 相关的学习资料,入门必备。也祈愿他能够在天堂安息吧。
  • Android MediaCodec stuff,包含了一些 MediaCodec 使用的示例代码,初次使用可以参考下这里。
  • Coding for NEON,一个系列教程,讲述了一些常用 Neon 指令使用方法。上面在介绍缩放的时候使用到了 Neon,事实上大部分音视频处理过程都会使用到,以 yuv 帧处理为例,缩放,旋转,镜像翻转都可以使用 Neon 来做优化。
  • libyuv,Google 开源的一个 YUV 处理库,上面只针对 1080p->540p 视频帧缩放的算法,而对于通用的压缩处理,可以直接使用这里的实现,对比起 FFmpeg 的速度快上不少。
引用
作者:周俊杰,微信 Android 客户端开发工程师,常年维护音视频相关模块的开发以及各类兼容性问题的处理,负责微信支付相关需求的开发。
责编:唐门教 主(tangxy@csdn.net)
声明:本文为 CSDN《程序员》原创文章,未经许可,请勿转载,如需转载,请留言。

  • 大小: 65.7 KB
  • 大小: 67.6 KB
  • 大小: 30.6 KB
0
0
评论 共 0 条 请登录后发表评论

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 微信Android视频编码爬过的那些坑

    Google针对摄像头以及视频编码相关的API,控制力一直非常差,导致不同厂商对这两个 API的实现有不少差异,...文件,大体上遵循以下流程:图1Android视频流编码流程图从摄像头输出的YUV帧经过预处理之后,送入编码器,

  • 微信Android 视频编码爬过的那些坑

    Google 针对摄像头以及视频编码相关的 API,控制力一直非常差,导致不同厂商对这两个 API 的实现有不少差异,而且从 API 的设计来看,一直以来优化也相当有限,甚至有人认为这是“Android 上最难用的 API 之一”。...

  • 这些年,我爬过的 Android 坑 | 持续更新

    AndroidStudio 提示 Please select Android SDK 关于 Java 中字符与字节的编码关系认识 关于 emoji 编码的长度计算问题 <> 如何理解非主线程可以更新UI 谷歌在 viewRootImpl 中检查更新ui的线程 void checkThread() ...

  • 腾讯团队,微信中使用到的视频技术,音视频研究

     微信商业化是三层架构:最底层是微信的社交平台,它聚集了海量的用户,这是商业化的养分;第二层是开放公众平台,它连接所有的主体(服务和内容提供方),这是商业化的土壤;第三层是业务,包括游戏、支付业务、...

  • 微信公众号抓取 操作手机相关操作 ②

    # 点击退出登录 quit_logging_util = wait.until(EC.presence_of_element_located((By.XPATH, "/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget....

  • android音视频总结

    github上十二款最著名的Android播放器开源项目 视频播放(media palyer, video player): ijkplayer(c语言):https://github.com/Bilibili/ijkplayer Exoplayer:https://github.com/google/ (google开源) ...

  • 移动大脑-SpringMVc搭建RestFul后台服务(六)-微信支付(Android)

    我的视频课程(基础):《(NDK)FFmpeg...我的视频课程(编码直播推流):《Android视频编码和直播推流》   目录:  移动大脑-SpringMVc搭建RestFul后台服务(一)-环境搭建  移动大脑-SpringMVc搭建Res...

  • 音视频开发总结之二Android平台相关

    串联整个音视频录制流程,完成音视频的采集、编码、封包成 mp4 输出。 通过摄像头和麦克风获得实时的音视频数据; 播放流程: 获取流—>解码—>播放。 录制播放路程: 录制音频视频—>视频处理—>编码—...

  • 微信小程序

    当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索词条触发该索引时,小程序的页面将可能展示在搜索结果中 5.3.1在小程序管理的后台配置是否允许被搜索到 打开小程序的管理...

  • 微信发送自定义语音

    很皮微信前景开发遇到的难题收集语音素材问题解决录制音频如何变声音频格式转换amr问题(ffmpeg&amr)音频方面的知识点音频文件的组成PCM&...

  • Python自动抢红包,超详细教程,再也不会错过微信红包了

    利用 AirtestIDE 创建一个项目时,设备类型选中 Android,就会在编码区生成一段初始化的代码。 from airtest.core.api import * auto_setup(__file__) from poco.drivers.android.uiautomation import ...

  • 视频教程-微信公众平台开发实战PHP版-微信开发

    微信公众平台开发实战PHP版 互联商通创始人/架构师, 在IT领域摸爬滚打2...

  • 美团外卖商家端视频探索之旅

    背景 美团外卖至今已迅猛发展了六年,随着外卖业务量级与日俱增,单一的文字...为此,商家端引入了视频功能,进行了一系列视频功能开发,核心功能包含视频处理(混音,滤镜,加水印,动画等)、视频拍摄、合成等,最...

  • 张小龙发布2018微信全新计划(内附演讲全文)

    欢迎大家来到微信公开课。 刚刚出现的是我打游戏的画面,被大家看到了,那个不是我最好的水平,因为有点紧张,我最高分曾打到6000多分。当然我是练习了很久了,并不是我比大家更厉害,而是我有很多时间去...

  • 毕业设计&课设_CUMT 信息安全专业毕业设计:基于区块链的能源交易系统,含架构、部署等多方面详细介绍.zip

    毕业设计&课设_CUMT 信息安全专业毕业设计:基于区块链的能源交易系统,含架构、部署等多方面详细介绍.zip

  • 用Python分析文本数据项目

    用Python分析文本数据项目

  • 基于SpringBoot的养老院管理系统源码数据库文档.zip

    基于SpringBoot的养老院管理系统源码数据库文档.zip

  • AshampooUnInstaller v15.00.22 Portable一款强大的卸载工具,彻底、智能著称阿香婆强制卸载软件.rar

    阿香婆软件强制卸载软件 Ashampoo UnInstaller 是一款强大的卸载工具,彻底、智能著称,如果您选择使用Ashampoo安装程序,它会自动分析软件从开始安装到结束的全过程,并为该软件建立一个日志,以便以后更彻底的卸载它,Ashampoo UnInstaller 全面提速!程序采用了新的技术,速度得到了全面提高。Windows 默认应用现在也可以像其它无用程序和恶意软件一样被无忧卸载。摆脱工具栏、插件和其它隐藏的追踪软件,享受安全快捷的上网体验!新的快照比较功能,可以非常简单的创建安装日志,这是旧版本 UnInstaller 粉丝最急需的!新的界面,便捷的批量卸载,以及内置的在线搜索,Ashampoo UnInstaller 一定会让你惊叹无比。快来体验更清晰、更详尽、更快速的 Ashampoo UnInstaller !Ashampoo UnInstallerAshampoo UnInstaller彻底删除无用程序的所有痕迹! 让安装、试用、卸载程序更加轻松。只需点几下,无用的软件、浏览器扩展和工具栏就能从你的硬盘中

Global site tag (gtag.js) - Google Analytics