ffmpeg | 8路摄像头视频录制方案

1. 背景介绍

最近用c++写了个多路摄像头同时录制视频的工具,要求

  • 实现在界面上可以预览多个摄像头的画面
  • 点击录制按钮,多个摄像头可以同时录制视频
  • 录制完成后,录制的视频要保存成文件,并且同步录制的视频帧数必须相同,否则无法对齐
  • cpu利用率要尽可能低,如果利用率过高则会导致视频录制出现卡顿情况

2. 需求分析

这里也不是什么特别专业的分析,就是把项目过程中的一些细节和问题介绍一下,需求分析的顺序也是我解决问题的顺序

  • 多个摄像头需要同时预览、录制,会用到多线程方法
  • 如果用opencv编码视频,面临的问题就是无法设置码率,opencv的码率超级大,也会导致编码视频超级大,录制几秒钟就有十几MB
  • 8路摄像头同时录制,如果所有任务都使用cpu执行,尽管是8核cpu,在编码时cpu也会满载
  • 点击录制按钮,所有摄像头开始录制,如何确保所有摄像头同时开始编码第一帧,以及录制过程中如果对齐所有帧。如果不解决此问题,会导致每个摄像头录制的视频帧数有差别

3. 方案

3.1 多路预览

这里使用的是软龙格的VM16图像采集卡(不是广告,淘宝上我没找到相关的),他们提供了采集卡的c++ SKD,还提供了带有多线程预览的MFC界面,我们只需要二次开发即可。

3.2 码率设置

opencv虽然很方便,但是没法设置码率,会导致编码得到的视频超大。首先要注意码率是bit rate,并不是帧率frame rate。这个问题可以使用ffmpeg来解决,ffmpeg有非常多的编码格式,常用的就是使用h264编码成mp4文件。

3.3 降低CPU负载

这部分的解决方法是利用ffmpeg做硬件加速,由于机器是英特尔CPU,可以使用h264 qsv做硬件加速。可以通过下面的指令查看可以使用的编码器

ffmpeg -codecs

此时可以通过指令,试试设备能不能用硬件加速

ffmpeg -f dshow -video_size 1024x768 -i video="Webcam C170" -vcodec h264_qsv D:\test.mp4

但是这时要注意,摄像头读取的图片是YUV422(UYVY422)格式的,预览时是使用opencv的灰度Mat预览,编码时需要将UYVY转换成NV12格式才可以开启硬编码加速,否则会报错。而为了减少使用opencv Mat的中间过程,我们使用了libyuv这个库直接将UYVY数据转换成NV12数据。

例如:宽4 高2的图片

至此,在win10的intel i7上可以实现8路全开录制的CPU利用率只有50%左右了。

3.4 帧同步

本来以为开启硬件加速就可以解放出cpu,画面不同步的问题也就自然解决了,事实证明没有。原因是我们无法知道到底哪一个子线程最先抓取到图片,也无法知道哪个子线程偷懒少写几帧或者比较勤奋多写了几帧。

我的解决方案是:给定一个全局的缓冲区数组,用来存储8个摄像头抓到图片的数据,每个摄像头抓到一阵就存到数组对应的位置上,当判断8个摄像头都抓到图的时候,开启一个子线程,在子线程中遍历缓冲区数组,将每一个摄像头抓到的帧数据编码。

如果不使用子线程写,则写操作会占用当前摄像头的资源,导致当前摄像头不能抓图预览,卡住不动,同时其他摄像头抓到的帧数据只能做预览,而不能做编码,导致预览的这几帧全都浪费掉。

如果在程序最开始就开启线程,那么就要使用一个while true大循环,多占用资源。

应该还有更好的解决方案,但是当前的方案足以满足需求。

可以使用下面的指令验证每个视频的帧数是否相同

ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 input.mp4

编码帧数据代码

FrameWrite::FrameWrite(string filename, int w, int h)
    this->filename = filename;
    this->codec_id = AV_CODEC_ID_H264;
    this->width = w;
    this->height = h;
    this->i = 0;
    init();
FrameWrite::~FrameWrite()
    if(pCodecCtx != NULL){ avcodec_free_context(&pCodecCtx);pCodecCtx = nullptr;}
    if (pFmtCtx != NULL){avformat_free_context(pFmtCtx);pFmtCtx = nullptr;}
    if (pFrame != NULL) {av_frame_free(&pFrame);pFrame = nullptr;}
    if (vStream != NULL){av_freep(vStream); vStream = nullptr;}
    if (pkt != NULL){av_packet_free(&pkt);pkt = nullptr;}
    if (opt != NULL){av_dict_free(&opt);opt = nullptr;}
int FrameWrite::init()
    if (avformat_alloc_output_context2(&pFmtCtx, nullptr, nullptr, filename.c_str()) < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Could not allocate context.\n");
        return -1;
    //Create and initialize a AVIOContext for accessing the resource indicated by url
    if (avio_open2(&pFmtCtx->pb, filename.c_str(), AVIO_FLAG_READ_WRITE, nullptr, nullptr) < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Failed to open output file.\n");
        return -1;
    //Add a new stream to a media file
    vStream = avformat_new_stream(pFmtCtx, nullptr);
    if (!vStream) {
        av_log(nullptr, AV_LOG_FATAL, "Failed to create stream channel.\n");
        return -1;
    //Set codec parameter
    vStream->codecpar->width = width;
    vStream->codecpar->height = height;
    vStream->codecpar->bit_rate = 4000000;  # 码率
    vStream->codecpar->format = AV_PIX_FMT_NV12;  # NV12 格式
    vStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    vStream->codecpar->codec_id = AV_CODEC_ID_H264;
    pCodec = avcodec_find_encoder_by_name("h264_qsv");
    if (!pCodec) {
        av_log(nullptr, AV_LOG_FATAL, "Can not find encoder.\n");
        return -1;
    //Allocate an AVCodecContext and set its fields to default values
    pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx) {
        av_log(nullptr, AV_LOG_FATAL, "Could not allocate a codec context.\n");
        return -1;
    //Fill the codec context based on the values from the supplied codec parameters
    if (avcodec_parameters_to_context(pCodecCtx, vStream->codecpar) < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Codec not fill the codec context.\n");
        return -1;
    //Set codec context parameter
    pCodecCtx->gop_size = 250;
    pCodecCtx->max_b_frames = 0;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = FRAME_RATE;
    pCodecCtx->bit_rate = 4000000;
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    pCodecCtx->qcompress = 0.6;
    av_opt_set(pCodecCtx->priv_data, "preset", "veryfast", 0);
    //Open codec
    if (avcodec_open2(pCodecCtx, pCodec, nullptr) != 0) {
        av_log(nullptr, AV_LOG_FATAL, "Open encoder failure.\n");
        return -1;
    //Allocate an AVPacketand set its fields to default values
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(nullptr, AV_LOG_FATAL, "Failed to initialize AVpacket.\n");
        return -1;
    pFrame = av_frame_alloc();
    if (!pFrame) {
        av_log(nullptr, AV_LOG_FATAL, "Failed to allocate an AVFrame.\n");
        return -1;
    pFrame->width = pCodecCtx->width;
    pFrame->height = pCodecCtx->height;
    pFrame->format = pCodecCtx->pix_fmt;
    //Allocate new buffer(s) for audio or video data
    if (av_frame_get_buffer(pFrame, 0) < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Could not allocate the video frame data.\n");
        return -1;
    //int frame_rate = FRAME_RATE;
    //av_dict_set_int(&opt, "video_track_timescale", frame_rate, 0);
    //Allocate the stream private data and write the stream header to an output media file
    //avformat_write_header(pFmtCtx, &opt);
    return 1;
// 写入一帧NV12数据: y通道数据, uv通道数据
int FrameWrite::write(unsigned char* yBuf, unsigned char* uvBuf)
    pFrame->data[0] = yBuf;
    pFrame->data[1] = uvBuf;
    pFrame->pts = i++;
    int res = avcodec_send_frame(pCodecCtx, pFrame);
    if (res < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Error sending a frame for encoding.\n");
        return -1;
    while (res >= 0) {
        //Read encoded data from the encoder
        res = avcodec_receive_packet(pCodecCtx, pkt);
        if (res == AVERROR(EAGAIN) || res == AVERROR_EOF) {
            break;
        if (res < 0) {
            av_log(nullptr, AV_LOG_FATAL, "Error during encoding.\n");
            return -1;
        if (res >= 0) {
            pkt->stream_index = 0;
            //Write a packet to an output media file
            av_write_frame(pFmtCtx, pkt);
            av_packet_unref(pkt);
    av_log(nullptr, AV_LOG_INFO, "frame pts: %d\n", i);
// 录制好后,清空缓存,写入视频尾
int FrameWrite::flush()
    int res = avcodec_send_frame(pCodecCtx, NULL);
    if (res < 0) {
        av_log(nullptr, AV_LOG_FATAL, "Error sending a frame for encoding.\n");
        return -1;
    while (res >= 0) {
        //Read encoded data from the encoder
        res = avcodec_receive_packet(pCodecCtx, pkt);
        if (res == AVERROR(EAGAIN) || res == AVERROR_EOF) {
            break;
        if (res < 0) {
            av_log(nullptr, AV_LOG_FATAL, "Error during encoding.\n");
            return -1;
        if (res >= 0) {
            pkt->stream_index = 0;
            //Write a packet to an output media file
            av_write_frame(pFmtCtx, pkt);
            av_packet_unref(pkt);
    //Write the stream trailer to an output media file and free the file private data
    av_packet_free(&pkt);