1. 项目背景与需求分析
作为一名长期从事AI模型训练的开发者,我最近遇到了一个看似简单却颇具挑战性的需求。在训练QwenVL多模态模型时,需要从客户的远程桌面(通过向日葵远程控制软件连接)录制大量视频素材作为训练数据。这个需求看似普通,却引发了我从付费软件到自主开发的技术探索之旅。
最初,我尝试了市面上常见的几款录屏软件,但很快就遇到了几个痛点:
- 免费版本功能受限(如1分钟时长限制)
- 付费订阅价格不合理(基础功能也需要高价套餐)
- 软件体积臃肿,附带不必要的功能
- 录制参数无法精细控制(如帧率、画质等)
更令人困扰的是,当我检查这些软件的安装目录时,发现它们底层都在使用FFmpeg这个开源工具。这让我意识到:既然商业软件也是基于FFmpeg,为什么不自己开发一个完全符合需求的解决方案呢?
2. 技术选型与架构设计
2.1 核心工具选择
FFmpeg 作为本项目的核心引擎,它是一个完整的、跨平台的解决方案,可以用于录制、转换和流式传输音视频。选择FFmpeg的主要原因包括:
- 开源免费,无任何使用限制
- 支持几乎所有视频/音频格式
- 提供丰富的参数调节选项
- 可通过命令行或API调用
- 社区活跃,文档完善
Qt6 作为GUI开发框架,主要考虑因素有:
- 跨平台支持(Windows/macOS/Linux)
- 成熟的UI组件库
- 强大的事件处理机制
- 与C++的良好集成(便于调用FFmpeg)
- 活跃的开发者社区
2.2 系统架构设计
整个录屏工具采用经典的三层架构:
code复制┌───────────────────────┐
│ UI层 │
│ (Qt6 Widgets应用) │
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 逻辑控制层 │
│ (FFmpeg命令组装与执行)│
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 系统资源层 │
│ (屏幕捕获/音频采集) │
└───────────────────────┘
3. 核心功能实现细节
3.1 区域选择功能实现
区域选择是录屏工具的核心交互功能,需要解决两个关键问题:
- 如何实现半透明遮罩效果
- 如何获取用户选择的矩形区域
透明遮罩实现代码:
cpp复制// 在RegionSelector类中
void RegionSelector::paintEvent(QPaintEvent* event) {
QPainter painter(this);
// 绘制半透明黑色背景(100表示透明度)
painter.fillRect(rect(), QColor(0, 0, 0, 100));
// 清除选中区域(完全透明)
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.fillRect(selectedRect, Qt::transparent);
// 绘制选中区域边框
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
painter.setPen(QPen(Qt::red, 2));
painter.drawRect(selectedRect);
}
鼠标交互逻辑:
cpp复制void RegionSelector::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
startPos = event->pos();
selectedRect = QRect(startPos, QSize(0, 0));
}
}
void RegionSelector::mouseMoveEvent(QMouseEvent* event) {
if (event->buttons() & Qt::LeftButton) {
selectedRect = QRect(startPos, event->pos()).normalized();
update(); // 触发重绘
}
}
3.2 FFmpeg录制命令构建
根据用户选择的参数,动态构建FFmpeg命令:
cpp复制QString buildFFmpegCommand(const RecordingParams& params) {
QStringList args;
// 输入设备设置(Windows平台使用gdigrab)
args << "-f" << "gdigrab";
args << "-framerate" << QString::number(params.fps);
args << "-i" << QString("desktop");
// 区域选择(如果有)
if (!params.region.isNull()) {
args << "-offset_x" << QString::number(params.region.x());
args << "-offset_y" << QString::number(params.region.y());
args << "-video_size" << QString("%1x%2")
.arg(params.region.width())
.arg(params.region.height());
}
// 音频输入(如果启用)
if (params.audioEnabled) {
args << "-f" << "dshow";
args << "-i" << "audio=麦克风阵列 (Realtek Audio)";
}
// 视频编码参数
args << "-c:v" << "libx264";
args << "-preset" << "fast";
args << "-crf" << QString::number(params.crf);
// 输出文件
args << "-y" << params.outputPath;
return "ffmpeg " + args.join(" ");
}
3.3 录制控制逻辑
启动录制:
cpp复制void startRecording(const QString& ffmpegPath, const QString& command) {
ffmpegProcess = new QProcess(this);
ffmpegProcess->start(ffmpegPath, command.split(" "));
if (!ffmpegProcess->waitForStarted()) {
qDebug() << "Failed to start FFmpeg process";
return;
}
// 启动计时器
recordingTimer.start(1000, this);
}
停止录制:
cpp复制void stopRecording() {
if (!ffmpegProcess) return;
// 优雅停止(发送'q'命令)
ffmpegProcess->write("q");
// 等待5秒让FFmpeg完成写入
if (!ffmpegProcess->waitForFinished(5000)) {
ffmpegProcess->kill(); // 强制终止
}
recordingTimer.stop();
}
4. 关键技术问题与解决方案
4.1 FFmpeg路径问题
问题现象:
程序运行时提示"FFmpeg Not Found",尽管系统PATH中已配置FFmpeg。
原因分析:
Qt应用程序运行时环境与命令行环境不同,可能无法继承用户的环境变量。
解决方案:
- 硬编码指定FFmpeg路径(不推荐)
- 运行时检测系统PATH(推荐)
改进后的路径检测代码:
cpp复制QString findFFmpeg() {
// 检查常见安装路径
QStringList possiblePaths = {
"C:/Program Files/ffmpeg/bin/ffmpeg.exe",
"C:/ffmpeg/bin/ffmpeg.exe",
"/usr/local/bin/ffmpeg",
"/usr/bin/ffmpeg"
};
// 检查系统PATH
QString path = qEnvironmentVariable("PATH");
foreach (QString dir, path.split(QDir::listSeparator())) {
possiblePaths << QDir(dir).filePath("ffmpeg");
possiblePaths << QDir(dir).filePath("ffmpeg.exe");
}
// 查找存在的路径
foreach (QString path, possiblePaths) {
if (QFile::exists(path)) {
return path;
}
}
return QString();
}
4.2 录制区域背景问题
问题现象:
选择录制区域时,整个屏幕背景全黑,无法看清内容。
技术分析:
默认情况下,Qt窗口不透明,且覆盖整个屏幕时会遮挡内容。
最终解决方案:
- 设置窗口透明属性
- 使用半透明遮罩
- 清除选中区域
cpp复制// 在RegionSelector构造函数中
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
setAttribute(Qt::WA_TranslucentBackground);
setWindowState(Qt::WindowFullScreen);
4.3 视频文件损坏问题
问题现象:
录制的视频文件无法播放,或播放时出现卡顿、花屏。
原因分析:
直接终止FFmpeg进程会导致视频文件未正确写入尾部信息。
解决方案对比:
| 方法 | 优点 | 缺点 |
|---|---|---|
| 发送'q'命令 | 优雅终止,文件完整 | 需要等待FFmpeg处理 |
| 使用Ctrl+C信号 | 类似命令行操作 | Windows平台实现复杂 |
| 设置录制时长 | 自动停止 | 需要提前知道时长 |
最优方案实现:
cpp复制void stopRecording() {
if (!ffmpegProcess) return;
// 方法1:发送'q'命令(推荐)
ffmpegProcess->write("q");
// 方法2:使用terminate()(跨平台)
ffmpegProcess->terminate();
// 等待最多5秒
if (!ffmpegProcess->waitForFinished(5000)) {
qWarning() << "FFmpeg did not exit gracefully, forcing kill";
ffmpegProcess->kill();
}
}
5. 功能扩展与参数优化
5.1 帧率与画质调节
帧率选择:
- 15fps:适合静态内容,文件体积小
- 30fps:流畅体验,平衡选择
- 60fps:高动态内容,文件较大
CRF(恒定质量因子)参数:
- 18-22:近乎无损,适合专业用途
- 23-28:良好平衡(默认23)
- 29-51:文件小但质量低
推荐参数组合:
| 使用场景 | 分辨率 | 帧率 | CRF | 预估文件大小(10分钟) |
|---|---|---|---|---|
| 文档演示 | 720p | 15 | 28 | ~50MB |
| 软件操作 | 1080p | 30 | 23 | ~200MB |
| 游戏录制 | 1080p | 60 | 18 | ~1GB |
5.2 音频录制集成
音频设备选择:
cpp复制QStringList getAudioDevices() {
QProcess ffmpeg;
ffmpeg.start("ffmpeg", {"-list_devices", "true", "-f", "dshow", "-i", "dummy"});
ffmpeg.waitForFinished();
QString output = ffmpeg.readAllStandardError();
QStringList devices;
// 解析输出获取音频设备列表
QRegularExpression re("\\\"(.*?)\\\"");
QRegularExpressionMatchIterator i = re.globalMatch(output);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
QString device = match.captured(1);
if (!device.contains("dummy") && !device.contains("Virtual"))
devices << device;
}
return devices;
}
音频参数优化:
cpp复制// 在FFmpeg命令中添加音频参数
if (audioEnabled) {
args << "-f" << "dshow";
args << "-i" << QString("audio=%1").arg(audioDevice);
args << "-c:a" << "aac";
args << "-b:a" << "128k";
args << "-ar" << "44100";
}
6. 实际应用与性能优化
6.1 多显示器支持
获取显示器信息:
cpp复制QList<QRect> getScreenGeometries() {
QList<QRect> geometries;
foreach (QScreen *screen, QGuiApplication::screens()) {
geometries.append(screen->geometry());
}
return geometries;
}
多显示器录制命令:
cpp复制// 录制第二个显示器(索引从0开始)
args << "-i" << "desktop";
args << "-offset_x" << QString::number(screenGeometries[1].x());
args << "-offset_y" << QString::number(screenGeometries[1].y());
args << "-video_size" << QString("%1x%2")
.arg(screenGeometries[1].width())
.arg(screenGeometries[1].height());
6.2 性能优化技巧
1. 内存管理优化:
- 使用QSharedPointer管理FFmpeg进程
- 及时释放不再使用的资源
- 避免在录制过程中频繁分配内存
2. CPU占用控制:
cpp复制// 使用更快的preset减少CPU占用
args << "-preset" << "fast";
// 或者使用硬件加速(如果可用)
#ifdef Q_OS_WIN
args << "-hwaccel" << "dxva2";
#elif defined(Q_OS_MAC)
args << "-hwaccel" << "videotoolbox";
#endif
3. 磁盘I/O优化:
- 将临时文件写入SSD
- 使用RAM磁盘存储临时文件
- 避免同时读写同一磁盘
7. 项目部署与打包
7.1 跨平台注意事项
Windows平台:
- 需要打包FFmpeg二进制文件
- 考虑使用windeployqt收集依赖
- 可制作NSIS或Inno Setup安装包
macOS平台:
- 使用macdeployqt打包
- 注意签名和公证流程
- 可能需要brew安装FFmpeg
Linux平台:
- 打包为AppImage或Snap
- 依赖系统中安装的FFmpeg
- 考虑Flatpak打包方案
7.2 一键打包脚本示例
bash复制#!/bin/bash
# Windows打包脚本
BUILD_DIR="build-win"
QMAKE_PATH="C:/Qt/6.5.0/msvc2019_64/bin/qmake"
DEPLOY_TOOL="C:/Qt/6.5.0/msvc2019_64/bin/windeployqt.exe"
mkdir -p $BUILD_DIR
cd $BUILD_DIR
# 编译
$QMAKE_PATH ../ScreenRecorder.pro
make -j8
# 拷贝FFmpeg
cp ../thirdparty/ffmpeg/bin/*.dll release/
# 部署Qt依赖
$DEPLOY_TOOL release/ScreenRecorder.exe
# 创建安装包
iscc ../installer/setup.iss
8. 项目总结与经验分享
在整个开发过程中,我积累了一些宝贵的经验,值得与大家分享:
1. AI辅助开发的正确姿势:
- 明确描述需求(包括环境、约束条件)
- 逐步细化问题(不要一次性问太复杂的问题)
- 验证生成的代码(AI可能忽略边界条件)
- 迭代优化(基于错误信息继续提问)
2. FFmpeg使用心得:
- 始终使用最新稳定版本
- 仔细阅读控制台输出(错误信息很有用)
- 复杂命令分步测试
- 善用
-preset参数平衡速度与压缩率
3. Qt开发技巧:
- 使用QLoggingCategory替代qDebug
- 善用信号槽的异步特性
- 跨线程操作注意线程安全
- 资源文件使用Qt资源系统管理
这个项目让我深刻体会到,现代开发已经进入了"AI+开源工具"的新范式。AI不是要取代开发者,而是让我们能更高效地利用现有的强大工具。正如这个录屏工具,核心功能其实都建立在FFmpeg这个开源神器之上,而AI帮助我快速理解了如何正确使用它。