fix: close phase5 playback contract gaps

This commit is contained in:
BoatingCloud committed 2026-08-29 14:55:57 +08:00
1 parent 8a30e06621
commit d7cf273432
14 files changed
+374 -47

No files matched your search

+2
View File
@@ -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 错误不伪装成网络错误。
+3
View File
@@ -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 行为。
+25 -10
View File
@@ -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 事件/属性可改变对应诊断字段。
+2 -2
View File
@@ -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 不得改变新会话状态。
+1 -1
View File
@@ -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 路径入口。
+31
View File
@@ -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);
+6
View File
@@ -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
+1 -1
View File
@@ -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。
+106
View File
@@ -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<int>(track);
else if (getNamedInt64(env, argv[0], "audio_track_id", track)) request.audioTrackId = static_cast<int>(track);
if (getNamedInt64(env, argv[0], "subtitleTrackId", track)) request.subtitleTrackId = static_cast<int>(track);
else if (getNamedInt64(env, argv[0], "subtitle_track_id", track)) request.subtitleTrackId = static_cast<int>(track);
bool enabled = true;
if (getNamedBool(env, argv[0], "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0;
else if (getNamedBool(env, argv[0], "subtitle_enabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0;
napi_value headers = nullptr;
bool hasHeaders = false;
@@ -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},
+51 -12
View File
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mutex_);
snapshot_.subtitleDelayMs = static_cast<int64_t>(seconds * 1000.0);
}
return ok;
}
bool PlaybackEngine::setSubtitleScale(double scale)
@@ -444,7 +453,12 @@ bool PlaybackEngine::setSubtitleScale(double scale)
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
snapshot_.subtitleScale = scale;
}
return ok;
}
bool PlaybackEngine::setSubtitlePosition(double position)
@@ -452,7 +466,12 @@ bool PlaybackEngine::setSubtitlePosition(double position)
std::lock_guard<std::mutex> 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<std::mutex> 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<int>(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<double*>(property->data);
break;
case kDuration:
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.duration = *static_cast<double*>(property->data);
if (property->format == MPV_FORMAT_DOUBLE) {
snapshot_.duration = *static_cast<double*>(property->data);
snapshot_.media.duration = snapshot_.duration;
}
break;
case kPause:
if (property->format == MPV_FORMAT_FLAG && *static_cast<int*>(property->data) != 0 &&
@@ -886,7 +915,9 @@ void PlaybackEngine::eventLoop()
auto* message = static_cast<mpv_event_log_message*>(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<char>(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;
+6
View File
@@ -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;
@@ -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]' : ''))
+72 -18
View File
@@ -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; }
+12
View File
@@ -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;
}