当前位置: 首页 > news >正文

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,确保时间戳的准确性。
在这里插入图片描述

二、录屏类实现

#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 };// 正确计算 ptsqint64 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;
}

三、界面类

#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 {// 遍历摄像头列表并添加到 QComboBoxfor (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:00QString 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);
}

四、结果与总结

在这里插入图片描述

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

相关文章:

  • vue+cesium示例:3D热力图(附源码下载)
  • pkg-config --cflags --libs opencv4详细解释
  • LangGraph基础知识(Graph-GraphState)
  • Ansible 错误处理:确保高效自动化
  • 大模型——基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程
  • 河南建筑安全员C证考试常见题及答案解析
  • MyBatis中关于缓存的理解
  • stm32进入Infinite_Loop原因(因为有系统中断函数未自定义实现)
  • 门静脉高压——检查
  • FreeRTOS学习01_移植FreeRTOS到STM32(图文详解)
  • 从0到1构建我的AI星逻系统: LLM智能控制 + Streamlit前端实战
  • Netty
  • 简繁体智能翻译软件
  • ThreadLocal 源码
  • 7种分类数据编码技术详解:从原理到实战
  • 学习日记-day25-6.9
  • ArcGIS应用与FLUS模型预测:从安装到土地利用建模,数据管理、地图制作、遥感解译、空间分析、地形分析及案例分析攻略
  • 篇章二 论坛系统——系统设计
  • 【记录坑点问题】IDEA运行:maven-resources-production:XX: OOM: Java heap space
  • 监控升级:可视化如何让每一个细节 “说话”
  • 网页设计企业网站设计的功能/怎样做电商 入手
  • 网站建设合同书保密条款/sem代运营托管公司
  • 宣传网站建设方案模板/百度推广一天费用200
  • 最好的装饰公司营销型网站/全网营销图片
  • 淮安哪里有做网站的/google play应用商店
  • 住房和城乡建设部网站园林一级/优化什么