一. 主要函数说明
- 创建解码描述器
使用CMVideoFormatDescriptionCreateFromH264ParameterSets()
创建解码描述器。
CMVideoFormatDescriptionCreateFromH264ParameterSets(
// 分配器
CFAllocatorRef _Nullable allocator,
// 参数个数
size_t parameterSetCount,
// 参数集指针
const uint8_t *const _Nonnull * _Nonnull parameterSetPointers,
// 参数集中每个元素大小的集合
const size_t * _Nonnull parameterSetSizes,
// NAL单元头部的长度
int NALUnitHeaderLength,
// 接受生成的描述器的地址
CMFormatDescriptionRef _Nullable * _Nonnull formatDescriptionOut
)
- 创建会话
使用VTDecompressionSessionCreate()
创建解码会话。
VTDecompressionSessionCreate(
// 分配器
CFAllocatorRef _Nullable allocator,
// 解码描述器
CMVideoFormatDescriptionRef _Nonnull videoFormatDescription,
// 必须使用的特殊的解码器,若传入NULL,表示让VideoToolBox自行选择
CFDictionaryRef _Nullable videoDecoderSpecification,
// 对图像缓冲区的需求,若传入NULL,表示没有需求
CFDictionaryRef _Nullable destinationImageBufferAttributes,
// 包含解码后的回调函数的结构体
const VTDecompressionOutputCallbackRecord * _Nullable outputCallback,
// 接受创建的会话的地址
VTDecompressionSessionRef _Nullable * _Nonnull decompressionSessionOut
)
- 设置解码会话属性
使用VTSessionSetProperty()
来完成编码属性的设置。
VTSessionSetProperty(
VTSessionRef _Nonnull session, // 要设置的编码会话
CFStringRef _Nonnull propertyKey, // 属性的key
CFTypeRef _Nullable propertyValue // 属性的value
)
- 创建CMBlockBuffer
使用CMBlockBufferCreateWithMemoryBlock()
创建CMBlockBuffer
。
CMBlockBufferCreateWithMemoryBlock(
// 分配器
CFAllocatorRef _Nullable structureAllocator,
// 内存块
void * _Nullable memoryBlock,
// 内存块大小
size_t blockLength,
// 内存块的分配器,若传NULL,表示使用默认的分配器
CFAllocatorRef _Nullable blockAllocator,
// 自定义的内存块指针,若不传NULL,该指针将会被用来创建和释放内存块
const CMBlockBufferCustomBlockSource * _Nullable customBlockSource,
// 数据偏移
size_t offsetToData,
// 数据长度
size_t dataLength,
// 特征和功能的标识
CMBlockBufferFlags flags,
// 用来接受生成的CMBlockBuffer的地址
CMBlockBufferRef _Nullable * _Nonnull blockBufferOut
)
- 创建CMSampleBuffer
使用CMSampleBufferCreateReady()
创建CMSampleBuffer
。
CMSampleBufferCreateReady(
// 分配器
CFAllocatorRef _Nullable allocator,
// blockBuffer
CMBlockBufferRef _Nullable dataBuffer,
// 解码描述器描述器
CMFormatDescriptionRef _Nullable formatDescription,
// CMSampleBuffer 个数
CMItemCount numSamples,
// sampleTimingArray的入口,必须为0、1或者numSamples
CMItemCount numSampleTimingEntries,
// 样本信息的数组,可以传NULL
const CMSampleTimingInfo * _Nullable sampleTimingArray,
// sampleSizeArray入口的个数,默认为1.(必须为0、1或者numSamples)
CMItemCount numSampleSizeEntries,
// 存储内存块大小的数组
const size_t * _Nullable sampleSizeArray,
// 接受生成的CMSampleBuffer的地址
CMSampleBufferRef _Nullable * _Nonnull sampleBufferOut
)
- 解码
使用VTDecompressionSessionDecodeFrame()
执行解码操作。
VTDecompressionSessionDecodeFrame(
VTDecompressionSessionRef _Nonnull session, // 解码会话
CMSampleBufferRef _Nonnull sampleBuffer, // 要解码的内容CMSampleBuffer
VTDecodeFrameFlags decodeFlags, // 指示解码器同步或异步的标识,若没有指定,回调方法会在该函数完成前被调用
void * _Nullable sourceFrameRefCon, // 接受解码后数据的地址
VTDecodeInfoFlags * _Nullable infoFlagsOut // 接受解码操作是同步/异步的地址,若传NULL,标识不接受此信息
)
- 结束解码
使用VTDecompressionSessionInvalidate()
结束解码。
VTDecompressionSessionInvalidate(
VTDecompressionSessionRef _Nonnull session // 解码会话
)
- 回调函数结构体
typedef void (*VTDecompressionOutputCallback)(
void * CM_NULLABLE decompressionOutputRefCon, // 回调函数的引用数据
void * CM_NULLABLE sourceFrameRefCon, // 接受解码后数据的地址
OSStatus status, // 解码执行结果
VTDecodeInfoFlags infoFlags, // 解码操作的信息(异步/被丢弃/可安全修改)
CM_NULLABLE CVImageBufferRef imageBuffer, // 解码后的结果
CMTime presentationTimeStamp, // 展示的时间戳
CMTime presentationDuration // 展示的持续时间
);
struct VTDecompressionOutputCallbackRecord {
CM_NULLABLE VTDecompressionOutputCallback decompressionOutputCallback; // 回调函数
void * CM_NULLABLE decompressionOutputRefCon; // 回调函数的引用数据
};
typedef struct VTDecompressionOutputCallbackRecord VTDecompressionOutputCallbackRecord;
二. 解码流程
三. 具体实现
1. 解析数据
我们拿到的数据一般都是一个NSData
,所以需要先转换成解码器可以解码的数据。
- 首先将
NSData
转换成二进制字节流,并且拿到字节流的长度。
uint8_t *nalUnit = (uint8_t *)data.bytes;
uint32_t nalUnitLength = (uint32_t)data.length;
- 获取NAL单元的类型,并做小端到大端的转换。
int nalType = nalUnit[4] & 0x1F;
uint32_t nalSize = nalUnitLength - 4;
uint8_t *nalSizePointer = (uint8_t *)(&nalSize);
nalUnit[0] = *(nalSizePointer + 3);
nalUnit[1] = *(nalSizePointer + 2);
nalUnit[2] = *(nalSizePointer + 1);
nalUnit[3] = *(nalSizePointer + 0);
关于类型,可以参考这个下面的表。
nal_unit_type | 类型 |
---|---|
0 | 未定义 |
1 | 非IDA图像中不采用数据划分片段 |
2 | 非IDA图像中A类数据划分片段 |
3 | 非IDA图像中B类数据划分片段 |
4 | 非IDA图像中C类数据划分片段 |
5 | IDA图像的片(I帧/关键帧) |
6 | 补充增强信息单元(SEI) |
7 | 序列餐数据(SPS) |
8 | 图像参数集(PPS) |
9 | 分界符 |
10 | 序列结束 |
11 | 码流结束 |
12 | 填充 |
13-23 | 保留 |
24-31 | 不保留(RTP打包时会用到) |
其中,nal_unit_type
= 6时,类型为补充增强信息单元(SEI),是没有图像数据信息的,单独处理没有意义。
根据上表,我们可以拿到SPS和PPS保存下来,SEI不做处理,其余都进行解码操作。
2. 创建解码描述器
我们拿到了SPS和PPS,就可以使用它们创建解码描述器了。
CMVideoFormatDescriptionRef decodeDesc;
const uint8_t * parameterSetPointers[] = {self.sps, self.pps};
const size_t parameterSetSizes[] = {self.spsSize, self.ppsSize};
int NALUnitLength = 4;
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, NALUnitLength, &decodeDesc);
if (status != noErr) {
NSLog(@"format description create error [ status : %d ]", status);
}
3. 创建会话
VTDecompressionSessionRef decodeSession;
NSDictionary *destinationImageBufferAttributes = @{
// 摄像头的输出数据格式
(id)kCVPixelBufferPixelFormatTypeKey : [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
// 图像数据的宽
(id)kCVPixelBufferWidthKey : [NSNumber numberWithInteger:self.width],
// 图像数据的高
(id)kCVPixelBufferHeightKey : [NSNumber numberWithInteger:self.height],
// 是否允许OpenGL直接绘制解码后的图像
(id)kCVPixelBufferOpenGLCompatibilityKey : [NSNumber numberWithBool:YES]
};
VTDecompressionOutputCallbackRecord record;
record.decompressionOutputCallback = decodeComplete;
record.decompressionOutputRefCon = (__bridge void * _Nullable)self;
OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault, self.decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationImageBufferAttributes), &record, &decodeSession);
if (status != noErr) {
NSLog(@"decode session create error! [ ststua : %d ]", status);
}
4. 设置解码会话属性
OSStatus status = VTSessionSetProperty(decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
NSLog(@"decode session set property error! [ status : %d ]", status);
5. 创建CMSampleBuffer
- 创建
CMBlockBuffer
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags blockBufferFlags = 0;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, nalUnit, nalUnitLength, kCFAllocatorNull, NULL, 0, nalUnitLength, blockBufferFlags, &blockBuffer);
if (status != noErr) {
NSLog(@"blockBuffer create error! [ status : %d ]", status);
}
- 创建
CMSampleBuffer
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {nalUnitLength};
OSStatus status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, self.decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != noErr) {
NSLog(@"sampleBuffer create error! [ status : %d ]", status);
}
6. 解码
拿到了CMSampleBuffer
后,我们就可以解码了。
VTDecodeFrameFlags frameFlag = kVTDecodeFrame_1xRealTimePlayback;
VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;
status = VTDecompressionSessionDecodeFrame(decodeSession, sampleBuffer, frameFlag, NULL, &infoFlag);
if (status == kVTInvalidSessionErr) {
NSLog(@"decode invalid iession error! [ status : %d ]", status);
} else if (status == kVTVideoDecoderBadDataErr) {
NSLog(@"decode bad data error! [ status : %d ]", status);
} else if (status != noErr) {
NSLog(@"decode frame error! [ status : %d ]", status);
}
CFRelease(blockBuffer);
CFRelease(sampleBuffer);
7. 解码后处理
解码后的数据就会输出到我们的回调函数中,我们可以在会调函数中根据需求进行Open GL
渲染或者直接返回。
void decodeComplete(void * CM_NULLABLE decompressionOutputRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CM_NULLABLE CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration) {
if (status != noErr) {
NSLog(@"decode callback error! [ status : %d ]", status);
}
KKKVideoDecoder *decoder = (__bridge KKKVideoDecoder *)decompressionOutputRefCon;
dispatch_async(decoder.callBackQueue, ^{
[decoder.delegate decoderGetImageBuffer:imageBuffer];
});
}
8. 结束解码
在dealloc
方法中,我们就可以结束编码,并且释放编码会话。
- (void)dealloc {
if (decodeSession) {
VTDecompressionSessionInvalidate(decodeSession);
CFRelease(decodeSession);
decodeSession = nil;
}
}