Android音视频开发笔记(一)--一些基础知识和ffmpeg的编译

1,257 阅读10分钟

笔者是在2015年正式成为Android App开发工程师,赶上了一波移动互联网的大潮。第一次正式接触音视频相关的内容是2016年在一家圈内知名的无人机公司。当时需要做的功能比较简单,从无线设备上接收RTP数据包并将搭载的h264视频数据提取出来解码、渲染。在那段时间网上可以参考的资料比较少,走了很多弯路。我正式从事这方面的工作时间比较短,是从2018年下半年开始成为一名专职的Android音视频开发工程师,在此之前的一段时间虽然对这方面内容很感兴趣,但无奈需要编写App的一些前端内容无法专心沉淀下来,所以现在就想分享一些个人在Android移动平台上音视频相关的一些经验,希望在可以帮助新人的同时能够得到圈内大神的指点,与大家一起共同进步。

谈一下需要用到的技术

  1. 编程语言方面:c/c++和Java/kotlin
  2. 轮子:OpenGL ES、OpenSL ES、ffmpeg、x264/openh264、fdk-aac
  3. tools: NDK、CMake、Android SDK、Gradle、AndroidStudio、git
  • OpenGL ES

    OpenGL:(全写Open Graphics Library)是个定义了一个跨编程语言、跨平台的编程接口的规格,它用于三维图象(二维的亦可)。OpenGL是个专业的图形程序接口,是一个功能强大,调用方便的底层图形库。

    OpenGL ES: (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。该API由Khronos集团定义推广,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准。

    Android系统集成OpenGL ES,目前它有固定管线的1.0版本和可以自定义顶点、像素计算,可以自己编写shader的2.0版本和OpenGL3.0版本(3.0和2.0的区别感兴趣的同学可以自行google)。本系列文章主要使用OpenGL ES 2.0版本的API。


  • OpenSL ES

    OpenSL ES (Open Sound Library for Embedded Systems)嵌入式音频加速标准。

    OpenSL ES™ 是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。它为嵌入式移动多媒体设备上的本地应用程序开发者提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台部署,降低执行难度,促进高级音频市场的发展。

    OpenSL ES也被内置在Android系统中,由于是较低层级的API,属于C语言API,所以只能在C/C++中调用。OpenSL ES提供了非常强大的音效处理、低延时播放等功能。比如Android手机的耳返功能就是基于它实现的。


  • 为什么要进行视频压缩编码

    我们知道视频实际上是一组图片按照某种频率来播放的。所以学习视频还是要从图像开始。我们都知道白光里的含有等量的红光、绿光(这里并没有某种颜色的帽子闪闪发光的意思)和蓝光。在现实生活中我们看到的物体的轮廓和颜色都是由于光的反射。而手机上的屏幕时可以自己发光的,原理也是一样。

    比如一块分辨率为1280*720的屏幕,他在x轴上有720个像素点,在y轴上有1280个像素点,而每个像素都是由RGB三种颜色组成。 而RGB的表示方法有两种,一种是使用0~1.0的浮点型,另一种是0~255的整形来表示。一般来说我们都是用整形来表示RGB,每个颜色通道也就是8个bit(255用二进制表示为11111111)。所以一张1280x720的Bitmap的size为:

                  1280x720x8x4 ≈ 3.516M
    

    这里乘4是因为还有一个alpha通道。按照一般的视频1秒钟有30帧来算的话,每秒的数据体积大概是105M。这么大的体积对于网络传输来说显然是不可接受的。

    当然我们也可以使用yuv来表示图像数据,虽然比RGB数据体积要小不少,但是用于网络传输的话,体积还是太大。YUV百度百科

    所以,我们需要采用一些视频压缩编码算法在保证一定的图像质量的前提下,压缩我们的图像数据(目前大部分视频编码技术都是有损的)。

  • H.264简介

    说到H.264相信大家应该都听说过ffmpeg吧。ffmpeg全拼为Fast forwar motion jpeg,其中jpeg全拼为Joint Photographic Experts Group(联合图像专家小组),是一种常见的图像格式,它由联合照片专家组开发并命名为"ISO 10918-1",JPEG仅仅是一种俗称而已。 当然视频也有定制标准:Motion Jpeg(简称Mpeg)

    Mpeg发展历史:Mpeg1(VCD) Mepg2(DVD) Mpeg4(AVC) 其中ITU-T(国际电信联盟电信标准分局)定制的H.261、H.262、H.263和H.264是一套单独的体系。这里面H.264集中了以往标准的所有优点,也是目前最流行的编码格式,所以本系列文章主要的视频编解码部分都是针对于H.264的。

    • 编码流程:

      帧间和帧内预测(Estimation)-->变换(Transform)-->量化(Quantization)-->环路滤波(Loop Filter)-->熵编码(Entropy Coding)

      1.IBP帧 I帧(Intra Picture):帧内编码帧。I帧通常是每个GOP(Group Of Picture)的第一个帧 P帧:向前参考帧 B帧:双向参考帧

    • H.264都是由连续的NALU(NAL Unit)组成的。

      它的功能分为两层VCL(Video Coding Layer)视频编码层和NAL(Network Abstraction Layer)网络提取层。 VCL:它包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能的独立于网络进行高效的编码。 NAL:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片(slice)级以上的语法级别。

      一帧图片经过H.264编码后,就被编码成一个或多个片(slice),装载slice的载体就是NALU

    • 切片(slice)

      切片的主要做那个与是用于宏块(Macroblck)的载体。片之所以被创造出来,主要是为了限制误码的扩散和传输。每个片都应该是互相独立被传输的,某片的预测(片内预测和片间预测)不能以其他片中的宏块作为参考对象

      一帧图像可以包含一个或多个分片(slice),每一个分片包含数个宏块,即每片至少一个宏块,最多时每片包含整个图像的宏块。 分片头中包含着分片类型、分片中的宏块类型、分片帧的数量、分片属于哪个图像以及对应的帧的设置和参数等信息。 分片数据中则是宏块,这里就是我们要找的,存储像素的地方。

    • 宏块

      宏块是视频信息的主要承载者,因为它包含着每一个像素的亮度和色度信息。视频解码最主要的工作则是提供高效的方式从码流中获取宏块中的像素阵列。

      组成部分:一个宏块是由一个16x16亮度像素和附加的一个8x8 Cr彩色像素块组成。每个图像中,若干宏块被排列成片的形式

      宏块中包含了宏块类型、预测类型、Code Block Pattern(CPB) 、Quantization Parameter、像素的亮度和色度数据集等信息

    • 切片类型和宏块类型的关系

      P-slice. Consists of P-macroblocks (each macro block is predicted using one reference frame) and / or I-macroblocks.

      B-slice. Consists of B-macroblocks (each macroblock is predicted using one or two reference frames) and / or I-macroblocks.

      I-slice. Contains only I-macroblocks. Each macroblock is predicted from previously coded blocks of the same slice.

      SP-slice. Consists of P and / or I-macroblocks and lets you switch between encoded streams.

      SI-slice. It consists of a special type of SI-macroblocks and lets you switch between encoded streams.

      I片:只包含I宏块,I宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测)

      P片:可包含P和I宏块,P宏块利用前面已编码图像作为参考图像进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即16x16、16x8、8x16或8x8亮度像素块(以及附带的彩色像素);如果选了8x8的子宏块,则可再分成各种子宏块的分割,其尺寸为8x8、8x4、4x8或4x4亮度像素块(以及附带的彩色像素)。

      B片:可包含B和I宏块,B宏块则利用双向的参考图像(当前和已编码图像帧)进行帧内预测。

      SP片:用于不同编码流之间的切换,包含P和/或I宏块

      SI片:扩展当次中必须具有的切换,它包含了一种特殊类型的编码宏块,叫做SI宏块,SI也是扩展当次中必备功能

说了这么多,上个图看一下h264整体的结构吧

编码后视频的每一组图像(GOP)都给予了传输中的序列sps(Sequence Parameter Set)和本身这个帧的图像参数pps(Picture Paramter Set)

GOP Group Of Picture 图像组。主要用作形容一个I帧到下一个I帧之间间隔了多少个帧(I帧间隔),增大图片组能有效的减少编码后的视频体积,但同时也会降低视频质量

那么我们平时在开发中如何判断一个NALU的类型呢?
如果是带起始码的数据比如0001的话,我们就需要获取这组数据的第5个字节(如果起始码是001的话就是第4个字节)的低5位。
int parseNaluType(uint8_t *data) {
    int type = -1;
    if (data[0] == 0x00 && data[1] == 0x00 
        && data[2] == 0x00 && data[3] == 0x01) {
        type = data[4] & 0x1f;
    }
    return type;
}

关于H.264暂时就介绍这么多,后面我们具体用到的时候会再具体介绍。

  • libx264

    笔者有点懒,这里就不介绍x264的特点了。简单点说,ffmpeg提供了x264的接口,我们可以在编译ffmpeg时在配置选项里打开x264的支持,x264可以为我们提供h264编码的功能。


使用NDKr16b编译ffmpeg+libx264

现在网上大部分的编译脚本都是使用了比较古老的NDK来进行编译,但是由于公司项目中其他部分的.so动态库文件是使用ndk r16b来编译的,为了避免一些莫名其妙的崩溃,我在网上找了很久,终于找到了可以正常使用的编译脚本点这里

#!/bin/bash
FF_VERSION="3.4.5" #这里可以改成其他版本
SOURCE="ffmpeg-$FF_VERSION"
SHELL_PATH=`pwd`
FF_PATH=$SHELL_PATH/$SOURCE
#输出路径
PREFIX=$SHELL_PATH/FFmpeg_android
COMP_BUILD=$1
    
#需要编译的Android API版本
ANDROID_API=19
#需要编译的NDK路径,NDK版本需大等于r15c
NDK=/usr/home/android/ndk #这里换成自己的NDK路径
...

修改好脚本后记得给权限,它会自动去下载最新版本的x264和指定版本的ffmpeg并开始编译,最后输出一个libffmpeg.so文件。

注意:在高版本的ffmpeg configure选项里已经没有了ffserver。

结语

这篇文章我们简单介绍了一部分Android音视频开发需要掌握的一些技术和基本概念。我们还介绍了如何在ndk15--17版本编译ffmpeg+libx264。这为我们在之后学习的内容做了一点点铺垫。在下一期的文章里,我们会把ffmpeg强大的命令行工具集成到我们的Android项目中,我们还会开始编写摄像头部分的代码,我们会使用OpenGL ES来渲染我们的预览数据。今天就到这里,预祝大家春节愉快!