FFmpeg 从seek闪退问题分析ts时长duration计算方法

    • 背景
    • HTTP点播seek闪退分析
    • FFmpeg解析ts duration流程分析
    • 解决思路

背景

FFmpeg是非常优秀的开源框架,在使用其进行二次开发及适配的过程中,难免会遇到各种各样的问题。
这次要分析的问题是基于FFmpeg的播放器在HTTP点播seek的时候,出现闪退,从而引申出FFmpeg中ts流duration计算方法的分析。

HTTP点播seek闪退分析

从日志看,发现seek的位置是10分钟左右,小于duration,但是从HTTP的请求来看,请求的大小已经和整个片源大小差不多了,如下:
FFmpeg 从seek闪退问题分析ts时长duration计算方法_第1张图片
最开始我怀疑是seek的模式不对(FFmpeg有几种seek模式)导致HTTP请求的点不对,跟进后并无发现明显异常。
于是将整个ts流dump下来。发现其实整个片源才有不到5分钟,这下问题很明显了:FFmpeg计算的ts时长不对,seek时间点其实已经超过时长了,所以HTTP请求每次都请求到文件末端,一请求就end of file了,看起来效果就是闪退。
所以引申出文章的主题,FFmpeg是如何解析ts的duration的。
FFmpeg 从seek闪退问题分析ts时长duration计算方法_第2张图片

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_pts
    if ((!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,代码比较简单,如下:
1)获取文件大小,seek到倒数DURATION_MAX_READ_SIZE<

2)读取第一个ts,根据起始PTS或者DTS。计算出当前ts的PTS差距,则为duration,并将此次计算得duration记录下来,为last_duration。

3)循环读取ts,如果此次计算的duration更大,且和last_duration间隔小于60LL*st->time_base.den / st->time_base.num,则更新duration。这是为了防止有些PTS跳变的情况,不更新duration,但仍然会记录此次的计算得值为last_duration。如果后续新的ts的计算得到的duration和last_duration比较是连续的,则可以认为PTS跳变后又连续了,认为计算得到的duration是正确的。

ts流的st->time_base.den /st->time_base.num = 1/90000。因为mpeg的pts、dts都是以90kHz来采样的,所以采样间隔为1/90000秒。

4)如果在DURATION_MAX_READ_SIZE内已经解析到时长,完成,否则偏移更大数据量来解析PTS,计算duration。新数据量为DURATION_MAX_READ_SIZE<

5)重新seek回原本的偏移位置。

/* only usable for MPEG-PS streams */
static void estimate_timings_from_pts(AVFormatContext *ic, int64_t old_offset)
{
    AVPacket pkt1, *pkt = &pkt1;
    AVStream *st;
    int read_size, i, ret;
    int64_t end_time;
    int64_t filesize, offset, duration;
    int retry=0;

    /* flush packet queue */
    flush_packet_queue(ic);

    for (i=0; inb_streams; i++) {
        st = ic->streams[i];
        if (st->start_time == AV_NOPTS_VALUE && st->first_dts == 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= NULL;
        }
    }

    /* estimate the end time (duration) */
    /* XXX: may need to support wrapping */
    filesize = ic->pb ? avio_size(ic->pb) : 0;	//获取文件大小
    end_time = AV_NOPTS_VALUE;
    do{
        offset = filesize - (DURATION_MAX_READ_SIZE<pb, offset, SEEK_SET);	//seek到倒数位置
        read_size = 0;
        for(;;) {
            if (read_size >= DURATION_MAX_READ_SIZE<<(FFMAX(retry-1,0)))
                break;

            do {
                ret = ff_read_packet(ic, pkt);	//读取ts
            } while(ret == AVERROR(EAGAIN));
            if (ret != 0)
                break;
            read_size += pkt->size;
            st = ic->streams[pkt->stream_index];
            if (pkt->pts != AV_NOPTS_VALUE &&
                (st->start_time != AV_NOPTS_VALUE ||
                 st->first_dts  != AV_NOPTS_VALUE)) {
                duration = end_time = pkt->pts;
                if (st->start_time != AV_NOPTS_VALUE)
                    duration -= st->start_time;	//如果能解析出PTS,起始位置以PTS计算时长
                else
                    duration -= st->first_dts;	//否则以起始位置的DTS来计算时长
                if (duration > 0) {
                    if (st->duration == AV_NOPTS_VALUE || st->info->last_duration<=0 ||
                   		 //防止PTS跳变情况
                        (st->duration < duration && FFABS(duration - st->info->last_duration) < 60LL*st->time_base.den / st->time_base.num))
                        st->duration = duration;//更新时长
                    st->info->last_duration = duration;
                }
            }
            av_free_packet(pkt);
        }
    }while(   end_time==AV_NOPTS_VALUE		//没获取有效时长
           && filesize > (DURATION_MAX_READ_SIZE<pb, old_offset, SEEK_SET); 	//重新seek回原本的位置
    for (i=0; inb_streams; i++) {
        st= ic->streams[i];
        st->cur_dts= st->first_dts;
        st->last_IP_pts = AV_NOPTS_VALUE;
    }
}

estimate_timings_from_pts获取到duration后,update_stream_timings(ic);将其更新到AVFormatContext中。

分析完FFmpeg计算ts时长的流程后,我们重新回头来看下这个片源为什么会有问题,以及如何解决。

解决思路

用ts分析工具,可以看到初始PTS和中间有明显的PTS反转情况。

初始PTS:
FFmpeg 从seek闪退问题分析ts时长duration计算方法_第3张图片PTS反转:FFmpeg 从seek闪退问题分析ts时长duration计算方法_第4张图片
所以按照FFmpeg的计算流程,得到的duration才会特别大。
为兼容此流,做了以下修改:
1)计算得到PTS反转处为倒数1210340字节处,所以从倒数1.5MB处开始计算PTS。
2)处理PTS跳变时,如果跳变过大,像这种反转的情况,就直接不记录到last_duration。

   if (duration > 0) {
       if (st->duration == AV_NOPTS_VALUE || st->info->last_duration<=0 ||
       		//防止PTS跳变情况
           (st->duration < duration && FFABS(duration - st->info->last_duration) < 60LL*st->time_base.den / st->time_base.num))
           st->duration = duration;//更新时长
           //add for PTS 跳变过大的异常情况,直接忽略
            if (FFABS(duration - st->info->last_duration) > 600LL * st->time_base.den / st->time_base.num && st->info->last_duration > 0) {                   
                st->duration = st->info->last_duration;
                continue;
            }
            //add end
       st->info->last_duration = duration;
   }

测试可以获取到正常PTS,当然,这些修改并不是通用,只是我针对该异常ts流测试用,例如如果反转后的流还很长呢,这样的方法计算出来则duration会小一截。
最实际的方法,还是在流的制作时,正确打包PTS。

你可能感兴趣的:(FFmpeg,流媒体)