鸿蒙PC迁移:MoonPlayer Qt 视频播放器鸿蒙PC适配全记录
一、写在前面
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/
原项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_MoonPlayer
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
这篇文章记录的是 MoonPlayer 在 HarmonyOS PC / OpenHarmony PC 环境中的一次适配过程。
MoonPlayer 不是 Electron 项目,也不是一个普通 ArkUI 应用。它原本是一个 C++ / Qt Quick / QML 视频播放器,桌面版本以 Qt Quick 界面为主,播放核心依赖 mpv,并且还包含播放列表、网络解析、弹幕转换、下载器、插件、字幕等桌面播放器常见能力。
所以这次适配的重点不是把一个 Web 页面塞进 HAP,而是解决下面这组问题:
- 怎样让一个 Qt Quick/QML 桌面应用进入鸿蒙 Stage 模型。
- 怎样让
libentry.so作为 HAP native library 被启动。 - 怎样把 Qt for Harmony 的 QPA 插件、Qt 动态库、QML 模块、图片资源一起打包进 HAP。
- 怎样处理
libQt5QuickTemplates2.so、QtMultimedia、QML plugin 这些运行时依赖缺失导致的白屏。 - 怎样在暂时没有 HarmonyOS arm64
libmpv.so的情况下,先让播放器具备基础视频播放能力。 - 怎样处理鸿蒙文件选择器返回的
file:///content://URI,让视频能被 Qt 正常读取。 - 怎样根据鸿蒙 PC 的窗口比例调整播放器控制栏,修复按钮过小、无效按钮、进度条不动等真实设备问题。
本次适配采用的是一条逐步验证的路线:保留 MoonPlayer 原有 C++ / QML 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和播放器逻辑仍然由 Qt 运行时承载。

二、项目背景:MoonPlayer 是 Qt Quick/QML + mpv 的桌面播放器
MoonPlayer 原始项目结构大致如下:
moonplayer-develop/
├── CMakeLists.txt
├── 3rdparty/
│ └── danmaku2ass_cpp/
├── res/
├── scripts/
├── src/
│ ├── main.cpp
│ ├── application.cpp
│ ├── mpvObject.cpp
│ ├── playlistModel.cpp
│ ├── utils.cpp
│ ├── qml/
│ │ ├── main.qml
│ │ ├── main_ohos.qml
│ │ ├── ControlBar.qml
│ │ ├── Playlist.qml
│ │ ├── Sidebar.qml
│ │ └── Settings.qml
│ └── platform/
└── harmony_pc/
鸿蒙适配工程集中放在:
moonplayer-develop/harmony_pc/
├── AppScope/
│ └── app.json5
├── build-profile.json5
├── entry/
│ ├── build-profile.json5
│ ├── libs/arm64-v8a/
│ │ └── libplugins_platforms_qopenharmony.so
│ └── src/main/
│ ├── cpp/CMakeLists.txt
│ ├── ets/
│ │ ├── abilitystage/MyAbilityStage.ets
│ │ ├── entryability/EntryAbility.ets
│ │ └── pages/Index.ets
│ ├── module.json5
│ └── resources/
├── hvigor/
├── oh-package.json5
└── qtforharmony_sdk/
这次适配没有重写成 ArkUI 播放器,而是让 Qt Quick 继续负责界面,鸿蒙工程壳负责承载和启动。这样做的好处是:
- 原桌面代码可以继续维护,鸿蒙分支不会变成另一套完全不同的播放器。
- UI、播放列表、设置、弹幕、工具类等 C++ / QML 代码可以复用。
- 鸿蒙特有逻辑通过
Q_OS_OPENHARMONY、main_ohos.qml、mpvObject_stub.cpp、platform/paths_ohos.cpp等文件收敛起来。 - 后续如果拿到 HarmonyOS arm64 版本的
libmpv.so,可以再切回 mpv 播放后端。

三、鸿蒙工程壳:Ability + XComponent + Qt QPA
Qt for Harmony 的关键思路是:ArkTS 侧创建鸿蒙窗口,页面里放一个 XComponent,然后通过 Qt OpenHarmony QPA 插件把 Qt 窗口挂上去。
MoonPlayer 的 Index.ets 很薄,核心就是这个 XComponent:
XComponent({
id: this.windowId,
type: XComponentType.NODE,
libraryname: 'plugins_platforms_qopenharmony'
})
.width('100%')
.height('100%');
这里的 libraryname 指向:
harmony_pc/entry/libs/arm64-v8a/libplugins_platforms_qopenharmony.so
Ability 创建窗口后,会先加载 pages/Index,再把窗口交给 QPA 插件:
await windowStage.loadContent(this.loadContentUrl, localStore);
qpa.handleJsTopWindowCreated(this.name, this);
qpa.startQtApplication(this);
这套链路可以理解成:
MyAbilityStage.ets负责实例标识和qpa.attachAbilityStage(this)。EntryAbility.ets负责窗口生命周期、打开文件参数、启动 Qt 应用。Index.ets提供XComponent绘制承载点。libplugins_platforms_qopenharmony.so接管 Qt 窗口和输入输出。libentry.so是真正的 MoonPlayer Qt 应用入口。
AppScope/app.json5 中应用身份使用 MoonPlayer 自己的包名:
{
"app": {
"bundleName": "io.github.coslyk.moonplayer",
"vendor": "coslyk",
"versionCode": 4300000,
"versionName": "4.3",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
这里需要特别注意:如果参考其他 Qt 项目的鸿蒙模板,不能把模板项目的 logo、签名、包名、label 直接复制过来。MoonPlayer 这边只借鉴工程结构和 Qt 启动方式,应用身份、图标资源、签名配置都应该属于当前项目。
四、CMake 打包策略:把 MoonPlayer 编译成 libentry.so
鸿蒙 native 入口在:
harmony_pc/entry/src/main/cpp/CMakeLists.txt
它会从 harmony_pc 反向定位到 MoonPlayer 根目录:
get_filename_component(HARMONY_PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE)
get_filename_component(MOONPLAYER_ROOT "${HARMONY_PROJECT_ROOT}/.." ABSOLUTE)
然后通过 QT_PREFIX 找到项目内置的 Qt for Harmony SDK:
set(QT_PREFIX "qtforharmony_sdk" CACHE PATH "Qt for HarmonyOS SDK path")
if (NOT IS_ABSOLUTE "${QT_PREFIX}")
get_filename_component(QT_PREFIX "${HARMONY_PROJECT_ROOT}/${QT_PREFIX}" ABSOLUTE)
endif()
if (NOT EXISTS "${QT_PREFIX}/lib/cmake/Qt5/Qt5Config.cmake")
message(FATAL_ERROR "QT_PREFIX must point to the bundled Qt 5 for HarmonyOS SDK: ${QT_PREFIX}")
endif()
entry/build-profile.json5 里把参数传给 CMake:
"arguments": "-DQT_PREFIX=qtforharmony_sdk -DMPV_ROOT=mpv-ohos -DMOONPLAYER_ENABLE_MPV=AUTO -DMOONPLAYER_BUILD_HLSDL=OFF"
本次链接的 Qt 模块包括:
find_package(Qt5 REQUIRED COMPONENTS
Core
Gui
Qml
QmlModels
QmlWorkerScript
Quick
QuickControls2
QuickTemplates2
OpenGL
Network
OhExtras
Svg
Widgets
Multimedia
MultimediaQuick
)
QuickTemplates2 和 MultimediaQuick 很关键。前者关系到 Qt Quick Controls 2 的运行时依赖,后者关系到 QML Video 播放组件。
最终 native 侧生成:
libentry.so
并和 Qt 运行时一起打包到 HAP 的 native libs 目录。nativeLib.collectAllLibs 开启后,构建链路会尽量收集依赖库:
"nativeLib": {
"collectAllLibs": true,
"filter": {
"pickLasts": [
"**/libplugins_platforms_qopenharmony.so"
]
}
}
Qt/QML 模块也不能漏。MoonPlayer 的 CMake 会扫描 Qt SDK 的 QML import 目录,把 QtQuick、QtQuick/Controls.2、Qt/labs/platform、QtMultimedia 等模块写入 qrc:
set(MOONPLAYER_QT_QML_MODULE_ROOTS
QtQml
QtQuick.2
QtQuick/Controls.2
QtQuick/Templates.2
QtQuick/Layouts
QtQuick/Window.2
QtQuick/Dialogs
Qt/labs/platform
Qt/labs/settings
QtMultimedia
QtGraphicalEffects
)
这一点对 Qt Quick 项目非常重要:桌面环境中 QML import 目录通常在 Qt 安装路径里;但 HAP 运行时只看应用包内资源和 native libs,如果 QML 模块没有打进去,就会出现 QML 类型找不到、界面白屏、控件不显示等问题。

五、第一次白屏:先看 hilog,不要先改 QML
第一次把 HAP 安装到鸿蒙 PC 后,应用窗口能起来,但内容是白屏。这个时候最容易走偏:以为是 QML 首页写错、窗口没显示、资源没加载。实际上,日志已经把问题说得很清楚:
Failed to load QT application '/data/storage/el1/bundle/libs/arm64/libentry.so':
Error loading shared library libQt5QuickTemplates2.so:
No such file or directory
(needed by /data/storage/el1/bundle/libs/arm64/libQt5QuickControls2.so)
这说明问题发生在 native library 加载阶段,libentry.so 还没真正跑起来。也就是说,白屏不是业务逻辑问题,而是 HAP 里少了 Qt 动态库。
修复思路有两个点:
- CMake 链接
Qt5::QuickTemplates2。 - 打包时确保
libQt5QuickTemplates2.so进入 HAP。
修复后,再启动应用,日志不再停在 Failed to load QT application,而是进入 QML import、窗口创建和 Qt Quick 初始化阶段:
MoonPlayer Harmony QML import paths: (...)
MoonPlayer Harmony QML plugin paths: (...)
loadContent succeeded: pages/Index
这里有一个适配经验:Qt Quick Controls 2 项目不能只带 libQt5QuickControls2.so。QuickControls2 运行时还会依赖 QuickTemplates2,少后者就会白屏。
六、Qt Quick 渲染后端:视频画面黑屏的另一类问题
依赖库补齐后,MoonPlayer 能显示主界面。但视频播放器还有一个新的问题:界面能出来,控制栏能显示,视频区域却可能黑屏。
这个问题和上一节不同。上一节是 libentry.so 根本加载失败;这一节是 Qt Quick 已经运行,但视频帧没有正确合成到窗口中。
MoonPlayer 桌面版原来使用 Qt 6 / RHI 思路更多,而鸿蒙侧使用的是 Qt 5.15.12 for Harmony。实际设备测试后,鸿蒙侧需要把 scene graph 后端切到传统 OpenGL:
#if defined(Q_OS_OPENHARMONY)
QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGL);
#else
QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGLRhi);
#endif
这个改动放在 prepareMoonPlayerApplication() 里,只影响 Q_OS_OPENHARMONY:
void prepareMoonPlayerApplication()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#else
#if defined(Q_OS_OPENHARMONY)
QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGL);
#else
QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGLRhi);
#endif
registerMoonPlayerQmlTypes();
#endif
}
修复后,真机可以看到视频帧正常渲染。这个阶段的判断标准是:
- MoonPlayer 窗口能正常打开。
- 播放列表能显示导入的视频名称。
- 点击播放后,视频区域不是纯黑或纯白。
- 当前时间开始跳动。
七、没有 libmpv 时,先做 QtMultimedia fallback
MoonPlayer 桌面版的播放核心是 mpv。但在这次适配中,项目里还没有完整提供 HarmonyOS arm64 版本的:
harmony_pc/mpv-ohos/
├── include/mpv/client.h
├── include/mpv/render.h
├── include/mpv/render_gl.h
└── lib/libmpv.so
因此 CMake 做了一个开关:
set(MOONPLAYER_ENABLE_MPV "AUTO" CACHE STRING "Enable libmpv playback on HarmonyOS: AUTO, ON, or OFF")
规则是:
AUTO:如果mpv-ohos存在,就启用 mpv;如果不存在,就构建可启动 UI。ON:强制要求mpv-ohos,缺少就构建失败。OFF:始终使用无 mpv 构建。
没有 mpv 时,native 层使用 mpvObject_stub.cpp,避免整个项目因为缺少 libmpv 而无法启动:
void MpvObject::open(const QUrl& fileUrl, const QUrl& danmakuUrl, const QUrl& audioTrackUrl)
{
Q_UNUSED(danmakuUrl)
Q_UNUSED(audioTrackUrl)
qWarning() << "MoonPlayer was built without libmpv; playback is disabled:" << fileUrl;
m_state = STOPPED;
m_time = 0;
m_duration = 0;
emit stateChanged();
emit timeChanged();
emit durationChanged();
}
但是视频播放器如果只能打开空 UI,适配价值会很低。所以鸿蒙侧新增了一个 main_ohos.qml,在没有 mpv 的情况下使用 QtMultimedia 的 Video 组件做基础播放:
Video {
id: fallbackVideo
anchors.fill: parent
visible: !usingMpvBackend && fallbackHasMedia
fillMode: VideoOutput.PreserveAspectFit
volume: volumeSlider.value / 100
muted: false
autoPlay: false
notifyInterval: 250
}
这不是完整替代 mpv。它的定位是:
- 让鸿蒙 PC 版本先具备本地视频播放 MVP。
- 支持通过文件选择器打开常见视频格式。
- 让播放、暂停、停止、音量、进度条这些基本交互可验证。
- 为后续接入 HarmonyOS arm64
libmpv留出空间。
鸿蒙页面会根据 MoonPlayerHasMpv 判断当前使用哪个后端:
property bool usingMpvBackend: typeof MoonPlayerHasMpv !== "undefined" && MoonPlayerHasMpv
然后控制栏统一绑定:
isPlaying: usingMpvBackend ? mpv.state === MpvObject.VIDEO_PLAYING : fallbackIsPlaying
time: usingMpvBackend ? mpv.time : fallbackTimeSeconds
duration: usingMpvBackend ? mpv.duration : fallbackDurationSeconds
这样后续一旦补齐 libmpv.so,主界面仍然可以切回原来的 mpv 对象。

八、鸿蒙文件 URI:导入视频前先缓存到应用可读路径
播放器最核心的动作是“打开本地视频”。但鸿蒙 PC 上,文件选择器或系统打开方式传给应用的不一定是普通桌面路径,可能是:
file://...
content://...
如果直接把这个 URI 交给 Qt,可能出现路径解析失败、权限不可读、播放组件无法打开文件等问题。
所以 ArkTS EntryAbility.ets 里对外部 URI 做了一层缓存:
private async cacheExternalUri(uri: string): Promise<string> {
if (uri === '' || (!uri.startsWith('file://') && !uri.startsWith('content://'))) {
return uri;
}
let destinationPath = this.cachedOpenPath(uri);
let destinationUri = fileUri.getUriFromPath(destinationPath);
if (uri.startsWith('file://')) {
try {
let sourcePath = new fileUri.FileUri(uri).path;
await fs.copyFile(sourcePath, destinationPath, 0);
return destinationPath;
} catch (copyLocalFileError) {
// fallback
}
}
try {
await fs.copy(uri, destinationUri);
return destinationPath;
} catch (copyError) {
// fallback
}
return uri;
}
它的作用是把外部文件复制到应用自己的 cache 目录,然后再把 cache 路径作为参数传给 Qt:
this.launchParams = params.join(' ');
qpa.startQtApplication(this);
C++ 侧也对鸿蒙 URL 做了归一化。playlistModel.cpp 中会把 file:// / content:// 转成本地可读路径,并在必要时复制到应用缓存:
QUrl normalizeHarmonyPlaybackUrl(const QUrl& url)
{
const QString sourcePath = localPathFromHarmonyUrl(url);
...
const QString cachePath = harmonyMediaCacheDir() + '/' + hash + suffix;
if (QFile::copy(sourcePath, cachePath))
return QUrl::fromLocalFile(cachePath);
return QUrl::fromLocalFile(sourcePath);
}
这一步解决的是“文件能选中,但播放器打不开”的问题。尤其是在真实设备上,不同来源的视频文件权限不完全一样,先缓存到应用目录是更稳妥的做法。
九、控制栏适配:按钮太小、无效按钮、进度条不动
应用能启动、视频能播放之后,真实设备测试又暴露出几个 UI 问题。
第一,底部控制栏按钮太小。原桌面版控制栏更适合鼠标精细操作,在鸿蒙 PC 的窗口比例下,左下角播放、停止、音量图标显得过小。鸿蒙侧在 main_ohos.qml 中单独放大控制栏:
ControlBar {
barHeight: 104
iconSize: 56
sideMargin: 28
itemSpacing: 24
timeTextSize: 23
sliderHeight: 58
}
第二,右下角搜索、设置、主菜单按钮在当前鸿蒙 MVP 中没有完整功能,点击容易造成误解。因此 ControlBar.qml 增加三个显示开关:
property bool showExplorerButton: true
property bool showSettingsButton: true
property bool showSidebarButton: true
鸿蒙页面关闭这三个按钮:
showExplorerButton: false
showSettingsButton: false
showSidebarButton: false
这样底部控制栏只保留当前可用的播放、停止、音量、进度条。
第三,也是最后一个比较隐蔽的问题:首次导入 .mov 视频后,画面和当前时间都在走,但进度条不动,右侧总时长显示 00:00:00。
当时现象是:
当前时间:00:00:05
总时长:00:00:00
视频画面:正在播放
进度条:滑块仍在起点附近
这说明不是播放失败,而是 QtMultimedia 没有及时返回 duration。原来的 Slider 范围是:
to: Math.max(duration, 0)
enabled: duration > 0
当 duration == 0 时,滑块范围就是 0,自然无法跟随播放位置移动。
修复分两层。
第一层,在 ControlBar.qml 中支持“未知总时长”:
property bool durationKnown: true
property int unknownDurationWindow: 60
property int effectiveSliderDuration: durationKnown ? Math.max(duration, 0) :
(time > 0 ? Math.max(unknownDurationWindow, time + Math.floor(unknownDurationWindow / 2)) : 0)
Slider {
from: 0
to: Math.max(effectiveSliderDuration, 0)
enabled: effectiveSliderDuration > 0
}
Label {
text: durationKnown ? toHHMMSS(duration) : "--:--:--"
}
这样即使底层暂时不知道总时长,滑块也能随着当前时间移动,而不是锁死在起点。
第二层,对 MP4/MOV 做文件头时长解析兜底。Utils::mediaDurationMs() 会读取 moov/mvhd atom 中的 timescale 和 duration:
int Utils::mediaDurationMs(const QUrl& mediaUrl)
{
const QString path = localPathFromMediaUrl(mediaUrl);
QFile file(path);
if (!file.open(QFile::ReadOnly))
return 0;
const int durationMs = parseMp4DurationMs(file, quint64(file.size()));
if (durationMs > 0)
qInfo() << "MoonPlayer parsed media duration:" << durationMs << "ms";
return durationMs;
}
在加载 fallback 视频时先读一次:
fallbackVideo.stop()
fallbackPositionMs = 0
fallbackDurationMs = Utils.mediaDurationMs(mediaUrl)
fallbackVideo.source = mediaUrl
fallbackVideo.play()
如果解析成功,控制栏能显示真实总时长,进度条按真实比例推进;如果解析失败,也有未知时长模式兜底,不会出现“视频在播但进度条完全不动”的体验。

十、这次适配的阶段性结果
到目前为止,MoonPlayer 在鸿蒙 PC 上已经完成了首个可用版本:
- 可以作为 HAP 安装到鸿蒙 PC。
- 可以通过 Stage
UIAbility启动 Qt 应用。 - 可以加载 Qt Quick/QML 主界面。
- 项目内置
qtforharmony_sdk,DevEco Studio 导入后可以直接构建。 - 补齐了 Qt Quick Controls 2、QuickTemplates2、QtMultimedia 等关键运行时依赖。
- 修复了首次运行白屏、缺 so、视频黑屏、控制栏过小、无效按钮、进度条不动等问题。
- 在没有 HarmonyOS arm64
libmpv.so的情况下,通过 QtMultimedia fallback 支持本地视频基础播放。 - 针对鸿蒙文件 URI 做了缓存和路径归一化,提升文件选择器导入视频的稳定性。
同时也要明确目前的边界:
- 当前播放能力是 QtMultimedia fallback,不等同于桌面版完整 mpv 后端。
- 完整字幕、弹幕、复杂音轨、网络解析、硬解参数等能力后续仍需要接入 HarmonyOS arm64
libmpv及相关 FFmpeg 依赖。 yt-dlp、lux、ffmpeg、moonplayer-hlsdl等外部工具如果要在鸿蒙端完整启用,也需要准备对应平台的可执行文件或替代实现。
这次迁移最大的经验是:Qt 项目适配鸿蒙 PC 时,不要只盯着“能不能编译”。真正耗时间的往往是运行时依赖链、QML import、动态库打包、文件 URI、渲染后端和真实设备交互细节。尤其是白屏问题,一定要先看 hilog,判断是 native library 没加载、QML 没加载,还是画面渲染/播放器后端的问题。
MoonPlayer 这类播放器项目比普通 Qt 工具更复杂,因为它不仅要显示 UI,还要处理媒体文件、视频帧、音频输出、进度同步和文件权限。先做一个稳定的鸿蒙 MVP,再逐步补齐 mpv 后端,是这次适配里比较稳妥的路线。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)