前面文章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. 小团队如何妙用 JuiceFS

    早些年还在 ENJOY 的时候, 就已经在用 JuiceFS, 并且一路伴随着我工作过的四家小公司, 这玩意对我来说, 已经成了理所应当不可或缺的基础设施, 对于我服务过的小团队而言, 更是实实在在的 ...

  2. Adoquery.Refresh 慎用。。。。非常严重,会带来各种问题。

    adoquery.refresh 各种问题,根本启不到刷新的作用.完全不刷新的节奏. 修改成已经打印后,如果用adoquery.refresh的话,这两个订单 并不会被刷新掉,惨吧......

  3. NVME学习笔记六—Controller Architecture

    Controller架构   NVMe over Fabrics使用与NVMe基础规格说明书中定义相同的controller架构.这包括主机和controller之间使用SQ提交队列和CQ完成队列来执 ...

  4. Reactive 简介

    1. 概念 Reactive 非常适合低延迟.高吞吐量的工作负载. Reactive Processing 是一种范式(规范),它使开发人员能够构建非阻塞的.异步的应用程序,这些应用程序能够处理背压( ...

  5. phpBB3在Nginx反向代理中的X-Forwarded-For IP检查

    记录一下phpBB3对反向代理中的IP处理机制 处理几个phpBB3项目迁移, 部分运行场景转移到内网, 需要外网通过nginx/openresty之类的网关反向代理进行访问. 在网关处已经正确配置了 ...

  6. 用于解析FBNeo游戏数据的Python3脚本

    FBNeo在代码中存储了游戏的元数据, 其数据格式为 struct BurnDriver BurnDrvCpsStriderua = { "striderua", "st ...

  7. Java操作EasyExcel实现导入导出入门

    介绍 EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单.节省内存著称.EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从 ...

  8. SpringCloud SpringBoot 组件使用:使用Nacos作为服务的注册中心和配置中心

    基础篇 一.什么是Nacos? 官方介绍是这样的: Nacos 致力于帮助您发现.配置和管理微服务.Nacos 提供了一组简单易用的特性集,帮助您实现动态服务发现.服务配置管理.服务及流量管理. Na ...

  9. Golang微服务框架go-kratos分析:框架架构分析

    一.kratos设计理念 这里主要讲解 kratos v2 的设计理念. kratos 框架制定接口规范,然后通过插件来实现具体需求,实现自由定制.可插拔的微服务框架. 我们既可以选择 kratos ...

  10. 图书管理系统---基于form组件和modelform改造添加和编辑

    添加 基于form组件改造 步骤1 1.为了区分自己写的form类和视图逻辑,所以工作中需要区分开来,那么就可以在应用下创建一个叫utils的文件夹,专门存放我们写的form类,py文件名随便起 2. ...