目录
播放器播放控制添加
-
界面设计
仿优酷的界面设计,资源都是自己在优酷客户端上截取处理的,暂时没有对界面进行优化
-
暂停
在decode类中添加播放控制状态,考虑到后续的复用性,可以拷贝到其它地方直接使用,在这个类中不对ui进行设置,只更改状态
在decode类中构造函数初始化所有播放状态为true,代表没有线程在工作,解码线程和主线程已经退出
使用枚举类型代表播放、暂停、停止三个状态
添加播放控制函数
不要忘记在构造函数中给状态赋初值为Stop,否则可能会是一个垃圾值
m_playerState = PlayerState::Stop;
在打开文件后,改变播放状态为Playing
//打开文件
void Youku::on_pb_openfile_clicked()
{
//打开文件 弹出对话框 参数:父窗口, 标题, 默认路径, 筛选器
QString path = QFileDialog::getOpenFileName(this,"选择要播放的文件" , "F:/",
"视频文件 (*.flv *.rmvb *.avi *.MP4 *.mkv);; 所有文件(*.*);;");
if(!path.isEmpty())
{
qDebug()<< path ;
QFileInfo info(path);
//如果取到了路径
if( info.exists() )
{
//点击按钮开启线程 获取图片 显示到控件
decode->start();
//如果播放 你要先关闭
decode->stop( true );
//调用解码类打开文件
decode->setFileName(path);
//隐藏打开文件的按钮
ui->pb_openfile->hide();
//设置ui上的播放文件名
ui->lb_filename->setText( info.baseName() );
//改变播放状态
slot_PlayerStateChanged(PlayerState::Playing);
}
else
{
//没取到路径,打开失败
QMessageBox::information( this, "提示" , "打开文件失败");
}
}
}
点击UI界面的打开文件,调用解码类decode的设置文件名函数,把播放状态置为Playing,开启线程开始解码
void Decode::setFileName(const QString &fileName)
{
m_fileName = fileName;
m_playerState = PlayerState::Playing;
this->start();
}
根据对应状态,设置控件和标志位的函数
void Youku::slot_PlayerStateChanged(int state)
{
switch( state )
{
case PlayerState::Stop:
qDebug()<< "VideoPlayer::Stop";
ui->wdg_show->hide();
ui->wdg_black->show();
//m_Timer->stop();
ui->slider_progress->setValue(0);
ui->lb_totalTime->setText("00:00:00");
ui->lb_currentTime->setText("00:00:00");
ui->lb_filename->setText("");
this->update();
isStop = true;
break;
case PlayerState::Playing:
qDebug()<< "VideoPlayer::Playing";
ui->wdg_show->show();
ui->wdg_black->hide();
ui->pb_play->setIcon(QIcon(":/images/pause.png"));
//m_Timer->start();
this->update();
isStop = false;
break;
}
}
点击播放按钮
若此时正在播放,状态为Playing,应该单独切换按钮的ui,转变状态为Pause
若此时已经暂停,状态为Pause,转变状态为Playing
//播放/暂停
void Youku::on_pb_play_clicked()
{
if(isStop) return;
if( decode->getPlayerState() == PlayerState::Pause)
{
ui->pb_play->setIcon(QIcon(":/images/pause.png"));
this->update();
}else if(decode->getPlayerState() == PlayerState::Playing)
{
ui->pb_play->setIcon(QIcon(":/images/play.png"));
this->update();
}
decode->play();
}
在解码类中添加改变播放状态的函数
如果播放状态是Playing,则把isPause置为true,播放状态置为Pause
如果播放状态是Pause,则把isPause置为false,播放状态置为Playing
void Decode::play()
{
if(getPlayerState() == PlayerState::Playing)
{
//点击一下转为播放状态
m_videoState.isPause = true;
m_playerState = PlayerState::Pause;
}
else if(getPlayerState() == PlayerState::Pause)
{
m_videoState.isPause = false;
m_playerState = PlayerState::Playing;
}
}
在视频解码线程和音频解码函数中添加判断,当前暂停标志位是否被置为TRUE
如果是暂停状态,调用SDL_Delay延迟解码,直到标志位被置为FALSE时继续解码播放
-
停止
这个部分添加停止的思路是:
主线程run()函数,视频解码线程,音频解码线程
主线程读文件的标志是isreadFinished,读线程退出的标志是isreadThreadFinished
视频线程退出的标志是isvideoThreadFinished
通过这三个标志位来控制停止——线程退出
音频线程需要调用SDL库函数将其停止
主线程具体操作
在主线程run函数中添加下面一段代码
读取文件到packet中时,如果一次读取失败不认为读取完毕,而是用一个DelayCount判断
一次延时10ms,300次延时3秒钟没有读到内容,说明文件流读取完了,将readFinished置为true
if (av_read_frame(m_videoState.pFormatCtx, packet) < 0)
{
DelayCount++;
if( DelayCount>= 300)
{
m_videoState.readFinished = true;
DelayCount = 0 ;
}
SDL_Delay(10);
continue;
}
读取完毕文件流后,等待音视频解码线程退出
音视频解码线程退出后,回收音视频队列
//音视频队列回收
if( m_videoState.videoStream != -1)
m_videoState.videoq->packet_queue_flush();
if( m_videoState.audioStream != -1)
m_videoState.audioq->packet_queue_flush();
视频解码线程具体操作
从队列取包解码的操作
//从队列取 如果没取到
if (is->videoq->packet_queue_get(packet, 0) <= 0)
{
//判断读线程是否完毕,音频队列中包是不是已经没有了
if( is->isreadFinished && is->audioq->nb_packets == 0)
{
//如果是 跳出视频解码线程
break;
}else
{
//如果不是,则只是队列里面暂时没有数据而已
SDL_Delay(10);
continue;
}
}
音视频同步的操作
//视频同步到音频上
while(1)
{
//如果已经退出,不再延时
if( is->isquit) break;
//如果音频队列已经回收,退出视频解码线程,不然可能永远等不到音频队列的包中的音频时钟
if( is ->audioq->size == 0 ) break;
//获取音、视频时钟,当 视频时钟 > 音频时钟时 循环等待
audio_pts = is->audio_clock;
video_pts = is->video_clock;
if (video_pts <= audio_pts) break;
int delayTime = (video_pts - audio_pts) * 1000;
delayTime = delayTime > 5 ? 5:delayTime;
SDL_Delay(delayTime);
}
视频解码线程推出时的操作
//视频线程退出时,quit要置为true
if( !is->isquit)
{
is->isquit = true;
}
最后置视频解码线程退出标志为1
is->isvideoThreadFinished = true;
音频解码函数具体操作
//从音频队列取包解码
if(is->audioq->packet_queue_get(&pkt, 0) <= 0)
{
//读文件已经完毕,并且音频队列没有包了,把quit标志置为1 退出音频解码函数
if( is->isreadFinished && is->audioq->nb_packets == 0 )
is->isquit = true;
return -1;
}
ui界面控制操作
在解码类decode中添加stop函数具体内容
因为SDL音频解码线程不能自己退出,需要在这个地方加一段关闭设备的代码退出
退出后将标志位置为Stop,并通过信号传递给UI界面
void Decode::stop( bool isWait)
{
//按下stop,退出标志位置为1
m_videoState.isquit = 1;
if( isWait )
{
//阻塞一直等待主线程——读取线程退出
while(!m_videoState.isreadThreadFinished )
{
SDL_Delay(10);
}
}
//关闭 SDL 音频设备
if (m_videoState.audioID != 0)
{
SDL_LockAudio();
SDL_PauseAudioDevice(m_videoState.audioID,1);//停止播放,即停止音频回调函数
SDL_UnlockAudio();
m_videoState.audioID = 0;
}
//播放状态变为Stop
m_playerState = PlayerState::Stop;
Q_EMIT SIG_PlayerStateChanged(PlayerState::Stop);
}
在ui界面的构造函数中添加connect处理发来的状态转换信号
connect(decode,SIGNAL(SIG_PlayerStateChanged(int)),
this,SLOT(slot_PlayerStateChanged(int)));
通过ui界面上的停止按钮调用decode的stop函数
decode->stop(true);
添加进度条
-
分别获取当前时间和总时间
首先要读取视频的长度,在解码类decode中添加获取时间的代码,因为时钟是同步到音频时钟上的,所以当前时间以音频时间为基准
//获取当前时间
double VideoPlayer::getCurrentTime()
{
return m_videoState.audio_clock;
}
//获取总时间
int64_t VideoPlayer::getTotalTime()
{
if( m_videoState.pFormatCtx )
return m_videoState.pFormatCtx->duration;
return -1;
}
获取的时间单位都是微秒,需要将获取的总时间通过信号在文件开始读取之前发出去
添加信号
void SIG_TotalTime(qint64 uSec);
在ui界面添加槽函数处理
connect( decode, SIGNAL( SIG_TotalTime(qint64)) ,
this ,SLOT( slot_getTotalTime(qint64)) );
添加槽函数处理控件
//获取视频全部时间
void Youku::slot_getTotalTime(qint64 uSec)
{
//计算秒
qint64 Sec = uSec/1000000;
ui->slider_progress->setRange(0,Sec);
QString hStr = QString("00%1").arg(Sec/3600);
QString mStr = QString("00%1").arg(Sec/60%60);
QString sStr = QString("00%1").arg(Sec%60);
QString str =
QString("%1:%2:%3").arg(hStr.right(2)).arg(mStr.right(2)).arg(sStr.right(2));
ui->lb_totalTime->setText(str);
}
-
进度条功能采用QTimer定时器实现
包含定时器头文件QTimer
#include <QTimer>
首先在ui类中定义定时器对象
QTimer* m_Timer;
在ui类的构造函数中添加定时事件处理connect函数
//定时器 进度条切换 设置间隔500ms
m_Timer = new QTimer;
connect(m_Timer,SIGNAL(timeout()),this,SLOT(slot_TimerTimeout()));
m_Timer->setInterval(500);
添加处理定时器事件的槽函数,具体见注释
void VideoShow::slot_TimerTimeOut()
{
if (QObject::sender() == m_Timer)
{
//获取到的时间是微秒 换算为秒
qint64 Sec = m_Player->getCurrentTime()/1000000;
//设置进度条
ui->slider_progress->setValue(Sec);
//通过秒换算小时
QString hStr = QString("00%1").arg(Sec/3600);
//通过秒换算分钟
QString mStr = QString("00%1").arg(Sec/60%60);
//通过秒换算秒
QString sStr = QString("00%1").arg(Sec%60);
//在时间label上显示换算后的时间
QString str =
QString("%1:%2:%3").arg(hStr.right(2)).arg(mStr.right(2)).arg(sStr.right(2));
ui->lb_currentTime->setText(str);
}
}
在状态切换中打开与关闭定时器
最后的效果展示图