You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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.
问题分析与解决方案

核心问题

  1. PID获取错误:代码中ffmpegProcessId = getpid();获取的是当前程序的PID,而非FFmpeg子进程的PID;fileno(videoWriter)返回的是文件描述符,不是进程ID,导致kill调用失败,无法正常终止FFmpeg。
  2. 终止方式不优雅:即使PID正确,直接发送SIGTERM可能导致FFmpeg来不及写入文件索引和头信息,造成文件损坏。
  3. 无效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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 10:30:55