27_H.264解码实战
本文的主要内容:对H.264数据进行解码(解压缩)。
使用FFmpeg命令进行H.264解码
如果是命令行的操作,非常简单。
ffmpeg -c:v h264 -i in.h264 out_cmd.yuv # -c:v h264是指定使用h264作为解码器
使用FFmpeg代码进行H.264解码
接下来主要讲解如何通过代码的方式解码H.264数据,用到了avcodec、avutil两个库,整体过程跟《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_iterate
是parser
迭代器,其内部是在parser_list
数组中查找parser
(parser_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文件大小完全一样:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!