一、写在前面

欢迎加入鸿蒙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,而是解决下面这组问题:

  1. 怎样让一个 Qt Quick/QML 桌面应用进入鸿蒙 Stage 模型。
  2. 怎样让 libentry.so 作为 HAP native library 被启动。
  3. 怎样把 Qt for Harmony 的 QPA 插件、Qt 动态库、QML 模块、图片资源一起打包进 HAP。
  4. 怎样处理 libQt5QuickTemplates2.so、QtMultimedia、QML plugin 这些运行时依赖缺失导致的白屏。
  5. 怎样在暂时没有 HarmonyOS arm64 libmpv.so 的情况下,先让播放器具备基础视频播放能力。
  6. 怎样处理鸿蒙文件选择器返回的 file:// / content:// URI,让视频能被 Qt 正常读取。
  7. 怎样根据鸿蒙 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 继续负责界面,鸿蒙工程壳负责承载和启动。这样做的好处是:

  1. 原桌面代码可以继续维护,鸿蒙分支不会变成另一套完全不同的播放器。
  2. UI、播放列表、设置、弹幕、工具类等 C++ / QML 代码可以复用。
  3. 鸿蒙特有逻辑通过 Q_OS_OPENHARMONYmain_ohos.qmlmpvObject_stub.cppplatform/paths_ohos.cpp 等文件收敛起来。
  4. 后续如果拿到 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);

这套链路可以理解成:

  1. MyAbilityStage.ets 负责实例标识和 qpa.attachAbilityStage(this)
  2. EntryAbility.ets 负责窗口生命周期、打开文件参数、启动 Qt 应用。
  3. Index.ets 提供 XComponent 绘制承载点。
  4. libplugins_platforms_qopenharmony.so 接管 Qt 窗口和输入输出。
  5. 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
)

QuickTemplates2MultimediaQuick 很关键。前者关系到 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 目录,把 QtQuickQtQuick/Controls.2Qt/labs/platformQtMultimedia 等模块写入 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 动态库。

修复思路有两个点:

  1. CMake 链接 Qt5::QuickTemplates2
  2. 打包时确保 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.soQuickControls2 运行时还会依赖 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
}

修复后,真机可以看到视频帧正常渲染。这个阶段的判断标准是:

  1. MoonPlayer 窗口能正常打开。
  2. 播放列表能显示导入的视频名称。
  3. 点击播放后,视频区域不是纯黑或纯白。
  4. 当前时间开始跳动。

七、没有 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")

规则是:

  1. AUTO:如果 mpv-ohos 存在,就启用 mpv;如果不存在,就构建可启动 UI。
  2. ON:强制要求 mpv-ohos,缺少就构建失败。
  3. 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。它的定位是:

  1. 让鸿蒙 PC 版本先具备本地视频播放 MVP。
  2. 支持通过文件选择器打开常见视频格式。
  3. 让播放、暂停、停止、音量、进度条这些基本交互可验证。
  4. 为后续接入 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 中的 timescaleduration

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 上已经完成了首个可用版本:

  1. 可以作为 HAP 安装到鸿蒙 PC。
  2. 可以通过 Stage UIAbility 启动 Qt 应用。
  3. 可以加载 Qt Quick/QML 主界面。
  4. 项目内置 qtforharmony_sdk,DevEco Studio 导入后可以直接构建。
  5. 补齐了 Qt Quick Controls 2、QuickTemplates2、QtMultimedia 等关键运行时依赖。
  6. 修复了首次运行白屏、缺 so、视频黑屏、控制栏过小、无效按钮、进度条不动等问题。
  7. 在没有 HarmonyOS arm64 libmpv.so 的情况下,通过 QtMultimedia fallback 支持本地视频基础播放。
  8. 针对鸿蒙文件 URI 做了缓存和路径归一化,提升文件选择器导入视频的稳定性。

同时也要明确目前的边界:

  1. 当前播放能力是 QtMultimedia fallback,不等同于桌面版完整 mpv 后端。
  2. 完整字幕、弹幕、复杂音轨、网络解析、硬解参数等能力后续仍需要接入 HarmonyOS arm64 libmpv 及相关 FFmpeg 依赖。
  3. yt-dlpluxffmpegmoonplayer-hlsdl 等外部工具如果要在鸿蒙端完整启用,也需要准备对应平台的可执行文件或替代实现。

这次迁移最大的经验是:Qt 项目适配鸿蒙 PC 时,不要只盯着“能不能编译”。真正耗时间的往往是运行时依赖链、QML import、动态库打包、文件 URI、渲染后端和真实设备交互细节。尤其是白屏问题,一定要先看 hilog,判断是 native library 没加载、QML 没加载,还是画面渲染/播放器后端的问题。

MoonPlayer 这类播放器项目比普通 Qt 工具更复杂,因为它不仅要显示 UI,还要处理媒体文件、视频帧、音频输出、进度同步和文件权限。先做一个稳定的鸿蒙 MVP,再逐步补齐 mpv 后端,是这次适配里比较稳妥的路线。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐