这一段从三光吊舱接收数据时,因为对方外协了一个转换盒子,将同步422出来的h264编码的数据转成了RTP进行发送。我们能拿到的就是这个RTP数据。使用wireshark抓了一堆包,然后使用rtp_h264_extractor插件将里面的h264码流dump出来了,并使用ffplay进行播放,没问题。
但是我前面使用libjrtplib3监听了转换盒子发送出来的rtp数据流。然后使用getPayloadData()将所有的h264负载保存了下来。但是播放不了。对比了一下发现。我保存下来的rtp封包的h264数据。准确来说,就是对于单Nalu帧,它把前面的00 00 00 01这个start code给去掉了;对于FU-A这种分片帧,它把帧头的 00 00 00 01这个start code去掉了,同时把原来的一个字节的nalu_header_type给换成了2个字节,分别是FU indicator和FU header。这里需要把它们还原回去。至于怎么还原,可以看我昨天写的博文。
还原回去之后,发现还是不能播放。又去对比我转换后的文件和wireshark插件拖出来的h264数据。发现还少了SPS和PPS以及SEI帧。这些帧都在IDR帧之前。SPS里面存储了序列参数集合,而PPS存储了图像参数集合。没有这些参数,是无法解析h264的。
上图中,
- 00 00 00 01是NALU头,是序列的标识符的开头;
- 0x27转成二进制是100111,00111转成十进制是7,那么7对应NALU type=sps;
- 0x28转成二进制是101000,转成十进制是8,8对应NALU type=pps;
- 0x25转成二进制是100101,00101转成十进制是5,5对应的NALU type=IDR帧;
使用FFMPEG解码时,sps和pps是保存在AVCodecContext的extradata.data中,在解码提取sps和pps时,判断NALU type可以用extradata.data[ 4 ]&0x1f(结果是7是sps,8是pps,计算方式是先转成二进制,0x27&0x1f=11111&00111=00111=7,pps计算类似);
NALU type=1是splice,splice有三种编码模式,I_slice、P_slice、B_slice,I帧在编码时就分割保存在splice中。
-
NALU type值对应表如下:
实际上是因为我前面写的程序有问题。我是基于已经落盘的payload构成的数据集进行测试的(因为吊舱被拉走了,我没法直接测了)。
通过分析和查找资料。我确认了IDR是必须的。IDR一定是I帧,但是I帧不一定是IDR帧。这两东西在编码中是一样的。只是作用不同,所以起了个不同的名字罢了。
IDR是强制刷新帧,也就是说只要解码器碰到了IDR帧,就会把它前面保留的解码参考序列清空,后续的解码以此IDR帧为参考帧。这也是为什么对于没有IDR帧的视频无法随意拖动播放,而又IDR帧的视频可以随意拖动播放的原因。因为有IDR帧的,在拖动后,会从最近的地方找到一个IDR,然后解码后面的图像。没有IDR的,则需要依赖前面的I帧或者P帧,拖动后,无法找到前面的I帧或者P帧。
而SPS和PPS也是一个图像序列必须的。在我们wireshark得到的序列中,就存在很多的SPS和PPS帧,都在IDR之前。甚至还有SEI帧,也在IDR之前。但是这些并不是都需要的。在文件开始的时候只要有一套SPS、PPS、[SEI]、IDR、序列即可对整个h264序列解码(如果中间图像分辨率、大小之类的都没有变化的情况下),h264序列中的IDR前面没有SPS、PPS、[SEI]也是可以的。不过如果在使用过程中如果分辨率等发生了变化,则SPS\PPS是必须保留的。
后续的代码改进的地方是在getPayloadData()之后,
1. 判断是单NAL还是FU类型帧;
2. 如果是单NAL帧,则判断
2.1 当前帧k是不是SPS帧,k+1帧是不是PPS,k+2帧是不是IDR帧或者SEI帧;如果k+2帧是SEI,则k+3是不是IDR帧;如果都满足,则将它们都存下来(一般来说,SPS、PPS、SEI都非常小,在一个MTU中足以容纳;对于这种较小的情况,对于SPS,PPS,SEI直接在前面加上00 00 00 01,然后保存;对于IDR帧,则根据它是单NAL还是FU来判断如何处理);
2.2 如果是IDR帧或者P帧,则在前面直接加上00 00 00 01然后存储(一般IDR帧较大,所以这种情况出现的概率非常低);
3. 如果不是单NAL帧,而是FU-A帧
3.1 I和P帧的处理方式相同,首先找到首包,也就是FU header的最左位为1,然后计算fu indicator和fu header得到nal_header_type这个1个字节,然后使用00 00 00 01 nal_header_type加上后续数据构成首包,放到缓冲区;
3.2 接收后续的中间包(fu header的左边两位都是0),判断和前面的那个包类型是否一致(都是I或者P),然后将fu indicator和fu header两个字节删掉,追加到上述缓冲区后面;
3.3 接收到尾包后(fu header的左边第二位为1),说明该帧的数据接收完了,将该包的前fu indicator和fu header去掉,剩余部分追加到上述缓冲区。
4. 将上述完成的帧写入文件。
上述过程是指的写入文件的过程。如果是使用ffmpeg解码,则也是将相关完整帧序列传给ffmpeg进行解码和播放。
这里没有讨论聚合包和FU-B的情况。
如果有不正确的地方,请读者联系我指出来。