FFmpeg的H264解码实战

2023-11-03

本文的内容是解码裸流,即从本地读取h264码流,然后解码成YUV像素数据的过程。

1、FFmpeg视频解码流程

如上图所示是通过FFmpeg进行视频解码的流程。

音视频流媒体高级开发 学习路线​www.bilibili.com/video/BV1ka411P7HS

2、 代码实战

2.1、获取解码器

   enum AVCodecID audio_codec_id = AV_CODEC_ID_H264;
    const AVCodec *codec = avcodec_find_decoder(audio_codec_id);
    if (!codec) {
        fprintf(stderr, "Codec not found\n");
        return;
    }

通过调用 avcodec_find_decoder函数根据ID来查找注册的解码器,这里的ID在源码的libavcodec/codec_id.h文件中的AVCodecID枚举中有定义,我们用视频h264解码的ID使用AV_CODEC_ID_H264即可。当然你也可以使用avcodec_find_decoder_by_name函数通过传入解码器的名称来获取解码器,如:avcodec_find_decoder_by_name("libx264")来获取解码器。

2.2、初始化裸流解析器

// 获取裸流的解析器 AVCodecParserContext(数据)  +  AVCodecParser(方法)
AVCodecParserContext *parser = av_parser_init(codec->id);
if (!parser) {
    fprintf(stderr, "Parser not found\n");
    return;
}

调用av_parser_init函数来初始化一个裸流的解析器AVCodecParserContext。传入参数解码器的id。

2.3、 创建上下文

// 分配codec上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if(!codec_ctx) {
    fprintf(stderr, "Could not allocate audio codec context\n");
    return;
}

//将解码器和解码器上下文进行关联
int ret = avcodec_open2(codec_ctx, codec, NULL);
if(ret < 0) {
    fprintf(stderr, "Could not open codec\n");
    return;
}

avcodec_alloc_context3函数初始化一个上下文,为AVCodecContext分配内存,然后调用avcodec_open2函数打开解码器,将解码器和解码器上下文进行关联。

2.4、打开文件

// 打开输入文件
FILE *infile = fopen(in_file, "rb");
if (!infile) {
    fprintf(stderr, "Could not open %s\n", in_file);
    return;
}

// 打开输出文件
FILE *outfile = fopen(out_file, "wb");
if (!outfile) {
    fprintf(stderr, "Could not open %s\n", in_file);
    return;
}

in_file是输入文件的路径,即本地h264格式文件的路径, out_file是存储将h264码流解码后得到的视频像素格式yuv420p格式数据的文件路径。

2.5、创建AVPacket和AVFrame

AVPacket *pkt = av_packet_alloc();
if(!pkt) {
    fprintf(stderr, "Could not alloc avpacket\n");
    return;
}

AVFrame *decoded_frame = av_frame_alloc();
if(!decoded_frame) {
    fprintf(stderr, "Could not allocate audio frame\n");
    return;
}

2.6、读取数据并解码

// AV_INPUT_BUFFER_PADDING_SIZE 在输入比特流结尾的要求附加分配字节的数量上进行解码
    uint8_t inbuf[VIDEO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t *data = inbuf;
    size_t data_size = 0;
    //读取VIDEO_INBUF_SIZE大小的数据到缓存区
    data_size = fread(inbuf, 1, VIDEO_INBUF_SIZE, infile);
    while(data_size > 0) {
        ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size, data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE,  0);
        if (ret < 0)
        {
            fprintf(stderr, "Error while parsing\n");
            break;
        }
        data += ret;
        data_size -= ret;
        if(pkt->size)
            decoder(codec_ctx, pkt, decoded_frame, outfile);
        if(data_size < VIDEO_REFILL_THRESH) {
            memmove(inbuf, data, data_size);
            data = inbuf;
            size_t len = fread(data + data_size, 1, VIDEO_INBUF_SIZE - data_size, infile);
            if (len > 0)
                data_size += len;
        }
    }

如上代码所示是读取本地数据并进行解码的过程,首先我们创建了一个VIDEO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE大小的数据缓存区,加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界。然后调用fread函数从本地文件中每次读取VIDEO_INBUF_SIZE大小的数据到缓存区中。

av_parser_parse2函数用来解析出一个完整的Packet,是解码处理过程中的核心函数之一。 如下是官方对于该函数的参数说明。

/**

 * Parse a packet.
 * @param s             parser context.
 * @param avctx         codec context.
 * @param poutbuf       set to pointer to parsed buffer or NULL if not yet finished.
 * @param poutbuf_size  set to size of parsed buffer or zero if not yet finished.
 * @param buf           input buffer.
 * @param buf_size      buffer size in bytes without the padding. I.e. the full buffer
                        size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE.
                        To signal EOF, this should be 0 (so that the last frame
                        can be output).
 * @param pts           input presentation timestamp.
 * @param dts           input decoding timestamp.
 * @param pos           input byte position in stream.
 * @return the number of bytes of the input bitstream used.

 */

int av_parser_parse2(AVCodecParserContext *s,
                     AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size,
                     int64_t pts, int64_t dts,
                     int64_t pos);

1、s和avctx分别表示解码器和解码器的上下文
2、poutbuf:输出数据地址
3、poutbuf_size:输出数据大小,如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧
4、buf和buf_size分别表示输入数据和输入数据大小
5、函数执行完后返回已经使用的二进制流的数据长度

2.7、解码

decoder方法的代码如下:

static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
    av_strerror(errnum, err_buf, 128);
    return err_buf;
}

static void print_video_format(const AVFrame *frame)
{
    printf("width: %u\n", frame->width);
    printf("height: %u\n", frame->height);
    printf("format: %u\n", frame->format);
}

static void decoder(AVCodecContext *codec_ctx, AVPacket *pkt, AVFrame *frame, FILE *out_file){
    int ret = avcodec_send_packet(codec_ctx, pkt);
    if(ret == AVERROR(EAGAIN))
    {
        fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
    }
    else if (ret < 0)
    {
        fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n", av_get_err(ret), pkt->size);
        return;
    }
    while (ret >= 0) {
        ret = avcodec_receive_frame(codec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0)
        {
            fprintf(stderr, "Error during decoding\n");
            return;
        }
        static int s_print_format = 0;
        if(s_print_format == 0){
            s_print_format = 1;
            print_video_format(frame);
        }
        // frame->linesize[1]  对齐的问题
        // 正确写法  linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
        for(int j=0; j<frame->height; j++)
            fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, out_file);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, out_file);
        for(int j=0; j<frame->height/2; j++)
            fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, out_file);
    }
}

2.7.1、函数解析

avcodec_send_packet和avcodec_receive_frame是成双结对出现的。

1、调⽤avcodec_send_packet()给解码器传⼊包含原始的压缩数据的AVPacket对象,需要注意的是输⼊的avpkt-data缓冲区必须⼤于AV_INPUT_BUFFER_PADDING_SIZE,因为优化的字节流读取器必须⼀次读取32或者64⽐特的数据,且在将包发送给解码器的时候,AVCodecContext必须已经通过avcodec_open2打开。输⼊的参数AVPakcet,通常情况下,输⼊数据是⼀个单⼀的视频帧或者⼏个完整的⾳频帧。调⽤者保留包的原有属性,解码器不会修改包的内容。解码器可能创建对包的引⽤。如果包没有引⽤计数将拷⻉⼀份。跟以往的API不⼀样,输⼊的包的数据将被完全地消耗,如果包含有多个帧,要求多次调⽤avcodec_recvive_frame,直到 avcodec_recvive_frame返回AVERROR(EAGAIN)或AVERROR_EOF。输⼊参数可以为NULL,或者AVPacket的data域设置为NULL或者size域设置为0,表示将刷新所有的包,意味着数据流已经结束了。第⼀次发送刷新会总会成功,第⼆次发送刷新包是没有必要的,并且返回AVERROR_EOF,如果解码器缓存了⼀些帧,返回⼀个刷新包,将会返回所有的解码包。

返回值:

0: 表示成功
AVERROR(EAGAIN):当前状态不接受输⼊,⽤户必须先使⽤avcodec_receive_frame() 读取数据帧;
AVERROR_EOF:解码器已刷新,不能再向其发送新包;
AVERROR(EINVAL):没有打开解码器,或者这是⼀个编码器,或者要求刷新;
AVERRO(ENOMEN):⽆法将数据包添加到内部队列

2、调用avcodec_receive_frame从解码器返回已解码的输出数据。在⼀个循环体内去接收数据的输出,即周期性地调⽤avcodec_receive_frame()来接收输出的数据,直到返回AVERROR(EAGAIN)或其他错误。需要注意的是该函数在执⾏其他操作之前,函数内部将始终先调⽤av_frame_unref(frame)进行资源释放。

返回值:

0: 表示成功,返回⼀个帧
AVERROR(EAGAIN):该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器;
AVERROR_EOF:解码器已经被完全刷新,不再有输出帧;
AVERROR(EINVAL):编解码器没打开;

2.8、冲刷解码器

pkt->data = NULL; 
pkt->size = 0;
decoder(codec_ctx, pkt, decoded_frame, outfile);

2.9、释放资源

fclose(outfile);
fclose(infile);
avcodec_free_context(&codec_ctx);
av_parser_close(parser);
av_frame_free(&decoded_frame);
av_packet_free(&pkt);

2.10、播放yuv文件

解码后我们最终会得到一个yuv格式的文件,可以使用下面指令进行验证输出是否正确。

 ffplay -pixel_format yuv420p -video_size 768x320 -framerate 25 out.yuv

这里的768x320是视频数据的分辨率大小,是通过上述函数方法print_video_format获取的。

原文 FFmpeg的H264解码实战 - 掘金 

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

FFmpeg的H264解码实战 的相关文章

随机推荐

  • Vue:ElementUI怎么引入外部svg图标

    推荐阿里巴巴图标库 命令行运行npm install svg sprite loader 创建icons svg文件夹 将svg文件放在该文件夹下面 在components文件夹中创建svgiconfont vue文件 文件内容
  • 【啃书】《智能优化算法及其MATLAB实例》例7.2模拟退火算法进行函数寻优

    文章目录 问题描述 仿真过程 matlab源码 问题描述 仿真过程 matlab源码 该脚本要命名为func2 m 适应度函数 function value func2 x y value 5 cos x y x y y y y value
  • postgresql优化案例三:recheck cond

    文章目录 1 SQL语句 2 查看改善前执行计划 3 解决方案 3 1增加work mem的size 3 2 创建合适的索引 4 改善后执行计划 1 SQL语句 delete from sap dispatchingd hist a whe
  • 使用 VBA 自动化 Chrome / Edge

    介绍 Internet Explorer classic 下称IE 是基于ActiveX技术的 对于 Webscraping 或从 VBA 等 OLE 感知编程语言进行测试等任务 自动化 IE 非常容易 但微软将在不久的将来终止对 IE 的
  • C# 定义宏

    define ENABLE TEST 必须在 using 的上方定义 using System Collections using UnityEngine public class TestDefine MonoBehaviour void
  • 通配符*和?的区别

    通配符 和 的区别是 可以用来代替零个 单个或多个字符 而 仅可以使用代替一个字符 表示匹配的数量不受限制 而 的匹配字符数则受到限制
  • 17:UnicodeDecodeError: 'gbk' codec can't decode byte 0xff in position 0: illegal multibyte sequence

    菜鸟学python习题17运行错误 我的 直接在open里加了一个 errors ignore 即可 第一个人 使用python的时候经常会遇到文本的编码与解码问题 其中很常见的一种解码错误如题目所示 下面介绍该错误的解决方法 将 gbk
  • Rocketmq机制回顾总结

    1 应用场景如解耦 削峰填谷 异步处理 分布式事务中间协调 2 nameserver producer broker consumer 3 brokerName brokerId brokerRole Namesrvaddr常用配置参数 4
  • mysql查询语句group by 语句报错

    高版本的mysql 在查询语句时 有group by 会报错 报错信息如下 gt 1055 Expression 40 of SELECT list is not in GROUP BY clause and contains nonagg
  • [nifi] 数据管理分发工具

    官方文档 Apache NiFi 运行 HOME bin run nifi bat 打开网页 127 0 0 1 nifi 账号密码 HOME logs nifi app log 找到如下字段 开始界面 简单实例 1 本地 gt 本地 2
  • sqlplus,imp 等命令无效解决

    今天我在数据库上准备导入数据 但在输入sqlplus nolog 时报出 sqlplus 不是有效的内部命令 解决方案 在oracle ora92 bin 下找到了 sqlplus imp exp 等命令 点开可以执行 也可以在dos下进入
  • C# 算法系列 - 贪婪算法(背包问题)

    using System namespace ConsoleApp1 class Program static void Main string args 贪婪算法 背包问题 背包问题 Knapsack problem 是一种组合优化的NP
  • React 第七章 条件渲染

    根据条件渲染不同组件 可以实现组件的显示与隐藏 第一种 在函数内部通过属性props条件的判断 返回不同的组件 进行控制显示隐藏 如果返回null则表示不展示隐藏 1 条件渲染 function UserGreeting props ret
  • 银河麒麟软件源更新

    银河麒麟有用软件源 1 银河麒麟系统无法更新下载软件 原因是软件源失效 2 需要更换有效的软件源 3 修改软件源地址 在etc apt 的sources list文件里添加镜像源 deb http archive ubuntu com ub
  • js 在元素前后添加元素

    在元素前后添加创建的元素 话不多说 直接上代码 div div
  • axios上传formdata失败以及nodejs接受formdata失败

    axios上传formdata失败以及nodejs接受formdata失败 今天用nodejs写一个上传图片的功能 因为这个功能是用在vue中的 我首先在nodejs服务器上用jquery的ajax写好了这个功能 可是移植到vue中却出现了
  • 怎么查mac电脑是不是正品_mac电脑怎么投屏 教你选择适合自己的Mac投屏软件

    mac上有什么好的投屏软件嘛 苹果手机ios投屏到mac用哪款投屏软件 mac投屏ipad该用哪款软件怎么操作 macdown小编给大家介绍的这几款Mac投屏软件 各有各的特色 总有一款适合你投屏 1 AIrServer 7 for Mac
  • python人工智能,分类例解

    分类是一种常见的机器学习算法 是一种有监督的算法 简单说就是给出学习集数据都是带标签的 通过训练学习集数据获得模型 对未来给出的实际数据 根据模型进行分类 这里采用K最近邻算法 KNN K nearest neighbour KNN算法非常
  • 中国电子学会2023年05月份青少年软件编程C++等级考试试卷四级真题(含答案)

    1 怪盗基德的滑翔翼 怪盗基德是一个充满传奇色彩的怪盗 专门以珠宝为目标的超级盗窃犯 而他最为突出的地方 就是他每次都能逃脱中村警部的重重围堵 而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼 有一天 怪盗基德像往常一样偷走了一颗珍贵的
  • FFmpeg的H264解码实战

    本文的内容是解码裸流 即从本地读取h264码流 然后解码成YUV像素数据的过程 1 FFmpeg视频解码流程 如上图所示是通过FFmpeg进行视频解码的流程 音视频流媒体高级开发 学习路线 www bilibili com video BV