前面通過 H.264 編碼將 YUV 像素?cái)?shù)據(jù)壓縮生成了一個(gè) h264 文件快耿。那么想要播放 h264 文件泪喊,就需要解壓縮取出每一幀的具體像素?cái)?shù)據(jù)進(jìn)行播放。本文的內(nèi)容主要是解碼裸流洲敢,即從本地讀取 h264 文件蛀恩,解碼成 YUV 像素?cái)?shù)據(jù)的過程。
一镜硕、使用 FFmpeg 命令行進(jìn)行 H.264 解碼:
$ ffmpeg -c:v h264 -i in.h264 out.yuv
解碼時(shí) -c:v h264
是輸入?yún)?shù)运翼。查看本地的解碼器:
$ ffmpeg -decoders | grep 264
VFS..D h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
二、使用 FFmpeg 編程實(shí)現(xiàn) H.264 編碼
首先需要導(dǎo)入用到的 FFmpeg 庫 libavcodec
和 libavutil
兴枯。和前面 H.264 編碼用到的庫是一樣的血淌,并且 H.264 解碼流程和 AAC 解碼流程也是類似的。
1财剖、獲取解碼器
在我本地默認(rèn)的解碼器就是 h264
悠夯,通過 ID 或者名稱獲取到的 H.264 解碼器都是 h264
。
// 使用 ID 獲取編碼器:
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
// 或者使用名稱獲取編碼器:
codec = avcodec_find_decoder_by_name("h264");
2躺坟、初始化解析器上下文
通過 ID 創(chuàng)建 H.264 解析器上下文:
parserCtx = av_parser_init(codec->id);
查看函數(shù) av_parser_init
源碼:
// 源碼位置:ffmpeg-4.3.2/libavcodec/parser.c
AVCodecParserContext *av_parser_init(int codec_id)
{
AVCodecParserContext *s = NULL;
const AVCodecParser *parser;
void *i = 0;
int ret;
if (codec_id == AV_CODEC_ID_NONE)
return NULL;
while ((parser = av_parser_iterate(&i))) {
if (parser->codec_ids[0] == codec_id ||
parser->codec_ids[1] == codec_id ||
parser->codec_ids[2] == codec_id ||
parser->codec_ids[3] == codec_id ||
parser->codec_ids[4] == codec_id)
goto found;
}
return NULL;
found:
s = av_mallocz(sizeof(AVCodecParserContext));
if (!s)
goto err_out;
s->parser = (AVCodecParser*)parser;
s->priv_data = av_mallocz(parser->priv_data_size);
if (!s->priv_data)
goto err_out;
s->fetch_timestamp=1;
s->pict_type = AV_PICTURE_TYPE_I;
if (parser->parser_init) {
ret = parser->parser_init(s);
if (ret != 0)
goto err_out;
}
s->key_frame = -1;
#if FF_API_CONVERGENCE_DURATION
FF_DISABLE_DEPRECATION_WARNINGS
s->convergence_duration = 0;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
s->dts_sync_point = INT_MIN;
s->dts_ref_dts_delta = INT_MIN;
s->pts_dts_delta = INT_MIN;
s->format = -1;
return s;
err_out:
if (s)
av_freep(&s->priv_data);
av_free(s);
return NULL;
}
// 源碼片段 ffmpeg-4.3.2/libavcodec/parsers.c
const AVCodecParser *av_parser_iterate(void **opaque)
{
uintptr_t i = (uintptr_t)*opaque;
const AVCodecParser *p = parser_list[i];
if (p)
*opaque = (void*)(i + 1);
return p;
}
// 源碼片段 ffmpeg-4.3.2/libavcodec/parsers.c
AVCodecParser ff_h264_parser = {
.codec_ids = { AV_CODEC_ID_H264 },
.priv_data_size = sizeof(H264ParseContext),
.parser_init = init,
.parser_parse = h264_parse,
.parser_close = h264_close,
.split = h264_split,
};
源碼中的第一步就是通過 ID 查找 parser沦补,此處傳入的 codec->id
就是 AV_CODEC_ID_H264
。函數(shù) av_parser_iterate
是 parser 迭代器咪橙,其內(nèi)部是在 parser_list
數(shù)組中查找 parser(parser_list
在源碼文件 ffmpeg-4.3.2/libavcodec/parser_list.c 中)夕膀。最終找到的 H.264 解析器是 ff_h264_parser
。
3匣摘、創(chuàng)建解析器上下文
ctx = avcodec_alloc_context3(codec);
4店诗、創(chuàng)建AVPacket
pkt = av_packet_alloc();
5、創(chuàng)建AVFrame
frame = av_frame_alloc();
6音榜、打開解碼器
ret = avcodec_open2(ctx, codec, nullptr);
7、打開文件
inFile.open(QFile::ReadOnly)
outFile.open(QFile::WriteOnly)
inLen = inFile.read(inDataArray, IN_DATA_SIZE);
8捧弃、讀取文件數(shù)據(jù) & 解析數(shù)據(jù)
while ((inLen = inFile.read(inDataArray, IN_INBUF_SIZE)) > 0) {
inData = inDataArray;
while (inLen > 0) {
// 解析器解析數(shù)據(jù)
ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (uint8_t *) inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "av_parser_parse2 error" << errbuf;
goto end;
}
// 跳過已經(jīng)解析過的數(shù)據(jù)
inData += ret;
// 減去已經(jīng)解析過的數(shù)據(jù)大小
inLen -= ret;
qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;
// 解碼
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
}
}
通過和在終端使用命令行解碼生成的 YUV 文件大小進(jìn)行比較赠叼,發(fā)現(xiàn)通過代碼解碼生成的 YUV 像素?cái)?shù)據(jù)有丟失:
$ ls -al
-rw-r--r-- 1 mac staff 110131200 Apr 12 14:22 out_640x480_yuv420p_code.yuv
-rw-r--r-- 1 mac staff 110592000 Apr 12 14:19 out_640x480_yuv420p_terminal.yuv
通過打印可以發(fā)現(xiàn)解碼結(jié)束后 parser 中還剩余 703
字節(jié)的數(shù)據(jù)沒有送入 AVPacket
中,需要讓 paeser把剩余數(shù)據(jù)“吐出來”:
pkt->size: 473 ret: 473
解碼完成第 237 幀
pkt->size: 0 ret: 703
解碼完成第 238 幀
解碼完成第 239 幀
解決辦法就是當(dāng) h264 文件中數(shù)據(jù)全部讀完后再調(diào)用一次 av_parser_parse2
函數(shù)违霞,將代碼改造如下:
// 是否讀到文件尾部
int inEnd = 0;
do {
// 從文件中讀取h264數(shù)據(jù)
inLen = inFile.read(inDataArray, IN_INBUF_SIZE);
inData = inDataArray;
inEnd = !inLen;
while (inLen > 0 || inEnd) { // 到了文件尾部雖然沒有讀取到任何數(shù)據(jù)嘴办,也要調(diào)用,最后要刷出解析器上下文中的數(shù)據(jù)
ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_parser_parse2 error:" << errbuf;
goto end;
}
// 跳過解析過的數(shù)據(jù)
inData += ret;
// 減去已解析過的數(shù)據(jù)大小
inLen -= ret;
qDebug() << "inEnd:" << inEnd << "pkt->size:" << pkt->size << "ret:" << ret;
// 解碼
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
// 當(dāng)inEnd = 1時(shí)到了文件尾部
if (inEnd) break;
}
} while (!inEnd);
查看打印發(fā)現(xiàn) parser 中剩余數(shù)據(jù)已全部刷出买鸽,并且這次和在終端生成的 yuv 文件大小完全一樣:
inEnd: 0 pkt->size: 473 ret: 473
解碼完成第 237 幀
inEnd: 0 pkt->size: 0 ret: 703
inEnd: 1 pkt->size: 703 ret: 0
解碼完成第 238 幀
解碼完成第 239 幀
解碼完成第 240 幀
9涧郊、解碼
static int decode(AVCodecContext *ctx,
AVPacket *pkt,
AVFrame *frame,
QFile &outFile) {
// 發(fā)送壓縮數(shù)據(jù)到解碼器
int ret = avcodec_send_packet(ctx, pkt);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "avcodec_send_packet error" << errbuf;
return ret;
}
while (true) {
// 獲取解碼后的數(shù)據(jù)
ret = avcodec_receive_frame(ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "avcodec_receive_frame error" << errbuf;
return ret;
}
// 將解碼后的數(shù)據(jù)寫入文件
int imgSize = av_image_get_buffer_size(ctx->pix_fmt, ctx->width, ctx->height, 1);
outFile.write((char *) frame->data[0], imgSize);
}
}
使用以上方式直接從 frame->data[0]
中讀取一幀大小寫入文件,你可能會(huì)發(fā)現(xiàn)播放解碼后的 YUV 像素?cái)?shù)據(jù)會(huì)有如下問題:
是因?yàn)?
frame->data[0]
和 frame->data[1]
以及 frame->data[1]
和 frame->data[2]
之間是有 padding 的:
// 打印 frame->data:
qDebug() << frame->data[0] << frame->data[1] << frame->data[2];
// 輸出:
0x7fd554693000 0x7fd5546df000 0x7fd5546f2000
// 計(jì)算數(shù)據(jù)緩沖區(qū)各平面實(shí)際大醒畚濉:
frame->data[1] - frame->data[0] = 0x7fd5546df000 - 0x7fd554693000 = 311296 字節(jié) = 實(shí)際 Y 平面大小
frame->data[2] - frame->data[1] = 0x7fd5546f2000 - 0x7fd5546df000 = 77824 字節(jié) = 實(shí)際 U 平面大小
// 各平面的期望大凶彼摇:
Y 平面大小 = 640 * 480 * 1 = 307200 字節(jié)
U 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
V 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
可以使用下面方式將 YUV 像素?cái)?shù)據(jù)寫入文件彤灶,yuv420p 像素格式色度分量 U 和 V 是 1/2 垂直采樣,所以高度要除以 2:
outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);
10批旺、釋放資源
inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);
參考鏈接:https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_609A2E9F73AB634ED670392DD89A63400008@qq.com/
完整示例代碼:
h264_decode.pro:
macx {
INCLUDEPATH += /usr/local/ffmpeg/include
LIBS += -L/usr/local/ffmpeg/lib \
-lavcodec \
-lavutil
}
ffmpegutils.h:
#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H
extern "C" {
#include <libavformat/avformat.h>
}
// 解碼后的YUV參數(shù)
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat pixFmt;
int fps;
} VideoDecodeSpec;
class FFmpegUtils
{
public:
FFmpegUtils();
static void h264Decode(const char *inFilename, VideoDecodeSpec &out);
};
#endif // FFMPEGUTILS_H
ffmpegutils.cpp:
#include "ffmpegutils.h"
#include <QDebug>
#include <QFile>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
#define ERRBUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf))
// 輸入緩沖區(qū)大小 官方示例程序建議大小
#define IN_INBUF_SIZE 4096
FFmpegUtils::FFmpegUtils()
{
}
static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
int ret = 0;
// 發(fā)送數(shù)據(jù)到解碼器
ret = avcodec_send_packet(ctx, pkt);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "avcodec_send_packet error:" << errbuf;
return ret;
}
while (true) {
// 獲取解碼后的數(shù)據(jù)
ret = avcodec_receive_frame(ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
ERRBUF(ret);
qDebug() << "avcodec_receive_frame error:" << errbuf;
return ret;
}
// 將解碼后的數(shù)據(jù)寫入文件
// 寫入Y平面數(shù)據(jù)
outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
// 寫入U(xiǎn)平面數(shù)據(jù)
outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
// 寫入V平面數(shù)據(jù)
outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);
}
}
void FFmpegUtils::h264Decode(const char *inFilename, VideoDecodeSpec &out)
{
// 返回值
int ret = 0;
// 輸入文件(h264文件)
QFile inFile(inFilename);
// 輸出文件(yuv文件)
QFile outFile(out.filename);
// 解碼器
AVCodec *codec = nullptr;
// 解碼上下文
AVCodecContext *ctx = nullptr;
// 解析器上下文
AVCodecParserContext *parserCtx = nullptr;
// 存放解碼前的h264數(shù)據(jù)
AVPacket *pkt = nullptr;
// 存放解碼后的yuv數(shù)據(jù)
AVFrame *frame = nullptr;
// 存放讀取的h264文件數(shù)據(jù)
// 加上AV_INPUT_BUFFER_PADDING_SIZE是為了防止某些優(yōu)化過的reader一次性讀取過多導(dǎo)致越界(參考了FFmpeg示例代碼)
char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = nullptr;
// 輸入數(shù)據(jù)緩沖區(qū)中剩余的待解碼的數(shù)據(jù)長度
int inLen = 0;
// 是否讀取到了輸入文件尾部
int inEnd = 0;
// 獲取H264解碼器幌陕,也可以根據(jù)解碼器名稱獲取
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
qDebug() << "decoder h264 not found";
return;
}
// 初始化解析器上下文
parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
qDebug() << "av_parser_init error";
return;
}
// 創(chuàng)建解碼上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
qDebug() << "avcodec_alloc_context3 error";
goto end;
}
// 創(chuàng)建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
qDebug() << "av_packet_alloc error";
goto end;
}
// 創(chuàng)建AVFrame
frame = av_frame_alloc();
if (!frame) {
qDebug() << "av_frame_alloc error";
goto end;
}
// 打開解碼器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "open decoder error:" << errbuf;
goto end;
}
// 打開h264文件
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "open file failure:" << inFilename;
goto end;
}
// 打開yuv文件
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "open file failure:" << out.filename;
}
do {
// 從文件中讀取h264數(shù)據(jù)
inLen = inFile.read(inDataArray, AUDIO_INBUF_SIZE);
// inData指向inDataArray首元素
inData = inDataArray;
// 設(shè)置是否到了文件尾部
inEnd = !inLen;
while (inLen > 0 || inEnd) { // 到了文件尾部,雖然沒有讀取任何數(shù)據(jù)汽煮,但也要調(diào)用av_parser_parse2(修復(fù)bug)
ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "av_parser_parse2 error:" << errbuf;
goto end;
}
// 跳過解析過的數(shù)據(jù)
inData += ret;
// 減去已解析過的數(shù)據(jù)大小
inLen -= ret;
// 解碼
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
// 當(dāng)inEnd = 1時(shí)到了文件尾部
if (inEnd) break;
}
} while (!inEnd);
// 刷出緩沖區(qū)中剩余數(shù)據(jù)
// 方式一:
decode(ctx, nullptr, frame, outFile);
// 方式二:
// pkt->data = nullptr;
// pkt->size = 0;
// decode(ctx, pkt, frame, outFile);
// 輸出參數(shù)
out.width = ctx->width;
out.height = ctx->height;
out.pixFmt = ctx->pix_fmt;
// 用framerate.num獲取幀率搏熄,并不是time_base.den
out.fps = ctx->framerate.num;
end:
inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);
}
方法調(diào)用:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include <ffmpegutils.h>
extern "C" {
#include <libavutil/imgutils.h>
}
#define IN_FILE "/Users/mac/Downloads/pic/in_640x480_yuv420p.h264"
#define OUT_FILE "/Users/mac/Downloads/pic/out_640x480_yuv420p_code.yuv"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_decodeH264Button_clicked()
{
VideoDecodeSpec spec;
spec.filename = OUT_FILE;
FFmpegUtils::h264Decode(IN_FILE, spec);
qDebug() << "寬度:" << spec.width;
qDebug() << “高度:" << spec.height;
qDebug() << “像素格式:" << av_get_pix_fmt_name(spec.pixFmt);
qDebug() << “幀率:" << spec.fps;
}