用Qt和FFmpeg實(shí)現(xiàn)簡(jiǎn)單的YUV播放器

前面文章 FFmpeg像素格式轉(zhuǎn)換 中我們使用 FFmpeg 實(shí)現(xiàn)了一個(gè)像素格式轉(zhuǎn)換工具類,現(xiàn)在我們就可以在 Qt 中利用 QImage 很容易的實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 YUV 播放器了。

播放器功能很簡(jiǎn)單,只有播放、暫停和停止。我們定義了一個(gè)播放器類 YuvPlayer,首先在 .h 文件中定義外部調(diào)用的函數(shù),還需要一個(gè)設(shè)置播放文件的函數(shù),既然是播放 yuv 文件,那么就需要額外再告訴播放器視頻的寬高、像素格式以及幀率,我們定義了一個(gè)包括這些參數(shù)的結(jié)構(gòu)體 Yuv

#ifndef YUVPLAYER_H
#define YUVPLAYER_H

#include <QWidget>

typedef struct {
    // 文件路徑
    const char *filename;
    // yuv 的寬
    int width;
    // yuv 的高
    int height;
    // yuv 像素格式
    AVPixelFormat pixelFormat; 
    // 幀率
    int fps; 
} Yuv;

class YuvPlayer : public QWidget
{
    Q_OBJECT
public:

    // 播放器的狀態(tài)
    typedef enum {
        Stopped = 0, // 停止
        Playing, // 播放中
        Paused, // 暫停
        Finished // 播放完成
    } State;

    explicit YuvPlayer(QWidget *parent = nullptr);
    ~YuvPlayer();

    // 播放
    void play();
    // 暫停
    void pause();
    // 停止
    void stop();
    // 播放器是否播放中
    bool isPlaying();
    // 獲取播放器當(dāng)前狀態(tài)
    State getState();

    // 設(shè)置播放文件
    void setYuv(Yuv &yuv);
};

#endif // YUVPLAYER_H

setYuv 函數(shù)用來(lái)設(shè)置我們要播放的 yuv 文件,可以放到這個(gè)函數(shù)中的操作有:
1、打開(kāi) yuv 文件;
2、計(jì)算刷幀的時(shí)間間隔;
3、計(jì)算一幀圖像的大小;
4、計(jì)算視頻目標(biāo)尺寸,在播放控件中居中顯示視頻;

void YuvPlayer::setYuv(Yuv &yuv)
{
    _yuv = yuv;

    // 打開(kāi)文件
    _file = new QFile(yuv.filename);
    if (!_file->open(QFile::ReadOnly)) {
        qDebug() << "open file error:" << yuv.filename;
        return;
    }

    // 刷幀的時(shí)間間隔
    _interval = 1000 / _yuv.fps;

    // 計(jì)算一幀圖像的大小
    _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;

    // 縮放視頻,計(jì)算目標(biāo)尺寸
    if (dstW > w || dstH > h) {
        // 視頻的寬高比 > 播放器的寬高比,(dstW / dstH)  > (w / h) 變換而來(lái)
        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;
   // 計(jì)算后的視頻寬高
    _dstRect = QRect(dstX, dstY, dstW, dstH);
}

在播放器中完整居中顯示 YUV 視頻,會(huì)遇到四種情況:1、視頻寬高都小于等于播放器寬高;2、視頻寬大于播放器寬,視頻高小于播放器高;3、視頻高大于播放器高,視頻寬小于播放器寬;4、視頻寬高都大于播放器寬高(等同于情況 2 或者 3);總結(jié)下來(lái)實(shí)際有下圖三種情況,第 1 種情況,我們居中顯示視頻就可以,第 2、3、4 種情況需要視頻寬高比不變的情況下對(duì)視頻進(jìn)行等比例伸縮,需要伸縮到視頻可以在播放器中完整顯示。

play 函數(shù)中開(kāi)啟了一個(gè)定時(shí)器,定時(shí)器執(zhí)行間隔取決于幀率,執(zhí)行間隔在 setYuv 中計(jì)算得到,startTimer 是 QObject 中的方法,只要繼承 QObject 就可以使用這個(gè)函數(shù):

void YuvPlayer::play() {
    // 防止多次調(diào)用 play 函數(shù)開(kāi)啟多個(gè)定時(shí)器
    if (_state == Playing) return;
    // 狀態(tài)可能是:暫停、停止、正常完畢
    _timerId = startTimer(_interval);
    setState(Playing);
}

定時(shí)器開(kāi)啟后每隔一定間隔會(huì)調(diào)用 timerEvent 函數(shù),這個(gè)函數(shù)中我們從文件讀取一幀 yuv 數(shù)據(jù),使用我們之前實(shí)現(xiàn)的像素格式轉(zhuǎn)換工具將 yuv420p 格式數(shù)據(jù)轉(zhuǎn)換成 rgb24 格式數(shù)據(jù),然后將數(shù)據(jù)渲染到 QImage 上面,調(diào)用 update 函數(shù)刷新。此處需要注意一個(gè)問(wèn)題,像素格式轉(zhuǎn)換后的輸出視頻寬高不是 16 的倍數(shù)會(huì)降低轉(zhuǎn)碼速度,建議輸出視頻寬高是 16 倍數(shù):

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 { // 文件數(shù)據(jù)已經(jīng)讀取完畢
        // 停止定時(shí)器
        stopTimer();
        // 正常播放完畢
        setState(Finished);
    }
}

當(dāng)調(diào)用 update 函數(shù)的時(shí)候,就會(huì)觸發(fā) paintEvent,在這個(gè)函數(shù)中將圖片繪制到當(dāng)前組件上。當(dāng)組件想重繪的時(shí)候,也會(huì)調(diào)用這個(gè)函數(shù):

void YuvPlayer::paintEvent(QPaintEvent *event) {
    if (!_currentImage) return;
    // 將圖片繪制到當(dāng)前組件上
    QPainter(this).drawImage(_dstRect, *_currentImage);
}

接下來(lái)繼續(xù)實(shí)現(xiàn)暫停和停止功能:

void YuvPlayer::pause() {
    if (_state != Playing) return;
    // 狀態(tài)可能是:正在播放
    // 停止定時(shí)器
    stopTimer();
    // 改變狀態(tài)
    setState(Paused);
}

void YuvPlayer::stop() {
    if (_state == Stopped) return;
    // 狀態(tài)可能是:正在播放、暫停、正常完畢
    // 停止定時(shí)器
    stopTimer();
    // 釋放圖片
    freeCurrentImage();
    // 刷新
    update();
    // 改變狀態(tài)
    setState(Stopped);
}

QFile 會(huì)記錄上次讀取文件的位置,當(dāng)播放完畢時(shí),要將讀取指針回歸到最初始的位置。作為一個(gè)播放器,需要時(shí)刻向外界發(fā)送一些消息,比如暫停或者繼續(xù)播放等等需要通知外界,我們利用 Qt 信號(hào)和槽機(jī)制,在信號(hào)聲明區(qū)下面定義了一個(gè)信號(hào)stateChange,當(dāng)播放器狀態(tài)發(fā)生改變時(shí)我們發(fā)送一個(gè)信號(hào),外界與此信號(hào)關(guān)聯(lián)的槽函數(shù)就會(huì)被調(diào)用:

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>
}

typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixelFormat; 
    int fps; // 幀率
} Yuv;

class YuvPlayer : public QWidget
{
    Q_OBJECT
public:

    // 播放器的狀態(tài)
    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; // 先寫一個(gè)0,否則有可能是個(gè)垃圾值
    // 成員變量最好不要設(shè)置為引用,有可能引用外部的變量,如果引用的外部變量是一個(gè)臨時(shí)變量(比如??臻g變量,函數(shù)銷毀引用的內(nèi)存就會(huì)被銷毀),臨時(shí)變量被銷毀引用就會(huì)很危險(xiǎn),
    // Yuv &_yuv;
    Yuv _yuv;
    State _state = Stopped;
    QImage *_currentImage = nullptr;
    // 視頻大小
    QRect _dstRect;
    // 一幀圖片的大小
    int _imageSize = 0;
    int _imgSize;
    // 刷幀的時(shí)間間隔 
    int _interval;

    /** 改變狀態(tài) */
    void setState(State state);
    /** 釋放QImage */
    void freeCurrentImage();
    /** 殺掉定時(shí)器 */
    void stopTimer();

    void timerEvent(QTimerEvent *event);
    void paintEvent(QPaintEvent *event);

signals:
    void stateChanged();
};

#endif // YUVPLAYER_H

yuvplayer.m

#include "yuvplayer.h"

#include <QDebug>

#include <QPainter>

#include <ffmpegutils.h>

extern "C" {
    #include <libavutil/imgutils.h>
}

YuvPlayer::YuvPlayer(QWidget *parent) : QWidget(parent)
{
    // 設(shè)置控件背景色
    setAttribute(Qt::WA_StyledBackground);
    setStyleSheet("background: black");
}

YuvPlayer::~YuvPlayer()
{
    _file->close();
    freeCurrentImage();
}

// 播放
void YuvPlayer::play()
{
    if (getState() == Playing) return;
    // 開(kāi)啟定時(shí)器
    _timerId = startTimer(_interval);
    setState(Playing);
}

// 暫停
void YuvPlayer::pause()
{
    if (getState() != Playing) return;
    stopTimer();
    setState(Paused);
}

// 停止
void YuvPlayer::stop()
{
    if (getState() == Stopped) return;
    // 狀態(tài)可能是 正在播放 暫停 正常完畢
    stopTimer();
    // 清空屏幕
    freeCurrentImage();
    update();
    setState(Stopped);
}

// 設(shè)置播放器狀態(tài)
void YuvPlayer::setState(State state)
{
    if (_state == state) return;
    // 停止/播放完成狀態(tài),需要從文件開(kāi)始位置讀取
    if (state == Stopped || state == Finished) {
        _file->seek(0);
    }
    _state = state;
    // 發(fā)送狀態(tài)改變信號(hào)
    emit stateChanged();
}

void YuvPlayer::setYuv(Yuv &yuv)
{
    // 使用結(jié)構(gòu)體,賦值相當(dāng)于拷貝,引用的外部變量被銷毀,當(dāng)前結(jié)構(gòu)體還是可以用的
    _yuv = yuv;

    // 打開(kāi)文件
    _file = new QFile(yuv.filename);
    if (!_file->open(QFile::ReadOnly)) {
        qDebug() << "open file error:" << yuv.filename;
        return;
    }

    // 刷幀的時(shí)間間隔
    _interval = 1000 / _yuv.fps;

    // 計(jì)算一幀圖片的大小
    _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;

    // 計(jì)算目標(biāo)尺寸
    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;
}

// 獲取播放狀態(tài)
YuvPlayer::State YuvPlayer::getState()
{
    return _state;
}

// 定時(shí)器回調(diào)函數(shù),在此處播放 YUV
void YuvPlayer::timerEvent(QTimerEvent *event)
{

    char data[_imageSize];
    if (_file->read(data, _imageSize) > 0) {
        // 像素格式轉(zhuǎn)換 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);

        // 刷新 調(diào)用 update 函數(shù)會(huì)調(diào)用 paintEvent
        update();
    } else {
        // 文件已經(jīng)全部讀取完畢
        stopTimer();
        setState(Finished);
    }
}

// 當(dāng)組件需要重繪時(shí)會(huì)調(diào)用此函數(shù)
// 要繪制的內(nèi)容在此函數(shù)中實(shí)現(xiàn)
void YuvPlayer::paintEvent(QPaintEvent *event)
{
    if (!_currentImage) return;
    // 將圖片繪制到當(dāng)前組件上
    QPainter(this).drawImage(_dstRect, *_currentImage);
}

// 釋放圖片資源
void YuvPlayer::freeCurrentImage()
{
    if (!_currentImage) return;
    free(_currentImage->bits());
    delete _currentImage;
    _currentImage = nullptr;
}

// 停止定時(shí)器
void YuvPlayer::stopTimer()
{
    if (_timerId == 0) return;
    killTimer(_timerId);
    _timerId = 0;
}

播放器函數(shù)調(diào)用:

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "yuvplayer.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 創(chuàng)建播放器
    _player = new YuvPlayer(this);

    // 設(shè)置播放器的位置和尺寸
    int w = 500;
    int h = 400;
    int x = (width() - w) >> 1;
    int y = (height() - h) >> 1;
    _player->setGeometry(x, y, w, h);

    // 設(shè)置需要播放的文件
    Yuv yuv = {
        "/Users/mac/Downloads/pic/Dragon_Ball_640x480_yuv420p.yuv",
        640, 480,
        AV_PIX_FMT_YUV420P,
        30
    };
    _player->setYuv(yuv);

    // 監(jiān)聽(tīng)播放器
    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) { // 播放狀態(tài)
        ui->playButton->setText("Pause");
    } else { // 非播放狀態(tài)
        ui->playButton->setText("Play");
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容