当前位置:   article > 正文

ffplay/ijkplayer播放器的seek机制_播放器seek

播放器seek


seek 机制以 ijkplayer(ffplay) 为例进行研究

基于 ffmpeg 4.4.2

1. 整体过程

播放器检测到有 seek 请求的整个过程如下:

read_thread 线程:

  1. 调用 avformat_seek_file 函数
  2. 清空各路缓存的包数据,并置入 flush_pkt 包,开始新的播放序列
  3. 开始缓冲

video_thread 线程:

  • 解码前调用 packet_queue_get 取包,如取到的是 flush_pkt,则调用 avcodec_flush_buffers 进行解码器缓冲重置

2. 内部的 seek 过程(以 mp4 为例)

对于 mp4 来说,其本身具有帧索引,并且有对应的 seek 函数 – mov_read_seek

由 mp4 的结构我们可以知道,由 seek 时间得到 sample 索引,由索引得到对应的文件偏移量(位置),具体为以下步骤:

  1. 知道 seek 时间,可在 stts 中找到 decoding 时间最大且小于 seek 时间的 sample( stts 记录了每个 sample 的 dts)
  2. 找到时间对应的 sample 后,由于该 sample 不一定是关键帧,可在 stss 找到编号最大且小于该 sample 的 sync sample(stss 记录了所有的 sync sample 的编号)
  3. 找到 sync sample 后,可在 stsc 找到该 sample 属于哪个 chunck( stsc 记录了 sample 怎么被归到不同的 chunk )
  4. 拿到 chunk 后,可在 stco 中找到该 chuck 对应的文件偏移( stco 记录了
    每个 chunk 的文件偏移)
  5. 知道 chunk 的文件偏移,再根据 stsc 和 stsz 找到 sync sample 的文件偏移( stsz 记录了所有 sample 的大小)

FFmpeg 中,在 avformat_open_input 函数读 moov 头中,事先建立了 mp4 的索引表,录入了每个 sample 的信息,在后面每次需要 seek 时直接访问索引表进行查找,就可以很方便了

mp4 的索引表建立主要是在 mov_build_index 函数中,过程简略为以下:

/**************************************************
 * for(i = 0; i < sc->chunk_count; i++) 遍历所有的chunk
 *                   ↓
 * 该chunk在stsc表中的位置,假设用stsc_index表示,
 * 根据stsc_index字段,可以知道该chunk有多少个sample
 *                   ↓
 * for (j = 0; j < sc->stsc_data[stsc_index].count; j++) 
 *                   ↓     
 * 根据sample查询stsz表,可以知道该sample的大小,即sample_sise
 *                   ↓ 
 * 根据chunk查询stco,可以知道该chunk的偏移量,即current_offset
 *                   ↓
 *    sample偏移量:         current_offset
 *    sample dts:           current_dts
 *    sample的大小:          sample_size
 *    sample距离最近的关键帧: distance
 *    sample是否为关键帧:    AVINDEX_KEYFRAME
 *                   ↓
 *    current_offset += sample_size 在chunk内的偏移量
 *    current_dts += sc->stts_data[stts_index].duration 
 *    distance++
 **************************************************/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如果是 fmp4,也是事先建立索引表,过程简略如下:

/****************************************************
 * 由每一个moof可得到对应的moof_offset,sample_count
 *                   ↓
 * 读取trun,第一个sample偏移:offset+moof_offset
 *                   ↓
 * for (i = 0; i < entries && !pb->eof_reached; i++)
 *                   ↓
 *               pos += size
 *       size += trun中的sample_size
 *   timestamp += trun中的sample_duration
 ****************************************************/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

有了索引表 AVIndexEntry*, seek 的时候就可以二分查找, 在 mov_read_seek -> mov_seek_stream -> av_index_search_timestamp -> ff_index_search_timestamp 中进行, 也即 ff_index_search_timestamp 函数中执行二分查找的过程

经过不断的二分比较后,得出一个索引 m, 然后 m 再根据 seek 的 flags 标志进行调整:当 flags 不包含 AVSEEK_FLAG_ANY 的情况下, m 索引所在的 sample 不是关键帧, 会根据是否 AVSEEK_FLAG_BACKWARD 来对 m 进行加减操作,寻找到关键帧的索引,然后返回

得出索引后,在 mov_current_sample_set 中用 ff_index_search_timestamp 返回的索引去更新 MOVStreamContext* 中的 current_sample(当前索引),这样 seek后 继续读包时,调用到 mov_read_packet -> mov_find_next_sample,获取到的 sample 就是 AVIndexEntry[MOVStreamContext->current_sample] 对应的 sample

同样地,处理完视频流后,还需要对其他流也进行 seek,也是同样调用 mov_seek_stream,只不过传入的时间戳需要基于其他流的 time_base 进行转换

3. 基于字节位置 seek

当 flags 为 AVSEEK_FLAG_BYTE 时,走这一模式

主要调用 avio_seek,这个函数属于 FFmpeg IO层面,使用的主要结构体是 AVIOContext,其在内部提供了一个缓冲区,同时,也提供对资源不带缓冲的访问方式,这时候就直接使用底层的协议访问 API

AVIOContext 内部的缓冲区表示如下:

/*
 * The following shows the relationship between buffer, buf_ptr,
 * buf_ptr_max, buf_end, buf_size, and pos, when reading and when writing
 * (since AVIOContext is used for both):
 *
 **********************************************************************************
 *                                   READING
 **********************************************************************************
 *
 *                            |              buffer_size              |
 *                            |---------------------------------------|
 *                            |                                       |
 *
 *                         buffer          buf_ptr       buf_end
 *                            +---------------+-----------------------+
 *                            |/ / / / / / / /|/ / / / / / /|         |
 *  read buffer:              |/ / consumed / | to be read /|         |
 *                            |/ / / / / / / /|/ / / / / / /|         |
 *                            +---------------+-----------------------+
 *
 *                                                         pos
 *              +-------------------------------------------+-----------------+
 *  input file: |                                           |                 |
 *              +-------------------------------------------+-----------------+
 *
 */
/*
 **********************************************************************************
 *                                   WRITING
 **********************************************************************************
 *
 *                             |          buffer_size                 |
 *                             |--------------------------------------|
 *                             |                                      |
 *
 *                                                buf_ptr_max
 *                          buffer                 (buf_ptr)       buf_end
 *                             +-----------------------+--------------+
 *                             |/ / / / / / / / / / / /|              |
 *  write buffer:              | / / to be flushed / / |              |
 *                             |/ / / / / / / / / / / /|              |
 *                             +-----------------------+--------------+
 *                               buf_ptr can be in this
 *                               due to a backward seek
 *
 *                            pos
 *               +-------------+----------------------------------------------+
 *  output file: |             |                                              |
 *               +-------------+----------------------------------------------+
 *
 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

avio_seek 主要就是计算按指定方式seek后,最终文件的当前偏移量 AVIOContext->buf_ptr,分四类情况进行 seek 操作,并保证最终的文件偏移量在 AVIOContext 内部缓冲区的覆盖范围内,保证 AVIOContext 中关于缓冲区的相关指针都正确的赋值

四类情况:

  • 读写,时间上向前 seek,不支持直接底层 seek,文件偏移位置在 buffer 缓冲区有效范围内:直接更新AVIOContext->buf_ptr

  • 读,时间上向前 seek,不支持直接底层 seek,文件最终偏移不在当前缓冲有效范围内:循环调用 fill_buffer() 读取数据到缓冲区中直到偏移在缓冲区内;更新 AVIOContext->buf_ptr

  • 读,时间上向后seek,往后 seek 大小 < buffer_size / 2,支持直接底层 seek:计算底层 seek 后 buffer 起始对应的文件中的 pos,执行底层 seek,初始化所有指针并使用 fill_buffer 填充数据,此时将保持缓冲区含有有效数据。最后 avio_seek,将又进入情况一

  • 读写,支持直接底层 seek:执行底层的 seek, 此时,缓冲区中最终将不含有效数据,更新 AVIOContext->buf_ptr 指向缓冲区的起始位置

注:底层 seek,如果是 http 协议则是 http.c 中的 http_seek, 如果是 file 协议则是 file.c 中的 file_seek

4. seek 的 flags 标志位

再来看看 seek 的四种 flags 标志位

#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
#define AVSEEK_FLAG_BYTE     2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY      4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME    8 ///< seeking based on frame number
  • 1
  • 2
  • 3
  • 4

AVSEEK_FLAG_BACKWARD:seek 到离请求的 timestamp 之前的最近的关键帧

AVSEEK_FLAG_BYTE:seek 点对应文件中的位置(字节表示)

AVSEEK_FLAG_FRAME:seek 到离请求的 timestamp 之后的最近的关键帧

AVSEEK_FLAG_ANY: seek 到任意一帧,可能是非关键帧。在测试中,read_thread seek 后取包送入 packet队列中,video_thread 调用 decoder_decode_frame 进行解码;如果是非关键帧,则 avcodec_receive_frame 不会输出 frame,也即从 seek 的位置开始读取并解码,但直到解码 I 帧才进行输出

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/647026
推荐阅读
相关标签
  

闽ICP备14008679号