基于Qt、FFMpeg的音视频播放器设计四(视频播放进度控制)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hfuu1504011020/article/details/82705890

上面介绍了如何使用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音频处理原理以及实现。

下一篇链接:https://blog.csdn.net/hfuu1504011020/article/details/82722731

猜你喜欢

转载自blog.csdn.net/hfuu1504011020/article/details/82705890