FFmpeg API 之 libavcodec库

libavformat 库负责封装和解封装,而 libavcodec 则用于解码和编码。

类型 AVPacket 表示编码后的数据,其中包含一个或多个编码后的帧数据。类型 AVFrame 表示解码后,或者说原始的帧数据。编码和解码在某种程度来说,就是两者之间的互相转换。

 

1、编解码概述

FFmpeg 提供的 encode/decode API 有如下四个函数 avcodec_send_packet () / avcodec_receive_frame () / avcodec_send_frame () / avcodec_receive_packet(),它们可被分为用于输入和输出两种情况。

编解码音频和视频的 API 非常相似,其工作流程如下:

  • 设置并打开一个 AVCodecContext

  • 输入有效的数据:

    1. 对于解码来说,调用 avcodec_send_packet() 函数,我们将压缩后的原始数据以 AVPacket 的形式传递给它

    2. 对于编码来说,调用 avcodec_send_frame() 函数,我们将未压缩的音频或者视频数据以 AVFrame 的形式传递给它

    在上述这两种情况中,我们建议使用 “引用计数” 的 AVPacket 和 AVFrame,否则 libavcodec 可能必须得拷贝输入数据才行。(libavformat 总是返回“引用计数”的 AVPacket,而函数 av_frame_get_buffer() 则用于分配一个“引用计数”的 AVFrames)

  • 循环接收输出数据。也就是周期性地调用 avcodec_receive_*() 中的一个函数,然后处理它们的输出数据。

    1. 对于解码来说,我们调用 avcodec_receive_frame() 。如果成功,它将会返回一个 AVFrame,其中包含了未压缩的音频或视频数据。

    2. 对于编码来说,我们调用 avcodec_receive_packet() 。如果成功,它将会返回一个 AVPacket,其中包含压缩后的帧数据。

    重复调用 receive 函数,直到它返回 EAGAIN 或者 一个 error。返回值 EAGAIN 表示需要继续输入新的数据然后才能获取新的输出,此时,我们需要继续输入有效的数据。一般而言,每一个输入 frame/packet,都会产生一个输出 frame/packet,当然也可能是 0 个或者多个。

 

在编码或者解码的开始阶段,codec 可能会接收多个输入 packet/frame ,但却不返回任何数据,直到它的内部缓冲区被填满。

理论上,在没有将所有输出数据都 receive 的情况下,即内部缓冲区已满时,我们再次发送输入数据会导致 EAGAIN 发生。我们可以利用这个特点构建一种不同于上述建议的编解码循环。例如,你可以尝试在每次循环时发送新的输入数据,如果返回了 EAGAIN ,那么就去尝试获取输出数据。

 

在输入的数据流结束的时候,我们需要对 编解码器 进行 ”flush“ 操作,也就是清空编解码器中的内部缓存数据。之所要这么做,是因为出于性能或者其他需要(如对B帧进行考虑),编解码器内部可能会缓冲多个 frame 或者 packet。

那么,如何对编解码器进行 “flush” 操作:

  1. 不发送有效的输入数据,而是发送 NULL 给 avcodec_send_packet() 或 avcodec_send_frame() 函数。这样就会执行 “flush” 操作。

  2. 循环调用 avcodec_receive_frame() 或 avcodec_receive_packet() 函数,直到返回 AVERROR_EOF 错误。如果编解码器没有执行上一步的 “flush” 操作,函数会返回 AVERROR(EAGAIN) 错误。

  3. 再次恢复编码之前,我们必须使用 avcodec_flush_buffers() 函数重新设置编解码器。

 

FFmpeg 强烈建议按照上面的描述来使用其 API 。但是我们也可以不按照上述的严格模式来调用函数。例如,你可以重复调用 avcodec_send_packet() 函数,但却不调用 avcodec_receive_frame() ,在这种情况下,一开始的 avcodec_send_packet() 将会成功,直到编解码器的内部缓存区被填满(通常是在初始化输入后,每次输出一个帧),此时函数会返回 AVERROR(EAGAIN) 并拒绝输入,这时别无选择,只能读取一部分输出。

不是所有的编解码器都会遵循一个严格并可预测的数据流,例如输入一个packet即可解码获得一个frame,这是不被保证的。我们唯一可以保证的是:当我们调用某个 send/receive 函数时,如果该函数返回 AVERROR(EAGAIN) ,那么此时去调用对应的 send/receive 将成功,或至少不会失败(即返回 EAGAIN ),即任何情况下,总有一个会调用成功,不允许发生在同时两者调用都失败的情况。一般来说,没有解码器会允许无限缓冲的输入或输出。

 

上述 API 替代了如下列出的旧函数:

  • avcodec_decode_video2() 和 avcodec_decode_audio4():现在,我们使用 avcodec_send_packet() 将输入提供给解码器,然后使用 avcodec_receive_frame() 接收每个数据包解码后的帧,和旧的解码API不同,现在我们可以从一个 packet 中解码生成 1 到 多个 frame。你永远不需要将输入同一个 AVPacket 两次(除非返回EAGAIN,拒绝了该 packet 输入),如果重复提供了输入,相关函数也不会从 packet 中读取任何数据。此外,当我们想要进行 “flush” 操作时,也仅仅只需要一次调用即可。

  • avcodec_encode_video2() 和 avcodec_encode_audio2():我们使用 avcodec_send_frame() 将输入提供给编解码器,然后使用 avcodec_receive_packet() 接受经过编码的数据包。为 avcodec_receive_packet() 提供用户分配的缓存区是不可能的。

  • 新的 API 尚未支持处理字幕。

 

对同一个 AVCodecContext 混合使用新旧的API是不允许的,因为这样做会导致未定义行为。

有一部分编解码器需要使用新的API,使用旧的API会导致错误发生。所有编解码器都支持新的API。

一个 codec 不允许对 send 和 receive 函数都返回 AVERROR(EAGAIN),最多其中一个函数可返回这个错误。否则,这将是一个无效的状态,它将导致该 codec 的用户陷入到无尽的循环中。同时 send/receive API 也没有时间的概念,也就是说发生在不同时间的,同一个函数调用(在这期间没有其他调用发生)返回的结果是一样的。API 是一个严格的状态机,仅仅时间的流逝是不会影响它的。一些依赖时间的行为在某些特定情况下可以被视为可接受的。但它决不能导致 send/receive 在同一时刻均返回 EAGAIN 错误。

 

2、常用类型说明

AVCodecID,枚举类型,用于描述 FFmpeg 中有哪些被注册的编解码器。

AVMediaType,枚举类型,用于描述编解码器的类型,分为 video,audio,subtitle,data,attachment 5 种类型。

AVCodec,表示一个编解码器。

AVCodecContext,表示一个编解码器上下文。

 

这些就是 编解码 中最常用到的几个类型。其中 AVCodec 和 AVCodecContext 让人有些疑惑,不知道两者的区别是什么。

AVCodec 表示一个编解码器,它更多的表示了编解码器的共有特性,是对该编解码器的一个描述,它不是可以实际参与编解码的类型。而 AVCodecContext 则表示一个编解码器上下文,它实际参与编解码,它的字段中保存了当前实际的相关参数,当然也包含了一个 AVCodec 的实例。

通俗的说,libx264 是一个编解码器,那么其相关的 AVCodec 就用于描述这个编解码的特征,如有那些特征,那些属性,以及最重要的具体的编解码函数等等,但它只是描述,并没有具体的保存这些值,不能实际参与编解码。而AVCodecContext 则是具体保存这些值的类型,它们可以实际被使用。

在 FFmpeg 中我们可以看到,实际参与代码的都是 context 类型,这也算是一个可以理解代码的小知识点。

2.1、codec

AVCodec,表示一个编解码器,在 FFmpeg 中每一个 codec 都是被提前注册好的,它们都被分配了一个独立的ID(使用AVCodecID表示),名称等,是相对固定的结构,我们对它的操作不多,就现在看来,仅有一条:找到正确的 codec。

第一种情况:知道要用的 codec 类型,也就是知道 ID 和 name,然后获取对应的 codec。我们可以根据以下的函数直接获取 codec:

AVCodec *avcodec_find_decoder(enum AVCodecID id);
AVCodec *avcodec_find_decoder_by_name(const char *name);
AVCodec *avcodec_find_encoder(enum AVCodecID id);
AVCodec *avcodec_find_encoder_by_name(const char *name);

第二种情况:根据 AVStream 中的数据分析出实际的 codec ,然后获取对应的 codec 。在打开一个输入后,我们可以通过 AVFormatContext.stream 获取到打开的 stream,再通过 AVStream.codepar 或 AVStream.codec 就可以直接获取到 codec 或者其 ID,然后再通过上述函数就可以获取到正确的 codec 。

当我们获取到 codec 之后,我们还不能直接使用这个 codec ,我们通过 codec context 来使用与之关联的 codec 。首先需要分配一个 codec 对应的 codec context,然后打开这个codec context,之后就可以使用这个 codec context 了。

2.2、codec context

首先,我们使用 avcodec_alloc_context3() 来分配一个 AVCodecContext 结构体,然后再使用 avcodec_open2() 函数打开这个 AVCodecContext。注意:codec context 是为了使用对应的 codec,因此调用这两个函数时,传入的 codec 必须是同一个有效的 codec。

codec context 最基本的两个功能就是解码和编码,也就是 send/receive 函数对,如何使用请参照之前关于编解码的描述。

以下是codec context 的相关 API:

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

/*
    分配一个 codec context,并将其字段设置为默认值。生成的对象在使用完后,必须使用 avcodec_free_context() 函数释放掉。

    参数codec:如果非NULL,该函数为会为指定的codec分配其私有数据并默认初始化。
              如果是NULL,那么就不会初始化codec特定的相关默认值,这可能会导致其默认设置不合格或不正确(这主要对于encoder比较重要,如 libx264)

    返回值:成功返回一个 codec context,其字段被初始化为默认值;失败返回NULL。
*/

 

void avcodec_free_context(AVCodecContext **avctx);

/*
    释放 codec context 以及与之关联的所有内容,然后将提供的指针置为NULL。
 */
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

/*
    使用给定的 AVCodec 来初始化 AVCodecContext 。对AVCodecContext使用这个函数之前,
 必须使用 avcodec_alloc_context3() 来分配该AVCodecContext。 
 
    注意:在实际进行解码流程之前(如 avcodec_receive_frame()),必须要调用本函数。

    代码示例:
 * avcodec_register_all();
 * av_dict_set(&opts, "b", "2.5M", 0);
 * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
 * if (!codec)
 *     exit(1);
 *
 * context = avcodec_alloc_context3(codec);
 *
 * if (avcodec_open2(context, codec, opts) < 0)
 *     exit(1);

				  
     参数avctx:要被初始化的 AVCodecContext
     参数codec:要使用的 AVCodec ,这里打开一个与之相关的 codec context 就是为了使用这个 codec。
    如果在前面的代码中,如果已经传递了一个非空的 AVCodec 给 avcodec_alloc_context3() 而分配了 AVCodecContext,
    也就是第一个参数 avctx,那么此时这个参数必须是NULL或者就是之前同一个 AVCodec
     参数options:一个 dictionary,用于传递编解码器的共有和私有选项。在返回时,这个对象将会被传入后那些未被发现的options充满,
    即被使用的选项被会删除,未被使用的则留了下来。
 
    返回值:成功返回0,失败返回一个负的错误值
 */
int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

/*
     根据传入的 codec parameter 中的值,填充 codec context。在 codec context中凡是可以对应到 par 中的字段,通通一律释放,
    然后按照 par 中的值重新填充这些字段。而 codec context 中凡是没有定义的字段则不会被修改。

    返回值:成功,返回值大于等于0,失败则返回一个负值 AVERROR。
 */
int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);

int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt);

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

/*
    这四个函数不必多少,请参考上面介绍编解码流程的章节,其中有详细描述。
*/
int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
                         int *got_picture_ptr,
                         const AVPacket *avpkt);
                         
/*
    解码视频帧,即将 AVPacket 解码为 AVFrame。
    将 avpkt->data 指向的,大小为 avpkt->size 的已编码数据,解码到 picture 中。有些编码器可能会存在一个 AVPacket 中有多个 frame 的情况,
    此时本函数仅解码其中的第一个帧。此时返回值(消耗掉的字节数)会小于 avpkt->size。此我们应该再次调用本函数,传入含有上次 packet 中剩余数据的 AVPacket 去解码第二个帧,依次类推。
    即使没有 frame 返回,但相关的数据已经传入 decoder 中,我们只需要继续传入数据使其可以完整解码或者报错为止。


    警告:输入的 AVPacket 中的数据必须大于 AV_INPUT_BUFFER_PADDING_SIZE。
    警告:输入数据的末尾应该被设置为 0 值,以防止损坏的 MPEG 流发生过读。
 
    事实上,输入的数据应该为 inbuf+AV_INPUT_BUFFER_PADDING_SIZE 长度的缓冲区,其中 inbuf 部分为我们实际的输入,即上面的警告1.
    而  AV_INPUT_BUFFER_PADDING_SIZE 部分应该全部置为 0,即警告2.
 	
    警告:在解码的末尾,需要进行 flush 操作,以便读取剩余的 frame。所谓 flush 操作,就是将一个 avpkt->data = NULL,avpkt->size = 0 的 packet 传入本函数。

    参数 picture:我们将解码后的帧保存到这个参数里。注意,picture 在这里不需要自己分配空间,编解码器会为它分配所需的空间。
    我们知道 frame 是通过引用来保存相关数据的,如果 AVCodecContext.refcounted_frames 被设置为1,那么相关引用所有权归属调用者,所以在使用完之后,必须调用 av_frame_unref() 释放它。
    如果 AVCodecContext.refcounted_frames 被设置为0,那么返回的引用其所有权属于编解码器,其有效期直到下次调用这个函数或者关闭编解码器或者flush该编解码为止。

    参数 got_picture_ptr:返回被解码的帧数,当然由于本函数最多只解码 1 帧,因此只能返回 0 和 1。
 
    返回值:失败返回负的 error 值。成功返回消耗的字节数,如果没有解码任何帧,则返回0.
*/
int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
                          int *got_frame_ptr, const AVPacket *avpkt);

/*
    解码音频帧。
 	
    详情与 avcodec_decode_video2() 完全相同,不再赘述。
 */

 

3、解码前的分包操作

在一般的解码流程中,我们调用 av_read_frame() 从 format context 直接获取到了 packet,并没有涉及到从一个字节流中如何分包为一个 packet 的过程。当然,在一般情况下,我们也不需要关心是如何分包的,但有些时候,可能会需要对相关细节做一定的修改,因此,我们对这一部分做一个了解。

我们使用 AVCodecParserContext 结构体来完成分包,它表示一个 codec 相关的 parser,也就是它知道如何解析输入的字节流。

分包的过程其实非常简单:

  1. 调用 av_parser_init() 函数,返回一个指定 codec 类型的 parser context;或者调用 av_stream_get_parser() 根据一个 AVStream 来返回其 codec 相关的 parser context。

  2. 调用 av_parser_parse2() 函数进行分包

  3. 处理完输入后,调用 av_parser_close() 释放 parser context 。

    官方示例 decode_audio.c 中展示了分包的过程。

以下是其相关API:

AVCodecParserContext *av_parser_init(int codec_id);

/*
	使用 ID 初始化一个 codec parser context。
*/
struct AVCodecParserContext *av_stream_get_parser(const AVStream *s);

/*
	使用 stream 初始化一个 codec parser context。
*/
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);

/**
 * Parse a packet.
 
    根据输入的字节流数据 buf 和 buf_size 解析生成 AVPacket 数据,生成的数据会保存在 poutbuf 和 poutbuf_size 中,一般会在后续将其保存在 AVPacket 中,或者在调用时,直接传入AVPacket.data 和AVPacket.size。
 
 
     参数 s		: codec parser context
     参数 avctx		: codec context
     参数 poutbuf	:输出参数,指向保存解析后数据的缓冲,如果尚未有解析完成的数据,则传入NULL
     参数 poutbuf_size  : 输出参数,保存解析完成的数据的大小,如果尚未有解析完成的数据,则传入0
     参数 buf		:输入缓冲
     参数 buf_size	: 没有 padding 的缓冲字节大小。即,整个缓冲 buf 的大小应该是 
					buf_size + AV_INPUT_BUFFER_PADDING_SIZE。
                        为了通知输入的EOF,这个值应该为0,这样才能将最后一帧输出
     参数 pts		:输入显示时间戳
     参数 dts		:输入解码时间戳
     参数 pos		:流中的输入字节位置

     返回值:返回已经使用的输入位流中的字节量

 * 示例:
 *   while(in_len){
 *       len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
 *                                        in_data, in_len,
 *                                        pts, dts, pos);
 *       in_data += len;
 *       in_len  -= len;
 *
 *       if(size)
 *          decode_frame(data, size);
 *   }
 */
void av_parser_close(AVCodecParserContext *s);

/*
    关闭 parser context。
*/

 

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值