feat: complete phase5 playback network contract

This commit is contained in:
BoatingCloud committed 2026-08-29 16:49:38 +08:00
1 parent 49b078737b
commit c6f267d6dd
18 files changed
+182 -52

No files matched your search

+4 -2
View File
@@ -1,5 +1,7 @@
# Harmony 字体策略 # Harmony 字体策略
第一优先级使用系统字体;应用字体只允许由产品明确提供的开源字体;libass 使用 mpv 已配置的字体 provider,不新增商业字体包。 第一优先级使用系统字体;应用字体只允许由产品明确提供的开源字体;libass 使用 mpv 已配置的字体 provider,不新增商业字体包。当前 OHOS 字体栈按 `scripts/build-font-stack-ohos.sh` 构建:FreeType + HarfBuzz + FriBidi + libass,libass 明确关闭 fontconfig、DirectWrite、CoreText 和 iconv provider,因此不依赖 Linux `fonts.conf`。
中文、英文、日文、韩文采用字体 fallback 链,缺字时交给系统/provider 继续匹配。Playback Lab 只验证字幕渲染和缺字表现,不把字体文件或用户凭证提交到仓库。设备侧若 provider 行为与模拟器不同,应记录实际字体、脚本和缺字结果。 Playback Lab 初始化时将 mpv `sub-fonts-dir` 指向 `/system/fonts`;模拟器已确认该目录存在,`/system/etc/fontconfig` 与 `/system/etc/fonts` 不存在。libass 的 `ass_set_fonts_dir` 会从该目录加载字体,`sub-font-provider=auto` 在当前无系统 provider 时仍可使用目录字体和字幕内嵌字体。中文、英文、日文、韩文的缺字由目录字体顺序、libass fallback 与内嵌字体共同决定;应用若需补充字体,应通过 Resource Manager/应用沙箱提供明确的开源字体目录并在产品层映射到 `sub-fonts-dir`,不修改核心、不提交商业字体。
Playback Lab 只验证字幕渲染和缺字表现,不把字体文件或用户凭证提交到仓库。设备侧若系统字体目录、provider 行为或 fallback 顺序不同,应记录实际字体、脚本和缺字结果。
+1 -1
View File
@@ -63,7 +63,7 @@ Disabled:mpv CLI、tests、fuzzers、Lua/JavaScript、libcurl、桌面音视
Phase 4.5 的 FFmpeg fork 独立维护于 Phase 4.5 的 FFmpeg fork 独立维护于
`https://git.leee.xin/leeexin/Velix-FFmpeg-Harmony.git`,分支 `https://git.leee.xin/leeexin/Velix-FFmpeg-Harmony.git`,分支
`harmony-ohcodec-export`,当前分支 tip `990614364f`(代码修正 commit `harmony-ohcodec-export`,当前分支 tip `f3a036c695`(跨 origin 重定向丢弃敏感凭证;代码修正 commit
`aacbe7d578`)。MPV 不包含 FFmpeg `aacbe7d578`)。MPV 不包含 FFmpeg
源码,也不读取其私有头文件;需要切换时执行: 源码,也不读取其私有头文件;需要切换时执行:
+3 -1
View File
@@ -4,7 +4,9 @@
每次 load 先清空 http-header-fields,再设置本次请求的 headers;Cookie 作为本次请求的 Cookie header 发送,未启用 mpv 持久化 cookie 文件。请求可设置 user-agent、referrer、network-timeout、语言偏好和轨道选择。 每次 load 先清空 http-header-fields,再设置本次请求的 headers;Cookie 作为本次请求的 Cookie header 发送,未启用 mpv 持久化 cookie 文件。请求可设置 user-agent、referrer、network-timeout、语言偏好和轨道选择。
网络诊断只报告实际可取得的 mpv 属性:协议、host、networkState、cache duration/bytes、demuxer、container、当前 bitrate,以及从实际日志识别到的 HTTP status/redirect count。没有对应属性或日志时返回 0/空值,不做推断。 网络诊断只报告实际可取得的 mpv 属性:协议、host、networkState、cache duration/bytes、demuxer、container、当前 bitrate,以及从实际日志识别到的 HTTP status/redirect count;`rangeSupported` 仅在观察到 206 响应时为 1,`seekResult` 记录最近一次 seek 的 `ok` 或脱敏错误。没有对应属性或日志时返回 -1/0/空值,不做推断。
HTTP MP4 已在 Playback Lab 模拟器通过宿主受控 Range server 验证:请求日志出现 `bytes=0-`、后续大范围 byte range,播放结束且无核心错误。跨 origin 重定向的敏感头过滤由 FFmpeg-Harmony 固定分支提供,实际 HTTPS/HLS/鉴权资源仍需受控网络源继续验收。
HLS Master/Variant 的当前媒体分辨率来自 video-params/width/height,码率来自 demux-bitrate;当前 MPV 没有独立稳定的“variant 名称”属性,因此不新增伪造字段。 HLS Master/Variant 的当前媒体分辨率来自 video-params/width/height,码率来自 demux-bitrate;当前 MPV 没有独立稳定的“variant 名称”属性,因此不新增伪造字段。
+2 -2
View File
@@ -6,10 +6,10 @@
## 脱敏规则 ## 脱敏规则
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 日志。 Authorization、Proxy-Authorization、Cookie、X-Api-Key、X-Emby-Token、X-Auth-Token、X-Access-Token 及 token/api-key/signature 等认证头,以及 URL 中的 token、access_token、auth、key、api_key、apikey、auth_token、signature、x-amz-signature、sig、expires 参数在错误文本和日志中替换为 [REDACTED]。原始 mpv 日志不得直接写入 OH 日志。
## TLS、重定向与凭证 ## TLS、重定向与凭证
默认保持 tls-verify=yes。mpv 网络后端负责 TLS、重定向、Range 和 HLS;本层不复制 HTTP 行为。重定向是否携带 header 由 mpv/FFmpeg 网络后端实际决定,若运行时没有可观测证据,不向上层承诺跨 host 凭证继承。Referer、User-Agent、header 仅属于当前加载请求。 默认保持 tls-verify=yes。mpv 网络后端负责 TLS、重定向、Range 和 HLS;本层不复制 HTTP 行为。固定的 FFmpeg-Harmony 分支 `f3a036c695` 已在 `libavformat/http.c` 实现跨 origin(协议/host/port 任一变化)重定向时丢弃自定义 Authorization/Cookie/Token/API-key/Referer 等敏感头及 cookie 状态,同源重定向仍保留请求头;因此不会把凭证无条件转发到不同 host。Referer、User-Agent、header 仅属于当前加载请求。
实现依据:mpv 官方手册的 user-agent、referrer、cookies、http-header-fields、network-timeout、HLS 与 sub-add 选项说明。 实现依据:mpv 官方手册的 user-agent、referrer、cookies、http-header-fields、network-timeout、HLS 与 sub-add 选项说明。
+2 -2
View File
@@ -1,6 +1,6 @@
# Phase 5 状态 # Phase 5 状态
状态:CODE COMPLETE / emulator-network validation pending。 状态:CODE COMPLETE / HTTP Range emulator verified / device and remaining network validation pending。
已完成: 已完成:
@@ -10,7 +10,7 @@
- Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。 - Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。
- 文档、测试矩阵和构建矩阵已补齐。 - 文档、测试矩阵和构建矩阵已补齐。
- default/software/FFmpeg-Harmony 三套 AArch64 libmpv 构建、稳定 API 符号校验与 Playback Lab Native/ArkTS/PackageHap 编译已通过;使用 DevEco 自带 JBR 的 `--no-daemon` 流程生成 unsigned HAP,并由 devecocli 安装模拟器。 - 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/网络字幕/鉴权资源仍需真实网络源。 - 模拟器已启动 Playback Lab,完成本地 H.264/AAC MP4 首帧、软件解码、播放结束状态和渲染指标 smoke;通过宿主受控 HTTP Range server 完成 HTTP MP4 播放与 Range 请求验证;HTTPS/HLS/网络字幕/鉴权资源仍需真实网络源。
- 当前构建日志显示 iconv feature disabled,因此字幕适配器只承诺 UTF-8;GBK/Big5/Shift-JIS 会返回稳定 SUBTITLE 错误。 - 当前构建日志显示 iconv feature disabled,因此字幕适配器只承诺 UTF-8;GBK/Big5/Shift-JIS 会返回稳定 SUBTITLE 错误。
尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。 尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。
+3 -3
View File
@@ -5,7 +5,7 @@
| 本地 MP4 H264/AAC | Playback Lab | 模拟器安装启动并完成首帧/播放/结束 smoke;网络与实体机待确认 | | 本地 MP4 H264/AAC | Playback Lab | 模拟器安装启动并完成首帧/播放/结束 smoke;网络与实体机待确认 |
| 本地 MKV H264/AAC/SRT | Playback Lab | API/UI 编译覆盖;设备运行待确认 | | 本地 MKV H264/AAC/SRT | Playback Lab | API/UI 编译覆盖;设备运行待确认 |
| 本地 MKV HEVC/AAC/ASS(software) | software mpv | AArch64 构建通过;设备运行待确认 | | 本地 MKV HEVC/AAC/ASS(software) | software mpv | AArch64 构建通过;设备运行待确认 |
| HTTP MP4 | 受控测试源 | 网络环境待确认 | | HTTP MP4 | 宿主受控 Range server + Playback Lab | 模拟器通过;观察到 `bytes=0-`、后续 byte range,请求完成且 state=ENDED;`rangeSupported=1` |
| HTTPS MP4 | 受控测试源 | TLS 校验代码就绪;网络环境待确认 | | HTTPS MP4 | 受控测试源 | TLS 校验代码就绪;网络环境待确认 |
| HLS Master | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 | | HLS Master | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 |
| HLS Variant/chunked | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 | | HLS Variant/chunked | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 |
@@ -18,8 +18,8 @@
| Authorization header | 受控测试源 | 脱敏与隔离代码覆盖;服务端待确认 | | Authorization header | 受控测试源 | 脱敏与隔离代码覆盖;服务端待确认 |
| Cookie header | 受控测试源 | 单会话 Cookie header;服务端待确认 | | Cookie header | 受控测试源 | 单会话 Cookie header;服务端待确认 |
| Referer/User-Agent | 受控测试源 | 请求字段与 header 均支持;服务端待确认 | | Referer/User-Agent | 受控测试源 | 请求字段与 header 均支持;服务端待确认 |
| Range seek / 大 seek | 受控测试源 | exact/keyframe/percent 命令已接入;网络环境待确认 | | Range seek / 大 seek | 宿主受控 Range server + Playback Lab | 模拟器通过 HTTP Range 请求;最近 seek 结果由 `seekResult` 暴露;HTTPS 大 seek 待继续验收 |
| Redirect | 受控测试源 | 复用 mpv/FFmpeg;header 跨 host 不作承诺 | | Redirect | FFmpeg-Harmony `f3a036c695` + 受控测试源 | 跨 origin 丢弃 Authorization/Cookie/Token/API-key/Referer;同源保留;受控重定向运行验证待补 |
| 403 / 404 / 500 | 受控测试源 | 稳定 HTTP 错误码已接入;网络环境待确认 | | 403 / 404 / 500 | 受控测试源 | 稳定 HTTP 错误码已接入;网络环境待确认 |
| Broken media / reset / timeout / TLS | 受控测试源 | DEMUX/NETWORK/TLS 分类已接入;网络环境待确认 | | Broken media / reset / timeout / TLS | 受控测试源 | DEMUX/NETWORK/TLS 分类已接入;网络环境待确认 |
| 默认 FFmpeg(OHCodec enabled) | scripts/build-mpv-ohos.sh | AArch64 ELF 与符号校验通过 | | 默认 FFmpeg(OHCodec enabled) | scripts/build-mpv-ohos.sh | AArch64 ELF 与符号校验通过 |
+2
View File
@@ -9,3 +9,5 @@ mp_harmony_playback.h 是上层唯一稳定契约,提供 PlaybackRequest、Pla
Playback Lab 的 C++ 外壳只做 NAPI/渲染适配,所有请求、控制和状态快照均委托上述 C API;渲染器使用私有句柄桥接,不进入公共 ABI。 Playback Lab 的 C++ 外壳只做 NAPI/渲染适配,所有请求、控制和状态快照均委托上述 C API;渲染器使用私有句柄桥接,不进入公共 ABI。
当前 OHOS 构建未启用 iconv,外挂字幕 encoding 只接受 UTF-8/UTF8;其他编码通过稳定 SUBTITLE 错误拒绝。 当前 OHOS 构建未启用 iconv,外挂字幕 encoding 只接受 UTF-8/UTF8;其他编码通过稳定 SUBTITLE 错误拒绝。
起播 `start_position_ms` 在 `MPV_EVENT_FILE_LOADED` 后执行 exact seek,避免异步 load 竞态;`title` 映射到 `force-media-title`。MediaInfo 同时保留历史 `width`/`height`/`color_space` 字段,并提供 `video_width`/`video_height`/`color_matrix` 别名。NetworkInfo 的 `range_supported` 为 -1/0/1(未知/未观察到 206/观察到 206),`seek_result` 为最近一次 seek 的 `ok` 或脱敏错误。
+7
View File
@@ -143,6 +143,10 @@ typedef struct mp_harmony_media_info {
int64_t audio_bitrate; int64_t audio_bitrate;
int track_count; int track_count;
int subtitle_count; int subtitle_count;
/* Stable field names for bindings; width/height/color_space remain for ABI compatibility. */
int video_width;
int video_height;
char color_matrix[MP_HARMONY_TEXT];
} mp_harmony_media_info; } mp_harmony_media_info;
typedef struct mp_harmony_network_info { typedef struct mp_harmony_network_info {
@@ -157,6 +161,9 @@ typedef struct mp_harmony_network_info {
char demuxer[MP_HARMONY_TEXT]; char demuxer[MP_HARMONY_TEXT];
char container[MP_HARMONY_TEXT]; char container[MP_HARMONY_TEXT];
int64_t current_bitrate; int64_t current_bitrate;
/* -1 unknown, 0 no observed 206 response, 1 observed byte-range support. */
int range_supported;
char seek_result[MP_HARMONY_TEXT];
} mp_harmony_network_info; } mp_harmony_network_info;
typedef struct mp_harmony_playback_metrics { typedef struct mp_harmony_playback_metrics {
+6
View File
@@ -32,3 +32,9 @@
结论: Phase 5 收尾提交 `f9d1481fb8` 已推送并在 `origin/harmony-port` 远端核对一致,工作区干净。 结论: Phase 5 收尾提交 `f9d1481fb8` 已推送并在 `origin/harmony-port` 远端核对一致,工作区干净。
文件变更: memory/2026-08-29.md 文件变更: memory/2026-08-29.md
标签: #phase5 #remote #harmony-port #complete 标签: #phase5 #remote #harmony-port #complete
[PROJECT:Velix-MPV-Core] Phase 5 网络安全与诊断补齐
结论: 补齐 title/起播 exact seek/header 校验、媒体字段别名、Range/seek 诊断与顶部错误面板;模拟器通过宿主受控 HTTP MP4 Range 测试;FFmpeg-Harmony 跨 origin 凭证丢弃提交 `f3a036c695` 已推送,HTTPS/HLS/鉴权/重定向运行验证和实体机仍待补。
文件变更: player/harmony_playback.c, include/mpv/harmony_playback.h, playback_lab/entry/src/main/cpp/*, playback_lab/entry/src/main/ets/pages/Index.ets, DOCS/*, memory/*
教训: 当前 ArkUI SDK 下 Scroll 包裹既有 Column 会裁剪后续控件,保持平面 Column 并将关键错误信息置顶。
标签: #phase5 #network #range #security #emulator #ffmpeg-harmony
+1 -1
View File
@@ -1,7 +1,7 @@
# Velix-MPV-Core 记忆索引 # Velix-MPV-Core 记忆索引
- 项目:地产运维中台媒体核心的 OpenHarmony MPV 端口。 - 项目:地产运维中台媒体核心的 OpenHarmony MPV 端口。
- 当前阶段:Phase 5 CODE COMPLETE;真实 HTTP/HTTPS/HLS、网络字幕、鉴权资源和实体机验收待补。 - 当前阶段:Phase 5 CODE COMPLETE;模拟器已验证 HTTP MP4/Range,HTTPS/HLS、网络字幕、鉴权资源和实体机验收待补。
- 公共入口:`include/mpv/harmony_playback.h`,稳定 C API 不暴露 MPV/FFmpeg 内部类型。 - 公共入口:`include/mpv/harmony_playback.h`,稳定 C API 不暴露 MPV/FFmpeg 内部类型。
- Playback Lab:C++ 仅做 NAPI/渲染适配,核心请求、控制、状态统一委托 C API。 - Playback Lab:C++ 仅做 NAPI/渲染适配,核心请求、控制、状态统一委托 C API。
- 默认策略:软件解码;`harmony-ohcodec-gl` mapper 保持禁用,不声明零拷贝 GPU 能力。 - 默认策略:软件解码;`harmony-ohcodec-gl` mapper 保持禁用,不声明零拷贝 GPU 能力。
+1 -1
View File
@@ -21,6 +21,6 @@
- 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 构建入口:`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`。 - 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 首帧与结束状态可见。 - 模拟器验证:`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`。 - FFmpeg-Harmony:`../Velix-FFmpeg-Harmony`,origin `https://git.leee.xin/leeexin/Velix-FFmpeg-Harmony.git`,upstream `https://github.com/FFmpeg/FFmpeg.git`,branch `harmony-ohcodec-export`,tip `f3a036c695`;prefix `../Velix-FFmpeg-Harmony/build/ohos-arm64/prefix`。
- MPV fork 接入:`scripts/build-mpv-ohos-ffmpeg-harmony.sh`;可用 `FFMPEG_PREFIX` 与 `MPV_BUILD_ROOT` 覆盖默认路径,默认 MPV/软件脚本不变。 - MPV fork 接入:`scripts/build-mpv-ohos-ffmpeg-harmony.sh`;可用 `FFMPEG_PREFIX` 与 `MPV_BUILD_ROOT` 覆盖默认路径,默认 MPV/软件脚本不变。
- MPV fork 输出:切换脚本默认安装到 `build/ohos-arm64/prefix-mpv-ffmpeg-harmony`,不会覆盖默认 `prefix`。 - MPV fork 输出:切换脚本默认安装到 `build/ohos-arm64/prefix-mpv-ffmpeg-harmony`,不会覆盖默认 `prefix`。
+1
View File
@@ -25,3 +25,4 @@
- OHCodec capability 必须通过 `OH_AVCodec_GetCapabilityByCategory` 及 `OH_AVCapability_*` 查询;不能把 SDK 头文件存在误写成设备能力或硬件帧已产出。 - OHCodec capability 必须通过 `OH_AVCodec_GetCapabilityByCategory` 及 `OH_AVCapability_*` 查询;不能把 SDK 头文件存在误写成设备能力或硬件帧已产出。
- Gitea 反向代理对完整 FFmpeg pack 返回 413;保留完整历史的可行做法是先建立空远端,再按 first-parent checkpoint 分块推送,不能浅克隆或裁剪历史。 - Gitea 反向代理对完整 FFmpeg pack 返回 413;保留完整历史的可行做法是先建立空远端,再按 first-parent checkpoint 分块推送,不能浅克隆或裁剪历史。
- MPV 消费独立 FFmpeg prefix 时,pkg-config 必须把 fork 的 `lib/pkgconfig` 置于 base prefix 前,并保留绝对 include/lib 路径;仅改 `PREFIX` 会丢失 libass/libplacebo 等其他依赖。 - MPV 消费独立 FFmpeg prefix 时,pkg-config 必须把 fork 的 `lib/pkgconfig` 置于 base prefix 前,并保留绝对 include/lib 路径;仅改 `PREFIX` 会丢失 libass/libplacebo 等其他依赖。
- 当前 DevEco ArkUI SDK 下给既有平面 `Column` 外包 `Scroll` 会改变可见布局并裁掉后续控件;诊断页保持平面 Column,关键错误信息放在顶部,长列表改动需单独验收。
+4 -4
View File
@@ -2,13 +2,13 @@
## Velix-MPV-Core ## Velix-MPV-Core
- 状态:Phase 5 CODE COMPLETE / emulator-network validation pending;GPU INTEROP MAPPER 未进入;默认软件解码与网络播放公共 API 已完成,实体机/真实网络验收未完成。 - 状态:Phase 5 CODE COMPLETE / emulator HTTP Range verified;GPU INTEROP MAPPER 未进入;HTTPS/HLS、网络字幕、鉴权、重定向运行验证与实体机验收未完成。
- 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。 - 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。
- 产物:`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`。 - 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,343,104 bytes;软件构建位于 `build/ohos-arm64/build/mpv-software/libmpv.so.2.5.0`,FFmpeg-Harmony 构建位于 `build/ohos-arm64/build/mpv-ffmpeg-harmony/libmpv.so.2.5.0`;DT_NEEDED 含 `libohaudio.so`。
- 已完成:独立 HAP、XComponent/OHNativeWindow、EGL ES3、libmpv OpenGL Render、软件解码本地 MP4 首帧、播放控制与释放重入。 - 已完成:独立 HAP、XComponent/OHNativeWindow、EGL ES3、libmpv OpenGL Render、软件解码本地 MP4 首帧、播放控制与释放重入。
- 已完成:`ao_harmony` PCM float32→S16 fallback、采样率/mono-stereo 协商、pause/resume/stop/flush/release、timestamp epoch、starvation/flush metrics;Phase 5 HAP 已用 DevEco JBR 完成打包并安装模拟器。 - 已完成:`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 H.264/HEVC capability 查询、`hwdec=ohcodec` direct-surface 实验驱动、显式软件回退、Playback Lab requested/actual/metrics 诊断、软件/硬解/GL probe 构建矩阵;FFmpeg 未修改。
- 边界:官方 OHCodec 上游没有非 surface 的可导入硬件帧路径,EGL/NativeBuffer 仅保留禁用默认探针;无零拷贝、HDR 或硬件帧产出声明。 - 边界:官方 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。 - Phase 4.5:`../Velix-FFmpeg-Harmony` branch `harmony-ohcodec-export` 已推送 tip `f3a036c695`;跨 origin 重定向丢弃敏感自定义头/cookie,`scripts/build-mpv-ohos-ffmpeg-harmony.sh` 已消费其 prefix 构建 MPV。
- 待办:实体机 H.264/HEVC capability、surface 生命周期/stride/crop/fence、真实 GPU 互操作与 A/V sync;报告见两个仓库的 `DOCS/OHCODEC_*.md` 与 `DOCS/PHASE4_5_STATUS.md`。 - 待办:实体机 H.264/HEVC capability、surface 生命周期/stride/crop/fence、真实 GPU 互操作与 A/V sync;报告见两个仓库的 `DOCS/OHCODEC_*.md` 与 `DOCS/PHASE4_5_STATUS.md`。
- Phase 5 已完成:稳定 C API、请求/headers 会话隔离、错误/状态、轨道/媒体/网络/字幕控制、Playback Lab 诊断 UI 与文档;C++ 外壳已统一委托 C API,默认/软件/FFmpeg-Harmony 构建及 HAP 打包通过;模拟器已完成本地 MP4 首帧/播放结束 smoke,HTTP/HLS/网络字幕/鉴权运行验证待补。 - Phase 5 已完成:稳定 C API、请求/headers 会话隔离、错误/状态、轨道/媒体/网络/字幕控制、Playback Lab 诊断 UI 与文档;C++ 外壳已统一委托 C API,默认/软件/FFmpeg-Harmony 构建及 HAP 打包通过;模拟器已完成本地 MP4 与宿主受控 HTTP Range MP4 播放,HTTPS/HLS/网络字幕/鉴权/重定向运行验证待补。
+10 -2
View File
@@ -514,6 +514,10 @@ napi_value getState(napi_env env, napi_callback_info)
napi_set_named_property(env, network, "container", value); napi_set_named_property(env, network, "container", value);
napi_create_int64(env, state.network.currentBitrate, &value); napi_create_int64(env, state.network.currentBitrate, &value);
napi_set_named_property(env, network, "currentBitrate", value); napi_set_named_property(env, network, "currentBitrate", value);
napi_create_int32(env, state.network.rangeSupported, &value);
napi_set_named_property(env, network, "rangeSupported", value);
napi_create_string_utf8(env, state.network.seekResult.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, network, "seekResult", value);
napi_set_named_property(env, object, "network", network); napi_set_named_property(env, object, "network", network);
napi_value metrics = nullptr; napi_value metrics = nullptr;
napi_create_object(env, &metrics); napi_create_object(env, &metrics);
@@ -548,12 +552,16 @@ napi_value getState(napi_env env, napi_callback_info)
napi_set_named_property(env, media, "colorPrimaries", value); napi_set_named_property(env, media, "colorPrimaries", value);
napi_create_string_utf8(env, state.media.colorTransfer.c_str(), NAPI_AUTO_LENGTH, &value); napi_create_string_utf8(env, state.media.colorTransfer.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorTransfer", value); napi_set_named_property(env, media, "colorTransfer", value);
napi_create_string_utf8(env, state.media.colorSpace.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorMatrix", value);
napi_create_int32(env, state.media.width, &value); napi_create_int32(env, state.media.width, &value);
napi_set_named_property(env, media, "width", value); napi_set_named_property(env, media, "width", value);
napi_create_int32(env, state.media.height, &value); napi_create_int32(env, state.media.height, &value);
napi_set_named_property(env, media, "height", value); napi_set_named_property(env, media, "height", value);
napi_create_int32(env, state.media.videoWidth, &value);
napi_set_named_property(env, media, "videoWidth", value);
napi_create_int32(env, state.media.videoHeight, &value);
napi_set_named_property(env, media, "videoHeight", value);
napi_create_string_utf8(env, state.media.colorMatrix.c_str(), NAPI_AUTO_LENGTH, &value);
napi_set_named_property(env, media, "colorMatrix", value);
napi_create_double(env, state.media.fps, &value); napi_create_double(env, state.media.fps, &value);
napi_set_named_property(env, media, "fps", value); napi_set_named_property(env, media, "fps", value);
napi_create_int64(env, state.media.bitrate, &value); napi_create_int64(env, state.media.bitrate, &value);
@@ -517,6 +517,9 @@ void PlaybackEngine::syncCoreSnapshot()
snapshot_.media.colorSpace = core.media.color_space; snapshot_.media.colorSpace = core.media.color_space;
snapshot_.media.width = core.media.width; snapshot_.media.width = core.media.width;
snapshot_.media.height = core.media.height; snapshot_.media.height = core.media.height;
snapshot_.media.videoWidth = core.media.video_width;
snapshot_.media.videoHeight = core.media.video_height;
snapshot_.media.colorMatrix = core.media.color_matrix;
snapshot_.media.bitDepth = core.media.bit_depth; snapshot_.media.bitDepth = core.media.bit_depth;
snapshot_.media.audioSampleRate = core.media.audio_sample_rate; snapshot_.media.audioSampleRate = core.media.audio_sample_rate;
snapshot_.media.audioChannels = core.media.audio_channels; snapshot_.media.audioChannels = core.media.audio_channels;
@@ -527,6 +530,8 @@ void PlaybackEngine::syncCoreSnapshot()
snapshot_.media.audioBitrate = core.media.audio_bitrate; snapshot_.media.audioBitrate = core.media.audio_bitrate;
snapshot_.media.trackCount = core.media.track_count; snapshot_.media.trackCount = core.media.track_count;
snapshot_.media.subtitleCount = core.media.subtitle_count; snapshot_.media.subtitleCount = core.media.subtitle_count;
snapshot_.network.rangeSupported = core.network.range_supported;
snapshot_.network.seekResult = core.network.seek_result;
snapshot_.network.protocol = core.network.protocol; snapshot_.network.protocol = core.network.protocol;
snapshot_.network.host = core.network.host; snapshot_.network.host = core.network.host;
+5
View File
@@ -76,6 +76,9 @@ struct PlaybackMediaInfo {
int64_t audioBitrate = 0; int64_t audioBitrate = 0;
int trackCount = 0; int trackCount = 0;
int subtitleCount = 0; int subtitleCount = 0;
int videoWidth = 0;
int videoHeight = 0;
std::string colorMatrix;
}; };
struct PlaybackNetworkInfo { struct PlaybackNetworkInfo {
@@ -90,6 +93,8 @@ struct PlaybackNetworkInfo {
std::string demuxer; std::string demuxer;
std::string container; std::string container;
int64_t currentBitrate = 0; int64_t currentBitrate = 0;
int rangeSupported = -1;
std::string seekResult;
}; };
struct PlaybackSnapshot { struct PlaybackSnapshot {
@@ -80,6 +80,8 @@ interface NetworkState {
demuxer: string; demuxer: string;
container: string; container: string;
currentBitrate: number; currentBitrate: number;
rangeSupported: number;
seekResult: string;
} }
interface MediaState { interface MediaState {
@@ -94,6 +96,8 @@ interface MediaState {
colorMatrix: string; colorMatrix: string;
width: number; width: number;
height: number; height: number;
videoWidth: number;
videoHeight: number;
fps: number; fps: number;
bitrate: number; bitrate: number;
bitDepth: number; bitDepth: number;
@@ -187,9 +191,10 @@ struct Index {
speed: 1, volume: 100, muted: false, speed: 1, volume: 100, muted: false,
subtitleDelayMs: 0, subtitleScale: 1, subtitlePosition: 100, subtitleDelayMs: 0, subtitleScale: 1, subtitlePosition: 100,
network: { protocol: '', host: '', networkState: '', httpStatus: 0, redirectCount: 0, network: { protocol: '', host: '', networkState: '', httpStatus: 0, redirectCount: 0,
buffering: false, cacheDuration: 0, cacheBytes: 0, demuxer: '', container: '', currentBitrate: 0 }, buffering: false, cacheDuration: 0, cacheBytes: 0, demuxer: '', container: '', currentBitrate: 0,
rangeSupported: -1, seekResult: '' },
media: { container: '', demuxer: '', duration: 0, videoCodec: '', audioCodec: '', pixelFormat: '', colorPrimaries: '', colorTransfer: '', colorMatrix: '', 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, audioChannelLayout: '', width: 0, height: 0, videoWidth: 0, videoHeight: 0, fps: 0, bitrate: 0, bitDepth: 0, audioSampleRate: 0, audioChannels: 0, audioChannelLayout: '',
trackCount: 0, subtitleCount: 0 }, trackCount: 0, subtitleCount: 0 },
tracks: [] tracks: []
}; };
@@ -216,6 +221,8 @@ struct Index {
Text(`state: ${this.status}`).fontSize(16).fontColor($r('app.color.primary')).width('100%') Text(`state: ${this.status}`).fontSize(16).fontColor($r('app.color.primary')).width('100%')
Text(`surface=${this.state.surface} egl=${this.state.egl} mpv=${this.state.mpv}`) Text(`surface=${this.state.surface} egl=${this.state.egl} mpv=${this.state.mpv}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%') .fontSize(13).fontColor($r('app.color.muted')).width('100%')
Text(`error=${this.state.error.code} rawMessage=${this.state.error.rawMessage || 'none'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
XComponent({ id: 'playback_surface', type: XComponentType.SURFACE, libraryname: 'entry' }) XComponent({ id: 'playback_surface', type: XComponentType.SURFACE, libraryname: 'entry' })
.width('100%').height(250).backgroundColor(Color.Black) .width('100%').height(250).backgroundColor(Color.Black)
TextInput({ text: this.url, placeholder: 'file:///... or https://...' }) TextInput({ text: this.url, placeholder: 'file:///... or https://...' })
@@ -308,8 +315,12 @@ struct Index {
Text(`fallback=${this.state.hwdecFallbackReason || 'none'} capability=${this.state.hwCapability || 'DEVICE TEST REQUIRED'}`) Text(`fallback=${this.state.hwdecFallbackReason || 'none'} capability=${this.state.hwCapability || 'DEVICE TEST REQUIRED'}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3) .fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2) Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text(`metrics dropped=${this.state.metrics.droppedFrames} renders=${this.state.metrics.renderCount} cache=${this.state.metrics.cacheDuration.toFixed(2)}s/${this.state.metrics.cacheBytes}B bitrate=${this.state.metrics.currentBitrate}`)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('network=' + this.state.network.protocol + '://' + this.state.network.host + ' status=' + (this.state.network.httpStatus || 'unavailable') + ' redirects=' + this.state.network.redirectCount + ' buffering=' + this.state.network.buffering) Text('network=' + this.state.network.protocol + '://' + this.state.network.host + ' status=' + (this.state.network.httpStatus || 'unavailable') + ' redirects=' + this.state.network.redirectCount + ' buffering=' + this.state.network.buffering)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2) .fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('range=' + this.state.network.rangeSupported + ' seek=' + (this.state.network.seekResult || 'unavailable'))
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('media=' + this.state.media.container + '/' + this.state.media.videoCodec + '/' + this.state.media.audioCodec + ' ' + this.state.media.width + 'x' + this.state.media.height + ' bitrate=' + this.state.media.bitrate + ' tracks=' + this.state.media.trackCount + ' subs=' + this.state.media.subtitleCount) Text('media=' + this.state.media.container + '/' + this.state.media.videoCodec + '/' + this.state.media.audioCodec + ' ' + this.state.media.width + 'x' + this.state.media.height + ' bitrate=' + this.state.media.bitrate + ' tracks=' + this.state.media.trackCount + ' subs=' + this.state.media.subtitleCount)
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2) .fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
Text('subtitle delay=' + this.state.subtitleDelayMs + 'ms scale=' + this.state.subtitleScale + ' position=' + this.state.subtitlePosition) Text('subtitle delay=' + this.state.subtitleDelayMs + 'ms scale=' + this.state.subtitleScale + ' position=' + this.state.subtitlePosition)
+112 -31
View File
@@ -27,6 +27,7 @@ struct mp_harmony_player {
char current_url[MP_HARMONY_URL]; char current_url[MP_HARMONY_URL];
int64_t playlist_entry_id; int64_t playlist_entry_id;
int awaiting_start; int awaiting_start;
int64_t pending_start_position_ms;
mp_harmony_playback_snapshot snapshot; mp_harmony_playback_snapshot snapshot;
mp_harmony_track tracks[MP_HARMONY_MAX_TRACKS]; mp_harmony_track tracks[MP_HARMONY_MAX_TRACKS];
size_t track_count; size_t track_count;
@@ -39,6 +40,7 @@ enum {
}; };
static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *request); static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *request);
static int add_subtitle_locked(mp_harmony_player *p, const mp_harmony_subtitle_source *source);
/* Private renderer bridge; intentionally absent from the public header. */ /* Private renderer bridge; intentionally absent from the public header. */
MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player); MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player);
@@ -82,6 +84,22 @@ static int is_cookie_header(const char *name)
return name && strcasecmp(name, "cookie") == 0; return name && strcasecmp(name, "cookie") == 0;
} }
static int valid_header_name(const char *value)
{
if (!value || !value[0]) return 0;
for (const unsigned char *p = (const unsigned char *)value; *p; p++)
if (*p <= 0x20 || *p >= 0x7f || *p == ':') return 0;
return 1;
}
static int valid_header_value(const char *value)
{
if (!value) return 1;
for (const unsigned char *p = (const unsigned char *)value; *p; p++)
if (*p == '\r' || *p == '\n' || (*p < 0x20 && *p != '\t')) return 0;
return 1;
}
static void redact_text(char *dst, size_t size, const char *src) static void redact_text(char *dst, size_t size, const char *src)
{ {
size_t out = 0; size_t out = 0;
@@ -92,11 +110,18 @@ static void redact_text(char *dst, size_t size, const char *src)
else if (strncasecmp(p, "cookie:", 7) == 0) key_len = 7; else if (strncasecmp(p, "cookie:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "proxy-authorization:", 21) == 0) key_len = 21; else if (strncasecmp(p, "proxy-authorization:", 21) == 0) key_len = 21;
else if (strncasecmp(p, "x-api-key:", 10) == 0) key_len = 10; else if (strncasecmp(p, "x-api-key:", 10) == 0) key_len = 10;
else if (strncasecmp(p, "x-emby-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "x-auth-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "x-access-token:", 15) == 0) key_len = 15;
else if (strncasecmp(p, "x-api-token:", 12) == 0) key_len = 12;
else if (strncasecmp(p, "access-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "auth-token:", 11) == 0) key_len = 11;
else if (strncasecmp(p, "token:", 6) == 0) key_len = 6; else if (strncasecmp(p, "token:", 6) == 0) key_len = 6;
else if (strncasecmp(p, "api-key:", 8) == 0) key_len = 8; else if (strncasecmp(p, "api-key:", 8) == 0) key_len = 8;
else if (strncasecmp(p, "apikey:", 7) == 0) key_len = 7; else if (strncasecmp(p, "apikey:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "secret:", 7) == 0) key_len = 7; else if (strncasecmp(p, "secret:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "signature:", 10) == 0) key_len = 10; else if (strncasecmp(p, "signature:", 10) == 0) key_len = 10;
else if (strncasecmp(p, "sig:", 4) == 0) key_len = 4;
if (key_len) { if (key_len) {
size_t n = key_len; size_t n = key_len;
while (p[n] && p[n] != '\n' && p[n] != '\r') n++; while (p[n] && p[n] != '\n' && p[n] != '\r') n++;
@@ -112,7 +137,9 @@ static void redact_text(char *dst, size_t size, const char *src)
strncasecmp(q, "auth=", 5) == 0 || strncasecmp(q, "key=", 4) == 0 || strncasecmp(q, "auth=", 5) == 0 || strncasecmp(q, "key=", 4) == 0 ||
strncasecmp(q, "api_key=", 8) == 0 || strncasecmp(q, "apikey=", 7) == 0 || strncasecmp(q, "api_key=", 8) == 0 || strncasecmp(q, "apikey=", 7) == 0 ||
strncasecmp(q, "signature=", 10) == 0 || strncasecmp(q, "x-amz-signature=", 16) == 0 || strncasecmp(q, "signature=", 10) == 0 || strncasecmp(q, "x-amz-signature=", 16) == 0 ||
strncasecmp(q, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0) { strncasecmp(q, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0 ||
strncasecmp(q, "auth_token=", 11) == 0 || strncasecmp(q, "access-token=", 13) == 0 ||
strncasecmp(q, "api-key=", 8) == 0 || strncasecmp(q, "x-emby-token=", 13) == 0) {
dst[out++] = *p++; dst[out++] = *p++;
while (*p && *p != '&' && *p != ' ' && *p != '\n' && out + 1 < size) p++; while (*p && *p != '&' && *p != ' ' && *p != '\n' && out + 1 < size) p++;
if (out + 10 >= size) break; if (out + 10 >= size) break;
@@ -161,6 +188,7 @@ static mp_harmony_error_code classify_error(int code, const char *message)
if (text_has(message, "timeout") || text_has(message, "connection") || if (text_has(message, "timeout") || text_has(message, "connection") ||
text_has(message, "network") || text_has(message, "dns")) text_has(message, "network") || text_has(message, "dns"))
return MP_HARMONY_ERROR_NETWORK; return MP_HARMONY_ERROR_NETWORK;
if (code == MPV_ERROR_LOADING_FAILED) return MP_HARMONY_ERROR_NETWORK;
return MP_HARMONY_ERROR_UNKNOWN; return MP_HARMONY_ERROR_UNKNOWN;
} }
@@ -299,8 +327,14 @@ static void refresh_media_locked(mp_harmony_player *p)
v = mpv_get_property_string(p->handle, "video-format"); v = mpv_get_property_string(p->handle, "video-format");
if (v && v[0]) 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; 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, "width", MPV_FORMAT_INT64, &i64) >= 0) {
if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.height = (int)i64; p->snapshot.media.width = (int)i64;
p->snapshot.media.video_width = (int)i64;
}
if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) {
p->snapshot.media.height = (int)i64;
p->snapshot.media.video_height = (int)i64;
}
if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.video_bitrate = i64; if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.video_bitrate = i64;
if (mpv_get_property(p->handle, "audio-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.audio_bitrate = i64; if (mpv_get_property(p->handle, "audio-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.audio_bitrate = i64;
if (mpv_get_property(p->handle, "demux-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.bitrate = i64; if (mpv_get_property(p->handle, "demux-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.bitrate = i64;
@@ -319,7 +353,10 @@ static void refresh_media_locked(mp_harmony_player *p)
if (strcmp(key, "pixelformat") == 0) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), node_string(value)); if (strcmp(key, "pixelformat") == 0) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), node_string(value));
else if (strcmp(key, "primaries") == 0) copy_text(p->snapshot.media.color_primaries, sizeof(p->snapshot.media.color_primaries), node_string(value)); else if (strcmp(key, "primaries") == 0) copy_text(p->snapshot.media.color_primaries, sizeof(p->snapshot.media.color_primaries), node_string(value));
else if (strcmp(key, "gamma") == 0) copy_text(p->snapshot.media.color_transfer, sizeof(p->snapshot.media.color_transfer), node_string(value)); else if (strcmp(key, "gamma") == 0) copy_text(p->snapshot.media.color_transfer, sizeof(p->snapshot.media.color_transfer), node_string(value));
else if (strcmp(key, "colormatrix") == 0) copy_text(p->snapshot.media.color_space, sizeof(p->snapshot.media.color_space), node_string(value)); else if (strcmp(key, "colormatrix") == 0) {
copy_text(p->snapshot.media.color_space, sizeof(p->snapshot.media.color_space), node_string(value));
copy_text(p->snapshot.media.color_matrix, sizeof(p->snapshot.media.color_matrix), node_string(value));
}
else if (strcmp(key, "bit-depth") == 0) p->snapshot.media.bit_depth = node_int(value); else if (strcmp(key, "bit-depth") == 0) p->snapshot.media.bit_depth = node_int(value);
} }
} }
@@ -376,10 +413,12 @@ static void handle_log_locked(mp_harmony_player *p, const mpv_event_log_message
{ {
if (!log || !log->text) return; if (!log || !log->text) return;
char redacted[MP_HARMONY_TEXT]; redact_text(redacted, sizeof(redacted), log->text); char redacted[MP_HARMONY_TEXT]; redact_text(redacted, sizeof(redacted), log->text);
if (strstr(log->text, "HTTP/1.") || strstr(log->text, "http error")) { if (strstr(log->text, "HTTP/1.") || text_has(log->text, "http error")) {
int status = 0; int status = 0;
if (sscanf(log->text, "%*[^0-9]%d", &status) == 1 && status >= 100 && status <= 599) if (sscanf(log->text, "%*[^0-9]%d", &status) == 1 && status >= 100 && status <= 599) {
p->snapshot.network.http_status = status; p->snapshot.network.http_status = status;
if (status == 206) p->snapshot.network.range_supported = 1;
}
} }
if (text_has(log->text, "redirect")) p->snapshot.network.redirect_count++; if (text_has(log->text, "redirect")) p->snapshot.network.redirect_count++;
if (log->prefix && (strcmp(log->prefix, "cplayer") == 0 || strcmp(log->prefix, "ffmpeg") == 0) && if (log->prefix && (strcmp(log->prefix, "cplayer") == 0 || strcmp(log->prefix, "ffmpeg") == 0) &&
@@ -415,7 +454,22 @@ static void *event_loop(void *arg)
strcmp(loaded_url, p->current_url) == 0; strcmp(loaded_url, p->current_url) == 0;
if (loaded_url) mpv_free(loaded_url); if (loaded_url) mpv_free(loaded_url);
if (!matches) break; if (!matches) break;
p->awaiting_start = 0; refresh_media_locked(p); p->snapshot.state = MP_HARMONY_READY; p->awaiting_start = 0;
refresh_media_locked(p);
if (p->pending_start_position_ms >= 0) {
char position[64];
snprintf(position, sizeof(position), "%.3f", (double)p->pending_start_position_ms / 1000.0);
const char *seek[] = {"seek", position, "absolute", "exact", NULL};
int rc = mpv_command(p->handle, seek);
p->pending_start_position_ms = -1;
if (rc < 0) {
p->snapshot.network.seek_result[0] = '\0';
set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed");
} else {
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), "ok");
}
}
if (p->snapshot.state != MP_HARMONY_ERROR) p->snapshot.state = MP_HARMONY_READY;
break; break;
} }
case MPV_EVENT_PROPERTY_CHANGE: case MPV_EVENT_PROPERTY_CHANGE:
@@ -430,7 +484,10 @@ static void *event_loop(void *arg)
end->playlist_entry_id != p->playlist_entry_id) break; end->playlist_entry_id != p->playlist_entry_id) break;
if (end && end->reason == MPV_END_FILE_REASON_ERROR) { if (end && end->reason == MPV_END_FILE_REASON_ERROR) {
const char *s = mpv_error_string(end->error); const char *s = mpv_error_string(end->error);
set_error_locked(p, classify_error(end->error, s), s); mp_harmony_error_code error = classify_error(end->error, s);
if (p->snapshot.network.http_status >= 400 && p->snapshot.network.http_status <= 599)
error = MP_HARMONY_ERROR_HTTP;
set_error_locked(p, error, s);
} else if (end && end->reason == MPV_END_FILE_REASON_EOF) p->snapshot.state = MP_HARMONY_ENDED; } else if (end && end->reason == MPV_END_FILE_REASON_EOF) p->snapshot.state = MP_HARMONY_ENDED;
else p->snapshot.state = MP_HARMONY_STOPPED; else p->snapshot.state = MP_HARMONY_STOPPED;
break; break;
@@ -471,7 +528,7 @@ int mp_harmony_playback_initialize(mp_harmony_player *p)
const char *options[][2] = { const char *options[][2] = {
{"vo", "libmpv"}, {"terminal", "no"}, {"config", "no"}, {"hwdec", "no"}, {"vo", "libmpv"}, {"terminal", "no"}, {"config", "no"}, {"hwdec", "no"},
{"aid", "auto"}, {"cache", "auto"}, {"cache-pause", "yes"}, {"cookies", "no"}, {"aid", "auto"}, {"cache", "auto"}, {"cache-pause", "yes"}, {"cookies", "no"},
{"tls-verify", "yes"}, {NULL, NULL}, {"tls-verify", "yes"}, {"sub-fonts-dir", "/system/fonts"}, {NULL, NULL},
}; };
for (size_t i = 0; options[i][0]; i++) { for (size_t i = 0; options[i][0]; i++) {
if (mpv_set_option_string(p->handle, options[i][0], options[i][1]) < 0) { if (mpv_set_option_string(p->handle, options[i][0], options[i][1]) < 0) {
@@ -511,9 +568,13 @@ int mp_harmony_playback_initialize(mp_harmony_player *p)
static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *r) static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *r)
{ {
if (!r || (r->http_header_count && !r->http_headers)) return -1; if (!r || (r->http_header_count && !r->http_headers)) return -1;
if (!valid_header_value(r->cookies)) return -1;
size_t count = 0; size_t count = 0;
for (size_t i = 0; i < r->http_header_count; i++) { for (size_t i = 0; i < r->http_header_count; i++) {
const char *name = r->http_headers[i].name; const char *name = r->http_headers[i].name;
const char *value = r->http_headers[i].value;
if (name && name[0] && !valid_header_name(name)) return -1;
if (!valid_header_value(value)) return -1;
if (name && name[0] && !(r->cookies && r->cookies[0] && is_cookie_header(name))) count++; if (name && name[0] && !(r->cookies && r->cookies[0] && is_cookie_header(name))) count++;
} }
if (r->cookies && r->cookies[0]) count++; if (r->cookies && r->cookies[0]) count++;
@@ -575,8 +636,10 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req
p->snapshot.network = (mp_harmony_network_info){0}; p->snapshot.network = (mp_harmony_network_info){0};
p->snapshot.metrics = (mp_harmony_playback_metrics){0}; p->snapshot.metrics = (mp_harmony_playback_metrics){0};
p->snapshot.track_count = 0; p->track_count = 0; p->awaiting_start = 1; p->snapshot.track_count = 0; p->track_count = 0; p->awaiting_start = 1;
p->pending_start_position_ms = r->start_position_ms >= 0 ? r->start_position_ms : -1;
copy_text(p->current_url, sizeof(p->current_url), r->url); copy_text(p->current_url, sizeof(p->current_url), r->url);
p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0; p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0;
p->snapshot.network.range_supported = -1; p->snapshot.network.seek_result[0] = '\0';
if (r->subtitle_file_count && !r->subtitle_files) { if (r->subtitle_file_count && !r->subtitle_files) {
set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "subtitle list is null"); set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "subtitle list is null");
pthread_mutex_unlock(&p->lock); return -1; pthread_mutex_unlock(&p->lock); return -1;
@@ -592,8 +655,14 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "http-header-fields failed"); set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "http-header-fields failed");
pthread_mutex_unlock(&p->lock); return -1; pthread_mutex_unlock(&p->lock); return -1;
} }
if (!valid_header_value(r->user_agent) || !valid_header_value(r->referer) ||
!valid_header_value(r->title) || !valid_header_value(r->url)) {
set_error_locked(p, MP_HARMONY_ERROR_HTTP, "invalid header or URL text");
pthread_mutex_unlock(&p->lock); return -1;
}
if (set_property_string(p, "user-agent", r->user_agent ? r->user_agent : "") < 0 || if (set_property_string(p, "user-agent", r->user_agent ? r->user_agent : "") < 0 ||
set_property_string(p, "referrer", r->referer ? r->referer : "") < 0 || set_property_string(p, "referrer", r->referer ? r->referer : "") < 0 ||
set_property_string(p, "force-media-title", r->title ? r->title : "") < 0 ||
set_property_string(p, "alang", r->preferred_audio_language ? r->preferred_audio_language : "") < 0 || set_property_string(p, "alang", r->preferred_audio_language ? r->preferred_audio_language : "") < 0 ||
set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : "") < 0) { set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : "") < 0) {
pthread_mutex_unlock(&p->lock); return -1; pthread_mutex_unlock(&p->lock); return -1;
@@ -606,26 +675,23 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req
const char *args[] = {"loadfile", r->url, "replace", NULL}; const char *args[] = {"loadfile", r->url, "replace", NULL};
int rc = mpv_command(p->handle, args); 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)); if (rc < 0) set_error_locked(p, classify_error(rc, mpv_error_string(rc)), mpv_error_string(rc));
if (rc >= 0 && r->start_position_ms >= 0) { if (rc >= 0) {
char position[64];
snprintf(position, sizeof(position), "%.3f", (double)r->start_position_ms / 1000.0);
const char *seek[] = {"seek", position, "absolute", "exact", NULL};
if (mpv_command(p->handle, seek) < 0)
set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed");
}
pthread_mutex_unlock(&p->lock);
if (rc < 0) return rc;
for (size_t i = 0; i < r->subtitle_file_count; i++) { for (size_t i = 0; i < r->subtitle_file_count; i++) {
if (r->subtitle_files[i].url && r->subtitle_files[i].url[0] && 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; add_subtitle_locked(p, &r->subtitle_files[i]) < 0) {
rc = -1;
break;
} }
return 0; }
}
pthread_mutex_unlock(&p->lock);
return rc;
} }
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; } 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; }
int mp_harmony_playback_pause(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", "yes"); if (r >= 0) p->snapshot.state = MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_pause(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", "yes"); if (r >= 0) p->snapshot.state = MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_toggle_pause(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } int paused = 0; mpv_get_property(p->handle, "pause", MPV_FORMAT_FLAG, &paused); int r = set_property_string(p, "pause", paused ? "no" : "yes"); if (r >= 0) p->snapshot.state = paused ? MP_HARMONY_PLAYING : MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_toggle_pause(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } int paused = 0; mpv_get_property(p->handle, "pause", MPV_FORMAT_FLAG, &paused); int r = set_property_string(p, "pause", paused ? "no" : "yes"); if (r >= 0) p->snapshot.state = paused ? MP_HARMONY_PLAYING : MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_stop(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = -1; if (ensure_ready_locked(p) == 0) { r = mpv_command_string(p->handle, "stop"); if (r >= 0) { mp_harmony_playback_request empty = {0}; set_headers_locked(p, &empty); set_property_string(p, "user-agent", ""); set_property_string(p, "referrer", ""); p->snapshot.state = MP_HARMONY_STOPPED; } } pthread_mutex_unlock(&p->lock); return r; } int mp_harmony_playback_stop(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = -1; if (ensure_ready_locked(p) == 0) { r = mpv_command_string(p->handle, "stop"); if (r >= 0) { mp_harmony_playback_request empty = {0}; set_headers_locked(p, &empty); set_property_string(p, "user-agent", ""); set_property_string(p, "referrer", ""); set_property_string(p, "force-media-title", ""); p->pending_start_position_ms = -1; p->snapshot.state = MP_HARMONY_STOPPED; } } pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe) int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe)
{ {
@@ -637,8 +703,15 @@ int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, do
const char *flags = exact ? "exact" : (keyframe ? "keyframes" : NULL); const char *flags = exact ? "exact" : (keyframe ? "keyframes" : NULL);
const char *args[] = {"seek", amount, kind, flags, NULL}; const char *args[] = {"seek", amount, kind, flags, NULL};
int r = mpv_command(p->handle, args); int r = mpv_command(p->handle, args);
if (r >= 0) p->snapshot.state = MP_HARMONY_SEEKING; if (r >= 0) {
else set_error_locked(p, MP_HARMONY_ERROR_SEEK, mpv_error_string(r)); p->snapshot.state = MP_HARMONY_SEEKING;
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), "ok");
} else {
char redacted[MP_HARMONY_TEXT];
redact_text(redacted, sizeof(redacted), mpv_error_string(r));
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), redacted);
set_error_locked(p, MP_HARMONY_ERROR_SEEK, redacted);
}
pthread_mutex_unlock(&p->lock); return r; pthread_mutex_unlock(&p->lock); return r;
} }
@@ -649,17 +722,16 @@ int mp_harmony_playback_set_audio_enabled(mp_harmony_player *p, int enabled) { i
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_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_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_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) static int add_subtitle_locked(mp_harmony_player *p, const mp_harmony_subtitle_source *s)
{ {
if (!p || !s || !s->url || !s->url[0]) return -1; if (!s || !s->url || !s->url[0]) return -1;
pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) return -1;
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
if (!supported_subtitle_encoding(s->encoding)) { if (!supported_subtitle_encoding(s->encoding)) {
set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding"); set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding");
pthread_mutex_unlock(&p->lock); return -1; return -1;
} }
if (s->encoding && s->encoding[0] && set_property_string(p, "sub-codepage", s->encoding) < 0) { if (s->encoding && s->encoding[0] && set_property_string(p, "sub-codepage", s->encoding) < 0) {
pthread_mutex_unlock(&p->lock); return -1; return -1;
} }
char flags[64]; char flags[64];
snprintf(flags, sizeof(flags), "%s%s%s", s->select ? "select" : "auto", snprintf(flags, sizeof(flags), "%s%s%s", s->select ? "select" : "auto",
@@ -668,9 +740,18 @@ int mp_harmony_playback_add_subtitle(mp_harmony_player *p, const mp_harmony_subt
s->language ? s->language : "", NULL}; s->language ? s->language : "", NULL};
int r = mpv_command(p->handle, args); int r = mpv_command(p->handle, args);
if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r));
pthread_mutex_unlock(&p->lock); return r; 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_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);
int r = add_subtitle_locked(p, s);
pthread_mutex_unlock(&p->lock);
return r;
}
int mp_harmony_playback_remove_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 = -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 || !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_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_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_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; }