Qt 中使用 ffmpeg 获取采集卡数据录制视频

作者:billy

版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

前言

之前做了一个功能,从采集卡获取数据然后录制成视频,结果发现录制的视频内存占用非常大,1分钟的视频大概有 800MB 内存。在帧率和分辨率已确定的情况下,只能通过调整比特率来减少内存占用,但是设置比特率在不同编码器和平台支持情况都有所不同,有些编码器甚至不支持直接设置比特率,所以博主想起了 ffmpeg 这个神器。

这里先介绍一下视频的相关参数:

在用户视角:
清晰度 = 比特率(码率) / 分辨率
流畅度 = 帧率

在开发者视角:
影响内存的:主要是分辨率
影响 CPU 的:码率和编码格式
影响 GPU 的:分辨率和编码格式
影响体积大小和带宽:码率

ffmpeg 库功能测试

首先在网上找到了 ffmpeg 库的 windows 安装包来做下测试

百度网盘下载链接:ffmpeg(windows安装包)

提取码:fkmn

下载完成之后可以直接使用 bin\ffmpeg.exe 在命令行做测试,把 ffmpeg\bin 路径添加到环境变量中

  1. 确认 ffmpeg 版本和配置:ffmpeg -version

  2. 列举所有设备:ffmpeg -list_devices true -f dshow -i dummy

对已录制的内存占用较大的视频进行压缩:

  1. 设置码率:ffmpeg -i input.mp4 -b:v 1000k output.mp4
  2. 设置分辨率:ffmpeg -i input.mp4 -s 640x360 output.mp4
  3. 设置帧率:ffmpeg -i input.mp4 -r 30 output.mp4
  4. 综合调整:ffmpeg -i input.mp4 -b:v 800k -s 640x360 -r 30 output.mp4

也可以用 python 跑脚本:

import subprocess
def compress_video(input_file, output_file, bitrate='1000k'):
    command = [
        'ffmpeg',
        '-i', input_file,
        '-b:v', bitrate,
        output_file
    ]
    try:
        subprocess.run(command, check=True)
        print(f"视频压缩成功,输出文件为: {output_file}")
    except subprocess.CalledProcessError as e:
        print(f"视频压缩失败: {e}")
        
# 使用示例
input_file = 'input.mp4'
output_file = 'output.mp4'
compress_video(input_file, output_file, bitrate='800k')

再测试一下直接打开设备录制视频

ffmpeg -f dshow -i video="@device_pnp_\\?\usb#vid_2b89&pid_5647&mi_00#7&223c07ce&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" -c:v libx264 -max_delay 10000 -b:v 1000k -bufsize 200k -r 30 output.mp4 -y

命令解释
-c:v libx264: 指定使用libx264编码器进行视频编码。
-max_delay 10000: 设置编码器的最大延迟为10000毫秒(10秒)。这有助于控制视频编码的缓冲延迟。
-b:v 1000k: 设置视频比特率为1000 kbps(千比特每秒)。
-bufsize 200k: 设置编码器的缓冲区大小为200 kbps。这个参数用于控制编码器在遇到高负载或低负载时的比特率变化。
-r 30: 设置帧率为30帧每秒。
output.mp4: 输出文件名。
-y: 覆盖输出文件,如果文件已存在。

测试结果为用 ffmpeg 压缩过的视频内存占用率非常小,码率越小内存就越小。但是对于客户来说他不会使用命令行去做压缩,所以最终方案还是直接使用 ffmpeg 库来录制视频。

Qt 中集成 ffmpeg 库来录制视频

首先在网上下载编译完成的 ffmpeg 库,博主用的是 Qt 5.15.2 和 vs2019

百度网盘下载链接:ffmpeg(库文件)

提取码: cqwx

把库集成到 Qt 中:

INCLUDEPATH += $$PWD/ffmpeg/include

LIBS += -L$$PWD/ffmpeg/lib/ -lavcodec
LIBS += -L$$PWD/ffmpeg/lib/ -lavdevice
LIBS += -L$$PWD/ffmpeg/lib/ -lavfilter
LIBS += -L$$PWD/ffmpeg/lib/ -lavformat
LIBS += -L$$PWD/ffmpeg/lib/ -lavutil
LIBS += -L$$PWD/ffmpeg/lib/ -lpostproc
LIBS += -L$$PWD/ffmpeg/lib/ -lswresample
LIBS += -L$$PWD/ffmpeg/lib/ -lswscale

实现拍照和录屏的功能:

extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
}

void showErrorInfo(int ret)
{
    static char errbuf[AV_ERROR_MAX_STRING_SIZE];
    memset(errbuf, 0, AV_ERROR_MAX_STRING_SIZE);
    av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
    qDebug() << "Error info:" << errbuf;
}

void videoRecord()
{
	avformat_network_init();
    avdevice_register_all();

    // 遍历所有设备类型
    // const AVInputFormat *iformat = nullptr;
    // while ((iformat = av_input_audio_device_next(iformat))) {
    //     qDebug() << "Audio input device: " << iformat->name << " - " << iformat->long_name;
    // }
    // iformat = nullptr;
    // while ((iformat = av_input_video_device_next(iformat))) {
    //     qDebug() << "Video input device: " << iformat->name << " - " << iformat->long_name;
    // }

    const AVInputFormat *iformat_dshow = av_find_input_format("dshow");
    if (!iformat_dshow)
    {
        qDebug() << "Could not find input format !";
        return;
    }

    // 用于存储设备列表的上下文
    AVDeviceInfoList *deviceList = nullptr;
    int ret = avdevice_list_input_sources(iformat_dshow, nullptr, nullptr, &deviceList);
    if (ret < 0) {
        qDebug() << "Could not list input sources !";
        showErrorInfo(ret);
        return;
    }

    // 设备名称
    std::string deviceName = "";

    // 遍历设备列表
    // qDebug() << "Available DirectShow devices:";
    for (int i = 0; i < deviceList->nb_devices; ++i)
    {
        AVDeviceInfo *device = deviceList->devices[i];
        // qDebug() << "Device" << i << ":" << device->device_description << "(" << device->device_name << ")";

        // 获取绿联采集卡的设备名称
        QString description = QString(device->device_description);
        if ( description.contains("UGREEN") ) {
            deviceName = "video=" + description.toStdString();
            break;
        }
    }

    // 释放设备列表
    avdevice_free_list_devices(&deviceList);

    //------------------------------

    // 创建 AVFormatContext
    AVFormatContext *inputFormatContext = avformat_alloc_context();
    if (!inputFormatContext) {
        qDebug() << "Could not allocate AVFormatContext !";
        return;
    }

    // 设置附加参数
    AVDictionary *options = nullptr;
    int framerate = 30;
    av_dict_set(&options, "rtbufsize", "100M", 0);  // 设置缓冲区大小
    av_dict_set(&options, "framerate", "30", 0);    // 设置帧率

    // 打开输入设备
    ret = avformat_open_input(&inputFormatContext, deviceName.c_str(), iformat_dshow, &options);
    if (ret < 0) {
        qDebug() << "Could not open input device !";
        showErrorInfo(ret);
        return;
    }

    // 打印输入设备的信息
    // av_dump_format(inputFormatContext, 0, deviceName.c_str(), 0);

    // 查找输入流信息
    ret = avformat_find_stream_info(inputFormatContext, nullptr);
    if (ret < 0) {
        qDebug() << "Could not find stream information !";
        showErrorInfo(ret);
        return;
    }

    // 查找视频流
    int videoStreamIndex = -1;
    for (unsigned int i = 0; i < inputFormatContext->nb_streams; i++)
    {
        if (inputFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
        }
    }
    if (videoStreamIndex == -1) {
        qDebug() << "Could not find video stream !";
        return;
    }

    // 获取视频流参数
    AVCodecParameters *inputCodecParameters = inputFormatContext->streams[videoStreamIndex]->codecpar;

    // 查找输入解码器
    const AVCodec *inputCodec = avcodec_find_decoder(inputCodecParameters->codec_id);
    if (!inputCodec) {
        qDebug() << "Could not find codec !";
        return ;
    }

    // 创建输入解码器上下文
    AVCodecContext *inputCodecContext = avcodec_alloc_context3(inputCodec);
    if (!inputCodecContext) {
        qDebug() << "Could not allocate codec context !";
        return;
    }

    // 将视频流参数复制到输入解码器上下文
    ret = avcodec_parameters_to_context(inputCodecContext, inputCodecParameters);
    if (ret < 0) {
        qDebug() << "Could not copy codec parameters to context !";
        showErrorInfo(ret);
        return;
    }

    // 打开输入解码器
    ret = avcodec_open2(inputCodecContext, inputCodec, nullptr);
    if (ret < 0) {
        qDebug() << "Could not open codec !";
        showErrorInfo(ret);
        return;
    }

    //------------------------------

    // 分配帧和数据包
    AVFrame *frame = av_frame_alloc();
    AVPacket *packet = av_packet_alloc();
    if (!frame || !packet) {
        qDebug() << "Could not alloc frame and packet !";
        return;
    }

    // 分配图像转换上下文
    SwsContext *swsContext = sws_getContext(inputCodecContext->width, inputCodecContext->height, inputCodecContext->pix_fmt,
                                            inputCodecContext->width, inputCodecContext->height, AV_PIX_FMT_RGB24,
                                            SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!swsContext) {
        qDebug() << "Could not initialize SwsContext !";
        return;
    }

    // 分配 RGB 帧
    AVFrame *rgbFrame = av_frame_alloc();
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, inputCodecContext->width, inputCodecContext->height, 1);
    uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer, AV_PIX_FMT_RGB24,
                         inputCodecContext->width, inputCodecContext->height, 1);

    // 读取帧数据(保存10张图片)
    int frameCount = 0;
    while (av_read_frame(inputFormatContext, packet) >= 0 && frameCount < 11)
    {
        // 确保帧的数据类型和解码器的数据类型一致
        if (packet->stream_index == videoStreamIndex)
        {
            // 发送数据包到解码器
            ret = avcodec_send_packet(inputCodecContext, packet);
            if (ret < 0) {
                qDebug() << "Error sending packet to decoder !";
                showErrorInfo(ret);
                continue;
            }

            // 接收解码后的帧
            while (avcodec_receive_frame(inputCodecContext, frame) == 0)
            {
                // 舍弃第一帧
                if ( frameCount == 0 ) {
                    frameCount++;
                    continue;
                }

                // 转换图像格式
                sws_scale(swsContext, frame->data, frame->linesize, 0, inputCodecContext->height,
                          rgbFrame->data, rgbFrame->linesize);

                // 创建 QImage
                QImage image(rgbFrame->data[0], inputCodecContext->width, inputCodecContext->height, QImage::Format_RGB888);

                // 保存图像
                QString fileName = QString("frame_%1.jpg").arg(frameCount++);
                if (!image.save(fileName)) {
                    qDebug() << "Error saving image:" << fileName;
                } else {
                    qDebug() << "Image saved:" << fileName;
                }
            }
        }
        av_packet_unref(packet);
    }

    //------------------------------

    // 输出格式上下文
    AVFormatContext *outputFormatContext = nullptr;
    const char *outputFileName = "output.mp4";

    // 创建输出格式上下文
    ret = avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, outputFileName);
    if (!outputFormatContext) {
        qDebug() << "Could not create output context !";
        showErrorInfo(ret);
        return;
    }

    // 查找输出编码器
    const AVCodec *outputCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!outputCodec) {
        qDebug() << "Could not find output codec !";
        return;
    }

    // 创建输出流
    AVStream *outputStream = avformat_new_stream(outputFormatContext, outputCodec);
    if (!outputStream) {
        qDebug() << "Could not create output stream !";
        return;
    }

    // 打开输出编码器上下文
    AVCodecContext *outputCodecContext = avcodec_alloc_context3(outputCodec);
    if (!outputCodecContext) {
        qDebug() << "Could not allocate output codec context !";
        return;
    }

    // 设置输出编码器参数
    outputCodecContext->codec_id = outputCodec->id;
    outputCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    outputCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
    outputCodecContext->width = 1920;                                       // 设置分辨率
    outputCodecContext->height = 1080;
    outputCodecContext->time_base = {1, framerate};                         // 设置帧率
    outputCodecContext->framerate = {framerate, 1};
    outputCodecContext->bit_rate = 2000000;                                 // 设置比特率
    outputCodecContext->rc_buffer_size = 2 * outputCodecContext->bit_rate;  // 设置缓冲区大小为码率的两倍
    outputCodecContext->gop_size = 10;                                      // 设置关键帧间隔
    outputCodecContext->max_b_frames = 1;

    if (outputFormatContext->oformat->flags & AVFMT_GLOBALHEADER) {
        outputCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    // 打开输出编码器
    ret = avcodec_open2(outputCodecContext, outputCodec, nullptr);
    if (ret < 0) {
        qDebug() << "Could not open output codec !";
        showErrorInfo(ret);
        return;
    }

    // 复制输出编码器参数到输出流
    avcodec_parameters_from_context(outputStream->codecpar, outputCodecContext);
    outputStream->time_base = outputCodecContext->time_base;

    //------------------------------

    // 打开输出文件
    if (!(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&outputFormatContext->pb, outputFileName, AVIO_FLAG_WRITE);
        if (ret < 0) {
            qDebug() << "Could not open output file !";
            showErrorInfo(ret);
            return;
        }
    }

    // 写入文件头
    ret = avformat_write_header(outputFormatContext, nullptr);
    if (ret < 0) {
        qDebug() << "Could not write header !";
        showErrorInfo(ret);
        return;
    }

    // 分配帧和数据包
    AVFrame *inputFrame = av_frame_alloc();
    AVPacket *inputPacket = av_packet_alloc();
    AVFrame *outputFrame = av_frame_alloc();
    AVPacket *outputPacket = av_packet_alloc();
    if (!inputFrame || !inputPacket || !outputFrame || !outputPacket) {
        qDebug() << "Could not alloc frame and packet !";
        return;
    }

    // 分配图像转换上下文
    SwsContext *swsContext2 = sws_getContext(inputCodecContext->width, inputCodecContext->height, inputCodecContext->pix_fmt,
                                             outputCodecContext->width, outputCodecContext->height, outputCodecContext->pix_fmt,
                                             SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!swsContext2) {
        qDebug() << "Could not initialize SwsContext !";
        return;
    }

    // 分配输出帧数据
    outputFrame->format = AV_PIX_FMT_YUV420P;
    outputFrame->width = inputCodecContext->width;
    outputFrame->height = inputCodecContext->height;
    ret = av_frame_get_buffer(outputFrame, 0);
    if (ret < 0) {
        qDebug() << "Could not get frame buffer !";
        showErrorInfo(ret);
        return;
    }

    // 录屏60秒,帧率30,保存1800帧数据
    int frameCount2 = 0;
    while (av_read_frame(inputFormatContext, inputPacket) >= 0 && frameCount2 < 1800)
    {
        // 确保帧的数据类型和解码器的数据类型一致
        if (inputPacket->stream_index == videoStreamIndex)
        {
            // 发送输入数据包到解码器
            ret = avcodec_send_packet(inputCodecContext, inputPacket);
            if (ret < 0) {
                qDebug() << "Error sending packet to decoder !";
                showErrorInfo(ret);
                continue;
            }

            // 接收解码后的帧
            while (avcodec_receive_frame(inputCodecContext, inputFrame) == 0)
            {
                // 转换图像格式
                sws_scale(swsContext2, inputFrame->data, inputFrame->linesize, 0, inputCodecContext->height,
                          outputFrame->data, outputFrame->linesize);

                outputFrame->pts = frameCount2++;
                qDebug() << "frameCount2: " << frameCount2;

                // 发送输出帧到输出编码器
                ret = avcodec_send_frame(outputCodecContext, outputFrame);
                if (ret < 0) {
                    qDebug() << "Error sending frame to encoder !";
                    showErrorInfo(ret);
                    continue;
                }

                // 接收编码后的数据包
                while (avcodec_receive_packet(outputCodecContext, outputPacket) == 0)
                {
                    av_packet_rescale_ts(outputPacket, outputCodecContext->time_base, outputStream->time_base);
                    outputPacket->stream_index = outputStream->index;

                    // 写入数据包到输出文件
                    ret = av_interleaved_write_frame(outputFormatContext, outputPacket);
                    if (ret < 0) {
                        qDebug() << "Error writing packet to output file";
                        showErrorInfo(ret);
                        continue;
                    }

                    av_packet_unref(outputPacket);
                }
            }
        }
        av_packet_unref(inputPacket);
    }

    // 刷新编码器
    avcodec_send_frame(outputCodecContext, nullptr);
    while (avcodec_receive_packet(outputCodecContext, outputPacket) == 0)
    {
        av_packet_rescale_ts(outputPacket, outputCodecContext->time_base, outputStream->time_base);
        outputPacket->stream_index = outputStream->index;

        ret = av_interleaved_write_frame(outputFormatContext, outputPacket);
        if (ret < 0) {
            qDebug() << "Error writing packet to output file";
            showErrorInfo(ret);
        }
        av_packet_unref(outputPacket);
    }

    // 写入文件尾
    av_write_trailer(outputFormatContext);

    // 释放资源
    av_frame_free(&inputFrame);
    av_frame_free(&outputFrame);
    av_packet_free(&inputPacket);
    av_packet_free(&outputPacket);
    sws_freeContext(swsContext2);
    avcodec_free_context(&outputCodecContext);
    avformat_free_context(outputFormatContext);

    if (outputFormatContext && !(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
        avio_closep(&outputFormatContext->pb);
    }

    av_free(buffer);
    av_frame_free(&rgbFrame);
    av_frame_free(&frame);
    av_packet_free(&packet);
    sws_freeContext(swsContext);
    avcodec_free_context(&inputCodecContext);
    avformat_close_input(&inputFormatContext);
    avformat_free_context(inputFormatContext);
    av_dict_free(&options);

    avformat_network_deinit();
}
相关推荐
我真不会起名字啊2 小时前
“深入浅出”系列之杂谈篇:(3)Qt5和Qt6该学哪个?
开发语言·qt
laimaxgg2 小时前
Qt常用控件之单选按钮QRadioButton
开发语言·c++·qt·ui·qt5
牵牛老人2 小时前
Qt中使用QPdfWriter类结合QPainter类绘制并输出PDF文件
数据库·qt·pdf
水瓶丫头站住2 小时前
Qt的QStackedWidget样式设置
开发语言·qt
慕诗客5 小时前
QT基于Gstreamer采集的简单示例
开发语言·qt
Blasit5 小时前
C++ Qt建立一个HTTP服务器
服务器·开发语言·c++·qt·http
胖虎18 小时前
深入解析 iOS 视频录制(三):完整录制流程的实现与整合
ios·音视频·cocoa·ios视频录制·自定视频录制
AI服务老曹9 小时前
确保设备始终处于最佳运行状态,延长设备的使用寿命,保障系统的稳定运行的智慧地产开源了
人工智能·开源·云计算·音视频
@hdd10 小时前
深入解析Qt事件循环
qt
余~~1853816280013 小时前
短视频矩阵碰一碰发视频源码技术开发,支持OEM
网络·人工智能·线性代数·矩阵·音视频