热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

ffmpeg使用bsf后码流从avcc格式变成annex-b造成硬解异常问题的产生第一个ffmpeg拷贝(copy)第三方的流到源站,第二个ffmpeg进程的源流

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


  • 结合下边二进制分析
    • 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 &#61; 0; i < frag->nb_units; i&#43;&#43;) {// Data should already all have been written when we get here.av_assert0(frag->units[i].data);}max_size &#61; 0;for (i &#61; 0; i < frag->nb_units; i&#43;&#43;) {// Start code &#43; content with worst-case emulation prevention.max_size &#43;&#61; 3 &#43; frag->units[i].data_size * 3 / 2;}data &#61; av_malloc(max_size &#43; AV_INPUT_BUFFER_PADDING_SIZE);if (!data)return AVERROR(ENOMEM);dp &#61; 0;for (i &#61; 0; i < frag->nb_units; i&#43;&#43;) {CodedBitstreamUnit *unit &#61; &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");elsefrag->data_bit_padding &#61; unit->data_bit_padding;}&#96;&#96;&#96;以下代码以start_code的方式组织码率&#96;&#96;&#96;if ((ctx->codec->codec_id &#61;&#61; AV_CODEC_ID_H264 &&(unit->type &#61;&#61; H264_NAL_SPS ||unit->type &#61;&#61; H264_NAL_PPS)) ||(ctx->codec->codec_id &#61;&#61; AV_CODEC_ID_HEVC &&(unit->type &#61;&#61; HEVC_NAL_VPS ||unit->type &#61;&#61; HEVC_NAL_SPS ||unit->type &#61;&#61; HEVC_NAL_PPS)) ||i &#61;&#61; 0 /* (Assume this is the start of an access unit.) */) {// zero_bytedata[dp&#43;&#43;] &#61; 0;}// start_code_prefix_one_3bytesdata[dp&#43;&#43;] &#61; 0;data[dp&#43;&#43;] &#61; 0;data[dp&#43;&#43;] &#61; 1;zero_run &#61; 0;for (sp &#61; 0; sp < unit->data_size; sp&#43;&#43;) {if (zero_run < 2) {if (unit->data[sp] &#61;&#61; 0)&#43;&#43;zero_run;elsezero_run &#61; 0;} else {if ((unit->data[sp] & ~3) &#61;&#61; 0) {// emulation_prevention_three_bytedata[dp&#43;&#43;] &#61; 3;}zero_run &#61; unit->data[sp] &#61;&#61; 0;}data[dp&#43;&#43;] &#61; unit->data[sp];}}av_assert0(dp <&#61; max_size);err &#61; av_reallocp(&data, dp &#43; AV_INPUT_BUFFER_PADDING_SIZE);if (err)return err;memset(data &#43; dp, 0, AV_INPUT_BUFFER_PADDING_SIZE);frag->data_ref &#61; av_buffer_create(data, dp &#43; AV_INPUT_BUFFER_PADDING_SIZE,NULL, NULL, 0);if (!frag->data_ref) {av_freep(&data);return AVERROR(ENOMEM);}frag->data &#61; data;frag->data_size &#61; dp;return 0;
}

  • 分析代码确定bsf会将码流改成annex-b&#xff0c;那正常码流应该是annex-b&#xff0c;但是加bsf刚开始运行正常&#xff0c;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 &#61;&#61; AV_CODEC_ID_H264 || par->codec_id &#61;&#61; AV_CODEC_ID_MPEG4) {/* check if extradata looks like mp4 formatted */av_log(NULL, AV_LOG_INFO, "extradata_size---par->extradata_size&#61;%d,par->extradata&#61;%d\n", par->extradata_size,*(uint8_t *) par->extradata);if (par->extradata_size > 0 && *(uint8_t *) par->extradata !&#61; 1)//将annex-b转成avccif ((ret &#61; ff_avc_parse_nal_units_buf(pkt->data, &data, &size)) < 0)return ret;}

  • 以上代码分析ffmpeg加bsf可以保证正常播放。但是运行一段时间又出现无法播放的问题。

加bsf异常情况


  • 1.dump下来的流就是以上看到的二进制&#xff0c;avc sequence header以AVCDecoderConfigurationRecord组织的数据&#xff0c;而nalu又是以annex-b组织的数据。可以明确没有执行ff_avc_parse_nal_units_buf方法&#xff0c;但avc sequence header又是AVCDecoderConfigurationRecord&#xff08;疑问&#xff09;
  • 2.明确并需要了解的是avc sequence header是服务端发送给播发器。难道bsf运行一段时间服务端又再一次发送以AVCDecoderConfigurationRecord格式组织的avc sequence header到播放端&#xff1f;
    • 结合之前对srs流媒体服务的了解&#xff0c;模拟发送avc sequence header的情况&#xff0c;先保证第二个ffmpeg可以正常从srs拉流这时候服务端发送一次avc sequence header,要再次发送avc sequence header需要源流发生变化&#xff0c;或者拉取的源流中断再恢复&#xff0c;果然将第一个ffmpeg断开重启后问题复现。
  • 排查过程中将第二个ffmpeg的源流从srs拉问题复现&#xff0c;于是抓包分析看到avc sequence header会重新发送。
    image.png
    • avc sequence header以AVCDecoderConfigurationRecord组织数据是由于服务端重新发送导致。分析ffmpeg源码也可以看到更新的操作在flvdec.c文件flv_read_packet读取源流flv方法中执行flv_queue_extradata会填充flv->new_extradata

    if (type &#61;&#61; 0 && (!st->codecpar->extradata || st->codecpar->codec_id &#61;&#61; AV_CODEC_ID_AAC ||st->codecpar->codec_id &#61;&#61; AV_CODEC_ID_H264 || st->codecpar->codec_id &#61;&#61; AV_CODEC_ID_HEVC)) {AVDictionaryEntry *t;//extra_data 不为空if (st->codecpar->extradata) {if ((ret &#61; flv_queue_extradata(flv, s->pb, stream_type, size)) < 0)return ret;av_log(NULL, AV_LOG_ERROR, "flv_queue_extradata&#61;&#61;&#61;&#61;&#61;&#61;st->codecpar->extradata&#61;%d,size&#61;%d,stream_type&#61;%d\n", *(uint8_t*)st->codecpar->extradata,size,stream_type);ret &#61; FFERROR_REDO;goto leave;}//填充extra_dataif ((ret &#61; flv_get_extradata(s, st, size)) < 0) {return ret;}av_log(NULL, AV_LOG_ERROR, "flv_queue_extradata&#61;&#61;&#61;&#61;&#61;&#61;st->codecpar->extradata&#61;%d,size&#61;%d,stream_type&#61;%d\n", *(uint8_t*)st->codecpar->extradata,size,stream_type);/* Workaround for buggy Omnia A/XE encoder */t &#61; av_dict_get(s->metadata, "Encoder", NULL, 0);if (st->codecpar->codec_id &#61;&#61; AV_CODEC_ID_AAC && t && !strcmp(t->value, "Omnia A/XE"))st->codecpar->extradata_size &#61; 2;if (st->codecpar->codec_id &#61;&#61; AV_CODEC_ID_AAC && 0) {MPEG4AudioConfig cfg;if (avpriv_mpeg4audio_get_config(&cfg, st->codecpar->extradata,st->codecpar->extradata_size * 8, 1) >&#61; 0) {st->codecpar->channels &#61; cfg.channels;st->codecpar->channel_layout &#61; 0;if (cfg.ext_sample_rate)st->codecpar->sample_rate &#61; cfg.ext_sample_rate;elsest->codecpar->sample_rate &#61; 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 &#61; FFERROR_REDO;goto leave;}}

  • flv->new_extradata存在的话会将extra_data填充到side_data&#xff08;可以理解成extra_data缓存&#xff09;中

if (flv->new_extradata[stream_type]) {//新建side_datauint8_t *side &#61; 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] &#61; 0;}}

  • 在flvenc.c 中执行flv_write_packet也就是输出flv时会获取side_data数据,这时候par->extradata会被赋值成1看以下代码&#xff08;extradata的类型是uint8_t意味着只取par->extradata的第一个字节)。
  • 如果是annex-b的话extradata的第一个字节永远是0&#xff08;因为star_code分割是 00 00 00 01或者00 00 01&#xff09;&#xff0c;avcc的话第一个字节代表的是AVCDecoderConfigurationRecord的第一个字节configurationVersion一般是1
    • par->extradata&#61;0代表annex-b&#xff0c;par->extradata&#61;1代表avcc。

if (par->codec_id &#61;&#61; AV_CODEC_ID_AAC || par->codec_id &#61;&#61; AV_CODEC_ID_H264|| par->codec_id &#61;&#61; AV_CODEC_ID_MPEG4 || par->codec_id &#61;&#61; AV_CODEC_ID_HEVC) {int side_size &#61; 0;uint8_t *side &#61; av_packet_get_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA, &side_size);//如果extradata_size和side_data_size不一致会认为extradata发生改变&#xff0c;本来bsf是annex-b&#xff0c;却被更新成avcc&#xff08;par->extradata&#61;1&#xff09;if (side && side_size > 0 && (side_size !&#61; par->extradata_size || memcmp(side, par->extradata, side_size))) || {av_log(NULL, AV_LOG_ERROR, "flv_write_packet---side_size&#61;%d----extradata_size&#61;%d,extradata&#61;%d\n", side_size,par->extradata_size, *(uint8_t *) par->extradata);av_free(par->extradata);par->extradata &#61; av_mallocz(side_size &#43; AV_INPUT_BUFFER_PADDING_SIZE);if (!par->extradata) {par->extradata_size &#61; 0;return AVERROR(ENOMEM);}memcpy(par->extradata, side, side_size);par->extradata_size &#61; 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格式的数据&#xff0c;和之前的分析对应上了。

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

造成问题的原因


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

结论


  • 综合上边分析&#xff0c;如果第一个ffmpeg不重启&#xff0c;即使第二个ffmpeg使用bsf也没有问题。
  • 如果第一个ffmpeg重启&#xff0c;第二个ffmpeg又使用bsf造成硬解失败无法播放。&#xff08;第二个ffmpeg的源流是第一个ffmpeg的输出&#xff09;

进一步分析


  • 可以进一步通过extradata_size判断问题的原因&#xff0c;通过日志输出extradata_size
    • annex-b&#xff1a;extradata_size&#61;39&#xff08;sps&#xff09;&#43;4(pps)&#43;4&#43;4&#61;51 (二个4代表start_code)
    • avcc&#xff1a;AVCDecoderConfigurationRecord 除了sps pps数据为11个字节extradata_size&#61;11&#43;39&#xff08;sps&#xff09;&#43;4(sps)&#61;54
  • 通过extradata_size也可以判断出使用bsf发生问题前后extradata的变化。

收获


  • 了解ffmpeg bsf处理逻辑
  • 了解ffmpeg对extradata处理逻辑
  • 了解视频流的数据组织形式 annex-b和avcc
  • 了解avc sequence header

推荐阅读
  • 使用圣杯布局模式实现网站首页的内容布局
    本文介绍了使用圣杯布局模式实现网站首页的内容布局的方法,包括HTML部分代码和实例。同时还提供了公司新闻、最新产品、关于我们、联系我们等页面的布局示例。商品展示区包括了车里子和农家生态土鸡蛋等产品的价格信息。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文讨论了在Spring 3.1中,数据源未能自动连接到@Configuration类的错误原因,并提供了解决方法。作者发现了错误的原因,并在代码中手动定义了PersistenceAnnotationBeanPostProcessor。作者删除了该定义后,问题得到解决。此外,作者还指出了默认的PersistenceAnnotationBeanPostProcessor的注册方式,并提供了自定义该bean定义的方法。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • 导出功能protectedvoidbtnExport(objectsender,EventArgse){用来打开下载窗口stringfileName中 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • .NetCoreWebApi生成Swagger接口文档的使用方法
    本文介绍了使用.NetCoreWebApi生成Swagger接口文档的方法,并详细说明了Swagger的定义和功能。通过使用Swagger,可以实现接口和服务的可视化,方便测试人员进行接口测试。同时,还提供了Github链接和具体的步骤,包括创建WebApi工程、引入swagger的包、配置XML文档文件和跨域处理。通过本文,读者可以了解到如何使用Swagger生成接口文档,并加深对Swagger的理解。 ... [详细]
  • 本文介绍了禅道作为一款国产开源免费的测试管理工具的特点和功能,并提供了禅道的搭建和调试方法。禅道是一款B/S结构的项目管理工具,可以实现组织管理、后台管理、产品管理、项目管理和测试管理等功能。同时,本文还介绍了其他软件测试相关工具,如功能自动化工具和性能自动化工具,以及白盒测试工具的使用。通过本文的阅读,读者可以了解禅道的基本使用方法和优势,从而更好地进行测试管理工作。 ... [详细]
author-avatar
sdauilk_299
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有