24_用Qt和FFmpeg实现简单的YUV播放器
前面文章FFmpeg像素格式转换
中我们使用FFmpeg实现了一个像素格式转换工具类,现在我们就可以在Qt中利用QImage
很容易的实现一个简单的YUV播放器了。
播放器功能很简单,只有播放、暂停和停止。我们定义了一个播放器类YuvPlayer
,首先在yuvplayer.h
文件中定义外部调用的函数,还需要一个设置播放文件的函数,既然是播放yuv文件,那么就需要额外再告诉播放器视频的宽高、像素格式以及帧率,我们定义了一个包括这些参数的结构体Yuv:
#ifndef YUVPLAYER_H #define YUVPLAYER_H #include <QWidget> #include <QFile> extern "C"{ #include <libavutil/avutil.h> #include <libavutil/imgutils.h> } 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(); void setYuv(Yuv &yuv); State getState(); signals: private: QFile _file; int _timerId = 0; State _state = Stopped; Yuv _yuv; bool _playing; QImage *_currentImage = nullptr; QRect _dstRect; void timerEvent(QTimerEvent *event); void paintEvent(QPaintEvent *event); void freeCurrentImage(); }; #endif // YUVPLAYER_H
setYuv
函数用来设置我们要播放的 yuv 文件,可以放到这个函数中的操作有:
- 打开yuv文件;
- 计算刷帧的时间间隔;
- 计算一帧图像的大小;
- 计算视频目标尺寸,在播放控件中居中显示视频;
void YuvPlayer::setYuv(Yuv &yuv){ _yuv = yuv; // 关闭上一个文件 closeFile(); // 打开文件 _file = new QFile(yuv.filename); if(!_file->open(QFile::ReadOnly)){ qDebug()<< "file open error" << yuv.filename; } // 刷帧的时间间隔 _interval = 1000 / _yuv.fps; // 一帧图片的大小 _imgSize = av_image_get_buffer_size(_yuv.pixelFormat, _yuv.width, _yuv.height, 1); // 组件的尺寸 int w = width(); int h = height(); // 计算rect int dx = 0; int dy = 0; int dw = yuv.width; int dh = yuv.height; // 缩放视频,计算目标尺寸 if (dw > w || dh > h) { // 缩放 //视频的宽高比 > 播放器的宽高比,由(dstW / dstH) > (w / h) 变换而来 if (dw * h > w * dh) { dh = w * dh / dw; dw = w; } else { dw = h * dw / dh; dh = h; } } // 居中 dx = (w - dw) >> 1; dy = (h - dh) >> 1; // 计算后的视频宽高 _dstRect = QRect(dx, dy, dw, dh); qDebug() << "视频的矩形框" << dx << dy << dw << dh; }
在播放器中完整居中显示 YUV 视频,会遇到四种情况:
- 视频宽高都小于等于播放器宽高;
- 视频宽大于播放器宽,视频高小于播放器高;
- 视频高大于播放器高,视频宽小于播放器宽;
- 视频宽高都大于播放器宽高(等同于情况 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,_yuv.height, AV_PIX_FMT_RGB24 }; FFmpegUtils::convertRawVideo(in,out); freeCurrentImage(); _currentImage = new QImage((uchar *)out.pixels, out.width,out.height,QImage::Format_BGR888); // 刷新 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; // 状态可能是:正在播放、暂停、正常完毕 // 释放图片 freeCurrentImage(); // 停止定时器 stopTimer(); // 刷新,会触发paintEvent方法的执行 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 <QWidget> #include <QFile> extern "C"{ #include <libavutil/avutil.h> #include <libavutil/imgutils.h> } 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(); void setYuv(Yuv &yuv); State getState(); signals: void stateChanged(); private: QFile *_file = nullptr; int _timerId = 0;// 先写一个0,否则有可能是个垃圾值 State _state = Stopped; Yuv _yuv; bool _playing; QImage *_currentImage = nullptr; // 视频大小 QRect _dstRect; // 刷帧的时间间隔 int _interval; // 一帧图片的大小 int _imgSize = 0; void timerEvent(QTimerEvent *event); void paintEvent(QPaintEvent *event); void freeCurrentImage(); // 改变状态 void setState(State state); void stopTimer(); void closeFile(); }; #endif // YUVPLAYER_H
yuvplayer.cpp
#include "yuvplayer.h" #include <QDebug> #include <QPainter> #include "ffmpegutils.h" #define RET(judge, func) \ if (judge) { \ qDebug() << #func << "error" << SDL_GetError(); \ return; \ } YuvPlayer::YuvPlayer(QWidget *parent) : QWidget(parent){ // 设置背景色 // setAttribute(Qt::WA_StyledBackground,true); setAttribute(Qt::WA_StyledBackground); setStyleSheet("background:black"); } YuvPlayer::~YuvPlayer(){ closeFile(); freeCurrentImage(); stopTimer(); } void YuvPlayer::play(){ // 防止多次调用 play 函数开启多个定时器 if(_state == Playing) return; // 状态可能是:暂停、停止、正常完毕 // 开启定时器 _timerId = startTimer(_interval); setState(Playing); } void YuvPlayer::pause(){ if(_state != Playing) return; // 状态可能是:正在播放 // 停止定时器 stopTimer(); setState(Paused); } void YuvPlayer::stop(){ if(_state == Stopped) return; // 状态可能是:正在播放、暂停、正常完毕 // 释放图片 freeCurrentImage(); // 停止定时器 stopTimer(); // 刷新,会触发paintEvent方法的执行 update(); setState(Stopped); } bool YuvPlayer::isPlaying(){ return _state == YuvPlayer::Playing; } YuvPlayer::State YuvPlayer::getState(){ return _state; } 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; // 关闭上一个文件 closeFile(); // 打开文件 _file = new QFile(yuv.filename); if(!_file->open(QFile::ReadOnly)){ qDebug()<< "file open error" << yuv.filename; } // 刷帧的时间间隔 _interval = 1000 / _yuv.fps; // 一帧图片的大小 _imgSize = av_image_get_buffer_size(_yuv.pixelFormat, _yuv.width, _yuv.height, 1); // 组件的尺寸 int w = width(); int h = height(); // 计算rect int dx = 0; int dy = 0; int dw = yuv.width; int dh = yuv.height; // 缩放视频,计算目标尺寸 if (dw > w || dh > h) { // 缩放 //视频的宽高比 > 播放器的宽高比,由(dstW / dstH) > (w / h) 变换而来 if (dw * h > w * dh) { dh = w * dh / dw; dw = w; } else { dw = h * dw / dh; dh = h; } } // 居中 dx = (w - dw) >> 1; dy = (h - dh) >> 1; // 计算后的视频宽高 _dstRect = QRect(dx, dy, dw, dh); qDebug() << "视频的矩形框" << dx << dy << dw << dh; } // 当组件想重绘的时候,就会调用这个函数 // 想要绘制什么内容,在这个函数中实现 void YuvPlayer::paintEvent(QPaintEvent *event){ if (!_currentImage) return; // 将图片绘制到当前组件上 // QPainter(this).drawImage(QPoint(0,0), *_currentImage); QPainter(this).drawImage(_dstRect, *_currentImage); // QPainter(this).drawImage(QRect(0,0,width(),height()), *_currentImage); } 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,_yuv.height, AV_PIX_FMT_RGB24 }; FFmpegUtils::convertRawVideo(in,out); freeCurrentImage(); _currentImage = new QImage((uchar *)out.pixels, out.width,out.height,QImage::Format_BGR888); // 刷新 update(); }else{// 文件数据已经读取完毕 // 停止定时器 stopTimer(); // 正常播放完毕 setState(Finished); } } void YuvPlayer::freeCurrentImage() { if(!_currentImage) return; free(_currentImage->bits()); delete _currentImage; _currentImage = nullptr; } void YuvPlayer::stopTimer(){ if(_timerId == 0) return; killTimer(_timerId); _timerId = 0; } void YuvPlayer::closeFile(){ if(!_file) return; _file->close(); delete _file; _file = nullptr; }
mainwindow.cpp
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QDebug> #ifdef Q_OS_WIN #define FILENAME "../test/out_640x480.yuv" #else #define FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out_640x480.yuv" #endif MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow){ ui->setupUi(this); // 创建播放器 _player = new YuvPlayer(this); // 设置播放器的位置和尺寸 int w = 640; int h = 480; int x = (width() - w) >> 1; int y = (height() - h) >> 1; _player->setGeometry(x, y, w, h); // 设置需要播放的文件 Yuv yuv = { FILENAME, 640,480, AV_PIX_FMT_YUV420P, 30 }; _player->setYuv(yuv); // 监听播放 connect(_player,&YuvPlayer::stateChanged,this,&MainWindow::onPlayerStateChanged); } MainWindow::~MainWindow(){ delete ui; } void MainWindow::on_playButton_clicked(){ if(_player->isPlaying()){// 正在播放 _player->pause(); }else{// 没有正在播放 _player->play(); } } void MainWindow::on_stopButton_clicked(){ _player->stop(); } void MainWindow::onPlayerStateChanged(){ if(_player->getState() == YuvPlayer::Playing){ ui->playButton->setText("暂停"); }else{// 没有正在播放 ui->playButton->setText("播放"); } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!