目录
播放跳转
在解码线程类中添加跳转的控制标志,记录是否需要跳转
在主线程的循环中判断此变量,当需要跳转的时候就执行跳转操作
跳转操作可以直接使用 FFMPEG 的跳转函数 av_seek_frame 来实现
函数原型如下: int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,int flags);
-
首先添加状态位
//==========================跳转控制==========================
int seek_req; //跳转标志 -- 读线程
int64_t seek_pos; //跳转位置 -- 微秒
int seek_flag_audio; //跳转标志 -- 用于音频线程
int seek_flag_video; //跳转标志 -- 用于视频线程
double seek_time; //跳转时间(秒) 值和 seek_pos相同
-
跳转的三个问题
跳转延迟
跳转时,音视频队列中未解码的数据包是可以供解码播放几秒钟的,所以每次跳转时,需要将其清空
花屏现象
除了音频队列,解码器中也保留了上一帧视频的信息,而视频发生跳转时,解码器中保留的信息会对解码当前帧产生影响
例如:从3s处跳转到15min处,则3s处的包还残留在队列和解码器中,与15min处的结合会产生"花屏"
因此,清空队列的时候我们也要同时清空解码器的数据
这里的操作是,往队列中放入一个特殊的 packet,当解码线程取到这个 packet 的时候,就执行清除解码器数据的操作
在解码器类头文件中定义“清空”队列包的宏
#define FLUSH_DATA "FLUSH"
然后在主线程循环解码中,退出线程与暂停之间加入跳转的判断,因为在逻辑上,暂停时也可以跳转
此处需要注意一个细节:
当发生跳转时,由于前述的音视频同步操作是采用视频同步到音频上的方式进行,向前跳转音频时钟会变小,一定要重新将视频时钟归零,否则会一直SDL_Delay等待音频
// 跳转要放在暂停前面 暂停的时候也可以跳转
// 当跳转标志位 seek_req --> 1 清除队列里的缓存
if( m_videoState.seek_req )
{
int stream_index = -1;
int64_t seek_target = m_videoState.seek_pos;//微秒
if (m_videoState.videoStream >= 0)
stream_index = m_videoState.videoStream;
else if (m_videoState.audioStream >= 0)
stream_index = m_videoState.audioStream;
//时间基
AVRational aVRational = {1, AV_TIME_BASE};
if (stream_index >= 0)
{
//计算跳转到的位置 单位为微秒
seek_target = av_rescale_q(seek_target, aVRational,m_videoState.pFormatCtx->streams[stream_index]->time_base);
}
if (av_seek_frame(m_videoState.pFormatCtx, stream_index, seek_target,AVSEEK_FLAG_BACKWARD) < 0)
{
//跳转失败处理
fprintf(stderr, "%s: error while seeking\n",m_videoState.pFormatCtx->filename);
}else
{
//跳转成功
//处理音频队列
if (m_videoState.audioStream >= 0)
{
//分配一个 packet
AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));
av_new_packet(packet, 10);
//将FLUSH_DATA 清空标志 写入到包里
strcpy((char*)packet->data,FLUSH_DATA);
//清除队列
m_videoState.audioq->packet_queue_flush();
//往队列中存入用来清除的包
m_videoState.audioq->packet_queue_put(packet);
}
//处理视频队列 操作同上
if (m_videoState.videoStream >= 0)
{
AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));
av_new_packet(packet, 10);
strcpy((char*)packet->data,FLUSH_DATA);
m_videoState.videoq->packet_queue_flush();
m_videoState.videoq->packet_queue_put(packet);
//考虑到向左跳转 避免卡死
//向左跳转 音频时钟会变小 重新将视频时钟归零 否则会一直SDL_Delay等待音频
m_videoState.video_clock = 0;
}
}
//主线程的 跳转标志重新置为0 主线程继续进行处理
m_videoState.seek_req = 0;
//seek_time 精确到微秒
m_videoState.seek_time = m_videoState.seek_pos;
//音频、视频解码线程 跳转处理标志
m_videoState.seek_flag_audio = 1;
m_videoState.seek_flag_video = 1;
}
然后在视频解码线程中添加以下操作
//读取包后 比对包内容 如果是 清空队列的包
if(strcmp((char*)packet->data,FLUSH_DATA) == 0)
{
//调用函数清空解码器缓存 释放当前包
avcodec_flush_buffers(is->video_st->codec);
av_free_packet(packet);
//!!!视频时钟归零 否则 向左跳转时会一直等待音频时钟走到原来的位置才继续解码播放
is->video_clock = 0;
continue;
}
在音频解码中添加以下操作
//比对当前音频包 是不是清空队列包
if(strcmp((char*)pkt.data,FLUSH_DATA) == 0)
{
//如果是 清除解码器缓存
avcodec_flush_buffers(is->audio_st->codec);
av_free_packet(&pkt);
continue;
}
关键帧跳转
解码器解码时必须有关键帧的信息才能进行解码,因此跳转功能只能通过关键帧跳转
例如:关键帧在10s 和 15s处,则要跳转到13s位置,正确的做法是:跳转到10s位置,将10-13s的包全部丢弃,等待3s后继续解码播放
在计算视频帧时间戳后,添加判断
if (is->seek_flag_video)
{
//如果跳转的关键帧在需要跳转位置之前
if (video_pts < is->seek_time)
{
//丢弃当前帧 重新循环
av_free_packet(packet);
continue;
}else
{
is->seek_flag_video = 0;
}
}
在计算音频帧时间戳后,添加判断
if( is->seek_flag_audio)
{
//没有到目的时间
if( is ->audio_clock < is->seek_time)
{
if( pkt.pts != AV_NOPTS_VALUE)
{
//重新计算音频时钟
is->audio_clock = av_q2d( is->audio_st->time_base )*pkt.pts *1000000 ;
}
break;
}
else
{
if( pkt.pts != AV_NOPTS_VALUE)
{
is->audio_clock = av_q2d( is->audio_st->time_base )*pkt.pts *1000000 ;
}
is->seek_flag_audio = 0 ;
}
}
要注意这里一个细节,由于计算音频时间戳是采用:上一帧的音频时间戳 + 一帧音频时间的长度 的方式
所以在向前跳转时,音频时钟不能减小,需要重新给音频时钟赋值
-
UI界面控制
Qt自带的进度条默认只能拖动实现跳转,在这里我们想做到点击位置直接跳转的方式,就需要对Slider类的鼠标按下事件进行重写
添加一个C++类,继承QSlider,添加进度条值改变的信号 和 鼠标按下事件的重写
#ifndef VIDEOSLIDER_H
#define VIDEOSLIDER_H
#include <QSlider>
class VideoSlider : public QSlider
{
Q_OBJECT
public:
explicit VideoSlider(QWidget *parent = 0);
signals:
void SIG_valueChanged(int);
protected:
void mousePressEvent(QMouseEvent *event);
public slots:
};
#endif // VIDEOSLIDER_H
重写mousePressEvent
void VideoSlider::mousePressEvent(QMouseEvent *event)
{
int value = QStyle::sliderValueFromPosition(minimum(), maximum(), event->pos().x(), width());
setValue(value);
emit SIG_valueChanged(value);
}
将进度条提升为自己定义的VideoSlider
在UI类中添加connect处理信号SIG_valueChanged
//进度条改变
connect( ui->slider_progress , SIGNAL(SIG_valueChanged(int)),
this,SLOT(slot_videoSliderValueChanged(int)) );
添加处理槽函数
//视频进度通过手动点击改变
void Youku::slot_videoSliderValueChanged(int value)
{
if( QObject::sender() == ui->slider_progress )
{
//精确到毫秒
decode->seek((qint64)value*1000000);
}
}
优化
在测试跳转时,发现播放到末尾时,进度条没有刷新,且停留在最后一帧卡死,考虑可以通过更改状态位设置UI界面解决
在主线程末尾添加状态位设置
m_playerState = PlayerState::Stop;
在定时器响应控制函数中添加,如果进度条到达末尾,切换状态
if( ui->slider_progress->value() == ui->slider_progress->maximum()
&& decode->m_playerState == PlayerState::Stop )
{
slot_PlayerStateChanged( PlayerState::Stop );
}