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

2,077 阅读5分钟

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

问题的产生

  • 第一个ffmpeg拷贝(copy)第三方的流到源站,第二个ffmpeg进程的源流为第一个ffmpeg的输出,并使用bsf添加sei到码流中,偶发硬解无法播放的情况。
    • 重启第二个ffmpeg可以恢复播放。
    • 不使用bsf问题不复现。
    • 使用bsf偶发无法播放的情况(可疑点)。

排查过程

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

如何查看二进制

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

image.png

  • 结合下边二进制分析
    • FrameType=1代表关键帧
    • CodeID=7 代表AVC
    • AVCPacketType
      • 0:AVC sequence header
      • 1: NALU
      • 2:AVC END sequence

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