详解av_seek_frame()函数

int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);

参数说明:

s :指向已打开的媒体文件格式上下文(通过 avformat_open_input() 创建),包含媒体文件的所有流信息、格式信息等

stream_index: 指定用于定位的参考流索引,取值有两种情况:-1表示自动选择合适的流,也可指           定具体流索引(0,1)。

timestamp:目标定位的时间戳(单位:流的时间基 time_base 对应的单位),而非秒或毫秒。

flag: 定位策略标志,控制 seek 的行为,常用取值:

  • AVSEEK_FLAG_BACKWARD:定位到小于或等于目标时间戳的最近位置(默认行为)。
  • AVSEEK_FLAG_FRAME:timestamp 被视为帧索引(而非时间戳),适用于按帧定位的场景。
  • AVSEEK_FLAG_ANY:允许定位到非关键帧(可能导致后续解码出现花屏,需配合重新解码关键帧使用)。
  • AVSEEK_FLAG_BYTE:timestamp 被视为文件字节位置(而非时间戳),适用于按字节定位。
  • 组合使用:如 AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_ANY,表示允许定位到非关键帧且取小于目标的最近位置。

返回值:

0 : 表示定位成功

负数(-1):表示定位失败

播放器中定位到指定位置

bool Demux::Seek(double pos)
{
	mux.lock();
	if (!ic)
	{
		mux.unlock();
		return false;
	}

	avformat_flush(ic);
	long long seekPos = 0;
	seekPos = ic->streams[videoStreamIdx]->duration * pos;
	int ret =  av_seek_frame(ic, videoStreamIdx, seekPos, AVSEEK_FLAG_FRAME | AVSEEK_FLAG_BACKWARD);
	if (ret < 0) {
		mux.unlock();
		return false;
	}
	mux.unlock();
	return true;
}

首先传进来的pos是相对位置也就是QSlider::value()/QSlider::maximum()

seek时首先要清空格式上下文中缓存的数据,因为ffmpeg在读取媒体文件时会缓存一定量的AVPacket,例如正在播放第十秒的数据,他可能会缓存10~12秒的数据,如果在重新定位前不清空缓存的数据,导致定位后可能任然会播放一定时间定位前的画面。

计算seekPos为要定位的具体位置,总时长 * 相对位置

最后执行定位

整体定位功能的实现

void DemuxThread::seek(double pos)
{
	Clear();
	mux.lock();
	bool state = isPause;
	mux.unlock();
	setPause(true);
	mux.lock();
	if (dex) {
		dex->Seek(pos);
	}
	long long seekPos = pos * dex->getTotalTime();
	while (!isExit) {
		AVPacket* pkt = dex->ReadVideo();
		if (!pkt)break;
		if (vt->RepaintPts(seekPos, pkt)) {
			this->pts = seekPos;
			break;
		}
	}
	mux.unlock();
	if (!state) {
		setPause(false);
	}
}

定位前需要执行Clear()操作,目的是清空VideoThread和AudioThread维护的存放AVPacket的队列以及解码器上下文中缓冲区的清空,避免定位后出现错误画面

记录当前的播放状态(暂停 or 播放),在定位前一律先暂停,然后调用上面Demux::Seek()进行定位,seekPos为定位后的具体位置

然后执行ReadVideo函数循环读取一个视频的AVPacket

AVPacket* Demux::ReadVideo()
{
	AVPacket* pkt = NULL;
	for (int i = 0; i < 20; i++) {
		pkt = Recv();
		if (pkt->stream_index == videoStreamIdx) {
			break;
		}
		av_packet_free(&pkt);
	}
	return pkt;
}

一般是20遍之内可以找到视频包

将这个视频包解码后渲染到界面

bool VideoThread::RepaintPts(long long pts, AVPacket* pkt)
{
	mux.lock();
	if (vdecode) {
		if (!vdecode->Send(pkt)) {
			mux.unlock();
			return true;
		}
	}
	AVFrame* frame = vdecode->Recv();
	if (!frame) {
		mux.unlock();
		return false;
	}
	if (frame->pts >= pts) {
		if (video) {
			video->Repaint(frame);
			mux.unlock();
			return true;
		}
	}
	av_frame_free(&frame);
	mux.unlock();
	return false;
}

首先如果包发送失败直接返回true结束流程

解码得到视频帧,若帧无效重返回false重新找视频包解码,若得到的视频帧的pts小于定位后的时间点那么同样是返回false重新找视频包解码,若解码视频帧大于等于定位后的时间点,那么就将这一帧渲染在界面上

同样渲染后记录当前pts用于后续持续更新滑动条,并且给ui线程发送当前位置的信号用于更新

同时恢复定位前的状态

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐