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 查看码流二进制)
如何查看二进制
先了解flv中video tag封装格式,如下图:
结合下边二进制分析
0:AVC sequence header
1: NALU
2:AVC END sequence
FrameType=1代表关键帧
CodeID=7 代表AVC
AVCPacketType
AVC sequence header (也叫extra_data或者AVCDecoderConfigurationRecord)
结合上边的二进制和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会重新发送。
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