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

如题所述

第1个回答  2022-06-28

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