本文的主要内容:对H.264数据进行解码(解压缩)。

使用FFmpeg命令进行H.264解码

如果是命令行的操作,非常简单。

ffmpeg -c:v h264 -i in.h264 out_cmd.yuv
# -c:v h264是指定使用h264作为解码器

使用FFmpeg代码进行H.264解码

接下来主要讲解如何通过代码的方式解码H.264数据,用到了avcodecavutil两个库,整体过程跟《AAC解码实战》类似。

1、获取解码器

通过ID或者名称获取到的H.264解码器都是h264。

// 使用 ID 获取编码器:
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
// 或者使用名称获取编码器:
codec = avcodec_find_decoder_by_name("h264");
if (!codec) {
qDebug() << "decoder libfdk_aac not found";
return;
}

2、初始化解析器上下文

通过ID创建H.264解析器上下文:

parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
qDebug() << "av_parser_init error";
return;
}

查看函数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/h264_parser.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_iterateparser迭代器,其内部是在parser_list数组中查找parserparser_list在源码文件ffmpeg-4.3.2/libavcodec/h264_parser.c中)。最终找到的H.264解析器是ff_h264_parser

3、创建解析器上下文

ctx = avcodec_alloc_context3(codec);
if (!ctx) {
qDebug() << "avcodec_alloc_context3 error";
goto end;
}

4、创建AVPacket

pkt = av_packet_alloc();
if (!pkt) {
qDebug() << "av_packet_alloc error";
goto end;
}

5、创建AVFrame

frame = av_frame_alloc();
if (!frame) {
qDebug() << "av_frame_alloc error";
goto end;
}

6、打开解码器

ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "avcodec_open2 error" << errbuf;
goto end;
}

7、打开文件

if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "file open error:" << inFilename;
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "file open error:" << out.filename;
goto end;
}

8、读取文件数据 & 解析数据

// 读取数据
while ((inLen = inFile.read(inDataArray,IN_DATA_SIZE)) >0) {
// 让inData指向数组的首元素
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) {
ERROR_BUF(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;
}
}
}

关于av_parser_parse2函数可以参考 ffmpeg的av_parser_parse2( )

9、解码

static int decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile){
// 发送压缩数据到解码器
int ret = avcodec_send_packet(ctx,pkt);
if (ret < 0) {
ERROR_BUF(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) {
ERROR_BUF(ret);
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);
}
}

这里我们根据yuv420p的平面格式YUV紧挨着的规则来写数据,从frame->data[0]开始写,写一帧的大小imgSize

10、运行代码

然后运行代码生成YUV文件后,可以发现和通过FFmpeg命令行解码生成的YUV文件大小进行比较,发现通过代码解码生成的YUV像素数据有丢失:

那么这个代码生成的YUV文件可以播放吗?我们通过ffplay命令播放,可以发现可以播放,但是显示的不正常。

ffplay -video_size 640x480 -pix_fmt yuv420p out.yuv

分析

通过添加log来分析,在解码时添加

static int decode(AVCodecContext *ctx,AVPacket *pkt,AVFrame *frame,QFile &outFile){
...... while (true) {
...... qDebug()<<"解码出第"<< ++frameIdx << "帧";
qDebug() << frame->data[0] << frame->data[1]<< frame->data[2];// 0x96faa80 0x9746000 0x9758e00
/**
* frame->data[0] 0x96faa80
* frame->data[1] 0x9746000
* frame->data[1] 0x9758e00
*
* frame->data[1] - frame->data[0] = 308608 = Y平面大小
* frame->data[2] - frame->data[1] = 77312 =U平面大小
*
* Y平面大小 = 640 * 480 *1 = 307200
* U平面大小 = 640 * 480 *0.25 = 76800
* V平面大小 = 640 * 480 *0.25 = 76800
*/ // 将解码后的数据写入文件
int imgSize = av_image_get_buffer_size(ctx->pix_fmt,ctx->width,ctx->height,1);
qDebug()<<"每一帧大小:"<<imgSize;
outFile.write((char *)frame->data[0],imgSize);
}
}

通过上面的log打印信息分析来看,Y、U、V三个平面大小比实际的要大些,通过之前的yuv420p格式可以知道,它是平面格式的也就意味着yuv是紧挨着的,但是分析结果来看yuv的大小比实际的要大,感觉yuv并不是紧挨着的是有空隙。

那么如何处理呢?其实我们可以将Y、U、V分别写入

// 写入Y平面
outFile.write((char *) frame->data[0],frame->linesize[0] * ctx->height);
// 写入U平面
outFile.write((char *) frame->data[1],frame->linesize[1] * ctx->height >> 1);// 除以2
// 写入V平面
outFile.write((char *) frame->data[2],frame->linesize[2] * ctx->height >> 1);// 除以2

这样在运行生成yuv文件后,在使用ffplay命令播放,可以发现视频显示正常了,但是它的大小还是跟ffmpeg生成的不一样。我们可以计算它俩大小的差值,

116121600 - 115660800 = 460800

可以发现它们的差值正好是一帧的大小,说明代码生成的少了一帧数据没有写入到文件中。

这时我们在解析数据里在加个打印

qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;

通过打印可以发现解码结束后parser中还剩余2925字节的数据没有送入AVPacket中,需要让paeser把剩余数据继续送入到AVPacket中。

解决办法就是当h264文件中数据全部读完后再调用一次av_parser_parse2函数,可以参考https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_609A2E9F73AB634ED670392DD89A63400008@qq.com/的解决办法。

将代码改造如下:

// 读取数据
do{
// 从文件中读取h264数据
inLen = inFile.read(inDataArray, IN_DATA_SIZE);
// 设置是否到了文件尾部
inEnd = !inLen; // 让inData指向数组的首元素
inData = inDataArray;
// 只要输入缓冲区中还有等待进行解码的数据
while (inLen > 0 || inEnd) {
// 到了文件尾部(虽然没有读取任何数据,但也要调用av_parser_parse2,修复bug)
// 经过解析器解析
ret = av_parser_parse2(parserCtx, ctx,
&pkt->data, &pkt->size,
(uint8_t *) inData, inLen,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); if (ret < 0) {
ERROR_BUF(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;
} // 如果到了文件尾部
if (inEnd) break;
}
}while (!inEnd);

这个时候在运行代码,查看打印发现parser中剩余数据已全部刷出,并且这次和在ffmpeg生成的yuv文件大小完全一样:



代码链接

27_H.264解码实战的更多相关文章

  1. [ffmpeg] h.264解码所用的主要缓冲区介绍

    在进行h264解码过程中,有两个最重要的结构体,分别为H264Picture.H264SliceContext. H264Picture H264Picture用于维护一帧图像以及与该图像相关的语法元 ...

  2. <开源项目分析>Cisco的开源视频加解码器THOR(H.264解码)

    原创博客,转载请联系博主! 题外话:自学了快两个月的Perl语言,本来打算写两篇基础介绍的博文来科普一下一些小技巧,但是仔细想想还是没有必要了吧,毕竟现在无论是在用Perl5还是Perl6的人都是小众 ...

  3. [转]【基于zxing的编解码实战】精简Barcode Scanner篇

    通过<[搞定条形码]zxing项目源码解读(2.3.0版本,Android部分)>的分析,现在可以实现最终目标了:精简Barcode Scanner并将其中的编码和解码分离为两个独立的部分 ...

  4. FFmpegh.264解码

    - (int)DecodeH264Frames: (unsigned char*)inputBuffer withLength:(int)aLength { ; ; av_init_packet(&a ...

  5. h.264并行解码算法分析

    并行算法类型可以分为两类 Function-level Decomposition,按照功能模块进行并行 Data-level Decomposition,按照数据划分进行并行 Function-le ...

  6. H.264视频在android手机端的解码与播放(转)

    随着无线网络和智能手机的发展,智能手机与人们日常生活联系越来越紧密,娱乐.商务应用.金融应用.交通出行各种功能的软件大批涌现,使得人们的生活丰富多彩.快捷便利,也让它成为人们生活中不可取代的一部分.其 ...

  7. H.264开源解码器评测

    转自:http://wmnmtm.blog.163.com/blog/static/38245714201142883032575/ 要播放HDTV,就首先要正确地解开封装,然后进行视频音频解码.所以 ...

  8. 【图像处理】H.264开源解码器评测

    转自:http://wmnmtm.blog.163.com/blog/static/38245714201142883032575/ 要播放HDTV,就首先要正确地解开封装,然后进行视频音频解码.所以 ...

  9. (转载)H.264码流的RTP封包说明

    H.264的NALU,RTP封包说明(转自牛人) 2010-06-30 16:28 H.264 RTP payload 格式 H.264 视频 RTP 负载格式 1. 网络抽象层单元类型 (NALU) ...

  10. H.264码流结构解析

    from:http://wenku.baidu.com/link?url=hYQHJcAWUIS-8C7nSBbf-8lGagYGXKb5msVwQKWyXFAcPLU5gR4BKOVLrFOw4bX ...

随机推荐

  1. 限流设置之Nginx篇

    question1:为什么用到Nginx,Nginx有什么功能? 1.反向代理(建议先看正向代理,反向代理则是同样你要与对方服务器建立连接,但是,代理服务器和目标服务器在一个LAN下,所以我们需要与代 ...

  2. 云计算 - 负载均衡SLB方案全解与实战

    云计算 - 负载均衡SLB方案全解与实战,介绍SLB的核心技术.用户最佳实践.阿里云 SLB产品举例.应用场景. 关注[TechLeadCloud],分享互联网架构.云服务技术的全维度知识.作者拥有1 ...

  3. NC19832 1408

    题目链接 题目 题目描述 小m曾经给小t和小s讲过一个奇怪的故事.这个故事叫做1408.故事的大体内容如下. 主人公迈克·安瑟林(约翰·库萨克饰)是一个恐怖小说家.将装神弄鬼作为本职工作的迈克,平日里 ...

  4. js获取格式化日期方法

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 代码+案例,实战解析BeautifulSoup4

    本文分享自华为云社区<从HTML到实战:深入解析BeautifulSoup4的爬虫奇妙世界>,作者:柠檬味拥抱. 网络上的信息浩如烟海,而爬虫技术正是帮助我们从中获取有用信息的重要工具.在 ...

  6. 【树莓派】拷贝系统到新SD卡(系统备份/部署到另一台树莓派上)适用ubuntu 20.04.3

    本教程适用ubuntu 20.04.3 其他版本也大同小异.这种方法能更快的将系统部署下去,如果重新安装一遍加上各种配置相信你会比较疯狂即使做了自动化脚本! 一.树莓派sd卡拷贝 把旧SD卡插入树莓派 ...

  7. 统信UOS系统开发笔记(四):从Qt源码编译安装之编译安装QtCreator4.11.2,并配置编译测试Demo

    前言   上一篇已经从Qt源码编译了Qt,那么Qt开发的IDE为QtCreator,本篇从源码编译安装QtCreator,并配置好构建套件,运行Demo并测试.   统信UOS系统版本   系统版本: ...

  8. Qt三方库开发技术(一):QuaZIP介绍、编译和使用

    前言   Qt使用一些压缩解压功能,探讨过libzip库,zlib库,libzip库比较原始,还有其他库,都比较基础,而在基础库之上,又有高级封装库,Qt中的QuaZIP是一个很好的选择.  本文主要 ...

  9. 【LeetCode二叉树#00】二叉树的基础知识

    基础知识 分类 满二叉树 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树. 完全二叉树 除了底层外,其他部分是满的,且底层从左到右是连续的,称为完全二叉树 满二叉树一定是完全二 ...

  10. jquery实现轮播图切换

    这个是我模仿网易云的音乐界面写的轮播图,主要实现的功能有 1.每隔4秒图片和对应的背景颜色一起切换 2.点击翻页会跳转到相对应的图片以及背景上 3.点击左右翻页,实现顺序切换 <1>HTM ...