feat: unify phase5 playback lab with stable core api

This commit is contained in:
BoatingCloud committed 2026-08-29 15:42:26 +08:00
1 parent 0db086a1e9
commit f9d1481fb8
16 files changed
+590 -846

No files matched your search

+2
View File
@@ -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 错误不伪装成网络错误。
+1 -1
View File
@@ -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、重定向与凭证
+3 -1
View File
@@ -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 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。
+2 -2
View File
@@ -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 事件/属性可改变对应诊断字段。
+4
View File
@@ -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 错误拒绝。
+2 -2
View File
@@ -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 路径入口。
+12
View File
@@ -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
+11
View File
@@ -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`。
+2 -1
View File
@@ -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`。
+3 -3
View File
@@ -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/网络字幕/鉴权运行验证待补。
+24 -2
View File
@@ -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<double>(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<double>(state.renderCount), &value);
napi_set_named_property(env, metrics, "renderCount", value);
napi_get_boolean(env, state.network.buffering, &value);
napi_set_named_property(env, metrics, "buffering", value);
napi_create_double(env, state.network.cacheDuration, &value);
napi_set_named_property(env, metrics, "cacheDuration", value);
napi_create_int64(env, state.network.cacheBytes, &value);
napi_set_named_property(env, metrics, "cacheBytes", value);
napi_create_int64(env, state.network.currentBitrate, &value);
napi_set_named_property(env, metrics, "currentBitrate", value);
napi_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);
@@ -0,0 +1,16 @@
#pragma once
#include <mpv/client.h>
#include "mpv/harmony_playback.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Private renderer bridge; never install this declaration as public API. */
mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large. Load diff
+5 -22
View File
@@ -1,13 +1,12 @@
#pragma once
#include <mpv/client.h>
#include "harmony_playback_internal.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
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<bool> stopEvents_{false};
std::atomic<bool> 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;
};
@@ -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: []
};
+116 -35
View File
@@ -14,6 +14,7 @@
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <math.h>
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"};