1万流量网站 服务器配置,学校室内设计效果图,黄岛网站建设多少钱,网页界面设计中摘要#xff1a;前一段时间熟悉了下FFmpeg主流程源码实现#xff0c;对FFmpeg的整体框架有了个大概的认识#xff0c;因此在此做一个笔记#xff0c;希望以比较容易理解的文字描述FFmpeg本身的结构#xff0c;加深对FFmpeg的框架进行梳理加深理解#xff0c;如果文章中有… 摘要前一段时间熟悉了下FFmpeg主流程源码实现对FFmpeg的整体框架有了个大概的认识因此在此做一个笔记希望以比较容易理解的文字描述FFmpeg本身的结构加深对FFmpeg的框架进行梳理加深理解如果文章中有纰漏或者错误欢迎指出。本文描述了FFmpeg编解码框架的工程结构基本构成以及大体的调用流程。因为FFmpeg的滤镜是相对独立的一个模块因此在此不会进行描述。 关键字FFmpeg,Framework 阅读须知阅读本文前你首先需要了解最基本的音视频处理相关的知识对于这些知识你至少需要最基本的了解比如知道什么是容器什么是编解码器以及大概的工作流程即可。 FFmepg是一个用C语言实现的多媒体封装、解封转、编解码开源框架支持了多种IO协议操作媒体封装格式的封装与解封装以及编解码格式编解码器包括硬解和软解。任何软件都可以在FFmpeg的License范围内合理地基于FFmpeg进行开发。FFmpeg有两种开源协议
GPL该协议是具有传染性的如果使用了GPL部分的代码FFmpeg可以配置是否开关这部分代码对应的软件也必须开源否则有法律风险LGPL允许以动态发布的形式使用即将FFmpeg编译为动态库使用但是修改到了FFmpeg部分的代码修改的部分也需要开源一般商业软件都会采用这种方式来进行商业软件的开发。 FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations. 1 FFmpeg工程 本小节简单描述下FFmpeg的工程结构相关的内容以期对FFmpeg工程本身的基本构成有一个基本的认识。
1.1 FFmpeg工程结构 FFmpeg本身的目录结构比较清晰我们从目录名称中基本就能看出该目录下可能包含哪些文件具体用来干什么。
.当前目录下存储的是一些编译和项目相关的配置文件比如MakefileLicense等compat:兼容文件doc:文档以及一些FFmpeg使用的示例如果学习FFmpeg的话强烈建议阅读示例ffbuild:编译相关的一些文件比如依赖选项等等fftools:可以编译成可执行文件的一些工具实现比如ffplay,ffmpeg,ffprobe等工具;libavcodec:编解码核心编解码相关的文件都存放在这里比如h264dec.c等libavdevice:设备相关比如DShow等libavfilter:滤镜特效处理libavformat:IO操作以及封装格式的封装和转封装等处理libavutil:工具库比如一些基本的字符串操作图像操作等libavpostproc:一些效果后处理相关的内容一般通过filter处理libswresample:音频重采样处理libswscale:视频缩放、颜色空间转换以及色调映射等presets:编解码器的配置文件参考FFmpeg-Present-filestests:测试示例tools:一些简单的工具。
2 FFmpeg架构
2.1 FFmpeg的总体架构 FFmpeg各个模块是互相独立的都可以单独使用比如解封装器只用来对媒体进行解封装或者封装拿到编码器的裸流或者编解码器直接对裸流数据进行编解码亦或者使用工具集对已经解码完的数据尽兴处理。 编解码模块支持多种不同编解码器所有的编解码器所使用的参数和当前编解码器相关的Context都是使用AVCodecContext描述。而FFmpeg中每个具体的解码器都有一个静态的AVCodec描述当前解码器如何解码这个是有一套统一的接口来定义的。上层拿到AVCodecContext和AVCodec就可以初始化解码器进行解码了只不过使用FFmpeg提供的解码接口更加方便。FFmpeg并没有硬件解码器归类的AVCodec下面而是在其下层另外规定了一套AVHWAccel通过AVCodec来描述该硬件解码器。 封装和解封装支持多种不同的媒体文件类型FFmpeg中讲一个文件抽象为AVFormatContext而内部分别将输入流和输出流分别抽象为AVInputFormat,AVOutputFormat。AVInputFormat,AVOutputFormat用来描述当前媒体文件的相关参数以及对媒体文件进行封装和解封装而具体的操作通过AVIO来进行。AVIO抽象了具体的文件IO操作类似编解码器每种类型的输入流都有各自的描述封装器和解封装器同理。 工具集也是独立的只是一些工具函数的集合。 滤镜用来对裸数据进行一些特效上的处理。本文不会过多讨论滤镜
2.2 代码结构 FFmpeg虽然是用C语言写的但是其基本的实现思想是按照OOP的思想实现的每个具体的格式都有自己的Context和描述类然后通过函数指针来描述具体实例的实际实现也就是上面描述的Context-Context-Context-....Implementation这种形式为了对当前处理的对象统一抽象就会有一个Context来描述。而每个Context都有一个AVClass和opaue来描述当前结构的参数和独有的一些数据通过这种方式保持了接口的统一的同时又能兼顾差异性。 2.3 调用流程 FFmpeg的核心就是封装/解封装和解码那一套下面的流程图是一个大概有一部分调用被省略了。
3 Gif转码 上面大概描述了下FFmpeg的框架结构和基本的调用流程但是介绍的比较粗糙可能一个具体的例子更容易理解。因此下面会针对GIF图像的转码流程进行比较详细的流程跟踪FFmpeg的详细调用流程以及数据处理。选择GIF的原因是GIF图像的格式和编解码相比其他格式相对比较简单可以让我们更加关注主要的流程而不是具体某个格式的解封装或者解码。当然下面也会涉及的GIF的封装解封装编解码过程因此为了更加流畅的阅读最好提前了解下GIF文件格式和GIF编解码。
3.1 大体流程 总体的调用流程如下一般的转码的基本流程 一个流媒体文件的转码基本上包含了FFmpeg的主要内容从该过程入手我们能清晰的看到FFmpeg内部的实现逻辑。首先有一个流媒体文件比如Mp4,MKV等等我们期望是将其编码封装为另一种格式比如HEVC/MP4等等。 首先是一些环境的准备比如打开媒体文件这个时候FFmpeg会根据文件的流内容探测当前文件可能是什么格式来确定使用哪种解封装器。然后打开解码器和编码器解码器的参数是通过第一步探测到的而编码器的参数需要根据你的需要设置。 文件和解码器已经打开就可以开始解码了。因为流信息是按照帧存储的因此需要不断读取一帧一帧的压缩的流信息送给解码器进行解码。未压缩的数据存储在AVPacket中而解压完的裸数据存储在AVFrame中。拿到裸数据后就可以将该数据发送给编码器进行编码最后送到封装器进行封装存储就得到了一个完整的流媒体文件。
3.2 初始化Context FFmpeg中一个AVFormatContext表示一个媒体文件的抽象AVCodecContextt,AVCodec表示编解码参数和编解码器的抽象因此分别初始过程需要初始化读和写文件的AVFormatContext编码和解码的AVCodecContext以及打开编解码器。
3.2.1 解封装AVFormatContext初始化 解封装的AVFormatContextFFmpeg内部会自动探测不需要我们指定。该初始化过程主要涉及两个对外的APIavformat_open_input,avformat_find_stream_info前者用来打开文件后者用来进行流媒体信息探测。
3.2.1.1 avformat_open_input
avformat_open_input avformat_open_input会打开文件句柄探测当前文件的媒体格式读取基本的流媒体格式信息。 avformat_open_input首先会在堆上分配一个AVFormatContext下面称之为媒体句柄并将用户自定义个一些options拷贝到该Context中。 此时的媒体句柄只是一个带有输入参数和文件路径的空壳需要进一步的确认具体的媒体格式。之后会调用av_probe_input_format2记住这个API这里如果探测失败后续还会继续调用实际上内部调用的是av_probe_input_format3对媒体文件探测检测。探测的方式比较粗暴就是遍历当前FFmpeg支持的所有媒体格式然后调用对应媒体格式的read_probe函数指针拿到一个分值分值最高的那个就是当前媒体文件的格式。此时就会拿到对应文件的AVInputFormat赋值给媒体句柄中的iformat。伪代码如下
int maxscore 0;
AVInputFormat *tmp, *ret;
while(ret in [FFmpeg 支持的格式列表]){int score ret-read_probe();if(score maxscore){tmp ret;maxscore score;}
}return ret;因为上面的probe是第一次调用还没有打开文件IO无法访问文件数据因此大概率失败那为什么还要在打开文件IO前调用因为对于一些设置了AVFMT_NONFILE的输入比如DShow等就不需要打开文件IO进行。 然后就是调用媒体句柄中的io_open函数指针打开流该指针是在创建媒体句柄时设置的默认函数指针io_open_default。打开流是首先需要确认流的类型基本过程和媒体探测流程差不多根据文件名遍历FFmpeg支持的所有流格式拿到当前格式的URLProtocol比如本地文件就是ff_file_protocol确定流类型后就可以调用具体的函数指针url_open打开媒体文件了。对于本地文件的话就是posix那套文件操作比如open,lseek,fstat等之后文件读取也一样。打开文件后的文件句柄并不是URLProtocol的成员而是存储在priv_data中这也是FFmpeg中规避差异化的基本做法。 通过上述的操作我们只是拿到了URLContext还需要拿到AVIOContext。创建AVIOContext的过程比较简单就是堆上申请块儿对应的内存设置必要的参数然后返回。需要注意的是此时会申请一会儿缓冲区存放在VIOContext供后续读写文件使用。 拿到AVIOContext后也就意味着IO已经成功打开如果此时发现媒体句柄中没有iformat就会调用av_probe_input_buffer2再次探测。av_probe_input_buffer2内部会不断读取文件内容然后调用上面提到的APIav_probe_input_format2对文件内容进行探测直到确定媒体文件格式或者达到最大的probesize为止。 GIF的read_probe比较简单就是读取头部的标记确认是否为GIF文件。
static int gif_probe(const AVProbeData *p){/* check magick */if (memcmp(p-buf, gif87a_sig, 6) memcmp(p-buf, gif89a_sig, 6))return 0;/* width or height contains zero? */if (!AV_RL16(p-buf[6]) || !AV_RL16(p-buf[8]))return 0;return AVPROBE_SCORE_MAX;
}到目前为止我们只是打开了IO确认了媒体类型但是媒体的基本信息比如宽高等还不清楚剩下的工作就是调用iformat-read_header读取一些基本的信息写入到媒体句柄中。至此媒体流打开的工作就已经结束了。
gif_read_header 下面通过详细的注释描述读取header的过程
static int gif_read_header(AVFormatContext *s)
{GIFDemuxContext *gdc s-priv_data;AVIOContext *pb s-pb;AVStream *st;int type, width, height, ret, n, flags;int64_t nb_frames 0, duration 0;if ((ret resync(pb)) 0) //跳过开头89a和87a的标识符return ret;gdc-delay gdc-default_delay;width avio_rl16(pb); //gif中宽高存储在开头且分别占2个字节height avio_rl16(pb);flags avio_r8(pb); //读取标志位avio_skip(pb, 1); //背景色索引目前不需要就跳过n avio_r8(pb); //像素比if (width 0 || height 0)return AVERROR_INVALIDDATA;st avformat_new_stream(s, NULL); //动态图一定只有一个视频流这里只需要创建一个即可if (!st) return AVERROR(ENOMEM);if (flags 0x80) //跳过全局颜色表全局颜色表只有在解码时有用avio_skip(pb, 3 * (1 ((flags 0x07) 1)));while ((type avio_r8(pb)) ! GIF_TRAILER) { //每个block都有各自的标识符这里判断是否到达结尾if (avio_feof(pb)) break;if (type GIF_EXTENSION_INTRODUCER) { //0x21int subtype avio_r8(pb);if (subtype GIF_COM_EXT_LABEL) { //Comment ExtensionAVBPrint bp;int block_size;av_bprint_init(bp, 0, AV_BPRINT_SIZE_UNLIMITED);while ((block_size avio_r8(pb)) ! 0) {avio_read_to_bprint(pb, bp, block_size);}av_dict_set(s-metadata, comment, bp.str, 0);av_bprint_finalize(bp, NULL);} else if (subtype GIF_GCE_EXT_LABEL) { //Graphic Control Extension描述每一帧图像的内容int block_size avio_r8(pb);if (block_size 4) {int delay;avio_skip(pb, 1);delay avio_rl16(pb); //求delay总和得到gif的时长if (delay gdc-min_delay)delay gdc-default_delay;delay FFMIN(delay, gdc-max_delay);duration delay;avio_skip(pb, 1);} else {avio_skip(pb, block_size);}gif_skip_subblocks(pb);} else {gif_skip_subblocks(pb);}} else if (type GIF_IMAGE_SEPARATOR) { //Image Descriptor描述当前block的基本宽高等avio_skip(pb, 8);flags avio_r8(pb);if (flags 0x80) //跳过局部颜色表avio_skip(pb, 3 * (1 ((flags 0x07) 1)));avio_skip(pb, 1);gif_skip_subblocks(pb);nb_frames; //统计帧的数量} else {break;}}/* GIF format operates with time in hundredths of second,* therefore timebase is 1/100 */avpriv_set_pts_info(st, 64, 1, 100);st-codecpar-codec_type AVMEDIA_TYPE_VIDEO;st-codecpar-codec_id AV_CODEC_ID_GIF;st-codecpar-width width;st-codecpar-height height;st-start_time 0;st-duration duration;st-nb_frames nb_frames;if (n) {//计算宽高比st-codecpar-sample_aspect_ratio.num n 15;st-codecpar-sample_aspect_ratio.den 64;}/* jump to start because gif decoder needs header data too */if (avio_seek(pb, 0, SEEK_SET) ! 0)return AVERROR(EIO);return 0;
}read_header执行完就拿到了流的基本信息下面最后一步就是校正一些参数然后调用update_stream_avctx将部分参数拷贝给解码器的Context等。
3.2.1.2 avformat_find_stream_info avformat_open_input之后能够拿到基本的流信息但是具体的流信息但是媒体文件Header中存储的数据可能和帧中实际的信息不一致因此需要通过解封装解码获取具体的帧信息来矫正。实际探测信息时回尝试解码一部分帧来获取信息因此avformat_find_stream_info可能比较耗时。 avformat_find_stream_info 通过解码流获取详细的流信息但是到底探测多少内容下面是FFmpeg中决定探测多少流内容的阈值可以看到如果没有设置的话这里会使用一些经验值。阈值是探测流的时长而不是文件大小流越大解码耗时越久就会越慢。 这个函数非常长大概500行在详细了解具体实现前下面是大概调用流程的伪代码
int avformat_find_stream_info(){从已有的AVFormatContext和AVStream中获取和探测流相关的参数for(int i 0 - number of streams){初始化AVCodecParser拷贝参数到AVCodecContextfind_decoder()avcodec_open2()设置一些解码相关的参数}for(;;){for(int i 0 - number of streams){分析出一些探测时长的参数}if(readsize probesize) break;read_frame_internal()avpriv_packet_list_put();if(has_extradata){extract_extradata();}try_decode_frame();}if(flush_codecs) flush_codecs();for(int i 0 - number of streams){计算帧率}for(int i 0 - number of streams){add_coded_side_data();}
}上面是基本的调用流程下面一个一个流程详细说明 探测参数设置 首先便是从已经创建的Context中获取解封装相关的参数比如需要探测的码流时长等等。 int64_t max_analyze_duration ic-max_analyze_duration;max_stream_analyze_duration max_analyze_duration;max_subtitle_analyze_duration max_analyze_duration;if (!max_analyze_duration) {max_stream_analyze_duration max_analyze_duration 5*AV_TIME_BASE;max_subtitle_analyze_duration 30*AV_TIME_BASE;if (!strcmp(ic-iformat-name, flv))max_stream_analyze_duration 90*AV_TIME_BASE;if (!strcmp(ic-iformat-name, mpeg) || !strcmp(ic-iformat-name, mpegts))max_stream_analyze_duration 7*AV_TIME_BASE;}尝试解码 探测流是因为需要部分解码因此FFmpeg需要初始化解码器对一部分帧解码从解码的帧中获取流数据。解码的过程就是FFmpeg的基本流程先准备解码器然后调用avcodec_open2打开解码器。环境准备好之后调用read_frame_internal逐帧读取压缩的数据AVPacket然后调用try_decode_frame送给解码器解码。然后根据当前解码的帧参数更新当前解码器的AVCodecContext以及AVStream中的参数。
3.2.2 封装AVFormatContext初始化 封装时会调用avformat_alloc_output_context2初始化一个用于写文件的AVFormatContext。创建AVFormatContext主要是两部分首先在堆上分配AVFormatContext设置默认的参数然后调用oformat av_guess_format(NULL, filename, NULL);获取写文件的AVOutpuFormat对比输入时需要AVInputFormat。av_guess_format内部通过遍历FFmpeg支持的所有格式的AVOutputFormat对比扩展名得到一个分数取分数最高的格式作为当前文件的格式。 while ((fmt av_muxer_iterate(i))) {score 0;if (fmt-name short_name av_match_name(short_name, fmt-name))score 100;if (fmt-mime_type mime_type !strcmp(fmt-mime_type, mime_type))score 10;if (filename fmt-extensions av_match_ext(filename, fmt-extensions)) {score 5;}if (score score_max) {score_max score;fmt_found fmt;}}3.2.3 打开解码器 打开解码器的基本流程比较简单初始化Paser将流中的参数拷贝给AVCodecContext然后是根据解码器ID查找解码器最后就是直接调用avcodec_open2打开解码器了。 初始化paser就是遍历FFmpeg支持的paser的静态数组找到后存储到FFStream中。GIF的paser就是ff_gif_parser。 搜索解码器会调用avcodec_find_decoder遍历当前FFmpeg中支持的解码器类型直到找到相同解码器ID的AVCodec解码器。 准备好解码器和Context就会打开解码器。打开解码器前为了保证线程安全会锁住解码器锁解码器的锁是一个全局静态锁static AVMutex codec_mutex AV_MUTEX_INITIALIZER。打开解码器具体的内容就是设置解码器相关的参数分配一些解码过程需要用到的内部变量比如AVCodecInternal等。初始化codec时会创建一个AVCodecDescriptorcodec描述这个也是从一个内部的全局表格codec_descriptors中搜索得到的。之后会根据当前codec的类型分别调用ff_encode_preinit和ff_decode_preinit做一些基本的初始化这里面也是对当前codec的一些基本参数设置和一些和codec本身相关的对象的创建。 线程初始化。ff_thread_init用于初始化codec运行时的解码线程内部会创建多个线程的context并初始化初始化最终调用的是pthread_***_init接口进行初始化。解码线程的运行任务为frame_worker_thread。
err init_pthread(fctx, thread_ctx_offsets);
if (err 0) {free_pthread(fctx, thread_ctx_offsets);av_freep(avctx-internal-thread_ctx);return err;
}fctx-async_lock 1;
fctx-delaying 1;if (codec-type AVMEDIA_TYPE_VIDEO)avctx-delay src-thread_count - 1;fctx-threads av_mallocz_array(thread_count, sizeof(PerThreadContext));
if (!fctx-threads) {err AVERROR(ENOMEM);goto error;
}for (; i thread_count; ) {PerThreadContext *p fctx-threads[i];int first !i;err init_thread(p, i, fctx, avctx, src, codec, first);if (err 0)goto error;
}最后调用AVCodec的初始化函数指针初始化解码器完成后解锁。下面是GIF初始化解码器的实现主要就是设置当前解码的参数和分配解码器需要用到的缓存以及打开lzw解码器其实就是分配一个LZWState并且设置参数。
static av_cold int gif_decode_init(AVCodecContext *avctx){GifState *s avctx-priv_data;s-avctx avctx;avctx-pix_fmt AV_PIX_FMT_RGB32;s-frame av_frame_alloc();if (!s-frame)return AVERROR(ENOMEM);ff_lzw_decode_open(s-lzw);if (!s-lzw)return AVERROR(ENOMEM);return 0;
}3.2.4 打开编码器 打开编码器和打开解码器都是调用avcodec_open2基本流程差不多区别是解码器的参数是通过探测得来的而编码器的参数需要用户自己设置。编码器初始化时调用的函数指针为gif_encode_init线程初始化调用的是ff_frame_thread_encoder_init线程运行的任务是worker。 gif_encode_init只是创建内部使用的一些变量并做参数检查。
static av_cold int gif_encode_init(AVCodecContext *avctx){GIFContext *s avctx-priv_data;if (avctx-width 65535 || avctx-height 65535) {av_log(avctx, AV_LOG_ERROR, GIF does not support resolutions above 65535x65535\n);return AVERROR(EINVAL);}s-transparent_index -1;s-lzw av_mallocz(ff_lzw_encode_state_size);s-buf_size avctx-width*avctx-height*2 1000;s-buf av_malloc(s-buf_size);s-tmpl av_malloc(avctx-width);if (!s-tmpl || !s-buf || !s-lzw)return AVERROR(ENOMEM);if (avpriv_set_systematic_pal2(s-palette, avctx-pix_fmt) 0)av_assert0(avctx-pix_fmt AV_PIX_FMT_PAL8);return 0;
}3.2 解封装 av_read_frame用于从已经打开的文件中读取未经过解码的码流AVPacket对于视频帧就是一帧的压缩帧对于音频帧如果音频是固定大小的话则可以是多帧否则也是一帧。av_read_frame内部读取码流时调用avpriv_packet_list_get和av_read_frame_internal。 avpriv_packet_list_get比较简单就是从当前媒体的PackList中取出一帧。av_read_frame的函数实现比较长其大致流程为
调用ff_read_packet读取一帧码流如果1步骤失败则调用parse_packet刷新解析器否则继续到步骤3如果当前context需要更新解码器context则将internal的解码器context更新到stream的解码器context如果成功拿到预期的帧则下一步否则跳转到步骤1后续的工作就是解析元数据计算需要丢弃的数据大小等。 ff_read_packet会先检查缓冲区是否有帧没有的话就会调用s-iformat-read_packet即对应个是的解析码流的函数进行解码。 GIF图解封装就是调用的gif_read_packet解封装首先就是跳过图像中的头信息比如Image Descriptor等。然后不断遍历内部的流寻找一帧图像的Block找到后根据当前Block的size读取数据组装一个AVPacket设置AVPacket的参数然后更新GIFDemuxContext中存储的当前解封装读取到的位置dt 等参数返回帧。
3.3 解码 avcodec_send_packet首先是检查解码器的合法性以及数据是否为空如果输入数据和Context符合要求就会删除AVcodecContext-internal-buffer_pkt中缓存的一帧码流数据将输入的Packet拷贝到该buffer上。av_bsf_send_packet只是拷贝增加输入的Packet引用计数到AVBSFInternal-buffer_pkt最后如果缓存的buffer_frame是空的就会调用decode_receive_frame_internal解码帧该过程根据配置项可谓同步也可为异步。
3.3.1 decode_receive_frame_internal decode_receive_frame_internal内就是真正的调用解码流程如果解码器的receive_frame函数指针不为空就直接调用解码器的receive_frame进行解码该过程是同步的。否则就会调用decode_simple_receive_frame进行解码。解码完成后需要根据解码的数据和当前解码器Context的一些pts相关的值计算当前帧的具体pts和dts另外如果有指定FrameDecodeData还会调用后处理流程fdd-post_process进行解码。
3.3.2 decode_simple_receive_frame decode_simple_receive_frame主要是调用decode_simple_internal进行解码。这里使用的Packet就是前面存储在AVBSFInternal中的buffer_pkt。然后就是实际调用解码的流程如果没有配置解码线程就直接调用每个解码器对应的函数指针的avctx-codec-decode直接同步拿到帧。否则就会调用ff_thread_decode_frame进行多线程解码。 FFmpeg中每种格式解码器等都有自己的描述结构比如下面是gif的解码器描述。
static const AVClass decoder_class {.class_name gif decoder,.item_name av_default_item_name,.option options,.version LIBAVUTIL_VERSION_INT,.category AV_CLASS_CATEGORY_DECODER,
};const AVCodec ff_gif_decoder {.name gif,.long_name NULL_IF_CONFIG_SMALL(GIF (Graphics Interchange Format)),.type AVMEDIA_TYPE_VIDEO,.id AV_CODEC_ID_GIF,.priv_data_size sizeof(GifState),.init gif_decode_init,.close gif_decode_close,.decode gif_decode_frame,.capabilities AV_CODEC_CAP_DR1,.caps_internal FF_CODEC_CAP_INIT_THREADSAFE |FF_CODEC_CAP_INIT_CLEANUP,.priv_class decoder_class,
};ff_thread_decode_frame内都是通过锁和条件变量进行同步的。首先根据当前的状态获取一个解码线程的Context然后将当前的Packet提交到该线程上提交就是将一帧数据增加引用让解码Context的avpkt也占用输入帧的引用计数提交完成就会发送信号通知在等待的解码线程启动。 解码线程起始在avcodec_open2的时候就已经创建好了在wait数据。具体的执行函数就是frame_worker_thread该函数内就是调用codec-decode进行解码解码完成后就会发送通知到ff_thread_decode_frame中取解码完的帧。令条件if (!p-avctx-thread_safe_callbacks ( p-avctx-get_format ! avcodec_default_get_format || p-avctx-get_buffer2 ! avcodec_default_get_buffer2))为A如果A为true则当前线程是会被阻塞的完全就是同步运行否则就是多线程的。
if (!p-avctx-thread_safe_callbacks (p-avctx-get_format ! avcodec_default_get_format ||p-avctx-get_buffer2 ! avcodec_default_get_buffer2)) {while (atomic_load(p-state) ! STATE_SETUP_FINISHED atomic_load(p-state) ! STATE_INPUT_READY) {int call_done 1;pthread_mutex_lock(p-progress_mutex);while (atomic_load(p-state) STATE_SETTING_UP)pthread_cond_wait(p-progress_cond, p-progress_mutex);switch (atomic_load_explicit(p-state, memory_order_acquire)) {case STATE_GET_BUFFER:p-result ff_get_buffer(p-avctx, p-requested_frame, p-requested_flags);break;case STATE_GET_FORMAT:p-result_format ff_get_format(p-avctx, p-available_formats);break;default:call_done 0;break;}if (call_done) {atomic_store(p-state, STATE_SETTING_UP);pthread_cond_signal(p-progress_cond);}pthread_mutex_unlock(p-progress_mutex);}}3.3.2 avcodec_receive_frame avcodec_receive_frame比较简单先检查buffer_frame有没有数据有的话就直接返回没有即调用decode_receive_frame_internal进行解码。
3.3.3 gif_decode_frame gif_decode_frame中会将码流送给解码器进行解码然后将得到的数据填充到AVFrame返回给上层。代码中前面一大段都是读取当前Block的图像信息比如Image Header这些实际进行解码的是gif_parse_next_image其内部会根据当前block的类型调用具体的解码函数比如图像就是调用gif_read_image进行解码。
static int gif_parse_next_image(GifState *s, AVFrame *frame){while (bytestream2_get_bytes_left(s-gb) 0) {int code bytestream2_get_byte(s-gb);int ret;av_log(s-avctx, AV_LOG_DEBUG, code%02x %c\n, code, code);switch (code) {case GIF_IMAGE_SEPARATOR:return gif_read_image(s, frame);case GIF_EXTENSION_INTRODUCER:if ((ret gif_read_extension(s)) 0)return ret;break;case GIF_TRAILER:/* end of image */return AVERROR_EOF;default:/* erroneous block label */return AVERROR_INVALIDDATA;}}return AVERROR_EOF;
}gif_read_image解码过程中首先就是解析当前帧的局部颜色表以及GIF的存储模式如果没有的话就使用全局的颜色表。参数解析完后直接调用ff_lzw_decode解码读取到的LZW编码流最后将索引映射根据颜色表映射会帧图像。
3.4 编码 avcodec_send_frame用于在编码时将一帧raw数据发送给编码器其基本的调用流程比较简单主要工作就是将输入的数据ref到Internal Frame上。 avcodec_send_frame首先检查当前的codec是不是编码器且是否打开并且检查codec中的buffer是否有数据没有有的话就意味着上一帧的数据还没处理完需要等待这一帧处理完才能继续发送。 if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx-codec))return AVERROR(EINVAL);if (avci-draining)return AVERROR_EOF;if (avci-buffer_frame-data[0])return AVERROR(EAGAIN);然后是根据输入的frame是否为空来设置标志位如果为空就表示是最后一帧数据后续的数据就无效了。能够看到在最后如果codec中的packet buffer是空的就会尝试获取一帧packet。 if (!frame) {avci-draining 1;} else {ret encode_send_frame_internal(avctx, frame);if (ret 0)return ret;}if (!avci-buffer_pkt-data !avci-buffer_pkt-side_data) {ret encode_receive_packet_internal(avctx, avci-buffer_pkt);if (ret 0 ret ! AVERROR(EAGAIN) ret ! AVERROR_EOF)return ret;}encode_send_frame_internal比较简单主要就是针对音频数据进行参数检查并对数据进行填充最后调用av_frame_ref将输入的数据的引用计数1、
3.4.1 avcodec_receive_packet
3.4.1.1 基本流程 首先是检查当前codec是否为编码器并且是否打开如果是就继续。然后检查codec中的packet buffer是否有数据有的话就直接返回了不然就会调用encode_receive_packet_internal。
int attribute_align_arg avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt){AVCodecInternal *avci avctx-internal;int ret;av_packet_unref(avpkt);if (!avcodec_is_open(avctx) || !av_codec_is_encoder(avctx-codec))return AVERROR(EINVAL);if (avci-buffer_pkt-data || avci-buffer_pkt-side_data) {av_packet_move_ref(avpkt, avci-buffer_pkt);} else {ret encode_receive_packet_internal(avctx, avpkt);if (ret 0)return ret;}return 0;
}encode_receive_packet_internal首先就是参数检查然后根据codec的函数指针设置看调用哪个流程获取编码流。encode_simple_receive_packet就是个while循环调用encode_simple_internal直到获取编码数据或者出错为止。 if (avctx-codec-receive_packet) {ret avctx-codec-receive_packet(avctx, avpkt);if (ret 0)av_packet_unref(avpkt);else// Encoders must always return ref-counted buffers.// Side-data only packets have no data and can be not ref-counted.av_assert0(!avpkt-data || avpkt-buf);} elseret encode_simple_receive_packet(avctx, avpkt);encode_simple_internal除了前面一大坨参数检查主要救赎下面这块儿看是利用多线程编码还是利用codec的encode接口编码。 if (CONFIG_FRAME_THREAD_ENCODER avci-frame_thread_encoder (avctx-active_thread_type FF_THREAD_FRAME))/* This might modify frame, but it doesnt matter, because* the frame properties used below are not used for video* (due to the delay inherent in frame threaded encoding, it makes* no sense to use the properties of the current frame anyway). */ret ff_thread_video_encode_frame(avctx, avpkt, frame, got_packet);else {ret avctx-codec-encode2(avctx, avpkt, frame, got_packet);if (avctx-codec-type AVMEDIA_TYPE_VIDEO !ret got_packet !(avctx-codec-capabilities AV_CODEC_CAP_DELAY))avpkt-pts avpkt-dts frame-pts;}3.4.1.2 多线程 编码的线程和解码的线程一样都是在avcodec_open2时创建的编码是调用ff_frame_thread_encoder_init创建的其中主要就是调用pthread的接口创建线程和相关的参数可以看到其工作的函数为static void * attribute_align_arg worker(void *v)编码过程中有多个线程每个线程都运行一个worker任务通过信号量来进行消息的同步。该任务中最终会调用avctx-codec-encode2对数据进行编码。而所有的数据交互都是通过ThreadContext进行的无论是输入数还是输出的数据还是消息同步都是通过该Context进行的。
typedef struct{AVCodecContext *parent_avctx;pthread_mutex_t buffer_mutex;pthread_mutex_t task_fifo_mutex; /* Used to guard (next_)task_index */pthread_cond_t task_fifo_cond;unsigned max_tasks;Task tasks[BUFFER_SIZE];pthread_mutex_t finished_task_mutex; /* Guards tasks[i].finished */pthread_cond_t finished_task_cond;unsigned next_task_index;unsigned task_index;unsigned finished_task_index;pthread_t worker[MAX_THREADS];atomic_int exit;
} ThreadContext;当数据到达时主线程会先拷贝数据然后发送信号量signal给任务线程任务线程拿到消息后编码完成后给主线程发信号finish主线程取走数据。
3.4.2 gif_encode_frame GIF编码调用的是gif_encode_frame内部实际调用的gif_image_write_image。GIF编码和解码的流程基本相反除了写Block的流信息外先将当前图像的颜色映射根据颜色表映射到具体的索引然后调用ff_lzw_encode对流进行编码。 for (y 0; y height; y) {memcpy(s-tmpl, ptr, width);for (x 0; x width; x)if (ref[x] ptr[x])s-tmpl[x] trans;len ff_lzw_encode(s-lzw, s-tmpl, width);ptr linesize;ref ref_linesize;3.5 封装
3.5.1 avformat_write_header avformat_write_header比较简单直接调用的对应格式的write_header的函数指针。GIF的write_header做的事情比较少。
static int gif_write_header(AVFormatContext *s){if (s-nb_streams ! 1 ||s-streams[0]-codecpar-codec_type ! AVMEDIA_TYPE_VIDEO ||s-streams[0]-codecpar-codec_id ! AV_CODEC_ID_GIF) {av_log(s, AV_LOG_ERROR,GIF muxer supports only a single video GIF stream.\n);return AVERROR(EINVAL);}avpriv_set_pts_info(s-streams[0], 64, 1, 100);return 0;
}3.5.2 av_interleaved_write_frame 首先就是根据输入数据是否为空选择调用的函数如果为空就会调用interleaved_write_packet刷新数据否则调用write_packets_common写数据。 write_packets_common中,check_packet检查输入的数据和期望写入的媒体流是否能够对上。prepare_input_packet对输入数据进行修正如果pts和dts其中之一为NOPTS则设置为对方的值以及如果设置了is_intra_only则每一帧都会设置标志位AV_PKT_FLAG_KEY。而check_bitstream就是调用s-oformat-check_bitstream检查流是否符合对应的格式。最后才是调用write_packet_common进行写数据。如果有设置filter的话就调用write_packets_from_bsfs处理。 write_packet_common会根据输入的参数是否需要交织存储来调用具体的函数写packet。非交织的情况下就会调用write_packet该函数内部实际调用的s-oformat-write_packet和s-oformat-write_uncoded_frame写文件后者处理裸流。 interleaved_write_packet内如果AVOuputFormat设置了对应的函数指针则直接调用s-oformat-interleave_packet写文件否则就用FFmpeg提供的ff_interleave_packet_per_dts。我们重点看下这个函数实现。
3.5.2.1 ff_interleave_packet_per_dts ff_interleave_packet_per_dts只是针对当前的两个流的packet的时间戳进行比较避免在文件存储过程中距离太远导致解封转时要频繁seek文件。最终封装文件写入到磁盘还是需要write_packet。该函数首先将送入的pkt插入到缓存队列中然后在从当前缓存队列中选出一帧返回调用write_packet进行写入。 在看ff_interleave_add_packet函数的实现之前我们先简单看下帧比较函数interleave_compare_dts的实现该函数用来比较两个packet的dts。如果非音频流就是调用的av_compare_ts进行比较否则会根据当前音频流是否有preload去除preload的偏移
int preload st -codecpar-codec_type AVMEDIA_TYPE_AUDIO;
int preload2 st2-codecpar-codec_type AVMEDIA_TYPE_AUDIO;
if (preload ! preload2) {int64_t ts, ts2;preload * s-audio_preload;preload2 * s-audio_preload;//preload不同时需要减掉preload的偏移ts av_rescale_q(pkt -dts, st -time_base, AV_TIME_BASE_Q) - preload;ts2 av_rescale_q(next-dts, st2-time_base, AV_TIME_BASE_Q) - preload2;if (ts ts2) {ts ((uint64_t)pkt -dts*st -time_base.num*AV_TIME_BASE - (uint64_t)preload *st -time_base.den)*st2-time_base.den- ((uint64_t)next-dts*st2-time_base.num*AV_TIME_BASE - (uint64_t)preload2*st2-time_base.den)*st -time_base.den;ts2 0;}comp (ts2 ts) - (ts2 ts);
}重点就是下面的代码从当前buffer中找到当前帧的插入位置然后插入到packet的链表中。
if (st-internal-last_in_packet_buffer) {next_point (st-internal-last_in_packet_buffer-next);
} else {next_point s-internal-packet_buffer;
}
//省略部分代码.......
if (*next_point) {if (chunked !(pkt-flags CHUNK_START))goto next_non_null;if (compare(s, s-internal-packet_buffer_end-pkt, pkt)) {while ( *next_point ((chunked !((*next_point)-pkt.flagsCHUNK_START))|| !compare(s, (*next_point)-pkt, pkt)))next_point (*next_point)-next;if (*next_point)goto next_non_null;} else {next_point (s-internal-packet_buffer_end-next);}
}插入成功后回到ff_interleave_packet_per_dts中从当前的packet链表的头结点拿到一阵返回给write_packet写入。
3.5.2.2 gif_write_packet gif_write_packet比较简单就是根据当前的流信息写GCE等基本的BLOCK信息。 /* NETSCAPE EXTENSION for looped animation GIF */if (gif-loop 0) {avio_w8(pb, GIF_EXTENSION_INTRODUCER); /* GIF Extension code */avio_w8(pb, GIF_APP_EXT_LABEL); /* Application Extension Label */avio_w8(pb, 0x0b); /* Length of Application Block */avio_write(pb, NETSCAPE2.0, sizeof(NETSCAPE2.0) - 1);avio_w8(pb, 0x03); /* Length of Data Sub-Block */avio_w8(pb, 0x01);avio_wl16(pb, (uint16_t)gif-loop);avio_w8(pb, 0x00); /* Data Sub-block Terminator */}delay_pos gif_parse_packet(s, pkt-data off, pkt-size - off);if (delay_pos 0 delay_pos pkt-size - off - 2) {avio_write(pb, pkt-data off, delay_pos);avio_wl16(pb, gif_get_delay(gif, pkt, new_pkt));avio_write(pb, pkt-data off delay_pos 2, pkt-size - off - delay_pos - 2);} else {avio_write(pb, pkt-data off, pkt-size - off);}3.5.3 av_write_trailer av_write_trailer就做了两件是刷新缓冲区和写尾。GIF写尾调用的static int gif_write_trailer(AVFormatContext *s)。
static int gif_write_trailer(AVFormatContext *s){GIFContext *gif s-priv_data;AVIOContext *pb s-pb;if (!gif-prev_pkt)return AVERROR(EINVAL);gif_write_packet(s, NULL);if (!gif-have_end)avio_w8(pb, GIF_TRAILER);av_packet_free(gif-prev_pkt);return 0;
}3.6 销毁 完成任务后调用各自的现场清理函数。