FFmpeg5.0源码阅读——FFmpeg大体框架(以GIF转码为示例)

  摘要:前一段时间熟悉了下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本身的目录结构比较清晰,我们从目录名称中基本就能看出该目录下可能包含哪些文件具体用来干什么。

  • .:当前目录下存储的是一些编译和项目相关的配置文件,比如Makefile,License等;
  • 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-files
  • tests:测试示例;
  • tools:一些简单的工具。

2 FFmpeg架构

2.1 FFmpeg的总体架构

在这里插入图片描述

  FFmpeg各个模块是互相独立的,都可以单独使用,比如解封装器只用来对媒体进行解封装或者封装拿到编码器的裸流,或者编解码器直接对裸流数据进行编解码,亦或者使用工具集对已经解码完的数据尽兴处理。
  编解码模块支持多种不同编解码器,所有的编解码器所使用的参数和当前编解码器相关的Context都是使用AVCodecContext描述。而FFmpeg中每个具体的解码器都有一个静态的AVCodec描述当前解码器如何解码,这个是有一套统一的接口来定义的。上层拿到AVCodecContextAVCodec就可以初始化解码器进行解码了,只不过使用FFmpeg提供的解码接口更加方便。FFmpeg并没有硬件解码器归类的AVCodec下面,而是在其下层另外规定了一套AVHWAccel,通过AVCodec来描述该硬件解码器。
  封装和解封装支持多种不同的媒体文件类型,FFmpeg中讲一个文件抽象为AVFormatContext,而内部分别将输入流和输出流分别抽象为AVInputFormat,AVOutputFormatAVInputFormat,AVOutputFormat用来描述当前媒体文件的相关参数以及对媒体文件进行封装和解封装,而具体的操作通过AVIO来进行。AVIO抽象了具体的文件IO操作,类似编解码器每种类型的输入流都有各自的描述,封装器和解封装器同理。
  工具集也是独立的,只是一些工具函数的集合。
  滤镜用来对裸数据进行一些特效上的处理。(本文不会过多讨论滤镜)

2.2 代码结构

  FFmpeg虽然是用C语言写的但是其基本的实现思想是按照OOP的思想实现的,每个具体的格式都有自己的Context和描述类然后通过函数指针来描述具体实例的实际实现,也就是上面描述的Context->Context->Context->....>Implementation这种形式,为了对当前处理的对象统一抽象就会有一个Context来描述。而每个Context都有一个AVClassopaue来描述当前结构的参数和独有的一些数据,通过这种方式保持了接口的统一的同时,又能兼顾差异性。

在这里插入图片描述

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内部会自动探测,不需要我们指定。该初始化过程主要涉及两个对外的API:avformat_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_preinitff_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_getav_read_frame_internal
  avpriv_packet_list_get比较简单就是从当前媒体的PackList中取出一帧。av_read_frame的函数实现比较长,其大致流程为:

  1. 调用ff_read_packet读取一帧码流;
  2. 如果1步骤失败则调用parse_packet刷新解析器,否则继续到步骤3;
  3. 如果当前context需要更新解码器context,则将internal的解码器context更新到stream的解码器context;
  4. 如果成功拿到预期的帧则下一步,否则跳转到步骤1;
  5. 后续的工作就是解析元数据,计算需要丢弃的数据大小等。

  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 doesn't 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_packets->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.flags&CHUNK_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 销毁

  完成任务后调用各自的现场清理函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/63717.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于负载均衡的在线OJ实战项目

前言&#xff1a; 该篇讲述了实现基于负载均衡式的在线oj&#xff0c;即类似在线编程做题网站一样&#xff0c;文章尽可能详细讲述细节即实现&#xff0c;便于大家了解学习。 文章将采用单篇不分段形式&#xff08;ps&#xff1a;切着麻烦&#xff09;&#xff0c;附图文&#…

javacv 基础04-读取mp4,avi等视频文件并截图保存图片到本地

javacv 读取mp4,avi等视频文件并截图保存图片到本地 代码如下&#xff1a; package com.example.javacvstudy;import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter;import javax.imageio.Im…

wangluobiancheng

UDP send: receive: TCP

朝夕光年游戏自动化测试实践

朝夕光年是面向全球用户与开发者的游戏研发与发行业务品牌&#xff0c;致力于服务全球玩家&#xff0c;帮助玩家在令人惊叹的虚拟世界中一起玩耍与创造。 在游戏的研发过程中&#xff0c;游戏自动化一直是开展难度较大的工程&#xff0c;具体包括机房机架、设备调度、软件框架、…

理解底层— —Golang的log库,二开实现自定义Logger

理解底层— —Golang的log库&#xff0c;实现自定义Logger 1 分析实现思路 基于golang中自带的log库实现&#xff1a;对日志实现设置日志级别&#xff0c;每天生成一个文件&#xff0c;同时添加上前缀以及展示文件名等 日志级别&#xff0c;通过添加prefix&#xff1a;[INFO]、…

学生信息管理系统MIS(前端)

改造HTML文件 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>学生信息管理系统MIS</title><!-- link在HTML文件中,引入外部的css文件 rel的值是固定写法,stylesheet样式表href用来指定样式表的位置--><lin…

【LeetCode】剑指 Offer <二刷>(4)

目录 题目&#xff1a;剑指 Offer 09. 用两个栈实现队列 - 力扣&#xff08;LeetCode&#xff09; 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;剑指 Offer 10- I. 斐波那契数列 - 力扣&am…

MySQL 5种索引应用

文章目录 简介一、聚集索引二、唯一索引三、聚集索引和唯一索引对比四、非唯一&#xff08;普通&#xff09;索引五、全文索引六、组合索引七、索引验证总结 简介 在本篇文章中&#xff0c;我们将学习MySQL中5种不同类型的索引及其应用场景&#xff0c;以及它们的优缺点。 一…

WIFI与BT的PCB布局布线注意事项

1、模块整体布局时&#xff0c;WIFI模组要尽量远离DDR、HDMI、USB、LCD电路以及喇叭等易干扰模块或连接座&#xff1b; 2、晶体电路布局需要优先考虑&#xff0c;布局时应与芯片在同一层并尽量靠近放置以避免打过孔&#xff0c;晶体走线尽可能的短&#xff0c;远离干扰源&…

【MetaAI】2023年MetaAI发布的开源模型和工具

MetaAI开源模型和工具 MetaAILlamaSegment AnythingDINOv2ImageBindMMSLimaVoiceboxMusicGenLlama 2AudioCraftSeamlessM4T MetaAI Meta 首席执行官扎克伯格表示&#xff0c;与其他研究者分享 Meta 公司开发的模型可以帮助该公司促进创新、发现安全漏洞和降低成本。他今年 4 月…

概念解析 | 量子机器学习:将量子力学与人工智能的奇妙融合

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:量子机器学习。 量子机器学习:将量子力学与人工智能的奇妙融合 量子增强机器学习:量子经典混合卷积神经网络 量子机器学习是量子计算和机器学习的结合,它利用量子力学的特…

Opencv-C++笔记 (18) : 轮廓和凸包

文章目录 一、轮廓findContours发现轮廓drawContours绘制轮廓代码 二.几何及特性概括——凸包(Convex Hull)凸包概念凸包扫描算法介绍——Graham扫描算法 相关API介绍程序示例轮廓集合及特性性概括——轮廓周围绘制矩形框和圆形相关理论介绍轮廓周围绘制矩形 -API绘制步骤程序实…

Python数据分析案例30——中国高票房电影分析(爬虫获取数据及分析可视化全流程)

案例背景 最近总看到《消失的她》票房多少多少&#xff0c;《孤注一掷》票房又破了多少多少..... 于是我就想自己爬虫一下获取中国高票房的电影数据&#xff0c;然后分析一下。 数据来源于淘票票&#xff1a;影片总票房排行榜 (maoyan.com) 爬它就行。 代码实现 首先爬虫获…

<AMBA总线篇> AXI总线协议介绍

目录 01 AXI协议简介 AXI协议特性 AXI协议传输特性 02 AXI协议架构 AXI协议架构 write transaction(写传输) read tramsaction(读传输) Interface and interconnect 典型的AXI系统拓扑 03 文章总结 大家好&#xff0c;这里是程序员杰克。一名平平无奇的嵌入式软件工程…

Python 接口测试之Excel表格数据操作方法封装

引言 我们在做接口测试&#xff0c;经常会用到excel去管理测试数据&#xff0c;对Excel的操作比较频繁&#xff0c;那么使用python如何操作Excel文件的读与写呢&#xff1f;由于之前讲的都是大的框框&#xff0c;没有讲这么小的模块使用&#xff0c;现在就化整为0的讲解。 读…

基于OpenCV+LPR模型端对端智能车牌识别——深度学习和目标检测算法应用(含Python+Andriod全部工程源码)+CCPD数据集

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境OpenCV环境Android环境1. 开发软件和开发包2. JDK设置3. NDK设置 模块实现1. 数据预处理2. 模型训练1&#xff09;训练级联分类器2&#xff09;训练无分割车牌字符识别模型 3. APP构建1&#xff09;导入OpenCV库…

数据结构-第一期——数组(Python)

目录 00、前言&#xff1a; 01、一维数组 一维数组的定义和初始化 一维变长数组 一维正向遍历 一维反向遍历 一维数组的区间操作 竞赛小技巧&#xff1a;不用从a[0]开始&#xff0c;从a[1]开始 蓝桥杯真题练习1 读入一维数组 例题一 例题二​ 例题三 实战训…

在iPhone 15发布之前,iPhone在智能手机出货量上占据主导地位,这对安卓来说是个坏消息

可以说这是一记重拳&#xff0c;但似乎没有一个有价值的竞争者能与苹果今年迄今为止的智能手机出货量相媲美。 事实上&#xff0c;根据Omdia智能手机型号市场跟踪机构收集的数据&#xff0c;苹果的iPhone占据了前四名。位居榜首的是iPhone 14 Pro Max&#xff0c;2023年上半年…

详细教程:Stegsolve的下载,jdk的下载、安装以及环境的配置

最近在学习隐写术&#xff0c;下载stegsolve 以及使用stegsolve倒腾了很久&#xff0c;避免朋友们和我一样倒腾了很久&#xff0c;希望此文可以帮到刚在学习隐写的朋友们(win7下使用stegsolve) 文章目录 一、下载stegsolve链接二、jdk的下载三、jdk的安装四、配置环境变量五、检…

Redis——》Pipeline

推荐链接&#xff1a; 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…