33_音視頻播放器_畫面顯示

一、簡介

上節(jié)介紹了使用SDL播放音頻,這節(jié)介紹視頻顯示,其解碼流程跟音頻差不多。

解碼視頻是比較耗時的,需要我們自己開個線程去解碼,而音頻是SDL幫我們管理了子線程去解碼音頻,初始化音頻SDL后就開始進行播放(SDL_PauseAudio(0);)了,一播放就會調用回調函數(shù)(sdlAudioCallback),然后在去調用音頻解碼(decodeAudio),這個decodeAudio是被動調用,而且每次調用只解碼一個。但是視頻解碼是主動去調用,然后解碼所有東西,所有需要while循環(huán)。

二、視頻解碼

2.1 解碼視頻

void VideoPlayer::decodeVideo(){
    while (true) {
        _vMutex->lock();
        if(_vPktList->empty()){
            _vMutex->unlock();
            continue;
        }

        // 取出頭部的視頻包
        AVPacket pkt = _vPktList->front();
        _vPktList->pop_front();
        _vMutex->unlock();

        // 發(fā)送壓縮數(shù)據(jù)到解碼器
        int ret = avcodec_send_packet(_vDecodeCtx, &pkt);
        // 釋放pkt
        av_packet_unref(&pkt);
        CONTINUE(avcodec_send_packet);

        while (true) {
            ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else BREAK(avcodec_receive_frame);

            // 像素格式的轉換
            sws_scale(_vSwsCtx,
                      _vSwsInFrame->data, _vSwsInFrame->linesize,
                      0, _vDecodeCtx->height,
                      _vSwsOutFrame->data, _vSwsOutFrame->linesize);
            qDebug()<< _vSwsOutFrame->data[0];
        }
    }
}

2.2 調用解碼視頻方法

開啟子線程調用視頻解碼方法:

// 初始化視頻信息
int VideoPlayer::initVideoInfo() {
    int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);
    RET(initDecoder);

    // 初始化像素格式轉換
    ret = initSws();
    RET(initSws);

    // 開啟新的線程去解碼視頻數(shù)據(jù)
    std::thread([this](){
        decodeVideo();
    }).detach();
    return 0;
}

三、像素格式轉換

上面解碼出_vSwsInFrame后是YUV數(shù)據(jù),而顯示是需要RGB的,所以這里需要進行像素格式轉換。

3.1 初始化

初始化像素格式轉換:

int VideoPlayer::initSws(){
    int inW = _vDecodeCtx->width;
    int inH = _vDecodeCtx->height;

    // 輸出frame的參數(shù)
    _vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保證是16的倍數(shù)
    _vSwsOutSpec.height = inH >> 4 << 4;
    _vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;
    _vSwsOutSpec.size = av_image_get_buffer_size(
                            _vSwsOutSpec.pixFmt,
                            _vSwsOutSpec.width,
                            _vSwsOutSpec.height, 1);

    // 初始化像素格式轉換的上下文
    _vSwsCtx = sws_getContext(inW,
                              inH,
                              _vDecodeCtx->pix_fmt,

                              _vSwsOutSpec.width,
                              _vSwsOutSpec.height,
                              _vSwsOutSpec.pixFmt,

                              SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!_vSwsCtx) {
        qDebug() << "sws_getContext error";
        return -1;
    }

    // 初始化像素格式轉換的輸入frame
    _vSwsInFrame = av_frame_alloc();
    if (!_vSwsInFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    // 初始化像素格式轉換的輸出frame
    _vSwsOutFrame = av_frame_alloc();
    if (!_vSwsOutFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    // _vSwsOutFrame的data[0]指向的內存空間
//    int ret = av_image_alloc(_vSwsOutFrame->data,
//                             _vSwsOutFrame->linesize,
//                             _vSwsOutSpec.width,
//                             _vSwsOutSpec.height,
//                             _vSwsOutSpec.pixFmt,
//                             1);
//    RET(av_image_alloc);

    return 0;
}

在像素轉換的時候是有要求的,最好是16的倍數(shù),否則其他分辨率的不能播放,所以我們需要在輸出參數(shù)中控制寬高的大小

3.2 像素格式轉換

然后在視頻解碼方法中進行像素格式的轉換

// 像素格式的轉換
sws_scale(_vSwsCtx,
          _vSwsInFrame->data, _vSwsInFrame->linesize,
          0, _vDecodeCtx->height,
          _vSwsOutFrame->data, _vSwsOutFrame->linesize);
qDebug()<< _vSwsOutFrame->data[0];          

這里的_vSwsOutFrame->datadata[0]就是像素格式轉換后的RGB數(shù)據(jù)。現(xiàn)在通過運行打印data[0],可以發(fā)現(xiàn)它是空的。

這里跟音頻的重采樣里的data[0]道理是一樣,需要我們手動的給_vSwsOutFrame->data[0]創(chuàng)建一塊內存區(qū)域,這塊內存區(qū)域需要多大呢,因為視頻解碼avcodec_receive_frame解碼出來的就是一幀大小,所以這個_vSwsOutFrame->data[0]指向的內存空間只需要一幀大小就可以了。

// _vSwsOutFrame的data[0]指向的內存空間
int ret = av_image_alloc(_vSwsOutFrame->data,
                         _vSwsOutFrame->linesize,
                         _vSwsOutSpec.width,
                         _vSwsOutSpec.height,
                         _vSwsOutSpec.pixFmt,
                         1);
RET(av_image_alloc);

我們在調用avcodec_receive_frame_vSwsInFrame_vSwsInFrame.data[0]在其內部已經給創(chuàng)建好內存區(qū)域了,而且每次調用此方法其內部都會先自動銷毀_vSwsInFrame.data[0],然后在給其分配空間,所以不需要我們手動創(chuàng)建和銷毀data[0]
如果avcodec_receive_frame是最有一次調用,不會再有下一次調用了,那么最后一次內部分配的data[0]的空間是不是一直沒有釋放呢?其實是不會的,因為我們這個程序最后一次調用_vSwsInFrame是有值的,而且它的ret還是返回的是成功狀態(tài),那么它就會繼續(xù)執(zhí)行while循環(huán),當再次執(zhí)行時,因為已經沒有數(shù)據(jù)量,ret返回的狀態(tài)就會滿足if條件if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF),最后就會退出while循環(huán)。

四、顯示畫面

上面完成像素格式的轉換后,就需要通過發(fā)送信號出去給到VideoWidget進行顯示。

4.1 定義信號

videoplayer.h中定義信號

void frameDecoded(VideoPlayer *player,
                      uint8_t *data,
                      VideoSwsSpec &spec);

videoplayer_video.cpp的視頻解碼方法里最后進行發(fā)送信號

4.2 發(fā)送信號

// 發(fā)出信號
emit frameDecoded(this,
                  _vSwsOutFrame->data[0],
                  _vSwsOutSpec);

這里是不能直接發(fā)送_vSwsOutSpec類型的數(shù)據(jù)的,需要我們注冊此類型的數(shù)據(jù)。

// mainwindow.cpp的構造方法里

// 注冊信號的參數(shù)類型,保證能夠發(fā)出信號
qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");

4.3 定義槽函數(shù)

videowidget.h中定義槽函數(shù):

public slots:
    void onPlayerFrameDecoded(VideoPlayer *player,
                                  uint8_t *data,
                                  VideoPlayer::VideoSwsSpec &spec);

4.4 注冊監(jiān)聽信號

mainwindow.cpp中注冊監(jiān)聽信號:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow) {
    ......
    connect(_player, &VideoPlayer::frameDecoded,
            ui->videoWidget, &VideoWidget::onPlayerFrameDecoded);
    ......
}

4.5 畫面顯示

實現(xiàn)videowidget.cpp里的方法:

#include "videowidget.h"
#include <QDebug>
#include <QPainter>

VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
    // 設置背景色
    setAttribute(Qt::WA_StyledBackground);
    setStyleSheet("background: black");
}

VideoWidget::~VideoWidget() {
    if (_image) {
        delete _image;
        _image = nullptr;
    }
}

void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player,
                                       uint8_t *data,
                                       VideoPlayer::VideoSwsSpec &spec) {
    // 釋放之前的圖片
    if (_image) {
        delete _image;
        _image = nullptr;
    }

    // 創(chuàng)建新的圖片
    if (data != nullptr) {
        _image = new QImage((uchar *) data,
                            spec.width, spec.height,
                            QImage::Format_RGB888);

        // 計算最終的尺寸
        // 組件的尺寸
        int w = width();
        int h = height();

        // 計算rect
        int dx = 0;
        int dy = 0;
        int dw = spec.width;
        int dh = spec.height;

        // 計算目標尺寸
        if (dw > w || dh > 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;

        _rect = QRect(dx, dy, dw, dh);
    }

    update();//觸發(fā)paintEvent方法
}

void VideoWidget::paintEvent(QPaintEvent *event) {
    if (!_image) return;

    // 將圖片繪制到當前組件上
    QPainter(this).drawImage(_rect, *_image);
}

這里的計算最終的尺寸我們可以參考之前介紹的《24_用Qt和FFmpeg實現(xiàn)簡單的YUV播放器

此時我們運行播放的時候,可以發(fā)現(xiàn)視頻畫面非常的快速的播放完了,此時我們先使用SDL_Delay(33);控制播放速度,后面在進行音視頻同步的時候在處理。

五、釋放資源

void VideoPlayer::freeVideo(){
    clearVideoPktList();
    avcodec_free_context(&_vDecodeCtx);
    av_frame_free(&_vSwsInFrame);
    if (_vSwsOutFrame) {
        av_freep(&_vSwsOutFrame->data[0]);
        av_frame_free(&_vSwsOutFrame);
    }
    sws_freeContext(_vSwsCtx);
    _vSwsCtx = nullptr;
    _vStream = nullptr;
}

我們實現(xiàn)釋放資源后,再去運行后點擊停止會出現(xiàn)內存錯誤。
這是因為我們在像素格式轉換后,將_vSwsOutFrame->data[0]數(shù)據(jù)直接發(fā)送給onPlayerFrameDecoded方法的uint8_t *data,這里就會牽扯到多線程同時訪問一塊內存區(qū)域(橡樹轉換sws_scale是在子線程,渲染時在主線程)

解決辦法就是把_vSwsOutFrame->data[0]指向的RGB數(shù)據(jù)拷貝到另外一個內存空間

// 像素格式的轉換
sws_scale(_vSwsCtx,
          _vSwsInFrame->data, _vSwsInFrame->linesize,
          0, _vDecodeCtx->height,
          _vSwsOutFrame->data, _vSwsOutFrame->linesize);

uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size);
 memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size);
// 發(fā)出信號
emit frameDecoded(this,data,_vSwsOutSpec);
void VideoWidget::freeImage() {
    if (_image) {
        av_free(_image->bits());
        delete _image;
        _image = nullptr;
    }
}

六、細節(jié)處理

6.1 非音頻、視頻流補充av_packet_unref

非音頻、視頻流補充`av_packet_unref`

6.2 很快讀完了所有數(shù)據(jù)包,數(shù)據(jù)包過多

我們通過while循環(huán)只要不是停止狀態(tài)就拼命的讀av_read_frame,讀到了就往里面塞數(shù)據(jù)包(addAudioPkt(pkt)addVideoPkt(pkt))。實際上就會發(fā)現(xiàn)這個段代碼不管音視頻多大很快就會讀完,這樣如果音視頻非常大,一下載入到內存中就會有問題,所以需要做一下限制

#define AUDIO_MAX_PKT_SIZE 1000
#define VIDEO_MAX_PKT_SIZE 500

// 從輸入文件中讀取數(shù)據(jù)
AVPacket pkt;
while (_state != Stopped) {
   if (_vPktList->size() >= VIDEO_MAX_PKT_SIZE ||
           _aPktList->size() >= AUDIO_MAX_PKT_SIZE) {
        SDL_Delay(10);
       continue;
   }

   qDebug()<< _vPktList->size()<< _aPktList->size();

   ......
}

源碼鏈接

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容