C++音视频采集与播放实战:从FFmpeg、DirectShow到SDL2的完整实现

📅 2026/7/17 23:30:23
C++音视频采集与播放实战:从FFmpeg、DirectShow到SDL2的完整实现
1. 项目概述为什么选择C做音视频采集与播放如果你正在看这篇文章大概率是遇到了一个具体的需求需要用C来抓取摄像头或麦克风的数据然后实时播放出来或者存成文件。可能是想做一个视频会议客户端、一个直播推流工具、一个安防监控系统或者仅仅是好奇想自己动手实现一个“简易版QQ视频”。无论你的出发点是什么选择C这条路就意味着你选择了性能和控制力同时也选择了一条相对“硬核”的路径。我接触音视频开发有年头了从早期的DirectShow到现在的FFmpeg、WebRTCC一直是这个领域的基石。为什么因为音视频数据量巨大一帧1080P的未压缩图像就好几兆每秒30帧就是上百兆的数据吞吐。处理这些数据对实时性、延迟、CPU和内存占用都极其敏感。用Python或者Java的现成库虽然快但当你需要精细控制每一帧的采集时机、编码参数或者处理多路高清流时底层C/C库带来的性能优势和灵活性是无法替代的。FFmpeg、GStreamer这些核心框架本身就是C写的用C去调用和扩展是最自然、最直接的方式。这个教程的目标很明确不扯虚的直接带你从零开始用C实现一个最核心的音视频“采集-处理-播放”的闭环。我们会聚焦在Windows平台因为开发环境相对统一使用FFmpeg作为核心处理库DirectShow进行视频采集WASAPI进行音频采集SDL2进行播放。你会清晰地看到数据是如何从硬件“流”出来经过我们的代码再“流”到屏幕和扬声器上的。过程中每一个参数的选择、每一个API调用的顺序我都会解释清楚背后的原因。相信我当你亲手让第一帧图像从你自己的程序里播放出来时那种感觉比调用十次cv::imshow()要爽得多。2. 核心工具链选型与环境搭建工欲善其事必先利其器。在开始写代码之前我们必须把“战场”布置好。C音视频开发的环境搭建稍微有点繁琐但一旦配好就是一劳永逸的事情。2.1 为什么是FFmpeg SDL2 平台特定API这是一个经典的组合拳。FFmpeg音视频领域的“瑞士军刀”。它负责最复杂的部分封装格式的解析与生成MP4 AVI、编解码H.264 AAC、滤镜处理等。在我们的项目里它主要扮演“解码”和“格式转换”的角色虽然采集阶段我们用了更底层的API但FFmpeg的数据结构AVFrame、AVPacket是贯穿始终的通用语言。SDL2 (Simple DirectMedia Layer)一个跨平台的多媒体库封装了音频、视频、输入设备的底层操作。我们主要用它来播放。SDL2的音频回调机制和视频渲染接口非常简洁高效能让我们专注于填充数据而不必深陷于Windows GDI或DirectSound的细节中。DirectShow (Windows) / AVFoundation (macOS) / V4L2 (Linux)用于采集。我们选择平台原生API而不是FFmpeg的avdevice原因有两个一是控制更精细能更好地获取设备列表、设置分辨率帧率二是性能更优延迟更低。本教程以Windows的DirectShow为例因为它应用最广。2.2 详细环境搭建步骤Windows Visual Studio 2022这里我会给出非常具体的步骤避免你在配置环节踩坑。第一步安装和编译FFmpeg我不推荐直接下载预编译的二进制包因为版本和编译选项可能不符合我们的需求。我们从源码编译虽然耗时但最可控。安装MSYS2去官网下载安装MSYS2。这是一个在Windows上提供Linux-like环境的工具我们用它来运行编译脚本。打开MSYS2 MINGW64从开始菜单打开MSYS2 MINGW64注意不是MSYS2 UCRT64或MSYS2 CLANG64我们要用MinGW-w64的GCC编译器。安装依赖在MSYS2终端中依次执行以下命令pacman -Syu # 更新系统 pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake git make nasm pacman -S mingw-w64-x86_64-SDL2 # 顺便把SDL2也装了下载FFmpeg源码git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg cd ffmpeg配置与编译我们将编译一个静态库版本方便链接。./configure \ --prefix/mingw64/ffmpeg-build \ --enable-static \ --disable-shared \ --enable-gpl \ --enable-nonfree \ --enable-libx264 \ --enable-libx265 \ --enable-libvpx \ --enable-libmp3lame \ --enable-libfdk-aac \ --enable-libopus \ --enable-filterresample \ --disable-programs \ --disable-doc \ --archx86_64 \ --target-osmingw32 \ --cross-prefixx86_64-w64-mingw32- make -j8 # -j后面的数字根据你CPU核心数来加快编译速度 make install这个过程会比较长可能持续半小时到一小时。编译完成后你需要的头文件.h和静态库文件.a在Windows下相当于.lib就在/mingw64/ffmpeg-build目录下了。第二步准备SDL2由于我们通过MSYS2的pacman安装了SDL2它的开发文件头文件和库默认就在/mingw64/include/SDL2和/mingw64/lib目录下。这非常方便。第三步创建Visual Studio项目并配置打开VS2022创建一个新的“控制台应用”项目比如叫AVCapturePlayback。配置包含目录右键项目 - 属性 - C/C - 常规 - 附加包含目录添加D:\msys64\mingw64\ffmpeg-build\include D:\msys64\mingw64\include\SDL2请将D:\msys64替换为你的MSYS2实际安装路径配置库目录属性 - 链接器 - 常规 - 附加库目录添加D:\msys64\mingw64\ffmpeg-build\lib D:\msys64\mingw64\lib配置链接库属性 - 链接器 - 输入 - 附加依赖项添加一大堆.lib文件。这是最繁琐的一步因为FFmpeg库很多。一个相对完整的列表如下注意顺序有时很重要avcodec.lib avformat.lib avutil.lib avdevice.lib avfilter.lib swresample.lib swscale.lib postproc.lib SDL2.lib SDL2main.lib配置运行时库和子系统为了和MinGW编译的库兼容我们需要调整一些设置。C/C - 代码生成 - 运行时库改为多线程调试(/MTd)或多线程(/MT)对应Release。这很重要否则可能会链接错误。链接器 - 系统 - 子系统改为控制台 (/SUBSYSTEM:CONSOLE)。注意这是环境搭建中最容易出错的地方。如果编译时提示“找不到xxx符号”大概率是库没链接全或者库的顺序不对或者运行时库不匹配。一个技巧是可以去/mingw64/ffmpeg-build/lib目录下看看有哪些.a文件基本都需要链接进来。SDL2的依赖相对简单。3. 视频采集深入DirectShow框架采集是数据流的源头。在Windows上我们使用DirectShow。别被它的名字吓到我们不需要理解其复杂的COM架构全貌只需要掌握几个关键接口就能完成任务。3.1 DirectShow采集流程精讲DirectShow的工作模式是“过滤器图”Filter Graph。你可以把它想象成一个流水线源头Source Filter如摄像头生产数据中间经过各种处理过滤器Transform Filter如编码器最终到达渲染过滤器Render Filter如屏幕。我们的程序就是搭建和操控这条流水线的工程师。核心步骤和对应的COM接口如下初始化COM库CoInitializeEx(NULL COINIT_APARTMENTTHREADED);DirectShow基于COM必须先初始化。创建设备枚举器使用SystemDeviceEnum组件来枚举系统中的音视频设备。ICreateDevEnum *pDevEnum NULL; CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)pDevEnum); IEnumMoniker *pEnum NULL; pDevEnum-CreateClassEnumerator(CLSID_VideoInputDeviceCategory pEnum, 0);这段代码创建了一个枚举器专门用于枚举“视频输入设备类别”的所有设备。遍历并选择设备通过枚举器获取每一个设备的“标记”Moniker然后可以从标记中获取设备的友好名称如“Integrated Webcam”和创建对应的过滤器。IMoniker *pMoniker NULL; while (pEnum-Next(1 pMoniker, NULL) S_OK) { IPropertyBag *pPropBag; pMoniker-BindToStorage(0, 0, IID_IPropertyBag, (void**)pPropBag); VARIANT varName; VariantInit(varName); pPropBag-Read(LFriendlyName varName, 0); // 这里可以打印或比较 varName.bstrVal 选择你想要的设备 // ... pPropBag-Release(); pMoniker-Release(); }构建过滤器图这是核心。我们需要pMoniker-BindToObject(... IID_IBaseFilter, (void**)pSrcFilter)创建源头过滤器。创建“样本抓取过滤器”Sample Grabber Filter。这个过滤器是关键它不修改数据但允许我们注册一个回调函数每当有一帧新数据到来时就调用我们的函数。这是我们获取视频数据的“钩子”。创建“空渲染过滤器”Null Renderer Filter。数据最终需要有个去处我们不显示所以用一个“空渲染器”来接收并丢弃数据让流水线能跑起来。使用IGraphBuilder接口将这些过滤器添加到图中并用ICaptureGraphBuilder2接口智能地连接它们。设置回调与格式在样本抓取过滤器上我们需要设置它输出的媒体类型如RGB24或YUY2并设置回调接口。运行图调用IMediaControl-Run()整个采集流水线就开始工作了。我们的回调函数会不断被调用里面就能拿到一帧帧的视频数据。3.2 关键代码解析与避坑指南在设置样本抓取器的回调时有一个非常重要的细节线程模型。DirectShow的过滤器图运行在自己的线程里我们的回调函数也是在这个线程上下文中被调用的。这意味着不要在回调里做耗时操作比如复杂的图像处理、文件写入。这会阻塞整个采集流水线导致帧率下降甚至图停止。正确的做法是在回调里只做最必要的事情——将数据通常是BYTE*指针和长度拷贝到我们程序准备好的一个线程安全的缓冲区如环形队列中然后立即返回。由另一个专门的线程从这个缓冲区里取数据去做后续处理显示、编码等。数据格式转换摄像头采集的原始数据格式五花八门常见的有MJPG压缩格式、YUY2、NV12、RGB24。我们的播放器SDL2通常需要RGB24或YUV的某种特定格式如YUV420P。因此在回调函数里拿到数据后我们可能需要用FFmpeg的sws_scale函数进行格式转换。MJPG格式还需要先用FFmpeg的avcodec_decode_video2旧API或avcodec_send_packet/avcodec_receive_frame新API先解码成YUV。实操心得一开始调试时可以在回调函数里简单地将第一帧数据保存为.ppm或.bmp文件这两种格式头很简单看看是否能成功保存和打开这是验证采集是否成功的最直接方法。另外务必检查Sample Grabber设置的媒体类型是否与摄像头实际输出的类型匹配不匹配会导致连接失败。4. 音频采集WASAPI的低延迟之道音频采集我们选择WASAPIWindows Audio Session API它是Vista之后微软推出的新一代音频架构相比古老的WaveXXX API或DirectSound它提供了更低延迟、更精确的控制并且支持独占模式。4.1 WASAPI采集模式详解WASAPI有两种主要模式共享模式多个应用共享同一个音频设备由系统混音器进行混音。延迟稍高但兼容性好。独占模式应用独占音频设备直接与硬件驱动通信延迟极低。但其他应用将无法发声。对于采集我们通常使用共享模式就够了。核心接口是IMMDeviceEnumerator、IMMDevice和IAudioClient。采集流程如下枚举音频设备通过IMMDeviceEnumerator获取默认的录音设备或者枚举所有设备供用户选择。激活音频客户端从设备对象上获取IAudioClient接口。初始化音频客户端调用IAudioClient::Initialize。这里需要指定一个WAVEFORMATEX结构体描述我们想要的音频格式采样率、位深、声道数。我们可以先调用IAudioClient::GetMixFormat获取设备支持的默认格式然后基于这个格式来初始化成功率最高。WAVEFORMATEX *pwfx NULL; audioClient-GetMixFormat(pwfx); // 获取设备混合格式 // 可以修改pwfx中的参数比如将pwfx-wFormatTag改为WAVE_FORMAT_PCM audioClient-Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK, // 如果要采系统声用这个标志 hnsRequestedDuration, 0, pwfx, NULL);获取采集客户端初始化后通过IAudioClient::GetService获取IAudioCaptureClient接口。这个接口是用来从音频缓冲区中读取数据的。启动采集调用IAudioClient::Start()。循环读取数据在一个独立线程中循环调用IAudioCaptureClient::GetBuffer来获取可用的音频数据块处理数据比如放入环形缓冲区然后调用IAudioCaptureClient::ReleaseBuffer释放缓冲区。4.2 音频与视频的同步挑战音视频分开采集就带来了一个经典难题同步。摄像头和麦克风是两个独立的硬件它们的时钟源不同会产生微小的漂移。如果简单地将采集到的音频包和视频帧按各自的时间戳播放几分钟后可能就会“音画不同步”。解决思路我们需要一个主时钟。通常选择音频时钟作为主时钟因为人耳对声音的延迟和抖动比眼睛更敏感。具体做法是记录参考时间在采集到第一帧音频和视频时记录下系统参考时间如QueryPerformanceCounter。计算呈现时间戳对于后续的每一帧数据根据其采集顺序和采样率音频或帧率视频计算出相对于参考时间的“呈现时间戳”PTS。在播放端进行同步播放线程在渲染时不仅要根据数据本身的PTS还要对比当前的音频播放进度主时钟动态调整视频帧的显示时机。如果视频快了就多等一会儿如果慢了就丢弃一些非关键帧来追赶。注意事项在项目初期为了简化我们可以先不做复杂的同步而是采用“尽力而为”的方式即采集到一帧就立刻送去播放。这在小窗口、本地演示时可能看不出大问题但一旦涉及网络传输或长时间运行不同步的问题就会非常明显。同步逻辑是音视频播放器的核心难点之一值得单独花时间设计。5. 音视频播放SDL2的渲染艺术采集到的数据最终要呈现给用户SDL2在这里扮演了渲染引擎的角色。它抽象了不同操作系统的窗口、图形和音频接口让我们用一套代码就能实现跨平台播放。5.1 SDL2视频播放实现SDL2播放视频的核心是创建一个窗口和渲染器Renderer然后不断将图像数据更新到纹理Texture上最后将纹理复制到渲染器进行显示。步骤拆解初始化SDL视频子系统SDL_Init(SDL_INIT_VIDEO)。创建窗口和渲染器SDL_Window *window SDL_CreateWindow(AV Player, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_SHOWN); SDL_Renderer *renderer SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);创建纹理纹理的格式必须和我们视频数据的格式匹配。例如如果视频数据是YUV420P我们就创建SDL_PIXELFORMAT_IYUV的纹理。SDL_Texture *texture SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, width, height);SDL_TEXTUREACCESS_STREAMING表示我们会频繁地更新这个纹理的内容。渲染循环在主循环中我们需要做以下几件事从我们自己的视频帧缓冲区比如一个线程安全的队列中取出一帧。如果格式不匹配比如采集的是RGB但纹理是YUV用FFmpeg的sws_scale进行转换。更新纹理SDL_UpdateTexture(texture, NULL, frame_data, frame_linesize)。frame_linesize是每一行的字节数对于YUV420Pframe_data是一个数组包含了Y、U、V三个平面的数据指针。清空渲染器SDL_RenderClear(renderer)。将纹理复制到渲染器SDL_RenderCopy(renderer, texture, NULL, NULL)。呈现到屏幕SDL_RenderPresent(renderer)。控制帧率根据视频的期望帧率如30fps 即每帧33.3ms使用SDL_Delay或更精确的计时器来控制循环速度避免占用100%的CPU。5.2 SDL2音频播放实现SDL2的音频播放采用“回调驱动”模式。我们设置好音频参数采样率、格式、声道数、采样点数并提供一个回调函数。当音频设备需要新的数据时就会自动调用这个函数我们只需在函数里填充音频数据即可。设置流程指定音频参数填充一个SDL_AudioSpec结构体。SDL_AudioSpec wanted_spec, obtained_spec; wanted_spec.freq 44100; // 采样率 wanted_spec.format AUDIO_S16SYS; // 有符号16位系统字节序 wanted_spec.channels 2; // 立体声 wanted_spec.silence 0; wanted_spec.samples 1024; // 每次回调期望的采样点数必须是2的幂 wanted_spec.callback audio_callback; // 你的回调函数指针 wanted_spec.userdata audio_data; // 传递给回调的用户数据通常是你音频缓冲区的结构体指针打开音频设备SDL_OpenAudio(wanted_spec obtained_spec)。obtained_spec是SDL实际打开的音频参数可能与wanted_spec略有不同后续操作应以obtained_spec为准。启动播放SDL_PauseAudio(0)。参数0表示取消暂停开始播放。实现回调函数void audio_callback(void *userdata, Uint8 *stream, int len) { AudioBuffer *audio_buf (AudioBuffer*)userdata; // 从 audio_buf一个环形缓冲区中读取最多 len 字节的数据复制到 stream 中。 // 如果数据不够用静音silence填充剩余部分。 int bytes_copied ring_buffer_read(audio_buf, stream, len); if (bytes_copied len) { memset(stream bytes_copied, obtained_spec.silence, len - bytes_copied); // 这里可以记录一个 underrun 事件表示音频数据供给跟不上消耗可能视频太快了。 } }这个回调函数运行在SDL的音频线程中所以对共享音频缓冲区的操作必须是线程安全的。音画同步在播放端的实现我们之前提到了同步问题。在播放循环中我们需要维护一个音频时钟。这个时钟可以简单地定义为audio_clock first_audio_pts (total_samples_played / sample_rate)。在渲染视频帧时我们比较视频帧的PTS和当前的audio_clock。如果视频PTS比音频时钟慢了超过阈值如100ms就丢弃当前帧去取下一帧丢帧追赶。如果视频PTS比音频时钟快了如超过40ms就通过SDL_Delay稍微等待一下再显示当前帧。6. 核心环节整合与数据流设计现在我们有了三个独立的模块视频采集线程、音频采集线程、音视频播放主线程。如何让它们高效、安全地协同工作是项目成败的关键。这里我分享一个经过实践检验的架构设计。6.1 线程间通信与缓冲区设计绝不能在不同的线程中直接操作同一块内存。我们必须使用线程安全的缓冲区作为数据交换的“中转站”。视频帧缓冲区推荐使用一个固定大小的环形队列Circular Buffer队列中的每个元素是一帧完整的视频数据可能包含YUV三个平面的指针、宽度、高度、格式、PTS等信息。采集线程是生产者向队尾写入播放线程是消费者从队头读取。为什么用环形队列因为它能高效利用预分配的内存避免频繁的malloc/free。当队列满时采集线程可以选择丢弃最旧的一帧对于实时预览丢帧比卡住好当队列空时播放线程可以等待或重复上一帧。实现要点使用互斥锁std::mutex和条件变量std::condition_variable来保护队列。采集线程在写入后notify_one播放线程在读取前wait。队列大小可以根据帧率和延迟要求来设定比如存2秒的数据30fps * 2 60帧。音频采样缓冲区同样使用环形队列但存储的是原始的PCM采样数据char*或short*。因为音频数据是连续的流不像视频有明确的“帧”边界。我们按“包”来存储每个包包含一定数量的采样点例如1024个采样点。注意音频的写入采集和读取SDL回调频率非常高。因此缓冲区的锁竞争必须非常轻量级。可以考虑使用无锁队列Lock-free Queue或者使用双缓冲区交换Double Buffering技术来进一步降低延迟。6.2 主程序控制流一个清晰的主程序流程如下初始化初始化SDL、FFmpeg网络库avformat_network_init、COM库。启动采集线程分别创建视频采集线程和音频采集线程。这两个线程初始化各自的硬件设备DirectShow WASAPI开始采集并将数据放入对应的全局环形缓冲区。初始化播放器创建SDL窗口、渲染器、纹理打开SDL音频设备。进入主事件循环bool quit false; SDL_Event event; while (!quit) { // 1. 处理SDL事件如退出、窗口大小改变 while (SDL_PollEvent(event)) { if (event.type SDL_QUIT) { quit true; } } // 2. 检查视频缓冲区如果有新帧则更新纹理并渲染包含同步逻辑 present_video_frame(); // 3. 音频播放由SDL音频回调自动驱动我们只需保证音频缓冲区有数据。 // 4. 可以在这里做一些UI状态更新如显示帧率、缓冲大小等。 SDL_Delay(1); // 让出一点CPU时间 }清理退出设置退出标志通知采集线程停止。等待采集线程结束。关闭SDL窗口和音频设备。释放所有COM接口、FFmpeg资源、缓冲区内存。反初始化COM库。实操心得调试多线程程序时日志非常重要。在每个关键步骤如采集回调、入队、出队、渲染打上带时间戳和线程ID的日志能帮你快速定位是哪个环节出现了延迟或阻塞。可以使用std::cout但要注意线程安全最好用一个包装函数加锁输出或者用更专业的日志库。7. 常见问题排查与性能优化即使按照教程一步步做你也一定会遇到各种奇怪的问题。这里我总结了一些典型的“坑”和解决办法。7.1 编译与链接问题速查表问题现象可能原因解决方案链接错误LNK2001: 无法解析的外部符号 av_register_allFFmpeg库未正确链接或链接顺序不对或使用的是旧版FFmpeg4.0以后av_register_all被废弃1. 检查“附加依赖项”里是否包含了avcodec.lib等。2. 调整库顺序基础库如avutil放前面。3. 如果使用FFmpeg 4.0删除调用av_register_all()的代码。链接错误LNK2019: 无法解析的外部符号 SDL_InitSDL2库未链接或链接的是SDL1.2的库确保链接了SDL2.lib和SDL2main.lib并且包含目录指向的是SDL2。运行时崩溃0xC0000005: 访问冲突使用了未初始化的指针、野指针或跨DLL内存分配/释放问题。1. 检查所有指针是否在new/malloc后和delete/free前有效。2. 确保FFmpeg/SDL2和你的程序使用相同的运行时库/MT或/MD。这是最常见的原因必须统一。程序运行后立即退出黑框一闪而过可能是COM库初始化失败或SDL创建窗口失败但程序没有等待或报错。在main函数开头和关键API调用后添加getchar()或system(“pause”)暂停并检查每个函数调用的返回值HRESULT或NULL。7.2 运行时问题与优化技巧问题采集帧率很低或者播放卡顿检查1回调函数耗时。在DirectShow或WASAPI的回调函数里是否做了太多事情确保只是拷贝数据到缓冲区。检查2缓冲区大小。视频环形缓冲区是否太小生产者和消费者速度不匹配时缓冲区能起到平滑作用。太大则增加延迟太小容易卡顿。从2秒的容量开始调整。检查3格式转换开销。如果采集格式如MJPG与播放格式RGB24不同需要在回调里进行解码和转换这个计算量很大。考虑1尝试设置采集为RGB24或YUV格式避免解码2将格式转换放到播放线程去做分摊压力3使用FFmpeg的硬件加速解码如h264_cuvid。检查4SDL渲染效率。确保创建渲染器时使用了SDL_RENDERER_ACCELERATED标志并且纹理更新使用SDL_UpdateTexture而不是SDL_LockTexture/SDL_UnlockTexture后者更慢。问题音画不同步越来越严重原因音频和视频的时钟基准不同存在微小漂移。解决必须实现以音频为基准的同步逻辑。如第5.2节所述在播放端动态调整视频帧的显示时机。一个简单的起步方案是计算当前视频帧PTS与音频时钟的差值如果视频落后超过MAX_AUDIO_DIFF如0.1秒就丢帧如果视频超前超过MAX_VIDEO_DIFF如0.04秒就延迟显示。问题CPU占用率过高优化1降低采集分辨率/帧率。对于预览640x48030fps已经足够清晰流畅。优化2使用SDL的延迟。在主渲染循环中不要死循环用SDL_Delay(1)让出CPU时间片。优化3检查忙等待。确保消费者线程在缓冲区空时是在条件变量上wait而不是while(isEmpty) {}这样的忙等待。优化4使用硬件加速。探索使用Intel Media SDK、NVIDIA NVDEC或FFmpeg的硬件解码器来处理H.264/H.265编码流能极大降低CPU负载。问题内存泄漏工具使用Visual Studio的诊断工具调试 - 性能探测器 - 内存使用率或ValgrindLinux来检测。重点检查所有CoCreateInstance返回的接口、av_malloc/av_frame_alloc分配的内存、SDL创建的资源是否都有对应的Release/av_free/av_frame_free/SDL_DestroyXXX调用。确保所有执行路径包括异常路径都能正确释放资源。使用RAII资源获取即初始化思想用C的智能指针或自定义包装类来管理这些资源是避免泄漏的最佳实践。最后我想说的是音视频编程是一个对细节要求极高的领域一个参数设置不对一个回调函数里多了一句日志都可能导致整个流程崩溃。从能跑到稳定、高效、低延迟需要大量的测试、调试和迭代。这个教程给你搭好了骨架并指出了关键器官的位置和功能但要让这个“身体”健壮地跑起来还需要你亲手去填充血肉去解决那些独一无二的具体问题。当你成功的那一刻你会对“数据流”、“实时系统”、“多线程”这些概念有刻骨铭心的理解这远不是调用一个高级API所能比拟的。