1.头文件ffmpegOApi.h:#ifndef FFMPEG_O_H #define FFMPEG_O_H #include stdint.h // 跨平台导出宏定义 #ifdef _WIN32 #define CAM_API extern C __declspec(dllexport) #else #define CAM_API extern C __attribute__((visibility(default))) #endif // 统一错误码定义 /** * brief 错误码枚举C/C 共用 * 负数初始化相关错误 * 正数运行时相关错误 * 0成功 */ typedef enum { // 通用成功 CAMERA_RECORD_SUCCESS 0, // 初始化错误负数 CAMERA_RECORD_ERR_FFMPEG_NOT_FOUND -1, // FFmpeg 未找到 CAMERA_RECORD_ERR_INVALID_PARAM -2, // 初始化参数错误宽/高/帧率≤0 CAMERA_RECORD_ERR_EMPTY_CAM_NAME -3, // 摄像头名称为空 CAMERA_RECORD_ERR_NO_AVAILABLE_HANDLE -4,// 无可用句柄超过 MAX_CAM 限制 // 运行时错误正数 CAMERA_RECORD_ERR_INVALID_HANDLE -5, // 句柄无效≤0 或未初始化 CAMERA_RECORD_ERR_ALREADY_RECORDING -6, // 已在录制中 CAMERA_RECORD_ERR_FFMPEG_START_FAILED -7,// FFmpeg 进程启动失败 CAMERA_RECORD_ERR_NOT_MP4_FORMAT -8, // 保存路径非 MP4 格式后缀不是 .mp4 CAMERA_RECORD_ERR_NOT_RECORDING -9, // 未在录制中调用 Stop 时 CAMERA_RECORD_ERR_CAM_NOT_AVAILABLE -10, // 摄像头不可用无法打开 CAMERA_RECORD_ERR_SYSTEM-11 } CameraRecordErr; /** * brief 获取错误描述C 风格接口C# 可调用 * param err 错误码CameraRecordErr 枚举值 * return 错误描述字符串无需手动释放 */ CAM_API const char* CameraRecord_GetErrDesc(int err); /** * brief 初始化录制器通过摄像头名称精准指定设备 * param camName 摄像头名称不同平台格式不同 * - Windows: videoUSB Camera需和dshow枚举结果一致 * - Linux: /dev/video1 * - macOS: 1摄像头索引 * param width 录制宽度 * param height 录制高度 * param fps 帧率 * return 录制器句柄0成功负数错误码 CameraRecordErr */ CAM_API int CameraRecord_Init(const char* camName, int width, int height, int fps,uint32_t maxTime_s); /** * brief 开始录制视频强制MP4格式 * param handle 录制器句柄CameraRecord_Init 返回的值 * param savePath 视频保存路径必须是.mp4后缀如d:/test.mp4 * return 0成功正数错误码 CameraRecordErr */ CAM_API int CameraRecord_Start(int handle, const char* savePath); /** * brief 结束录制视频 * param handle 录制器句柄 * return 0成功正数错误码 CameraRecordErr */ CAM_API int CameraRecord_Stop(int handle); /** * brief 释放录制器资源 * param handle 录制器句柄 * return 0成功正数错误码 CameraRecordErr */ CAM_API int CameraRecord_Release(int handle); /** * brief 获取一帧保存为图片支持jpg/png/bmp自动识别后缀 * param handle 录制器句柄 * param savePath 图片保存路径如d:/frame.jpg、d:/frame.png、d:/frame.bmp * return 0成功正数错误码 CameraRecordErr */ CAM_API int GetFrames(int handle, const char* savePath); #endif // CAMERA_RECORD_H2.源文件ffmpegOApi.cpp#include ffmpegOApi.h #include cstdio #include string #include mutex #include algorithm #include filesystem #include iostream #include sstream #include cerrno #include cstring #ifdef _WIN32 #include windows.h #else #include unistd.h #include sys/wait.h #include signal.h #include fcntl.h #include sys/stat.h #endif namespace fs std::filesystem; constexpr int MAX_CAM 16; struct CameraRecorder { bool inited false; bool recording false; std::string camName; int width 0; int height 0; int fps 0; std::string fontFile; //字体路径 std::string savePath; //录制的路径 uint32_t maxTime_s 60; #ifdef _WIN32 HANDLE hProcess nullptr; HANDLE hStdinWrite nullptr; #else pid_t ffmpeg_pid -1; FILE* ffmpeg_stdin nullptr; #endif }; static CameraRecorder g_rec[MAX_CAM] {}; static std::mutex g_global_mtx; // 错误描述 const char* CameraRecord_GetErrDesc(int err) { switch (err) { case CAMERA_RECORD_SUCCESS: return 成功; case CAMERA_RECORD_ERR_FFMPEG_NOT_FOUND: return ffmpeg 不在 PATH 中; case CAMERA_RECORD_ERR_INVALID_PARAM: return 参数无效 (宽高帧率≤0); case CAMERA_RECORD_ERR_EMPTY_CAM_NAME: return 摄像头名称为空; case CAMERA_RECORD_ERR_NO_AVAILABLE_HANDLE: return 超过最大设备数; case CAMERA_RECORD_ERR_INVALID_HANDLE: return 句柄无效; case CAMERA_RECORD_ERR_ALREADY_RECORDING: return 已在录制; case CAMERA_RECORD_ERR_FFMPEG_START_FAILED: return ffmpeg 启动失败; case CAMERA_RECORD_ERR_NOT_MP4_FORMAT: return 不是 .mp4 路径; case CAMERA_RECORD_ERR_NOT_RECORDING: return 未在录制; case CAMERA_RECORD_ERR_CAM_NOT_AVAILABLE: return 摄像头不可用; case CAMERA_RECORD_ERR_SYSTEM: return 停止进程错误; default: return 未知错误; } } // 检查 FFmpeg static bool check_ffmpeg() { #ifdef _WIN32 // Windows 方案通过 %errorlevel% 获取退出码双重验证 char cmd_buf[256] { 0 }; // 执行ffmpeg -version并重定向输出捕获退出码 sprintf(cmd_buf, ffmpeg -version NUL 21 echo 1 || echo 0); FILE* fp _popen(cmd_buf, r); if (!fp) { return false; } char result[4] { 0 }; fgets(result, sizeof(result), fp); int exit_code atoi(result); // 1成功0失败 _pclose(fp); // 只有退出码为1ffmpeg执行成功才返回true return exit_code 1; #else // Linux 方案通过 pclose 返回值获取退出码 FILE* fp popen(ffmpeg -version /dev/null 21, r); if (!fp) { return false; } // pclose 返回值是子进程退出码WEXITSTATUS 解析出真实码0成功非0失败 int pclose_ret pclose(fp); bool success (WEXITSTATUS(pclose_ret) 0); return success; #endif } // 查找字体 static std::string find_first_ttf() { try { for (const auto entry : fs::directory_iterator(fs::current_path())) { if (!entry.is_regular_file()) continue; auto ext entry.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if (ext .ttf) { return entry.path().filename().string(); } } } catch (...) {} return ; } // 分配句柄 static int alloc_handle() { std::lock_guardstd::mutex lock(g_global_mtx); for (int i 1; i MAX_CAM; i) { if (!g_rec[i].inited) return i; } return CAMERA_RECORD_ERR_NO_AVAILABLE_HANDLE; } // 验证句柄 static bool is_handle_valid(int h) { if (h 1 || h MAX_CAM) return false; std::lock_guardstd::mutex lock(g_global_mtx); return g_rec[h].inited; } // 验证 MP4 路径 static bool is_mp4(const char* path) { if (!path) return false; std::string s(path); if (s.size() 4) return false; std::string ext s.substr(s.size() - 4); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); return ext .mp4; } // 验证图片路径 static bool is_image_supported(const char* path) { if (!path) return false; std::string s(path); std::transform(s.begin(), s.end(), s.begin(), ::tolower); if (s.size() 4) { std::string e4 s.substr(s.size() - 4); if (e4 .jpg || e4 .png || e4 .bmp) return true; } if (s.size() 5) { std::string e5 s.substr(s.size() - 5); if (e5 .jpeg) return true; } return false; } // 构建录制命令关键修改添加MP4索引优化参数 std::string join_args(const std::vectorstd::string args) { std::ostringstream oss; for (size_t i 0; i args.size(); i) { oss args[i]; // 最后一个参数后不加空格 if (i ! args.size() - 1) { oss ; } } return oss.str(); } std::vectorstd::string build_record_cmd(const CameraRecorder r) { std::vectorstd::string cmd; cmd.push_back(ffmpeg); cmd.push_back(-f); #ifdef _WIN32 cmd.push_back(dshow); cmd.push_back(-i); cmd.push_back(video\ r.camName \); cmd.push_back(-vf); if (!r.fontFile.empty()) { cmd.push_back(\drawtextfontfile\./r.fontFile\:text%{localtime\\\\:%Y/%m/%d %H-%M-%S}:fontsize24:x20:y20:fontcolorwhite:shadowcolorblack:shadowx2:shadowy2\); } else { cmd.push_back(\drawtexttext%{localtime\\\\:%Y/%m/%d %H-%M-%S}:fontsize24:x20:y20:fontcolorwhite:shadowcolorblack:shadowx2:shadowy2\); } #else cmd.push_back(v4l2); cmd.push_back(-input_format); cmd.push_back(mjpeg); cmd.push_back(-i); cmd.push_back(r.camName); cmd.push_back(-vf); if (!r.fontFile.empty()) { cmd.push_back(drawtextfontfile\./ r.fontFile \:text%{localtime\\\\:%Y/%m/%d %H-%M-%S}:fontsize24:x20:y20:fontcolorwhite:shadowcolorblack:shadowx2:shadowy2); } else { cmd.push_back(drawtexttext%{localtime\\\\:%Y/%m/%d %H-%M-%S}:fontsize24:x20:y20:fontcolorwhite:shadowcolorblack:shadowx2:shadowy2); } #endif cmd.push_back(-t); cmd.push_back(std::to_string(r.maxTime_s)); cmd.push_back(-s); cmd.push_back(std::to_string(r.width) x std::to_string(r.height)); cmd.push_back(-r); cmd.push_back(std::to_string(r.fps)); cmd.push_back(-c:v); cmd.push_back(libx264); cmd.push_back(-preset); cmd.push_back(fast); cmd.push_back(-crf); cmd.push_back(23); cmd.push_back(-pix_fmt); cmd.push_back(yuv420p); cmd.push_back(-y); cmd.push_back(r.savePath); return cmd; } // Windows: 创建进程仅修改保留stdout管道避免FFmpeg阻塞 #ifdef _WIN32 static bool create_ffmpeg_process(const std::string cmd, HANDLE hProcess, HANDLE hStdinWrite) { HANDLE hStdinRead nullptr; HANDLE hStdoutWrite nullptr; HANDLE hStdoutRead nullptr; SECURITY_ATTRIBUTES sa { sizeof(SECURITY_ATTRIBUTES) }; sa.bInheritHandle TRUE; sa.lpSecurityDescriptor nullptr; // 1. 创建标准输入管道用于发送q指令 if (!CreatePipe(hStdinRead, hStdinWrite, sa, 0)) { return false; } SetHandleInformation(hStdinWrite, HANDLE_FLAG_INHERIT, 0); // 2. 创建标准输出/错误管道避免FFmpeg输出缓冲区满阻塞 CreatePipe(hStdoutRead, hStdoutWrite, sa, 0); SetHandleInformation(hStdoutRead, HANDLE_FLAG_INHERIT, 0); STARTUPINFOA si { sizeof(si) }; si.dwFlags STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.hStdInput hStdinRead; // FFmpeg从管道读输入接收q si.hStdOutput hStdoutWrite; // FFmpeg输出到管道避免阻塞 si.hStdError hStdoutWrite; si.wShowWindow SW_HIDE; PROCESS_INFORMATION pi { 0 }; char* cmd_copy new char[cmd.size() 1]; strcpy_s(cmd_copy, cmd.size() 1, cmd.c_str()); BOOL ret CreateProcessA(NULL, cmd_copy, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, si, pi); delete[] cmd_copy; // 关闭子进程不需要的句柄保留hStdinWrite供发送q CloseHandle(hStdinRead); CloseHandle(hStdoutWrite); CloseHandle(hStdoutRead); if (!ret) { CloseHandle(hStdinWrite); return false; } CloseHandle(pi.hThread); hProcess pi.hProcess; return true; } #endif // 初始化 int CameraRecord_Init(const char* camName, int width, int height, int fps,uint32_t maxTime_s) { if (!camName || *camName 0) return CAMERA_RECORD_ERR_EMPTY_CAM_NAME; if (width 0 || height 0 || fps 0) return CAMERA_RECORD_ERR_INVALID_PARAM; if (!check_ffmpeg()) return CAMERA_RECORD_ERR_FFMPEG_NOT_FOUND; int h alloc_handle(); if (h 0) return h; std::string font find_first_ttf(); std::lock_guardstd::mutex lock(g_global_mtx); g_rec[h].inited true; g_rec[h].camName camName; g_rec[h].width width; g_rec[h].height height; g_rec[h].fps fps; g_rec[h].fontFile std::move(font); g_rec[h].recording false; g_rec[h].maxTime_s maxTime_s; #ifdef _WIN32 g_rec[h].hProcess nullptr; g_rec[h].hStdinWrite nullptr; #else g_rec[h].ffmpeg_pid -1; g_rec[h].ffmpeg_stdin nullptr; #endif return h; } // 开始录制 int CameraRecord_Start(int h, const char* savePath) { if (!is_handle_valid(h)) return CAMERA_RECORD_ERR_INVALID_HANDLE; if (!is_mp4(savePath)) return CAMERA_RECORD_ERR_NOT_MP4_FORMAT; auto rec g_rec[h]; std::lock_guardstd::mutex lock(g_global_mtx); if (rec.recording) return CAMERA_RECORD_ERR_ALREADY_RECORDING; rec.savePath savePath; std::vectorstd::string args build_record_cmd(rec); std::string cmd_str join_args(args); std::cout 开始录制命令: cmd_str std::endl; #ifdef _WIN32 // Windows分支保持原有逻辑如果可用 if (!create_ffmpeg_process(cmd_str, rec.hProcess, rec.hStdinWrite)) { rec.hProcess nullptr; rec.hStdinWrite nullptr; return CAMERA_RECORD_ERR_FFMPEG_START_FAILED; } std::cout FFmpeg 进程已启动句柄 rec.hProcess std::endl; rec.recording true; std::cout 录制已开始 std::endl; return CAMERA_RECORD_SUCCESS; #else // Linux 正确实现forkexecvp 启动FFmpeg //执行FFmpeg子进程会替换为FFmpeg进程 char* argv[args.size() 1]; // 数组大小参数数1留nullptr位置 for (int i 0; i args.size(); i) { argv[i] strdup(args[i].c_str()); // 拷贝字符串到堆内存 } argv[args.size()] nullptr; // 最后补nullptrexecvp必须 pid_t ffmpeg_pid fork(); if (ffmpeg_pid -1) { return CAMERA_RECORD_ERR_FFMPEG_START_FAILED; } else if (ffmpeg_pid 0) { int dev_null open(/dev/null, O_WRONLY); // 打开空设备 if (dev_null 0) { dup2(dev_null, STDOUT_FILENO); // 重定向stdout到/dev/null dup2(dev_null, STDERR_FILENO); // 重定向stderr到/dev/null close(dev_null); // 关闭原文件描述符已复制无需保留 } execvp(ffmpeg, argv); std::cerr execvp失败 strerror(errno) std::endl; exit(1); } else { rec.ffmpeg_pid ffmpeg_pid; rec.recording true; std::cout FFmpeg 进程已启动PID rec.ffmpeg_pid std::endl; return CAMERA_RECORD_SUCCESS; } #endif } // 停止录制 int CameraRecord_Stop(int h) { if (!is_handle_valid(h)) return CAMERA_RECORD_ERR_INVALID_HANDLE; auto rec g_rec[h]; std::lock_guardstd::mutex lock(g_global_mtx); if (!rec.recording) return CAMERA_RECORD_ERR_NOT_RECORDING; std::cout 正在优雅停止 FFmpeg... std::endl; int ret_code CAMERA_RECORD_SUCCESS; #ifdef _WIN32 // Windows分支保持不变 if (rec.hStdinWrite ! nullptr) { const char quit_cmd[] q\n; DWORD written 0; WriteFile(rec.hStdinWrite, quit_cmd, sizeof(quit_cmd) - 1, written, NULL); CloseHandle(rec.hStdinWrite); rec.hStdinWrite nullptr; } if (rec.hProcess ! nullptr) { WaitForSingleObject(rec.hProcess, 5000); TerminateProcess(rec.hProcess, 0); CloseHandle(rec.hProcess); rec.hProcess nullptr; } #else // Linux分支操作真实FFmpeg PID if (rec.ffmpeg_pid 0) { // 1. 关闭标准输入 if (rec.ffmpeg_stdin ! nullptr) { fclose(rec.ffmpeg_stdin); rec.ffmpeg_stdin nullptr; } // 2. 优雅停止SIGINT errno 0; if (kill(rec.ffmpeg_pid, SIGINT) -1) { if (errno ! ESRCH) { std::cerr 发送SIGINT失败 strerror(errno) std::endl; ret_code CAMERA_RECORD_ERR_SYSTEM; } else { std::cout FFmpeg进程已提前退出 std::endl; } } else { // 3. 等待5秒退出 int waited_ms 0; while (waited_ms 5000) { int wait_ret waitpid(rec.ffmpeg_pid, nullptr, WNOHANG); if (wait_ret rec.ffmpeg_pid) { std::cout FFmpeg优雅退出成功 std::endl; break; } else if (wait_ret 0) { usleep(100 * 1000); waited_ms 100; } else { break; } } // 4. 超时强制终止 if (waited_ms 5000) { std::cerr FFmpeg超时强制终止 std::endl; kill(rec.ffmpeg_pid, SIGKILL); waitpid(rec.ffmpeg_pid, nullptr, 0); } } rec.ffmpeg_pid -1; } #endif rec.recording false; std::cout 录制已停止 std::endl; return ret_code; } // 释放资源无修改 int CameraRecord_Release(int h) { if (!is_handle_valid(h)) return CAMERA_RECORD_ERR_INVALID_HANDLE; auto rec g_rec[h]; if (rec.recording) { CameraRecord_Stop(h); } std::lock_guardstd::mutex lock(g_global_mtx); rec.inited false; rec.recording false; rec.camName.clear(); rec.savePath.clear(); rec.fontFile.clear(); std::cout 录制器已释放 h std::endl; return CAMERA_RECORD_SUCCESS; } // 截图无修改 int GetFrames(int h, const char* savePath) { if (!is_handle_valid(h)) return CAMERA_RECORD_ERR_INVALID_HANDLE; if (!is_image_supported(savePath)) return CAMERA_RECORD_ERR_INVALID_PARAM; auto rec g_rec[h]; std::lock_guardstd::mutex lock(g_global_mtx); if (rec.recording) { return CAMERA_RECORD_ERR_ALREADY_RECORDING; } char cmd[8192] { 0 }; #ifdef _WIN32 snprintf(cmd, sizeof(cmd), ffmpeg -nostdin -f dshow -i video\%s\ -s %dx%d -r %d -vframes 1 \%s\ -y NUL 21, rec.camName.c_str(), rec.width, rec.height, rec.fps, savePath); #else snprintf(cmd, sizeof(cmd), ffmpeg -nostdin -f v4l2 -input_format mjpeg -i %s -s %dx%d -r %d -vframes 1 \%s\ -y /dev/null 21, rec.camName.c_str(), rec.width, rec.height, rec.fps, savePath); #endif std::cout 获取一帧命令: cmd std::endl; int ret system(cmd); return ret 0 ? CAMERA_RECORD_SUCCESS : CAMERA_RECORD_ERR_CAM_NOT_AVAILABLE; }