前面文章 FFmpeg像素格式轉(zhuǎn)換 中我們使用 FFmpeg 實(shí)現(xiàn)了一個(gè)像素格式轉(zhuǎn)換工具類屡穗,現(xiàn)在我們就可以在 Qt 中利用 QImage 很容易的實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 YUV 播放器了柑营。
播放器功能很簡(jiǎn)單队寇,只有播放萌壳、暫停和停止扫外。我們定義了一個(gè)播放器類 YuvPlayer
汰蜘,首先在 .h 文件中定義外部調(diào)用的函數(shù)辑奈,還需要一個(gè)設(shè)置播放文件的函數(shù)苛茂,既然是播放 yuv 文件,那么就需要額外再告訴播放器視頻的寬高鸠窗、像素格式以及幀率味悄,我們定義了一個(gè)包括這些參數(shù)的結(jié)構(gòu)體 Yuv
:
#ifndef YUVPLAYER_H
#define YUVPLAYER_H
#include <QWidget>
typedef struct {
// 文件路徑
const char *filename;
// yuv 的寬
int width;
// yuv 的高
int height;
// yuv 像素格式
AVPixelFormat pixelFormat;
// 幀率
int fps;
} Yuv;
class YuvPlayer : public QWidget
{
Q_OBJECT
public:
// 播放器的狀態(tài)
typedef enum {
Stopped = 0, // 停止
Playing, // 播放中
Paused, // 暫停
Finished // 播放完成
} State;
explicit YuvPlayer(QWidget *parent = nullptr);
~YuvPlayer();
// 播放
void play();
// 暫停
void pause();
// 停止
void stop();
// 播放器是否播放中
bool isPlaying();
// 獲取播放器當(dāng)前狀態(tài)
State getState();
// 設(shè)置播放文件
void setYuv(Yuv &yuv);
};
#endif // YUVPLAYER_H
setYuv
函數(shù)用來設(shè)置我們要播放的 yuv 文件,可以放到這個(gè)函數(shù)中的操作有:
1塌鸯、打開 yuv 文件;
2唐片、計(jì)算刷幀的時(shí)間間隔丙猬;
3、計(jì)算一幀圖像的大蟹丫隆茧球;
4、計(jì)算視頻目標(biāo)尺寸星持,在播放控件中居中顯示視頻抢埋;
void YuvPlayer::setYuv(Yuv &yuv)
{
_yuv = yuv;
// 打開文件
_file = new QFile(yuv.filename);
if (!_file->open(QFile::ReadOnly)) {
qDebug() << "open file error:" << yuv.filename;
return;
}
// 刷幀的時(shí)間間隔
_interval = 1000 / _yuv.fps;
// 計(jì)算一幀圖像的大小
_imageSize = av_image_get_buffer_size(yuv.pixelFormat, yuv.width, yuv.height, 1);
// 組件的尺寸(播放器)
int w = width();
int h = height();
// 原視頻的寬度 yuv.width 高度 yuv.height
int dstX = 0;
int dstY = 0;
int dstW = yuv.width;
int dstH = yuv.height;
// 縮放視頻,計(jì)算目標(biāo)尺寸
if (dstW > w || dstH > h) {
// 視頻的寬高比 > 播放器的寬高比督暂,(dstW / dstH) > (w / h) 變換而來
if ((dstW * h) > (w * dstH)) {
dstH = dstH * w / dstW ;
dstW = w;
} else {
dstW = dstW * h / dstH;
dstH = h;
}
}
// 居中視頻揪垄,每種情況都有的操作
dstX = (w - dstW) >> 1;
dstY = (h - dstH) >> 1;
// 計(jì)算后的視頻寬高
_dstRect = QRect(dstX, dstY, dstW, dstH);
}
在播放器中完整居中顯示 YUV 視頻,會(huì)遇到四種情況:1逻翁、視頻寬高都小于等于播放器寬高饥努;2、視頻寬大于播放器寬八回,視頻高小于播放器高酷愧;3、視頻高大于播放器高缠诅,視頻寬小于播放器寬溶浴;4、視頻寬高都大于播放器寬高(等同于情況 2 或者 3)管引;總結(jié)下來實(shí)際有下圖三種情況士败,第 1 種情況,我們居中顯示視頻就可以汉匙,第 2拱烁、3生蚁、4 種情況需要視頻寬高比不變的情況下對(duì)視頻進(jìn)行等比例伸縮,需要伸縮到視頻可以在播放器中完整顯示戏自。
play
函數(shù)中開啟了一個(gè)定時(shí)器邦投,定時(shí)器執(zhí)行間隔取決于幀率,執(zhí)行間隔在 setYuv
中計(jì)算得到擅笔,startTimer
是 QObject 中的方法志衣,只要繼承 QObject 就可以使用這個(gè)函數(shù):
void YuvPlayer::play() {
// 防止多次調(diào)用 play 函數(shù)開啟多個(gè)定時(shí)器
if (_state == Playing) return;
// 狀態(tài)可能是:暫停、停止猛们、正常完畢
_timerId = startTimer(_interval);
setState(Playing);
}
定時(shí)器開啟后每隔一定間隔會(huì)調(diào)用 timerEvent
函數(shù)念脯,這個(gè)函數(shù)中我們從文件讀取一幀 yuv 數(shù)據(jù),使用我們之前實(shí)現(xiàn)的像素格式轉(zhuǎn)換工具將 yuv420p 格式數(shù)據(jù)轉(zhuǎn)換成 rgb24 格式數(shù)據(jù)弯淘,然后將數(shù)據(jù)渲染到 QImage 上面绿店,調(diào)用 update
函數(shù)刷新。此處需要注意一個(gè)問題庐橙,像素格式轉(zhuǎn)換后的輸出視頻寬高不是 16 的倍數(shù)會(huì)降低轉(zhuǎn)碼速度假勿,建議輸出視頻寬高是 16 倍數(shù):
void YuvPlayer::timerEvent(QTimerEvent *event) {
// 圖片大小
char data[_imgSize];
if (_file->read(data, _imgSize) == _imgSize) {
RawVideoFrame in = {
data,
_yuv.width,
_yuv.height,
_yuv.pixelFormat
};
RawVideoFrame out = {
nullptr,
_yuv.width >> 4 << 4,
_yuv.height >> 4 << 4,
AV_PIX_FMT_RGB24
};
FFmpegs::convertRawVideo(in, out);
freeCurrentImage();
_currentImage = new QImage((uchar *) out.pixels,
out.width, out.height, QImage::Format_RGB888);
// 刷新
update();
} else { // 文件數(shù)據(jù)已經(jīng)讀取完畢
// 停止定時(shí)器
stopTimer();
// 正常播放完畢
setState(Finished);
}
}
當(dāng)調(diào)用 update
函數(shù)的時(shí)候,就會(huì)觸發(fā) paintEvent
态鳖,在這個(gè)函數(shù)中將圖片繪制到當(dāng)前組件上转培。當(dāng)組件想重繪的時(shí)候,也會(huì)調(diào)用這個(gè)函數(shù):
void YuvPlayer::paintEvent(QPaintEvent *event) {
if (!_currentImage) return;
// 將圖片繪制到當(dāng)前組件上
QPainter(this).drawImage(_dstRect, *_currentImage);
}
接下來繼續(xù)實(shí)現(xiàn)暫停和停止功能:
void YuvPlayer::pause() {
if (_state != Playing) return;
// 狀態(tài)可能是:正在播放
// 停止定時(shí)器
stopTimer();
// 改變狀態(tài)
setState(Paused);
}
void YuvPlayer::stop() {
if (_state == Stopped) return;
// 狀態(tài)可能是:正在播放浆竭、暫停浸须、正常完畢
// 停止定時(shí)器
stopTimer();
// 釋放圖片
freeCurrentImage();
// 刷新
update();
// 改變狀態(tài)
setState(Stopped);
}
QFile 會(huì)記錄上次讀取文件的位置,當(dāng)播放完畢時(shí)邦泄,要將讀取指針回歸到最初始的位置删窒。作為一個(gè)播放器,需要時(shí)刻向外界發(fā)送一些消息虎韵,比如暫鸵壮恚或者繼續(xù)播放等等需要通知外界,我們利用 Qt 信號(hào)和槽機(jī)制包蓝,在信號(hào)聲明區(qū)下面定義了一個(gè)信號(hào)stateChange
驶社,當(dāng)播放器狀態(tài)發(fā)生改變時(shí)我們發(fā)送一個(gè)信號(hào),外界與此信號(hào)關(guān)聯(lián)的槽函數(shù)就會(huì)被調(diào)用:
void YuvPlayer::setState(State state) {
if (state == _state) return;
if (state == Stopped || state == Finished) {
// 讓文件讀取指針回到文件首部
_file->seek(0);
}
_state = state;
emit stateChanged();
}
示例代碼:
yuvplayer.h
#ifndef YUVPLAYER_H
#define YUVPLAYER_H
#include <QWidget>
#include <QFile>
extern "C" {
#include <libavutil/avutil.h>
}
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat pixelFormat;
int fps; // 幀率
} Yuv;
class YuvPlayer : public QWidget
{
Q_OBJECT
public:
// 播放器的狀態(tài)
typedef enum {
Stopped = 0,
Playing,
Paused,
Finished
} State;
explicit YuvPlayer(QWidget *parent = nullptr);
~YuvPlayer();
void play();
void pause();
void stop();
bool isPlaying();
State getState();
void setYuv(Yuv &yuv);
private:
QFile _file;
int _timerId = 0; // 先寫一個(gè)0测萎,否則有可能是個(gè)垃圾值
// 成員變量最好不要設(shè)置為引用亡电,有可能引用外部的變量,如果引用的外部變量是一個(gè)臨時(shí)變量(比如椆枨疲空間變量份乒,函數(shù)銷毀引用的內(nèi)存就會(huì)被銷毀),臨時(shí)變量被銷毀引用就會(huì)很危險(xiǎn),
// Yuv &_yuv;
Yuv _yuv;
State _state = Stopped;
QImage *_currentImage = nullptr;
// 視頻大小
QRect _dstRect;
// 一幀圖片的大小
int _imageSize = 0;
int _imgSize;
// 刷幀的時(shí)間間隔
int _interval;
/** 改變狀態(tài) */
void setState(State state);
/** 釋放QImage */
void freeCurrentImage();
/** 殺掉定時(shí)器 */
void stopTimer();
void timerEvent(QTimerEvent *event);
void paintEvent(QPaintEvent *event);
signals:
void stateChanged();
};
#endif // YUVPLAYER_H
yuvplayer.m
#include "yuvplayer.h"
#include <QDebug>
#include <QPainter>
#include <ffmpegutils.h>
extern "C" {
#include <libavutil/imgutils.h>
}
YuvPlayer::YuvPlayer(QWidget *parent) : QWidget(parent)
{
// 設(shè)置控件背景色
setAttribute(Qt::WA_StyledBackground);
setStyleSheet("background: black");
}
YuvPlayer::~YuvPlayer()
{
_file->close();
freeCurrentImage();
}
// 播放
void YuvPlayer::play()
{
if (getState() == Playing) return;
// 開啟定時(shí)器
_timerId = startTimer(_interval);
setState(Playing);
}
// 暫停
void YuvPlayer::pause()
{
if (getState() != Playing) return;
stopTimer();
setState(Paused);
}
// 停止
void YuvPlayer::stop()
{
if (getState() == Stopped) return;
// 狀態(tài)可能是 正在播放 暫停 正常完畢
stopTimer();
// 清空屏幕
freeCurrentImage();
update();
setState(Stopped);
}
// 設(shè)置播放器狀態(tài)
void YuvPlayer::setState(State state)
{
if (_state == state) return;
// 停止/播放完成狀態(tài)或辖,需要從文件開始位置讀取
if (state == Stopped || state == Finished) {
_file->seek(0);
}
_state = state;
// 發(fā)送狀態(tài)改變信號(hào)
emit stateChanged();
}
void YuvPlayer::setYuv(Yuv &yuv)
{
// 使用結(jié)構(gòu)體瘾英,賦值相當(dāng)于拷貝,引用的外部變量被銷毀颂暇,當(dāng)前結(jié)構(gòu)體還是可以用的
_yuv = yuv;
// 打開文件
_file = new QFile(yuv.filename);
if (!_file->open(QFile::ReadOnly)) {
qDebug() << "open file error:" << yuv.filename;
return;
}
// 刷幀的時(shí)間間隔
_interval = 1000 / _yuv.fps;
// 計(jì)算一幀圖片的大小
_imageSize = av_image_get_buffer_size(yuv.pixelFormat, yuv.width, yuv.height, 1);
// 組件的尺寸(播放器)
int w = width();
int h = height();
// 原視頻的寬度 _yuv.width
int dstX = 0;
int dstY = 0;
int dstW = yuv.width;
int dstH = yuv.height;
// 計(jì)算目標(biāo)尺寸
if (dstW > w || dstH > h) {
// (dstW / dstH) * h * dstH > (w / h) * h * dstH
if ((dstW * h) > (w * dstH)) {
dstH = dstH * w / dstW ;
dstW = w;
} else {
dstW = dstW * h / dstH;
dstH = h;
}
}
// 居中視頻
dstX = (w - dstW) >> 1;
dstY = (h - dstH) >> 1;
qDebug() << "視頻的Frame:" << dstX << dstY << dstW << dstH;
_dstRect = QRect(dstX, dstY, dstW, dstH);
}
// 是否正在播放
bool YuvPlayer::isPlaying()
{
return _state == Playing;
}
// 獲取播放狀態(tài)
YuvPlayer::State YuvPlayer::getState()
{
return _state;
}
// 定時(shí)器回調(diào)函數(shù)缺谴,在此處播放 YUV
void YuvPlayer::timerEvent(QTimerEvent *event)
{
char data[_imageSize];
if (_file->read(data, _imageSize) > 0) {
// 像素格式轉(zhuǎn)換 yuv420p -> rgb24
RawVideoFrame in = {
data,
_yuv.width, _yuv.height,
_yuv.pixelFormat
};
RawVideoFrame out = {
nullptr,
_yuv.width >> 4 << 4, _yuv.height >> 4 << 4,
AV_PIX_FMT_RGB24
};
FFmpegUtils::convretRawVideo(in, out);
freeCurrentImage();
_currentImage = new QImage((uchar *)out.pixels, out.width, out.height, QImage::Format_RGB888);
// 刷新 調(diào)用 update 函數(shù)會(huì)調(diào)用 paintEvent
update();
} else {
// 文件已經(jīng)全部讀取完畢
stopTimer();
setState(Finished);
}
}
// 當(dāng)組件需要重繪時(shí)會(huì)調(diào)用此函數(shù)
// 要繪制的內(nèi)容在此函數(shù)中實(shí)現(xiàn)
void YuvPlayer::paintEvent(QPaintEvent *event)
{
if (!_currentImage) return;
// 將圖片繪制到當(dāng)前組件上
QPainter(this).drawImage(_dstRect, *_currentImage);
}
// 釋放圖片資源
void YuvPlayer::freeCurrentImage()
{
if (!_currentImage) return;
free(_currentImage->bits());
delete _currentImage;
_currentImage = nullptr;
}
// 停止定時(shí)器
void YuvPlayer::stopTimer()
{
if (_timerId == 0) return;
killTimer(_timerId);
_timerId = 0;
}
播放器函數(shù)調(diào)用:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "yuvplayer.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 創(chuàng)建播放器
_player = new YuvPlayer(this);
// 設(shè)置播放器的位置和尺寸
int w = 500;
int h = 400;
int x = (width() - w) >> 1;
int y = (height() - h) >> 1;
_player->setGeometry(x, y, w, h);
// 設(shè)置需要播放的文件
Yuv yuv = {
"/Users/mac/Downloads/pic/Dragon_Ball_640x480_yuv420p.yuv",
640, 480,
AV_PIX_FMT_YUV420P,
30
};
_player->setYuv(yuv);
// 監(jiān)聽播放器
connect(_player, &YuvPlayer::stateChanged, this, &MainWindow::onPlayerStateChanged);
}
MainWindow::~MainWindow()
{
delete _player;
delete ui;
}
void MainWindow::on_playButton_clicked()
{
if (_player->isPlaying()) { // 正在播放
_player->pause();
ui->playButton->setText("Play");
qDebug() << "暫停";
} else { // 暫停/停止播放
_player->play();
ui->playButton->setText("Pause");
qDebug() << "播放";
}
}
void MainWindow::on_stopButton_clicked()
{
if (_player->isPlaying()) { // 正在播放
_player->stop();
ui->playButton->setText("Play");
qDebug() << "停止";
}
}
void MainWindow::onPlayerStateChanged()
{
if (_player->getState() == YuvPlayer::Playing) { // 播放狀態(tài)
ui->playButton->setText("Pause");
} else { // 非播放狀態(tài)
ui->playButton->setText("Play");
}
}