Compare commits

...
59 Commits
Author SHA1 Message Date
BoatingCloud c49631d857 docs: record diagnostic metadata progress 2026-09-02 21:12:35 +08:00
BoatingCloud ecaad47c23 docs: record mpv9 emby a1 progress 2026-09-02 20:36:33 +08:00
BoatingCloud 1b24aebe77 fix: pass emby tls ca bundle to core 2026-08-31 23:34:32 +08:00
BoatingCloud 67ed15e5d4 docs: record emby runtime validation 2026-08-31 04:12:26 +08:00
BoatingCloud 6064d0e05a docs: record alpha interaction validation 2026-08-31 02:46:50 +08:00
BoatingCloud 246653e2e1 docs: record alpha resume and settings validation 2026-08-31 01:39:26 +08:00
BoatingCloud 5d411509a5 docs: record alpha diagnostics validation 2026-08-31 01:01:54 +08:00
BoatingCloud 7afc6919ce docs: record Emby alpha foundation 2026-08-30 23:47:27 +08:00
BoatingCloud cfa8fa3cef docs: record Emby credential validation 2026-08-30 22:32:00 +08:00
BoatingCloud c8b8e76705 docs: record Emby phase 8.1 foundation 2026-08-30 21:31:28 +08:00
BoatingCloud f67cf6ce00 docs: record Emby foundation build 2026-08-30 19:53:48 +08:00
BoatingCloud 6e06941fec docs: record Emby API inventory phase 2026-08-30 19:47:18 +08:00
BoatingCloud afad6a40fe docs: record http playback and release packaging 2026-08-30 17:50:58 +08:00
BoatingCloud f51544026b docs: record csr format correction 2026-08-30 16:19:46 +08:00
BoatingCloud 471c9dbb6c docs: record detail ui and csr 2026-08-30 16:03:32 +08:00
BoatingCloud a06e098787 docs: preserve daily tmdb history 2026-08-30 15:24:03 +08:00
BoatingCloud c3a4f9469a docs: record tmdb live emulator verification 2026-08-30 15:19:38 +08:00
BoatingCloud 8cae735e3c docs: record tmdb detail completeness 2026-08-30 14:05:35 +08:00
BoatingCloud 82325ddfa9 docs: record search pagination 2026-08-30 13:57:28 +08:00
BoatingCloud 9c8e679c94 docs: record tmdb edge fixes 2026-08-30 13:53:10 +08:00
BoatingCloud fe1935c3b3 docs: record tmdb data integration 2026-08-30 13:40:56 +08:00
BoatingCloud 8f58fad5cd docs: record scene continuity redesign 2026-08-30 08:51:00 +08:00
BoatingCloud 3bcdcfd2a8 docs: clarify navigation ambient tint 2026-08-30 08:35:40 +08:00
BoatingCloud e2f4fbd831 docs: record navigation contrast adjustment 2026-08-30 08:32:37 +08:00
BoatingCloud 29cd001796 docs: record immersive home redesign 2026-08-30 08:05:41 +08:00
BoatingCloud e34a718c48 docs: record fixed home actions 2026-08-30 07:53:36 +08:00
BoatingCloud f1f0484eb4 docs: record vui1 completion 2026-08-30 07:31:18 +08:00
BoatingCloud 32422c4e06 docs: record phase8 ui foundation status 2026-08-29 21:31:02 +08:00
BoatingCloud 7f3d2e0747 docs: record phase7 sdk integration status 2026-08-29 19:55:47 +08:00
BoatingCloud 14149854b8 feat: package core sdk and integrate velix player app 2026-08-29 19:52:38 +08:00
BoatingCloud e594f66c35 perf: complete phase6 playback benchmark baseline 2026-08-29 18:56:22 +08:00
BoatingCloud c6f267d6dd feat: complete phase5 playback network contract 2026-08-29 16:49:38 +08:00
BoatingCloud 49b078737b docs: record phase5 remote push 2026-08-29 15:42:58 +08:00
BoatingCloud f9d1481fb8 feat: unify phase5 playback lab with stable core api 2026-08-29 15:42:26 +08:00
BoatingCloud 0db086a1e9 docs: refresh phase5 artifact record 2026-08-29 14:57:48 +08:00
BoatingCloud d7cf273432 fix: close phase5 playback contract gaps 2026-08-29 14:55:57 +08:00
BoatingCloud 8a30e06621 feat: add phase5 playback and network capability 2026-08-29 14:34:50 +08:00
BoatingCloud e4bc586d77 docs: pin final FFmpeg-Harmony branch 2026-08-29 13:43:44 +08:00
BoatingCloud 5a50baf26a docs: pin final FFmpeg-Harmony tip 2026-08-29 13:41:36 +08:00
BoatingCloud d84fd47f9b docs: pin current FFmpeg-Harmony tip 2026-08-29 13:40:31 +08:00
BoatingCloud 09de481b03 build: isolate FFmpeg-Harmony MPV output prefix 2026-08-29 13:38:17 +08:00
BoatingCloud 329728ce56 docs: record Phase 4.5 fork status 2026-08-29 13:35:35 +08:00
BoatingCloud 46582ed8bd build: add FFmpeg-Harmony prefix switch 2026-08-29 13:32:57 +08:00
BoatingCloud e5f6c0ced2 docs: normalize Phase 4 status 2026-08-29 12:43:49 +08:00
BoatingCloud e1bc8cc52f docs: add Phase 4 OHCodec status 2026-08-29 12:43:14 +08:00
BoatingCloud c239b1c186 docs: record Phase 4 OHCodec validation 2026-08-29 12:41:34 +08:00
BoatingCloud 1f1a6e5962 docs: record OHCodec interop boundaries 2026-08-29 12:36:33 +08:00
BoatingCloud 37df87fc56 feat: surface OHCodec diagnostics in Playback Lab 2026-08-29 12:36:20 +08:00
BoatingCloud 3f6132637b feat: add Harmony OHCodec hwdec adapter 2026-08-29 12:36:14 +08:00
BoatingCloud 9fdf08073e docs(harmony-audio): record implementation and validation 2026-08-29 10:07:29 +08:00
BoatingCloud dfda10b6d8 harmony: playback-lab: expose audio diagnostics 2026-08-29 10:07:25 +08:00
BoatingCloud 9941ad1da9 harmony: meson: enable OHAudio backend on OHOS 2026-08-29 10:07:19 +08:00
BoatingCloud 7021f883cb harmony: audio: add OHAudio output backend 2026-08-29 10:07:13 +08:00
BoatingCloud c650a4214d docs(playback-lab): record emulator verification 2026-08-29 09:06:04 +08:00
BoatingCloud d4c84601fa feat(playback-lab): add EGL and libmpv playback bridge 2026-08-29 09:05:47 +08:00
BoatingCloud 2356e2f67b feat(playback-lab): add isolated HarmonyOS app shell 2026-08-29 09:05:42 +08:00
BoatingCloud 75ed9dd2f4 build: complete HarmonyOS arm64 libmpv phase 1 2026-08-28 19:08:53 +08:00
BoatingCloud f178d38ce6 docs: record Velix remote repository 2026-08-28 18:36:36 +08:00
BoatingCloud 2c469d4299 docs: complete HarmonyOS phase 0 research 2026-08-28 15:47:20 +08:00
140 changed files with 9831 additions and 1 deletions

No files matched your search

+4
View File
@@ -1,3 +1,7 @@
/subprojects/*
!/subprojects/packagefiles
!/subprojects/*.wrap
/build/
/dist/
.hvigor/
.DS_Store
+22
View File
@@ -0,0 +1,22 @@
# Phase 6 Benchmark 方法
## 执行
```sh
python3 benchmark/runner/run_emulator.py startup_local --repeat 20
python3 benchmark/runner/run_emulator.py seek_local --repeat 50
python3 benchmark/runner/run_emulator.py stress_lifecycle --repeat 100
python3 benchmark/runner/run_emulator.py play_pause --repeat 200
```
runner 通过 hdc/uitest 驱动 `Playback Lab`,每次 load 使用内置 `sample.mp4`,记录 timeline、错误、状态和进程采样。startup 的首轮标为 cold-ish,后续为 warm;OS page cache 未被强制清空。seek 使用加载后立即触发的原生 `+10s`,避免短样片在输入框多次 keyEvent 期间自然结束。
每个场景输出逐次 JSON 和 min/max/avg/median/P90/P95。百分位采用排序后的 nearest-rank;负时间线值是 unavailable,不参与统计。错误单独保留,不从成功样本中删除。
## 采样边界
- VmRSS、VmSize、Threads、fd count 和原始 CPU jiffies 来自 `/proc`;PSS、精确 native heap、可靠 CPU permille 未被模拟器公开,输出 null/原始值。
- host wall time 包含 hdc 往返,只用于发现自动化超时,不作为启动性能结论。
- `firstAudioCallbackMs` 是 Playback Lab 诊断轮询观察到 callback_count>0 的时间;要取得 callback 内精确时间需独立 tracing,因此不把它当作硬件时钟证据。
- `stress_lifecycle` 覆盖 load/stop;release/recreate 需要新 XComponent surface,另行作为实体设备/手工场景。
- HTTPS、HLS、鉴权、网络字幕必须使用受控 fixture;本轮没有把公网失败当作性能数据。
+7
View File
@@ -0,0 +1,7 @@
# Buffering Model
buffering 是 MPV `cache-buffering` 属性与网络状态的边沿诊断,不是 UI 的瞬时文案。进入时记录 `bufferingEnterCount` 和起始 monotonic 时间;退出时累加 `bufferingTotalMs`,并更新 `bufferingMaxSingleMs`。重复的同值属性通知不会重复计数。
网络快照同时保留 protocol、host、HTTP status、redirect count、cache duration/bytes、current bitrate、range support 和最近 seek result。`networkWaitMs` 只计算已观察到的 network-open 到 first-byte/file-loaded 间隔;file:// 输入为 -1。
本地短片通常在首帧后很快 EOF,因此 buffering 计数为 0 不代表网络流在真实设备上不会重缓冲。受控 HTTP Range fixture 已在 Phase 5 验证;HTTPS/HLS、鉴权和弱网重缓冲应在实体设备用固定服务端脚本复测。
+97
View File
@@ -0,0 +1,97 @@
# Phase 1 Build Issues
## BUILD-001
Component: FFmpeg configure
Command: `scripts/build-ffmpeg-ohos.sh`
Original Error: `Unknown option "--disable-postproc".`
File: `scripts/build-ffmpeg-ohos.sh`
Root Cause: 当前 FFmpeg 已移除独立 postproc 配置项,Phase 0 示例参数过期。
Platform Difference: 无;属于当前 upstream 配置接口变化。
Fix: 删除不存在的 `--disable-postproc`,以当前 `configure --help` 为准。
Files Changed: `scripts/build-ffmpeg-ohos.sh`
Patch Required: Build script only
Result: configure 继续执行。
Regression Risk: Low
## BUILD-002
Component: FFmpeg compiler/linker probe
Command: `scripts/build-ffmpeg-ohos.sh`
Original Error: `ld: library 'System' not found`,随后 `C compiler test failed`。
File: `scripts/build-ffmpeg-ohos.sh`、`ffbuild/config.log`
Root Cause: `--sysroot` 不会自动改变 Clang target;链接探针仍按 macOS 目标寻找 `libSystem`。
Platform Difference: macOS arm64 与 OHOS arm64 架构相同,但 ABI、对象格式和系统库不同。
Fix: 同时向 CFLAGS 和 LDFLAGS 显式传入 `--target=aarch64-linux-ohos`。
Files Changed: `scripts/build-ffmpeg-ohos.sh`
Patch Required: Build script only
Result: FFmpeg configure、build、install 成功,产物为 AArch64 ELF。
Regression Risk: Low
## BUILD-003
Component: FreeType zlib resolution
Command: `scripts/build-font-stack-ohos.sh`
Original Error: `ERROR: Subproject zlib is buildable: NO`,Meson 尝试下载 WrapDB zlib。
File: `scripts/build-font-stack-ohos.sh`
Root Cause: FreeType 的 `zlib=external` 表示构建 fallback subproject,不表示使用外部系统库。
Platform Difference: SDK 已提供 target `libz.so`,不应另行下载 target zlib。
Fix: 使用当前 FreeType 定义的 `zlib=system`,通过隔离 `.pc` 指向 SDK AArch64 zlib。
Files Changed: `scripts/build-font-stack-ohos.sh`
Patch Required: Build script only
Result: FreeType 2.14.3 静态库构建成功。
Regression Risk: Low
## BUILD-004
Component: Meson cross-file pkg-config resolution
Command: `scripts/build-font-stack-ohos.sh`
Original Error: HarfBuzz 报 `Dependency 'freetype2' is required but not found`,包装器被解析到 HarfBuzz 源码目录。
File: `toolchains/ohos-api24-arm64.ini`
Root Cause: `@GLOBAL_SOURCE_ROOT@` 指向当前被配置项目的 source root,不固定指向 Velix-MPV-Core。
Platform Difference: 多个独立 Meson 源码树共享一个 target prefix。
Fix: 使用以 Cross File 自身为基准的 `@DIRNAME@/../` 定位包装器与 prefix。
Files Changed: `toolchains/ohos-api24-arm64.ini`
Patch Required: Cross File only
Result: FreeType、HarfBuzz、libass 和 MPV 均只从 OHOS prefix 解析依赖。
Regression Risk: Low
+64
View File
@@ -0,0 +1,64 @@
# HarmonyOS Native Build Strategy
## Scope
Phase 1 只构建 HarmonyOS API 24 arm64 `libmpv`。不包含播放器 UI、XComponent 集成、`ao_harmony`、MPV OHCodec backend、性能优化、HDR 或真机运行声明。
## Toolchain
| Item | Value |
|---|---|
| SDK | `/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony` |
| Version | 6.1.1.125 / API 24 |
| Target | `aarch64-linux-ohos` |
| Sysroot | `<SDK>/native/sysroot` |
| Compiler | OHOS Clang 15.0.4 |
| Meson | 1.9.0,仓库隔离 venv |
| Ninja | SDK 1.12.0 |
| pkgconf | 3.0.6,仓库隔离 host tool |
| Cross File | `toolchains/ohos-api24-arm64.ini` |
| Prefix | `build/ohos-arm64/prefix` |
`scripts/setup-ohos-env.sh` 清空 `CPATH`、`C_INCLUDE_PATH`、`CPLUS_INCLUDE_PATH`、`LIBRARY_PATH`、`CMAKE_PREFIX_PATH`,并将 pkg-config 限制到 OHOS prefix。target 编译和链接必须同时带 `--target` 与 `--sysroot`。
## Reproducible Build
```sh
scripts/build-all-ohos.sh
```
顺序固定为:host tools → pinned sources → C/C++ probes → SDK zlib metadata → OpenSSL → FFmpeg → FreeType/FriBidi/HarfBuzz/libass → libplacebo → MPV → 全量 ABI/污染/符号校验。
各步骤也可单独执行:
```sh
scripts/verify-toolchain.sh
scripts/build-openssl-ohos.sh
scripts/build-ffmpeg-ohos.sh
scripts/build-font-stack-ohos.sh
scripts/build-libplacebo-ohos.sh
scripts/build-mpv-ohos.sh
scripts/verify-ohos-prefix.sh
```
所有下载源码按 `scripts/fetch-phase1-sources.sh` 的 tag/commit 固定。构建目录可删除后重建,禁止从 Homebrew 或 macOS prefix 提供 target library。
## MPV Configuration
MPV 使用 `-Dauto_features=disabled`,再显式启用 `libmpv`、GPL、`plain-gl`、OpenGL、vector、zlib。CLI、测试、脚本语言、桌面后端、平台硬件加速 backend 和 Vulkan 均关闭。实际 feature 列表以 `DOCS/MPV_BUILD_REPORT.md` 和 `build/ohos-arm64/logs/mpv.log` 为准。
MPV 源码仅增加明确的 `host_machine.system() == 'ohos'` 检测与 `ohos` feature。POSIX 路径继续复用现有 threads、timer、path、poll、IPC、subprocess 实现;这些能力当前只能标记 compile verified。
## Output and Gates
主产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`。
`scripts/verify-ohos-prefix.sh` 必须同时证明:
- 所有 prefix 静态库成员为 AArch64 ELF;
- `libmpv` 为 AArch64 ELF shared object;
- 动态依赖仅来自 HarmonyOS SDK allowlist(启用 `harmony-audio` 时包含 `libohaudio.so`);
- `.pc`/CMake/Ninja target metadata 无 Homebrew、`/usr/local`、`.dylib` 或 Apple framework;
- Client API 与 OpenGL Render API 必需符号存在。
真机行为、CA store、XComponent/EGL 生命周期及 OHCodec buffer 语义留到后续阶段验证。
+11
View File
@@ -0,0 +1,11 @@
# Core API versioning
当前冻结版本:`VELIX_CORE_API_VERSION = 0x000100`(v0)。
- `MAJOR` 变化表示 ABI/API 不兼容,必须升级主 App 适配层。
- `MINOR` 变化只增加尾部字段或能力,旧调用方保持可用。
- `PATCH` 变化只修复实现,不改变公开结构布局。
版本宏、`velix_core_get_api_version()`、`velix_core_get_version()` 和 `velix_core_get_build_info()` 位于 `include/velix/core.h`,C ABI 使用 opaque handle 与固定宽度整数。变更 API 时同步更新 `metadata/build-info.json`、此文档与集成文档,并在发布前检查动态符号。
v0 不把 `mpv_handle`、`mpv_node`、FFmpeg、OHCodec、EGL、OHAudio 或 OHNativeWindow 类型带出 SDK。
+27
View File
@@ -0,0 +1,27 @@
# Velix Core SDK
Phase 7 SDK v0 以 `include/velix/core.h` 与 `libvelix_core_binding.so` 为产品边界,主 App 不编译 MPV/FFmpeg 源码。
## 目录
```text
velix-core/
├── include/velix/core.h
├── include/mpv/harmony_playback.h
├── lib/arm64-v8a/libvelix_core_binding.so
├── lib/arm64-v8a/libmpv.so.2
├── lib/arm64-v8a/libc++_shared.so
├── metadata/build-info.json
└── licenses/
```
`libvelix_core_binding.so` 仅提供产品生命周期、控制、状态、媒体、音轨、字幕、指标与 build-info NAPI;XComponent 的 OHNativeWindow/EGL/mpv render context 仍由 Core 内部实现。主 App 只能看到 `PlaybackEngine`/`PlaybackController` 的业务类型。
默认 profile 为 software decode,OHCodec GL mapper 保持关闭。SDK 产物位于 `dist/velix-core`,已在 `.gitignore` 中忽略,禁止把 34MB `libmpv` 二进制提交到 Git。
构建与 staging:
```sh
./scripts/package-velix-core-sdk.sh --profile default
../Velix/scripts/build-velix-app.sh --release-core --build-mode debug
```
+81
View File
@@ -0,0 +1,81 @@
# FFmpeg Strategy
## 决策
Phase 1 使用独立构建的 **官方 FFmpeg upstream**,不把源码揉进 MPV,也暂不建立 `Velix-FFmpeg-Harmony`。
研究基线:FFmpeg master `3acec0a1af2dda0a0838689b8b8649e7deb080a0`(2026-08-28)。该 commit 已提供 OpenHarmony `ohcodec` hwcontext/pixel format 以及 H.264/HEVC decoder/encoder,显著降低自行 fork 的必要性。
## MPV 依赖约束
MPV 当前 Meson 最低要求:
| Library | 最低版本 |
|---|---:|
| libavcodec | 60.31.102 |
| libavfilter | 9.12.100 |
| libavformat | 60.16.100 |
| libavutil | 58.29.100 |
| libswresample | 4.12.100 |
| libswscale | 7.5.100 |
必须同时生成 pkg-config 文件供 MPV cross build 使用。
## 两种方案评估
### A. 官方 FFmpeg 独立构建(采用)
- 独立源码目录与安装前缀。
- arm64 OHOS toolchain 交叉编译。
- Phase 1 首先保留 software decode、demux、HTTP/HLS、字幕相关基础;OHCodec 可编译但不要求首次 MPV 构建即启用硬解。
- TLS 使用 FFmpeg 已支持并经过许可审核的第三方 backend(优先 OpenSSL);Harmony `libnet_ssl.so` 不能直接替代。
- 优点:上游同步清晰、已有 OHCodec、MPV 仓库 patch 最小。
### B. Velix-FFmpeg-Harmony fork(暂不采用)
只有满足以下任一条件才触发:
1. 官方 OHCodec 在 API 24 上存在阻断编译/运行问题,且修复无法及时 upstream。
2. 需要 OHNativeBuffer/GPU import 或 HDR metadata 等实质性 FFmpeg API 扩展。
3. 网络/TLS 或 Harmony 生命周期需要不可在外部构建脚本解决的源码 patch。
建立后必须同样保留完整历史、配置 `upstream`、小 patch 分支,并单独维护许可证审计。
## Phase 1 建议配置轮廓
实际参数必须先由交叉编译探针和 FFmpeg `configure --help` 固化;以下是预计命令,不是已成功结果:
```sh
./configure \
--prefix="$FFMPEG_PREFIX" \
--target-os=linux \
--arch=aarch64 \
--cc="$OHOS_CLANG --target=aarch64-linux-ohos --sysroot=$OHOS_SYSROOT" \
--cxx="$OHOS_CLANGXX --target=aarch64-linux-ohos --sysroot=$OHOS_SYSROOT" \
--ar="$OHOS_LLVM_AR" \
--strip="$OHOS_LLVM_STRIP" \
--enable-cross-compile \
--enable-pic \
--enable-shared \
--disable-programs \
--disable-doc \
--enable-network \
--enable-openssl
```
注意:FFmpeg 当前 configure 未发现独立 `target-os=ohos` 结论,是否应使用 `linux` 必须由真实配置日志决定;编译器会同时定义 `__OHOS__` 与 `__linux__`。`--enable-ohcodec` 在首次软件解码 build 可暂缓,第二个受控构建再开启并验证其五个 Native media 库。
## 网络与 TLS
- HTTP/HLS 主要由 libavformat protocol/demuxer 提供,不新增 MPV 自定义网络栈。
- socket/DNS 可直接使用 OHOS libc。
- HTTPS 需要 FFmpeg 支持的 TLS backend。首选独立交叉构建 OpenSSL,并固定 CA bundle/系统证书策略;证书路径、SNI、代理、IPv6、重定向、Range 请求必须真机验证。
- 禁止将“SDK 有 `libnet_ssl.so`”写成“FFmpeg HTTPS 已支持”。
## 验收与 fork 决策点
1. `configure` 识别 arm64 OHOS、pthreads、network、dlopen。
2. 所需 FFmpeg libraries 完整生成,pkg-config 不泄漏宿主机路径。
3. software decode + HTTP/HLS 单测通过。
4. `--enable-ohcodec` 能链接 API 24 SDK,并枚举 `h264_oh` / `hevc_oh`。
5. 任何源码 patch 先形成最小 diff、根因、upstream 可接受性评估,再决定是否 fork。
+102
View File
@@ -0,0 +1,102 @@
# HarmonyOS OHAudio Backend
状态:Phase 3 CODE COMPLETE / DEVICE VALIDATION PENDING。
代码、OHOS arm64 交叉编译和模拟器 H.264/AAC smoke test 已完成。实体设备上的真实输出路由、延迟、A/V sync 精度、蓝牙/耳机、多声道和长时间稳定性仍未宣称完成。
## Architecture
```text
MPV decoder/filter
↓
MPV AO buffer (audio/out/buffer.c)
↓ pull callback
audio/out/ao_harmony.c
↓
OH_AudioRenderer write callback
↓
HarmonyOS audio device
```
`ao_harmony` 是 MPV pull AO。PCM 始终在 native 层流动;ArkTS 只负责播放控制和诊断展示,不参与持续 PCM 搬运。
实现文件:
- `audio/out/ao_harmony.c`:AO、OHAudio renderer、callback、时钟和统计。
- `audio/out/ao_harmony.h`:Playback Lab 使用的只读诊断 ABI。
- `audio/out/ao.c`、`meson.build`、`meson.options`:driver 注册和 `harmony-audio` feature。
## OHAudio API
实现以当前 API 24 SDK headers 为准:
- `ohaudio/native_audiostreambuilder.h`
- `ohaudio/native_audiorenderer.h`
- `ohaudio/native_audiostream_base.h`
- `multimedia/native_audio_channel_layout.h`
- 链接库:`libohaudio.so`
Builder 设置 `SamplingRate`、`ChannelCount`、`ChannelLayout`、`SampleFormat`、`EncodingType(RAW)`、`LatencyMode(NORMAL)`、`RendererInfo(USAGE_MOVIE)` 和 write/device/error callbacks,然后 `GenerateRenderer` 并销毁 Builder。Renderer 使用 `Start`、`Pause`、`Stop`、`Flush`、`Release`、`SetVolume`、`GetFrameSizeInCallback`、`GetLatency` 和 `GetAudioTimestampInfo`。
## MPV AO / callback model
`write` 保持 NULL,`start` 触发 OHAudio。OHAudio 回调只计算 frame 数、调用 `ao_read_data(..., pad_silence=true, blocking=false)`,不足部分填静音并更新轻量原子计数;不访问网络、不调用 N-API/ArkTS、不分配大对象、不逐回调打印日志。回调首包仅在 debug 日志中记录一次。
MPV AO buffer 是 producer,OHAudio callback 是 consumer。`ao_read_data` 返回的 PCM 只在当前 callback 内有效,系统提供的 `audioData` 由 OHAudio 所有;没有 AVFrame 或 PCM 指针跨生命周期保存。
## Format / sample rate / channel layout
- 仅接受 PCM;MPV float 优先映射 `AUDIOSTREAM_SAMPLE_F32LE`,创建失败时回退 `AF_FORMAT_S16` / `AUDIOSTREAM_SAMPLE_S16LE`。
- 采样率使用 MPV 请求值,不强制改成 48 kHz;设备拒绝时返回真实 renderer 创建错误,由 MPV 上层重新协商/重采样。
- 单声道映射 `CH_LAYOUT_MONO`,其余首版限制为双声道 `CH_LAYOUT_STEREO`;超过 2 声道先通过 MPV channel map downmix 到 stereo。没有声称 5.1/7.1/Atmos/Dolby 已支持,实体设备能力仍待验证。
模拟器实际日志:请求 48 kHz、5.1、floatp,经 MPV downmix 后创建 48 kHz、2 声道、float32 renderer。
## Pause / resume / stop / seek / flush
- pause:`OH_AudioRenderer_Pause`。
- resume/start:`OH_AudioRenderer_Start`。
- stop:MPV 的 stop/EOF 通过 AO reset/uninit 路径停止 renderer;不把 pause、stop、release 混为 destroy。
- seek/reset:先禁止新 callback,`Stop`,等待 inflight callback 清零,再 `Flush`;清空本地提交/播放 epoch,增加 `audioFlushCount` 与 `seekAudioResetCount`,下一次 `start` 重新开始。
- release:禁止 callback → Stop → 等待 callback → 停止并 join timestamp 线程 → Release renderer;回调不会持有释放后的 player/AO 指针。
所有 OHAudio 返回值都检查;失败通过 MPV AO error/reload 路径处理,使用 `AUDIO_BUILDER_FAILED`、`AUDIO_RENDERER_CREATE_FAILED`、`AUDIO_RENDERER_START_FAILED`、`AUDIO_FLUSH_FAILED`、`AUDIO_RELEASE_ERROR`、`AUDIO_UNSUPPORTED_FORMAT` 等明确日志。
## A/V clock and delay
timestamp 线程每 20 ms 调用 `OH_AudioRenderer_GetAudioTimestampInfo`。`submittedFrames` 在 callback 完成填充后递增;`playedFrames` 使用 renderer frame position 减去本次 Flush 的 timestamp origin,避免设备在 Flush 后继续返回旧 epoch。延迟计算为:
```text
pending = max(submittedFrames - playedFrames, 0)
estimatedDelayMs = rendererLatencyMs + pending * 1000 / sampleRate
```
`audioTimestamp` 保留 OHAudio 的 monotonic timestamp。MPV 播放倍速仍由 MPV 音频滤镜/时钟负责,不叠加 renderer speed。算法已编译并在模拟器观察到稳定计数;绝对 A/V 偏差与设备路由连续性必须实体机测量。
## Volume and underrun
Renderer volume 固定为 unity,最终音量由 MPV 软件 gain 处理,避免双重增益。API 24 当前没有可用的系统 underrun counter,因此不伪造 `systemUnderrun`;callback 返回帧不足时只增加 Velix-side `bufferStarvation`。
## Playback Lab diagnostics
Playback Lab 保持原有 video-only 启动路径,新增 `Audio On/Off`(切换 MPV `aid=auto/no`)和以下 native 状态:`audioBackend`、`sampleRate`、`sampleFormat`、`channels`、`channelLayout`、`submittedFrames`、`playedFrames`、`estimatedAudioDelayMs`、`audioBufferStarvation`、`audioFlushCount`、`seekAudioResetCount`、`audioState`。返回值使用 ArkTS Number,避免 BigInt 让 Inspector 无法序列化。
## Validation
- Audio-enabled:OHOS arm64 `libmpv.so.2.5.0` clean build,DT_NEEDED 新增 `libohaudio.so`,Playback Lab HAP 构建成功。
- Video-only:使用 `-Dharmony-audio=disabled` 的独立 Meson build 编译成功。
- Emulator smoke:H.264/AAC MP4 上确认 renderer create、start、callback PCM consumed、pause、resume、seek/flush、stop、release;示例观察到 `starvation=0`,模拟器日志无新增崩溃。
- 未验证:实体设备、真实 latency/A-V 偏差、蓝牙/有线/扬声器切换、系统 audio focus/interruption、5.1/7.1、30 分钟压力。
## Known limitations and physical TODO
1. 当前仅实现 OHAudio renderer;没有 OHCodec/hwdec、Vulkan、音频焦点业务和设备专属路由策略。
2. `systemUnderrun` 需要 SDK 提供真实接口后再接入;当前仅有 `bufferStarvation`。
3. 多声道真实 capability、采样率拒绝后的设备行为、timestamp 在路由切换后的连续性需要实体设备矩阵。
4. 上层应用负责电话/音频焦点策略;Core 只接受 pause/resume 或 AO reload。
Phase 4 不在本次变更中启动。未来 OHCodec 评估至少需要单独验证 surface/buffer 同步、stride/format、fence 与生命周期、零拷贝/DRM 约束、不同设备实现差异五类风险。
## License
本次仅修改 Velix-MPV-Core 内的 MPV 集成代码和 Playback Lab;MPV 原有 GPL/LGPL 许可保持不变。HarmonyOS SDK 头文件/系统库按 SDK 自身许可使用,没有复制第三方实现源码。
+59
View File
@@ -0,0 +1,59 @@
# HarmonyOS Audio Research
> 状态:IMPLEMENTED(代码、OHOS arm64 编译、Playback Lab 模拟器音频 smoke test 已完成);实体设备输出路由、真实延迟、A/V sync 精度、多声道与长稳仍为 NEED PHYSICAL DEVICE VERIFY。
## 1. 推荐方案
新增 MPV 音频输出 `ao_harmony`,直接接 HarmonyOS OHAudio:
```text
MPV audio pipeline -> native PCM ring/buffer -> OHAudio write callback -> AudioRenderer
```
禁止 PCM 逐块跨 N-API/ArkTS。ArkTS 只发送播放器级控制和生命周期事件。
## 2. SDK 依据
- headers:`ohaudio/native_audiostreambuilder.h`、`native_audiorenderer.h`、`native_audiostream_base.h`、`multimedia/native_audio_channel_layout.h`
- library:`libohaudio.so`
- SDK 依据:当前 API 24 headers;builder 基础接口从 API 10 起,独立 write callback 从 API 12 起,`GetAudioTimestampInfo` 从 API 15 起,`GetLatency` 从 API 23 起。
- builder:采样率、声道数、sample format、encoding、latency mode、channel layout、usage、write callback、callback frame size
- renderer:Start、Pause、Stop、Flush、Release、`GetAudioTimestampInfo`、`SetVolume`、`GetLatency`;API 24 没有可用的系统 underrun counter,Velix 只记录 callback starvation。
API 24 还提供 advanced callback 等新接口,但首版使用最低且稳定的 callback API,避免无必要抬高最低版本。
## 3. 与 MPV AO 的映射
| MPV / 播放需求 | OHAudio 映射 | 设计 |
|---|---|---|
| PCM 格式协商 | `SetSamplingRate`、`SetChannelCount/Layout`、`SetSampleFormat` | `init()` 将 MPV `ao->format` 映射为 OHAudio;不支持时返回 unsupported,让 MPV 重采样 |
| 数据输出 | renderer write callback | 回调只读取 MPV AO buffer,禁止阻塞、分配大对象或调用 ArkTS |
| Pause/Resume | `OH_AudioRenderer_Pause/Start` | 与 MPV `control()` 状态串行化 |
| Stop/Seek | `Stop/Flush/Start` | Seek 时停止 callback、清空 OHAudio、重建 timestamp epoch,避免旧 PCM 泄漏 |
| Volume | `OH_AudioRenderer_SetVolume` | 系统 renderer 固定 unity;最终增益由 MPV 软件音量处理,避免重复增益 |
| Speed | MPV 音频滤镜为主 | 不直接用 Renderer speed 改变 MPV 时钟;系统 speed 仅后续专项验证 |
| A/V Sync | `GetAudioTimestampInfo` | 驱动 delay 由 callback 已提交帧与 timestamp epoch 内的硬件播放位置计算;路由切换仍需实体设备测试 |
| 低延迟 | `SetLatencyMode` | 视频播放默认 normal;fast 仅在设备支持和稳定性测试后启用 |
| 多声道 | channel layout + capability | 先 stereo;5.1/7.1 按设备能力协商,失败回退重混音,不 fake success |
## 4. MPV 接入位置
- 新文件建议:`audio/out/ao_harmony.c`。
- 注册位置:`meson.build` 的 audio outputs 区域和 `audio/out/ao.c` driver list。
- 参考结构:`ao_aaudio.c` 的 callback/pull 模式及状态控制;不能复制其 Android API。
- OHAudio callback 与 MPV AO buffer 的线程安全遵循 `audio/out/buffer.c` 现有模型;回调只消费系统请求的 frame 数。
## 5. 格式策略
首版候选优先级:float32(若稳定支持)→ signed 16-bit。采样率优先内容原始值,设备拒绝则让 MPV/libswresample 转为 48 kHz。声道首版 2.0,随后验证 5.1/7.1。所有实际支持值通过 builder 创建结果与设备行为确认,不从枚举存在推断硬件支持。
## 6. 风险与验证
1. write callback 的实时线程约束和最大执行时间。
2. `GetTimestamp` 在暂停、Flush、蓝牙/有线切换后的连续性。
3. underrun、首包预填充与首音频时间。
4. Surface 暂停但音频继续、应用后台、音频焦点中断。
5. 播放速度与 MPV 时钟双重调整风险。
6. 多声道 layout 枚举和实际设备 route 不一致。
代码验收:PCM 正常播放;pause/resume/seek/flush/release 生命周期已在模拟器通过;starvation 与 timestamp metrics 已接入。1 小时稳定性、真实 underrun、A/V sync 偏差、路由与多声道仍需实体设备,不宣称低延迟或多声道完成。
+65
View File
@@ -0,0 +1,65 @@
# HarmonyOS Native Capability Research
## 1. 取证基线
- SDK:`/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony`
- Native / Toolchains:`6.1.1.125`,API 24
- 目标:`aarch64-linux-ohos`
- 编译器:OHOS Clang 15.0.4
- 最小头文件探针已通过:pthread、dlopen、mmap、epoll、poll、socket、DNS、EGL、GLES3、Vulkan、XComponent、OHAudio、Native AVCodec 可被同一 C translation unit 包含。
- 编译器预定义:`__OHOS__`、`__linux__`、`__unix__`、`__aarch64__`、`__LP64__`。EGL 平台头使用 `OHOS_PLATFORM` 选择 `OHNativeWindow *`,构建时必须显式定义该宏。
以下“最低 API”来自本机 API 24 SDK 头文件的 `@since` / availability;运行行为仍需 Phase 1/2 真机验证。
## 2. 能力矩阵
| 能力 | C/C++ API / header | library | 最低 API | 生命周期与限制 | 结论 |
|---|---|---|---:|---|---|
| ArkUI Surface | `OH_NativeXComponent_RegisterCallback`;`OnSurfaceCreated/Changed/Destroyed`;`ace/xcomponent/native_interface_xcomponent.h` | `libace_ndk.z.so` | 8 | `window` 只在组件 Surface 生命周期内有效;销毁前停止渲染并释放 EGL/Vulkan surface | 可用 |
| Native Window | `OHNativeWindow`;`native_window/external_window.h` | `libnative_window.so` | 8 起,部分新 buffer API 23 | XComponent 回调交付 window;不得替换成 Android `ANativeWindow` | 可用 |
| Native Buffer | `OH_NativeBuffer_*`;`native_buffer/native_buffer.h` | `libnative_buffer.so` | 11 起,具体函数不同 | 用于后续互操作;格式、usage、fence 必须真机查询 | 可用,Phase 0 不承诺零拷贝 |
| OpenGL ES | `<GLES2/gl2.h>`、`<GLES3/gl3.h>` | `libGLESv2.so` / `libGLESv3.so` | SDK 基础能力 | 实际 GLES 版本、扩展、10-bit/HDR 格式按设备查询 | 可用 |
| EGL | `eglGetDisplay`、`eglCreateWindowSurface`、`eglSwapBuffers`;`<EGL/egl.h>` | `libEGL.so` | SDK 基础能力 | `EGLNativeWindowType` 在 `OHOS_PLATFORM` 下是 `OHNativeWindow *`;上下文线程归属必须固定 | 可用,首选渲染入口 |
| Vulkan | `VK_OHOS_surface`、`vkCreateSurfaceOHOS`;`<vulkan/vulkan.h>` / `vulkan_ohos.h` | `libvulkan.so` | 10 | Surface 绑定 `OHNativeWindow`;扩展和 format 支持需设备枚举 | 可用,但不是 libmpv 公共 Render API |
| VSync | `OH_NativeVSync_*`;`native_vsync/native_vsync.h` | `libnative_vsync.so` | 9 | 回调与 Surface 生命周期协调 | 可用,首版可先由 EGL swap/MPV 调度 |
| Native Audio | `OH_AudioStreamBuilder_*`、`OH_AudioRenderer_*`;`ohaudio/*.h` | `libohaudio.so` | 10 | builder 生成 renderer 后销毁;renderer 使用写回调拉取 PCM;Start/Pause/Stop/Flush/Release 有明确状态机 | 可用,需新增 AO |
| Native Video Decoder | `OH_VideoDecoder_*`;`multimedia/player_framework/native_avcodec_videodecoder.h` | `libnative_media_vdec.so`、`libnative_media_codecbase.so`、`libnative_media_core.so`、`libnative_window.so` | 9;buffer 新接口 11 | Create→Configure→SetSurface→Prepare→Start;Seek 需要 Flush 后重启;输出可直接 RenderOutputBuffer 到 Surface | 可用 |
| Codec Capability | `OH_AVCodec_GetCapability*`、`OH_AVCapability_*`;`native_avcapability.h` | Native media libs | 10 | 必须在目标设备查询硬件/软件、尺寸、帧率、profile/level;SDK 头不能证明某机型支持 4K/10-bit/HDR | 可用,能力值待真机 |
| POSIX file | `open/read/write/stat`;`fcntl.h`、`unistd.h`、`sys/stat.h` | libc | 基础 | 受应用沙箱与授权 URI 限制;ArkUI picker URI/FD 需桥接到 native | 可用但路径权限需集成层处理 |
| Packaged rawfile | `OH_ResourceManager_*` / `rawfile/*.h` | `librawfile.z.so` | 8 起 | 只适合包内资源,不是通用媒体文件系统 | 可用,不替代 POSIX file |
| pthread | `pthread_create`、mutex/cond;`pthread.h` | libc | 基础 | MPV `threads-posix.c` 可优先复用;线程名函数需 Meson 实测 | 可复用 |
| socket | BSD sockets;`sys/socket.h`、`netinet/in.h` | libc | 基础 | 需要网络权限;移动网络切换、IPv6、代理需真机测试 | 可复用 |
| DNS | `getaddrinfo/freeaddrinfo`;`netdb.h` | libc | 基础 | DNS/网络切换错误传播由 FFmpeg 网络层处理 | 可复用 |
| Native HTTP | `OH_Http_*`;`network/netstack/net_http.h` | `libnet_http.so` | 20 | 与 FFmpeg AVIO 不直接兼容;不作为首版 MPV 网络栈 | API 24 可用但不采用 |
| TLS certificate verification | `OH_NetStack_CertVerification` 等;`network/netstack/net_ssl/net_ssl_c.h` | `libnet_ssl.so` | 11 | 这是证书链验证接口,不是 FFmpeg TLS transport backend;首版需交叉构建 FFmpeg 支持的 TLS 库 | 系统验证能力可用,MPV 路径需第三方 TLS |
| Shared library | `dlopen/dlsym/dlclose`;`dlfcn.h` | `libdl.a` / libc | 基础 | 插件路径和符号可见性受沙箱约束 | 编译可用,运行待测 |
| Timer | `clock_gettime`、`nanosleep`;`time.h` | libc | 基础 | MPV `timer-linux.c` 实际只依赖这两个 POSIX API;源码应改名/条件化以免平台语义错误 | 可复用实现,需整理检测 |
| Memory mapping | `mmap/munmap`;`sys/mman.h` | libc | 基础 | 文件映射权限及大文件压力需真机测试 | 编译可用 |
| poll / epoll | `poll`、`epoll_*`;`poll.h`、`sys/epoll.h` | libc | 基础 | 头文件存在且探针通过;运行语义、FD 类型兼容性仍需测 | 可用,优先 POSIX poll |
| System information | `uname`、`sysconf`、`getauxval`;`sys/utsname.h`、`unistd.h`、`sys/auxv.h`;`OH_GetDeviceType` (`deviceinfo.h`) | libc / `libdeviceinfo_ndk.z.so` | API 10 起(DeviceInfo) | 只用于诊断与保守能力探测,不按设备名硬编码功能 | 可用 |
| CPU detection | AArch64 编译宏、`getauxval`;FFmpeg CPU detection | libc | 基础 | arm64 基线已有 NEON;不得未经实测强开额外 ISA | 可复用 |
| Lifecycle | XComponent Surface callbacks + N-API/ArkTS 页面生命周期桥接 | `libace_ndk.z.so` / `libace_napi.z.so` | 8 | MPV Core 仅接受 surface attach/detach、pause/resume 等播放器级事件;业务生命周期留在宿主 | 需薄桥接 |
| Power | 系统 running lock / 后台任务 API主要由应用层管理 | 系统服务 | 版本依 API | Core 不自行申请长期后台权限;向宿主暴露播放状态,由宿主管理屏幕常亮/后台播放 | 不建 MPV backend |
## 3. 直接可复用的 MPV/POSIX 区域
- `osdep/threads-posix.c`:pthread 基础满足。
- `osdep/path-unix.c`、`osdep/language-posix.c`:大部分 POSIX 路径/locale 逻辑可编译,沙箱路径语义需测试。
- `osdep/timer-linux.c` 的实现仅用 `clock_gettime`/`nanosleep`,能力可复用,但构建选择需改为 `ohos` 明确分支或重命名为 POSIX timer。
- socket、DNS、poll、mmap、dlopen 头文件均存在,先通过 feature detection,不因 `__linux__` 自动宣称全部 Linux 特性可用。
- demux、字幕、滤镜、同步、命令和 libmpv client API 与平台无关,可直接复用。
## 4. 必须新增或调整的边界
1. Meson 增加 `host_machine.system() == 'ohos'` / `features['harmony']`,生成 `HAVE_HARMONY`,防止把 OHOS 的 `__linux__` 等同桌面 Linux。
2. 新增 OHAudio AO,不能复用 Android AAudio/OpenSL ES。
3. 若构建 MPV 自管 VO,新增 Harmony EGL/Vulkan context;首版 Playback Lab 可由宿主持有 EGL context,使用 libmpv OpenGL Render API,减少 upstream patch。
4. 硬解需要 MPV 识别 FFmpeg 的 `AV_PIX_FMT_OHCODEC` 与 `AV_HWDEVICE_TYPE_OHCODEC`;该工作晚于软件解码首帧。
5. 日志可先复用 stderr;实验室集成阶段增加 HiLog sink,不能让日志 backend 阻塞首次构建。
## 5. 未验证项
- 各目标设备具体 Vulkan/GLES 扩展、HDR/10-bit swapchain、codec profile/level、最大分辨率/码率。
- `epoll`、`dlopen`、mmap 大文件在应用沙箱内的运行行为。
- 后台播放、音频焦点、路由切换、蓝牙延迟和 Surface 重建行为。
- Native HTTP/TLS 与系统代理/证书策略;首版按 FFmpeg + 独立 TLS 依赖验证。
+7
View File
@@ -0,0 +1,7 @@
# Harmony 字体策略
第一优先级使用系统字体;应用字体只允许由产品明确提供的开源字体;libass 使用 mpv 已配置的字体 provider,不新增商业字体包。当前 OHOS 字体栈按 `scripts/build-font-stack-ohos.sh` 构建:FreeType + HarfBuzz + FriBidi + libass,libass 明确关闭 fontconfig、DirectWrite、CoreText 和 iconv provider,因此不依赖 Linux `fonts.conf`。
Playback Lab 初始化时将 mpv `sub-fonts-dir` 指向 `/system/fonts`;模拟器已确认该目录存在,`/system/etc/fontconfig` 与 `/system/etc/fonts` 不存在。libass 的 `ass_set_fonts_dir` 会从该目录加载字体,`sub-font-provider=auto` 在当前无系统 provider 时仍可使用目录字体和字幕内嵌字体。中文、英文、日文、韩文的缺字由目录字体顺序、libass fallback 与内嵌字体共同决定;应用若需补充字体,应通过 Resource Manager/应用沙箱提供明确的开源字体目录并在产品层映射到 `sub-fonts-dir`,不修改核心、不提交商业字体。
Playback Lab 只验证字幕渲染和缺字表现,不把字体文件或用户凭证提交到仓库。设备侧若系统字体目录、provider 行为或 fallback 顺序不同,应记录实际字体、脚本和缺字结果。
+64
View File
@@ -0,0 +1,64 @@
# HarmonyOS Hardware Decode Research
## 1. 关键发现
截至 FFmpeg 官方 master `3acec0a1af2dda0a0838689b8b8649e7deb080a0`(2026-08-28),upstream 已包含 OpenHarmony Codec:
- configure:`--enable-ohcodec`
- hw device:`AV_HWDEVICE_TYPE_OHCODEC`
- pixel format:`AV_PIX_FMT_OHCODEC`
- public device context:`AVOHCodecDeviceContext.native_window`
- decoder/encoder:`h264_oh`、`hevc_oh`
- SDK linkage:`native_media_vdec`、`native_media_venc`、`native_media_codecbase`、`native_media_core`、`native_window`
其 surface 路径把 `OHNativeWindow *` 放入 hw device,FFmpeg 配置 `AV_PIXEL_FORMAT_SURFACE_FORMAT`,并在 AVFrame release 时调用 `OH_VideoDecoder_RenderOutputBuffer()`。因此“不存在 FFmpeg Harmony hwcontext”已不是事实;当前缺口主要转移到 MPV 对 OHCODEC 的识别、Surface 所有权和呈现同步。
## 2. Codec 矩阵
| Codec | Harmony Native Support | FFmpeg Support | MPV Support | Surface Decode | Pixel Format | Copy | Zero Copy Feasibility | 10-bit / HDR | Known Limitation | Complexity |
|---|---|---|---|---|---|---|---|---|---|---|
| H.264 | Native decoder API 存在;具体 profile/4K 由设备 capability 查询 | `h264_oh` 已在 upstream master | 无 OHCODEC 映射/backend | 已实现 | `AV_PIX_FMT_OHCODEC` / surface format | 解码器到 Surface 可避免 CPU frame;是否经过系统内部 copy 不可见 | 高,但直出播放 Surface 与 MPV shader/字幕合成冲突 | 设备查询;H.264 10-bit 通常不作为首要目标,不能猜 | FFmpeg 实现新、MPV 未接入、字幕/OSD 合成路径未解决 | 高 |
| HEVC | Native decoder API 存在;profile/level/10-bit 待设备查询 | `hevc_oh` 已在 upstream master | 无 OHCODEC 映射/backend | 已实现 | 同上 | 同上 | 高 | Main10/HDR 必须按真机 capability + 实际流验证 | HDR metadata 传递和 Surface colorspace 未证明 | 高 |
| AV1 | SDK API 可按 MIME 查询,不代表设备必有硬件 | 当前 OHCodec backend 未声明 `av1_oh` | 无 | FFmpeg OH 路径无 | 无 | 未定 | 未定 | 未定 | upstream FFmpeg 尚未接 OH AV1 | 很高 |
| VP9 | SDK API可查询,不代表设备必有硬件 | 当前 OHCodec backend 未声明 `vp9_oh` | 无 | FFmpeg OH 路径无 | 无 | 未定 | 未定 | 未定 | upstream FFmpeg 尚未接 OH VP9 | 很高 |
## 3. MPV 缺口
1. `video/fmt-conversion.c` 没有 `IMGFMT_OHCODEC <-> AV_PIX_FMT_OHCODEC`。
2. `video/decode/vd_lavc.c` 尚无 OHCODEC device/backend 选择。
3. GPU interop driver 列表没有 Harmony/OHNativeBuffer 路径。
4. libmpv OpenGL Render API的目标 Surface与 OHCodec 直接输出 Surface存在所有权冲突:同一个播放画面若让 decoder 直接呈现,MPV 的 shader、OSD、ASS 字幕、色彩管理无法自然叠加。
5. FFmpeg 当前 OHCodec surface frame 在 AVFrame release 时触发呈现,呈现时钟、drop/late frame 与 MPV VO 调度需要逐帧语义验证。
## 4. 推荐路线
### Stage 1:软件解码基线
先以 FFmpeg software decode + libmpv GLES Render API 跑通首帧、Seek、字幕、网络和稳定性。该基线用于证明其后优化收益。
### Stage 2:FFmpeg OHCodec 可用性验证
独立验证官方 `h264_oh` / `hevc_oh`:创建 decoder、Flush/Seek、Surface destroy/recreate、4K、高码率、Main10;记录设备 capability。此阶段不修改 MPV 大量代码。
### Stage 3:选择合成架构
- 路线 3A,直接 Surface decode:最快接通、copy 少,但 MPV GPU shader/ASS/OSD 不能直接叠加。只适合验证硬解吞吐,不直接定为正式架构。
- 路线 3B,OHNativeBuffer/GPU import:decoder 输出可导入 GLES/Vulkan,再由 MPV/libplacebo 合成。最符合最终产品,但 FFmpeg 当前 OHCodec API是否暴露可导入 buffer 尚需源码和真机证明。
- 路线 3C,双 Surface/system overlay:视频与字幕分层,由系统合成。同步、HDR、截图和滤镜语义复杂,仅作备选。
正式路线优先研究 3B;在其不可行证据充分前,不宣称零拷贝。
## 5. Patch 边界
如进入实现,优先局限在:
- `video/fmt-conversion.c` / image format 定义
- `video/decode/vd_lavc.c` 的 hw device 选择
- `video/out/hwdec/hwdec_ohcodec.c` 或明确命名的 Harmony interop driver
- `meson.build` Harmony feature gate
任何不支持格式明确返回 unsupported;不得以 CPU 黑帧、fake success 或注释掉检查伪装完成。
## 6. 当前判定
**不创建 Velix-FFmpeg-Harmony。** 官方 FFmpeg 已有关键 OHCodec 基础,Phase 1 应固定官方 commit 并先编译;只有真实 blocker 证明需要 patch 且无法快速 upstream 后,才建立独立 FFmpeg fork。AV1、VP9、buffer import、HDR metadata 仍为 `NEED VERIFY`。
+74
View File
@@ -0,0 +1,74 @@
# HarmonyOS Rendering Research
## 1. 结论
第一阶段推荐 **OpenGL ES + EGL + libmpv OpenGL Render API**:ArkUI XComponent 仅交付 `OHNativeWindow *`,Native 层创建 EGL window surface/context,并在 GL 线程调用 `mpv_render_context_render()` 渲染到默认 FBO,随后 `eglSwapBuffers()`。视频帧不经过 ArkTS/N-API。
```text
XComponent Surface
-> OHNativeWindow
-> EGLSurface + GLES context(Native 渲染线程)
-> libmpv OpenGL Render API
-> GPU render -> eglSwapBuffers -> Display
```
依据:本机 API 24 SDK 明确把 `EGLNativeWindowType` 定义为 `struct NativeWindow *`;MPV 公共 Render API 当前只公开 `MPV_RENDER_API_TYPE_OPENGL` 与软件渲染,未公开 Vulkan Render API。
## 2. 方案 A:OpenGL ES / EGL
### 调用边界
1. `OH_NativeXComponent_RegisterCallback()` 接收 Surface 创建/变化/销毁。
2. `OnSurfaceCreated` 的 `void *window` 按 SDK 契约转换为 `OHNativeWindow *`。
3. Native GL 线程执行 `eglGetDisplay`、`eglInitialize`、`eglChooseConfig`、`eglCreateWindowSurface`、`eglCreateContext`。
4. 用 `MPV_RENDER_PARAM_API_TYPE=MPV_RENDER_API_TYPE_OPENGL` 和 `mpv_opengl_init_params` 创建 render context。
5. update callback 只唤醒渲染线程;该线程 make-current,调用 `mpv_render_context_render()`,目标为默认 FBO 0 和当前 Surface 尺寸,再 swap。
6. Surface changed 更新 viewport/FBO 尺寸;Surface destroyed 先停止提交帧,再释放 render context 的 surface 相关状态和 EGLSurface。
### 评估
| 维度 | 结论 |
|---|---|
| 实现复杂度 | 中;MPV 已有完整 OpenGL Render API,Harmony 只负责 EGL/XComponent 生命周期 |
| MPV 支持度 | 高;公共 API 稳定,`include/mpv/render.h`、`render_gl.h` 为直接依据 |
| HarmonyOS 支持度 | 高;API 24 提供 EGL、GLES2/3、OHNativeWindow |
| Surface 生命周期 | 必须单线程串行化 create/change/destroy,销毁后禁止 swap |
| Copy 次数 | 软件解码时 FFmpeg frame 上传 GPU,至少一次 CPU→GPU;无逐帧 N-API copy |
| 4K | 渲染路径可行,性能取决于上传、shader、带宽;需 Benchmark |
| 10-bit | 软件帧可由 MPV shader 处理;Surface 色深/输出格式待设备 EGLConfig 验证 |
| HDR | MPV 色彩处理可复用,但 Harmony Surface 色域、transfer、HDR metadata 通路尚未证明 |
| 风险 | EGL context 线程归属、Surface 快速重建、默认 FBO 方向、vsync/帧节奏 |
### MPV 修改范围
- Phase 1 构建 `libmpv` 时启用 `plain-gl`,无需先新增完整 `vo_harmony`。
- Playback Lab 的 Harmony glue 独立放在后续 `playback_lab/native/`,不把 ArkTS 业务代码放进 MPV core。
- 只有确认必须让 MPV 自管 window/context 时,才新增 `video/out/opengl/context_harmony.c` 并在 Meson 中以 `features['harmony']` 注册。
## 3. 方案 B:Vulkan
### 可行基础
API 24 提供 `VK_OHOS_surface`、`VkSurfaceCreateInfoOHOS`、`vkCreateSurfaceOHOS()` 与 `libvulkan.so`,可从 `OHNativeWindow *` 创建 `VkSurfaceKHR`。MPV 内部 `vo_gpu_next` / libplacebo 支持 Vulkan,但 libmpv 公共 Render API 没有 Vulkan 类型。
| 维度 | 结论 |
|---|---|
| 实现复杂度 | 高;需要新增/扩展 MPV Vulkan context 或设计受支持的嵌入路径 |
| MPV 支持度 | 内部高、公共嵌入 API 低 |
| HarmonyOS 支持度 | API 10+ 有 surface 扩展,设备扩展需枚举 |
| Surface 生命周期 | 需重建 swapchain,处理 out-of-date/suboptimal、旋转与尺寸变化 |
| Copy 次数 | 软件解码仍需上传;未来 native buffer/Vulkan interop 有降 copy 潜力 |
| 4K | 理论适合,未实测不能优于 GLES |
| 10-bit/HDR | Vulkan format/colorspace 路径潜力更强,但 Harmony 设备与系统合成支持待证 |
| 风险 | libplacebo/Vulkan 版本、OHOS surface 接入、swapchain、同步/fence、驱动差异 |
## 4. 禁止路径
`MPV -> CPU frame -> Native Buffer -> ArkTS -> ArkUI Image` 不进入正式架构。它会增加逐帧跨 N-API、CPU copy、GC/调度与背压风险,也无法作为 4K 主路径。
## 5. Phase 门禁
- Phase 1:只证明 `libmpv` arm64 编译,OpenGL Render API 符号存在。
- Phase 2:Playback Lab 证明 XComponent→EGL→首帧;记录 Surface 重建和旋转。
- Phase 3:软件解码基线后再接 OHCodec;分别测 CPU upload 与 surface decode。
- Vulkan 只有在 GLES 基线与瓶颈证据齐备后启动,不以“理论更快”替代 Benchmark。
+7
View File
@@ -0,0 +1,7 @@
# OHCodec HDR pipeline notes
本阶段只保留 metadata 研究,不声明 HDR 完成。
FFmpeg upstream OHCodec stream-change 路径会把存在的 matrix、primaries、transfer、range 和 SAR 元数据复制到 `AVCodecContext`;MPV 现有 color-management pipeline 继续消费这些 metadata。设备是否输出 10-bit、支持 Main10 或 HDR surface,必须由 API24 capability 和真实设备 frame metadata 证明。
当前实现不添加 HDR transfer、tone mapping、Dolby Vision 或 10-bit surface 特判。Playback Lab 的 capability/diagnostics 只显示真实查询结果;没有设备数据时显示 `DEVICE TEST REQUIRED`,不得显示 `HDR YES`。
+79
View File
@@ -0,0 +1,79 @@
# HarmonyOS First Native Build Report
## Result
Phase 1 已在 DevEco Studio OpenHarmony SDK 6.1.1.125 / API 24 下生成真实 arm64 `libmpv`。本报告只证明编译、链接和静态检查,不声明真机运行语义已验证。
| 项目 | 结果 |
|---|---|
| Origin | `https://git.leee.xin/leeexin/Velix-MPV-Core.git` |
| origin/harmony-port | 以本次 Phase 1 提交推送后的 commit 为准 |
| MPV | `182fa6ca49f455cadb884858f386e2f00540aeb7` / 0.41.0-UNKNOWN |
| FFmpeg | `3acec0a1af2dda0a0838689b8b8649e7deb080a0` |
| libmpv | `build/ohos-arm64/prefix/lib/libmpv.so.2.5.0` |
| Size | 34,314,184 bytes |
| SHA-256 | `80e4c6fda86d9822543615a97045fff3641e9125b23a4f736fb8511fe5a47790` |
| Architecture / ABI | ELF64, little-endian, AArch64, System V ABI, shared object, SONAME `libmpv.so.2` |
| Toolchain | OHOS Clang/LLD 15.0.4, target `aarch64-linux-ohos`, musl sysroot |
| MPV / build files | 7:`ao_harmony.c/.h`、AO 注册、Meson feature、OHOS build script、依赖 allowlist;第三方依赖源码修改 0 |
| Build issues | 4,全部关闭 |
## Features
MPV enabled:`clone`、`ffmpeg`、`gl`、`glibc-thread-name`、`glob`、`glob-posix`、`gpl`、`harmony-audio`、`libass`、`libdl`、`libplacebo`、`linux-fstatfs`、`memrchr`、`ohos`、`posix`、`posix-shm`、`ppoll`、`pthread-condattr-setclock`、`vector`、`vt.h`、`zlib`、`libmpv shared`、`plain-gl`。
FFmpeg enabled:软件解码、demux、network、HTTP/HTTPS/TCP/TLS、OpenSSL 3.6.4、SDK zlib、OHCodec H.264/HEVC decoder/encoder。FFmpeg GPL/version3/nonfree 均为 0。
Disabled:mpv CLI、tests、fuzzers、Lua/JavaScript、libcurl、桌面音视频后端、X11、Wayland、DRM/GBM、Cocoa、Windows backend、Android-specific backend、Vulkan、VAAPI、VDPAU、CUDA。libplacebo 的 Vulkan/OpenGL 自有 backend 关闭;MPV 自身 OpenGL Render API 保留。
## Verification
- 最小 C/C++ 探针均为 OHOS AArch64 ELF;C++ 探针依赖 SDK `libc++_shared.so`。
- prefix 内 13 个静态库的全部可见 archive object 均通过 AArch64 ELF 检查。
- `libmpv.so.2.5.0` 动态依赖为 `libc.so`、`libz.so`、`libnative_media_vdec.so`、`libnative_media_venc.so`、`libnative_media_codecbase.so`、`libnative_media_core.so`、`libnative_window.so`、`libohaudio.so`;均来自 HarmonyOS SDK allowlist。
- target metadata 未发现 Homebrew、`/usr/local`、`.dylib`、Apple framework 或 Mach-O 污染。
- 已验证 `mpv_create`、`mpv_initialize`、`mpv_command`、`mpv_set_property`、`mpv_get_property`、`mpv_render_context_create`、`mpv_render_context_render`、`mpv_render_context_set_update_callback`、`mpv_render_context_free`。
- `scripts/build-all-ohos.sh` 已完成本次 Phase 3 全链路构建与最终校验(`verified_archives=13`)。
## Warnings
剩余 18 条构建 warning:FFmpeg 5 条 upstream fallthrough/unused-function;FriBidi 5 条 enum-conversion;HarfBuzz 1 条仅开发者需要 Ragel;libass/MPV 4 条非阻塞 CMake compiler-state 探测;libplacebo 3 条无 graphics backend/name-prefix 提示。均不影响当前 target 产物,未通过关闭警告掩盖问题。
## Dependency Graph
实际依赖图见 `DOCS/PHASE1_DEPENDENCY_MATRIX.md`。所有第三方库均静态进入 `libmpv`,SDK zlib、OHCodec 与 libc 保持系统动态依赖。
## License
仓库与 `libmpv` 当前按 MPV GPL-2.0-or-later 路线管理。FFmpeg 为 LGPL 配置且未启用 GPL/nonfree;OpenSSL 为 Apache-2.0;静态 LGPL 组件仍需在正式发布时履行可重新链接、源码与 notices 义务。本文件不是最终发行 notice。
## Phase 2 Risks
1. POSIX、DNS、HTTP/HTTPS 与动态库加载目前仅 compile/link verified,尚未真机 runtime verified。
2. XComponent / OHNativeWindow 生命周期、线程归属、窗口销毁与 EGL context loss 尚未验证。
3. MPV plain-gl 与 HarmonyOS EGL/GLES 的实际格式、扩展、同步和首帧路径尚未联调。
4. FFmpeg OHCodec 已编译,但 MPV hwdec 选择、surface/output buffer 生命周期及零拷贝均未验证。
5. OpenSSL CA 路径与 HAP 内依赖打包、SDK系统库版本兼容仍需真机验证。
## Phase 2 Entry Condition
已具备 XComponent + OHNativeWindow + EGL + libmpv OpenGL Render API 真机首帧测试的编译侧条件:SDK API 可用、libmpv 可链接、OpenGL Render API symbols 齐全。尚不代表首帧已经成功;Phase 2 必须在真机建立窗口和 EGL context 后验证。
## FFmpeg-Harmony export prefix
Phase 4.5 的 FFmpeg fork 独立维护于
`https://git.leee.xin/leeexin/Velix-FFmpeg-Harmony.git`,分支
`harmony-ohcodec-export`,当前分支 tip `f3a036c695`(跨 origin 重定向丢弃敏感凭证;代码修正 commit
`aacbe7d578`)。MPV 不包含 FFmpeg
源码,也不读取其私有头文件;需要切换时执行:
```sh
./scripts/build-mpv-ohos-ffmpeg-harmony.sh
```
脚本默认消费 `../Velix-FFmpeg-Harmony/build/ohos-arm64/prefix`,也可用
`FFMPEG_PREFIX=/path/to/prefix MPV_BUILD_ROOT=/path/to/mpv-build MPV_PREFIX=/path/to/mpv-prefix` 覆盖。切换脚本默认把 MPV 安装到
`build/ohos-arm64/prefix-mpv-ffmpeg-harmony`,避免覆盖默认 prefix。默认
`build-mpv-ohos.sh` 与 `build-mpv-ohos-software.sh` 的原有前缀和
`harmony-ohcodec-gl=disabled` 行为不变。本次切换构建已通过 AArch64 ELF、符号
和动态依赖校验,但没有进入 GPU mapper 或设备硬解验证。
+20
View File
@@ -0,0 +1,20 @@
# MPV 与 OHCodec 缺口分析
扫描基线:MPV 当前 `harmony-port` 工作树,FFmpeg OHCodec 头文件来自锁定 commit `3acec0a1af2dda0a0838689b8b8649e7deb080a0`。
| MPV location | 当前行为 | OHCodec requirement | 本阶段改动/结论 | 风险与 upstreamability |
|---|---|---|---|---|
| `video/img_format.h` / `video/fmt-conversion.c` | 只有通用 hardware format,没有 OHCodec 专用 ID。 | `AV_PIX_FMT_OHCODEC` 必须成为 MPV hardware image format。 | 新增 `IMGFMT_OHCODEC ↔ AV_PIX_FMT_OHCODEC` 单一映射。 | 纯 MPV 架构改动,可 upstream;依赖 FFmpeg enum。 |
| `video/decode/vd_lavc.c:add_all_hwdec_methods()` | 动态枚举 FFmpeg HW config,并使用 `av_hwdevice_get_type_name()` 生成 method。 | 能识别 `AV_HWDEVICE_TYPE_OHCODEC`、`h264_oh`、`hevc_oh`。 | 不改 decoder core;FFmpeg config 会自然生成 `hwdec=ohcodec`,且仅 H.264/HEVC 由 adapter 放行。 | upstreamable;真实 capability 仍需设备验证。 |
| `video/hwdec.c:hwcontext_fns[]` | 未注册 OHCodec copy-device callback;未知类型会尝试空 device。 | copy path 必须不能伪造 null-surface 成功。 | 注册 `hwcontext_fns_ohcodec`,明确拒绝无独占窗口的 copy path,并触发 software fallback。 | 纯 MPV 保护逻辑。 |
| `video/out/gpu/hwdec.c` | 只注册已有 GPU interop/overlay drivers。 | Direct Surface 需要独立 backend,不能复用 MediaCodec/DRM。 | 注册 `ohcodec-direct-surface-experimental`,仅在 host 提供 `ohos_direct_surface` native resource 时创建设备。 | 当前 Playback Lab 不提供该 resource,故默认不会抢占 EGL Surface。 |
| `video/out/gpu/video.c` / `gpu_next` | mapper 或 overlay 二选一;overlay 会绕过视频 GL 合成但保留 OSD。 | Direct Surface 不能与同一 EGL window 双重呈现。 | OHCodec 只使用 overlay probe;没有独占窗口时 init 失败。 | Direct Surface 不是最终 ASS/shader/color-management 产品路径。 |
| `video/mp_image.c:mp_image_from_av_frame()` | 复制 AVFrame buffers/data;无 OHCodec 特判。 | frame 需要保留 `buf[0]` 生命周期和硬件格式。 | 现有 generic path 已满足:`data[3]` 作为 opaque plane,`buf[0]` 持有 release。 | 不能把 opaque plane 当 CPU pixels 或 fd。 |
| `video/out/gpu/ra.h` native resources | 可由 host 注入平台资源。 | Surface ownership 需要明确边界。 | 约定 `ohos_direct_surface` 为独占、已由 host 管理的 `OHNativeWindow*`。 | 当前没有 host 注入,设备验证前不能宣称 direct active。 |
| Diagnostics / Playback Lab | 原来只显示 software audio/video 状态。 | 必须显示 requested/actual decoder、hw state、fallback/capability。 | 新增导出统计 API、日志识别和 Playback Lab 面板字段。 | actual decoder 在设备上需以 FFmpeg 日志和真实 frame 验证。 |
## 选择路径
`hwdec=ohcodec` → FFmpeg OHCodec method → MPV 请求 `IMGFMT_OHCODEC` → GPU backend 请求 OHCodec device → adapter 查询 capability → 检查独占 Surface → 创建 `AVHWDeviceContext` → Direct Surface overlay。
任何一步失败均返回负值并保留 software decoder。GPU interop 不走猜测性的 opaque-pointer reinterpretation。
+13
View File
@@ -0,0 +1,13 @@
# 网络播放
网络播放复用 mpv/FFmpeg 的单一网络栈,不在产品中创建第二 HTTP client。支持范围是 mpv 已提供的 HTTP、HTTPS、HLS、重定向、Range seek、chunked 和大范围 seek 能力。
每次 load 先清空 http-header-fields,再设置本次请求的 headers;Cookie 作为本次请求的 Cookie header 发送,未启用 mpv 持久化 cookie 文件。请求可设置 user-agent、referrer、network-timeout、语言偏好和轨道选择。
网络诊断只报告实际可取得的 mpv 属性:协议、host、networkState、cache duration/bytes、demuxer、container、当前 bitrate,以及从实际日志识别到的 HTTP status/redirect count;`rangeSupported` 仅在观察到 206 响应时为 1,`seekResult` 记录最近一次 seek 的 `ok` 或脱敏错误。没有对应属性或日志时返回 -1/0/空值,不做推断。
HTTP MP4 已在 Playback Lab 模拟器通过宿主受控 Range server 验证:请求日志出现 `bytes=0-`、后续大范围 byte range,播放结束且无核心错误。跨 origin 重定向的敏感头过滤由 FFmpeg-Harmony 固定分支提供,实际 HTTPS/HLS/鉴权资源仍需受控网络源继续验收。
HLS Master/Variant 的当前媒体分辨率来自 video-params/width/height,码率来自 demux-bitrate;当前 MPV 没有独立稳定的“variant 名称”属性,因此不新增伪造字段。
DNS、TCP connect、connection reset、timeout 和 FFmpeg open failure 统一归入 NETWORK,并保留脱敏原始消息;TLS handshake/certificate 归入 TLS,HTTP status 归入 HTTP,demux/decoder 错误不伪装成网络错误。
+15
View File
@@ -0,0 +1,15 @@
# 网络安全
## Header 与会话隔离
每次 load 都覆盖 http-header-fields,不共享上一个 session 的 header、Cookie 或 URL。禁止把认证信息写入仓库、默认日志或诊断快照。Playback Lab 输入框可以编辑 header,但诊断默认只显示字段名和掩码值。
## 脱敏规则
Authorization、Proxy-Authorization、Cookie、X-Api-Key、X-Emby-Token、X-Auth-Token、X-Access-Token 及 token/api-key/signature 等认证头,以及 URL 中的 token、access_token、auth、key、api_key、apikey、auth_token、signature、x-amz-signature、sig、expires 参数在错误文本和日志中替换为 [REDACTED]。原始 mpv 日志不得直接写入 OH 日志。
## TLS、重定向与凭证
默认保持 tls-verify=yes。mpv 网络后端负责 TLS、重定向、Range 和 HLS;本层不复制 HTTP 行为。固定的 FFmpeg-Harmony 分支 `f3a036c695` 已在 `libavformat/http.c` 实现跨 origin(协议/host/port 任一变化)重定向时丢弃自定义 Authorization/Cookie/Token/API-key/Referer 等敏感头及 cookie 状态,同源重定向仍保留请求头;因此不会把凭证无条件转发到不同 host。Referer、User-Agent、header 仅属于当前加载请求。
实现依据:mpv 官方手册的 user-agent、referrer、cookies、http-header-fields、network-timeout、HLS 与 sub-add 选项说明。
+34
View File
@@ -0,0 +1,34 @@
# OHCodec GPU interop 设计与当前 blocker
## 目标
将 OHCodec hardware frame 交给 MPV OpenGL composition,同时保留 ASS、OSD、shader 和 color management。该路径必须是显式实验开关 `harmony-ohcodec-gl`,不能成为默认 hwdec。
## API24 合法链路(仅设计验证)
SDK 头文件提供:
1. `OH_AVBuffer_GetNativeBuffer(OH_AVBuffer*)`,返回一个需要 `OH_NativeBuffer_Unreference()` 的 `OH_NativeBuffer*`。
2. `OH_NativeWindow_CreateNativeWindowBufferFromNativeBuffer()`,把 NativeBuffer 转成 `OHNativeWindowBuffer*`,并要求随后销毁 wrapper。
3. `EGL_OHOS_image_native_buffer` / `EGL_NATIVE_BUFFER_OHOS`、`eglCreateImageKHR()` 和 `glEGLImageTargetTexture2DOES()`。
这证明 API 类型是 HarmonyOS 原生接口,不应使用 Android `AHardwareBuffer`、fd 猜测或指针重解释。未来 mapper 的资源生命周期应为:
`OH_AVBuffer` frame ref → `OH_NativeBuffer_Get/Reference` → `OHNativeWindowBuffer` wrapper → EGLImage → external texture → unmap 时反向销毁。
## OHCODEC-BLOCKER-001
当前 upstream `libavcodec/ohdec.c` 只有两种输出:
- 有 OHCodec device:强制 `output_to_window=true`、配置 `AV_PIXEL_FORMAT_SURFACE_FORMAT`、`SetSurface(window)`;
- 无 device:CPU NV12 buffer,复制到 software frame。
hardware frame 的 `OH_AVBuffer*` 只存在于带 Surface 的分支,且自定义 `AVBuffer` release 会调用 `OH_VideoDecoder_RenderOutputBuffer()`。因此 MPV 不能在不修改 FFmpeg 的前提下请求“NativeBuffer hardware output but no decoder Surface present”模式。若 mapper 同时导入该 buffer 到 EGL,decoder release 仍可能向另一 Surface present,导致两个 owner、未定义 fence/queue ownership 和 seek/recreate race。
结论:本阶段不提交伪 interop mapper,不调用 `eglFinish/glFinish` 充当同步,不宣称零拷贝/GPU interop/HDR 完成。`harmony-ohcodec-gl` 作为 disabled-by-default feature 保留 API 探针入口;启用它前必须先解决该 upstream contract blocker,并补齐 output buffer、fence、crop/stride 和 surface ownership 的真实设备验证。
## Direct Surface ownership
`ohos_direct_surface` 是 adapter 专用 native resource。只有 host 明确注入一个不等于 Playback Lab EGL window 的独占窗口时,`ohcodec-direct-surface-experimental` 才会取得引用并创建设备;当前 Playback Lab 不注入此 resource,所以 decoder 不会和 EGL/libmpv 抢同一 Surface。
Surface destroy/recreate 必须停止 render/decoder、等待所有 AVFrame release、递增 generation,再以新窗口重新创建 OHCodec device。当前只完成 MPV adapter 侧的 ownership guard,实体设备上的 fence、present count、surface recreate 仍为 `DEVICE TEST REQUIRED`。
+42
View File
@@ -0,0 +1,42 @@
# MPV OHCodec 集成说明
## 已实现路径
`hwdec=ohcodec` 由 MPV/FFmpeg 的动态 hardware-decoder 枚举进入独立
`AV_HWDEVICE_TYPE_OHCODEC` backend。`IMGFMT_OHCODEC` 与
`AV_PIX_FMT_OHCODEC` 使用单一映射。当前 adapter 只允许 H.264 和 HEVC,先
查询 API 24 OHCodec hardware capability,再检查 host 注入的独占
`ohos_direct_surface`,最后创建带 `OHNativeWindow*` 的 FFmpeg hwdevice。
Direct Surface backend 的名称是
`ohcodec-direct-surface-experimental`。它只负责证明 device/decoder/surface
plumbing,不与 Playback Lab 的 EGL window 共享呈现权;没有独占窗口时明确失败,
MPV 保留 software decoder fallback。
## 状态与回退
适配器记录 `HWDEC_DISABLED`、`HWDEC_REQUESTED`、`HWDEC_CAPABILITY_CHECK`、
`HWDEC_DEVICE_CREATED`、`HWDEC_DECODER_CREATED`、`HWDEC_DIRECT_SURFACE`、
`HWDEC_GPU_INTEROP`、`HWDEC_ACTIVE`、`HWDEC_FALLBACK`、`HWDEC_ERROR`。
Playback Lab 通过 libmpv 诊断 API 与属性显示 requested/actual decoder、active
状态、backend、capability、fallback reason 及 hardware/software frame 计数。
capability 查询失败、codec/profile/level 不支持、surface 不可用、device/decoder
创建失败或后续 interop 失败都必须返回失败并回到 software path;不把空 device、
opaque pointer 或 CPU readback 当成成功的硬件帧。
## Surface 生命周期约束
`ohos_direct_surface` 的引用由 adapter 持有,host 负责提供一个不等于
Playback Lab EGL window 的独占窗口。Surface 销毁或重建时,host 必须先停止
decoder/render、等待 AVFrame release,再销毁旧 device 并用新窗口重建;当前代
码只完成 ownership guard,generation/fence/present 的实体设备验证仍待做。
## GPU interop 边界
`harmony-ohcodec-gl` 默认关闭,仅编译 API probe:API24 可探测
`OH_AVBuffer_GetNativeBuffer`、NativeWindow wrapper、EGL native-buffer image
和 GLES external texture 扩展。但 FFmpeg upstream 的硬件分支是 surface-first,
没有“不 present 到 decoder Surface、直接导出可导入 NativeBuffer”的契约;见
`DOCS/OHCODEC_GPU_INTEROP_DESIGN.md` 中的 `OHCODEC-BLOCKER-001`。因此当前不
提交 mapper、不宣称 zero-copy,也不宣称 HDR。
+28
View File
@@ -0,0 +1,28 @@
# FFmpeg OHCodec upstream analysis
基线:FFmpeg `3acec0a1af2dda0a0838689b8b8649e7deb080a0`。本阶段没有修改 FFmpeg。
## 已核对的源码事实
| 位置 | 事实 | 对 MPV 的结论 |
|---|---|---|
| `libavutil/hwcontext.h` | 注册 `AV_HWDEVICE_TYPE_OHCODEC`。 | 设备类型可作为独立 MPV hwdec method,不能伪装成 MediaCodec/VAAPI。 |
| `libavutil/pixfmt.h` | 注册 `AV_PIX_FMT_OHCODEC`,并标记为 OpenHarmony hardware decoding。 | MPV 需要自己的 `IMGFMT_OHCODEC` 映射。 |
| `libavutil/hwcontext_oh.h` | `AVOHCodecDeviceContext` 只有 `void *native_window`,注释为 `OHNativeWindow`。 | FFmpeg 公共设备上下文只承载 Surface 指针,没有 device handle、fence 或 NativeBuffer export 字段。 |
| `libavutil/hwcontext_oh.c:27-45` | `oh_device_create()` 只拒绝非空 device string;初始化不创建窗口、不取得引用,也不做 capability 检查。 | MPV 不能把 `av_hwdevice_ctx_create(..., NULL, ...)` 当成可呈现设备;Surface ownership 必须在 MPV/host adapter 中完成。 |
| `libavcodec/ohdec.c:150-197` | OHCodec device 被发现后读取 `native_window`,设置 `output_to_window=true`,使用 `AV_PIXEL_FORMAT_SURFACE_FORMAT`,并调用 `OH_VideoDecoder_SetSurface(window)`。 | upstream 的硬件输出是 Direct Surface 语义。空窗口不是合法的 GPU interop 设备。 |
| `libavcodec/ohdec.c:219-289` | stream change 读取 width/height/stride/slice height/pixel format;Surface 输出时设置 `avctx->pix_fmt=AV_PIX_FMT_OHCODEC`,并复制颜色元数据与 SAR。 | 尺寸/stride/pixel format 可进入 MPV diagnostics;不能由 enum 推导设备能力或 HDR 支持。 |
| `libavcodec/ohdec.c:432-451` | AVFrame buffer release 时读取 buffer attr;非 discard 调 `OH_VideoDecoder_RenderOutputBuffer()`,否则 `FreeOutputBuffer()`;随后释放 decoder ref。 | AVFrame 生命周期会触发 Surface present/release。Surface 的 decoder ownership 必须持续到最后一个 frame 引用释放。 |
| `libavcodec/ohdec.c:453-486` | hardware frame 的 `format=AV_PIX_FMT_OHCODEC`;`buf[0]` 持有 `OH_AVBuffer*` 的自定义 release;`data[3]=buf[0]->data`。 | 当前 FFmpeg 没有 `AVHWFramesContext`、transfer/map/export 回调。`data[3]` 是 upstream 当前内部布局,不应被重解释成 fd/Android hardware buffer。 |
| `libavcodec/ohdec.c:489-532` | 非 Surface 分支才取得 CPU 地址并复制到 software frame。 | 当前 upstream 没有“NativeBuffer output、但不直接 present Surface”的公共 decoder 配置路径。 |
| `libavcodec/ohdec.c` decoder close/flush | decoder 可能因硬件 frame 引用延迟释放;flush 会清空队列并重启 decoder。 | seek/flush 必须让 MPV 丢弃旧 generation 的 frame;Surface recreate 需要完整 decoder recreate。 |
## 关键问题结论
1. `AV_PIX_FMT_OHCODEC` frame 确实携带 `OH_AVBuffer*` 指针(`data[3]`),但 upstream 没有把它作为稳定的 MPV interop contract 暴露,也没有 non-surface output 模式。
2. API24 SDK 的 `OH_AVBuffer_GetNativeBuffer()` 可以合法取得 `OH_NativeBuffer*`,但要导入 EGL 还需要转换为 `OHNativeWindowBuffer*`;FFmpeg 当前仍会在 frame release 时调用 `RenderOutputBuffer`,因此不能安全地同时让 decoder Surface 和 libmpv EGL surface 认为自己拥有呈现权。
3. 因此本阶段在 MPV adapter 层实现了独立的 `ohcodec-direct-surface-experimental` 探针;GPU interop 保留 feature gate 与设计文档,明确记录 upstream blocker,不修改 FFmpeg。
## 变更边界
本仓库只新增 MPV image-format mapping、OHCodec hwdevice/Surface adapter、capability/fallback diagnostics 和 Playback Lab diagnostics。没有复制或修改 FFmpeg 源码。
+18
View File
@@ -0,0 +1,18 @@
# Phase 6 性能改动与证据
## 已采用
1. **事件驱动渲染更新**:`render.cpp` 不再每 16ms 无条件调用 `mpv_render_context_update`,只在 mpv callback/窗口事件唤醒时更新;为避免个别会话漏回调,最多每 250ms 做一次安全探测。5 次 startup 的 update 累计从优化前 617 降到最终实现 364,且 20 次 startup 未复现永久 BUFFERING。
2. **核心快照合并**:`PlaybackEngine` 对 100ms 内的重复 `get_snapshot/get_tracks/get_media_info/get_network_info` 做合并,动作仍立即提交到 MPV,状态最多延迟一个合并窗口。200 次 play/pause 末次面板显示 `coreSyncSkipped=239`;无 error,状态为 PAUSED。
3. **NAPI 诊断轮询降频**:Playback Lab 面板由 250ms 调整为 500ms;播放控制、音频 callback 和渲染线程不依赖该轮询。这样减少纯 UI 诊断序列化,不改变 MPV 时钟。旧/新间隔在 `Index.ets` 可审计,运行中由 `getStateCalls`、`serializeUsTotal` 记录。
4. **session id 原子读取**:渲染线程不再为每次唤醒获取 `PlaybackEngine` 快照互斥锁,session 更新仍由核心快照写入 release/acquire atomic;首帧 session 隔离 smoke 和 stress 均通过。
## 未采用
- 不做主动丢帧、降低音频采样率或缩小视频分辨率;这些会改变播放质量。
- 不做 GPU mapper、OHCodec 直出或 FFmpeg-Harmony 源码改动;实体设备 capability 和 fence 证据不足。
- 不把 PSS、heap、CPU permille、2 小时长跑结果伪装成已测数据。
## 回归与开销
默认构建、软件构建和 FFmpeg-Harmony 构建均通过。默认 HAP 在模拟器上完成 startup/seek/audio 状态面板、load/stop 100 次和 play/pause 100 次。release/profile 构建未启用持续 tracing;诊断仅累加轻量计数和受控序列化时间。
+20
View File
@@ -0,0 +1,20 @@
# Phase 6 性能指标
所有时间均为单调时钟相对当前 load session 的毫秒数。`-1` 表示该事件没有发生或无法可靠观察;runner 会把负值转换为 `null`,不参与百分位统计。
| 类别 | 字段/来源 | 语义 | 模拟器可用性 |
| --- | --- | --- | --- |
| 启动 | `timeline.startupTotalMs` / `firstSwapMs` | load 到首个成功 EGL swap | 可用 |
| 启动分段 | create、initialize、load、START_FILE、FILE_LOADED、decoder reconfig、render context | 各阶段首次事件 | 可用,缺失阶段为 -1 |
| 网络 | `firstByteMs`、`networkWaitMs`、HTTP status、206/range | 只记录观察到的请求/日志证据 | HTTP fixture 可用,HTTPS/HLS pending |
| seek | `seekCommandLatencyMs`、`seekRecoveryMs`、`seekFirstSwapMs` | seek request/command 到恢复首帧/首 swap | 相对 `+10s` 可用 |
| buffering | enter/exit 次数、累计时长、单次最大时长 | cache-buffering 状态边沿 | 字段可用;本地短片通常为 0 |
| 音频 | renderer start、首 callback、submitted/played、delay、starvation、flush/reset | 首 callback 时间由诊断轮询观察,不冒充 callback 内精确时间 | 模拟器字段可用,设备路由 pending |
| 视频 | dropped、late、mistimed、decoder frames、render count | MPV stats 或 renderer 计数 | 计数接口可用,部分 MPV 值可能为 -1 |
| 内存 | `/proc/<pid>/status` VmRSS/VmSize | 进程 RSS/虚拟地址空间 | 可用;PSS/精确 heap 为 null |
| CPU | `/proc/<pid>/stat` utime+stime | 原始 jiffies | 原始值可用;未取得可靠 tick/采样归一化,不输出 permille |
| 线程/资源 | Threads、`/proc/<pid>/fd` | 进程线程和 fd 快照 | 可用;fd=0 视为不可用 |
| NAPI | getState 次数、序列化微秒、估算 payload | NAPI 状态导出成本 | 可用;payload 是上界估算,不是分配追踪 |
| 渲染诊断 | render update/frame update、core sync/skipped | 优化前后计数及合并效果 | 可用 |
NAPI 返回使用 Number 以保持 Inspector 可序列化;大整数仅用于内部原子计数。任何未被设备或 API 真实提供的数据都保持 unavailable,不由模型推断。
+40
View File
@@ -0,0 +1,40 @@
# Phase 1 Dependency Matrix
基线:MPV `182fa6ca49f455cadb884858f386e2f00540aeb7`。状态来自当前 `meson.build`、实际 Meson 配置与最终产物,不沿用 Phase 0 假设。
| Dependency | Version / Commit | Required / Optional | Build System | License | Link | OHOS Status | pkg-config | Blocking libmpv |
|---|---|---|---|---|---|---|---|---|
| libavcodec | 63.8.101 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libavcodec` | Yes |
| libavfilter | 12.3.101 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libavfilter` | Yes |
| libavformat | 63.6.100 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libavformat` | Yes |
| libavutil | 61.6.100 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libavutil` | Yes |
| libswresample | 7.2.100 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libswresample` | Yes |
| libswscale | 10.2.100 / FFmpeg `3acec0a1` | Required | configure + Make | LGPL-2.1+ | Static | Built AArch64 | `libswscale` | Yes |
| libplacebo | 7.360.1 / `cee9b076` | Required | Meson | LGPL-2.1+ | Static | Built AArch64; graphics backends off | `libplacebo` | Yes |
| libass | 0.17.5 / `4a05d812` | Required | Meson | ISC | Static | Built AArch64 | `libass` | Yes |
| FreeType | 2.14.3 / `0a0221a1` | libass transitive | Meson | FTL/GPL-2.0 dual | Static | Built AArch64 | `freetype2` | Yes via libass |
| HarfBuzz | 14.4.0 / `36cb489c` | libass transitive | Meson | MIT | Static | Built AArch64 | `harfbuzz` | Yes via libass |
| FriBidi | 1.0.16 / `68162bab` | libass transitive | Meson | LGPL-2.1+ | Static | Built AArch64 | `fribidi` | Yes via libass |
| OpenSSL | 3.6.4 / `d3c1b116` | HTTPS transitive | Configure + Make | Apache-2.0 | Static | Built AArch64 | `openssl` | Yes for HTTPS |
| zlib | SDK 1.2.13 | MPV optional; enabled | SDK | Zlib | Dynamic | SDK AArch64 `libz.so` | generated `zlib` | No |
| threads / dl / m / rt | SDK API 24 | Platform required | SDK | SDK system | Dynamic/system | Compile verified | Meson compiler/system | Yes |
| EGL / GLES | SDK API 24 | Phase 2 integration | SDK | SDK system | Not linked | Headers/libs available | No target `.pc` used | No |
| libcurl, libarchive, libbluray, libavdevice | Optional | Disabled | Various | Various | None | Not built | Not queried or skipped | No |
| CD/DVD, Lua, MuJS, uchardet, VapourSynth, zimg, rubberband | Optional | Disabled | Various | Various | None | Not built | Skipped | No |
| ALSA, AudioUnit, CoreAudio, JACK, OpenAL, AAudio, OpenSLES, OSS, PipeWire, Pulse, sndio, WASAPI | Optional AO | Disabled | Various | Various | None | Not applicable | Skipped | No |
| X11, Wayland, DRM/GBM, Cocoa, D3D11, Vulkan, VAAPI, VDPAU, CUDA, Android Media NDK | Optional VO/hwaccel | Disabled | Various | Various | None | Not applicable | Skipped | No |
实际依赖图:
```text
OpenSSL ───────────────┐
SDK zlib ──────────────┼─> FFmpeg ───────────────┐
└─> FreeType ─> HarfBuzz ─┤
FriBidi ──────────────────────────────┐ │
FreeType + HarfBuzz + FriBidi ───────> libass ───┤
libplacebo ──────────────────────────────────────┼─> libmpv.so
SDK libc/pthread/dl/m/rt/z ──────────────────────┘
SDK native_media_* + native_window <─ FFmpeg OHCodec
```
所有 `.pc` 仅由 `scripts/pkg-config-ohos.sh` 从 OHOS prefix 解析。最终校验未发现 `/opt/homebrew`、`/usr/local`、Mach-O 或 `.dylib` target 输入。
+67
View File
@@ -0,0 +1,67 @@
# Phase 2 Playback Lab 报告
日期:2026-08-29
分支:`harmony-port`
## 范围
`playback_lab/` 是独立 HarmonyOS 验证工程,仅链接 Phase 1 生成的 arm64 `libmpv.so.2`,不复用 Velix 主应用的页面或业务逻辑。当前只验证 GLES/EGL、软件解码和本地 MP4 首帧,不接入音频或硬解。
## 构建与产物
环境:DevEco Studio SDK API 24,HarmonyOS emulator 6.1.0.126,`aarch64-linux-ohos`。构建命令:
```sh
./scripts/stage-mpv.sh
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home \
PATH="$JAVA_HOME/bin:$PATH" \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
--no-daemon assembleHap --mode module -p product=default
```
结果:`BUILD SUCCESSFUL`。HAP:`playback_lab/entry/build/default/outputs/default/entry-default-unsigned.hap`,约 31 MiB,SHA-256 `877568c34a02a4d61b8c4ed7721dd2e6486cb675cd1a58a8eaf44c9b00de05f5`。包内包含 `libentry.so`、`libmpv.so.2` 和 `libc++_shared.so`;未提交 34 MiB 的 staged native 库。
构建仅保留两项预期警告:NAPI 动态模块在当前 SDK 尚未完成类型校验,以及调试 HAP 未配置正式签名;两者不影响本轮模拟器安装验证。
## 模拟器验证
目标:`127.0.0.1:5555`,虚拟设备进程配置为 Pura 90 Pro Max。
| 项目 | 结果 | 证据 |
| --- | --- | --- |
| HAP 安装、启动 | 通过 | `hdc install -r`、`aa start` 成功 |
| XComponent / OHNativeWindow | 通过 | 页面显示 `surface=READY`;Native 回调无崩溃 |
| EGLDisplay / EGLConfig / EGLContext / WindowSurface / makeCurrent | 通过 | 页面显示 `egl=READY`;日志 `EGL vendor=Express ... renderer=Mali-G77` |
| libmpv 初始化与 Render API | 通过 | 页面显示 `mpv=READY`;日志 `libmpv initialized` |
| 本地 H.264 MP4 软件解码 | 通过 | 本地 `sample.mp4` 首帧及持续播放成功 |
| Pause / Resume / Stop / Release | 通过 | UI 状态分别出现 `PAUSED`、`PLAYING`、`STOPPED`、`RELEASED` |
| Seek | 通过 | 有效播放中 `-10s/+10s` 不报错;跳转按 duration 做边界裁剪 |
| 释放后重新进入 | 通过 | `RELEASED` 后重新启动回到 `PLAYER_CREATED`,Surface/EGL/mpv 再次 READY |
| BigInt 状态序列化 | 通过 | `renderCount` 改为 Number,Inspector 不再出现 BigInt 序列化错误 |
一次本地播放快照:`position=2.00`、`duration=5.05`、`fps=29.97`、`dropped=0`、`firstFrameMs=622`、`renderCount=52`。另一轮快照首帧为 117 ms;`firstFrameMs` 从 `load()` 调用开始计时,并在第一次 `eglSwapBuffers` 后记录。
## First Frame Timeline
1. T0:XComponent 创建并取得回调传入的 `OHNativeWindow`,增加 NativeObject 引用。
2. T1:专用渲染线程创建 EGLDisplay、RGBA8 EGLConfig、ES3 EGLContext 和 EGLWindowSurface,执行 `eglMakeCurrent`。
3. T2:同一渲染线程创建 mpv OpenGL Render Context,注册 update callback。
4. T3:`loadfile` 完成本地文件打开,MPV 软件解码产生可渲染帧。
5. T4:渲染线程执行 `mpv_render_context_render`、`eglSwapBuffers`,记录 `firstFrameMs` 和 `renderCount`。
## 尚未完成的真机验收
本轮只有模拟器,没有接入 HarmonyOS 实体手机,因此 Phase 2 不能标记为最终 COMPLETE。以下项目保持待验证:实体机 HAP 安装、实体机本地 1080P H.264/AAC、HTTP MP4、HTTPS MP4、HLS、20 次 Surface recreate、20 次进出、50 次 Play/Pause、50 次 Seek、横竖屏 20 次、30 分钟稳定性、CPU/RAM 曲线。
## 风险状态
| 风险 | 当前状态 |
| --- | --- |
| RISK-01 POSIX / DNS / network runtime | 未验证,网络链路留待实体机 |
| RISK-02 XComponent / OHNativeWindow lifecycle | 模拟器已验证;实体机压力测试待做 |
| RISK-03 EGL context / Surface / plain-gl | 模拟器已验证;旋转与重建待做 |
| RISK-04 OHCodec buffer lifecycle | 保持开放;Phase 2 明确禁用硬解 |
| RISK-05 CA / HAP packaging | 未签名调试 HAP 安装通过;正式签名待接入 |
新增 MPV upstream patch:0;未修改第三方源码。进入 Phase 3(`ao_harmony + OHAudio`)的前置条件是实体机首帧、控制和压力矩阵完成。
+47
View File
@@ -0,0 +1,47 @@
# Phase 4 状态
## 结论
**Phase 4 CODE COMPLETE / DEVICE VALIDATION PENDING**
GPU interop:**BLOCKED**(见 `OHCODEC-BLOCKER-001`)。
代码完成了 MPV 识别 FFmpeg OHCodec、H.264/HEVC decoder selection、能力检查、
direct-surface 实验接入、软件回退、诊断和构建矩阵。当前没有实体设备,因此
不能写成 hardware decode complete、zero-copy complete 或 HDR complete。
## 验证矩阵
| 项目 | 状态 |
| --- | --- |
| `AV_PIX_FMT_OHCODEC` → `IMGFMT_OHCODEC` | COMPILE VERIFIED |
| `AV_HWDEVICE_TYPE_OHCODEC` 独立 backend | COMPILE VERIFIED |
| H.264 / HEVC selection guard | COMPILE VERIFIED;DEVICE TEST REQUIRED |
| API24 OHCodec capability 查询 | COMPILE VERIFIED;设备返回值待验 |
| Direct Surface plumbing | COMPILE VERIFIED;需独占窗口 |
| GPU interop mapper | BLOCKED:上游无可导入 non-surface output contract |
| Software decode / OHAudio | CLEAN BUILD VERIFIED |
| OHCodec-enabled / software / GL-probe | AArch64 BUILD VERIFIED |
| Playback Lab HAP | BUILD VERIFIED;unsigned |
| Emulator | APP START + EGL SMOKE VERIFIED;不是硬解验证 |
| Physical device | PENDING |
## 产物与审计
- `build/ohos-arm64/prefix/lib/libmpv.so.2.5.0` 为 AArch64 ELF,保留 OHCodec、
OHAudio 及诊断符号。
- `scripts/build-mpv-ohos-software.sh` 使用独立 build 目录关闭 OHCodec,避免
软件回退配置与硬解构建互相污染。
- FFmpeg 保持锁定 upstream commit,未复制 SDK/第三方实现;许可证审计见
`LICENSE_AUDIT.md`。
- 当前 blocker、NativeBuffer 生命周期、EGL/fence/crop/stride 待验证项见
`DOCS/OHCODEC_GPU_INTEROP_DESIGN.md` 与 `DOCS/HDR_PIPELINE_NOTES.md`。
- Phase 4.5 FFmpeg export contract 独立维护于
`../Velix-FFmpeg-Harmony`;MPV 只提供
`scripts/build-mpv-ohos-ffmpeg-harmony.sh` 前缀切换,未接入 mapper。
## 下一阶段最大风险
实体设备上的 codec capability、surface recreate、output buffer ownership、
acquire/release fence、crop/stride/color metadata,以及如何在不修改 FFmpeg
surface contract 的前提下把硬件帧交给 MPV GPU composition。
+18
View File
@@ -0,0 +1,18 @@
# Phase 5 状态
状态:CODE COMPLETE / HTTP Range emulator verified / device and remaining network validation pending。
已完成:
- 默认软件解码路径保持 hwdec=no,未启用 harmony-ohcodec-gl。
- 新增 include/mpv/harmony_playback.h 稳定 C API;公共结构不包含 mpv_handle、mpv_node、AVFrame。
- 请求字段、会话 generation、线程安全控制、稳定状态/错误、轨道、媒体信息和网络缓存诊断已实现。
- Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。
- 文档、测试矩阵和构建矩阵已补齐。
- default/software/FFmpeg-Harmony 三套 AArch64 libmpv 构建、稳定 API 符号校验与 Playback Lab Native/ArkTS/PackageHap 编译已通过;使用 DevEco 自带 JBR 的 `--no-daemon` 流程生成 unsigned HAP,并由 devecocli 安装模拟器。
- 模拟器已启动 Playback Lab,完成本地 H.264/AAC MP4 首帧、软件解码、播放结束状态和渲染指标 smoke;通过宿主受控 HTTP Range server 完成 HTTP MP4 播放与 Range 请求验证;HTTPS/HLS/网络字幕/鉴权资源仍需真实网络源。
- 当前构建日志显示 iconv feature disabled,因此字幕适配器只承诺 UTF-8;GBK/Big5/Shift-JIS 会返回稳定 SUBTITLE 错误。
尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。
许可:新增代码沿用 MPV 的 LGPL/ISC 边界,不引入商业字体或新的第三方网络库;公共头文件保持独立 ABI。Phase 6 最大风险是将 Provider 业务请求正确转换为通用请求,以及在实体设备上确认网络、音频路由和字幕字体 provider 行为。
+31
View File
@@ -0,0 +1,31 @@
# Phase 5 测试矩阵
| 场景 | 方式 | 结果 |
|---|---|---|
| 本地 MP4 H264/AAC | Playback Lab | 模拟器安装启动并完成首帧/播放/结束 smoke;网络与实体机待确认 |
| 本地 MKV H264/AAC/SRT | Playback Lab | API/UI 编译覆盖;设备运行待确认 |
| 本地 MKV HEVC/AAC/ASS(software) | software mpv | AArch64 构建通过;设备运行待确认 |
| HTTP MP4 | 宿主受控 Range server + Playback Lab | 模拟器通过;观察到 `bytes=0-`、后续 byte range,请求完成且 state=ENDED;`rangeSupported=1` |
| HTTPS MP4 | 受控测试源 | TLS 校验代码就绪;网络环境待确认 |
| HLS Master | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 |
| HLS Variant/chunked | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 |
| 多音轨 MKV | Playback Lab | Track API/UI 已接入;设备运行待确认 |
| 内封 SRT | Playback Lab | Track API/UI 已接入;设备运行待确认 |
| 内封 ASS/SSA | Playback Lab | libass 路径保留原样式;设备运行待确认 |
| 外挂 SRT | Playback Lab | URL 与本地 file 路径入口已接入;设备运行待确认 |
| 外挂 ASS/SSA | Playback Lab | URL 与本地 file 路径入口已接入;设备运行待确认 |
| 网络字幕/WebVTT | Playback Lab | API/UI 已接入;网络环境待确认 |
| Authorization header | 受控测试源 | 脱敏与隔离代码覆盖;服务端待确认 |
| Cookie header | 受控测试源 | 单会话 Cookie header;服务端待确认 |
| Referer/User-Agent | 受控测试源 | 请求字段与 header 均支持;服务端待确认 |
| Range seek / 大 seek | 宿主受控 Range server + Playback Lab | 模拟器通过 HTTP Range 请求;最近 seek 结果由 `seekResult` 暴露;HTTPS 大 seek 待继续验收 |
| Redirect | FFmpeg-Harmony `f3a036c695` + 受控测试源 | 跨 origin 丢弃 Authorization/Cookie/Token/API-key/Referer;同源保留;受控重定向运行验证待补 |
| 403 / 404 / 500 | 受控测试源 | 稳定 HTTP 错误码已接入;网络环境待确认 |
| Broken media / reset / timeout / TLS | 受控测试源 | DEMUX/NETWORK/TLS 分类已接入;网络环境待确认 |
| 默认 FFmpeg(OHCodec enabled) | scripts/build-mpv-ohos.sh | AArch64 ELF 与符号校验通过 |
| software MPV(OHCodec disabled) | scripts/build-mpv-ohos-software.sh | AArch64 ELF 校验通过 |
| FFmpeg-Harmony | scripts/build-mpv-ohos-ffmpeg-harmony.sh | AArch64 ELF 与符号校验通过 |
| audio-enabled MPV | 默认脚本 `harmony-audio=enabled` | 与默认构建一并通过 |
| Playback Lab HAP | hvigor assembleHap --no-daemon | CompileArkTS/Native/PackageHap 通过;无正式签名,devecocli 已安装模拟器 |
产品禁止以 UI 定时器伪造 buffering;只有 mpv 事件/属性可改变对应诊断字段。
+33
View File
@@ -0,0 +1,33 @@
# Phase 6 状态
状态:**CODE COMPLETE / EMULATOR BENCHMARKED / DEVICE BENCHMARK PENDING**。
Phase 6 交付范围是可重复的 Playback Lab 性能时间线、资源采样、NAPI 诊断、四项低风险优化和模拟器回归。没有修改 GPU mapper,也没有修改 `Velix-FFmpeg-Harmony`。
## 已完成
- 启动、seek、buffering、音频、渲染、NAPI 和资源指标已接入稳定快照;未能可靠取得的 PSS、精确 heap、归一化 CPU 均明确返回 `null` 或文档标记为 unavailable。
- `benchmark/runner/run_emulator.py` 支持 startup、seek、load/stop stress、play/pause stress,输出重复统计(min/max/avg/median/P90/P95)和逐次资源采样。
- 渲染循环改为 mpv update callback 驱动,并保留 250ms 安全探测;快照同步增加 100ms 合并窗口;session id 读取改为 atomic;诊断轮询从 250ms 调整为 500ms。
- default、software、FFmpeg-Harmony 三套 AArch64 libmpv 均通过符号和 OHOS ELF 校验;Playback Lab HAP 编译、安装和模拟器启动通过。
## 模拟器结果
测试设备:`127.0.0.1:5555`,本地 H.264/AAC MP4,软件解码,`ao_harmony`。
| 场景 | 结果文件 | 关键结果 |
| --- | --- | --- |
| startup_local ×20 | `benchmark/results/startup_local-1787999362.json` | 首帧 P50 506ms,P95 1150ms;20/20 暴露时间线,无错误 |
| seek_local ×50 | `benchmark/results/seek_local-1787999663.json` | 恢复 P50 277ms,P95 631ms;49/50 有恢复时间线,1 次 `SEEK` 命令错误 |
| play_pause ×200 | `benchmark/results/play_pause-1788000475.json` | 无错误;末次 RSS 213228KB、线程 39、fd 1 |
| load/stop ×100 | `benchmark/results/stress_lifecycle-1788000229.json` | 进程保持存活;RSS/线程/fd 采样完成,PSS 不可用 |
优化前后计数基线:同等 5 次 startup 中,渲染 update 累计由 617 降至最终版本 364;NAPI/核心同步计数通过面板快照记录。200 次 play/pause 无错误,末次 `coreSyncSkipped=239`,说明合并窗口实际生效。
最终 5 次 smoke 还观察到 `firstAudioCallbackMs` P50 256ms、`audioRendererStartMs` P50 720ms;这些值来自 500ms 诊断轮询,不能替代设备音频 trace。file:// 场景的 `networkWaitMs` 保持 -1,buffering 计数按真实状态边沿记录。
## 尚待实体设备
实体设备上的真实 PSS/heap、CPU 利用率、显示合成延迟、音频路由/A-V 偏差、OHCodec capability、长时间(2h)稳定性、HTTPS/HLS/鉴权/网络字幕仍未验证。Phase 7 建议先建立实体设备矩阵和受控 HTTPS/HLS fixture,再评估硬解直出和端到端 A/V 指标。
许可边界沿用 MPV LGPL/ISC;本阶段未引入新的第三方依赖或商业资源。
+13
View File
@@ -0,0 +1,13 @@
# PlaybackEngine
`PlaybackEngine` 是主 App 的唯一播放抽象,`MpvPlaybackEngine` 是 Core binding 的适配器,`PlaybackController` 负责页面状态刷新与生命周期编排。
业务层只使用:
- `PlaybackSource { kind, url, headers }`
- `PlaybackState { state, position, duration, media, tracks, metrics }`
- play/pause/stop/release、seek、音量/倍速、音轨/字幕操作
provider 接口暂不绑定 Emby、Jellyfin、百度网盘等服务。HTTP(S) 源支持有限 headers;复杂 header 编辑器、登录与播放列表不在 v0 范围。
默认解码策略为软件解码;`requestedDecoder=software` 是主 App 的固定默认值,OHCodec GL mapper 不在本阶段启用。
+13
View File
@@ -0,0 +1,13 @@
# Playback 公共 API
mp_harmony_playback.h 是上层唯一稳定契约,提供 PlaybackRequest、PlaybackState、Track、MediaInfo、PlaybackMetrics 和 PlaybackError 模型别名。PlaybackRequest 只携带 URL、标题、起始位置、User-Agent、Referer、Cookie、任意 HTTP headers、字幕源、语言偏好、轨道选择、速度和超时。空值或负值按头文件注释表示未设置。
公开控制命令:Play、Pause、TogglePause、SeekAbsolute、SeekRelative、SeekPercent、Stop、SetSpeed、SetVolume、Mute、SetAudioEnabled、SelectAudioTrack、SelectSubtitleTrack、DisableSubtitle、AddSubtitle、RemoveSubtitle、SetSubtitleDelay、SetSubtitleScale、SetSubtitlePosition、SetAssOverride、GetTracks、GetMediaInfo、GetNetworkInfo、GetPlaybackState。
所有控制入口串行化;请求中的字符串在调用返回前复制到 mpv 所需的临时节点,调用者可以立即释放输入内存。session id 随每次 load 增长,旧 playlist event 不得改变新会话状态。
Playback Lab 的 C++ 外壳只做 NAPI/渲染适配,所有请求、控制和状态快照均委托上述 C API;渲染器使用私有句柄桥接,不进入公共 ABI。
当前 OHOS 构建未启用 iconv,外挂字幕 encoding 只接受 UTF-8/UTF8;其他编码通过稳定 SUBTITLE 错误拒绝。
起播 `start_position_ms` 在 `MPV_EVENT_FILE_LOADED` 后执行 exact seek,避免异步 load 竞态;`title` 映射到 `force-media-title`。MediaInfo 同时保留历史 `width`/`height`/`color_space` 字段,并提供 `video_width`/`video_height`/`color_matrix` 别名。NetworkInfo 的 `range_supported` 为 -1/0/1(未知/未观察到 206/观察到 206),`seek_result` 为最近一次 seek 的 `ok` 或脱敏错误。
+14
View File
@@ -0,0 +1,14 @@
# Player lifecycle
产品调用顺序:
```text
create → attachSurface → prepare(source) → play
↘ pause / seekTo / seekBy / tracks / subtitles
stop → prepare 或 release
detachSurface → release
```
`onForeground`、`onBackground`、`onSurfaceAvailable`、`onSurfaceDestroyed` 是显式适配接口。当前后台策略只释放/暂停 Surface 相关资源,不承诺后台播放、PiP 或通知栏控制。Surface 与 player 生命周期解耦:Surface 销毁不会自动销毁 Core player,新的 Surface 可重新 attach。
页面状态以 500ms 轮询读取,错误通过 `errorCode` 与 `lastError` 映射到 UI;首帧、缓冲、seek、音频与 renderer 指标仅在“显示调试统计”打开时显示。
+70
View File
@@ -0,0 +1,70 @@
# HarmonyOS Porting Plan
## 1. 源码扫描结论
MPV 核心播放、demux、filter、subtitle、A/V sync、command、client API 基本跨平台。平台耦合集中在 `meson.build`、`osdep/`、`audio/out/`、`video/out/`、`video/decode/` 与 hwdec interop。当前仓库搜索不到 Harmony/OHOS 代码。
MPV 当前检测逻辑 `posix = not win32`;非 Darwin POSIX 自动选择 `osdep/timer-linux.c`。OHOS 编译器同时定义 `__OHOS__` 与 `__linux__`,因此所有 Linux feature 必须由 Meson probe 或显式 Harmony gate 决定,不能只看 `__linux__`。
## 2. 文件级移植矩阵
| MPV source / file | 当前依赖 API / 行为 | HarmonyOS 替代或复用 | Patch | 风险 | 阶段 | 工作量 |
|---|---|---|---:|---|---|---|
| `meson.build` OS detection | win32/darwin/posix/android/linux 分支 | 增加 `ohos = host_machine.system() == 'ohos'`、`features['harmony']`、config `HAVE_HARMONY` | 是 | 高 | P1-01 | 1–2d |
| `meson.build` platform sources | 非 Darwin POSIX选 Linux timer;Android 单独 EGL/media | 为 OHOS 明确选择 POSIX可复用源、Harmony AO/可选 context;不进入 Android 分支 | 是 | 高 | P1-01 | 1d |
| `osdep/threads-posix.c` | pthread、cond、mutex、TLS | 直接使用 SDK `pthread.h`;线程名由 feature probe | 预计否/小 | 中 | P1-02 | 0.5–1d |
| `osdep/timer-linux.c` | `clock_gettime`、`nanosleep` | API均可用;可先明确选该实现,后续重命名/拆成 POSIX timer 以便 upstream | 小 | 低 | P1-02 | 0.5d |
| `osdep/path-unix.c` | POSIX paths、HOME/XDG 等桌面约定 | 文件 API复用;应用 data/cache 路径最终由宿主注入,不能依赖 HOME | 小/后续 | 中 | P1-03 | 1–2d |
| `osdep/language-posix.c` | locale/environment | 先复用;设备语言由宿主 API注入作为后续增强 | 预计否 | 低 | P1-03 | 0.5d |
| `osdep/subprocess-posix.c` | fork/exec/process | Harmony app 不需要 subprocess;若 probe失败明确选 `subprocess-dummy.c` 并返回 unsupported | 小 | 中 | P1-03 | 0.5d |
| `osdep/terminal-unix.c` | tty/terminal | libmpv build 不需要交互 terminal;cplayer关闭 | 配置 | 低 | P1-01 | 0.2d |
| `input/ipc-unix.c` | Unix domain socket | 首版 libmpv 可关闭/保留按 probe;产品 API不暴露 IPC | 小/配置 | 低 | P1-03 | 0.5d |
| `stream/` + FFmpeg AVIO | POSIX socket、DNS、FFmpeg HTTP/TLS | socket/DNS复用;HTTP/HLS由 FFmpeg;TLS 使用交叉构建 OpenSSL | MPV 否 | 高 | P1-04 | 3–5d |
| `demux/demux_lavf.c` | libavformat | 直接复用官方 FFmpeg OHOS build | 否 | 中 | P1-04 | 1d 验证 |
| `sub/`、`sub/osd_libass.c` | libass、字体栈 | 直接复用;字体发现/应用字体目录需配置验证 | 小/配置 | 中 | P1-05 | 2–3d |
| `include/mpv/client.h` | libmpv C API | 直接复用仅作内部依赖;最终 Velix API再封装 | 否 | 低 | P1 | 0.5d |
| `include/mpv/render*.h` | OpenGL / software public render API | 直接使用 OpenGL API;宿主提供 GLES proc resolver/FBO | 否 | 中 | P2-01 | 2–4d lab glue |
| `video/out/vo_libmpv.c` | libmpv render bridge | 直接复用 | 预计否 | 中 | P2-01 | 1d 验证 |
| `video/out/opengl/libmpv_gl.c` | OpenGL Render API | 直接复用 GLES/EGL context;验证所需 GL symbols | 小/预计否 | 中 | P2-01 | 2d |
| `video/out/opengl/context*.c` | Android/X11/Wayland/DRM/Windows context | 首版 libmpv plain-gl 不新增;如 MPV自管 context,再新增 `context_harmony.c` | 后续 | 中 | P2-02 | 3–5d |
| `video/out/vulkan/context.c` | libplacebo/Vulkan context registry | 若采用 Vulkan新增 `context_harmony.c`,调用 `vkCreateSurfaceOHOS` | 后续是 | 高 | P4 | 7–12d |
| `audio/out/ao.c` | AO driver registry | 注册 `audio_out_harmony` | 是 | 低 | P2-03 | 0.5d |
| `audio/out/ao_aaudio.c` | Android AAudio callback模型 | 仅作结构参考;实际新增 `ao_harmony.c` 使用 OHAudio | 新文件 | 高 | P2-03 | 5–8d |
| `audio/out/buffer.c` | MPV AO buffering | 直接复用,由 OHAudio callback消费 | 预计否 | 中 | P2-03 | 1–2d 验证 |
| `video/fmt-conversion.c` | FFmpeg pixfmt↔MPV imgfmt | 增加 `AV_PIX_FMT_OHCODEC` 映射前先确认 MPV image semantics | 后续是 | 高 | P3-02 | 1–2d |
| `video/decode/vd_lavc.c` | hw device选择、frames | 加入 `AV_HWDEVICE_TYPE_OHCODEC` 并传递 native window/device ctx | 后续是 | 高 | P3-02 | 3–6d |
| `video/out/gpu/hwdec.c` | GPU interop registry | 注册 Harmony interop driver,仅在真实 buffer import 方案明确后 | 后续是 | 极高 | P3-03 | 7–15d |
| `video/out/hwdec/` | VAAPI/DRM/VT/Android interop | 新增隔离的 `hwdec_ohcodec.c`,不散落 OHOS ifdef | 后续是 | 极高 | P3-03 | 10–20d |
| `video/out/opengl/egl_helpers.c` | 通用 EGL config/context helpers | 优先复用;验证 GLES profile、10-bit config、context loss | 可能小 | 中 | P2-01 | 2d |
| `misc/io_utils.c` / poll wrapper | poll/FD helper | POSIX poll 复用;epoll不作为无证据优化 | 预计否 | 低 | P1-02 | 0.5d |
| logging (`common/msg.c`) | stderr/terminal | 首编译复用;lab阶段加 HiLog adapter,不能替换核心 logger | 后续小 | 低 | P2 | 1–2d |
## 3. 分阶段执行
### P1:First Build
1. P1-01:Meson 识别 OHOS,构建只产 libmpv。
2. P1-02:逐项编译 POSIX 基础层;每个 blocker 单独记录。
3. P1-03:明确 subprocess/IPC/path 不支持或可复用边界。
4. P1-04:构建 FFmpeg software decode + network + TLS。
5. P1-05:构建 libass/font stack、libplacebo,链接 libmpv。
门禁:AArch64 libmpv 真实产物、导出符号、依赖清单、无 Android API、无宿主机库。
### P2:Playback Lab 基线
XComponent + EGL + libmpv OpenGL Render API;新增 OHAudio AO;URL/local file、play/pause/seek/stop/release、日志和基础 metrics。无正式产品 UI。
### P3:硬件解码
先验证 FFmpeg `h264_oh` / `hevc_oh`,再决定 direct surface 或 GPU import。所有优化必须对比软件解码基线。
### P4:定点性能与 Vulkan
只有 Benchmark 证明 GLES/上传或合成是瓶颈时,研究 Vulkan/HDR/zero-copy;每个 patch 必须有前后数据。
## 4. 可直接复用与必须新增
可直接复用:libmpv client/render API、player core、demux、software decode glue、filters、A/V sync、subtitle/libass、playlist/property/command、POSIX threads/timer/socket/DNS/poll/mmap 的已验证子集、OpenGL renderer。
必须新增:Harmony Meson detection、OHAudio AO、Playback Lab 的 XComponent/EGL native glue。硬解阶段预计新增 OHCodec image/device/interoperability backend。Vulkan context 不是首版必需。
+35
View File
@@ -0,0 +1,35 @@
# Risk Assessment
## Top 10
| # | 风险 | 可能性 / 影响 | 证据 | 缓解与验收 |
|---:|---|---|---|---|
| 1 | MPV 无 OHOS 平台检测,OHOS 被泛 POSIX/Linux 逻辑误分类 | 高 / 高 | 当前 `meson.build` 无 harmony/ohos;编译器同时定义 `__linux__` | 显式 `features['harmony']`;每个 Linux feature 用 compile/link probe |
| 2 | 交叉依赖链复杂且 pkg-config 污染宿主库 | 高 / 高 | 本机无 Meson/Ninja/pkg-config;MPV 强依赖 FFmpeg/libplacebo/libass | 单一 staging prefix、清空默认 `PKG_CONFIG_PATH`、产物 readelf 审计 |
| 3 | HTTPS/TLS 无可直接复用的 FFmpeg Harmony backend | 高 / 高 | SDK有 `libnet_ssl.so`,FFmpeg不使用它 | 交叉构建 OpenSSL;真机证书/SNI/Range/HLS 测试 |
| 4 | Surface 生命周期与 GL 线程竞态导致黑屏/崩溃 | 高 / 高 | XComponent异步 create/change/destroy,EGL context 有线程归属 | 单一 render thread 状态机;销毁屏障;旋转/后台压力测试 |
| 5 | OHAudio callback 与 MPV buffer/clock 语义不匹配 | 中高 / 高 | MPV无 Harmony AO | realtime-safe callback;timestamp/delay 校准;seek/route switch 测试 |
| 6 | FFmpeg OHCodec虽已 upstream,但 MPV完全不识别 OHCODEC | 高 / 高 | MPV无 `AV_PIX_FMT_OHCODEC` / `AV_HWDEVICE_TYPE_OHCODEC` | 软件基线后小步接入;先独立验证 FFmpeg backend |
| 7 | Direct Surface decode 绕过 MPV shader/ASS/色彩管理 | 高 / 高 | FFmpeg OHCodec向 window直接 render | 优先研究 OHNativeBuffer GPU import;direct surface仅作验证路径 |
| 8 | 4K/10-bit/HDR 能力因机型/驱动不同,SDK存在不等于设备支持 | 高 / 高 | capability API要求运行时查询 | 保存设备矩阵、profile/level/format/extension;不做泛化承诺 |
| 9 | GPL/LGPL 与静态链接/FFmpeg选项污染商业发布 | 中高 / 极高 | MPV默认 GPLv2+;开关不自动产生 LGPL授权 | 默认 GPL 管理;保存完整 build manifest;发布前法务审计 |
| 10 | arm64 OHOS ABI/符号/打包规则与普通 Linux不同 | 中 / 高 | target 是 `aarch64-linux-ohos`,系统库命名含 `.z.so` | 小探针→依赖→libmpv;检查 SONAME、NEEDED、符号可见性和 HAP 装载 |
## 其他风险
- 字体发现和字体包体积影响 ASS 首帧及中日韩字幕正确性。
- 网络切换、代理、IPv6、慢速 Range、HLS discontinuity 对稳定性影响大。
- MPV master 与 FFmpeg master 同时变化,需锁 commit 并记录组合。
- API 24 SDK header含更低 API接口,但实际目标机系统版本仍要匹配。
- Vulkan headers/loader存在不代表驱动质量和 HDR swapchain 可用。
## 决策门禁
| 决策 | 必需证据 |
|---|---|
| 进入 Phase 1 | Phase 0 文档完成;origin/upstream/branch明确;依赖与 license 策略确认 |
| 开始 Playback Lab | libmpv arm64真实产物及 build report |
| 启用 OHCodec | software decode 基线 + 官方 FFmpeg OHCodec 独立验证 |
| 宣称 low-copy/zero-copy | 帧路径、buffer ownership、GPU import、同步与 copy 测量证据 |
| 采用 Vulkan | GLES benchmark证明瓶颈且 Vulkan原型在目标机显著改善 |
| 商业发布 | 实际链接清单、许可证正文、源码义务、法务审核全部完成 |
+20
View File
@@ -0,0 +1,20 @@
# SDK packaging
使用 `scripts/package-velix-core-sdk.sh` 生成 staging 目录。脚本按 profile 选择已构建的 `libmpv`,编译 binding,复制公开 headers、运行库、许可证,并生成包含 API 版本、commit、profile、尺寸和 SHA-256 的 `metadata/build-info.json`。
支持 profile:
```sh
./scripts/package-velix-core-sdk.sh --profile default
./scripts/package-velix-core-sdk.sh --profile software
./scripts/package-velix-core-sdk.sh --profile ffmpeg-harmony
```
ABI 检查重点:
- `libvelix_core_binding.so` 只导出 3 个 `velix_core_get_*` ABI 符号;NAPI 模块描述由系统注册段加载,不把内部 C++ 符号作为 ABI;
- `libmpv.so.2` 保持自身运行时导出,不将其头文件类型传给主 App;
- 所有动态依赖必须存在于 `lib/arm64-v8a` 或 OpenHarmony 系统库中;
- staging 与构建目录不应进入 Git,发布包必须保留 LGPL/GPL/第三方声明。
验证:`./scripts/verify-velix-core-sdk.sh`。
+15
View File
@@ -0,0 +1,15 @@
# Seek Pipeline
```text
seek request
-> mpv seek command
-> SEEK event / demux recovery
-> VIDEO_RECONFIG
-> PLAYBACK_RESTART
-> first video frame
-> first render / EGL swap
```
`seekCommandLatencyMs` 是 API request 到 command 提交的耗时;`seekRecoveryMs` 是 request 到 `PLAYBACK_RESTART` 的耗时;`seekFirstVisibleFrameMs` 是 request 到首个成功 swap 的耗时。若 MPV 没有产生对应事件,字段为 -1。每次新 seek 会重置上一次 seek 字段,不把旧会话值带入新会话。
模拟器 runner 使用 `+10s` 相对 seek,50 次中 49 次产生 recovery,1 次返回稳定 `SEEK / error running command`;该失败保留在 JSON,不被隐藏。精确/百分比 seek 入口仍由公共 API 暴露,但短样片不足以让输入框自动化可靠覆盖,需更长受控 fixture。
+19
View File
@@ -0,0 +1,19 @@
# Startup Pipeline
```text
session setup
-> mpv_create / mpv_initialize
-> load command
-> START_FILE
-> network open / first byte (network only)
-> FILE_LOADED (demux ready)
-> VIDEO_RECONFIG / AUDIO_RECONFIG
-> render context ready
-> first video frame available
-> first render begin/end
-> first EGL swap (startupTotalMs)
```
时间线在 core 内以 `clock_gettime(CLOCK_MONOTONIC)` 记录,所有阶段只写首次发生值。文件输入没有网络阶段时保持 -1;网络阶段只在真实 MPV log/event 证据出现时写入。`startupTotalMs` 在首个成功 `eglSwapBuffers` 写入,`firstFrameMs` 保留外壳兼容字段并与首 swap 对齐。
音频 renderer start 和首 callback 由 OHAudio stats poll 观察;这两个字段可能晚于真实 callback,因此用于阶段诊断,不替代设备级 audio trace。
+7
View File
@@ -0,0 +1,7 @@
# 字幕管线
内部字幕和外部字幕统一由 mpv/libass 管线处理。外部源通过 sub-add 加入当前 session,可选中或移除,不销毁播放 session;sub-remove 仅移除指定轨道。language/title 通过 sub-add 元数据传入,forced/default 映射为 mpv 的同名 track flags。
目标格式:SRT、ASS/SSA、WebVTT;ASS 默认保留原样式与 libass override 语义。上层可开关、选择字幕、调整 delay、scale、position、override(具体控件按实际可用属性开放)。
当前 OHOS 构建未启用 iconv,因此只接受 UTF-8/UTF8;GBK、Big5、Shift-JIS 等输入明确返回稳定 SUBTITLE 错误,不静默走 Latin-1 fallback。启用并验证 iconv 后再扩展编码白名单。Playback Lab 同时提供网络 URL 与本地 file 路径入口。
+5
View File
@@ -0,0 +1,5 @@
# Track 模型
上层只接收固定 Track 字段:id、type、title、language、codec、default、forced、selected、external。mpv_node 只在实现层解析,不跨 NAPI/C API 边界。
音频通过 aid 选择,字幕通过 sid 选择或设置为 no。语言偏好使用 mpv 的 alang/slang,可接受 zh、zho、chi、en、eng、ja、jpn 等媒体元数据值。不可用的字段保持空值或 false。
+10
View File
@@ -0,0 +1,10 @@
# Velix 主 App 集成
主 App 工程为 `../Velix`,当前是独立的 Stage Model/API 24 工程,目录本身没有 Git 元数据。集成脚本提供两种依赖模式:
- `scripts/sync-velix-core-sdk.sh --local`:从相邻 Core 重新构建并 staging,适合本地开发。
- `scripts/sync-velix-core-sdk.sh --release --sdk PATH`:消费已经发布的 SDK 目录,适合 release 验证。
`scripts/build-velix-app.sh` 会先同步 SDK,再执行 `devecocli build`。主 App 只将 `libvelix_core_binding.so`、`libmpv.so.2` 与 `libc++_shared.so` 放进 `entry/libs/arm64-v8a`,不引入 Core/FFmpeg 源码。
ArkTS 入口为 `domain/playback/PlaybackEngine.ets` → `MpvPlaybackEngine.ets` → native binding;页面为 `pages/PlayerPage.ets`。XComponent 的 `libraryname` 为 `velix_core_binding`,Surface 回调由 Core binding 注册,页面不重复实现 EGL renderer。
+338
View File
@@ -0,0 +1,338 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+501
View File
@@ -0,0 +1,501 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Moe Ghoul>, 1 April 1990
Moe Ghoul, President of Vice
That's all there is to it!
+6
View File
@@ -0,0 +1,6 @@
# License texts
- `GPL-2.0-or-later.txt`:MPV 默认构建所适用的 GPL v2 文本。
- `LGPL-2.1-or-later.txt`:MPV 合规 LGPL-only 构建及 FFmpeg 默认构建所涉及的 LGPL v2.1 文本。
Phase 1 已锁定第三方源码,实际版本、原始许可证文件位置和源码仓库见 `LICENSE_AUDIT.md`。正式发行包必须从对应锁定 commit 收集原始许可证与版权声明;本目录当前只保存 MPV/FFmpeg 核心 GPL/LGPL 正文,不得作为完整 Release notices 使用。
+38
View File
@@ -0,0 +1,38 @@
# License Audit
审计基线:MPV `182fa6ca49f455cadb884858f386e2f00540aeb7`,Phase 1 实际构建。本文是工程记录,不构成法律意见。
## Conclusion
Velix-MPV-Core 与最终 `libmpv` 按 MPV 默认 GPL-2.0-or-later 路线管理。独立仓库及私有 Gitea 不改变 upstream 义务,也不使许可证自动隔离。当前不得宣称 LGPL-only。
FFmpeg 实际配置为 `CONFIG_GPL=0`、`CONFIG_VERSION3=0`、`CONFIG_NONFREE=0`。所有第三方库静态进入 `libmpv`,因此正式发布仍需满足 LGPL 组件的源码、版权、修改说明与可重新链接要求。
| Component | Version / Commit | License | Feature | Link | Modified | Source Location |
|---|---|---|---|---|---|---|
| MPV | 0.41.0-UNKNOWN / `182fa6ca49` | GPL-2.0-or-later default | libmpv, plain-gl, subtitles, network | Shared output | Yes, OHOS Meson detection | `upstream` remote / current repository |
| FFmpeg | `3acec0a1` | LGPL-2.1-or-later configuration | decode, demux, filter, network, HTTPS, OHCodec | Static | No | `https://git.ffmpeg.org/ffmpeg.git` |
| libplacebo | 7.360.1 / `cee9b076` | LGPL-2.1-or-later | core/shader generation | Static | No | `https://code.videolan.org/videolan/libplacebo.git` |
| libass | 0.17.5 / `4a05d812` | ISC | subtitle rendering | Static | No | `https://github.com/libass/libass.git` |
| FreeType | 2.14.3 / `0a0221a1` | FreeType License or GPL-2.0 | font rasterization | Static | No | `https://github.com/freetype/freetype.git` |
| HarfBuzz | 14.4.0 / `36cb489c` | MIT | text shaping | Static | No | `https://github.com/harfbuzz/harfbuzz.git` |
| FriBidi | 1.0.16 / `68162bab` | LGPL-2.1-or-later | bidirectional text | Static | No | `https://github.com/fribidi/fribidi.git` |
| OpenSSL | 3.6.4 / `d3c1b116` | Apache-2.0 | FFmpeg TLS/HTTPS | Static | No | `https://github.com/openssl/openssl.git` |
| zlib | SDK 1.2.13 | Zlib | FFmpeg/FreeType/MPV compression | System dynamic | No | HarmonyOS Native SDK |
| HarmonyOS Native SDK stubs | 6.1.1.125 / API 24 | SDK terms and header notices | libc, zlib, OHCodec, native window | System dynamic | No | DevEco Studio SDK |
## Phase 4 additions
Phase 4 新增的 MPV 代码(`video/harmony_hwdec.[ch]`、`video/out/hwdec/hwdec_ohcodec.c`、Playback Lab diagnostics)沿用本仓库 MPV GPL-2.0-or-later 路线;没有复制 FFmpeg 源码,也没有修改锁定的 FFmpeg commit。OHCodec capability、NativeWindow 和 NativeBuffer 仅通过 HarmonyOS Native SDK 的公开头文件/系统库使用。`harmony-ohcodec-gl` 是 disabled-by-default 的实验性 API probe,当前因 upstream output ownership blocker 不构成产品 interop 实现。
## Actual Link State
`libmpv.so.2.5.0` 的 `DT_NEEDED` 仅包含 `libc.so`、`libz.so`、`libnative_media_vdec.so`、`libnative_media_venc.so`、`libnative_media_codecbase.so`、`libnative_media_core.so`、`libnative_window.so`。其余依赖为静态链接。未发现 FFmpeg nonfree、Homebrew 或 macOS target library。
## Release Gates
1. 保存锁定 commit、构建选项、`config.h`、link state 与 archive 清单。
2. 随发行物提供对应许可证正文、版权声明、源码获取方式和修改说明。
3. 为静态 LGPL 组件提供满足许可证要求的重新链接材料。
4. 复核 OpenSSL、FFmpeg、MPV GPLv2+ 组合以及 HarmonyOS SDK redistributable 边界。
5. 商业发布前完成法务审核;`THIRD_PARTY_NOTICES.md` 当前仍不是最终 Release notice。
+18
View File
@@ -0,0 +1,18 @@
# Velix-MPV-Core Memory
- 项目:独立 HarmonyOS MPV 播放内核,禁止混入 Velix 主应用源码。
- 当前阶段:Phase 5 CODE COMPLETE / emulator-network validation pending;GPU INTEROP MAPPER 未进入;默认软件解码与网络播放公共 API 已构建验证。
- 基线:MPV `182fa6ca49f455cadb884858f386e2f00540aeb7`,分支 `harmony-port`。
- SDK:OpenHarmony API 24 / 6.1.1.125 / `aarch64-linux-ohos` / Clang 15.0.4。
- 关键结论:首版 GLES+EGL+libmpv Render API;OHAudio AO;官方 FFmpeg OHCodec 路线。
- Phase 1 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,AArch64 ELF,OpenGL Render API symbols 完整;当前默认重建大小约 34.3 MB。
- 远程:`origin` 为私有 Gitea 仓库,默认分支 `harmony-port`;`upstream` 为 MPV 官方仓库。
- Phase 4.5:独立 `../Velix-FFmpeg-Harmony`,分支 `harmony-ohcodec-export`,远程 tip `990614364f`;MPV 通过 `scripts/build-mpv-ohos-ffmpeg-harmony.sh` 选择其 prefix。
- Phase 2:`playback_lab/` 独立 HAP,XComponent/OHNativeWindow/EGL/libmpv/软件解码已通;实体设备待验。
- Phase 3:`audio/out/ao_harmony.c` + OHAudio pull callback、pause/flush/clock/metrics 已通;实体机路由/A-V sync/压力待验。
- Phase 4:`IMGFMT_OHCODEC`、`hwdec=ohcodec`、API24 capability adapter、direct-surface experimental driver、软件构建矩阵、Playback Lab 诊断已提交;FFmpeg 保持 upstream。
- Phase 4 边界:官方 OHCodec hwcontext/decoder 仅提供 surface 输出,NativeBuffer→EGL 仅做禁用默认 API probe,未宣称零拷贝/HDR/真实硬件帧;详见 `DOCS/OHCODEC_GPU_INTEROP_DESIGN.md`。
- Phase 4.5 边界:FFmpeg export 模式不调用 RenderOutputBuffer,NativeBuffer/accessor/refcount/build probes 已完成;crop/fence/真实硬解仍需设备验证,`harmony-ohcodec-gl` 继续 disabled。
- Phase 5:include/mpv/harmony_playback.h + player/harmony_playback.c 提供不暴露 mpv 类型的稳定 PlaybackRequest/Track/Media/Network/State/Error API;Playback Lab 已接入请求 headers、脱敏、轨道、字幕和诊断面板。
- Phase 5 构建:默认、software、FFmpeg-Harmony 三套 AArch64 libmpv 构建及 Native/ArkTS 编译通过;HAP PackageHap 因当前环境缺 Java Runtime 未完成,网络/HLS/实体设备运行待验证。
- 索引:`memory/projects.md`、`memory/infra.md`、`memory/lessons.md`、`memory/2026-08-28.md`。
+20
View File
@@ -0,0 +1,20 @@
# Third-Party Notices
Velix-MPV-Core 基于 MPV upstream Git 历史维护,当前默认按 GNU GPL v2 or later 管理。MPV 完整版权与许可证说明保留在 `Copyright`、`LICENSE.GPL`、`LICENSE.LGPL` 与 `LICENSES/`。
Phase 1 实际构建包含:
| Component | Version / Commit | License | Link | Modified |
|---|---|---|---|---|
| FFmpeg | `3acec0a1` | LGPL-2.1-or-later configuration | Static | No |
| libplacebo | 7.360.1 | LGPL-2.1-or-later | Static | No |
| libass | 0.17.5 | ISC | Static | No |
| FreeType | 2.14.3 | FreeType License / GPL-2.0 dual | Static | No |
| HarfBuzz | 14.4.0 | MIT | Static | No |
| FriBidi | 1.0.16 | LGPL-2.1-or-later | Static | No |
| OpenSSL | 3.6.4 | Apache-2.0 | Static | No |
| zlib | HarmonyOS SDK 1.2.13 | Zlib | System dynamic | No |
源码仓库、完整 commit、启用功能与实际链接状态见 `UPSTREAM.md`、`LICENSE_AUDIT.md`、`DOCS/PHASE1_DEPENDENCY_MATRIX.md`。依赖源码由 `scripts/fetch-phase1-sources.sh` 获取,构建过程不修改其源码。
本文是 Phase 1 工程清单,不是最终发行 notice。正式发布必须补齐各组件原始版权声明、许可证正文、源码交付/获取方式、修改说明及 LGPL 静态链接所需的重新链接材料。
+48
View File
@@ -0,0 +1,48 @@
# Upstream 基线
| 项目 | 值 |
|---|---|
| MPV Repository | `https://github.com/mpv-player/mpv.git` |
| MPV Current Commit | `182fa6ca49f455cadb884858f386e2f00540aeb7` |
| Commit Date | 2026-08-26 |
| MPV Version / Tag | `MPV_VERSION=0.41.0-UNKNOWN`;`git describe=git-release-292-g182fa6ca49`;HEAD 无 tag |
| Fork Date | 2026-08-28 |
| Development Branch | `harmony-port` |
| HarmonyOS Target | HarmonyOS NEXT / OpenHarmony Native API 24 |
| HarmonyOS SDK | DevEco Studio 内置 OpenHarmony SDK `6.1.1.125`,API 24 |
| Target Architecture | `arm64-v8a` / `aarch64-linux-ohos` |
| Compiler | OHOS Clang `15.0.4` (`99548401bd80576251755b185ab95945e5fe7884`) |
| Build System | MPV Meson `>=1.3.0` + Ninja;依赖使用各自原生构建系统 |
Velix-MPV-Core 是基于 MPV upstream Git 历史维护的独立派生仓库,不是 GitHub Fork。仓库完整保留 upstream history、基线 commit、MPV 版权与许可证文件,并持续保留 `upstream` remote。
## FFmpeg 基线
| 项目 | 值 |
|---|---|
| Repository | `https://git.ffmpeg.org/ffmpeg.git` |
| Commit | `3acec0a1af2dda0a0838689b8b8649e7deb080a0` |
| Commit Date | 2026-08-28 |
| git describe | `3acec0a`(该 commit 无可达 tag) |
| License Mode | LGPL-2.1-or-later;`CONFIG_GPL=0`、`CONFIG_VERSION3=0`、`CONFIG_NONFREE=0` |
| Build | 静态 PIC;禁用 programs、docs、debug、avdevice 与 autodetect |
| Enabled | network、OpenSSL、zlib、OHCodec |
| OHCodec | `AV_HWDEVICE_TYPE_OHCODEC`、`AV_PIX_FMT_OHCODEC`、H.264/HEVC OH decoder/encoder 已编译 |
FFmpeg configure 参数由 `scripts/build-ffmpeg-ohos.sh` 固定。当前不创建 Velix FFmpeg fork;只有 upstream 出现需要长期维护的真实 blocker 时才重新评估。
## 远程状态
- `upstream` 已指向 MPV 官方仓库。
- `origin` 已指向独立仓库 `https://git.leee.xin/leeexin/Velix-MPV-Core.git`。
- 远程默认分支为 `harmony-port`,同时保留与官方基线一致的 `master` 和 89 个官方标签。
```sh
git fetch upstream
git fetch origin
git log --oneline --decorate harmony-port..upstream/master
git diff harmony-port...upstream/master
git range-diff <old-upstream>..harmony-port upstream/master..<rebased-candidate>
```
基线更新必须先检查 `git log`、`git diff` 和适用的 `git range-diff`,再评估使用 merge 或 rebase,并同步更新本文件。禁止直接强制覆盖 `harmony-port`。
+4
View File
@@ -55,6 +55,7 @@ extern const struct ao_driver audio_out_pcm;
extern const struct ao_driver audio_out_lavc;
extern const struct ao_driver audio_out_sdl;
extern const struct ao_driver audio_out_aaudio;
extern const struct ao_driver audio_out_harmony;
static const struct ao_driver * const audio_out_drivers[] = {
// native:
@@ -64,6 +65,9 @@ static const struct ao_driver * const audio_out_drivers[] = {
#if HAVE_AAUDIO
&audio_out_aaudio,
#endif
#if HAVE_HARMONY_AUDIO
&audio_out_harmony,
#endif
#if HAVE_AUDIOUNIT
&audio_out_audiounit,
#endif
+446
View File
@@ -0,0 +1,446 @@
/* HarmonyOS OHAudio output for the MPV pull-based audio contract. */
#include <stdatomic.h>
#include <stdint.h>
#include <string.h>
#include "config.h"
#include "ao.h"
#include "audio/format.h"
#include "audio/out/ao_harmony.h"
#include "common/msg.h"
#include "internal.h"
#include "osdep/threads.h"
#include "osdep/timer.h"
#if HAVE_HARMONY_AUDIO
#include <ohaudio/native_audiostreambuilder.h>
#include <ohaudio/native_audiorenderer.h>
#include <multimedia/native_audio_channel_layout.h>
struct priv {
OH_AudioRenderer *renderer;
mp_thread clock_thread;
bool clock_thread_valid;
atomic_bool stop_clock;
atomic_bool callback_enabled;
atomic_int callbacks_inflight;
_Atomic int64_t submitted_frames;
_Atomic int64_t played_frames;
_Atomic int64_t raw_played_frames;
_Atomic int64_t timestamp_origin_raw;
_Atomic int64_t audio_timestamp_ns;
_Atomic int64_t estimated_delay_ms;
_Atomic int64_t buffer_starvation;
_Atomic int64_t callback_count;
int32_t latency_ms;
int32_t frame_size;
int32_t channel_count;
uint64_t channel_layout;
};
static _Atomic int32_t g_state = MPV_HARMONY_AUDIO_DISABLED;
static _Atomic int32_t g_sample_rate;
static _Atomic int32_t g_sample_format;
static _Atomic int32_t g_channels;
static _Atomic uint64_t g_channel_layout;
static _Atomic int64_t g_submitted_frames;
static _Atomic int64_t g_played_frames;
static _Atomic int64_t g_audio_timestamp_ns;
static _Atomic int64_t g_estimated_delay_ms;
static _Atomic int64_t g_buffer_starvation;
static _Atomic int64_t g_system_underrun;
static _Atomic int64_t g_callback_count;
static _Atomic int64_t g_audio_flush_count;
static _Atomic int64_t g_seek_audio_reset_count;
static void reset_stats(void)
{
atomic_store(&g_state, MPV_HARMONY_AUDIO_DISABLED);
atomic_store(&g_sample_rate, 0);
atomic_store(&g_sample_format, AF_FORMAT_UNKNOWN);
atomic_store(&g_channels, 0);
atomic_store(&g_channel_layout, 0);
atomic_store(&g_submitted_frames, 0);
atomic_store(&g_played_frames, 0);
atomic_store(&g_audio_timestamp_ns, 0);
atomic_store(&g_estimated_delay_ms, 0);
atomic_store(&g_buffer_starvation, 0);
atomic_store(&g_system_underrun, 0);
atomic_store(&g_callback_count, 0);
atomic_store(&g_audio_flush_count, 0);
atomic_store(&g_seek_audio_reset_count, 0);
}
static void set_state(int32_t state)
{
atomic_store(&g_state, state);
}
static void log_result(struct ao *ao, const char *operation, OH_AudioStream_Result result)
{
MP_ERR(ao, "%s failed: %d\n", operation, result);
}
static OH_AudioStream_SampleFormat sample_format_for(int format)
{
return af_fmt_is_float(format) ? AUDIOSTREAM_SAMPLE_F32LE : AUDIOSTREAM_SAMPLE_S16LE;
}
static void fill_silence(struct ao *ao, void *data, int samples)
{
if (samples > 0)
af_fill_silence(data, (size_t)samples * ao->sstride, ao->format);
}
static OH_AudioData_Callback_Result write_data(OH_AudioRenderer *renderer, void *user_data,
void *audio_data, int32_t audio_data_size)
{
(void)renderer;
struct ao *ao = user_data;
struct priv *p = ao->priv;
atomic_fetch_add(&p->callbacks_inflight, 1);
int64_t callback_number = atomic_fetch_add(&p->callback_count, 1) + 1;
atomic_fetch_add(&g_callback_count, 1);
int stride = ao->sstride;
if (audio_data == NULL || audio_data_size <= 0 || stride <= 0) {
atomic_fetch_sub(&p->callbacks_inflight, 1);
return AUDIO_DATA_CALLBACK_RESULT_INVALID;
}
int samples = audio_data_size / stride;
int remainder = audio_data_size - samples * stride;
if (!atomic_load(&p->callback_enabled) || samples <= 0) {
memset(audio_data, 0, (size_t)audio_data_size);
atomic_fetch_sub(&p->callbacks_inflight, 1);
return AUDIO_DATA_CALLBACK_RESULT_VALID;
}
if (callback_number == 1)
MP_VERBOSE(ao, "OHAudio first callback: %d frames, %d bytes\n", samples, audio_data_size);
void *planes[1] = {audio_data};
int64_t end_time = mp_time_ns() + MP_TIME_MS_TO_NS(p->latency_ms);
end_time += MP_TIME_S_TO_NS(samples / (double)ao->samplerate);
int got = ao_read_data(ao, planes, samples, end_time, NULL, true, false);
if (got < samples) {
fill_silence(ao, (char *)audio_data + (size_t)got * stride, samples - got);
atomic_fetch_add(&p->buffer_starvation, 1);
atomic_fetch_add(&g_buffer_starvation, 1);
}
if (remainder > 0)
memset((char *)audio_data + (size_t)samples * stride, 0, (size_t)remainder);
atomic_fetch_add(&p->submitted_frames, samples);
atomic_fetch_add(&g_submitted_frames, samples);
atomic_fetch_sub(&p->callbacks_inflight, 1);
return AUDIO_DATA_CALLBACK_RESULT_VALID;
}
static void renderer_error(OH_AudioRenderer *renderer, void *user_data,
OH_AudioStream_Result error)
{
(void)renderer;
struct ao *ao = user_data;
log_result(ao, "renderer error callback", error);
set_state(MPV_HARMONY_AUDIO_ERROR);
ao_request_reload(ao);
}
static void output_device_changed(OH_AudioRenderer *renderer, void *user_data,
OH_AudioStream_DeviceChangeReason reason)
{
(void)renderer;
struct ao *ao = user_data;
MP_VERBOSE(ao, "output device changed: %d\n", reason);
ao_request_reload(ao);
}
static MP_THREAD_VOID clock_loop(void *arg)
{
struct ao *ao = arg;
struct priv *p = ao->priv;
mp_thread_set_name("ao/harmony-clock");
while (!atomic_load(&p->stop_clock)) {
int64_t played = 0;
int64_t timestamp = 0;
if (OH_AudioRenderer_GetAudioTimestampInfo(p->renderer, &played, &timestamp) == AUDIOSTREAM_SUCCESS) {
atomic_store(&p->raw_played_frames, played);
int64_t origin = atomic_load(&p->timestamp_origin_raw);
int64_t effective_played = played > origin ? played - origin : 0;
atomic_store(&p->played_frames, effective_played);
atomic_store(&p->audio_timestamp_ns, timestamp);
atomic_store(&g_played_frames, effective_played);
atomic_store(&g_audio_timestamp_ns, timestamp);
int64_t submitted = atomic_load(&p->submitted_frames);
int64_t pending = submitted > effective_played ? submitted - effective_played : 0;
int64_t delay = p->latency_ms + pending * 1000 / MPMAX(1, ao->samplerate);
atomic_store(&p->estimated_delay_ms, delay);
atomic_store(&g_estimated_delay_ms, delay);
}
mp_sleep_ns(MP_TIME_MS_TO_NS(20));
}
MP_THREAD_RETURN();
}
static OH_AudioStream_Result create_renderer(struct ao *ao, int format)
{
struct priv *p = ao->priv;
p->renderer = NULL;
OH_AudioStreamBuilder *builder = NULL;
OH_AudioStream_Result result = OH_AudioStreamBuilder_Create(&builder, AUDIOSTREAM_TYPE_RENDERER);
if (result != AUDIOSTREAM_SUCCESS || builder == NULL) {
log_result(ao, "AUDIO_BUILDER_FAILED", result);
return result;
}
OH_AudioStream_SampleFormat sample_format = sample_format_for(format);
OH_AudioChannelLayout layout = p->channel_count == 1 ? CH_LAYOUT_MONO : CH_LAYOUT_STEREO;
result = OH_AudioStreamBuilder_SetSamplingRate(builder, ao->samplerate);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetChannelCount(builder, p->channel_count);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetChannelLayout(builder, layout);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetSampleFormat(builder, sample_format);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetEncodingType(builder, AUDIOSTREAM_ENCODING_TYPE_RAW);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetLatencyMode(builder, AUDIOSTREAM_LATENCY_MODE_NORMAL);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetRendererInfo(builder, AUDIOSTREAM_USAGE_MOVIE);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetRendererWriteDataCallback(builder, write_data, ao);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetRendererOutputDeviceChangeCallback(builder,
output_device_changed, ao);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_SetRendererErrorCallback(builder, renderer_error, ao);
if (result == AUDIOSTREAM_SUCCESS)
result = OH_AudioStreamBuilder_GenerateRenderer(builder, &p->renderer);
OH_AudioStreamBuilder_Destroy(builder);
if (result != AUDIOSTREAM_SUCCESS || p->renderer == NULL) {
if (p->renderer != NULL) {
OH_AudioRenderer_Release(p->renderer);
p->renderer = NULL;
}
log_result(ao, "AUDIO_RENDERER_CREATE_FAILED", result);
return result;
}
if (OH_AudioRenderer_SetVolume(p->renderer, 1.0f) != AUDIOSTREAM_SUCCESS)
MP_WARN(ao, "renderer unity volume could not be set\n");
if (OH_AudioRenderer_GetFrameSizeInCallback(p->renderer, &p->frame_size) != AUDIOSTREAM_SUCCESS)
p->frame_size = 0;
if (OH_AudioRenderer_GetLatency(p->renderer, AUDIOSTREAM_LATENCY_TYPE_ALL, &p->latency_ms) != AUDIOSTREAM_SUCCESS)
p->latency_ms = 0;
MP_VERBOSE(ao, "OHAudio renderer ready: %d Hz, %d channels, %s, frame_size=%d, latency=%d ms\n",
ao->samplerate, p->channel_count, af_fmt_to_str(format), p->frame_size, p->latency_ms);
return AUDIOSTREAM_SUCCESS;
}
static int init(struct ao *ao)
{
struct priv *p = ao->priv;
reset_stats();
atomic_init(&p->stop_clock, false);
atomic_init(&p->callback_enabled, false);
atomic_init(&p->callbacks_inflight, 0);
atomic_init(&p->submitted_frames, 0);
atomic_init(&p->played_frames, 0);
atomic_init(&p->raw_played_frames, 0);
atomic_init(&p->timestamp_origin_raw, 0);
atomic_init(&p->audio_timestamp_ns, 0);
atomic_init(&p->estimated_delay_ms, 0);
atomic_init(&p->buffer_starvation, 0);
atomic_init(&p->callback_count, 0);
p->renderer = NULL;
p->clock_thread_valid = false;
p->frame_size = 0;
p->latency_ms = 0;
if (!af_fmt_is_pcm(ao->format)) {
MP_ERR(ao, "AUDIO_UNSUPPORTED_FORMAT: %s\n", af_fmt_to_str(ao->format));
set_state(MPV_HARMONY_AUDIO_ERROR);
return -1;
}
ao->format = af_fmt_is_float(ao->format) ? AF_FORMAT_FLOAT : AF_FORMAT_S16;
if (ao->channels.num < 1) {
MP_ERR(ao, "AUDIO_UNSUPPORTED_CHANNEL_LAYOUT\n");
set_state(MPV_HARMONY_AUDIO_ERROR);
return -1;
}
p->channel_count = ao->channels.num == 1 ? 1 : 2;
if (ao->channels.num != p->channel_count)
mp_chmap_from_channels(&ao->channels, p->channel_count);
p->channel_layout = p->channel_count == 1 ? CH_LAYOUT_MONO : CH_LAYOUT_STEREO;
OH_AudioStream_Result result = create_renderer(ao, ao->format);
if (result != AUDIOSTREAM_SUCCESS && ao->format == AF_FORMAT_FLOAT) {
ao->format = AF_FORMAT_S16;
result = create_renderer(ao, ao->format);
}
if (result != AUDIOSTREAM_SUCCESS) {
set_state(MPV_HARMONY_AUDIO_ERROR);
return -1;
}
atomic_store(&g_sample_rate, ao->samplerate);
atomic_store(&g_sample_format, ao->format);
atomic_store(&g_channels, p->channel_count);
atomic_store(&g_channel_layout, p->channel_layout);
atomic_store(&g_estimated_delay_ms, p->latency_ms);
set_state(MPV_HARMONY_AUDIO_READY);
if (mp_thread_create(&p->clock_thread, clock_loop, ao)) {
MP_ERR(ao, "AUDIO_TIMESTAMP_ERROR: clock thread creation failed\n");
OH_AudioRenderer_Release(p->renderer);
p->renderer = NULL;
set_state(MPV_HARMONY_AUDIO_ERROR);
return -1;
}
p->clock_thread_valid = true;
return 0;
}
static void uninit(struct ao *ao)
{
struct priv *p = ao->priv;
MP_VERBOSE(ao, "OHAudio release\n");
atomic_store(&p->callback_enabled, false);
if (p->renderer != NULL) {
OH_AudioRenderer_Stop(p->renderer);
while (atomic_load(&p->callbacks_inflight) > 0)
mp_sleep_ns(MP_TIME_MS_TO_NS(1));
}
atomic_store(&p->stop_clock, true);
if (p->clock_thread_valid)
mp_thread_join(p->clock_thread);
if (p->renderer != NULL) {
OH_AudioStream_Result result = OH_AudioRenderer_Release(p->renderer);
if (result != AUDIOSTREAM_SUCCESS)
log_result(ao, "AUDIO_RELEASE_ERROR", result);
p->renderer = NULL;
}
set_state(MPV_HARMONY_AUDIO_RELEASED);
}
static void start(struct ao *ao)
{
struct priv *p = ao->priv;
if (p->renderer == NULL)
return;
atomic_store(&p->callback_enabled, true);
OH_AudioStream_Result result = OH_AudioRenderer_Start(p->renderer);
if (result != AUDIOSTREAM_SUCCESS) {
log_result(ao, "AUDIO_RENDERER_START_FAILED", result);
set_state(MPV_HARMONY_AUDIO_ERROR);
ao_request_reload(ao);
return;
}
MP_VERBOSE(ao, "OHAudio start\n");
set_state(MPV_HARMONY_AUDIO_RUNNING);
}
static bool set_pause(struct ao *ao, bool paused)
{
struct priv *p = ao->priv;
if (p->renderer == NULL)
return false;
OH_AudioStream_Result result = paused ? OH_AudioRenderer_Pause(p->renderer)
: OH_AudioRenderer_Start(p->renderer);
if (result != AUDIOSTREAM_SUCCESS) {
log_result(ao, paused ? "pause" : "resume", result);
set_state(MPV_HARMONY_AUDIO_ERROR);
return false;
}
MP_VERBOSE(ao, "OHAudio %s\n", paused ? "pause" : "resume");
set_state(paused ? MPV_HARMONY_AUDIO_PAUSED : MPV_HARMONY_AUDIO_RUNNING);
return true;
}
static void reset(struct ao *ao)
{
struct priv *p = ao->priv;
if (p->renderer == NULL)
return;
atomic_store(&p->callback_enabled, false);
OH_AudioStream_Result stop_result = OH_AudioRenderer_Stop(p->renderer);
if (stop_result != AUDIOSTREAM_SUCCESS && stop_result != AUDIOSTREAM_ERROR_ILLEGAL_STATE)
log_result(ao, "AUDIO_STOP_FAILED", stop_result);
while (atomic_load(&p->callbacks_inflight) > 0)
mp_sleep_ns(MP_TIME_MS_TO_NS(1));
OH_AudioStream_Result result = OH_AudioRenderer_Flush(p->renderer);
if (result != AUDIOSTREAM_SUCCESS) {
log_result(ao, "AUDIO_FLUSH_FAILED", result);
set_state(MPV_HARMONY_AUDIO_ERROR);
return;
}
atomic_store(&p->timestamp_origin_raw, atomic_load(&p->raw_played_frames));
atomic_store(&p->submitted_frames, 0);
atomic_store(&p->played_frames, 0);
atomic_store(&p->audio_timestamp_ns, 0);
atomic_store(&g_submitted_frames, 0);
atomic_store(&g_played_frames, 0);
atomic_store(&g_audio_timestamp_ns, 0);
atomic_fetch_add(&g_audio_flush_count, 1);
atomic_fetch_add(&g_seek_audio_reset_count, 1);
MP_VERBOSE(ao, "OHAudio flush\n");
set_state(MPV_HARMONY_AUDIO_STOPPED);
}
static void get_state(struct ao *ao, struct mp_pcm_state *state)
{
struct priv *p = ao->priv;
int64_t submitted = atomic_load(&p->submitted_frames);
int64_t played = atomic_load(&p->played_frames);
int64_t pending = submitted > played ? submitted - played : 0;
state->free_samples = 0;
state->queued_samples = pending;
state->delay = pending / (double)MPMAX(1, ao->samplerate) + p->latency_ms / 1000.0;
state->playing = atomic_load(&g_state) == MPV_HARMONY_AUDIO_RUNNING;
}
const struct ao_driver audio_out_harmony = {
.description = "HarmonyOS OHAudio output",
.name = "harmony",
.init = init,
.uninit = uninit,
.reset = reset,
.set_pause = set_pause,
.start = start,
.get_state = get_state,
.priv_size = sizeof(struct priv),
};
#else
MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats)
{
if (stats != NULL)
memset(stats, 0, sizeof(*stats));
return 0;
}
#endif
#if HAVE_HARMONY_AUDIO
MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats)
{
if (stats == NULL)
return 0;
stats->state = atomic_load(&g_state);
stats->sample_rate = atomic_load(&g_sample_rate);
stats->sample_format = atomic_load(&g_sample_format);
stats->channels = atomic_load(&g_channels);
stats->channel_layout = atomic_load(&g_channel_layout);
stats->submitted_frames = atomic_load(&g_submitted_frames);
stats->played_frames = atomic_load(&g_played_frames);
stats->audio_timestamp_ns = atomic_load(&g_audio_timestamp_ns);
stats->estimated_delay_ms = atomic_load(&g_estimated_delay_ms);
stats->buffer_starvation = atomic_load(&g_buffer_starvation);
stats->system_underrun = atomic_load(&g_system_underrun);
stats->callback_count = atomic_load(&g_callback_count);
stats->audio_flush_count = atomic_load(&g_audio_flush_count);
stats->seek_audio_reset_count = atomic_load(&g_seek_audio_reset_count);
return 1;
}
#endif
+49
View File
@@ -0,0 +1,49 @@
/*
* HarmonyOS audio diagnostics exported for the isolated Playback Lab.
* The AO remains usable without consuming this optional diagnostics API.
*/
#ifndef MPV_AO_HARMONY_H
#define MPV_AO_HARMONY_H
#include <stdint.h>
#include "mpv/client.h"
#ifdef __cplusplus
extern "C" {
#endif
enum mpv_harmony_audio_state {
MPV_HARMONY_AUDIO_DISABLED = 0,
MPV_HARMONY_AUDIO_READY = 1,
MPV_HARMONY_AUDIO_RUNNING = 2,
MPV_HARMONY_AUDIO_PAUSED = 3,
MPV_HARMONY_AUDIO_STOPPED = 4,
MPV_HARMONY_AUDIO_ERROR = 5,
MPV_HARMONY_AUDIO_RELEASED = 6,
};
struct mpv_harmony_audio_stats {
int32_t state;
int32_t sample_rate;
int32_t sample_format;
int32_t channels;
uint64_t channel_layout;
int64_t submitted_frames;
int64_t played_frames;
int64_t audio_timestamp_ns;
int64_t estimated_delay_ms;
int64_t buffer_starvation;
int64_t system_underrun;
int64_t callback_count;
int64_t audio_flush_count;
int64_t seek_audio_reset_count;
};
MPV_EXPORT int mpv_harmony_audio_get_stats(struct mpv_harmony_audio_stats *stats);
#ifdef __cplusplus
}
#endif
#endif
+5
View File
@@ -0,0 +1,5 @@
results/*.json
reports/*.md
runner/__pycache__/
!results/.gitkeep
!reports/.gitkeep
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+385
View File
@@ -0,0 +1,385 @@
#!/usr/bin/env python3
"""Repeatable Playback Lab emulator benchmark runner.
Only uses the standard library and the OpenHarmony hdc/uitest command line.
Metrics unavailable from the emulator are emitted as null, never inferred.
"""
from __future__ import annotations
import argparse
import json
import math
import os
import re
import shlex
import subprocess
import tempfile
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable
ROOT = Path(__file__).resolve().parents[2]
PACKAGE = "com.velix.playbacklab"
DEFAULT_SERIAL = "127.0.0.1:5555"
DEFAULT_HDC = "/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc"
def command(argv: list[str], timeout: float = 30.0) -> str:
result = subprocess.run(argv, check=False, capture_output=True, text=True, timeout=timeout)
if result.returncode != 0:
raise RuntimeError(f"command failed ({result.returncode}): {shlex.join(argv)}\n{result.stderr.strip()}")
return result.stdout.strip()
class Emulator:
def __init__(self, serial: str, hdc: str) -> None:
self.serial = serial
self.hdc = hdc
self.layout_counter = 0
def hdc_shell(self, text: str, timeout: float = 30.0) -> str:
return command([self.hdc, "-t", self.serial, "shell", text], timeout)
def click(self, x: int, y: int) -> None:
self.hdc_shell(f"uitest uiInput click {x} {y}", timeout=10)
def layout(self) -> dict[str, Any]:
self.layout_counter += 1
marker = self.hdc_shell(f"uitest dumpLayout -b {PACKAGE} -m true -d 0", timeout=10)
match = re.search(r"saved to:(\S+)", marker)
if not match:
raise RuntimeError(f"layout dump did not return a path: {marker}")
remote = match.group(1)
local = Path(tempfile.gettempdir()) / f"velix-layout-{os.getpid()}-{self.layout_counter}.json"
command([self.hdc, "-t", self.serial, "file", "recv", remote, str(local)], timeout=10)
with local.open(encoding="utf-8") as stream:
return json.load(stream)
def pid(self) -> int | None:
output = self.hdc_shell(f"pidof {PACKAGE}", timeout=10)
match = re.search(r"\b(\d+)\b", output)
return int(match.group(1)) if match else None
def process_sample(self) -> dict[str, Any]:
pid = self.pid()
sample: dict[str, Any] = {"pid": pid, "rssKb": None, "pssKb": None, "heapKb": None,
"virtualMemoryKb": None,
"threadCount": None, "fdCount": None, "cpuPermille": None,
"cpuSupported": False}
if pid is None:
return sample
status = self.hdc_shell(f"cat /proc/{pid}/status", timeout=10)
for key, field in (("rssKb", "VmRSS"), ("virtualMemoryKb", "VmSize"), ("threadCount", "Threads")):
match = re.search(rf"^{field}:\s+(\d+)", status, re.MULTILINE)
if match:
sample[key] = int(match.group(1))
fd_output = self.hdc_shell(f"ls /proc/{pid}/fd 2>&1 | wc -l", timeout=10)
if fd_output.isdigit() and fd_output != "0":
sample["fdCount"] = int(fd_output)
stat = self.hdc_shell(f"cat /proc/{pid}/stat", timeout=10)
match = re.match(r"^\d+ \(.+\) (.*)$", stat)
if match:
fields = match.group(1).split()
if len(fields) > 12:
sample["cpuJiffies"] = int(fields[11]) + int(fields[12])
return sample
def product_version(self) -> str | None:
try:
value = self.hdc_shell("param get const.product.software.version", timeout=10)
except RuntimeError:
return None
return value or None
def walk_nodes(node: dict[str, Any]) -> Iterable[dict[str, Any]]:
yield node
for child in node.get("children", []):
if isinstance(child, dict):
yield from walk_nodes(child)
def node_text(node: dict[str, Any]) -> str:
attrs = node.get("attributes", {})
return str(attrs.get("text") or attrs.get("originalText") or "")
def find_node(layout: dict[str, Any], text: str) -> dict[str, Any] | None:
for node in walk_nodes(layout):
if node_text(node) == text:
return node
return None
def center(bounds: str) -> tuple[int, int]:
values = [int(value) for value in re.findall(r"\d+", bounds)]
if len(values) != 4:
raise ValueError(f"invalid bounds: {bounds}")
return (values[0] + values[2]) // 2, (values[1] + values[3]) // 2
def click_text(emulator: Emulator, text: str) -> None:
node = find_node(emulator.layout(), text)
if node is None:
raise RuntimeError(f"UI node not found: {text}")
emulator.click(*center(node["attributes"]["bounds"]))
def all_texts(layout: dict[str, Any]) -> list[str]:
return [node_text(node) for node in walk_nodes(layout) if node_text(node)]
def text_matching(layout: dict[str, Any], prefix: str) -> str | None:
for value in all_texts(layout):
if value.startswith(prefix):
return value
return None
def parse_int(text: str | None, pattern: str) -> int | None:
if not text:
return None
match = re.search(pattern, text)
return int(match.group(1)) if match else None
def parse_state(layout: dict[str, Any]) -> str | None:
value = text_matching(layout, "state: ")
return value.split(": ", 1)[1] if value and ": " in value else None
def parse_run(layout: dict[str, Any]) -> dict[str, Any]:
startup = text_matching(layout, "startup=") or ""
buffer_line = text_matching(layout, "buffer enter=") or ""
napi_line = text_matching(layout, "napi calls=") or ""
render_line = text_matching(layout, "render updates=") or napi_line
error = text_matching(layout, "error=") or ""
timeline: dict[str, Any] = {}
for key, value in re.findall(r"(startup|firstSwap|seekRecovery|audioStart|audioCallback)=(-?\d+)", startup):
timeline[{"startup": "startupTotalMs", "firstSwap": "firstSwapMs", "seekRecovery": "seekRecoveryMs",
"audioStart": "audioRendererStartMs", "audioCallback": "firstAudioCallbackMs"}[key]] = int(value)
first_frame = parse_int(text_matching(layout, "dropped="), r"firstFrameMs=(-?\d+)")
if first_frame is None or first_frame < 0:
first_frame = timeline.get("firstSwapMs")
if first_frame is not None and first_frame < 0:
first_frame = None
startup_ms = timeline.get("startupTotalMs")
first_swap_ms = timeline.get("firstSwapMs")
seek_recovery_ms = timeline.get("seekRecoveryMs")
audio_start_ms = timeline.get("audioRendererStartMs")
audio_callback_ms = timeline.get("firstAudioCallbackMs")
if startup_ms is not None and startup_ms < 0:
startup_ms = None
if first_swap_ms is not None and first_swap_ms < 0:
first_swap_ms = None
if seek_recovery_ms is not None and seek_recovery_ms < 0:
seek_recovery_ms = None
if audio_start_ms is not None and audio_start_ms < 0:
audio_start_ms = None
if audio_callback_ms is not None and audio_callback_ms < 0:
audio_callback_ms = None
return {
"state": parse_state(layout),
"firstFrameMs": first_frame,
"renderCount": parse_int(text_matching(layout, "dropped="), r"renderCount=(-?\d+)"),
"startupTotalMs": startup_ms,
"firstSwapMs": first_swap_ms,
"seekRecoveryMs": seek_recovery_ms,
"audioRendererStartMs": audio_start_ms,
"firstAudioCallbackMs": audio_callback_ms,
"bufferingEnterCount": parse_int(buffer_line, r"buffer enter=(-?\d+)"),
"bufferingExitCount": parse_int(buffer_line, r"exit=(-?\d+)"),
"napiCalls": parse_int(napi_line, r"napi calls=(-?\d+)"),
"renderUpdateCalls": parse_int(render_line, r"updates=(-?\d+)/"),
"renderFrameUpdates": parse_int(render_line, r"updates=-?\d+/(-?\d+)"),
"coreSyncCalls": parse_int(render_line, r"sync=(-?\d+)/"),
"coreSyncSkipped": parse_int(render_line, r"sync=-?\d+/(-?\d+)"),
"error": error,
}
def wait_for_first_frame(emulator: Emulator, timeout: float = 10.0) -> tuple[dict[str, Any], list[dict[str, Any]]]:
deadline = time.monotonic() + timeout
samples: list[dict[str, Any]] = []
latest: dict[str, Any] = {}
next_sample = 0.0
while time.monotonic() < deadline:
latest = emulator.layout()
now = time.monotonic()
if now >= next_sample:
samples.append({"atMs": int((timeout - max(0.0, deadline - now)) * 1000),
**emulator.process_sample()})
next_sample = now + 1.0
run = parse_run(latest)
if run.get("startupTotalMs") is not None or run.get("state") == "ERROR":
return latest, samples
time.sleep(0.15)
raise TimeoutError(f"playback did not expose first-frame timeline: {parse_run(latest)}")
def stats(values: list[float | int]) -> dict[str, float | int | None]:
numbers = sorted(float(value) for value in values if value is not None and math.isfinite(float(value)))
if not numbers:
return {"count": 0, "min": None, "max": None, "avg": None, "median": None, "p90": None, "p95": None}
def percentile(fraction: float) -> float:
return numbers[max(0, min(len(numbers) - 1, math.ceil(fraction * len(numbers)) - 1))]
return {"count": len(numbers), "min": numbers[0], "max": numbers[-1],
"avg": sum(numbers) / len(numbers), "median": percentile(0.5),
"p90": percentile(0.9), "p95": percentile(0.95)}
def git_revision(path: Path) -> str | None:
try:
return command(["git", "-C", str(path), "rev-parse", "HEAD"], timeout=10)
except RuntimeError:
return None
def load_local(emulator: Emulator) -> tuple[dict[str, Any], list[dict[str, Any]]]:
click_text(emulator, "Load Local Test")
return wait_for_first_frame(emulator)
def startup_local(emulator: Emulator, repeat: int) -> list[dict[str, Any]]:
runs: list[dict[str, Any]] = []
for index in range(repeat):
began = time.monotonic()
click_text(emulator, "Load Local Test")
layout, samples = wait_for_first_frame(emulator)
run = parse_run(layout)
run.update({"runIndex": index + 1, "cacheClass": "cold-ish" if index == 0 else "warm",
"hostFirstFrameMs": int((time.monotonic() - began) * 1000),
"processSamples": samples})
runs.append(run)
return runs
def wait_for_seek_recovery(emulator: Emulator, timeout: float = 5.0) -> tuple[dict[str, Any], list[dict[str, Any]]]:
deadline = time.monotonic() + timeout
samples: list[dict[str, Any]] = []
latest: dict[str, Any] = {}
next_sample = 0.0
while time.monotonic() < deadline:
latest = emulator.layout()
now = time.monotonic()
if now >= next_sample:
samples.append({"atMs": int((timeout - max(0.0, deadline - now)) * 1000),
**emulator.process_sample()})
next_sample = now + 1.0
run = parse_run(latest)
if run.get("seekRecoveryMs") is not None and run.get("seekRecoveryMs") >= 0:
return latest, samples
if run.get("state") == "ERROR":
return latest, samples
time.sleep(0.1)
raise TimeoutError(f"seek did not expose recovery timeline: {parse_run(latest)}")
def seek_local(emulator: Emulator, repeat: int) -> list[dict[str, Any]]:
runs: list[dict[str, Any]] = []
for index in range(repeat):
click_text(emulator, "Load Local Test")
# The bundled clip is intentionally short and may finish before a
# multi-command text-input interaction completes. The native +/-10s
# action exercises the same seek API without that race.
time.sleep(0.05)
click_text(emulator, "+10s")
latest, samples = wait_for_seek_recovery(emulator)
run = parse_run(latest)
run.update({"runIndex": index + 1, "mode": "relative", "offsetSeconds": 10,
"processSamples": samples})
runs.append(run)
return runs
def stress_lifecycle(emulator: Emulator, repeat: int) -> list[dict[str, Any]]:
initial = emulator.layout()
load_center = center(find_node(initial, "Load Local Test")["attributes"]["bounds"])
stop_center = center(find_node(initial, "Stop")["attributes"]["bounds"])
runs: list[dict[str, Any]] = []
for index in range(repeat):
emulator.click(*load_center)
time.sleep(0.08)
emulator.click(*stop_center)
time.sleep(0.08)
layout = emulator.layout()
run = parse_run(layout)
run.update({"runIndex": index + 1, "processSample": emulator.process_sample()})
runs.append(run)
return runs
def play_pause(emulator: Emulator, repeat: int) -> list[dict[str, Any]]:
load_local(emulator)
initial = emulator.layout()
play_center = center(find_node(initial, "Play")["attributes"]["bounds"])
pause_center = center(find_node(initial, "Pause")["attributes"]["bounds"])
runs: list[dict[str, Any]] = []
for index in range(repeat):
emulator.click(*play_center)
emulator.click(*pause_center)
if index % 10 == 0:
time.sleep(0.05)
run = {"runIndex": index + 1, "processSample": emulator.process_sample()}
if index % 10 == 0 or index + 1 == repeat:
run.update(parse_run(emulator.layout()))
runs.append(run)
return runs
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("scenario", choices=["startup_local", "seek_local", "stress_lifecycle", "play_pause"])
parser.add_argument("--repeat", type=int, default=None)
parser.add_argument("--serial", default=os.environ.get("DEVECO_DEVICE", DEFAULT_SERIAL))
parser.add_argument("--hdc", default=os.environ.get("HDC", DEFAULT_HDC))
parser.add_argument("--output", type=Path)
args = parser.parse_args()
defaults = {"startup_local": 20, "seek_local": 50, "stress_lifecycle": 100, "play_pause": 200}
repeat = args.repeat if args.repeat is not None else defaults[args.scenario]
emulator = Emulator(args.serial, args.hdc)
if args.scenario == "startup_local":
runs = startup_local(emulator, repeat)
elif args.scenario == "seek_local":
runs = seek_local(emulator, repeat)
elif args.scenario == "stress_lifecycle":
runs = stress_lifecycle(emulator, repeat)
else:
runs = play_pause(emulator, repeat)
metric_names = ["firstFrameMs", "startupTotalMs", "firstSwapMs", "seekRecoveryMs", "audioRendererStartMs",
"firstAudioCallbackMs", "bufferingEnterCount", "bufferingExitCount", "hostFirstFrameMs", "hostTerminalMs"]
output = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"gitCommit": git_revision(ROOT),
"mpvCommit": git_revision(ROOT),
"ffmpegCommit": git_revision(ROOT.parent / "Velix-FFmpeg-Harmony"),
"deviceType": "emulator",
"deviceSerial": args.serial,
"harmonyVersion": emulator.product_version(),
"testScenario": args.scenario,
"mediaId": "sample.mp4",
"protocol": "file",
"codec": "H.264/AAC",
"resolution": None,
"audio": "ao_harmony",
"subtitle": "none",
"hwdec": "software",
"repeat": repeat,
"metrics": {name: stats([run.get(name) for run in runs]) for name in metric_names},
"runs": runs,
"errors": [run.get("error") for run in runs if run.get("error") and "error=NONE" not in run.get("error", "")],
"sampling": {"pss": "unsupported unless exposed by device", "cpu": "raw /proc stat only when available"},
}
output_path = args.output or ROOT / "benchmark" / "results" / f"{args.scenario}-{int(time.time())}.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(output, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(output_path)
print(json.dumps(output["metrics"], ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+7
View File
@@ -0,0 +1,7 @@
{
"name": "audio_on_off",
"media": "sample.mp4",
"repeat": 10,
"variants": ["audio_on", "audio_off"],
"notes": "Compare firstAudioCallbackMs and startupTotalMs without changing decode quality."
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "http_range",
"media": "controlled-http-mp4",
"repeat": 10,
"requiredServerEvidence": ["Range bytes=0-", "206 response", "seekResult"],
"notes": "HTTPS/HLS are separate controlled-network scenarios and remain pending without a reliable source."
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "seek_local",
"media": "sample.mp4",
"positionsPercent": [10, 25, 50, 75, 90],
"repeatPerPosition": 10,
"modes": ["fast", "exact"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "startup_local",
"media": "sample.mp4",
"repeat": 20,
"coldIsh": true,
"warm": true,
"notes": "OS page cache is not controlled; firstFrame/startup timeline comes from the Playback Lab."
}
@@ -0,0 +1,6 @@
{
"name": "stress_lifecycle",
"loadStopCycles": 100,
"playPauseCycles": 200,
"notes": "Release/recreate requires a fresh XComponent surface and is kept separate from load/stop stress."
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "subtitle_ass",
"media": "sample.mp4",
"repeat": 10,
"variants": ["none", "embedded_srt", "embedded_ass", "external_ass"],
"notes": "Run only when the corresponding controlled fixture is installed on the emulator."
}
+303
View File
@@ -0,0 +1,303 @@
/* Stable Harmony playback contract. This header intentionally does not
* expose libmpv, FFmpeg or render implementation types. */
#ifndef MPV_HARMONY_PLAYBACK_H_
#define MPV_HARMONY_PLAYBACK_H_
#include <stddef.h>
#include <stdint.h>
#ifndef MP_HARMONY_EXPORT
#ifdef _WIN32
#define MP_HARMONY_EXPORT __declspec(dllexport)
#elif defined(__GNUC__) || defined(__clang__)
#define MP_HARMONY_EXPORT __attribute__((visibility("default")))
#else
#define MP_HARMONY_EXPORT
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define MP_HARMONY_PLAYBACK_API_VERSION 1u
#define MP_HARMONY_TEXT 256
#define MP_HARMONY_URL 2048
#define MP_HARMONY_MAX_HEADERS 32
#define MP_HARMONY_MAX_SUBTITLES 32
#define MP_HARMONY_MAX_TRACKS 64
typedef struct mp_harmony_player mp_harmony_player;
typedef enum mp_harmony_state {
MP_HARMONY_IDLE = 0,
MP_HARMONY_OPENING,
MP_HARMONY_BUFFERING,
MP_HARMONY_READY,
MP_HARMONY_PLAYING,
MP_HARMONY_PAUSED,
MP_HARMONY_SEEKING,
MP_HARMONY_ENDED,
MP_HARMONY_STOPPED,
MP_HARMONY_ERROR,
MP_HARMONY_RELEASED,
} mp_harmony_state;
typedef enum mp_harmony_error_code {
MP_HARMONY_ERROR_NONE = 0,
MP_HARMONY_ERROR_NETWORK,
MP_HARMONY_ERROR_TLS,
MP_HARMONY_ERROR_HTTP,
MP_HARMONY_ERROR_DEMUX,
MP_HARMONY_ERROR_UNSUPPORTED_CONTAINER,
MP_HARMONY_ERROR_UNSUPPORTED_VIDEO_CODEC,
MP_HARMONY_ERROR_UNSUPPORTED_AUDIO_CODEC,
MP_HARMONY_ERROR_SUBTITLE,
MP_HARMONY_ERROR_DECODER,
MP_HARMONY_ERROR_AUDIO_OUTPUT,
MP_HARMONY_ERROR_VIDEO_OUTPUT,
MP_HARMONY_ERROR_SEEK,
MP_HARMONY_ERROR_FILE_NOT_FOUND,
MP_HARMONY_ERROR_PERMISSION,
MP_HARMONY_ERROR_UNKNOWN,
} mp_harmony_error_code;
typedef enum mp_harmony_seek_mode {
MP_HARMONY_SEEK_RELATIVE = 0,
MP_HARMONY_SEEK_ABSOLUTE,
MP_HARMONY_SEEK_PERCENT,
} mp_harmony_seek_mode;
typedef enum mp_harmony_track_type {
MP_HARMONY_TRACK_VIDEO = 0,
MP_HARMONY_TRACK_AUDIO,
MP_HARMONY_TRACK_SUBTITLE,
} mp_harmony_track_type;
typedef struct mp_harmony_http_header {
const char *name;
const char *value;
} mp_harmony_http_header;
typedef struct mp_harmony_subtitle_source {
const char *url;
const char *title;
const char *language;
const char *encoding;
int forced;
int select;
int is_default;
} mp_harmony_subtitle_source;
typedef struct mp_harmony_playback_request {
const char *url;
const char *title;
int64_t start_position_ms; /* < 0 means unset. */
const char *user_agent;
const char *referer;
const char *cookies;
const mp_harmony_http_header *http_headers;
size_t http_header_count;
const mp_harmony_subtitle_source *subtitle_files;
size_t subtitle_file_count;
const char *preferred_audio_language;
const char *preferred_subtitle_language;
int subtitle_enabled; /* < 0 means unset, 0 disables, 1 enables. */
int audio_track_id; /* < 0 means auto. */
int subtitle_track_id; /* < 0 means auto. */
double playback_speed; /* <= 0 means unset. */
double network_timeout; /* <= 0 means mpv default. */
const char *tls_ca_file; /* Optional PEM/CA bundle path for TLS verification. */
} mp_harmony_playback_request;
typedef struct mp_harmony_track {
int id;
mp_harmony_track_type type;
char title[MP_HARMONY_TEXT];
char language[MP_HARMONY_TEXT];
char codec[MP_HARMONY_TEXT];
int is_default;
int forced;
int selected;
int external;
} mp_harmony_track;
typedef struct mp_harmony_media_info {
char container[MP_HARMONY_TEXT];
char demuxer[MP_HARMONY_TEXT];
double duration;
char video_codec[MP_HARMONY_TEXT];
char audio_codec[MP_HARMONY_TEXT];
char pixel_format[MP_HARMONY_TEXT];
char color_primaries[MP_HARMONY_TEXT];
char color_transfer[MP_HARMONY_TEXT];
char color_space[MP_HARMONY_TEXT];
int width;
int height;
int bit_depth;
int audio_sample_rate;
int audio_channels;
char audio_channel_layout[MP_HARMONY_TEXT];
double fps;
int64_t bitrate;
int64_t video_bitrate;
int64_t audio_bitrate;
int track_count;
int subtitle_count;
/* Stable field names for bindings; width/height/color_space remain for ABI compatibility. */
int video_width;
int video_height;
char color_matrix[MP_HARMONY_TEXT];
} mp_harmony_media_info;
typedef struct mp_harmony_network_info {
char protocol[MP_HARMONY_TEXT];
char host[MP_HARMONY_TEXT];
char network_state[MP_HARMONY_TEXT];
int http_status;
int redirect_count;
int buffering;
double cache_duration;
int64_t cache_bytes;
char demuxer[MP_HARMONY_TEXT];
char container[MP_HARMONY_TEXT];
int64_t current_bitrate;
/* -1 unknown, 0 no observed 206 response, 1 observed byte-range support. */
int range_supported;
char seek_result[MP_HARMONY_TEXT];
} mp_harmony_network_info;
typedef struct mp_harmony_playback_metrics {
int64_t dropped_frames;
int64_t render_count;
int buffering;
double cache_duration;
int64_t cache_bytes;
int64_t current_bitrate;
int64_t buffering_enter_count;
int64_t buffering_exit_count;
int64_t buffering_total_ms;
int64_t buffering_max_single_ms;
int64_t network_wait_ms;
int64_t late_frames;
int64_t mistimed_frames;
int64_t decoder_frames;
} mp_harmony_playback_metrics;
typedef struct mp_harmony_playback_error {
mp_harmony_error_code code;
char raw_message[MP_HARMONY_TEXT];
} mp_harmony_playback_error;
/* Elapsed milliseconds from the current load request; -1 means unavailable. */
typedef struct mp_harmony_playback_timeline {
int64_t session_setup_ms;
int64_t mpv_create_ms;
int64_t mpv_initialize_ms;
int64_t load_command_ms;
int64_t start_file_ms;
int64_t network_open_begin_ms;
int64_t network_open_end_ms;
int64_t first_byte_ms;
int64_t demux_ready_ms;
int64_t file_loaded_ms;
int64_t video_decoder_ready_ms;
int64_t audio_decoder_ready_ms;
int64_t video_reconfig_ms;
int64_t audio_reconfig_ms;
int64_t render_context_ready_ms;
int64_t first_video_frame_available_ms;
int64_t first_render_begin_ms;
int64_t first_render_end_ms;
int64_t first_swap_ms;
int64_t startup_total_ms;
int64_t audio_renderer_start_ms;
int64_t first_audio_callback_ms;
int64_t playback_restart_ms;
int64_t seek_request_ms;
int64_t seek_command_ms;
int64_t seek_demux_ms;
int64_t seek_video_reconfig_ms;
int64_t seek_playback_restart_ms;
int64_t seek_first_frame_ms;
int64_t seek_first_swap_ms;
int64_t seek_command_latency_ms;
int64_t seek_recovery_ms;
int64_t seek_first_visible_frame_ms;
} mp_harmony_playback_timeline;
typedef struct mp_harmony_playback_snapshot {
uint64_t session_id;
mp_harmony_state state;
mp_harmony_error_code error_code;
char error_message[MP_HARMONY_TEXT];
double position;
double duration;
double fps;
int64_t dropped_frames;
int64_t first_frame_ms;
int64_t render_count;
int volume;
int muted;
double speed;
int64_t subtitle_delay_ms;
double subtitle_scale;
double subtitle_position;
int track_count;
mp_harmony_playback_error error;
mp_harmony_playback_metrics metrics;
mp_harmony_media_info media;
mp_harmony_network_info network;
mp_harmony_playback_timeline timeline;
} mp_harmony_playback_snapshot;
/* Stable model aliases for language bindings. */
typedef mp_harmony_playback_request PlaybackRequest;
typedef mp_harmony_playback_snapshot PlaybackState;
typedef mp_harmony_track Track;
typedef mp_harmony_media_info MediaInfo;
typedef mp_harmony_playback_metrics PlaybackMetrics;
typedef mp_harmony_playback_error PlaybackError;
MP_HARMONY_EXPORT mp_harmony_player *mp_harmony_playback_create(void);
MP_HARMONY_EXPORT int mp_harmony_playback_initialize(mp_harmony_player *player);
MP_HARMONY_EXPORT void mp_harmony_playback_destroy(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_load(mp_harmony_player *player,
const mp_harmony_playback_request *request);
MP_HARMONY_EXPORT int mp_harmony_playback_play(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_pause(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_toggle_pause(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_stop(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_seek(mp_harmony_player *player, mp_harmony_seek_mode mode,
double value, int exact, int keyframe);
MP_HARMONY_EXPORT int mp_harmony_playback_set_speed(mp_harmony_player *player, double speed);
MP_HARMONY_EXPORT int mp_harmony_playback_set_volume(mp_harmony_player *player, double volume);
MP_HARMONY_EXPORT int mp_harmony_playback_set_mute(mp_harmony_player *player, int muted);
MP_HARMONY_EXPORT int mp_harmony_playback_set_audio_enabled(mp_harmony_player *player, int enabled);
MP_HARMONY_EXPORT int mp_harmony_playback_select_audio(mp_harmony_player *player, int track_id);
MP_HARMONY_EXPORT int mp_harmony_playback_select_subtitle(mp_harmony_player *player, int track_id);
MP_HARMONY_EXPORT int mp_harmony_playback_disable_subtitle(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_add_subtitle(mp_harmony_player *player,
const mp_harmony_subtitle_source *source);
MP_HARMONY_EXPORT int mp_harmony_playback_remove_subtitle(mp_harmony_player *player, int track_id);
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *player, double seconds);
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *player, double scale);
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_position(mp_harmony_player *player, double position);
MP_HARMONY_EXPORT int mp_harmony_playback_set_ass_override(mp_harmony_player *player, const char *mode);
MP_HARMONY_EXPORT int mp_harmony_playback_get_tracks(mp_harmony_player *player,
mp_harmony_track *tracks, size_t capacity,
size_t *count);
MP_HARMONY_EXPORT int mp_harmony_playback_get_media_info(mp_harmony_player *player,
mp_harmony_media_info *info);
MP_HARMONY_EXPORT int mp_harmony_playback_get_network_info(mp_harmony_player *player,
mp_harmony_network_info *info);
MP_HARMONY_EXPORT int mp_harmony_playback_get_snapshot(mp_harmony_player *player,
mp_harmony_playback_snapshot *snapshot);
MP_HARMONY_EXPORT const char *mp_harmony_playback_state_name(mp_harmony_state state);
MP_HARMONY_EXPORT const char *mp_harmony_playback_error_name(mp_harmony_error_code error);
#ifdef __cplusplus
}
#endif
#endif
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <stdint.h>
#if defined(__GNUC__) || defined(__clang__)
#define VELIX_CORE_API __attribute__((visibility("default")))
#else
#define VELIX_CORE_API
#endif
#define VELIX_CORE_API_VERSION 0x000100u
#ifdef __cplusplus
extern "C" {
#endif
typedef struct velix_core_build_info {
uint32_t api_version;
const char* sdk_version;
const char* mpv_version;
const char* mpv_commit;
const char* ffmpeg_commit;
const char* ffmpeg_harmony_commit;
const char* build_date;
const char* target;
const char* architecture;
const char* profile;
const char* feature_flags;
} velix_core_build_info;
VELIX_CORE_API uint32_t velix_core_get_api_version(void);
VELIX_CORE_API const char* velix_core_get_version(void);
VELIX_CORE_API const velix_core_build_info* velix_core_get_build_info(void);
#ifdef __cplusplus
}
#endif
+17
View File
@@ -0,0 +1,17 @@
[PROJECT:Velix-MPV-Core] 完成 Phase 0 移植研究
结论: 基于 MPV 182fa6ca49 和 OpenHarmony API 24 完成全部研究文档,首版采用 GLES/EGL、OHAudio、官方 FFmpeg OHCodec 路线。
文件变更: UPSTREAM.md, LICENSE_AUDIT.md, THIRD_PARTY_NOTICES.md, LICENSES/, DOCS/*_RESEARCH.md, DOCS/PORTING_PLAN.md, DOCS/FFMPEG_STRATEGY.md, DOCS/BUILD_STRATEGY.md, DOCS/RISK_ASSESSMENT.md
教训: OHOS 会定义 __linux__;FFmpeg master 已有 OHCodec;Native TLS验证接口不能替代 FFmpeg TLS backend。
标签: #mpv #harmonyos #phase0 #ffmpeg #ohcodec #license
[PROJECT:Velix-MPV-Core] 配置并推送独立远程
结论: origin 已指向私有 Gitea,master、harmony-port、完整历史及 89 个官方标签均已推送,默认分支为 harmony-port。
文件变更: UPSTREAM.md, MEMORY.md, memory/projects.md, memory/infra.md, memory/lessons.md, memory/2026-08-28.md
教训: 单次 HTTPS pack 受限时可分历史基线和增量推送,保持完整历史不变。
标签: #git #gitea #remote #history
[PROJECT:Velix-MPV-Core] 完成 Phase 1 HarmonyOS Native Build
结论: API 24 arm64 libmpv 已真实生成并重复构建通过,13 个静态库与最终 ELF、动态依赖、Client/OpenGL Render API symbols、宿主污染均完成核验。
文件变更: meson.build, scripts/, toolchains/, UPSTREAM.md, LICENSE_AUDIT.md, THIRD_PARTY_NOTICES.md, DOCS/BUILD_STRATEGY.md, DOCS/PHASE1_DEPENDENCY_MATRIX.md, DOCS/BUILD_ISSUES.md, DOCS/MPV_BUILD_REPORT.md, memory/*
教训: Clang target 必须显式用于编译与链接;Meson 共享 Cross File 使用 @DIRNAME@;FreeType SDK zlib 使用 system 模式。
标签: #phase1 #libmpv #ohos #aarch64 #cross-compile
+58
View File
@@ -0,0 +1,58 @@
[PROJECT:Velix-MPV-Core] 完成 Phase 4.5 FFmpeg-Harmony export contract 与 MPV 前缀切换
结论: 独立 FFmpeg 完整历史分支 `harmony-ohcodec-export` 推送 tip `990614364f`(代码修正 `aacbe7d578`);OHOS export/software 构建、正式 accessor、NativeBuffer ref/unref、EGL/API probes 通过,MPV 仅新增 prefix switch 并完成 AArch64 构建;状态为 DEVICE VALIDATION PENDING。
文件变更: ../Velix-FFmpeg-Harmony/libavcodec/ohdec.c, libavutil/hwcontext_oh.*, scripts/*, DOCS/*; Velix-MPV-Core/scripts/build-mpv-ohos*.sh, scripts/pkg-config-ohos.sh, DOCS/MPV_BUILD_REPORT.md, DOCS/PHASE4_STATUS.md, memory/*
教训: emulator 可用于设备连接与既有 HAP/EGL 日志复核,但不能把 compile/link probe 写成 FFmpeg export 硬解运行证据;Gitea 大 pack 需分块保留完整历史。
标签: #phase4-5 #ffmpeg #ohcodec #nativebuffer #prefix-switch #emulator #pending-device
[PROJECT:Velix-MPV-Core] 完成 Phase 5 Playback Capability & Network Compatibility
结论: 新增不暴露 mpv 内部类型的稳定 Playback C API、请求/会话隔离、错误/状态/轨道/媒体/网络/字幕能力及 Playback Lab 诊断 UI;默认、software、FFmpeg-Harmony AArch64 构建与 Native/ArkTS 编译通过,HAP 打包因缺 Java Runtime 阻断,真实网络/实体设备待验证。
文件变更: include/mpv/harmony_playback.h, player/harmony_playback.c, playback_lab/entry/src/main/cpp/{player.h,player.cpp,bridge.cpp}, playback_lab/entry/src/main/ets/pages/Index.ets, DOCS/PHASE5_*.md, DOCS/PLAYBACK_PUBLIC_API.md, DOCS/NETWORK_*.md, DOCS/TRACK_MODEL.md, DOCS/SUBTITLE_PIPELINE.md, DOCS/HARMONY_FONT_STRATEGY.md
教训: hvigor 的 CompileArkTS/BuildNativeWithNinja 可在无 Java 环境通过,但 PackageHap 仍需 JDK;HAP 不应把 Native/ArkTS 编译成功误报为完整打包成功。
标签: #phase5 #playback-api #network #subtitle #redaction #software-decode #pending-runtime
[PROJECT:Velix-MPV-Core] Phase 5 最终构建与契约审计
结论: 修正 C API 时长/错误同步、请求校验、网络日志诊断与敏感 query 脱敏;默认、software、FFmpeg-Harmony AArch64 构建及 Native/ArkTS 编译通过,PackageHap 仍因缺 Java Runtime 待补,模拟器可启动既有 Playback Lab。
文件变更: player/harmony_playback.c, playback_lab/entry/src/main/cpp/player.cpp, DOCS/PHASE5_TEST_MATRIX.md, DOCS/PHASE5_STATUS.md, memory/projects.md
教训: 网络 HTTP status 采集必须覆盖 cplayer/ffmpeg 日志前缀;HLS/鉴权/字幕运行证据仍需真实网络与可安装 HAP,不能用编译结果替代。
标签: #phase5 #build #emulator #hap #java-runtime #network-pending
[PROJECT:Velix-MPV-Core] Phase 5 最终实现与模拟器 smoke
结论: C++ Playback Lab 已统一委托稳定 C 播放核心;default/software/FFmpeg-Harmony 构建、PackageHap(DevEco JBR)和 devecocli 安装启动通过;模拟器完成本地 H.264/AAC 首帧、软件解码、渲染指标与结束状态验证,真实 HTTP/HTTPS/HLS/网络字幕仍待受控源。
文件变更: playback_lab/entry/src/main/cpp/{player.h,player.cpp,harmony_playback_internal.h}, player/harmony_playback.c, DOCS/PHASE5_*.md, memory/*
教训: hvigor 默认守护进程会复用无 Java 环境;使用 DevEco JBR 并加 `--no-daemon` 才能稳定执行 PackageHap。设备 UI smoke 不能替代真实网络验收。
标签: #phase5 #c-api #playback-lab #hap #emulator #software-decode #network-pending
[PROJECT:Velix-MPV-Core] Phase 5 收尾校验与推送准备
结论: 修复普通相对 seek 的无效 default 参数,并保留 EOF 后已获取的媒体快照;三套 AArch64 产物、符号/ELF 校验和 HAP 产物均已复核,准备提交远端。
文件变更: player/harmony_playback.c, memory/2026-08-29.md
教训: mpv seek 未指定 exact/keyframe 时必须省略 flags 参数;EOF 后属性可能短暂不可读,诊断快照不能无条件清零。
标签: #phase5 #seek #eof #validation #push
[PROJECT:Velix-MPV-Core] 推送完成
结论: Phase 5 收尾提交 `f9d1481fb8` 已推送并在 `origin/harmony-port` 远端核对一致,工作区干净。
文件变更: memory/2026-08-29.md
标签: #phase5 #remote #harmony-port #complete
[PROJECT:Velix-MPV-Core] Phase 5 网络安全与诊断补齐
结论: 补齐 title/起播 exact seek/header 校验、媒体字段别名、Range/seek 诊断与顶部错误面板;模拟器通过宿主受控 HTTP MP4 Range 测试;FFmpeg-Harmony 跨 origin 凭证丢弃提交 `f3a036c695` 已推送,HTTPS/HLS/鉴权/重定向运行验证和实体机仍待补。
文件变更: player/harmony_playback.c, include/mpv/harmony_playback.h, playback_lab/entry/src/main/cpp/*, playback_lab/entry/src/main/ets/pages/Index.ets, DOCS/*, memory/*
教训: 当前 ArkUI SDK 下 Scroll 包裹既有 Column 会裁剪后续控件,保持平面 Column 并将关键错误信息置顶。
标签: #phase5 #network #range #security #emulator #ffmpeg-harmony
[PROJECT:Velix-MPV-Core] Phase 6 性能基线与优化完成
结论: 新增完整 playback timeline、buffer/audio/NAPI/资源诊断与 stdlib benchmark runner;事件驱动渲染+250ms安全探测、100ms核心快照合并、NAPI轮询降频、atomic session 读取通过模拟器回归,状态为 CODE COMPLETE / EMULATOR BENCHMARKED / DEVICE BENCHMARK PENDING。
文件变更: include/mpv/harmony_playback.h, player/harmony_playback.c, playback_lab/entry/src/main/cpp/{player.h,player.cpp,render.cpp,bridge.cpp}, playback_lab/entry/src/main/ets/pages/Index.ets, benchmark/*, DOCS/{PHASE6_STATUS.md,PERFORMANCE_METRICS.md,PERFORMANCE_CHANGES.md,BENCHMARK_METHODOLOGY.md,STARTUP_PIPELINE.md,SEEK_PIPELINE.md,BUFFERING_MODEL.md}
教训: 完全依赖 mpv update callback 会导致重复 load 偶发 BUFFERING 卡住;保留低频安全探测。模拟器不提供 PSS/精确 heap/归一化 CPU,报告必须明确 unavailable;seek 50 次保留 1 次真实 SEEK 命令错误。
标签: #phase6 #benchmark #timeline #buffering #napi #emulator #device-pending
[PROJECT:Velix Phase7] Core SDK 打包与主 App 集成
结论: Core SDK ABI `0x000100`、预编译 arm64 库、构建信息与集成文档已入库;`14149854b8` 已推送 `origin/harmony-port`。主 App 已接入 PlaybackEngine/Controller、MpvPlaybackEngine 与 PlayerPage,release/debug HAP 通过,模拟器完成本地 MP4、暂停/seek、软件统计与 release 验收;设备与真实网络仍待验证。
文件变更: ../Velix-MPV-Core/{include/velix, sdk/native, scripts/package-velix-core-sdk.sh, scripts/verify-velix-core-sdk.sh, DOCS/*}; ../Velix/Velix/{scripts, entry/src/main/ets, entry/src/main/cpp, entry/src/main/resources, entry/.gitignore}; memory/*
教训: 主 App 目录无 Git 元数据;SDK 通过 `entry/libs/arm64-v8a` 预编译库接入,不复制 MPV/FFmpeg 源码;HTTP 受控宿主试验未形成稳定成功证据,不能写成运行验收通过。
标签: #phase7 #sdk #integration #openharmony #emulator #device-pending
[PROJECT:Velix Phase8] UI Foundation + Home Experience 完成
结论: 主 App 独立 Git 提交 `058c7a8`;按 HarmonyOS 官方系统资源和参考图完成 TMDB Discovery 首页、Hero、来源切换、悬浮导航、详情页、图片状态缓存与 PlayerPage 视觉统一,Debug/Release HAP 构建成功并在 `127.0.0.1:5555` 回归;TMDB live credentials pending,Core 未修改。
文件变更: ../Velix/{entry/src/main/ets/domain/home,entry/src/main/ets/ui,entry/src/main/ets/pages/{HomePage,MediaDetailPage,PlayerPage}.ets,DOCS/PHASE8_UI.md,design-qa.md}; memory/*
教训: 参考图需要沉浸式大图与底部信息层;无 TMDB key 时仍以 Mock Provider 展示真实远程图并保留浅色占位,不能让网络失败破坏首页结构。
标签: #phase8 #ui #harmonyos #tmdb #mock #emulator #git #pending-credentials
+113
View File
@@ -0,0 +1,113 @@
[PROJECT:Velix VUI1] 沉浸式视觉重构完成
结论: 首页完成全屏 Hero、内容氛围色、4 级材质和 HDS 沉浸浮动导航;详情、来源 Sheet、播放器及手机/平板/深色回归均通过,Core 未修改。
文件变更: ../Velix/entry/src/main/ets/{domain/home,pages,ui}, ../Velix/DOCS/PHASE8_UI.md, ../Velix/design-qa.md, memory/*
教训: API 24 可用 HDS 导航沉浸材质,但通用 uiMaterial 需 API 26;平板模拟器需单独启动以避免与手机 HDC 端口冲突。
标签: #vui1 #harmonyos #hds #immersive #responsive #emulator
[PROJECT:Velix VUI1] 首页顶部控件固定
结论: 来源胶囊、搜索和个人入口已与 Scroll 解耦,上滑时与底部导航一样固定浮在内容之上。
文件变更: ../Velix/entry/src/main/ets/pages/HomePage.ets, memory/projects.md, memory/2026-08-30.md
标签: #vui1 #fixed-overlay #home #emulator
[PROJECT:Velix VUI1] Full-Bleed Immersive Home
结论: 首页以 Hero 的 backdrop、transition 与 ambient browsing surface 连续构成;语义环境色控制内容面和悬浮材质,Pura 90 Pro Max 初始态及滚动回归通过。
文件变更: ../Velix/entry/src/main/ets/{domain/home,pages,ui}, ../Velix/design-qa.md, memory/projects.md, memory/2026-08-30.md
标签: #vui1 #full-bleed #ambient-palette #hds #emulator
[PROJECT:Velix VUI1] 底栏图标实心化
结论: HDS 底栏保留沉浸光感,Tab 前景保持实心,选中图标继续按当前影片环境色取色。
文件变更: ../Velix/entry/src/main/ets/ui/components/FloatingNavigation.ets, ../Velix/design-qa.md, memory/projects.md, memory/2026-08-30.md
标签: #vui1 #bottom-navigation #hds #emulator
[PROJECT:Velix VUI1] Scene Continuity
结论: 背景改为覆盖 Hero 与首个 Section 的单一长尾 Scene,Backdrop 与环境色在正文后方逐步衰减,轮播同步更新背景。
文件变更: ../Velix/entry/src/main/ets/{pages,ui,domain/home}, ../Velix/design-qa.md, memory/projects.md, memory/2026-08-30.md
标签: #vui1 #scene-continuity #ambient #emulator
[PROJECT:Velix VUI1] Hero Motion Handoff
结论: 原生 Swiper 保持拖拽/速度吸附;Hero visual 底部用纵向场景渐隐交接固定 Background,只有分页 settle 后才以 420ms 固定位置过渡背景 A→B。
文件变更: ../Velix/entry/src/main/ets/pages/HomePage.ets, ../Velix/entry/src/main/ets/ui/components/HeroSection.ets
教训: loop Swiper 的 animation-end 可能返回回收页索引,需优先使用 animation-start target,并在回弹时清理 pager 状态;背景切换不能放在逐帧拖动回调。
标签: #swiper #motion-handoff #ambient
[PROJECT:Velix VUI1] VUI3 位移渐衰
结论: Hero visual 采用 160–220vp smoothstep 分段反向位移与 Alpha 交接,固定背景只在 Swiper settle 后固定位置换图;静止态保持原有 full-page scene。
文件变更: ../Velix/entry/src/main/ets/pages/HomePage.ets, ../Velix/entry/src/main/ets/ui/components/HeroSection.ets, ../Velix/entry/src/main/ets/ui/components/RemoteImage.ets
教训: 静止态由 FixedBackgroundScene 承担当前画面,滑动时才显示独立 Hero artwork bands;HDC 远程 shell 后台执行手势再延时 snapshot 可抓到真实中帧。
标签: #vui3 #hero-paging #handoff
[PROJECT:Velix VUI1] TMDB Live Data Foundation
结论: 首页生产路径已移除 Mock fallback,接入 TmdbRepository/DiscoveryProvider/API client;认证支持 API key/Bearer,首页、详情、TV season 与搜索均走统一 DTO Mapper 和 CDN resolver,凭据缺失明确报错。
文件变更: ../Velix/entry/src/main/ets/domain/home/{HomeModels,HomeRepository,DiscoveryProvider,TmdbRepository,TmdbDiscoveryProvider,tmdb/*}, ../Velix/entry/src/main/ets/pages/{HomePage,MediaDetailPage}.ets, ../Velix/scripts/build-velix-app.sh, ../Velix/TMDB.md
教训: rawfile 是本地构建注入点,tmdb.local.properties 与生成的 rawfile 必须忽略;没有真实凭据时只能验证未配置/401 错误态,不能声称 TMDB live feed 已通过。
标签: #tmdb #data-layer #cache #search
[PROJECT:Velix VUI1] TMDB Edge Fixes
结论: 详情请求等待凭据初始化,Hero 显式过滤 person;补齐 imdbId/tvdbId 映射,详情主操作改为“查找可播放资源”,演员列表支持展开查看全部。
文件变更: ../Velix/entry/src/main/ets/domain/home/{HomeModels,HomeRepository,TmdbDiscoveryProvider,tmdb/TmdbMapper}.ets, ../Velix/entry/src/main/ets/pages/MediaDetailPage.ets
标签: #tmdb #detail #edge-cases
[PROJECT:Velix VUI1] TMDB Search Pagination
结论: 搜索 UI 增加真实 TMDB total_pages/total_results 状态与“加载更多”,继续沿用 380ms 防抖和旧查询结果隔离。
文件变更: ../Velix/entry/src/main/ets/pages/HomePage.ets
标签: #tmdb #search #pagination
[PROJECT:Velix VUI1] TMDB Detail Completeness
结论: 详情模型补齐 first/last air date 与 tvdbId,推荐为空时回退 similar,Specials 可作为首个可见季;详情失败显示重试态,最终应用提交 `42c23e5`。
文件变更: ../Velix/entry/src/main/ets/domain/home/{HomeModels,tmdb/TmdbMapper}.ets, ../Velix/entry/src/main/ets/pages/MediaDetailPage.ets
教训: 详情页请求必须等待 Repository 初始化,否则合法凭据也可能被未配置竞态抢先返回。
标签: #tmdb #detail #tv #error-state
[PROJECT:Velix VUI1] TMDB Live Emulator Verification
结论: 用户提供的 Bearer Token 仅写入主 App 被 .gitignore 忽略的本地配置;模拟器实测首页 6 个 Hero、7 个分区共 130 项,电影详情、TV 详情/首季与第 2 季、搜索与加载更多均返回真实 TMDB 数据。
文件变更: ../Velix/entry/src/main/ets/domain/home/TmdbDiscoveryProvider.ets, ../Velix/entry/src/main/ets/pages/MediaDetailPage.ets, ../Velix/tmdb.local.properties(忽略)
教训: TV 详情响应通常只含季元数据,进入详情后需按当前季补发 `/tv/{id}/season/{n}`;日志只记录 endpoint、状态、延迟和映射数量,不记录凭据。
标签: #tmdb #live #emulator #tv #search
[PROJECT:Velix VUI1] 详情页沉浸控件修正与签名请求
结论: 详情页仅移除底部导航,保留剧照/海报;详情 Hero 使用 TMDB logo 艺术图并将顶部按钮下移到安全区;CSR 已生成,私钥/口令留在本地忽略目录。
文件变更: ../Velix/entry/src/main/ets/pages/MediaDetailPage.ets, ../Velix/entry/src/main/ets/domain/home/{HomeModels,TmdbDiscoveryProvider,tmdb/TmdbMapper,TmdbImageResolver}.ets, ../Velix/entry/src/main/ets/ui/components/RemoteImage.ets, ../Velix/certs/velix-signing.csr
教训: 详情底部导航与内容图集是两套独立结构,验收时需区分;CSR 可提交但 p12/口令必须忽略。
标签: #vui1 #detail #hds #csr
[PROJECT:Velix VUI1] CSR 格式修正
结论: 原 RSA/SHA384 CSR 被 Huawei 拒绝,已改用 P-256 EC 与 SHA256withECDSA 重新生成,并验证 PKCS#10 自签名。
文件变更: ../Velix/certs/velix-signing.csr, ../Velix/certs/velix-signing.p12(忽略), ../Velix/certs/velix-signing.pass(忽略)
教训: HarmonyOS CSR 应通过 DevEco 兼容流程生成;上传前检查曲线、签名算法和完整 PEM 头尾。
标签: #csr #harmonyos #signing
[PROJECT:Velix VUI1] HTTP 视频与 Release 打包验证
结论: emulator 已通过公开 HTTP MP4 `http://vjs.zencdn.net/v/oceans.mp4` 播放测试(首帧约 954ms、时长 46.6s);111.cer 已导入匹配 p12,Release HAP 构建成功但正式签名仍需对应 AGC `.p7b` Profile。
文件变更: ../Velix/certs/111.cer(本地), ../Velix/entry/build/default/outputs/default/entry-default-unsigned.hap(构建产物)
教训: HarmonyOS signingConfigs 的 profile 字段必填,只有 `.cer` 不能生成可发布签名包;不能伪造或用证书链替代 `.p7b`。
标签: #http #playback #release #signing
[PROJECT:Velix Alpha] Emby foundation
结论: 主 App 提交 `540aee5` 补齐 Emby 首页库数据、剧集季集、`/Search/Hints` 搜索、ProviderIds 资源匹配、DirectPlay PlaybackInfo 与播放器 Session 上报接线;debug HAP 构建并在 `127.0.0.1:5555` 安装启动通过。
文件变更: ../Velix/entry/src/main/ets/domain/{emby,home,playback}, ../Velix/entry/src/main/ets/pages/PlayerPage.ets, ../Velix/DOCS/ALPHA_STATUS.md
教训: 用户提供的 Emby 服务器为 4.8.11.0,认证稳定返回 HTTP 401;在有效账号和签名 Profile 到位前不能声称真实库/DirectPlay/Session 或正式 Alpha 通过。
标签: #alpha #emby #direct-play #playback-session #emulator
[PROJECT:Velix VUI1] Emby API inventory blocked
结论: 当前工作区、用户目录和 Git 历史均未找到 `embyapi.json`,按要求未猜测接口或创建不完整清单。
文件变更: 无
教训: Emby 阶段必须先提供项目指定的 `embyapi.json`,不能用网上 Emby/Jellyfin 文档替代。
标签: #emby #api-inventory #blocked
[PROJECT:Velix VUI1] Emby API inventory complete
结论: 已完整读取项目根 `embyapi.json`,覆盖 356 paths/447 operations;生成五份 Emby 文档并加入最小 API/认证/DTO 骨架,App 提交 `2d3c255`、`ff28ae1`,debug HAP 构建通过。
文件变更: ../docs/emby/{EMBY_API_INVENTORY,EMBY_PHASE8_ENDPOINTS,MEDIA_SOURCE_MODEL,DIRECT_PLAY_FLOW,EMBY_API_RISKS}.md, ../Velix/entry/src/main/ets/domain/emby/*
教训: 该 spec 版本为 4.1.1.0,且 ticks 描述写作 “1 tick = 10000 ms”;4.9.x 真机验证前必须经统一 EmbyTimeConverter 校准,不能按经验猜换算。
标签: #emby #api-inventory #phase8 #direct-play #harmonyos
[PROJECT:Velix Phase8.1] Emby 连接基础链路
结论: 主 App 完成多服务器添加表单、URL 规范化、稳定 DeviceId、Asset 安全 Token 存储、统一 X-Emby-Authorization、运行时探针和脱敏验证报告;debug HAP 已构建并在 Pura 90 Pro Max 模拟器启动,真实 Emby 4.9.x 尚未提供地址/账号。
文件变更: ../Velix/entry/src/main/ets/domain/emby/*, ../Velix/entry/src/main/ets/domain/home/HomeRepository.ets, ../Velix/entry/src/main/ets/pages/HomePage.ets, ../Velix/entry/src/main/ets/ui/components/SourceSheet.ets, ../Velix/entry/src/main/module.json5, ../docs/emby/EMBY_RUNTIME_VALIDATION.md
教训: OpenHarmony API 24 SDK 中 `STORE_PERSISTENT_DATA` 的 permission definition 为 `provisionEnable=false`;不能用明文 Preferences 兜底,必须在目标签名/真机确认 Asset 持久化能力;未拿到真实 Emby 时不能声称 4.9.x 或 PlaybackInfo 运行时通过。
标签: #emby #phase8.1 #secure-storage #runtime-probe #emulator
[PROJECT:Velix Phase8.1] Emby 真实连接复核
结论: 模拟器表单与主机 curl 使用同一地址/认证头复核;公共信息通过并识别为 SaturDay.Lite 4.8.11.0,AuthenticateByName 返回 HTTP 401“无效用户名或密码”,未保存密码/token。
文件变更: ../Velix/entry/src/main/ets/domain/emby/EmbyApiClient.ets, ../docs/emby/EMBY_RUNTIME_VALIDATION.md
教训: 401 需区分服务器认证拒绝与客户端请求错误;先用无敏感输出的公共信息和认证复核确认,不能把 4.8.11.0 当作用户要求的 4.9.x。
标签: #emby #runtime #http401 #security
+29
View File
@@ -0,0 +1,29 @@
[PROJECT:Velix Alpha] 诊断与构建回归
结论: 修复 API 24 设备上的 `TabBarSymbol` 运行时崩溃,改用稳定 `TabBarOptions`;完成统一脱敏日志、轮转文件、LogViewer、诊断模式、ZIP 导出和播放异常 marker。debug/release unsigned HAP 构建成功,模拟器安装启动与诊断导出无崩溃。
文件变更: ../Velix/entry/src/main/ets/domain/diagnostics/*, ../Velix/entry/src/main/ets/pages/{DiagnosticsPage,LogViewerPage}.ets, ../Velix/entry/src/main/ets/ui/components/FloatingNavigation.ets, ../Velix/entry/src/main/ets/entryability/EntryAbility.ets, ../Velix/entry/src/main/ets/pages/PlayerPage.ets, ../Velix/{DOCS,ALPHA_BUILD_PROFILE.md,CHANGELOG.md,KNOWN_ISSUES_ALPHA.md}
教训: API 24 不应实例化会引用更新运行时符号的 `BottomTabBarStyle`;unsigned HAP 只能用于本地回归,正式 Alpha 仍需匹配签名 Profile。诊断包验证必须通过系统保存选择器,且所有输出持续脱敏。
标签: #alpha #diagnostics #logging #crash-marker #emulator #unsigned-hap
[PROJECT:Velix Alpha] Resume 与设置回归
结论: 主 App 提交 `30aa760`;Emby Resume/起播位置、音轨字幕选择和强制 Progress 已接入,设置/关于页可用且快进/快退时长持久化读取;Debug/Release 构建、安装启动和崩溃窗口检查通过。
文件变更: ../Velix/entry/src/main/ets/domain/{emby,home,playback,settings}, ../Velix/entry/src/main/ets/pages/{HomePage,MediaDetailPage,PlayerPage,SettingsPage,AboutPage}.ets, ../Velix/DOCS/ALPHA_STATUS.md
教训: ArkUI @Builder 参数是值传递时不会可靠刷新动态文本,使用 getter 闭包读取状态;真实 Emby 账号仍需有效凭据,unsigned HAP 不能替代正式签名。
标签: #alpha #resume #playback #settings #emulator #unsigned-hap
[PROJECT:Velix Alpha] 播放交互与状态回归
结论: 主 App 提交 `b9380df`;播放器 Slider/速度/音轨字幕/双击/长按临时倍速、前后台桥接、详情骨架与重试、Emby 源管理和媒体卡长按菜单已完成。Debug HAP 构建、安装、媒体卡菜单→详情跳转和模拟器崩溃窗口检查通过。
文件变更: ../Velix/entry/src/main/ets/{pages/{PlayerPage,MediaDetailPage,HomePage,SettingsPage}.ets,ui/components/MediaCard.ets,domain/playback/PlaybackLifecycleBridge.ets,domain/{emby,home,playback}/*,entryability/EntryAbility.ets}, ../Velix/DOCS/ALPHA_STATUS.md
教训: 滚动容器中自定义图片节点的长按可能被消费,卡片需在可触摸显示层上用短计时器判定,并把上下文菜单放入同一 Stack;真实 Emby 账号仍返回 401,未保存凭据。
标签: #alpha #player #gesture #emby #emulator #unsigned-hap
[PROJECT:Velix Alpha] Emby 真实运行验证
结论: 修复服务器要求的 `Pw` 登录字段、ArkUI headers 对象和 Asset Token 读取后,真实 Emby 登录/Token 恢复、库/详情/Series/Search、PlaybackInfo 已通过模拟器;HTTP Direct Play 首帧约 3.5 秒,Playing/Progress/Stopped 均 204;HTTPS Core 播放仍受 TLS 信任链阻塞。
文件变更: ../Velix/entry/src/main/ets/domain/emby/{EmbyApiClient,EmbyMediaProvider,EmbyModels,EmbyRuntimeProbe,EmbySecureTokenStore}.ets, ../Velix/entry/src/main/ets/pages/HomePage.ets, ../Velix/docs/emby/{EMBY_RUNTIME_VALIDATION,EMBY_API_RISKS}.md, ../Velix/Velix/DOCS/ALPHA_STATUS.md
教训: Emby 运行时 body/响应形状可能偏离 OpenAPI;嵌套 CollectionFolder 需要 Recursive=true,Hints 空数组应回退 Items 搜索;HTTPS 不能通过关闭证书校验规避。
标签: #emby #alpha #directplay #tls #emulator #runtime
[PROJECT:Velix Alpha] HTTPS Core 复测与 Alpha 文档更新
结论: 修复并重建 libmpv 后,应用 CA bundle 已使真实 Emby HTTPS Direct Play 在模拟器通过;首帧约 10.5 秒,Playing/Progress/Stopped 均返回 204。
文件变更: ../Velix/entry/src/main/resources/rawfile/ca-certificates.pem, ../Velix/entry/src/main/ets/domain/emby/EmbyTlsTrustStore.ets, ../Velix/entry/src/main/ets/entryability/EntryAbility.ets, ../Velix/entry/src/main/ets/domain/playback/{PlaybackTypes,MpvPlaybackEngine}.ets, ../Velix/entry/src/main/ets/pages/PlayerPage.ets, ../Velix-MPV-Core/{include/mpv/harmony_playback.h,player/harmony_playback.c,sdk/native/product_binding.cpp,playback_lab/entry/src/main/cpp/{bridge,player,player.h}}, ../docs/emby/{EMBY_RUNTIME_VALIDATION,EMBY_API_RISKS}.md, ../Velix/DOCS/ALPHA_STATUS.md, ../alpha.md
教训: 修改 MPV 播放核心后必须显式重建 libmpv,单独重打绑定层不会更新预编译播放库;证书资源必须用 openssl 校验后再打包。
标签: #emby #https #tls #directplay #alpha #emulator
+11
View File
@@ -0,0 +1,11 @@
[PROJECT:Velix] 执行 mpv9 A1
结论: 补齐 Emby 添加服务器正式组件、启动账户鉴权恢复与真实媒体库域映射,debug HAP 构建并在模拟器验证通过。
文件变更: Velix/entry/src/main/ets/ui/components/AddEmbyServerPage.ets;HomePage.ets;HomeRepository.ets;EmbyServerManager.ets;EmbyMediaProvider.ets;EmbyModels.ets;HomeModels.ets;DOCS/ALPHA_STATUS.md;DOCS/EMBY.md
教训: ArkTS 组件事件不能通过 @Prop 函数传递,改用 @Link 请求状态由父页面轮询消费。
标签: #velix #emby #mpv9 #harmonyos
[PROJECT:Velix] 诊断元数据
结论: 诊断导出已接入真实 Bundle、HarmonyOS 设备与运行时 Provider/播放快照,未知字段仅保留给运行时无法可靠获取的发布信息。
文件变更: Velix/entry/src/main/ets/domain/diagnostics/VelixDiagnosticsRuntime.ets;Velix/entry/src/main/ets/domain/diagnostics/VelixDiagnosticsExporter.ets;Velix/entry/src/main/ets/domain/home/HomeRepository.ets;Velix/entry/src/main/ets/pages/PlayerPage.ets;Velix/entry/src/main/ets/pages/DiagnosticsPage.ets;Velix/DOCS/ALPHA_STATUS.md
教训: ArkTS 禁止对象展开与匿名对象类型,诊断导出使用显式 class DTO;Bundle/设备 API 失败时保留安全 fallback,不能阻断用户导出。
标签: #velix #diagnostics #alpha #harmonyos
+20
View File
@@ -0,0 +1,20 @@
# Velix-MPV-Core 记忆索引
- 项目:地产运维中台媒体核心的 OpenHarmony MPV 端口。
- 当前阶段:VUI1 COMPLETE / Pura 90 Pro Max + MatePad Pro 13 UI verified;TMDB live credentials 已完成模拟器闭环,HTTPS/HLS、网络字幕、鉴权资源和实体机验收待补。
- 公共入口:`include/mpv/harmony_playback.h`,稳定 C API 不暴露 MPV/FFmpeg 内部类型。
- Playback Lab:C++ 仅做 NAPI/渲染适配,核心请求、控制、状态统一委托 C API。
- Phase 6:启动/seek/buffering/audio/资源/NAPI 时间线与 benchmark runner 已入库;5 次启动 update 617→364,20 次 startup、50 次 seek、100 次 load-stop、200 次 play-pause 已跑模拟器。
- 默认策略:软件解码;`harmony-ohcodec-gl` mapper 保持禁用,不声明零拷贝 GPU 能力。
- 构建:default/software/FFmpeg-Harmony AArch64 与 Velix Core SDK 打包/校验通过;HAP 使用 DevEco JBR + `--no-daemon` 通过。
- 设备:模拟器 `127.0.0.1:5555`,Playback Lab 已安装启动并完成本地 MP4 smoke。
- Phase 7:Core 提交 `14149854b8` 已推送 `origin/harmony-port`;主 App 已接入 `PlaybackEngine`/`MpvPlaybackEngine`/`PlaybackController` 与 `PlayerPage`,本地 MP4、暂停/seek、统计、release 已在模拟器验证。
- VUI1:主 App 完成沉浸式全屏 Hero、Ambient Palette、4 级材质、官方 HDS 沉浸导航、响应式详情页与自动隐藏播放器控制层;手机/平板/深色已回归,Core 未修改。
- 部署约定:服务器只按 `deploy.sh`、`deploy.sh status/logs/clean/clean-all` 执行。
- 详细状态:`memory/projects.md`;基础设施:`memory/infra.md`;教训:`memory/lessons.md`。
- Emby/Alpha foundation:主 App 已接入 Emby Views、Items、Resume、Latest、Next Up、Seasons/Episodes、Search/Hints、ProviderIds 资源匹配、PlaybackInfo/DirectPlay、Playback Start/Progress/Stop 生命周期;提交 `540aee5`,debug HAP 构建并在模拟器启动通过。后续真实运行兼容修复见 Emby runtime 条目。
- Alpha diagnostics:主 App 提交 `33c39ff`,已接入脱敏 VelixLogger、1000 条 ring、2 MiB/5 级轮转、日志查看/筛选/复制、15/30 分钟诊断模式、系统文件选择器 ZIP 导出及播放异常标记;debug/release unsigned HAP 构建并在模拟器无崩溃启动。真实 Emby、真机和正式签名仍待完成。
- Alpha playback/settings:主 App 提交 `30aa760`,Emby Resume 位置、音轨/字幕选择、暂停/Seek/切轨强制 Progress 已接入 Core;Settings/About 页面可用,快进/快退设置持久化并由 PlayerPage 读取。Debug/Release 构建及模拟器启动无崩溃;真机、正式签名和 HTTPS Core TLS 验证仍待完成。
- Alpha interactions:主 App 提交 `b9380df`,播放器 Slider/速度/音轨字幕/双击/长按临时倍速、Ability 前后台桥接、详情骨架与重试、服务器管理和媒体卡长按菜单已接入;debug 构建、安装、详情跳转与模拟器崩溃窗口检查通过。
- Emby runtime:主 App 当前修复 `Pw` 登录字段、ArkUI HTTP headers 对象和 Asset Token 明文读取;真实登录/Token 恢复、Views/Items/详情/Series Seasons-Episodes/Search 回退、PlaybackInfo 已在模拟器通过。应用将受控 PEM CA bundle 传入 Core,HTTP Direct Play 首帧约 3.5 秒、HTTPS 首帧约 10.5 秒,Playing/Progress/Stopped 均返回 204;服务器为 Emby 4.8.11.0,4.9.x/真机/正式签名仍待完成。
- mpv9 A1:主 App 新增 `AddEmbyServerPage`(焦点/按压/错误态、密码显示/隐藏),启动对持久化 Emby 账户执行 User/System 鉴权校验,Views 映射为带中文类型的 `MediaLibrary`,条目保留 ProviderIds 原始/归一化键;debug HAP 已在模拟器恢复真实 Emby 首页。
+31
View File
@@ -0,0 +1,31 @@
# Infrastructure
- DevEco Studio:`/Applications/DevEco-Studio.app`
- OpenHarmony SDK:`/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony`
- SDK:API 24,版本 6.1.1.125
- Native sysroot:`<SDK>/native/sysroot`
- Toolchain:`<SDK>/native/llvm/bin`
- Target:`aarch64-linux-ohos`
- Compiler:OHOS Clang 15.0.4
- Build tools:Meson 1.9.0、Ninja 1.12.0、pkgconf 3.0.6,均由仓库脚本隔离管理
- OHOS prefix:`build/ohos-arm64/prefix`
- libmpv:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`
- Git origin:`https://git.leee.xin/leeexin/Velix-MPV-Core.git`,默认分支 `harmony-port`
- MPV upstream:`https://github.com/mpv-player/mpv.git`
- hdc:`/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc`
- 模拟器目标:`127.0.0.1:5555`,HarmonyOS 6.1.0.126 / API 24
- Playback Lab 构建:在 `playback_lab/` 执行 `scripts/stage-mpv.sh` 后用 hvigor `--no-daemon assembleHap --mode module -p product=default`
- HAP 构建:使用 DevEco 自带 `/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home`,并以 `hvigorw.js assembleHap --no-daemon --mode module -p product=default` 完成 PackageHap;默认未配置正式签名。
- HAP 环境:需设置 `JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home`、`DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk`;SDK native 路径仍使用 `.../sdk/default/openharmony`。
- OHAudio:API 24 headers `ohaudio/native_audiostreambuilder.h` / `native_audiorenderer.h` / `native_audiostream_base.h`,动态库 `libohaudio.so`;Meson 使用 `-Dharmony-audio=enabled`。
- Phase 3 HAP:`playback_lab/entry/build/default/outputs/default/entry-default-unsigned.hap`;模拟器 H.264/AAC smoke target `127.0.0.1:5555`。
- Phase 4 构建入口:`scripts/build-mpv-ohos.sh`(OHCodec enabled)、`scripts/build-mpv-ohos-software.sh`(OHCodec disabled)、`scripts/build-all-ohos.sh`(全量矩阵);GL API probe 通过单独 Meson `harmony-ohcodec-gl=enabled` 构建验证。
- Phase 4 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`;软件构建位于 `build/ohos-arm64/build/mpv-software/libmpv.so.2.5.0`;HAP 位于 `playback_lab/entry/build/default/outputs/default/entry-default-unsigned.hap`。
- 模拟器验证:`devecocli run --module entry --device 127.0.0.1:5555 --skip-build` 成功安装/启动;Playback Lab 本地 H.264/AAC MP4 首帧与结束状态可见。
- FFmpeg-Harmony:`../Velix-FFmpeg-Harmony`,origin `https://git.leee.xin/leeexin/Velix-FFmpeg-Harmony.git`,upstream `https://github.com/FFmpeg/FFmpeg.git`,branch `harmony-ohcodec-export`,tip `f3a036c695`;prefix `../Velix-FFmpeg-Harmony/build/ohos-arm64/prefix`。
- MPV fork 接入:`scripts/build-mpv-ohos-ffmpeg-harmony.sh`;可用 `FFMPEG_PREFIX` 与 `MPV_BUILD_ROOT` 覆盖默认路径,默认 MPV/软件脚本不变。
- MPV fork 输出:切换脚本默认安装到 `build/ohos-arm64/prefix-mpv-ffmpeg-harmony`,不会覆盖默认 `prefix`。
- Phase 6 runner:`python3 benchmark/runner/run_emulator.py {startup_local,seek_local,stress_lifecycle,play_pause} --repeat N`;结果写入被忽略的 `benchmark/results/`。
- Phase 7 SDK:`scripts/package-velix-core-sdk.sh [--profile default|software|ffmpeg-harmony] [--out DIR]`;校验使用 `scripts/verify-velix-core-sdk.sh [SDK_DIR]`,默认输出 `dist/velix-core/`。
- Velix 主 App:`../Velix`;同步 Core 使用 `scripts/sync-velix-core-sdk.sh --local|--release --sdk DIR`,构建使用 `scripts/build-velix-app.sh --local-core|--release-core --build-mode debug|release`;最终 HAP `../Velix/entry/build/default/outputs/default/entry-default-unsigned.hap`。
- Velix 主 App Git:独立仓库 `../Velix/.git`,分支 `main`,Phase 8 提交 `058c7a8`;无远端配置。UI 验证截图:`/tmp/velix-v8-final-home2.jpeg`、`/tmp/velix-v8-final-sheet.jpeg`、`/tmp/velix-v8-final-detail.jpeg`、`/tmp/velix-v8-final-player-ready.jpeg`。
+34
View File
@@ -0,0 +1,34 @@
# Lessons
## High
- OHOS 同时定义 `__OHOS__` 和 `__linux__`;必须显式平台检测,不能把全部 Linux API视为可用。
- 最新 FFmpeg upstream 已有 OHCodec H.264/HEVC;研究当前源码后再决定 fork,避免重复维护。
- `libnet_ssl.so` 主要提供证书验证接口,不是 FFmpeg TLS transport backend。
- Clang 仅设置 sysroot 不会切换 target;macOS arm64 交叉编译必须在编译和链接两侧显式传 `--target=aarch64-linux-ohos`。
## Medium
- MPV upstream 使用 `DOCS/`;大小写不敏感文件系统不能可靠并存 `docs/`,沿用 upstream 目录。
- 私有 Gitea 反向代理限制单次 HTTPS pack 时,可先推送历史基线 commit,再推增量分支;不得以浅克隆或裁剪历史规避限制。
- Meson Cross File 的 `@GLOBAL_SOURCE_ROOT@` 随被构建项目变化;共享工具路径应以 `@DIRNAME@` 相对 Cross File 定位。
- FreeType `zlib=external` 表示 fallback subproject;使用 SDK zlib 必须选择 `zlib=system` 并隔离 pkg-config。
- XComponent Surface 回调的 `surface` 已是 `OHNativeWindow*`,应直接转换并 `NativeObjectReference`;再次调用 `CreateNativeWindow` 会在模拟器触发 SIGSEGV。
- `hdc file send` 语法为 `file send -b <bundle> <local> <remote>`;应用沙箱媒体必须发送到 `/data/storage/el2/base/haps/entry/files/`。
- libmpv 诊断计数返回 ArkTS Number;返回 BigInt 会导致 Inspector 无法序列化组件状态。
- MPV 事件线程需高频轮询;200ms 轮询会使属性/命令事件堆积,seek 报 `MPV_ERROR_EVENT_QUEUE_FULL`。
- MPV `audio` 是 `aid` 别名(音轨选择),不能传布尔 `yes/no`;Playback Lab 音频开关必须使用 `aid=auto/no`。
- OHAudio seek/reset 先 `Stop` 再 `Flush`,并等待 inflight callback;只 Flush 后重新 Start 在模拟器返回 `AUDIOSTREAM_ERROR_ILLEGAL_STATE`。
- OHAudio timestamp 在 Flush 后可能保留旧 epoch;统计需保存 raw frame origin,否则会出现 `playedFrames > submittedFrames`。
- `setup-ohos-env.sh` 在 zsh 中的循环变量 `path` 会覆盖特殊 PATH 数组;运行脚本应使用 `/bin/sh` 或避免 source 到交互 zsh。
- FFmpeg upstream OHCodec API24 路径是 surface-first:`AV_PIX_FMT_OHCODEC` 的 hwcontext 只保存 `OHNativeWindow*`,decoder 硬件帧通过 surface 输出;NativeBuffer/EGL API 存在不等于 MPV 可合法导入的非 surface 帧。
- OHCodec capability 必须通过 `OH_AVCodec_GetCapabilityByCategory` 及 `OH_AVCapability_*` 查询;不能把 SDK 头文件存在误写成设备能力或硬件帧已产出。
- Gitea 反向代理对完整 FFmpeg pack 返回 413;保留完整历史的可行做法是先建立空远端,再按 first-parent checkpoint 分块推送,不能浅克隆或裁剪历史。
- MPV 消费独立 FFmpeg prefix 时,pkg-config 必须把 fork 的 `lib/pkgconfig` 置于 base prefix 前,并保留绝对 include/lib 路径;仅改 `PREFIX` 会丢失 libass/libplacebo 等其他依赖。
- 当前 DevEco ArkUI SDK 下给既有平面 `Column` 外包 `Scroll` 会改变可见布局并裁掉后续控件;诊断页保持平面 Column,关键错误信息放在顶部,长列表改动需单独验收。
- 渲染 update 不能完全依赖 callback:部分重复 load 会话可能漏通知;事件驱动应保留低频(250ms)安全探测,否则会卡在 BUFFERING。
- 短媒体自动化 seek 若先等待首帧再执行多次 hdc keyEvent,媒体可能已 EOF;用立即触发的原生相对 seek 或更长受控 fixture,且保留真实命令错误。
- `/proc` 在模拟器不一定提供 PSS、精确 heap 或可校准 CPU tick;benchmark 必须输出 null/原始 jiffies,不能自行推断 permille。
- ArkTS UI:`@Prop` 不接受函数类型,交互回调应改用 `@Link` 状态或组件内路由;`Stack` 不支持 `justifyContent`,需使用 `alignContent`。
- HarmonyOS UI:系统资源必须使用已存在的 `$r('sys.color...')` / `$r('sys.symbol...')` 名称;无效资源名会在 CompileArkTS 直接失败。图片缓存只保留 URL 状态/metadata,feed/details 轻量 JSON 可放 Preferences,不能把图片字节写入偏好存储。
- `setup-ohos-env.sh` 被 zsh source 后可能剥离系统 PATH;跨脚本调用系统工具时使用绝对路径或 POSIX shell,不能假设 `dirname`/`cp`/`stat` 仍可查找。
+47
View File
@@ -0,0 +1,47 @@
# Projects
## Velix-MPV-Core
- 当前状态:Phase 7 CODE COMPLETE / EMULATOR INTEGRATION VERIFIED / DEVICE VALIDATION PENDING;Core 提交 `14149854b8` 已推送 `origin/harmony-port`。
- Phase 7 产物:`include/velix/core.h`、`sdk/native/`、`scripts/package-velix-core-sdk.sh`、`scripts/verify-velix-core-sdk.sh` 与 `DOCS/CORE_*.md`/集成文档;SDK ABI 版本 `0x000100`,仅导出 3 个 `velix_core_get_*` C ABI 符号。
- 主 App:`../Velix` 已通过 `scripts/sync-velix-core-sdk.sh` 接入预编译 Core,新增 `PlaybackEngine`、`MpvPlaybackEngine`、`PlaybackController`、`PlayerPage`;release/debug HAP 均可打包,产物为 `Velix/entry/build/default/outputs/default/entry-default-unsigned.hap`。
- 模拟器验收:PlayerPage surface/EGL/mpv READY;打包 sample MP4 本地播放、暂停、seek、软件解码统计、release 通过;HTTP URL/headers 路径已接入但受控宿主 HTTP 试验未形成稳定成功证据,实体机与真实网络待补。主 App 已于 Phase 8 建立独立 Git。
- Phase 8 UI:主 App `../Velix` 已初始化独立 Git,提交 `058c7a8`;`HomeSource`、Discovery/Media Provider 边界、Mock/TMDB skeleton、RemoteImage/cache、Source Capsule/Sheet、Floating Navigation、Home/Detail、Player token integration 已完成。TMDB live credentials 已在本地注入并完成模拟器验证;Core 未修改。
- Phase 8 模拟器:`127.0.0.1:5555` 已安装主 App HAP;首页远程图片、来源 Sheet、Hero → Detail、PlayerPage 本地 MP4 均已回归。截图见 `../Velix/design-qa.md`。
- Alpha 诊断基础:主 App 提交 `33c39ff`;`VelixLogger` 统一接入并自动脱敏,内存 ring 1000 条、本地日志 2 MiB/5 级轮转;LogViewer 支持实时刷新、暂停、Level/Category/关键词筛选、复制与清空;诊断模式支持 15/30 分钟;ZIP 导出已通过系统文件选择器在模拟器保存验证;播放异常结束 marker 已接入。debug/release unsigned HAP 均构建成功并启动无崩溃。
- Alpha 当前阻塞:真实 Emby 4.8.11.0 账号已完成认证、库/详情/搜索/PlaybackInfo 及 HTTP/HTTPS Direct Play+Session E2E;4.9.x 目标版本、其他 CA 链、AGC `.p7b` Profile、真机与压力回归仍待完成。
- VUI1:主 App 沉浸式视觉重构已完成;`AmbientPalette` 驱动全屏 Hero/页面氛围色,浮层共用 1–4 级 `MaterialSurface`,底部导航使用 HDS `HdsTabs` immersive material。Pura 90 Pro Max 已回归首页、Sheet、详情、播放器自动隐藏和深色;MatePad Pro 13 已回归宽屏首页与详情分栏。Core 未修改,主 App 等待提交。
- VUI1 修正:首页的来源胶囊、搜索与个人入口已从 Scroll 抽离,与底部导航一样作为固定浮层;Pura 90 Pro Max 上滑回归通过。
- VUI1 首页重构:首页改为 Full-Bleed Immersive Home;Hero 的 backdrop/transition/ambient browsing surface 三层连续,环境色改用语义 Ambient Palette,顶部与 HDS 底栏统一使用动态氛围材质;Pura 90 Pro Max 初始态与滚动回归通过,主 App 待提交。
- VUI1 底栏修正:保留 HDS 沉浸光感底材,Tab 前景保持不透明,选中图标继续使用当前影片的环境取色;Pura 90 Pro Max 回归通过。
- VUI1 Scene Continuity:首页背景重构为跨 Hero 与第一分区的单一 Background Scene;Backdrop 长尾衰减至正文,正文 Column 透明,Hero 轮播时背景 URL 与环境色同步刷新;Pura 90 Pro Max 已回归。
- VUI1 Motion Handoff:保留双层 artwork 复用;Hero visual 底部加入纵向渐隐交接区,背景仅在原生 Swiper settle 后以 420ms 固定位置过渡;拖动/回弹/快速 flick 已在 127.0.0.1:5555 回归。
- VUI1 VUI3:Hero 顶部保持原生 100% 跟手,底部 160–220vp 以 smoothstep 位移渐衰 + Alpha 交接固定同图背景;静止、30/50% 中帧、回弹、flick 与 settle 已回归。
- VUI1 TMDB:生产首页已移除 Mock fallback,接入 API key/Bearer、统一 DTO/Mapper、内存+Preferences 缓存、详情/TV season/搜索;模拟器已验证无 key 与 401 错误态不崩溃,合法 Bearer Token 的 live feed 已回归。
- VUI1 TMDB edge:详情等待凭据初始化,Hero 过滤 person,保存 imdbId/tvdbId,详情入口不再伪装播放,演员可展开查看全部;应用提交 `c975fb2`。
- VUI1 TMDB search:搜索结果已接入 `total_pages/total_results` 与加载更多,应用提交 `f98e7e8`。
- VUI1 TMDB detail completeness:补齐 TV 日期/Specials/外部 ID/相关推荐回退和详情错误重试态,应用提交 `42c23e5`。
- VUI1 TMDB live verification:应用提交 `4380ab9`;模拟器实测首页 6 Hero/7 分区/130 项,`Reacher` TV 详情自动加载第 1 季与切换第 2 季各 8 集,`the` 搜索分页由 19 增至 39 条;映射数量以 `[TMDB]` 安全日志记录。
- VUI1 详情页修正:详情页底部导航移除,剧照/海报保留;详情 Hero 优先显示 TMDB logo 艺术图,顶部按钮下移 22vp;CSR 已按 P-256 EC/SHA256withECDSA 重新生成。
- VUI1 HTTP/打包验证:emulator 已播放公开 HTTP MP4;`111.cer` 已导入匹配 `.p12`,Release HAP 已构建,但正式签名包还缺 AGC 对应 `.p7b Profile`。
- Emby 阶段:已完整解析项目根 `embyapi.json`(OpenAPI 3.0.1、356 paths、447 operations),生成 `../docs/emby/` 五份清单/模型/播放链路/风险文档;主 App 提交 `2d3c255`/`ff28ae1` 加入 EmbyApiClient、EmbyRequestContext、EmbyAuthProvider 与 DTO 基础骨架,未实现具体业务 endpoint;debug HAP 构建通过。
- Emby Phase 8.1:主 App 提交 `1cd396d`。Source Capsule 已可打开添加 Emby 表单;`EmbyServerUrlNormalizer` 支持 host/host:port、http(s) 与单一 `/emby` 后缀;`VelixDeviceIdentity` 持久化安装级 DeviceId;`EmbySecureTokenStore` 使用 Asset persistent/accessibility tags(API 24 SDK 需目标签名确认 `STORE_PERSISTENT_DATA`);`EmbyServerManager` 支持多服务器、活动源、登出/移除与 Token 失效;`EmbyRuntimeProbe` 执行 PublicSystemInfo、AuthenticateByName、User、System、Views、MediaFolders、Items 和条件 PlaybackInfo,输出脱敏 `EMBY_RUNTIME_VALIDATION.md`。模拟器已安装并启动 debug HAP,表单必填错误态可见、无新增崩溃;真实 Emby 4.9.x 网络/ProviderIds/ticks/PlaybackInfo 仍待运行时验证。
- Emby 连接实测:服务器返回 `SaturDay.Lite`、Emby `4.8.11.0`;该实例要求 `Pw` 登录字段,修复后 `/Users/AuthenticateByName`、用户/库/Items/Resume/Latest/NextUp 均成功,Token 可在应用重启后恢复。`/Search/Hints` 空数组由 Items 搜索回退;Series Seasons/Episodes 与 PlaybackInfo 已验证。应用 CA bundle 已接入 Core,HTTP/HTTPS Direct Play 与 Playing/Progress/Stopped 204 均已在模拟器验证。
- Alpha Emby foundation:主 App 提交 `540aee5`,补齐 Emby Resume/Latest/Next Up、Series Seasons/Episodes、`/Search/Hints` 分页搜索、ProviderIds 优先资源匹配、DirectPlay PlaybackInfo/URL/headers;`PlaybackLaunch.remoteSession` 已接入 PlayerPage 的 Start/8 秒节流 Progress/Stop。后续兼容修复尚未提交,当前产物仍为 unsigned debug HAP。
- mpv9 A1:主 App 已补齐正式 `AddEmbyServerPage`、密码显示/隐藏与表单交互态;启动自动校验持久化账户并在 401 清除 Token;Emby 首页展示真实 Views 媒体库及类型,ProviderIds 保存原始/归一化键;debug HAP 已重新构建并在 127.0.0.1:5555 启动。
- Alpha playback/settings:主 App 提交 `30aa760`,`UserData.PlaybackPositionTicks` 进入 PlaybackLaunch/Core 起播位置,详情页提供继续/从头,音轨/字幕选择会同步 Emby Session;暂停、Seek、切轨强制上报 Progress。新增持久化 Settings/About 入口,PlayerPage 读取快进/快退时长;Debug/Release HAP 与模拟器启动通过。
- Alpha diagnostics metadata:主 App 已补齐诊断运行时快照与真实 Bundle/设备信息采集;导出包会记录当前 Provider、服务器版本、连接状态及最近播放摘要,Git/FFmpeg/正式构建类型仍待发布上下文注入。
- Alpha interactions:主 App 提交 `b9380df`;播放器加入 Slider、速度、音轨/字幕、双击快进/快退和长按临时倍速,前后台桥接按 Alpha 策略暂停并补报进度;详情骨架/错误重试、搜索/连接诊断入口、Emby 源退出/移除、媒体卡长按上下文菜单已完成。模拟器已验证长按菜单、详情跳转及真实 Emby HTTP/HTTPS Direct Play,无新增崩溃;真机横屏、压力和正式签名仍待完成。
- Phase 6 产物:`benchmark/runner/run_emulator.py` 与场景 manifest;启动 P50/P95 506/1150ms,seek recovery P50/P95 277/631ms;100 load/stop、200 play/pause 无错误,PSS/精确 heap/CPU permille unavailable。
- Phase 6 文档:`DOCS/PHASE6_STATUS.md`、`PERFORMANCE_METRICS.md`、`PERFORMANCE_CHANGES.md`、`BENCHMARK_METHODOLOGY.md`、`STARTUP_PIPELINE.md`、`SEEK_PIPELINE.md`、`BUFFERING_MODEL.md`。
- 状态:Phase 5 CODE COMPLETE / emulator HTTP Range verified;GPU INTEROP MAPPER 未进入;HTTPS/HLS、网络字幕、鉴权、重定向运行验证与实体机验收未完成。
- 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。
- 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,346,792 bytes;软件构建位于 `build/ohos-arm64/build/mpv-software/libmpv.so.2.5.0`,FFmpeg-Harmony 构建位于 `build/ohos-arm64/build/mpv-ffmpeg-harmony/libmpv.so.2.5.0`;DT_NEEDED 含 `libohaudio.so`。
- 已完成:独立 HAP、XComponent/OHNativeWindow、EGL ES3、libmpv OpenGL Render、软件解码本地 MP4 首帧、播放控制与释放重入。
- 已完成:`ao_harmony` PCM float32→S16 fallback、采样率/mono-stereo 协商、pause/resume/stop/flush/release、timestamp epoch、starvation/flush metrics;Phase 5 HAP 已用 DevEco JBR 完成打包并安装模拟器。
- 已完成:OHCodec H.264/HEVC capability 查询、`hwdec=ohcodec` direct-surface 实验驱动、显式软件回退、Playback Lab requested/actual/metrics 诊断、软件/硬解/GL probe 构建矩阵;FFmpeg 未修改。
- 边界:官方 OHCodec 上游没有非 surface 的可导入硬件帧路径,EGL/NativeBuffer 仅保留禁用默认探针;无零拷贝、HDR 或硬件帧产出声明。
- Phase 4.5:`../Velix-FFmpeg-Harmony` branch `harmony-ohcodec-export` 已推送 tip `f3a036c695`;跨 origin 重定向丢弃敏感自定义头/cookie,`scripts/build-mpv-ohos-ffmpeg-harmony.sh` 已消费其 prefix 构建 MPV。
- 待办:实体机 H.264/HEVC capability、surface 生命周期/stride/crop/fence、真实 GPU 互操作与 A/V sync;报告见两个仓库的 `DOCS/OHCODEC_*.md` 与 `DOCS/PHASE4_5_STATUS.md`。
- Phase 5 已完成:稳定 C API、请求/headers 会话隔离、错误/状态、轨道/媒体/网络/字幕控制、Playback Lab 诊断 UI 与文档;C++ 外壳已统一委托 C API,默认/软件/FFmpeg-Harmony 构建及 HAP 打包通过;模拟器已完成本地 MP4 与宿主受控 HTTP Range MP4 播放,HTTPS/HLS/网络字幕/鉴权/重定向运行验证待补。
+72 -1
View File
@@ -72,6 +72,7 @@ sources = files(
'audio/fmt-conversion.c',
'audio/format.c',
'audio/out/ao.c',
'audio/out/ao_harmony.c',
'audio/out/ao_lavc.c',
'audio/out/ao_null.c',
'audio/out/ao_pcm.c',
@@ -167,6 +168,7 @@ sources = files(
## Player
'player/audio.c',
'player/harmony_playback.c',
'player/client.c',
'player/command.c',
'player/configfiles.c',
@@ -224,6 +226,7 @@ sources = files(
'video/filter/vf_format.c',
'video/filter/vf_sub.c',
'video/fmt-conversion.c',
'video/harmony_hwdec.c',
'video/hwdec.c',
'video/image_loader.c',
'video/image_writer.c',
@@ -324,10 +327,12 @@ if cc.get_id() == 'gcc'
endif
darwin = host_machine.system() == 'darwin'
ohos = host_machine.system() == 'ohos'
win32 = host_machine.system() == 'cygwin' or host_machine.system() == 'windows'
posix = not win32
features += {'darwin': darwin}
features += {'ohos': ohos}
features += {'posix': posix}
features += {'dos-paths': win32, 'win32': win32}
@@ -913,6 +918,50 @@ if features['aaudio']
sources += files('audio/out/ao_aaudio.c')
endif
harmony_audio_opt = get_option('harmony-audio').require(
ohos,
error_message: 'HarmonyOS OHAudio is only available on OHOS targets!',
)
harmony_audio = cc.find_library('ohaudio', required: harmony_audio_opt)
harmony_audio_headers = cc.has_header('ohaudio/native_audiostreambuilder.h',
required: harmony_audio_opt)
features += {'harmony-audio': harmony_audio.found() and harmony_audio_headers}
if harmony_audio_opt.enabled() and not features['harmony-audio']
error('HarmonyOS OHAudio headers or libohaudio.so were not found')
endif
if features['harmony-audio']
dependencies += harmony_audio
endif
harmony_ohcodec_opt = get_option('harmony-ohcodec').require(
ohos,
error_message: 'HarmonyOS OHCodec is only available on OHOS targets!',
)
harmony_ohcodec_libs = [
cc.find_library('native_media_codecbase', required: harmony_ohcodec_opt),
cc.find_library('native_media_core', required: harmony_ohcodec_opt),
cc.find_library('native_media_vdec', required: harmony_ohcodec_opt),
cc.find_library('native_window', required: harmony_ohcodec_opt),
]
harmony_ohcodec_headers = cc.has_header('multimedia/player_framework/native_avcapability.h',
required: harmony_ohcodec_opt) and \
cc.has_header('multimedia/player_framework/native_avcodec_videodecoder.h',
required: harmony_ohcodec_opt) and \
cc.has_header('multimedia/player_framework/native_avbuffer.h',
required: harmony_ohcodec_opt) and \
cc.has_header('native_window/external_window.h',
required: harmony_ohcodec_opt)
features += {'harmony-ohcodec': harmony_ohcodec_headers and harmony_ohcodec_libs[0].found() and
harmony_ohcodec_libs[1].found() and harmony_ohcodec_libs[2].found() and
harmony_ohcodec_libs[3].found()}
if harmony_ohcodec_opt.enabled() and not features['harmony-ohcodec']
error('HarmonyOS OHCodec headers or native media libraries were not found')
endif
if features['harmony-ohcodec']
dependencies += harmony_ohcodec_libs
sources += files('video/out/hwdec/hwdec_ohcodec.c')
endif
opensles = cc.find_library('OpenSLES', required: get_option('opensles'))
features += {'opensles': opensles.found()}
if features['opensles']
@@ -1315,6 +1364,26 @@ if plain_gl.allowed()
features += {'gl': true}
endif
harmony_ohcodec_gl_opt = get_option('harmony-ohcodec-gl').require(
features['harmony-ohcodec'] and features['gl'] and ohos,
error_message: 'HarmonyOS OHCodec GPU interop requires OHCodec and OpenGL!',
)
harmony_ohcodec_gl_egl = cc.find_library('EGL', required: harmony_ohcodec_gl_opt)
harmony_ohcodec_gl_lib = cc.find_library('native_buffer', required: harmony_ohcodec_gl_opt)
harmony_ohcodec_gl_headers = cc.has_header('native_buffer/native_buffer.h',
required: harmony_ohcodec_gl_opt) and \
cc.has_header('EGL/eglext.h', required: harmony_ohcodec_gl_opt) and \
cc.has_header('GLES2/gl2ext.h', required: harmony_ohcodec_gl_opt)
features += {'harmony-ohcodec-gl': harmony_ohcodec_gl_headers and
harmony_ohcodec_gl_egl.found() and harmony_ohcodec_gl_lib.found()}
if harmony_ohcodec_gl_opt.enabled() and not features['harmony-ohcodec-gl']
error('HarmonyOS OHCodec GPU interop headers or EGL/native_buffer libraries were not found')
endif
if features['harmony-ohcodec-gl']
dependencies += [harmony_ohcodec_gl_egl, harmony_ohcodec_gl_lib]
sources += files('video/out/hwdec/hwdec_ohcodec_gl.c')
endif
if features['egl'] or features['egl-android'] or features['egl-angle-win32']
sources += files('video/out/opengl/egl_helpers.c')
endif
@@ -1846,7 +1915,8 @@ if get_option('libmpv')
description: 'mpv media player client library')
headers = ['include/mpv/client.h', 'include/mpv/render.h',
'include/mpv/render_gl.h', 'include/mpv/stream_cb.h']
'include/mpv/render_gl.h', 'include/mpv/stream_cb.h',
'include/mpv/harmony_playback.h']
install_headers(headers, subdir: 'mpv')
# Allow projects to build with libmpv by cloning into ./subprojects/mpv
@@ -1948,6 +2018,7 @@ summary({'cocoa': features['cocoa'] and features['swift'],
'curl': features['libcurl'],
'd3d11': features['d3d11'],
'drm': features['drm'],
'harmony-audio': features['harmony-audio'],
'javascript': features['javascript'],
'libmpv': get_option('libmpv'),
'lua': features['lua'],
+3
View File
@@ -50,6 +50,9 @@ option('jack', type: 'feature', value: 'auto', description: 'JACK audio output')
option('openal', type: 'feature', value: 'disabled', description: 'OpenAL audio output')
option('audiotrack', type: 'feature', value: 'auto', description: 'Android AudioTrack audio output')
option('aaudio', type: 'feature', value: 'auto', description: 'Android AAudio audio output')
option('harmony-audio', type: 'feature', value: 'auto', description: 'HarmonyOS OHAudio output')
option('harmony-ohcodec', type: 'feature', value: 'auto', description: 'HarmonyOS OHCodec hardware decode')
option('harmony-ohcodec-gl', type: 'feature', value: 'disabled', description: 'EXPERIMENTAL HarmonyOS OHCodec GPU interop')
option('opensles', type: 'feature', value: 'auto', description: 'OpenSL ES audio output')
option('oss-audio', type: 'feature', value: 'auto', description: 'OSSv4 audio output')
option('pipewire', type: 'feature', value: 'auto', description: 'PipeWire audio output')
+6
View File
@@ -0,0 +1,6 @@
entry/libs/arm64-v8a/*.so*
entry/build/
build/
oh_modules/
.hvigor/
local.properties
+10
View File
@@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.velix.playbacklab",
"vendor": "Velix",
"versionCode": 1,
"versionName": "0.1.0",
"icon": "$media:icon",
"label": "$string:app_name"
}
}
+16
View File
@@ -0,0 +1,16 @@
# Velix Playback Lab
独立的 HarmonyOS Native XComponent + OHNativeWindow + EGL + libmpv 播放验证工程。
## 构建
```sh
./scripts/stage-mpv.sh
./hvigorw assembleHap --mode module -p product=default
```
`stage-mpv.sh` 只把本地 Phase 1 产物复制到未跟踪的 HAP native 库目录,不把 34 MB 的二进制提交到仓库。
面板中的 `OHCodec Probe` 只请求 `hwdec=ohcodec`,不会把当前 EGL window 注入 Direct Surface。没有独占 `ohos_direct_surface` 时,MPV 必须回退软件解码;面板会显示 requested/actual decoder、hwdec state、capability 摘要和回退原因。
Phase 5 的网络请求对象、轨道/媒体/网络诊断和字幕控制见 `DOCS/PLAYBACK_PUBLIC_API.md` 及 `DOCS/NETWORK_SECURITY.md`。header 输入仅用于当前 load,诊断和日志脱敏;默认构建仍使用软件解码。
+30
View File
@@ -0,0 +1,30 @@
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"files": {
"ignore": ["**/entry/libs/**"]
}
}
+3
View File
@@ -0,0 +1,3 @@
build/
.cxx/
libs/arm64-v8a/*.so*
+14
View File
@@ -0,0 +1,14 @@
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "-std=c++17 -fvisibility=hidden",
"abiFilters": ["arm64-v8a"]
}
},
"targets": [
{ "name": "default" }
]
}
+6
View File
@@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks,
plugins: []
};
+1
View File
@@ -0,0 +1 @@
# Playback Lab keeps native bridge names stable for NAPI lookup.
+6
View File
@@ -0,0 +1,6 @@
{
"modelVersion": "6.1.1",
"name": "entry",
"version": "1.0.0",
"description": "Playback Lab entry module"
}
@@ -0,0 +1,35 @@
cmake_minimum_required(VERSION 3.5.0)
project(playback_lab)
set(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../")
set(MPV_INCLUDE_DIR "${REPO_ROOT}/build/ohos-arm64/prefix/include")
set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/arm64-v8a/libmpv.so.2")
if(NOT EXISTS "${MPV_LIBRARY}")
message(FATAL_ERROR "Missing ${MPV_LIBRARY}; run scripts/stage-mpv.sh first")
endif()
include_directories(${MPV_INCLUDE_DIR})
include_directories(${REPO_ROOT})
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
add_library(mpv SHARED IMPORTED)
set_target_properties(mpv PROPERTIES IMPORTED_LOCATION "${MPV_LIBRARY}")
add_library(entry SHARED
bridge.cpp
player.cpp
render.cpp
)
target_compile_features(entry PRIVATE cxx_std_17)
target_compile_options(entry PRIVATE -fvisibility=hidden -Wall -Wextra -Werror=return-type)
target_link_libraries(entry PUBLIC
mpv
libace_napi.z.so
libace_ndk.z.so
libEGL.so
libGLESv3.so
libnative_window.so
libhilog_ndk.z.so
)
+807
View File
@@ -0,0 +1,807 @@
#include "player.h"
#include "render.h"
#include "audio/out/ao_harmony.h"
#include "video/harmony_hwdec.h"
#include <ace/xcomponent/native_interface_xcomponent.h>
#include <hilog/log.h>
#include <napi/native_api.h>
#include <vector>
#include <string>
#include <atomic>
#include <chrono>
#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0xD001
#define LOG_TAG "VelixBridge"
namespace {
PlaybackEngine gPlayer;
EglRenderer gRenderer(gPlayer);
std::atomic<uint64_t> gStateCalls{0};
std::atomic<uint64_t> gStateSerializeUs{0};
std::atomic<uint64_t> gStatePayloadBytes{0};
void onSurfaceCreated(OH_NativeXComponent* component, void* window)
{
gRenderer.onSurfaceCreated(component, window);
}
void onSurfaceChanged(OH_NativeXComponent* component, void* window)
{
gRenderer.onSurfaceChanged(component, window);
}
void onSurfaceDestroyed(OH_NativeXComponent*, void* window)
{
gRenderer.onSurfaceDestroyed(window);
}
void onTouch(OH_NativeXComponent*, void*) {}
bool getStringArg(napi_env env, napi_callback_info info, std::string& value)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false;
size_t size = 0;
if (napi_get_value_string_utf8(env, argv[0], nullptr, 0, &size) != napi_ok) return false;
value.resize(size + 1);
if (napi_get_value_string_utf8(env, argv[0], value.data(), value.size(), &size) != napi_ok) return false;
value.resize(size);
return true;
}
bool getDoubleArg(napi_env env, napi_callback_info info, double& value)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false;
return napi_get_value_double(env, argv[0], &value) == napi_ok;
}
bool getNamedString(napi_env env, napi_value object, const char* name, std::string& value)
{
bool has = false;
if (napi_has_named_property(env, object, name, &has) != napi_ok || !has) return false;
napi_value property = nullptr;
size_t size = 0;
if (napi_get_named_property(env, object, name, &property) != napi_ok ||
napi_get_value_string_utf8(env, property, nullptr, 0, &size) != napi_ok) return false;
value.resize(size + 1);
if (napi_get_value_string_utf8(env, property, value.data(), value.size(), &size) != napi_ok) return false;
value.resize(size);
return true;
}
bool getNamedDouble(napi_env env, napi_value object, const char* name, double& value)
{
napi_value property = nullptr;
bool has = false;
return napi_has_named_property(env, object, name, &has) == napi_ok && has &&
napi_get_named_property(env, object, name, &property) == napi_ok &&
napi_get_value_double(env, property, &value) == napi_ok;
}
bool getNamedInt64(napi_env env, napi_value object, const char* name, int64_t& value)
{
double number = 0;
if (!getNamedDouble(env, object, name, number)) return false;
value = static_cast<int64_t>(number);
return true;
}
bool getNamedBool(napi_env env, napi_value object, const char* name, bool& value)
{
napi_value property = nullptr;
bool has = false;
return napi_has_named_property(env, object, name, &has) == napi_ok && has &&
napi_get_named_property(env, object, name, &property) == napi_ok &&
napi_get_value_bool(env, property, &value) == napi_ok;
}
bool getRequest(napi_env env, napi_callback_info info, PlaybackRequestModel& request)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false;
napi_valuetype type = napi_undefined;
napi_typeof(env, argv[0], &type);
if (type == napi_string) {
return getStringArg(env, info, request.url);
}
if (type != napi_object) return false;
if (!getNamedString(env, argv[0], "url", request.url) || request.url.empty()) return false;
getNamedString(env, argv[0], "title", request.title);
getNamedString(env, argv[0], "userAgent", request.userAgent);
if (request.userAgent.empty()) getNamedString(env, argv[0], "user_agent", request.userAgent);
getNamedString(env, argv[0], "referer", request.referer);
getNamedString(env, argv[0], "cookies", request.cookies);
getNamedString(env, argv[0], "preferredAudioLanguage", request.preferredAudioLanguage);
if (request.preferredAudioLanguage.empty()) getNamedString(env, argv[0], "preferred_audio_language", request.preferredAudioLanguage);
getNamedString(env, argv[0], "preferredSubtitleLanguage", request.preferredSubtitleLanguage);
if (request.preferredSubtitleLanguage.empty()) getNamedString(env, argv[0], "preferred_subtitle_language", request.preferredSubtitleLanguage);
double number = 0;
if (getNamedDouble(env, argv[0], "playbackSpeed", number)) request.playbackSpeed = number;
else if (getNamedDouble(env, argv[0], "playback_speed", number)) request.playbackSpeed = number;
if (getNamedDouble(env, argv[0], "networkTimeout", number)) request.networkTimeout = number;
else if (getNamedDouble(env, argv[0], "network_timeout", number)) request.networkTimeout = number;
getNamedString(env, argv[0], "tlsCaFile", request.tlsCaFile);
if (request.tlsCaFile.empty()) getNamedString(env, argv[0], "tls_ca_file", request.tlsCaFile);
int64_t start = -1;
if (getNamedInt64(env, argv[0], "startPositionMs", start)) request.startPositionMs = start;
else if (getNamedInt64(env, argv[0], "start_position_ms", start)) request.startPositionMs = start;
int64_t track = -1;
if (getNamedInt64(env, argv[0], "audioTrackId", track)) request.audioTrackId = static_cast<int>(track);
else if (getNamedInt64(env, argv[0], "audio_track_id", track)) request.audioTrackId = static_cast<int>(track);
if (getNamedInt64(env, argv[0], "subtitleTrackId", track)) request.subtitleTrackId = static_cast<int>(track);
else if (getNamedInt64(env, argv[0], "subtitle_track_id", track)) request.subtitleTrackId = static_cast<int>(track);
bool enabled = true;
if (getNamedBool(env, argv[0], "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0;
else if (getNamedBool(env, argv[0], "subtitle_enabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0;
napi_value headers = nullptr;
bool hasHeaders = false;
if (napi_has_named_property(env, argv[0], "httpHeaders", &hasHeaders) == napi_ok && hasHeaders &&
napi_get_named_property(env, argv[0], "httpHeaders", &headers) == napi_ok) {
uint32_t length = 0;
if (napi_get_array_length(env, headers, &length) == napi_ok) {
for (uint32_t i = 0; i < length && i < 32; ++i) {
napi_value item = nullptr;
if (napi_get_element(env, headers, i, &item) != napi_ok) continue;
PlaybackHeader header;
getNamedString(env, item, "name", header.name);
getNamedString(env, item, "value", header.value);
if (!header.name.empty()) request.headers.push_back(std::move(header));
}
}
}
if (!hasHeaders && napi_has_named_property(env, argv[0], "http_headers", &hasHeaders) == napi_ok && hasHeaders &&
napi_get_named_property(env, argv[0], "http_headers", &headers) == napi_ok) {
uint32_t length = 0;
if (napi_get_array_length(env, headers, &length) == napi_ok) {
for (uint32_t i = 0; i < length && i < 32; ++i) {
napi_value item = nullptr;
if (napi_get_element(env, headers, i, &item) != napi_ok) continue;
PlaybackHeader header;
getNamedString(env, item, "name", header.name);
getNamedString(env, item, "value", header.value);
if (!header.name.empty()) request.headers.push_back(std::move(header));
}
}
}
napi_value subtitles = nullptr;
bool hasSubtitles = false;
if (napi_has_named_property(env, argv[0], "subtitleFiles", &hasSubtitles) == napi_ok && hasSubtitles &&
napi_get_named_property(env, argv[0], "subtitleFiles", &subtitles) == napi_ok) {
uint32_t length = 0;
if (napi_get_array_length(env, subtitles, &length) == napi_ok) {
for (uint32_t i = 0; i < length && i < 32; ++i) {
napi_value item = nullptr;
if (napi_get_element(env, subtitles, i, &item) != napi_ok) continue;
PlaybackSubtitleSource subtitle;
getNamedString(env, item, "url", subtitle.url);
getNamedString(env, item, "title", subtitle.title);
getNamedString(env, item, "language", subtitle.language);
getNamedString(env, item, "encoding", subtitle.encoding);
getNamedBool(env, item, "forced", subtitle.forced);
getNamedBool(env, item, "select", subtitle.select);
getNamedBool(env, item, "default", subtitle.isDefault);
if (!subtitle.url.empty()) request.subtitleFiles.push_back(std::move(subtitle));
}
}
}
if (!hasSubtitles && napi_has_named_property(env, argv[0], "subtitle_files", &hasSubtitles) == napi_ok && hasSubtitles &&
napi_get_named_property(env, argv[0], "subtitle_files", &subtitles) == napi_ok) {
uint32_t length = 0;
if (napi_get_array_length(env, subtitles, &length) == napi_ok) {
for (uint32_t i = 0; i < length && i < 32; ++i) {
napi_value item = nullptr;
if (napi_get_element(env, subtitles, i, &item) != napi_ok) continue;
PlaybackSubtitleSource subtitle;
getNamedString(env, item, "url", subtitle.url);
getNamedString(env, item, "title", subtitle.title);
getNamedString(env, item, "language", subtitle.language);
getNamedString(env, item, "encoding", subtitle.encoding);
getNamedBool(env, item, "forced", subtitle.forced);
getNamedBool(env, item, "select", subtitle.select);
getNamedBool(env, item, "default", subtitle.isDefault);
if (!subtitle.url.empty()) request.subtitleFiles.push_back(std::move(subtitle));
}
}
}
return true;
}
napi_value undefined(napi_env env)
{
napi_value result = nullptr;
napi_get_undefined(env, &result);
return result;
}
napi_value load(napi_env env, napi_callback_info info)
{
PlaybackRequestModel request;
if (getRequest(env, info, request)) gPlayer.load(request);
return undefined(env);
}
napi_value play(napi_env env, napi_callback_info)
{
gPlayer.play();
return undefined(env);
}
napi_value setAudioEnabled(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
bool enabled = true;
bool value = false;
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) == napi_ok && argc > 0 &&
napi_get_value_bool(env, argv[0], &value) == napi_ok) {
enabled = value;
}
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.setAudioEnabled(enabled), &result);
return result;
}
napi_value setHardwareDecode(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
bool enabled = false;
bool value = false;
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) == napi_ok && argc > 0 &&
napi_get_value_bool(env, argv[0], &value) == napi_ok)
enabled = value;
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.setHardwareDecode(enabled), &result);
return result;
}
napi_value pause(napi_env env, napi_callback_info)
{
gPlayer.pause();
return undefined(env);
}
napi_value togglePause(napi_env env, napi_callback_info)
{
gPlayer.togglePause();
return undefined(env);
}
napi_value seek(napi_env env, napi_callback_info info)
{
double delta = 0;
if (getDoubleArg(env, info, delta)) gPlayer.seek(delta);
return undefined(env);
}
napi_value seekAbsolute(napi_env env, napi_callback_info info)
{
double seconds = 0;
if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, false, false);
return undefined(env);
}
napi_value seekRelative(napi_env env, napi_callback_info info)
{
double delta = 0;
if (getDoubleArg(env, info, delta)) gPlayer.seek(delta);
return undefined(env);
}
napi_value seekExact(napi_env env, napi_callback_info info)
{
double seconds = 0;
if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, true, false);
return undefined(env);
}
napi_value seekKeyframe(napi_env env, napi_callback_info info)
{
double seconds = 0;
if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, false, true);
return undefined(env);
}
napi_value seekPercent(napi_env env, napi_callback_info info)
{
double percent = 0;
if (getDoubleArg(env, info, percent)) gPlayer.seekPercent(percent, false, false);
return undefined(env);
}
napi_value setSpeed(napi_env env, napi_callback_info info)
{
double speed = 1;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, speed) && gPlayer.setSpeed(speed), &result);
return result;
}
napi_value setVolume(napi_env env, napi_callback_info info)
{
double volume = 100;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, volume) && gPlayer.setVolume(volume), &result);
return result;
}
napi_value setMute(napi_env env, napi_callback_info info)
{
size_t argc = 1;
napi_value argv[1] = {nullptr};
bool muted = false;
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1 ||
napi_get_value_bool(env, argv[0], &muted) != napi_ok) return undefined(env);
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.setMute(muted), &result);
return result;
}
napi_value selectAudioTrack(napi_env env, napi_callback_info info)
{
double id = -1;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.selectAudioTrack(static_cast<int>(id)), &result);
return result;
}
napi_value selectSubtitleTrack(napi_env env, napi_callback_info info)
{
double id = -1;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.selectSubtitleTrack(static_cast<int>(id)), &result);
return result;
}
napi_value disableSubtitle(napi_env env, napi_callback_info)
{
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.disableSubtitle(), &result);
return result;
}
napi_value addSubtitle(napi_env env, napi_callback_info info)
{
PlaybackSubtitleSource source;
size_t argc = 1;
napi_value argv[1] = {nullptr};
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return undefined(env);
napi_valuetype type = napi_undefined;
napi_typeof(env, argv[0], &type);
if (type == napi_string) {
size_t size = 0;
napi_get_value_string_utf8(env, argv[0], nullptr, 0, &size);
source.url.resize(size + 1);
napi_get_value_string_utf8(env, argv[0], source.url.data(), source.url.size(), &size);
source.url.resize(size);
} else if (type == napi_object) {
getNamedString(env, argv[0], "url", source.url);
getNamedString(env, argv[0], "title", source.title);
getNamedString(env, argv[0], "language", source.language);
getNamedString(env, argv[0], "encoding", source.encoding);
getNamedBool(env, argv[0], "forced", source.forced);
getNamedBool(env, argv[0], "select", source.select);
getNamedBool(env, argv[0], "default", source.isDefault);
}
napi_value result = nullptr;
napi_get_boolean(env, gPlayer.addSubtitle(source), &result);
return result;
}
napi_value removeSubtitle(napi_env env, napi_callback_info info)
{
double id = -1;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.removeSubtitle(static_cast<int>(id)), &result);
return result;
}
napi_value setSubtitleDelay(napi_env env, napi_callback_info info)
{
double seconds = 0;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, seconds) && gPlayer.setSubtitleDelay(seconds), &result);
return result;
}
napi_value setSubtitleScale(napi_env env, napi_callback_info info)
{
double scale = 1;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, scale) && gPlayer.setSubtitleScale(scale), &result);
return result;
}
napi_value setSubtitlePosition(napi_env env, napi_callback_info info)
{
double position = 100;
napi_value result = nullptr;
napi_get_boolean(env, getDoubleArg(env, info, position) && gPlayer.setSubtitlePosition(position), &result);
return result;
}
napi_value setAssOverride(napi_env env, napi_callback_info info)
{
std::string mode;
napi_value result = nullptr;
napi_get_boolean(env, getStringArg(env, info, mode) && gPlayer.setAssOverride(mode), &result);
return result;
}
napi_value stop(napi_env env, napi_callback_info)
{
gPlayer.stop();
return undefined(env);
}
napi_value release(napi_env env, napi_callback_info)
{
gRenderer.onSurfaceDestroyed(nullptr);
gPlayer.release();
return undefined(env);
}
napi_value getState(napi_env env, napi_callback_info)
{
PlaybackSnapshot state = gPlayer.snapshot();
const auto serializeBegin = std::chrono::steady_clock::now();
const uint64_t stateCall = gStateCalls.fetch_add(1, std::memory_order_relaxed) + 1;
napi_value object = nullptr;
napi_create_object(env, &object);
napi_value value = nullptr;
napi_create_string_utf8(env, state.state.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "state", value);
napi_create_double(env, state.position, &value);
napi_set_named_property(env, object, "position", value);
napi_create_double(env, state.duration, &value);
napi_set_named_property(env, object, "duration", value);
napi_create_double(env, state.fps, &value);
napi_set_named_property(env, object, "fps", value);
napi_create_int64(env, state.dropped, &value);
napi_set_named_property(env, object, "dropped", value);
napi_create_int64(env, state.firstFrameMs, &value);
napi_set_named_property(env, object, "firstFrameMs", value);
// Render count is a diagnostic value consumed by ArkTS as a number.
// Returning a BigInt makes the inspector unable to serialize the state.
napi_create_double(env, static_cast<double>(state.renderCount), &value);
napi_set_named_property(env, object, "renderCount", value);
napi_create_string_utf8(env, state.lastError.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "lastError", value);
napi_create_string_utf8(env, state.errorCode.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "errorCode", value);
napi_value error = nullptr;
napi_create_object(env, &error);
napi_create_string_utf8(env, state.errorCode.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, error, "code", value);
napi_create_string_utf8(env, state.lastError.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, error, "rawMessage", value);
napi_set_named_property(env, object, "error", error);
napi_create_double(env, static_cast<double>(state.sessionId), &value);
napi_set_named_property(env, object, "sessionId", value);
napi_create_double(env, state.speed, &value);
napi_set_named_property(env, object, "speed", value);
napi_create_double(env, state.volume, &value);
napi_set_named_property(env, object, "volume", value);
napi_get_boolean(env, state.muted, &value);
napi_set_named_property(env, object, "muted", value);
napi_create_int64(env, state.subtitleDelayMs, &value);
napi_set_named_property(env, object, "subtitleDelayMs", value);
napi_create_double(env, state.subtitleScale, &value);
napi_set_named_property(env, object, "subtitleScale", value);
napi_create_double(env, state.subtitlePosition, &value);
napi_set_named_property(env, object, "subtitlePosition", value);
napi_value network = nullptr;
napi_create_object(env, &network);
napi_create_string_utf8(env, state.network.protocol.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "protocol", value);
napi_create_string_utf8(env, state.network.host.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "host", value);
napi_create_string_utf8(env, state.network.networkState.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "networkState", value);
napi_create_int32(env, state.network.httpStatus, &value);
napi_set_named_property(env, network, "httpStatus", value);
napi_create_int32(env, state.network.redirectCount, &value);
napi_set_named_property(env, network, "redirectCount", value);
napi_get_boolean(env, state.network.buffering, &value);
napi_set_named_property(env, network, "buffering", value);
napi_create_double(env, state.network.cacheDuration, &value);
napi_set_named_property(env, network, "cacheDuration", value);
napi_create_int64(env, state.network.cacheBytes, &value);
napi_set_named_property(env, network, "cacheBytes", value);
napi_create_string_utf8(env, state.network.demuxer.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "demuxer", value);
napi_create_string_utf8(env, state.network.container.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "container", value);
napi_create_int64(env, state.network.currentBitrate, &value);
napi_set_named_property(env, network, "currentBitrate", value);
napi_create_int32(env, state.network.rangeSupported, &value);
napi_set_named_property(env, network, "rangeSupported", value);
napi_create_string_utf8(env, state.network.seekResult.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "seekResult", value);
napi_set_named_property(env, object, "network", network);
napi_value metrics = nullptr;
napi_create_object(env, &metrics);
napi_create_int64(env, state.dropped, &value);
napi_set_named_property(env, metrics, "droppedFrames", value);
napi_create_double(env, static_cast<double>(state.renderCount), &value);
napi_set_named_property(env, metrics, "renderCount", value);
napi_get_boolean(env, state.network.buffering, &value);
napi_set_named_property(env, metrics, "buffering", value);
napi_create_double(env, state.network.cacheDuration, &value);
napi_set_named_property(env, metrics, "cacheDuration", value);
napi_create_int64(env, state.network.cacheBytes, &value);
napi_set_named_property(env, metrics, "cacheBytes", value);
napi_create_int64(env, state.network.currentBitrate, &value);
napi_set_named_property(env, metrics, "currentBitrate", value);
napi_create_int64(env, state.metrics.buffering_enter_count, &value);
napi_set_named_property(env, metrics, "bufferingEnterCount", value);
napi_create_int64(env, state.metrics.buffering_exit_count, &value);
napi_set_named_property(env, metrics, "bufferingExitCount", value);
napi_create_int64(env, state.metrics.buffering_total_ms, &value);
napi_set_named_property(env, metrics, "bufferingTotalMs", value);
napi_create_int64(env, state.metrics.buffering_max_single_ms, &value);
napi_set_named_property(env, metrics, "bufferingMaxSingleMs", value);
napi_create_int64(env, state.metrics.network_wait_ms, &value);
napi_set_named_property(env, metrics, "networkWaitMs", value);
napi_create_int64(env, state.metrics.late_frames, &value);
napi_set_named_property(env, metrics, "lateFrames", value);
napi_create_int64(env, state.metrics.mistimed_frames, &value);
napi_set_named_property(env, metrics, "mistimedFrames", value);
napi_create_int64(env, state.metrics.decoder_frames, &value);
napi_set_named_property(env, metrics, "decoderFrames", value);
napi_set_named_property(env, object, "metrics", metrics);
napi_value timeline = nullptr;
napi_create_object(env, &timeline);
auto setTimeline = [&](const char* name, int64_t milliseconds) {
napi_value item = nullptr;
napi_create_int64(env, milliseconds, &item);
napi_set_named_property(env, timeline, name, item);
};
setTimeline("sessionSetupMs", state.timeline.session_setup_ms);
setTimeline("mpvCreateMs", state.timeline.mpv_create_ms);
setTimeline("mpvInitializeMs", state.timeline.mpv_initialize_ms);
setTimeline("loadCommandMs", state.timeline.load_command_ms);
setTimeline("startFileMs", state.timeline.start_file_ms);
setTimeline("networkOpenBeginMs", state.timeline.network_open_begin_ms);
setTimeline("networkOpenEndMs", state.timeline.network_open_end_ms);
setTimeline("firstByteMs", state.timeline.first_byte_ms);
setTimeline("demuxReadyMs", state.timeline.demux_ready_ms);
setTimeline("fileLoadedMs", state.timeline.file_loaded_ms);
setTimeline("videoDecoderReadyMs", state.timeline.video_decoder_ready_ms);
setTimeline("audioDecoderReadyMs", state.timeline.audio_decoder_ready_ms);
setTimeline("videoReconfigMs", state.timeline.video_reconfig_ms);
setTimeline("audioReconfigMs", state.timeline.audio_reconfig_ms);
setTimeline("renderContextReadyMs", state.timeline.render_context_ready_ms);
setTimeline("firstVideoFrameAvailableMs", state.timeline.first_video_frame_available_ms);
setTimeline("firstRenderBeginMs", state.timeline.first_render_begin_ms);
setTimeline("firstRenderEndMs", state.timeline.first_render_end_ms);
setTimeline("firstSwapMs", state.timeline.first_swap_ms);
setTimeline("startupTotalMs", state.timeline.startup_total_ms);
setTimeline("audioRendererStartMs", state.timeline.audio_renderer_start_ms);
setTimeline("firstAudioCallbackMs", state.timeline.first_audio_callback_ms);
setTimeline("playbackRestartMs", state.timeline.playback_restart_ms);
setTimeline("seekRequestMs", state.timeline.seek_request_ms);
setTimeline("seekCommandMs", state.timeline.seek_command_ms);
setTimeline("seekDemuxMs", state.timeline.seek_demux_ms);
setTimeline("seekVideoReconfigMs", state.timeline.seek_video_reconfig_ms);
setTimeline("seekPlaybackRestartMs", state.timeline.seek_playback_restart_ms);
setTimeline("seekFirstFrameMs", state.timeline.seek_first_frame_ms);
setTimeline("seekFirstSwapMs", state.timeline.seek_first_swap_ms);
setTimeline("seekCommandLatencyMs", state.timeline.seek_command_latency_ms);
setTimeline("seekRecoveryMs", state.timeline.seek_recovery_ms);
setTimeline("seekFirstVisibleFrameMs", state.timeline.seek_first_visible_frame_ms);
napi_set_named_property(env, object, "timeline", timeline);
napi_value media = nullptr;
napi_create_object(env, &media);
napi_create_string_utf8(env, state.media.container.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "container", value);
napi_create_string_utf8(env, state.media.demuxer.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "demuxer", value);
napi_create_double(env, state.media.duration, &value);
napi_set_named_property(env, media, "duration", value);
napi_create_string_utf8(env, state.media.videoCodec.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "videoCodec", value);
napi_create_string_utf8(env, state.media.audioCodec.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "audioCodec", value);
napi_create_string_utf8(env, state.media.pixelFormat.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "pixelFormat", value);
napi_create_string_utf8(env, state.media.colorPrimaries.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorPrimaries", value);
napi_create_string_utf8(env, state.media.colorTransfer.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorTransfer", value);
napi_create_int32(env, state.media.width, &value);
napi_set_named_property(env, media, "width", value);
napi_create_int32(env, state.media.height, &value);
napi_set_named_property(env, media, "height", value);
napi_create_int32(env, state.media.videoWidth, &value);
napi_set_named_property(env, media, "videoWidth", value);
napi_create_int32(env, state.media.videoHeight, &value);
napi_set_named_property(env, media, "videoHeight", value);
napi_create_string_utf8(env, state.media.colorMatrix.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorMatrix", value);
napi_create_double(env, state.media.fps, &value);
napi_set_named_property(env, media, "fps", value);
napi_create_int64(env, state.media.bitrate, &value);
napi_set_named_property(env, media, "bitrate", value);
napi_create_int32(env, state.media.bitDepth, &value);
napi_set_named_property(env, media, "bitDepth", value);
napi_create_int32(env, state.media.audioSampleRate, &value);
napi_set_named_property(env, media, "audioSampleRate", value);
napi_create_int32(env, state.media.audioChannels, &value);
napi_set_named_property(env, media, "audioChannels", value);
napi_create_string_utf8(env, state.media.audioChannelLayout.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "audioChannelLayout", value);
napi_create_int32(env, state.media.trackCount, &value);
napi_set_named_property(env, media, "trackCount", value);
napi_create_int32(env, state.media.subtitleCount, &value);
napi_set_named_property(env, media, "subtitleCount", value);
napi_set_named_property(env, object, "media", media);
napi_value tracks = nullptr;
napi_create_array_with_length(env, state.tracks.size(), &tracks);
for (size_t i = 0; i < state.tracks.size(); ++i) {
const PlaybackTrack& track = state.tracks[i];
napi_value item = nullptr;
napi_create_object(env, &item);
napi_create_int32(env, track.id, &value); napi_set_named_property(env, item, "id", value);
napi_create_string_utf8(env, track.type.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "type", value);
napi_create_string_utf8(env, track.title.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "title", value);
napi_create_string_utf8(env, track.language.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "language", value);
napi_create_string_utf8(env, track.codec.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "codec", value);
napi_get_boolean(env, track.isDefault, &value); napi_set_named_property(env, item, "default", value);
napi_get_boolean(env, track.forced, &value); napi_set_named_property(env, item, "forced", value);
napi_get_boolean(env, track.selected, &value); napi_set_named_property(env, item, "selected", value);
napi_get_boolean(env, track.external, &value); napi_set_named_property(env, item, "external", value);
napi_set_element(env, tracks, i, item);
}
napi_set_named_property(env, object, "tracks", tracks);
napi_create_string_utf8(env, state.requestedDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "requestedDecoder", value);
napi_create_string_utf8(env, state.actualDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "actualDecoder", value);
napi_get_boolean(env, state.hwdecRequested, &value);
napi_set_named_property(env, object, "hwdecRequested", value);
napi_get_boolean(env, state.hwdecActive, &value);
napi_set_named_property(env, object, "hwdecActive", value);
napi_create_string_utf8(env, state.hwdecBackend.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwdecBackend", value);
napi_create_string_utf8(env, state.hwFallbackReason.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwdecFallbackReason", value);
napi_create_string_utf8(env, state.hwCapability.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "hwCapability", value);
napi_create_int64(env, state.hardwareFrameCount, &value);
napi_set_named_property(env, object, "hardwareFrameCount", value);
napi_create_int64(env, state.softwareFrameCount, &value);
napi_set_named_property(env, object, "softwareFrameCount", value);
napi_create_int64(env, state.hwFallbackCount, &value);
napi_set_named_property(env, object, "hwFallbackCount", value);
napi_create_int64(env, state.nativeBufferImportCount, &value);
napi_set_named_property(env, object, "nativeBufferImportCount", value);
napi_create_int64(env, state.nativeBufferImportFailure, &value);
napi_set_named_property(env, object, "nativeBufferImportFailure", value);
napi_create_int64(env, state.surfacePresentCount, &value);
napi_set_named_property(env, object, "surfacePresentCount", value);
napi_create_int64(env, state.gpuInteropFrameCount, &value);
napi_set_named_property(env, object, "gpuInteropFrameCount", value);
napi_create_string_utf8(env, gRenderer.surfaceStatus(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "surface", value);
napi_create_string_utf8(env, gRenderer.eglStatus(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "egl", value);
napi_create_string_utf8(env, gRenderer.mpvStatus(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "mpv", value);
mpv_harmony_audio_stats audio = {};
bool audioAvailable = mpv_harmony_audio_get_stats(&audio) != 0;
napi_get_boolean(env, audioAvailable, &value);
napi_set_named_property(env, object, "audioAvailable", value);
napi_create_string_utf8(env, audioAvailable ? "harmony" : "disabled", NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, object, "audioBackend", value);
napi_create_int32(env, audio.sample_rate, &value);
napi_set_named_property(env, object, "sampleRate", value);
napi_create_int32(env, audio.sample_format, &value);
napi_set_named_property(env, object, "sampleFormat", value);
napi_create_int32(env, audio.channels, &value);
napi_set_named_property(env, object, "channels", value);
napi_create_double(env, static_cast<double>(audio.channel_layout), &value);
napi_set_named_property(env, object, "channelLayout", value);
napi_create_int64(env, audio.submitted_frames, &value);
napi_set_named_property(env, object, "submittedFrames", value);
napi_create_int64(env, audio.played_frames, &value);
napi_set_named_property(env, object, "playedFrames", value);
napi_create_int64(env, audio.estimated_delay_ms, &value);
napi_set_named_property(env, object, "estimatedAudioDelayMs", value);
napi_create_int64(env, audio.buffer_starvation, &value);
napi_set_named_property(env, object, "audioBufferStarvation", value);
napi_create_int64(env, audio.audio_flush_count, &value);
napi_set_named_property(env, object, "audioFlushCount", value);
napi_create_int64(env, audio.seek_audio_reset_count, &value);
napi_set_named_property(env, object, "seekAudioResetCount", value);
napi_create_int32(env, audio.state, &value);
napi_set_named_property(env, object, "audioState", value);
napi_value profiling = nullptr;
napi_create_object(env, &profiling);
const uint64_t payloadBytes = static_cast<uint64_t>(state.lastError.size() + state.errorCode.size() +
state.network.protocol.size() + state.network.host.size() +
state.tracks.size() * 64 + 2048);
const uint64_t serializeUs = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - serializeBegin).count());
gStateSerializeUs.fetch_add(serializeUs, std::memory_order_relaxed);
gStatePayloadBytes.fetch_add(payloadBytes, std::memory_order_relaxed);
napi_create_int64(env, static_cast<int64_t>(stateCall), &value);
napi_set_named_property(env, profiling, "getStateCalls", value);
napi_create_int64(env, static_cast<int64_t>(gStateSerializeUs.load(std::memory_order_relaxed)), &value);
napi_set_named_property(env, profiling, "serializeUsTotal", value);
napi_create_int64(env, static_cast<int64_t>(gStatePayloadBytes.load(std::memory_order_relaxed)), &value);
napi_set_named_property(env, profiling, "payloadBytesTotal", value);
napi_create_int64(env, static_cast<int64_t>(serializeUs), &value);
napi_set_named_property(env, profiling, "lastSerializeUs", value);
napi_create_int64(env, static_cast<int64_t>(payloadBytes), &value);
napi_set_named_property(env, profiling, "lastPayloadBytes", value);
napi_create_int64(env, static_cast<int64_t>(state.renderUpdateCalls), &value);
napi_set_named_property(env, profiling, "renderUpdateCalls", value);
napi_create_int64(env, static_cast<int64_t>(state.renderFrameUpdates), &value);
napi_set_named_property(env, profiling, "renderFrameUpdates", value);
napi_create_int64(env, static_cast<int64_t>(state.coreSyncCalls), &value);
napi_set_named_property(env, profiling, "coreSyncCalls", value);
napi_create_int64(env, static_cast<int64_t>(state.coreSyncSkipped), &value);
napi_set_named_property(env, profiling, "coreSyncSkipped", value);
napi_set_named_property(env, object, "napiProfiling", profiling);
return object;
}
napi_value Init(napi_env env, napi_value exports)
{
gPlayer.initialize();
napi_value xcomponent = nullptr;
if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &xcomponent) == napi_ok) {
OH_NativeXComponent* component = nullptr;
if (napi_unwrap(env, xcomponent, reinterpret_cast<void**>(&component)) == napi_ok && component != nullptr) {
static OH_NativeXComponent_Callback callbacks = {
onSurfaceCreated, onSurfaceChanged, onSurfaceDestroyed, onTouch
};
OH_NativeXComponent_RegisterCallback(component, &callbacks);
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "XComponent callback registered");
}
}
napi_property_descriptor properties[] = {
{"load", nullptr, load, nullptr, nullptr, nullptr, napi_default, nullptr},
{"play", nullptr, play, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setAudioEnabled", nullptr, setAudioEnabled, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setHardwareDecode", nullptr, setHardwareDecode, nullptr, nullptr, nullptr, napi_default, nullptr},
{"pause", nullptr, pause, nullptr, nullptr, nullptr, napi_default, nullptr},
{"togglePause", nullptr, togglePause, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seek", nullptr, seek, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seekAbsolute", nullptr, seekAbsolute, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seekRelative", nullptr, seekRelative, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seekExact", nullptr, seekExact, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seekKeyframe", nullptr, seekKeyframe, nullptr, nullptr, nullptr, napi_default, nullptr},
{"seekPercent", nullptr, seekPercent, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setSpeed", nullptr, setSpeed, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setVolume", nullptr, setVolume, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setMute", nullptr, setMute, nullptr, nullptr, nullptr, napi_default, nullptr},
{"selectAudioTrack", nullptr, selectAudioTrack, nullptr, nullptr, nullptr, napi_default, nullptr},
{"selectSubtitleTrack", nullptr, selectSubtitleTrack, nullptr, nullptr, nullptr, napi_default, nullptr},
{"disableSubtitle", nullptr, disableSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
{"addSubtitle", nullptr, addSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
{"removeSubtitle", nullptr, removeSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setSubtitleDelay", nullptr, setSubtitleDelay, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setSubtitleScale", nullptr, setSubtitleScale, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setSubtitlePosition", nullptr, setSubtitlePosition, nullptr, nullptr, nullptr, napi_default, nullptr},
{"setAssOverride", nullptr, setAssOverride, nullptr, nullptr, nullptr, napi_default, nullptr},
{"stop", nullptr, stop, nullptr, nullptr, nullptr, napi_default, nullptr},
{"release", nullptr, release, nullptr, nullptr, nullptr, napi_default, nullptr},
{"getState", nullptr, getState, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties);
return exports;
}
}
NAPI_MODULE(entry, Init)
@@ -0,0 +1,30 @@
#pragma once
#include <mpv/client.h>
#include "mpv/harmony_playback.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Private renderer bridge; never install this declaration as public API. */
mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player);
enum mp_harmony_internal_timeline_event {
MP_HARMONY_INTERNAL_RENDER_CONTEXT_READY = 1,
MP_HARMONY_INTERNAL_FIRST_VIDEO_FRAME,
MP_HARMONY_INTERNAL_FIRST_RENDER_BEGIN,
MP_HARMONY_INTERNAL_FIRST_RENDER_END,
MP_HARMONY_INTERNAL_FIRST_SWAP,
MP_HARMONY_INTERNAL_AUDIO_RENDERER_START,
MP_HARMONY_INTERNAL_FIRST_AUDIO_CALLBACK,
MP_HARMONY_INTERNAL_SEEK_FIRST_FRAME,
MP_HARMONY_INTERNAL_SEEK_FIRST_SWAP,
};
int mp_harmony_playback_internal_mark_timeline(mp_harmony_player *player, int event);
#ifdef __cplusplus
}
#endif
+675
View File
@@ -0,0 +1,675 @@
#include "player.h"
#include "video/harmony_hwdec.h"
#include "audio/out/ao_harmony.h"
#include <hilog/log.h>
#include <chrono>
#include <cmath>
#include <algorithm>
#include <cctype>
#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0xD001
#define LOG_TAG "VelixPlayback"
namespace {
bool supportedSubtitleEncoding(const std::string& encoding)
{
if (encoding.empty()) return true;
std::string value = encoding;
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char character) { return static_cast<char>(std::tolower(character)); });
return value == "utf-8" || value == "utf8";
}
const char* trackTypeName(mp_harmony_track_type type)
{
switch (type) {
case MP_HARMONY_TRACK_VIDEO: return "video";
case MP_HARMONY_TRACK_AUDIO: return "audio";
case MP_HARMONY_TRACK_SUBTITLE: return "sub";
default: return "unknown";
}
}
}
PlaybackEngine::PlaybackEngine()
{
mpv_harmony_hwdec_set_requested(0);
}
PlaybackEngine::~PlaybackEngine()
{
release();
}
bool PlaybackEngine::initialize()
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (initialized_.load()) return true;
mp_harmony_player* player = mp_harmony_playback_create();
if (player == nullptr) {
setError("playback core create failed");
return false;
}
if (mp_harmony_playback_initialize(player) < 0) {
setError("playback core initialize failed");
mp_harmony_playback_destroy(player);
return false;
}
mpv_handle* handle = mp_harmony_playback_internal_handle(player);
if (handle == nullptr) {
setError("playback core handle unavailable");
mp_harmony_playback_destroy(player);
return false;
}
corePlayer_ = player;
handle_ = handle;
initialized_.store(true);
startedAt_ = std::chrono::steady_clock::now();
setState("IDLE");
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "playback core initialized");
return true;
}
bool PlaybackEngine::setAudioEnabled(bool enabled)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_audio_enabled(corePlayer_, enabled ? 1 : 0);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setHardwareDecode(bool enabled)
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (handle_ == nullptr) return false;
if (mpv_set_property_string(handle_, "hwdec", enabled ? "ohcodec" : "no") < 0) {
setError("hardware decode toggle failed");
return false;
}
mpv_harmony_hwdec_set_requested(enabled ? 1 : 0);
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.hwdecRequested = enabled;
snapshot_.requestedDecoder = enabled ? "ohcodec" : "software";
if (!enabled) {
snapshot_.hwdecActive = false;
snapshot_.actualDecoder = "software";
} else {
snapshot_.actualDecoder = "pending";
snapshot_.hwFallbackReason.clear();
}
return true;
}
void PlaybackEngine::load(const std::string& url)
{
PlaybackRequestModel request;
request.url = url;
load(request);
}
bool PlaybackEngine::load(const PlaybackRequestModel& request)
{
if (request.url.empty()) {
setError("empty URL");
return false;
}
if (request.headers.size() > MP_HARMONY_MAX_HEADERS ||
request.subtitleFiles.size() > MP_HARMONY_MAX_SUBTITLES ||
(request.playbackSpeed != 0.0 &&
(!std::isfinite(request.playbackSpeed) || request.playbackSpeed <= 0.0)) ||
(request.networkTimeout != 0.0 &&
(!std::isfinite(request.networkTimeout) || request.networkTimeout < 0.0))) {
setError("invalid playback request");
return false;
}
for (const auto& subtitle : request.subtitleFiles) {
if (!supportedSubtitleEncoding(subtitle.encoding)) {
setError("unsupported subtitle encoding");
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.errorCode = "SUBTITLE";
return false;
}
}
if (!initialize()) return false;
std::vector<mp_harmony_http_header> headers;
headers.reserve(request.headers.size());
for (const auto& header : request.headers) {
headers.push_back({header.name.c_str(), header.value.c_str()});
}
std::vector<mp_harmony_subtitle_source> subtitles;
subtitles.reserve(request.subtitleFiles.size());
for (const auto& subtitle : request.subtitleFiles) {
subtitles.push_back({subtitle.url.c_str(), subtitle.title.c_str(), subtitle.language.c_str(),
subtitle.encoding.c_str(), subtitle.forced ? 1 : 0,
subtitle.select ? 1 : 0, subtitle.isDefault ? 1 : 0});
}
mp_harmony_playback_request coreRequest = {};
coreRequest.url = request.url.c_str();
coreRequest.title = request.title.c_str();
coreRequest.start_position_ms = request.startPositionMs;
coreRequest.user_agent = request.userAgent.c_str();
coreRequest.referer = request.referer.c_str();
coreRequest.cookies = request.cookies.c_str();
coreRequest.http_headers = headers.empty() ? nullptr : headers.data();
coreRequest.http_header_count = headers.size();
coreRequest.subtitle_files = subtitles.empty() ? nullptr : subtitles.data();
coreRequest.subtitle_file_count = subtitles.size();
coreRequest.preferred_audio_language = request.preferredAudioLanguage.c_str();
coreRequest.preferred_subtitle_language = request.preferredSubtitleLanguage.c_str();
coreRequest.subtitle_enabled = request.subtitleEnabled;
coreRequest.audio_track_id = request.audioTrackId;
coreRequest.subtitle_track_id = request.subtitleTrackId;
coreRequest.playback_speed = request.playbackSpeed;
coreRequest.network_timeout = request.networkTimeout;
coreRequest.tls_ca_file = request.tlsCaFile.c_str();
{
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.renderCount = 0;
snapshot_.firstFrameMs = -1;
snapshot_.metrics = {};
snapshot_.timeline = {};
startedAt_ = std::chrono::steady_clock::now();
}
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_load(corePlayer_, &coreRequest);
}
syncCoreSnapshot(true);
return rc >= 0;
}
void PlaybackEngine::play()
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_play(corePlayer_);
}
if (rc < 0) setError("play failed");
syncCoreSnapshot();
}
void PlaybackEngine::pause()
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_pause(corePlayer_);
}
if (rc < 0) setError("pause failed");
syncCoreSnapshot();
}
void PlaybackEngine::togglePause()
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_toggle_pause(corePlayer_);
}
if (rc < 0) setError("toggle pause failed");
syncCoreSnapshot();
}
bool PlaybackEngine::seekAbsolute(double seconds, bool exact, bool keyframe)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr)
rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_ABSOLUTE, seconds,
exact ? 1 : 0, keyframe ? 1 : 0);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::seekPercent(double percent, bool exact, bool keyframe)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr)
rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_PERCENT, percent,
exact ? 1 : 0, keyframe ? 1 : 0);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setSpeed(double speed)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_speed(corePlayer_, speed);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setVolume(double volume)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_volume(corePlayer_, volume);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setMute(bool muted)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_mute(corePlayer_, muted ? 1 : 0);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::selectAudioTrack(int id)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_select_audio(corePlayer_, id);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::selectSubtitleTrack(int id)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_select_subtitle(corePlayer_, id);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::disableSubtitle()
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_disable_subtitle(corePlayer_);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::addSubtitle(const PlaybackSubtitleSource& source)
{
if (source.url.empty()) return false;
if (!supportedSubtitleEncoding(source.encoding)) {
setError("unsupported subtitle encoding");
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.errorCode = "SUBTITLE";
return false;
}
mp_harmony_subtitle_source coreSource = {source.url.c_str(), source.title.c_str(), source.language.c_str(),
source.encoding.c_str(), source.forced ? 1 : 0,
source.select ? 1 : 0, source.isDefault ? 1 : 0};
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_add_subtitle(corePlayer_, &coreSource);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::removeSubtitle(int id)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_remove_subtitle(corePlayer_, id);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setSubtitleDelay(double seconds)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_delay(corePlayer_, seconds);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setSubtitleScale(double scale)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_scale(corePlayer_, scale);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setSubtitlePosition(double position)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_position(corePlayer_, position);
}
syncCoreSnapshot();
return rc >= 0;
}
bool PlaybackEngine::setAssOverride(const std::string& mode)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_ass_override(corePlayer_, mode.c_str());
}
syncCoreSnapshot();
return rc >= 0;
}
void PlaybackEngine::seek(double delta)
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_RELATIVE,
delta, 0, 0);
}
if (rc < 0) setError("seek failed");
syncCoreSnapshot();
}
void PlaybackEngine::stop()
{
int rc = -1;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr) rc = mp_harmony_playback_stop(corePlayer_);
}
if (rc < 0) setError("stop failed");
syncCoreSnapshot();
}
void PlaybackEngine::release()
{
mp_harmony_player* player = nullptr;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
player = corePlayer_;
corePlayer_ = nullptr;
handle_ = nullptr;
initialized_.store(false);
if (player != nullptr) mp_harmony_playback_destroy(player);
}
if (player != nullptr) {
mpv_harmony_hwdec_set_requested(0);
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.state = "RELEASED";
snapshot_.hwdecRequested = false;
snapshot_.hwdecActive = false;
snapshot_.actualDecoder = "software";
}
}
void PlaybackEngine::markFrameRendered()
{
std::lock_guard<std::mutex> lock(mutex_);
++snapshot_.renderCount;
if (!snapshot_.hwdecActive) mpv_harmony_hwdec_record_software_frame();
if (snapshot_.firstFrameMs < 0) {
const auto elapsed = std::chrono::steady_clock::now() - startedAt_;
snapshot_.firstFrameMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
}
}
uint64_t PlaybackEngine::sessionId() const
{
return sessionIdAtomic_.load(std::memory_order_acquire);
}
void PlaybackEngine::markRenderUpdate(bool frameAvailable)
{
renderUpdateCalls_.fetch_add(1, std::memory_order_relaxed);
if (frameAvailable) renderFrameUpdates_.fetch_add(1, std::memory_order_relaxed);
}
void PlaybackEngine::markTimeline(int event)
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr)
mp_harmony_playback_internal_mark_timeline(corePlayer_, event);
}
void PlaybackEngine::syncAudioTimeline()
{
mpv_harmony_audio_stats audio = {};
if (!mpv_harmony_audio_get_stats(&audio)) return;
if (audio.state == MPV_HARMONY_AUDIO_RUNNING)
markTimeline(MP_HARMONY_INTERNAL_AUDIO_RENDERER_START);
if (audio.callback_count > 0)
markTimeline(MP_HARMONY_INTERNAL_FIRST_AUDIO_CALLBACK);
}
PlaybackSnapshot PlaybackEngine::snapshot() const
{
const_cast<PlaybackEngine*>(this)->syncAudioTimeline();
const_cast<PlaybackEngine*>(this)->syncCoreSnapshot();
std::lock_guard<std::mutex> lock(mutex_);
const_cast<PlaybackEngine*>(this)->syncHwdecStatsLocked();
const_cast<PlaybackEngine*>(this)->snapshot_.renderUpdateCalls =
renderUpdateCalls_.load(std::memory_order_relaxed);
const_cast<PlaybackEngine*>(this)->snapshot_.renderFrameUpdates =
renderFrameUpdates_.load(std::memory_order_relaxed);
const_cast<PlaybackEngine*>(this)->snapshot_.coreSyncCalls =
coreSyncCalls_.load(std::memory_order_relaxed);
const_cast<PlaybackEngine*>(this)->snapshot_.coreSyncSkipped =
coreSyncSkipped_.load(std::memory_order_relaxed);
return snapshot_;
}
void PlaybackEngine::syncCoreSnapshot(bool force)
{
const auto now = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(syncThrottleMutex_);
if (!force && coreSyncHasRun_ && now - lastCoreSync_ < std::chrono::milliseconds(100)) {
coreSyncSkipped_.fetch_add(1, std::memory_order_relaxed);
return;
}
lastCoreSync_ = now;
coreSyncHasRun_ = true;
}
coreSyncCalls_.fetch_add(1, std::memory_order_relaxed);
mp_harmony_playback_snapshot core = {};
mp_harmony_track coreTracks[MP_HARMONY_MAX_TRACKS] = {};
size_t coreTrackCount = 0;
mp_harmony_media_info coreMedia = {};
mp_harmony_network_info coreNetwork = {};
bool tracksAvailable = false;
bool available = false;
{
std::lock_guard<std::mutex> apiLock(apiMutex_);
if (corePlayer_ != nullptr && mp_harmony_playback_get_snapshot(corePlayer_, &core) >= 0) {
available = true;
tracksAvailable = mp_harmony_playback_get_tracks(corePlayer_, coreTracks,
MP_HARMONY_MAX_TRACKS, &coreTrackCount) >= 0;
if (mp_harmony_playback_get_media_info(corePlayer_, &coreMedia) >= 0) core.media = coreMedia;
if (mp_harmony_playback_get_network_info(corePlayer_, &coreNetwork) >= 0) core.network = coreNetwork;
}
}
if (!available) return;
std::lock_guard<std::mutex> lock(mutex_);
const uint64_t renderCount = snapshot_.renderCount;
const int64_t firstFrameMs = snapshot_.firstFrameMs;
const bool hwdecRequested = snapshot_.hwdecRequested;
const bool hwdecActive = snapshot_.hwdecActive;
const std::string requestedDecoder = snapshot_.requestedDecoder;
const std::string actualDecoder = snapshot_.actualDecoder;
const std::string hwdecBackend = snapshot_.hwdecBackend;
const std::string hwFallbackReason = snapshot_.hwFallbackReason;
const std::string hwCapability = snapshot_.hwCapability;
const int64_t hardwareFrameCount = snapshot_.hardwareFrameCount;
const int64_t softwareFrameCount = snapshot_.softwareFrameCount;
const int64_t hwFallbackCount = snapshot_.hwFallbackCount;
const int64_t nativeBufferImportCount = snapshot_.nativeBufferImportCount;
const int64_t nativeBufferImportFailure = snapshot_.nativeBufferImportFailure;
const int64_t surfacePresentCount = snapshot_.surfacePresentCount;
const int64_t gpuInteropFrameCount = snapshot_.gpuInteropFrameCount;
snapshot_.state = mp_harmony_playback_state_name(core.state);
snapshot_.sessionId = core.session_id;
sessionIdAtomic_.store(core.session_id, std::memory_order_release);
snapshot_.position = core.position;
snapshot_.duration = core.duration;
snapshot_.fps = core.fps;
snapshot_.dropped = core.dropped_frames;
snapshot_.lastError = core.error.raw_message;
snapshot_.errorCode = mp_harmony_playback_error_name(core.error_code);
snapshot_.speed = core.speed;
snapshot_.volume = core.volume;
snapshot_.muted = core.muted != 0;
snapshot_.subtitleDelayMs = core.subtitle_delay_ms;
snapshot_.subtitleScale = core.subtitle_scale;
snapshot_.subtitlePosition = core.subtitle_position;
snapshot_.media.container = core.media.container;
snapshot_.media.demuxer = core.media.demuxer;
snapshot_.media.duration = core.media.duration;
snapshot_.media.videoCodec = core.media.video_codec;
snapshot_.media.audioCodec = core.media.audio_codec;
snapshot_.media.pixelFormat = core.media.pixel_format;
snapshot_.media.colorPrimaries = core.media.color_primaries;
snapshot_.media.colorTransfer = core.media.color_transfer;
snapshot_.media.colorSpace = core.media.color_space;
snapshot_.media.width = core.media.width;
snapshot_.media.height = core.media.height;
snapshot_.media.videoWidth = core.media.video_width;
snapshot_.media.videoHeight = core.media.video_height;
snapshot_.media.colorMatrix = core.media.color_matrix;
snapshot_.media.bitDepth = core.media.bit_depth;
snapshot_.media.audioSampleRate = core.media.audio_sample_rate;
snapshot_.media.audioChannels = core.media.audio_channels;
snapshot_.media.audioChannelLayout = core.media.audio_channel_layout;
snapshot_.media.fps = core.media.fps;
snapshot_.media.bitrate = core.media.bitrate;
snapshot_.media.videoBitrate = core.media.video_bitrate;
snapshot_.media.audioBitrate = core.media.audio_bitrate;
snapshot_.media.trackCount = core.media.track_count;
snapshot_.media.subtitleCount = core.media.subtitle_count;
snapshot_.network.rangeSupported = core.network.range_supported;
snapshot_.network.seekResult = core.network.seek_result;
snapshot_.network.protocol = core.network.protocol;
snapshot_.network.host = core.network.host;
snapshot_.network.networkState = core.network.network_state;
snapshot_.network.httpStatus = core.network.http_status;
snapshot_.network.redirectCount = core.network.redirect_count;
snapshot_.network.buffering = core.network.buffering != 0;
snapshot_.network.cacheDuration = core.network.cache_duration;
snapshot_.network.cacheBytes = core.network.cache_bytes;
snapshot_.network.demuxer = core.network.demuxer;
snapshot_.network.container = core.network.container;
snapshot_.network.currentBitrate = core.network.current_bitrate;
snapshot_.metrics = core.metrics;
snapshot_.metrics.dropped_frames = core.dropped_frames;
snapshot_.metrics.render_count = static_cast<int64_t>(renderCount);
snapshot_.timeline = core.timeline;
if (tracksAvailable) {
snapshot_.tracks.clear();
const size_t count = std::min(coreTrackCount, static_cast<size_t>(MP_HARMONY_MAX_TRACKS));
snapshot_.tracks.reserve(count);
for (size_t i = 0; i < count; ++i) {
PlaybackTrack track;
track.id = coreTracks[i].id;
track.type = trackTypeName(coreTracks[i].type);
track.title = coreTracks[i].title;
track.language = coreTracks[i].language;
track.codec = coreTracks[i].codec;
track.isDefault = coreTracks[i].is_default != 0;
track.forced = coreTracks[i].forced != 0;
track.selected = coreTracks[i].selected != 0;
track.external = coreTracks[i].external != 0;
snapshot_.tracks.push_back(std::move(track));
}
}
snapshot_.renderCount = renderCount;
snapshot_.firstFrameMs = firstFrameMs;
snapshot_.hwdecRequested = hwdecRequested;
snapshot_.hwdecActive = hwdecActive;
snapshot_.requestedDecoder = requestedDecoder;
snapshot_.actualDecoder = actualDecoder;
snapshot_.hwdecBackend = hwdecBackend;
snapshot_.hwFallbackReason = hwFallbackReason;
snapshot_.hwCapability = hwCapability;
snapshot_.hardwareFrameCount = hardwareFrameCount;
snapshot_.softwareFrameCount = softwareFrameCount;
snapshot_.hwFallbackCount = hwFallbackCount;
snapshot_.nativeBufferImportCount = nativeBufferImportCount;
snapshot_.nativeBufferImportFailure = nativeBufferImportFailure;
snapshot_.surfacePresentCount = surfacePresentCount;
snapshot_.gpuInteropFrameCount = gpuInteropFrameCount;
}
void PlaybackEngine::syncHwdecStatsLocked()
{
mpv_harmony_hwdec_stats stats = {};
if (!mpv_harmony_hwdec_get_stats(&stats)) return;
snapshot_.hwdecRequested = stats.hwdec_requested != 0;
snapshot_.hwdecActive = stats.hwdec_active != 0;
snapshot_.hardwareFrameCount = stats.hardware_frame_count;
snapshot_.softwareFrameCount = stats.software_frame_count;
snapshot_.hwFallbackCount = stats.hw_fallback_count;
snapshot_.nativeBufferImportCount = stats.native_buffer_import_count;
snapshot_.nativeBufferImportFailure = stats.native_buffer_import_failure;
snapshot_.surfacePresentCount = stats.surface_present_count;
snapshot_.gpuInteropFrameCount = stats.gpu_interop_frame_count;
snapshot_.hwdecBackend = stats.hwdec_backend;
snapshot_.hwFallbackReason = stats.fallback_reason;
snapshot_.hwCapability = stats.capability_summary;
if (stats.decoder_name[0] != '\0') snapshot_.actualDecoder = stats.decoder_name;
else if (snapshot_.hwdecRequested && !snapshot_.hwdecActive && stats.hw_fallback_count > 0)
snapshot_.actualDecoder = "software";
}
void PlaybackEngine::setState(const char* state)
{
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.state = state ? state : "UNKNOWN";
}
void PlaybackEngine::setError(const std::string& error)
{
std::lock_guard<std::mutex> lock(mutex_);
snapshot_.state = "ERROR";
snapshot_.lastError = error;
snapshot_.errorCode = "UNKNOWN";
}
+203
View File
@@ -0,0 +1,203 @@
#pragma once
#include "harmony_playback_internal.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>
struct PlaybackHeader {
std::string name;
std::string value;
};
struct PlaybackSubtitleSource {
std::string url;
std::string title;
std::string language;
std::string encoding;
bool forced = false;
bool select = false;
bool isDefault = false;
};
struct PlaybackRequestModel {
std::string url;
std::string title;
int64_t startPositionMs = -1;
std::string userAgent;
std::string referer;
std::string cookies;
std::vector<PlaybackHeader> headers;
std::vector<PlaybackSubtitleSource> subtitleFiles;
std::string preferredAudioLanguage;
std::string preferredSubtitleLanguage;
int subtitleEnabled = -1;
int audioTrackId = -1;
int subtitleTrackId = -1;
double playbackSpeed = 0.0;
double networkTimeout = 0.0;
std::string tlsCaFile;
};
struct PlaybackTrack {
int id = 0;
std::string type;
std::string title;
std::string language;
std::string codec;
bool isDefault = false;
bool forced = false;
bool selected = false;
bool external = false;
};
struct PlaybackMediaInfo {
std::string container;
std::string demuxer;
double duration = 0.0;
std::string videoCodec;
std::string audioCodec;
std::string pixelFormat;
std::string colorPrimaries;
std::string colorTransfer;
std::string colorSpace;
int width = 0;
int height = 0;
int bitDepth = 0;
int audioSampleRate = 0;
int audioChannels = 0;
std::string audioChannelLayout;
double fps = 0.0;
int64_t bitrate = 0;
int64_t videoBitrate = 0;
int64_t audioBitrate = 0;
int trackCount = 0;
int subtitleCount = 0;
int videoWidth = 0;
int videoHeight = 0;
std::string colorMatrix;
};
struct PlaybackNetworkInfo {
std::string protocol;
std::string host;
std::string networkState;
int httpStatus = 0;
int redirectCount = 0;
bool buffering = false;
double cacheDuration = 0.0;
int64_t cacheBytes = 0;
std::string demuxer;
std::string container;
int64_t currentBitrate = 0;
int rangeSupported = -1;
std::string seekResult;
};
struct PlaybackSnapshot {
std::string state = "IDLE";
uint64_t sessionId = 0;
double position = 0.0;
double duration = 0.0;
double fps = 0.0;
int64_t dropped = 0;
int64_t firstFrameMs = -1;
uint64_t renderCount = 0;
std::string lastError;
std::string errorCode = "NONE";
PlaybackMediaInfo media;
PlaybackNetworkInfo network;
std::vector<PlaybackTrack> tracks;
double speed = 1.0;
double volume = 100.0;
bool muted = false;
int64_t subtitleDelayMs = 0;
double subtitleScale = 1.0;
double subtitlePosition = 100.0;
std::string requestedDecoder = "software";
std::string actualDecoder = "software";
std::string hwdecBackend;
std::string hwFallbackReason;
std::string hwCapability;
bool hwdecRequested = false;
bool hwdecActive = false;
int64_t hardwareFrameCount = 0;
int64_t softwareFrameCount = 0;
int64_t hwFallbackCount = 0;
int64_t nativeBufferImportCount = 0;
int64_t nativeBufferImportFailure = 0;
int64_t surfacePresentCount = 0;
int64_t gpuInteropFrameCount = 0;
uint64_t renderUpdateCalls = 0;
uint64_t renderFrameUpdates = 0;
uint64_t coreSyncCalls = 0;
uint64_t coreSyncSkipped = 0;
mp_harmony_playback_metrics metrics = {};
mp_harmony_playback_timeline timeline = {};
};
class PlaybackEngine {
public:
PlaybackEngine();
~PlaybackEngine();
bool initialize();
bool setAudioEnabled(bool enabled);
bool setHardwareDecode(bool enabled);
void load(const std::string& url);
bool load(const PlaybackRequestModel& request);
void play();
void pause();
void togglePause();
void seek(double delta);
bool seekAbsolute(double seconds, bool exact, bool keyframe);
bool seekPercent(double percent, bool exact, bool keyframe);
bool setSpeed(double speed);
bool setVolume(double volume);
bool setMute(bool muted);
bool selectAudioTrack(int id);
bool selectSubtitleTrack(int id);
bool disableSubtitle();
bool addSubtitle(const PlaybackSubtitleSource& source);
bool removeSubtitle(int id);
bool setSubtitleDelay(double seconds);
bool setSubtitleScale(double scale);
bool setSubtitlePosition(double position);
bool setAssOverride(const std::string& mode);
void stop();
void release();
PlaybackSnapshot snapshot() const;
mpv_handle* handle() const { return handle_; }
uint64_t sessionId() const;
void markRenderUpdate(bool frameAvailable);
void markFrameRendered();
void markTimeline(int event);
private:
void setState(const char* state);
void setError(const std::string& error);
void syncHwdecStatsLocked();
void syncAudioTimeline();
void syncCoreSnapshot(bool force = false);
mutable std::mutex mutex_;
mutable std::mutex apiMutex_;
PlaybackSnapshot snapshot_;
mp_harmony_player* corePlayer_ = nullptr;
mpv_handle* handle_ = nullptr;
std::atomic<bool> initialized_{false};
std::atomic<uint64_t> sessionIdAtomic_{0};
std::atomic<uint64_t> renderUpdateCalls_{0};
std::atomic<uint64_t> renderFrameUpdates_{0};
std::atomic<uint64_t> coreSyncCalls_{0};
std::atomic<uint64_t> coreSyncSkipped_{0};
mutable std::mutex syncThrottleMutex_;
std::chrono::steady_clock::time_point lastCoreSync_;
bool coreSyncHasRun_ = false;
std::chrono::steady_clock::time_point startedAt_;
};
+277
View File
@@ -0,0 +1,277 @@
#include "render.h"
#include <GLES3/gl3.h>
#include <EGL/egl.h>
#include <hilog/log.h>
#include <mpv/render_gl.h>
#include <cstring>
#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0xD001
#define LOG_TAG "VelixRender"
EglRenderer::EglRenderer(PlaybackEngine& player) : player_(player) {}
EglRenderer::~EglRenderer()
{
onSurfaceDestroyed(nullptr);
}
const char* EglRenderer::surfaceStatus() const
{
switch (surfaceStatus_.load()) {
case 1: return "READY";
case 2: return "ERROR";
case 3: return "RELEASED";
default: return "WAITING";
}
}
const char* EglRenderer::eglStatus() const
{
switch (eglStatus_.load()) {
case 1: return "READY";
case 2: return "ERROR";
case 3: return "RELEASED";
default: return "WAITING";
}
}
const char* EglRenderer::mpvStatus() const
{
switch (mpvStatus_.load()) {
case 1: return "READY";
case 2: return "ERROR";
case 3: return "RELEASED";
default: return "WAITING";
}
}
void EglRenderer::onSurfaceCreated(OH_NativeXComponent* component, void* surface)
{
onSurfaceDestroyed(nullptr);
OHNativeWindow* window = static_cast<OHNativeWindow*>(surface);
if (window == nullptr) {
surfaceStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "OHNativeWindow creation failed");
return;
}
if (OH_NativeWindow_NativeObjectReference(window) != 0) {
surfaceStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "OHNativeWindow reference failed");
return;
}
uint64_t width = 1;
uint64_t height = 1;
if (component != nullptr) {
OH_NativeXComponent_GetXComponentSize(component, surface, &width, &height);
}
{
std::lock_guard<std::mutex> lock(mutex_);
window_ = window;
width_ = width > 0 ? static_cast<int>(width) : 1;
height_ = height > 0 ? static_cast<int>(height) : 1;
stop_.store(false);
surfaceReady_.store(true);
redraw_ = true;
}
surfaceStatus_.store(1);
eglStatus_.store(0);
mpvStatus_.store(0);
thread_ = std::thread(&EglRenderer::renderLoop, this);
wake_.notify_one();
}
void EglRenderer::onSurfaceChanged(OH_NativeXComponent* component, void* surface)
{
uint64_t width = 1;
uint64_t height = 1;
if (component != nullptr) {
OH_NativeXComponent_GetXComponentSize(component, surface, &width, &height);
}
{
std::lock_guard<std::mutex> lock(mutex_);
width_ = width > 0 ? static_cast<int>(width) : width_;
height_ = height > 0 ? static_cast<int>(height) : height_;
redraw_ = true;
}
wake_.notify_one();
}
void EglRenderer::onSurfaceDestroyed(void*)
{
surfaceReady_.store(false);
stop_.store(true);
wake_.notify_one();
if (thread_.joinable()) {
thread_.join();
}
std::lock_guard<std::mutex> lock(mutex_);
if (window_ != nullptr) {
OH_NativeWindow_NativeObjectUnreference(window_);
window_ = nullptr;
}
surfaceStatus_.store(3);
eglStatus_.store(3);
mpvStatus_.store(3);
}
void EglRenderer::requestRender()
{
{
std::lock_guard<std::mutex> lock(mutex_);
redraw_ = true;
}
wake_.notify_one();
}
bool EglRenderer::initializeEgl(OHNativeWindow* window)
{
display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display_ == EGL_NO_DISPLAY || eglInitialize(display_, nullptr, nullptr) != EGL_TRUE) {
eglStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL initialize failed: 0x%{public}x", eglGetError());
return false;
}
const EGLint configAttrs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
EGL_NONE
};
EGLConfig config = nullptr;
EGLint count = 0;
if (eglChooseConfig(display_, configAttrs, &config, 1, &count) != EGL_TRUE || count == 0) {
eglStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL config selection failed: 0x%{public}x", eglGetError());
destroyEgl();
return false;
}
const EGLint contextAttrs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttrs);
surface_ = eglCreateWindowSurface(display_, config, reinterpret_cast<EGLNativeWindowType>(window), nullptr);
if (context_ == EGL_NO_CONTEXT || surface_ == EGL_NO_SURFACE ||
eglMakeCurrent(display_, surface_, surface_, context_) != EGL_TRUE) {
eglStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "EGL context/surface failed: 0x%{public}x", eglGetError());
destroyEgl();
return false;
}
eglSwapInterval(display_, 1);
glViewport(0, 0, width_, height_);
eglStatus_.store(1);
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "EGL vendor=%{public}s version=%{public}s renderer=%{public}s",
eglQueryString(display_, EGL_VENDOR), eglQueryString(display_, EGL_VERSION),
reinterpret_cast<const char*>(glGetString(GL_RENDERER)));
return true;
}
void EglRenderer::destroyEgl()
{
if (display_ != EGL_NO_DISPLAY) {
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (surface_ != EGL_NO_SURFACE) eglDestroySurface(display_, surface_);
if (context_ != EGL_NO_CONTEXT) eglDestroyContext(display_, context_);
eglTerminate(display_);
}
display_ = EGL_NO_DISPLAY;
context_ = EGL_NO_CONTEXT;
surface_ = EGL_NO_SURFACE;
}
void* EglRenderer::getProcAddress(void*, const char* name)
{
return reinterpret_cast<void*>(eglGetProcAddress(name));
}
void EglRenderer::onMpvUpdate(void* ctx)
{
if (ctx != nullptr) static_cast<EglRenderer*>(ctx)->requestRender();
}
void EglRenderer::renderLoop()
{
OHNativeWindow* window = nullptr;
{
std::lock_guard<std::mutex> lock(mutex_);
window = window_;
}
if (window == nullptr || !initializeEgl(window)) {
mpvStatus_.store(2);
return;
}
const char* api = MPV_RENDER_API_TYPE_OPENGL;
mpv_opengl_init_params glParams = {getProcAddress, this};
mpv_render_param createParams[] = {
{MPV_RENDER_PARAM_API_TYPE, const_cast<char*>(api)},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &glParams},
{MPV_RENDER_PARAM_INVALID, nullptr},
};
int code = mpv_render_context_create(&mpvRender_, player_.handle(), createParams);
if (code < 0) {
mpvStatus_.store(2);
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "mpv render context failed: %{public}s", mpv_error_string(code));
destroyEgl();
return;
}
mpvStatus_.store(1);
player_.markTimeline(MP_HARMONY_INTERNAL_RENDER_CONTEXT_READY);
timelineSession_ = player_.sessionId();
firstFramePending_ = true;
mpv_render_context_set_update_callback(mpvRender_, onMpvUpdate, this);
auto lastUpdate = std::chrono::steady_clock::now();
while (!stop_.load()) {
int width = 1;
int height = 1;
bool shouldUpdate = false;
{
std::unique_lock<std::mutex> lock(mutex_);
wake_.wait_for(lock, std::chrono::milliseconds(16), [this] { return redraw_ || stop_.load(); });
shouldUpdate = redraw_;
redraw_ = false;
width = width_;
height = height_;
}
if (stop_.load()) break;
const auto now = std::chrono::steady_clock::now();
if (!shouldUpdate && now - lastUpdate < std::chrono::milliseconds(250)) continue;
lastUpdate = now;
const uint64_t session = player_.sessionId();
if (session != timelineSession_) {
timelineSession_ = session;
firstFramePending_ = true;
}
uint64_t flags = mpv_render_context_update(mpvRender_);
player_.markRenderUpdate((flags & MPV_RENDER_UPDATE_FRAME) != 0);
if ((flags & MPV_RENDER_UPDATE_FRAME) == 0) continue;
mpv_opengl_fbo fbo = {0, width, height, GL_RGBA8};
int flip = 1;
mpv_render_param renderParams[] = {
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
{MPV_RENDER_PARAM_FLIP_Y, &flip},
{MPV_RENDER_PARAM_INVALID, nullptr},
};
const bool firstFrame = firstFramePending_;
if (firstFrame) {
player_.markTimeline(MP_HARMONY_INTERNAL_FIRST_VIDEO_FRAME);
player_.markTimeline(MP_HARMONY_INTERNAL_FIRST_RENDER_BEGIN);
}
if (mpv_render_context_render(mpvRender_, renderParams) >= 0) {
if (firstFrame) player_.markTimeline(MP_HARMONY_INTERNAL_FIRST_RENDER_END);
if (eglSwapBuffers(display_, surface_) == EGL_TRUE) {
mpv_render_context_report_swap(mpvRender_);
if (firstFrame) {
player_.markTimeline(MP_HARMONY_INTERNAL_FIRST_SWAP);
firstFramePending_ = false;
}
player_.markFrameRendered();
}
}
}
mpv_render_context_set_update_callback(mpvRender_, nullptr, nullptr);
mpv_render_context_free(mpvRender_);
mpvRender_ = nullptr;
destroyEgl();
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "player.h"
#include <mpv/render.h>
#include <ace/xcomponent/native_interface_xcomponent.h>
#include <native_window/external_window.h>
#include <EGL/egl.h>
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
class EglRenderer {
public:
explicit EglRenderer(PlaybackEngine& player);
~EglRenderer();
void onSurfaceCreated(OH_NativeXComponent* component, void* surface);
void onSurfaceChanged(OH_NativeXComponent* component, void* surface);
void onSurfaceDestroyed(void* surface);
void requestRender();
bool hasSurface() const { return surfaceReady_.load(); }
const char* surfaceStatus() const;
const char* eglStatus() const;
const char* mpvStatus() const;
private:
void renderLoop();
bool initializeEgl(OHNativeWindow* window);
void destroyEgl();
static void onMpvUpdate(void* ctx);
static void* getProcAddress(void* ctx, const char* name);
PlaybackEngine& player_;
std::thread thread_;
mutable std::mutex mutex_;
std::condition_variable wake_;
std::atomic<bool> stop_{false};
std::atomic<bool> surfaceReady_{false};
std::atomic<int> surfaceStatus_{0};
std::atomic<int> eglStatus_{0};
std::atomic<int> mpvStatus_{0};
bool redraw_ = false;
OHNativeWindow* window_ = nullptr;
int width_ = 1;
int height_ = 1;
uint64_t timelineSession_ = 0;
bool firstFramePending_ = true;
EGLDisplay display_ = EGL_NO_DISPLAY;
EGLContext context_ = EGL_NO_CONTEXT;
EGLSurface surface_ = EGL_NO_SURFACE;
mpv_render_context* mpvRender_ = nullptr;
};
@@ -0,0 +1,8 @@
import { UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index');
}
}
@@ -0,0 +1,444 @@
import playbackLab from 'libentry.so';
interface PlaybackState {
state: string;
position: number;
duration: number;
fps: number;
dropped: number;
firstFrameMs: number;
renderCount: number;
lastError: string;
surface: string;
egl: string;
mpv: string;
audioAvailable: boolean;
audioBackend: string;
sampleRate: number;
sampleFormat: number;
channels: number;
channelLayout: number;
submittedFrames: number;
playedFrames: number;
estimatedAudioDelayMs: number;
audioBufferStarvation: number;
audioFlushCount: number;
seekAudioResetCount: number;
audioState: number;
requestedDecoder: string;
actualDecoder: string;
hwdecRequested: boolean;
hwdecActive: boolean;
hwdecBackend: string;
hwdecFallbackReason: string;
hwCapability: string;
hardwareFrameCount: number;
softwareFrameCount: number;
hwFallbackCount: number;
nativeBufferImportCount: number;
nativeBufferImportFailure: number;
surfacePresentCount: number;
gpuInteropFrameCount: number;
sessionId: number;
errorCode: string;
error: PlaybackErrorState;
metrics: PlaybackMetricsState;
timeline: PlaybackTimelineState;
napiProfiling: NapiProfilingState;
speed: number;
volume: number;
muted: boolean;
subtitleDelayMs: number;
subtitleScale: number;
subtitlePosition: number;
network: NetworkState;
media: MediaState;
tracks: TrackState[];
}
interface NapiProfilingState {
getStateCalls: number;
serializeUsTotal: number;
payloadBytesTotal: number;
lastSerializeUs: number;
lastPayloadBytes: number;
renderUpdateCalls: number;
renderFrameUpdates: number;
coreSyncCalls: number;
coreSyncSkipped: number;
}
interface PlaybackErrorState {
code: string;
rawMessage: string;
}
interface PlaybackMetricsState {
droppedFrames: number;
renderCount: number;
buffering: boolean;
cacheDuration: number;
cacheBytes: number;
currentBitrate: number;
bufferingEnterCount: number;
bufferingExitCount: number;
bufferingTotalMs: number;
bufferingMaxSingleMs: number;
networkWaitMs: number;
lateFrames: number;
mistimedFrames: number;
decoderFrames: number;
}
interface PlaybackTimelineState {
sessionSetupMs: number;
mpvCreateMs: number;
mpvInitializeMs: number;
loadCommandMs: number;
startFileMs: number;
networkOpenBeginMs: number;
networkOpenEndMs: number;
firstByteMs: number;
demuxReadyMs: number;
fileLoadedMs: number;
videoDecoderReadyMs: number;
audioDecoderReadyMs: number;
videoReconfigMs: number;
audioReconfigMs: number;
renderContextReadyMs: number;
firstVideoFrameAvailableMs: number;
firstRenderBeginMs: number;
firstRenderEndMs: number;
firstSwapMs: number;
startupTotalMs: number;
audioRendererStartMs: number;
firstAudioCallbackMs: number;
playbackRestartMs: number;
seekRequestMs: number;
seekCommandMs: number;
seekDemuxMs: number;
seekVideoReconfigMs: number;
seekPlaybackRestartMs: number;
seekFirstFrameMs: number;
seekFirstSwapMs: number;
seekCommandLatencyMs: number;
seekRecoveryMs: number;
seekFirstVisibleFrameMs: number;
}
interface NetworkState {
protocol: string;
host: string;
networkState: string;
httpStatus: number;
redirectCount: number;
buffering: boolean;
cacheDuration: number;
cacheBytes: number;
demuxer: string;
container: string;
currentBitrate: number;
rangeSupported: number;
seekResult: string;
}
interface MediaState {
container: string;
demuxer: string;
duration: number;
videoCodec: string;
audioCodec: string;
pixelFormat: string;
colorPrimaries: string;
colorTransfer: string;
colorMatrix: string;
width: number;
height: number;
videoWidth: number;
videoHeight: number;
fps: number;
bitrate: number;
bitDepth: number;
audioSampleRate: number;
audioChannels: number;
audioChannelLayout: string;
trackCount: number;
subtitleCount: number;
}
interface TrackState {
id: number;
type: string;
title: string;
language: string;
codec: string;
default: boolean;
forced: boolean;
selected: boolean;
external: boolean;
}
const LOCAL_TEST_URL: string = 'file:///data/storage/el2/base/haps/entry/files/sample.mp4';
interface HeaderInput {
name: string;
value: string;
}
function parseHeaders(value: string): HeaderInput[] {
const result: HeaderInput[] = [];
value.split('\n').forEach((line: string) => {
const separator: number = line.indexOf(':');
if (separator > 0) {
const name: string = line.substring(0, separator).trim();
const headerValue: string = line.substring(separator + 1).trim();
if (name.length > 0) result.push({ name: name, value: headerValue });
}
});
return result;
}
function maskHeaders(value: string, reveal: boolean): string {
if (reveal) return value || '(none)';
const result: string[] = [];
value.split('\n').forEach((line: string) => {
const separator: number = line.indexOf(':');
if (separator > 0) result.push(line.substring(0, separator).trim() + ': [REDACTED]');
});
return result.length > 0 ? result.join(' | ') : '(none)';
}
function localSubtitleUrl(value: string): string {
return value.startsWith('file://') ? value : 'file://' + value;
}
function nextSeekMode(value: string): string {
if (value === 'relative') return 'absolute';
if (value === 'absolute') return 'percent';
return 'relative';
}
@Entry
@Component
struct Index {
@State url: string = LOCAL_TEST_URL;
@State status: string = 'IDLE';
@State audioEnabled: boolean = true;
@State detail: string = 'Surface waiting';
@State headersText: string = '';
@State subtitleUrl: string = '';
@State speedInput: string = '1';
@State volumeInput: string = '100';
@State subtitleDelayInput: string = '0';
@State revealHeaders: boolean = false;
@State seekMode: string = 'relative';
@State seekInput: string = '10';
@State state: PlaybackState = {
state: 'IDLE', position: 0, duration: 0, fps: 0, dropped: 0,
firstFrameMs: -1, renderCount: 0, lastError: '', surface: 'WAITING', egl: 'WAITING', mpv: 'WAITING',
audioAvailable: false, audioBackend: 'disabled', sampleRate: 0, sampleFormat: 0, channels: 0,
channelLayout: 0, submittedFrames: 0, playedFrames: 0, estimatedAudioDelayMs: 0,
audioBufferStarvation: 0, audioFlushCount: 0, seekAudioResetCount: 0, audioState: 0,
requestedDecoder: 'software', actualDecoder: 'software', hwdecRequested: false,
hwdecActive: false, hwdecBackend: '', hwdecFallbackReason: '', hwCapability: '',
hardwareFrameCount: 0, softwareFrameCount: 0, hwFallbackCount: 0,
nativeBufferImportCount: 0, nativeBufferImportFailure: 0, surfacePresentCount: 0,
gpuInteropFrameCount: 0,
sessionId: 0, errorCode: 'NONE', error: { code: 'NONE', rawMessage: '' },
metrics: { droppedFrames: 0, renderCount: 0, buffering: false, cacheDuration: 0, cacheBytes: 0, currentBitrate: 0,
bufferingEnterCount: 0, bufferingExitCount: 0, bufferingTotalMs: 0, bufferingMaxSingleMs: 0, networkWaitMs: -1,
lateFrames: -1, mistimedFrames: -1, decoderFrames: -1 },
timeline: { sessionSetupMs: -1, mpvCreateMs: -1, mpvInitializeMs: -1, loadCommandMs: -1, startFileMs: -1,
networkOpenBeginMs: -1, networkOpenEndMs: -1, firstByteMs: -1, demuxReadyMs: -1, fileLoadedMs: -1,
videoDecoderReadyMs: -1, audioDecoderReadyMs: -1, videoReconfigMs: -1, audioReconfigMs: -1,
renderContextReadyMs: -1, firstVideoFrameAvailableMs: -1, firstRenderBeginMs: -1, firstRenderEndMs: -1,
firstSwapMs: -1, startupTotalMs: -1, audioRendererStartMs: -1, firstAudioCallbackMs: -1, playbackRestartMs: -1,
seekRequestMs: -1, seekCommandMs: -1, seekDemuxMs: -1, seekVideoReconfigMs: -1,
seekPlaybackRestartMs: -1, seekFirstFrameMs: -1, seekFirstSwapMs: -1, seekCommandLatencyMs: -1,
seekRecoveryMs: -1, seekFirstVisibleFrameMs: -1 },
napiProfiling: { getStateCalls: 0, serializeUsTotal: 0, payloadBytesTotal: 0, lastSerializeUs: 0, lastPayloadBytes: 0,
renderUpdateCalls: 0, renderFrameUpdates: 0, coreSyncCalls: 0, coreSyncSkipped: 0 },
speed: 1, volume: 100, muted: false,
subtitleDelayMs: 0, subtitleScale: 1, subtitlePosition: 100,
network: { protocol: '', host: '', networkState: '', httpStatus: 0, redirectCount: 0,
buffering: false, cacheDuration: 0, cacheBytes: 0, demuxer: '', container: '', currentBitrate: 0,
rangeSupported: -1, seekResult: '' },
media: { container: '', demuxer: '', duration: 0, videoCodec: '', audioCodec: '', pixelFormat: '', colorPrimaries: '', colorTransfer: '', colorMatrix: '',
width: 0, height: 0, videoWidth: 0, videoHeight: 0, fps: 0, bitrate: 0, bitDepth: 0, audioSampleRate: 0, audioChannels: 0, audioChannelLayout: '',
trackCount: 0, subtitleCount: 0 },
tracks: []
};
private poller: number = 0;
aboutToAppear(): void {
this.poller = setInterval(() => {
this.state = playbackLab.getState() as PlaybackState;
this.status = this.state.state;
this.detail = this.state.lastError || `position ${this.state.position.toFixed(2)} / ${this.state.duration.toFixed(2)}s`;
}, 500);
}
aboutToDisappear(): void {
if (this.poller !== 0) {
clearInterval(this.poller);
this.poller = 0;
}
}
build() {
Column({ space: 10 }) {
Text('Velix Playback Lab').fontSize(24).fontWeight(FontWeight.Bold).width('100%')
Text(`state: ${this.status}`).fontSize(16).fontColor($r('app.color.primary')).width('100%')
Text(`surface=${this.state.surface} egl=${this.state.egl} mpv=${this.state.mpv}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`error=${this.state.error.code} rawMessage=${this.state.error.rawMessage || 'none'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
Text(`startup=${this.state.timeline.startupTotalMs}ms firstSwap=${this.state.timeline.firstSwapMs}ms seekRecovery=${this.state.timeline.seekRecoveryMs}ms audioStart=${this.state.timeline.audioRendererStartMs}ms audioCallback=${this.state.timeline.firstAudioCallbackMs}ms`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`napi calls=${this.state.napiProfiling.getStateCalls} serialize=${this.state.napiProfiling.serializeUsTotal}us payload=${this.state.napiProfiling.payloadBytesTotal}B sync=${this.state.napiProfiling.coreSyncCalls}/${this.state.napiProfiling.coreSyncSkipped} updates=${this.state.napiProfiling.renderUpdateCalls}/${this.state.napiProfiling.renderFrameUpdates}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`buffer enter=${this.state.metrics.bufferingEnterCount} exit=${this.state.metrics.bufferingExitCount} total=${this.state.metrics.bufferingTotalMs}ms max=${this.state.metrics.bufferingMaxSingleMs}ms networkWait=${this.state.metrics.networkWaitMs}ms`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
XComponent({ id: 'playback_surface', type: XComponentType.SURFACE, libraryname: 'entry' })
.width('100%').height(250).backgroundColor(Color.Black)
TextInput({ text: this.url, placeholder: 'file:///... or https://...' })
.width('100%').onChange((value: string) => this.url = value)
Row({ space: 8 }) {
Button('Load Local Test').onClick(() => {
this.url = LOCAL_TEST_URL;
playbackLab.load(LOCAL_TEST_URL);
})
Button('Load URL').onClick(() => playbackLab.load({
url: this.url, httpHeaders: parseHeaders(this.headersText)
}))
Button(this.audioEnabled ? 'Audio On' : 'Audio Off').onClick(() => {
const enabled: boolean = !this.audioEnabled;
if (playbackLab.setAudioEnabled(enabled) as boolean) this.audioEnabled = enabled;
})
}.width('100%')
Row({ space: 8 }) {
Button('Play').onClick(() => playbackLab.play())
Button('Pause').onClick(() => playbackLab.pause())
Button('Stop').onClick(() => playbackLab.stop())
Button('Release').onClick(() => playbackLab.release())
}.width('100%')
Row({ space: 8 }) {
Button(this.state.hwdecRequested ? 'OHCodec On' : 'OHCodec Probe').onClick(() => {
const enabled: boolean = !this.state.hwdecRequested;
playbackLab.setHardwareDecode(enabled);
})
}.width('100%')
Row({ space: 8 }) {
Button('-10s').onClick(() => playbackLab.seek(-10))
Button('+10s').onClick(() => playbackLab.seek(10))
Button('Toggle').onClick(() => playbackLab.togglePause())
}.width('100%')
Row({ space: 8 }) {
Button('Mode: ' + this.seekMode).onClick(() => this.seekMode = nextSeekMode(this.seekMode))
TextInput({ text: this.seekInput, placeholder: 'seek value' }).width('25%')
.onChange((value: string) => this.seekInput = value)
Button('Seek').onClick(() => {
const value: number = Number(this.seekInput);
if (this.seekMode === 'absolute') playbackLab.seekAbsolute(value);
else if (this.seekMode === 'percent') playbackLab.seekPercent(value);
else playbackLab.seek(value);
})
}.width('100%')
TextInput({ text: this.headersText, placeholder: 'Headers (one Name: Value per line; values masked in diagnostics)' })
.width('100%').onChange((value: string) => this.headersText = value)
Row({ space: 8 }) {
Text('Header diagnostics: ' + maskHeaders(this.headersText, this.revealHeaders))
.fontSize(12).fontColor($r('app.color.muted')).layoutWeight(1).maxLines(2)
Button(this.revealHeaders ? 'Mask' : 'Reveal (debug)').onClick(() => this.revealHeaders = !this.revealHeaders)
}.width('100%')
Row({ space: 8 }) {
TextInput({ text: this.speedInput, placeholder: 'speed' }).width('25%')
.onChange((value: string) => this.speedInput = value)
Button('Set Speed').onClick(() => playbackLab.setSpeed(Number(this.speedInput)))
TextInput({ text: this.volumeInput, placeholder: 'volume' }).width('25%')
.onChange((value: string) => this.volumeInput = value)
Button('Set Volume').onClick(() => playbackLab.setVolume(Number(this.volumeInput)))
Button(this.state.muted ? 'Unmute' : 'Mute').onClick(() => playbackLab.setMute(!this.state.muted))
}.width('100%')
TextInput({ text: this.subtitleUrl, placeholder: 'external subtitle URL (SRT/ASS/WebVTT)' })
.width('100%').onChange((value: string) => this.subtitleUrl = value)
Row({ space: 8 }) {
Button('Add Subtitle').onClick(() => playbackLab.addSubtitle({
url: this.subtitleUrl, select: true
}))
Button('Add Local').onClick(() => playbackLab.addSubtitle({
url: localSubtitleUrl(this.subtitleUrl), select: true
}))
Button('Subtitle Off').onClick(() => playbackLab.disableSubtitle())
TextInput({ text: this.subtitleDelayInput, placeholder: 'delay s' }).width('22%')
.onChange((value: string) => this.subtitleDelayInput = value)
Button('Set Delay').onClick(() => playbackLab.setSubtitleDelay(Number(this.subtitleDelayInput)))
}.width('100%')
Text(`position=${this.state.position.toFixed(2)} duration=${this.state.duration.toFixed(2)} fps=${this.state.fps.toFixed(2)}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`dropped=${this.state.dropped} firstFrameMs=${this.state.firstFrameMs} renderCount=${this.state.renderCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`audio=${this.state.audioBackend} state=${audioStateLabel(this.state.audioState)} rate=${this.state.sampleRate} format=${audioFormatLabel(this.state.sampleFormat)} channels=${this.state.channels}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`submitted=${this.state.submittedFrames} played=${this.state.playedFrames} delay=${this.state.estimatedAudioDelayMs}ms starvation=${this.state.audioBufferStarvation}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`flushes=${this.state.audioFlushCount} seekResets=${this.state.seekAudioResetCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`decoder=${this.state.requestedDecoder} -> ${this.state.actualDecoder} requested=${this.state.hwdecRequested} active=${this.state.hwdecActive} backend=${this.state.hwdecBackend || 'none'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`hwFrames=${this.state.hardwareFrameCount} swFrames=${this.state.softwareFrameCount} fallbacks=${this.state.hwFallbackCount} surfacePresent=${this.state.surfacePresentCount} interop=${this.state.gpuInteropFrameCount}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`fallback=${this.state.hwdecFallbackReason || 'none'} capability=${this.state.hwCapability || 'DEVICE TEST REQUIRED'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`metrics dropped=${this.state.metrics.droppedFrames} renders=${this.state.metrics.renderCount} cache=${this.state.metrics.cacheDuration.toFixed(2)}s/${this.state.metrics.cacheBytes}B bitrate=${this.state.metrics.currentBitrate}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`buffer enter=${this.state.metrics.bufferingEnterCount} exit=${this.state.metrics.bufferingExitCount} total=${this.state.metrics.bufferingTotalMs}ms max=${this.state.metrics.bufferingMaxSingleMs}ms napi=${this.state.napiProfiling.getStateCalls}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`render updates=${this.state.napiProfiling.renderUpdateCalls} frames=${this.state.napiProfiling.renderFrameUpdates} sync=${this.state.napiProfiling.coreSyncCalls}/${this.state.napiProfiling.coreSyncSkipped}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('network=' + this.state.network.protocol + '://' + this.state.network.host + ' status=' + (this.state.network.httpStatus || 'unavailable') + ' redirects=' + this.state.network.redirectCount + ' buffering=' + this.state.network.buffering)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('range=' + this.state.network.rangeSupported + ' seek=' + (this.state.network.seekResult || 'unavailable'))
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('media=' + this.state.media.container + '/' + this.state.media.videoCodec + '/' + this.state.media.audioCodec + ' ' + this.state.media.width + 'x' + this.state.media.height + ' bitrate=' + this.state.media.bitrate + ' tracks=' + this.state.media.trackCount + ' subs=' + this.state.media.subtitleCount)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('subtitle delay=' + this.state.subtitleDelayMs + 'ms scale=' + this.state.subtitleScale + ' position=' + this.state.subtitlePosition)
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
ForEach(this.state.tracks, (track: TrackState) => {
Row({ space: 6 }) {
Text('track ' + track.id + ' ' + track.type + ' ' + (track.language || 'und') + ' ' + (track.codec || 'unknown') + (track.selected ? ' [selected]' : ''))
.fontSize(12).fontColor($r('app.color.muted')).layoutWeight(1)
if (track.type === 'audio') {
Button('Use').onClick(() => playbackLab.selectAudioTrack(track.id))
} else if (track.type === 'sub') {
Button('Use').onClick(() => playbackLab.selectSubtitleTrack(track.id))
}
}.width('100%')
}, (track: TrackState) => track.type + '-' + track.id)
}
.padding(16).width('100%').height('100%').backgroundColor($r('app.color.background'))
}
}
function detailLabel(value: string): string {
return `detail: ${value}`;
}
function audioFormatLabel(value: number): string {
if (value === 5) return 'float32';
if (value === 2) return 's16';
return 'n/a';
}
function audioStateLabel(value: number): string {
switch (value) {
case 1: return 'READY';
case 2: return 'RUNNING';
case 3: return 'PAUSED';
case 4: return 'STOPPED';
case 5: return 'ERROR';
case 6: return 'RELEASED';
default: return 'DISABLED';
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:ability_desc",
"icon": "$media:icon",
"label": "$string:app_name",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
@@ -0,0 +1,9 @@
{
"color": [
{ "name": "background", "value": "#101318" },
{ "name": "start_window_background", "value": "#101318" },
{ "name": "panel", "value": "#1A202A" },
{ "name": "primary", "value": "#6EA8FE" },
{ "name": "muted", "value": "#A9B4C2" }
]
}
@@ -0,0 +1,8 @@
{
"string": [
{ "name": "app_name", "value": "Velix Playback Lab" },
{ "name": "module_desc", "value": "Native libmpv playback verification" },
{ "name": "ability_desc", "value": "Playback Lab main ability" },
{ "name": "status_idle", "value": "IDLE" }
]
}
@@ -0,0 +1,4 @@
<svg width="96" height="96" xmlns="http://www.w3.org/2000/svg">
<rect width="96" height="96" rx="20" fill="#101318"/>
<path d="M38 27v42l32-21z" fill="#6EA8FE"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

@@ -0,0 +1,3 @@
{
"src": ["pages/Index"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"modelVersion": "6.1.1",
"dependencies": {}
}
+6
View File
@@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks,
plugins: []
};
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more