极客时间已完结课程限时免费阅读

38 | 移动开发新大陆:Android音视频开发

38 | 移动开发新大陆:Android音视频开发-极客时间

38 | 移动开发新大陆:Android音视频开发

讲述:冯永吉

时长19:15大小17.58M

你好,我是张绍文。俊杰目前负责微信的音视频开发工作,无论是我们平常经常使用到的小视频,还是最新推出的即刻视频,都是出自他手。俊杰在音视频方面有非常丰富的经验,今天有幸请到他来给我们分享他个人对 Android 音视频开发的心得和体会。
在日常生活中,视频类应用占据了我们越来越多的时间,各大公司也纷纷杀入这个战场,不管是抖音、快手等短视频类型,虎牙、斗鱼等直播类型,腾讯视频、爱奇艺、优酷等长视频类型,还是 Vue、美拍等视频编辑美颜类型,总有一款适合你。
未来随着 5G 普及以及网络资费的下降,音视频的前景是非常广阔的。但是另一方面,无论是音视频的编解码和播放器、视频编辑和美颜的各种算法,还是视频与人工智能的结合(AI 剪片、视频修复、超清化等),它们都涉及了方方面面的底层知识,学习曲线比较陡峭,门槛相对比较高,所以也造成了目前各大公司音视频相关人才的紧缺。如果你对音视频开发感兴趣,我也非常建议你去往这个方向尝试,我个人是非常看好音视频开发这个领域的。
当然音视频开发的经验是靠着一次又一次的“填坑”成长起来的,下面我们一起来看看俊杰同学关于音视频的认识和思考。
大家好,我是周俊杰,现在在微信从事 Android 多媒体开发。应绍文的邀请,分享一些我关于 Android 音视频开发的心得和体会。
不管作为开发者还是用户,现在我们每天都会接触到各种各样的短视频、直播类的 App,与之相关的音视频方向的开发也变得越来越重要。但是对于大多数 Android 开发者来说,从事 Android 音视频相关的开发可能目前还算是个小众领域,虽然可能目前深入这个领域的开发者还不是太多,但这个方向涉及的知识点可一点都不少。

音视频的基础知识

1. 音视频相关的概念
说到音视频,先从我们熟悉也陌生的视频格式说起。
对于我们来说,最常见的视频格式就是MP4格式,这是一个通用的容器格式。所谓容器格式,就意味内部要有对应的数据流用来承载内容。而且既然是一个视频,那必然有音轨和视轨,而音轨、视轨本身也有对应的格式。常见的音轨、视轨格式包括:
视轨:H.265(HEVC)H.264。其中,目前大部分 Android 手机都支持 H.264 格式的直接硬件编码和解码;对于 H.265 来说,Android 5.0 以上的机器就支持直接硬件解码了,但是对于硬件编码,目前只有一部分高端芯片可以支持,例如高通的 8xx 系列、华为的 98x 系列。对于视轨编码来说,分辨率越大性能消耗也就越大,编码所需的时间就越长。
音轨:AAC。这是一种历史悠久音频编码格式,Android 手机基本可以直接硬件编解码,几乎很少遇到兼容性问题。可以说作为视频的音轨格式,AAC 已经非常成熟了。
对于编码本身,上面提到的这些格式都是有损编码,因此压缩编码本身还需要一个衡量压缩之后,数据量多少的指标,这个标准就是码率。同一个压缩格式下,码率越高质量也就越好。更多 Android 本身支持的编解码格式,你可以参考官方文档
小结一下,要拍摄一个 MP4 视频,我们需要将视轨 + 音轨分别编码,然后作为 MP4 的数据流,再合成出一个 MP4 文件。
2. 音视频编码的流程
接下来,我们再来看看一个视频是怎么拍摄出来的。首先,既然是拍摄,少不了跟摄像头、麦克风打交道。从流程来说,以 H.264/AAC 编码为例,录制视频的总体流程是:
我们分别从摄像头 / 录音设备采集数据,将数据送入编码器,分别编码出视轨 / 音轨之后,再送入合成器(MediaRemuxer 或者类似 mp4v2、FFmpeg 之类的处理库),最终输出 MP4 文件。接下来,我主要以视轨为例,来介绍下编码的流程。
首先,直接使用系统的MediaRecorder录制整个视频,这是最简单的方法,直接就能输出 MP4 文件。但是这个接口可定制化很差,比如我们想录制一个正方形的视频,除非摄像头本身支持宽高一致的分辨率,否则只能后期处理或者各种 Hack。另外,在实际 App 中,除非对视频要求不是特别高,一般也不会直接使用 MediaRecorder。
视轨的处理是录制视频中相对比较复杂的部分,输入源头是 Camera 的数据,最终输出是编码的 H.264/H.265 数据。下面我来介绍两种处理模型。
第一种方法是利用 Camera 获取摄像头输出的原始数据接口(例如 onPreviewFrame),经过预处理,例如缩放、裁剪之后,送入编码器,输出 H.264/H.265。
摄像头输出的原始数据格式为 NV21,这是YUV颜色格式的一种。区别于 RGB 颜色,YUV 数据格式占用空间更少,在视频编码领域使用十分广泛。
一般来说,因为摄像头直接输出的 NV21 格式大小跟最终视频不一定匹配,而且编码器往往也要求输入另外一种 YUV 格式(一般来说是 YUV420P),因此在获取到 NV21 颜色格式之后,还需要进行各种缩放、裁剪之类的操作,一般会使用FFmpeglibyuv这样的库处理 YUV 数据。
最后会将数据送入到编码器。在视频编码器的选择上,我们可以直接选择系统的 MediaCodec,利用手机本身的硬件编码能力。但如果对最终输出的视频大小要求比较严格的话,使用的码率会偏低,这种情况下大部分手机的硬件编码器输出的画质可能会比较差。另外一种常见的选择是利用x264来进行编码,画质表现相对较好,但是比起硬件编码器,速度会慢很多,因此在实际使用时最好根据场景进行选择。
除了直接处理摄像头原始数据以外,还有一种常见的处理模型,利用 Surface 作为编码器的输入源。
对于 Android 摄像头的预览,需要设置一张 Surface/SurfaceTexture 来作为摄像头预览数据的输出,而 MediaCodec 在 API 18+ 之后,可以通过createInputSurface来创建一张 Surface 作为编码器的输入。这里所说的另外一种方式就是,将摄像头预览 Surface 的内容,输出到 MediaCodec 的 InputSurface 上。
而在编码器的选择上,虽然 InputSurface 是通过 MediaCodec 来创建的,乍看之下似乎只能通过 MediaCodec 来进行编码,无法使用 x264 来编码,但利用 PreviewSurface,我们可以创建一个 OpenGL 的上下文,这样所有绘制的内容,都可以通过 glReadPixel 来获取,然后再讲读取数据转换成 YUV 再输入到 x264 即可(另外,如果是在 GLES 3.0 的环境,我们还可以利用PBO来加速 glReadPixles 的速度)。
由于这里我们创建了一个 OpenGL 的上下文,对于目前的视频类 App 来说,还有各种各样的滤镜和美颜效果,实际上都可以基于 OpenGL 来实现。
而至于这种方式录制视频具体实现代码,你可以参考下 grafika 中示例

视频处理

1. 视频编辑
在当下视频类 App 中,你可以见到各种视频裁剪、视频编辑的功能,例如:
裁剪视频的一部分。
多个视频进行拼接。
对于视频裁剪、拼接来说,Android 直接提供了MediaExtractor的接口,结合 seek 以及对应读取帧数据 readSampleData 的接口,我们可以直接获取对应时间戳的帧的内容,这样读取出来的是已经编码好的数据,因此无需重新编码,直接可以输入合成器再次合成为 MP4。
我们只需要 seek 到需要裁剪原视频的时间戳,然后一直读取 sampleData,送入 MediaMuxer 即可,这是视频裁剪最简单的实现方式。
但在实践时会发现,seekTo并不会对所有时间戳都生效。比如说,一个 4min 左右的视频,我们想要 seek 到大概 2min 左右的位置,然后从这个位置读取数据,但实际调用 seekTo 到 2min 这个位置之后,再从 MediaExtractor 读取数据,你会发现实际获取的数据上可能是从 2min 这里前面一点或者后面一点位置的内容。这是因为 MediaExtractor 这个接口只能 seek 到视频关键帧的位置,而我们想要的位置并不一定有关键帧。这个问题还是要回到视频编码,在视频编码时两个关键帧之间是有一定间隔距离的。
如上图所示,关键帧被成为 I 帧,可以被认为是一帧没有被压缩的画面,解码的时候无需要依赖其他视频帧。但是在两个关键帧之间,还存在这 B 帧P 帧这样的压缩帧,需要依赖其他帧才能完整解码出一个画面。至于两个关键帧之间的间隔,被称为一个GOP ,在 GOP 内的帧,MediaExtractor 是无法直接 seek 到的,因为这个类不负责解码,只能 seek 到前后的关键帧。但如果 GOP 过大,就会导致视频编辑非常不精准了(实际上部分手机的 ROM 有改动,实现的 MediaExtractor 也能精确 seek)。
既然如此,那要实现精确裁剪也就只能去依赖解码器了。解码器本身能够解出所有帧的内容,在引入解帧之后,整个裁剪的流程就变成了下面的样子。
我们需要先 seek 到需要位置的前一 I 帧上,然后送入解码器,解码器解除一帧之后,判断当前帧的PTS是否在需要的时间戳范围内,如果是的话,再将数据送入编码器,重新编码再次得到 H.264 视轨数据,然后合成 MP4 文件。
上面是基础的视频裁剪流程,对于视频拼接,也是类似得到多段 H.264 数据之后,才一同送入合成器。
另外,在实际视频编辑中,我们还会添加不少视频特效和滤镜。前面在视频拍摄的场景下,我们利用 Surface 作为 MediaCodec 的输入源,并且利用 Surface 创建了 OpenGL 的上下文。而 MediaCodec 作为解码器的时候,也可以在configure的时候,指定一张 Surface 作为其解码的输出。大部分视频特效都是可以通过 OpenGL 来实现的,因此要实现视频特效,一般的流程是下面这样的。
我们将解码之后的渲染交给 OpenGL,然后输出到编码器的 InputSurface 上,来实现整套编码流程。
2. 视频播放
任何视频类 App 都会涉及视频播放,从录制、剪辑再到播放,构成完整的视频体验。对于要播放一个 MP4 文件,最简单的方式莫过于直接使用系统的MediaPlayer,只需要简单几行代码,就能直接播放视频。对于本地视频播放来说,这是最简单的实现方式,但实际上我们可能会有更复杂的需求:
需要播放的视频可能本身并不在本地,很多可能都是网络视频,有边下边播的需求。
播放的视频可能是作为视频编辑的一部分,在剪辑时需要实时预览视频特效。
对于第二种场景,我们可以简单配置播放视频的 View 为一个 GLSurfaceView,有了 OpenGL 的环境,我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置,MediaPlayer 也有直接的接口可以设置。
更为常见的是第一种场景,例如一个视频流界面,大部分视频都是在线视频,虽然 MediaPlayer 也能实现在线视频播放,但实际使用下来,会有两个问题:
通过设置 MediaPlayer 视频 URL 方式下载下来的视频,被放到了一个私有的位置,App 不容易直接访问,这样会导致我们没法做视频预加载,而且之前已经播放完、缓冲完的视频,也不能重复利用原有缓冲内容。
同视频剪辑直接使用 MediaExtractor 返回的数据问题一样,MediaPlayer 同样无法精确 seek,只能 seek 到有关键帧的地方。
对于第一个问题,我们可以通过视频 URL 代理下载的方式来解决,通过本地使用 Local HTTP Server 的方式代理下载到一个指定的地方。现在开源社区已经有很成熟的项目实现,例如AndroidVideoCache
而对于第二个问题来说,没法精确 seek 的问题在有些 App 上是致命的,产品可能无法接受这样的体验。那同视频编辑一样,我们只能直接基于 MediaCodec 来自行实现播放器,这部分内容就比较复杂了。当然你也可以直接使用 Google 开源的ExoPlayer,简单又快捷,而且也能支持设置在线视频 URL。
看似所有问题都有了解决方案,是不是就万事大吉了呢?
常见的网络边下边播视频的格式都是 MP4,但有些视频直接上传到服务器上的时候,我们会发现无论是使用 MediaPlayer 还是 ExoPlayer,似乎都只能等待到整个视频都下载完才能开始播放,没有达到边下边播的体验。这个问题的原因实际上是因为 MP4 的格式导致的,具体来看,是跟 MP4格式中的 moov 有关。
MP4 格式中有一个叫作 moov 的地方存储这当前 MP4 文件的元信息,包括当前 MP4 文件的音轨视轨格式、视频长度、播放速率、视轨关键帧位置偏移量等重要信息,MP4 文件在线播放的时候,需要 moov 中的信息才能解码音轨视轨。
而上述问题发生的原因在于,当 moov 在 MP4 文件尾部的时候,播放器没有足够的信息来进行解码,因此视频变得需要直接下载完之后才能解码播放。因此,要实现 MP4 文件的边下边播,则需要将 moov 放到文件头部。目前来说,业界已经有非常成熟的工具,FFmpegmp4v2都可以将一个 MP4 文件的 moov 提前放到文件头部。例如使用 FFmpeg,则是如下命令:
ffmpeg -i input.mp4 -movflags faststart -acodec copy -vcodec copy output.mp4
使用-movflags faststart,我们就可以把视频文件中的 moov 提前了。
另外,如果想要检测一个 MP4 的 moov 是否在前面,可以使用类似AtomicParsley的工具来检测。
在视频播放的实践中,除了 MP4 格式来作为边下边播的格式以外,还有更多的场景需要使用其他格式,例如 m3u8、FLV 之类,业界在客户端中常见的实现包括ijkplayerExoPlayer,有兴趣的同学可以参考下它们的实现。

音视频开发的学习之路

音视频相关开发涉及面很广,今天我也只是简单介绍一下其中基本的架构,如果想继续深入这个领域发展,从我个人学习的经历来看,想要成为一名合格的开发者,除了基础的 Android 开发知识以外,还要深入学习,我认为还需要掌握下面的技术栈。
语言
C/C++:音视频开发经常需要跟底层代码打交道,掌握 C/C++ 是必须的技能。这方面资料很多,相信我们都能找到。
ARM NEON 汇编:这是一项进阶技能,在视频编解码、各种帧处理低下时很多都是利用 NEON 汇编加速,例如 FFmpeg/libyuv 底层都大量利用了 NEON 汇编来加速处理过程。虽说它不是必备技能,但有兴趣也可以多多了解,具体资料可以参考 ARM 社区的教程
框架
FFmpeg:可以说是业界最出名的音视频处理框架了,几乎囊括音视频开发的所有流程,可以说是必备技能。
libyuv:Google 开源的 YUV 帧处理库,因为摄像头输出、编解码输入输出也是基于 YUV 格式,所以也经常需要这个库来操作数据(FFmpeg 也有提供了这个库里面所有的功能,在libswscale都可以找到类似的实现。不过这个库性能更好,也是基于 NEON 汇编加速)。
libx264/libx265:目前业界最为广泛使用的 H.264/H.265 软编解码库。移动平台上虽然可以使用硬编码,但很多时候出于兼容性或画质的考虑,因为不少低端的 Android 机器,在低码率的场景下还是软编码的画质会更好,最终可能还是得考虑使用软编解码。
OpenGL ES:当今,大部分视频特效、美颜算法的处理,最终渲染都是基于 GLES 来实现的,因此想要深入音视频的开发,GLES 是必备的知识。另外,除了 GLES 以外,Vulkan也是近几年开始发展起来的一个更高性能的图形 API,但目前来看,使用还不是特别广泛。
ExoPlayer/ijkplayer:一个完整的视频类 App 肯定会涉及视频播放的体验,这两个库可以说是当下业界最为常用的视频播放器了,支持众多格式、协议,如果你想要深入学习视频播放处理,它们几乎也算是必备技能。
从实际需求出发,基于上述技术栈,我们可以从下面两条路径来深入学习。
1. 视频相关特效开发
直播、小视频相关 App 目前越来越多,几乎每个 App 相关的特效,往往都是利用 OpenGL 本身来实现。对于一些简单的特效,可以使用类似Color Look Up Table的技术,通过修改素材配合 Shader 来查找颜色替换就能实现。如果要继续学习更加复杂的滤镜,推荐你可以去shadertoy学习参考,上面有非常多 Shader 的例子。
而美颜、美型相关的效果,特别是美型,需要利用人脸识别获取到关键点,对人脸纹理进行三角划分,然后再通过 Shader 中放大、偏移对应关键点纹理坐标来实现。如果想要深入视频特效类的开发,我推荐可以多学习 OpenGL 相关的知识,这里会涉及很多优化点。
2. 视频编码压缩算法
H.264/H.265 都是非常成熟的视频编码标准,如何利用这些视频编码标准,在保证视频质量的前提下,将视频大小最小化,从而节省带宽,这就需要对视频编码标准本身要有非常深刻的理解。这可能是一个门槛相对较高的方向,我也尚处学习阶段,有兴趣的同学可以阅读相关编码标准的文档。
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。我也为认真思考、积极分享的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 23

提建议

上一篇
37 | 移动开发新大陆:工作三年半,移动开发转型手游开发
下一篇
39 | 移动开发新大陆: 边缘智能计算的趋势
unpreview
 写留言

精选留言(17)

  • blithe
    2019-11-13
    4年开发,3年app,1年音视频,去年干了1年的音视频工作,出来后没找到合适的音视频工作,现在又回去做电商app了,唉,我都怀疑我做的对不对了,而且音视频里面涉及的东西确实多,楼主说的都是我知道的东西,就当复习一遍了
    11
  • 易水南风
    2019-10-22
    看现在做音视频的职位还是挺少的,以后需要量会增加么?

    作者回复: 现在直播那么火,职位还是挺多的,特别是音视频编辑

    共 3 条评论
    2
  • 周刚
    2019-08-23
    讲的太好了
    2
  • RainFool
    2019-05-02
    前段时间没有相关背景,直接粗略看的时候,感觉什么都没有讲 最近有音视频相关需求了,再来细看,边看边做笔记,发现面面俱到,覆盖了音视频开发的所有方向 感谢文俊同学的整理

    作者回复: 音视频是一个很大的话题,限于边幅只能浅尝辄止

    1
  • 耿森
    2019-04-13
    很棒的内容,确实是今后不错的发展方向,真牛,对我这种直接用阿里云做视频还直播的人,很是汗颜,希望老师可以推荐一些学习教材。
    1
  • warmcheng
    2019-04-06
    不知道小视频这些是不是也是一阵风,之后对相关技术的需求量会不会变化等等,是否路比较窄,望大佬指点下小白

    作者回复: 视频相关的人才现在还是挺缺的,而且替换成本比较高,当然视频深入下去也不是那么容易

    1
  • 董尚斌
    2019-04-04
    一直想往音视频上深入去发展,但是是市面上的学习资料很少。 看雷神的博文,也不足以

    作者回复: 音视频的体系化深入资料的确相对少一些

    1
  • Geek_a5b1b8
    2022-04-23
    找了一个音视频剪辑的工作,我负责的是应用层,很想继续深入,可音视频的代码是公司机密,之前几年也没有安卓转岗音视频的先例,请问博主,我应该离职去找其他的可以转服务端的工作吗?安卓应用层我怕以后真的没饭吃
  • Rouse
    2020-04-24
    音视频这一块,只使用过ijkplayer... 这块水还是很深啊
  • 程序员小跃
    2019-07-26
    自己在一家直播公司工作,但是Android端在我来之前就已经实现了播放器的功能,对于音视频,都不好意思说自己是直播公司了。 这节课的内容,需要我好好消化,并做好规划,感谢感谢
  • Justin
    2019-07-23
    webrtc,降到一定(270x480)分辨率,释放编码器,重新创建编码器,发现这一步创建编码器出现异常,导致调用 C 函数 abort了,有遇到过吗,俊杰老师?
  • 左手木亽
    2019-05-27
    深入浅出 优秀。
  • 一路向北
    2019-05-21
    很不错的文章,正在朝音视频方向努力
  • 似水丨炁..``
    2019-05-02
    您好,请问,如何获取手机的解 码 器个数?
  • Object
    2019-04-18
    说实话,看的有点晚,也不知道周学位会不会看到,要说:看了两遍基本了解了,不过还是有很多不懂得地方,是不是做直播和sdk就这些了

    作者回复: 音视频的水还是非常深的,还有视频编辑那一块

  • 赵帅
    2019-04-17
    对于第二种场景,我们可以简单配置播放视频的 View 为一个 GLSurfaceView,有了 OpenGL 的环境,我们就可以在这上实现各种特效、滤镜的效果了。而对于视频编辑常见的快进、倒放之类的播放配置,MediaPlayer 也有直接的接口可以设置。 你好,文中这块的描述说MediaPlayer可以直接有接口配置倒放功能,我实在没找到相关配置😂 请俊杰老师有时间指点一下~
    展开
  • 问答
    2019-04-04
    很棒,谢谢