一、实现seek功能
我们主要是使用ffmpeg的av_seek_frame
方法实现seek功能,但是我们给外界提供seek功能的方法可以不叫seek,我这里使用setTime
方法,单位是秒。
/**
* 移动视频到指定的关键帧位置
*
* @param s 输入媒体的上下文
* @param stream_index 流索引,要seek的那个流,-1代表默认情况,FFmpeg自动选择音频还是视频做seek操作,
* @param timestamp 起始位置的时间戳,单位是AVStream.time_base
* @param flags seek的具体策略
* @return >= 0 on success
*/
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,
int flags);
第三个参数timestamp的详细说明:
这里的timestamp代表的是想要移动到的起始位置的时间戳,注意这里是起始位置的时间戳,不是起始位置的秒数! 通俗地讲,它就是起始位置的pts,因此一个10s的视频,你想移动到5s的位置,直接传5是不对的。在 FFmpeg 中,时间戳(timestamp)的单位是时间基数(time_base),时间戳值乘以时间基,可以得到实际的时刻值(以秒等为单位)。例如,如果一个视频帧的 dts 是 40,pts 是 160,其 time_base 是 1/1000 秒,那么可以计算出此视频帧的解码时刻是 40 毫秒(40/1000),显示时刻是 160 毫秒(160/1000)。FFmpeg 中时间戳(pts/dts)的类型是int64_t 类型,如果把一个 time_base 看作一个时钟脉冲,那么 dts/pts 则可以看作是时钟脉冲的计数。第四个参数flags的详细说明:
该参数一共有以下四种具体取值:
#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
- AVSEEK_FLAG_BACKWARD
则优, 例如:拖到8的位置是B帧,则找附件的关键帧6,如果找不到它会花屏- AVSEEK_FLAG_BYTE
基于字节位置的跳转- AVSEEK_FLAG_ANY
(老实)直接精准到拖动的位置,问题:如果不是关键帧,B帧 可能回造成花屏情况- AVSEEK_FLAG_FRAME
找关键帧(非常不准确,可能会跳的太多),一般不会直接用,但是会配合用可以参考:
音视频技术应用(18)- 控制播放进度——av_seek_frame()
音视频从入门到精通——FFmpeg之av_seek_frame函数分析
我们在使用av_read_frame
方法不断的从视频文件中读取数据出来,当我们想要实现seek的时候,就需要重新地位到seek的地方,在重新使用av_read_frame
方法开始读取数据,因此av_seek_frame
方法需要在av_read_frame
方法之前使用。
// 现实时间 -> 时间戳
AVRational timeBase = _fmtCtx->streams[streamIdx]->time_base;
int64_t ts = _seekTime / av_q2d(timeBase);
ret = av_seek_frame(_fmtCtx, streamIdx, ts, AVSEEK_FLAG_BACKWARD);
if(ret < 0){// seek失败
qDebug() << "seek失败" << _seekTime << ts << streamIdx;
_seekTime = -1;
}else{// seek成功
qDebug() << "seek成功" << _seekTime << ts << streamIdx;
// 清空之前读取的数据包
clearAudioPktList();
clearVideoPktList();
_vSeekTime = _seekTime;
_aSeekTime = _seekTime;
_seekTime = -1;
// 恢复时钟
_aTime = 0;
_vTime = 0;
}
二、解决点击seek操作时会出现画面快速闪过
上面代码进行运行后进行seek会发现,在seek到那个位置的时候,会出现画面快速的闪过,而且快速闪过的画面是seek点附近的画面。
我们同在videoplayer_video.cpp
里的decodeVideo
方法里的最后添加一条打印qDebug()<< "渲染了一帧"<< _vTime << _aTime;
通过上面的日志打印,可以看到我们要seek到89,音频到了88确实差不多到了89,但是视频没有直接到89,而是经历了好多帧,因此_vTime < _aTime
,所以下面代码中while (_vTime > _aTime && _state == Playing)
不成立,就会走下面的代码,就会送到解码中去显示,就会出现画面快速闪过。
找到原因了,我们就知道该如何去解决了,我们可以把那些多余的帧扔掉
从上面我们知道音频也不是很准确的定位到seek的位置,因此音频也可以进行控制
三、实现暂停功能
我们在VideoPlayer
类里的pause
方法里修改了状态为暂停状态,所以我们要根据这个pause
状态来做事,而要实现暂停功能,有两个方面要暂停,一个是音频暂停还有一个是视频暂停
3.1、音频暂停
我们需要在videoplayer_audio.cpp
里的sdlAudioCallback
方法里添加if判断
3.2、视频暂停
我们需要在videoplayer_video.cpp
里的decodeVideo
方法里添加if判读
这里我们if判断不能跟音频一样使用break
,因为我们的decodeVideo
方法是在子线程中执行的,你一但break
退出循环了,那么这个子线程就销毁了,就再也没有机会解码视频了,所以这里需要用continue
。
我们还要实现暂停的时候能够实现seek功能,所以还要判断_vSeekTime == -1
表面当前没有seek操作