一、簡介
上節(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->data的data[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

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();
......
}