播放器中的seek操作实现
详解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线程发送当前位置的信号用于更新
同时恢复定位前的状态
更多推荐



所有评论(0)