跳至主要内容

FFmpeg 核心概念与 `0_hello_world.c` 示例深度解析 系列一

· 閱讀時間約 21 分鐘
w0x7ce
MySelf

发布于 2025-05-17 12:42:42(微信公众号导出记录)。

本文来自公众号后台的“导出文章内容”功能。博客正文由导出长图进行本地 OCR 转写,并保留原始排版图用于逐段核对。

原文链接:查看原文

OCR 转写有效文字约 14026 字;代码、流程图和版式以文末原始排版图为准。

正文(本地 OCR 转写)

original we×7ce EI V1ajero 2825年5月17日 12:43 中国垂港 FFmpeg是一个强大的开源多媒体处理工具集,其核心库(libav)允许开发者对音 视却数据进行底层的编解码、格式转换、滤镜处理等操作。本文将通过分 leandromoreira/ffmpeg-libav-tutorial仓库 析 中 的0_hello_world.c示例,深入探讨FFmpeg的核心概念和基本工作流程。 0_hello_world.c的目标是:打开一个煤体文件,解析其结构,解码视频流中的最 初若干数据包,并将解码后的帧保存为PGM(便携式灰度图)图像文件。

一、视频压缩与献类型的核心理论

视频压缩技术旨在减少表示视频内容所需的数据量,其基础在于利用视频数据中存在的 究余信息。 .1.视频数据的冗余性: 。·时间冗余(TemporalRedundancy):视频由连续的图像序列构成,相邻帧 之间通常内容高度相似。 。·空间余(SpatialRedundancy):单顿图像内部,相邻像素也常具有相 关性。 .2.帧类型与压缩: 为了高效去除时间余,视频编码标准(如H.264,H.265)引I入了不同类型的 赖:

  • IDR-帧(Instantaneous Decoder Refresh/即时解码器刷新

帧):IDR帧是一种特殊的I-帧。当解码器遇到IDR帧时,它会清空所有参考帧缓 冲区,意味着IDR帧之后的所有帧都不能参考IDR帧之前的任何赖。IDR帧提供了

一个绝对的解码刷新点。

  • SP-帧(SwitchingP-frames)和SI-帧(SwitchingI-

frames):用于在不同码率或编码参数的视频流之间平滑切换,常见于自适应流 煤体(ABR)技术。 。·视频编码时,帧通常被组织成一个个GOP。一个GOP以一个I-帧开始,后面跟着 若干P-帧和B-帧(例如IBBPBBPBBPBB)。GOP的长度和结构(如B赖数量)会 影哺视频的压缩率、随机访问性能和错误恢复能力。 。·工作原理:利用更充分的时间瓦余,可以同时参考其前面的一个I/P帧和其后 面的一个I/P帧(未来的参考帧)进行双向预测。编码时记录当前帧与这两个参考 帧的差异及相应的运动矢量。 。·特性:解码时需要其前后两个参考帧均已被解码:通常是三种帧中数据量最 小的,压缩效率最高:由于依赖未来的帧,导致视频码流中帧的解码顺序(DTS

  • Decode Time Stamp) 和显示顺序 (PTS·Presentation Time

Stamp)往往不一致,解码器需要进行帧重排:会增加解码器的解码延退。 。·必要性:在对延迟要求不高的场景下(如视频点播、文件存储),可以最大限 度地提高压缩率。 。·工作原理:利用时间瓦余,基于其前面的一个I-帧或P-帧(称为参考帧)进 行预测编码。编码时,主要记录当前帧与参考帧之间的差异信息(残差)以及描述 这种差异的运动矢量。 。·特性:解码时依赖其前面的参考帧:数据量通常远小于I-帧,压缩效率较高。 。·必要性:大幅提高视频的压缩率。 。·工作原理:独立编码,不依赖其他任何帧,仅利用当前帧内部的空间瓦余进行 压缩,类似于一张JPEG图片。 。·特性:可独立解码,是解码的起始点:作为随机访问点(用于播放器的跳转操 作):是后续P-帧和B-帧进行预测的参考基础:通常数据量在三种帧类型中最 大;具有一定的错误恢复能力,解码错误通常不会传播过一个I-帧。 。·必要性:提供解码入口、实现seek功能、同期性剧新图像以抑制误差累积。 。·I-帧(Intra-codedPicture/帧内编码帧)·关键帧 。·P-帧(Predicted Picture/向前预测编码帧) 。·B-帧(Bi-directionally Predicted Picture/双向预测编码帧)

  • 图像组(GOP-Group of Pictures):

。·其他帧类型简介:

二、 FFmpeg 核心组件回原

在深入代码之前,再次明确FFmpeg中几个核心结构体的作用:

  • ·AVFormatContext:代表整个媒体文件(容器)的上下文,用于解封装(读取

文件头、分离流等)。

  • ·AVStream:代表容器中的一个具体的数据流(如视频流、音频流、字幕流)。

  • ·AVCodecParameters:描述一个流的编码参数,例如编解码器类型、分辨率、

帧率、像素格式、采样率、声道数等。

  • ·AVCodec:代表一个具体的编解码器实现(例如H.264解码器、AAC解码器)。

  • ·AVCodecContext:编解码操作的上下文,包含了编解码器实例、运行时参数和

状态信息

  • ·AVPacket:存储从流中读取出来的、已编码(压缩)的数据块,是解码器的输

  • ·AVFrame:存储解码后的、原始的(未压缩)的视频帧(包含像素数据)或音频

采样(包含PCM数据),是解码器的输出。 核心解码流程: .1.使用 avcodec_send_packet(avctx,pkt)将 AVPacket(压缩数据) 发送给解码器上下文avctx .2.在一个循环中调用 avcodec_receive_frame(avctx,frame)来尝试从 解码器获取解码后的AVFrame(原始数据)。此函数可能返回: 。·日:成功获取一帧。 。·AVERROR(EAGAIN):解码器需要更多的输人包才能输出当前帧,或者当前没 有立即可用的赖。 。·AVERROR_EOF:输入流已结束(通过发送NULL包 给avcodec_send_packet来指示),且解码器内部所有缓冲的帧都已成功取 出。

三、o_hello_world.c 代码分析

以下是对θ_hello_world.c核心逻辑的分析: /*

  • Format (Container) - a wrapper, providing sync, metadata and

  • Stream - a continuous stream (audio or video) of data over t

  • and DECoded (from Packet to Frame).

  • Packet · are the data (kind of slices of the stream data) to

  • Frame - a decoded raw frame (to be encoded or filtered).

#include<libavcodec/avcodec.h> #include<stdarg.h> #include<stdlib.h> #include<inttypes.h> //辅助函数:打印日志信息 staticvoidlogging(constchar *fmt, ...); //辅助函数:解码数据包到帧 staticintdecode_packet(AvPacket *pPacket, AVCodecContext *pCode //辅助函数:将灰度帧保存为PGM文件 staticvoidsave_gray_frame(unsignedchar *buf, int wrap, int xsiz intmain(int argc, constchar *argv[]) //1.检查命令行参数,确保指定了媒体文件 if (argc < 2){ printf(*You need to specify a media file.\n"); return-1; logging(*initializing all the containers, codecs and protocol //FFmpeg 在早期版本需要调用av_register_all()来注册所有组件。 //现代版本中,很多组件是自动注册的。 //2.分配AVFormatContext 结构体内存 //AVFormatContext用于存储媒体文件的格式信息(容器信息) AVFormatContext *pFormatContext = avformat_alloc_context(); if (!pFormatContext){ logging(“ERRoR could not allocate memory for Format Context return-1; //3.打开输入文件并读取文件头 //argv[1]是命令行传入的媒体文件名 //第三个参数 AVInputFormat 传NULL,FFmpeg会自动检测输入格式 //第四个参数AVDictionary传NULL,表示没有给解封装器传递额外选项 logging(*opening the input file (%s) and loading format (cont if (avformat_open_input(&pFormatContext, argv[1], NULL, NULL) ! logging(“ERROR could not open the file"); avformat_free_context(pFormatContext):// 打开失败,释放已分配8 return-1; //4.打印容器的基本信息 //pFormatContext->iformat->name:输入格式的名称(如“mov,mp4,m4a, //pFormatContext->duration:文件总时长,单位是 AV_TIME_BASE(通常员 //pFormatContext->bit_rate:平均码率(比特/秒) logging(*format %s, duration %lld us, bit_rate lld”, pFormat //5.查找流信息 //此函数会读取媒体文件的一部分数据包,以获取更详细的流信息, //例如精确的帧率、编码参数等,并填充 pFormatContext->streams数组。 logging(finding stream info from format"); if (avformat_find_stream_info(pFormatContext,NULL) < θ) { logging("ERRoR could not get the stream info"); avformat_close_input(&pFormatContext);// 出错,关闭已打开的输 return-1; //初始化用于存储视频流信息的变量 AVCodecpCodec=NULL;// 指向找到的视频解码器 AVCodecParameters *pCodecParameters=NULL;// 指向视频流的编码 int video_stream_index = -1;// 视频流在 pFormatContext->streams //6.避历文件中的所有流(pFormatContext->nb_streams是流的总数)

pLocalCodecParameters = pFormatContext->streams[i] ->codecpa //打印当前流的一些时问基和帧率信息(解码器打开前) logging("AvStream->r_frame_rate before open coded %d/%d", p logging("AVStream->start_time %* PRId64, pFormatContext->st logging("AVStream->duration %" PRId64, pFormatContext->stre logging(finding the proper decoder (coDEc)"); AVCodec pLocalCodec = NULL; //根据编码参数中的codec_id查找对应的已注册的解码器 pLocalCodec = avcodec_ find_decoder(pLocalCodecParameters->c if (pLocalCodec==NULL){ logging(“ERRoR unsupported codec for stream %d!", i); continue;//如果找不到解码器或不支持该编码,则跳过此流 //判断流的类型,并保存第一个找到的视颜流的相关信息 if (pLocalCodecParameters ->codec_type == AVMEDIA_TYPE_VIDE0 if(video_stream_index ==-1){// 只处理第—个视频流 video_stream_index=i;// 保存视频流的索引 pCodec=pLocalCodec;//保存视频解码器指针 pCodecParameters =pLocalCodecParameters;// 保存视频流的 logging("Video Codec: resolution %d x %d", pLocalCodecPar } elseif (pLocalCodecParameters->codec_type == AVMEDIA_TYPE logging("Audio Codec: %d channels, sample rate %d", pLoca //打印解码器的长名称、ID和流的码率 logging("\tCodec %s ID %d bit_rate %lld", pLocalCodec->name //检查是否找到了视频流 if (video_stream_index == -1) { logging("File %s does not contain a video strean!", argv[1] avformat_close_input(&pFormatContext); // 清理 return-1; //7.为视频流准备解码器上下文(AVCodecContext) //pCodec是上一步找到的视频解码器 AVCodecContext pCodecContext = avcodec_alloc_context3(pCodec if (!pCodecContext){ logging("failed to allocated memory for AVCodecContext"); avformat_close_input(&pFormatContext); // 清理 return-1; //8.从流的编码参数(pCodecParameters)填充解码器上下文 if (avcodec_parameters_to_context(pCodecContext, pCodecParamete logging("failed to copy codec params to codec context"); avcodec_free_context(&pCodecContext);// 清理 avformat_close_input(&pFormatContext) ; return-1; //9.打开解码器 //使用指定的解码器(pCodec)打开/初始化解码器上下文(pCodecContext) //第三个参数可以传递一些解码器特定的选项(AVDictionary) if (avcodec_open2(pCodecContext, pCodec, NULL) < θ) { logging("failed to open codec through avcodec_open2"); avcodec_free_context(&pCodecContext): // 清理 avformat_close_input(&pFormatContext); return-1; // 16.分配 AVFrame 和 AVPacket 结构体内存 AVFramepFrame =av_frame_alloc();//用于存储解码后的原始视频帧姜 if (!pFrame){ logging("failed to allocate memory for AVFrame); avcodec_free_context(&pCodecContext) ; avformat_close_input(&pFormatContext) ; return-1; 上 AVPacketpPacket=av_packet_alloc():// 用于存储从文件中读取的日 if (!pPacket){ logging("failed to allocate memory for AVPacket*); av_frane_free(&pFrame); avcodec_free_context(&pCodecContext); avformat_close_input(SpFormatContext); return-1; int response=0;// 用于存储 FFmpeg API函数的返回值 //这个变量控制了main函数尝试向decode_packet发送多少个属于视频流的数 //初始值为8,但在实验中可以修改此值(例如改为10,20,56)来观察结果 int how_many_packets_to_process = 8; //11,主解码循环:从文件中读取数据包并进行解码 while (av_read_frame(pFormatContext, pPacket) >= 0){ // 从容器叶 //检查这个包是否属于我们之前确定的视频流 if (pPacket->stream_index == video_stream_index) { logging("AVPacket->pts %" PRId64, pPacket->pts); // 打印包 //调用自定义的解码函数来处理这个包 response = decode_packet(pPacket, pCodecContext, pFrame); if(response<B){//如果解码函数返回了不可恢复的错误 break;//终止主循环 //每当一个视類包被发送给decode_packet 函数(无论是否成功解码出额 // how_many_packets_to_process 计数器减—。 //当处理了指定数量的视频包后,停止从文件读取更多包。 if (--how_many_packets_to_process <= 0) { logging("Processed target number of video packets, stop break; ) //**重要:**释放pPacket引用的数据缓冲区,并将其他字段重置为默认促 //每次av_read_frame 准备读取新的包之前,前一个包必须被unref。 av_packet_unref (pPacket) ; //提示(Flush Decoder): //当所有需的包都已发送给解码器后(或av_read_frame返回文件尾), //解码器内部可能仍有一些缓冲的帧(特别是由于B帧依赖)。 //为了获取这些剩余的帧,应该向解码器发送一个NULL的AVPacket, //这会能发解码器的“flush”(冲洗)模式。 // logging("Flushing decoder by sending NULL packet to decode_p // decode_packet(NULL, pCodecContext,pFrame);// 发送 NULL 包以 //12.释放所有已分配的资源 logging(releasing all the resources); avformat_close_input(&pFormatContext):// 关闭输入文件并释放 AVF: //释放AVPacket结构体 av_frame_free(&pFrame); avcodec_free_context(&pCodecContext):// 释放 AVCodecContext ( return6; //日志函数 staticvoidlogging(constchar fmt,...)( va_list args; fprintf( stderr, "L0G: " ); va_start( args, fmt ); vfprintf( stderr, fnt, args ); va_end( args ); fprintf( stderr, "\n ); { //解码数据包函数 staticintdecode_packet(AVPacket *pPacket, AVCodecContext pCode //步骤1:将压缩的数据包(pPacket)发送给解码器(pCodecContext) //如果pPacket 为NULL,则表示输入流结束,要求解码器输出所有内部缓冲的帧 int response = avcodec_send_packet(pCodecContext, pPacket); if (response <0){ //如果是 AVERROR_EOF 并且是在flush 模式(pPacket为NULL),这是I if (response == AVERROR_EOF && pPacket == NULL) { logging("Decoder flushed (send_packet returned EoF)."); //不需要返回错误,因为接收循环仍可能取出缓冲帧。 }else { Logging(“Error sending packet to decoder: %s, av_err2s ) //对于AVERROR_EOF,也返回它,让接收循环知道输入已结束。 //对于其他错误,直接返回。 return response; //步骤2:循环从解码器接收解码后的帧 //因为一个包可能不会立即产生帧,或者解码器有内部缓冲(特别是B帧), //或者在某些特定情况下(主要是音频),一个包可能产生多个帧。 //因此,avcodec_receive_frane 需要在一个循环中被调用。 while (response >= 6){// 只要上—次的 send_packet 或 receive_fre response = avcodec_receive_frame(pCodecContext, pFrame); // if (response == AVERROR(EAGAIN)) { //AVERROR(EAGAIN)表示:解码器当前没有足够的已解码数据来输出一帧。 //它需要你通过 avcodec_send_packet()发送更多的数据包(如果 pP //如果 pPacket 是NULL(flush模式),则理论上不应轻易出现 EAGAI // logging("Decoder needs more input or no frame availabl break;//退出当前receive_frame循环,等待main函数发送下一个 }elseif (response == AVERROR_EOF) { //AVERROR_EOF表示:输入已经结束(之前通过发送NULLpacket 指示 //并且解码器内部所有缓冲的帧都已经成功取出了。没有更多赖可以接收了。 logging("End of stream reached in decoder, no more frames break;// 退出当前 receive_frame 循环 }elseif (response < θ){ //其他不可恢复的解码错误 logging("Error receiving frame from decoder: %s", av_err2 return response;//将错误码返回给调用者(main 函数) //如果response>=9,说明成功解码并获取到了一帧! )6u66o1 “Frame %d (type=sc, size=%d bytes, format=sd) pts %lld pCodecContext->frame_number,//解码器成功输出的的序号(从 av_get_picture_type_char(pFrame ->pict_type), // 帧类型( pFrame->pkt_size,//生成此帧的原始数据包的大小(仅供参考) pFrame->format,//解码后帧的像素格式(例如 AV_PIX_FMT_YU (longlong)pFrame->pts, //**显示时间(Presentation pFrame->key_frame,//是否是关键帧(1表示是,θ表示否) (longlong)pFrame->pkt_dts // **解码时间戳(Decode Timesta ): char frame_filename[1024]; //使用解码器输出的顿序号(pCodecContext->frame_number)来命名P snprintf(frame_filename, sizeof(frame_filename), "%s-d.pgm //示例视频 smal1_bunny_1080p_60fps.mp4是YUV428P 格式。 //save_gray_frame 函数只保存Y(亮度)分量。 if (pFrame->format != AV_PIX_FMT_YUV42oP) { logging("Warning: frame format is not YUV42eP, PGM might 1 //调用函数将解码后帧的亮度分量保存为PGM图像文件 save_gray_frame (pFrane->data[] , pFrame->linesize[9], pFram //处理完一帧后,通常需要调用av_frame_unref(pFrame)来释放pFram //以便pFrame 结构体可以被下一次 avcodec_receive_frame 调用安全 //此示例中pFrame在main函数中分配并在最后统一释放,依赖于avcod //更安全的做法是在这里unref。 //如果是因为EAGAIN退出的循环,说明当前包已发送,但解码器需要更多包。 //如果是因为E0F或其他原因(如正常解码完当前包能出的所有帧),也表示当前包 //返回0表示与当前输入包相关的解码尝试已完成。 //如果想让上层知道是EAGAIN,可以返回response。这里简化处理。 return (response == AVERROR(EAGAIN) 1l response == AVERROR_EOF) //保存灰度帧为PGMP5文件 staticvoidsave_gray_frame(unsignedchar buf, int wrap, int xsiz FILE f; int i; f=fopen(filename,“wb");//“wb表示以二进制写模式打开文件 if (f == NULL) { Logging("ERRoR: Could not open file %s for writing.", f return: //写入PGMP5格式的文件头: //“P5”表示二进制灰度图 //"%d%d”是图像的宽度和高度 //“d”是最大的灰度值(通常是255,表示8位灰度) fprintf(f, "P5\n%d %d\nd\n", xsize, ysize, 255); //逐行写人像素数据 //buf指向帧数据中亮度分量(Yplane)的起始地址 //wrap(也叫Linesize 或stride)是图像数据中一行的实际字节数,可 for (i = θ; i < ysize; i++) { fwrite(buf+iwrap,1,xsize,f);//每次写人一行的实际 fclose(f);//关闭文件

四、how_many_packets_to_process 与PGM 文件数量的深入理解

实验中修改 0_hello_world.c 中how_many_packets_to_process 的初始值 (例如从B改成10、20、58)。并观察到输出PGM文件数量(即pCodecContext- >frame_number 的最大值)大致为设定的包数减去一个小的固定值(例如N-2), 这个现象揭示了视频解码的内在机制。 .1.how_many_packets_to_process 的角色: 。·它在main 函数中,用于限制调用 decode_packet 函数来处理从指定视频 流中读取到的数据包的次数上限。如果设置为N,那么主循环在处理了N个视频 包后就会停止读取。 .2.为何输出帧数通常略少于处理的包数(尤其在视频开头)一解码器启动延迟和B 帧效应 。·实验中“N个包输出N-X帧”的模式,正是这种解码器启动行为和处理B帧依赖 在视频起始部分的体现。这个“X”不是一个固定的FFmpeg规则,而是特定于测 试视频文件(smalL_bunny_1080p_60fps.mp4)的G0P(图像组)结构、起 始部分的B帧数量和位置,以及0_hello_world.c当前解码逻辑(特别是它没 有在最后显式地调用decode_packet(NULL,...)来冲洗解玛器缓冲)共同 作用下的一个经验结果。对于不同的视频文件或视频的不同片段,这个“X”值可 能会不同。 。·B帧依赖性:B帧的解码需要其前面和后面的参考帧(I帧或P额)。这意味着 解码器必须先接收并解码掉B赖所依赖的那个“未来”的P帧(或I帧),然后才能回 头处理这些B帧。在此期间,即使发送了B赖的包, avcodec_receive_frame()也可能因数据不完整而返 回AVERROR(EAGAIN)。导致这一轮 decode_packet 调用没有保存图像。 码上下文”、“满足间依赖关系”,或者解码出了一些由于PTS顺序靠后(因B帧 重排)而暂时不能立即被pCodecContext->frame_number 计的帧。直到 解码器内部的“流水线“被填满并且依赖关系得到满足,后续的包才能更稳定地产出 对应的帧。 。·解码器“预热”/“填补流水线”:视频解码器(特别是处理像H.264这样包含 帧。 。·观察到的"N-X"模式: .3.帧的“身份"与区分(时间的量要性): 。·pCodecContext->frame_number(解码器输出帧的流水号):解码器成功 输出一帧时内部递增的计数器(从1开始)。0_hello_world.c用它来命名 PGM文件。它代表帧从解码器出来的顺序。 。·pFrame->pts(显示时间戳-Presentation Timestamp):这是最重要 的时间,指示该帧应在视频的哪个时间点被显示。单位是流的时间基 (AVStream->time_base)。PTS的顺序决定了视频的视觉播放顺序。 。·pFrame->pkt_dts(解码时间戳-Decode Timestamp):指示包含此赖 (或其主要部分)的数据包应何时被解码。DTS通常是单调递增的,反映了数据在 原始码流中的顺序。解码器严格按DTS顺序处理包。

五、参数修改建议与应用场景(优化与扩展)

基于对IBP帧和解码流程的理解,可以对这个基础程序进行多种修改以适应不同需 求: .1.精确提取指定数量的输出帧: 。·在decode_packet成功保存一帧后,使用一个外部计数器(例如 在main 中定义并传递指针给decode_packet)来记录实际保存的顿数。当 达到目标数量时,终止main函数的循环。 。·关键:在main循环结束后(无论是正常完成还是因达到帧数限制而中 断),务必调用 decode_packet(NULL,pCodecContext,pFrame);来 发送一个NULL包给解码器,以“冲洗”(flush)解码器内部缓冲的所有剩余帧。 这样可以确保得到所有应该输出的赖,特别是视频尾部的帧。 .2.提取指定时间范围/特定时间点的帧(用于缩略图、预览等): o·计算目标时间(如視频总时长的38%)对应的int64_ttarget_pts(注意 从秒转换为流的时问基单位)。 。·使用av_seek_frame(pFormatContext,video_stream_index, target_pts, AVSEEK FLAG BACKWARD); 定位到 target_pts 之前最近 的关键帧。 重要:av_seek_frame之后,必须调 用avcodec_flush_buffers(pCodecContext):来清空解码器内部的陈旧 状态和缓冲数据。 。·然后,循环调用av_read_frame和decode_packet。 在decode_packet 中,当 avcodec_receive_frame 成功获取一后, 比较 pFrame->pts 与target_pts。当找到第-个 pFrame->pts >= target_pts 的帧时,保存它,然后可以停止这个时间点的提取。 。·应用:批量缩略图生成器、视频预览条的帧提取。 .3.保存为彩色图像(PPMP6)或常用格式(JPEG/PNG): 。·彩色PPM(P6格式): 。·JPEG/PNG:需要将解码后的AVFrame 再次通过Libavcodec进行图像 编码(使用MJPEG或PNG编码器),得到包含图像数据的AVPacket,然后将该 包数据写入文件。 。·应用:生成高质量的彩色缩略图。

1.1.将解码后的AVFrame(通常是YUV格式)使用Libswscale 库转换

为AV_PIX_FMT_RGB24 格式。

2.2.修改 save_gray_frame 为 save_rgb_ppm_frame,写入PPM P6头

("P6\n%d%d\n255\n"),然后写入RGB数据。

4.视频内容基础分析:

o·解码视频帧后,访问AVFrame->data(Y,U,V分量的像素数据)进行计 算,如平均亮度、帧间差异等。 。·应用:视频质量自动化检测、内容标记辅助。 通过不断地实践、修改代码、观察结果,并对照FFmpeg官方文档深入理解每个API 函数的行为,就能真正掌握FFmpeg编程的精髓。 ffmpeg·目录三 下一篇·FFmpeg深度学习笔记(二):Remuxing(转封装)的艺术-2_remuxing.C...> 作者提示:个人观点,仅供参考

原始排版图

FFmpeg 核心概念与 0_hello_world.c 示例深度解析 系列一:微信公众号导出原始排版图