diff --git a/DOCS/NETWORK_PLAYBACK.md b/DOCS/NETWORK_PLAYBACK.md index 224307c38a..fe2d1f2b4c 100644 --- a/DOCS/NETWORK_PLAYBACK.md +++ b/DOCS/NETWORK_PLAYBACK.md @@ -6,4 +6,6 @@ 网络诊断只报告实际可取得的 mpv 属性:协议、host、networkState、cache duration/bytes、demuxer、container、当前 bitrate,以及从实际日志识别到的 HTTP status/redirect count。没有对应属性或日志时返回 0/空值,不做推断。 +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 错误不伪装成网络错误。 diff --git a/DOCS/NETWORK_SECURITY.md b/DOCS/NETWORK_SECURITY.md index a564803c36..1f688d2267 100644 --- a/DOCS/NETWORK_SECURITY.md +++ b/DOCS/NETWORK_SECURITY.md @@ -6,7 +6,7 @@ ## 脱敏规则 -Authorization、Proxy-Authorization、Cookie、X-Api-Key 以及 URL 中的 token、auth、key、signature、sig、expires 参数在错误文本和日志中替换为 [REDACTED]。原始 mpv 日志不得直接写入 OH 日志。 +Authorization、Proxy-Authorization、Cookie、X-Api-Key 以及 URL 中的 token、access_token、auth、key、api_key、apikey、signature、x-amz-signature、sig、expires 参数在错误文本和日志中替换为 [REDACTED]。原始 mpv 日志不得直接写入 OH 日志。 ## TLS、重定向与凭证 diff --git a/DOCS/PHASE5_STATUS.md b/DOCS/PHASE5_STATUS.md index 9df5bb2237..f13d2342f2 100644 --- a/DOCS/PHASE5_STATUS.md +++ b/DOCS/PHASE5_STATUS.md @@ -9,7 +9,9 @@ - 请求字段、会话 generation、线程安全控制、稳定状态/错误、轨道、媒体信息和网络缓存诊断已实现。 - Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。 - 文档、测试矩阵和构建矩阵已补齐。 -- default/software/FFmpeg-Harmony 三套 AArch64 libmpv 构建、稳定 API 符号校验与 Playback Lab Native/ArkTS 编译已通过;PackageHap 因环境缺少 Java Runtime 保持待补。 +- 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;HTTPS/HLS/网络字幕/鉴权资源仍需真实网络源。 +- 当前构建日志显示 iconv feature disabled,因此字幕适配器只承诺 UTF-8;GBK/Big5/Shift-JIS 会返回稳定 SUBTITLE 错误。 尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。 diff --git a/DOCS/PHASE5_TEST_MATRIX.md b/DOCS/PHASE5_TEST_MATRIX.md index 78786e6d8e..83d60fd1b3 100644 --- a/DOCS/PHASE5_TEST_MATRIX.md +++ b/DOCS/PHASE5_TEST_MATRIX.md @@ -2,7 +2,7 @@ | 场景 | 方式 | 结果 | |---|---|---| -| 本地 MP4 H264/AAC | Playback Lab | Native/ArkTS 编译通过;设备运行待确认 | +| 本地 MP4 H264/AAC | Playback Lab | 模拟器安装启动并完成首帧/播放/结束 smoke;网络与实体机待确认 | | 本地 MKV H264/AAC/SRT | Playback Lab | API/UI 编译覆盖;设备运行待确认 | | 本地 MKV HEVC/AAC/ASS(software) | software mpv | AArch64 构建通过;设备运行待确认 | | HTTP MP4 | 受控测试源 | 网络环境待确认 | @@ -26,6 +26,6 @@ | 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 | CompileArkTS/Native 通过;PackageHap 缺 Java Runtime | +| Playback Lab HAP | hvigor assembleHap --no-daemon | CompileArkTS/Native/PackageHap 通过;无正式签名,devecocli 已安装模拟器 | 产品禁止以 UI 定时器伪造 buffering;只有 mpv 事件/属性可改变对应诊断字段。 diff --git a/DOCS/PLAYBACK_PUBLIC_API.md b/DOCS/PLAYBACK_PUBLIC_API.md index fb1ed42a55..e48109c3ae 100644 --- a/DOCS/PLAYBACK_PUBLIC_API.md +++ b/DOCS/PLAYBACK_PUBLIC_API.md @@ -5,3 +5,7 @@ mp_harmony_playback.h 是上层唯一稳定契约,提供 PlaybackRequest、Pla 公开控制命令: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 错误拒绝。 diff --git a/DOCS/SUBTITLE_PIPELINE.md b/DOCS/SUBTITLE_PIPELINE.md index afde279a78..44b656d12d 100644 --- a/DOCS/SUBTITLE_PIPELINE.md +++ b/DOCS/SUBTITLE_PIPELINE.md @@ -1,7 +1,7 @@ # 字幕管线 -内部字幕和外部字幕统一由 mpv/libass 管线处理。外部源通过 sub-add 加入当前 session,可选中或移除,不销毁播放 session;sub-remove 仅移除指定轨道。 +内部字幕和外部字幕统一由 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(具体控件按实际可用属性开放)。 -编码优先 UTF-8;GBK、Big5、Shift-JIS 只在现有 FFmpeg/mpv/iconv 能识别时启用。当前适配器拒绝未验证编码并返回稳定 SUBTITLE 错误,不静默按错误编码显示。Playback Lab 同时提供网络 URL 与本地 file 路径入口。 +当前 OHOS 构建未启用 iconv,因此只接受 UTF-8/UTF8;GBK、Big5、Shift-JIS 等输入明确返回稳定 SUBTITLE 错误,不静默走 Latin-1 fallback。启用并验证 iconv 后再扩展编码白名单。Playback Lab 同时提供网络 URL 与本地 file 路径入口。 diff --git a/memory/2026-08-29.md b/memory/2026-08-29.md index 9f5b4ce044..bb99ff7ca7 100644 --- a/memory/2026-08-29.md +++ b/memory/2026-08-29.md @@ -15,3 +15,15 @@ 文件变更: 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 diff --git a/memory/MEMORY.md b/memory/MEMORY.md new file mode 100644 index 0000000000..07e092a42a --- /dev/null +++ b/memory/MEMORY.md @@ -0,0 +1,11 @@ +# Velix-MPV-Core 记忆索引 + +- 项目:地产运维中台媒体核心的 OpenHarmony MPV 端口。 +- 当前阶段:Phase 5 CODE COMPLETE;真实 HTTP/HTTPS/HLS、网络字幕、鉴权资源和实体机验收待补。 +- 公共入口:`include/mpv/harmony_playback.h`,稳定 C API 不暴露 MPV/FFmpeg 内部类型。 +- Playback Lab:C++ 仅做 NAPI/渲染适配,核心请求、控制、状态统一委托 C API。 +- 默认策略:软件解码;`harmony-ohcodec-gl` mapper 保持禁用,不声明零拷贝 GPU 能力。 +- 构建:default/software/FFmpeg-Harmony AArch64 通过;HAP 使用 DevEco JBR + `--no-daemon` 通过。 +- 设备:模拟器 `127.0.0.1:5555`,Playback Lab 已安装启动并完成本地 MP4 smoke。 +- 部署约定:服务器只按 `deploy.sh`、`deploy.sh status/logs/clean/clean-all` 执行。 +- 详细状态:`memory/projects.md`;基础设施:`memory/infra.md`;教训:`memory/lessons.md`。 diff --git a/memory/infra.md b/memory/infra.md index 50e9f1ddb2..d229c4973c 100644 --- a/memory/infra.md +++ b/memory/infra.md @@ -15,11 +15,12 @@ - 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 构建:当前环境无可用 Java Runtime,hvigor 可完成 Native/ArkTS 编译但 PackageHap 阶段失败;安装 JDK 后重试。 +- HAP 构建:使用 DevEco 自带 `/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home`,并以 `hvigorw.js assembleHap --no-daemon --mode module -p product=default` 完成 PackageHap;默认未配置正式签名。 - 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`;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`。 diff --git a/memory/projects.md b/memory/projects.md index a3d1181cba..2b2f28b811 100644 --- a/memory/projects.md +++ b/memory/projects.md @@ -4,11 +4,11 @@ - 状态:Phase 5 CODE COMPLETE / emulator-network validation pending;GPU INTEROP MAPPER 未进入;默认软件解码与网络播放公共 API 已完成,实体机/真实网络验收未完成。 - 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。 -- 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,340,576 bytes;切换构建位于 `build/ohos-arm64/prefix-mpv-ffmpeg-harmony/lib/libmpv.so.2.5.0`;DT_NEEDED 含 `libohaudio.so`。 +- 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,341,696 bytes;切换构建位于 `build/ohos-arm64/prefix-mpv-ffmpeg-harmony/lib/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;历史 HAP Native/ArkTS 通过,当前 Phase 5 PackageHap 仍受 Java Runtime 环境阻断。 +- 已完成:`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 `990614364f`;`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 与文档;默认/软件/FFmpeg-Harmony 构建通过,HAP 打包缺 Java,HTTP/HLS/字幕运行验证待补。 +- Phase 5 已完成:稳定 C API、请求/headers 会话隔离、错误/状态、轨道/媒体/网络/字幕控制、Playback Lab 诊断 UI 与文档;C++ 外壳已统一委托 C API,默认/软件/FFmpeg-Harmony 构建及 HAP 打包通过;模拟器已完成本地 MP4 首帧/播放结束 smoke,HTTP/HLS/网络字幕/鉴权运行验证待补。 diff --git a/playback_lab/entry/src/main/cpp/bridge.cpp b/playback_lab/entry/src/main/cpp/bridge.cpp index c5397427e4..cc9aeddd8e 100644 --- a/playback_lab/entry/src/main/cpp/bridge.cpp +++ b/playback_lab/entry/src/main/cpp/bridge.cpp @@ -97,7 +97,7 @@ bool getNamedBool(napi_env env, napi_value object, const char* name, bool& value napi_get_value_bool(env, property, &value) == napi_ok; } -bool getRequest(napi_env env, napi_callback_info info, PlaybackRequest& request) +bool getRequest(napi_env env, napi_callback_info info, PlaybackRequestModel& request) { size_t argc = 1; napi_value argv[1] = {nullptr}; @@ -217,7 +217,7 @@ napi_value undefined(napi_env env) napi_value load(napi_env env, napi_callback_info info) { - PlaybackRequest request; + PlaybackRequestModel request; if (getRequest(env, info, request)) gPlayer.load(request); return undefined(env); } @@ -469,6 +469,13 @@ napi_value getState(napi_env env, napi_callback_info) 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(state.sessionId), &value); napi_set_named_property(env, object, "sessionId", value); napi_create_double(env, state.speed, &value); @@ -508,6 +515,21 @@ napi_value getState(napi_env env, napi_callback_info) napi_create_int64(env, state.network.currentBitrate, &value); napi_set_named_property(env, network, "currentBitrate", 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(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_set_named_property(env, object, "metrics", metrics); napi_value media = nullptr; napi_create_object(env, &media); napi_create_string_utf8(env, state.media.container.c_str(), NAPI_AUTO_LENGTH, &value); diff --git a/playback_lab/entry/src/main/cpp/harmony_playback_internal.h b/playback_lab/entry/src/main/cpp/harmony_playback_internal.h new file mode 100644 index 0000000000..c949b28a3a --- /dev/null +++ b/playback_lab/entry/src/main/cpp/harmony_playback_internal.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#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); + +#ifdef __cplusplus +} +#endif diff --git a/playback_lab/entry/src/main/cpp/player.cpp b/playback_lab/entry/src/main/cpp/player.cpp index 7be7f845f3..442419c1af 100644 --- a/playback_lab/entry/src/main/cpp/player.cpp +++ b/playback_lab/entry/src/main/cpp/player.cpp @@ -4,12 +4,10 @@ #include -#include #include -#include #include -#include -#include +#include +#include #undef LOG_DOMAIN #undef LOG_TAG @@ -17,44 +15,26 @@ #define LOG_TAG "VelixPlayback" namespace { -constexpr uint64_t kTimePos = 1; -constexpr uint64_t kDuration = 2; -constexpr uint64_t kPause = 3; -constexpr uint64_t kFps = 4; -constexpr uint64_t kDropped = 5; -constexpr uint64_t kTracks = 9; -constexpr uint64_t kCacheDuration = 10; -constexpr uint64_t kCacheState = 11; -constexpr uint64_t kBuffering = 12; -constexpr uint64_t kVolume = 13; -constexpr uint64_t kMute = 14; -constexpr uint64_t kSpeed = 15; - -void logMpvError(const char* operation, int code) -{ - OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s failed: %{public}s (%{public}d)", - operation, mpv_error_string(code), code); -} - -bool containsInsensitive(const std::string& value, const char* needle) -{ - std::string lower = value; - std::string target = needle ? needle : ""; - std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - std::transform(target.begin(), target.end(), target.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); - return !target.empty() && lower.find(target) != std::string::npos; -} 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 c) { return static_cast(std::tolower(c)); }); - return value == "utf-8" || value == "utf8" || value == "gbk" || - value == "gb18030" || value == "big5" || value == "shift-jis" || - value == "sjis"; + [](unsigned char character) { return static_cast(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() @@ -69,74 +49,50 @@ PlaybackEngine::~PlaybackEngine() bool PlaybackEngine::initialize() { - if (initialized_.load()) { - return true; - } - handle_ = mpv_create(); - if (handle_ == nullptr) { - setError("mpv_create failed"); + std::lock_guard 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; } - const char* options[][2] = { - {"vo", "libmpv"}, - {"terminal", "no"}, - {"config", "no"}, - {"hwdec", "no"}, - {"aid", "auto"}, - {"video-timing-offset", "0"}, - {nullptr, nullptr}, - }; - for (size_t i = 0; options[i][0] != nullptr; ++i) { - int code = mpv_set_option_string(handle_, options[i][0], options[i][1]); - if (code < 0) { - logMpvError(options[i][0], code); - setError(std::string("mpv option failed: ") + options[i][0]); - mpv_terminate_destroy(handle_); - handle_ = nullptr; - return false; - } - } - int code = mpv_initialize(handle_); - if (code < 0) { - logMpvError("mpv_initialize", code); - setError("mpv_initialize failed"); - mpv_terminate_destroy(handle_); - handle_ = nullptr; + if (mp_harmony_playback_initialize(player) < 0) { + setError("playback core initialize failed"); + mp_harmony_playback_destroy(player); return false; } - mpv_request_log_messages(handle_, "debug"); - observeProperties(); + 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); - stopEvents_.store(false); startedAt_ = std::chrono::steady_clock::now(); setState("IDLE"); - eventThread_ = std::thread(&PlaybackEngine::eventLoop, this); - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "libmpv initialized"); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "playback core initialized"); return true; } bool PlaybackEngine::setAudioEnabled(bool enabled) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) - return false; - int code = mpv_set_property_string(handle_, "aid", enabled ? "auto" : "no"); - if (code < 0) { - logMpvError("audio", code); - setError("audio toggle failed"); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_audio_enabled(corePlayer_, enabled ? 1 : 0); } - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setHardwareDecode(bool enabled) { std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) - return false; - int code = mpv_set_property_string(handle_, "hwdec", enabled ? "ohcodec" : "no"); - if (code < 0) { - logMpvError("hwdec", code); + if (handle_ == nullptr) return false; + if (mpv_set_property_string(handle_, "hwdec", enabled ? "ohcodec" : "no") < 0) { setError("hardware decode toggle failed"); return false; } @@ -154,600 +110,477 @@ bool PlaybackEngine::setHardwareDecode(bool enabled) return true; } -void PlaybackEngine::observeProperties() -{ - mpv_observe_property(handle_, kTimePos, "time-pos", MPV_FORMAT_DOUBLE); - mpv_observe_property(handle_, kDuration, "duration", MPV_FORMAT_DOUBLE); - mpv_observe_property(handle_, kPause, "pause", MPV_FORMAT_FLAG); - mpv_observe_property(handle_, kFps, "estimated-vf-fps", MPV_FORMAT_DOUBLE); - mpv_observe_property(handle_, kDropped, "vo-drop-frame-count", MPV_FORMAT_INT64); - mpv_observe_property(handle_, kHwdecCurrent, "hwdec-current", MPV_FORMAT_STRING); - mpv_observe_property(handle_, kHwdecInterop, "hwdec-interop", MPV_FORMAT_STRING); - mpv_observe_property(handle_, kDecoder, "decoder", MPV_FORMAT_STRING); - mpv_observe_property(handle_, kTracks, "track-list", MPV_FORMAT_NODE); - mpv_observe_property(handle_, kCacheDuration, "demuxer-cache-duration", MPV_FORMAT_DOUBLE); - mpv_observe_property(handle_, kCacheState, "demuxer-cache-state", MPV_FORMAT_NODE); - mpv_observe_property(handle_, kBuffering, "cache-buffering-state", MPV_FORMAT_INT64); - mpv_observe_property(handle_, kVolume, "volume", MPV_FORMAT_DOUBLE); - mpv_observe_property(handle_, kMute, "mute", MPV_FORMAT_FLAG); - mpv_observe_property(handle_, kSpeed, "speed", MPV_FORMAT_DOUBLE); -} - void PlaybackEngine::load(const std::string& url) { - PlaybackRequest request; + PlaybackRequestModel request; request.url = url; load(request); } -bool PlaybackEngine::load(const PlaybackRequest& request) +bool PlaybackEngine::load(const PlaybackRequestModel& request) { - if (!initialize() || request.url.empty()) { - setError(request.url.empty() ? "empty URL" : "player unavailable"); + if (request.url.empty()) { + setError("empty URL"); return false; } - std::lock_guard apiLock(apiMutex_); - { - std::lock_guard stateLock(mutex_); - snapshot_.position = 0.0; - snapshot_.duration = 0.0; - snapshot_.firstFrameMs = -1; - snapshot_.renderCount = 0; - snapshot_.lastError.clear(); - snapshot_.errorCode = "NONE"; - snapshot_.media = PlaybackMediaInfo{}; - snapshot_.network = PlaybackNetworkInfo{}; - snapshot_.tracks.clear(); - snapshot_.subtitleDelayMs = 0; - snapshot_.subtitleScale = 1.0; - snapshot_.subtitlePosition = 100.0; - snapshot_.sessionId = ++sessionId_; - awaitingStart_ = true; - startedAt_ = std::chrono::steady_clock::now(); - } - if (!setHeadersLocked(request)) { - setError("http-header-fields failed"); - return false; - } - if (mpv_set_property_string(handle_, "user-agent", request.userAgent.c_str()) < 0) - setError("user-agent failed"); - if (mpv_set_property_string(handle_, "referrer", request.referer.c_str()) < 0) - setError("referrer failed"); - mpv_set_property_string(handle_, "alang", request.preferredAudioLanguage.c_str()); - mpv_set_property_string(handle_, "slang", request.preferredSubtitleLanguage.c_str()); - std::string timeout = request.networkTimeout > 0 ? std::to_string(request.networkTimeout) : "0"; - mpv_set_property_string(handle_, "network-timeout", timeout.c_str()); - if (request.playbackSpeed > 0) { - std::string speed = std::to_string(request.playbackSpeed); - mpv_set_property_string(handle_, "speed", speed.c_str()); - std::lock_guard stateLock(mutex_); - snapshot_.speed = request.playbackSpeed; - } - std::string audioId = request.audioTrackId >= 0 ? std::to_string(request.audioTrackId) : "auto"; - mpv_set_property_string(handle_, "aid", audioId.c_str()); - if (request.subtitleTrackId >= 0) { - std::string id = std::to_string(request.subtitleTrackId); - mpv_set_property_string(handle_, "sid", id.c_str()); - } else if (request.subtitleEnabled == 0) { - mpv_set_property_string(handle_, "sid", "no"); - } else { - mpv_set_property_string(handle_, "sid", "auto"); - } - const char* command[] = {"loadfile", request.url.c_str(), "replace", nullptr}; - int code = mpv_command(handle_, command); - if (code < 0) { - logMpvError("loadfile", code); - setError(std::string("load failed: ") + mpv_error_string(code)); + 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 (subtitle.url.empty()) continue; if (!supportedSubtitleEncoding(subtitle.encoding)) { setError("unsupported subtitle encoding"); + std::lock_guard lock(mutex_); + snapshot_.errorCode = "SUBTITLE"; return false; } - if (!subtitle.encoding.empty()) - mpv_set_property_string(handle_, "sub-codepage", subtitle.encoding.c_str()); - const char* flags = (subtitle.select || subtitle.isDefault) ? "select" : "auto"; - const char* subCommand[] = {"sub-add", subtitle.url.c_str(), flags, - subtitle.title.c_str(), subtitle.language.c_str(), nullptr}; - mpv_command(handle_, subCommand); } - if (request.startPositionMs >= 0) { - std::string seconds = std::to_string(static_cast(request.startPositionMs) / 1000.0); - const char* seekCommand[] = {"seek", seconds.c_str(), "absolute", "exact", nullptr}; - mpv_command(handle_, seekCommand); + if (!initialize()) return false; + + std::vector 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 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; + { - std::lock_guard stateLock(mutex_); - snapshot_.state = "OPENING"; + std::lock_guard lock(mutex_); + snapshot_.renderCount = 0; + snapshot_.firstFrameMs = -1; + startedAt_ = std::chrono::steady_clock::now(); } - return true; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_load(corePlayer_, &coreRequest); + } + syncCoreSnapshot(); + return rc >= 0; } void PlaybackEngine::play() { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) { - setError("player unavailable"); - return; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_play(corePlayer_); } - int code = mpv_set_property_string(handle_, "pause", "no"); - if (code < 0) { - logMpvError("play", code); - setError("play failed"); - return; - } - setState("PLAYING"); + if (rc < 0) setError("play failed"); + syncCoreSnapshot(); } void PlaybackEngine::pause() { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) { - return; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_pause(corePlayer_); } - int code = mpv_set_property_string(handle_, "pause", "yes"); - if (code < 0) { - logMpvError("pause", code); - setError("pause failed"); - return; - } - setState("PAUSED"); + if (rc < 0) setError("pause failed"); + syncCoreSnapshot(); } void PlaybackEngine::togglePause() { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) return; - int paused = 0; - if (mpv_get_property(handle_, "pause", MPV_FORMAT_FLAG, &paused) < 0) { - setError("toggle pause failed"); - return; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_toggle_pause(corePlayer_); } - if (mpv_set_property_string(handle_, "pause", paused ? "no" : "yes") < 0) { - setError("toggle pause failed"); - return; - } - setState(paused ? "PLAYING" : "PAUSED"); + if (rc < 0) setError("toggle pause failed"); + syncCoreSnapshot(); } bool PlaybackEngine::seekAbsolute(double seconds, bool exact, bool keyframe) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(seconds) || seconds < 0) return false; - std::string amount = std::to_string(seconds); - const char* mode = exact ? "exact" : (keyframe ? "keyframes" : "default"); - const char* command[] = {"seek", amount.c_str(), "absolute", mode, nullptr}; - int code = mpv_command(handle_, command); - if (code < 0) { - setError(std::string("seek failed: ") + mpv_error_string(code)); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) + rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_ABSOLUTE, seconds, + exact ? 1 : 0, keyframe ? 1 : 0); } - setState("SEEKING"); - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::seekPercent(double percent, bool exact, bool keyframe) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(percent)) return false; - percent = std::clamp(percent, 0.0, 100.0); - std::string amount = std::to_string(percent); - const char* mode = exact ? "exact" : (keyframe ? "keyframes" : "default"); - const char* command[] = {"seek", amount.c_str(), "absolute-percent", mode, nullptr}; - int code = mpv_command(handle_, command); - if (code < 0) { - setError(std::string("seek percent failed: ") + mpv_error_string(code)); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) + rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_PERCENT, percent, + exact ? 1 : 0, keyframe ? 1 : 0); } - setState("SEEKING"); - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setSpeed(double speed) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(speed) || speed <= 0) return false; - std::string value = std::to_string(speed); - if (mpv_set_property_string(handle_, "speed", value.c_str()) < 0) { - setError("speed failed"); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_speed(corePlayer_, speed); } - std::lock_guard lock(mutex_); - snapshot_.speed = speed; - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setVolume(double volume) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(volume) || volume < 0) return false; - std::string value = std::to_string(volume); - if (mpv_set_property_string(handle_, "volume", value.c_str()) < 0) { - setError("volume failed"); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_volume(corePlayer_, volume); } - std::lock_guard lock(mutex_); - snapshot_.volume = volume; - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setMute(bool muted) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) return false; - if (mpv_set_property_string(handle_, "mute", muted ? "yes" : "no") < 0) { - setError("mute failed"); - return false; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_mute(corePlayer_, muted ? 1 : 0); } - std::lock_guard lock(mutex_); - snapshot_.muted = muted; - return true; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::selectAudioTrack(int id) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || id < 0) return false; - std::string value = std::to_string(id); - return mpv_set_property_string(handle_, "aid", value.c_str()) >= 0; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_select_audio(corePlayer_, id); + } + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::selectSubtitleTrack(int id) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || id < 0) return false; - std::string value = std::to_string(id); - return mpv_set_property_string(handle_, "sid", value.c_str()) >= 0; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_select_subtitle(corePlayer_, id); + } + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::disableSubtitle() { - std::lock_guard apiLock(apiMutex_); - return handle_ != nullptr && mpv_set_property_string(handle_, "sid", "no") >= 0; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_disable_subtitle(corePlayer_); + } + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::addSubtitle(const PlaybackSubtitleSource& source) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || source.url.empty()) return false; + if (source.url.empty()) return false; if (!supportedSubtitleEncoding(source.encoding)) { setError("unsupported subtitle encoding"); + std::lock_guard lock(mutex_); + snapshot_.errorCode = "SUBTITLE"; return false; } - if (!source.encoding.empty()) - mpv_set_property_string(handle_, "sub-codepage", source.encoding.c_str()); - const char* flags = (source.select || source.isDefault) ? "select" : "auto"; - const char* command[] = {"sub-add", source.url.c_str(), flags, source.title.c_str(), - source.language.c_str(), nullptr}; - int code = mpv_command(handle_, command); - if (code < 0) setError(std::string("subtitle failed: ") + mpv_error_string(code)); - return code >= 0; + 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 apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_add_subtitle(corePlayer_, &coreSource); + } + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::removeSubtitle(int id) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || id < 0) return false; - std::string value = std::to_string(id); - const char* command[] = {"sub-remove", value.c_str(), nullptr}; - return mpv_command(handle_, command) >= 0; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_remove_subtitle(corePlayer_, id); + } + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setSubtitleDelay(double seconds) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(seconds)) return false; - const std::string value = std::to_string(seconds); - const bool ok = mpv_set_property_string(handle_, "sub-delay", value.c_str()) >= 0; - if (ok) { - std::lock_guard lock(mutex_); - snapshot_.subtitleDelayMs = static_cast(seconds * 1000.0); + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_delay(corePlayer_, seconds); } - return ok; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setSubtitleScale(double scale) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(scale) || scale <= 0) return false; - const std::string value = std::to_string(scale); - const bool ok = mpv_set_property_string(handle_, "sub-scale", value.c_str()) >= 0; - if (ok) { - std::lock_guard lock(mutex_); - snapshot_.subtitleScale = scale; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_scale(corePlayer_, scale); } - return ok; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setSubtitlePosition(double position) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || !std::isfinite(position) || position < 0 || position > 100) return false; - const std::string value = std::to_string(position); - const bool ok = mpv_set_property_string(handle_, "sub-pos", value.c_str()) >= 0; - if (ok) { - std::lock_guard lock(mutex_); - snapshot_.subtitlePosition = position; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_set_subtitle_position(corePlayer_, position); } - return ok; + syncCoreSnapshot(); + return rc >= 0; } bool PlaybackEngine::setAssOverride(const std::string& mode) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr || mode.empty()) return false; - return mpv_set_property_string(handle_, "sub-ass-override", mode.c_str()) >= 0; + int rc = -1; + { + std::lock_guard 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) { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) { - return; - } - if (!std::isfinite(delta)) { - setError("seek delta is not finite"); - return; - } - double target = 0.0; - double duration = 0.0; + int rc = -1; { - std::lock_guard lock(mutex_); - target = snapshot_.position + delta; - duration = snapshot_.duration; + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_seek(corePlayer_, MP_HARMONY_SEEK_RELATIVE, + delta, 0, 0); } - if (duration > 0.0) target = std::clamp(target, 0.0, duration); - int code = mpv_set_property(handle_, "time-pos", MPV_FORMAT_DOUBLE, &target); - if (code < 0) { - logMpvError("seek", code); - setError("seek failed"); - return; - } - std::lock_guard lock(mutex_); - snapshot_.lastError.clear(); + if (rc < 0) setError("seek failed"); + syncCoreSnapshot(); } void PlaybackEngine::stop() { - std::lock_guard apiLock(apiMutex_); - if (handle_ == nullptr) { - return; + int rc = -1; + { + std::lock_guard apiLock(apiMutex_); + if (corePlayer_ != nullptr) rc = mp_harmony_playback_stop(corePlayer_); } - int code = mpv_command_string(handle_, "stop"); - if (code < 0) { - logMpvError("stop", code); - setError("stop failed"); - return; - } - PlaybackRequest emptyRequest; - setHeadersLocked(emptyRequest); - mpv_set_property_string(handle_, "user-agent", ""); - mpv_set_property_string(handle_, "referrer", ""); - setState("STOPPED"); + if (rc < 0) setError("stop failed"); + syncCoreSnapshot(); } void PlaybackEngine::release() { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "release called initialized=%{public}d", initialized_.load()); - if (!initialized_.exchange(false)) { - return; - } - stopEvents_.store(true); - if (handle_ != nullptr) { - mpv_wakeup(handle_); - } - if (eventThread_.joinable()) { - eventThread_.join(); - } - std::lock_guard apiLock(apiMutex_); - if (handle_ != nullptr) { - mpv_terminate_destroy(handle_); + mp_harmony_player* player = nullptr; + { + std::lock_guard 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 lock(mutex_); + snapshot_.state = "RELEASED"; + snapshot_.hwdecRequested = false; + snapshot_.hwdecActive = false; + snapshot_.actualDecoder = "software"; } - mpv_harmony_hwdec_set_requested(0); - setState("RELEASED"); } void PlaybackEngine::markFrameRendered() { - bool hwdec_active = false; std::lock_guard lock(mutex_); - hwdec_active = snapshot_.hwdecActive; ++snapshot_.renderCount; - if (!hwdec_active) - mpv_harmony_hwdec_record_software_frame(); + if (!snapshot_.hwdecActive) mpv_harmony_hwdec_record_software_frame(); if (snapshot_.firstFrameMs < 0) { - auto elapsed = std::chrono::steady_clock::now() - startedAt_; + const auto elapsed = std::chrono::steady_clock::now() - startedAt_; snapshot_.firstFrameMs = std::chrono::duration_cast(elapsed).count(); - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, - "first frame: %{public}lld ms", static_cast(snapshot_.firstFrameMs)); } } PlaybackSnapshot PlaybackEngine::snapshot() const { + const_cast(this)->syncCoreSnapshot(); std::lock_guard lock(mutex_); - const_cast(this)->syncHwdecStatsLocked(); + const_cast(this)->syncHwdecStatsLocked(); return snapshot_; } -bool PlaybackEngine::setHeadersLocked(const PlaybackRequest& request) +void PlaybackEngine::syncCoreSnapshot() { - std::vector values; - values.reserve(request.headers.size() + 1); - for (const auto& header : request.headers) { - if (!header.name.empty()) - values.push_back(header.name + ": " + header.value); + 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 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 (!request.cookies.empty()) - values.push_back("Cookie: " + request.cookies); - std::vector nodes(values.size()); - for (size_t i = 0; i < values.size(); ++i) { - nodes[i].format = MPV_FORMAT_STRING; - nodes[i].u.string = const_cast(values[i].c_str()); - } - mpv_node_list list = {}; - list.num = static_cast(nodes.size()); - list.values = nodes.empty() ? nullptr : nodes.data(); - mpv_node node = {}; - node.format = MPV_FORMAT_NODE_ARRAY; - node.u.list = &list; - int code = mpv_set_property(handle_, "http-header-fields", MPV_FORMAT_NODE, &node); - if (code < 0) { - logMpvError("http-header-fields", code); - return false; - } - return true; -} + if (!available) return; -void PlaybackEngine::refreshTracksLocked() -{ - if (handle_ == nullptr) return; - mpv_node node = {}; - if (mpv_get_property(handle_, "track-list", MPV_FORMAT_NODE, &node) < 0) return; - snapshot_.tracks.clear(); - if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list != nullptr) { - for (int i = 0; i < node.u.list->num; ++i) { - const mpv_node& item = node.u.list->values[i]; - if (item.format != MPV_FORMAT_NODE_MAP || item.u.list == nullptr) continue; + std::lock_guard 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; + 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.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.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; + + if (tracksAvailable) { + snapshot_.tracks.clear(); + const size_t count = std::min(coreTrackCount, static_cast(MP_HARMONY_MAX_TRACKS)); + snapshot_.tracks.reserve(count); + for (size_t i = 0; i < count; ++i) { PlaybackTrack track; - for (int j = 0; j < item.u.list->num; ++j) { - const char* key = item.u.list->keys[j]; - const mpv_node& value = item.u.list->values[j]; - if (key == nullptr) continue; - if (std::strcmp(key, "id") == 0 && value.format == MPV_FORMAT_INT64) - track.id = static_cast(value.u.int64); - else if (std::strcmp(key, "type") == 0 && value.format == MPV_FORMAT_STRING) - track.type = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "title") == 0 && value.format == MPV_FORMAT_STRING) - track.title = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "lang") == 0 && value.format == MPV_FORMAT_STRING) - track.language = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "codec") == 0 && value.format == MPV_FORMAT_STRING) - track.codec = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "default") == 0 && value.format == MPV_FORMAT_FLAG) - track.isDefault = value.u.flag != 0; - else if (std::strcmp(key, "forced") == 0 && value.format == MPV_FORMAT_FLAG) - track.forced = value.u.flag != 0; - else if (std::strcmp(key, "selected") == 0 && value.format == MPV_FORMAT_FLAG) - track.selected = value.u.flag != 0; - else if (std::strcmp(key, "external") == 0 && value.format == MPV_FORMAT_FLAG) - track.external = value.u.flag != 0; - } + 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)); } } - mpv_free_node_contents(&node); - snapshot_.media.trackCount = static_cast(snapshot_.tracks.size()); - snapshot_.media.subtitleCount = static_cast(std::count_if( - snapshot_.tracks.begin(), snapshot_.tracks.end(), - [](const PlaybackTrack& track) { return track.type == "sub"; })); -} -void PlaybackEngine::refreshMediaLocked() -{ - if (handle_ == nullptr) return; - auto getString = [this](const char* name) { - char* value = mpv_get_property_string(handle_, name); - std::string result = value ? value : ""; - if (value) mpv_free(value); - return result; - }; - snapshot_.media.container = getString("file-format"); - snapshot_.media.demuxer = getString("current-demuxer"); - snapshot_.media.videoCodec = getString("video-codec"); - snapshot_.media.audioCodec = getString("audio-codec"); - snapshot_.media.pixelFormat = getString("video-format"); - int64_t number = 0; - if (mpv_get_property(handle_, "width", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.width = static_cast(number); - if (mpv_get_property(handle_, "height", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.height = static_cast(number); - if (mpv_get_property(handle_, "video-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.videoBitrate = number; - if (mpv_get_property(handle_, "audio-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.audioBitrate = number; - if (mpv_get_property(handle_, "demux-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.bitrate = number; - if (mpv_get_property(handle_, "container-fps", MPV_FORMAT_DOUBLE, &snapshot_.media.fps) < 0) snapshot_.media.fps = 0; - if (mpv_get_property(handle_, "duration", MPV_FORMAT_DOUBLE, &snapshot_.media.duration) >= 0) - snapshot_.duration = snapshot_.media.duration; - mpv_node node = {}; - if (mpv_get_property(handle_, "video-params", MPV_FORMAT_NODE, &node) >= 0) { - if (node.format == MPV_FORMAT_NODE_MAP && node.u.list != nullptr) { - for (int i = 0; i < node.u.list->num; ++i) { - const char* key = node.u.list->keys[i]; - const mpv_node& value = node.u.list->values[i]; - if (!key) continue; - if (std::strcmp(key, "pixelformat") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.pixelFormat = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "primaries") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorPrimaries = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "gamma") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorTransfer = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "colormatrix") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorSpace = value.u.string ? value.u.string : ""; - else if (std::strcmp(key, "bit-depth") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.bitDepth = static_cast(value.u.int64); - } - } - mpv_free_node_contents(&node); - } - node = {}; - if (mpv_get_property(handle_, "audio-params", MPV_FORMAT_NODE, &node) >= 0) { - if (node.format == MPV_FORMAT_NODE_MAP && node.u.list != nullptr) { - for (int i = 0; i < node.u.list->num; ++i) { - const char* key = node.u.list->keys[i]; - const mpv_node& value = node.u.list->values[i]; - if (!key) continue; - if (std::strcmp(key, "samplerate") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.audioSampleRate = static_cast(value.u.int64); - else if (std::strcmp(key, "channels") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.audioChannels = static_cast(value.u.int64); - else if (std::strcmp(key, "channel-layout") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.audioChannelLayout = value.u.string ? value.u.string : ""; - } - } - mpv_free_node_contents(&node); - } - refreshTracksLocked(); - refreshNetworkLocked(); -} - -void PlaybackEngine::refreshNetworkLocked() -{ - if (handle_ == nullptr) return; - const std::string url = [&] { - char* value = mpv_get_property_string(handle_, "stream-open-filename"); - std::string result = value ? value : ""; - if (value) mpv_free(value); - return result; - }(); - const size_t schemeEnd = url.find("://"); - if (schemeEnd != std::string::npos) { - snapshot_.network.protocol = url.substr(0, schemeEnd); - const size_t hostStart = schemeEnd + 3; - const size_t hostEnd = url.find_first_of("/?#", hostStart); - snapshot_.network.host = url.substr(hostStart, hostEnd == std::string::npos ? std::string::npos : hostEnd - hostStart); - } - int network = 0; - if (mpv_get_property(handle_, "demuxer-via-network", MPV_FORMAT_FLAG, &network) >= 0) - snapshot_.network.networkState = network ? "network" : "local"; - if (mpv_get_property(handle_, "demuxer-cache-duration", MPV_FORMAT_DOUBLE, &snapshot_.network.cacheDuration) < 0) - snapshot_.network.cacheDuration = 0; - int64_t buffering = 0; - if (mpv_get_property(handle_, "cache-buffering-state", MPV_FORMAT_INT64, &buffering) >= 0) - snapshot_.network.buffering = buffering > 0; - mpv_node cache = {}; - if (mpv_get_property(handle_, "demuxer-cache-state", MPV_FORMAT_NODE, &cache) >= 0 && - cache.format == MPV_FORMAT_NODE_MAP && cache.u.list != nullptr) { - for (int i = 0; i < cache.u.list->num; ++i) { - if (std::strcmp(cache.u.list->keys[i], "total-bytes") == 0 && - cache.u.list->values[i].format == MPV_FORMAT_INT64) - snapshot_.network.cacheBytes = cache.u.list->values[i].u.int64; - } - mpv_free_node_contents(&cache); - } - snapshot_.network.demuxer = snapshot_.media.demuxer; - snapshot_.network.container = snapshot_.media.container; - if (mpv_get_property(handle_, "demux-bitrate", MPV_FORMAT_INT64, &snapshot_.network.currentBitrate) < 0) - snapshot_.network.currentBitrate = 0; + 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; + 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; @@ -760,263 +593,21 @@ void PlaybackEngine::syncHwdecStatsLocked() 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; + 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::handleLogMessage(const mpv_event_log_message* message) -{ - if (!message || !message->text) - return; - const std::string text = redactSensitive(message->text); - if (containsInsensitive(text, "http/1.") || containsInsensitive(text, "http error")) { - std::istringstream stream(text); - std::string token; - while (stream >> token) { - if (token.size() == 3 && std::isdigit(static_cast(token[0])) && - std::isdigit(static_cast(token[1])) && - std::isdigit(static_cast(token[2]))) { - const int status = std::atoi(token.c_str()); - if (status >= 100 && status <= 599) { - std::lock_guard lock(mutex_); - snapshot_.network.httpStatus = status; - break; - } - } - } - } - if (containsInsensitive(text, "redirect")) { - std::lock_guard lock(mutex_); - ++snapshot_.network.redirectCount; - } - const char* rawText = message->text; - if (std::strstr(rawText, "h264_ohcodec") || std::strstr(rawText, "hevc_ohcodec")) { - std::lock_guard lock(mutex_); - snapshot_.actualDecoder = std::strstr(rawText, "hevc_ohcodec") ? "hevc_ohcodec" : "h264_ohcodec"; - snapshot_.hwdecActive = true; - } - if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0 && - (std::strstr(rawText, "fallback") || std::strstr(rawText, "falling back") || - std::strstr(rawText, "No hardware decoding"))) { - bool requested = false; - { - std::lock_guard lock(mutex_); - requested = snapshot_.hwdecRequested; - } - if (requested) - mpv_harmony_hwdec_set_fallback(text.c_str()); - } - if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0) { - OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, - "mpv[%{public}s] %{public}s", message->prefix, text.c_str()); - } -} - void PlaybackEngine::setState(const char* state) { std::lock_guard lock(mutex_); - snapshot_.state = state; + snapshot_.state = state ? state : "UNKNOWN"; } void PlaybackEngine::setError(const std::string& error) { std::lock_guard lock(mutex_); snapshot_.state = "ERROR"; - snapshot_.lastError = redactSensitive(error); - snapshot_.errorCode = classifyError(snapshot_.lastError); - OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s", snapshot_.lastError.c_str()); -} - -void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property* property) -{ - if (property == nullptr || property->data == nullptr) { - return; - } - std::lock_guard lock(mutex_); - switch (userdata) { - case kTimePos: - if (property->format == MPV_FORMAT_DOUBLE) snapshot_.position = *static_cast(property->data); - break; - case kDuration: - if (property->format == MPV_FORMAT_DOUBLE) { - snapshot_.duration = *static_cast(property->data); - snapshot_.media.duration = snapshot_.duration; - } - break; - case kPause: - if (property->format == MPV_FORMAT_FLAG && *static_cast(property->data) != 0 && - snapshot_.state == "PLAYING") snapshot_.state = "PAUSED"; - break; - case kFps: - if (property->format == MPV_FORMAT_DOUBLE) snapshot_.fps = *static_cast(property->data); - break; - case kDropped: - if (property->format == MPV_FORMAT_INT64) snapshot_.dropped = *static_cast(property->data); - break; - case kTracks: - refreshTracksLocked(); - break; - case kCacheDuration: - case kCacheState: - case kBuffering: - refreshNetworkLocked(); - if (userdata == kBuffering && property->format == MPV_FORMAT_INT64) { - const int64_t buffering = *static_cast(property->data); - if (buffering > 0) snapshot_.state = "BUFFERING"; - else if (snapshot_.state == "BUFFERING") snapshot_.state = "PLAYING"; - } - break; - case kVolume: - if (property->format == MPV_FORMAT_DOUBLE) snapshot_.volume = *static_cast(property->data); - break; - case kMute: - if (property->format == MPV_FORMAT_FLAG) snapshot_.muted = *static_cast(property->data) != 0; - break; - case kSpeed: - if (property->format == MPV_FORMAT_DOUBLE) snapshot_.speed = *static_cast(property->data); - break; - case kHwdecCurrent: - if (property->format == MPV_FORMAT_STRING) { - const char *value = *static_cast(property->data); - snapshot_.hwdecActive = value && std::strcmp(value, "no") != 0; - } - break; - case kHwdecInterop: - if (property->format == MPV_FORMAT_STRING) { - const char *value = *static_cast(property->data); - snapshot_.hwdecBackend = value ? value : ""; - } - break; - case kDecoder: - if (property->format == MPV_FORMAT_STRING) { - const char *value = *static_cast(property->data); - if (value && value[0]) { - snapshot_.actualDecoder = value; - mpv_harmony_hwdec_set_decoder(value); - } - } - break; - default: - break; - } -} - -void PlaybackEngine::eventLoop() -{ - while (!stopEvents_.load() && handle_ != nullptr) { - mpv_event* event = mpv_wait_event(handle_, 0.02); - if (event == nullptr) { - continue; - } - switch (event->event_id) { - case MPV_EVENT_LOG_MESSAGE: { - auto* message = static_cast(event->data); - if (message != nullptr && message->prefix != nullptr && - (std::strncmp(message->prefix, "ao", 2) == 0 || - std::strncmp(message->prefix, "vd", 2) == 0 || - std::strncmp(message->prefix, "cplayer", 7) == 0 || - std::strncmp(message->prefix, "ffmpeg", 6) == 0)) - handleLogMessage(message); - break; - } - case MPV_EVENT_PROPERTY_CHANGE: - updateProperty(event->reply_userdata, static_cast(event->data)); - break; - case MPV_EVENT_START_FILE: - { - auto* start = static_cast(event->data); - std::lock_guard lock(mutex_); - awaitingStart_ = true; - if (start) playlistEntryId_ = start->playlist_entry_id; - snapshot_.state = "OPENING"; - } - break; - case MPV_EVENT_FILE_LOADED: - { - std::lock_guard lock(mutex_); - awaitingStart_ = false; - refreshMediaLocked(); - snapshot_.state = "READY"; - } - break; - case MPV_EVENT_END_FILE: { - auto* end = static_cast(event->data); - { - std::lock_guard lock(mutex_); - if (end != nullptr && end->playlist_entry_id != 0 && playlistEntryId_ != 0 && - end->playlist_entry_id != playlistEntryId_) break; - } - if (end != nullptr && end->reason == MPV_END_FILE_REASON_ERROR) { - setError(std::string("end-file: ") + mpv_error_string(end->error)); - } else if (end != nullptr && end->reason == MPV_END_FILE_REASON_EOF) { - setState("ENDED"); - } else { - setState("STOPPED"); - } - break; - } - case MPV_EVENT_SHUTDOWN: - return; - default: - break; - } - } -} - -std::string PlaybackEngine::redactSensitive(const std::string& value) -{ - std::string result = value; - auto lowerCopy = [](const std::string& input) { - std::string lower = input; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return lower; - }; - const char* keys[] = {"authorization:", "cookie:", "proxy-authorization:", "x-api-key:", - "token:", "api-key:", "apikey:", "secret:", "signature:"}; - for (const char* key : keys) { - size_t position = 0; - while ((position = lowerCopy(result).find(key, position)) != std::string::npos) { - const size_t valueStart = position + std::strlen(key); - const size_t end = result.find_first_of("\r\n", valueStart); - result.replace(valueStart, (end == std::string::npos ? result.size() : end) - valueStart, " [REDACTED]"); - position = valueStart + 11; - } - } - const char* queryKeys[] = {"token=", "access_token=", "auth=", "key=", "api_key=", "apikey=", - "signature=", "x-amz-signature=", "sig=", "expires="}; - for (const char* key : queryKeys) { - size_t position = 0; - while ((position = lowerCopy(result).find(key, position)) != std::string::npos) { - const size_t end = result.find_first_of("& \r\n", position); - result.replace(position, (end == std::string::npos ? result.size() : end) - position, std::string(key) + "[REDACTED]"); - position += std::strlen(key) + 10; - } - } - return result; -} - -std::string PlaybackEngine::classifyError(const std::string& value) -{ - if (containsInsensitive(value, "403") || containsInsensitive(value, "404") || - containsInsensitive(value, "500") || containsInsensitive(value, "http error")) - return "HTTP"; - if (containsInsensitive(value, "tls") || containsInsensitive(value, "certificate") || - containsInsensitive(value, "ssl")) return "TLS"; - if (containsInsensitive(value, "not found") || containsInsensitive(value, "no such file")) return "FILE_NOT_FOUND"; - if (containsInsensitive(value, "permission") || containsInsensitive(value, "access denied")) return "PERMISSION"; - if (containsInsensitive(value, "subtitle") || containsInsensitive(value, "sub ")) return "SUBTITLE"; - if (containsInsensitive(value, "video codec") || containsInsensitive(value, "video decoder")) return "UNSUPPORTED_VIDEO_CODEC"; - if (containsInsensitive(value, "audio codec") || containsInsensitive(value, "audio decoder")) return "UNSUPPORTED_AUDIO_CODEC"; - if (containsInsensitive(value, "unsupported") && containsInsensitive(value, "container")) return "UNSUPPORTED_CONTAINER"; - if (containsInsensitive(value, "demux") || containsInsensitive(value, "format")) return "DEMUX"; - if (containsInsensitive(value, "decoder")) return "DECODER"; - if (containsInsensitive(value, "audio output")) return "AUDIO_OUTPUT"; - if (containsInsensitive(value, "video output")) return "VIDEO_OUTPUT"; - if (containsInsensitive(value, "seek")) return "SEEK"; - if (containsInsensitive(value, "timeout") || containsInsensitive(value, "connection") || - containsInsensitive(value, "network") || containsInsensitive(value, "dns")) return "NETWORK"; - return "UNKNOWN"; + snapshot_.lastError = error; + snapshot_.errorCode = "UNKNOWN"; } diff --git a/playback_lab/entry/src/main/cpp/player.h b/playback_lab/entry/src/main/cpp/player.h index 23f31e28df..f1a00c8774 100644 --- a/playback_lab/entry/src/main/cpp/player.h +++ b/playback_lab/entry/src/main/cpp/player.h @@ -1,13 +1,12 @@ #pragma once -#include +#include "harmony_playback_internal.h" #include #include #include #include #include -#include #include struct PlaybackHeader { @@ -25,7 +24,7 @@ struct PlaybackSubtitleSource { bool isDefault = false; }; -struct PlaybackRequest { +struct PlaybackRequestModel { std::string url; std::string title; int64_t startPositionMs = -1; @@ -138,7 +137,7 @@ public: bool setAudioEnabled(bool enabled); bool setHardwareDecode(bool enabled); void load(const std::string& url); - bool load(const PlaybackRequest& request); + bool load(const PlaybackRequestModel& request); void play(); void pause(); void togglePause(); @@ -165,32 +164,16 @@ public: void markFrameRendered(); private: - void eventLoop(); - void observeProperties(); void setState(const char* state); void setError(const std::string& error); - void updateProperty(uint64_t userdata, const mpv_event_property* property); void syncHwdecStatsLocked(); - void handleLogMessage(const mpv_event_log_message* message); - void refreshTracksLocked(); - void refreshMediaLocked(); - void refreshNetworkLocked(); - bool setHeadersLocked(const PlaybackRequest& request); - static std::string redactSensitive(const std::string& value); - static std::string classifyError(const std::string& value); + void syncCoreSnapshot(); mutable std::mutex mutex_; mutable std::mutex apiMutex_; PlaybackSnapshot snapshot_; + mp_harmony_player* corePlayer_ = nullptr; mpv_handle* handle_ = nullptr; - std::thread eventThread_; - std::atomic stopEvents_{false}; std::atomic initialized_{false}; - uint64_t sessionId_ = 0; - int64_t playlistEntryId_ = 0; - bool awaitingStart_ = false; std::chrono::steady_clock::time_point startedAt_; - static constexpr uint64_t kHwdecCurrent = 6; - static constexpr uint64_t kHwdecInterop = 7; - static constexpr uint64_t kDecoder = 8; }; diff --git a/playback_lab/entry/src/main/ets/pages/Index.ets b/playback_lab/entry/src/main/ets/pages/Index.ets index baf55622a5..2cb3f4f940 100644 --- a/playback_lab/entry/src/main/ets/pages/Index.ets +++ b/playback_lab/entry/src/main/ets/pages/Index.ets @@ -41,6 +41,8 @@ interface PlaybackState { gpuInteropFrameCount: number; sessionId: number; errorCode: string; + error: PlaybackErrorState; + metrics: PlaybackMetricsState; speed: number; volume: number; muted: boolean; @@ -52,6 +54,20 @@ interface PlaybackState { tracks: TrackState[]; } +interface PlaybackErrorState { + code: string; + rawMessage: string; +} + +interface PlaybackMetricsState { + droppedFrames: number; + renderCount: number; + buffering: boolean; + cacheDuration: number; + cacheBytes: number; + currentBitrate: number; +} + interface NetworkState { protocol: string; host: string; @@ -83,6 +99,7 @@ interface MediaState { bitDepth: number; audioSampleRate: number; audioChannels: number; + audioChannelLayout: string; trackCount: number; subtitleCount: number; } @@ -165,12 +182,14 @@ struct Index { hardwareFrameCount: 0, softwareFrameCount: 0, hwFallbackCount: 0, nativeBufferImportCount: 0, nativeBufferImportFailure: 0, surfacePresentCount: 0, gpuInteropFrameCount: 0, - sessionId: 0, errorCode: 'NONE', speed: 1, volume: 100, muted: false, + sessionId: 0, errorCode: 'NONE', error: { code: 'NONE', rawMessage: '' }, + metrics: { droppedFrames: 0, renderCount: 0, buffering: false, cacheDuration: 0, cacheBytes: 0, currentBitrate: 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 }, media: { container: '', demuxer: '', duration: 0, videoCodec: '', audioCodec: '', pixelFormat: '', colorPrimaries: '', colorTransfer: '', colorMatrix: '', - width: 0, height: 0, fps: 0, bitrate: 0, bitDepth: 0, audioSampleRate: 0, audioChannels: 0, + width: 0, height: 0, fps: 0, bitrate: 0, bitDepth: 0, audioSampleRate: 0, audioChannels: 0, audioChannelLayout: '', trackCount: 0, subtitleCount: 0 }, tracks: [] }; diff --git a/player/harmony_playback.c b/player/harmony_playback.c index 18c50fa825..15505daa3e 100644 --- a/player/harmony_playback.c +++ b/player/harmony_playback.c @@ -14,6 +14,7 @@ #include #include #include +#include struct mp_harmony_player { pthread_mutex_t lock; @@ -23,6 +24,7 @@ struct mp_harmony_player { int stop_events; mpv_handle *handle; uint64_t session_id; + char current_url[MP_HARMONY_URL]; int64_t playlist_entry_id; int awaiting_start; mp_harmony_playback_snapshot snapshot; @@ -38,6 +40,9 @@ enum { static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *request); +/* Private renderer bridge; intentionally absent from the public header. */ +MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player); + static void copy_text(char *dst, size_t size, const char *src) { if (!dst || size == 0) return; @@ -60,10 +65,21 @@ static int text_has(const char *text, const char *needle) static int supported_subtitle_encoding(const char *encoding) { if (!encoding || !encoding[0]) return 1; - return strcasecmp(encoding, "utf-8") == 0 || strcasecmp(encoding, "utf8") == 0 || - strcasecmp(encoding, "gbk") == 0 || strcasecmp(encoding, "gb18030") == 0 || - strcasecmp(encoding, "big5") == 0 || strcasecmp(encoding, "shift-jis") == 0 || - strcasecmp(encoding, "sjis") == 0; + /* The current OHOS matrix has no iconv feature; accept UTF-8 only so + * unsupported input cannot silently fall back to mojibake. */ + return strcasecmp(encoding, "utf-8") == 0 || strcasecmp(encoding, "utf8") == 0; +} + +static int supported_ass_override(const char *mode) +{ + return mode && (strcmp(mode, "no") == 0 || strcmp(mode, "yes") == 0 || + strcmp(mode, "force") == 0 || strcmp(mode, "scale") == 0 || + strcmp(mode, "strip") == 0); +} + +static int is_cookie_header(const char *name) +{ + return name && strcasecmp(name, "cookie") == 0; } static void redact_text(char *dst, size_t size, const char *src) @@ -234,8 +250,8 @@ static int node_int(mpv_node *node) static void refresh_tracks_locked(mp_harmony_player *p) { mpv_node node = {0}; - p->track_count = 0; if (mpv_get_property(p->handle, "track-list", MPV_FORMAT_NODE, &node) < 0) return; + p->track_count = 0; if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list) { for (int i = 0; i < node.u.list->num && p->track_count < MP_HARMONY_MAX_TRACKS; i++) { mpv_node *item = &node.u.list->values[i]; @@ -273,15 +289,15 @@ static void refresh_tracks_locked(mp_harmony_player *p) static void refresh_media_locked(mp_harmony_player *p) { char *v = mpv_get_property_string(p->handle, "file-format"); - copy_text(p->snapshot.media.container, sizeof(p->snapshot.media.container), v); if (v) mpv_free(v); + if (v && v[0]) copy_text(p->snapshot.media.container, sizeof(p->snapshot.media.container), v); if (v) mpv_free(v); v = mpv_get_property_string(p->handle, "current-demuxer"); - copy_text(p->snapshot.media.demuxer, sizeof(p->snapshot.media.demuxer), v); if (v) mpv_free(v); + if (v && v[0]) copy_text(p->snapshot.media.demuxer, sizeof(p->snapshot.media.demuxer), v); if (v) mpv_free(v); v = mpv_get_property_string(p->handle, "video-codec"); - copy_text(p->snapshot.media.video_codec, sizeof(p->snapshot.media.video_codec), v); if (v) mpv_free(v); + if (v && v[0]) copy_text(p->snapshot.media.video_codec, sizeof(p->snapshot.media.video_codec), v); if (v) mpv_free(v); v = mpv_get_property_string(p->handle, "audio-codec"); - copy_text(p->snapshot.media.audio_codec, sizeof(p->snapshot.media.audio_codec), v); if (v) mpv_free(v); + if (v && v[0]) copy_text(p->snapshot.media.audio_codec, sizeof(p->snapshot.media.audio_codec), v); if (v) mpv_free(v); v = mpv_get_property_string(p->handle, "video-format"); - copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), v); if (v) mpv_free(v); + if (v && v[0]) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), v); if (v) mpv_free(v); int64_t i64 = 0; if (mpv_get_property(p->handle, "width", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.width = (int)i64; if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.height = (int)i64; @@ -290,8 +306,9 @@ static void refresh_media_locked(mp_harmony_player *p) if (mpv_get_property(p->handle, "demux-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.bitrate = i64; if (mpv_get_property(p->handle, "container-fps", MPV_FORMAT_DOUBLE, &p->snapshot.media.fps) < 0) p->snapshot.media.fps = 0; - if (mpv_get_property(p->handle, "duration", MPV_FORMAT_DOUBLE, &p->snapshot.duration) < 0) - p->snapshot.duration = 0; + double duration = 0; + if (mpv_get_property(p->handle, "duration", MPV_FORMAT_DOUBLE, &duration) >= 0) + p->snapshot.duration = duration; p->snapshot.media.duration = p->snapshot.duration; mpv_node node = {0}; if (mpv_get_property(p->handle, "video-params", MPV_FORMAT_NODE, &node) >= 0) { @@ -392,8 +409,15 @@ static void *event_loop(void *arg) break; } case MPV_EVENT_FILE_LOADED: + { + char *loaded_url = mpv_get_property_string(p->handle, "stream-open-filename"); + int matches = !loaded_url || !loaded_url[0] || !p->current_url[0] || + strcmp(loaded_url, p->current_url) == 0; + if (loaded_url) mpv_free(loaded_url); + if (!matches) break; p->awaiting_start = 0; refresh_media_locked(p); p->snapshot.state = MP_HARMONY_READY; break; + } case MPV_EVENT_PROPERTY_CHANGE: update_property_locked(p, event->reply_userdata, event->data); break; @@ -432,7 +456,7 @@ mp_harmony_player *mp_harmony_playback_create(void) if (!p) return NULL; if (pthread_mutex_init(&p->lock, NULL) != 0) { free(p); return NULL; } p->lock_ready = 1; p->snapshot.state = MP_HARMONY_IDLE; - p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.speed = 1.0; + p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.speed = 1.0; p->snapshot.volume = 100; p->snapshot.subtitle_scale = 1.0; p->snapshot.subtitle_position = 100.0; return p; } @@ -466,7 +490,7 @@ int mp_harmony_playback_initialize(mp_harmony_player *p) mpv_observe_property(p->handle, PROP_DURATION, "duration", MPV_FORMAT_DOUBLE); mpv_observe_property(p->handle, PROP_PAUSE, "pause", MPV_FORMAT_FLAG); mpv_observe_property(p->handle, PROP_FPS, "estimated-vf-fps", MPV_FORMAT_DOUBLE); - mpv_observe_property(p->handle, PROP_DROPPED, "vo-drop-frame-count", MPV_FORMAT_INT64); + mpv_observe_property(p->handle, PROP_DROPPED, "frame-drop-count", MPV_FORMAT_INT64); mpv_observe_property(p->handle, PROP_TRACKS, "track-list", MPV_FORMAT_NODE); mpv_observe_property(p->handle, PROP_CACHE_DURATION, "demuxer-cache-duration", MPV_FORMAT_DOUBLE); mpv_observe_property(p->handle, PROP_CACHE_STATE, "demuxer-cache-state", MPV_FORMAT_NODE); @@ -486,8 +510,14 @@ int mp_harmony_playback_initialize(mp_harmony_player *p) static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *r) { - size_t count = r->http_header_count + (r->cookies && r->cookies[0] ? 1 : 0); - if (count > MP_HARMONY_MAX_HEADERS) count = MP_HARMONY_MAX_HEADERS; + if (!r || (r->http_header_count && !r->http_headers)) return -1; + size_t count = 0; + for (size_t i = 0; i < r->http_header_count; i++) { + const char *name = r->http_headers[i].name; + if (name && name[0] && !(r->cookies && r->cookies[0] && is_cookie_header(name))) count++; + } + if (r->cookies && r->cookies[0]) count++; + if (count > MP_HARMONY_MAX_HEADERS) return -1; mpv_node *values = calloc(count ? count : 1, sizeof(*values)); char **owned = calloc(count ? count : 1, sizeof(*owned)); if (!values || !owned) { free(values); free(owned); return -1; } @@ -495,15 +525,23 @@ static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_re for (size_t i = 0; r->http_headers && i < r->http_header_count && index < count; i++) { const char *name = r->http_headers[i].name ? r->http_headers[i].name : ""; const char *value = r->http_headers[i].value ? r->http_headers[i].value : ""; + if (!name[0] || (r->cookies && r->cookies[0] && is_cookie_header(name))) continue; size_t n = strlen(name) + strlen(value) + 3; owned[index] = malloc(n); - if (!owned[index]) continue; + if (!owned[index]) { + for (size_t j = 0; j < index; j++) free(owned[j]); + free(owned); free(values); return -1; + } snprintf(owned[index], n, "%s: %s", name, value); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++; } if (r->cookies && r->cookies[0] && index < count) { size_t n = strlen(r->cookies) + 9; owned[index] = malloc(n); - if (owned[index]) { snprintf(owned[index], n, "Cookie: %s", r->cookies); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++; } + if (!owned[index]) { + for (size_t j = 0; j < index; j++) free(owned[j]); + free(owned); free(values); return -1; + } + snprintf(owned[index], n, "Cookie: %s", r->cookies); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++; } mpv_node_list list = {(int)index, values, NULL}; mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u.list = &list}; @@ -517,6 +555,12 @@ static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_re int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_request *r) { if (!p || !r || !r->url || !r->url[0]) return -1; + if (r->http_header_count > MP_HARMONY_MAX_HEADERS || + r->subtitle_file_count > MP_HARMONY_MAX_SUBTITLES || + (r->playback_speed != 0.0 && + (!isfinite(r->playback_speed) || r->playback_speed <= 0)) || + (r->network_timeout != 0.0 && + (!isfinite(r->network_timeout) || r->network_timeout < 0))) return -1; if (mp_harmony_playback_initialize(p) < 0) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } @@ -527,6 +571,11 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req p->snapshot.subtitle_delay_ms = 0; p->snapshot.subtitle_scale = 1.0; p->snapshot.subtitle_position = 100.0; + p->snapshot.media = (mp_harmony_media_info){0}; + p->snapshot.network = (mp_harmony_network_info){0}; + p->snapshot.metrics = (mp_harmony_playback_metrics){0}; + p->snapshot.track_count = 0; p->track_count = 0; p->awaiting_start = 1; + copy_text(p->current_url, sizeof(p->current_url), r->url); p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0; if (r->subtitle_file_count && !r->subtitle_files) { set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "subtitle list is null"); @@ -551,9 +600,9 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req } { char timeout[32]; snprintf(timeout, sizeof(timeout), "%.3f", r->network_timeout > 0 ? r->network_timeout : 0.0); if (set_property_string(p, "network-timeout", timeout) < 0) { pthread_mutex_unlock(&p->lock); return -1; } } if (r->playback_speed > 0) { char speed[32]; snprintf(speed, sizeof(speed), "%.4f", r->playback_speed); if (set_property_string(p, "speed", speed) < 0) { pthread_mutex_unlock(&p->lock); return -1; } p->snapshot.speed = r->playback_speed; } - { char id[32]; snprintf(id, sizeof(id), "%d", r->audio_track_id); set_property_string(p, "aid", r->audio_track_id >= 0 ? id : "auto"); } - if (r->subtitle_track_id >= 0) { char id[32]; snprintf(id, sizeof(id), "%d", r->subtitle_track_id); set_property_string(p, "sid", id); } - else set_property_string(p, "sid", r->subtitle_enabled == 0 ? "no" : "auto"); + { char id[32]; snprintf(id, sizeof(id), "%d", r->audio_track_id); if (set_property_string(p, "aid", r->audio_track_id >= 0 ? id : "auto") < 0) { pthread_mutex_unlock(&p->lock); return -1; } } + if (r->subtitle_track_id >= 0) { char id[32]; snprintf(id, sizeof(id), "%d", r->subtitle_track_id); if (set_property_string(p, "sid", id) < 0) { pthread_mutex_unlock(&p->lock); return -1; } } + else if (set_property_string(p, "sid", r->subtitle_enabled == 0 ? "no" : "auto") < 0) { pthread_mutex_unlock(&p->lock); return -1; } const char *args[] = {"loadfile", r->url, "replace", NULL}; int rc = mpv_command(p->handle, args); if (rc < 0) set_error_locked(p, classify_error(rc, mpv_error_string(rc)), mpv_error_string(rc)); @@ -565,9 +614,12 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed"); } pthread_mutex_unlock(&p->lock); - if (rc >= 0) - for (size_t i = 0; i < r->subtitle_file_count; i++) mp_harmony_playback_add_subtitle(p, &r->subtitle_files[i]); - return rc; + if (rc < 0) return rc; + for (size_t i = 0; i < r->subtitle_file_count; i++) { + if (r->subtitle_files[i].url && r->subtitle_files[i].url[0] && + mp_harmony_playback_add_subtitle(p, &r->subtitle_files[i]) < 0) return -1; + } + return 0; } int mp_harmony_playback_play(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "pause", "no"); if (r >= 0) p->snapshot.state = MP_HARMONY_PLAYING; pthread_mutex_unlock(&p->lock); return r; } @@ -577,12 +629,12 @@ int mp_harmony_playback_stop(mp_harmony_player *p) { if (!p) return -1; pthread_ int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe) { - if (!p || value != value) return -1; + if (!p || !isfinite(value) || mode < MP_HARMONY_SEEK_RELATIVE || mode > MP_HARMONY_SEEK_PERCENT) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } char amount[64]; snprintf(amount, sizeof(amount), "%.6f", value); const char *kind = mode == MP_HARMONY_SEEK_ABSOLUTE ? "absolute" : (mode == MP_HARMONY_SEEK_PERCENT ? "absolute-percent" : "relative"); - const char *flags = exact ? "exact" : (keyframe ? "keyframes" : "default"); + const char *flags = exact ? "exact" : (keyframe ? "keyframes" : NULL); const char *args[] = {"seek", amount, kind, flags, NULL}; int r = mpv_command(p->handle, args); if (r >= 0) p->snapshot.state = MP_HARMONY_SEEKING; @@ -590,19 +642,39 @@ int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, do pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_speed(mp_harmony_player *p, double speed) { if (!p || speed <= 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.4f", speed); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "speed", v); if (r >= 0) p->snapshot.speed = speed; pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_volume(mp_harmony_player *p, double volume) { if (!p || volume < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.3f", volume); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "volume", v); if (r >= 0) p->snapshot.volume = (int)volume; pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_speed(mp_harmony_player *p, double speed) { if (!p || !isfinite(speed) || speed <= 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.4f", speed); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "speed", v); if (r >= 0) p->snapshot.speed = speed; pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_volume(mp_harmony_player *p, double volume) { if (!p || !isfinite(volume) || volume < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.3f", volume); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "volume", v); if (r >= 0) p->snapshot.volume = (int)volume; pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_set_mute(mp_harmony_player *p, int muted) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "mute", muted ? "yes" : "no"); if (r >= 0) p->snapshot.muted = muted != 0; pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_set_audio_enabled(mp_harmony_player *p, int enabled) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", enabled ? "auto" : "no"); pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_select_audio(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", v); pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_select_subtitle(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", v); pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_select_audio(mp_harmony_player *p, int id) { if (!p || id < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", v); pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_select_subtitle(mp_harmony_player *p, int id) { if (!p || id < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", v); pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_disable_subtitle(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", "no"); pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_add_subtitle(mp_harmony_player *p, const mp_harmony_subtitle_source *s) { if (!p || !s || !s->url || !s->url[0]) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } if (!supported_subtitle_encoding(s->encoding)) { set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding"); pthread_mutex_unlock(&p->lock); return -1; } if (s->encoding && s->encoding[0]) set_property_string(p, "sub-codepage", s->encoding); const char *flags = (s->select || s->is_default) ? "select" : "auto"; const char *args[] = {"sub-add", s->url, flags, s->title ? s->title : "", s->language ? s->language : "", NULL}; int r = mpv_command(p->handle, args); if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_add_subtitle(mp_harmony_player *p, const mp_harmony_subtitle_source *s) +{ + if (!p || !s || !s->url || !s->url[0]) return -1; + pthread_mutex_lock(&p->lock); + if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } + if (!supported_subtitle_encoding(s->encoding)) { + set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding"); + pthread_mutex_unlock(&p->lock); return -1; + } + if (s->encoding && s->encoding[0] && set_property_string(p, "sub-codepage", s->encoding) < 0) { + pthread_mutex_unlock(&p->lock); return -1; + } + char flags[64]; + snprintf(flags, sizeof(flags), "%s%s%s", s->select ? "select" : "auto", + s->forced ? "+forced" : "", s->is_default ? "+default" : ""); + const char *args[] = {"sub-add", s->url, flags, s->title ? s->title : "", + s->language ? s->language : "", NULL}; + int r = mpv_command(p->handle, args); + if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); + pthread_mutex_unlock(&p->lock); return r; +} int mp_harmony_playback_remove_subtitle(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = -1; if (ensure_ready_locked(p) == 0) { const char *args[] = {"sub-remove", v, NULL}; r = mpv_command(p->handle, args); } if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *p, double seconds) { if (!p || seconds != seconds) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", seconds); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-delay", value); if (r >= 0) p->snapshot.subtitle_delay_ms = (int64_t)(seconds * 1000.0); pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *p, double scale) { if (!p || scale <= 0) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", scale); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-scale", value); if (r >= 0) p->snapshot.subtitle_scale = scale; pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_subtitle_position(mp_harmony_player *p, double position) { if (!p || position < 0 || position > 100) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", position); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-pos", value); if (r >= 0) p->snapshot.subtitle_position = position; pthread_mutex_unlock(&p->lock); return r; } -int mp_harmony_playback_set_ass_override(mp_harmony_player *p, const char *mode) { if (!p || !mode) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-ass-override", mode); pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *p, double seconds) { if (!p || !isfinite(seconds)) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", seconds); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-delay", value); if (r >= 0) p->snapshot.subtitle_delay_ms = (int64_t)(seconds * 1000.0); pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *p, double scale) { if (!p || !isfinite(scale) || scale <= 0) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", scale); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-scale", value); if (r >= 0) p->snapshot.subtitle_scale = scale; pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_subtitle_position(mp_harmony_player *p, double position) { if (!p || !isfinite(position) || position < 0 || position > 100) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", position); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-pos", value); if (r >= 0) p->snapshot.subtitle_position = position; pthread_mutex_unlock(&p->lock); return r; } +int mp_harmony_playback_set_ass_override(mp_harmony_player *p, const char *mode) { if (!p || !supported_ass_override(mode)) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-ass-override", mode); pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_get_tracks(mp_harmony_player *p, mp_harmony_track *tracks, size_t capacity, size_t *count) { if (!p || !count) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_tracks_locked(p); size_t n = p->track_count < capacity ? p->track_count : capacity; if (tracks && n) memcpy(tracks, p->tracks, n * sizeof(*tracks)); *count = p->track_count; pthread_mutex_unlock(&p->lock); return 0; } int mp_harmony_playback_get_media_info(mp_harmony_player *p, mp_harmony_media_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_media_locked(p); *info = p->snapshot.media; pthread_mutex_unlock(&p->lock); return 0; } int mp_harmony_playback_get_network_info(mp_harmony_player *p, mp_harmony_network_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } update_network_locked(p); *info = p->snapshot.network; pthread_mutex_unlock(&p->lock); return 0; } @@ -626,6 +698,15 @@ void mp_harmony_playback_destroy(mp_harmony_player *p) free(p); } +MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *p) +{ + if (!p) return NULL; + pthread_mutex_lock(&p->lock); + mpv_handle *handle = p->handle; + pthread_mutex_unlock(&p->lock); + return handle; +} + const char *mp_harmony_playback_state_name(mp_harmony_state state) { static const char *names[] = {"IDLE", "OPENING", "BUFFERING", "READY", "PLAYING", "PAUSED", "SEEKING", "ENDED", "STOPPED", "ERROR", "RELEASED"};