1. 程式人生 > >基於Qt、FFMpeg的音視訊播放器設計四(視訊播放進度控制)

基於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,在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音訊處理原理以及實現。