前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FFmpeg4.0+SDL2.0笔记07:Seeking

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

原创
作者头像
非一
修改2021-04-13 17:40:58
6150
修改2021-04-13 17:40:58
举报
文章被收录于专栏:非一

环境

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 环境
  • 实现seek
  • 清空缓存队列
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档