侧边栏
首页代码

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

  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("播放");
    }
}

代码连接

posted @ 2022-10-15 20:51  咸鱼Jay  阅读(877)  评论(0编辑  收藏  举报
页脚HTML代码