前面通过 H.264 编码将 YUV 像素数据压缩生成了一个 h264 文件。那么想要播放 h264 文件,就需要解压缩取出每一帧的具体像素数据进行播放。本文的内容主要是解码裸流,即从本地读取 h264 文件,解码成 YUV 像素数据的过程。
一、使用 FFmpeg 命令行进行 H.264 解码:
$ ffmpeg -c:v h264 -i in.h264 out.yuv
解码时 -c:v h264
是输入参数。查看本地的解码器:
$ ffmpeg -decoders | grep 264
VFS..D h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
二、使用 FFmpeg 编程实现 H.264 编码
首先需要导入用到的 FFmpeg 库 libavcodec
和 libavutil
。和前面 H.264 编码用到的库是一样的,并且 H.264 解码流程和 AAC 解码流程也是类似的。
1、获取解码器
在我本地默认的解码器就是 h264
,通过 ID 或者名称获取到的 H.264 解码器都是 h264
。
// 使用 ID 获取编码器:
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
// 或者使用名称获取编码器:
codec = avcodec_find_decoder_by_name("h264");
2、初始化解析器上下文
通过 ID 创建 H.264 解析器上下文:
parserCtx = av_parser_init(codec->id);
查看函数 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
。函数 av_parser_iterate
是 parser 迭代器,其内部是在 parser_list
数组中查找 parser(parser_list
在源码文件 ffmpeg-4.3.2/libavcodec/parser_list.c 中)。最终找到的 H.264 解析器是 ff_h264_parser
。
3、创建解析器上下文
ctx = avcodec_alloc_context3(codec);
4、创建AVPacket
pkt = av_packet_alloc();
5、创建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、读取文件数据 & 解析数据
while ((inLen = inFile.read(inDataArray, IN_INBUF_SIZE)) > 0) {
inData = inDataArray;
while (inLen > 0) {
// 解析器解析数据
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;
}
// 跳过已经解析过的数据
inData += ret;
// 减去已经解析过的数据大小
inLen -= ret;
qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;
// 解码
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
}
}
通过和在终端使用命令行解码生成的 YUV 文件大小进行比较,发现通过代码解码生成的 YUV 像素数据有丢失:
$ 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
通过打印可以发现解码结束后 parser 中还剩余 703
字节的数据没有送入 AVPacket
中,需要让 paeser把剩余数据“吐出来”:
pkt->size: 473 ret: 473
解码完成第 237 帧
pkt->size: 0 ret: 703
解码完成第 238 帧
解码完成第 239 帧
解决办法就是当 h264 文件中数据全部读完后再调用一次 av_parser_parse2
函数,将代码改造如下:
// 是否读到文件尾部
int inEnd = 0;
do {
// 从文件中读取h264数据
inLen = inFile.read(inDataArray, IN_INBUF_SIZE);
inData = inDataArray;
inEnd = !inLen;
while (inLen > 0 || inEnd) { // 到了文件尾部虽然没有读取到任何数据,也要调用,最后要刷出解析器上下文中的数据
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;
}
// 跳过解析过的数据
inData += ret;
// 减去已解析过的数据大小
inLen -= ret;
qDebug() << "inEnd:" << inEnd << "pkt->size:" << pkt->size << "ret:" << ret;
// 解码
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
// 当inEnd = 1时到了文件尾部
if (inEnd) break;
}
} while (!inEnd);
查看打印发现 parser 中剩余数据已全部刷出,并且这次和在终端生成的 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) {
// 发送压缩数据到解码器
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) {
// 获取解码后的数据
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;
}
// 将解码后的数据写入文件
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]
中读取一帧大小写入文件,你可能会发现播放解码后的 YUV 像素数据会有如下问题:
是因为
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
// 计算数据缓冲区各平面实际大小:
frame->data[1] - frame->data[0] = 0x7fd5546df000 - 0x7fd554693000 = 311296 字节 = 实际 Y 平面大小
frame->data[2] - frame->data[1] = 0x7fd5546f2000 - 0x7fd5546df000 = 77824 字节 = 实际 U 平面大小
// 各平面的期望大小:
Y 平面大小 = 640 * 480 * 1 = 307200 字节
U 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字节
V 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字节
可以使用下面方式将 YUV 像素数据写入文件,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/[email protected]/
完整示例代码:
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
}
// 解码后的YUV参数
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
#include
extern "C" {
#include
#include
#include
}
#define ERRBUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf))
// 输入缓冲区大小 官方示例程序建议大小
#define IN_INBUF_SIZE 4096
FFmpegUtils::FFmpegUtils()
{
}
static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
int ret = 0;
// 发送数据到解码器
ret = avcodec_send_packet(ctx, pkt);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "avcodec_send_packet error:" << errbuf;
return ret;
}
while (true) {
// 获取解码后的数据
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;
}
// 将解码后的数据写入文件
// 写入Y平面数据
outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
// 写入U平面数据
outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
// 写入V平面数据
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数据
AVPacket *pkt = nullptr;
// 存放解码后的yuv数据
AVFrame *frame = nullptr;
// 存放读取的h264文件数据
// 加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界(参考了FFmpeg示例代码)
char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = nullptr;
// 输入数据缓冲区中剩余的待解码的数据长度
int inLen = 0;
// 是否读取到了输入文件尾部
int inEnd = 0;
// 获取H264解码器,也可以根据解码器名称获取
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;
}
// 创建解码上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
qDebug() << "avcodec_alloc_context3 error";
goto end;
}
// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
qDebug() << "av_packet_alloc error";
goto end;
}
// 创建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数据
inLen = inFile.read(inDataArray, AUDIO_INBUF_SIZE);
// inData指向inDataArray首元素
inData = inDataArray;
// 设置是否到了文件尾部
inEnd = !inLen;
while (inLen > 0 || inEnd) { // 到了文件尾部,虽然没有读取任何数据,但也要调用av_parser_parse2(修复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;
}
// 跳过解析过的数据
inData += ret;
// 减去已解析过的数据大小
inLen -= ret;
// 解码
if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
goto end;
}
// 当inEnd = 1时到了文件尾部
if (inEnd) break;
}
} while (!inEnd);
// 刷出缓冲区中剩余数据
// 方式一:
decode(ctx, nullptr, frame, outFile);
// 方式二:
// pkt->data = nullptr;
// pkt->size = 0;
// decode(ctx, pkt, frame, outFile);
// 输出参数
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);
}
方法调用:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
#include
extern "C" {
#include
}
#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;
}