前面文章 FFmpeg像素格式转换 中我们使用 FFmpeg 实现了一个像素格式转换工具类,现在我们就可以在 Qt 中利用 QImage 很容易的实现一个简单的 YUV 播放器了。
播放器功能很简单,只有播放、暂停和停止。我们定义了一个播放器类 YuvPlayer
,首先在 .h 文件中定义外部调用的函数,还需要一个设置播放文件的函数,既然是播放 yuv 文件,那么就需要额外再告诉播放器视频的宽高、像素格式以及帧率,我们定义了一个包括这些参数的结构体 Yuv
:
#ifndef YUVPLAYER_H
#define YUVPLAYER_H
#include
typedef struct {
// 文件路径
const char *filename;
// yuv 的宽
int width;
// yuv 的高
int height;
// yuv 像素格式
AVPixelFormat pixelFormat;
// 帧率
int fps;
} Yuv;
class YuvPlayer : public QWidget
{
Q_OBJECT
public:
// 播放器的状态
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);
};
#endif // YUVPLAYER_H
setYuv
函数用来设置我们要播放的 yuv 文件,可以放到这个函数中的操作有:
1、打开 yuv 文件;
2、计算刷帧的时间间隔;
3、计算一帧图像的大小;
4、计算视频目标尺寸,在播放控件中居中显示视频;
void YuvPlayer::setYuv(Yuv &yuv)
{
_yuv = yuv;
// 打开文件
_file = new QFile(yuv.filename);
if (!_file->open(QFile::ReadOnly)) {
qDebug() << "open file error:" << yuv.filename;
return;
}
// 刷帧的时间间隔
_interval = 1000 / _yuv.fps;
// 计算一帧图像的大小
_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;
// 缩放视频,计算目标尺寸
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;
// 计算后的视频宽高
_dstRect = QRect(dstX, dstY, dstW, dstH);
}
在播放器中完整居中显示 YUV 视频,会遇到四种情况:1、视频宽高都小于等于播放器宽高;2、视频宽大于播放器宽,视频高小于播放器高;3、视频高大于播放器高,视频宽小于播放器宽;4、视频宽高都大于播放器宽高(等同于情况 2 或者 3);总结下来实际有下图三种情况,第 1 种情况,我们居中显示视频就可以,第 2、3、4 种情况需要视频宽高比不变的情况下对视频进行等比例伸缩,需要伸缩到视频可以在播放器中完整显示。
play
函数中开启了一个定时器,定时器执行间隔取决于帧率,执行间隔在 setYuv
中计算得到,startTimer
是 QObject 中的方法,只要继承 QObject 就可以使用这个函数:
void YuvPlayer::play() {
// 防止多次调用 play 函数开启多个定时器
if (_state == Playing) return;
// 状态可能是:暂停、停止、正常完毕
_timerId = startTimer(_interval);
setState(Playing);
}
定时器开启后每隔一定间隔会调用 timerEvent
函数,这个函数中我们从文件读取一帧 yuv 数据,使用我们之前实现的像素格式转换工具将 yuv420p 格式数据转换成 rgb24 格式数据,然后将数据渲染到 QImage 上面,调用 update
函数刷新。此处需要注意一个问题,像素格式转换后的输出视频宽高不是 16 的倍数会降低转码速度,建议输出视频宽高是 16 倍数:
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 { // 文件数据已经读取完毕
// 停止定时器
stopTimer();
// 正常播放完毕
setState(Finished);
}
}
当调用 update
函数的时候,就会触发 paintEvent
,在这个函数中将图片绘制到当前组件上。当组件想重绘的时候,也会调用这个函数:
void YuvPlayer::paintEvent(QPaintEvent *event) {
if (!_currentImage) return;
// 将图片绘制到当前组件上
QPainter(this).drawImage(_dstRect, *_currentImage);
}
接下来继续实现暂停和停止功能:
void YuvPlayer::pause() {
if (_state != Playing) return;
// 状态可能是:正在播放
// 停止定时器
stopTimer();
// 改变状态
setState(Paused);
}
void YuvPlayer::stop() {
if (_state == Stopped) return;
// 状态可能是:正在播放、暂停、正常完毕
// 停止定时器
stopTimer();
// 释放图片
freeCurrentImage();
// 刷新
update();
// 改变状态
setState(Stopped);
}
QFile 会记录上次读取文件的位置,当播放完毕时,要将读取指针回归到最初始的位置。作为一个播放器,需要时刻向外界发送一些消息,比如暂停或者继续播放等等需要通知外界,我们利用 Qt 信号和槽机制,在信号声明区下面定义了一个信号stateChange
,当播放器状态发生改变时我们发送一个信号,外界与此信号关联的槽函数就会被调用:
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
#include
extern "C" {
#include
}
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat pixelFormat;
int fps; // 帧率
} Yuv;
class YuvPlayer : public QWidget
{
Q_OBJECT
public:
// 播放器的状态
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; // 先写一个0,否则有可能是个垃圾值
// 成员变量最好不要设置为引用,有可能引用外部的变量,如果引用的外部变量是一个临时变量(比如栈空间变量,函数销毁引用的内存就会被销毁),临时变量被销毁引用就会很危险,
// Yuv &_yuv;
Yuv _yuv;
State _state = Stopped;
QImage *_currentImage = nullptr;
// 视频大小
QRect _dstRect;
// 一帧图片的大小
int _imageSize = 0;
int _imgSize;
// 刷帧的时间间隔
int _interval;
/** 改变状态 */
void setState(State state);
/** 释放QImage */
void freeCurrentImage();
/** 杀掉定时器 */
void stopTimer();
void timerEvent(QTimerEvent *event);
void paintEvent(QPaintEvent *event);
signals:
void stateChanged();
};
#endif // YUVPLAYER_H
yuvplayer.m
#include "yuvplayer.h"
#include
#include
#include
extern "C" {
#include
}
YuvPlayer::YuvPlayer(QWidget *parent) : QWidget(parent)
{
// 设置控件背景色
setAttribute(Qt::WA_StyledBackground);
setStyleSheet("background: black");
}
YuvPlayer::~YuvPlayer()
{
_file->close();
freeCurrentImage();
}
// 播放
void YuvPlayer::play()
{
if (getState() == Playing) return;
// 开启定时器
_timerId = startTimer(_interval);
setState(Playing);
}
// 暂停
void YuvPlayer::pause()
{
if (getState() != Playing) return;
stopTimer();
setState(Paused);
}
// 停止
void YuvPlayer::stop()
{
if (getState() == Stopped) return;
// 状态可能是 正在播放 暂停 正常完毕
stopTimer();
// 清空屏幕
freeCurrentImage();
update();
setState(Stopped);
}
// 设置播放器状态
void YuvPlayer::setState(State state)
{
if (_state == state) return;
// 停止/播放完成状态,需要从文件开始位置读取
if (state == Stopped || state == Finished) {
_file->seek(0);
}
_state = state;
// 发送状态改变信号
emit stateChanged();
}
void YuvPlayer::setYuv(Yuv &yuv)
{
// 使用结构体,赋值相当于拷贝,引用的外部变量被销毁,当前结构体还是可以用的
_yuv = yuv;
// 打开文件
_file = new QFile(yuv.filename);
if (!_file->open(QFile::ReadOnly)) {
qDebug() << "open file error:" << yuv.filename;
return;
}
// 刷帧的时间间隔
_interval = 1000 / _yuv.fps;
// 计算一帧图片的大小
_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;
// 计算目标尺寸
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;
}
// 获取播放状态
YuvPlayer::State YuvPlayer::getState()
{
return _state;
}
// 定时器回调函数,在此处播放 YUV
void YuvPlayer::timerEvent(QTimerEvent *event)
{
char data[_imageSize];
if (_file->read(data, _imageSize) > 0) {
// 像素格式转换 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);
// 刷新 调用 update 函数会调用 paintEvent
update();
} else {
// 文件已经全部读取完毕
stopTimer();
setState(Finished);
}
}
// 当组件需要重绘时会调用此函数
// 要绘制的内容在此函数中实现
void YuvPlayer::paintEvent(QPaintEvent *event)
{
if (!_currentImage) return;
// 将图片绘制到当前组件上
QPainter(this).drawImage(_dstRect, *_currentImage);
}
// 释放图片资源
void YuvPlayer::freeCurrentImage()
{
if (!_currentImage) return;
free(_currentImage->bits());
delete _currentImage;
_currentImage = nullptr;
}
// 停止定时器
void YuvPlayer::stopTimer()
{
if (_timerId == 0) return;
killTimer(_timerId);
_timerId = 0;
}
播放器函数调用:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "yuvplayer.h"
#include
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 创建播放器
_player = new YuvPlayer(this);
// 设置播放器的位置和尺寸
int w = 500;
int h = 400;
int x = (width() - w) >> 1;
int y = (height() - h) >> 1;
_player->setGeometry(x, y, w, h);
// 设置需要播放的文件
Yuv yuv = {
"/Users/mac/Downloads/pic/Dragon_Ball_640x480_yuv420p.yuv",
640, 480,
AV_PIX_FMT_YUV420P,
30
};
_player->setYuv(yuv);
// 监听播放器
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) { // 播放状态
ui->playButton->setText("Pause");
} else { // 非播放状态
ui->playButton->setText("Play");
}
}