diff --git a/DOCS/NETWORK_PLAYBACK.md b/DOCS/NETWORK_PLAYBACK.md index 8a979e120e..224307c38a 100644 --- a/DOCS/NETWORK_PLAYBACK.md +++ b/DOCS/NETWORK_PLAYBACK.md @@ -5,3 +5,5 @@ 每次 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/空值,不做推断。 + +DNS、TCP connect、connection reset、timeout 和 FFmpeg open failure 统一归入 NETWORK,并保留脱敏原始消息;TLS handshake/certificate 归入 TLS,HTTP status 归入 HTTP,demux/decoder 错误不伪装成网络错误。 diff --git a/DOCS/PHASE5_STATUS.md b/DOCS/PHASE5_STATUS.md index 3fdfa49191..9df5bb2237 100644 --- a/DOCS/PHASE5_STATUS.md +++ b/DOCS/PHASE5_STATUS.md @@ -9,5 +9,8 @@ - 请求字段、会话 generation、线程安全控制、稳定状态/错误、轨道、媒体信息和网络缓存诊断已实现。 - Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。 - 文档、测试矩阵和构建矩阵已补齐。 +- default/software/FFmpeg-Harmony 三套 AArch64 libmpv 构建、稳定 API 符号校验与 Playback Lab Native/ArkTS 编译已通过;PackageHap 因环境缺少 Java Runtime 保持待补。 尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。 + +许可:新增代码沿用 MPV 的 LGPL/ISC 边界,不引入商业字体或新的第三方网络库;公共头文件保持独立 ABI。Phase 6 最大风险是将 Provider 业务请求正确转换为通用请求,以及在实体设备上确认网络、音频路由和字幕字体 provider 行为。 diff --git a/DOCS/PHASE5_TEST_MATRIX.md b/DOCS/PHASE5_TEST_MATRIX.md index 169096a9ba..78786e6d8e 100644 --- a/DOCS/PHASE5_TEST_MATRIX.md +++ b/DOCS/PHASE5_TEST_MATRIX.md @@ -2,15 +2,30 @@ | 场景 | 方式 | 结果 | |---|---|---| -| 本地 MP4 H264/AAC | Playback Lab | 编译覆盖,设备运行待确认 | -| 本地 MKV H264/AAC/SRT | Playback Lab | 编译覆盖,设备运行待确认 | -| 本地 MKV HEVC/AAC/ASS 软件解码 | software mpv | 编译覆盖,设备运行待确认 | -| HTTP/HTTPS MP4 | 受控测试源 | 网络环境待确认 | -| HLS、chunked、Range、大 seek | 受控测试源 | 网络环境待确认 | -| 多音轨、内置/外置 SRT/ASS、WebVTT | Playback Lab | API/UI 已接入,设备运行待确认 | -| Authorization/Cookie/Referer | 受控测试源 | 脱敏与隔离代码已覆盖,服务端待确认 | -| 重定向、403、404、500、断连、超时、TLS | 受控测试源 | 稳定错误映射已覆盖,网络环境待确认 | -| 默认 FFmpeg / FFmpeg-Harmony | scripts/build-mpv-ohos*.sh | 构建矩阵待执行 | -| Playback Lab HAP | hvigor assemble | 构建矩阵待执行 | +| 本地 MP4 H264/AAC | Playback Lab | Native/ArkTS 编译通过;设备运行待确认 | +| 本地 MKV H264/AAC/SRT | Playback Lab | API/UI 编译覆盖;设备运行待确认 | +| 本地 MKV HEVC/AAC/ASS(software) | software mpv | AArch64 构建通过;设备运行待确认 | +| HTTP MP4 | 受控测试源 | 网络环境待确认 | +| HTTPS MP4 | 受控测试源 | TLS 校验代码就绪;网络环境待确认 | +| HLS Master | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 | +| HLS Variant/chunked | 受控测试源 | 复用 mpv/FFmpeg;网络环境待确认 | +| 多音轨 MKV | Playback Lab | Track API/UI 已接入;设备运行待确认 | +| 内封 SRT | Playback Lab | Track API/UI 已接入;设备运行待确认 | +| 内封 ASS/SSA | Playback Lab | libass 路径保留原样式;设备运行待确认 | +| 外挂 SRT | Playback Lab | URL 与本地 file 路径入口已接入;设备运行待确认 | +| 外挂 ASS/SSA | Playback Lab | URL 与本地 file 路径入口已接入;设备运行待确认 | +| 网络字幕/WebVTT | Playback Lab | API/UI 已接入;网络环境待确认 | +| Authorization header | 受控测试源 | 脱敏与隔离代码覆盖;服务端待确认 | +| Cookie header | 受控测试源 | 单会话 Cookie header;服务端待确认 | +| Referer/User-Agent | 受控测试源 | 请求字段与 header 均支持;服务端待确认 | +| Range seek / 大 seek | 受控测试源 | exact/keyframe/percent 命令已接入;网络环境待确认 | +| Redirect | 受控测试源 | 复用 mpv/FFmpeg;header 跨 host 不作承诺 | +| 403 / 404 / 500 | 受控测试源 | 稳定 HTTP 错误码已接入;网络环境待确认 | +| Broken media / reset / timeout / TLS | 受控测试源 | DEMUX/NETWORK/TLS 分类已接入;网络环境待确认 | +| 默认 FFmpeg(OHCodec enabled) | scripts/build-mpv-ohos.sh | AArch64 ELF 与符号校验通过 | +| software MPV(OHCodec disabled) | scripts/build-mpv-ohos-software.sh | AArch64 ELF 校验通过 | +| FFmpeg-Harmony | scripts/build-mpv-ohos-ffmpeg-harmony.sh | AArch64 ELF 与符号校验通过 | +| audio-enabled MPV | 默认脚本 `harmony-audio=enabled` | 与默认构建一并通过 | +| Playback Lab HAP | hvigor assembleHap | CompileArkTS/Native 通过;PackageHap 缺 Java Runtime | 产品禁止以 UI 定时器伪造 buffering;只有 mpv 事件/属性可改变对应诊断字段。 diff --git a/DOCS/PLAYBACK_PUBLIC_API.md b/DOCS/PLAYBACK_PUBLIC_API.md index 107a8b4257..fb1ed42a55 100644 --- a/DOCS/PLAYBACK_PUBLIC_API.md +++ b/DOCS/PLAYBACK_PUBLIC_API.md @@ -1,7 +1,7 @@ # Playback 公共 API -mp_harmony_playback.h 是上层唯一稳定契约。PlaybackRequest 只携带 URL、标题、起始位置、User-Agent、Referer、Cookie、任意 HTTP headers、字幕源、语言偏好、轨道选择、速度和超时。空值或负值按头文件注释表示未设置。 +mp_harmony_playback.h 是上层唯一稳定契约,提供 PlaybackRequest、PlaybackState、Track、MediaInfo、PlaybackMetrics 和 PlaybackError 模型别名。PlaybackRequest 只携带 URL、标题、起始位置、User-Agent、Referer、Cookie、任意 HTTP headers、字幕源、语言偏好、轨道选择、速度和超时。空值或负值按头文件注释表示未设置。 -公开控制命令:Play、Pause、TogglePause、SeekAbsolute、SeekRelative、SeekPercent、Stop、SetSpeed、SetVolume、Mute、SelectAudioTrack、SelectSubtitleTrack、DisableSubtitle、AddSubtitle、RemoveSubtitle、GetTracks、GetMediaInfo、GetPlaybackState。 +公开控制命令: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 不得改变新会话状态。 diff --git a/DOCS/SUBTITLE_PIPELINE.md b/DOCS/SUBTITLE_PIPELINE.md index 19707a11c5..afde279a78 100644 --- a/DOCS/SUBTITLE_PIPELINE.md +++ b/DOCS/SUBTITLE_PIPELINE.md @@ -4,4 +4,4 @@ 目标格式:SRT、ASS/SSA、WebVTT;ASS 默认保留原样式与 libass override 语义。上层可开关、选择字幕、调整 delay、scale、position、override(具体控件按实际可用属性开放)。 -编码优先 UTF-8;GBK、Big5、Shift-JIS 只在现有 FFmpeg/mpv/iconv 能识别时启用。无法识别或不支持时返回稳定 SUBTITLE 错误,不静默按错误编码显示。 +编码优先 UTF-8;GBK、Big5、Shift-JIS 只在现有 FFmpeg/mpv/iconv 能识别时启用。当前适配器拒绝未验证编码并返回稳定 SUBTITLE 错误,不静默按错误编码显示。Playback Lab 同时提供网络 URL 与本地 file 路径入口。 diff --git a/include/mpv/harmony_playback.h b/include/mpv/harmony_playback.h index badcaafda9..50a73216cc 100644 --- a/include/mpv/harmony_playback.h +++ b/include/mpv/harmony_playback.h @@ -86,6 +86,7 @@ typedef struct mp_harmony_subtitle_source { const char *encoding; int forced; int select; + int is_default; } mp_harmony_subtitle_source; typedef struct mp_harmony_playback_request { @@ -123,6 +124,7 @@ typedef struct mp_harmony_track { typedef struct mp_harmony_media_info { char container[MP_HARMONY_TEXT]; char demuxer[MP_HARMONY_TEXT]; + double duration; char video_codec[MP_HARMONY_TEXT]; char audio_codec[MP_HARMONY_TEXT]; char pixel_format[MP_HARMONY_TEXT]; @@ -136,6 +138,7 @@ typedef struct mp_harmony_media_info { int audio_channels; char audio_channel_layout[MP_HARMONY_TEXT]; double fps; + int64_t bitrate; int64_t video_bitrate; int64_t audio_bitrate; int track_count; @@ -156,6 +159,20 @@ typedef struct mp_harmony_network_info { int64_t current_bitrate; } mp_harmony_network_info; +typedef struct mp_harmony_playback_metrics { + int64_t dropped_frames; + int64_t render_count; + int buffering; + double cache_duration; + int64_t cache_bytes; + int64_t current_bitrate; +} mp_harmony_playback_metrics; + +typedef struct mp_harmony_playback_error { + mp_harmony_error_code code; + char raw_message[MP_HARMONY_TEXT]; +} mp_harmony_playback_error; + typedef struct mp_harmony_playback_snapshot { uint64_t session_id; mp_harmony_state state; @@ -170,11 +187,24 @@ typedef struct mp_harmony_playback_snapshot { int volume; int muted; double speed; + int64_t subtitle_delay_ms; + double subtitle_scale; + double subtitle_position; int track_count; + mp_harmony_playback_error error; + mp_harmony_playback_metrics metrics; mp_harmony_media_info media; mp_harmony_network_info network; } mp_harmony_playback_snapshot; +/* Stable model aliases for language bindings. */ +typedef mp_harmony_playback_request PlaybackRequest; +typedef mp_harmony_playback_snapshot PlaybackState; +typedef mp_harmony_track Track; +typedef mp_harmony_media_info MediaInfo; +typedef mp_harmony_playback_metrics PlaybackMetrics; +typedef mp_harmony_playback_error PlaybackError; + MP_HARMONY_EXPORT mp_harmony_player *mp_harmony_playback_create(void); MP_HARMONY_EXPORT int mp_harmony_playback_initialize(mp_harmony_player *player); MP_HARMONY_EXPORT void mp_harmony_playback_destroy(mp_harmony_player *player); @@ -189,6 +219,7 @@ MP_HARMONY_EXPORT int mp_harmony_playback_seek(mp_harmony_player *player, mp_har MP_HARMONY_EXPORT int mp_harmony_playback_set_speed(mp_harmony_player *player, double speed); MP_HARMONY_EXPORT int mp_harmony_playback_set_volume(mp_harmony_player *player, double volume); MP_HARMONY_EXPORT int mp_harmony_playback_set_mute(mp_harmony_player *player, int muted); +MP_HARMONY_EXPORT int mp_harmony_playback_set_audio_enabled(mp_harmony_player *player, int enabled); MP_HARMONY_EXPORT int mp_harmony_playback_select_audio(mp_harmony_player *player, int track_id); MP_HARMONY_EXPORT int mp_harmony_playback_select_subtitle(mp_harmony_player *player, int track_id); MP_HARMONY_EXPORT int mp_harmony_playback_disable_subtitle(mp_harmony_player *player); diff --git a/memory/2026-08-29.md b/memory/2026-08-29.md index ddeea4c7c1..9f5b4ce044 100644 --- a/memory/2026-08-29.md +++ b/memory/2026-08-29.md @@ -9,3 +9,9 @@ 文件变更: include/mpv/harmony_playback.h, player/harmony_playback.c, playback_lab/entry/src/main/cpp/{player.h,player.cpp,bridge.cpp}, playback_lab/entry/src/main/ets/pages/Index.ets, DOCS/PHASE5_*.md, DOCS/PLAYBACK_PUBLIC_API.md, DOCS/NETWORK_*.md, DOCS/TRACK_MODEL.md, DOCS/SUBTITLE_PIPELINE.md, DOCS/HARMONY_FONT_STRATEGY.md 教训: hvigor 的 CompileArkTS/BuildNativeWithNinja 可在无 Java 环境通过,但 PackageHap 仍需 JDK;HAP 不应把 Native/ArkTS 编译成功误报为完整打包成功。 标签: #phase5 #playback-api #network #subtitle #redaction #software-decode #pending-runtime + +[PROJECT:Velix-MPV-Core] Phase 5 最终构建与契约审计 +结论: 修正 C API 时长/错误同步、请求校验、网络日志诊断与敏感 query 脱敏;默认、software、FFmpeg-Harmony AArch64 构建及 Native/ArkTS 编译通过,PackageHap 仍因缺 Java Runtime 待补,模拟器可启动既有 Playback Lab。 +文件变更: player/harmony_playback.c, playback_lab/entry/src/main/cpp/player.cpp, DOCS/PHASE5_TEST_MATRIX.md, DOCS/PHASE5_STATUS.md, memory/projects.md +教训: 网络 HTTP status 采集必须覆盖 cplayer/ffmpeg 日志前缀;HLS/鉴权/字幕运行证据仍需真实网络与可安装 HAP,不能用编译结果替代。 +标签: #phase5 #build #emulator #hap #java-runtime #network-pending diff --git a/memory/projects.md b/memory/projects.md index b8be31ad14..3394c3aedb 100644 --- a/memory/projects.md +++ b/memory/projects.md @@ -6,7 +6,7 @@ - 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。 - 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,314,184 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;`build-all-ohos.sh`、video-only/audio-enabled 构建和 Playback Lab HAP 均通过。 +- 已完成:`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 环境阻断。 - 已完成: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。 diff --git a/playback_lab/entry/src/main/cpp/bridge.cpp b/playback_lab/entry/src/main/cpp/bridge.cpp index c5394a9942..c5397427e4 100644 --- a/playback_lab/entry/src/main/cpp/bridge.cpp +++ b/playback_lab/entry/src/main/cpp/bridge.cpp @@ -111,20 +111,29 @@ bool getRequest(napi_env env, napi_callback_info info, PlaybackRequest& request) if (!getNamedString(env, argv[0], "url", request.url) || request.url.empty()) return false; getNamedString(env, argv[0], "title", request.title); getNamedString(env, argv[0], "userAgent", request.userAgent); + if (request.userAgent.empty()) getNamedString(env, argv[0], "user_agent", request.userAgent); getNamedString(env, argv[0], "referer", request.referer); getNamedString(env, argv[0], "cookies", request.cookies); getNamedString(env, argv[0], "preferredAudioLanguage", request.preferredAudioLanguage); + if (request.preferredAudioLanguage.empty()) getNamedString(env, argv[0], "preferred_audio_language", request.preferredAudioLanguage); getNamedString(env, argv[0], "preferredSubtitleLanguage", request.preferredSubtitleLanguage); + if (request.preferredSubtitleLanguage.empty()) getNamedString(env, argv[0], "preferred_subtitle_language", request.preferredSubtitleLanguage); double number = 0; if (getNamedDouble(env, argv[0], "playbackSpeed", number)) request.playbackSpeed = number; + else if (getNamedDouble(env, argv[0], "playback_speed", number)) request.playbackSpeed = number; if (getNamedDouble(env, argv[0], "networkTimeout", number)) request.networkTimeout = number; + else if (getNamedDouble(env, argv[0], "network_timeout", number)) request.networkTimeout = number; int64_t start = -1; if (getNamedInt64(env, argv[0], "startPositionMs", start)) request.startPositionMs = start; + else if (getNamedInt64(env, argv[0], "start_position_ms", start)) request.startPositionMs = start; int64_t track = -1; if (getNamedInt64(env, argv[0], "audioTrackId", track)) request.audioTrackId = static_cast(track); + else if (getNamedInt64(env, argv[0], "audio_track_id", track)) request.audioTrackId = static_cast(track); if (getNamedInt64(env, argv[0], "subtitleTrackId", track)) request.subtitleTrackId = static_cast(track); + else if (getNamedInt64(env, argv[0], "subtitle_track_id", track)) request.subtitleTrackId = static_cast(track); bool enabled = true; if (getNamedBool(env, argv[0], "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0; + else if (getNamedBool(env, argv[0], "subtitle_enabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0; napi_value headers = nullptr; bool hasHeaders = false; @@ -142,6 +151,60 @@ bool getRequest(napi_env env, napi_callback_info info, PlaybackRequest& request) } } } + if (!hasHeaders && napi_has_named_property(env, argv[0], "http_headers", &hasHeaders) == napi_ok && hasHeaders && + napi_get_named_property(env, argv[0], "http_headers", &headers) == napi_ok) { + uint32_t length = 0; + if (napi_get_array_length(env, headers, &length) == napi_ok) { + for (uint32_t i = 0; i < length && i < 32; ++i) { + napi_value item = nullptr; + if (napi_get_element(env, headers, i, &item) != napi_ok) continue; + PlaybackHeader header; + getNamedString(env, item, "name", header.name); + getNamedString(env, item, "value", header.value); + if (!header.name.empty()) request.headers.push_back(std::move(header)); + } + } + } + napi_value subtitles = nullptr; + bool hasSubtitles = false; + if (napi_has_named_property(env, argv[0], "subtitleFiles", &hasSubtitles) == napi_ok && hasSubtitles && + napi_get_named_property(env, argv[0], "subtitleFiles", &subtitles) == napi_ok) { + uint32_t length = 0; + if (napi_get_array_length(env, subtitles, &length) == napi_ok) { + for (uint32_t i = 0; i < length && i < 32; ++i) { + napi_value item = nullptr; + if (napi_get_element(env, subtitles, i, &item) != napi_ok) continue; + PlaybackSubtitleSource subtitle; + getNamedString(env, item, "url", subtitle.url); + getNamedString(env, item, "title", subtitle.title); + getNamedString(env, item, "language", subtitle.language); + getNamedString(env, item, "encoding", subtitle.encoding); + getNamedBool(env, item, "forced", subtitle.forced); + getNamedBool(env, item, "select", subtitle.select); + getNamedBool(env, item, "default", subtitle.isDefault); + if (!subtitle.url.empty()) request.subtitleFiles.push_back(std::move(subtitle)); + } + } + } + if (!hasSubtitles && napi_has_named_property(env, argv[0], "subtitle_files", &hasSubtitles) == napi_ok && hasSubtitles && + napi_get_named_property(env, argv[0], "subtitle_files", &subtitles) == napi_ok) { + uint32_t length = 0; + if (napi_get_array_length(env, subtitles, &length) == napi_ok) { + for (uint32_t i = 0; i < length && i < 32; ++i) { + napi_value item = nullptr; + if (napi_get_element(env, subtitles, i, &item) != napi_ok) continue; + PlaybackSubtitleSource subtitle; + getNamedString(env, item, "url", subtitle.url); + getNamedString(env, item, "title", subtitle.title); + getNamedString(env, item, "language", subtitle.language); + getNamedString(env, item, "encoding", subtitle.encoding); + getNamedBool(env, item, "forced", subtitle.forced); + getNamedBool(env, item, "select", subtitle.select); + getNamedBool(env, item, "default", subtitle.isDefault); + if (!subtitle.url.empty()) request.subtitleFiles.push_back(std::move(subtitle)); + } + } + } return true; } @@ -220,6 +283,27 @@ napi_value seekAbsolute(napi_env env, napi_callback_info info) return undefined(env); } +napi_value seekRelative(napi_env env, napi_callback_info info) +{ + double delta = 0; + if (getDoubleArg(env, info, delta)) gPlayer.seek(delta); + return undefined(env); +} + +napi_value seekExact(napi_env env, napi_callback_info info) +{ + double seconds = 0; + if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, true, false); + return undefined(env); +} + +napi_value seekKeyframe(napi_env env, napi_callback_info info) +{ + double seconds = 0; + if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, false, true); + return undefined(env); +} + napi_value seekPercent(napi_env env, napi_callback_info info) { double percent = 0; @@ -299,6 +383,7 @@ napi_value addSubtitle(napi_env env, napi_callback_info info) getNamedString(env, argv[0], "encoding", source.encoding); getNamedBool(env, argv[0], "forced", source.forced); getNamedBool(env, argv[0], "select", source.select); + getNamedBool(env, argv[0], "default", source.isDefault); } napi_value result = nullptr; napi_get_boolean(env, gPlayer.addSubtitle(source), &result); @@ -392,6 +477,12 @@ napi_value getState(napi_env env, napi_callback_info) napi_set_named_property(env, object, "volume", value); napi_get_boolean(env, state.muted, &value); napi_set_named_property(env, object, "muted", value); + napi_create_int64(env, state.subtitleDelayMs, &value); + napi_set_named_property(env, object, "subtitleDelayMs", value); + napi_create_double(env, state.subtitleScale, &value); + napi_set_named_property(env, object, "subtitleScale", value); + napi_create_double(env, state.subtitlePosition, &value); + napi_set_named_property(env, object, "subtitlePosition", value); napi_value network = nullptr; napi_create_object(env, &network); napi_create_string_utf8(env, state.network.protocol.c_str(), NAPI_AUTO_LENGTH, &value); @@ -423,24 +514,36 @@ napi_value getState(napi_env env, napi_callback_info) napi_set_named_property(env, media, "container", value); napi_create_string_utf8(env, state.media.demuxer.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, media, "demuxer", value); + napi_create_double(env, state.media.duration, &value); + napi_set_named_property(env, media, "duration", value); napi_create_string_utf8(env, state.media.videoCodec.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, media, "videoCodec", value); napi_create_string_utf8(env, state.media.audioCodec.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, media, "audioCodec", value); napi_create_string_utf8(env, state.media.pixelFormat.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, media, "pixelFormat", value); + napi_create_string_utf8(env, state.media.colorPrimaries.c_str(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, media, "colorPrimaries", value); + napi_create_string_utf8(env, state.media.colorTransfer.c_str(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, media, "colorTransfer", value); + napi_create_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_set_named_property(env, media, "width", value); napi_create_int32(env, state.media.height, &value); napi_set_named_property(env, media, "height", value); napi_create_double(env, state.media.fps, &value); napi_set_named_property(env, media, "fps", value); + napi_create_int64(env, state.media.bitrate, &value); + napi_set_named_property(env, media, "bitrate", value); napi_create_int32(env, state.media.bitDepth, &value); napi_set_named_property(env, media, "bitDepth", value); napi_create_int32(env, state.media.audioSampleRate, &value); napi_set_named_property(env, media, "audioSampleRate", value); napi_create_int32(env, state.media.audioChannels, &value); napi_set_named_property(env, media, "audioChannels", value); + napi_create_string_utf8(env, state.media.audioChannelLayout.c_str(), NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, media, "audioChannelLayout", value); napi_create_int32(env, state.media.trackCount, &value); napi_set_named_property(env, media, "trackCount", value); napi_create_int32(env, state.media.subtitleCount, &value); @@ -552,6 +655,9 @@ napi_value Init(napi_env env, napi_value exports) {"togglePause", nullptr, togglePause, nullptr, nullptr, nullptr, napi_default, nullptr}, {"seek", nullptr, seek, nullptr, nullptr, nullptr, napi_default, nullptr}, {"seekAbsolute", nullptr, seekAbsolute, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seekRelative", nullptr, seekRelative, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seekExact", nullptr, seekExact, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"seekKeyframe", nullptr, seekKeyframe, nullptr, nullptr, nullptr, napi_default, nullptr}, {"seekPercent", nullptr, seekPercent, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setSpeed", nullptr, setSpeed, nullptr, nullptr, nullptr, napi_default, nullptr}, {"setVolume", nullptr, setVolume, nullptr, nullptr, nullptr, napi_default, nullptr}, diff --git a/playback_lab/entry/src/main/cpp/player.cpp b/playback_lab/entry/src/main/cpp/player.cpp index f86d4ca230..7be7f845f3 100644 --- a/playback_lab/entry/src/main/cpp/player.cpp +++ b/playback_lab/entry/src/main/cpp/player.cpp @@ -198,6 +198,9 @@ bool PlaybackEngine::load(const PlaybackRequest& request) snapshot_.media = PlaybackMediaInfo{}; snapshot_.network = PlaybackNetworkInfo{}; snapshot_.tracks.clear(); + snapshot_.subtitleDelayMs = 0; + snapshot_.subtitleScale = 1.0; + snapshot_.subtitlePosition = 100.0; snapshot_.sessionId = ++sessionId_; awaitingStart_ = true; startedAt_ = std::chrono::steady_clock::now(); @@ -217,6 +220,7 @@ bool PlaybackEngine::load(const PlaybackRequest& request) if (request.playbackSpeed > 0) { std::string speed = std::to_string(request.playbackSpeed); mpv_set_property_string(handle_, "speed", speed.c_str()); + std::lock_guard stateLock(mutex_); snapshot_.speed = request.playbackSpeed; } std::string audioId = request.audioTrackId >= 0 ? std::to_string(request.audioTrackId) : "auto"; @@ -244,7 +248,7 @@ bool PlaybackEngine::load(const PlaybackRequest& request) } if (!subtitle.encoding.empty()) mpv_set_property_string(handle_, "sub-codepage", subtitle.encoding.c_str()); - const char* flags = subtitle.select ? "select" : "auto"; + const char* flags = (subtitle.select || subtitle.isDefault) ? "select" : "auto"; const char* subCommand[] = {"sub-add", subtitle.url.c_str(), flags, subtitle.title.c_str(), subtitle.language.c_str(), nullptr}; mpv_command(handle_, subCommand); @@ -414,7 +418,7 @@ bool PlaybackEngine::addSubtitle(const PlaybackSubtitleSource& source) } if (!source.encoding.empty()) mpv_set_property_string(handle_, "sub-codepage", source.encoding.c_str()); - const char* flags = source.select ? "select" : "auto"; + const char* flags = (source.select || source.isDefault) ? "select" : "auto"; const char* command[] = {"sub-add", source.url.c_str(), flags, source.title.c_str(), source.language.c_str(), nullptr}; int code = mpv_command(handle_, command); @@ -436,7 +440,12 @@ bool PlaybackEngine::setSubtitleDelay(double seconds) std::lock_guard apiLock(apiMutex_); if (handle_ == nullptr || !std::isfinite(seconds)) return false; const std::string value = std::to_string(seconds); - return mpv_set_property_string(handle_, "sub-delay", value.c_str()) >= 0; + const bool ok = mpv_set_property_string(handle_, "sub-delay", value.c_str()) >= 0; + if (ok) { + std::lock_guard lock(mutex_); + snapshot_.subtitleDelayMs = static_cast(seconds * 1000.0); + } + return ok; } bool PlaybackEngine::setSubtitleScale(double scale) @@ -444,7 +453,12 @@ bool PlaybackEngine::setSubtitleScale(double scale) std::lock_guard apiLock(apiMutex_); if (handle_ == nullptr || !std::isfinite(scale) || scale <= 0) return false; const std::string value = std::to_string(scale); - return mpv_set_property_string(handle_, "sub-scale", value.c_str()) >= 0; + const bool ok = mpv_set_property_string(handle_, "sub-scale", value.c_str()) >= 0; + if (ok) { + std::lock_guard lock(mutex_); + snapshot_.subtitleScale = scale; + } + return ok; } bool PlaybackEngine::setSubtitlePosition(double position) @@ -452,7 +466,12 @@ bool PlaybackEngine::setSubtitlePosition(double position) std::lock_guard apiLock(apiMutex_); if (handle_ == nullptr || !std::isfinite(position) || position < 0 || position > 100) return false; const std::string value = std::to_string(position); - return mpv_set_property_string(handle_, "sub-pos", value.c_str()) >= 0; + const bool ok = mpv_set_property_string(handle_, "sub-pos", value.c_str()) >= 0; + if (ok) { + std::lock_guard lock(mutex_); + snapshot_.subtitlePosition = position; + } + return ok; } bool PlaybackEngine::setAssOverride(const std::string& mode) @@ -502,6 +521,10 @@ void PlaybackEngine::stop() setError("stop failed"); return; } + PlaybackRequest emptyRequest; + setHeadersLocked(emptyRequest); + mpv_set_property_string(handle_, "user-agent", ""); + mpv_set_property_string(handle_, "referrer", ""); setState("STOPPED"); } @@ -642,7 +665,10 @@ void PlaybackEngine::refreshMediaLocked() if (mpv_get_property(handle_, "height", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.height = static_cast(number); if (mpv_get_property(handle_, "video-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.videoBitrate = number; if (mpv_get_property(handle_, "audio-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.audioBitrate = number; + if (mpv_get_property(handle_, "demux-bitrate", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.bitrate = number; if (mpv_get_property(handle_, "container-fps", MPV_FORMAT_DOUBLE, &snapshot_.media.fps) < 0) snapshot_.media.fps = 0; + if (mpv_get_property(handle_, "duration", MPV_FORMAT_DOUBLE, &snapshot_.media.duration) >= 0) + snapshot_.duration = snapshot_.media.duration; mpv_node node = {}; if (mpv_get_property(handle_, "video-params", MPV_FORMAT_NODE, &node) >= 0) { if (node.format == MPV_FORMAT_NODE_MAP && node.u.list != nullptr) { @@ -713,7 +739,7 @@ void PlaybackEngine::refreshNetworkLocked() } snapshot_.network.demuxer = snapshot_.media.demuxer; snapshot_.network.container = snapshot_.media.container; - if (mpv_get_property(handle_, "video-bitrate", MPV_FORMAT_INT64, &snapshot_.network.currentBitrate) < 0) + if (mpv_get_property(handle_, "demux-bitrate", MPV_FORMAT_INT64, &snapshot_.network.currentBitrate) < 0) snapshot_.network.currentBitrate = 0; } @@ -814,7 +840,10 @@ void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property* if (property->format == MPV_FORMAT_DOUBLE) snapshot_.position = *static_cast(property->data); break; case kDuration: - if (property->format == MPV_FORMAT_DOUBLE) snapshot_.duration = *static_cast(property->data); + if (property->format == MPV_FORMAT_DOUBLE) { + snapshot_.duration = *static_cast(property->data); + snapshot_.media.duration = snapshot_.duration; + } break; case kPause: if (property->format == MPV_FORMAT_FLAG && *static_cast(property->data) != 0 && @@ -886,7 +915,9 @@ void PlaybackEngine::eventLoop() auto* message = static_cast(event->data); if (message != nullptr && message->prefix != nullptr && (std::strncmp(message->prefix, "ao", 2) == 0 || - std::strncmp(message->prefix, "vd", 2) == 0)) + std::strncmp(message->prefix, "vd", 2) == 0 || + std::strncmp(message->prefix, "cplayer", 7) == 0 || + std::strncmp(message->prefix, "ffmpeg", 6) == 0)) handleLogMessage(message); break; } @@ -937,20 +968,28 @@ void PlaybackEngine::eventLoop() std::string PlaybackEngine::redactSensitive(const std::string& value) { std::string result = value; - const char* keys[] = {"authorization:", "cookie:", "proxy-authorization:", "x-api-key:"}; + auto lowerCopy = [](const std::string& input) { + std::string lower = input; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return lower; + }; + const char* keys[] = {"authorization:", "cookie:", "proxy-authorization:", "x-api-key:", + "token:", "api-key:", "apikey:", "secret:", "signature:"}; for (const char* key : keys) { size_t position = 0; - while ((position = result.find(key, position)) != std::string::npos) { + while ((position = lowerCopy(result).find(key, position)) != std::string::npos) { const size_t valueStart = position + std::strlen(key); const size_t end = result.find_first_of("\r\n", valueStart); result.replace(valueStart, (end == std::string::npos ? result.size() : end) - valueStart, " [REDACTED]"); position = valueStart + 11; } } - const char* queryKeys[] = {"token=", "auth=", "key=", "signature=", "sig=", "expires="}; + const char* queryKeys[] = {"token=", "access_token=", "auth=", "key=", "api_key=", "apikey=", + "signature=", "x-amz-signature=", "sig=", "expires="}; for (const char* key : queryKeys) { size_t position = 0; - while ((position = result.find(key, position)) != std::string::npos) { + while ((position = lowerCopy(result).find(key, position)) != std::string::npos) { const size_t end = result.find_first_of("& \r\n", position); result.replace(position, (end == std::string::npos ? result.size() : end) - position, std::string(key) + "[REDACTED]"); position += std::strlen(key) + 10; diff --git a/playback_lab/entry/src/main/cpp/player.h b/playback_lab/entry/src/main/cpp/player.h index 19c9cec613..23f31e28df 100644 --- a/playback_lab/entry/src/main/cpp/player.h +++ b/playback_lab/entry/src/main/cpp/player.h @@ -22,6 +22,7 @@ struct PlaybackSubtitleSource { std::string encoding; bool forced = false; bool select = false; + bool isDefault = false; }; struct PlaybackRequest { @@ -57,6 +58,7 @@ struct PlaybackTrack { struct PlaybackMediaInfo { std::string container; std::string demuxer; + double duration = 0.0; std::string videoCodec; std::string audioCodec; std::string pixelFormat; @@ -70,6 +72,7 @@ struct PlaybackMediaInfo { int audioChannels = 0; std::string audioChannelLayout; double fps = 0.0; + int64_t bitrate = 0; int64_t videoBitrate = 0; int64_t audioBitrate = 0; int trackCount = 0; @@ -107,6 +110,9 @@ struct PlaybackSnapshot { double speed = 1.0; double volume = 100.0; bool muted = false; + int64_t subtitleDelayMs = 0; + double subtitleScale = 1.0; + double subtitlePosition = 100.0; std::string requestedDecoder = "software"; std::string actualDecoder = "software"; std::string hwdecBackend; diff --git a/playback_lab/entry/src/main/ets/pages/Index.ets b/playback_lab/entry/src/main/ets/pages/Index.ets index 0f27e4154f..baf55622a5 100644 --- a/playback_lab/entry/src/main/ets/pages/Index.ets +++ b/playback_lab/entry/src/main/ets/pages/Index.ets @@ -44,6 +44,9 @@ interface PlaybackState { speed: number; volume: number; muted: boolean; + subtitleDelayMs: number; + subtitleScale: number; + subtitlePosition: number; network: NetworkState; media: MediaState; tracks: TrackState[]; @@ -66,12 +69,17 @@ interface NetworkState { interface MediaState { container: string; demuxer: string; + duration: number; videoCodec: string; audioCodec: string; pixelFormat: string; + colorPrimaries: string; + colorTransfer: string; + colorMatrix: string; width: number; height: number; fps: number; + bitrate: number; bitDepth: number; audioSampleRate: number; audioChannels: number; @@ -111,6 +119,26 @@ function parseHeaders(value: string): HeaderInput[] { return result; } +function maskHeaders(value: string, reveal: boolean): string { + if (reveal) return value || '(none)'; + const result: string[] = []; + value.split('\n').forEach((line: string) => { + const separator: number = line.indexOf(':'); + if (separator > 0) result.push(line.substring(0, separator).trim() + ': [REDACTED]'); + }); + return result.length > 0 ? result.join(' | ') : '(none)'; +} + +function localSubtitleUrl(value: string): string { + return value.startsWith('file://') ? value : 'file://' + value; +} + +function nextSeekMode(value: string): string { + if (value === 'relative') return 'absolute'; + if (value === 'absolute') return 'percent'; + return 'relative'; +} + @Entry @Component struct Index { @@ -123,6 +151,9 @@ struct Index { @State speedInput: string = '1'; @State volumeInput: string = '100'; @State subtitleDelayInput: string = '0'; + @State revealHeaders: boolean = false; + @State seekMode: string = 'relative'; + @State seekInput: string = '10'; @State state: PlaybackState = { state: 'IDLE', position: 0, duration: 0, fps: 0, dropped: 0, firstFrameMs: -1, renderCount: 0, lastError: '', surface: 'WAITING', egl: 'WAITING', mpv: 'WAITING', @@ -135,10 +166,11 @@ struct Index { nativeBufferImportCount: 0, nativeBufferImportFailure: 0, surfacePresentCount: 0, gpuInteropFrameCount: 0, sessionId: 0, errorCode: 'NONE', 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: '', videoCodec: '', audioCodec: '', pixelFormat: '', - width: 0, height: 0, fps: 0, bitDepth: 0, audioSampleRate: 0, audioChannels: 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, trackCount: 0, subtitleCount: 0 }, tracks: [] }; @@ -199,8 +231,24 @@ struct Index { Button('+10s').onClick(() => playbackLab.seek(10)) Button('Toggle').onClick(() => playbackLab.togglePause()) }.width('100%') + Row({ space: 8 }) { + Button('Mode: ' + this.seekMode).onClick(() => this.seekMode = nextSeekMode(this.seekMode)) + TextInput({ text: this.seekInput, placeholder: 'seek value' }).width('25%') + .onChange((value: string) => this.seekInput = value) + Button('Seek').onClick(() => { + const value: number = Number(this.seekInput); + if (this.seekMode === 'absolute') playbackLab.seekAbsolute(value); + else if (this.seekMode === 'percent') playbackLab.seekPercent(value); + else playbackLab.seek(value); + }) + }.width('100%') TextInput({ text: this.headersText, placeholder: 'Headers (one Name: Value per line; values masked in diagnostics)' }) .width('100%').onChange((value: string) => this.headersText = value) + Row({ space: 8 }) { + Text('Header diagnostics: ' + maskHeaders(this.headersText, this.revealHeaders)) + .fontSize(12).fontColor($r('app.color.muted')).layoutWeight(1).maxLines(2) + Button(this.revealHeaders ? 'Mask' : 'Reveal (debug)').onClick(() => this.revealHeaders = !this.revealHeaders) + }.width('100%') Row({ space: 8 }) { TextInput({ text: this.speedInput, placeholder: 'speed' }).width('25%') .onChange((value: string) => this.speedInput = value) @@ -216,6 +264,9 @@ struct Index { Button('Add Subtitle').onClick(() => playbackLab.addSubtitle({ url: this.subtitleUrl, select: true })) + Button('Add Local').onClick(() => playbackLab.addSubtitle({ + url: localSubtitleUrl(this.subtitleUrl), select: true + })) Button('Subtitle Off').onClick(() => playbackLab.disableSubtitle()) TextInput({ text: this.subtitleDelayInput, placeholder: 'delay s' }).width('22%') .onChange((value: string) => this.subtitleDelayInput = value) @@ -240,8 +291,10 @@ struct Index { Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2) Text('network=' + this.state.network.protocol + '://' + this.state.network.host + ' status=' + (this.state.network.httpStatus || 'unavailable') + ' redirects=' + this.state.network.redirectCount + ' buffering=' + this.state.network.buffering) .fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2) - Text('media=' + this.state.media.container + '/' + this.state.media.videoCodec + '/' + this.state.media.audioCodec + ' ' + this.state.media.width + 'x' + this.state.media.height + ' 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) + Text('subtitle delay=' + this.state.subtitleDelayMs + 'ms scale=' + this.state.subtitleScale + ' position=' + this.state.subtitlePosition) + .fontSize(13).fontColor($r('app.color.muted')).width('100%') ForEach(this.state.tracks, (track: TrackState) => { Row({ space: 6 }) { Text('track ' + track.id + ' ' + track.type + ' ' + (track.language || 'und') + ' ' + (track.codec || 'unknown') + (track.selected ? ' [selected]' : '')) diff --git a/player/harmony_playback.c b/player/harmony_playback.c index cef8dd7980..18c50fa825 100644 --- a/player/harmony_playback.c +++ b/player/harmony_playback.c @@ -1,3 +1,10 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the FSF. + */ + #include "mpv/harmony_playback.h" #include "mpv/client.h" @@ -29,6 +36,8 @@ enum { PROP_VOLUME, PROP_MUTE, PROP_SPEED, }; +static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *request); + static void copy_text(char *dst, size_t size, const char *src) { if (!dst || size == 0) return; @@ -67,6 +76,11 @@ 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, "proxy-authorization:", 21) == 0) key_len = 21; else if (strncasecmp(p, "x-api-key:", 10) == 0) key_len = 10; + 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, "apikey:", 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; if (key_len) { size_t n = key_len; while (p[n] && p[n] != '\n' && p[n] != '\r') n++; @@ -78,8 +92,10 @@ static void redact_text(char *dst, size_t size, const char *src) } if (*p == '?' || *p == '&') { const char *q = p + 1; - if (strncasecmp(q, "token=", 6) == 0 || strncasecmp(q, "auth=", 5) == 0 || - strncasecmp(q, "key=", 4) == 0 || strncasecmp(q, "signature=", 10) == 0 || + if (strncasecmp(q, "token=", 6) == 0 || strncasecmp(q, "access_token=", 13) == 0 || + strncasecmp(q, "auth=", 5) == 0 || strncasecmp(q, "key=", 4) == 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, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0) { dst[out++] = *p++; while (*p && *p != '&' && *p != ' ' && *p != '\n' && out + 1 < size) p++; @@ -98,10 +114,14 @@ static void set_error_locked(mp_harmony_player *p, mp_harmony_error_code code, c p->snapshot.state = MP_HARMONY_ERROR; p->snapshot.error_code = code; redact_text(p->snapshot.error_message, sizeof(p->snapshot.error_message), message); + p->snapshot.error.code = code; + copy_text(p->snapshot.error.raw_message, sizeof(p->snapshot.error.raw_message), + p->snapshot.error_message); } static mp_harmony_error_code classify_error(int code, const char *message) { + (void)code; if (text_has(message, "403") || text_has(message, "404") || text_has(message, "500") || text_has(message, "http error")) return MP_HARMONY_ERROR_HTTP; if (text_has(message, "tls") || text_has(message, "certificate") || text_has(message, "ssl")) @@ -125,7 +145,7 @@ static mp_harmony_error_code classify_error(int code, const char *message) if (text_has(message, "timeout") || text_has(message, "connection") || text_has(message, "network") || text_has(message, "dns")) return MP_HARMONY_ERROR_NETWORK; - return code == MPV_ERROR_OPTION_ERROR ? MP_HARMONY_ERROR_UNKNOWN : MP_HARMONY_ERROR_NETWORK; + return MP_HARMONY_ERROR_UNKNOWN; } static int set_property_string(mp_harmony_player *p, const char *name, const char *value) @@ -183,8 +203,12 @@ static void update_network_locked(mp_harmony_player *p) if (demuxer) mpv_free(demuxer); if (container) mpv_free(container); int64_t bitrate = 0; - if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &bitrate) >= 0) + if (mpv_get_property(p->handle, "demux-bitrate", MPV_FORMAT_INT64, &bitrate) >= 0) p->snapshot.network.current_bitrate = bitrate; + p->snapshot.metrics.buffering = p->snapshot.network.buffering; + p->snapshot.metrics.cache_duration = p->snapshot.network.cache_duration; + p->snapshot.metrics.cache_bytes = p->snapshot.network.cache_bytes; + p->snapshot.metrics.current_bitrate = p->snapshot.network.current_bitrate; } static const char *node_string(mpv_node *node) @@ -263,10 +287,12 @@ static void refresh_media_locked(mp_harmony_player *p) if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.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, "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, "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; + p->snapshot.media.duration = p->snapshot.duration; mpv_node node = {0}; if (mpv_get_property(p->handle, "video-params", MPV_FORMAT_NODE, &node) >= 0) { if (node.format == MPV_FORMAT_NODE_MAP && node.u.list) { @@ -303,9 +329,15 @@ static void update_property_locked(mp_harmony_player *p, uint64_t id, mpv_event_ { if (!property || !property->data) return; if (id == PROP_TIME_POS && property->format == MPV_FORMAT_DOUBLE) p->snapshot.position = *(double *)property->data; - else if (id == PROP_DURATION && property->format == MPV_FORMAT_DOUBLE) p->snapshot.duration = *(double *)property->data; + else if (id == PROP_DURATION && property->format == MPV_FORMAT_DOUBLE) { + p->snapshot.duration = *(double *)property->data; + p->snapshot.media.duration = p->snapshot.duration; + } else if (id == PROP_FPS && property->format == MPV_FORMAT_DOUBLE) p->snapshot.fps = *(double *)property->data; - else if (id == PROP_DROPPED && property->format == MPV_FORMAT_INT64) p->snapshot.dropped_frames = *(int64_t *)property->data; + else if (id == PROP_DROPPED && property->format == MPV_FORMAT_INT64) { + p->snapshot.dropped_frames = *(int64_t *)property->data; + p->snapshot.metrics.dropped_frames = p->snapshot.dropped_frames; + } else if (id == PROP_PAUSE && property->format == MPV_FORMAT_FLAG) { if (*(int *)property->data) p->snapshot.state = MP_HARMONY_PAUSED; else if (p->snapshot.state == MP_HARMONY_PAUSED || p->snapshot.state == MP_HARMONY_READY) p->snapshot.state = MP_HARMONY_PLAYING; @@ -401,6 +433,7 @@ mp_harmony_player *mp_harmony_playback_create(void) 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.subtitle_scale = 1.0; p->snapshot.subtitle_position = 100.0; return p; } @@ -489,15 +522,35 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } p->session_id++; p->snapshot.session_id = p->session_id; p->snapshot.state = MP_HARMONY_OPENING; p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.error_message[0] = '\0'; + p->snapshot.error.code = MP_HARMONY_ERROR_NONE; p->snapshot.error.raw_message[0] = '\0'; p->snapshot.position = 0; p->snapshot.duration = 0; + p->snapshot.subtitle_delay_ms = 0; + p->snapshot.subtitle_scale = 1.0; + p->snapshot.subtitle_position = 100.0; p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0; - set_headers_locked(p, r); - set_property_string(p, "user-agent", r->user_agent ? r->user_agent : ""); - set_property_string(p, "referrer", r->referer ? r->referer : ""); - set_property_string(p, "alang", r->preferred_audio_language ? r->preferred_audio_language : ""); - set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : ""); - { char timeout[32]; snprintf(timeout, sizeof(timeout), "%.3f", r->network_timeout > 0 ? r->network_timeout : 0.0); set_property_string(p, "network-timeout", timeout); } - if (r->playback_speed > 0) { char speed[32]; snprintf(speed, sizeof(speed), "%.4f", r->playback_speed); set_property_string(p, "speed", speed); p->snapshot.speed = r->playback_speed; } + if (r->subtitle_file_count && !r->subtitle_files) { + set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "subtitle list is null"); + pthread_mutex_unlock(&p->lock); return -1; + } + for (size_t i = 0; i < r->subtitle_file_count; i++) { + if (!r->subtitle_files[i].url || !r->subtitle_files[i].url[0]) continue; + if (!supported_subtitle_encoding(r->subtitle_files[i].encoding)) { + set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding"); + pthread_mutex_unlock(&p->lock); return -1; + } + } + if (set_headers_locked(p, r) < 0) { + set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "http-header-fields failed"); + pthread_mutex_unlock(&p->lock); return -1; + } + 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, "alang", r->preferred_audio_language ? r->preferred_audio_language : "") < 0 || + set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : "") < 0) { + pthread_mutex_unlock(&p->lock); return -1; + } + { 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"); @@ -520,7 +573,7 @@ int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_req 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_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 = ensure_ready_locked(p) < 0 ? -1 : mpv_command_string(p->handle, "stop"); if (r >= 0) 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", ""); 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) { @@ -540,14 +593,15 @@ int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, do 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_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_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 ? "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); 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_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) 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); 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); 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); 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_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; } diff --git a/test/harmony_playback_api_compile.c b/test/harmony_playback_api_compile.c index 6a83451f60..da21479816 100644 --- a/test/harmony_playback_api_compile.c +++ b/test/harmony_playback_api_compile.c @@ -4,7 +4,19 @@ int main(void) { mp_harmony_playback_request request = {0}; mp_harmony_playback_snapshot snapshot = {0}; + PlaybackRequest stable_request = {0}; + PlaybackState stable_state = {0}; + Track track = {0}; + MediaInfo media = {0}; + PlaybackMetrics metrics = {0}; + PlaybackError error = {0}; request.url = "file:///sample.mp4"; + stable_request.url = request.url; + (void)stable_state; + (void)track; + (void)media; + (void)metrics; + (void)error; (void)snapshot; return request.url == NULL; }