资讯动态

纯FFmpeg+C++音视频播放器:从解码到OpenGL/Vulkan渲染闭环

发布时间:2026/10/16 13:15:35 来源:尧图企业网站定制
简介这是一份基于FFmpeg 5.1开发的C音视频简易播放器实战项目面向多媒体开发初学者与C进阶学习者旨在帮助读者系统掌握FFmpeg核心API在真实播放器场景中的集成与应用。资源包含294个文件主体为7个关键FFmpeg静态库如libavcodec.dll.a、libavformat.dll.a等、231个头文件含SDL_config.h.cmake等跨平台适配头及8个C源码文件如audio.cpp辅以VS工程文件sln/vcxproj和构建配置整体包体积达83MB结构完整、开箱可编译。已有265人下载学习项目覆盖从FFmpeg初始化、媒体打开与流分析、音视频解码、像素/音频格式转换到SDL渲染与基础控制逻辑的全流程实现代码注释清晰、模块划分合理特别适合通过源码逆向理解播放器底层机制并为后续扩展音视频同步、进度跳转或GUI界面打下坚实基础。1. 一个能跑通的音视频播放器不依赖 Qt/SDL纯 FFmpeg C 实现解码渲染闭环你有没有试过在嵌入式设备或无图形环境里只靠 FFmpeg 自己把 H.264 视频帧喂给 OpenGL 或 Vulkan 渲染或者在 Windows 控制台下用 GDI 把 YUV 数据转 RGB 再贴到窗口上这个项目就是干这个的——它不是封装好的 GUI 播放器而是一份「最小可行闭环」从avformat_open_input开始到BitBltWindows或glTexImage2DLinux/macOS结束中间没有第三方多媒体框架兜底。它适合三类人想真正搞懂 FFmpeg 解封装→解码→色彩空间转换→渲染链路的 C 开发者需要在定制系统中嵌入轻量播放能力的固件工程师以及被各种“FFmpeg 播放器教程”里缺失的sws_scale调用时机、AVFrame生命周期管理、时钟同步抖动问题卡住超过 2 小时的实战派。项目代码不到 1500 行但每行都对应真实播放流程中的一个关键决策点——比如为什么avcodec_receive_frame必须循环调用、为什么pts不能直接当Sleep()参数、为什么sws_getContext的srcW/srcH必须和AVCodecContext-width/height对齐。这不是玩具是我在某跨平台图像处理 Demo 中剥离出的播放内核已稳定运行在 ARM64 Linux 容器和 Windows Server 2019 服务进程中。2. 架构设计与模块职责五个核心模块如何协作完成一帧播放这个播放器采用分层流水线设计不追求线程安全封装而是暴露每个环节的控制权。整个流程可拆解为输入层 → 解封装层 → 解码层 → 转换层 → 渲染层五部分各模块通过裸指针传递AVPacket/AVFrame避免拷贝开销。所有模块均以 C 类封装但接口完全基于 FFmpeg C API确保跨平台可移植性。关键设计原则有三点第一解封装与解码解耦——Demuxer类只负责读包、送包不碰AVCodecContext第二时间轴由渲染层主导——Renderer计算next_pts并反向驱动Decoder的avcodec_send_packet节奏避免音频时钟漂移导致视频卡顿第三内存生命周期显式管理——所有av_frame_alloc()/av_packet_alloc()配套av_frame_free()/av_packet_free()且AVFrame-data[0]的内存由sws_scale分配而非 FFmpeg 自动管理规避av_frame_unref后野指针风险。这种设计牺牲了易用性换来了对每一帧延迟、丢帧策略、色彩精度的绝对控制权——当你需要在 30fps 视频流中强制插入 1 帧诊断画面或在低带宽下动态降级sws_scale的SWS_FAST_BILINEAR算法时这种裸露的控制力就是救命稻草。2.1 输入层Demuxer 类实现文件/URL 流式拉取Demuxer是整个流水线的源头职责明确打开媒体源、探测格式、初始化流索引、持续读取AVPacket。它不关心 packet 是视频还是音频只保证按时间戳顺序输出。核心逻辑在read_packet()方法中// demuxer.cpp int Demuxer::read_packet(AVPacket* pkt) { int ret av_read_frame(format_ctx_, pkt); if (ret 0) { if (ret AVERROR_EOF) { // 文件结束但需清空解码器缓冲区 av_packet_unref(pkt); return AVERROR_EOF; } return ret; } // 关键重映射 pts/dts 到流时间基避免后续计算溢出 if (pkt-pts ! AV_NOPTS_VALUE) { pkt-pts av_rescale_q(pkt-pts, format_ctx_-streams[pkt-stream_index]-time_base, stream_time_base_); } if (pkt-dts ! AV_NOPTS_VALUE) { pkt-dts av_rescale_q(pkt-dts, format_ctx_-streams[pkt-stream_index]-time_base, stream_time_base_); } return 0; }提示stream_time_base_是预设的统一时间基如AVRational{1, 1000}所有流的时间戳都会被重映射至此。这是后续Renderer计算播放时刻的基础——若跳过此步不同流如 H.264 视频流 time_base1/1200AAC 音频流 time_base1/44100的 pts 直接相减会得到错误毫秒值。Demuxer初始化时需手动查找视频流索引并缓存其AVStream*和AVRational time_base。常见误操作是直接用format_ctx_-streams[0]但实际视频流可能在索引 1如 MP4 中音频流在前。正确做法是遍历format_ctx_-nb_streams检查codecpar-codec_type AVMEDIA_TYPE_VIDEO。2.2 解封装层AVFormatContext 初始化与流信息解析Demuxer::open()方法完成AVFormatContext的创建与配置这是整个播放器的“入口签证”。它必须处理三类关键配置协议层选项如 HTTP 超时、格式探测参数、线程模型。代码中显式禁用自动探测format_ctx_-flags | AVFMT_FLAG_NOBUFFER并设置probesize和analyzeduration防止长文件分析超时// demuxer.cpp int Demuxer::open(const char* url) { // 1. 分配上下文 int ret avformat_open_input(format_ctx_, url, nullptr, options_); if (ret 0) return ret; // 2. 强制探测但限制资源消耗 av_dict_set(options_, probesize, 32768, 0); // 最大探测字节数 av_dict_set(options_, analyzeduration, 5000000, 0); // 最大分析微秒数5s av_dict_set(options_, fflags, nobufferfastseek, 0); // 3. 查找流信息关键必须在 open 后调用 ret avformat_find_stream_info(format_ctx_, nullptr); if (ret 0) return ret; // 4. 查找视频流并缓存参数 video_stream_index_ -1; for (int i 0; i format_ctx_-nb_streams; i) { if (format_ctx_-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { video_stream_index_ i; video_stream_ format_ctx_-streams[i]; break; } } if (video_stream_index_ -1) return AVERROR_STREAM_NOT_FOUND; // 5. 设置统一时间基为后续重映射做准备 stream_time_base_ {1, 1000}; // 所有时间戳转为毫秒精度 return 0; }avformat_find_stream_info()是高频翻车点若未调用video_stream_-codecpar中的width/height可能为 0若调用后未检查返回值遇到损坏文件会直接 crash。我一般会在open()结束后加一句av_dump_format(format_ctx_, 0, url, 0)打印流信息到控制台这是最廉价的调试手段——看到Video: h264, yuv420p, 1280x720就说明流识别成功。2.3 解码层Decoder 类管理 AVCodecContext 生命周期Decoder封装AVCodecContext的创建、参数配置、发送/接收帧逻辑。它不持有Demuxer只接收AVPacket*并输出AVFrame*。关键设计是异步解码模式send_packet()不阻塞receive_frame()循环拉取直到AVERROR(EAGAIN)这符合 FFmpeg 3.1 的推荐用法。Decoder构造时需根据AVCodecParameters查找解码器// decoder.cpp int Decoder::init(const AVCodecParameters* codec_par) { const AVCodec* codec avcodec_find_decoder(codec_par-codec_id); if (!codec) return AVERROR_DECODER_NOT_FOUND; ctx_ avcodec_alloc_context3(codec); if (!ctx_) return AVERROR(ENOMEM); // 复制参数关键必须复制不能直接赋值 int ret avcodec_parameters_to_context(ctx_, codec_par); if (ret 0) return ret; // 启用多线程解码根据 CPU 核心数 ctx_-thread_count std::thread::hardware_concurrency(); ctx_-thread_type FF_THREAD_FRAME | FF_THREAD_SLICE; // 打开解码器 ret avcodec_open2(ctx_, codec, nullptr); if (ret 0) return ret; // 分配帧缓冲供 receive_frame 复用 frame_ av_frame_alloc(); if (!frame_) return AVERROR(ENOMEM); return 0; } int Decoder::receive_frame(AVFrame* frame) { int ret avcodec_receive_frame(ctx_, frame); if (ret AVERROR(EAGAIN) || ret AVERROR_EOF) { return ret; } if (ret 0) return ret; // 成功接收一帧拷贝关键元数据 frame-pts av_frame_get_best_effort_timestamp(frame_); return 0; }注意avcodec_parameters_to_context()是必须步骤直接ctx_-width codec_par-width会导致解码失败。av_frame_get_best_effort_timestamp()返回的 pts 已根据AVStream-time_base校准可直接用于渲染调度。2.4 转换层SwsContext 管理 YUV→RGB 色彩空间转换Converter类封装sws_getContext创建与sws_scale调用。它解决的核心问题是FFmpeg 解码输出的AVFrame-data[0]是 YUV420P 格式Planar而 OpenGL/Vulkan/GDI 需要 RGB24 或 RGBA。sws_scale不仅做色彩转换还负责缩放如 1920x1080 → 1280x720。关键参数必须严格匹配// converter.cpp int Converter::init(int src_w, int src_h, AVPixelFormat src_fmt, int dst_w, int dst_h, AVPixelFormat dst_fmt) { // 检查输入尺寸是否被 2 整除YUV420P 要求 if (src_w % 2 ! 0 || src_h % 2 ! 0) { fprintf(stderr, Warning: YUV420P requires even width/height\n); // 实际项目中此处应裁剪或填充本例直接截断 src_w ~1; src_h ~1; } sws_ctx_ sws_getContext( src_w, src_h, src_fmt, dst_w, dst_h, dst_fmt, SWS_BICUBIC, // 算法选择SWS_FAST_BILINEAR快 vs SWS_BICUBIC准 nullptr, nullptr, nullptr ); if (!sws_ctx_) return AVERROR_UNKNOWN; // 分配目标帧缓冲RGB dst_frame_ av_frame_alloc(); if (!dst_frame_) return AVERROR(ENOMEM); // 为 RGB 数据分配内存关键必须用 av_image_alloc int dst_linesize[4]; uint8_t* dst_data[4]; int ret av_image_alloc(dst_data, dst_linesize, dst_w, dst_h, dst_fmt, 1); if (ret 0) return ret; // 将分配的内存绑定到 dst_frame_ av_image_fill_arrays(dst_frame_-data, dst_frame_-linesize, dst_data[0], dst_fmt, dst_w, dst_h, 1); return 0; } int Converter::convert(const AVFrame* src, AVFrame* dst) { const uint8_t* src_data[4] {src-data[0], src-data[1], src-data[2]}; int src_linesize[4] {src-linesize[0], src-linesize[1], src-linesize[2]}; return sws_scale(sws_ctx_, src_data, src_linesize, 0, src-height, dst-data, dst-linesize); }av_image_alloc()是唯一安全的 RGB 缓冲分配方式。若手动malloc()sws_scale可能因内存对齐问题崩溃。SWS_BICUBIC在画质和速度间折中实测比SWS_FAST_BILINEAR多耗 15% CPU但文字边缘锐度提升明显——这对监控画面中的车牌识别至关重要。2.5 渲染层Renderer 类实现跨平台帧显示Renderer是平台相关模块本项目提供 Windows GDI 和 Linux X11 两套实现。核心逻辑是接收AVFrame*RGB 数据将其绘制到窗口 DC/XImage。Windows 版本使用CreateDIBSection创建兼容位图SetDIBits更新像素BitBlt拷贝到窗口// renderer_win.cpp bool Renderer::render(const AVFrame* frame) { if (!hdc_ || !hbitmap_) return false; // 锁定位图内存 void* bits; GetBitmapBits(hbitmap_, frame-height * frame-linesize[0], bits); // 直接 memcpyRGB24 无需转换 memcpy(bits, frame-data[0], frame-height * frame-linesize[0]); // 绘制到位图 SetDIBits(hdc_, hbitmap_, 0, frame-height, frame-data[0], bmi_, DIB_RGB_COLORS); // 拷贝到窗口 BitBlt(hwnd_, 0, 0, frame-width, frame-height, hdc_, 0, 0, SRCCOPY); return true; }SetDIBits和BitBlt的组合比StretchDIBits更稳定——后者在高 DPI 缩放时易出现撕裂。Linux 版本使用 XShm共享内存加速避免XPutImage的内存拷贝开销。Renderer还负责播放时钟同步它维护last_pts_和last_render_time_计算下一帧应等待的毫秒数// renderer.cpp int Renderer::get_delay_ms(int64_t pts) { static int64_t last_pts -1; static int64_t last_render_time 0; if (pts AV_NOPTS_VALUE) { return 40; // 默认 25fps } int64_t now av_gettime() / 1000; // 当前毫秒时间 int64_t target_time last_render_time (pts - last_pts); int64_t delay target_time - now; last_pts pts; last_render_time (delay 0) ? target_time : now; return (delay 0) ? static_castint(delay) : 0; }该算法实现简单播放器的“软同步”虽不如libavdevice的硬件时钟精准但在 CPU 占用率敏感场景如树莓派 4足够可靠。3. 编译与运行CMake 构建全流程及平台适配要点本项目使用 CMake 作为构建系统支持 WindowsMSVC/MinGW、LinuxGCC/Clang、macOSClang。关键在于 FFmpeg 库的链接方式——必须静态链接libavcodec,libavformat,libswscale,libavutil避免运行时 DLL 版本冲突。CMakeLists.txt 的核心配置如下# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(ff_simple_player LANGUAGES CXX) # 查找 FFmpeg要求 pkg-config 可用 find_package(PkgConfig REQUIRED) pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavcodec libavformat libswscale libavutil) # 添加可执行文件 add_executable(ff_simple_player main.cpp demuxer.cpp decoder.cpp converter.cpp renderer_win.cpp # Windows 平台 # renderer_x11.cpp # Linux 平台注释启用 ) # 链接 FFmpeg 库 target_link_libraries(ff_simple_player PkgConfig::FFMPEG) # Windows 特定设置 if(WIN32) target_compile_definitions(ff_simple_player PRIVATE _CRT_SECURE_NO_WARNINGS) target_link_libraries(ff_simple_player gdi32.lib) endif() # Linux 特定设置 if(UNIX AND NOT APPLE) find_package(X11 REQUIRED) target_link_libraries(ff_simple_player ${X11_LIBRARIES}) target_include_directories(ff_simple_player PRIVATE ${X11_INCLUDE_DIR}) endif()3.1 Windows 下编译MSVC 2019 静态 FFmpeg 库在 Windows 上我推荐使用 gyan.dev 提供的静态编译版 FFmpegffmpeg-release-essentials.zip。解压后将ffmpeg-xx\lib加入 CMake 的CMAKE_PREFIX_PATH# 假设 FFmpeg 解压到 D:\ffmpeg mkdir build cd build cmake -G Visual Studio 16 2019 ^ -A x64 ^ -DCMAKE_PREFIX_PATHD:/ffmpeg ^ .. cmake --build . --config Release提示MSVC 链接静态 FFmpeg 时需额外链接ws2_32.lib网络协议和secur32.libHTTPS否则avformat_open_input(https://...)会链接失败。在target_link_libraries中追加即可。生成的ff_simple_player.exe无需任何 DLL双击即可运行。测试命令ff_simple_player.exe test.mp4 ff_simple_player.exe rtsp://192.168.1.100/stream3.2 Linux 下编译GCC 11 pkg-config 自动发现Linux 用户应优先使用系统包管理器安装 FFmpeg 开发库# Ubuntu/Debian sudo apt install ffmpeg libavcodec-dev libavformat-dev \ libswscale-dev libavutil-dev libswresample-dev # CentOS/RHEL sudo yum install ffmpeg-devel然后直接构建mkdir build cd build cmake .. make -j$(nproc)若需支持 RTSP确保ffmpeg编译时启用了--enable-librtspUbuntu 22.04 默认启用。验证方法ffmpeg -protocols | grep rtsp输出rtsp即可。3.3 macOS 下编译Clang Homebrew FFmpegmacOS 用户通过 Homebrew 安装 FFmpegbrew install ffmpegCMake 会自动找到pkg-config路径。注意 macOS 的 OpenGL 渲染需替换renderer_win.cpp为renderer_metal.cpp本项目暂未提供但Converter输出的 RGB 数据可直接喂给 MetalMTLTexture。3.4 常见问题排查5 条血泪经验总结现象程序启动后立即崩溃报错Access violation reading location 0x00000000原因Demuxer::open()中avformat_open_input失败但后续代码未检查返回值直接访问空指针format_ctx_解决在Demuxer::open()每个 FFmpeg API 调用后加if (ret 0) return ret;并在main()中捕获错误码打印av_err2str(ret)现象视频能解码但画面全绿/全紫原因Converter::init()中src_fmt传错如将AV_PIX_FMT_YUV420P误写为AV_PIX_FMT_YUV444P导致sws_scale内存越界解决在Decoder::receive_frame()后打印frame-format确认与Converter::init()的src_fmt一致现象播放 10 秒后卡死CPU 占用 100%原因Decoder::receive_frame()未处理AVERROR(EAGAIN)陷入无限循环调用avcodec_receive_frame解决严格按 FFmpeg 文档逻辑send_packet()后循环receive_frame()直到EAGAIN再send_packet()下一包现象窗口显示黑屏但av_log显示解码正常原因Renderer::render()中BitBlt的源 DC 未正确选入位图或hbitmap_创建时BITMAPINFO的biHeight为正数应为负数表示自顶向下解决Windows GDI 中biHeight必须为负值CreateDIBSection的hSection参数传NULL现象RTSP 流播放 30 秒后断开日志显示Connection timed out原因AVFormatContext未设置超时选项TCP 连接空闲超时解决在Demuxer::open()中添加av_dict_set(options_, stimeout, 30000000, 0); // 30秒超时微秒 av_dict_set(options_, rtsp_transport, tcp, 0); // 强制 TCP4. 核心播放流程代码详解从 main() 到第一帧渲染的完整链路整个播放器的主循环在main.cpp中仅 80 行却覆盖全部关键路径。我们逐段拆解聚焦第一帧如何从文件落到屏幕这一最基础闭环// main.cpp int main(int argc, char* argv[]) { if (argc 2) { fprintf(stderr, Usage: %s input_file\n, argv[0]); return -1; } // 1. 初始化各模块 Demuxer demuxer; Decoder decoder; Converter converter; Renderer renderer; // 2. 打开媒体源 int ret demuxer.open(argv[1]); if (ret 0) { fprintf(stderr, Failed to open %s: %s\n, argv[1], av_err2str(ret)); return -1; } // 3. 初始化解码器使用视频流参数 ret decoder.init(demuxer.get_video_codecpar()); if (ret 0) { fprintf(stderr, Failed to init decoder: %s\n, av_err2str(ret)); return -1; } // 4. 初始化转换器输入解码输出格式输出RGB24 const AVCodecParameters* par demuxer.get_video_codecpar(); ret converter.init(par-width, par-height, static_castAVPixelFormat(par-format), 1280, 720, AV_PIX_FMT_RGB24); if (ret 0) { fprintf(stderr, Failed to init converter: %s\n, av_err2str(ret)); return -1; } // 5. 初始化渲染器传入窗口句柄或 X11 Display ret renderer.init(); // Windows: CreateWindowEx; Linux: XOpenDisplay if (ret 0) return -1; // 6. 主播放循环 AVPacket pkt; av_init_packet(pkt); pkt.data nullptr; pkt.size 0; while (true) { // 步骤 A从 Demuxer 读取一包 ret demuxer.read_packet(pkt); if (ret AVERROR_EOF) break; if (ret 0) continue; // 步骤 B若为视频包送入解码器 if (pkt.stream_index demuxer.get_video_stream_index()) { ret decoder.send_packet(pkt); if (ret 0 ret ! AVERROR(EAGAIN)) { fprintf(stderr, Send packet error: %s\n, av_err2str(ret)); } } av_packet_unref(pkt); // 释放 packet 内存 // 步骤 C从解码器拉取帧 AVFrame* frame av_frame_alloc(); ret decoder.receive_frame(frame); if (ret AVERROR(EAGAIN) || ret AVERROR_EOF) { av_frame_free(frame); continue; } if (ret 0) { av_frame_free(frame); continue; } // 步骤 D转换色彩空间 AVFrame* rgb_frame converter.get_dst_frame(); ret converter.convert(frame, rgb_frame); if (ret 0) { av_frame_free(frame); av_frame_free(rgb_frame); continue; } // 步骤 E渲染到屏幕 int64_t pts frame-pts; renderer.render(rgb_frame); int delay_ms renderer.get_delay_ms(pts); if (delay_ms 0) Sleep(delay_ms); // Windows // 清理 av_frame_free(frame); av_frame_free(rgb_frame); } return 0; }这段代码揭示了 FFmpeg 播放器的黄金五步Readdemuxer.read_packet()获取压缩包Senddecoder.send_packet()将包送入解码器缓冲区Receivedecoder.receive_frame()从缓冲区拉取解码后的帧可能多帧/空帧Convertconverter.convert()将 YUV 转 RGB 并缩放Renderrenderer.render()绘制并Sleep()同步其中最易被忽略的是步骤 B 和 C 的异步性一个pkt可能触发多个frame输出B帧依赖也可能receive_frame()返回EAGAIN解码器缓冲区空。这就是为什么主循环中send_packet和receive_frame是解耦的——它们不是 1:1 关系而是生产者-消费者模型。av_packet_unref(pkt)的位置也极关键必须在send_packet后立即调用否则pkt.data被解码器内部引用av_packet_unref会提前释放内存。这是 FFmpeg 3.1 的新行为旧教程常在此处翻车。5. 二次开发改造指南3 个高频需求的落地实现当你把播放器跑起来后下一步往往是定制化。这里给出三个最常被问到的需求添加音频播放、支持硬解码、注入自定义滤镜。每个方案都给出可直接粘贴的代码片段和避坑提示。5.1 需求一添加基础音频播放SDL2 集成本项目默认只处理视频但音频是播放器的另一半。推荐用 SDL2 实现音频输出因其跨平台且 API 简洁。改造分三步第一步修改 CMakeLists.txt 添加 SDL2find_package(SDL2 REQUIRED) target_link_libraries(ff_simple_player SDL2::SDL2)第二步新增 AudioPlayer 类audio_player.cpp#include SDL2/SDL.h #include SDL2/SDL_audio.h class AudioPlayer { SDL_AudioDeviceID dev_; AVCodecContext* audio_ctx_; SwrContext* swr_ctx_; uint8_t* audio_buf_; size_t audio_buf_size_; public: int init(AVCodecParameters* par) { // 1. 打开 SDL 音频设备44100Hz, stereo, S16 SDL_AudioSpec want {}, have; want.freq 44100; want.format AUDIO_S16SYS; want.channels 2; want.samples 1024; want.callback audio_callback; want.userdata this; dev_ SDL_OpenAudioDevice(nullptr, 0, want, have, 0); if (!dev_) return -1; // 2. 初始化重采样器FFmpeg 音频格式 → SDL 格式 swr_ctx_ swr_alloc_set_opts(nullptr, av_get_default_channel_layout(have.channels), AV_SAMPLE_FMT_S16, have.freq, av_get_default_channel_layout(par-channels), static_castAVSampleFormat(par-format), par-sample_rate, 0, nullptr); if (!swr_ctx_ || swr_init(swr_ctx_) 0) return -1; // 3. 分配音频缓冲区 audio_buf_size_ have.size; audio_buf_ (uint8_t*)av_malloc(audio_buf_size_); return 0; } static void audio_callback(void* userdata, uint8_t* stream, int len) { AudioPlayer* player (AudioPlayer*)userdata; // 此处填入重采样后的 PCM 数据 memset(stream, 0, len); // 占位实际需从 Decoder 拉取 } };第三步在 main() 中集成// main.cpp 新增 AudioPlayer audio_player; if (demuxer.has_audio_stream()) { audio_player.init(demuxer.get_audio_codecpar()); SDL_PauseAudioDevice(audio_player.dev_, 0); // 启动播放 }避坑SDL2 音频回调是独立线程audio_callback中不能调用avcodec_receive_frame线程不安全。正确做法是用std::queueAVFrame*缓存解码帧回调中pop并swr_convert。本例省略队列实现但务必记住音频输出必须异步不能阻塞主播放循环。5.2 需求二启用 NVIDIA NVDEC 硬解码在 Windows/Linux 上启用 GPU 硬解可降低 70% CPU 占用。FFmpeg 4.0 支持cuvidNVIDIA和qsvIntel。以 NVDEC 为例第一步修改 Decoder::init()// decoder.cpp int Decoder::init(const AVCodecParameters* codec_par) { const AVCodec* codec; if (use_hwaccel_) { // 使用 CUDA 硬解 codec avcodec_find_decoder_by_name(h264_cuvid); if (!codec) { fprintf(stderr, CUDA decoder not available\n); use_hwaccel_ false; } } if (!use_hwaccel_) { codec avcodec_find_decoder(codec_par-codec_id); } ctx_ avcodec_alloc_context3(codec); // ... 其他初始化 if (use_hwaccel_) { // 设置硬件设备上下文 AVBufferRef* hw_device_ctx nullptr; av_hwdevice_ctx_create(hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0); ctx_-hw_device_ctx av_buffer_ref(hw_device_ctx); av_buffer_unref(hw_device_ctx); } return avcodec_open2(ctx_, codec, nullptr); }第二步修改 receive_frame() 处理硬件帧int Decoder::receive_frame(AVFrame* frame) { int ret avcodec_receive_frame(ctx_, frame); if (ret 0) return ret; // 若为硬件帧需拷贝到系统内存 if (frame-format AV_PIX_FMT_CUDA) { AVFrame* sw_frame av_frame_alloc(); sw_frame-format AV_PIX_FMT_NV12; // 或 AV_PIX_FMT_YUV420P av_frame_get_buffer(sw_frame, 32); // 拷贝 CUDA 帧到系统内存 ret av_hwframe_transfer_data(sw_frame, frame, 0); if (ret 0) { av_frame_free(sw_frame); return ret; } av_frame_move_ref(frame, sw_frame); av_frame_free(sw_frame); } return 0; }注意启用硬解后Converter::init()的src_fmt需改为AV_PIX_FMT_NV12NVDEC 输出格式且sws_getContext必须支持该格式FFmpeg 编译时需--enable-cuda --enable-cuvid。5.3 需求三在渲染前注入 OpenCV 滤镜很多用户需要在播放时叠加人脸检测、运动跟踪等算法。OpenCV 是最佳选择因其cv::Mat可直接映射AVFrame-data[0]内存// filter_opencv.cpp #include opencv2/opencv.hpp class OpenCVFilter { cv::CascadeClassifier face_cascade_; public: bool init(const char* cascade_path) { return face_cascade_.load(cascade_path); } void apply(AVFrame* frame) { // 将 AVFrame (RGB24) 映射为 cv::Mat cv::Mat mat(frame-height, frame p a hrefhttps://download.csdn.net/download/u011153259/86783157 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑