FFmpeg4.0+SDL2.0笔记07:Seeking

2021-04-13 17:40:58 浏览数 (1)

环境

背景:在系统性学习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

0 人点赞