H264 demux后AVPacket送去decode时出错


本人刚刚接触多媒体开发。最近遇到一个棘手的问题,某些MP4文件无法在我们的系统(类linux)上播放。
今天写下解决心路历程。代码上以逻辑分析为主,毕竟公司代码不好直接复制。
等不及看结论的朋友。请直接到结论部分看了以后,再想回过头来看的,烦劳费神了。轻喷,刚刚接触多媒体,发发心路历程。小儿科的话。就不要喷我了。

背景

问题背景:前面已经提到了,某些MP4文件无法播放。大部分MP4可以正常播放。
技术背景:刚刚接触,整个代码流程梳理了下,我们采用的是ffmpeg进行的a/v demuxer分流后将ffmpeg分流得到的video一路AVPacket*传入到专门用Gstreamer硬解码的(相信对于不用Gst硬解的方案,也有其他的硬解,比如类似android openMax,windows的什么的,原理类似,都是分流后将AVPacket的data传入)。

心路历程
第一步 复现

下载测试提到的几个问题视频,同时准备1-2个正常能播放的视频。
抓取日志。搜索一些多媒体的tag,搜索一些error,warn之类,不多表述了。日志在decode这部分有提示WARNING(get bad buff)。对应代码上,看到了buffer的转换看到了对于某个指针的拷贝,出现了拷贝长度与目标之间的不匹配导致了buffer无法传递。问题点是:mp4toannexB(uint8_t *codecBuf, uint8_t *sourceBuf, const int32_t length)这么个方法打印出来的。

第二步 深入代码逻辑

那么,仔细分析代码逻辑,知道了从AVDemuxer分流以后,在decode这个类中,需要将分出来的AVPacket的data进行一下转换。

  • 第一次
    AVPacket给过来的时候,似乎有干点什么事情(只在第一次,并只干一次的事),提取了点什么东西(后面就讲到),memcpy给了新的bufferCurrent,再行对AVPacket的data进行mp4toannexB处理后,拼接进去。最后才把bufferCurrent送到decode去;
  • 第2+次
    就是将AVPacket的data使用mp4toannexB(uint8_t *codecBuf, uint8_t *sourceBuf, const int32_t length)这个转换的方法,就干了一件事情就是将前面的4个字节替换为0x00 00 00 01.(与第一步的后半部分一致)。
    这里有2个疑问?为什么第一次有一个变量控制,只干一次的事情?为什么有要加AVPacket的data进行替换4个字节0x00 00 00 01后再传给decode?
第三步 上网

https://blog.csdn.net/leixiaohua1020/article/details/11800877
https://blog.csdn.net/qingkongyeyue/article/details/54023323
第一篇是leixiaohua的,大家都知道国内算是最早最专业了的吧,可惜天妒英才,哎。第二篇是基于他讲的丰富了下更加详细,能更清晰的理解为何要这么做,而且也有替代方案。题外话,第二篇是我几乎快理解H264的NALU结构的时候,才找到,正好印证了无所谓decode是硬解还是其他的软件方案的想法。
从帖子中可以看出回答了我前面的2个问题。
但是为什么是这样呢???
于是开始学习H264的基本原理。下面内容是花了2天整理出来的。于是理解了帖子和代码上的,
1. 第一次需要拼接pps和sps到前面;
2. AVPacket对应的就是一个NALU,或多个NALU,每段NALU的开头需要转换成start code(0x00 00 00 01)送给decode才行。

第四步 尝试

结合代码逻辑提示的buffer bad日志,再结合学习的2-3天的原理。开始分析自己的实际问题。
显然我的代码已经正确的进行处理,为什么会有的MP4无法播放呢?仔细查看提示buff bad的地方,进行的前面4字节替换,估计是出现了问题。
于是编写了一个printHex函数。这里也记录下来,以备后续分析日志使用。

	//使用实例:
//printHex(buff, sizeof(buff)/sizeof(*buff), 50)
//也可以将char*改成uint8_t* 根据你的需要
static void printHex(char* buff, int buff_len, int max_print_len)
{
    int siz = 2*max_print_len + 1 + max_print_len / 3;
    char str[siz];
    bzero(str, siz);
    int ret = 0;
    int ret_offset = 0;
    for (int i = 0; i < buff_len; i++) {
        if (i % 4 == 0 && i > 0) {
            ret += sprintf(str + ret, "%c", ',');
            ret_offset++;
        }
        ret += sprintf(str + ret, "%02x", buff[i]);
        if (ret == max_print_len + ret_offset) {
            break;
        }
    }
    printf("printHex:%s\n", str);
}

通过对比正常视频和出问题的视频的日志打印,发现经过MP4toannexB的处理以后,对于问题视频打印出来的二进制,前面的字节替换是有问题的。最后才发现根本原因在nal length size上。

结论

nal length size引起的问题

查了很多帖子都没有,这或许是全网第一次,遇到nal length size不对导致的无法播放的第一例吧。

通过ffprobe -show_streams xxx可以看到问题视频 nal length size 是2(0xfD), 正常的视频是4(0xff)。查看关注二进制章节。这个就是问题的根本原因点。

这部分在网上有介绍nal length size是什么,但是作用是什么?

NALULengthSizeMinusOne, 即nal length size,用几个字节的来存储NALU的长度。什么意思呢?如果是1个字节的长度,就是255字节,太小,不太适用;2个字节长度,就是64k字节;3字节,没有广泛支持;4字节,适用最多。

意思就是AVPacket的data的开头只有nal length size个字节,表示接下来的长度。如果是4的话,本身这个开头就是4个字节;而如果是2(0xFD)的话,就是用2个字节表示长度了,即第三位就是真实的data了。

因此解决方案是:
我们不能像leixiaohua原帖那样(网上的帖子都是基于这个帖子的发散,甚至传播到了国外网站我都找到了)直接复写前四个字节了。而应该先给整段buff增加2个字节,先赋值0x00 00 00 01再行将data拷贝到buffCurrent后面去!
这一步我已经提前了2天发现了。但是仍然有bug。

但是为何又拖了2天呢。因为+2个字节的动作又问题了小插曲。

第2+个AVPacket这样处理就好了。但是,第一个AVPacket不对。我们printHex来看,它的开头事实上有0x0019(不一定是这个)表示长度,而整段buffer,9000+个字节,显然不对。又查询了相关,知道第一个AVPacket包含2个NALU: 即第一段的0x19长的SEI和和9000+的IDR。另外还需要给它拼接pps和sps。

因此,需要补的字节是每次出现一个长度(即出现AVPacket的NALU header)就多增加2个字节。注意一个AVPacket可能存在NALU多个。

至此,解决。

至于为什么原贴等网上都不用这样处理呢。我推测完网上大部分是ffmpeg全套,是因为他们的AVPacket其实是送给了ffmpeg做的av_xxx_decode()去解码的。那么,ffmpeg内部估计做了这种处理。但本文章,可以给其他的gstreamer或者openMax等等可能有参考意义。

文章提到的名词如PPS,SEI等,和H264相关,都总结在后面。再见咯。

附录 H264 小白基础知识

知道了的人可以跳过本章节。不知道的新手,可以有所帮助。这是我总结了多篇博客,以及翻阅H264 spec pdf总结的部分内容。
名词:

NAL :   network abstraction layer(网络抽象层)

VCL:     visual codec layer? 视频编码层

NALU:     network abstraction layer units 每个NALU包可以被单独的解析和处理,一帧数据被分割为多个NALU

AU:      Access Units 包含了一个完整的帧,1个或者多个NALU组成。

VCL:  NALU的格式类型,  Video Coding Layer packets contain the actual visual information.  即视频编码后的数据

non-VCL: NALU的格式类型,contain metadata that may or may not be required to decode the video.  非视频数据,配置信息
简述

在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。因此我们平时的每帧数据就是一个NAL单元(SPS与PPS除外)。在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符,一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧……

NALU格式

NALU的格式类型,主要分2类VCL和non-VCL,共19种:

0      Unspecified                                                    non-VCL
1      Coded slice of a non-IDR picture                               VCL
2      Coded slice data partition A                                   VCL
3      Coded slice data partition B                                   VCL
4      Coded slice data partition C                                   VCL
5      Coded slice of an IDR picture                                  VCL
6      Supplemental enhancement information (SEI)                     non-VCL
7      Sequence parameter set !!!                                     non-VCL
8      Picture parameter set  !!!                                     non-VCL
9      Access unit delimiter                                          non-VCL
10     End of sequence                                                non-VCL
11     End of stream                                                  non-VCL
12     Filler data                                                    non-VCL
13     Sequence parameter set extension                               non-VCL
14     Prefix NAL unit                                                non-VCL
15     Subset sequence parameter set                                  non-VCL
16     Depth parameter set                                            non-VCL
17..18 Reserved                                                       non-VCL
19     Coded slice of an auxiliary coded picture without partitioning non-VCL
20     Coded slice extension                                          non-VCL
21     Coded slice extension for depth view components                non-VCL
22..23 Reserved                                                       non-VCL
24..31 Unspecified                                                    non-VCL

SPS(Sequence Parameter Set, 序列参数集)。此non-VCL NALU包含配置解码器所需的信息,例如profile, level, resolution, frame rate(配置文件、级别、分辨率、帧速率)。

PPS(图片参数集, Picture Parameter Set)。与SPS类似,这种non-VCL包含熵编码模式、切片组、运动预测和解块滤波器的信息(entropy coding mode, slice groups, motion prediction and deblocking filters)。

IDR(即时解码器刷新, Instantaneous Decoder Refresh)。这个VCL-NALU是一个独立的图像切片(slice)。也就是说,一个IDR可以被解码和显示,而无需参考任何其他NALU SPS和PPS。

AUD(访问单元分隔符,Access Unit Delimiter)。AUD是可选的NALU,可用于在基本流中定界帧。它不是必需的(除非容器/协议(如TS)另有说明),并且通常不是为了节省空间而包含的,但是在不必完全解析每个NALU的情况下查找帧的开头是有用的。

两种封装格式
1. annex-B格式

别名H264, packet-tansport Protocol

  • 适用
    用于实时的流格式,比如说传输流,通过无线传输的广播、DVD等。在这些格式中通常会周期性的重复SPS和PPS包,经常是在每一个关键帧之前,因此据此可以建立解码器一个随机访问的点,这样就可以加入一个正在进行的流,及播放一个已经在传输的流。
  • NALU start code

这是annex-Bz中定义的,另一种H264格式(AVCC)没有的。

一个NALU包中的数据并不包含它的大小(长度)信息,因此不能简单的连接NALU包来建立一个流,因为你不知道一个包从哪里结束,另一个包从哪里开始。
Annex B格式用开始码来解决这个问题,即给每个NALU加上前缀码:2个或者3个0x00,后面再加一个0x01, 如:0x000001或者0x00000001。
4字节类型的开始码在在连续的数据传输中非常有用,因为用字节来对齐、分割流数据,比如:用连续的31个bit0后接一个bit1来分割流数据,是很容易的。如果接下来的bit是0(因为每个NALU都以bit0开始),那么这就是一个NALU包数据的起始位置了。4字节类型的开始码通常只用于标识流中的随机访问点,如SPS PPS AUD和IDR,然后其他地方都用3字节类型的开始码以减少数据量。简而言之,4字节的起始码0x00000001通常标志流的随机访问点SPS, PPS, AUD,IDR,其他NALU使用3字节的起始码(比如打开某个直播或电视节目,我们就开始从数据中检索00 00 00 01拿到一些关键信息,就可以开始工作了)。

那么,它是如何起作用的呢?是因为:

  • Emulation Prevention Bytes, 防竞争字节

开始码能起作用是因为3字节的序列0x000000,0x000001,0x000002和0x000003(应该是所有的0x0000**)在non-VCL(原文是non-RBSP,译者修改)NALU包中是非法的,所以在构建ANLU包时,必须确保排除这些数值序列,这是由向每个这种类型的序列插入防竞争字节0x03实现的,那么插入防竞争字节后,0x000001变成了0x00000301。
当解码的时候,查找和去除防竞争字节非常重要。因为防竞争字节可能出现在NALU包的任意位置,在文档中通常更方便的做法是假定它们已经被去除了,Raw Byte Sequence Payload原始字节序列负载 (RBSP)表示没有防竞争字节的数据序列(包)。

  • 例子
    0x0000 | 00 00 00 01 67 64 00 0A AC 72 84 44 26 84 00 00
    0x0010 | 03 00 04 00 00 03 00 CA 3C 48 96 11 80 00 00 00
    0x0020 | 01 68 E8 43 8F 13 21 30 00 00 01 65 88 81 00 05
    0x0030 | 4E 7F 87 DF 61 A5 8B 95 EE A4 E9 38 B7 6A 30 6A
    0x0040 | 71 B9 55 60 0B 76 2E B5 0E E4 80 59 27 B8 67 A9
    0x0050 | 63 37 5E 82 20 55 FB E4 6A E9 37 35 72 E2 22 91
    0x0060 | 9E 4D FF 60 86 CE 7E 42 B7 95 CE 2A E1 26 BE 87
    0x0070 | 73 84 26 BA 16 36 F4 E6 9F 17 DA D8 64 75 54 B1
    0x0080 | F3 45 0C 0B 3C 74 B3 9D BC EB 53 73 87 C3 0E 62
    0x0090 | 47 48 62 CA 59 EB 86 3F 3A FA 86 B5 BF A8 6D 06
    0x00A0 | 16 50 82 C4 CE 62 9E 4E E6 4C C7 30 3E DE A1 0B
    0x00B0 | D8 83 0B B6 B8 28 BC A9 EB 77 43 FC 7A 17 94 85
    0x00C0 | 21 CA 37 6B 30 95 B5 46 77 30 60 B7 12 D6 8C C5
    0x00D0 | 54 85 29 D8 69 A9 6F 12 4E 71 DF E3 E2 B1 6B 6B
    0x00E0 | BF 9F FB 2E 57 30 A9 69 76 C4 46 A2 DF FA 91 D9
    0x00F0 | 50 74 55 1D 49 04 5A 1C D6 86 68 7C B6 61 48 6C
    0x0100 | 96 E6 12 4C 27 AD BA C7 51 99 8E D0 F0 ED 8E F6
    0x0110 | 65 79 79 A6 12 A1 95 DB C8 AE E3 B6 35 E6 8D BC
    0x0120 | 48 A3 7F AF 4A 28 8A 53 E2 7E 68 08 9F 67 77 98
    0x0130 | 52 DB 50 84 D6 5E 25 E1 4A 99 58 34 C7 11 D6 43
    0x0140 | FF C4 FD 9A 44 16 D1 B2 FB 02 DB A1 89 69 34 C2
    0x0150 | 32 55 98 F9 9B B2 31 3F 49 59 0C 06 8C DB A5 B2
    0x0160 | 9D 7E 12 2F D0 87 94 44 E4 0A 76 EF 99 2D 91 18
    0x0170 | 39 50 3B 29 3B F5 2C 97 73 48 91 83 B0 A6 F3 4B
    0x0180 | 70 2F 1C 8F 3B 78 23 C6 AA 86 46 43 1D D7 2A 23
    0x0190 | 5E 2C D9 48 0A F5 F5 2C D1 FB 3F F0 4B 78 37 E9
    0x01A0 | 45 DD 72 CF 80 35 C3 95 07 F3 D9 06 E5 4A 58 76
    0x01B0 | 03 6C 81 20 62 45 65 44 73 BC FE C1 9F 31 E5 DB
    0x01C0 | 89 5C 6B 79 D8 68 90 D7 26 A8 A1 88 86 81 DC 9A
    0x01D0 | 4F 40 A5 23 C7 DE BE 6F 76 AB 79 16 51 21 67 83
    0x01E0 | 2E F3 D6 27 1A 42 C2 94 D1 5D 6C DB 4A 7A E2 CB
    0x01F0 | 0B B0 68 0B BE 19 59 00 50 FC C0 BD 9D F5 F5 F8
    0x0200 | A8 17 19 D6 B3 E9 74 BA 50 E5 2C 45 7B F9 93 EA
    0x0210 | 5A F9 A9 30 B1 6F 5B 36 24 1E 8D 55 57 F4 CC 67
    0x0220 | B2 65 6A A9 36 26 D0 06 B8 E2 E3 73 8B D1 C0 1C
    0x0230 | 52 15 CA B5 AC 60 3E 36 42 F1 2C BD 99 77 AB A8
    0x0240 | A9 A4 8E 9C 8B 84 DE 73 F0 91 29 97 AE DB AF D6
    0x0250 | F8 5E 9B 86 B3 B3 03 B3 AC 75 6F A6 11 69 2F 3D
    0x0260 | 3A CE FA 53 86 60 95 6C BB C5 4E F3

这是一个完整的访问单元(AU),包括3个NALU包,如你所见,数据序列以开始码开始,后面接了一个SPS(SPS 以0x67开始),在SPS中,你可以看到有2个防竞争字节。没有这些字节那么非法的数据序列就会出现在这些位置。然后可以看到一个开始码后面接着一个PPS(PPS 以0x68开始),然后是一个最后的开始码,后面跟着一个IDR包。这是一个完整的H.264流,如果你把这些数据以16进制的方式保存到一个以.264为后缀名的文件中,可以把这些数据转换成一张图片(略不展示了)。

2. avcC格式

别名,avcc,AVC1, byte-stream format。不带开始码start code。

  • 适用
    优点是在开始配置解码器的时候可以跳到流的中间播放,这种格式通常用于可以被随机访问的多媒体数据(即方便seek),如存储在硬盘的文件。也因为这个特性,MP4、MKV通常用AVCC格式来存储。适合视频文件的存储。
    虽然AVCC格式不使用起始码,防竞争字节还是有的。
  • extradata
    别名:头,sequence header。
    这个头给每个NALU包的前面,都加上了指定其长度的前缀。这种模式比较便于解析,但去除了另外一种格式(annex-B)字节对齐的特性,前缀可以是1,2,4(NAL Unit Size)。基本格式如下:
    bits
    8 version ( always 0x01 )
    8 avc profile ( sps[0][1] )
    8 avc compatibility ( sps[0][2] )
    8 avc level ( sps[0][3] )
    6 reserved ( all bits on )
    2 NALULengthSizeMinusOne //这个值是(前缀长度-1),值如果是3,那前缀就是4,因为4-1=3
    3 reserved ( all bits on )
    5 number of SPS NALUs (usually 1) repeated once per SPS:
    16 SPS size variable SPS NALU data
    8 number of PPS NALUs (usually 1) repeated once per PPS
    16 PPS size variable PPS NALU data
    使用上面的例子,那么AVCC extradata看起来像是这样的:
    0x0000 | 01 64 00 0A FF E1 00 19 67 64 00 0A AC 72 84 44
    0x0010 | 26 84 00 00 03 00 04 00 00 03 00 CA 3C 48 96 11
    0x0020 | 80 01 07 68 E8 43 8F 13 21 30
    你会发现SPS和PPS被存储在了非NALU包中(out of band带外),即独立于基本流数据。这些数据的存储和传输是文件容器的任务,比如MP4Box的,mdia-> minf -> …->avcC, 我们可以看到(这是我自己的视频文件,这里保留了avcC字节)。
        61 76 63 43 01 42 00 0B   FD E1 00 0E 67 42 00 0B
        BB 40 A0 FD 80 80 F0 80   42 A0 01 00 05 68 CE 0C
        6C 80 00 00 00 14 62 74   72 74 00 00 29 C0
    
  • nal length size
    比如在上图中MP4box中mdia-> minf -> …->avcC,后的第5位。另名,NALULengthSizeMinusOne, 即,用几个字节的来存储NALU的长度。什么意思呢?如果是1个字节的长度,就是255字节,太小,不太适用;2个字节长度,就是64k字节;3字节,没有广泛支持;4字节,适用最多。
主要关注的二进制
  • 对于网络流即annexB(TODO,或者视频解码中的ffmpeg解码后还需要再转annexB的步骤也会变成这样):
  1. 起始: 0x000001或者 0x00000001表示NALU的开始码(annexB才有);

紧随开始码之后的叫做profile_idc,表示类型,即如下几个常用:

  1. 第五位:0x06 : SEI类型。补充增强信息(SEI)。
    接着,第六位:0x05:SEI payload type,user_data_unregistered(),自定义消息
    接着,第七位:0x?? : 表示SEI payload size, 比如说15就是,21个字节。
    接着的16个字节:表示uuid。
  2. 第五位: 0x67 : SPS类型。
  3. 第五位: 0x68: PPS类型。
  4. 第五位: 0x65: IDR类型。 对应IDR图像中的片(I帧),但是注意IDR是I帧,但不代表所有I帧是IDR。IDR是为了防止错误的传播。
  5. 第五位: 0x41: 【不分区、非IDR图像的片】,在baseline的档次中就是P帧,因为baseline没有B帧。
    紧接着0x41后面的一个字节第六位:
    0xe0,或者0xe1或者0xe2,与上0x80:
  6. 第五位:NALU类型 & 31 = 5 即为I帧,比如0x65
  • 对于本地视频文件流
  1. 0x61 76 63 43 :即avcC。先从box中分析,找到,avcC或者H265的hvcC(TODO还没有学习。
  2. 再接着avcC后面第五位个字节,代表nal length size:看前面avcC的extradata章节描述,前6bit on,后2bit配置。结合起来:
    0—表NALU长度的字节数为1字节----------------------------0xFC
    1—表NALU长度的字节数为2字节----------------------------0xFD -----小部分视频为2
    2—表NALU长度的字节数为3字节----------------------------0xFE
    3—表NALU长度的字节数为4字节----------------------------0xFF ------4比较常用
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 T113 平台上使用 Buildroot 并开启 H.264 插件,您可以按照以下步骤进行操作: 1. 进入 Buildroot 的配置界面: ``` make rockchip_tinkerboard_defconfig make menuconfig ``` 如果您使用的是自定义的配置文件,则直接执行 `make menuconfig` 命令。 2. 找到 "Target packages" -> "Audio and video applications",打开 Audio and video applications 的支持。 3. 找到 "Target packages" -> "Audio and video libraries",打开 Audio and video libraries 的支持。 4. 找到 "Target packages" -> "gstreamer1.0", 打开 gstreamer1.0 的支持。 5. 找到 "Target packages" -> "gstreamer1.0-plugins-base", 打开 gstreamer1.0-plugins-base 的支持。 6. 找到 "Target packages" -> "gstreamer1.0-plugins-good", 打开 gstreamer1.0-plugins-good 的支持。 7. 找到 "Target packages" -> "gstreamer1.0-plugins-bad", 打开 gstreamer1.0-plugins-bad 的支持。 8. 找到 "Target packages" -> "rockchip-gstreamer", 打开 rockchip-gstreamer 的支持。 9. 找到 "Target packages" -> "libva-rockchip", 打开 libva-rockchip 的支持。 10. 找到 "Target packages" -> "libvdpau-rockchip", 打开 libvdpau-rockchip 的支持。 11. 重新编译 Buildroot: ``` make ``` 12. 在 T113 平台上安装 GStreamer 相关的软件包: ``` sudo apt-get install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly ``` 13. 使用 `gst-launch` 命令播放 H.264 格式的视频: ``` gst-launch-1.0 filesrc location=/path/to/video.mp4 ! qtdemux name=demux \ demux.video_0 ! h264parse ! mppvideodec ! rkximagesink ``` 其中,/path/to/video.mp4 是待播放的 MP4 视频文件路径。 希望这能帮助您解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值