iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件

时间:2022-06-06 15:22:27

本文档为iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):1 概述续篇,主要描述:

  • CMSampleBufferRef读取实际数据

  • 序列参数集(Sequence Parameter Set, SPS)

  • 图像序列参数(Picture Parameter Set, PPS)

等内容。

1、视频实际内容数据持久化

1.1、可攻可受的CMSampleBufferRef

文档1 概述中回调函数

static void compressionOutputCallback(                                      
                                       void * CM_NULLABLE outputCallbackRefCon,                                      
                                       void * CM_NULLABLE sourceFrameRefCon,                                      
                                       OSStatus status,                                      
                                       VTEncodeInfoFlags infoFlags,                                      
                                       CM_NULLABLE CMSampleBufferRef sampleBuffer)

参数sampleBuffer为已编码的视频图像数据结构。CMSampleBufferRef是一个容易让人误解的数据结构,它可以包含已压缩数据(CMBlockBuffer)或未压缩数据(CVPixelBuffer)及相关描述信息,如下图所示。

iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件

CMSampleBufferRef两种形态

1.2、读取CMSampleBufferRef的BlockBuffer数据

这里的CMSampleBufferRef装载了已压缩数据,无法直接让它生成图片。解码回调函数的CMSampleBufferRef因为装载了未压缩数据,才能创建CGImage或UIImage。为方便调试,我们可以将视频数据写成文件,用VLC等工具分析生成的内容。不写文件,直接推流也行,视具体业务而定。

1.2.1、访问BlockBuffer数据

从前面的图片可知,CMSampleBufferRef在其CMBlockBufferRef字段中存放实际已压缩图像数据,那么我们需要访问它。

CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t totalLength;
char *dataPointer;
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, NULL, &totalLength, &dataPointer);

dataPointer指向图像数据的起始位置,图像数据总长度为totalLength,示例如下。

iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件

已压缩的图像数据

1.2.2、读取图像数据

上图的开始数据是00 00 00 29 06 05 23 47 56,那么这段slice长度是前四个字节:00 00 00 29 => 0x29 = 41,以大端字节序表示。直接赋值给整型变量,则iOS以小端字节序读取,结果数值为687865856,显然与预期不符,需要转换。转换思路就是逐字节读取,数组或移位操作都可实现,甚至还能偷懒,调用Core Foundation的接口CFSwapInt32BigToHost。转换完,正确的数据是29 00 00 00,在Xcode中查看内存也是如此。有些反直觉,比如00 00 3E 70 => 0x3E70 = 15984 => 70 3E 00 00。然而,在计算器中输入70 3E得不到15984,输入0x3E70才是对的。

iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件

小端字节序表示图1

iOS VideoToolbox硬编H.265(HEVC)H.264(AVC):2 H264数据写入文件

小端字节序表示图2

有关大小端字节序、网络字节序与本地字节序问题,操作系统或计算机网络等课程有详细描述。

为方便阅读,重新排版上图数据。

01 02 03 04 05 // 列序
---------------
// slice 1(长度:0x29 = 41),SEI帧
00 00 00 29
06 05 23 47 56
4A DC 5C 4C 43
3F 94 EF C5 11
3C D1 43 A8 00
00 03 00 00 03
00 05 1D BC A9  
01 FF CC CC FF  
02 00 4C 4B 40
80
// slice 2(长度:0x3E70 = 15984),I帧
00 00 3E 70
25 B8 20 06 FF  
FF F8 68 48 A0
// ...

从上面可清晰看出分割slice的思路,按长度逐一读取NALU。

现在,回到写入文件这一主题。如果要按Annex-B格式写成文件,则需要将每个slice的长度替换成起始码(start code),追加写入。如果还用iOS解码,则直接给Video Toolbox解码即可。

2、序列参数集(Sequence Parameter Set, SPS)、图像参数集(Picture Parameter Set, PPS)

以下测试中的长度单位为字节(bytes)个数。

2.1、Profile、Level的编码输出

这里以iPhone 6p为例,图像大小为1080P,测试各个Profile、Level输出的数据变化。

2.1.1、Baseline

经测试,

  • kVTProfileLevel_H264_Baseline_1_3

  • kVTProfileLevel_H264_Baseline_3_0

  • kVTProfileLevel_H264_Baseline_3_1

都返回status = -12902,kVTParameterErr参数错误。可知,Video Toolbox可解码这几个规格的H.264压缩数据,但不支持编码1080P图像,因为超出Profile限制。

2.1.1.1、kVTProfileLevel_H264_Baseline_3_2

// SPS 长度 = 17

<27640028 ac56c078 0227e59b 81010152 04>
// PPS 长度 = 4
<28ee3cb0>
// SEI 长度 = 41
<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300028f 5c2801dd ccccdd02 004c4b40 80>// IDR 长度 = 738700 00 1C DB
25 B8 20 06// P帧 长度 = 496600 00 13 66 21 E1 08 46

2.1.1.2、kVTProfileLevel_H264_Baseline_4_0

// SPS 长度 = 17<27640028 ac56c078 0227e59b 81010152 04>// PPS 长度 = 4<28ee3cb0>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901dd ccccdd02 004c4b40 80>// IDR 长度 = 1256800 00 31 18 25 B8 10// P帧 长度 = 650300 00 19 67 21 E1 08 46

2.1.1.3、kVTProfileLevel_H264_Baseline_4_1

// SPS<27420029 ab403c01 13f2cdc0 8080a902>// PPS<28ce3c30>// SEI<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300028f 5c2801ff ccccff02 004c4b40 80>// IDR00 00 23 8E
25 B8 20 06// P帧00 00 0D 9C
21 E3 18 06

SPS比kVTProfileLevel_H264_Baseline_4_0少了末尾的80,一个字节。PPS长度不变,内容不同。

2.1.1.4、kVTProfileLevel_H264_Baseline_4_2

// SPS 长度 = 16<2742002a ab403c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ce3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901ff ccccff02 004c4b40 80>// IDR 长度 = 1201000 00 2E EA
25 B8 20 06// P帧 长度 = 1623000 00 3F 66 21 E1 08 0C

经多次测试,发现几乎每个P帧都比I帧大。

2.1.1.5、kVTProfileLevel_H264_Baseline_5_0

// SPS 长度 = 16<27420032 ab403c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ce3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300028f 5c2801ff ccccff02 004c4b40 80>// IDR 长度 = 767500 00 1D FB25 B8 20 06// P帧 长度 = 5238900 00 39 33 21 E1 08 0C

经多次测试,发现P帧有时比I帧大。

2.1.1.6、kVTProfileLevel_H264_Baseline_5_1

// SPS 长度 = 16<27420033 ab403c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ce3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901ff ccccff02 004c4b40 80>// IDR 长度 = 1162000 00 2D 64 25 B8 20 06// P帧 长度 = 1595600 00 3E 50 21 E1 08 0C

经多次测试,发现多数时候P帧都比I帧大。

2.1.1.7、kVTProfileLevel_H264_Baseline_5_2

// SPS 长度 = 16<27420034 ab403c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ce3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901ff ccccff02 004c4b40 80>// IDR 长度 = 1186000 00 2E 54 25 B8 20 06// P帧 长度 = 5397600 00 D2 D8
21 E2 10 04

经多次测试,发现多数时候P帧都比I帧大。

2.1.1.8、kVTProfileLevel_H264_Baseline_AutoLevel

// SPS 长度 = 16<27420028 ab403c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ce3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300028f 5c2801ff ccccff02 004c4b40 80>// IDR 长度 = 738300 00 1C D7
25 B8 20 06// P帧 长度 = 5473100 00 D5 C7
21 E2 10 04

2.1.2、Baseline总结

  • kVTProfileLevel_H264_Baseline_3_2

  • kVTProfileLevel_H264_Baseline_4_0

输出的SPS、PPS相同,SEI略有区别。

  • kVTProfileLevel_H264_Baseline_AutoLevel

2.2.1、Main

经测试,

  • kVTProfileLevel_H264_Main_3_0

  • kVTProfileLevel_H264_Main_3_1

  • kVTProfileLevel_H264_Main_3_2

都返回status = -12902,kVTParameterErr参数错误。可知,Video Toolbox可解码这几个规格的H.264压缩数据,但不支持编码1080P图像,因为超出Profile限制。

与Baseline不同的是,kVTProfileLevel_H264_Main_3_2不能编码1080P图像。

2.2.1.1、kVTProfileLevel_H264_Main_4_0

// SPS 长度 = 16<274d0028 ab603c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ee3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901dd ccccdd02 004c4b40 80>// IDR 长度 = 558700 00 15 D3
25 B8 20 06// P帧 长度 = 344000 00 0D 70 21 E1 08 46

多次测试,静止画面P帧平均长度为:
Pavg = (3440 + 3887 + 4326 + 626 + 846 + 956 + 522 + 262 + 149 + 91) / 10 = 15105

2.2.1.2、kVTProfileLevel_H264_Main_4_1

// SPS 长度 = 16<274d0029 ab603c01 13f2cdc0 8080a902>// PPS 长度 = 4<28ee3c30>// SEI 长度 = 41<06052347 564adc5c 4c433f94 efc5113c d143a800 00030000 0300051d bca901dd ccccdd02 004c4b40 80>// IDR 长度 = 390100 00 0F 3D
25 B8 20 06// P帧 长度 = 268400 00 0A 7C
21 E1 08 46

多次测试,静止画面P帧平均长度为:
Pavg = (2684 + 5435 + 322 + 3673 + 615 + 285 + 265 + 197 + 292 + 87) / 10 = 1385.5

2.2.1.3、kVTProfileLevel_H264_Main_4_2

2.2.1.4、kVTProfileLevel_H264_Main_5_0

// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧

2.2.2、Main总结

2.3.1、Hight

// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧
// SPS// PPS// SEI// IDR// P帧

2.3.2、Hight总结

讨论

问题:实时接收的H264数据写入文件时,是不是最开始是要写文件头?
分析:
可以直接写文件。写文件头是为了标注视频的一些识别信息,好方便标记。最好从接受到的第一个I帧写起,否则使用一些播放器等可能不能播放。