开源 C++ QT QML 开发(二十二)多媒体--ffmpeg编码和录像
文章的目的为了记录使用QT QML开发学习的经历。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。
相关链接:
开源 C++ QT QML 开发(一)基本介绍
开源 C++ QT QML 开发(二)工程结构
开源 C++ QT QML 开发(三)常用控件
开源 C++ QT QML 开发(四)复杂控件--Listview
开源 C++ QT QML 开发(五)复杂控件--Gridview
开源 C++ QT QML 开发(六)自定义控件--波形图
开源 C++ QT QML 开发(七)自定义控件--仪表盘
开源 C++ QT QML 开发(八)自定义控件--圆环
开源 C++ QT QML 开发(九)文件--文本和二进制
开源 C++ QT QML 开发(十)通讯--串口
开源 C++ QT QML 开发(十一)通讯--TCP服务器端
开源 C++ QT QML 开发(十二)通讯--TCP客户端
开源 C++ QT QML 开发(十三)多线程
开源 C++ QT QML 开发(十四)进程用途
开源 C++ QT QML 开发(十五)通讯--http下载
开源 C++ QT QML 开发(十六)进程--共享内存
开源 C++ QT QML 开发(十七)进程--LocalSocket
开源 C++ QT QML 开发(十八)多媒体--音频播放
开源 C++ QT QML 开发(十九)多媒体--音频录制
开源 C++ QT QML 开发(二十)多媒体--摄像头拍照
开源 C++ QT QML 开发(二十一)多媒体--视频播放
推荐链接:
开源 C# 快速开发(一)基础知识
开源 C# 快速开发(二)基础控件
开源 C# 快速开发(三)复杂控件
开源 C# 快速开发(四)自定义控件--波形图
开源 C# 快速开发(五)自定义控件--仪表盘
开源 C# 快速开发(六)自定义控件--圆环
开源 C# 快速开发(七)通讯--串口
开源 C# 快速开发(八)通讯--Tcp服务器端
开源 C# 快速开发(九)通讯--Tcp客户端
开源 C# 快速开发(十)通讯--http客户端
开源 C# 快速开发(十一)线程
开源 C# 快速开发(十二)进程监控
开源 C# 快速开发(十三)进程--管道通讯
开源 C# 快速开发(十四)进程--内存映射
开源 C# 快速开发(十五)进程--windows消息
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:一个使用外部FFmpeg进程进行摄像头录制的Qt QML应用程序。
1.代码分析
2.所有源码
3.效果演示
一、代码分析
QML部分函数分析
1. 初始化函数
Component.onCompleted: {console.log("初始化摄像头...")if (QtMultimedia.availableCameras.length > 0) {camera.start()statusMessage = "摄像头就绪 - 点击开始录制"} else {statusMessage = "未检测到摄像头"startButton.enabled = false}
}
功能:应用程序启动时自动执行
检查系统可用摄像头数量
启动第一个可用摄像头
设置初始状态消息
无摄像头时禁用开始按钮
2. 开始录制函数
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 = savePathstartRecordingTimer.start()
}
功能:启动录制流程
生成带时间戳的文件名
构建完整保存路径
停止Qt摄像头预览释放设备
启动延迟定时器
参数处理:
timestamp: 格式化为 2025-10-16T15-10-02
fileName: 固定前缀 + 时间戳 + .mp4后缀
savePath: 硬编码桌面路径
3. 停止摄像头预览函数
function stopCameraPreview() {console.log("停止摄像头预览")cameraActive = falsecamera.stop()
}
功能:释放摄像头资源
设置 cameraActive = false 隐藏视频预览
调用 camera.stop() 释放硬件设备
4. 停止录制函数
function stopRecording() {console.log("停止FFmpeg录制")ffmpegRecorder.stopRecording()
}
功能:委托给C++后端停止录制
简单的代理函数
调用C++的 stopRecording() 方法
5. 状态文本转换函数
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种标准摄像头状态
提供默认"未知"状态处理
6. 时间格式化函数
function formatTime(seconds) {var hours = Math.floor(seconds / 3600)var minutes = Math.floor((seconds % 3600) / 60)var secs = seconds % 60return (hours < 10 ? "0" + hours : hours) + ":" +(minutes < 10 ? "0" + minutes : minutes) + ":" +(secs < 10 ? "0" + secs : secs)
}
功能:将秒数格式化为 HH:MM:SS
数学计算时分秒
补零格式化(01:05:09)
支持超过24小时的显示
7. 定时器更新函数
function updateTimerDisplay() {timerText.text = "录制时间: " + formatTime(recordingSeconds)
}
功能:更新界面计时器显示
组合文本和时间格式
每秒调用一次
C++部分函数分析
1. 构造函数
explicit FFmpegRecorder(QObject *parent = nullptr) : QObject(parent), m_isRecording(false) {}
功能:初始化录制器
设置父对象
初始化录制状态为false
2. 属性读取函数
bool isRecording() const { return m_isRecording; }
QString statusMessage() const { return m_statusMessage; }
功能:QML属性绑定支持
提供只读属性访问
用于QML的数据绑定
3. 开始录制函数(QML可调用)
Q_INVOKABLE void startRecording(const QString &outputPath) {if (m_isRecording) {setStatusMessage("已经在录制中");return;}m_outputPath = outputPath;startFFmpegRecording(outputPath);
}
功能:录制入口点
检查重复录制
保存输出路径
调用内部录制函数
4. 停止录制函数(QML可调用)
Q_INVOKABLE void stopRecording() {if (m_process && m_isRecording) {// 向FFmpeg发送q信号来优雅停止m_process->write("q");m_process->closeWriteChannel();setStatusMessage("正在停止录制...");}
}
功能:优雅停止FFmpeg
向FFmpeg进程发送"q"信号
关闭写入通道
更新状态消息
5. 内部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: 输出文件路径
6. 进程完成处理函数
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状态变化
7. 进程错误处理函数
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();
}
功能:处理进程启动/运行错误
记录错误类型
重置录制状态
清理资源
8. 状态消息设置函数
void setStatusMessage(const QString &message) {if (m_statusMessage != message) {m_statusMessage = message;emit statusMessageChanged();}
}
功能:状态消息管理
避免重复设置相同消息
触发属性变化信号
定时器功能分析
1. 录制计时器
Timer {id: recordTimerinterval: 1000repeat: truerunning: ffmpegRecorder.isRecordingonTriggered: {recordingSeconds++updateTimerDisplay()}
}
功能:录制时长计数
每秒触发一次
仅在录制时运行
累加录制秒数
2. 延迟启动定时器
Timer {id: startRecordingTimerproperty string savedPath: ""interval: 1000onTriggered: {console.log("延迟启动FFmpeg录制:", savedPath)ffmpegRecorder.startRecording(savedPath)recordingSeconds = 0updateTimerDisplay()}
}
功能:确保摄像头完全释放
1秒延迟避免设备冲突
保存路径传递
重置计时器
3. 预览重启定时器
Timer {id: restartPreviewTimerinterval: 500onTriggered: {console.log("重新启动摄像头预览")cameraActive = trueif (camera.cameraStatus !== Camera.ActiveStatus) {camera.start()}}
}
功能:录制完成后恢复预览
500ms延迟确保FFmpeg完全退出
重新激活摄像头
条件启动避免重复调用
信号连接分析
Connections {target: ffmpegRecorderfunction 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.14ApplicationWindow {id: windowwidth: 800height: 600visible: truetitle: qsTr("摄像头录像 - FFmpeg外部进程")property int recordingSeconds: 0property string statusMessage: "正在初始化..."property bool cameraActive: true// 摄像头组件Camera {id: cameradeviceId: 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: videoOutputanchors.fill: parentsource: cameravisible: cameraActive}// 录制时的占位背景Rectangle {anchors.fill: parentcolor: "black"visible: !cameraActiveColumn {anchors.centerIn: parentspacing: 20Text {text: "● 正在录制中"color: "#e74c3c"font.pixelSize: 32font.bold: true}Text {text: timerText.textcolor: "white"font.pixelSize: 24font.family: "Monospace"}Text {text: "录制完成后将自动恢复预览"color: "#aaaaaa"font.pixelSize: 16}}}// 控制面板Rectangle {width: parent.widthheight: 160anchors.bottom: parent.bottomcolor: "#CC000000"ColumnLayout {anchors.fill: parentanchors.margins: 10spacing: 10// 状态显示Text {Layout.fillWidth: truetext: ffmpegRecorder.statusMessage || statusMessagecolor: ffmpegRecorder.isRecording ? "#e74c3c" : "white"font.pixelSize: 14wrapMode: Text.Wrap}// 录制时间Text {id: timerTextLayout.fillWidth: truetext: "录制时间: " + formatTime(recordingSeconds)color: "#e74c3c"font.bold: truefont.pixelSize: 18font.family: "Monospace"visible: ffmpegRecorder.isRecording}// 按钮行RowLayout {Layout.fillWidth: truespacing: 15Button {id: startButtontext: "● 开始录制"enabled: !ffmpegRecorder.isRecording && cameraActiveonClicked: startRecording()background: Rectangle {color: startButton.enabled ? "#27ae60" : "#666666"radius: 5}contentItem: Text {text: startButton.textcolor: "white"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenterfont.bold: true}Layout.preferredWidth: 120Layout.preferredHeight: 40}Button {id: stopButtontext: "■ 停止录制"enabled: ffmpegRecorder.isRecordingonClicked: stopRecording()background: Rectangle {color: stopButton.enabled ? "#e74c3c" : "#666666"radius: 5}contentItem: Text {text: stopButton.textcolor: "white"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenterfont.bold: true}Layout.preferredWidth: 120Layout.preferredHeight: 40}// 录制指示器Rectangle {width: 20height: 20radius: 10color: ffmpegRecorder.isRecording ? "#e74c3c" : "transparent"border.color: "white"border.width: 2SequentialAnimation on opacity {running: ffmpegRecorder.isRecordingloops: Animation.InfiniteNumberAnimation { from: 1.0; to: 0.3; duration: 500 }NumberAnimation { from: 0.3; to: 1.0; duration: 500 }}}Item { Layout.fillWidth: true }}// 调试信息Text {Layout.fillWidth: truetext: "预览状态: " + (cameraActive ? "开启" : "关闭") +" | 摄像头: " + getCameraStatusText(camera.cameraStatus) +" | 录制器: " + (ffmpegRecorder.isRecording ? "录制中" : "空闲")color: "#666666"font.pixelSize: 10}}}// 录制计时器Timer {id: recordTimerinterval: 1000repeat: truerunning: ffmpegRecorder.isRecordingonTriggered: {recordingSeconds++updateTimerDisplay()}}// 启动录制定时器(延迟启动)Timer {id: startRecordingTimerproperty string savedPath: ""interval: 1000onTriggered: {console.log("延迟启动FFmpeg录制:", savedPath)ffmpegRecorder.startRecording(savedPath)recordingSeconds = 0updateTimerDisplay()}}// 重新启动预览定时器Timer {id: restartPreviewTimerinterval: 500onTriggered: {console.log("重新启动摄像头预览")cameraActive = trueif (camera.cameraStatus !== Camera.ActiveStatus) {camera.start()}}}// 连接FFmpegRecorder信号Connections {target: ffmpegRecorderfunction 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/" + fileNameconsole.log("准备开始录制:", savePath)// 先停止摄像头预览stopCameraPreview()// 延迟启动FFmpeg录制startRecordingTimer.savedPath = savePathstartRecordingTimer.start()}function stopRecording() {console.log("停止FFmpeg录制")ffmpegRecorder.stopRecording()}function stopCameraPreview() {console.log("停止摄像头预览")cameraActive = falsecamera.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 % 60return (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_OBJECTQ_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"
三、效果演示
、