鸿蒙PC迁移:Minitube Qt YouTube 客户端鸿蒙PC适配全记录

一、写在前面

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Minitube

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

这篇文章记录的是 Minitube 在 HarmonyOS PC / OpenHarmony PC 环境中的一次完整适配过程。

Minitube 不是 Electron 项目,也不是一个普通 ArkUI 应用。它原本是一个 C++ / Qt Widgets 的 YouTube 桌面客户端,桌面版本以 Qt Widgets 界面为主,播放核心默认依赖 mpv,并且包含搜索、播放列表、频道订阅、下载、地区切换、快照等桌面客户端常见能力。它的代码还被拆成了 7 个 lib/* git 子模块(ythttpjspromisesidlemediaqt-reusable-widgets)。

和很多 Web 套壳项目不同,这次适配的重点不是把一个网页塞进 HAP,而是解决下面这组真实问题:

  1. 怎样让一个 Qt Widgets 桌面应用进入鸿蒙 Stage 模型,并由 libentry.so 启动。
  2. 怎样把 Qt for Harmony 的 QPA 插件、Qt 动态库、图片/SQLite/媒体插件、OpenSSL 一起打包进 HAP,解决一连串白屏问题。
  3. 在没有 HarmonyOS arm64 libmpv.so 的情况下,怎样改用 Qt Multimedia(MEDIA_QT)后端让视频先播起来。
  4. 项目的 lib/* 子模块在新检出里是空的、而本机又被代理挡住 GitHub,怎样把真源码拉下来。
  5. SDK 是 Qt 5.15,而部分库代码是按 Qt 6 写的,怎样把 mediaqt.cpp 等做 Qt6→Qt5 回移。
  6. minitube 是纯 YouTube 客户端,国内连不上 YouTube,怎样在保留原版界面的前提下,把搜索/取流层从 YouTube 换成国内可用的源。
  7. 怎样把鸿蒙上的播放器前端美化成现代播放器(进度条、控制栏、列表选中效果)。

本次适配采用逐步验证的路线:保留 Minitube 原有 C++ 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和逻辑仍由 Qt 运行时承载。

二、项目背景:Minitube 是 Qt Widgets + 子模块拆分的桌面客户端

确认它确实是 Qt 项目很简单:根目录有 minitube.proresources.qrcsrc/main.cpp 里用的是 QApplication.pro 里写着:

pro 复制代码
QT += widgets network sql qml
DEFINES += MEDIA_MPV
include(lib/yt/yt.pri)
include(lib/http/http.pri)
include(lib/media/media.pri)
# ... qt-reusable-widgets / idle / js / promises

原始项目结构大致如下:

text 复制代码
minitube-master/
├── minitube.pro
├── resources.qrc
├── src/                 # 47 个 .cpp:mainwindow / mediaview / searchview / homeview ...
├── icons/  data/  flags/  sounds/  locale/
├── lib/                 # 7 个 git 子模块(新检出里通常是空的)
│   ├── yt/  http/  js/  promises/  idle/  media/  qt-reusable-widgets/
└── harmony_pc/          # 本次新增的鸿蒙工程壳

鸿蒙适配工程集中放在:

text 复制代码
minitube-master/harmony_pc/
├── AppScope/app.json5
├── build-profile.json5
├── entry/
│   ├── build-profile.json5
│   ├── libs/arm64-v8a/                # QPA 插件 + 媒体插件 + OpenSSL
│   └── src/main/
│       ├── cpp/CMakeLists.txt         # 把 minitube 编译成 libentry.so
│       ├── ets/                        # AbilityStage / EntryAbility / Index
│       ├── module.json5
│       └── resources/
├── hvigor/  oh-package.json5
└── qtforharmony_sdk/                  # 项目自带的 Qt 5.15.12 for Harmony SDK

这次没有把 minitube 重写成 ArkUI,而是让 Qt Widgets 继续负责界面,鸿蒙工程壳负责承载和启动。好处是原桌面代码可以继续复用,鸿蒙特有逻辑通过 Q_OS_OPENHARMONY 收敛。

三、鸿蒙工程壳:Ability + XComponent + Qt QPA

Qt for Harmony 的关键思路是:ArkTS 侧创建鸿蒙窗口,页面里放一个 XComponent,再由 Qt OpenHarmony QPA 插件把 Qt 窗口挂上去。

Index.ets 很薄,核心就是这个 XComponent

ts 复制代码
XComponent({
  id: this.windowId,
  type: XComponentType.NODE,
  libraryname: 'plugins_platforms_qopenharmony'
})
  .width('100%')
  .height('100%');

EntryAbility.ets 负责窗口生命周期并启动 Qt。这里有一个很关键、踩过坑的字段 :QPA 插件要靠 launchApplication 才知道去 dlopen 哪个 .so

ts 复制代码
export default class EntryAbility extends UIAbility {
  private launchApplication = 'libentry.so';   // 少了这行,会去加载 libs/arm64/undefined → 白屏
  private launchParams = '';
  // ...
  async onWindowStageCreate(windowStage: Window.WindowStage) {
    await windowStage.loadContent(this.loadContentUrl, localStore);
    qpa.handleJsTopWindowCreated(this.name, this);
    qpa.startQtApplication(this);
  }
}

当时第一次运行就是白屏,hilog 里写得很清楚:

text 复制代码
load qt application /data/storage/el1/bundle/libs/arm64/undefined
Failed to load QT application '.../libs/arm64/undefined': No such file or directory

补回 launchApplication = 'libentry.so' 后,libentry.so 才被正确加载。

应用身份用 minitube 自己的包名 org.tordini.flavio.minitube,图标用 minitube 自己的 data/512x512/minitube.png,签名留空交给 DevEco Studio 生成------不要把参考模板项目的 logo、包名、签名复制过来


四、自带 Qt SDK:把 SDK 放进自己项目、引用自己的

适配里有一条硬性要求:Qt for Harmony SDK 必须放进当前项目目录、引用项目内自己的 SDK,而不是引用别的工程的 SDK。所以把整套 234MB 的 Qt 5.15.12 for Harmony SDK 复制到:

text 复制代码
harmony_pc/qtforharmony_sdk/
  lib/cmake/Qt5/Qt5Config.cmake
  lib/libQt5Core.so / libQt5Widgets.so / libQt5Sql.so / ...

entry/src/main/cpp/CMakeLists.txtharmony_pc 反向定位项目根目录,并强制使用项目内置 SDK:

cmake 复制代码
get_filename_component(HARMONY_PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE)
get_filename_component(MINITUBE_ROOT "${HARMONY_PROJECT_ROOT}/.." ABSOLUTE)

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 this project's bundled Qt 5 for HarmonyOS SDK")
endif()

要特别注意:这个 SDK 是 Qt 5.15,不是 Qt 6。后面很多坑都来自"库代码是按 Qt 6 写的,但运行环境是 Qt 5"。

五、先验证链路:UI 壳 + Qt Multimedia 播放(一连串白屏)

因为一开始 lib/* 子模块是空的,没法直接编译完整 minitube,所以先写了一个自包含的 UI 壳 (一个纯 Qt Widgets 的 QMainWindow + QMediaPlayer + QVideoWidget),先把"ArkTS → XComponent → QPA → libentry.so → 可见 Qt 窗口 → 真正能播视频"这条链路打通。

src/main.cpp 里加了鸿蒙入口别名(QPA 插件实际调用的是 qtmain):

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)
extern "C" int qtmain(int argc, char *argv[]) { return main(argc, argv); }
#endif

这一步连续踩了 4 个白屏/加载失败,全靠 hilog 一个个定位:

  1. libs/arm64/undefined :缺 launchApplication(见第三节)。
  2. libQt5Svg.so: No such file or directory (needed by libplugins_imageformats_qsvg.so) :hvigor 的 collectAllLibs 会把 Qt 的图片格式插件(qsvg 等)打进 HAP,但 qsvg 依赖的 libQt5Svg.so 没被收集。解决:CMake 里 target_link_libraries(... Qt5::Svg),让 libQt5Svg.so 进包。
  3. 媒体插件没进包QMediaPlayer 在鸿蒙上的后端是 libplugins_mediaservice_qtmedia_openharmony.so(它只依赖 Qt5 Widgets/Multimedia,不依赖 Quick),collectAllLibs 不会自动收。解决:手动把它和 libplugins_audio_qtaudio_ohaudiodevice.solibplugins_playlistformats_qtmultimedia_m3u.so 复制进 entry/libs/arm64-v8a/
  4. libQt5OpenGL.so: No such file (needed by libQt5MultimediaWidgets.so) :解决:target_link_libraries(... Qt5::OpenGL)

打通后,缩略图还加载不出来------根因是 QtNetwork 走 HTTPS 需要 OpenSSL,而 Qt 是运行时 dlopen libssl.so.1.1 / libcrypto.so.1.1 的,collectAllLibs 抓不到。把 SDK 里的 libssl.solibcrypto.so(含 .so.1.1)一并放进 entry/libs/arm64-v8a/,HTTPS 才通、真实封面图才加载出来。

这一阶段的结论:Qt 适配鸿蒙,"能编译"只是第一步,运行时动态库链(QPA 插件、图片/媒体插件、OpenSSL)缺一个就白屏 ,必须先看 hilog 判断是哪一环。


六、拿到真源码:用 codeload.github.com 下载缺失的子模块

要做"原版界面",就必须把 lib/* 7 个子模块的真源码拿下来。但本机直连 GitHub 被代理挡住(github.com / raw.githubusercontent.com 返回 000),git submodule update 用不了。

测了一圈下载源后发现:codeload.github.com 是可达的github.com 被挡、codeload 却能通)。于是直接拉每个库的 tar 包:

bash 复制代码
for lib in qt-reusable-widgets http idle js promises yt media; do
  curl -sL -o "$lib.tgz" \
    "https://codeload.github.com/flaviotordini/$lib/tar.gz/refs/heads/master"
  mkdir -p "lib/$lib" && tar xzf "$lib.tgz" -C "lib/$lib" --strip-components=1
done

7 个库全部拉齐,无嵌套子模块。读它们的 .pri 后确定了关键点:

  • lib/media/media.pri 里除了 MEDIA_MPV,还有一个 MEDIA_QT 后端(src/qt/mediaqt.cpp,基于 QtMultimedia)。这正好和我们已经验证可用的 Qt Multimedia 播放对上------不需要 libmpv
  • lib/idle/idle.pri 只有 mac/win/android/linux 后端,鸿蒙没有,需要补一个桩。
  • lib/yt/yt.pri 走的是 ytjs(在 JS 引擎里跑 YouTube.js 解析 YouTube)。

七、把真 minitube 编译成 libentry.so:MEDIA_QT + idle 桩 + Qt6→Qt5 回移

CMake 从"glob 子模块"改成按每个 .pri 精确选源,并选用 MEDIA_QT 后端:

cmake 复制代码
target_compile_definitions(entry PRIVATE
    APP_VERSION="4.0" APP_NAME="Minitube" APP_UNIX_NAME="minitube"
    APP_SNAPSHOT QT_USE_QSTRINGBUILDER
    MEDIA_QT HTTP OPENHARMONY NAPI_DISABLE_CPP_EXCEPTIONS)

# media:用 Qt Multimedia 后端,不用 mpv
list(APPEND MINITUBE_SOURCES "${MINITUBE_ROOT}/lib/media/src/qt/mediaqt.cpp")
# idle:鸿蒙桩
list(APPEND MINITUBE_SOURCES "${MINITUBE_ROOT}/lib/idle/src/idle_ohos.cpp")

编译时集中暴露了"库是按 Qt 6 写的、运行在 Qt 5"的问题,逐个回移:

  1. mediaqt.cpp 整体回移到 Qt5 :Qt6 的 setSource()→Qt5 setMedia(QMediaContent)audioOutput/QAudioOutput→Qt5 把音量/静音放在 QMediaPlayer 上(setVolume(0..100)/setMuted);playbackState()/PlaybackStatestate()/StateerrorOccurred→重载的 error 信号;bufferProgressChangedbufferStatusChanged
  2. actionbutton.cppQAction::visibleChanged 是 Qt6 才有的,Qt5 用 QAction::changed 覆盖,加 #if QT_VERSION >= 0x060000 守卫。
  3. header-only 的 Q_OBJECT 类MediaSuggesterpromises 的几个):qmake 把它们放在 HEADERS 里让 moc 处理,CMake AUTOMOC 必须把这些头文件也加进 target sources,否则链接时报 undefined symbol: vtable for ...
  4. idle_ohos.cpp :实现 Idle 接口为空操作(鸿蒙暂不接电源锁,不影响播放)。

改完之后,真 minitube 完整编译并链接成功,libentry.so 约 57MB。

八、原版界面跑起来,与 "fetch is not defined"

装上真 minitube 后,原版首页完整出现 :电视机图标、Welcome to "Minitube"、关键词搜索框、Popular/Subscriptions 标签、底部状态栏------和桌面版一模一样(即第二节那张图)。

但一搜索就报错:

text 复制代码
ReferenceError: fetch is not defined

原因是 minitube 用 ytjs 在 Qt 的 QJSEngine 里跑 YouTube.js 解析脚本,而该脚本用到了浏览器的 fetch,Qt 的 JS 环境里没有;况且 YouTube 在国内本身就连不上。也就是说------真 minitube 是纯 YouTube 客户端,国内即使编译成功也搜不出、播不了,这是它的设计决定的,不是适配缺陷。

于是进入最后一步:保留原版界面,把数据源换掉。


九、把搜索/取流层从 YouTube 换成国内源(maccms)

minitube 的搜索入口很清晰,MediaView::search() 里:

cpp 复制代码
VideoSource *search = new SearchVideoSource(searchParams);  // 原本走 YouTube

VideoSource 是抽象基类,loadVideos() 里产出一批 Video*,每个 Video 通过 loadStreamUrl() 拿播放地址。所以替换方案是做一个新的 VideoSource 子类 ,去查国内通用的 maccms provide/vod JSON 接口(传关键词回一个带 vod_name/vod_pic/vod_play_url 的列表,里面是 m3u8 直链),并让 Video 直接带上 m3u8、跳过 YouTube 解析:

cpp 复制代码
// 1) Video 支持直接给定播放地址,loadStreamUrl 立即返回
void Video::setStreamUrl(const QString &value) { streamUrl = value; }
void Video::loadStreamUrl() {
    if (!streamUrl.isEmpty()) {                 // 已经有直链,直接发信号
        QString url = streamUrl;
        QTimer::singleShot(0, this, [this, url] { emit gotStreamUrl(url, QString()); });
        return;
    }
    loadStreamUrlJS();                          // 否则才走原 YouTube 解析
}

// 2) MaccmsSearchSource:查国内接口,解析出封面 + m3u8,产出 Video
//    (接口地址是可替换的示例,正式用请换成你自己有版权/合规的内容接口)
v->setTitle(vod_name);
v->setChannelTitle(type_name + " · " + vod_remarks);
v->setStreamUrl(firstM3u8);     // 从 vod_play_url 里正则取第一个 .m3u8
v->addThumb(320, 180, vod_pic); // 封面

// 3) MediaView::search 改成用国内源
VideoSource *search = new MaccmsSearchSource(searchParams);

// 4) 鸿蒙上跳过 YouTube JS 初始化,消除 fetch 报错
#ifndef Q_OS_OPENHARMONY
    JS::instance().initialize(QUrl(Constants::WEBSITE "-ws/bundle3.js"));
#endif

m3u8 的实际解码播放交给鸿蒙媒体框架(libplugins_mediaservice_qtmedia_openharmony.so 原生支持 HLS)。改完后在真机上输入 斗罗大陆 回车,左侧出现带封面的原版结果列表,点击即播------界面是原版 minitube 的,数据源是国内可用的。

十、播放器前端美化:让控制栏像个现代播放器

原版 minitube 的进度条是 knobless(隐藏滑块、很细的轨道),加上老式的工具栏按钮,在鸿蒙大屏上显得很简陋------"连进度条都几乎看不见"。

由于 minitube 用 QSS 控制样式(:/style.css),不必改布局,直接在 main.cpp 里对鸿蒙追加一段现代播放器样式即可(粗的彩色进度条 + 真实圆形滑块、扁平圆角的播放/暂停按钮、药丸形搜索框、精致的音量条、列表选中高亮):

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)
styleSheet += QLatin1String(R"OHQSS(
SeekSlider[knobless="true"]::groove:horizontal {
    height: 6px; border-radius: 3px; background: rgba(0,0,0,0.14); }
SeekSlider[knobless="true"]::sub-page:horizontal {
    height: 6px; border-radius: 3px; background: #ff2d55; }   /* 已播放进度:醒目色 */
SeekSlider[knobless="true"]::handle:horizontal {
    width: 16px; height: 16px; margin: -5px 0; border-radius: 8px;
    background: #ff2d55; border: 3px solid #ffffff; }          /* 真实可见的圆形滑块 */
QToolBar QToolButton, ActionButton {
    background: transparent; border: none; border-radius: 19px; padding: 7px; }
QToolBar QToolButton:hover { background: rgba(0,0,0,0.06); }    /* 现代 hover 反馈 */
SearchLineEdit {
    background: #f1f2f4; border: 1px solid #e4e4e7; border-radius: 17px; padding: 6px 14px; }
PlaylistView::item:selected { background: rgba(255,45,85,0.14); border-radius: 10px; }
)OHQSS");
#endif

这样进度条变成一条醒目的粗条 + 圆形滑块,播放/暂停/音量/搜索都变成现代圆角风格,列表点击也有了高亮反馈,整体在鸿蒙大屏上就像一个正常的现代播放器。

十一、这次适配的阶段性结果与边界

到目前为止,Minitube 在鸿蒙 PC 上已经完成一个可用版本:

  1. 可以作为 HAP 安装到鸿蒙 PC,并通过 Stage UIAbility 启动 Qt 应用。
  2. 跑的是真·原版 minitube 源码和界面(首页、工具栏、播放列表、播放页都与桌面版一致)。
  3. 项目内置自己的 qtforharmony_sdk,DevEco Studio 导入后可直接构建。
  4. 在没有 HarmonyOS arm64 libmpv.so 的情况下,用 MEDIA_QT(Qt Multimedia + 鸿蒙媒体框架)支持视频播放,HLS(m3u8)原生可放。
  5. 把库代码里的 Qt6 写法回移到了 Qt5(mediaqt.cppactionbutton.cpp、header-only moc 等)。
  6. 修复了 launchApplication 缺失、libQt5Svg/libQt5OpenGL/媒体插件/OpenSSL 未打包等一连串白屏问题。
  7. 在保留原版界面的前提下,把搜索/取流层从 YouTube 换成国内可用的源,做到"原版界面 + 国内能搜能播"。
  8. 给鸿蒙端补了一套现代播放器 QSS,进度条、控制栏、列表选中都焕然一新。

同时也明确目前的边界:

  1. 当前播放是 Qt Multimedia + 鸿蒙媒体框架,不等同于桌面版完整 mpv 后端(部分编码/字幕/音轨能力后续仍需接入 HarmonyOS arm64 libmpv 与 FFmpeg 依赖)。
  2. 国内内容源是第三方聚合接口示例,仅用于打通"搜索→列表→播放"链路,正式使用请替换为有版权/合规的内容接口。
  3. minitube 原生的频道订阅、下载、地区切换等能力深度绑定 YouTube,国内场景下需要再做替换或裁剪。

这次迁移最大的经验是:Qt 项目适配鸿蒙 PC,真正耗时间的不是"能不能编译",而是运行时依赖链(QPA 插件、图片/媒体插件、OpenSSL)、Qt 版本差异(库按 Qt6 写、环境是 Qt5)、以及"应用本身的数据源在目标地区能不能用"。 遇到白屏一定先看 hilog,判断是 native library 没加载、动态库缺失,还是渲染/播放后端的问题;遇到"界面对了但功能空",再回到数据源这一层去解决。

相关推荐
芦芭荞1 小时前
QGgraphicsView鼠标缩放
qt
小鹏linux2 小时前
鸿蒙PC使用 Electron 迁移:Beekeeper Studio 适配全记录
华为·electron·harmonyos
前端不太难2 小时前
Agent First:鸿蒙 App 的下一代 AI Runtime 架构
人工智能·架构·harmonyos
轻口味2 小时前
轻规划鸿蒙开发实战11:自研 Haptic Canvas 粒子系统,纯 ArkUI 高性能烟花渲染与性能避
华为·harmonyos·鸿蒙
狼哥16862 小时前
学习卡片案例新特性接入
ui·华为·harmonyos
Davina_yu2 小时前
JSON数据处理:字符串序列化与反序列化实战(20)
harmonyos·鸿蒙·鸿蒙系统
小鹏linux2 小时前
鸿蒙PC迁移:Tesseract OCR C++ 三方库鸿蒙适配全记录
c++·ocr·harmonyos
森G2 小时前
65、UDP协议(拓展选学)---------网络编程
网络·c++·qt·网络协议·tcp/ip·udp