admin管理员组文章数量:1660604
- 背景
- 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_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<<retry处下载ts。retry为重试次数。DURATION_MAX_READ_SIZE在FFmpeg中设置为250KB。
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<<retry。这是有必要的,对于大码率的ts流,250KB很可能是不足一帧视频数据的,以FFmpeg设置的默认值为例,第二次则为500KB,第三次1000KB。retry最大值为4。
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; i<ic->nb_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<<retry);//倒数DURATION_MAX_READ_SIZE<<retry处
if (offset < 0)
offset = 0;
avio_seek(ic->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<<retry) //不会超过文件大小
&& ++retry <= DURATION_MAX_RETRY); //不会超过重试次数
fill_all_stream_timings(ic);
avio_seek(ic->pb, old_offset, SEEK_SET); //重新seek回原本的位置
for (i=0; i<ic->nb_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:
PTS反转:
所以按照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。
本文标签: 时长计算方法seekffmpegDuration
版权声明:本文标题:FFmpeg 从seek闪退问题分析ts时长duration计算方法 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1729851110a1215424.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论