FFmpeg源码分析:av_seek_frame()与avformat_seek_file()
在播放視頻過程中,想要跳過中間直接看精彩片段怎么辦呢?或者看到精彩片段,想回到某個位置重新觀看又該怎么辦呢?所以播放器得提供seek操作實現快進快退功能,FFmpeg在libavformat模塊提供此功能的API,av_seek_frame()屬于舊版API,而avformat_seek_file()屬于新版API并且兼容舊版本。
首先,我們來看看av_seek_frame()函數定義,位于libavformat/avformat.h。根據描述,該函數用于移動到指定時間戳的關鍵幀位置,其定義如下:
/*** Seek to the keyframe at timestamp.** @param s media file handle* @param stream_index If stream_index is (-1), a default* stream is selected, and timestamp is automatically converted* from AV_TIME_BASE units to the stream specific time_base.* @param timestamp Timestamp in AVStream.time_base units* @param flags flags which select direction and seeking mode* @return >= 0 on success*/ int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);然后對比一下,新版本API的avformat_seek_file()函數定義,同樣位于libavformat/avformat.h。根據描述,移動到時間戳最近鄰的位置(在min_ts與max_ts范圍內),其定義如下:
/*** Seek to timestamp ts.* Seeking will be done so that the point from which all active streams* can be presented successfully will be closest to ts and within min/max_ts.** If flags contain AVSEEK_FLAG_BYTE, then all timestamps are in bytes.* If flags contain AVSEEK_FLAG_FRAME, then all timestamps are in frames.* If flags contain AVSEEK_FLAG_ANY, then non-keyframes are treated as keyframes.* If flags contain AVSEEK_FLAG_BACKWARD, it is ignored.** @param s media file handle* @param stream_index index of the stream which is used as time base reference* @param min_ts smallest acceptable timestamp* @param ts target timestamp* @param max_ts largest acceptable timestamp* @param flags flags* @return >=0 on success, error code otherwise*/ int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);上面提及的flag是seek模式,總共有4種模式,聲明也是位于libavformat/avformat.h。AVSEEK_FLAG_BACKWARD是往回seek,即seek到時間戳的上一個關鍵幀;AVSEEK_FLAG_BYTE是以字節數方式seek;AVSEEK_FLAG_ANY支持seek到任意音視頻幀,包括非關鍵幀,即精準seek,會比較耗時;AVSEEK_FLAG_FRAME是以幀數量方式seek。具體描述如下:
#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?avformat_seek_file()函數調用流程如下:
1、avformat_seek_file
avformat_seek_file函數優先調用read_seek2函數,如果不支持read_seek2就回退到舊版的av_seek_frame函數。具體調用流程如下:
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts,int64_t ts, int64_t max_ts, int flags) {if (min_ts > ts || max_ts < ts)return -1;if (stream_index < -1 || stream_index >= (int)s->nb_streams)return AVERROR(EINVAL);if (s->seek2any>0)flags |= AVSEEK_FLAG_ANY;flags &= ~AVSEEK_FLAG_BACKWARD;// 優先調用read_seek2if (s->iformat->read_seek2) {int ret;ff_read_frame_flush(s);if (stream_index == -1 && s->nb_streams == 1) {AVRational time_base = s->streams[0]->time_base;ts = av_rescale_q(ts, AV_TIME_BASE_Q, time_base);min_ts = av_rescale_rnd(min_ts, time_base.den,time_base.num * (int64_t)AV_TIME_BASE,AV_ROUND_UP | AV_ROUND_PASS_MINMAX);max_ts = av_rescale_rnd(max_ts, time_base.den,time_base.num * (int64_t)AV_TIME_BASE,AV_ROUND_DOWN | AV_ROUND_PASS_MINMAX);stream_index = 0;}ret = s->iformat->read_seek2(s, stream_index, min_ts,ts, max_ts, flags);if (ret >= 0)ret = avformat_queue_attached_pictures(s);return ret;}// 回退舊版API,使用av_seek_frameif (s->iformat->read_seek || 1) {int dir = (ts - (uint64_t)min_ts > (uint64_t)max_ts - ts ? AVSEEK_FLAG_BACKWARD : 0);int ret = av_seek_frame(s, stream_index, ts, flags | dir);if (ret<0 && ts != min_ts && max_ts != ts) {ret = av_seek_frame(s, stream_index, dir ? max_ts : min_ts, flags | dir);if (ret >= 0)ret = av_seek_frame(s, stream_index, ts, flags | (dir^AVSEEK_FLAG_BACKWARD));}return ret;}return -1; }2、read_seek2與read_seek
read_seek2和read_seek屬于AVInputFormat結構體的函數指針。以mp4封裝格式為例(位于libavformat/mov.c),對應的AVInputFormat結構體如下:
AVInputFormat ff_mov_demuxer = {.name = "mov,mp4,m4a,3gp,3g2,mj2",.long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),.priv_class = &mov_class,.priv_data_size = sizeof(MOVContext),.extensions = "mov,mp4,m4a,3gp,3g2,mj2,psp,m4b,ism,ismv,isma,f4v",.read_probe = mov_probe,.read_header = mov_read_header,.read_packet = mov_read_packet,.read_close = mov_read_close,.read_seek = mov_read_seek,.flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS, };?由此可見,read_seek2的函數還沒有實現,而read_seek函數指針指向mov_read_seek()。具體實現如下:
static int mov_read_seek(AVFormatContext *s, int stream_index, int64_t sample_time, int flags) {MOVContext *mc = s->priv_data;AVStream *st;int sample;int i;if (stream_index >= s->nb_streams)return AVERROR_INVALIDDATA;st = s->streams[stream_index];sample = mov_seek_stream(s, st, sample_time, flags);if (sample < 0)return sample;if (mc->seek_individually) {/* adjust seek timestamp to found sample timestamp */int64_t seek_timestamp = st->index_entries[sample].timestamp;st->internal->skip_samples = mov_get_skip_samples(st, sample);for (i = 0; i < s->nb_streams; i++) {int64_t timestamp;st = s->streams[i];if (stream_index == i)continue;timestamp = av_rescale_q(seek_timestamp, s->streams[stream_index]->time_base, st->time_base);sample = mov_seek_stream(s, st, timestamp, flags);if (sample >= 0)st->internal->skip_samples = mov_get_skip_samples(st, sample);}} else {for (i = 0; i < s->nb_streams; i++) {MOVStreamContext *sc;st = s->streams[i];sc = st->priv_data;mov_current_sample_set(sc, 0);}while (1) {MOVStreamContext *sc;AVIndexEntry *entry = mov_find_next_sample(s, &st);if (!entry)return AVERROR_INVALIDDATA;sc = st->priv_data;if (sc->ffindex == stream_index && sc->current_sample == sample)break;mov_current_sample_inc(sc);}}return 0; }3、av_seek_frame
av_seek_frame函數首先判斷是否存在iformat->read_seek2,如果存在就調用對應read_seek2(),如果不存在則調用seek_frame_internal()去執行seek操作:
int av_seek_frame(AVFormatContext *s, int stream_index,int64_t timestamp, int flags) {int ret;// 如果存在read_seek2的API,調用avformat_seek_fileif (s->iformat->read_seek2 && !s->iformat->read_seek) {int64_t min_ts = INT64_MIN, max_ts = INT64_MAX;if ((flags & AVSEEK_FLAG_BACKWARD))max_ts = timestamp;elsemin_ts = timestamp;return avformat_seek_file(s, stream_index, min_ts, timestamp, max_ts,flags & ~AVSEEK_FLAG_BACKWARD);}// 調用內部的尋幀方法ret = seek_frame_internal(s, stream_index, timestamp, flags);if (ret >= 0)ret = avformat_queue_attached_pictures(s);return ret; }4、seek_frame_internal
接下來我們看看seek_frame_internal函數實現:
static int seek_frame_internal(AVFormatContext *s, int stream_index,int64_t timestamp, int flags) {int ret;AVStream *st;// 以字節數方式尋幀if (flags & AVSEEK_FLAG_BYTE) {if (s->iformat->flags & AVFMT_NO_BYTE_SEEK)return -1;ff_read_frame_flush(s);return seek_frame_byte(s, stream_index, timestamp, flags);}if (stream_index < 0) {stream_index = av_find_default_stream_index(s);if (stream_index < 0)return -1;st = s->streams[stream_index];timestamp = av_rescale(timestamp, st->time_base.den,AV_TIME_BASE * (int64_t) st->time_base.num);}// 以指定方式尋幀if (s->iformat->read_seek) {ff_read_frame_flush(s);ret = s->iformat->read_seek(s, stream_index, timestamp, flags);} elseret = -1;if (ret >= 0)return 0;if (s->iformat->read_timestamp &&!(s->iformat->flags & AVFMT_NOBINSEARCH)) {ff_read_frame_flush(s);// 以二分查找方式尋幀return ff_seek_frame_binary(s, stream_index, timestamp, flags);} else if (!(s->iformat->flags & AVFMT_NOGENSEARCH)) {ff_read_frame_flush(s);// 以通用方式尋幀return seek_frame_generic(s, stream_index, timestamp, flags);} elsereturn -1; }由此可見,該函數按照順序有4個執行步驟:
5、seek_frame_byte
我們看看byte方式的seek操作,直接調用avio_seek移動到指定位置:
static int seek_frame_byte(AVFormatContext *s, int stream_index,int64_t pos, int flags) {int64_t pos_min, pos_max;pos_min = s->internal->data_offset;pos_max = avio_size(s->pb) - 1;if (pos < pos_min)pos = pos_min;else if (pos > pos_max)pos = pos_max;avio_seek(s->pb, pos, SEEK_SET);s->io_repositioned = 1;return 0; }6、ff_seek_frame_binary
再看看二分查找方式的seek操作:
int ff_seek_frame_binary(AVFormatContext *s, int stream_index,int64_t target_ts, int flags) {const AVInputFormat *avif = s->iformat;int64_t av_uninit(pos_min), av_uninit(pos_max), pos, pos_limit;int64_t ts_min, ts_max, ts;int index;int64_t ret;AVStream *st;if (stream_index < 0)return -1;st = s->streams[stream_index];if (st->index_entries) {AVIndexEntry *e;// 根據時間戳找到對應的音視頻幀索引index = av_index_search_timestamp(st, target_ts, flags | AVSEEK_FLAG_BACKWARD);index = FFMAX(index, 0);e = &st->index_entries[index];if (e->timestamp <= target_ts || e->pos == e->min_distance) {pos_min = e->pos;ts_min = e->timestamp;} else {av_assert1(index == 0);}index = av_index_search_timestamp(st, target_ts, flags & ~AVSEEK_FLAG_BACKWARD);if (index >= 0) {e = &st->index_entries[index];av_assert1(e->timestamp >= target_ts);pos_max = e->pos;ts_max = e->timestamp;pos_limit = pos_max - e->min_distance;}}// 根據時間戳搜索對應positionpos = ff_gen_search(s, stream_index, target_ts, pos_min, pos_max, pos_limit,ts_min, ts_max, flags, &ts, avif->read_timestamp);if (pos < 0)return -1;// 調用avio_seek跳轉到指定positionif ((ret = avio_seek(s->pb, pos, SEEK_SET)) < 0)return ret;ff_read_frame_flush(s);ff_update_cur_dts(s, st, ts);return 0; }由此可見,?ff_seek_frame_binary函數主要有3個步驟:
- 調用av_index_search_timestamp()根據時間戳尋找數組索引;
- 根據時間戳搜索對應position;
- 調用avio_seek跳轉到指定position;
?其中,av_index_search_timestamp函數又調用ff_index_search_timestamp函數,如下所示:
int av_index_search_timestamp(AVStream *st, int64_t wanted_timestamp, int flags) {return ff_index_search_timestamp(st->index_entries, st->nb_index_entries,wanted_timestamp, flags); }?我們來看看ff_index_search_timestamp函數的具體實現,主要用二分查找法根據指定時間戳查找數組對應的下標索引:
int ff_index_search_timestamp(const AVIndexEntry *entries, int nb_entries,int64_t wanted_timestamp, int flags) {int a, b, m;int64_t timestamp;a = -1;b = nb_entries;if (b && entries[b - 1].timestamp < wanted_timestamp)a = b - 1;// 二分查找法while (b - a > 1) {m = (a + b) >> 1;while ((entries[m].flags & AVINDEX_DISCARD_FRAME) && m < b && m < nb_entries - 1) {m++;if (m == b && entries[m].timestamp >= wanted_timestamp) {m = b - 1;break;}}timestamp = entries[m].timestamp;if (timestamp >= wanted_timestamp)b = m;if (timestamp <= wanted_timestamp)a = m;}m = (flags & AVSEEK_FLAG_BACKWARD) ? a : b;if (!(flags & AVSEEK_FLAG_ANY))while (m >= 0 && m < nb_entries &&!(entries[m].flags & AVINDEX_KEYFRAME))m += (flags & AVSEEK_FLAG_BACKWARD) ? -1 : 1;if (m == nb_entries)return -1;return m; }?而ff_gen_search()函數使用3種查找方式:插值查找、二分查找、線性查找。具體如下:
int64_t ff_gen_search(AVFormatContext *s, int stream_index, int64_t target_ts,int64_t pos_min, int64_t pos_max, int64_t pos_limit,int64_t ts_min, int64_t ts_max,int flags, int64_t *ts_ret,int64_t (*read_timestamp)(struct AVFormatContext *, int,int64_t *, int64_t)) {......while (pos_min < pos_limit) {if (no_change == 0) {int64_t approximate_keyframe_distance = pos_max - pos_limit;// 插值查找pos = av_rescale(target_ts - ts_min, pos_max - pos_min,ts_max - ts_min) +pos_min - approximate_keyframe_distance;} else if (no_change == 1) {// 二分查找pos = (pos_min + pos_limit) >> 1;} else {// 線性查找pos = pos_min;}......// 讀取時間戳ts = ff_read_timestamp(s, stream_index, &pos, INT64_MAX, read_timestamp);......}pos = (flags & AVSEEK_FLAG_BACKWARD) ? pos_min : pos_max;ts = (flags & AVSEEK_FLAG_BACKWARD) ? ts_min : ts_max;*ts_ret = ts;return pos; }7、seek_frame_generic
?最后看看seek_frame_generic函數實現:
static int seek_frame_generic(AVFormatContext *s, int stream_index,int64_t timestamp, int flags) {int index;int64_t ret;AVStream *st;AVIndexEntry *ie;st = s->streams[stream_index];// 根據時間戳找到對應的音視頻幀索引index = av_index_search_timestamp(st, timestamp, flags);if (index < 0 && st->nb_index_entries &×tamp < st->index_entries[0].timestamp)return -1;if (index < 0 || index == st->nb_index_entries - 1) {AVPacket *pkt = s->internal->pkt;int nonkey = 0;// 調用avio_seek跳轉到指定positionif (st->nb_index_entries) {av_assert0(st->index_entries);ie = &st->index_entries[st->nb_index_entries - 1];if ((ret = avio_seek(s->pb, ie->pos, SEEK_SET)) < 0)return ret;ff_update_cur_dts(s, st, ie->timestamp);} else {if ((ret = avio_seek(s->pb, s->internal->data_offset, SEEK_SET)) < 0)return ret;}av_packet_unref(pkt);for (;;) {int read_status;do {read_status = av_read_frame(s, pkt);} while (read_status == AVERROR(EAGAIN));if (read_status < 0)break;if (stream_index == pkt->stream_index && pkt->dts > timestamp) {if (pkt->flags & AV_PKT_FLAG_KEY) {av_packet_unref(pkt);break;}if (nonkey++ > 1000 && st->codecpar->codec_id != AV_CODEC_ID_CDGRAPHICS) {av_packet_unref(pkt);break;}}av_packet_unref(pkt);}index = av_index_search_timestamp(st, timestamp, flags);}if (index < 0)return -1;ff_read_frame_flush(s);if (s->iformat->read_seek)if (s->iformat->read_seek(s, stream_index, timestamp, flags) >= 0)return 0;ie = &st->index_entries[index];if ((ret = avio_seek(s->pb, ie->pos, SEEK_SET)) < 0)return ret;// 更新當前dtsff_update_cur_dts(s, st, ie->timestamp);return 0; }seek_frame_generic函數主要有3個步驟:
- 調用av_index_search_timestamp()根據時間戳尋找數組索引;
- 調用avio_seek跳轉到指定position;
- 調用ff_update_cur_dts()更新當前dts;
至此,av_seek_frame()和avformat_seek_file()函數分析完畢。
總結
以上是生活随笔為你收集整理的FFmpeg源码分析:av_seek_frame()与avformat_seek_file()的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BootStrap框架的优缺点
- 下一篇: Linpack安装