环境
背景:在系统性学习FFmpeg时,发现官方推荐教程还是15年的,不少接口已经弃用,大版本也升了一级,所以在这里记录下FFmpeg4.0 SDL2.0的学习过程。
win10,VS2019,FFmpeg4.3.2,SDL2.0.14
原文地址:http://dranger.com/ffmpeg/tutorial07.html
实现seek
现在我们要给播放器加上seek功能,这一章将会展示如何使用av_seek_frame。
实现效果是这样的:按下左右方向键时我们让视频前进/后退10s,按上下方向键让视频前进/后退60s,因此程序首先要能够捕捉按键事件。
代码语言:javascript复制
typedef struct VideoState {
...
int seekReq; //是否需要seek
int seekFlags; //前进/后退
int64_t seekPos; //seek进度
AVPacket flushPacket;
...
}VideoState;
for (;;) {
double increment = 0, pos;
SDL_WaitEvent(&event);
switch (event.type)
{
case SDL_KEYDOWN:
switch (event.key.keysym.sym)
{
case SDLK_LEFT:
increment = -10.0;
break;
case SDLK_RIGHT:
increment = 10.0;
break;
case SDLK_UP:
increment = 60.0;
break;
case SDLK_DOWN:
increment = -60.0;
break;
default:
break;
}
if (increment != 0) {
pos = getMasterClock(pVideoState);
pos = increment;
streamSeek(pVideoState, (int64_t)(pos * AV_TIME_BASE), increment);
}
break;
来看看SDL_KEYDOWN事件,event.key.keysym.sym表明具体按了哪个键,该调整多少秒。
之后我们通过getMasterClock获取当前播放进度,计算出目标进度,再通过streamSeek方法更新共享的seek标志位和进度。读取音视频包的线程检测到后,再调用av_seek_frame,实现seek功能。
下面是streamSeek方法的具体实现,seekReq代表是否要seek,seekFlags 代表前进还是后退。
代码语言:javascript复制void streamSeek(VideoState* pVideoState, int64_t pos, double increment)
{
if (!pVideoState->seekReq) {
pVideoState->seekPos = pos;
pVideoState->seekFlags = increment < 0 ? AVSEEK_FLAG_BACKWARD : 0;
pVideoState->seekReq = 1;
}
}
eventloop里处理完了,下面是读取音视频包的线程parseMediaFileThread,之前该线程只管av_read_frame,现在我们加上av_seek_frame。
av_seek_frame总共有4个参数,AVFormatContext,streamIndex,timestamp和包含了一堆标志位的flags。调用该方法将把视频包的读取进度seek到timestamp处。注意这里的timestamp不是秒,而是avcodec的内部单位,因此要做两处转换,先乘以AV_TIME_BASE,再用av_rescale_q转换(其实直接除以AVStream::time_base就行了)。而streamIndex我们填入videoStreamIndex或audioStreamIndex就行,默认填入-1也可以,但可能会出问题所以不推荐。
代码语言:javascript复制 if (pVideoState->seekReq) {
int streamIndex = -1;
int64_t seekTarget = pVideoState->seekPos;
if (pVideoState->iVideoStreamIndex >= 0) streamIndex = pVideoState->iVideoStreamIndex;
else if (pVideoState->iAudioStreamIndex >= 0)streamIndex = pVideoState->iAudioStreamIndex;
if (streamIndex >= 0) {
//#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
//第二个参数原先是AV_TIME_BASE_Q,但是AVRational两边的括号导致在vs2019里编译不通过
seekTarget = av_rescale_q(seekTarget, AVRational{ 1, AV_TIME_BASE }, pVideoState->pFormatCtx->streams[streamIndex]->time_base);
}
if (av_seek_frame(pVideoState->pFormatCtx, streamIndex, seekTarget, pVideoState->seekFlags) < 0) {
cerr << "av_seek_frame failed" << endl;
}
else {
// 清空缓存队列
...
}
pVideoState->seekReq = 0;
}
清空缓存队列
以上seek部分就完成了,但我们还有一些事要做。记得音频队列视频队列吗,它们缓存了之前读取的音视频包,如果不清空的话就无法即时seek,不仅如此,avcodec内部也有缓存需要我们调接口来清空。
清空队列在av_seek_frame后就能做,清空avcodec缓存则需要在解码线程里去做。如何来协调不同线程呢?思路是这样的:清空队列后立刻给队列塞入一个特殊的flush packet,解码线程检测到该packet后,立刻清空avcodec缓存,然后等待下一个packet的到来。
清空队列很简单,就是删除链表的所有节点。下面是调用部分
代码语言:javascript复制
// 清空缓存队列
if (pVideoState->iVideoStreamIndex >= 0) {
pVideoState->videoQueue.flush();
pVideoState->videoQueue.push(&pVideoState->flushPacket);
}
if (pVideoState->iAudioStreamIndex >= 0) {
pVideoState->audioQueue.flush();
pVideoState->audioQueue.push(&pVideoState->flushPacket);
}
视频解码线程部分,音频解码线程同理
代码语言:javascript复制 if (packet.data == pVideoState->flushPacket.data) {
avcodec_flush_buffers(pCodecCtx);
continue;
}
别忘了初始化,以及入队的条件做一点调整
代码语言:javascript复制 av_init_packet(&pVideoState->flushPacket);
pVideoState->flushPacket.data = (uint8_t*)"FLUSH";
pVideoState->videoQueue.setFlushPacket(&pVideoState->flushPacket);
pVideoState->audioQueue.setFlushPacket(&pVideoState->flushPacket);
class PacketQueue {
public:
...
int push(AVPacket* packet) {
if (packet != flushPacket_ && av_packet_make_refcounted(packet) < 0)
return -1;
...
}
void setFlushPacket(AVPacket* flushPacket) {
flushPacket_ = flushPacket;
}
private:
...
AVPacket* flushPacket_;
};
以上就是seek功能的全部内容。
代码:https://github.com/onlyandonly/ffmpeg_sdl_player