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封装格式,如下图:
- 结合下边二进制分析
- FrameType=1代表关键帧
- CodeID=7 代表AVC
- AVCPacketType
- 0:AVC sequence header
- 1: NALU
- 2:AVC END sequence
-
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