鸿蒙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 子模块(yt、http、js、promises、idle、media、qt-reusable-widgets)。
和很多 Web 套壳项目不同,这次适配的重点不是把一个网页塞进 HAP,而是解决下面这组真实问题:
- 怎样让一个 Qt Widgets 桌面应用进入鸿蒙 Stage 模型,并由
libentry.so启动。 - 怎样把 Qt for Harmony 的 QPA 插件、Qt 动态库、图片/SQLite/媒体插件、OpenSSL 一起打包进 HAP,解决一连串白屏问题。
- 在没有 HarmonyOS arm64
libmpv.so的情况下,怎样改用 Qt Multimedia(MEDIA_QT)后端让视频先播起来。 - 项目的
lib/*子模块在新检出里是空的、而本机又被代理挡住 GitHub,怎样把真源码拉下来。 - SDK 是 Qt 5.15,而部分库代码是按 Qt 6 写的,怎样把
mediaqt.cpp等做 Qt6→Qt5 回移。 - minitube 是纯 YouTube 客户端,国内连不上 YouTube,怎样在保留原版界面的前提下,把搜索/取流层从 YouTube 换成国内可用的源。
- 怎样把鸿蒙上的播放器前端美化成现代播放器(进度条、控制栏、列表选中效果)。
本次适配采用逐步验证的路线:保留 Minitube 原有 C++ 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和逻辑仍由 Qt 运行时承载。

二、项目背景:Minitube 是 Qt Widgets + 子模块拆分的桌面客户端
确认它确实是 Qt 项目很简单:根目录有 minitube.pro、resources.qrc,src/main.cpp 里用的是 QApplication,.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
原始项目结构大致如下:
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/ # 本次新增的鸿蒙工程壳
鸿蒙适配工程集中放在:
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:
XComponent({
id: this.windowId,
type: XComponentType.NODE,
libraryname: 'plugins_platforms_qopenharmony'
})
.width('100%')
.height('100%');
EntryAbility.ets 负责窗口生命周期并启动 Qt。这里有一个很关键、踩过坑的字段:QPA 插件要靠 launchApplication 才知道去 dlopen 哪个 .so:
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 里写得很清楚:
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 复制到:
harmony_pc/qtforharmony_sdk/
lib/cmake/Qt5/Qt5Config.cmake
lib/libQt5Core.so / libQt5Widgets.so / libQt5Sql.so / ...
entry/src/main/cpp/CMakeLists.txt 从 harmony_pc 反向定位项目根目录,并强制使用项目内置 SDK:
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):
#if defined(Q_OS_OPENHARMONY)
extern "C" int qtmain(int argc, char *argv[]) { return main(argc, argv); }
#endif
这一步连续踩了 4 个白屏/加载失败,全靠 hilog 一个个定位:
libs/arm64/undefined:缺launchApplication(见第三节)。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进包。- 媒体插件没进包:
QMediaPlayer在鸿蒙上的后端是libplugins_mediaservice_qtmedia_openharmony.so(它只依赖 Qt5 Widgets/Multimedia,不依赖 Quick),collectAllLibs不会自动收。解决:手动把它和libplugins_audio_qtaudio_ohaudiodevice.so、libplugins_playlistformats_qtmultimedia_m3u.so复制进entry/libs/arm64-v8a/。 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.so、libcrypto.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 包:
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 后端:
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"的问题,逐个回移:
mediaqt.cpp整体回移到 Qt5:Qt6 的setSource()→Qt5setMedia(QMediaContent);audioOutput/QAudioOutput→Qt5 把音量/静音放在QMediaPlayer上(setVolume(0..100)/setMuted);playbackState()/PlaybackState→state()/State;errorOccurred→重载的error信号;bufferProgressChanged→bufferStatusChanged。actionbutton.cpp:QAction::visibleChanged是 Qt6 才有的,Qt5 用QAction::changed覆盖,加#if QT_VERSION >= 0x060000守卫。- header-only 的 Q_OBJECT 类(
Media、Suggester、promises的几个):qmake 把它们放在HEADERS里让 moc 处理,CMake AUTOMOC 必须把这些头文件也加进 target sources,否则链接时报undefined symbol: vtable for ...。 idle_ohos.cpp:实现Idle接口为空操作(鸿蒙暂不接电源锁,不影响播放)。
改完之后,真 minitube 完整编译并链接成功,libentry.so 约 57MB。

八、原版界面跑起来,与 “fetch is not defined”
装上真 minitube 后,原版首页完整出现:电视机图标、Welcome to "Minitube"、关键词搜索框、Popular/Subscriptions 标签、底部状态栏——和桌面版一模一样(即第二节那张图)。
但一搜索就报错:
ReferenceError: fetch is not defined
原因是 minitube 用 ytjs 在 Qt 的 QJSEngine 里跑 YouTube.js 解析脚本,而该脚本用到了浏览器的 fetch,Qt 的 JS 环境里没有;况且 YouTube 在国内本身就连不上。也就是说——真 minitube 是纯 YouTube 客户端,国内即使编译成功也搜不出、播不了,这是它的设计决定的,不是适配缺陷。
于是进入最后一步:保留原版界面,把数据源换掉。
九、把搜索/取流层从 YouTube 换成国内源(maccms)
minitube 的搜索入口很清晰,MediaView::search() 里:
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 解析:
// 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 里对鸿蒙追加一段现代播放器样式即可(粗的彩色进度条 + 真实圆形滑块、扁平圆角的播放/暂停按钮、药丸形搜索框、精致的音量条、列表选中高亮):
#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 上已经完成一个可用版本:
- 可以作为 HAP 安装到鸿蒙 PC,并通过 Stage
UIAbility启动 Qt 应用。 - 跑的是真·原版 minitube 源码和界面(首页、工具栏、播放列表、播放页都与桌面版一致)。
- 项目内置自己的
qtforharmony_sdk,DevEco Studio 导入后可直接构建。 - 在没有 HarmonyOS arm64
libmpv.so的情况下,用MEDIA_QT(Qt Multimedia + 鸿蒙媒体框架)支持视频播放,HLS(m3u8)原生可放。 - 把库代码里的 Qt6 写法回移到了 Qt5(
mediaqt.cpp、actionbutton.cpp、header-only moc 等)。 - 修复了
launchApplication缺失、libQt5Svg/libQt5OpenGL/媒体插件/OpenSSL 未打包等一连串白屏问题。 - 在保留原版界面的前提下,把搜索/取流层从 YouTube 换成国内可用的源,做到"原版界面 + 国内能搜能播"。
- 给鸿蒙端补了一套现代播放器 QSS,进度条、控制栏、列表选中都焕然一新。
同时也明确目前的边界:
- 当前播放是 Qt Multimedia + 鸿蒙媒体框架,不等同于桌面版完整 mpv 后端(部分编码/字幕/音轨能力后续仍需接入 HarmonyOS arm64
libmpv与 FFmpeg 依赖)。 - 国内内容源是第三方聚合接口示例,仅用于打通"搜索→列表→播放"链路,正式使用请替换为有版权/合规的内容接口。
- minitube 原生的频道订阅、下载、地区切换等能力深度绑定 YouTube,国内场景下需要再做替换或裁剪。
这次迁移最大的经验是:Qt 项目适配鸿蒙 PC,真正耗时间的不是"能不能编译",而是运行时依赖链(QPA 插件、图片/媒体插件、OpenSSL)、Qt 版本差异(库按 Qt6 写、环境是 Qt5)、以及"应用本身的数据源在目标地区能不能用"。 遇到白屏一定先看 hilog,判断是 native library 没加载、动态库缺失,还是渲染/播放后端的问题;遇到"界面对了但功能空",再回到数据源这一层去解决。
更多推荐



所有评论(0)