用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ù)用來設(shè)置我們要播放的 yuv 文件,可以放到這個(gè)函數(shù)中的操作有:
1塌鸯、打開 yuv 文件;
2唐片、計(jì)算刷幀的時(shí)間間隔丙猬;
3、計(jì)算一幀圖像的大蟹丫隆茧球;
4、計(jì)算視頻目標(biāo)尺寸星持,在播放控件中居中顯示視頻抢埋;

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

    // 打開文件
    _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) 變換而來
        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é)下來實(shí)際有下圖三種情況士败,第 1 種情況,我們居中顯示視頻就可以汉匙,第 2拱烁、3生蚁、4 種情況需要視頻寬高比不變的情況下對(duì)視頻進(jìn)行等比例伸縮,需要伸縮到視頻可以在播放器中完整顯示戏自。

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

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

定時(shí)器開啟后每隔一定間隔會(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è)問題庐橙,像素格式轉(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);
}

接下來繼續(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í)變量(比如椆枨疲空間變量份乒,函數(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;
    // 開啟定時(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)或辖,需要從文件開始位置讀取
    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;

    // 打開文件
    _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)聽播放器
    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)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市耳鸯,隨后出現(xiàn)的幾起案子湿蛔,更是在濱河造成了極大的恐慌,老刑警劉巖县爬,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阳啥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡财喳,警方通過查閱死者的電腦和手機(jī)察迟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耳高,“玉大人卷拘,你說我怎么就攤上這事∽8撸” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵污筷,是天一觀的道長(zhǎng)工闺。 經(jīng)常有香客問我,道長(zhǎng)瓣蛀,這世上最難降的妖魔是什么陆蟆? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮惋增,結(jié)果婚禮上叠殷,老公的妹妹穿的比我還像新娘。我一直安慰自己诈皿,他們只是感情好林束,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稽亏,像睡著了一般壶冒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上截歉,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天胖腾,我揣著相機(jī)與錄音,去河邊找鬼。 笑死咸作,一個(gè)胖子當(dāng)著我的面吹牛锨阿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播记罚,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼墅诡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了毫胜?” 一聲冷哼從身側(cè)響起书斜,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酵使,沒想到半個(gè)月后荐吉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡口渔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年样屠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缺脉。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痪欲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出攻礼,到底是詐尸還是另有隱情业踢,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布礁扮,位于F島的核電站知举,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏太伊。R本人自食惡果不足惜雇锡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望僚焦。 院中可真熱鬧锰提,春花似錦、人聲如沸芳悲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽名扛。三九已至赛不,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罢洲,已是汗流浹背踢故。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工文黎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人殿较。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓耸峭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親淋纲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劳闹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容