阅读 313

ffmpeg使用bsf后码流从avcc格式变成annex-b造成硬解异常

ffmpeg使用bsf后码流从avcc格式变成annex-b造成硬解异常

问题的产生

  • 第一个ffmpeg拷贝(copy)第三方的流到源站,第二个ffmpeg进程的源流为第一个ffmpeg的输出,并使用bsf添加sei到码流中,偶发硬解无法播放的情况。

    • 重启第二个ffmpeg可以恢复播放。

    • 不使用bsf问题不复现。

    • 使用bsf偶发无法播放的情况(可疑点)。

排查过程

  • dump流

    • curl "http://domain/xxx/xxx.flv" > xx.flv

  • 1.dump出问题的流发现源流从avcc变成annex-b格式(因为源流是avcc只是用了ffmpeg copy不应该发生格式变化),如图:(hexdump -C xx.flv | more 查看码流二进制)

  • image.png

如何查看二进制

  • 先了解flv中video tag封装格式,如下图:

image.png

  • 结合下边二进制分析

    • 0:AVC sequence header

    • 1: NALU

    • 2:AVC END sequence

    • FrameType=1代表关键帧

    • CodeID=7 代表AVC

    • AVCPacketType

image.png

  • AVC sequence header (也叫extra_data或者AVCDecoderConfigurationRecord)

image.png

  • 结合上边的二进制和AVC sequence header数据格式分析

    • AVC sequence header总共54个字节

    • sps数据为39个字节

    • pps数据为4个字节

  • 结合上边的二进制分析可以看到avc sequence header以AVCDecoderConfigurationRecord格式组织的数据,nalu又以start_code格式分割,也就是annex-b。

  • 其实已经发现问题,如果是annex-b格式的话其实nalu都是以start_code分割的(sps,pps也是nalu),而avcc才是通过AVCDecoderConfigurationRecord格式把sps,pps发送到服务端的,并且数据以NALU Length + NALU Data的方式来组织。上边的码流avc sequence header以AVCDecoderConfigurationRecord格式组织sps pps 而视频数据又是以start_code分割的有明显的问题。

复现问题

  • 先不考虑avc sequence header的问题,出问题dump下来的流nalu以start_code组织数据,不加bsf一直没问题,怀疑bsf有可能将avcc流转成annex-b。

ffmpeg加bsf正常的情况

  • 分析ffmpeg源码关于bsf的代码,果然bsf会将源流转换成annex-b代码如下

static int cbs_h2645_assemble_fragment(CodedBitstreamContext *ctx,                                        CodedBitstreamFragment *frag) {     uint8_t *data;     size_t max_size, dp, sp;     int err, i, zero_run;     for (i = 0; i < frag->nb_units; i++) {         // Data should already all have been written when we get here.         av_assert0(frag->units[i].data);     }     max_size = 0;     for (i = 0; i < frag->nb_units; i++) {         // Start code + content with worst-case emulation prevention.         max_size += 3 + frag->units[i].data_size * 3 / 2;     }     data = av_malloc(max_size + AV_INPUT_BUFFER_PADDING_SIZE);     if (!data)         return AVERROR(ENOMEM);     dp = 0;     for (i = 0; i < frag->nb_units; i++) {         CodedBitstreamUnit *unit = &frag->units[i];         if (unit->data_bit_padding > 0) {             if (i < frag->nb_units - 1)                 av_log(ctx->log_ctx, AV_LOG_WARNING, "Probably invalid "                        "unaligned padding on non-final NAL unit.\n");             else                 frag->data_bit_padding = unit->data_bit_padding;         }         ```以下代码以start_code的方式组织码率```         if ((ctx->codec->codec_id == AV_CODEC_ID_H264 &&              (unit->type == H264_NAL_SPS ||               unit->type == H264_NAL_PPS)) ||             (ctx->codec->codec_id == AV_CODEC_ID_HEVC &&              (unit->type == HEVC_NAL_VPS ||               unit->type == HEVC_NAL_SPS ||               unit->type == HEVC_NAL_PPS)) ||             i == 0 /* (Assume this is the start of an access unit.) */) {             // zero_byte             data[dp++] = 0;         }         // start_code_prefix_one_3bytes         data[dp++] = 0;         data[dp++] = 0;         data[dp++] = 1;         zero_run = 0;         for (sp = 0; sp < unit->data_size; sp++) {             if (zero_run < 2) {                 if (unit->data[sp] == 0)                     ++zero_run;                 else                     zero_run = 0;             } else {                 if ((unit->data[sp] & ~3) == 0) {                     // emulation_prevention_three_byte                     data[dp++] = 3;                 }                 zero_run = unit->data[sp] == 0;             }             data[dp++] = unit->data[sp];         }     }     av_assert0(dp <= max_size);     err = av_reallocp(&data, dp + AV_INPUT_BUFFER_PADDING_SIZE);     if (err)         return err;     memset(data + dp, 0, AV_INPUT_BUFFER_PADDING_SIZE);     frag->data_ref = av_buffer_create(data, dp + AV_INPUT_BUFFER_PADDING_SIZE,                                       NULL, NULL, 0);     if (!frag->data_ref) {         av_freep(&data);         return AVERROR(ENOMEM);     }     frag->data = data;     frag->data_size = dp;     return 0; } 复制代码
  • 分析代码确定bsf会将码流改成annex-b,那正常码流应该是annex-b,但是加bsf刚开始运行正常,dump可以播放的流却是avcc,应该是ffmpeg又把annex-b转成avcc,果然在输出packet的时候有此逻辑在flvenc.c flv_write_packet(AVFormatContext *s, AVPacket *pkt) 方法中执行ff_avc_parse_nal_units_buf方法将annex-b转avcc代码如下

    if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4) {         /* check if extradata looks like mp4 formatted */         av_log(NULL, AV_LOG_INFO, "extradata_size---par->extradata_size=%d,par->extradata=%d\n", par->extradata_size,                *(uint8_t *) par->extradata);         if (par->extradata_size > 0 && *(uint8_t *) par->extradata != 1)             //将annex-b转成avcc             if ((ret = ff_avc_parse_nal_units_buf(pkt->data, &data, &size)) < 0)                 return ret;     }  复制代码
  • 以上代码分析ffmpeg加bsf可以保证正常播放。但是运行一段时间又出现无法播放的问题。

加bsf异常情况

  • 1.dump下来的流就是以上看到的二进制,avc sequence header以AVCDecoderConfigurationRecord组织的数据,而nalu又是以annex-b组织的数据。可以明确没有执行ff_avc_parse_nal_units_buf方法,但avc sequence header又是AVCDecoderConfigurationRecord(疑问)

  • 2.明确并需要了解的是avc sequence header是服务端发送给播发器。难道bsf运行一段时间服务端又再一次发送以AVCDecoderConfigurationRecord格式组织的avc sequence header到播放端?

    • 结合之前对srs流媒体服务的了解,模拟发送avc sequence header的情况,先保证第二个ffmpeg可以正常从srs拉流这时候服务端发送一次avc sequence header,要再次发送avc sequence header需要源流发生变化,或者拉取的源流中断再恢复,果然将第一个ffmpeg断开重启后问题复现。

  • 排查过程中将第二个ffmpeg的源流从srs拉问题复现,于是抓包分析看到avc sequence header会重新发送。

image.png

  • avc sequence header以AVCDecoderConfigurationRecord组织数据是由于服务端重新发送导致。分析ffmpeg源码也可以看到更新的操作在flvdec.c文件flv_read_packet读取源流flv方法中执行flv_queue_extradata会填充flv->new_extradata

  if (type == 0 && (!st->codecpar->extradata || st->codecpar->codec_id == AV_CODEC_ID_AAC ||                         st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_HEVC)) {           AVDictionaryEntry *t;           //extra_data 不为空           if (st->codecpar->extradata) {               if ((ret = flv_queue_extradata(flv, s->pb, stream_type, size)) < 0)                   return ret;               av_log(NULL, AV_LOG_ERROR, "flv_queue_extradata======st->codecpar->extradata=%d,size=%d,stream_type=%d\n", *(uint8_t*)st->codecpar->extradata,size,stream_type);               ret = FFERROR_REDO;               goto leave;           }           //填充extra_data           if ((ret = flv_get_extradata(s, st, size)) < 0) {               return ret;           }           av_log(NULL, AV_LOG_ERROR, "flv_queue_extradata======st->codecpar->extradata=%d,size=%d,stream_type=%d\n", *(uint8_t*)st->codecpar->extradata,size,stream_type);           /* Workaround for buggy Omnia A/XE encoder */           t = av_dict_get(s->metadata, "Encoder", NULL, 0);           if (st->codecpar->codec_id == AV_CODEC_ID_AAC && t && !strcmp(t->value, "Omnia A/XE"))               st->codecpar->extradata_size = 2;           if (st->codecpar->codec_id == AV_CODEC_ID_AAC && 0) {               MPEG4AudioConfig cfg;               if (avpriv_mpeg4audio_get_config(&cfg, st->codecpar->extradata,                                                st->codecpar->extradata_size * 8, 1) >= 0) {                   st->codecpar->channels = cfg.channels;                   st->codecpar->channel_layout = 0;                   if (cfg.ext_sample_rate)                       st->codecpar->sample_rate = cfg.ext_sample_rate;                   else                       st->codecpar->sample_rate = cfg.sample_rate;                   av_log(s, AV_LOG_TRACE, "mp4a config channels %d sample rate %d\n",                          st->codecpar->channels, st->codecpar->sample_rate);               }           }           ret = FFERROR_REDO;           goto leave;       }   } 复制代码
  • flv->new_extradata存在的话会将extra_data填充到side_data(可以理解成extra_data缓存)中

 if (flv->new_extradata[stream_type]) {         //新建side_data         uint8_t *side = av_packet_new_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA,                                                 flv->new_extradata_size[stream_type]);         if (side) {             memcpy(side, flv->new_extradata[stream_type],                    flv->new_extradata_size[stream_type]);             av_freep(&flv->new_extradata[stream_type]);             flv->new_extradata_size[stream_type] = 0;         }     } 复制代码
  • 在flvenc.c 中执行flv_write_packet也就是输出flv时会获取side_data数据,这时候par->extradata会被赋值成1看以下代码(extradata的类型是uint8_t意味着只取par->extradata的第一个字节)。

  • 如果是annex-b的话extradata的第一个字节永远是0(因为star_code分割是 00 00 00 01或者00 00 01),avcc的话第一个字节代表的是AVCDecoderConfigurationRecord的第一个字节configurationVersion一般是1

    • par->extradata=0代表annex-b,par->extradata=1代表avcc。

 if (par->codec_id == AV_CODEC_ID_AAC || par->codec_id == AV_CODEC_ID_H264         || par->codec_id == AV_CODEC_ID_MPEG4 || par->codec_id == AV_CODEC_ID_HEVC) {         int side_size = 0;         uint8_t *side = av_packet_get_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA, &side_size);         //如果extradata_size和side_data_size不一致会认为extradata发生改变,本来bsf是annex-b,却被更新成avcc(par->extradata=1)         if (side && side_size > 0 && (side_size != par->extradata_size || memcmp(side, par->extradata, side_size))) ||  {             av_log(NULL, AV_LOG_ERROR, "flv_write_packet---side_size=%d----extradata_size=%d,extradata=%d\n", side_size,                    par->extradata_size, *(uint8_t *) par->extradata);             av_free(par->extradata);             par->extradata = av_mallocz(side_size + AV_INPUT_BUFFER_PADDING_SIZE);             if (!par->extradata) {                 par->extradata_size = 0;                 return AVERROR(ENOMEM);             }             memcpy(par->extradata, side, side_size);             par->extradata_size = side_size;             flv_write_codec_header(s, par, pkt->dts);         }     } 复制代码
  • par->extradata被赋值成1将不会执行ff_avc_parse_nal_units_buf表示使用bsf后annex-b无法转成avcc,而avc sequence header成为AVCDecoderConfigurationRecord格式的数据,和之前的分析对应上了。

if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4) {         /* check if extradata looks like mp4 formatted */         av_log(NULL, AV_LOG_INFO, "extradata_size---par->extradata_size=%d,par->extradata=%d\n", par->extradata_size,                *(uint8_t *) par->extradata);         if (par->extradata_size > 0 && *(uint8_t *) par->extradata != 1)             //annex-b转avcc             if ((ret = ff_avc_parse_nal_units_buf(pkt->data, &data, &size)) < 0)                 return ret;  } 复制代码

造成问题的原因

  • 综上问题原因:ffmpeg使用bsf源流默认会改成annex-b格式,如果此时extradata被更新成avcc,使annex-b无法转avcc,造成码流的avc sequence header是AVCDecoderConfigurationRecord格式,其他nalu又是start_code分割,硬解失败。

结论

  • 综合上边分析,如果第一个ffmpeg不重启,即使第二个ffmpeg使用bsf也没有问题。

  • 如果第一个ffmpeg重启,第二个ffmpeg又使用bsf造成硬解失败无法播放。(第二个ffmpeg的源流是第一个ffmpeg的输出)

进一步分析

  • 可以进一步通过extradata_size判断问题的原因,通过日志输出extradata_size

    • annex-b:extradata_size=39(sps)+4(pps)+4+4=51 (二个4代表start_code)

    • avcc:AVCDecoderConfigurationRecord 除了sps pps数据为11个字节extradata_size=11+39(sps)+4(sps)=54

  • 通过extradata_size也可以判断出使用bsf发生问题前后extradata的变化。

收获

  • 了解ffmpeg bsf处理逻辑

  • 了解ffmpeg对extradata处理逻辑

  • 了解视频流的数据组织形式 annex-b和avcc

  • 了解avc sequence header


作者:shb
链接:https://juejin.cn/post/7028857858523398180

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐