Compare commits
59
Commits
master
...
harmony-port
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49631d857 | ||
|
|
ecaad47c23 | ||
|
|
1b24aebe77 | ||
|
|
67ed15e5d4 | ||
|
|
6064d0e05a | ||
|
|
246653e2e1 | ||
|
|
5d411509a5 | ||
|
|
7afc6919ce | ||
|
|
cfa8fa3cef | ||
|
|
c8b8e76705 | ||
|
|
f67cf6ce00 | ||
|
|
6e06941fec | ||
|
|
afad6a40fe | ||
|
|
f51544026b | ||
|
|
471c9dbb6c | ||
|
|
a06e098787 | ||
|
|
c3a4f9469a | ||
|
|
8cae735e3c | ||
|
|
82325ddfa9 | ||
|
|
9c8e679c94 | ||
|
|
fe1935c3b3 | ||
|
|
8f58fad5cd | ||
|
|
3bcdcfd2a8 | ||
|
|
e2f4fbd831 | ||
|
|
29cd001796 | ||
|
|
e34a718c48 | ||
|
|
f1f0484eb4 | ||
|
|
32422c4e06 | ||
|
|
7f3d2e0747 | ||
|
|
14149854b8 | ||
|
|
e594f66c35 | ||
|
|
c6f267d6dd | ||
|
|
49b078737b | ||
|
|
f9d1481fb8 | ||
|
|
0db086a1e9 | ||
|
|
d7cf273432 | ||
|
|
8a30e06621 | ||
|
|
e4bc586d77 | ||
|
|
5a50baf26a | ||
|
|
d84fd47f9b | ||
|
|
09de481b03 | ||
|
|
329728ce56 | ||
|
|
46582ed8bd | ||
|
|
e5f6c0ced2 | ||
|
|
e1bc8cc52f | ||
|
|
c239b1c186 | ||
|
|
1f1a6e5962 | ||
|
|
37df87fc56 | ||
|
|
3f6132637b | ||
|
|
9fdf08073e | ||
|
|
dfda10b6d8 | ||
|
|
9941ad1da9 | ||
|
|
7021f883cb | ||
|
|
c650a4214d | ||
|
|
d4c84601fa | ||
|
|
2356e2f67b | ||
|
|
75ed9dd2f4 | ||
|
|
f178d38ce6 | ||
|
|
2c469d4299 |
No files matched your search
@@ -1,3 +1,7 @@
|
||||
/subprojects/*
|
||||
!/subprojects/packagefiles
|
||||
!/subprojects/*.wrap
|
||||
/build/
|
||||
/dist/
|
||||
.hvigor/
|
||||
.DS_Store
|
||||
@@ -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;本轮没有把公网失败当作性能数据。
|
||||
@@ -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、鉴权和弱网重缓冲应在实体设备用固定服务端脚本复测。
|
||||
@@ -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
|
||||
@@ -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 语义留到后续阶段验证。
|
||||
@@ -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。
|
||||
@@ -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
|
||||
```
|
||||
@@ -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。
|
||||
@@ -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 自身许可使用,没有复制第三方实现源码。
|
||||
@@ -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 偏差、路由与多声道仍需实体设备,不宣称低延迟或多声道完成。
|
||||
@@ -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 依赖验证。
|
||||
@@ -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 顺序不同,应记录实际字体、脚本和缺字结果。
|
||||
@@ -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`。
|
||||
@@ -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。
|
||||
@@ -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`。
|
||||
@@ -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 或设备硬解验证。
|
||||
@@ -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。
|
||||
@@ -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 错误不伪装成网络错误。
|
||||
@@ -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 选项说明。
|
||||
@@ -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`。
|
||||
@@ -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。
|
||||
@@ -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 源码。
|
||||
@@ -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;诊断仅累加轻量计数和受控序列化时间。
|
||||
@@ -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,不由模型推断。
|
||||
@@ -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 输入。
|
||||
@@ -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`)的前置条件是实体机首帧、控制和压力矩阵完成。
|
||||
@@ -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。
|
||||
@@ -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 行为。
|
||||
@@ -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 事件/属性可改变对应诊断字段。
|
||||
@@ -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;本阶段未引入新的第三方依赖或商业资源。
|
||||
@@ -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 不在本阶段启用。
|
||||
@@ -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` 或脱敏错误。
|
||||
@@ -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 指标仅在“显示调试统计”打开时显示。
|
||||
@@ -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 不是首版必需。
|
||||
@@ -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原型在目标机显著改善 |
|
||||
| 商业发布 | 实际链接清单、许可证正文、源码义务、法务审核全部完成 |
|
||||
@@ -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`。
|
||||
@@ -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。
|
||||
@@ -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。
|
||||
@@ -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 路径入口。
|
||||
@@ -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。
|
||||
@@ -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。
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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 使用。
|
||||
@@ -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。
|
||||
@@ -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`。
|
||||
@@ -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
@@ -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`。
|
||||
@@ -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
|
||||
|
||||
@@ -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, ×tamp) == 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
results/*.json
|
||||
reports/*.md
|
||||
runner/__pycache__/
|
||||
!results/.gitkeep
|
||||
!reports/.gitkeep
|
||||
Whitespace-only changes.
Whitespace-only changes.
Executable
+385
@@ -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())
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "seek_local",
|
||||
"media": "sample.mp4",
|
||||
"positionsPercent": [10, 25, 50, 75, 90],
|
||||
"repeatPerPosition": 10,
|
||||
"modes": ["fast", "exact"]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 首页。
|
||||
@@ -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`。
|
||||
@@ -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` 仍可查找。
|
||||
@@ -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
@@ -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'],
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
entry/libs/arm64-v8a/*.so*
|
||||
entry/build/
|
||||
build/
|
||||
oh_modules/
|
||||
.hvigor/
|
||||
local.properties
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.velix.playbacklab",
|
||||
"vendor": "Velix",
|
||||
"versionCode": 1,
|
||||
"versionName": "0.1.0",
|
||||
"icon": "$media:icon",
|
||||
"label": "$string:app_name"
|
||||
}
|
||||
}
|
||||
@@ -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,诊断和日志脱敏;默认构建仍使用软件解码。
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files": {
|
||||
"ignore": ["**/entry/libs/**"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
build/
|
||||
.cxx/
|
||||
libs/arm64-v8a/*.so*
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hapTasks,
|
||||
plugins: []
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
# Playback Lab keeps native bridge names stable for NAPI lookup.
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"modelVersion": "6.1.1",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user