前面使用 SDL 顯示了一張 YUV 圖片以及 YUV 視頻。接下來(lái)使用 Qt 中的 QImage 來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 YUV 播放器讥耗,查看 QImage 支持的像素格式打厘,你會(huì)發(fā)現(xiàn) QImage 僅支持顯示 RGB 像素格式數(shù)據(jù),并不支持直接顯示 YUV 像素格式數(shù)據(jù)秫逝,但是 YUV 和 RGB 之間是可以相互轉(zhuǎn)換的恕出,我們將 YUV 像素格式數(shù)據(jù)轉(zhuǎn)換成 RGB 像素格式數(shù)據(jù)就可以使用 QImage 顯示了。
YUV 轉(zhuǎn) RGB 常見(jiàn)有三種方式:
1违帆、使用 FFmpeg 提供的庫(kù) libswscale
:
優(yōu)點(diǎn):同一個(gè)函數(shù)實(shí)現(xiàn)了像素格式轉(zhuǎn)換和分辨率縮放以及前后圖像濾波處理浙巫;
缺點(diǎn):速度慢。
2刷后、使用 Google 提供的 libyuv:
優(yōu)點(diǎn):兼容性好功能全面的畴;速度快,僅次于 OpenGL shader尝胆;
缺點(diǎn):暫無(wú)丧裁。
3、使用 OpenGL shader:
優(yōu)點(diǎn):速度快班巩,不增加包體積渣慕;
缺點(diǎn):兼容性一般嘶炭。
下面主要介紹如何使用 FFmpeg 提供的庫(kù) libswscale
進(jìn)行轉(zhuǎn)換,其他轉(zhuǎn)換方式將會(huì)在后面介紹逊桦。
1眨猎、像素格式轉(zhuǎn)換核心函數(shù) sws_scale
sws_scale
函數(shù)主要是用來(lái)做像素格式和分辨率的轉(zhuǎn)換,每次轉(zhuǎn)換一幀數(shù)據(jù):
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
參數(shù)說(shuō)明:
c:轉(zhuǎn)換上下文强经,可以通過(guò)函數(shù) sws_getContext
創(chuàng)建睡陪;
srcSlice[]:輸入緩沖區(qū),元素指向一幀中每個(gè)平面的數(shù)據(jù)匿情,以 yuv420p
為例兰迫,{指向每幀中 Y 平面數(shù)據(jù)的指針,指向每幀中 U 平面數(shù)據(jù)的指針炬称,指向每幀中 V 平面數(shù)據(jù)的指針汁果,null}
;
srcStride[]:每個(gè)平面一行的大小玲躯,以 yuv420p
為例据德,{每幀中 Y 平面一行的長(zhǎng)度,每幀中 U 平面一行的長(zhǎng)度跷车,每幀中 U 平面一行的長(zhǎng)度棘利,0}
;
srcSliceY:輸入圖像上開(kāi)始處理區(qū)域的起始位置朽缴。
srcSliceH:處理多少行善玫。如果 srcSliceY = 0,srcSliceH = height
密强,表示一次性處理完整個(gè)圖像茅郎。這種設(shè)置是為了多線程并行,例如可以創(chuàng)建兩個(gè)線程誓斥,第一個(gè)線程處理 [0, h/2-1]
行只洒,第二個(gè)線程處理 [h/2, h-1]
行,并行處理加快速度劳坑。
dst[]:輸出的圖像數(shù)據(jù),和輸入?yún)?shù) srcSlice[]
類似成畦。
dstStride[]:和輸入?yún)?shù) srcStride[]
類似距芬。
注意:sws_scale 函數(shù)不會(huì)為傳入的輸入數(shù)據(jù)和輸出數(shù)據(jù)創(chuàng)建堆空間。
2循帐、獲取轉(zhuǎn)換上下文函數(shù)
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);
參數(shù)說(shuō)明:
srcW, srcH, srcFormat:輸入圖像寬高和輸入圖像像素格式(我們這里輸入圖像像素格式是 yuv420p)框仔;
dstW, dstH, dstFormat:輸出圖像寬高和輸出圖像像素格式(我們這里輸出圖像像素格式是 rgb24),不僅可以轉(zhuǎn)換像素格式拄养,也可以分辨率縮放离斩;
flag:指定使用何種算法银舱,例如快速線性、差值和矩陣等等跛梗,不同的算法性能也不同寻馏,快速線性算法性能相對(duì)較高。只針對(duì)尺寸的變換核偿。
/* values for the flags, the stuff on the command line is different */
#define SWS_FAST_BILINEAR 1
#define SWS_BILINEAR 2
#define SWS_BICUBIC 4
#define SWS_X 8
#define SWS_POINT 0x10
#define SWS_AREA 0x20
#define SWS_BICUBLIN 0x40
#define SWS_GAUSS 0x80
#define SWS_SINC 0x100
#define SWS_LANCZOS 0x200
#define SWS_SPLINE 0x400
srcFilter, stFilter:這兩個(gè)參數(shù)是做過(guò)濾器用的诚欠,目前暫時(shí)沒(méi)有用到,傳 nullptr 即可漾岳;
param:和 flag 算法相關(guān)轰绵,也可以傳 nullptr;
返回值:成功返回轉(zhuǎn)換格式上下文指針尼荆,失敗返回 NULL左腔;
注意:sws_getContext
函數(shù)注釋中有提示我們最后使用完上下文不要忘記調(diào)用函數(shù) sws_freeContext
釋放,一般函數(shù)名中有 create 或者 alloc 等單詞的函數(shù)需要我們釋放捅儒,為什么調(diào)用 sws_getContext
后也需要釋放呢翔悠?此時(shí)我們可以參考一下源碼:
ffmpeg-4.3.2/libswscale/utils.c
發(fā)現(xiàn)源碼當(dāng)中調(diào)用了 sws_alloc_set_opts
,所以最后是需要釋放上下文的野芒。當(dāng)然我們也可以使用如下方式創(chuàng)建轉(zhuǎn)換上下文蓄愁,最后同樣需要調(diào)用 sws_freeContext
釋放上下文:
ctx = sws_alloc_context();
av_opt_set_int(ctx, "srcw", in.width, 0);
av_opt_set_int(ctx, "srch", in.height, 0);
av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
av_opt_set_int(ctx, "dstw", out.width, 0);
av_opt_set_int(ctx, "dsth", out.height, 0);
av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0);
if (sws_init_context(ctx, nullptr, nullptr) < 0) {
// sws_freeContext(ctx);
goto end;
}
3、創(chuàng)建輸入輸出緩沖區(qū)
首先我們創(chuàng)建需要的局部變量:
// 輸入/輸出緩沖區(qū)狞悲,元素指向每幀中每一個(gè)平面的數(shù)據(jù)
uint8_t *inData[4], *outData[4];
// 每個(gè)平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖像的大小
int inFrameSize, outFrameSize;
// 此處需要注意的是下面寫(xiě)法是錯(cuò)誤的撮抓,*是跟著最右邊的變量名的:
uint8_t *inData[4], outData[4];
// 其等價(jià)于:
uint8_t *inData[4];
uint8_t outData[4];
我們創(chuàng)建好了輸入輸出緩沖區(qū)變量,然后需要為輸入輸出緩沖區(qū)各開(kāi)辟一塊堆空間(sws_scale
函數(shù)不會(huì)為我們開(kāi)辟輸入輸出緩沖區(qū)堆空間摇锋,可查看源碼)丹拯,F(xiàn)Fmpeg 為我們提供了現(xiàn)成的函數(shù) av_image_alloc
:
ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
// 最后不要忘記釋放輸入輸出緩沖區(qū)
av_freep(&inData[0]);
av_freep(&outData[0]);
建議 inData 數(shù)組和 inStrides 數(shù)組的大小是 4,雖然我們目前的輸入像素格式 yuv420p
有 Y 荸恕、U 和 V 共 3 個(gè)平面乖酬,但是有可能會(huì)有 4 個(gè)平面的情況,比如可能會(huì)多 1 個(gè)透明度平面融求。有多少個(gè)平面取決于像素格式咬像。
以 yuv420p
像素格式數(shù)據(jù)舉例:
// 每一幀的 Y 平面數(shù)據(jù)、U 平面數(shù)據(jù)和 V 平面數(shù)據(jù)是緊挨在一起的
// inData[0] -> Y 平面數(shù)據(jù)
// inData[1] -> U 平面數(shù)據(jù)
// inData[2] -> V 平面數(shù)據(jù)
inData[0] = (uint8_t *)malloc(inFrameSize);
inData[1] = inData[0] + 每幀中 Y 平面數(shù)據(jù)長(zhǎng)度;
inData[2] = inData[0] + 每幀中 Y 平面數(shù)據(jù)長(zhǎng)度 + 每幀中 U 平面數(shù)據(jù)長(zhǎng)度;
關(guān)于 inStrides 的理解生宛,inStrides 中存放的是每個(gè)平面每一行的大小县昂,以當(dāng)前輸入數(shù)據(jù)舉例(視頻寬高:640x480 像素格式:yuv420p):
Y 平面:
------ 640列 ------
YY...............YY |
YY...............YY |
YY...............YY
................... 480行
YY...............YY
YY...............YY |
YY...............YY |
U 平面:
--- 320列 ---
UU........UU |
UU........UU
............ 240行
UU........UU
UU........UU |
V 平面:
--- 320列 ---
VV........VV |
VV........VV
............ 240行
VV........VV
VV........VV |
inStrides[0] = Y 平面每一行的大小 = 640
inStrides[1] = U 平面每一行的大小 = 320
inStrides[2] = V 平面每一行的大小 = 320
我們也可以參考前面用到的開(kāi)辟輸入輸出緩沖區(qū)函數(shù) av_image_alloc
,調(diào)用函數(shù)時(shí) 我們把 inStrides 傳給了參數(shù) linesizes陷舅,linesizes 就很好理解了是每一幀平面一行的大小倒彰。
int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
int w, int h, enum AVPixelFormat pix_fmt, int align);
outData 和 outStrides 是同樣的道理。輸出像素格式 rgb24
只有 1 個(gè)平面(yuv444 packed
像素格式也只有一個(gè)平面)莱睁。
示例代碼:
在 .pro 中引入庫(kù):
macx {
INCLUDEPATH += /usr/local/ffmpeg/include
LIBS += -L/usr/local/ffmpeg/lib -lavutil -lswscale
}
ffmpegutils.h:
#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H
extern "C" {
#include <libavutil/avutil.h>
}
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat format;
} RawVideoFile;
typedef struct {
char *pixels;
int width;
int height;
AVPixelFormat format;
} RawVideoFrame;
class FFmpegUtils
{
public:
FFmpegUtils();
// file -> file
static void convretRawVideo(RawVideoFile &in, RawVideoFile &out);
// pixels -> pixels待讳,默認(rèn)傳入一幀數(shù)據(jù)芒澜,輸出一幀數(shù)據(jù)
static void convretRawVideo(RawVideoFrame &in, RawVideoFrame &out);
};
#endif // FFMPEGUTILS_H
ffmpegutils.cpp:
#include "ffmpegutils.h"
#include <QDebug>
#include <QFile>
extern "C" {
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
}
FFmpegUtils::FFmpegUtils()
{
}
// file -> file
void FFmpegUtils::convretRawVideo(RawVideoFile &in, RawVideoFile &out)
{
int ret = 0;
// 轉(zhuǎn)換上下文
SwsContext *ctx = nullptr;
// 輸入/輸出緩沖區(qū),元素指向每幀中每一個(gè)平面的數(shù)據(jù)
uint8_t *inData[4], *outData[4];
// 每個(gè)平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖片的大小
int inFrameSize, outFrameSize;
// 輸入文件
QFile inFile(in.filename);
// 輸出文件
QFile outFile(out.filename);
// 創(chuàng)建輸入緩沖區(qū)
ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc inData error:" << errbuf;
goto end;
}
// 創(chuàng)建輸出緩沖區(qū)
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc outData error:" << errbuf;
goto end;
}
// 創(chuàng)建轉(zhuǎn)換上下文
// 方式一:
ctx = sws_getContext(in.width, in.height, in.format,
out.width, out.height, out.format,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!ctx) {
qDebug() << "sws_getContext error";
goto end;
}
// 方式二:
// ctx = sws_alloc_context();
// av_opt_set_int(ctx, "srcw", in.width, 0);
// av_opt_set_int(ctx, "srch", in.height, 0);
// av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
// av_opt_set_int(ctx, "dstw", out.width, 0);
// av_opt_set_int(ctx, "dsth", out.height, 0);
// av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
// av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0);
// if (sws_init_context(ctx, nullptr, nullptr) < 0) {
// qDebug() << "sws_init_context error";
// goto end;
// }
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "open in file failure";
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "open out file failure";
goto end;
}
// 計(jì)算一幀圖像大小
inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1);
while (inFile.read((char *)inData[0], inFrameSize) == inFrameSize) {
// 每一幀的轉(zhuǎn)換
sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);
// 每一幀寫(xiě)入文件
outFile.write((char *)outData[0], outFrameSize);
}
end:
av_freep(&inData[0]);
av_freep(&outData[0]);
sws_freeContext(ctx);
}
// pixels -> pixels创淡,默認(rèn)傳入一幀數(shù)據(jù)痴晦,輸出一幀數(shù)據(jù)
void FFmpegUtils::convretRawVideo(RawVideoFrame &in, RawVideoFrame &out)
{
int ret = 0;
// 轉(zhuǎn)換上下文
SwsContext *ctx = nullptr;
// 輸入/輸出緩沖區(qū),元素指向每幀中每一個(gè)平面的數(shù)據(jù)
uint8_t *inData[4], *outData[4];
// 每個(gè)平面一行的大小
int inStrides[4], outStrides[4];
// 每一幀圖片的大小
int inFrameSize, outFrameSize;
// 創(chuàng)建輸入緩沖區(qū)
ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc inData error:" << errbuf;
goto end;
}
// 創(chuàng)建輸出緩沖區(qū)
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc outData error:" << errbuf;
goto end;
}
// 創(chuàng)建轉(zhuǎn)換上下文
ctx = sws_getContext(in.width, in.height, in.format,
out.width, out.height, out.format,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!ctx) {
qDebug() << "sws_getContext error";
goto end;
}
// 計(jì)算一幀圖像大小
inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1);
// 拷貝輸入像素?cái)?shù)據(jù)到 inData[0]
memcpy(inData[0], in.pixels, inFrameSize);
// 每一幀的轉(zhuǎn)換
sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);
// 拷貝像素?cái)?shù)據(jù)到 outData[0]
out.pixels = (char *)malloc(outFrameSize);
memcpy(out.pixels, outData[0], outFrameSize);
end:
av_freep(&inData[0]);
av_freep(&outData[0]);
sws_freeContext(ctx);
}