基于 Qt 6 / QML 的通用插件化应用宿主。以应用面板为主页,插件以卡片形式展示,点击进入;业务能力由插件提供,支持运行时动态加载 .dll/.so 插件。
当前仓库同时包含一个播放领域的 Media SDK 演进实现:PlayPlugin 通过 SDK core 解封装/解码,通过独立帧通道把音视频帧交给 Qt 展示层或 SDK runtime,控制事件与帧数据通道已经解耦。
PluginBased/
├── CMakeLists.txt # 顶层构建
├── README.md
├── BUILD.md # 详细构建 & 打包说明
├── package.sh # 一键打包脚本(macOS/Linux/Windows)
│
├── app/ # 主程序
│ ├── main.cpp # 入口:读配置 → 初始化日志 → 加载插件 → 启动 QML
│ ├── AppConfig.h/.cpp # INI 配置管理(QSettings,零额外依赖)
│ ├── AppController.h/.cpp # 应用级单例,QML_SINGLETON
│ ├── AppLanguageService.h/.cpp # 语言切换与宿主翻译安装
│ ├── CrashHandler.h/.cpp # Crash 捕获(Windows MiniDump / Unix signal)
│ └── qml/
│ ├── main.qml # 根窗口 + StackView 导航
│ ├── HomePanel.qml # 应用主页(插件卡片面板)
│
├── core/ # 宿主核心接口
│ ├── PluginManager.h/.cpp # 插件管理器,QML_SINGLETON
│ └── CMakeLists.txt # PluginBasedCore 接口目标
│
├── logger/ # 共享日志库(PluginBasedLogger)
│ ├── Logger.h/.cpp # spdlog 封装,供 core/plugin 使用
│ ├── QmlLogger.h/.cpp # QML 日志桥接
│ └── CMakeLists.txt
│
├── plugin/ # 插件接口定义
│ └── IAppPlugin.h # 通用插件接口
│
├── plugins/ # 插件实现
│ ├── DummyPlugin/ # 最小示例插件
│ │ ├── qml/ # 示例插件界面
│ │ └── translations/ # 插件自带翻译 TS
│ └── PlayPlugin/ # 自包含视频播放器插件
│ ├── PlayPlugin.h/.cpp # IAppPlugin 实现
│ ├── qml/ # 播放器界面、控制栏、播放列表
│ ├── shaders/ # YUV 视频渲染 shader
│ ├── translations/ # 插件自带翻译 TS
│ └── src/ # Qt 播放桥、渲染器、Presenter、QML-facing 模型
│
├── sdk/ # 播放领域 SDK
│ ├── media_core/ # FFmpeg demux/decode、硬解后端、帧推送接口
│ ├── media_playback_runtime/ # A/V 时钟、渲染队列背压、runtime 播放调度
│ └── media_platform_audio_macos/ # macOS AudioUnit/CoreAudio 音频输出
│
├── docs/ # 设计文档与阶段任务记录
├── translations/ # 宿主应用翻译 TS
├── tests/ # 架构约束、回归检查和 CTest 脚本
└── tools/ # 打包、部署、依赖验证工具
| 库 | 版本 | 用途 |
|---|---|---|
| Qt | 6.5+ | 框架:Core / Gui / Quick / QuickControls2 / Multimedia |
| spdlog | 1.12+ | 结构化日志 |
| fmt | 10+ | 格式化(spdlog 依赖) |
| FFmpeg | 5.0+ | Media SDK demux/decode、重采样、像素转换 |
| pkgconf/pkg-config | 任意 | CMake 查找 FFmpeg pkg-config 模块 |
| CoreAudio / AudioUnit | macOS 系统框架 | SDK runtime 的 macOS 音频输出 |
项目顶层通过 FetchContent 引入相邻目录 ../QtQuickComponents,构建前请确保该目录存在,或在 CMakeLists.txt 中替换为对应的远程仓库来源。
# 配置(vcpkg manifest 会自动安装 spdlog、fmt、pkgconf、ffmpeg)
cmake -B build -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
# 构建
cmake --build build --parallel
# 运行
./build/app/PluginBasedApp
# 轻量回归检查
python3 tests/playplugin_regression_checks.py
python3 tests/plugin_generator_checks.py
# CTest 汇总验证
ctest --test-dir build --output-on-failure详细说明、Release 构建、打包步骤见 BUILD.md。
项目启用 CTest,当前注册 36 项检查。验证覆盖宿主、插件、播放 SDK core、runtime、macOS 音频平台层和架构约束:
| 范围 | 代表性测试 |
|---|---|
| PlayPlugin 架构与回归 | playplugin_regression_checks、playplugin_qt_playback_adapter_checks、playplugin_bplus_completion_checks |
| 播放队列与 seek | playplugin_frame_queue、playplugin_playback_data_bridge、playplugin_pending_seek_requests |
| Media SDK core | media_sdk_core_frame_contract、media_sdk_core_ffmpeg_integration、media_sdk_core_playback_worker |
| SDK runtime | media_sdk_playback_runtime_frame_queue、media_sdk_playback_runtime_av_sync_scheduler、media_sdk_playback_runtime_player_mock |
| macOS 音频平台层 | media_sdk_platform_audio_macos_engine、media_sdk_platform_audio_macos_ring_buffer |
| 宿主与插件基础能力 | plugin_metadata_validator、plugin_discovery、plugin_generator_backend_smoke、i18n_architecture_checks |
本地运行:
ctest --test-dir build --output-on-failureGitHub Actions workflow 位于 .github/workflows/ci.yml。CI 在 macOS 上 checkout 本仓库、相邻 QtQuickComponents 仓库和 vcpkg,随后执行 configure、build、CTest。
主页 HomePanel 作为 StackView 的初始页,卡片点击后 push 通用插件页面容器。插件若声明 hasQmlUI(),宿主会通过 Loader 加载插件返回的 qmlComponentUrl();顶部栏根据 StackView 深度自动显示返回按钮。
首次运行自动生成 <AppLocalDataLocation>/config/pluginbased.ini,支持运行时热重载(调用 AppController.reloadConfig()):
[log]
level = debug ; trace | debug | info | warn | error | critical | off
dir = logs
max_file_size_mb = 5
max_files = 3
flush_on = warn
[ui]
theme = dark ; themes/<id>.json,内置支持 dark / light
language = en_US ; 默认 en_US,当前支持 en_US / zh_CN宿主和插件统一使用 QuickUI.Components 1.0 的 ComponentTheme。当前主题 ID 保存到 pluginbased.ini 的 [ui] theme,启动时会从 themes/<id>.json 加载主题并启用文件热加载;修改当前主题 JSON 后,绑定到 ComponentTheme token 的 QML 会即时刷新。插件 QML 应避免写死宿主级颜色,优先使用 ComponentTheme.surface、ComponentTheme.textPrimary、ComponentTheme.textSecondary、ComponentTheme.accent 等 token。
运行时主题目录约定:
- 开发构建:
<build>/themes - 普通发布:
<app>/themes - macOS bundle:
PluginBasedApp.app/Contents/Resources/themes
宿主优先加载运行时目录中的 themes/<id>.json,找不到时由 QtQuickComponents 内置 dark / light JSON 作为 fallback。
应用默认语言为英文 en_US,当前内置中文 zh_CN。语言选择保存到 pluginbased.ini 的 [ui] language,运行时切换语言时会更新宿主 translator、插件 translator,并调用 QQmlEngine::retranslate() 刷新已加载页面。
翻译资源按归属拆分:
- 宿主文本维护在
translations/pluginbased_zh_CN.ts,由app/CMakeLists.txt编译为:/i18n/pluginbased_zh_CN.qm - 插件文本维护在各自目录,例如
plugins/PlayPlugin/translations/PlayPlugin_zh_CN.ts - 插件通过
IAppPlugin::translationResourcePaths(languageName)返回自己的.qm资源路径 PluginManager负责安装和卸载插件 translator,插件不直接依赖宿主 QML 模块
插件 QML 中的可见文本应使用普通 qsTr(...);插件 C++ 中显示到主页卡片的文本应使用 tr(...)。不要为了刷新语言在插件 QML 中导入 PluginBased 1.0 或绑定 AppController.currentLanguage。
实现 IAppPlugin 通用插件接口,编译为动态库并在根目录 plugins.json 中启用后,即可从构建或发布包的 plugins/ 目录加载。卡片面板会在 pluginsReady 信号触发后动态渲染插件卡片。
宿主只负责插件加载、生命周期和页面路由;播放器、转码器或其他具体业务能力都由插件内部实现。当前 PlayPlugin 是一个自包含播放器插件,内部包含播放桥、音视频渲染、播放列表模型和 QML 界面;解封装、解码、硬解后端和 runtime 播放调度逐步收敛到 sdk/。
播放链路按“控制事件”和“帧数据”拆分:
sdk/media_core负责打开媒体、FFmpeg demux/decode、VideoToolbox 后端、帧格式封装和media_sdk::Player控制入口。media_sdk::IEventSink只传递控制事件,例如媒体信息、状态、seek 完成、进度、EOF 和错误。media_sdk::IDecodeFrameSink只传递解码后的音频/视频帧,返回结构化推帧状态,区分 accepted、backpressured、stale generation、cancelled 和 closed。plugins/PlayPlugin/src/playback/QtPlaybackAdapter是 Legacy Qt 播放链路的 SDK 边界,把 SDK frame sink 推入现有 Qt 帧队列,并把控制事件 marshal 回 Qt 对象线程。plugins/PlayPlugin/src/playback/PlaybackDataBridge管理 session/generation 过滤、可取消背压、EOF drain 和数据通道诊断。seek 取消和真正 stop/abort 使用不同状态,避免把旧 generation 唤醒误报为解码错误。sdk/media_playback_runtime提供 runtime 侧 A/V 时钟、渲染帧队列背压、present tracking、native fallback 和 mock player 测试覆盖。sdk/media_platform_audio_macos使用 AudioUnit/CoreAudio 输出音频,实时回调路径使用专用 ring buffer,避免在音频回调中走阻塞锁。
当前 PlayPlugin 仍保留 Legacy Qt 展示链路,SDK runtime 可作为后续切换目标;插件层只做展示、QML 状态和 Qt RHI/Surface 对接,不再依赖通过 PlayerEvent 发送音视频帧。
| 类型 | 所属模块 | 注册方式 | QML 用法 |
|---|---|---|---|
AppController |
PluginBased 1.0 |
QML_SINGLETON |
AppController.quit() |
PluginManager |
PluginBased 1.0 |
QML_SINGLETON |
PluginManager.pluginCount |
PlayerEngine |
PlayPlugin 1.0 |
QML_ELEMENT |
PlayerEngine { } |
PlaylistModel |
PlayPlugin 1.0 |
QML_ELEMENT |
PlaylistModel { } |
FFmpegSurface |
PlayPlugin 1.0 |
QML_ELEMENT |
播放器视频输出项 |
PlaybackContext |
PlayPlugin 1.0 |
QML_SINGLETON |
插件内部共享播放上下文 |
MediaInfo |
PlayPlugin 1.0 |
QML_UNCREATABLE |
只读,由 C++ 创建 |
| 平台 | 机制 | 产物 |
|---|---|---|
| Windows | SetUnhandledExceptionFilter + MiniDumpWriteDump |
dumps/crash_YYYYMMDD_HHMMSS.dmp |
| Linux / macOS | signal(SIGSEGV/SIGABRT) + backtrace |
dumps/crash_YYYYMMDD_HHMMSS.log |
IAppPlugin 是宿主加载插件的唯一接口,负责元信息、生命周期、主页卡片和可选 QML 页面。插件可以在内部实现自己的业务能力;宿主不会为播放器、转码器或其他具体领域定义专用插件接口。
推荐使用可视化生成器创建插件骨架:
cmake --build build --target PluginGeneratorApp --parallel
./build/tools/plugin_generator/PluginGeneratorApp在界面中填写插件名、显示名、描述、文字图标、可选图片图标和输出目录,并选择插件类型。选择图片图标时,生成器会把图片复制到插件 assets/ 目录并嵌入 Qt resource;未选择图片时会使用文字图标作为卡片 fallback。
生成器会默认生成插件内置多语言结构:
translations/<PluginName>_zh_CN.tsqt_add_translations(<PluginName> ... RESOURCE_PREFIX "/<PluginName>/i18n")translationResourcePaths()override- C++ 卡片标题/描述使用
tr(...) - QML 可见文本使用
qsTr(...)
| 类型 | 说明 |
|---|---|
| 带 QML 页面 | 生成 qml/<PluginName>View.qml,首页卡片点击后会打开插件页面 |
| No-QML 后台插件 | 只生成 C++ 插件骨架,不提供可打开页面 |
生成后的目录复制或直接生成到 plugins/ 下即可。项目通过根目录 plugins.json 控制哪些插件参与构建和运行时加载;新增插件默认不会被编译或加载,确认要启用后把插件目录名加入清单,再重新配置/构建。
{
"plugins": [
"DummyPlugin",
"PlayPlugin",
"MyPlugin"
]
}手工创建插件时需要保持以下约定:
- 在
plugins/下新建目录,CMakeLists.txt使用add_library(MyPlugin MODULE ...) - 将插件目录名加入根目录
plugins.json - 插件继承
IAppPlugin,在类声明中加入:Q_OBJECT Q_INTERFACES(IAppPlugin) Q_PLUGIN_METADATA(IID IAppPlugin_IID FILE "MyPlugin.json")
- 插件实现元信息、生命周期和可选 QML 页面:
QString id() const override { return "my-plugin"; } QString name() const override { return "MyPlugin"; } QString version() const override { return "1.0.0"; } QString description() const override { return tr("我的插件"); } bool initialize() override; void shutdown() override; bool hasQmlUI() const override { return true; } QUrl qmlComponentUrl() const override { return QUrl(QStringLiteral("qrc:/MyPlugin/qml/MyPluginView.qml")); } QStringList translationResourcePaths(const QString& languageName) const override;
QStringList MyPlugin::translationResourcePaths(const QString& languageName) const { if (languageName == QStringLiteral("zh_CN")) return { QStringLiteral(":/MyPlugin/i18n/MyPlugin_zh_CN.qm") }; return {}; }
- 编写插件元数据文件:
{ "IID": "com.pluginbased.IAppPlugin/1.0", "MetaData": { "schemaVersion": 1, "apiVersion": 1, "abiVersion": 2, "id": "my-plugin", "name": "MyPlugin", "version": "1.0.0", "description": "我的插件", "hasQml": true } } - 在插件
CMakeLists.txt中编译插件自己的翻译资源:qt_add_translations(MyPlugin TS_FILES "${CMAKE_CURRENT_SOURCE_DIR}/translations/MyPlugin_zh_CN.ts" RESOURCE_PREFIX "/MyPlugin/i18n" )
- 编译后插件产物和同名 JSON 放入
build/plugins/,PluginManager::loadAll()启动时只加载根目录plugins.json中列出的插件。
宿主会在实例化动态库前校验插件 JSON:IID、schemaVersion、apiVersion、abiVersion、id、name、version、description、hasQml 都必须有效;name 必须与 plugins.json 中的插件名一致。apiVersion 或 abiVersion 与宿主不匹配时,插件会被拒绝加载,但不会影响其他插件。
参考实现:plugins/DummyPlugin/