在播放视频过程中,想要跳过中间直接看精彩片段怎么办呢?或者看到精彩片段,想回到某个位置重新观看又该怎么办呢?所以播放器得提供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()函数调用流程如下:
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_seek2
if (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_frame
if (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;
}
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;
}
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_file
if (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;
else
min_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;
}
接下来我们看看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);
} else
ret = -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);
} else
return -1;
}
由此可见,该函数按照顺序有4个执行步骤:
我们看看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;
}
再看看二分查找方式的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;
}
}
// 根据时间戳搜索对应position
pos = 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跳转到指定position
if ((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函数又调用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;
}
最后看看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 &&
timestamp < 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跳转到指定position
if (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;
// 更新当前dts
ff_update_cur_dts(s, st, ie->timestamp);
return 0;
}
seek_frame_generic函数主要有3个步骤:
至此,av_seek_frame()和avformat_seek_file()函数分析完毕。