文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
开源 C++ QT QML 开发(十一)通讯--TCP服务器端
开源 C++ QT QML 开发(十二)通讯--TCP客户端
开源 C++ QT QML 开发(十五)通讯--http下载
开源 C++ QT QML 开发(十七)进程--LocalSocket
开源 C++ QT QML 开发(二十)多媒体--摄像头拍照
开源 C++ QT QML 开发(二十一)多媒体--视频播放
推荐链接:
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:一个使用外部FFmpeg进程进行摄像头录制的Qt QML应用程序。
1.代码分析
2.所有源码
3.效果演示
一、代码分析
QML部分函数分析
-
初始化函数
Component.onCompleted: {
console.log("初始化摄像头...")
if (QtMultimedia.availableCameras.length > 0) {
camera.start()
statusMessage = "摄像头就绪 - 点击开始录制"
} else {
statusMessage = "未检测到摄像头"
startButton.enabled = false
}
}
功能:应用程序启动时自动执行
检查系统可用摄像头数量
启动第一个可用摄像头
设置初始状态消息
无摄像头时禁用开始按钮
-
开始录制函数
function startRecording() {
var timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
var fileName = "ffmpeg_record_" + timestamp + ".mp4"
var savePath = "C:/Users/Administrator/Desktop/" + fileNameconsole.log("准备开始录制:", savePath) // 先停止摄像头预览 stopCameraPreview() // 延迟启动FFmpeg录制 startRecordingTimer.savedPath = savePath startRecordingTimer.start()
}
功能:启动录制流程
生成带时间戳的文件名
构建完整保存路径
停止Qt摄像头预览释放设备
启动延迟定时器
参数处理:
timestamp: 格式化为 2025-10-16T15-10-02
fileName: 固定前缀 + 时间戳 + .mp4后缀
savePath: 硬编码桌面路径
-
停止摄像头预览函数
function stopCameraPreview() {
console.log("停止摄像头预览")
cameraActive = false
camera.stop()
}
功能:释放摄像头资源
设置 cameraActive = false 隐藏视频预览
调用 camera.stop() 释放硬件设备
-
停止录制函数
function stopRecording() {
console.log("停止FFmpeg录制")
ffmpegRecorder.stopRecording()
}
功能:委托给C++后端停止录制
简单的代理函数
调用C++的 stopRecording() 方法
-
状态文本转换函数
function getCameraStatusText(status) {
switch(status) {
case Camera.ActiveStatus: return "活动"
case Camera.LoadingStatus: return "加载中"
case Camera.StartingStatus: return "启动中"
case Camera.StoppingStatus: return "停止中"
case Camera.StandbyStatus: return "待机"
case Camera.UnavailableStatus: return "不可用"
default: return "未知"
}
}
功能:将摄像头状态码转换为可读文本
处理6种标准摄像头状态
提供默认"未知"状态处理
-
时间格式化函数
function formatTime(seconds) {
var hours = Math.floor(seconds / 3600)
var minutes = Math.floor((seconds % 3600) / 60)
var secs = seconds % 60
return (hours < 10 ? "0" + hours : hours) + ":" +
(minutes < 10 ? "0" + minutes : minutes) + ":" +
(secs < 10 ? "0" + secs : secs)
}
功能:将秒数格式化为 HH:MM:SS
数学计算时分秒
补零格式化(01:05:09)
支持超过24小时的显示
-
定时器更新函数
function updateTimerDisplay() {
timerText.text = "录制时间: " + formatTime(recordingSeconds)
}
功能:更新界面计时器显示
组合文本和时间格式
每秒调用一次
C++部分函数分析
-
构造函数
explicit FFmpegRecorder(QObject *parent = nullptr)
: QObject(parent), m_isRecording(false) {}
功能:初始化录制器
设置父对象
初始化录制状态为false
-
属性读取函数
bool isRecording() const { return m_isRecording; }
QString statusMessage() const { return m_statusMessage; }
功能:QML属性绑定支持
提供只读属性访问
用于QML的数据绑定
-
开始录制函数(QML可调用)
Q_INVOKABLE void startRecording(const QString &outputPath) {
if (m_isRecording) {
setStatusMessage("已经在录制中");
return;
}m_outputPath = outputPath; startFFmpegRecording(outputPath);
}
功能:录制入口点
检查重复录制
保存输出路径
调用内部录制函数
-
停止录制函数(QML可调用)
Q_INVOKABLE void stopRecording() {
if (m_process && m_isRecording) {
// 向FFmpeg发送q信号来优雅停止
m_process->write("q");
m_process->closeWriteChannel();
setStatusMessage("正在停止录制...");
}
}
功能:优雅停止FFmpeg
向FFmpeg进程发送"q"信号
关闭写入通道
更新状态消息
-
内部FFmpeg启动函数
void startFFmpegRecording(const QString &outputPath) {
QString ffmpegPath = "C:/ffmpeg/bin/ffmpeg.exe";QStringList arguments; arguments << "-f" << "dshow" << "-i" << "video=Integrated Camera" << "-t" << "300" // 录制5分钟 << "-y" // 覆盖已存在文件 << QDir::toNativeSeparators(outputPath); qDebug() << "启动FFmpeg录制..."; m_process = new QProcess(this); // 连接信号槽 connect(m_process, &QProcess::started, this, [this]() { m_isRecording = true; setStatusMessage("正在录制中..."); emit isRecordingChanged(); }); // ... 其他连接 m_process->start(ffmpegPath, arguments); if (!m_process->waitForStarted(3000)) { setStatusMessage("启动FFmpeg失败: " + m_process->errorString()); // 清理资源 }
}
功能:核心录制逻辑
构建FFmpeg命令行参数
创建并配置QProcess
连接进程信号
启动进程并处理超时
FFmpeg参数详解:
-f dshow: Windows DirectShow输入格式
-i video=Integrated Camera: 指定摄像头设备
-t 300: 录制300秒(5分钟)
-y: 覆盖输出文件
outputPath: 输出文件路径
-
进程完成处理函数
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "FFmpeg进程结束,退出码:" << exitCode;m_isRecording = false; if (exitCode == 0) { setStatusMessage("录制完成: " + m_outputPath); } else { setStatusMessage("录制失败,退出码: " + QString::number(exitCode)); } if (m_process) { m_process->deleteLater(); m_process = nullptr; } emit isRecordingChanged();
}
功能:处理FFmpeg进程结束
解析退出码判断成功/失败
清理进程资源
通知QML状态变化
-
进程错误处理函数
void onProcessError(QProcess::ProcessError error) {
qDebug() << "FFmpeg进程错误:" << error;m_isRecording = false; setStatusMessage("FFmpeg错误: " + QString::number(error)); if (m_process) { m_process->deleteLater(); m_process = nullptr; } emit isRecordingChanged();
}
功能:处理进程启动/运行错误
记录错误类型
重置录制状态
清理资源
-
状态消息设置函数
void setStatusMessage(const QString &message) {
if (m_statusMessage != message) {
m_statusMessage = message;
emit statusMessageChanged();
}
}
功能:状态消息管理
避免重复设置相同消息
触发属性变化信号
定时器功能分析
-
录制计时器
Timer {
id: recordTimer
interval: 1000
repeat: true
running: ffmpegRecorder.isRecording
onTriggered: {
recordingSeconds++
updateTimerDisplay()
}
}
功能:录制时长计数
每秒触发一次
仅在录制时运行
累加录制秒数
-
延迟启动定时器
Timer {
id: startRecordingTimer
property string savedPath: ""
interval: 1000
onTriggered: {
console.log("延迟启动FFmpeg录制:", savedPath)
ffmpegRecorder.startRecording(savedPath)
recordingSeconds = 0
updateTimerDisplay()
}
}
功能:确保摄像头完全释放
1秒延迟避免设备冲突
保存路径传递
重置计时器
-
预览重启定时器
Timer {
id: restartPreviewTimer
interval: 500
onTriggered: {
console.log("重新启动摄像头预览")
cameraActive = true
if (camera.cameraStatus !== Camera.ActiveStatus) {
camera.start()
}
}
}
功能:录制完成后恢复预览
500ms延迟确保FFmpeg完全退出
重新激活摄像头
条件启动避免重复调用
信号连接分析
Connections {
target: ffmpegRecorder
function onStopCameraPreview() {
console.log("停止摄像头预览")
stopCameraPreview()
}
function onStartCameraPreview() {
console.log("启动摄像头预览")
restartPreviewTimer.start()
}
}
功能:C++到QML的通信桥梁
响应C++发出的控制信号
但当前代码中C++并未实际发出这些信号(代码不完整)
二、所有源码
在Qt中使用摄像头进行录像,通常涉及到视频捕获和编码处理。FFmpeg是一个非常强大的开源库,它提供了广泛的视频和音频格式的支持,包括编码、解码、转码、录制等功能。在Qt中使用FFmpeg来实现摄像头录像的功能,有几个关键原因:
广泛的格式支持:FFmpeg支持几乎所有常见的视频和音频格式,包括但不限于H.264、H.265、VP8、VP9等。这使得使用FFmpeg可以很容易地录制多种格式的视频。
硬件加速:FFmpeg支持多种硬件加速技术,如NVENC(用于NVIDIA显卡)、VA-API(用于Intel显卡)和AMD的VCE。这些硬件加速可以显著提高视频编码的效率,减少CPU的负担,尤其是在处理高清视频时。
灵活的API:FFmpeg提供了丰富的API,允许开发者以编程方式控制几乎所有的视频处理功能。这包括设置视频编码参数(如比特率、帧率、分辨率等)、音频处理等。
跨平台:FFmpeg是跨平台的,可以在Windows、macOS、Linux等多种操作系统上运行。这使得使用FFmpeg开发的视频处理应用具有很好的移植性。
集成到Qt中:虽然Qt本身提供了视频和相机相关的类(如QCamera和QCameraImageCapture),但这些类在某些复杂场景下可能不够用,或者需要额外的功能支持。通过集成FFmpeg,可以扩展Qt应用在视频处理方面的能力。
安装FFmpeg(必要步骤):
下载FFmpeg:https://www.gyan.dev/ffmpeg/builds/
解压到 C:\ffmpeg\
将 C:\ffmpeg\bin 添加到系统PATH环境变量
重启电脑
.pro文件需要添加
QT += quick quickcontrols2 multimedia multimediawidgets
main.qml文件源码
import QtQuick 2.14
import QtQuick.Window 2.14
import QtMultimedia 5.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
ApplicationWindow {
id: window
width: 800
height: 600
visible: true
title: qsTr("摄像头录像 - FFmpeg外部进程")
property int recordingSeconds: 0
property string statusMessage: "正在初始化..."
property bool cameraActive: true
// 摄像头组件
Camera {
id: camera
deviceId: QtMultimedia.availableCameras.length > 0 ? QtMultimedia.availableCameras[0].deviceId : ""
onError: {
console.error("摄像头错误:", errorString)
statusMessage = "摄像头错误: " + errorString
}
onCameraStatusChanged: {
console.log("摄像头状态:", cameraStatus)
if (cameraStatus === Camera.ActiveStatus) {
statusMessage = "摄像头就绪 - 点击开始录制"
}
}
}
// 视频输出 - 只在摄像头激活时显示
VideoOutput {
id: videoOutput
anchors.fill: parent
source: camera
visible: cameraActive
}
// 录制时的占位背景
Rectangle {
anchors.fill: parent
color: "black"
visible: !cameraActive
Column {
anchors.centerIn: parent
spacing: 20
Text {
text: "● 正在录制中"
color: "#e74c3c"
font.pixelSize: 32
font.bold: true
}
Text {
text: timerText.text
color: "white"
font.pixelSize: 24
font.family: "Monospace"
}
Text {
text: "录制完成后将自动恢复预览"
color: "#aaaaaa"
font.pixelSize: 16
}
}
}
// 控制面板
Rectangle {
width: parent.width
height: 160
anchors.bottom: parent.bottom
color: "#CC000000"
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 状态显示
Text {
Layout.fillWidth: true
text: ffmpegRecorder.statusMessage || statusMessage
color: ffmpegRecorder.isRecording ? "#e74c3c" : "white"
font.pixelSize: 14
wrapMode: Text.Wrap
}
// 录制时间
Text {
id: timerText
Layout.fillWidth: true
text: "录制时间: " + formatTime(recordingSeconds)
color: "#e74c3c"
font.bold: true
font.pixelSize: 18
font.family: "Monospace"
visible: ffmpegRecorder.isRecording
}
// 按钮行
RowLayout {
Layout.fillWidth: true
spacing: 15
Button {
id: startButton
text: "● 开始录制"
enabled: !ffmpegRecorder.isRecording && cameraActive
onClicked: startRecording()
background: Rectangle {
color: startButton.enabled ? "#27ae60" : "#666666"
radius: 5
}
contentItem: Text {
text: startButton.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
}
Layout.preferredWidth: 120
Layout.preferredHeight: 40
}
Button {
id: stopButton
text: "■ 停止录制"
enabled: ffmpegRecorder.isRecording
onClicked: stopRecording()
background: Rectangle {
color: stopButton.enabled ? "#e74c3c" : "#666666"
radius: 5
}
contentItem: Text {
text: stopButton.text
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.bold: true
}
Layout.preferredWidth: 120
Layout.preferredHeight: 40
}
// 录制指示器
Rectangle {
width: 20
height: 20
radius: 10
color: ffmpegRecorder.isRecording ? "#e74c3c" : "transparent"
border.color: "white"
border.width: 2
SequentialAnimation on opacity {
running: ffmpegRecorder.isRecording
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 0.3; duration: 500 }
NumberAnimation { from: 0.3; to: 1.0; duration: 500 }
}
}
Item { Layout.fillWidth: true }
}
// 调试信息
Text {
Layout.fillWidth: true
text: "预览状态: " + (cameraActive ? "开启" : "关闭") +
" | 摄像头: " + getCameraStatusText(camera.cameraStatus) +
" | 录制器: " + (ffmpegRecorder.isRecording ? "录制中" : "空闲")
color: "#666666"
font.pixelSize: 10
}
}
}
// 录制计时器
Timer {
id: recordTimer
interval: 1000
repeat: true
running: ffmpegRecorder.isRecording
onTriggered: {
recordingSeconds++
updateTimerDisplay()
}
}
// 启动录制定时器(延迟启动)
Timer {
id: startRecordingTimer
property string savedPath: ""
interval: 1000
onTriggered: {
console.log("延迟启动FFmpeg录制:", savedPath)
ffmpegRecorder.startRecording(savedPath)
recordingSeconds = 0
updateTimerDisplay()
}
}
// 重新启动预览定时器
Timer {
id: restartPreviewTimer
interval: 500
onTriggered: {
console.log("重新启动摄像头预览")
cameraActive = true
if (camera.cameraStatus !== Camera.ActiveStatus) {
camera.start()
}
}
}
// 连接FFmpegRecorder信号
Connections {
target: ffmpegRecorder
function onStopCameraPreview() {
console.log("停止摄像头预览")
stopCameraPreview()
}
function onStartCameraPreview() {
console.log("启动摄像头预览")
restartPreviewTimer.start()
}
}
// 初始化
Component.onCompleted: {
console.log("初始化摄像头...")
if (QtMultimedia.availableCameras.length > 0) {
camera.start()
statusMessage = "摄像头就绪 - 点击开始录制"
} else {
statusMessage = "未检测到摄像头"
startButton.enabled = false
}
}
// 函数定义
function startRecording() {
var timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
var fileName = "ffmpeg_record_" + timestamp + ".mp4"
var savePath = "C:/Users/Administrator/Desktop/" + fileName
console.log("准备开始录制:", savePath)
// 先停止摄像头预览
stopCameraPreview()
// 延迟启动FFmpeg录制
startRecordingTimer.savedPath = savePath
startRecordingTimer.start()
}
function stopRecording() {
console.log("停止FFmpeg录制")
ffmpegRecorder.stopRecording()
}
function stopCameraPreview() {
console.log("停止摄像头预览")
cameraActive = false
camera.stop()
}
function updateTimerDisplay() {
timerText.text = "录制时间: " + formatTime(recordingSeconds)
}
function getCameraStatusText(status) {
switch(status) {
case Camera.ActiveStatus: return "活动"
case Camera.LoadingStatus: return "加载中"
case Camera.StartingStatus: return "启动中"
case Camera.StoppingStatus: return "停止中"
case Camera.StandbyStatus: return "待机"
case Camera.UnavailableStatus: return "不可用"
default: return "未知"
}
}
function formatTime(seconds) {
var hours = Math.floor(seconds / 3600)
var minutes = Math.floor((seconds % 3600) / 60)
var secs = seconds % 60
return (hours < 10 ? "0" + hours : hours) + ":" +
(minutes < 10 ? "0" + minutes : minutes) + ":" +
(secs < 10 ? "0" + secs : secs)
}
}
main.cpp文件源码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QProcess>
#include <QDebug>
#include <QDir>
#include <QTimer>
class FFmpegRecorder : public QObject
{
Q_OBJECT
Q_PROPERTY(bool isRecording READ isRecording NOTIFY isRecordingChanged)
Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged)
public:
explicit FFmpegRecorder(QObject *parent = nullptr) : QObject(parent), m_isRecording(false) {}
bool isRecording() const { return m_isRecording; }
QString statusMessage() const { return m_statusMessage; }
Q_INVOKABLE void startRecording(const QString &outputPath) {
if (m_isRecording) {
setStatusMessage("已经在录制中");
return;
}
m_outputPath = outputPath;
startFFmpegRecording(outputPath);
}
Q_INVOKABLE void stopRecording() {
if (m_process && m_isRecording) {
// 向FFmpeg发送q信号来优雅停止
m_process->write("q");
m_process->closeWriteChannel();
setStatusMessage("正在停止录制...");
}
}
signals:
void isRecordingChanged();
void statusMessageChanged();
private:
void startFFmpegRecording(const QString &outputPath) {
QString ffmpegPath = "C:/ffmpeg/bin/ffmpeg.exe";
QStringList arguments;
arguments << "-f" << "dshow"
<< "-i" << "video=Integrated Camera"
<< "-t" << "300" // 录制5分钟
<< "-y" // 覆盖已存在文件
<< QDir::toNativeSeparators(outputPath);
qDebug() << "启动FFmpeg录制...";
m_process = new QProcess(this);
connect(m_process, &QProcess::started, this, [this]() {
m_isRecording = true;
setStatusMessage("正在录制中...");
emit isRecordingChanged();
});
connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, &FFmpegRecorder::onProcessFinished);
connect(m_process, &QProcess::errorOccurred, this, &FFmpegRecorder::onProcessError);
connect(m_process, &QProcess::readyReadStandardError, this, [this]() {
QString errorOutput = m_process->readAllStandardError();
qDebug() << "FFmpeg输出:" << errorOutput;
});
m_process->start(ffmpegPath, arguments);
if (!m_process->waitForStarted(3000)) {
setStatusMessage("启动FFmpeg失败: " + m_process->errorString());
delete m_process;
m_process = nullptr;
m_isRecording = false;
emit isRecordingChanged();
}
}
private slots:
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "FFmpeg进程结束,退出码:" << exitCode;
m_isRecording = false;
if (exitCode == 0) {
setStatusMessage("录制完成: " + m_outputPath);
} else {
setStatusMessage("录制失败,退出码: " + QString::number(exitCode));
}
if (m_process) {
m_process->deleteLater();
m_process = nullptr;
}
emit isRecordingChanged();
}
void onProcessError(QProcess::ProcessError error) {
qDebug() << "FFmpeg进程错误:" << error;
m_isRecording = false;
setStatusMessage("FFmpeg错误: " + QString::number(error));
if (m_process) {
m_process->deleteLater();
m_process = nullptr;
}
emit isRecordingChanged();
}
private:
void setStatusMessage(const QString &message) {
if (m_statusMessage != message) {
m_statusMessage = message;
emit statusMessageChanged();
}
}
private:
QProcess *m_process = nullptr;
bool m_isRecording;
QString m_statusMessage;
QString m_outputPath;
};
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<FFmpegRecorder>("FFmpeg", 1, 0, "FFmpegRecorder");
QQmlApplicationEngine engine;
FFmpegRecorder recorder;
engine.rootContext()->setContextProperty("ffmpegRecorder", &recorder);
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
#include "main.moc"
三、效果演示

、
