前面文章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 文件,可以放到这个函数中的操作有:

  1. 打开yuv文件;
  2. 计算刷帧的时间间隔;
  3. 计算一帧图像的大小;
  4. 计算视频目标尺寸,在播放控件中居中显示视频;
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 视频,会遇到四种情况:

  1. 视频宽高都小于等于播放器宽高;
  2. 视频宽大于播放器宽,视频高小于播放器高;
  3. 视频高大于播放器高,视频宽小于播放器宽;
  4. 视频宽高都大于播放器宽高(等同于情况 2 或者 3);

总结下来实际有下图三种情况,第 1 种情况,我们居中显示视频就可以,第 2、3、4 种情况需要视频宽高比不变的情况下对视频进行等比例伸缩,需要伸缩到视频可以在播放器中完整显示。

play函数中开启了一个定时器,定时器执行间隔取决于帧率,执行间隔在setYuv中计算得到,startTimerQObject中的方法,只要继承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("播放");
}
}

代码连接

24_用Qt和FFmpeg实现简单的YUV播放器的更多相关文章

  1. 简单的音乐播放器(VS 2010 + Qt 4.8.5)

    昨天历经千辛万苦,配置好了VS 2010中的Qt环境(包括Qt for VS插件),今天决定浅浅地品味一下将两者结合进行编程的魅力. 上网查了一些资料,学习了一些基础知识,决定做一个简单的音乐播放器, ...

  2. swift3.0 简单直播和简单网络音乐播放器

    本项目采用swift3.0所写,适配iOS9.0+,所有界面均采用代码布局. 第一个tab写的是简单直播,传统MVC模式,第二个tab写的是简单网络音乐播放器.传说MVVM模式(至于血统是否纯正我就不 ...

  3. 用PHP+H5+Boostrap做简单的音乐播放器(进阶版)

    前言:之前做了一个音乐播放器(纯前端),意外的受欢迎,然后有人建议我把后台一起做了,正好也想学习后台,所以学了两天php(不要吐槽我的速度,慢工出细活嘛~)然后在之前的基础上也又完善了一些功能,所以这 ...

  4. ffmpeg+SDL2实现的音频播放器V2.0(无杂音)

    1. 前言 目前为止,学习了并记录了ffmpeg+SDL2显示视频以及事件(event)的内容. 这篇中记录ffmpeg+SDL2播放音频,没加入事件处理. 接下来加入事件处理并继续学习音视频同步,再 ...

  5. 一个基于JRTPLIB的轻量级RTSP客户端(myRTSPClient)——解码篇:(三)一个简单的rtsp播放器

    该篇内容简单的将前两篇内容组合在一起,创建了2个线程,分别播放音频和视频. int main(int argc, char * argv[]) { RtspClient Client; pthread ...

  6. 使用Service组件实现简单的音乐播放器功能 --Android基础

    1.本例利用Service实现简单的音乐播放功能,下面是效果图.(点击开始播放开启服务,音乐播放,点击“停止播放”关闭服务,音乐停止播放.) 2.核心代码: MusicService.java: pa ...

  7. Android课程---简单的音乐播放器

    第一个:用Activity实现 activity_music_play1.xml <?xml version="1.0" encoding="utf-8" ...

  8. 用H5+Boostrap做简单的音乐播放器

    前言:这个是综合一下我最近在学的东西做的小Demo,到实际使用还有距离,但是用来练手巩固知识点还是不错的,最近在二刷JS书和Boostrap.css的源码,做完这个Demo也算是暂告一段落,接下来是j ...

  9. HTML5+CSS3+jquery实现简单的音乐播放器

    ...最近天热的,感觉就像煎饼...然后别人在把妹子的时候,只有偶们这帮苦逼的程序员在那边撸代码...我日哦! 然后今天晒的是偶早年写的一个播放器...看上去是不是很有感觉的样子!一番宝物,Lisa唱 ...

  10. Android——用Activity和Service实现简单的音乐播放器

    一.只用Activity 容易出现问题 xml <?xml version="1.0" encoding="utf-8"?> <LinearL ...

随机推荐

  1. Python中os.walk函数说明

    这个函数对于文件方面的遍历等其他方面的操作来说功能很强大,比如批量修改文件名.批量移动文件.将所有不在一个文件夹下的文件移动到同一个文件夹下等等. 这个其实很简单的,用一个示例就能明白这个函数的具体用 ...

  2. Cnpack ctrl+alt+v 来回切换 变量声明区,和代码写区,非常方便

    Cnpack ctrl+alt+v 来回切换 变量声明区,和代码写区,非常方便 非常方便

  3. .NET Core 在 K8S 上的开发实践--学习笔记

    摘要 本主题受众是架构师,开发人员,互联网企业 IT 运维人员.大纲:1. K8S 对应用的要求:2. .NET Core 上 K8S 的优势:3. K8S 下的 .NET Core 配置:4. .N ...

  4. JS 可选链操作符?. 空值合并运算符?? 详解,更精简的安全取值与默认值设置小技巧

    壹 ❀ 引 说来也比较惭愧,可选链操作符?.在公司项目中使用特别频繁,而我在之前一直以为是类似奇技淫巧的写法,所以也没去查阅相关文档,直到在学习typescript时碰巧看到了可选链操作符与空值合并操 ...

  5. JS Leetcode 264. 丑数 II 题解分析,当暴力无法暴力,让我们弃武从文了解三指针

    壹 ❀ 引 我在JS Leetcode 263. 丑数 题解分析,来认识有趣的丑数吧一文中记录了简单难度的丑数题,那么这篇题解是它的升级版,题目来自LeetCode264. 丑数 II,题目描述如下: ...

  6. STC89C52控制74HC595,74HC138双色16x16点阵屏循环显示汉字

    简介 常见的LED点阵除了使用MAX7219, 还有一部分是使用74HC595, 前者能主动刷新, 后者需要上位机主动扫描刷新. 手里这块是德飞莱的16x16LED点阵模块, 板上印的型号LY-LED ...

  7. 使用yum 报错 :This system is not registered with RHN

    解决办法:(假定你已安装yum,且网络畅通)更改yum的源, 即更换/etc/yum.repos.d/rhel-debuginfo.repo 这个文件.首先备份,如下所示: [root@localho ...

  8. vue+antv g6+element-ui完整流程图

    最近一直在研究流程图相关的技术,一次在逛GitHub时发现了一个技术栈为vue+g6+element-ui的项目,基础功能完好,如node与edge的托拉拽,主界面如下:

  9. 美团面试:说说OOM三大场景和解决方案? (绝对史上最全)

    首先,咱们先聊聊,什么是OOM? 小伙伴们,有没有遇到过程序突然崩溃,然后抛出一个OutOfMemoryError的异常?这就是我们俗称的OOM,也就是内存溢出.简单来说,就是你的Java应用想要的内 ...

  10. Dubbo使用APISIX作为网关

    为什么使用网关 Dubbo服务本身没有暴露HTTP接口,客户端(如:Web,APP)无法直接调用其提供的方法. 而APISIX可以通过dubbo-proxy插件为Dubbo服务提供外部访问的HTTP接 ...