作者:2502910的 | 来源:互联网 | 2023-06-11 00:32
背景 HTTP点播seek闪退分析 FFmpeg解析ts duration流程分析 解决思路
背景 FFmpeg是非常优秀的开源框架,在使用其进行二次开发及适配的过程中,难免会遇到各种各样的问题。 这次要分析的问题是基于FFmpeg的播放器在HTTP点播seek的时候,出现闪退,从而引申出FFmpeg中ts流duration计算方法的分析。
HTTP点播seek闪退分析 从日志看,发现seek的位置是10分钟左右,小于duration,但是从HTTP的请求来看,请求的大小已经和整个片源大小差不多了,如下: 最开始我怀疑是seek的模式不对(FFmpeg有几种seek模式)导致HTTP请求的点不对,跟进后并无发现明显异常。 于是将整个ts流dump下来。发现其实整个片源才有不到5分钟,这下问题很明显了:FFmpeg计算的ts时长不对,seek时间点其实已经超过时长了,所以HTTP请求每次都请求到文件末端,一请求就end of file了,看起来效果就是闪退。 所以引申出文章的主题,FFmpeg是如何解析ts的duration的。
FFmpeg解析ts duration流程分析 1、首先ts流并不像MP4这种,在头部信息里就已经携带duration了,ts的PSI/SI信息并无携带duration。一般来说,ts的时长是由PTS计算得到,当然也可以计算比特率,但对于VBR(动态比特率)的片源,用比特率来计算其实得到的时长并不可靠。 2、FFmpeg中对于ts的时长计算确实也是基于PTS的。 3、分析如下: 在avformat_find_stream_info阶段过来,我们就可以知道片源的duration,所以我们先从avformat_find_stream_info里分析。 可以看到,计算时长的函数为estimate_timings
avformat_find_stream_info:if(ic->probesize)estimate_timings(ic, old_offset);
跟进estimate_timings,可以看到对于ts流来说,计算方式是estimate_timings_from_pts(其他不同格式还有不同的方式,不详细分析)。
static void estimate_timings(AVFormatContext *ic, int64_t old_offset) { ......省略 //对于ts流来说,计算方式是estimate_timings_from_ptsif ((!strcmp(ic->iformat->name, "mpeg") ||!strcmp(ic->iformat->name, "mpegts")) &&file_size && ic->pb->seekable) {/* get accurate estimate from the PTSes */estimate_timings_from_pts(ic, old_offset);ic->duration_estimation_method = AVFMT_DURATION_FROM_PTS;} ......省略
跟进estimate_timings_from_pts&#xff0c;代码比较简单&#xff0c;如下&#xff1a; 1&#xff09;获取文件大小&#xff0c;seek到倒数DURATION_MAX_READ_SIZE< 2&#xff09;读取第一个ts&#xff0c;根据起始PTS或者DTS。计算出当前ts的PTS差距&#xff0c;则为duration&#xff0c;并将此次计算得duration记录下来&#xff0c;为last_duration。
3&#xff09;循环读取ts&#xff0c;如果此次计算的duration更大&#xff0c;且和last_duration间隔小于60LL*st->time_base.den / st->time_base.num&#xff0c;则更新duration。这是为了防止有些PTS跳变的情况&#xff0c;不更新duration&#xff0c;但仍然会记录此次的计算得值为last_duration。如果后续新的ts的计算得到的duration和last_duration比较是连续的&#xff0c;则可以认为PTS跳变后又连续了&#xff0c;认为计算得到的duration是正确的。
ts流的st->time_base.den /st->time_base.num &#61; 1/90000。因为mpeg的pts、dts都是以90kHz来采样的&#xff0c;所以采样间隔为1/90000秒。
4&#xff09;如果在DURATION_MAX_READ_SIZE内已经解析到时长&#xff0c;完成&#xff0c;否则偏移更大数据量来解析PTS&#xff0c;计算duration。新数据量为DURATION_MAX_READ_SIZE< 5&#xff09;重新seek回原本的偏移位置。
/* only usable for MPEG-PS streams */ static void estimate_timings_from_pts(AVFormatContext *ic, int64_t old_offset) {AVPacket pkt1, *pkt &#61; &pkt1;AVStream *st;int read_size, i, ret;int64_t end_time;int64_t filesize, offset, duration;int retry&#61;0;/* flush packet queue */flush_packet_queue(ic);for (i&#61;0; inb_streams; i&#43;&#43;) {st &#61; ic->streams[i];if (st->start_time &#61;&#61; AV_NOPTS_VALUE && st->first_dts &#61;&#61; AV_NOPTS_VALUE)av_log(st->codec, AV_LOG_WARNING, "start time is not set in estimate_timings_from_pts\n");if (st->parser) {av_parser_close(st->parser);st->parser&#61; NULL;}}/* estimate the end time (duration) *//* XXX: may need to support wrapping */filesize &#61; ic->pb ? avio_size(ic->pb) : 0; //获取文件大小end_time &#61; AV_NOPTS_VALUE;do{offset &#61; filesize - (DURATION_MAX_READ_SIZE<pb, offset, SEEK_SET); //seek到倒数位置read_size &#61; 0;for(;;) {if (read_size >&#61; DURATION_MAX_READ_SIZE<<(FFMAX(retry-1,0)))break;do {ret &#61; ff_read_packet(ic, pkt); //读取ts} while(ret &#61;&#61; AVERROR(EAGAIN));if (ret !&#61; 0)break;read_size &#43;&#61; pkt->size;st &#61; ic->streams[pkt->stream_index];if (pkt->pts !&#61; AV_NOPTS_VALUE &&(st->start_time !&#61; AV_NOPTS_VALUE ||st->first_dts !&#61; AV_NOPTS_VALUE)) {duration &#61; end_time &#61; pkt->pts;if (st->start_time !&#61; AV_NOPTS_VALUE)duration -&#61; st->start_time; //如果能解析出PTS&#xff0c;起始位置以PTS计算时长elseduration -&#61; st->first_dts; //否则以起始位置的DTS来计算时长if (duration > 0) {if (st->duration &#61;&#61; AV_NOPTS_VALUE || st->info->last_duration<&#61;0 ||//防止PTS跳变情况(st->duration info->last_duration) <60LL*st->time_base.den / st->time_base.num))st->duration &#61; duration;//更新时长st->info->last_duration &#61; duration;}}av_free_packet(pkt);}}while( end_time&#61;&#61;AV_NOPTS_VALUE //没获取有效时长&& filesize > (DURATION_MAX_READ_SIZE<pb, old_offset, SEEK_SET); //重新seek回原本的位置for (i&#61;0; inb_streams; i&#43;&#43;) {st&#61; ic->streams[i];st->cur_dts&#61; st->first_dts;st->last_IP_pts &#61; AV_NOPTS_VALUE;} }
estimate_timings_from_pts获取到duration后&#xff0c;update_stream_timings(ic);将其更新到AVFormatContext中。
分析完FFmpeg计算ts时长的流程后&#xff0c;我们重新回头来看下这个片源为什么会有问题&#xff0c;以及如何解决。
解决思路 用ts分析工具&#xff0c;可以看到初始PTS和中间有明显的PTS反转情况。
初始PTS&#xff1a; PTS反转&#xff1a; 所以按照FFmpeg的计算流程&#xff0c;得到的duration才会特别大。 为兼容此流&#xff0c;做了以下修改&#xff1a; 1&#xff09;计算得到PTS反转处为倒数1210340字节处&#xff0c;所以从倒数1.5MB处开始计算PTS。 2&#xff09;处理PTS跳变时&#xff0c;如果跳变过大&#xff0c;像这种反转的情况&#xff0c;就直接不记录到last_duration。
if (duration > 0) {if (st->duration &#61;&#61; AV_NOPTS_VALUE || st->info->last_duration<&#61;0 ||//防止PTS跳变情况(st->duration info->last_duration) <60LL*st->time_base.den / st->time_base.num))st->duration &#61; duration;//更新时长//add for PTS 跳变过大的异常情况&#xff0c;直接忽略if (FFABS(duration - st->info->last_duration) > 600LL * st->time_base.den / st->time_base.num && st->info->last_duration > 0) { st->duration &#61; st->info->last_duration;continue;}//add endst->info->last_duration &#61; duration;}
测试可以获取到正常PTS&#xff0c;当然&#xff0c;这些修改并不是通用&#xff0c;只是我针对该异常ts流测试用&#xff0c;例如如果反转后的流还很长呢&#xff0c;这样的方法计算出来则duration会小一截。 最实际的方法&#xff0c;还是在流的制作时&#xff0c;正确打包PTS。