开源 C++ QT QML 开发(二十一)多媒体--视频播放
文章的目的为了记录使用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# 快速开发(一)基础知识
开源 C# 快速开发(二)基础控件
开源 C# 快速开发(三)复杂控件
开源 C# 快速开发(四)自定义控件--波形图
开源 C# 快速开发(五)自定义控件--仪表盘
开源 C# 快速开发(六)自定义控件--圆环
开源 C# 快速开发(七)通讯--串口
开源 C# 快速开发(八)通讯--Tcp服务器端
开源 C# 快速开发(九)通讯--Tcp客户端
开源 C# 快速开发(十)通讯--http客户端
开源 C# 快速开发(十一)线程
开源 C# 快速开发(十二)进程监控
开源 C# 快速开发(十三)进程--管道通讯
开源 C# 快速开发(十四)进程--内存映射
开源 C# 快速开发(十五)进程--windows消息
开源 C# 快速开发(十六)数据库--sqlserver增删改查
本章节主要内容是:使用qml编写了媒体播放器,使用了K-Lite Codec Pack Mega作为解码包,实现mp4的播放。
1.代码分析
2.所有源码
3.效果演示
一、代码分析1. 主窗口和属性定义
ApplicationWindow {id: mainWindowwidth: 1024height: 720visible: truetitle: "MP4播放器 - 完全兼容版"minimumWidth: 800minimumHeight: 600property string currentVideo: "" // 当前视频文件路径property bool hasVideo: videoPlayer.hasVideo // 绑定到Video组件的hasVideo属性property bool isSeekable: videoPlayer.seekable // 是否支持跳转property string currentStatus: "等待选择文件" // 当前状态显示文本
}
2. 核心视频播放组件
Video组件及其信号处理
Video {id: videoPlayeranchors.fill: parentanchors.margins: 2source: currentVideo // 绑定到currentVideo属性fillMode: VideoOutput.PreserveAspectFit // 保持宽高比volume: volumeSlider.value // 绑定音量滑块autoPlay: true // 自动播放// 状态变化信号处理onStatusChanged: {console.log("视频状态变化:", status)updateStatusDisplay() // 更新状态显示}// 播放状态变化信号处理onPlaybackStateChanged: {console.log("播放状态变化:", playbackState)updatePlayButton() // 更新播放按钮文本}// 视频时长变化信号处理onDurationChanged: {console.log("视频时长:", duration, "ms")progressSlider.to = Math.max(1, duration) // 设置进度条最大值}// 播放位置变化信号处理onPositionChanged: {if (!progressSlider.pressed) { // 避免拖动时冲突progressSlider.value = position // 更新进度条位置}}
}
3. 主要控制函数
播放/暂停切换函数
function togglePlayPause() {if (!hasVideo || currentVideo === "") return // 安全检查if (videoPlayer.playbackState === MediaPlayer.PlayingState) {videoPlayer.pause() // 如果正在播放,则暂停} else {videoPlayer.play() // 否则开始播放}
}
状态显示更新函数
function updateStatusDisplay() {var status = videoPlayer.status // 获取当前视频状态switch(status) {case MediaPlayer.NoMedia:currentStatus = "无媒体文件"breakcase MediaPlayer.Loading:currentStatus = "加载中..."breakcase MediaPlayer.Loaded:currentStatus = "已加载"breakcase MediaPlayer.Stalling:currentStatus = "缓冲中..."breakcase MediaPlayer.Buffering:currentStatus = "缓冲中"breakcase MediaPlayer.Buffered:currentStatus = "就绪"breakcase MediaPlayer.EndOfMedia:currentStatus = "播放结束"breakcase MediaPlayer.InvalidMedia:currentStatus = "格式不支持"breakcase MediaPlayer.UnknownStatus:currentStatus = "未知状态"breakdefault:currentStatus = "就绪"}console.log("状态更新:", currentStatus)
}
播放按钮更新函数
function updatePlayButton() {switch(videoPlayer.playbackState) {case MediaPlayer.PlayingState:playButton.text = "⏸️ 暂停" // 播放中显示暂停breakcase MediaPlayer.PausedState:playButton.text = "▶️ 继续" // 暂停中显示继续breakdefault:playButton.text = "▶️ 播放" // 默认显示播放}
}
4. UI状态显示函数
状态图标获取函数
function getStatusIcon() {if (currentVideo === "") return "📁" // 无文件if (videoPlayer.status === MediaPlayer.InvalidMedia) return "❌" // 格式错误if (videoPlayer.status === MediaPlayer.Loading) return "⏳" // 加载中if (!hasVideo) return "🎬" // 无视频流return "✅" // 正常状态
}
状态消息获取函数
function getStatusMessage() {if (currentVideo === "") return "请选择视频文件开始播放" // 初始提示if (videoPlayer.status === MediaPlayer.InvalidMedia) return "无法播放此文件" // 错误提示if (videoPlayer.status === MediaPlayer.Loading) return "正在加载..." // 加载提示if (!hasVideo) return "准备播放" // 准备状态return "正在播放: " + getFileName(currentVideo) // 播放状态显示文件名
}
错误详情获取函数
function getErrorDetails() {if (videoPlayer.status === MediaPlayer.InvalidMedia) {return "此文件需要H.264解码器\n请安装K-Lite Codec Pack\n下载: https://codecguide.com/download_kl.htm"}return "" // 无错误时返回空字符串
}
5. 工具函数
状态颜色获取函数
function getStatusColor() {if (videoPlayer.status === MediaPlayer.InvalidMedia) return "#e74c3c" // 红色-错误if (videoPlayer.status === MediaPlayer.Buffered) return "#2ecc71" // 绿色-就绪if (videoPlayer.status === MediaPlayer.Loading) return "#f39c12" // 橙色-加载中return "#3498db" // 蓝色-默认
}
文件名提取函数
function getFileName(filePath) {var path = filePath.toString() // 转换为字符串var fileName = path.split('/').pop() || path.split('\\').pop() // 从路径中提取文件名return fileName || "未知文件" // 返回文件名或默认值
}
时间格式化函数
function formatTime(milliseconds) {if (!milliseconds || isNaN(milliseconds)) return "00:00" // 无效时间处理var totalSeconds = Math.floor(milliseconds / 1000) // 转换为秒var hours = Math.floor(totalSeconds / 3600) // 计算小时var minutes = Math.floor((totalSeconds % 3600) / 60) // 计算分钟var seconds = totalSeconds % 60 // 计算秒数if (hours > 0) {// 有时长格式:HH:MM:SSreturn hours.toString().padStart(2, '0') + ":" +minutes.toString().padStart(2, '0') + ":" +seconds.toString().padStart(2, '0')} else {// 无时长格式:MM:SSreturn minutes.toString().padStart(2, '0') + ":" +seconds.toString().padStart(2, '0')}
}
6. 进度条控制函数
进度条拖动处理
Slider {id: progressSlider// ... 其他属性onMoved: {if (isSeekable) { // 检查是否支持跳转videoPlayer.seek(value) // 跳转到指定位置}}
}
7. C++主程序函数
环境设置函数
int main(int argc, char *argv[])
{// 关键环境变量设置qputenv("QT_MEDIA_BACKEND", "windows"); // 强制使用Windows媒体后端qputenv("QT_LOGGING_RULES", "qt.multimedia.*=true"); // 启用多媒体调试日志qputenv("MF_DEBUG", "1"); // 启用Media Foundation调试// 应用程序属性设置QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 高DPI支持QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL); // 软件渲染QGuiApplication app(argc, argv);// 应用程序信息app.setApplicationName("MP4播放器");app.setApplicationVersion("1.0");app.setOrganizationName("MyCompany");
}
系统诊断函数
// 详细的多媒体支持信息
qDebug() << "=== 多媒体系统诊断信息 ===";
qDebug() << "应用程序目录:" << QDir::currentPath();
qDebug() << "临时目录:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation);
qDebug() << "支持的MIME类型数量:" << QMediaPlayer::supportedMimeTypes().count();// 输出支持的格式
auto mimeTypes = QMediaPlayer::supportedMimeTypes();
for (int i = 0; i < qMin(10, mimeTypes.count()); ++i) {qDebug() << "支持格式:" << mimeTypes[i];
}
8. 文件对话框处理
FileDialog {id: fileDialogtitle: "选择视频文件"nameFilters: [ /* 文件过滤器列表 */ ]onAccepted: {currentVideo = fileDialog.fileUrl // 获取选择的文件URLconsole.log("加载文件:", currentVideo) // 调试日志}
}
二、所有源码
1.安装正确的解码器包
推荐方案:K-Lite Codec Pack Mega
下载地址:https://codecguide.com/download_kl.htm
选择版本:下载 Mega 版本(包含所有解码器)
安装步骤:运行安装程序
2.pro文件
QT += quick quickcontrols2 multimedia multimediawidgets
3.main.qml文件源码
import QtQuick 2.14
import QtQuick.Window 2.14
import QtMultimedia 5.14
import QtQuick.Controls 2.14
import QtQuick.Dialogs 1.3
import QtQuick.Layouts 1.14ApplicationWindow {id: mainWindowwidth: 1024height: 720visible: truetitle: "MP4播放器 - 完全兼容版"minimumWidth: 800minimumHeight: 600property string currentVideo: ""property bool hasVideo: videoPlayer.hasVideoproperty bool isSeekable: videoPlayer.seekableproperty string currentStatus: "等待选择文件"// 背景渐变Rectangle {anchors.fill: parentgradient: Gradient {GradientStop { position: 0.0; color: "#2c3e50" }GradientStop { position: 1.0; color: "#3498db" }}}ColumnLayout {anchors.fill: parentanchors.margins: 10spacing: 10// 标题栏Rectangle {Layout.fillWidth: trueheight: 60color: "transparent"RowLayout {anchors.fill: parentText {text: "🎬 MP4播放器"color: "white"font.pixelSize: 24font.bold: true}Item { Layout.fillWidth: true }Text {text: "Powered by Qt5.14"color: "lightgray"font.pixelSize: 12}}}// 视频显示区域Rectangle {id: videoContainerLayout.fillWidth: trueLayout.fillHeight: truecolor: "black"radius: 8clip: trueVideo {id: videoPlayeranchors.fill: parentanchors.margins: 2source: currentVideofillMode: VideoOutput.PreserveAspectFitvolume: volumeSlider.valueautoPlay: true// 状态变化处理 - 使用正确的信号onStatusChanged: {console.log("视频状态变化:", status)updateStatusDisplay()}onPlaybackStateChanged: {console.log("播放状态变化:", playbackState)updatePlayButton()}onDurationChanged: {console.log("视频时长:", duration, "ms")progressSlider.to = Math.max(1, duration)}onPositionChanged: {if (!progressSlider.pressed) {progressSlider.value = position}}}// 加载指示器BusyIndicator {id: loadingIndicatoranchors.centerIn: parentrunning: videoPlayer.status === MediaPlayer.Loading ||videoPlayer.status === MediaPlayer.Stalling ||videoPlayer.status === MediaPlayer.Bufferingvisible: running}// 居中状态信息Column {id: centerInfoanchors.centerIn: parentspacing: 10visible: currentVideo === "" || videoPlayer.status === MediaPlayer.InvalidMedia || !hasVideoText {id: statusIconanchors.horizontalCenter: parent.horizontalCentercolor: "white"font.pixelSize: 48text: getStatusIcon()}Text {id: statusMessageanchors.horizontalCenter: parent.horizontalCentercolor: "white"font.pixelSize: 18text: getStatusMessage()horizontalAlignment: Text.AlignHCenter}Text {id: errorDetailsanchors.horizontalCenter: parent.horizontalCentercolor: "yellow"font.pixelSize: 12text: getErrorDetails()visible: text !== ""horizontalAlignment: Text.AlignHCenterwidth: 400wrapMode: Text.WordWrap}}// 点击控制播放/暂停MouseArea {anchors.fill: parentonClicked: togglePlayPause()enabled: hasVideo && currentVideo !== ""}}// 进度控制区域ColumnLayout {Layout.fillWidth: truespacing: 5// 进度条Slider {id: progressSliderLayout.fillWidth: truefrom: 0to: 100value: 0enabled: isSeekable && hasVideoonMoved: {if (isSeekable) {videoPlayer.seek(value)}}// 自定义样式background: Rectangle {implicitHeight: 6color: "#5a5a5a"radius: 3}handle: Rectangle {x: progressSlider.leftPadding + progressSlider.visualPosition * (progressSlider.availableWidth - width)y: progressSlider.topPadding + progressSlider.availableHeight / 2 - height / 2implicitWidth: 16implicitHeight: 16radius: 8color: progressSlider.pressed ? "#f0f0f0" : "#ffffff"border.color: "#bdbebf"}}// 时间信息RowLayout {Layout.fillWidth: trueText {color: "white"text: formatTime(videoPlayer.position)font.pixelSize: 12}Item { Layout.fillWidth: true }Text {color: "white"text: formatTime(videoPlayer.duration)font.pixelSize: 12}}}// 控制按钮区域Rectangle {Layout.fillWidth: trueheight: 70color: "transparent"RowLayout {anchors.fill: parentspacing: 10// 文件操作按钮Button {text: "📁 打开文件"onClicked: fileDialog.open()background: Rectangle {color: "#27ae60"radius: 5}contentItem: Text {text: parent.textcolor: "white"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenterfont.bold: true}}// 播放控制按钮Button {id: playButtontext: "▶️ 播放"onClicked: togglePlayPause()enabled: hasVideo && currentVideo !== ""background: Rectangle {color: enabled ? "#2980b9" : "#7f8c8d"radius: 5}contentItem: Text {text: parent.textcolor: "white"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenterfont.bold: true}}Button {text: "⏹️ 停止"onClicked: {videoPlayer.stop()progressSlider.value = 0}enabled: hasVideo && currentVideo !== ""background: Rectangle {color: enabled ? "#e74c3c" : "#7f8c8d"radius: 5}contentItem: Text {text: parent.textcolor: "white"horizontalAlignment: Text.AlignHCenterverticalAlignment: Text.AlignVCenter}}Item { Layout.fillWidth: true }// 音量控制RowLayout {spacing: 5Text {text: "🔊"color: "white"font.pixelSize: 16}Slider {id: volumeSliderfrom: 0to: 1value: 0.8stepSize: 0.1Layout.preferredWidth: 100background: Rectangle {implicitHeight: 4color: "#5a5a5a"radius: 2}}Text {color: "white"text: Math.round(volumeSlider.value * 100) + "%"font.pixelSize: 12Layout.preferredWidth: 40}}}}// 状态信息栏Rectangle {Layout.fillWidth: trueheight: 30color: "#34495e"radius: 4RowLayout {anchors.fill: parentanchors.margins: 5Text {color: "white"text: "文件: " + (currentVideo ? getFileName(currentVideo) : "未选择")font.pixelSize: 11elide: Text.ElideMiddleLayout.fillWidth: true}Text {color: getStatusColor()text: currentStatusfont.pixelSize: 11font.bold: true}Text {color: hasVideo ? "lightgreen" : "red"text: hasVideo ? "🎥 有视频" : "❌ 无视频"font.pixelSize: 11}}}}FileDialog {id: fileDialogtitle: "选择视频文件"nameFilters: ["MP4文件 (*.mp4)","AVI文件 (*.avi)","MKV文件 (*.mkv)","MOV文件 (*.mov)","WMV文件 (*.wmv)","所有文件 (*.*)"]onAccepted: {currentVideo = fileDialog.fileUrlconsole.log("加载文件:", currentVideo)}}// 功能函数function togglePlayPause() {if (!hasVideo || currentVideo === "") returnif (videoPlayer.playbackState === MediaPlayer.PlayingState) {videoPlayer.pause()} else {videoPlayer.play()}}function updateStatusDisplay() {var status = videoPlayer.statusswitch(status) {case MediaPlayer.NoMedia:currentStatus = "无媒体文件"breakcase MediaPlayer.Loading:currentStatus = "加载中..."breakcase MediaPlayer.Loaded:currentStatus = "已加载"breakcase MediaPlayer.Stalling:currentStatus = "缓冲中..."breakcase MediaPlayer.Buffering:currentStatus = "缓冲中"breakcase MediaPlayer.Buffered:currentStatus = "就绪"breakcase MediaPlayer.EndOfMedia:currentStatus = "播放结束"breakcase MediaPlayer.InvalidMedia:currentStatus = "格式不支持"breakcase MediaPlayer.UnknownStatus:currentStatus = "未知状态"breakdefault:currentStatus = "就绪"}console.log("状态更新:", currentStatus)}function updatePlayButton() {switch(videoPlayer.playbackState) {case MediaPlayer.PlayingState:playButton.text = "⏸️ 暂停"breakcase MediaPlayer.PausedState:playButton.text = "▶️ 继续"breakdefault:playButton.text = "▶️ 播放"}}function getStatusIcon() {if (currentVideo === "") return "📁"if (videoPlayer.status === MediaPlayer.InvalidMedia) return "❌"if (videoPlayer.status === MediaPlayer.Loading) return "⏳"if (!hasVideo) return "🎬"return "✅"}function getStatusMessage() {if (currentVideo === "") return "请选择视频文件开始播放"if (videoPlayer.status === MediaPlayer.InvalidMedia) return "无法播放此文件"if (videoPlayer.status === MediaPlayer.Loading) return "正在加载..."if (!hasVideo) return "准备播放"return "正在播放: " + getFileName(currentVideo)}function getErrorDetails() {if (videoPlayer.status === MediaPlayer.InvalidMedia) {return "此文件需要H.264解码器\n请安装K-Lite Codec Pack\n下载: https://codecguide.com/download_kl.htm"}return ""}function getStatusColor() {if (videoPlayer.status === MediaPlayer.InvalidMedia) return "#e74c3c"if (videoPlayer.status === MediaPlayer.Buffered) return "#2ecc71"if (videoPlayer.status === MediaPlayer.Loading) return "#f39c12"return "#3498db"}function getFileName(filePath) {var path = filePath.toString()var fileName = path.split('/').pop() || path.split('\\').pop()return fileName || "未知文件"}function formatTime(milliseconds) {if (!milliseconds || isNaN(milliseconds)) return "00:00"var totalSeconds = Math.floor(milliseconds / 1000)var hours = Math.floor(totalSeconds / 3600)var minutes = Math.floor((totalSeconds % 3600) / 60)var seconds = totalSeconds % 60if (hours > 0) {return hours.toString().padStart(2, '0') + ":" +minutes.toString().padStart(2, '0') + ":" +seconds.toString().padStart(2, '0')} else {return minutes.toString().padStart(2, '0') + ":" +seconds.toString().padStart(2, '0')}}Component.onCompleted: {console.log("播放器初始化完成")console.log("视频输出支持:", videoPlayer.availability)console.log("是否有视频:", videoPlayer.hasVideo)console.log("是否可跳转:", videoPlayer.seekable)}
}
4. main.cpp文件源码
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QMediaPlayer>
#include <QDebug>
#include <QDir>
#include <QStandardPaths>int main(int argc, char *argv[])
{// 设置环境变量 - 在创建QGuiApplication之前qputenv("QT_MEDIA_BACKEND", "windows");qputenv("QT_LOGGING_RULES", "qt.multimedia.*=true");qputenv("MF_DEBUG", "1");QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);QGuiApplication app(argc, argv);// 设置应用程序信息app.setApplicationName("MP4播放器");app.setApplicationVersion("1.0");app.setOrganizationName("MyCompany");// 详细的多媒体支持信息qDebug() << "=== 多媒体系统诊断信息 ===";qDebug() << "应用程序目录:" << QDir::currentPath();qDebug() << "临时目录:" << QStandardPaths::writableLocation(QStandardPaths::TempLocation);qDebug() << "支持的MIME类型数量:" << QMediaPlayer::supportedMimeTypes().count();// 输出前10个支持的格式auto mimeTypes = QMediaPlayer::supportedMimeTypes();for (int i = 0; i < qMin(10, mimeTypes.count()); ++i) {qDebug() << "支持格式:" << mimeTypes[i];}qDebug() << "环境变量:";qDebug() << "QT_MEDIA_BACKEND =" << qgetenv("QT_MEDIA_BACKEND");qDebug() << "PATH =" << qgetenv("PATH");QQmlApplicationEngine engine;// 连接加载失败信号QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,&app, [](QObject *obj, const QUrl &objUrl) {if (!obj) {qCritical() << "QML加载失败:" << objUrl;QCoreApplication::exit(-1);} else {qDebug() << "QML加载成功";}}, Qt::QueuedConnection);engine.load(QUrl(QStringLiteral("qrc:/main.qml")));return app.exec();
}
三、效果演示
选择MP4文件,进行播放,可以拖动和暂停。需要注意的是必须要安装解码器才行。