第六章 音视频的采集与编码

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第六章 音视频的采集与编码相关的知识,希望对你有一定的参考价值。

参考技术A

ios平台提供了多套API采集音频,如果开发者想要直接指定一个路径,则可以将录制的音频编码到文件中,可以使用 AVAudioRecorder 这套API。

iOS平台提供了两个层次的API来协助实现,第一种方式是使用 AudioQueue ,第二种方式是使用 AudioUnit ,实际上AudioQueue是AudioUnit更高级的封装。

使用场景
1. AVAudioRecorder 简单易用
2. AudioQueue 仅仅是要获取内存中的录音数据,然后再进行编码输出(有可能是输出到本地磁盘,也有可能 是网络)
3. AudioUnit 要使用更多的音效处理,以及实时的监听。

ExtAudioFile ,iOS提供的这个API只需要设置好输入格式、输出格式以及输出文件路径和文件格式即可。

视频画面的采集主要是使用各个平台提供的 摄像头API 来实现的, 在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到屏幕上提供给用户预览,然后将该视频帧 编码 到一个视频文件中,其使用的编码格式一般是 H264 。

本节会设计并实现一个基于摄像头采集,最终用 OpenGL ES 渲染到 UIView 上,并且可以支持后期视频特效处理,以及编码视频帧的架构。

首先来看一下整体架构图

接下来分析一下该架构

我们就可以 抽象出以下两个规则。

基于上面的分析,我们可以画出节点的类图关系

由于我们要获取摄像头采集的数据,所以这里需重写该Protocol里面约定的方法,也就是摄像头用来输出数据的方法,签名如下:

最重要的是 CMSample-Buffer 类型的 sampleBuffer ,其中实际存储着摄像头采集到的图像, CMSampleBuffer 结构体由以下三个部分组成。

iOS平台不允许App进入 后台 的时候还进行 OpenGL 的渲染操作,如果App依然进行 渲染操作 的话,那么系统就会强制杀掉该App。

在iOS平台上的 CoreVideo 这个 framework 中提供了
CVOpenGLESTextureCacheCreateTextureFromImage 方法,可以使得整个交换过程更加高效,因为 CVPixelBuffer 是 YUV 数据格式的,所以可以分配以下两个纹理对象。

为什么非要转换为RGBA格式呢?

因为在 OpenGL 中纹理的默认格式都是 RGBA 格式的,并且也要为后续的纹理处理以及渲染到屏幕上打下基础,最终编码器也是以 RGBA 格式为基础进行转换和处理的。

YUV转RGBA
在 FragmentShader 中将 YUV 转换为 RGBA 格式。

无论是单独的音频编码,还是视频编码中的音频流部分,使用得最广泛的都是 AAC 的编码格式。

首先是比特率,也就是最终编码出来的文件的码率,接着是声道 数、采样率,这两个将不再赘述,然后是最终编码的文件路径,最后是编码器的名字。

销毁前面所分配的资源以及打开的连接通道。

可使用 AudioToolbox 下的 Audio Converter Services 来完成硬件编码。

AudioToolbox 中编码出来的AAC数据也是裸数据,在写入文件之前 也需要添加上 ADTS 头信息,最终写出来的文件才可以被系统播放器播放。

类似于软件编码提供的三个接口方法,这里也提供了三个接口方法,分别用于完成 初始化 、 编码数据 和 销毁编码器 的操作。

iOS平台提供了音视频的API,如果需要用到硬件Device相关的API,就需要配置各种 Session ;如果要用到与提供的软件相关的API,就需要配置各种 Description 以描述配置的信息,而在这里需要配置的Description就是前面介绍的AudioUnit部分所配置的 Description 。

软件编码实际使用的库是 libx264 库,但是开发是基于FFmpeg的API进行的。

而编码的输入就是本文前面摄像头捕捉的纹理图像(显存中的表示),输出是 H264 的 Annexb 封装格式的流。

由于输入是一张 纹理 ,输出是 H264 的裸流。

VideoEncoderAdapter 。为一个类命名其实就是根据该类的职责而确定的,上面这个类实际上就是将输入的 纹理ID 做一个转换,使得转换之后的数据可以作为具体 编码器 的输入。

从全局来看一下软件编码器的整体结 构,如下图所示。

从上图中可以看到整个软件编码器模块的整体结构,其实, 纹理拷贝线程 是一个生产者,它生产的视频帧会放入 VideoFrameQueue 中; 而 编码线程 则是一个消费者,其可从 VideoFrameQueue 中取出视频帧, 再进行 编码 ,编码好的 H264 数据将输出到目标文件中。

Video-FrameQueue ,这是一个我们自己实现的 保证线程安全 的队列,实际上就是一个 链表 ,链表中每个 Node 节点内部的元素均是一个 VideoFrame 的结构体。

编码线程 ,在编码线程中首先需要实例化编码器,然后进入一个循环,不断从 VideoFrameQueue 里面取出视频帧元素,调用编码器进行编码,如果从 VideoFrameQueue 中获取元素的返回值是 -1 ,则跳出循环,最后销毁编码器。

纹理拷贝线程 ,该线程首先需要初始化OpenGL ES的上下文环境,然后 绑定到新建立的这个纹理拷贝线程之上。

帧缓存对象 是任何一个 OpenGL Program 渲染的目标。

在iOS8.0以后,系统提供了 VideoToolbox 编码API,该API可以充分 使用硬件来做编码工作以提升性能和编码速度。

首先来介绍 VideoToolbox 如何将一帧视频帧数据编码为 H264 的压缩数据,并把它封装到 H264HWEncoderImpl 类中,然后再将封装好的这个类集成进前面的预览系统中,集成进去之后,对于原来仅仅是预览的项目,也可以将其保存到一个 H264 文件中了。

使用 VideoToolbox 可以为系统带来以下几个优点,

而VideoToolbox是iOS 8.0 以后才公开的API,既可以做编码又可以做解码工作。

VideoToolbox的编码原理如下图所示

左边的三帧视频帧是发送给编码器之前的数据,开发者必须将原始图像数据封装为 CVPixelBuffer 的数据结构,该数据结构是使用 VideoToolbox 编解码的核心。

iOS的 CoreVideo 这个 framework 提供的方法 CVOpenGLESTextureCacheCreateTextureFromImage 就是专门用来将 纹理对象 关联到 CVPixelBuffer 表示视频帧的方法。

下面来看这个编码器输出的对象, Camera 预览返回的 CMSampleBuffer 中存储的数据是一个 CVPixelBuffer ,而经过 VideoToolbox 编码输出的 CMSampleBuffer 中存储的数据是一个 CMBlockBuffer 的引用,如下图所示。

如何构建编码器,使用 Camera 的时候使用的是 AVCaptureSession ,而这里使用的会话就是 VTCompressionSession ,这个会话就代表要 使用编码器 ,等后续讲到 硬件解码场景 时将要使用的会话就是 VTDecompressionSessionRef 。

为什么要判断关键帧呢?因为 VideoToolbox 编码器在每一个关键帧前面都会输出 SPS 和 PPS 信息,所以如果本帧是关键帧,则取出对应的 SPS 和 PPS 信息。

那么如何取出对应的SPS和PPS信息呢?前面提到 CMSampleBuffer 中有一个成员是 CMVideoFormatDesc ,而 SPS 和 PPS 信息就存在于这个对于视频格式的描述里面。

Video-Encoder 也是一个输出节点,该输出节点是编码并写到磁盘中的。

有两点需要注意。
第一点,由于要将纹理对象渲染之后再放到编码器中。
第二点,由于渲染到的目标纹理对象需要交给编码器进行编码。

如上图所示,iOS平台提供的多媒体接口是从底层到上层的结 构,之前都是直接使用 VideoToolbox ,而 AVFoundation 是基于 VideoToolbox 进行的封装。它们的关注点不一样。

重点来看一下 AVFoundation 这个层次提供的几个主要API。

为了写入本地文件而提供的API,该类可 以方便地将图像和音频写成一个完整的本地视频文件。

该类可以 方便地将本地文件中的音频和视频解码出来。

这个类的使用场景比较多,比如拼接视频、合并音频与视频、转换格式,以及压缩视频等多种场景,其实是一个更高层次的封装。

项目链接地址如下:

iOS-FDKAACEncoder
iOS-AudioToolboxEncoder
android-CameraPreview
iOS-VideoToolboxEncoder

数据结构与算法(周鹏-未出版)-第六章 树-6.5 Huffman 树

6.5 Huffman


  Huffman 树又称最优树,可以用来构造最优编码,用于信息传输、数据压缩等方面,是一类有着广泛应用的二叉树。


6.5.1 二叉编码树


  在计算机系统中,符号数据在处理之前首先需要对符号进行二进制编码。例如,在计算机中使用的英文字符的 ASCII 编码就是 8 位二进制编码,由于 ASCII 码使用固定长度的二进制位表示字符,因此 ASCII 码是一种定长编码。为了缩短数据编码长度,可以采用不定长编码。其基本思想是:给使用频度较高的字符编较短的编码,这是数据压缩技术的最基本思想。如何给数据中的字符编以不定长编码,而使数据编码的平均长度最短呢?


  首先分析第一个问题:如何对字符集进行不定长编码。在一个编码系统中,任何一个编码都不是其他编码的前缀,则称该编码系统的编码是前缀码。例如: 01, 10, 110, 111, 101 就不是前缀编码,因为 10 101 的前缀,如果去掉 10 101 就是前缀编码。当在一个编码系统中采用定长编码时,可以不需要分隔符;如果采用不
是前缀编码,因为 10 101 的前缀,如果去掉 10 101 就是前缀编码。当在一个编码系统中采用定长编码时,可以不需要分隔符;如果采用不定长编码时,必须使用前缀编码或分隔符,否则在解码时会产生歧义。所谓解码就是由二进制位串还原字符数据的过程。而使用分隔符会加大编码长度,因此一般采用前缀编码。例 6-1 说明了这个问题。

 

6-1 假设字符集为{A, B, C, D},原文为 ABACCDA


一种等长编码方案为 A:00 B:01 C:10 D:11,此时编解码不会产生歧义,过程如下。
编码: ABACCDA 00010010101100
解码: 00010010101100 ABACCDA
一种不等长编码方案为: A:0 B:00 C:1 D:01,由于此编码不是前缀码,此时在编解码的过程中会产生歧义。对于同一编码可以有不同的解码,过程如下。
编码: ABACCDA 000011010
解码: 000011010 AAAACCDA
000011010 BBCCDA 错误!出现歧义。

 

  为产生没有歧义的前缀编码,可以使用二叉编码树来实现。使用二叉树对字符集中的字符进行编码的方法是,将字符集中的所有字符作为二叉树的叶子结点;在二叉树中,每一个“父亲—左孩子”关系对应一位二进制位 0,每一个“父亲—右孩子”关系对应一位二进制位 1 ;于是从根结点通往每个叶子结点的路径,就对应于相应字符的二进制编码。每个字符编码的长度 L 等于对应路径的长度,也等于该叶子结点的层次数。例如对于例 6-1 中的每个字符可以按照图 6-15 所示的二叉编码树
进行编码。按照图 6-15 中的二叉编码树对 ABCD 四个字符进行编码,则 A 的编码是 0B 的编码是 100C 的编码是 11 D的编码是 101
。这个编码显然是一个前缀编码。

 

  由于在二叉树中任何一个叶子结点都不会出现在根到其他叶子结点的路径上,那么按照上述二叉编码树的编码方法,任何一个叶子结点表示的编码都不会是任何其他叶子表示编码的前缀,因此由二叉编码树得到的编码都是前缀码。反过来如果要进行解码,也可以由二叉编码树便捷的完成。解码的过程是从头开始扫描二进制编码位串,并从二叉编码树的根结点开始,根据比特位不断进入下一层结点,当碰到0 时向左深入,为 1 时向右深入;到达叶子结点后输出其对应的字符,然后重新回到根结点,并继续扫描二进制位串直到完毕。还是如图 6-15 所示,此时将 ABACCDA 进行编码得到: 0100011111010。解码过程是从左到右扫描二进制位串。在读出最前端的 0 后,相应的从根结点到达结点,于是输出 A重新回到根结点;依次扫描后续二进制位 100,到达叶子结点 B,于是输出 B,重新回到根结点;读出下一个二进制位 0,输出 A;读出 11 ,输出 C;读出 11 ,输出 C;读出 101,输D;最后读出 0,输出 A;此时二进制位串扫描完毕,相应的解码工作也完成,最后得到字符数据 ABACCDA


6.5.2 Huffman 树及 Huffman 编码


  在上一小节中介绍了如何对字符集进行不定长编码的方法,但是同时我们看到对于同一个字符集进行编码的二叉编码树可以有很多,只要叶子结点个数与字符个数对应即可。例如
对例 6-1 中字符即进行编码的二叉树就可以有,但不限于图 6-16 所示的二叉树。在这些不同的编码中哪个才是使得编码长度最小的呢?例如在例 6-1 中,选择图 6-15 中的编码方案比选择图 6-16 中的两种编码方案好。由于
字符 ABCD 分别出现了 3 次、 1 次、
2 次、 1 次。使用图 6-15 的编码方案,编码的长度为 3×1+1×3+2×2+1×3=13;使用图 6-16a)的编码方案,编码的长度为 3×3+1×2+2×3+1×1
=18;使用图 6-16b)的编码方案,编码的长度为 3×3+1×2+2×1+1×3=16

 

字符集中各种字符出现的概率是不同的,字符的出现概率决定了编码方案的选择。

 

 

  当引入以上概念以后,求最佳编码方案实际上就抽象为求在叶子结点个数与权确定时带权路径长度最小的二叉树。那么什么样的树带权路径长度最小呢?
对于给定n个权值w1, w2, … wnn≥2),求一棵具有n个叶子结点的二叉树,使其带权路径长度∑ WiLi最小。由于Huffman给出了构造具有这种树的方法,因此这种树称为Huffman树。


Huffman 树: 它是由 n 个带权叶子结点构成的所有二叉树中带权路径长度最小的二叉树, Huffman 树又称最优二叉树。

《信息与编码》考试复习笔记6----第六章连续信源熵和信道容量相关例题

JAVA-初步认识-第六章-类类型参数

《信息与编码》考试复习笔记6----第六章连续信源熵和信道容量(考点在连续信道容量)

数据结构与算法(周鹏-未出版)-第六章 树-6.5 Huffman 树

研究Android音视频-3-在Android设备上采集音视频并使用MediaCodec编码为H.264

采集音频和摄像头视频并实时H264编码及AAC编码