QT开发技术【ffmpeg EVideo录屏软件 一】

一、思路与界面设计

在现代软件开发中,屏幕录制功能有着广泛的应用,如教学演示、游戏直播、软件操作记录等。本项目旨在使用 Qt 和 FFmpeg 库实现一个屏幕录制程序,利用 Qt 进行屏幕画面的捕获和界面交互,借助 FFmpeg 强大的音视频处理能力完成视频的编码和保存。

整体架构设计

整个屏幕录制程序主要分为两个核心模块:屏幕捕获模块和视频编码保存模块。屏幕捕获模块负责定时从屏幕抓取画面,视频编码保存模块则将捕获的画面进行编码并写入视频文件。

各模块实现思路

(一)屏幕捕获模块

该模块基于 Qt 框架实现,主要利用 QGuiApplication 和 QScreen 类来完成屏幕画面的捕获。具体步骤如下:

获取主屏幕对象:通过 QGuiApplication::primaryScreen() 方法获取当前系统的主屏幕对象。

捕获屏幕画面:调用 grabWindow(0) 方法抓取整个屏幕的画面,返回一个 QPixmap 对象,再将其转换为 QImage 对象以便后续处理。

像素格式转换:将捕获的 QImage 对象转换为 QImage::Format_ARGB32 格式,确保与后续 FFmpeg 处理的像素格式兼容。

定时捕获:使用 QTimer 定时器,按照设定的帧率(如 30fps)定时触发屏幕捕获操作。

(二)视频编码保存模块

此模块基于 FFmpeg 库实现,主要完成视频的编码和保存任务。具体步骤如下:

  1. 初始化 FFmpeg
    设置日志级别:调用 av_log_set_level(AV_LOG_ERROR) 设置 FFmpeg 的日志级别,只输出错误信息。
    创建输出上下文:使用 avformat_alloc_output_context2 函数创建输出文件的格式上下文,指定输出文件路径。
    查找编码器:调用 avcodec_find_encoder(AV_CODEC_ID_H264) 查找 H.264 编码器。
    创建视频流:使用 avformat_new_stream 函数在输出上下文中创建一个新的视频流。
    分配编码器上下文:调用 avcodec_alloc_context3 函数分配编码器上下文,并设置编码器参数,如视频分辨率、帧率、比特率等。
    打开编码器:使用 avcodec_open2 函数打开编码器。
    打开输出文件:调用 avio_open 函数打开输出文件,准备写入数据。
    写入文件头:使用 avformat_write_header 函数写入视频文件头。
    分配视频帧和数据包:调用 av_frame_alloc 和 av_packet_alloc 函数分别分配视频帧和数据包。
    初始化缩放上下文:使用 sws_getContext 函数初始化图像缩放上下文,用于将捕获的屏幕画面转换为编码器所需的像素格式。
  2. 视频编码
    像素格式转换:使用 sws_scale 函数将捕获的 QImage 数据转换为编码器所需的 AVFrame 数据。
    设置时间戳:为 AVFrame 设置正确的时间戳 pts,确保视频播放的时间顺序正确。
    发送帧到编码器:调用 avcodec_send_frame 函数将 AVFrame 发送到编码器进行编码。
    接收编码后的数据包:当编码器缓冲区满时,使用 avcodec_receive_packet 函数接收编码后的 AVPacket,并将其写入输出文件。
  3. 结束录制
    刷新编码器:调用 avcodec_send_frame 函数发送空帧,刷新编码器缓冲区,确保所有编码数据都被输出。
    接收剩余数据包:使用 avcodec_receive_packet 函数接收编码器输出的剩余数据包,并写入输出文件。
    写入文件尾:调用 av_write_trailer 函数写入视频文件尾。
    释放资源:释放 FFmpeg 分配的所有资源,包括视频帧、数据包、编码器上下文、输出上下文等。
    关键问题及解决方案
    (一)帧率控制问题
    为了保证录制的视频帧率稳定,使用 QTimer 定时器按照设定的帧率定时触发屏幕捕获操作。同时,在捕获帧时,记录上一帧的时间戳,计算需要等待的时间,使用 QThread::msleep 进行精确等待,确保每帧之间的时间间隔均匀。

(二)时间戳计算问题

时间戳 pts 的计算直接影响视频的播放时长和流畅度。使用 av_gettime_relative 记录录制开始时间,在每一帧捕获时计算当前时间与开始时间的差值,再使用 av_rescale_q 函数将其转换为编码器时间基下的 pts,确保时间戳的准确性。

二、录屏类实现

cpp 复制代码
#include "../Include/ScreenRecorder.h"
#include <QGuiApplication>
#include <QScreen>
#include <QPixmap>
#include <QDebug>

CScreenRecorder::CScreenRecorder(QObject* parent)
    : QObject(parent),
    m_eRecordState(RECORD_STATE_STOP),
    m_bRun(true),
    m_pFormatContext(nullptr),
    m_pVideoStream(nullptr),
    m_pCodecContext(nullptr),
    m_pFrame(nullptr),
    m_pSwsContext(nullptr),
    m_pPacket(nullptr),
    m_nFrameCount(0),
    m_bFFmpegInited(false),
    m_pFrameTimer(new QTimer(this))
{
    connect(m_pFrameTimer, &QTimer::timeout, this, &CScreenRecorder::SlotCaptureFrame);
}

CScreenRecorder::~CScreenRecorder()
{
    Stop();
}

void CScreenRecorder::SlotStartRecording(const std::string& strOutputPath)
{
    StartRecording(strOutputPath);
}

void CScreenRecorder::SlotStopRecording()
{
    StopRecording();
}

void CScreenRecorder::SlotStop()
{
    Stop();
}

void CScreenRecorder::StartRecording(const std::string& strOutputPath)
{
    m_eRecordState = RECORD_STATE_START;
    m_strOutputPath = strOutputPath;
    if (!m_bFFmpegInited)
    {
        InitFFmpeg();
    }
    m_nFrameCount = 0;
    m_startTime = av_gettime_relative();
    m_pFrameTimer->start(1000 / 30); // 30fps
}

void CScreenRecorder::StopRecording()
{
    m_eRecordState = RECORD_STATE_STOP;
    m_pFrameTimer->stop();
    if (m_bFFmpegInited)
    {
        int nRet = 0;
        // 发送空帧以刷新编码器
        nRet = avcodec_send_frame(m_pCodecContext, nullptr);
        if (nRet < 0 && nRet != AVERROR_EOF)
        {
            qDebug() << "StopRecording Error sending flush frame to encoder:" << nRet;
            DebugLog(nRet);
        }

        while (true)
        {
            nRet = avcodec_receive_packet(m_pCodecContext, m_pPacket);
            if (nRet == AVERROR(EAGAIN) || nRet == AVERROR_EOF)
            {
                break;
            }
            else if (nRet < 0)
            {
                qDebug() << "StopRecording Error receiving packet:" << nRet;
                DebugLog(nRet);
                break;
            }

            av_packet_rescale_ts(m_pPacket, m_pCodecContext->time_base, m_pVideoStream->time_base);
            m_pPacket->stream_index = m_pVideoStream->index;

            // 写入数据包
            if (av_interleaved_write_frame(m_pFormatContext, m_pPacket) < 0)
            {
                qDebug() << "Error writing packet during flush";
            }
            // 释放数据包
            av_packet_unref(m_pPacket);
        }

        if (av_write_trailer(m_pFormatContext) < 0)
        {
            qDebug() << "Error writing trailer";
        }
        CleanFFmpeg();
    }
}

void CScreenRecorder::Stop()
{
    if (m_eRecordState == RECORD_STATE_START)
    {
        StopRecording();
    }
    CleanFFmpeg();
}

void CScreenRecorder::SlotCaptureFrame()
{
    if (!m_bFFmpegInited || m_eRecordState != RECORD_STATE_START) {
        return;
    }

   

    // 捕获屏幕
    QImage screen = QGuiApplication::primaryScreen()->grabWindow(0).toImage();
    screen = screen.convertToFormat(QImage::Format_ARGB32);

    RecordFrame(screen);
}

void CScreenRecorder::RecordFrame(const QImage& screen)
{
    // 转换图像格式
    if (!m_pFrame || !m_pSwsContext)
    {
        return;
    }

    const uchar* bitsPointer = screen.bits();
    const int stride[] = { static_cast<int>(screen.bytesPerLine()) };
    int nRet = sws_scale(m_pSwsContext, &bitsPointer, stride, 0, screen.height(),
        m_pFrame->data, m_pFrame->linesize);
    if (nRet < 0)
    {
        qDebug() << "Error scaling frame";
        return;
    }

    // 显式定义 AVRational 变量
    AVRational av_time_base_q = { 1, AV_TIME_BASE };
    // 正确计算 pts
    qint64 currentTime = av_gettime_relative() - m_startTime;
    m_pFrame->pts = av_rescale_q(currentTime, av_time_base_q, m_pCodecContext->time_base);

    // 发送帧到编码器
    nRet = avcodec_send_frame(m_pCodecContext, m_pFrame);
    while (nRet == AVERROR(EAGAIN))
    {
        // 编码器缓冲区已满,先接收数据包
        nRet = avcodec_receive_packet(m_pCodecContext, m_pPacket);
        while (nRet == 0)
        {
            av_packet_rescale_ts(m_pPacket, m_pCodecContext->time_base, m_pVideoStream->time_base);
            m_pPacket->stream_index = m_pVideoStream->index;

            // 写入数据包
            if (av_interleaved_write_frame(m_pFormatContext, m_pPacket) < 0)
            {
                qDebug() << "Error writing packet";
            }
            av_packet_unref(m_pPacket);
            nRet = avcodec_receive_packet(m_pCodecContext, m_pPacket);
        }
        nRet = avcodec_send_frame(m_pCodecContext, m_pFrame);
    }

    if (nRet < 0 && nRet != AVERROR_EOF)
    {
        qDebug() << "Error sending frame to encoder:" << nRet;
        return;
    }

    // 从编码器接收所有剩余数据包
    while (true) {
        nRet = avcodec_receive_packet(m_pCodecContext, m_pPacket);
        if (nRet == AVERROR(EAGAIN) || nRet == AVERROR_EOF) {
            break;
        }
        else if (nRet < 0) {
            qDebug() << "Error receiving packet from encoder:" << nRet;
            break;
        }
        av_packet_rescale_ts(m_pPacket, m_pCodecContext->time_base, m_pVideoStream->time_base);
        m_pPacket->stream_index = m_pVideoStream->index;

        // 写入数据包
        if (av_interleaved_write_frame(m_pFormatContext, m_pPacket) < 0) {
            qDebug() << "Error writing packet";
        }
        av_packet_unref(m_pPacket);
    }
}

void CScreenRecorder::InitFFmpeg()
{
    av_log_set_level(AV_LOG_ERROR);

    // 打开输出文件
    if (avformat_alloc_output_context2(&m_pFormatContext, nullptr, nullptr, m_strOutputPath.c_str()) < 0) {
        qDebug() << "Could not create output context";
        return;
    }

    // 查找视频编码器
    AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "Video codec not found";
        return;
    }

    // 创建视频流
    m_pVideoStream = avformat_new_stream(m_pFormatContext, codec);
    if (!m_pVideoStream) {
        qDebug() << "Could not create video stream";
        return;
    }

    // 分配编码器上下文
    m_pCodecContext = avcodec_alloc_context3(codec);
    if (!m_pCodecContext) {
        qDebug() << "Could not allocate codec context";
        return;
    }

    // 设置编码器参数
    m_pCodecContext->codec_id = AV_CODEC_ID_H264;
    m_pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    m_pCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
    m_pCodecContext->width = QGuiApplication::primaryScreen()->geometry().width();
    m_pCodecContext->height = QGuiApplication::primaryScreen()->geometry().height();
    m_pCodecContext->time_base = { 1, 30 };
    m_pCodecContext->framerate = { 30, 1 };
    m_pCodecContext->bit_rate = 4000000;
    m_pCodecContext->gop_size = 10;
    m_pCodecContext->max_b_frames = 1;

    if (m_pFormatContext->oformat->flags & AVFMT_GLOBALHEADER)
    {
        m_pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    // 打开编码器
    AVDictionary* pOpts = nullptr;
    av_dict_set(&pOpts, "preset", "ultrafast", 0);
    av_dict_set(&pOpts, "tune", "zerolatency", 0);
    if (avcodec_open2(m_pCodecContext, codec, &pOpts) < 0)
    {
        qDebug() << "Could not open codec";
        return;
    }
    av_dict_free(&pOpts);

    // 复制编码器参数到视频流
    avcodec_parameters_from_context(m_pVideoStream->codecpar, m_pCodecContext);
    m_pVideoStream->time_base = m_pCodecContext->time_base;

    // 打开输出文件
    if (!(m_pFormatContext->oformat->flags & AVFMT_NOFILE))
    {
        if (avio_open(&m_pFormatContext->pb, m_strOutputPath.c_str(), AVIO_FLAG_WRITE) < 0)
        {
            qDebug() << "Could not open output file";
            return;
        }
    }

    // 写入文件头
    if (avformat_write_header(m_pFormatContext, nullptr) < 0)
    {
        qDebug() << "Error occurred when opening output file";
        return;
    }

    // 分配视频帧
    m_pFrame = av_frame_alloc();
    if (!m_pFrame)
    {
        qDebug() << "Could not allocate video frame";
        return;
    }

    m_pFrame->format = m_pCodecContext->pix_fmt;
    m_pFrame->width = m_pCodecContext->width;
    m_pFrame->height = m_pCodecContext->height;
    if (av_frame_get_buffer(m_pFrame, 0) < 0)
    {
        qDebug() << "Could not allocate the video frame data";
        return;
    }

    // 分配数据包
    m_pPacket = av_packet_alloc();
    if (!m_pPacket)
    {
        qDebug() << "Could not allocate packet";
        return;
    }

    // 初始化缩放上下文
    m_pSwsContext = sws_getContext(m_pCodecContext->width, m_pCodecContext->height, AV_PIX_FMT_BGRA,
        m_pCodecContext->width, m_pCodecContext->height, m_pCodecContext->pix_fmt,
        SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!m_pSwsContext)
    {
        qDebug() << "Could not initialize sws context";
        return;
    }

    m_bFFmpegInited = true;
}

void CScreenRecorder::CleanFFmpeg()
{
    if (m_pPacket) {
        av_packet_free(&m_pPacket);
        m_pPacket = nullptr;
    }
    if (m_pFrame) {
        av_frame_free(&m_pFrame);
        m_pFrame = nullptr;
    }
    if (m_pSwsContext) {
        sws_freeContext(m_pSwsContext);
        m_pSwsContext = nullptr;
    }
    if (m_pCodecContext) {
        avcodec_free_context(&m_pCodecContext);
        m_pCodecContext = nullptr;
    }
    if (m_pFormatContext)
    {
        if (!(m_pFormatContext->oformat->flags & AVFMT_NOFILE))
        {
            avio_closep(&m_pFormatContext->pb);
        }
        avformat_free_context(m_pFormatContext);
        m_pFormatContext = nullptr;
    }
    m_bFFmpegInited = false;
}

void CScreenRecorder::DebugLog(int nError)
{
    char cErrbuf[1024];
    av_strerror(nError, cErrbuf, sizeof(cErrbuf));
    qDebug() << cErrbuf;
}

三、界面类

cpp 复制代码
#include "../Include/EVideoWidget.h"
#include "ui_EVideoWidget.h"
#include "QtGui/Include/Conversion.h"
#include <QCameraInfo>
#include <QCamera>
#include <QMessageBox>
#include <QDebug>
#include <QDir>

CEVideoWidget::CEVideoWidget(QWidget* parent)
	: QWidget(parent)
	, ui(std::make_unique<Ui::CEVideoWidget>())
{
	ui->setupUi(this);
    InitUI();
}

CEVideoWidget::~CEVideoWidget()
{
    Q_EMIT SigStop();
    m_pScreenRecorderThread->quit();
    m_pScreenRecorderThread->wait();
}

void CEVideoWidget::InitUI()
{
    QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
    if (cameras.isEmpty()) 
    {
        // 若没有可用摄像头,添加默认项
        ui->comboBox_Camera->addItem(TransString2Unicode("未检测到摄像头"));
    }
    else 
    {
        // 遍历摄像头列表并添加到 QComboBox
        for (const QCameraInfo& cameraInfo : cameras) 
        {
            ui->comboBox_Camera->addItem(cameraInfo.description());
        }
    }

    QDir dir(QCoreApplication::applicationDirPath() + "/Data");
    if (!dir.exists())
    {
        dir.mkpath(QCoreApplication::applicationDirPath() + "/Data");
    }

   m_pScreenRecorder = std::make_unique<CScreenRecorder>();
   m_pScreenRecorderThread = std::make_unique<QThread>();
   m_pScreenRecorder->moveToThread(m_pScreenRecorderThread.get());
   connect(this, &CEVideoWidget::SigStop, m_pScreenRecorder.get(), &CScreenRecorder::SlotStop);
   qRegisterMetaType<std::string>("std::string");
   connect(this, &CEVideoWidget::SigStartRecording, m_pScreenRecorder.get(), &CScreenRecorder::SlotStartRecording);
   connect(this, &CEVideoWidget::SigStopRecording, m_pScreenRecorder.get(), &CScreenRecorder::SlotStopRecording);
   m_pScreenRecorderThread->start();
   ui->pushButton_Start->setEnabled(true);
   ui->pushButton_Stop->setEnabled(false);

   m_pTimer = new QTimer(this);
   connect(m_pTimer, &QTimer::timeout, this, &CEVideoWidget::SlotUpdateRecordTime);
}

void CEVideoWidget::on_pushButton_Start_clicked()
{
    if (ui->radioButton_Capture->isChecked())
    {
        QDateTime dtNow = QDateTime::currentDateTime();
        QString qstrTime = dtNow.toString("yyyyMMddhhmmsszzz");
        m_strVideoPath = TransUnicode2String(QCoreApplication::applicationDirPath() + "/Data/" + qstrTime) + "_capture.mp4";
        ui->lineEdit_CapturePath->setText(TransString2Unicode(m_strVideoPath));
        Q_EMIT SigStartRecording(m_strVideoPath);
        m_pTimer->start(1000);
        m_dtStartRecord = dtNow;
    }
    else if (ui->radioButton_Live->isChecked())
    {

    }
    else
    {
        QMessageBox::warning(this, TransString2Unicode("警告"), TransString2Unicode("请选择模式"));
    }
    ui->pushButton_Start->setEnabled(false);
    ui->pushButton_Stop->setEnabled(true);
}

void CEVideoWidget::on_pushButton_Stop_clicked()
{
    if (ui->radioButton_Capture->isChecked())
    {
        m_pTimer->stop();
        Q_EMIT SigStopRecording();
    }
    else if (ui->radioButton_Live->isChecked())
    {

    }
    else
    {
        QMessageBox::warning(this, TransString2Unicode("警告"), TransString2Unicode("请选择模式"));
    }
    ui->pushButton_Start->setEnabled(true);
    ui->pushButton_Stop->setEnabled(false);
}

void CEVideoWidget::SlotUpdateRecordTime()
{
    QDateTime dtNow = QDateTime::currentDateTime();
    qint64 nDiff = 0;
    nDiff = m_dtStartRecord.secsTo(dtNow);
    //00:00:00
    QString qstrTime = QString("%1:%2:%3").arg(nDiff / 3600, 2, 10, QChar('0')).arg((nDiff % 3600) / 60, 2, 10, QChar('0')).arg(nDiff % 60, 2, 10, QChar('0'));
    ui->label_RecordTime->setText(qstrTime);
}

四、结果与总结

修改实现了目前录制功能 ,目前只实现了录制电脑桌面视频没有加入音频,后续加入音频完善,并完成直播推流功能

相关推荐
liujing102329291 小时前
Day13_C语言基础&项目实战
c语言·开发语言
周振超的1 小时前
c++编译第三方项目报错# pragma warning( disable: 4273)
开发语言·c++
JH30732 小时前
Java Stream API 在企业开发中的实战心得:高效、优雅的数据处理
java·开发语言·oracle
呆呆的小草5 小时前
Cesium距离测量、角度测量、面积测量
开发语言·前端·javascript
uyeonashi5 小时前
【QT系统相关】QT文件
开发语言·c++·qt·学习
冬天vs不冷6 小时前
Java分层开发必知:PO、BO、DTO、VO、POJO概念详解
java·开发语言
sunny-ll6 小时前
【C++】详解vector二维数组的全部操作(超细图例解析!!!)
c语言·开发语言·c++·算法·面试
猎人everest7 小时前
Django的HelloWorld程序
开发语言·python·django
嵌入式@秋刀鱼7 小时前
《第四章-筋骨淬炼》 C++修炼生涯笔记(基础篇)数组与函数
开发语言·数据结构·c++·笔记·算法·链表·visual studio code
嵌入式@秋刀鱼7 小时前
《第五章-心法进阶》 C++修炼生涯笔记(基础篇)指针与结构体⭐⭐⭐⭐⭐
c语言·开发语言·数据结构·c++·笔记·算法·visual studio code