目录
- 一 音频的采集
- 二 视频画面的采集
- 三 音频的编码
- 3.1 libfdk_aac编码AAC
- 3.2 iOS平台的硬件编码器AudioToolbox
- 四 视频画面的编码
- 4.1 libx264编码H264
- 4.2 iOS平台的硬件编码器
一 音频的采集
iOS平台提供了多套API采集音频,如果开发者想要直接指定一个路径,则可以将录制的音频编码到文件中,可以使用AVAudioRecorder
这套API。
iOS平台提供了两个层次的API来协助实现,第一种方式是使用AudioQueue
,第二种方式是使用 AudioUnit
,实际上AudioQueue是AudioUnit更高级的封装。
使用场景
1.AVAudioRecorder
简单易用
2.AudioQueue
仅仅是要获取内存中的录音数据,然后再进行编码输出(有可能是输出到本地磁盘,也有可能 是网络)
3.AudioUnit
要使用更多的音效处理,以及实时的监听。
1.2 AVAudioSession的具体使用方法如下
- 获得AVAudioSession的实例
- 为用户输送一路监听耳返,所以这里选择使用类别
AVAudioSessionCategoryPlayAndRecord
,设置这个类别的目的是告诉系统的硬件应该为我们的App提供什么样的服务。 - 为AudioSession设置预设的
采样率
。 - 启用
AudioSession
。 - 为
AudioSession
设置路由监听器
,目的就是在采集音频或者音频输出的线路发生变化的时候(比如插拔耳机、蓝牙设备连接成功等)回调此方法,以便开发者可以重新设置使用当前最新的麦克风或扬声器。
1.3 构造AUGraph
1.4 写文件
ExtAudioFile
,iOS提供的这个API只需要设置好输入格式、输出格式以及输出文件路径和文件格式即可。
二 视频画面的采集
视频画面的采集主要是使用各个平台提供的摄像头API
来实现的, 在为摄像头设置了合适的参数之后,将摄像头实时采集的视频帧渲染到屏幕上提供给用户预览,然后将该视频帧编码
到一个视频文件中,其使用的编码格式一般是H264
。
2.2 iOS平台的视频画面采集
本节会设计并实现一个基于摄像头采集,最终用OpenGL ES
渲染到UIView
上,并且可以支持后期视频特效处理,以及编码视频帧的架构。
首先来看一下整体架构图
-
GLImageView
屏幕渲染,实现预览的场景 -
VideoEncoder
编码操作,实现图像处理的需求
接下来分析一下该架构
ELImageProgram
(EL是整个项目的前缀),用于把OpenGL的Program
的构建、查找属性、使用等这些操作以面向对象的形式封装起来,每个节点都会有一个该类的引用实例。ELImageTextureFrame
,用于将纹理对象和帧缓存对象的创建、绑定、销毁等操作以面向对象的方式封装起来,使得每个节点的使用都更加方便。要想使用
OpenGL ES
,必须要有上下文
以及关联的线程
,之前也提到过iOS
平台为OpenGL ES
提供了EAGL
作为OpenGL ES
的上下文
。在整个架构的所有组件中,要针对编码器组件单独开辟一个线程,因为我们不希望它阻塞预览线程,从而影响预览的流畅效果,所以它也需要一个单独的OpenGL上下文,并且需要和渲染线程共享OpenGL
上下文(两个 OpenGL ES线程共享上下文或者共享一个组,则代表可以互相使用对方的纹理对象以及帧缓存对象),只有这样,在编码线程中才可以正确访问到预览线程中的纹理对象、帧缓存对象。
我们就可以 抽象出以下两个规则。
- 规则一:凡是需要输入纹理对象的节点都是
Input
类型。 - 规则二:凡是需要向后级节点输出纹理对象的节点都是
Output
类型。
基于上面的分析,我们可以画出节点的类图关系
2.3 摄像头配置
- 既然要使用摄像头,就要使用
AVCaptureSession
,因为在iOS平台开发中只要是与硬件相关的都要从会话开始进行配置。 - 配置
AVCaptureDeviceInput
,其实该变量就是用于指定需 要使用哪一个摄像头。 - 配置
AVCaptureVideoDataOutput
,其实该变量是用于配置如何接收摄像头采集的数据的。
2.3 摄像头采集数据处理
由于我们要获取摄像头采集的数据,所以这里需重写该Protocol里面约定的方法,也就是摄像头用来输出数据的方法,签名如下:
-(void)captureOutput:(AVCaptureOutput*)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection*)connection
最重要的是CMSample-Buffer
类型的sampleBuffer
,其中实际存储着摄像头采集到的图像,CMSampleBuffer
结构体由以下三个部分组成。
-
CMTime
代表了这一帧图像的时间。 -
CMVideoFormatDescription
代表了对于这一帧图像格式的描述。 -
CVPixelBuffer
代表了这一帧图像的具体数据。
iOS平台不允许App进入后台
的时候还进行OpenGL
的渲染操作,如果App依然进行渲染操作
的话,那么系统就会强制杀掉该App。
在iOS平台上的CoreVideo
这个framework
中提供了
CVOpenGLESTextureCacheCreateTextureFromImage
方法,可以使得整个交换过程更加高效,因为CVPixelBuffer
是YUV
数据格式的,所以可以分配以下两个纹理对象。
CVOpenGLESTextureRef luminanceTextureRef = NULL;
CVOpenGLESTextureRef chrominanceTextureRef = NULL;
为什么非要转换为RGBA格式呢?
因为在OpenGL
中纹理的默认格式都是RGBA
格式的,并且也要为后续的纹理处理以及渲染到屏幕上打下基础,最终编码器也是以RGBA
格式为基础进行转换和处理的。
YUV转RGBA
在FragmentShader
中将YUV
转换为RGBA
格式。
三 音频的编码
无论是单独的音频编码,还是视频编码中的音频流部分,使用得最广泛的都是AAC
的编码格式。
3.1 libfdk_aac编码AAC
- 初始化
int init(int bitRate, int channels, int sampleRate, int bitsPerSample, const char* aacFilePath, const char * codec_name);
首先是比特率,也就是最终编码出来的文件的码率,接着是声道 数、采样率,这两个将不再赘述,然后是最终编码的文件路径,最后是编码器的名字。
- 编码
void encode(byte* buffer, int size);
- 销毁
void destroy();
销毁前面所分配的资源以及打开的连接通道。
3.2 iOS平台的硬件编码器AudioToolbox
可使用AudioToolbox
下的 Audio Converter Services
来完成硬件编码。
AudioToolbox
中编码出来的AAC数据也是裸数据,在写入文件之前 也需要添加上ADTS
头信息,最终写出来的文件才可以被系统播放器播放。
类似于软件编码提供的三个接口方法,这里也提供了三个接口方法,分别用于完成初始化
、编码数据
和销毁编码器
的操作。
- 填充PCM数据,让编码器进行编码
- (UInt32) fillAudioData:(uint8_t*) sampleBuffer bufferSize:(UInt32) bufferSize;
- 添加ADTS头信息
- (void) outputAACPakcet:(NSData*) data presentationTimeMills: (int64_t)presentationTimeMills error:(NSError*) error;
- 资源销毁与关闭
- (void) onCompletion;
iOS平台提供了音视频的API,如果需要用到硬件Device相关的API,就需要配置各种Session
;如果要用到与提供的软件相关的API,就需要配置各种Description
以描述配置的信息,而在这里需要配置的Description就是前面介绍的AudioUnit部分所配置的Description
。
四 视频画面的编码
软件编码实际使用的库是libx264
库,但是开发是基于FFmpeg的API进行的。
而编码的输入就是本文前面摄像头捕捉的纹理图像(显存中的表示),输出是H264
的Annexb
封装格式的流。
4.1 libx264编码H264
由于输入是一张纹理
,输出是H264
的裸流。
VideoEncoderAdapter
。为一个类命名其实就是根据该类的职责而确定的,上面这个类实际上就是将输入的纹理ID
做一个转换,使得转换之后的数据可以作为具体编码器
的输入。
从全局来看一下软件编码器的整体结 构,如下图所示。
从上图中可以看到整个软件编码器模块的整体结构,其实,纹理拷贝线程
是一个生产者,它生产的视频帧会放入VideoFrameQueue
中; 而编码线程
则是一个消费者,其可从VideoFrameQueue
中取出视频帧, 再进行编码
,编码好的H264
数据将输出到目标文件中。
Video-FrameQueue
,这是一个我们自己实现的保证线程安全
的队列,实际上就是一个链表
,链表中每个Node
节点内部的元素均是一个VideoFrame
的结构体。
编码线程
,在编码线程中首先需要实例化编码器,然后进入一个循环,不断从VideoFrameQueue
里面取出视频帧元素,调用编码器进行编码,如果从VideoFrameQueue
中获取元素的返回值是-1
,则跳出循环,最后销毁编码器。
纹理拷贝线程
,该线程首先需要初始化OpenGL ES的上下文环境,然后 绑定到新建立的这个纹理拷贝线程之上。
帧缓存对象
是任何一个OpenGL Program
渲染的目标。
4.2 iOS平台的硬件编码器
在iOS8.0以后,系统提供了VideoToolbox
编码API,该API可以充分 使用硬件来做编码工作以提升性能和编码速度。
首先来介绍VideoToolbox
如何将一帧视频帧数据编码为H264
的压缩数据,并把它封装到H264HWEncoderImpl
类中,然后再将封装好的这个类集成进前面的预览系统中,集成进去之后,对于原来仅仅是预览的项目,也可以将其保存到一个H264
文件中了。
4.2.1 使用VideoToolbox构造自己的编码器
使用VideoToolbox
可以为系统带来以下几个优点,
- 提高编码性能 (使得CPU的使用率大大降低)
- 增加编码效率(使得编码一帧的时间 缩短)
- 延长电量使用(耗电量大大降低)
而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
信息就存在于这个对于视频格式的描述里面。
4.2.2 将编码器集成进系统
Video-Encoder
也是一个输出节点,该输出节点是编码并写到磁盘中的。
有两点需要注意。
第一点,由于要将纹理对象渲染之后再放到编码器中。
第二点,由于渲染到的目标纹理对象需要交给编码器进行编码。
4.2.3 iOS平台高层次的硬件编解码API的理解
如上图所示,iOS平台提供的多媒体接口是从底层到上层的结 构,之前都是直接使用VideoToolbox
,而AVFoundation
是基于VideoToolbox
进行的封装。它们的关注点不一样。
- VideoToolbox更关注编码成为内存中的
CM-SampleBuffer
结构体,以及解码成为主内存(或者理解为显存)中的CVPixelBuffer
结构体, -
AVFoundation
则更关注于解码后直接显示以及直接编码到文件中。
重点来看一下AVFoundation
这个层次提供的几个主要API。
AVAssetWriter
为了写入本地文件而提供的API,该类可 以方便地将图像和音频写成一个完整的本地视频文件。
AVAssetReader
该类可以 方便地将本地文件中的音频和视频解码出来。
AVAssetExportSession
这个类的使用场景比较多,比如拼接视频、合并音频与视频、转换格式,以及压缩视频等多种场景,其实是一个更高层次的封装。
项目链接地址如下:
iOS-FDKAACEncoder
iOS-AudioToolboxEncoder
Android-CameraPreview
iOS-VideoToolboxEncoder