赞
踩
基于 ffmpeg 4.4.2
播放器检测到有 seek 请求的整个过程如下:
read_thread 线程:
video_thread 线程:
对于 mp4 来说,其本身具有帧索引,并且有对应的 seek 函数 – mov_read_seek
由 mp4 的结构我们可以知道,由 seek 时间得到 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++
**************************************************/
如果是 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
****************************************************/
有了索引表 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 进行转换
当 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: | | |
* +-------------+----------------------------------------------+
*
*/
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
再来看看 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
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 帧才进行输出
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。