技术标签: FFmpeg API 详解
libavformat 库负责封装和解封装,而 libavcodec 则用于解码和编码。
类型 AVPacket 表示编码后的数据,其中包含一个或多个编码后的帧数据。类型 AVFrame 表示解码后,或者说原始的帧数据。编码和解码在某种程度来说,就是两者之间的互相转换。
FFmpeg 提供的 encode/decode API 有如下四个函数 avcodec_send_packet () / avcodec_receive_frame () / avcodec_send_frame () / avcodec_receive_packet(),它们可被分为用于输入和输出两种情况。
编解码音频和视频的 API 非常相似,其工作流程如下:
设置并打开一个 AVCodecContext
输入有效的数据:
对于解码来说,调用 avcodec_send_packet() 函数,我们将压缩后的原始数据以 AVPacket 的形式传递给它
对于编码来说,调用 avcodec_send_frame() 函数,我们将未压缩的音频或者视频数据以 AVFrame 的形式传递给它
在上述这两种情况中,我们建议使用 “引用计数” 的 AVPacket 和 AVFrame,否则 libavcodec 可能必须得拷贝输入数据才行。(libavformat 总是返回“引用计数”的 AVPacket,而函数 av_frame_get_buffer() 则用于分配一个“引用计数”的 AVFrames)
循环接收输出数据。也就是周期性地调用 avcodec_receive_*() 中的一个函数,然后处理它们的输出数据。
对于解码来说,我们调用 avcodec_receive_frame() 。如果成功,它将会返回一个 AVFrame,其中包含了未压缩的音频或视频数据。
对于编码来说,我们调用 avcodec_receive_packet() 。如果成功,它将会返回一个 AVPacket,其中包含压缩后的帧数据。
重复调用 receive 函数,直到它返回 EAGAIN 或者 一个 error。返回值 EAGAIN 表示需要继续输入新的数据然后才能获取新的输出,此时,我们需要继续输入有效的数据。一般而言,每一个输入 frame/packet,都会产生一个输出 frame/packet,当然也可能是 0 个或者多个。
在编码或者解码的开始阶段,codec 可能会接收多个输入 packet/frame ,但却不返回任何数据,直到它的内部缓冲区被填满。
理论上,在没有将所有输出数据都 receive 的情况下,即内部缓冲区已满时,我们再次发送输入数据会导致 EAGAIN 发生。我们可以利用这个特点构建一种不同于上述建议的编解码循环。例如,你可以尝试在每次循环时发送新的输入数据,如果返回了 EAGAIN ,那么就去尝试获取输出数据。
在输入的数据流结束的时候,我们需要对 编解码器 进行 ”flush“ 操作,也就是清空编解码器中的内部缓存数据。之所要这么做,是因为出于性能或者其他需要(如对B帧进行考虑),编解码器内部可能会缓冲多个 frame 或者 packet。
那么,如何对编解码器进行 “flush” 操作:
不发送有效的输入数据,而是发送 NULL 给 avcodec_send_packet() 或 avcodec_send_frame() 函数。这样就会执行 “flush” 操作。
循环调用 avcodec_receive_frame() 或 avcodec_receive_packet() 函数,直到返回 AVERROR_EOF 错误。如果编解码器没有执行上一步的 “flush” 操作,函数会返回 AVERROR(EAGAIN) 错误。
再次恢复编码之前,我们必须使用 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 错误。
AVCodecID,枚举类型,用于描述 FFmpeg 中有哪些被注册的编解码器。
AVMediaType,枚举类型,用于描述编解码器的类型,分为 video,audio,subtitle,data,attachment 5 种类型。
AVCodec,表示一个编解码器。
AVCodecContext,表示一个编解码器上下文。
这些就是 编解码 中最常用到的几个类型。其中 AVCodec 和 AVCodecContext 让人有些疑惑,不知道两者的区别是什么。
AVCodec 表示一个编解码器,它更多的表示了编解码器的共有特性,是对该编解码器的一个描述,它不是可以实际参与编解码的类型。而 AVCodecContext 则表示一个编解码器上下文,它实际参与编解码,它的字段中保存了当前实际的相关参数,当然也包含了一个 AVCodec 的实例。
通俗的说,libx264 是一个编解码器,那么其相关的 AVCodec 就用于描述这个编解码的特征,如有那些特征,那些属性,以及最重要的具体的编解码函数等等,但它只是描述,并没有具体的保存这些值,不能实际参与编解码。而AVCodecContext 则是具体保存这些值的类型,它们可以实际被使用。
在 FFmpeg 中我们可以看到,实际参与代码的都是 context 类型,这也算是一个可以理解代码的小知识点。
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 了。
首先,我们使用 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() 完全相同,不再赘述。
*/
在一般的解码流程中,我们调用 av_read_frame() 从 format context 直接获取到了 packet,并没有涉及到从一个字节流中如何分包为一个 packet 的过程。当然,在一般情况下,我们也不需要关心是如何分包的,但有些时候,可能会需要对相关细节做一定的修改,因此,我们对这一部分做一个了解。
我们使用 AVCodecParserContext 结构体来完成分包,它表示一个 codec 相关的 parser,也就是它知道如何解析输入的字节流。
分包的过程其实非常简单:
调用 av_parser_init() 函数,返回一个指定 codec 类型的 parser context;或者调用 av_stream_get_parser() 根据一个 AVStream 来返回其 codec 相关的 parser context。
调用 av_parser_parse2() 函数进行分包
处理完输入后,调用 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。
*/
取名QTranser,代表快速翻译的意思,实际上真的是最快(方便)的翻译软件~~简介:我为什么要写这款软件:其实还是有不少朋友使用windows写代码。写代码的时候经常会翻译英文单词。但是所有的翻译方式都“不够优雅”要么是局限在浏览器内。要么会在屏幕中央弹出一个小窗口。软件使用方式:Ctrl+CCtrl+C 可能是最优雅的翻译方式:这款翻译软件 利用Ctrl+C将单词复制进剪切板,将翻译结果显示在..._针对c语言注释进行翻译
Kettle使用的是JavaScript来作为它的脚本实现,使用的是mozilla 的rhino 1.5r5版本实现,如果你打算实现一些复杂的计算过程,比如字符串分割,数据类型转换,条件计算等等,你都应该使用脚本语言来搞定。我们在某种应用环境下使用脚本语言来实现一些动态的功能大部分原因都是为了避免编程,一个复杂一点的应用程序,比如像是Kettle这种工具,或是报表工具,它们不可能提供全部功_kettle javascript transform scripts
package com.java.design.java8.Stream.StreamDetail.BaseStreamDetail;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springfr...
小编使用STM32已经接近一年,在去年寒假期间师兄教学STM32最小系统接入OneNET云平台上传DHT11的温湿度数据,今年参加很多的专业竞赛。在参加数学建模期间,写下这一个简单的教程。一开始使用OneNET云平台,都不知道还有麒麟座开发板的源代码可以借鉴,所以接入云平台就比较麻烦或者说是困难,在多次实践中不断的摸索,在今年3月份成功的用正点原子开发板和ESP8266,EDP协议接入了中移物联网的云平台,实现了控制开发板上面的灯。暑假期间,参加竞赛之余,实现云端控制继电器,进而实现远程控制风扇。实物图_stm32连接onenet
这年头,搞个智能DNS还是很简单的,理论知识就不说了,这里把实战的步骤罗列一下,以期能对你有帮助。1、测试机器规划角色操作系统Ip地址内存MasterUbuntu 13.1010.32.240.18/232GSlaveUbuntu 13.1010.32.240.71/23_named bind
1,4,3,2Promise是一个micro task 主线程是一个task micro task queue会在task后面执行setTimeout返回的函数是一个新的task macro task queue所以Promise会先于新task执行根据html标准 一个task执行完后 ui会重渲染vue源码: nextTick异步更新dom操作..._用promise 返回一个 settimout 和直接写一个settimout 区别
一、准备工作:1、网页分析:进入目标网页,按下键盘F12,必须要认识图中画圈的部分。箭头:这个小箭头非常实用,点击后,在正常网页中点击哪个部分,代码区高光就会找到相应的代码。Element:包含网页源码,很多数据都从这里获得。NetWork:网络工作记录,按下图顺序点击,会得到很多响应信息。比如:请求头(Header)信息、Cookie、User-Agent等等,作用下面说。2、环境配置:需要提前下载好,这部分下载去网上搜就行,这里不做过多描述。.._爬虫保姆级教程
编程时经常遇到一些编译错误,这些都是我遇到过的,去百度、Google找了半天找到的,现在整理一下,方便以后查找。。。统一格式:ID.【错误提示】:编译器提示的错误信息 【说 明】:产生此错误的根本原因 【解决方法】:解决此错误的具体方法 1. 【错误提示】: libcmtd.lib(crt0.obj) : error LNK2001: unre
ORACLE的配置方案所有连接到ORACLE的用户必须执行两个代码模块可存取一个ORACLE数据库实例:应用或ORACLE工具:一数据库用户执行一数据库应用或一个ORACLE工具,可向ORACLE数据库发出SQL语句。ORACLE服务器程序:负责解释和处理应用中的SQL语句。在多进程实例中,连接用户的代码可按下列三种方案之一配置:对于每一个用户,其数据库应用程序和服务器程序组合成单个用户进程对于每..._pegehelper oracle配置
一、环境系统环境:windows8Python版本:python3.7IDE:Pycharm二、问题今天用pip安装torch,安装方式:pip install torch == 1.3.0出现了如下错误:Could not find a version that satisfies the requirement torch == 1.3.0 (from version:0.1.2. 0.1.2.post1. 0.1.2.post2)No matching distribution fou_could not find a version that satisfies the requirement torch (from versions
分享D瓜哥最近攒的资料()2013年8月27日D瓜哥发表评论阅读评论2,141 人阅读 扯扯蛋以前见过零零散散地介绍一些知名网站架构的分析文章。最近D瓜哥也想研究一下各大知名网站的架构。所以,就搜集了一下这方面资料。限于时间问题,这篇文章分享的文章并没有都看完,所以不保证所有文章的质量。另外,如果有朋友发现更好的文章,欢迎留言告知。再补充进来。
[uboot] (第四章)uboot流程——uboot编译流程http://blog.csdn.net/ooonebook/article/details/53000893以下例子都以project X项目tiny210(s5pv210平台,armv7架构)为例[uboot] uboot流程系列: [project X] tiny210(s5pv210)上电启动流程(BL0-B_uboot的编译流程