iOS音视频:直播

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、知识点
    • 1、视频封装格式
    • 2、视音频编解码方式
    • 3、直播APP流程
    • 4、直播APP架构
    • 5、H264压缩技术
    • 6、H264的结构与码流
    • 7、VideoToolBox框架
    • 8、YUV颜色体系
    • 9、PCM脉冲编码调制
  • 二、VideoToolBox框架的使用演示
    • 1、框架和属性
    • 2、开始捕捉视频方法的实现
    • 3、使用 VideoToolBox 进行编码方法的实现
    • 4、停止捕捉视频方法的实现
    • 5、AVCaptureVideoDataOutputSampleBufferDelegate 捕捉到视频帧进行编码
    • 6、编码完成后的回调函数
  • 三、H264硬编码解码工具类封装实现
    • 1、辅助工具类
    • 2、H264硬编码工具类
    • 3、H264硬解码工具类
  • 四、音频AAC硬编码解码工具类封装实现
    • 1、音频AAC硬编码器
    • 2、音频AAC硬解码器
    • 3、音频PCM播放
  • Demo
  • 参考文献

一、视频知识点

1、视频封装格式

封装格式:就是将已经编码压缩好的视频数据和音频数据按照一定的格式放到一个文件中。通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据,例如字幕。

AVI:是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
MOV:Quicktime封装
WMV:微软推出的
mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
flv:这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
MP4:mpeg4的封装,主要在手机上使用。


2、视音频编解码方式

H264编码的优势

具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍。原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。

AAC音频编码格式

AAC是目前比较热门的有损压缩编码技术,在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。网易云音乐等音乐类APP多使用MP3和WMV无损压缩编码格式。


3、直播APP流程

❶ 采集音频、视频
摄像头、麦克风
  • 摄像头:CCD(图像传感器)
  • 麦克风:拾音器(声音传感器)
采集音视频数据
  • 导入AV Foundation.Framework 框架
  • CaptureSession会话的回调中获取音视频数据
❷ 视频滤镜
  • 使用GPUlmage处理美颜、水印
❸ 音频、视频编码压缩
硬编码
  • 视频:VideoToolBox 框架
  • 音频:AudioToolBox框架
视频压缩软编码
  • 视频编码:MPEGH264
  • X264把视频原数据YUV/RGB编码为H264
音频压缩软编码
  • 音频编码: mp3AAC
  • fdk aac将音频数据PCMAAC
❹ 推流
  • 什么是推流:将采集的音频,视频数据通过流媒体协议发送到流媒体服务器
  • muxing(封装):音视频封包成FLV或者TS
推流技术
  • 流媒体协议:RTMP\RTSP\HL S\FLV
  • 视频封装格式:TS\FLV
  • 音频封装格式:mp3\AAC
❺流媒体服务器处理数据
  • 数据分发(CDN)
  • 截屏
  • 录制
  • 实时转码
❻ 拉流
  • 什么是拉流:从流媒体服务器获取音频、视频数据
  • 流媒体协议:RTMP\RTSP\HLS\FLV
❼ 音频、视频解码
  • demuxing(解封装):将FLV\TS文件分离出音视频
  • 视频解码:硬解码(VideoToolbox)、软解码(X264)
  • 音频解码:硬解码(AudioToolBox)、软解码(fdk_ aac)
❽ 播放
  • lijkplayer:一款开源强大的播放器
❾ 聊天互动
  • IM及时通讯
  • 聊天堂功能
  • 第三方及时通讯:融云、腾讯云

4、直播APP架构

❶ 采集端
  • ⾳视频采集
  • 视频处理(美颜)
  • 音视频编码压缩
  • 把音视频封装成FLV/TS
❷ 服务器
  • 数据分发(CDN)
  • 鉴定数据合法
  • 截屏:封⾯
  • 实时转码
❸ 播放端
  • 分离⾳视频
  • ⾳视频解码
  • 播放
  • 聊天互动
❹ 采集端常⽤框架
  • AV Foundation:采集数据
  • GPUImage:滤镜美颜
  • FFmpeg:⾳视频编码
  • videoToolBox:视频编码
  • AudioToolBox:音频编码
  • Libremp:推流
❺ 服务器常⽤框架
  • SNS
  • BMS
  • nginx
❻ 播放端常⽤框架
  • FFmpeg:音视频解码
  • videoToolBox:视频解码
  • AudioToolBox:⾳频解码
  • ijkplayer:播放

5、H264压缩技术

帧内预测压缩

解决的是空域数据冗余问题。什么是空域数据?就是这幅图里数据在宽高空间内包含了很多颜色和光亮,其中有些是人的肉眼很难察觉的数据,对于这些数据,我们可以认作是冗余的,直接将其压缩掉。

帧间预测压缩

解决的是时域数据冗余问题。摄像头在一段时间内所捕捉的数据没有较大的变化,我们针对这一段时间将相同的数据压缩掉,这叫时域数据压缩。

I帧: 关键帧

属于帧内压缩技术。如果摄像头对着你拍摄,1秒之内实际你发生的变化是非常少的。摄像机一般一秒钟会抓取几十帧的数据,比如像动画就是25帧/s一般视频文件都是在30帧/s左右,对于一些要求比较高的对动作的精细度有要求想要捕捉到完整的动作的高级的摄像机一般是60帧/s。那些对于一组帧的它的变化很小,为了便于压缩数据那怎么办?可以将第一帧完整的保存下来作为关键帧。如果没有这个关键帧后面解码数据是完成不了的,所以I帧特别关键。

P帧: 向前参考帧

压缩时只参考前一个帧,属于帧间压缩技术。视频的第一帧会被作为关键帧完整保存下来,而后面的帧会向前依赖,也就是第二帧依赖于第一个帧。后面所有的帧只存储于前一帧的差异,这样就能将数据大大的减少,从而达到一个高压缩率的效果。

B帧: 双向参考帧

压缩时既参考前一帧也参考后一帧,属于帧间压缩技术。这样就使得它的压缩率更高,存储的数据量更小,如果B帧的数量越多,你的压缩率就越高,这是B帧的优点。B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,在网络中就要等待后面的帧传输过来。这就与网络状态有关了,如果网络状态很好的话,解码会比较快,如果网络状态不好,解码会稍微慢一些,丢包时还需要重传,所以对实时互动的直播,一般不会使用B帧。如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧。

GOF(Group of Frame)一组帧

如果在一秒钟内,有30帧,这30帧可以划为一组。如果摄像机一分钟之内它都没有发生大的变化,那也可以把这一分钟内所有的帧画做一组。什么叫一组帧?就是一个I帧到下一个I帧,包括B帧/P帧,这一组的数据,我们称为GOF。

SPS/PPS
  • SPS(Sequence Parameter Set,序列参数集):存放帧数,参考帧数目,解码图像尺寸等
  • PPS(Picture Parameter Set,图像参数集):存放熵编码模式选择标识,片组数目,初始量化参数等(与图像相关的信息)

在一组帧之前我们首先收到的是SPS/PPS数据,如果没有这组参数的话,我们是无法解码。如果我们在解码时发生错误,首先要检查是否有SPS/PPS。如果没有,就需要判断是因为对端没有发送过来还是因为对端在发送过程中丢失了。

视频花屏/卡顿原因

花屏是因为你丢了P帧或者I帧,导致解码错误.。卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了,直达下一组正确的GOP再重新刷屏,而这中间的时间差,就是我们所感受的卡顿。


6、H264的结构与码流

H264结构

H264结构中,一个视频图像编码后的数据叫做一帧,一帧由一个片(slice)或多个片组成,一个片由一个或多个宏块(MB)组成,一个宏块由16x16的亮度像素数据组成。宏块作为H264编码的基本单位。宏块利用从当前片中已解码的像素作为参考进行帧内预测。

H264码流
  • NALU:就是生成压缩流之后,我们还要在每个帧之前加一个起始位,起始位一般是十六进制的0001。
  • NALU = NAL Header + NAL Body:H.264码流在网络中传输时实际是以NALU的形式进行传输的。每个NALU由一个字节的HeaderRBSP组成。
  • F(forbidden_ zero_ bit):在H.264规范中规定了第一位必须为0
  • NRI:用于表示当前NALU的重要性,值越大,越重要。解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU
  • TYPE:5表示IDR图像的片(可以理解为I帧,l帧由多个|片组成)。7表示序列参数集(sps)。8表示图像参数集(pps)
H264编码分层
NAL层(Network Abstraction Layer,视频数据网络抽象层)

在网络上传输的过程每个包以太网是1500字节,而H264的帧往往会大于1500字节的,所以就要进行拆包,将一个帧拆成多个包进行传输,所有的拆包或者组包都是通过NAL层去处理的。

NAL 单元是由一个NALU头部+一个切片组成。切片又可以细分成"切片头+切片数据"。我们之间了解过一个H254的帧是由多个切片构成的,因为一帧数据一次有可能传不完。每个切片都包括切片头+切片数据,每个切片数据包括了很多宏块。每个宏块包括了宏块的类型、宏块的预测、残差数据。

VCL层(Video Coding Layer,视频数据编码层)

对视频原始数据进行压缩


7、VideoToolBox框架

VideoToolbox.framework 是基于Core FoundationcoreMediacoreVideo库,提供C语言API。实际上属于低级框架,可以直接访问硬件编码器和解码器。它为视频压缩和解压缩以及存储在像素缓存区中的数据转换提供服务。从coreMediacoreVideo框架衍生出时间或帧管理数据类型,CMTimeCVPixelBuffer(提供未编码的帧或者解码后的帧)。

硬编码的优点
  • 提高编码性能(使用CPU的使用率大大降低,倾向使用GPU)
  • 增加编码效率(将编码一帧的时间缩短)
  • 延长电量使用(耗电量大大降低)
VideoToolBox框架的流程
  • 创建session
  • 设置编码相关参数
  • 开始编码
  • 循环获取采集数据
  • 获取编码后数据
  • 将数据写入H264文件
编码的输入和输出

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

FFmpeg、 硬编码的方式,都是这样的格式


8、YUV颜色体系

我们比较熟悉的颜色系统RGB每一个颜色通道占有1个字节。对YUV颜色体系,大多数人就比较陌生了,因为这个YUV颜色体系是做音视频这一块业务的开发者比较熟悉的。

YUV(也称为YCbCr)是电视系统所采用的一种颜色编码方法。Y表示明亮度,也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像的色彩及饱和度,它们用于指定像素的颜色。那YUV和我们的视频有什么关系呢?关系就是我们的摄像机录制出来的视频就是YUV。常用的YUV4:2:0 (YCbCr 4:2:0)会比RGB少二分之一的存储空间。

YUV4:2:0并不意味着只有Y,Cb2个分量,而没有Cr分量。它实际指的是对于每行扫描线来说只有一种色度分量,它以2:1的抽样率进行存储。相邻的扫描行存储不同的色度分量,也就是说,如果一行是4:2:0,下一行就是4:0:2,再下一行是4:2:0,以此类推。


9、PCM脉冲编码调制

通过采样——>量化——>编码得到PCM数据。

音频压缩的需求

数字音频的质量取决于采样频率和量化位数。这2个参数为了保证取样点尽量密集,要求取样频率要高,量化⽐特率要大,导致的结果就是存储容量过大及传输通道压⼒过大。

  • ⾳频信号的传输率 = 取样频率 * 样本量化比特数 * 通道数
  • 取样频率 = 44.1kHz
  • 样本值的量化比特数 = 16
  • 普通立体声的信号通道数 = 2
  • 数字信号传输码流大约 1.4M bit/s,即一秒钟的数据量为 1.4Mbit / (8/Byte) ,达176.4Byte(字节),等于88200个汉字的数据量
⾳频压缩的原理
  • 消除冗余数据(有损编码):因为采集过程会采集各种频率声音,所以我们可以丢弃人耳无法听到那一部分声音数据,这样可以大大减少数据的存储。
  • 哈夫曼无损编码:除了人耳部分听不到声音压缩之外,其他的声音数据都原样保留,压缩后数据能够完全复原。(短码高频,长码低频)
  • 频域遮蔽效应:一个较弱的声音的听觉会被另一个较强的声音影响
  • 时域遮蔽效应:声音传播的过程中可能会被遮蔽掉
将PCM转换为AAC⾳频流
  • 设置编码器(codec),并开始录制
  • 收集PCM数据,传给编码器
  • 编码完成后回调callback写⼊文件

二、VideoToolBox框架的使用演示

运行效果
2020-12-29 11:34:27.828445+0800 VideoToolBoxDemo[3872:568129] 成功新建文件
2020-12-29 11:34:27.828512+0800 VideoToolBoxDemo[3872:568129] 文件存储路径为:/var/mobile/Containers/Data/Application/8CAB5972-6C9D-4ACE-AC52-5AFBDE4040FF/Documents/video.h264
2020-12-29 11:34:27.870635+0800 VideoToolBoxDemo[3872:568129] 调用VTCompressionSessionCreate创建编码session:0
2020-12-29 11:34:28.215580+0800 VideoToolBoxDemo[3872:568165] H.264在VTCompressionSessionEncodeFrame时成功
2020-12-29 11:34:28.215824+0800 VideoToolBoxDemo[3872:568164] didCompressH264 编码完成后的回调函数被调用 status状态为:0,infoFlags为:1
2020-12-29 11:34:28.216039+0800 VideoToolBoxDemo[3872:568164] gotSpsPp 14 4
2020-12-29 11:34:28.216645+0800 VideoToolBoxDemo[3872:568164] 将nalu数据写入到文件,数据长度为:41

1、框架和属性

导入框架
#import 
#import 
扩展属性
@interface ViewController ()

@property(nonatomic,strong)UILabel *tipLabel;
@property(nonatomic,strong)AVCaptureSession *capturesession;//捕捉会话,用于输入输出设备之间的数据传递
@property(nonatomic,strong)AVCaptureDeviceInput *captureDeviceInput;//捕捉输入
@property(nonatomic,strong)AVCaptureVideoDataOutput *captureDataOutput;//捕捉输出
@property(nonatomic,strong)AVCaptureVideoPreviewLayer *previewLayer;//预览图层

@end
成员变量
@implementation ViewController
{
    int  frameID; //帧ID
    dispatch_queue_t captureQueue; //捕获队列
    dispatch_queue_t encodeQueue;  //编码队列
    VTCompressionSessionRef encodeingSession;//编码session
    CMFormatDescriptionRef format; //编码格式
    NSFileHandle *fileHandele;
}

2、开始捕捉视频方法的实现

流程
  1. 使用AVFoundation进行数据采集。AVFoundation采集基于一个系统相机,没有涉及视频编码
  2. 采集之后使用VideoToolBox进行视频编码
  3. 编码完成后转为H264文件
  4. 写入本地沙盒或者通过网络传给后端
❶ 初始化CaptureSession
- (void)startCapture
{
    self.capturesession = [[AVCaptureSession alloc]init];
    .......
}
❷ 设置捕捉分辨率
self.capturesession.sessionPreset = AVCaptureSessionPreset640x480;
❸ 使用函数dispath_get_global_queue去初始化队列
captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
encodeQueue  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
❹ 配置输入设备
AVCaptureDevice *inputCamera = nil;
//获取用于视频捕捉的所有设备,例如前置摄像头、后置摄像头等
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices)
{
    //拿到后置摄像头
    if ([device position] == AVCaptureDevicePositionBack)
    {
        inputCamera = device;
    }
}
//将捕捉设备 封装成 AVCaptureDeviceInput 对象
self.captureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil];

//判断是否能加入后置摄像头作为输入设备
if ([self.capturesession canAddInput:self.captureDeviceInput])
{
    //将设备添加到会话中
    [self.capturesession addInput:self.captureDeviceInput];
}
❺ 配置输出设备
self.captureDataOutput = [[AVCaptureVideoDataOutput alloc] init];

设置丢弃最后的video frameNO

[self.captureDataOutput setAlwaysDiscardsLateVideoFrames:NO];

设置video的视频捕捉的像素点压缩方式为YUV4:2:0。

[self.captureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];

设置捕捉代理和捕捉队列

[self.captureDataOutput setSampleBufferDelegate:self queue:captureQueue];

添加输出设备

if ([self.capturesession canAddOutput:self.captureDataOutput])
{
    //添加输出
    [self.capturesession addOutput:self.captureDataOutput];
}

创建连接

AVCaptureConnection *connection = [self.captureDataOutput connectionWithMediaType:AVMediaTypeVideo];

设置连接的方向

[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
❻ 添加预览图层

初始化图层

self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.capturesession];

设置视频重力为自适应

[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];

设置图层的frame大小

[self.previewLayer setFrame:self.view.bounds];

添加预览图层

[self.view.layer addSublayer:self.previewLayer];
❼ 存储捕捉到的文件

获取沙盒路径

NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject] stringByAppendingPathComponent:@"video.h264"];

先移除已存在的文件。这里比较粗暴,不管有没有,先干掉再说

[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];

新建文件

BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
if (!createFile)
{
    NSLog(@"新建文件失败");
}
else
{
    NSLog(@"成功新建文件");
}
NSLog(@"文件存储路径为:%@",filePath);

写入文件到存储路径

fileHandele = [NSFileHandle fileHandleForWritingAtPath:filePath];
❽ 使用 VideoToolBox 进行编码

这里封装成了一个方法。

[self initVideoToolBox];
❾ 开始捕捉视频
[self.capturesession startRunning];

3、使用 VideoToolBox 进行编码方法的实现

- (void)initVideoToolBox
{
    // 编码队列
    dispatch_sync(encodeQueue, ^{
        // 第一帧
        frameID = 0;
        
        // width和height表示分辨率,需要和AVFoundation的分辨率保持一致
        int width = 480, height = 640;
        
        .......
    });
}
❶ 调用VTCompressionSessionCreate创建编码session
  • 参数1:分配器。设置NULL为默认分配
  • 参数2:分辨率的宽
  • 参数3:分辨率的高
  • 参数4:编码类型。如kCMVideoCodecType_H264使用H264
  • 参数5:编码规范。设置NULLvideoToolbox自己选择
  • 参数6:源像素缓冲区属性。设置NULLvideToolbox默认创建
  • 参数7:压缩数据分配器。设置NULL,使用默认的分配
  • 参数8:编码完成之后的回调,使用进行回调的函数名
  • 参数9:self桥接。C语言函数想要调用OC的方法,需要将self传递过去
  • 参数10:编码会话变量
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &encodeingSession);
NSLog(@"调用VTCompressionSessionCreate创建编码session:%d",(int)status);
if (status != 0)// 0表示没有错误
{
    NSLog(@"创建H264编码session失败");
    return ;
}
❷ 配置编码session的参数

OC中设置参数使用的是对象的属性,而在C语言中通过调用函数来实现。VTSessionSetProperty函数用来设置编码的参数。参数一表示参数设置对象。参数二表示属性名称。参数三表示属性对应值。以下代码设置实时编码输出(避免延迟)。

VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);

是否产生B帧。因为B帧在解码时并不是必要的,所以是可以抛弃的。

VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_ProfileLevel,kVTProfileLevel_H264_Baseline_AutoLevel);
VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);

VTSessionSetProperty不能直接设置int/float为属性值,需要通过 CFNumberRef做类型的转换。参数一为分配器,通过使用默认分配器。参数二为需要转化为的数据类型。参数三为值的存储地址。以下代码设置关键帧(GOPsize)间隔,GOP太小的话图像会模糊。

int frameInterval = 10;
CFNumberRef frameIntervalRaf = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRaf);

设置期望帧率,不是实际帧率,指的是帧率上限。

int fps = 10;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

设置码率均值和上限,单位是bps。码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看。

int bitRate = width * height * 3 * 4 * 8;// 码率计算公式
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(encodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
❸ 开始进行编码
VTCompressionSessionPrepareToEncodeFrames(encodeingSession);

4、停止捕捉视频方法的实现

❶ 停止捕捉
[self.capturesession stopRunning];
❷ 移除预览图层
[self.previewLayer removeFromSuperlayer];
❸ 结束videoToolbBox编码
[self endVideoToolBox];

- (void)endVideoToolBox
{
    VTCompressionSessionCompleteFrames(encodeingSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(encodeingSession);
    CFRelease(encodeingSession);
    encodeingSession = NULL;
}
❹ 关闭文件存储
[fileHandele closeFile];
fileHandele = NULL;

5、AVCaptureVideoDataOutputSampleBufferDelegate 捕捉到视频帧进行编码

AVFoundation的代理方法
  • AVCaptureFileOutputRecordingDelegate:用于拍照和录制视频
  • AVCaptureMetadataOutputObjectsDelegate:用于二维码和人脸识别(含有媒体信息)
  • AVCaptureVideoDataOutputSampleBufferDelegate:用于直播(含有视频帧)

AV Foundation 获取到视频/音频流后,无论音频还是视频都会走到AVCaptureVideoDataOutputSampleBufferDelegate代理方法中,所以要对二者进行区分。最简单最直接的区分音频和视频数据的方法就是AVCaptureVideoDataOutput / AVCaptureAudionDataOutput

CMSampleBuffer的数据结构
  • CMTime
  • CMVideoFormatDesc
  • 未编码的视频帧中为 CVPixelBuffer / 编码后的视频帧中为CMBlockBuffer
❶ 获取到摄像头的视频帧

开始视频录制,获取到摄像头的视频帧,传入encode方法中进行编码。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    dispatch_sync(encodeQueue, ^{
        [self encode:sampleBuffer];
    });
}
❷ 对视频帧进行编码

拿到视频流中的每一帧未编码数据,即一张张图片。

- (void)encode:(CMSampleBufferRef)sampleBuffer
{
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    ......
}

设置帧时间,如果不设置会导致时间轴过长。

CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
用于编码的函数
  • 参数1:编码会话变量
  • 参数2:未编码数据
  • 参数3:获取到的这个sample buffer数据的展示时间戳
  • 参数4:对于获取到的sample buffer数据这个帧的展示时间。如果没有时间信息,可设置kCMTimeInvalid
  • 参数5:包含这个帧的属性。常设为NULL
  • 参数6:回调函数,用于获取到H.264编码成功后的NALU数据进行处理
  • 参数7:flags设置同步或者异步
VTEncodeInfoFlags flags;
OSStatus statusCode = VTCompressionSessionEncodeFrame(encodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);

if (statusCode != noErr)
{
    NSLog(@"H.264在VTCompressionSessionEncodeFrame时出错:%d",(int)statusCode);
    
    // 结束编码
    VTCompressionSessionInvalidate(encodeingSession);
    // 安全释放
    CFRelease(encodeingSession);
    encodeingSession = NULL;
    return;
}

NSLog(@"H.264在VTCompressionSessionEncodeFrame时成功");

6、编码完成后的回调函数

❶ 回调函数里对数据的处理流程
  • H264硬编码完成后,回调VTCompressionOutputCallback
  • 将硬编码成功的CMSampleBuffer转换成H264码流,通过网络进行传递
  • 解析出参数集SPS & PPS,加上起始码组装成 NALU,将NALU发送出去
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
{
    NSLog(@"didCompressH264 编码完成后的回调函数被调用 status状态为:%d,infoFlags为:%d",(int)status,(int)infoFlags);

    // 状态错误
    if (status != 0)
    {
        return;
    }
    
    // 没准备好
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
        NSLog(@"didCompressH264函数中的sampleBuffer数据没有准备好");
        return;
    }
    
    ......
}
❷ C语言中的函数调用OC中的方法

outputCallbackRefCon就是之前传递的self,就是控制器ViewController

ViewController *encoder = (__bridge ViewController *)outputCallbackRefCon;
❸ 判断当前帧是否为关键帧
bool keyFrame = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);

也可以写为分步骤判断

CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
bool isKeyFrame = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
❹ 获取sps & pps 数据
  • sps & pps 数据只获取1次,保存在h264文件开头的第一帧中。
  • sps(sample per second 采样次数/s)是衡量模数转换(ADC)时采样速率的单位。

获取图像存储方式,编码器等格式描述。

if (keyFrame)
{
    //获取图像存储方式,编码器等格式描述
    CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
    ......
}

通过下面这个函数获取到spscount / size / content

size_t sparameterSetSize,sparameterSetCount;
const uint8_t *sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);

通过下面这个函数获取到ppscount / size / content。唯一的区别是将0改为1。

//获取pps
size_t pparameterSetSize,pparameterSetCount;
const uint8_t *pparameterSet;

//从第一个关键帧获取pps
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
❺ sps和pps获取成功后将其写入到文件

sps/pps转化为NSData

NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];

sps/pps写入文件。

if (encoder)
{
    [encoder gotSpsPps:sps pps:pps];
}
❻ 第一帧写入 sps & pps
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]);
    
    // 写入之前的起始位
    const char bytes[] = "\x00\x00\x00\x01";
    // 删去'/0'
    size_t length = (sizeof bytes) - 1;
    
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    // 写入sps
    [fileHandele writeData:ByteHeader];
    [fileHandele writeData:sps];
    [fileHandele writeData:ByteHeader];
    [fileHandele writeData:pps];
}
❼ 经过sps & pps即编码后的H264的NALU数据
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);

调用用来获取blockBuffer信息的函数,可以获取到单个数据块的长度,整个数据块的总长度和数据块的首地址

size_t length,totalLength;
char *dataPointer;
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);

创建成功后读取数据

if (statusCodeRet == noErr)
{
    size_t bufferOffset = 0;
    //返回的nalu数据前4个字节不是001的startcode,而是大端模式的帧长度length
    static const int AVCCHeaderLength = 4;
    
    //循环获取nalu数据
    while (bufferOffset < totalLength - AVCCHeaderLength)
    {
        uint32_t NALUnitLength = 0;
        
        //读取 一单元长度的 nalu
        memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
        
        //从大端模式转换为系统端模式
        NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
        
        //获取nalu数据
        NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
        
        //将nalu数据写入到文件
        [encoder gotEncodedData:data isKeyFrame:keyFrame];
        
        //读取下一个nalu 一次回调可能包含多个nalu数据
        bufferOffset += AVCCHeaderLength + NALUnitLength;
    }
}
❽ 将nalu数据写入到文件
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"将nalu数据写入到文件,数据长度为:%d",(int)[data length]);
    
    if (fileHandele != NULL)
    {
        // 创建起始位
        const char bytes[] ="\x00\x00\x00\x01";
        
        // 计算长度
        size_t length = (sizeof bytes) - 1;
        
        // 将头字节bytes转化为NSData
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        
        // 写入头字节。注意在写入NSLU数据之前,先写入起始位
        [fileHandele writeData:ByteHeader];
        
        // 写入H264数据到沙盒文件中
        [fileHandele writeData:data];
    }
}

三、H264硬编码解码工具类封装实现

运行效果

1、辅助工具类

a、捕获音视频的工具类
❶ 接口提供的方法

捕获类型

typedef NS_ENUM(int,SystemCaptureType)
{
    SystemCaptureTypeVideo = 0,
    SystemCaptureTypeAudio,
    SystemCaptureTypeAll
};

捕捉到音视频数据后的委托回调

@protocol SystemCaptureDelegate 

@optional
- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (SystemCaptureType)type;

@end

@property (nonatomic, weak) id delegate;

预览层

/** 预览层 */
@property (nonatomic, strong) UIView *preview;
/** 捕获视频的宽 */
@property (nonatomic, assign, readonly) NSUInteger witdh;
/** 捕获视频的高 */
@property (nonatomic, assign, readonly) NSUInteger height;

初始化音视频捕捉器

- (instancetype)initWithType:(SystemCaptureType)type;

准备工作

/** 准备工作(只捕获音频时调用) */
- (void)prepare;
/** 捕获内容包括视频时调用(预览层大小,添加到view上用来显示) */
- (void)prepareWithPreviewSize:(CGSize)size;

控制捕捉流程

/** 开始捕捉 */
- (void)start;
/** 结束捕捉 */
- (void)stop;
/** 切换摄像头 */
- (void)changeCamera;

授权检测

+ (int)checkMicrophoneAuthor;
+ (int)checkCameraAuthor;
❷ 工具类的使用方式
// 创建捕获会话
- (void)createCaptureSession
{
    // 检查摄像头授权状态
    [SystemCapture checkCameraAuthor];
    
    // 捕获视频
    _capture = [[SystemCapture alloc] initWithType:SystemCaptureTypeVideo];
    
    // 传入预览层大小来创建预览层
    CGSize size = CGSizeMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
    [_capture prepareWithPreviewSize:size];
    _capture.preview.frame = CGRectMake(0, 100, size.width, size.height);
    [self.view addSubview:_capture.preview];
    
    // 捕捉到音视频数据后的委托回调
    self.capture.delegate = self;
}
❸ 实现捕获音视频的回调方法

将捕获到的数据进行H264硬编码。

- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (SystemCaptureType)type
{
    if (type == SystemCaptureTypeAudio)
    {
        NSLog(@"录音");
    }
    else
    {
        // 将捕获到的数据进行H264硬编码
        [_videoEncoder encodeVideoSampleBuffer:sampleBuffer];
    }
}

b、音视频配置工具类
音频配置
@interface AudioConfig : NSObject

/** 码率 默认96000 */
@property (nonatomic, assign) NSInteger bitrate;
/** 声道 默认单声道 */
@property (nonatomic, assign) NSInteger channelCount;
/** 采样率 默认44100 */
@property (nonatomic, assign) NSInteger sampleRate;
/** 采样点量化 默认16 */
@property (nonatomic, assign) NSInteger sampleSize;

/** 使用默认音频配置 */
+ (instancetype)defaultAudioConfig;

@end
视频配置
@interface VideoConfig : NSObject

/** 可选,分辨率的宽 */
@property (nonatomic, assign) NSInteger width;//系统支持的分辨率,采集
/** 可选,分辨率的高 */
@property (nonatomic, assign) NSInteger height;
/** 码率 */
@property (nonatomic, assign) NSInteger bitrate;
/** 帧数 */
@property (nonatomic, assign) NSInteger fps;

/** 使用默认视频配置 */
+ (instancetype)defaultVideoConfig;

@end

c、展示解码后的视频工具类
  • 提示:'CAEAGLLayer' is deprecated: first deprecated in iOS 12.0 - OpenGLES is deprecated
  • CAEAGLLayer 是专门用来渲染OpenGL ES的图层,继承自CALayer
  • OpenGL ES只负责渲染,不关心图层,所以支持跨平台
@interface AAPLEAGLLayer : CAEAGLLayer

// 用于渲染的解码后的数据
@property CVPixelBufferRef pixelBuffer;

// 展示的图层尺寸
- (id)initWithFrame:(CGRect)frame;

// 重新渲染
- (void)resetRenderBuffer;

@end

2、H264硬编码工具类

a、提供的接口方法

h264编码回调代理

@protocol VideoEncoderDelegate 

/** Video-H264数据编码完成回调 */
- (void)videoEncodeCallback:(NSData *)h264Data;
/** Video-SPS&PPS数据编码回调 */
- (void)videoEncodeCallbacksps:(NSData *)sps pps:(NSData *)pps;

@end

@property (nonatomic, weak) id delegate;

初始化配置

- (instancetype)initWithConfig:(VideoConfig *)config;

进行编码。注意编码和回调均在异步队列执行

-(void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
b、导入的框架和私密属性

导入的框架

#import 
#import 

扩展里面的私密属性

@interface VideoEncoder ()

// 编码队列
@property (nonatomic, strong) dispatch_queue_t encodeQueue;
// 回调队列
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
// 编码会话
@property (nonatomic) VTCompressionSessionRef encodeSesion;

@end

成员变量

@implementation VideoEncoder
{
    long frameID;// 帧的递增序标识
    BOOL hasSpsPps;// 判断是否已经获取到pps和sps
}
c、在初始化方法中配置编码参数

初始化编码和回调队列

- (instancetype)initWithConfig:(VideoConfig *)config
{
    self = [super init];
    if (self)
    {
        // 初始化编码和回调队列
        _config = config;
        _encodeQueue = dispatch_queue_create("h264 hard encode queue", DISPATCH_QUEUE_SERIAL);
        _callbackQueue = dispatch_queue_create("h264 hard encode callback queue", DISPATCH_QUEUE_SERIAL);
        .......
    }
    return self;
}

创建编码会话

OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, (int32_t)_config.width, (int32_t)_config.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoEncodeCallback, (__bridge void * _Nullable)(self), &_encodeSesion);
if (status != noErr)
{
    NSLog(@"VTCompressionSession create failed. status = %d", (int)status);
    return self;
}

接下来设置编码器属性。首先设置是否实时执行。

status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
NSLog(@"VTSessionSetProperty: set RealTime return: %d", (int)status);

指定编码比特流的配置文件和级别。直播一般使用baseline,因为只需要i帧和p帧即可解码成功,而b帧需要前后参考,会由于需要下一帧才能解码导致延时,这里抛弃b帧可减少由于b帧带来的延时。

status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
NSLog(@"VTSessionSetProperty: set profile return: %d", (int)status);

设置码率均值

  • 比特率可以高于此码率均值
  • 默认比特率为零,表示视频编码器
  • 应该确定压缩数据的大小
  • 比特率设置只在定时的时候有效
CFNumberRef bit = (__bridge CFNumberRef)@(_config.bitrate);
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AverageBitRate, bit);
NSLog(@"VTSessionSetProperty: set AverageBitRate return: %d", (int)status);

对码率进行限制(只在定时时起作用)

CFArrayRef limits = (__bridge CFArrayRef)@[@(_config.bitrate / 4), @(_config.bitrate * 4)];
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits,limits);
NSLog(@"VTSessionSetProperty: set DataRateLimits return: %d", (int)status);

设置关键帧间隔(GOPSize)。GOP太大图像会模糊。

CFNumberRef maxKeyFrameInterval = (__bridge CFNumberRef)@(_config.fps * 2);
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, maxKeyFrameInterval);
NSLog(@"VTSessionSetProperty: set MaxKeyFrameInterval return: %d", (int)status);

设置fps

CFNumberRef expectedFrameRate = (__bridge CFNumberRef)@(_config.fps);
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, expectedFrameRate);
NSLog(@"VTSessionSetProperty: set ExpectedFrameRate return: %d", (int)status);

准备进行编码

status = VTCompressionSessionPrepareToEncodeFrames(_encodeSesion);
NSLog(@"VTSessionSetProperty: set PrepareToEncodeFrames return: %d", (int)status);

以上属性设置在控制台的输出结果为

2020-12-29 11:45:11.826297+0800 001-Demo[33733:810078] VTSessionSetProperty: set RealTime return: 0
2020-12-29 11:45:11.826399+0800 001-Demo[33733:810078] VTSessionSetProperty: set profile return: 0
2020-12-29 11:45:11.826459+0800 001-Demo[33733:810078] VTSessionSetProperty: set AverageBitRate return: 0
2020-12-29 11:45:11.826510+0800 001-Demo[33733:810078] VTSessionSetProperty: set DataRateLimits return: 0
2020-12-29 11:45:11.826586+0800 001-Demo[33733:810078] VTSessionSetProperty: set MaxKeyFrameInterval return: 0
2020-12-29 11:45:11.826659+0800 001-Demo[33733:810078] VTSessionSetProperty: set ExpectedFrameRate return: 0
2020-12-29 11:45:11.826738+0800 001-Demo[33733:810078] VTSessionSetProperty: set PrepareToEncodeFrames return: 0

d、获取到sampleBuffer数据进行H264硬编码
- (void)encodeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    CFRetain(sampleBuffer);
    dispatch_async(_encodeQueue, ^{
        // 帧数据
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        // 该帧的时间戳
        frameID++;
        CMTime timeStamp = CMTimeMake(frameID, 1000);
        // 持续时间
        CMTime duration = kCMTimeInvalid;
        // 编码
        VTEncodeInfoFlags flags;
        OSStatus status = VTCompressionSessionEncodeFrame(self.encodeSesion, imageBuffer, timeStamp, duration, NULL, NULL, &flags);
        if (status != noErr)
        {
            NSLog(@"VTCompression: encode failed: status=%d",(int)status);
        }
        CFRelease(sampleBuffer);
    });
}

e、 编码成功回调

获取视频编码器。

void VideoEncodeCallback(void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon,OSStatus status, VTEncodeInfoFlags infoFlags,  CMSampleBufferRef sampleBuffer )
{
    
    if (status != noErr)
    {
        NSLog(@"VideoEncodeCallback: encode error, status = %d", (int)status);
        return;
    }
    
    if (!CMSampleBufferDataIsReady(sampleBuffer))
    {
        NSLog(@"VideoEncodeCallback: data is not ready");
        return;
    }

    VideoEncoder *encoder = (__bridge VideoEncoder *)(outputCallbackRefCon);
    ......
    }
}

判断是否为关键帧。

BOOL keyFrame = NO;
CFArrayRef attachArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
keyFrame = !CFDictionaryContainsKey(CFArrayGetValueAtIndex(attachArray, 0), kCMSampleAttachmentKey_NotSync); 

获取sps & pps 数据 ,只需获取一次,保存在h264文件开头即可。

// startCode 长度 4
const Byte startCode[] = "\x00\x00\x00\x01";

if (keyFrame && !encoder->hasSpsPps)
{
    size_t spsSize, spsCount;
    size_t ppsSize, ppsCount;
    const uint8_t *spsData, *ppsData;
    
    // 获取图像源格式
    CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
    OSStatus status1 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 0, &spsData, &spsSize, &spsCount, 0);
    OSStatus status2 = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(formatDesc, 1, &ppsData, &ppsSize, &ppsCount, 0);
    
    // 判断sps/pps是否获取成功
    if (status1 == noErr & status2 == noErr)
    {
        NSLog(@"VideoEncodeCallback: get sps, pps success");
        
        encoder->hasSpsPps = true;
        //sps data
        NSMutableData *sps = [NSMutableData dataWithCapacity:4 + spsSize];
        [sps appendBytes:startCode length:4];
        [sps appendBytes:spsData length:spsSize];
        
        //pps data
        NSMutableData *pps = [NSMutableData dataWithCapacity:4 + ppsSize];
        [pps appendBytes:startCode length:4];
        [pps appendBytes:ppsData length:ppsSize];
        
        dispatch_async(encoder.callbackQueue, ^{
            // 通过回调方法传递sps/pps
            [encoder.delegate videoEncodeCallbacksps:sps pps:pps];
        });
    }
    else
    {
        NSLog(@"VideoEncodeCallback: get sps/pps failed spsStatus=%d, ppsStatus=%d", (int)status1, (int)status2);
    }
}

用来获取NALU数据。

size_t lengthAtOffset, totalLength;
char *dataPoint;

将数据复制到dataPoint

CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus error = CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &totalLength, &dataPoint);
if (error != kCMBlockBufferNoErr)
{
    NSLog(@"VideoEncodeCallback: get datapoint failed, status = %d", (int)error);
    return;
}

循环获取nalu数据。

size_t offet = 0;
// 返回的nalu数据前四个字节不是0001的startcode(不是系统端的0001),而是大端模式的帧长度length
const int lengthInfoSize = 4;

// 循环获取nalu数据
while (offet < totalLength - lengthInfoSize)
{
    uint32_t naluLength = 0;
    // 获取nalu数据长度
    memcpy(&naluLength, dataPoint + offet, lengthInfoSize);
    // 大端转系统端
    naluLength = CFSwapInt32BigToHost(naluLength);
    
    //获取到编码好的视频数据
    NSMutableData *data = [NSMutableData dataWithCapacity:4 + naluLength];
    [data appendBytes:startCode length:4];
    [data appendBytes:dataPoint + offet + lengthInfoSize length:naluLength];
    
    //将NALU数据回调到代理中
    dispatch_async(encoder.callbackQueue, ^{
        [encoder.delegate videoEncodeCallback:data];
    });
    
    //移动下标,继续读取下一个数据
    offet += lengthInfoSize + naluLength;
}

f、销毁编码器
- (void)dealloc
{
    if (_encodeSesion)
    {
        VTCompressionSessionCompleteFrames(_encodeSesion, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_encodeSesion);
        
        CFRelease(_encodeSesion);
        _encodeSesion = NULL;
    }
}
g、实现H264硬编码的回调方法

针对SPS/PPS,不能直接一起解码,需要分开传输。

- (void)videoEncodeCallbacksps:(NSData *)sps pps:(NSData *)pps
{
    // 可以将获取到的sps/pps数据直接写入文件
    //[_handle seekToEndOfFile];
    //[_handle writeData:sps];
    //[_handle seekToEndOfFile];
    //[_handle writeData:pps];
    
    // 也可以选择对sps/pps进行解码
    [_videoDecoder decodeNaluData:sps];
    [_videoDecoder decodeNaluData:pps];
}

对数据进行解码。

- (void)videoEncodeCallback:(NSData *)h264Data
{
    // 可以将获取到的NSAL数据直接写入文件
    //[_handle seekToEndOfFile];
    //[_handle writeData:h264Data];
    
    // 也可以选择对数据进行解码
    [_videoDecoder decodeNaluData:h264Data];
}

3、H264硬解码工具类

a、解码原理分析
解码的思路
  1. 解析数据(NALU Unit),判断其为I/P/B中的哪一类。
  2. 初始化解码器
  3. 将解析后的H264 NALU Unit输入解码器
  4. 在解码完成回调中输出解码数据
  5. 通过OpenGL ES(IOS12后已废弃)显示解码数据
解码的三个核心函数
  • VTDecompressionSessionCreate:创建Session
  • VTDecompressionSessionDecodeFrame:解码一个frame
  • VTDecompressionSessionInvalidate:销毁解码Session
H264原始码流转为NALU
  • l帧:保留了一张完整视频帧,这是解码关键!
  • P帧:向前参考帧,保存差异数据,解码时需要依赖于I帧
  • B帧:双向参考帧,解码时既需要依赖I帧,也需要依赖P帧
  • 如果H264码流中I帧错误/丢失,就会导致错误传递。P/B帧单独完成不了解码工作,导致花屏的现象产生
  • H264硬编码I帧,需要手动加入SPS/PPS,因为在解码时需要使用SPS/PPS数据来对解码器进行初始化
解析数据
  • 因为NALU一个接一个传入,所以需要我们进行实时解码
  • 首先要对数据进行解析,因为NALU数据前面4个字节是标识一个NALU的开始的起始位,我们并不需要获取,所以从第5位才开始获取,其后才是NALU数据类型
  • 获取到第5位数据,将其转化十进制,然后根据表格判断它的数据类型,判断好数据类型,才能将NALU送入解码器
  • CVPixelBufferRef保存的是解码后的数据
四个动作
  • 采集视频源数据的时候是CVPixelBuffer
  • 编码(硬编码VideoToolBox)的时候是CMBlockBuffer表示的NALU数据
  • 需要将frame转化为CMBlockBufferRef,再将CMBlockBufferRef转化为CMSampleBufferRef,因为解码函数所需要数据类型CMSampleBufferRef,解码后的数据类型为CVPixelBuffer
  • 解码(硬解码VideoToolBox)的时候是CVPixelBuffer表示的NALU数据
  • 通过OpenGL ES将数据显示到屏幕上

b、提供的接口方法

H264解码回调代理

@protocol VideoDecoderDelegate 

/** 解码后H264数据回调 */
- (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer;

@end

@property (nonatomic, weak) id delegate;

初始化解码器

- (instancetype)initWithConfig:(VideoConfig*)config;

解码H264数据

- (void)decodeNaluData:(NSData *)frame;
c、导入的框架和私密属性

导入的框架

#import 
#import 

扩展里面的私密属性

@interface VideoDecoder ()

// 解码队列
@property (nonatomic, strong) dispatch_queue_t decodeQueue;
// 回调队列
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
// 解码会话
@property (nonatomic) VTDecompressionSessionRef decodeSesion;

@end

成员变量

{
    // SPS
    uint8_t *_sps;
    NSUInteger _spsSize;
    
    // PPS
    uint8_t *_pps;
    NSUInteger _ppsSize;
    
    // 解密格式信息
    CMVideoFormatDescriptionRef _decodeDesc;
}

d、实现解码功能的方法

公有的解码Nalu数据方法。首先要对数据进行解析,因为NALU数据前面4个字节是标识一个NALU的开始的起始位,我们并不需要获取,所以需要将其转化为二进制数据,从第5位才开始获取,其后才是NALU数据类型。

- (void)decodeNaluData:(NSData *)frame
{
    // 将解码放在异步队列
    dispatch_async(_decodeQueue, ^{
        // 获取frame,将NSData转化为二进制数据
        uint8_t *nalu = (uint8_t *)frame.bytes;
        // 调用私有的解码Nalu数据方法,参数1:数据 参数2:数据长度
        [self decodeNaluData:nalu size:(uint32_t)frame.length];
    });
}

私有的解码Nalu数据方法。参数1:数据 参数2:数据长度。

- (void)decodeNaluData:(uint8_t *)frame size:(uint32_t)size
{
    ......
}

frame的前4个字节是NALU数据的开始码,也就是00 00 00 01,第5个字节表示数据类型,转为10进制后根据表格判断它的数据类型,7是sps,8是pps,5是IDR(I帧)信息。

int type = (frame[4] & 0x1F);

将NALU的开始码转为4字节大端NALU的长度信息。

uint32_t naluSize = size - 4;
uint8_t *pNaluSize = (uint8_t *)(&naluSize);
CVPixelBufferRef pixelBuffer = NULL;
frame[0] = *(pNaluSize + 3);
frame[1] = *(pNaluSize + 2);
frame[2] = *(pNaluSize + 1);
frame[3] = *(pNaluSize);

解析时第一次获取到关键帧的时候初始化解码器initDecoder。关键帧/其他帧数据通过调用[self decode:frame withSize:size]方法进行解码。sps/pps数据则简单赋值到_sps/_pps中进行保存,但是没有对其进行解码操作。

switch (type)
{
    case 0x05:// 关键帧
        if ([self initDecoder])
        {
            pixelBuffer= [self decode:frame withSize:size];
        }
        break;
    case 0x06:
        NSLog(@"SEI");// 增强信息
        break;
    case 0x07:// sps
        _spsSize = naluSize;
        _sps = malloc(_spsSize);
        memcpy(_sps, &frame[4], _spsSize);
        break;
    case 0x08:// pps
        _ppsSize = naluSize;
        _pps = malloc(_ppsSize);
        memcpy(_pps, &frame[4], _ppsSize);// 从第5位开始赋值数据
        break;
    default:// 其他帧(1-5)
        if ([self initDecoder])// 初始化解码器
        {
            pixelBuffer = [self decode:frame withSize:size];// 进行解码
        }
        break;
}

e、初始化解码器

初始化配置信息、解码队列、回调队列。

- (instancetype)initWithConfig:(VideoConfig *)config
{
    self = [super init];
    if (self)
    {
        // 初始化VideoConfig 信息
        _config = config;
        
        // 创建解码队列与回调队列
        _decodeQueue = dispatch_queue_create("h264 hard decode queue", DISPATCH_QUEUE_SERIAL);
        _callbackQueue = dispatch_queue_create("h264 hard decode callback queue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

解码器用来创建decodeSesion,通过判断它是否存在,可以保证只创建一次解码器,因为要防止每帧解码都会创建解码器。

- (BOOL)initDecoder
{
    if (_decodeSesion) return true;
    ......
}
根据sps pps设置解码参数
  • kCFAllocatorDefault:分配器
  • 2:参数个数
  • parameterSetPointers:参数集指针
  • parameterSetSizes:参数集大小
  • naluHeaderLen:nalu start code 的长度 4
  • _decodeDesc:解码器描述,即视频输出格式
  • return:状态
// 包含了sps/pps,用来保存参数集
const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
// 用来用来保存参数集的尺寸
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
// Nalu Header的长度
int naluHeaderLen = 4;

OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_decodeDesc);
if (status != noErr)
{
    NSLog(@"Video hard DecodeSession create H264ParameterSets(sps, pps) failed status= %d", (int)status);
    return false;
}
解码参数
  • kCVPixelBufferPixelFormatTypeKey:摄像头的输出数据格式
  • kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:摄像头的输出格式,YUV420一般用于标清视频
  • kCVPixelBufferWidthKey/kCVPixelBufferHeightKey:捕获的视频的分辨率的宽高
  • kCVPixelBufferOpenGLCompatibilityKey:它允许在 OpenGL 的上下文中直接绘制解码后的图像,而不是从总线和 CPU 之间复制数据,被称为零拷贝通道,因为在绘制过程中没有解码的图像被拷贝
NSDictionary *destinationPixBufferAttrs =
@{
  (id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
  (id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:_config.width],
  (id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:_config.height],
  (id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
};
解码回调设置

VTDecompressionOutputCallbackRecord是一个简单的结构体,它带有一个指针 (decompressionOutputCallback),指向帧解压完成后的回调方法。你需要提供可以找到这个回调方法的实例 (decompressionOutputRefCon)。VTDecompressionOutputCallback回调方法包括以下参数:

  • 参数1:回调的引用
  • 参数2:帧的引用
  • 参数3:一个状态标识 (包含未定义的代码)
  • 参数4:指示同步/异步解码,或者解码器是否打算丢帧的标识
  • 参数5:实际图像的缓冲
  • 参数6:出现的时间戳
  • 参数7:出现的持续时间
VTDecompressionOutputCallbackRecord callbackRecord;
callbackRecord.decompressionOutputCallback = videoDecompressionOutputCallback;
callbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
创建用于解压缩视频帧的会话
  • allocator:通常使用默认的kCFAllocatorDefault内存分配器
  • videoFormatDescription:描述源视频帧的视频格式
  • videoDecoderSpecification:是否需要使用特定视频解码器,通常设置为NULL
  • destinationImageBufferAttributes:描述源像素缓冲区的要求,通常设置为NULL
  • outputCallback:已解码完成的帧调用的回调
  • decompressionSessionOut:指向一个变量以接收新的解压会话,解压后的帧将通过调用OutputCallback发出
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationPixBufferAttrs), &callbackRecord, &_decodeSesion);

if (status != noErr)
{
    NSLog(@"Video hard DecodeSession create failed status= %d", (int)status);
    return false;
}

设置解码会话属性为实时解码。

status = VTSessionSetProperty(_decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
NSLog(@"Vidoe hard decodeSession set property RealTime status = %d", (int)status);

f、实现解码函数
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize
{    
    CVPixelBufferRef outputPixelBuffer = NULL;
    CMBlockBufferRef blockBuffer = NULL;
    CMBlockBufferFlags flag0 = 0;
    .......
}
创建blockBuffer
  • kCFAllocatorDefault:默认内存分配
  • frame:内容
  • frameSize:内容大小
  • kCFAllocatorNull:通常NULL
  • customBlockSource:通常NULL
  • offsetToData: 数据偏移
  • dataLength:数据长度
  • flags: 功能和控制标志
  • newBBufOut:blockBuffer地址,不能为空
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);

if (status != kCMBlockBufferNoErr)
{
    NSLog(@"Video hard decode create blockBuffer error code=%d", (int)status);
    return outputPixelBuffer;
}
创建sampleBuffer
  • allocator:使用默认内存分配
  • blockBuffer:需要解码的数据
  • formatDescription:视频输出格式
  • numSamples:CMSampleBuffer的个数
  • sampleTimingArray:空数组
  • numSampleSizeEntries:默认为1
  • sampleBuffer:对象
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};

status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);

if (status != noErr || !sampleBuffer)
{
    NSLog(@"Video hard decode create sampleBuffer failed status=%d", (int)status);
    CFRelease(blockBuffer);
    return outputPixelBuffer;
}
进行解码
  • decodeSesion:解码的session
  • sampleBuffer:源数据,包含一个或多个视频帧的CMsampleBuffer
  • flag1:解码标志
  • outputPixelBuffer:解码后数据
  • infoFlag:同步/异步解码标识
// 向视频解码器提示使用低功耗模式是可以的
VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback;
// 使用异步解码
VTDecodeInfoFlags  infoFlag = kVTDecodeInfo_Asynchronous;

status = VTDecompressionSessionDecodeFrame(_decodeSesion, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag);

if (status == kVTInvalidSessionErr)
{
    NSLog(@"Video hard decode  InvalidSessionErr status =%d", (int)status);
}
else if (status == kVTVideoDecoderBadDataErr)
{
    NSLog(@"Video hard decode  BadData status =%d", (int)status);
}
else if (status != noErr)
{
    NSLog(@"Video hard decode failed status =%d", (int)status);
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);

return outputPixelBuffer;

g、解码完成后的回调函数

解码失败后到回调函数

void videoDecompressionOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
                                      void * CM_NULLABLE sourceFrameRefCon,
                                      OSStatus status,
                                      VTDecodeInfoFlags infoFlags,
                                      CM_NULLABLE CVImageBufferRef imageBuffer,
                                      CMTime presentationTimeStamp,
                                      CMTime presentationDuration )
{
    // 解码失败后到回调函数
    if (status != noErr)
    {
        NSLog(@"Video hard decode callback error status=%d", (int)status);
        return;
    }
    ......
}

将解码后的数据 sourceFrameRefCon -> CVPixelBufferRef

CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(imageBuffer);

获取self

VideoDecoder *decoder = (__bridge VideoDecoder *)(decompressionOutputRefCon);

调用回调队列将解码后的数据交给decoder代理

dispatch_async(decoder.callbackQueue, ^{
    // 将解码后的数据交给decoder代理
    [decoder.delegate videoDecodeCallback:imageBuffer];
    
    // 释放数据
    CVPixelBufferRelease(imageBuffer);
});

h、实现H264解码的回调方法

0penGL ES默认的颜色体系为RGB,所以需要将YUV->RGB。图片数据只有Y数据时显示黑白,存在UV信息图片才会变成彩色,即视频由2个图层构成——Y图层纹理+UV图层纹理。所谓视频渲染——>纹理的渲染——>片元着色器填充——>width * height正方形(渲染2个纹理,即亮度和色度纹理)。

- (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer
{
    // 通过OpenGL ES渲染,将解码后的数据显示在屏幕上
    if (imageBuffer)
    {
        _displayLayer.pixelBuffer = imageBuffer;
    }
}

四、音频AAC硬编码解码工具类封装实现

运行效果
2020-12-30 17:52:19.440469+0800 VideoDecode[6477:1110601] 获取到了pcmData
2020-12-30 17:52:19.463654+0800 VideoDecode[6477:1110601] 获取到了pcmData
2020-12-30 17:52:19.486823+0800 VideoDecode[6477:1110427] 获取到了pcmData
........

1、音频AAC硬编码器

a、提供的接口方法

AAC编码器代理

@protocol AudioEncoderDelegate 

- (void)audioEncodeCallback:(NSData *)aacData;

@end

@property (nonatomic, weak) id delegate;

编码器配置

/** 编码器配置 */
@property (nonatomic, strong) AudioConfig *config;
 
/** 初始化时传入编码器配置 */
- (instancetype)initWithConfig:(AudioConfig *)config;

进行编码。编码和回调均在异步队列执行。

- (void)encodeAudioSamepleBuffer: (CMSampleBufferRef)sampleBuffer;

b、导入的框架和私密属性

导入的框架

#import 
#import 

扩展里面的私有属性

@interface AudioEncoder()

// 编码队列
@property (nonatomic, strong) dispatch_queue_t encoderQueue;
// 回调队列
@property (nonatomic, strong) dispatch_queue_t callbackQueue;

// 音频转换器对象
@property (nonatomic, unsafe_unretained) AudioConverterRef audioConverter;
// PCM缓存区
@property (nonatomic) char *pcmBuffer;
// PCM缓存区大小
@property (nonatomic) size_t pcmBufferSize;

@end

c、实现捕获音频的回调方法

可以选择直接播放PCM数据或者对其进行AAC编码。注意PCM数据实时获取和实时编码。

- (void)captureSampleBuffer:(CMSampleBufferRef)sampleBuffer type: (SystemCaptureType)type
{
    if (type == SystemCaptureTypeAudio)
    {
        // 使用方式一:直接播放PCM数据
        // NSData *pcmData = [_audioEncoder convertAudioSamepleBufferToPcmData:sampleBuffer];
        // [_pcmPalyer palyePCMData:pcmData];
        
        // 使用方式二:AAC编码
        [_audioEncoder encodeAudioSamepleBuffer:sampleBuffer];
    }
}

可以直接播放PCM数据,只需要将sampleBuffer数据提取出的PCM数据返回给ViewController即可

- (NSData *)convertAudioSamepleBufferToPcmData: (CMSampleBufferRef)sampleBuffer
{
    // 获取pcm数据大小
    size_t size = CMSampleBufferGetTotalSampleSize(sampleBuffer);
    
    // 分配空间
    int8_t *audio_data = (int8_t *)malloc(size);
    memset(audio_data, 0, size);// 所有数据初始化为0
    
    // 获取CMBlockBuffer, 这里面保存了PCM数据
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    
    // 将数据copy到我们分配的空间audio_data中
    CMBlockBufferCopyDataBytes(blockBuffer, 0, size, audio_data);
    // 将audio_data转化为NSData
    NSData *data = [NSData dataWithBytes:audio_data length:size];
    free(audio_data);
    
    return data;
}

d、初始化时传入编码器配置
- (instancetype)initWithConfig:(AudioConfig *)config
{
    self = [super init];
    if (self)
    {
        // 音频编码队列
        _encoderQueue = dispatch_queue_create("aac hard encoder queue", DISPATCH_QUEUE_SERIAL);
        // 音频回调队列
        _callbackQueue = dispatch_queue_create("aac hard encoder callback queue", DISPATCH_QUEUE_SERIAL);
        // 音频转换器
        _audioConverter = NULL;
        _pcmBufferSize = 0;
        _pcmBuffer = NULL;
        _config = config;
        if (config == nil)
        {
            _config = [[AudioConfig alloc] init];
        }
        
    }
    return self;
}

e、当AVFoundation捕获到音频内容之后进行音频编码

判断音频转换器是否创建成功。如果未创建成功,则配置音频编码参数且创建转码器。

- (void)encodeAudioSamepleBuffer: (CMSampleBufferRef)sampleBuffer
{
    CFRetain(sampleBuffer);
    
    if (!_audioConverter)
    {
        [self setupEncoderWithSampleBuffer:sampleBuffer];
    }
    ......
}

来到音频编码异步队列后,获取保存了PCM数据的CMBlockBuffer

dispatch_async(_encoderQueue, ^{
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    CFRetain(blockBuffer);
    ......
});

获取BlockBuffer中音频数据大小以及音频数据地址。

OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
NSError *error = nil;
if (status != kCMBlockBufferNoErr)
{
    error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
    NSLog(@"Error: ACC encode get data point error: %@",error);
    return;
}

开辟_pcmBuffsize大小的pcm内存空间,将_pcmBufferSize数据setpcmBuffer中。

uint8_t *pcmBuffer = malloc(_pcmBufferSize);
memset(pcmBuffer, 0, _pcmBufferSize);

pcmBuffer数据填充到outAudioBufferList对象中。

AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)_config.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)_pcmBufferSize;
outAudioBufferList.mBuffers[0].mData = pcmBuffer;

// AudioBufferList的结构
typedef struct AudioBufferList {
    UInt32 mNumberBuffers;
    AudioBuffer mBuffers[1];
} AudioBufferList;

// AudioBuffer的结构
struct AudioBuffer
{
    UInt32              mNumberChannels;
    UInt32              mDataByteSize;
    void* __nullable    mData;
};
typedef struct AudioBuffer  AudioBuffer;
配置填充函数,获取输出数据
  • inAudioConverter:音频转换器
  • inInputDataProc:提供要编码的音频数据的回调函数
  • ioOutputDataPacketSize:输出缓冲区的大小
  • outOutputData:需要编码的音频数据
  • outPacketDescription:输出包信息
// 输出包大小为1
UInt32 outputDataPacketSize = 1;
status = AudioConverterFillComplexBuffer(_audioConverter, aacEncodeInputDataProc, (__bridge void * _Nullable)(self), &outputDataPacketSize, &outAudioBufferList, NULL);

如果要获取音频裸流则不需要写入ADTS头,直接解码即可。如果想要写入文件,则必须添加ADTS头再写入文件。添加ADTS头的方式如下

// ADTS头只获取一次
@property (nonatomic, assign) BOOL isHeader;

NSMutableData *fullData = [[NSMutableData alloc] init];
if(_isHeader == NO)
{
    _isHeader = YES;
    NSData *adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
    [fullData appendData:adtsHeader];
}
[fullData appendData:rawAAC];

这里使用直接将裸流数据传递到回调队列中的方式,不添加ADTS头。

if (status == noErr)
{
    // 获取数据
    NSData *rawAAC = [NSData dataWithBytes: outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
    
    // 释放pcmBuffer
    free(pcmBuffer);

    // 将裸流数据传递到回调队列中
    dispatch_async(_callbackQueue, ^{
        [_delegate audioEncodeCallback:rawAAC];
    });
}
else
{
    error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}

CFRelease(blockBuffer);
CFRelease(sampleBuffer);
if (error)
{
    NSLog(@"error: AAC编码失败 %@",error);
}

f、配置音频编码参数

获取输入参数

AudioStreamBasicDescription inputAduioDes = *CMAudioFormatDescriptionGetStreamBasicDescription( CMSampleBufferGetFormatDescription(sampleBuffer));

设置输出参数

AudioStreamBasicDescription outputAudioDes = {0};
outputAudioDes.mSampleRate = (Float64)_config.sampleRate;       // 采样率
outputAudioDes.mFormatID = kAudioFormatMPEG4AAC;                // 输出格式
outputAudioDes.mFormatFlags = kMPEG4Object_AAC_LC;              // 如果设为0 代表无损编码
outputAudioDes.mBytesPerPacket = 0;                             // 自己确定每个packet大小
outputAudioDes.mFramesPerPacket = 1024;                         // 每一个packet帧数 AAC-1024
outputAudioDes.mBytesPerFrame = 0;                              // 每一帧大小
outputAudioDes.mChannelsPerFrame = (uint32_t)_config.channelCount; // 输出声道数
outputAudioDes.mBitsPerChannel = 0;                             // 数据帧中每个通道的采样位数
outputAudioDes.mReserved =  0;                                  // 对齐方式 0(8字节对齐)

填充输出相关信息

UInt32 outDesSize = sizeof(outputAudioDes);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &outDesSize, &outputAudioDes);

获取编码器的描述信息

AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:outputAudioDes.mFormatID fromManufacture:kAppleSoftwareAudioCodecManufacturer];
创建converter
  • inputAduioDes:输入音频格式描述
  • outputAudioDes:输出音频格式描述
  • _audioConverter:创建的解码器
OSStatus status = AudioConverterNewSpecific(&inputAduioDes, &outputAudioDes, 1, audioClassDesc, &_audioConverter);
if (status != noErr)
{
    NSLog(@"Error!:硬编码AAC创建失败, status= %d", (int)status);
    return;
}

设置编解码质量

UInt32 temp = kAudioConverterQuality_High;
// 编解码器的呈现质量
AudioConverterSetProperty(_audioConverter, kAudioConverterCodecQuality, sizeof(temp), &temp);

设置比特率

uint32_t audioBitrate = (uint32_t)self.config.bitrate;
uint32_t audioBitrateSize = sizeof(audioBitrate);
status = AudioConverterSetProperty(_audioConverter, kAudioConverterEncodeBitRate, audioBitrateSize, &audioBitrate);
if (status != noErr)
{
    NSLog(@"Error!:硬编码AAC 设置比特率失败");
}

g、在编码器回调函数中不断填充PCM数据

获取self

AudioEncoder *aacEncoder = (__bridge AudioEncoder *)(inUserData);

判断pcmBuffsize大小,为空则表示不需要再填充数据了

if (!aacEncoder.pcmBufferSize)
{
    *ioNumberDataPackets = 0;
    return  - 1;
}

填充PCM数据

ioData->mBuffers[0].mData = aacEncoder.pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (uint32_t)aacEncoder.pcmBufferSize;
ioData->mBuffers[0].mNumberChannels = (uint32_t)aacEncoder.config.channelCount;

填充完毕则清空数据

aacEncoder.pcmBufferSize = 0;
*ioNumberDataPackets = 1;
return noErr;

H、实现AAC编码的回调方法

在获取到编码完成之后的数据后的回调方法中可以选择写入文件或者直接解码。

- (void)audioEncodeCallback:(NSData *)aacData
{
    // 使用方式一:写入文件
    // [_handle seekToEndOfFile];
    // [_handle writeData:aacData];

    // 使用方式二:直接解码
    [_audioDecoder decodeAudioAACData:aacData];
}

2、音频AAC硬解码器

a、提供的接口方法

AAC解码回调代理

@protocol AudioDecoderDelegate 

- (void)audioDecodeCallback:(NSData *)pcmData;

@end

提供的接口方法和属性

@interface AudioDecoder : NSObject

@property (nonatomic, weak) id delegate;

/** 解码配置 */
@property (nonatomic, strong) AudioConfig *config;
/** 初始化时传入解码配置 */
- (instancetype)initWithConfig:(AudioConfig *)config;

/** 解码AAC */
- (void)decodeAudioAACData: (NSData *)aacData;

@end

b、导入的框架和私密属性

导入的框架

#import 
#import 

私密属性

@interface AudioDecoder()

// 音频解码器
@property (nonatomic) AudioConverterRef audioConverter;
// 音频解码条件
@property (strong, nonatomic) NSCondition *converterCond;
// 解码队列
@property (nonatomic, strong) dispatch_queue_t decoderQueue;
// 回调队列
@property (nonatomic, strong) dispatch_queue_t callbackQueue;

@property (nonatomic) char *aacBuffer;
@property (nonatomic) UInt32 aacBufferSize;
@property (nonatomic) AudioStreamPacketDescription *packetDesc;

@end

结构体

typedef struct
{
    char * data;
    UInt32 size;
    UInt32 channelCount;
    AudioStreamPacketDescription packetDesc;
} AudioUserData;

c、初始化时传入解码配置
- (instancetype)initWithConfig:(AudioConfig *)config
{
    self = [super init];
    if (self)
    {
        _decoderQueue = dispatch_queue_create("aac hard decoder queue", DISPATCH_QUEUE_SERIAL);
        _callbackQueue = dispatch_queue_create("aac hard decoder callback queue", DISPATCH_QUEUE_SERIAL);
        _audioConverter = NULL;
        _aacBufferSize = 0;
        _aacBuffer = NULL;
        
        _config = config;
        if (_config == nil)
        {
            _config = [[AudioConfig alloc] init];
        }
        
        AudioStreamPacketDescription desc = {0};
        _packetDesc = &desc;
        
        // 配置解码器的格式
        [self setupDecoder];
    }
    return self;
}

d、配置解码器的格式

输入参数AAC

AudioStreamBasicDescription inputAduioDes = {0};
inputAduioDes.mSampleRate = (Float64)_config.sampleRate;
inputAduioDes.mFormatID = kAudioFormatMPEG4AAC;
inputAduioDes.mFormatFlags = kMPEG4Object_AAC_LC;
inputAduioDes.mFramesPerPacket = 1024;
inputAduioDes.mChannelsPerFrame = (UInt32)_config.channelCount;

输出参数PCM

AudioStreamBasicDescription outputAudioDes = {0};
outputAudioDes.mSampleRate = (Float64)_config.sampleRate;       //采样率
outputAudioDes.mChannelsPerFrame = (UInt32)_config.channelCount; //输出声道数
outputAudioDes.mFormatID = kAudioFormatLinearPCM;                //输出格式
outputAudioDes.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked); //编码 12
outputAudioDes.mFramesPerPacket = 1;                            //每一个packet帧数 ;
outputAudioDes.mBitsPerChannel = 16;                             //数据帧中每个通道的采样位数。
outputAudioDes.mBytesPerFrame = outputAudioDes.mBitsPerChannel / 8 *outputAudioDes.mChannelsPerFrame;                              //每一帧大小(采样位数 / 8 *声道数)
outputAudioDes.mBytesPerPacket = outputAudioDes.mBytesPerFrame * outputAudioDes.mFramesPerPacket;                             //每个packet大小(帧大小 * 帧数)
outputAudioDes.mReserved =  0;                                  //对其方式 0(8字节对齐)

填充输入信息

UInt32 inDesSize = sizeof(inputAduioDes);
AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &inDesSize, &inputAduioDes);

获取解码器的描述信息

AudioClassDescription *audioClassDesc = [self getAudioCalssDescriptionWithType:outputAudioDes.mFormatID fromManufacture:kAppleSoftwareAudioCodecManufacturer];

创建解码器

OSStatus status = AudioConverterNewSpecific(&inputAduioDes, &outputAudioDes, 1, audioClassDesc, &_audioConverter);
if (status != noErr)
{
    NSLog(@"Error!:硬解码AAC创建失败, status= %d", (int)status);
    return;
}

e、实现解码AAC的方法

记录aac的信息

- (void)decodeAudioAACData:(NSData *)aacData
{
   
    if (!_audioConverter) { return; }
    
    dispatch_async(_decoderQueue, ^{
        AudioUserData userData = {0};
        userData.channelCount = (UInt32)_config.channelCount;
        userData.data = (char *)[aacData bytes];
        userData.size = (UInt32)aacData.length;
        userData.packetDesc.mDataByteSize = (UInt32)aacData.length;
        userData.packetDesc.mStartOffset = 0;
        userData.packetDesc.mVariableFramesInPacket = 0;
        .....
    });
}

输出大小和packet个数

UInt32 pcmBufferSize = (UInt32)(2048 * _config.channelCount);
UInt32 pcmDataPacketSize = 1024;

创建临时容器pcm

uint8_t *pcmBuffer = malloc(pcmBufferSize);
memset(pcmBuffer, 0, pcmBufferSize);

输出buffer

AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = (uint32_t)_config.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize = (UInt32)pcmBufferSize;
outAudioBufferList.mBuffers[0].mData = pcmBuffer;

配置填充函数,获取输出数据

// 输出描述
AudioStreamPacketDescription outputPacketDesc = {0};

OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, &AudioDecoderConverterComplexInputDataProc, &userData, &pcmDataPacketSize, &outAudioBufferList, &outputPacketDesc);
if (status != noErr)
{
    NSLog(@"Error: AAC Decoder error, status=%d",(int)status);
    return;
}

如果获取到数据则通过代理传递出去

if (outAudioBufferList.mBuffers[0].mDataByteSize > 0)
{
    NSData *rawData = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData length:outAudioBufferList.mBuffers[0].mDataByteSize];
    dispatch_async(_callbackQueue, ^{
        [_delegate audioDecodeCallback:rawData];
    });
}
free(pcmBuffer);

f、在解码器的回调函数不断填充数据
static OSStatus AudioDecoderConverterComplexInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData,  AudioStreamPacketDescription **outDataPacketDescription,  void *inUserData)
{
    AudioUserData *audioDecoder = (AudioUserData *)(inUserData);
    if (audioDecoder->size <= 0)
    {
        ioNumberDataPackets = 0;
        return -1;
    }
   
    // 填充数据
    *outDataPacketDescription = &audioDecoder->packetDesc;
    (*outDataPacketDescription)[0].mStartOffset = 0;
    (*outDataPacketDescription)[0].mDataByteSize = audioDecoder->size;
    (*outDataPacketDescription)[0].mVariableFramesInPacket = 0;
    
    ioData->mBuffers[0].mData = audioDecoder->data;
    ioData->mBuffers[0].mDataByteSize = audioDecoder->size;
    ioData->mBuffers[0].mNumberChannels = audioDecoder->channelCount;
    
    return noErr;
}

3、音频PCM播放

a、提供的接口方法
- (instancetype)initWithConfig:(AudioConfig *)config;

/** 播放pcm */
- (void)playPCMData:(NSData *)data;

/** 设置音量增量 0.0 - 1.0 */
- (void)setupVoice:(Float32)gain;

/** 销毁播放器 */
- (void)dispose;

b、导入的框架和私密属性

导入的框架

#import 
#import 

常量

#define MIN_SIZE_PER_FRAME 2048 // 每帧最小数据长度
static const int kNumberBuffers_play = 3;

结构体

typedef struct AudioPlayerState
{
    AudioStreamBasicDescription   mDataFormat;
    AudioQueueRef                 mQueue;
    AudioQueueBufferRef           mBuffers[kNumberBuffers_play];
    AudioStreamPacketDescription  *mPacketDescs;
}AudioPlayerState;

私密属性

@property (nonatomic, assign) AudioPlayerState audioPlayerState;
@property (nonatomic, strong) AudioConfig *config;
@property (nonatomic, assign) BOOL isPlaying;

c、初始化配置

将会话设置为活动或非活动。请注意,激活音频会话是一个同步(阻塞)操作。再设置会话类别为可录制和播放音频。

- (void)setupSession
{
    NSError *error = nil;
    [[AVAudioSession sharedInstance] setActive:YES error:&error];
    if (error)
    {
        NSLog(@"Error: audioQueue palyer AVAudioSession error, error: %@", error);
    }
    
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
    if (error)
    {
        NSLog(@"Error: audioQueue palyer AVAudioSession error, error: %@", error);
    }
}

创建播放队列。

OSStatus status = AudioQueueNewOutput(&_audioPlayerState.mDataFormat, TMAudioQueueOutputCallback, NULL, NULL, NULL, 0, &_audioPlayerState.mQueue);
if (status != noErr)
{
    NSError *error = [[NSError alloc] initWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
    NSLog(@"Error: AudioQueue create error = %@", [error description]);
    return self;
}

设置音量增量

  • mQueue:要开始的音频队列
  • kAudioQueueParam_Volume:属性
  • localGain:
[self setupVoice:1];

- (void)setupVoice:(Float32)gain
{
    Float32 localGain = gain;
    if (gain < 0)
    {
        localGain = 0;
    }
    else if (gain > 1)
    {
        localGain = 1;
    }
    
    // 设置播放音频队列参数值
    AudioQueueSetParameter(_audioPlayerState.mQueue, kAudioQueueParam_Volume, localGain);
}

d、实现播放pcm的方法
要求音频队列对象分配音频队列缓冲区
  • mQueue:要分配缓冲区的音频队列
  • MIN_SIZE_PER_FRAME:新缓冲区所需的容量(字节)
  • inBuffer:输出数据,指向新分配的音频队列缓冲区
  • memcpy:data里的数据拷贝到inBuffer.mAudioData
  • mAudioDataByteSize:设置大小
// 指向音频队列缓冲区
AudioQueueBufferRef inBuffer;
// 要求音频队列对象分配音频队列缓冲区
AudioQueueAllocateBuffer(_audioPlayerState.mQueue, MIN_SIZE_PER_FRAME, &inBuffer);
// 将data里的数据拷贝到inBuffer.mAudioData中
memcpy(inBuffer->mAudioData, data.bytes, data.length);
// 设置inBuffer.mAudioDataByteSize
inBuffer->mAudioDataByteSize = (UInt32)data.length;
将缓冲区添加到录制或播放音频队列的缓冲区队列
  • mQueue:拥有音频队列缓冲区的音频队列
  • inBuffer:要添加到缓冲区队列的音频队列缓冲区
OSStatus status = AudioQueueEnqueueBuffer(_audioPlayerState.mQueue, inBuffer, 0, NULL);
if (status != noErr)
{
    NSLog(@"Error: audio queue palyer  enqueue error: %d",(int)status);
}
开始播放或录制音频
  • mQueue:要开始的音频队列
  • NULL:音频队列应开始的时间。要指定相对于关联音频设备时间线的开始时间,请使用audioTimestamp结构的msampletime字段。使用NULL表示音频队列应尽快启动
AudioQueueStart(_audioPlayerState.mQueue, NULL);

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

你可能感兴趣的:(iOS音视频:直播)