基於Qt、FFMpeg的音視頻播放器設計四(視頻播放進度控制)

上面介紹瞭如何使用opengl繪製視頻和Qt的界面設計,也比較簡單,現在我們看下如何控制視頻播放及進度的控制,內容主要分爲以下幾個部分

1、創建解碼線程控制播放速度

2、通過Qt打開外部視頻

3、視頻總時間顯示和播放的當前時間顯示

4、進度條顯示播放進度、拖動進度條控制播放位置

5、控制視頻播放和暫停

6、視頻顯示和窗口大小變化同步

7、重載Qt滑動條類鼠標點擊移動滑動條並跳轉到相應視頻位置

一、創建解碼線程控制播放速度

上一篇中我們說了播放視頻時不是很順,有些卡頓。因爲我們將解碼過程以及轉RGB過程都放在QT的槽中即paintEVent中,這是一個重繪的過程,通常來說對於這個過程實現的都是一些比較簡單的內容,所以對於讀取視頻,解碼過程我們重新創建一個線程進行實現。這裏我們創建一個XVideoThread類(繼承自QThread),用於讀取,解碼以及控制讀取的速度。考慮到實際中解碼後的視頻幀,在重繪時不一定需要那麼幀(一個視頻中原fps爲250幀,在我重繪顯示時只需要25幀的情況,也需要知道fps),這裏我們只是控制它的讀取速度。所以首先我們需要原視頻的fps,在XFFMpeg.h中申明變量fps,在XFFMpeg.cpp文件的Open函數中打開解碼器過程中判斷是否爲視頻內加入fps = r2d(ic->streams[i]->avg_frame_rate);//獲得視頻得fps,其中的r2d函數是避免計算時分母爲0時的特判情況,代碼如下。

static double r2d(AVRational r)
{
	return r.den == 0 ? 0 : (double)r.num / (double)r.den;
}

if (enc->codec_type == AVMEDIA_TYPE_VIDEO)//判斷是否爲視頻
		{
			videoStream = i;
			fps = r2d(ic->streams[i]->avg_frame_rate);//獲得視頻得fps
			AVCodec *codec = avcodec_find_decoder(enc->codec_id);//查找解碼器

原視頻的fps我們已經獲得了,現在需要實現讀取視頻、解碼以及控制讀取的速度了,在XVideoThread.h中

#pragma once
#include <QThread>
class XVideoThread:public QThread
{
public:
	static XVideoThread *Get()//創建單例模式
	{
		static XVideoThread vt;
		return &vt;
	}
	void run();//線程的運行
	XVideoThread();
	virtual ~XVideoThread();
};

在XVideoThread.cpp中

#include "XVideoThread.h"
#include "XFFmpeg.h"

bool isexit = false;//線程未退出
XVideoThread::XVideoThread()
{
}


XVideoThread::~XVideoThread()
{
}

void XVideoThread::run()
{
	while (!isexit)//線程未退出
	{
		AVPacket pkt = XFFmpeg::Get()->Read();
		if (pkt.size <= 0)//未打開視頻
		{
			msleep(10);
			continue;
		}
		if (pkt.stream_index != XFFmpeg::Get()->videoStream)
		{
			av_packet_unref(&pkt);//不爲視頻時釋放pkt
			continue;
		}
		XFFmpeg::Get()->Decode(&pkt);//解碼視頻幀

		av_packet_unref(&pkt);
		if (XFFmpeg::Get()->fps > 0)//控制解碼的進度
			msleep(1000/XFFmpeg::Get()->fps);

	}

}

首先設置線程未退出,讀取AVPacket包,若未打開視頻,此時pkt.size必然小於0,線程睡眠一段時間繼續。否則我們開始解碼視頻幀,同時利用線程的睡眠時間控制解碼進度進而來控制播放的速度。

最後我們在VideoWidget.cpp中開啓線程。

VideoWidget::VideoWidget(QWidget *parent) :QOpenGLWidget(parent)
{
	XFFmpeg::Get()->Open("1080.mp4");//打開視頻
	startTimer(20);//設置定時器
	XVideoThread::Get()->start();//開啓讀取視頻、解碼、控制播放速度線程
}

二、通過Qt打開外部視頻

上面我們的視頻文件的打開Open函數都是確定了某個視頻,這裏我們通過Qt的控件按鈕自定義打開視頻文件,進入Qt的設計界面選中openButton打開文件這個按鈕,然後Qt上的任務欄中找到編輯信號/槽,點擊,之後按住openButton控件拖動時出現紅線,將紅線拖動到agineXplay這個界面處(因爲我的項目名稱就叫做agineXplay,大家的可能都不一樣),這裏要注意agineXplay的界面和我們的openGL Widget界面不一樣的,openButton和playButton都在openGL Widget中,通過點擊他們在agineXplay中響應的,(當然此時的openGl Widget我已經將他更改爲VideoWidget類,上面也說到了,對於VS、Qt如何添加信號槽的也可以百度瞭解下)

然後出現如上的界面,點擊編輯即可得到右側的槽和信號的窗口,我們加入槽open()函數,之後點擊clicked()信號和open()函數,此時我們的信號和槽就連接上了。現在我們在aginexplay.h中申明槽函數open()。

#ifndef AGINEXPLAY_H
#define AGINEXPLAY_H

#include <QtWidgets/QWidget>
#include "ui_aginexplay.h"

class agineXplay : public QWidget
{
	Q_OBJECT

public:
	agineXplay(QWidget *parent = 0);
	~agineXplay();
public slots:
	void open();//槽函數用來響應打開文件的按鈕

private:
	Ui::agineXplayClass ui;
};

#endif // AGINEXPLAY_H

相應的在aginexplay.cpp中的定義。

#include "aginexplay.h"
#include <QFileDialog>
#include <QMessageBox>
#include "XFFmpeg.h"
agineXplay::agineXplay(QWidget *parent)
	: QWidget(parent)
{
	ui.setupUi(this);
}

agineXplay::~agineXplay()
{

}

void agineXplay::open()
{
	QString name = QFileDialog::getOpenFileName(this,QString::fromLocal8Bit("選擇視頻文件"));//打開視頻文件
	if (name.isEmpty())
		return;
	this->setWindowTitle(name);//設置窗口的標題
	if(!XFFmpeg::Get()->Open(name.toLocal8Bit()))//未打開視頻成功
	{
		QMessageBox::information(this,"err","file open failed!");//彈出錯誤窗口

	}

}

三、視頻總時間顯示和播放的當前時間顯示

現在我們開始對視頻當前的進度進行設置,如何顯示總時間以及當前的時間呢?總時間比較好獲得,在解封裝時已經得到,對於當前時間,我們可以利用decode時它的pts來表示,然後一比較就能獲得當前的播放位置。現在我們對Qt界面進行設置。在界面中加入兩個label,第一個label中內容設置爲000:00 /  ,第二個label中內容設置爲000:00,分別代表當前播放時間和視頻總時間,對象名分別爲playtime和totaltime,如下圖。

     因爲需要獲得解封裝視頻後的總時間,所以這裏我們需要對XFFMpeg.cpp中Open()函數的返回值進行修改,這裏改爲返回int代表視頻總時間totalMs。

在aginexplay.cpp中的open()函數更改如下內容。

void agineXplay::open()
{
	QString name = QFileDialog::getOpenFileName(this,QString::fromLocal8Bit("選擇視頻文件"));//打開視頻文件
	if (name.isEmpty())
		return;
	this->setWindowTitle(name);//設置窗口的標題
	int totalMs = XFFmpeg::Get()->Open(name.toLocal8Bit());//獲取視頻總時間
	if(totalMs<= 0)//未打開成功
	{
		QMessageBox::information(this,"err","file open failed!");//彈出錯誤窗口
		return;
	}
	char buf[1024] = {0};//用來存放總時間
	int min = (totalMs)/60;
	int sec = (totalMs) % 60;
	sprintf(buf, "%03d:%02d",min,sec);//存入buf中
	ui.totaltime->setText(buf);//顯示在界面中

}

從而獲取視頻的總時間並顯示在界面中。現在我們來獲取播放視頻得當前時間,在XFFmpeg.cpp的Decode()函數中我們獲得當前播放的的pts。

mutex.unlock();
    pts = yuv->pts*r2d(ic->streams[pkt->stream_index]->time_base)*1000;//設置當前播放的pts
	return yuv;

然後設置定時器,按照一秒25幀,即定時器的時間設置爲40ms,在aginexplay.h中申明定時器函數timerEvent(),在aginexplay.cpp定義如下。

void agineXplay::timerEvent(QTimerEvent *event)
{
	int min = (XFFmpeg::Get()->pts ) / 60;//視頻播放當前的分鐘	
	int sec = (XFFmpeg::Get()->pts ) % 60;//視頻播放當前的秒
	char buf[1024] = {0};
	sprintf(buf,"%03d:%02d / ",min,sec);//存入buf中
	ui.playtime->setText(buf);//顯示在界面中
}

然後在aginexplay.cpp的構造函數中啓動定時器,達到一秒25幀的播放速率,至此結束。

startTimer(40);

四、進度條顯示播放進度、拖動進度條控制播放位置

通常我們使用的播放器不僅有以上功能,還可以顯示播放進度以及我們拖動進度條時控制它的播放位置。首先我們進入Qt的設計界面,加入一個水平滑動條,對象名改爲playslider,在vs中的aginexplay.cpp的timerEvent()函數中增添如下代碼。

void agineXplay::timerEvent(QTimerEvent *event)
{
	int min = (XFFmpeg::Get()->pts ) / 60;//視頻播放當前的分鐘	
	int sec = (XFFmpeg::Get()->pts ) % 60;//視頻播放當前的秒
	char buf[1024] = {0};
	sprintf(buf,"%03d:%02d /  ",min,sec);//存入buf中
	ui.playtime->setText(buf);//顯示在界面中

	if (XFFmpeg::Get()->totalMs > 0)//判斷視頻得總時間
	{
		float rate = (float)XFFmpeg::Get()->pts / (float)XFFmpeg::Get()->totalMs;//當前播放的時間與視頻總時間的比值
		ui.playslider->setValue(rate * 1000);//設置當前進度條位置
	}

}

rate*1000是因爲我在Qt設計界面中將進度條的取值設置在0~999,所以需要這樣轉化。現在設置進度條的拖動來顯示播放,在XFFMPeg.h中申明函數Seek(),此函數主要是當我們通過鼠標拖動進度條時能更新到當前視頻,函數的定義如下。

bool XFFmpeg::Seek(float pos)
{
	mutex.lock();
	if (!ic)//未打開視頻
	{
		mutex.unlock();
		return false;
	}
	int64_t stamp = 0;
	stamp = pos * ic->streams[videoStream]->duration;//當前它實際的位置
	int re = av_seek_frame(ic, videoStream, stamp,
		AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);//將視頻移至到當前點擊滑動條位置
	avcodec_flush_buffers(ic->streams[videoStream]->codec);//刷新緩衝,清理掉
     mutex.unlock();
	if (re > 0)
		return true;
	return false;
}

對於 av_seek_frame(ic, videoStream, stamp,  AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME)函數;

這個函數前三個函數比較好理解,對於第四個參數,我們先要知道,視頻幀分爲I、B、P,這個前面提到過,當我們拖動進度條時,不可能恰好拖到它的有效幀上(比如有效的是80 、120,而我們剛好拖動到了100,此時這裏的第四個參數AVSEEK_FLAG_BACKWARD意義就是從它的前一幀80開始重新解析,避免視頻幀的遺漏,當然從120解析也是可以的,改變第四個參數就可以了),這裏的AVSEEK_FLAG_FRAME代表的是有效幀,

對於函數avcodec_flush_buffers(ic->streams[videoStream]->codec)函數;

在我們點擊滑動條更新視頻位置後,由於此時緩衝區中還有先前未滑動時解碼到的視頻幀,這樣的幀對於我們已經滑動後的位置已沒有意義了,應該從緩衝區中清理掉。

現在我們需要確定按住滑動條直至鬆開後滑動條的位置,首先我們在Qt設計界面中對於滑動條設計兩個相應槽,分別相應滑動條的按下和鬆開時的操作,信號函數爲sliderPressed()和sliderReleased(),這裏的槽函數我也是定義了sliderPressed()和sliderReleased(),然後進入aginexplay.h中申明


public slots:
	void open();//槽函數用來響應打開文件的按鈕
	void sliderPressed();//按下進度條時		
	void sliderReleased();//鬆開進度條時

在aginexplay.cpp中定義如下,當鬆開滑動條時,獲得當前滑動條位置和總滑動條長度比例,放入Seek()函數中進行滑動處理.

void agineXplay::sliderPressed()
{
	isPressSlider = true;
}

void agineXplay::sliderReleased()
{
	isPressSlider = false;
	float pos = 0;

	//鬆開時此時滑動條的位置與滑動條的總長度
	pos = (float)ui.playslider->value() / (float)(ui.playslider->maximum() + 1);
	XFFmpeg::Get()->Seek(pos);
}

isPressSlider 是在aginexplay.cpp中定義的靜態變量,用來控制是否按下了進度條

static bool isPressSlider;//是否按下進度條

同時在計時器timerEvent中修改部分內容,即只有我們鬆開滑動條或者未對滑動條操作時才進入  ui.playslider->setValue(rate * 1000);//設置當前進度條位置

if (XFFmpeg::Get()->totalMs > 0)//判斷視頻得總時間
	{
		float rate = (float)XFFmpeg::Get()->pts / (float)XFFmpeg::Get()->totalMs;//當前播放的時間與視頻總時間的比值
		if (!isPressSlider) //當鬆開時繼續刷新進度條位置
		   ui.playslider->setValue(rate * 1000);//設置當前進度條位置
	}

五、控制視頻得播放暫停

打開Qt設計器,點擊信號槽,再點擊播放按鈕將紅線拖動至窗口界面,增加一個play()槽,然後在VS中的aginexplay.h中申明play()槽,在aginexplay.cpp中定義如下:

void agineXplay::play()
{
	isPlay = !isPlay;//播放取反
	if (isPlay)//如果播放了
	{
		ui.playButton->setStyleSheet(PLAY);//顯示播放按鈕狀態
	}
	else
	{
		ui.playButton->setStyleSheet(PAUSE);//顯示暫停播放按鈕狀態
	}

}

對於PLAY和PAUSE對應的是圖標的暫停和播放,aginexplay.cpp定義如下

static bool isPlay = false;//是否播放
#define  PAUSE "QPushButton\
{border-image: url\
(:/agineXplay/Resources/stop.jpg);}"//css語法,暫停按鈕
#define  PLAY "QPushButton\
{border-image: url\
(:/agineXplay/Resources/play.jpg);}"//播放按鈕

目前只是實現了它播放暫停的界面,現在實現它點擊暫停時畫面的暫停和重新播放畫面恢復,我們在XFFMpeg.h中申明

bool isPlay = false;//播放暫停

然後我們在線程XVideoThread.cpp中讀取每幀數據前判斷是否暫停了,若暫停了睡眠一段時間跳出,這樣就不進行下面的解碼、重繪過程,畫面定格在這裏,在run()中添加以下部分代碼。

void XVideoThread::run()
{
	while (!isexit)//線程未退出
	{
		if (!XFFmpeg::Get()->isPlay)//如果爲暫停狀態,不處理
		{
			msleep(10);
			continue;
		}
		AVPacket pkt = XFFmpeg::Get()->Read();

在aginexplay.cpp中的play()中將暫停播放狀態傳遞給XFFMpeg中的isPlay,如下

void agineXplay::play()
{
	isPlay = !isPlay;//播放取反
	XFFmpeg::Get()->isPlay = isPlay;//將播放狀態傳遞於XFFMpeg中的isPlay
	if (isPlay)//如果播放了

現在有一個問題,當我們暫停後拉動滑動條時,此時滑動條過不去,雖然繼續播放後視頻處於我們滑動到的位置,但現在我們無法知道滑動到哪裏。這個就與我們的Seek函數有關的,因爲我們已經暫停了,不再解碼,但要想獲得此時滑動條到達的時間我們可以利用它的滑動位置乘以它的時間基數,從而得到它當前的pts,在Seek()中加入這樣一句pts。

int64_t stamp = 0;
	stamp = pos * ic->streams[videoStream]->duration;//當前它實際的位置
	pts = stamp * r2d(ic->streams[videoStream]->time_base);//獲得滑動條滑動後的時間戳

六、視頻顯示和窗口大小變化同步

之前的窗口都是固定大小的,當我們全屏時,視頻窗口不會隨之改變,現在需要將它修改爲符合的窗口。在aginexplay.h中申明事件void resizeEvent(QResizeEvent *event);//改變窗口大小,在aginexplay.cpp中

void agineXplay::resizeEvent(QResizeEvent *event)
{
	ui.openGLWidget->resize(size());//設置視頻窗口和界面的相同大小
	ui.playButton->move(this->width() / 2 + 50, this->height() - 80);//放大縮小後播放按鈕位置
	ui.openButton->move(this->width() / 2 - 50, this->height() - 80);//........打開文件按鈕位置,以下幾個同樣意義
	ui.playslider->move(25,this->height()-120);
	ui.playslider->resize(this->width()-50,ui.playslider->height());
	ui.playtime->move(25, ui.playButton->y());
	
	ui.totaltime->move(130,ui.playButton->y());

}

需要注意的是ui.openGLWidget->resize(size());//設置視頻窗口和界面的相同大小,對於這個函數由於是重新確定視頻窗口和界面大小的一致,在VideoWidget.cpp中的paintEvent函數中,在改變窗口大小時我們需要重新分配Image內存空間,所以我們需要加入這樣一段內容即可。

void VideoWidget::paintEvent(QPaintEvent *e)
{//繪製
	static QImage *image = NULL;
	static int w = 0;
	static int h = 0;
	if (w != width() || h != height())//當縮小窗口或者方法窗口時,刪除image,重新繪製
	{
		if (image)
		{
			delete image->bits();//刪除內容
		    delete image;
			image = NULL;
		}
		

	}
	if (image == NULL)
	{
		uchar *buf = new uchar[width()*height() * 4];//存放解碼後的視頻空間
		image = new QImage(buf, width(), height(), QImage::Format_ARGB32);
	}

 

七、重載Qt滑動條類鼠標點擊移動滑動條並跳轉到相應視頻位置

對於Qt的滑動條,我們拖動時是沒有問題的,但是當鼠標在滑動條某個位置按下它不能指定到該位置,我們現在實現它。增加一個類XSlider(在Qt設計器中按照上一篇中方法將QSlider類提升爲XSlider),在XSlider.h中申明

#pragma once
#include "qobject.h"
#include <QSlider>
class XSlider :
	public QSlider
{
	Q_OBJECT

public:
	XSlider(QWidget *parent);
	~XSlider();
	void mousePressEvent(QMouseEvent *ev);//鼠標按下事件
};

在XSlider.cpp中定義

#include "XSlider.h"
#include <QMouseEvent>

XSlider::XSlider(QWidget *p /*= NULL*/) :QSlider(p)
{

}


XSlider::~XSlider()
{
}

void XSlider::mousePressEvent(QMouseEvent *ev)
{
	double pos = (double)ev->pos().x() / (double)width();//當前鼠標位置比率
	setValue(pos*this->maximum());//設置位置
	QSlider::mousePressEvent(ev);
}

此時獲得鼠標點擊滑動條任意位置時的播放界面,至此視頻播放過程結束,內容雖有些多,但實現的過程還是比較清晰的,在下一篇中我們對FFMPEG音頻處理原理以及實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章