在这一篇记录h264码流annexb格式转封装到AVCC格式过程中sps pps变化的情况.

h264封装转换

转封装是流媒体服务基本的功能, h264码流从annexb格式转到AVCC格式通常的场景是:

  • ts to flv/rtmp
  • annexb raw h264 to flv/rtmp

这种情况下源流的sps pps发生变化时处理链逻辑应该是向输出的flv码流中插入一个sequence header类型的tag.

ffmpeg转封装

使用ffmpeg处理转封装是常见的手段,ffmpeg处理转封装流程如下:

  1. 使用接口打开输入源文件
  2. 使用接口打开输出文件
  3. av_read_frame接口读取一个AVPacket
  4. 输出AVPacket

ffmpeg很好的做了中间层抽象,通常情况它都能工作的很好,比如

1
ffmpeg -i xx.ts -c copy -f rtmp://xxxx/xx/xx

当ts源流sps pps发生变化时,它也能正确向flv流中插入sequence header型的tag.

但是实际使用场景中,很多时候我们并不是在每个环节都使用ffmpeg去处理流媒体数据,有些时候我们用它读取源数据,解封装, 然后送入自己的处理链条中,有些情况我们用它做封装功能,将我们自己程序链条中的h264比特流数据封装到各种容器中.一个典型的 例子是obs,它很多地方使用了ffmpeg,解封装,编解码,封装,大部分都是将ffmpeg处理过的数据对接他自己的处理链条.

来看一个实际使用过程的例子,场景是将一串annexb格式的h264码流推送到rtmp服务器上,h264码流已经按照一个一个NAL unit分离,现在使用ffmpeg 将它推送到rtmp服务器上.

使用方式是:

1
2
3
4
5
6
int ret = av_new_packet(&packet, len);

packet.data = data;
packet.size = len;
packet.dts = dts;
packet.pts = pts;

构造一个AVPacket对象,用单个NAL unit数据填充它,设置它的各种属性,后续转封装过程就是使用典型的ffmpeg接口打开目的文件,将AVPacket 写出.这样做在一些情况下能很好工作,但是有坑,或者说做法是错误的.碰到以下两种情况就不行了:

  1. sps pps发生变化
  2. h264编码方式是multi slice

第二种方式是一帧完整的视频h264视频数据被编码输出为多个slice, 也就是多个NAL unit组成,而ffmpeg的AVPacket代表视频帧时是完整的一帧视频,如果 按照NAL unit拆分送入AVPacket,那么封装后的flv tag将不是完整帧,很多客户端都无法处理这种码流,解决方式是拆分annexb码流时需要组为完整一帧 再封装为flv tag.这里不再赘述了.

来看第一种情况,h264的sps pps变化了,如果rtmp服务收到的数据流就有些不标准了. 变化后的sps pps已经写到 AVPacket中了,抓包调试可以看到ffmpeg封装的flv tag包中AVCPacketType类型为1, 显然类型不太正确,正确的类型应该是0. 因为处理码流时没有探测到sps pps变化的情况,来看转封装写入逻辑libavformat/flvenc.c中的flv_write_packet函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264
         || par->codec_id == AV_CODEC_ID_MPEG4) {
     size_t side_size;
     uint8_t *side = av_packet_get_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA, &side_size); type: AV
     if (side && side_size > 0 && (side_size != par->extradata_size || memcmp(side, par->extradata,   side_size))) { s1: side s2: par->extradata n: side_size
         ret = ff_alloc_extradata(par, side_size); size: side_size
         if (ret < 0)
             return ret;
         memcpy(par->extradata, side, side_size); dest: par->extradata src: side n: side_size
         flv_write_codec_header(s, par, pkt->dts); ts: pkt->dts
     }
 }

在前一篇h264码流sps pps发生变化处理相关(1)中我们提到的AVPacketSideData又出现了,如果通过ffmpeg读取接口读取AVPacket,它会在sps pps变化时插入AVPacketSideData.而我们自己构造的AVPacket没有插入这个属性.

这种情况下的解决方案也就不言自明了,需要自己在输入码流的sps pps变化时插入AVPacketSideData属性,后续转封装为flv就会插入正确的sequence header类型的tag了.

总结

h264码流中sps pps变化的场景, 需要小心应对,使用一些工具处理或者我们自己开发时要确认一下是否能兼容这种情况.