一、思路与界面设计
在现代软件开发中,屏幕录制功能有着广泛的应用,如教学演示、游戏直播、软件操作记录等。本项目旨在使用 Qt 和 FFmpeg 库实现一个屏幕录制程序,利用 Qt 进行屏幕画面的捕获和界面交互,借助 FFmpeg 强大的音视频处理能力完成视频的编码和保存。
整体架构设计
整个屏幕录制程序主要分为两个核心模块:屏幕捕获模块和视频编码保存模块。屏幕捕获模块负责定时从屏幕抓取画面,视频编码保存模块则将捕获的画面进行编码并写入视频文件。
各模块实现思路
(一)屏幕捕获模块
该模块基于 Qt 框架实现,主要利用 QGuiApplication 和 QScreen 类来完成屏幕画面的捕获。具体步骤如下:
获取主屏幕对象:通过 QGuiApplication::primaryScreen() 方法获取当前系统的主屏幕对象。
捕获屏幕画面:调用 grabWindow(0) 方法抓取整个屏幕的画面,返回一个 QPixmap 对象,再将其转换为 QImage 对象以便后续处理。
像素格式转换:将捕获的 QImage 对象转换为 QImage::Format_ARGB32 格式,确保与后续 FFmpeg 处理的像素格式兼容。
定时捕获:使用 QTimer 定时器,按照设定的帧率(如 30fps)定时触发屏幕捕获操作。
(二)视频编码保存模块
此模块基于 FFmpeg 库实现,主要完成视频的编码和保存任务。具体步骤如下:
- 初始化 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 函数初始化图像缩放上下文,用于将捕获的屏幕画面转换为编码器所需的像素格式。 - 视频编码
像素格式转换:使用 sws_scale 函数将捕获的 QImage 数据转换为编码器所需的 AVFrame 数据。
设置时间戳:为 AVFrame 设置正确的时间戳 pts,确保视频播放的时间顺序正确。
发送帧到编码器:调用 avcodec_send_frame 函数将 AVFrame 发送到编码器进行编码。
接收编码后的数据包:当编码器缓冲区满时,使用 avcodec_receive_packet 函数接收编码后的 AVPacket,并将其写入输出文件。 - 结束录制
刷新编码器:调用 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);
}
四、结果与总结

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