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

目录
  • 一 音频的采集
  • 二 视频画面的采集
  • 三 音频的编码
    • 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
image.png
1.4 写文件

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

二 视频画面的采集

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

2.2 iOS平台的视频画面采集

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

首先来看一下整体架构图

image.png
  • GLImageView 屏幕渲染,实现预览的场景
  • VideoEncoder 编码操作,实现图像处理的需求

接下来分析一下该架构

  • ELImageProgram(EL是整个项目的前缀),用于把OpenGL的Program的构建、查找属性、使用等这些操作以面向对象的形式封装起来,每个节点都会有一个该类的引用实例。

  • ELImageTextureFrame,用于将纹理对象和帧缓存对象的创建、绑定、销毁等操作以面向对象的方式封装起来,使得每个节点的使用都更加方便。

  • 要想使用OpenGL ES,必须要有上下文以及关联的线程,之前也提到过iOS平台为OpenGL ES提供了EAGL作为OpenGL ES上下文。在整个架构的所有组件中,要针对编码器组件单独开辟一个线程,因为我们不希望它阻塞预览线程,从而影响预览的流畅效果,所以它也需要一个单独的OpenGL上下文,并且需要和渲染线程共享OpenGL上下文(两个 OpenGL ES线程共享上下文或者共享一个组,则代表可以互相使用对方的纹理对象以及帧缓存对象),只有这样,在编码线程中才可以正确访问到预览线程中的纹理对象、帧缓存对象。

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

  • 规则一:凡是需要输入纹理对象的节点都是Input类型。
  • 规则二:凡是需要向后级节点输出纹理对象的节点都是Output类型。

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


image.png
2.3 摄像头配置
  1. 既然要使用摄像头,就要使用AVCaptureSession,因为在iOS平台开发中只要是与硬件相关的都要从会话开始进行配置。
  2. 配置AVCaptureDeviceInput,其实该变量就是用于指定需 要使用哪一个摄像头。
  3. 配置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方法,可以使得整个交换过程更加高效,因为CVPixelBufferYUV数据格式的,所以可以分配以下两个纹理对象。

CVOpenGLESTextureRef luminanceTextureRef = NULL;
CVOpenGLESTextureRef chrominanceTextureRef = NULL;

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

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

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

三 音频的编码

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

3.1 libfdk_aac编码AAC
  1. 初始化
int init(int bitRate, int channels, int sampleRate, int bitsPerSample, const char* aacFilePath, const char * codec_name);

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

  1. 编码
void encode(byte* buffer, int size);
  1. 销毁
void destroy();

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

3.2 iOS平台的硬件编码器AudioToolbox

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

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

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

  1. 填充PCM数据,让编码器进行编码
- (UInt32) fillAudioData:(uint8_t*) sampleBuffer bufferSize:(UInt32) bufferSize;
  1. 添加ADTS头信息
- (void) outputAACPakcet:(NSData*) data presentationTimeMills: (int64_t)presentationTimeMills error:(NSError*) error;
  1. 资源销毁与关闭
- (void) onCompletion;

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

四 视频画面的编码

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

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

4.1 libx264编码H264

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

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

image.png

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

image.png

从上图中可以看到整个软件编码器模块的整体结构,其实,纹理拷贝线程是一个生产者,它生产的视频帧会放入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可以为系统带来以下几个优点,

  1. 提高编码性能 (使得CPU的使用率大大降低)
  2. 增加编码效率(使得编码一帧的时间 缩短)
  3. 延长电量使用(耗电量大大降低)

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

VideoToolbox的编码原理如下图所示

image.png

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

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

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

image.png

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

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

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

4.2.2 将编码器集成进系统

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

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

4.2.3 iOS平台高层次的硬件编解码API的理解
image.png

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

  • VideoToolbox更关注编码成为内存中的CM-SampleBuffer结构体,以及解码成为主内存(或者理解为显存)中的CVPixelBuffer结构体,
  • AVFoundation则更关注于解码后直接显示以及直接编码到文件中。

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

  • AVAssetWriter

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

  • AVAssetReader

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

  • AVAssetExportSession

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


项目链接地址如下:

iOS-FDKAACEncoder
iOS-AudioToolboxEncoder
Android-CameraPreview
iOS-VideoToolboxEncoder


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容