前言
好记性不如烂笔头,最近有点空把一些知识也整理了一遍,后面陆续写一些总结吧!先从这个不太熟悉的音视频这块开始吧,2016年可谓是直播元年,这块的技术也没什么很陌生了,后知后觉的自己最近也开始学习学习,有挺多调用 C 函数,如果是进行编码封装格式最好还是用c语言写跨平台且效率也更佳,后面有时间尝试用c写下,此文只是直播技术的一部分,未完待续,如有误欢迎指正,当然如果看完觉得有帮助也请帮忙点个喜欢❤️。
技术整体概览
-
直播的技术
- 直播技术总体来说分为采集、前处理、编码、传输、解码、渲染这几个环节
-
流程图例:
音视频采集
采集介绍
音视频的采集是直播架构的第一个环节,也是直播的视频来源,视频采集有多个应用场景,比如二维码开发。音视频采集包括两部分:
视频采集
音频采集
iOS 开发中,是可以同步采集音视频,相关的采集 API 封装在 AVFoundation 框架中,使用方式简单
采集步骤
- 导入框架 AVFoundation 框架
- 创建捕捉会话(AVCaptureSession)
- 该会话用于连接之后的输入源&输出源
- 输入源:摄像头&话筒
- 输出源:拿到对应的音频&视频数据的出口
- 会话:用于将输入源&输出源连接起来
- 设置视频输入源&输出源相关属性
- 输入源(AVCaptureDeviceInput):从摄像头输入
- 输出源(AVCaptureVideoDataOutput):可以设置代理,在代理中处理对应输入后得到的数据,以及设置例如丢帧等情况的处理
- 将输入&输出添加到会话中
采集代码
- 自定义一个继承
NSObject
的CCVideoCapture
类,用用于处理音视频的采集 -
CCVideoCapture
代码如下:
//
// CCVideoCapture.m
// 01.视频采集
//
// Created by zerocc on 2017/3/29.
// Copyright © 2017年 zerocc. All rights reserved.
//
#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>
@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@end
@implementation CCVideoCapture
- (void)startCapturing:(UIView *)preView
{
// =============== 采集视频 ===========================
// 1. 创建 session 会话
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPreset1280x720;
self.session = session;
// 2. 设置音视频的输入
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:&error];
[self.session addInput:videoInput];
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:&error];
[self.session addInput:audioInput];
// 3. 设置音视频的输出
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
dispatch_queue_t videoQueue = dispatch_queue_create("Video Capture Queue", DISPATCH_QUEUE_SERIAL);
[videoOutput setSampleBufferDelegate:self queue:videoQueue];
[videoOutput setAlwaysDiscardsLateVideoFrames:YES];
if ([session canAddOutput:videoOutput]) {
[self.session addOutput:videoOutput];
}
AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
dispatch_queue_t audioQueue = dispatch_queue_create("Audio Capture Queue", DISPATCH_QUEUE_SERIAL);
[audioOutput setSampleBufferDelegate:self queue:audioQueue];
if ([session canAddOutput:audioOutput]) {
[session addOutput:audioOutput];
}
// 4. 获取视频输入与输出连接,用于分辨音视频数据
// 视频输出方向 默认方向是相反设置方向,必须在将 output 添加到 session 之后
AVCaptureConnection *videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];
self.videoConnection = videoConnection;
if (videoConnection.isVideoOrientationSupported) {
videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
[self.session commitConfiguration];
}else {
NSLog(@"不支持设置方向");
}
// 5. 添加预览图层
AVCaptureVideoPreviewLayer *layer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
self.layer = layer;
layer.frame = preView.bounds;
[preView.layer insertSublayer:layer atIndex:0];
// 6. 开始采集
[self.session startRunning];
}
// 丢帧视频情况
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (self.videoConnection == connection) {
dispatch_sync(queue, ^{
});
NSLog(@"采集到视频数据");
} else {
dispatch_sync(queue, ^{
});
NSLog(@"采集到音频数据");
}
NSLog(@"采集到视频画面");
}
- (void)stopCapturing
{
[self.encoder endEncode];
[self.session stopRunning];
[self.layer removeFromSuperlayer];
}
@end
音视频编码
音视频压缩编码介绍
-
不经编码的视频非常庞大,存储起来都麻烦,更何况网络传输
- 编码通过压缩音视频数据来减少数据体积,方便音视频数据的推流、拉流和存储,能大大提高存储传输效率
- 以录制视频一秒钟为例,需要裸流视频多大空间呢?
- 视频无卡顿效果一秒钟至少16帧(正常开发一般 30FPS )
- 假如该视频是一个1280*720分辨率的视频
- 对应像素用RGB,3*8
- 结果多大:(1280 x 720 x 30 x 24) / (1024 x 1024) = 79MB
-
录制一秒钟音频,又需要多大空间?
- 取样频率若为44.1KHz
- 样本的量化比特数为16
- 普通立体声的信号通道数为2
- 音频信号的传输率 = 取样频率 x 样本的量化比特数 x 通道数;那么计算一秒钟音频数据量大概为1.4MB
音视频中的冗余信息
视频(裸流)存在冗余信息,空间冗余、时间冗余、视觉冗余等等,经过一系列的去除冗余信息,可以大大的降低视频的数据量,更利用视频的保存、传输。去除冗余信息的过程,我们就称之为视频图像压缩编码。
数字音频信号包含了对人感受可以忽略的冗余信息,时域冗余、频域冗余、听觉冗余等等。
-
音视频编码方式:
- 硬编码:使用非 CPU 进行编码,如利用系统提供的显卡 GPU、专用 DSP 芯片等
- 软编码:使用 CPU 进行编码(手机容易发热)
-
各个平台处理:
- iOS端:硬编码兼容性较好,可以直接进行硬编码,苹果在iOS 8.0后,开放系统的硬件编码解码功能,Video ToolBox 的框架来处理硬件的编码和解码,苹果将该框架引入iOS系统。
- Android端:硬编码较难,难找到统一的库兼容各个平台(推荐使用软编码)
-
编码标准:
视频编码:H.26X 系列、MPEG 系列(由ISO[国际标准组织机构]下属的MPEG[运动图像专家组]开发)、C-1、VP8、VP9等
H.261:主要在老的视频会议和视频电话产品中使用
H.263:主要用在视频会议、视频电话和网络视频上
H.264:H.264/MPEG-4 第十部分。或称AVC (Advanced Video Coding, 高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频录制、压缩和发布格式。
H.265:高效率视频编码(High Efficency Video Coding, 简称HEVC) 是一种视频压缩标准,H.264/MPEG-4 AVC 的继承者。可支持4K 分辨率甚至到超高画质电视,最高分辨率可达到8192*4320(8K分辨率),这是目前发展的趋势,尚未有大众化编码软件出现
MPEG-1第二部分:MPEG-1第二部分主要使用在VCD 上,有写在有线视频也是用这种格式
MPEG-2第二部分:MPEG-2第二部分等同于H.262,使用在DVD、SVCD和大多数数字视频广播系统中
MPEG-4第二部分:MPEG-4第二部分标准可以使用在网络传输、广播和媒体存储上
音频编码:AAC、Opus、MP3、AC-3等
视频压缩编码 H.264 (AVC)
-
序列(GOP - Group of picture)
- 在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流
- 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是I帧图像
- H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列
- 这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会
- IDR 图像之后的图像永远不会使用 IDR 之前的图像的数据来解码
- 一个序列就是一段内容差异不太大的图像编码后生成的一串数据流
- 当运动变化较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,I帧做为随机访问的参考点,然后一直P帧、B帧了
- 当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧
-
在H264协议里定义了三种帧
- I帧:完整编码的叫做I帧,关键帧
- P帧:参考之前的I帧生成的只包含差异部分编码的帧叫做P帧
- B帧:参考前后的帧(I&P)编码的帧叫B帧
-
H264的压缩方法
- 分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化帧数不宜取多
- 定义帧:将每组内容帧图像定义为三种类型:既I帧、P帧、B帧
- 预测帧:以I帧做为基础帧,以I帧预测P帧,再由P帧预测B帧
- 数据传输:最后将I帧数据与预测的差值信息进行存储和传输
- H264采用的核心算法是帧内压缩和帧间压缩
- 帧内压缩也称为空间压缩是生成I帧的算法,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。
- 帧间压缩也称为时间压缩是生成P帧和B帧的算法,它通过比较时间轴上不同帧之间的数据进行压缩,通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。
-
分层设计
- H264算法在概念上分为两层:视频编码层(VCL:Video Coding Layer)负责高效的视频内容表示;网络提取层(NAL:Network Abstraction Layer)负责以网络所要求的恰当的方式对数据进行打包和传送。这样,高效编码和网络友好性分别VCL 和 NAL 分别完成
- NAL设计目的,根据不同的网络把数据打包成相应的格式,将VCL产生的比特字符串适配到各种各样的网络和多元环境中
-
NAL 的封装方式
H.264流数据正是由一系列的 NALU 单元(NAL Unit, 简称NALU)组成的。
NALU 是将每一帧数据写入到一个 NALU 单元中,进行传输或存储的
NALU 分为 NALU 头和 NALU 体
NALU 头通常为00 00 00 01,作为要给新的 NALU 的起始标识
-
NALU体封装着VCL编码后的信息或者其他信息
-
封装过程
- 对于流数据来说,一个NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作为开头,
0x00 00 01因此被称为开始码(Start code)。 - 提取 SPS (Picture Parameter Sets) 图像参数集和 PPS (Sequence Parameter Set)序列参数集 生成 FormatDesc 非VCL的NAL单元,一般来说编码器编出的首帧数据为 PPS 与 SPS 。
- 每个NALU的开始码是0x00 00 01,按照开始码定位NALU
- 通过类型信息找到sps和pps并提取,开始码后第一个byte的后5位,7代表sps,8代表pps
- 使用
CMSampleBufferGetFormatDescription
函数来构建CMVideoFormatDescriptionRef
- 对于流数据来说,一个NAUL的Header中,可能是0x00 00 01或者是0x00 00 00 01作为开头,
提取视频图像数据生成
CMBlockBuffer
,编码后的I帧、后续B帧、P帧数据自定义一个
CCH264Encoder
类处理 视频(裸流) h264 硬编码,代码如下:
//
// CCH264Encoder.h
// 01.视频采集
//
// Created by zerocc on 2017/4/4.
// Copyright © 2017年 zerocc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
@interface CCH264Encoder : NSObject
- (void)prepareEncodeWithWidth:(int)width height:(int)height;
- (void)encoderFram:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end
//
// CCH264Encoder.m
// 01.视频采集
//
// Created by zerocc on 2017/4/4.
// Copyright © 2017年 zerocc. All rights reserved.
//
#import "CCH264Encoder.h"
#import <VideoToolbox/VideoToolbox.h>
#import <AudioToolbox/AudioToolbox.h>
@interface CCH264Encoder () {
int _spsppsFound;
}
@property (nonatomic, assign) VTCompressionSessionRef compressionSession; // coreFoundation中的对象 == c语言对象 不用*,strong只用来修饰oc对象,VTCompressionSessionRef对象本身就是指针 用assign修饰
@property (nonatomic, assign) int frameIndex;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@property (nonatomic, strong) NSString *documentDictionary;
@end
@implementation CCH264Encoder
#pragma mark - lazyload
- (NSFileHandle *)fileHandle {
if (!_fileHandle) {
// 这里只写一个裸流,只有视频流没有音频流
NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) firstObject] stringByAppendingPathComponent:@"video.h264"];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isExistPath = [fileManager isExecutableFileAtPath:filePath];
if (isExistPath) {
[fileManager removeItemAtPath:filePath error:nil];
}
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
}
return _fileHandle;
}
/**
准备编码
@param width 采集的宽度
@param height 采集的高度
*/
- (void)prepareEncodeWithWidth:(int)width height:(int)height
{
// 0. 设置默认是第0帧
self.frameIndex = 0;
// 1. 创建 VTCompressionSessionRef
/**
VTCompressionSessionRef
@param NULL CFAllocatorRef - CoreFoundation分配内存的模式,NULL默认
@param width int32_t - 视频宽度
@param height 视频高度
@param kCMVideoCodecType_H264 CMVideoCodecType - 编码的标准
@param NULL CFDictionaryRef encoderSpecification,
@param NULL CFDictionaryRef sourceImageBufferAttributes
@param NULL CFAllocatorRef compressedDataAllocator
@param didComparessionCallback VTCompressionOutputCallback - 编码成功后的回调函数c函数
@param _Nullable void * - 传递到回调函数中参数
@return session
*/
VTCompressionSessionCreate(NULL,
width,
height,
kCMVideoCodecType_H264,
NULL, NULL, NULL,
didComparessionCallback,
(__bridge void * _Nullable)(self),
&_compressionSession);
// 2. 设置属性
// 2.1 设置实时编码
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_RealTime,
kCFBooleanTrue);
// 2.2 设置帧率
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_ExpectedFrameRate,
(__bridge CFTypeRef _Nonnull)(@24));
// 2.3 设置比特率(码率) 1500000/s
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_AverageBitRate,
(__bridge CFTypeRef _Nonnull)(@1500000)); // 每秒有150万比特 bit
// 2.4 关键帧最大间隔,也就是I帧。
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_DataRateLimits,
(__bridge CFTypeRef _Nonnull)(@[@(1500000/8), @1])); // 单位是 8 byte
// 2.5 设置GOP的大小
VTSessionSetProperty(_compressionSession,
kVTCompressionPropertyKey_MaxKeyFrameInterval,
(__bridge CFTypeRef _Nonnull)(@20));
// 3. 准备编码
VTCompressionSessionPrepareToEncodeFrames(_compressionSession);
}
/**
开始编码
@param sampleBuffer CMSampleBufferRef
*/
- (void)encoderFram:(CMSampleBufferRef)sampleBuffer
{
// 2. 开始编码
// 将CMSampleBufferRef 转换成 CVImageBufferRef,
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime pts = CMTimeMake(self.frameIndex, 24);
CMTime duration = kCMTimeInvalid;
VTEncodeInfoFlags flags;
/**
硬编码器 - 将携带视频帧数据的imageBuffer送到硬编码器中,进行编码。
@param session#> VTCompressionSessionRef
@param imageBuffer#> CVImageBufferRef
@param presentationTimeStamp#> CMTime - pts
@param duration#> CMTime - 传一个固定的无效的时间
@param frameProperties#> CFDictionaryRef
@param sourceFrameRefCon#> void - 编码后回调函数中第二个参数
@param infoFlagsOut#> VTEncodeInfoFlags - 编码后回调函数中第四个参数
@return
*/
VTCompressionSessionEncodeFrame(self.compressionSession,
imageBuffer,
pts,
duration,
NULL,
NULL,
&flags);
NSLog(@"开始编码一帧数据");
}
#pragma mark - 获取编码后的数据 c语言函数 - 编码后的回调函数
void didComparessionCallback (void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer)
{
// c语言中不能调用当前对象self.语法不行,只有通过指针去做相应操作
// 获取当前 CCH264Encoder 对象,通过传入的 self 参数(VTCompressionSessionCreate中传入了self)
CCH264Encoder *encoder = (__bridge CCH264Encoder *)(outputCallbackRefCon);
// 1. 判断该帧是否为关键帧
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
BOOL isKeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
// 2. 如果是关键帧 -> 获取 SPS/PPS 数据 其保存了h264视频的一些必要信息方便解析 -> 并且写入文件
if (isKeyFrame && !encoder->_spsppsFound) {
encoder->_spsppsFound = 1;
// 2.1. 从CMSampleBufferRef获取CMFormatDescriptionRef
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
// 2.2. 获取 SPS /pps的信息
const uint8_t *spsOut;
size_t spsSize, spsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
0,
&spsOut,
&spsSize,
&spsCount,
NULL);
const uint8_t *ppsOut;
size_t ppsSize, ppsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format,
1,
&ppsOut,
&ppsSize,
&ppsCount,
NULL);
// 2.3. 将SPS/PPS 转成NSData,并且写入文件
NSData *spsData = [NSData dataWithBytes:spsOut length:spsSize];
NSData *ppsData = [NSData dataWithBytes:ppsOut length:ppsSize];
// 2.4. 写入文件 (NALU单元特点:起始都是有 0x00 00 00 01 每个NALU需拼接)
[encoder writeH264Data:spsData];
[encoder writeH264Data:ppsData];
}
// 3. 获取编码后的数据,写入文件
// 3.1. 获取 CMBlockBufferRef
CMBlockBufferRef blcokBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
// 3.2. 从blockBuffer 中获取起始位置的内存地址
size_t totalLength = 0;
char *dataPointer;
CMBlockBufferGetDataPointer(blcokBuffer, 0, NULL, &totalLength, &dataPointer);
// 3.3. 一帧的图像可能需要写入多个NALU 单元 --> Slice切换
static const int H264HeaderLength = 4; // 头部长度一般为 4
size_t bufferOffset = 0;
while (bufferOffset < totalLength - H264HeaderLength) {
// 3.4 从起始位置拷贝H264HeaderLength长度的地址,计算NALULength
int NALULength = 0;
memcpy(&NALULength, dataPointer + bufferOffset, H264HeaderLength);
// H264 编码的数据是大端模式(字节序),转化为iOS系统的模式,计算机内一般都是小端,而网络和文件中一般都是大端
NALULength = CFSwapInt32BigToHost(NALULength);
// 3.5 从dataPointer开始,根据长度创建NSData
NSData *data = [NSData dataWithBytes:(dataPointer + bufferOffset + H264HeaderLength) length:NALULength];
// 3.6 写入文件
[encoder writeH264Data:data];
// 3.7 重新设置 bufferOffset
bufferOffset += NALULength + H264HeaderLength;
}
NSLog(@"。。。。。。。。编码出一帧数据");
};
- (void)writeH264Data:(NSData *)data
{
// 1. 先获取 startCode
const char bytes[] = "\x00\x00\x00\x01";
// 2. 获取headerData
// 减一的原因: byts 拼接的是字符串,而字符串最后一位有个 \0;所以减一才是其正确长度
NSData *headerData = [NSData dataWithBytes:bytes length:(sizeof(bytes) - 1)];
[self.fileHandle writeData:headerData];
[self.fileHandle writeData:data];
}
- (void)endEncode
{
VTCompressionSessionInvalidate(self.compressionSession);
CFRelease(_compressionSession);
}
@end
调试步骤
-
下载 VLC 播放器
音频编码 AAC
- AAC音频格式有ADIF和ADTS:
- ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,ADIF只有一个统一的头,所以必须得到所有的数据后解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
- ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。语音系统对实时性要求较高,基本流程是采集音频数据,本地编码,数据上传,服务器处理,数据下发,本地解码,下面学习分析也都以这个格式为例进行。
- AAC原始码流(又称为“裸流”)是由一个一个的ADTS frame组成的。其中每个ADTS frame之间通过syncword(同步字)进行分隔。同步字为0xFFF(二进制“111111111111”)。AAC码流解析的步骤就是首先从码流中搜索0x0FFF,分离出ADTS frame;然后再分析ADTS frame的首部各个字段。ADTS frame的组成:
- ADTS帧头包含固定帧头、可变帧头,其中定义了音频采样率、音频声道数、帧长度等关键信息,用于帧净荷数据的解析和解码。
- ADTS帧净荷主要由原始帧组成。
- 下图中表示出了ADTS一帧的简明结构,其两边的空白矩形表示一帧前后的数据。
ADTS 内容及结构
-
ADTS的头信息,一般都是7个字节 (也有是9字节的),分为2部分:
adts_fixed_header()
和adts_variable_header()
。- ADTS 的固定头
adts_fixed_header()
结构组成:
- syncword:同步字,12比特的 "1111 1111 1111"
- ID:MPEG 标志符,0表示MPEG-4, 1表示MPEG-2
- layer:表示音频编码的层
- protection_absent:表示是否误码校验
- profile:表示使用哪个级别的 AAC
- sampling_frequency_index:表示使用的采样率索引
- channel_configuration:表示声道数
- ADTS 的可变头
adts_variable_header()
结构组成:
- aac_frame_lenth:ADTS帧的长度
- adts_buffer_fullness:0x7FF 说明是码流可变的码流
- number_of_raw_data_blocks_in_frame:每一个ADTS帧中原始帧的数量
- ADTS 的固定头
自定义一个
CCAACEncoder
类处理 音频(裸流) AAC 硬编码,代码如下:
//
// CCH264Encoder.h
// 01.视频采集
//
// Created by zerocc on 2017/4/4.
// Copyright © 2017年 zerocc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
@interface CCAACEncoder : NSObject
- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer;
- (void)endEncodeAAC;
@end
//
// CCH264Encoder.h
// 01.视频采集
//
// Created by zerocc on 2017/4/4.
// Copyright © 2017年 zerocc. All rights reserved.
//
#import "CCAACEncoder.h"
@interface CCAACEncoder()
@property (nonatomic, assign) AudioConverterRef audioConverter;
@property (nonatomic, assign) uint8_t *aacBuffer;
@property (nonatomic, assign) NSUInteger aacBufferSize;
@property (nonatomic, assign) char *pcmBuffer;
@property (nonatomic, assign) size_t pcmBufferSize;
@property (nonatomic, strong) NSFileHandle *audioFileHandle;
@end
@implementation CCAACEncoder
- (void) dealloc {
AudioConverterDispose(_audioConverter);
free(_aacBuffer);
}
- (NSFileHandle *)audioFileHandle {
if (!_audioFileHandle) {
NSString *audioFile = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"audio.aac"];
[[NSFileManager defaultManager] removeItemAtPath:audioFile error:nil];
[[NSFileManager defaultManager] createFileAtPath:audioFile contents:nil attributes:nil];
_audioFileHandle = [NSFileHandle fileHandleForWritingAtPath:audioFile];
}
return _audioFileHandle;
}
- (id)init {
if (self = [super init]) {
_audioConverter = NULL;
_pcmBufferSize = 0;
_pcmBuffer = NULL;
_aacBufferSize = 1024;
_aacBuffer = malloc(_aacBufferSize * sizeof(uint8_t));
memset(_aacBuffer, 0, _aacBufferSize);
}
return self;
}
- (void)encodeAAC:(CMSampleBufferRef)sampleBuffer
{
CFRetain(sampleBuffer);
// 1. 创建audio encode converter
if (!_audioConverter) {
// 1.1 设置编码参数
AudioStreamBasicDescription inputAudioStreamBasicDescription = *CMAudioFormatDescriptionGetStreamBasicDescription((CMAudioFormatDescriptionRef)CMSampleBufferGetFormatDescription(sampleBuffer));
AudioStreamBasicDescription outputAudioStreamBasicDescription = {
.mSampleRate = inputAudioStreamBasicDescription.mSampleRate,
.mFormatID = kAudioFormatMPEG4AAC,
.mFormatFlags = kMPEG4Object_AAC_LC,
.mBytesPerPacket = 0,
.mFramesPerPacket = 1024,
.mBytesPerFrame = 0,
.mChannelsPerFrame = 1,
.mBitsPerChannel = 0,
.mReserved = 0
};
static AudioClassDescription description;
UInt32 encoderSpecifier = kAudioFormatMPEG4AAC;
UInt32 size;
AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
sizeof(encoderSpecifier),
&encoderSpecifier,
&size);
unsigned int count = size / sizeof(AudioClassDescription);
AudioClassDescription descriptions[count];
AudioFormatGetProperty(kAudioFormatProperty_Encoders,
sizeof(encoderSpecifier),
&encoderSpecifier,
&size,
descriptions);
for (unsigned int i = 0; i < count; i++) {
if ((kAudioFormatMPEG4AAC == descriptions[i].mSubType) &&
(kAppleSoftwareAudioCodecManufacturer == descriptions[i].mManufacturer)) {
memcpy(&description , &(descriptions[i]), sizeof(description));
}
}
AudioConverterNewSpecific(&inputAudioStreamBasicDescription,
&outputAudioStreamBasicDescription,
1,
&description,
&_audioConverter);
}
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
CFRetain(blockBuffer);
OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &_pcmBufferSize, &_pcmBuffer);
NSError *error = nil;
if (status != kCMBlockBufferNoErr) {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
memset(_aacBuffer, 0, _aacBufferSize);
AudioBufferList outAudioBufferList = {0};
outAudioBufferList.mNumberBuffers = 1;
outAudioBufferList.mBuffers[0].mNumberChannels = 1;
outAudioBufferList.mBuffers[0].mDataByteSize = (int)_aacBufferSize;
outAudioBufferList.mBuffers[0].mData = _aacBuffer;
AudioStreamPacketDescription *outPacketDescription = NULL;
UInt32 ioOutputDataPacketSize = 1;
status = AudioConverterFillComplexBuffer(_audioConverter, inInputDataProc, (__bridge void *)(self), &ioOutputDataPacketSize, &outAudioBufferList, outPacketDescription);
if (status == 0) {
NSData *rawAAC = [NSData dataWithBytes:outAudioBufferList.mBuffers[0].mData
length:outAudioBufferList.mBuffers[0].mDataByteSize];
NSData *adtsHeader = [self adtsDataForPacketLength:rawAAC.length];
NSMutableData *fullData = [NSMutableData dataWithData:adtsHeader];
[fullData appendData:rawAAC];
[self.audioFileHandle writeData:fullData];
} else {
error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
}
/**
编码器回调 C函数
@param inAudioConverter xx
@param ioNumberDataPackets xx
@param ioData xx
@param outDataPacketDescription xx
@param inUserData xx
@return xx
*/
OSStatus inInputDataProc(AudioConverterRef inAudioConverter,
UInt32 *ioNumberDataPackets,
AudioBufferList *ioData,
AudioStreamPacketDescription **outDataPacketDescription,
void *inUserData)
{
CCAACEncoder *encoder = (__bridge CCAACEncoder *)(inUserData);
UInt32 requestedPackets = *ioNumberDataPackets;
// 填充PCM到缓冲区
size_t copiedSamples = encoder.pcmBufferSize;
ioData->mBuffers[0].mData = encoder.pcmBuffer;
ioData->mBuffers[0].mDataByteSize = (int)encoder.pcmBufferSize;
encoder.pcmBuffer = NULL;
encoder.pcmBufferSize = 0;
if (copiedSamples < requestedPackets) {
//PCM 缓冲区还没满
*ioNumberDataPackets = 0;
return -1;
}
*ioNumberDataPackets = 1;
return noErr;
}
/**
* Add ADTS header at the beginning of each and every AAC packet.
* This is needed as MediaCodec encoder generates a packet of raw
* AAC data.
*/
- (NSData *) adtsDataForPacketLength:(NSUInteger)packetLength {
int adtsLength = 7;
char *packet = malloc(sizeof(char) * adtsLength);
// Variables Recycled by addADTStoPacket
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 1; //MPEG-4 Audio Channel Configuration. 1 Channel front-center
NSUInteger fullLength = adtsLength + packetLength;
// fill in ADTS data
packet[0] = (char)0xFF; // 11111111 = syncword
packet[1] = (char)0xF9; // 1111 1 00 1 = syncword MPEG-2 Layer CRC
packet[2] = (char)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
packet[3] = (char)(((chanCfg&3)<<6) + (fullLength>>11));
packet[4] = (char)((fullLength&0x7FF) >> 3);
packet[5] = (char)(((fullLength&7)<<5) + 0x1F);
packet[6] = (char)0xFC;
NSData *data = [NSData dataWithBytesNoCopy:packet length:adtsLength freeWhenDone:YES];
return data;
}
- (void)endEncodeAAC
{
AudioConverterDispose(_audioConverter);
_audioConverter = nil;
}
@end
- 测试音视频
- 采集音视频
CCVideoCapture
类中导入CCH264Encoder
和CCAACEncoder
- 采集音视频
#import "CCVideoCapture.h"
#import <AVFoundation/AVFoundation.h>
#import "CCH264Encoder.h"
#import "CCAACEncoder.h"
@interface CCVideoCapture () <AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *layer;
@property (nonatomic, strong) AVCaptureConnection *videoConnection;
@property (nonatomic, strong) CCH264Encoder *videoEncoder;
@property (nonatomic , strong) CCAACEncoder *audioEncoder;
@end
@implementation CCVideoCapture
#pragma mark - lazyload
- (CCH264Encoder *)videoEncoder {
if (!_videoEncoder) {
_videoEncoder = [[CCH264Encoder alloc] init];
}
return _videoEncoder;
}
- (CCAACEncoder *)audioEncoder {
if (!_audioEncoder) {
_audioEncoder = [[CCAACEncoder alloc] init];
}
return _audioEncoder;
}
- (void)startCapturing:(UIView *)preView
{
// =============== 准备编码 ===========================
[self.videoEncoder prepareEncodeWithWidth:720 height:1280];
// =============== 采集视频 ===========================
// 1. 创建 session 会话
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPreset1280x720;
self.session = session;
.
.
.
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (self.videoConnection == connection) {
dispatch_sync(queue, ^{
[self.videoEncoder encoderFram:sampleBuffer];
});
NSLog(@"采集到视频数据");
} else {
dispatch_sync(queue, ^{
[self.audioEncoder encodeAAC:sampleBuffer];
});
NSLog(@"采集到音频数据");
}
NSLog(@"采集到视频画面");
}
- (void)stopCapturing
{
[self.videoEncoder endEncode];
[self.audioEncoder endEncodeAAC];
[self.session stopRunning];
[self.layer removeFromSuperlayer];
}
未完待续
参考资料
编码和封装、推流和传输
AAC以 ADTS 格式封装的分析
[音频介绍](https://wenku.baidu.com/view/278f415e804d2b160b4ec0ac.html%20%20)