一个专注音视频领域问答的小圈子

最近在做 Seek 相关功能时遇到的问题排查,顺便也学到了一些新的东西,和大家分享下。

在视频播放时执行 Seek 到任意点的操作,一般都是 Seek 到任意点往前最近的 I 帧,然后再逐帧解码到指定时间点。

这里可以优化,假设当前时间和指定时间在一个 GOP 内,就可以不用 seek ,直接顺序向下解码就好。

而正是这个优化出现了一点问题,现象如下:

已经判断播放点 A 和 Seek 点 B 不在一个 GOP 内,然后执行 av_seek_frame 方法还是把时间点 A 所在 GOP 全部解码了,导致播放上出现了卡顿。

这里就很奇怪了,明明判断不在一个 GOP ,那 Seek 时就应该从时间点 B 所在 GOP 的 I 帧开始解码, 但执行时还是解码了上一个 GOP 的内容。

到底是判断是否同一个 GOP 的函数出问题了还是 Seek 方法有问题呢?

带着疑问开始深入源码探索。

FFmpeg 没有直接提供判断两帧是否同一个 GOP 的方法,所以通过 av_index_search_timestamp 方法得到传入时间点最近的 I 帧的 index 索引,如果两个时间点的索引相同则表示为同一个 GOP 内,因为最近的 I 帧相同。

然而 av_index_search_timestamp 方法是通过 AVIndexEntry 中的 timestamp 来判断的,它是一个 DTS 值,通过二分查找得到最近的索引。

在没有 B 帧的情况下,I 帧的 PTS 等于 DTS ,所以判断不会出问题。然而正是有了 B 帧,如果 I 帧的 PTS 和 DTS 不相等的话,那么上面的判断相当于是拿一个 PTS 值和 I 帧的 DTS 比较是否同一个 GOP 了。

如果将错就错,判断 GOP 时得到结论是非同一个 GOP ,那么 Seek 也应该是非同一个 GOP ,但现实恰恰相反,Seek 当做了同一个 GOP ,这里面肯定有计算出问题了,继续深入源码。

通过在 mov.c 源码中看到了如下的操作:

static int mov_seek_stream(AVFormatContext *s, AVStream *st, int64_t timestamp, int flags)
{
    MOVStreamContext *sc = st->priv_data;
    FFStream *const sti = ffstream(st);
    int sample, time_sample, ret;
    unsigned int i;
    // Here we consider timestamp to be PTS, hence try to offset it so that we
    // can search over the DTS timeline.
    timestamp -= (sc->min_corrected_pts + sc->dts_shift);
    ret = mov_seek_fragment(s, st, timestamp);
    if (ret < 0)
        return ret;
    sample = av_index_search_timestamp(st, timestamp, flags);
    av_log(s, AV_LOG_TRACE, "stream %d, timestamp %"PRId64", sample %d\n", st->index, timestamp, sample);
    // 省略部分代码

注意到如下一行代码:

timestamp -= (sc->min_corrected_pts + sc->dts_shift);

也就是说我们传入的时间都会被减上一个值,然后再执行 av_index_search_timestamp 方法,而这个值导致判断 GOP 和 Seek 之间的关键帧索引出问题了。

正如代码中的注释所示,假设传入的时间是 PTS 值,然后给它减去偏移以得到 DTS 值,因为 av_index_search_timestamp 方法就通过 DTS 进行比较的嘛。

出现问题的原因就是 seek 的时间点正好在 I 帧的 PTS 和 DTS 范围之间了,执行 seek 时减去偏差值就小于 DTS 了,所以变成了同一个 GOP 。

现在要解决问题就是如何得到 sc->min_corrected_pts + sc->dts_shif 之和,然后判断 GOP 时减去它以修正得到 DTS 值。

还好通过遍历源码发现它的值是不会运行时改变的,一旦决定了就定下来了。另外我们可以用第一个 I 帧的 DTS 值作为偏移值。

  auto indexEntry = avStream->index_entries;
  auto nbIndexEntry = avStream->nb_index_entries;
  for (int i = 0; i < nbIndexEntry; ++i) {
    if (indexEntry[i].flags == AVINDEX_KEYFRAME) {
      DTSOffset = indexEntry[i].timestamp;
      return;
    }
  }

如果没有 B 帧,DTS 值为 0 ,有 B 帧,那么首帧的 DTS 值就可以用来做偏差值进行计算了。

知识星球

一个专注音视频领域问答的小圈子

公众号音视频开发进阶对应的知识星球,一个编程开发领域的专业圈子,贩卖知识和技巧!

※ 入群须知:了解该星球能提供的价值和帮助,在提问时务必阐述好背景,附带相关的信息。

iOS 用户可以加我微信 ezglumes 邀请你进星球,有疑问也可以加我微信咨询。

※ 星球内容:

基础教程:

在知识星球连载的干货教程,可以在专栏中找到,随着时间的推移,教程也会越来越多:

- 音视频基础概念
- WebRTC 入门教程及源码实践
- 播放器教程及源码实践
- OpenGL 和特效开发教程
- Vulkan 入门教程

部分内容可以在博客 https://glumes.com 中检索到,后面会在星球里持续更新.

干货分享:

涵盖了移动开发和音视频工程领域的绝大部分,从项目实战角度出发,提升能力,包括但不限于以下领域:

- Android/iOS 移动开发
- Camera 开发
- 短视频编辑 SDK 项目实践
- 在线直播和推流
- WebRTC 开发
- 播放器基础和提高
- OpenGL 图像渲染及特效开发
- C++ 基础和提高
- FFmpeg 使用和分析
- 干货资源和书籍分享

不止于技术方面的,各种 IT 新闻、茶余饭后、生活趣事也欢迎大家分享!!!

技术答疑解惑:

针对上述基础教程和干货分享的答疑,另外还有音视频和 IT 开发中的各种交流讨论。

- 基础知识点答疑
- 工业项目实践答疑
- 问题排查思路分析

一个 BUG 排查很久,不如来星球里提个问题,效率提升百倍。

求职和面试辅导:

一站式职场服务,每份工作都值得用心对待!!!

- 面试题和面试经验分享
- 简历修改和模拟面试
- 大厂内推和信息同步
- 职场经验分享
- 职业规划和发展分析

※ 星主和合伙人介绍

星主是公众号音视频开发进阶的作者,也是网站 https://glumes.com 的作者,曾参与过抖音、剪映等头部音视频 APP 底层 SDK 的开发。

合伙人也是在头条、快手从事音视频架构师的职位,具有多年的音视频开发经验,能力圈覆盖了音视频的绝大多数领域,资深音视频从业人员为你保驾护航。

微信公众号

扫描下面的二维码关注我的微信公众号《音视频开发进阶》,推送更多精彩内容!

添加我的微信 ezglumes 拉你入音视频与图形图像技术群一起交流学习~

wechat-account-qrcode

原创文章,转载请注明来源:    Seek策略以及在有B帧情况下的处理