如何用FFmpeg安全停止RTSP摄像头录像避免文件损坏?
问题描述
我用C++实现了一个调用FFmpeg从RTSP网络摄像头录制视频并保存为.avi文件的类,但终止录制时视频文件经常损坏。需要更安全的停止录制方法。因追求录制效率,选择直接调用FFmpeg进程而非OpenCV或FFmpeg的API。
当前代码
.cpp文件
#include "videorecorder.h" VideoRecorder::VideoRecorder(const std::string& rtspUrl) : url(rtspUrl), recording(false) { } VideoRecorder::~VideoRecorder() { end_record(); } bool VideoRecorder::start_record(const std::string &fileName) { if (recording) { std::cerr << "Already recording." << std::endl; return false; } std::string command = "ffmpeg -rtsp_transport udp -i " + url + " -c:v mjpeg -preset fast -qp 0 " + fileName; videoWriter = popen(command.c_str(), "w"); if (!videoWriter) { std::cerr << "Error opening ffmpeg process." << std::endl; return false; } recording = true; ffmpegProcessId = getpid(); std::cout << "Recording started." << std::endl; return true; } bool VideoRecorder::end_record() { if (recording) { if (videoWriter) { pid_t ffmpegPID = fileno(videoWriter); if (kill(ffmpegPID, SIGTERM) == 0) { std::cout << "Recording terminated successfully." << std::endl; } else { std::cerr << "Error terminating recording." << std::endl; return false; } int status = pclose(videoWriter); if (status == 0) { std::cout << "Recording ended successfully." << std::endl; } else { std::cerr << "Error ending recording. pclose status: " << status << std::endl; return false; } } else { std::cerr << "Error ending recording. videoWriter is nullptr." << std::endl; return false; } recording = false; return true; } return false; }
.h文件
#ifndef VIDEORECORDER_H #define VIDEORECORDER_H #include <string> #include <iostream> #include <fstream> #include <cstdlib> #include <csignal> #include <sys/wait.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> #include <libavutil/opt.h> #include <libswscale/swscale.h> #include <unistd.h> } #include <linux/videodev2.h> #include <opencv2/opencv.hpp> #include <opencv2/videoio.hpp> #include <opencv2/highgui/highgui.hpp> class VideoRecorder { public: VideoRecorder(const std::string& rtspUrl); ~VideoRecorder(); bool start_record(const std::string& fileName); bool end_record(); private: std::string url; AVFormatContext *formatContext; AVStream *videoStream; AVCodecContext *codecContext; AVCodec *codec; SwsContext *swsContext; AVFrame *frame; AVPacket packet; bool recording; pid_t ffmpegProcessId; FILE* videoWriter; }; #endif // VIDEORECORDER_H
录制10秒后的终端输出
Recording started. ffmpeg version 4.3.6-0+deb11u1+rpt5 Copyright (c) 2000-2023 the FFmpeg developers built with gcc 10 (Debian 10.2.1-6) configuration: --prefix=/usr --extra-version=0+deb11u1+rpt5 --toolchain=hardened --incdir=/usr/include/aarch64-linux-gnu --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --disable-mmal --enable-neon --enable-v4l2-request --enable-libudev --enable-epoxy --enable-sand --libdir=/usr/lib/aarch64-linux-gnu --arch=arm64 --enable-pocketsphinx --enable-libdc1394 --enable-libdrm --enable-vout-drm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared libavutil 56. 51.100 / 56. 51.100 libavcodec 58. 91.100 / 58. 91.100 libavformat 58. 45.100 / 58. 45.100 libavdevice 58. 10.100 / 58. 10.100 libavfilter 7. 85.100 / 7. 85.100 libavresample 4. 0. 0 / 4. 0. 0 libswscale 5. 7.100 / 5. 7.100 libswresample 3. 7.100 / 3. 7.100 libpostproc 55. 7.100 / 55. 7.100 Input #0, rtsp, from 'rtsp://admin:[password]@[ip]:[port]/live/0/MAIN': Metadata: title : RTSP Server Duration: N/A, start: 0.280000, bitrate: N/A Stream #0:0: Video: h264 (Main), yuvj420p(pc, bt709, progressive), 1920x1080, 25 fps, 25 tbr, 90k tbn, 50 tbc Codec AVOption preset (Configuration preset) specified for output file #0 (/home/guardian-tech/Pictures/output_frame.avi) has not been used for any stream. The most likely reason is either wrong type (e.g. a video option with no video streams) or that it is a private option of some encoder which was not actually used for any stream. Codec AVOption qp (Constant quantization parameter rate control method) specified for output file #0 (/home/guardian-tech/Pictures/output_frame.avi) has not been used for any stream. The most likely reason is either wrong type (e.g. a video option with no video streams) or that it is a private option of some encoder which was not actually used for any stream. Stream mapping: Stream #0:0 -> #0:0 (h264 (native) -> mjpeg (native)) Press [q] to stop, [?] for help Output #0, avi, to '/home/guardian-tech/Pictures/output_frame.avi': Metadata: INAM : RTSP Server ISFT : Lavf58.45.100 Stream #0:0: Video: mjpeg (MJPG / 0x47504A4D), yuvj420p(pc), 1920x1080, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc Metadata: encoder : Lavc58.91.100 mjpeg Side data: cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A [rtsp @ 0x5592e7bb00] max delay reached. need to consume packet [rtsp @ 0x5592e7bb00] RTP: missed 212 packets [h264 @ 0x5592ebb790] concealing 2192 DC, 2192 AC, 2192 MV errors in I frame rtsp://admin:[password]@[ip]:[port]/live/0/MAIN: corrupt decoded frame in stream 0 [rtsp @ 0x5592e7bb00] max delay reached. need to consume packet [rtsp @ 0x5592e7bb00] RTP: missed 6 packets [rtsp @ 0x5592e7bb00] max delay reached. need to consume packet [rtsp @ 0x5592e7bb00] RTP: missed 14 packets [h264 @ 0x5592f1bd30] cabac decode of qscale diff failed at 42 29 [h264 @ 0x5592f1bd30] error while decoding MB 42 29, bytestream 0 [h264 @ 0x5592f1bd30] concealing 4687 DC, 4687 AC, 4687 MV errors in I frame rtsp://admin:[password]@[ip]:[port]/live/0/MAIN: corrupt decoded frame in stream 0 Error terminating recording.
问题分析与解决方案
核心问题
- PID获取错误:代码中
ffmpegProcessId = getpid();获取的是当前程序的PID,而非FFmpeg子进程的PID;fileno(videoWriter)返回的是文件描述符,不是进程ID,导致kill调用失败,无法正常终止FFmpeg。 - 终止方式不优雅:即使PID正确,直接发送
SIGTERM可能导致FFmpeg来不及写入文件索引和头信息,造成文件损坏。 - 无效FFmpeg参数:
-preset fast -qp 0是H.264编码器的参数,当前用的是MJPEG编码器,这些参数无效,会触发警告。
修正方案
1. 正确获取FFmpeg子进程PID
放弃popen,改用fork+execvp启动FFmpeg,直接获取子进程PID。
2. 优雅终止FFmpeg
优先通过向FFmpeg标准输入写入'q'字符(和手动按q效果一致),让它优雅关闭文件;如果失败,再发送SIGINT或SIGTERM作为兜底。
修改后的代码
.cpp文件
#include "videorecorder.h" #include <vector> #include <cstring> VideoRecorder::VideoRecorder(const std::string& rtspUrl) : url(rtspUrl), recording(false), ffmpegProcessId(-1), videoWriter(nullptr) { } VideoRecorder::~VideoRecorder() { end_record(); } bool VideoRecorder::start_record(const std::string &fileName) { if (recording) { std::cerr << "Already recording." << std::endl; return false; } // 拆分命令为参数列表,避免shell解析特殊字符问题 std::vector<char*> args; args.push_back(const_cast<char*>("ffmpeg")); args.push_back(const_cast<char*>("-rtsp_transport")); args.push_back(const_cast<char*>("udp")); args.push_back(const_cast<char*>("-i")); args.push_back(const_cast<char*>(url.c_str())); args.push_back(const_cast<char*>("-c:v")); args.push_back(const_cast<char*>("mjpeg")); args.push_back(const_cast<char*>(fileName.c_str())); args.push_back(nullptr); // execvp需要以nullptr结尾 // 创建管道用于控制FFmpeg标准输入 int pipefd[2]; if (pipe(pipefd) == -1) { std::cerr << "Failed to create pipe." << std::endl; return false; } pid_t pid = fork(); if (pid == -1) { std::cerr << "Failed to fork process." << std::endl; close(pipefd[0]); close(pipefd[1]); return false; } else if (pid == 0) { // 子进程:执行FFmpeg close(pipefd[1]); dup2(pipefd[0], STDIN_FILENO); execvp(args[0], args.data()); std::cerr << "Failed to execute ffmpeg." << std::endl; exit(EXIT_FAILURE); } else { // 父进程:保存PID和管道写端 ffmpegProcessId = pid; close(pipefd[0]); videoWriter = fdopen(pipefd[1], "w"); if (!videoWriter) { std::cerr << "Failed to open pipe for writing." << std::endl; kill(pid, SIGKILL); waitpid(pid, nullptr, 0); ffmpegProcessId = -1; close(pipefd[1]); return false; } recording = true; std::cout << "Recording started." << std::endl; return true; } } bool VideoRecorder::end_record() { if (!recording) return false; bool success = true; if (ffmpegProcessId != -1 && videoWriter) { // 发送'q'指令让FFmpeg优雅收尾 fputc('q', videoWriter); fflush(videoWriter); // 等待子进程结束 int status; if (waitpid(ffmpegProcessId, &status, 0) == -1) { std::cerr << "Failed to wait for ffmpeg process." << std::endl; success = false; } else { if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { std::cout << "Recording ended successfully." << std::endl; } else { std::cerr << "FFmpeg exited with non-zero status: " << WEXITSTATUS(status) << std::endl; success = false; } } fclose(videoWriter); videoWriter = nullptr; ffmpegProcessId = -1; } else { std::cerr << "Invalid ffmpeg process or pipe." << std::endl; success = false; } recording = false; return success; }
.h文件
#ifndef VIDEORECORDER_H #define VIDEORECORDER_H #include <string> #include <iostream> #include <cstdlib> #include <csignal> #include <sys/wait.h> #include <unistd.h> #include <cstdio> class VideoRecorder { public: VideoRecorder(const std::string& rtspUrl); ~VideoRecorder(); bool start_record(const std::string& fileName); bool end_record(); private: std::string url; bool recording; pid_t ffmpegProcessId; FILE* videoWriter; }; #endif // VIDEORECORDER_H
额外优化
- 移除了代码中未使用的FFmpeg API和OpenCV头文件,减少冗余依赖。
- 用
execvp替代shell命令拼接,避免URL或文件名含特殊字符时的解析错误。 - 优先使用管道发送
q指令,保证FFmpeg完成文件收尾工作,彻底避免文件损坏。
内容的提问来源于stack exchange,提问作者Jorge Augusto Wilchen
相关产品推荐
相关产品推荐

