1. 项目概述为什么选择FFmpeg作为音视频开发的起点如果你正在用C或C做开发并且对处理视频、音频、直播流这些内容感兴趣那么FFmpeg几乎是你绕不开的一个名字。它不是一个简单的播放器而是一个功能极其强大的多媒体处理“瑞士军刀”库。很多我们日常使用的软件像VLC播放器、OBS直播软件、甚至一些视频剪辑工具其底层或多或少都依赖FFmpeg来处理音视频的编解码、封装、转码等核心任务。对于开发者来说直接使用FFmpeg的库进行编程意味着你获得了对音视频数据最底层的控制权可以定制化地实现任何你能想到的处理流程比如从抖音视频里提取音频、给视频批量添加水印、或者搭建自己的流媒体服务器。很多人一开始会被FFmpeg庞大的命令集和复杂的API吓到觉得入门门槛很高。确实如果你只停留在使用ffmpeg -i input.mp4 output.avi这种命令行层面很难体会到其精髓。真正的“编程入门”是指我们作为C/C开发者如何在自己的代码里调用libavcodec、libavformat、libavutil这些核心库去读取一帧帧的视频画面解码成YUV数据然后进行修改再重新编码和封装。这个过程听起来复杂但一旦理清了FFmpeg处理多媒体数据的核心流程和数据模型你就会发现它有一套非常清晰和统一的逻辑。本篇文章的目的就是带你穿透命令行工具的表象直接深入到FFmpeg的库编程层面用实际的代码示例一步步拆解这个流程让你能亲手写出一个可以处理音视频数据的C/C程序。2. 核心概念与数据模型拆解在动手写代码之前我们必须先理解FFmpeg是如何看待和抽象化多媒体世界的。如果你用过面向对象语言可以把它想象成FFmpeg定义了几个核心的“类”和它们之间的关系。理解这几个核心结构体是读懂任何FFmpeg代码的前提。2.1 核心结构体AVFormatContext, AVCodecContext, AVFrame, AVPacketFFmpeg用C语言实现但其设计思想非常面向对象。我们打交道的主要是以下几个结构体AVFormatContext封装格式上下文这是整个媒体文件的“总管”。它包含了文件或网络流的格式信息比如是MP4还是FLV、所有的流Stream信息、以及一些全局的元数据Metadata。你可以把它理解为一个文件的句柄通过它我们能知道这个文件里有多少条音轨、多少条视频轨每条轨是什么编码格式。AVStream流一个媒体文件容器里可能包含多条流最常见的就是一条视频流和一条音频流。AVStream结构体就代表其中一条流。它里面有一个非常重要的成员叫AVCodecParameters描述了这条流的编码参数比如视频的宽度、高度、像素格式音频的采样率、声道数。AVCodecContext编解码器上下文这是对某一条流进行编解码操作的“工作间”。它包含了编解码所需要的所有参数和状态。我们需要根据AVStream里的AVCodecParameters来初始化和配置一个AVCodecContext然后才能用它对这条流的数据进行解码或编码。AVPacket数据包这是从媒体文件中读取出来的、经过封装压缩后的原始数据单元。对于视频一个Packet里可能包含一帧或多帧压缩后的数据如一个GOP的多个帧对于音频它包含一段连续的压缩音频数据。AVPacket本身不包含解码后的像素或声音它只是承载压缩数据的容器其data成员指向压缩数据size表示数据大小。AVFrame帧这是解码后的原始数据单元。对于视频一个AVFrame包含一帧图像的YUV或RGB像素数据对于音频它包含一段PCM采样数据。这是我们开发者最常直接操作的数据结构比如你想对视频画面做滤镜、人脸识别或者对音频做降噪操作的对象就是AVFrame里的数据。它们之间的关系可以用一个简单的数据流向来描述AVFormatContext-AVStream-AVCodecContext。我们从AVFormatContext中解封装Demux出AVPacket然后将AVPacket送入对应的AVCodecContext进行解码得到AVFrame。反之编码和封装就是逆过程。注意在较新的FFmpeg API中比如4.0推荐使用AVCodecParameters来传递流的编码信息而不是直接从AVCodecContext中获取。AVCodecContext更多用于编解码过程本身的控制。在初始化解码器时正确的做法是将AVStream-codecpar复制到新创建的AVCodecContext中。2.2 核心流程解封装、解码、处理、编码、封装无论你的应用场景多么复杂一个标准的FFmpeg处理流程几乎都遵循以下步骤我把它称为“音视频处理五部曲”解封装Demux使用libavformat库打开输入文件读取文件头解析出其中的AVFormatContext从而知道里面有哪些流AVStream。然后通过av_read_frame()函数循环读取每次读出一个AVPacket并附带其所属流的索引。解码Decode根据AVPacket的流索引找到对应的AVCodecContext解码器上下文。然后调用avcodec_send_packet()将压缩包送入解码器再循环调用avcodec_receive_frame()从解码器取出解码后的AVFrame。这里send和receive的调用可能不是一对一的因为解码器可能有缓存。处理Process这是我们开发者大展拳脚的地方。拿到了原始的AVFrame视频YUV帧或音频PCM帧我们就可以对其进行任何处理缩放、裁剪、颜色转换、添加水印、人脸识别、音频混音、变速等等。这一步完全在你的业务逻辑控制之下。编码Encode处理后的AVFrame需要被重新压缩以便存储或传输。我们配置一个编码器的AVCodecContext然后调用avcodec_send_frame()将处理后的帧送入编码器再循环调用avcodec_receive_packet()获取压缩后的AVPacket。封装Mux将编码后得到的AVPacket根据其流索引写入到输出文件的AVFormatContext中即调用av_interleaved_write_frame()或av_write_frame()。最后写入文件尾并释放所有资源。这个流程是单向的流水线理解了这个骨架任何复杂的音视频应用都是在它的基础上进行添枝加叶。3. 环境搭建与第一个工程理论讲得再多不如一行代码。让我们从最实际的环境搭建开始。3.1 获取与编译FFmpeg库对于Windows开发者最省事的方法是使用官方提供的已编译的dev和shared版本。你可以去FFmpeg官网的“Get the packages”找到Windows版本的构建通常由gyan.dev等提供。下载后你会得到两个zip包一个是开发文件包含include头文件和lib导入库另一个是运行时DLL文件。对于Linux或macOS开发者使用包管理器是最快的比如apt-get install libavcodec-dev libavformat-dev libavutil-dev libswscale-devUbuntu或brew install ffmpegmacOS。但如果你想使用最新特性或进行定制化编译从源码编译是更好的选择。编译命令通常如下./configure --prefix/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree make -j8 sudo make install这里--enable-shared生成动态库--disable-static不生成静态库--enable-gpl和--enable-nonfree允许使用一些有专利的编解码器如H.264。编译完成后头文件在/usr/local/ffmpeg/include库文件在/usr/local/ffmpeg/lib。3.2 在Visual Studio或VSCode中配置项目Visual Studio创建一个新的C控制台项目。右键项目 - 属性。C/C-常规-附加包含目录添加FFmpeg的include目录路径例如D:\ffmpeg\include。链接器-常规-附加库目录添加FFmpeg的lib目录路径例如D:\ffmpeg\lib。链接器-输入-附加依赖项添加需要链接的库文件基本的有avcodec.lib; avformat.lib; avutil.lib; swscale.lib; swresample.lib;根据你的需求增减。将FFmpeg的bin目录下的DLL文件如avcodec-58.dll复制到你的项目生成的可执行文件.exe所在目录或者将其路径添加到系统的PATH环境变量中。VSCode CMake 如果你使用CMake配置会更清晰。创建一个CMakeLists.txt文件关键内容如下cmake_minimum_required(VERSION 3.10) project(FFmpegDemo) set(CMAKE_CXX_STANDARD 11) # 设置FFmpeg库的路径请根据你的实际安装路径修改 set(FFMPEG_DIR D:/ffmpeg) # Windows示例 # set(FFMPEG_DIR /usr/local/ffmpeg) # Linux/macOS示例 # 查找头文件 include_directories(${FFMPEG_DIR}/include) # 查找库文件 link_directories(${FFMPEG_DIR}/lib) add_executable(FFmpegDemo main.cpp) # 链接FFmpeg库 target_link_libraries(FFmpegDemo avcodec avformat avutil swscale)然后在VSCode中安装CMake扩展就可以正常编译和调试了。3.3 第一个程序打印视频文件信息让我们写一个最简单的程序它不处理音视频数据只是打开一个文件打印出它的基本信息。这个程序会用到AVFormatContext和AVStream。#include stdio.h #include libavformat/avformat.h int main(int argc, char* argv[]) { if (argc 2) { printf(Usage: %s input_file\n, argv[0]); return -1; } const char* filename argv[1]; AVFormatContext* fmt_ctx NULL; // 1. 初始化网络库如果需要打开网络流 // avformat_network_init(); // 2. 打开输入文件并解析其头部信息填充fmt_ctx if (avformat_open_input(fmt_ctx, filename, NULL, NULL) 0) { fprintf(stderr, Could not open source file %s\n, filename); return -1; } // 3. 读取文件中的流信息主要是为了获取每个流的codecpar if (avformat_find_stream_info(fmt_ctx, NULL) 0) { fprintf(stderr, Could not find stream information\n); avformat_close_input(fmt_ctx); return -1; } // 4. 打印格式信息 av_dump_format(fmt_ctx, 0, filename, 0); // 5. 遍历所有流打印详细信息 for (unsigned int i 0; i fmt_ctx-nb_streams; i) { AVStream* stream fmt_ctx-streams[i]; AVCodecParameters* codecpar stream-codecpar; printf(\nStream #%d:\n, i); printf( Type: %s\n, av_get_media_type_string(codecpar-codec_type)); printf( Codec: %s (ID: %d)\n, avcodec_get_name(codecpar-codec_id), codecpar-codec_id); if (codecpar-codec_type AVMEDIA_TYPE_VIDEO) { printf( Resolution: %d x %d\n, codecpar-width, codecpar-height); printf( Pixel Format: %s\n, av_get_pix_fmt_name(codecpar-format)); // 注意avg_frame_rate 是 AVRational 类型需要转换 AVRational fr stream-avg_frame_rate; printf( Avg Frame Rate: %d/%d ≈ %.2f fps\n, fr.num, fr.den, av_q2d(fr)); } else if (codecpar-codec_type AVMEDIA_TYPE_AUDIO) { printf( Sample Rate: %d Hz\n, codecpar-sample_rate); printf( Channels: %d\n, codecpar-channels); printf( Channel Layout: %lu\n, codecpar-channel_layout); printf( Sample Format: %s\n, av_get_sample_fmt_name(codecpar-format)); } printf( Bitrate: %ld bps\n, codecpar-bit_rate); } // 6. 清理资源 avformat_close_input(fmt_ctx); return 0; }编译并运行这个程序传入一个视频文件路径比如./demo test.mp4。你会看到类似FFmpeg命令行ffmpeg -i test.mp4的输出详细列出了容器格式、每条流的编码器、分辨率、帧率、采样率等信息。这个程序虽然简单但它完成了FFmpeg编程的第一步成功打开文件并获取了元数据。avformat_open_input和avformat_close_input是必须成对使用的资源管理函数务必注意。4. 核心流程实战实现视频转码与缩放现在我们来完成一个更实际的任务将一个视频文件转码为H.264编码并同时将其分辨率缩放为原来的一半。这个例子将完整走通“解封装-解码-处理-编码-封装”的全流程。4.1 打开输入与输出上下文首先我们需要打开输入文件并创建输出文件上下文。AVFormatContext* input_ctx NULL; AVFormatContext* output_ctx NULL; // 打开输入 if (avformat_open_input(input_ctx, input_filename, NULL, NULL) 0) { // 错误处理 } if (avformat_find_stream_info(input_ctx, NULL) 0) { // 错误处理 } // 创建输出上下文猜测格式根据输出文件后缀名如.mp4 avformat_alloc_output_context2(output_ctx, NULL, NULL, output_filename); if (!output_ctx) { // 如果根据后缀名猜测失败可以指定格式如“mp4” avformat_alloc_output_context2(output_ctx, NULL, mp4, output_filename); } if (!output_ctx) { // 错误处理 }4.2 为每条流创建输出流并配置编码器输入文件里可能有视频流、音频流甚至字幕流。我们需要为每一条我们希望保留并转码的流在输出上下文中创建一条对应的流并为其配置编码器。// 用于保存输入流索引到输出流编码器上下文的映射 AVCodecContext** enc_ctx_list av_mallocz_array(input_ctx-nb_streams, sizeof(*enc_ctx_list)); for (int i 0; i input_ctx-nb_streams; i) { AVStream* in_stream input_ctx-streams[i]; AVCodecParameters* in_codecpar in_stream-codecpar; // 只处理视频和音频流忽略其他如字幕、数据流 if (in_codecpar-codec_type ! AVMEDIA_TYPE_VIDEO in_codecpar-codec_type ! AVMEDIA_TYPE_AUDIO) { enc_ctx_list[i] NULL; continue; } // 在输出上下文中创建一条新流 AVStream* out_stream avformat_new_stream(output_ctx, NULL); if (!out_stream) { // 错误处理 } // 根据输入流的编码器ID寻找对应的编码器这里以视频H.264音频AAC为例 const AVCodec* encoder NULL; if (in_codecpar-codec_type AVMEDIA_TYPE_VIDEO) { encoder avcodec_find_encoder(AV_CODEC_ID_H264); } else if (in_codecpar-codec_type AVMEDIA_TYPE_AUDIO) { encoder avcodec_find_encoder(AV_CODEC_ID_AAC); } if (!encoder) { // 错误处理未找到编码器 } // 为编码器创建编码上下文 AVCodecContext* enc_ctx avcodec_alloc_context3(encoder); if (!enc_ctx) { // 错误处理 } // 配置编码参数 if (in_codecpar-codec_type AVMEDIA_TYPE_VIDEO) { // 视频设置分辨率、像素格式、码率、帧率等 enc_ctx-width in_codecpar-width / 2; // 缩放为一半 enc_ctx-height in_codecpar-height / 2; enc_ctx-sample_aspect_ratio in_stream-sample_aspect_ratio; // 使用yuv420p最通用的像素格式 enc_ctx-pix_fmt encoder-pix_fmts ? encoder-pix_fmts[0] : AV_PIX_FMT_YUV420P; enc_ctx-framerate in_stream-avg_frame_rate; enc_ctx-time_base av_inv_q(enc_ctx-framerate); // 时间基 1/帧率 // 设置码率这里用固定码率也可用CRF等质量模式 enc_ctx-bit_rate 1000000; // 1 Mbps enc_ctx-gop_size 12; // 关键帧间隔 } else if (in_codecpar-codec_type AVMEDIA_TYPE_AUDIO) { // 音频设置采样率、声道数、采样格式、码率等 enc_ctx-sample_rate in_codecpar-sample_rate; enc_ctx-channel_layout in_codecpar-channel_layout; enc_ctx-channels av_get_channel_layout_nb_channels(enc_ctx-channel_layout); enc_ctx-sample_fmt encoder-sample_fmts[0]; enc_ctx-bit_rate 128000; // 128 kbps enc_ctx-time_base (AVRational){1, enc_ctx-sample_rate}; } // 将编码器参数复制到输出流的codecpar中重要 if (avcodec_parameters_from_context(out_stream-codecpar, enc_ctx) 0) { // 错误处理 } // 设置输出流的时间基与编码器上下文一致 out_stream-time_base enc_ctx-time_base; // 打开编码器 if (avcodec_open2(enc_ctx, encoder, NULL) 0) { // 错误处理 } // 保存编码器上下文到映射列表 enc_ctx_list[i] enc_ctx; }实操心得编码器参数配置是转码质量的关键。对于H.264视频bit_rate控制文件大小和清晰度gop_size影响seek速度和压缩效率。time_base时间基是FFmpeg中时间管理的核心它表示每个刻度代表多少秒。编码器上下文、输出流、以及后续的Packet和Frame都带有时间基信息必须正确设置和转换否则会导致音视频不同步。视频的time_base通常设为1/framerate音频的设为1/sample_rate。4.3 打开输出文件并写入头部配置好所有输出流后就可以打开输出文件并写入文件头了。// 如果输出格式需要如MP4并且不是纯流格式则写入头部 if (!(output_ctx-oformat-flags AVFMT_NOFILE)) { if (avio_open(output_ctx-pb, output_filename, AVIO_FLAG_WRITE) 0) { // 错误处理无法打开输出文件 } } // 写入文件头 if (avformat_write_header(output_ctx, NULL) 0) { // 错误处理 }4.4 主循环读取、解码、缩放、编码、写入这是最核心的循环。我们将从输入文件中读取AVPacket解码成AVFrame对视频帧进行缩放处理然后重新编码成AVPacket最后写入输出文件。AVPacket* packet av_packet_alloc(); AVFrame* frame av_frame_alloc(); // 对于视频缩放我们需要一个SwsContext struct SwsContext* sws_ctx NULL; while (av_read_frame(input_ctx, packet) 0) { int stream_index packet-stream_index; AVCodecContext* dec_ctx dec_ctx_list[stream_index]; // 假设之前已创建并打开了解码器上下文 AVCodecContext* enc_ctx enc_ctx_list[stream_index]; if (!enc_ctx) { // 如果该流不需要重新编码如字幕直接复制Packet // 需要重新计算Packet的pts/dts等时间戳 av_packet_rescale_ts(packet, input_ctx-streams[stream_index]-time_base, output_ctx-streams[stream_index]-time_base); av_interleaved_write_frame(output_ctx, packet); av_packet_unref(packet); continue; } // 发送Packet到解码器 if (avcodec_send_packet(dec_ctx, packet) 0) { // 错误处理 } while (avcodec_receive_frame(dec_ctx, frame) 0) { // 成功解码出一帧 AVFrame* out_frame av_frame_alloc(); av_frame_copy_props(out_frame, frame); // 复制时间戳等属性 if (dec_ctx-codec_type AVMEDIA_TYPE_VIDEO) { // 视频处理缩放 if (!sws_ctx) { // 创建图像缩放转换上下文 sws_ctx sws_getContext(dec_ctx-width, dec_ctx-height, dec_ctx-pix_fmt, enc_ctx-width, enc_ctx-height, enc_ctx-pix_fmt, SWS_BILINEAR, NULL, NULL, NULL); } // 为输出帧分配内存 out_frame-width enc_ctx-width; out_frame-height enc_ctx-height; out_frame-format enc_ctx-pix_fmt; av_frame_get_buffer(out_frame, 0); // 执行缩放 sws_scale(sws_ctx, (const uint8_t* const*)frame-data, frame-linesize, 0, dec_ctx-height, out_frame-data, out_frame-linesize); } else if (dec_ctx-codec_type AVMEDIA_TYPE_AUDIO) { // 音频处理这里示例直接复制实际可能涉及重采样使用SwrContext // 如果输入输出音频参数采样率、声道数、格式不一致需要重采样 // 为简化假设参数一致直接引用输入帧数据 av_frame_ref(out_frame, frame); } // 将处理后的帧发送到编码器 if (avcodec_send_frame(enc_ctx, out_frame) 0) { // 错误处理 } av_frame_unref(out_frame); av_frame_free(out_frame); // 从编码器接收编码后的Packet AVPacket* enc_pkt av_packet_alloc(); while (avcodec_receive_packet(enc_ctx, enc_pkt) 0) { // 设置输出Packet的流索引和时间戳 enc_pkt-stream_index stream_index; av_packet_rescale_ts(enc_pkt, enc_ctx-time_base, output_ctx-streams[stream_index]-time_base); // 写入输出文件 if (av_interleaved_write_frame(output_ctx, enc_pkt) 0) { // 错误处理 } av_packet_unref(enc_pkt); } av_packet_free(enc_pkt); } av_packet_unref(packet); } // 刷新编码器发送NULL帧取出缓存中剩余的Packet // ... (此处省略刷新编码器和写入的代码)注意事项av_read_frame返回的AVPacket的时间戳pts, dts是基于其所在输入流的时间基的。在写入输出文件前必须使用av_packet_rescale_ts将其转换为输出流的时间基这是保证音视频同步的关键一步。同样解码出的AVFrame的pts在送入编码器前也需要根据编码器的时间基进行调整。av_frame_copy_props可以方便地复制这些属性。4.5 收尾工作循环结束后需要刷新编码器发送NULL帧写入文件尾并释放所有资源。// 刷新所有编码器 for (int i 0; i input_ctx-nb_streams; i) { if (enc_ctx_list[i]) { // 发送NULL帧到编码器表示刷新 avcodec_send_frame(enc_ctx_list[i], NULL); AVPacket* enc_pkt av_packet_alloc(); while (avcodec_receive_packet(enc_ctx_list[i], enc_pkt) 0) { enc_pkt-stream_index i; av_packet_rescale_ts(enc_pkt, enc_ctx_list[i]-time_base, output_ctx-streams[i]-time_base); av_interleaved_write_frame(output_ctx, enc_pkt); av_packet_unref(enc_pkt); } av_packet_free(enc_pkt); } } // 写入文件尾 av_write_trailer(output_ctx); // 关闭输出文件如果已打开 if (output_ctx !(output_ctx-oformat-flags AVFMT_NOFILE)) { avio_closep(output_ctx-pb); } // 释放所有资源 avformat_close_input(input_ctx); if (output_ctx) { avformat_free_context(output_ctx); } for (int i 0; i input_ctx-nb_streams; i) { if (enc_ctx_list[i]) { avcodec_free_context(enc_ctx_list[i]); } } av_freep(enc_ctx_list); av_packet_free(packet); av_frame_free(frame); if (sws_ctx) { sws_freeContext(sws_ctx); }资源管理是C语言编程的重点务必确保每一个av_xxx_alloc都有对应的av_xxx_free每一个avformat_open_input都有对应的avformat_close_input防止内存泄漏。5. 进阶话题与性能优化当你掌握了基础流程后下面这些进阶话题能帮助你写出更高效、更稳定的程序。5.1 硬件加速解码与编码使用CPU进行软件编解码尤其是高清视频会消耗大量计算资源。利用GPU进行硬件加速可以极大提升性能。FFmpeg支持多种硬件加速方案如NVIDIA的NVENC/NVDEC通过h264_nvenc编解码器、Intel的QSVQuick Sync Video、AMD的AMF等。使用硬件解码器通常只需在查找解码器时指定特定的名称并配置一些硬件设备上下文。例如使用CUDA进行硬件解码和缩放// 查找CUDA硬件解码器 const AVCodec* decoder avcodec_find_decoder_by_name(h264_cuvid); // 配置解码器上下文时指定硬件设备类型 AVBufferRef* hw_device_ctx NULL; av_hwdevice_ctx_create(hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, NULL, NULL, 0); dec_ctx-hw_device_ctx av_buffer_ref(hw_device_ctx);硬件编码类似使用h264_nvenc等编码器。需要注意的是硬件编解码器输入输出的AVFrame格式可能是特定的硬件帧格式如AV_PIX_FMT_CUDA在CPU上进行处理前可能需要通过av_hwframe_transfer_data将其传输到系统内存。5.2 多线程处理FFmpeg内部已经为许多编解码器实现了帧级或切片级的多线程。你可以在创建编解码器上下文时设置thread_count参数enc_ctx-thread_count 4; // 使用4个线程进行编码对于解码可以设置dec_ctx-thread_count。此外你也可以在自己的处理逻辑中实现多线程例如使用一个生产者-消费者模型一个线程专门负责读取和解码放入队列另一个或多个线程从队列中取出帧进行处理和编码。这需要谨慎处理线程间的同步和AVFrame的内存管理。5.3 滤镜Filter的使用FFmpeg提供了一个强大的滤镜系统libavfilter可以让你以图形化的管道方式处理音视频数据而无需手动操作sws_scale或swr_convert。例如实现视频缩放、加水印、叠加文字或者音频混音、淡入淡出用滤镜会简单很多。使用滤镜的基本步骤是创建滤镜图FilterGraph和滤镜上下文FilterContext将解码后的AVFrame“推送”av_buffersrc_add_frame到滤镜图再从滤镜图“拉取”av_buffersink_get_frame处理后的AVFrame。滤镜描述字符串如scale640:480定义了处理过程。虽然初学有一定门槛但对于复杂的处理链滤镜能极大简化代码。6. 常见问题排查与调试技巧在实际开发中你一定会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决方法。6.1 内存泄漏排查FFmpeg程序容易发生内存泄漏。除了确保每个alloc都有对应的free外还可以在程序结束时调用av_log_set_level(AV_LOG_DEBUG)开启调试日志FFmpeg会在内部资源未释放时给出警告。更专业的方法是使用ValgrindLinux或Visual Studio的内存诊断工具来检测。6.2 时间戳与音视频同步问题音视频不同步是最常见的问题之一。请牢记以下检查点时间基转换任何涉及AVPacket或AVFrame的pts/dts在不同上下文间传递时都必须用av_rescale_q或av_packet_rescale_ts进行时间基转换。编码器延迟有些编码器如B帧编码会有延迟即送入一帧不会立即输出Packet。必须在循环结束和刷新时向编码器发送NULL帧来取出所有缓存的Packet。起始时间戳确保输出文件的第一帧视频和音频的pts从0或一个合理的值开始。有时输入文件的起始pts可能不是0需要做偏移校正。6.3 编解码器不支持或参数错误如果avcodec_open2失败首先检查avcodec_find_encoder返回的编码器是否为空。确保你编译的FFmpeg库包含了对应的编解码器例如默认编译可能不包含H.264编码器需要--enable-gpl --enable-libx264。其次仔细检查传递给编码器上下文的参数是否有效且兼容例如pix_fmt是否在编码器支持的列表里bit_rate是否设置得太低等。使用av_log设置回调函数可以捕获FFmpeg内部的详细错误信息。6.4 帧处理中的图像格式转换当你对视频帧进行操作如用OpenCV处理时经常需要将FFmpeg的AVFrame通常是YUV格式转换成OpenCV的MatBGR格式。这需要用到sws_scale进行颜色空间和像素格式的转换。一个常见的错误是源和目标的width、height、linesize不匹配。linesize是每行数据对齐后的字节数可能不等于width * pixel_size。在分配目标缓冲区和使用sws_scale时要使用frame-linesize而不是计算值。最后调试FFmpeg程序多使用av_log输出关键变量的值如pts,time_base,width/height并和FFmpeg命令行工具ffprobe的输出进行对比这是定位问题最快的方法。从简单的任务开始逐步增加复杂度理解每一个API调用和数据结构的生命周期是掌握FFmpeg编程的不二法门。