feat: add phase5 playback and network capability
This commit is contained in:
1 parent
e4bc586d77
commit
8a30e06621
21 files changed
+2039
-27
No files matched your search
@@ -0,0 +1,5 @@
|
||||
# Harmony 字体策略
|
||||
|
||||
第一优先级使用系统字体;应用字体只允许由产品明确提供的开源字体;libass 使用 mpv 已配置的字体 provider,不新增商业字体包。
|
||||
|
||||
中文、英文、日文、韩文采用字体 fallback 链,缺字时交给系统/provider 继续匹配。Playback Lab 只验证字幕渲染和缺字表现,不把字体文件或用户凭证提交到仓库。设备侧若 provider 行为与模拟器不同,应记录实际字体、脚本和缺字结果。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 网络播放
|
||||
|
||||
网络播放复用 mpv/FFmpeg 的单一网络栈,不在产品中创建第二 HTTP client。支持范围是 mpv 已提供的 HTTP、HTTPS、HLS、重定向、Range seek、chunked 和大范围 seek 能力。
|
||||
|
||||
每次 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/空值,不做推断。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 网络安全
|
||||
|
||||
## Header 与会话隔离
|
||||
|
||||
每次 load 都覆盖 http-header-fields,不共享上一个 session 的 header、Cookie 或 URL。禁止把认证信息写入仓库、默认日志或诊断快照。Playback Lab 输入框可以编辑 header,但诊断默认只显示字段名和掩码值。
|
||||
|
||||
## 脱敏规则
|
||||
|
||||
Authorization、Proxy-Authorization、Cookie、X-Api-Key 以及 URL 中的 token、auth、key、signature、sig、expires 参数在错误文本和日志中替换为 [REDACTED]。原始 mpv 日志不得直接写入 OH 日志。
|
||||
|
||||
## TLS、重定向与凭证
|
||||
|
||||
默认保持 tls-verify=yes。mpv 网络后端负责 TLS、重定向、Range 和 HLS;本层不复制 HTTP 行为。重定向是否携带 header 由 mpv/FFmpeg 网络后端实际决定,若运行时没有可观测证据,不向上层承诺跨 host 凭证继承。Referer、User-Agent、header 仅属于当前加载请求。
|
||||
|
||||
实现依据:mpv 官方手册的 user-agent、referrer、cookies、http-header-fields、network-timeout、HLS 与 sub-add 选项说明。
|
||||
@@ -0,0 +1,13 @@
|
||||
# Phase 5 状态
|
||||
|
||||
状态:CODE COMPLETE / emulator-network validation pending。
|
||||
|
||||
已完成:
|
||||
|
||||
- 默认软件解码路径保持 hwdec=no,未启用 harmony-ohcodec-gl。
|
||||
- 新增 include/mpv/harmony_playback.h 稳定 C API;公共结构不包含 mpv_handle、mpv_node、AVFrame。
|
||||
- 请求字段、会话 generation、线程安全控制、稳定状态/错误、轨道、媒体信息和网络缓存诊断已实现。
|
||||
- Playback Lab 已接入请求对象、脱敏 header 编辑器、速度/音量/静音、外部字幕、轨道/媒体/网络面板。
|
||||
- 文档、测试矩阵和构建矩阵已补齐。
|
||||
|
||||
尚待真实环境确认:HTTPS/HLS、重定向 header 继承、鉴权资源、网络字幕和物理设备音频/视频输出。不可从本地编译结果推断这些运行时结论。
|
||||
@@ -0,0 +1,16 @@
|
||||
# Phase 5 测试矩阵
|
||||
|
||||
| 场景 | 方式 | 结果 |
|
||||
|---|---|---|
|
||||
| 本地 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 | 构建矩阵待执行 |
|
||||
|
||||
产品禁止以 UI 定时器伪造 buffering;只有 mpv 事件/属性可改变对应诊断字段。
|
||||
@@ -0,0 +1,7 @@
|
||||
# Playback 公共 API
|
||||
|
||||
mp_harmony_playback.h 是上层唯一稳定契约。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。
|
||||
|
||||
所有控制入口串行化;请求中的字符串在调用返回前复制到 mpv 所需的临时节点,调用者可以立即释放输入内存。session id 随每次 load 增长,旧 playlist event 不得改变新会话状态。
|
||||
@@ -0,0 +1,7 @@
|
||||
# 字幕管线
|
||||
|
||||
内部字幕和外部字幕统一由 mpv/libass 管线处理。外部源通过 sub-add 加入当前 session,可选中或移除,不销毁播放 session;sub-remove 仅移除指定轨道。
|
||||
|
||||
目标格式:SRT、ASS/SSA、WebVTT;ASS 默认保留原样式与 libass override 语义。上层可开关、选择字幕、调整 delay、scale、position、override(具体控件按实际可用属性开放)。
|
||||
|
||||
编码优先 UTF-8;GBK、Big5、Shift-JIS 只在现有 FFmpeg/mpv/iconv 能识别时启用。无法识别或不支持时返回稳定 SUBTITLE 错误,不静默按错误编码显示。
|
||||
@@ -0,0 +1,5 @@
|
||||
# Track 模型
|
||||
|
||||
上层只接收固定 Track 字段:id、type、title、language、codec、default、forced、selected、external。mpv_node 只在实现层解析,不跨 NAPI/C API 边界。
|
||||
|
||||
音频通过 aid 选择,字幕通过 sid 选择或设置为 no。语言偏好使用 mpv 的 alang/slang,可接受 zh、zho、chi、en、eng、ja、jpn 等媒体元数据值。不可用的字段保持空值或 false。
|
||||
@@ -1,7 +1,7 @@
|
||||
# Velix-MPV-Core Memory
|
||||
|
||||
- 项目:独立 HarmonyOS MPV 播放内核,禁止混入 Velix 主应用源码。
|
||||
- 当前阶段:Phase 4.5 CODE COMPLETE / DEVICE VALIDATION PENDING;GPU INTEROP MAPPER 未进入;FFmpeg-Harmony export contract 已构建验证,模拟器仅有既有 MPV/EGL smoke。
|
||||
- 当前阶段:Phase 5 CODE COMPLETE / emulator-network validation pending;GPU INTEROP MAPPER 未进入;默认软件解码与网络播放公共 API 已构建验证。
|
||||
- 基线:MPV `182fa6ca49f455cadb884858f386e2f00540aeb7`,分支 `harmony-port`。
|
||||
- SDK:OpenHarmony API 24 / 6.1.1.125 / `aarch64-linux-ohos` / Clang 15.0.4。
|
||||
- 关键结论:首版 GLES+EGL+libmpv Render API;OHAudio AO;官方 FFmpeg OHCodec 路线。
|
||||
@@ -13,4 +13,6 @@
|
||||
- Phase 4:`IMGFMT_OHCODEC`、`hwdec=ohcodec`、API24 capability adapter、direct-surface experimental driver、软件构建矩阵、Playback Lab 诊断已提交;FFmpeg 保持 upstream。
|
||||
- Phase 4 边界:官方 OHCodec hwcontext/decoder 仅提供 surface 输出,NativeBuffer→EGL 仅做禁用默认 API probe,未宣称零拷贝/HDR/真实硬件帧;详见 `DOCS/OHCODEC_GPU_INTEROP_DESIGN.md`。
|
||||
- Phase 4.5 边界:FFmpeg export 模式不调用 RenderOutputBuffer,NativeBuffer/accessor/refcount/build probes 已完成;crop/fence/真实硬解仍需设备验证,`harmony-ohcodec-gl` 继续 disabled。
|
||||
- Phase 5:include/mpv/harmony_playback.h + player/harmony_playback.c 提供不暴露 mpv 类型的稳定 PlaybackRequest/Track/Media/Network/State/Error API;Playback Lab 已接入请求 headers、脱敏、轨道、字幕和诊断面板。
|
||||
- Phase 5 构建:默认、software、FFmpeg-Harmony 三套 AArch64 libmpv 构建及 Native/ArkTS 编译通过;HAP PackageHap 因当前环境缺 Java Runtime 未完成,网络/HLS/实体设备运行待验证。
|
||||
- 索引:`memory/projects.md`、`memory/infra.md`、`memory/lessons.md`、`memory/2026-08-28.md`。
|
||||
@@ -0,0 +1,218 @@
|
||||
/* Stable Harmony playback contract. This header intentionally does not
|
||||
* expose libmpv, FFmpeg or render implementation types. */
|
||||
#ifndef MPV_HARMONY_PLAYBACK_H_
|
||||
#define MPV_HARMONY_PLAYBACK_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef MP_HARMONY_EXPORT
|
||||
#ifdef _WIN32
|
||||
#define MP_HARMONY_EXPORT __declspec(dllexport)
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
#define MP_HARMONY_EXPORT __attribute__((visibility("default")))
|
||||
#else
|
||||
#define MP_HARMONY_EXPORT
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MP_HARMONY_PLAYBACK_API_VERSION 1u
|
||||
#define MP_HARMONY_TEXT 256
|
||||
#define MP_HARMONY_URL 2048
|
||||
#define MP_HARMONY_MAX_HEADERS 32
|
||||
#define MP_HARMONY_MAX_SUBTITLES 32
|
||||
#define MP_HARMONY_MAX_TRACKS 64
|
||||
|
||||
typedef struct mp_harmony_player mp_harmony_player;
|
||||
|
||||
typedef enum mp_harmony_state {
|
||||
MP_HARMONY_IDLE = 0,
|
||||
MP_HARMONY_OPENING,
|
||||
MP_HARMONY_BUFFERING,
|
||||
MP_HARMONY_READY,
|
||||
MP_HARMONY_PLAYING,
|
||||
MP_HARMONY_PAUSED,
|
||||
MP_HARMONY_SEEKING,
|
||||
MP_HARMONY_ENDED,
|
||||
MP_HARMONY_STOPPED,
|
||||
MP_HARMONY_ERROR,
|
||||
MP_HARMONY_RELEASED,
|
||||
} mp_harmony_state;
|
||||
|
||||
typedef enum mp_harmony_error_code {
|
||||
MP_HARMONY_ERROR_NONE = 0,
|
||||
MP_HARMONY_ERROR_NETWORK,
|
||||
MP_HARMONY_ERROR_TLS,
|
||||
MP_HARMONY_ERROR_HTTP,
|
||||
MP_HARMONY_ERROR_DEMUX,
|
||||
MP_HARMONY_ERROR_UNSUPPORTED_CONTAINER,
|
||||
MP_HARMONY_ERROR_UNSUPPORTED_VIDEO_CODEC,
|
||||
MP_HARMONY_ERROR_UNSUPPORTED_AUDIO_CODEC,
|
||||
MP_HARMONY_ERROR_SUBTITLE,
|
||||
MP_HARMONY_ERROR_DECODER,
|
||||
MP_HARMONY_ERROR_AUDIO_OUTPUT,
|
||||
MP_HARMONY_ERROR_VIDEO_OUTPUT,
|
||||
MP_HARMONY_ERROR_SEEK,
|
||||
MP_HARMONY_ERROR_FILE_NOT_FOUND,
|
||||
MP_HARMONY_ERROR_PERMISSION,
|
||||
MP_HARMONY_ERROR_UNKNOWN,
|
||||
} mp_harmony_error_code;
|
||||
|
||||
typedef enum mp_harmony_seek_mode {
|
||||
MP_HARMONY_SEEK_RELATIVE = 0,
|
||||
MP_HARMONY_SEEK_ABSOLUTE,
|
||||
MP_HARMONY_SEEK_PERCENT,
|
||||
} mp_harmony_seek_mode;
|
||||
|
||||
typedef enum mp_harmony_track_type {
|
||||
MP_HARMONY_TRACK_VIDEO = 0,
|
||||
MP_HARMONY_TRACK_AUDIO,
|
||||
MP_HARMONY_TRACK_SUBTITLE,
|
||||
} mp_harmony_track_type;
|
||||
|
||||
typedef struct mp_harmony_http_header {
|
||||
const char *name;
|
||||
const char *value;
|
||||
} mp_harmony_http_header;
|
||||
|
||||
typedef struct mp_harmony_subtitle_source {
|
||||
const char *url;
|
||||
const char *title;
|
||||
const char *language;
|
||||
const char *encoding;
|
||||
int forced;
|
||||
int select;
|
||||
} mp_harmony_subtitle_source;
|
||||
|
||||
typedef struct mp_harmony_playback_request {
|
||||
const char *url;
|
||||
const char *title;
|
||||
int64_t start_position_ms; /* < 0 means unset. */
|
||||
const char *user_agent;
|
||||
const char *referer;
|
||||
const char *cookies;
|
||||
const mp_harmony_http_header *http_headers;
|
||||
size_t http_header_count;
|
||||
const mp_harmony_subtitle_source *subtitle_files;
|
||||
size_t subtitle_file_count;
|
||||
const char *preferred_audio_language;
|
||||
const char *preferred_subtitle_language;
|
||||
int subtitle_enabled; /* < 0 means unset, 0 disables, 1 enables. */
|
||||
int audio_track_id; /* < 0 means auto. */
|
||||
int subtitle_track_id; /* < 0 means auto. */
|
||||
double playback_speed; /* <= 0 means unset. */
|
||||
double network_timeout; /* <= 0 means mpv default. */
|
||||
} mp_harmony_playback_request;
|
||||
|
||||
typedef struct mp_harmony_track {
|
||||
int id;
|
||||
mp_harmony_track_type type;
|
||||
char title[MP_HARMONY_TEXT];
|
||||
char language[MP_HARMONY_TEXT];
|
||||
char codec[MP_HARMONY_TEXT];
|
||||
int is_default;
|
||||
int forced;
|
||||
int selected;
|
||||
int external;
|
||||
} mp_harmony_track;
|
||||
|
||||
typedef struct mp_harmony_media_info {
|
||||
char container[MP_HARMONY_TEXT];
|
||||
char demuxer[MP_HARMONY_TEXT];
|
||||
char video_codec[MP_HARMONY_TEXT];
|
||||
char audio_codec[MP_HARMONY_TEXT];
|
||||
char pixel_format[MP_HARMONY_TEXT];
|
||||
char color_primaries[MP_HARMONY_TEXT];
|
||||
char color_transfer[MP_HARMONY_TEXT];
|
||||
char color_space[MP_HARMONY_TEXT];
|
||||
int width;
|
||||
int height;
|
||||
int bit_depth;
|
||||
int audio_sample_rate;
|
||||
int audio_channels;
|
||||
char audio_channel_layout[MP_HARMONY_TEXT];
|
||||
double fps;
|
||||
int64_t video_bitrate;
|
||||
int64_t audio_bitrate;
|
||||
int track_count;
|
||||
int subtitle_count;
|
||||
} mp_harmony_media_info;
|
||||
|
||||
typedef struct mp_harmony_network_info {
|
||||
char protocol[MP_HARMONY_TEXT];
|
||||
char host[MP_HARMONY_TEXT];
|
||||
char network_state[MP_HARMONY_TEXT];
|
||||
int http_status;
|
||||
int redirect_count;
|
||||
int buffering;
|
||||
double cache_duration;
|
||||
int64_t cache_bytes;
|
||||
char demuxer[MP_HARMONY_TEXT];
|
||||
char container[MP_HARMONY_TEXT];
|
||||
int64_t current_bitrate;
|
||||
} mp_harmony_network_info;
|
||||
|
||||
typedef struct mp_harmony_playback_snapshot {
|
||||
uint64_t session_id;
|
||||
mp_harmony_state state;
|
||||
mp_harmony_error_code error_code;
|
||||
char error_message[MP_HARMONY_TEXT];
|
||||
double position;
|
||||
double duration;
|
||||
double fps;
|
||||
int64_t dropped_frames;
|
||||
int64_t first_frame_ms;
|
||||
int64_t render_count;
|
||||
int volume;
|
||||
int muted;
|
||||
double speed;
|
||||
int track_count;
|
||||
mp_harmony_media_info media;
|
||||
mp_harmony_network_info network;
|
||||
} mp_harmony_playback_snapshot;
|
||||
|
||||
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);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_load(mp_harmony_player *player,
|
||||
const mp_harmony_playback_request *request);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_play(mp_harmony_player *player);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_pause(mp_harmony_player *player);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_toggle_pause(mp_harmony_player *player);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_stop(mp_harmony_player *player);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_seek(mp_harmony_player *player, mp_harmony_seek_mode mode,
|
||||
double value, int exact, int keyframe);
|
||||
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_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);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_add_subtitle(mp_harmony_player *player,
|
||||
const mp_harmony_subtitle_source *source);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_remove_subtitle(mp_harmony_player *player, int track_id);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *player, double seconds);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *player, double scale);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_set_subtitle_position(mp_harmony_player *player, double position);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_set_ass_override(mp_harmony_player *player, const char *mode);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_get_tracks(mp_harmony_player *player,
|
||||
mp_harmony_track *tracks, size_t capacity,
|
||||
size_t *count);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_get_media_info(mp_harmony_player *player,
|
||||
mp_harmony_media_info *info);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_get_network_info(mp_harmony_player *player,
|
||||
mp_harmony_network_info *info);
|
||||
MP_HARMONY_EXPORT int mp_harmony_playback_get_snapshot(mp_harmony_player *player,
|
||||
mp_harmony_playback_snapshot *snapshot);
|
||||
MP_HARMONY_EXPORT const char *mp_harmony_playback_state_name(mp_harmony_state state);
|
||||
MP_HARMONY_EXPORT const char *mp_harmony_playback_error_name(mp_harmony_error_code error);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -3,3 +3,9 @@
|
||||
文件变更: ../Velix-FFmpeg-Harmony/libavcodec/ohdec.c, libavutil/hwcontext_oh.*, scripts/*, DOCS/*; Velix-MPV-Core/scripts/build-mpv-ohos*.sh, scripts/pkg-config-ohos.sh, DOCS/MPV_BUILD_REPORT.md, DOCS/PHASE4_STATUS.md, memory/*
|
||||
教训: emulator 可用于设备连接与既有 HAP/EGL 日志复核,但不能把 compile/link probe 写成 FFmpeg export 硬解运行证据;Gitea 大 pack 需分块保留完整历史。
|
||||
标签: #phase4-5 #ffmpeg #ohcodec #nativebuffer #prefix-switch #emulator #pending-device
|
||||
|
||||
[PROJECT:Velix-MPV-Core] 完成 Phase 5 Playback Capability & Network Compatibility
|
||||
结论: 新增不暴露 mpv 内部类型的稳定 Playback C API、请求/会话隔离、错误/状态/轨道/媒体/网络/字幕能力及 Playback Lab 诊断 UI;默认、software、FFmpeg-Harmony AArch64 构建与 Native/ArkTS 编译通过,HAP 打包因缺 Java Runtime 阻断,真实网络/实体设备待验证。
|
||||
文件变更: 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
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
- hdc:`/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc`
|
||||
- 模拟器目标:`127.0.0.1:5555`,HarmonyOS 6.1.0.126 / API 24
|
||||
- Playback Lab 构建:在 `playback_lab/` 执行 `scripts/stage-mpv.sh` 后用 hvigor `--no-daemon assembleHap --mode module -p product=default`
|
||||
- HAP 构建 Java:`/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home`;未配置签名时产物为 `entry-default-unsigned.hap`。
|
||||
- HAP 构建:当前环境无可用 Java Runtime,hvigor 可完成 Native/ArkTS 编译但 PackageHap 阶段失败;安装 JDK 后重试。
|
||||
- OHAudio:API 24 headers `ohaudio/native_audiostreambuilder.h` / `native_audiorenderer.h` / `native_audiostream_base.h`,动态库 `libohaudio.so`;Meson 使用 `-Dharmony-audio=enabled`。
|
||||
- Phase 3 HAP:`playback_lab/entry/build/default/outputs/default/entry-default-unsigned.hap`;模拟器 H.264/AAC smoke target `127.0.0.1:5555`。
|
||||
- Phase 4 构建入口:`scripts/build-mpv-ohos.sh`(OHCodec enabled)、`scripts/build-mpv-ohos-software.sh`(OHCodec disabled)、`scripts/build-all-ohos.sh`(全量矩阵);GL API probe 通过单独 Meson `harmony-ohcodec-gl=enabled` 构建验证。
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## Velix-MPV-Core
|
||||
|
||||
- 状态:Phase 4.5 CODE COMPLETE / DEVICE VALIDATION PENDING;GPU INTEROP MAPPER 未进入;FFmpeg-Harmony export contract 已完成独立构建/探针,实体机硬解验收未完成。
|
||||
- 状态:Phase 5 CODE COMPLETE / emulator-network validation pending;GPU INTEROP MAPPER 未进入;默认软件解码与网络播放公共 API 已完成,实体机/真实网络验收未完成。
|
||||
- 已完成:远程闭环、固定依赖、隔离工具链、C/C++ 探针、FFmpeg/OpenSSL/字体栈/libplacebo、AArch64 `libmpv.so.2.5.0`、ABI/符号/污染/许可证审计。
|
||||
- 产物:`build/ohos-arm64/prefix/lib/libmpv.so.2.5.0`,当前重建 34,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 首帧、播放控制与释放重入。
|
||||
@@ -11,3 +11,4 @@
|
||||
- 边界:官方 OHCodec 上游没有非 surface 的可导入硬件帧路径,EGL/NativeBuffer 仅保留禁用默认探针;无零拷贝、HDR 或硬件帧产出声明。
|
||||
- Phase 4.5:`../Velix-FFmpeg-Harmony` branch `harmony-ohcodec-export` 已推送 tip `990614364f`;`scripts/build-mpv-ohos-ffmpeg-harmony.sh` 已消费其 prefix 构建 MPV。
|
||||
- 待办:实体机 H.264/HEVC capability、surface 生命周期/stride/crop/fence、真实 GPU 互操作与 A/V sync;报告见两个仓库的 `DOCS/OHCODEC_*.md` 与 `DOCS/PHASE4_5_STATUS.md`。
|
||||
- Phase 5 已完成:稳定 C API、请求/headers 会话隔离、错误/状态、轨道/媒体/网络/字幕控制、Playback Lab 诊断 UI 与文档;默认/软件/FFmpeg-Harmony 构建通过,HAP 打包缺 Java,HTTP/HLS/字幕运行验证待补。
|
||||
+3
-1
@@ -168,6 +168,7 @@ sources = files(
|
||||
|
||||
## Player
|
||||
'player/audio.c',
|
||||
'player/harmony_playback.c',
|
||||
'player/client.c',
|
||||
'player/command.c',
|
||||
'player/configfiles.c',
|
||||
@@ -1914,7 +1915,8 @@ if get_option('libmpv')
|
||||
description: 'mpv media player client library')
|
||||
|
||||
headers = ['include/mpv/client.h', 'include/mpv/render.h',
|
||||
'include/mpv/render_gl.h', 'include/mpv/stream_cb.h']
|
||||
'include/mpv/render_gl.h', 'include/mpv/stream_cb.h',
|
||||
'include/mpv/harmony_playback.h']
|
||||
install_headers(headers, subdir: 'mpv')
|
||||
|
||||
# Allow projects to build with libmpv by cloning into ./subprojects/mpv
|
||||
|
||||
@@ -12,3 +12,5 @@
|
||||
`stage-mpv.sh` 只把本地 Phase 1 产物复制到未跟踪的 HAP native 库目录,不把 34 MB 的二进制提交到仓库。
|
||||
|
||||
面板中的 `OHCodec Probe` 只请求 `hwdec=ohcodec`,不会把当前 EGL window 注入 Direct Surface。没有独占 `ohos_direct_surface` 时,MPV 必须回退软件解码;面板会显示 requested/actual decoder、hwdec state、capability 摘要和回退原因。
|
||||
|
||||
Phase 5 的网络请求对象、轨道/媒体/网络诊断和字幕控制见 `DOCS/PLAYBACK_PUBLIC_API.md` 及 `DOCS/NETWORK_SECURITY.md`。header 输入仅用于当前 load,诊断和日志脱敏;默认构建仍使用软件解码。
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <hilog/log.h>
|
||||
#include <napi/native_api.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#undef LOG_DOMAIN
|
||||
@@ -56,6 +57,94 @@ bool getDoubleArg(napi_env env, napi_callback_info info, double& value)
|
||||
return napi_get_value_double(env, argv[0], &value) == napi_ok;
|
||||
}
|
||||
|
||||
bool getNamedString(napi_env env, napi_value object, const char* name, std::string& value)
|
||||
{
|
||||
bool has = false;
|
||||
if (napi_has_named_property(env, object, name, &has) != napi_ok || !has) return false;
|
||||
napi_value property = nullptr;
|
||||
size_t size = 0;
|
||||
if (napi_get_named_property(env, object, name, &property) != napi_ok ||
|
||||
napi_get_value_string_utf8(env, property, nullptr, 0, &size) != napi_ok) return false;
|
||||
value.resize(size + 1);
|
||||
if (napi_get_value_string_utf8(env, property, value.data(), value.size(), &size) != napi_ok) return false;
|
||||
value.resize(size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getNamedDouble(napi_env env, napi_value object, const char* name, double& value)
|
||||
{
|
||||
napi_value property = nullptr;
|
||||
bool has = false;
|
||||
return napi_has_named_property(env, object, name, &has) == napi_ok && has &&
|
||||
napi_get_named_property(env, object, name, &property) == napi_ok &&
|
||||
napi_get_value_double(env, property, &value) == napi_ok;
|
||||
}
|
||||
|
||||
bool getNamedInt64(napi_env env, napi_value object, const char* name, int64_t& value)
|
||||
{
|
||||
double number = 0;
|
||||
if (!getNamedDouble(env, object, name, number)) return false;
|
||||
value = static_cast<int64_t>(number);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getNamedBool(napi_env env, napi_value object, const char* name, bool& value)
|
||||
{
|
||||
napi_value property = nullptr;
|
||||
bool has = false;
|
||||
return napi_has_named_property(env, object, name, &has) == napi_ok && has &&
|
||||
napi_get_named_property(env, object, name, &property) == napi_ok &&
|
||||
napi_get_value_bool(env, property, &value) == napi_ok;
|
||||
}
|
||||
|
||||
bool getRequest(napi_env env, napi_callback_info info, PlaybackRequest& request)
|
||||
{
|
||||
size_t argc = 1;
|
||||
napi_value argv[1] = {nullptr};
|
||||
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return false;
|
||||
napi_valuetype type = napi_undefined;
|
||||
napi_typeof(env, argv[0], &type);
|
||||
if (type == napi_string) {
|
||||
return getStringArg(env, info, request.url);
|
||||
}
|
||||
if (type != napi_object) return false;
|
||||
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);
|
||||
getNamedString(env, argv[0], "referer", request.referer);
|
||||
getNamedString(env, argv[0], "cookies", request.cookies);
|
||||
getNamedString(env, argv[0], "preferredAudioLanguage", request.preferredAudioLanguage);
|
||||
getNamedString(env, argv[0], "preferredSubtitleLanguage", request.preferredSubtitleLanguage);
|
||||
double number = 0;
|
||||
if (getNamedDouble(env, argv[0], "playbackSpeed", number)) request.playbackSpeed = number;
|
||||
if (getNamedDouble(env, argv[0], "networkTimeout", number)) request.networkTimeout = number;
|
||||
int64_t start = -1;
|
||||
if (getNamedInt64(env, argv[0], "startPositionMs", start)) request.startPositionMs = start;
|
||||
int64_t track = -1;
|
||||
if (getNamedInt64(env, argv[0], "audioTrackId", track)) request.audioTrackId = static_cast<int>(track);
|
||||
if (getNamedInt64(env, argv[0], "subtitleTrackId", track)) request.subtitleTrackId = static_cast<int>(track);
|
||||
bool enabled = true;
|
||||
if (getNamedBool(env, argv[0], "subtitleEnabled", enabled)) request.subtitleEnabled = enabled ? 1 : 0;
|
||||
|
||||
napi_value headers = nullptr;
|
||||
bool hasHeaders = false;
|
||||
if (napi_has_named_property(env, argv[0], "httpHeaders", &hasHeaders) == napi_ok && hasHeaders &&
|
||||
napi_get_named_property(env, argv[0], "httpHeaders", &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));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
napi_value undefined(napi_env env)
|
||||
{
|
||||
napi_value result = nullptr;
|
||||
@@ -65,8 +154,8 @@ napi_value undefined(napi_env env)
|
||||
|
||||
napi_value load(napi_env env, napi_callback_info info)
|
||||
{
|
||||
std::string url;
|
||||
if (getStringArg(env, info, url)) gPlayer.load(url);
|
||||
PlaybackRequest request;
|
||||
if (getRequest(env, info, request)) gPlayer.load(request);
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
@@ -111,6 +200,12 @@ napi_value pause(napi_env env, napi_callback_info)
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
napi_value togglePause(napi_env env, napi_callback_info)
|
||||
{
|
||||
gPlayer.togglePause();
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
napi_value seek(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double delta = 0;
|
||||
@@ -118,6 +213,138 @@ napi_value seek(napi_env env, napi_callback_info info)
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
napi_value seekAbsolute(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double seconds = 0;
|
||||
if (getDoubleArg(env, info, seconds)) gPlayer.seekAbsolute(seconds, false, false);
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
napi_value seekPercent(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double percent = 0;
|
||||
if (getDoubleArg(env, info, percent)) gPlayer.seekPercent(percent, false, false);
|
||||
return undefined(env);
|
||||
}
|
||||
|
||||
napi_value setSpeed(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double speed = 1;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, speed) && gPlayer.setSpeed(speed), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setVolume(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double volume = 100;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, volume) && gPlayer.setVolume(volume), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setMute(napi_env env, napi_callback_info info)
|
||||
{
|
||||
size_t argc = 1;
|
||||
napi_value argv[1] = {nullptr};
|
||||
bool muted = false;
|
||||
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1 ||
|
||||
napi_get_value_bool(env, argv[0], &muted) != napi_ok) return undefined(env);
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, gPlayer.setMute(muted), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value selectAudioTrack(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double id = -1;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.selectAudioTrack(static_cast<int>(id)), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value selectSubtitleTrack(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double id = -1;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.selectSubtitleTrack(static_cast<int>(id)), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value disableSubtitle(napi_env env, napi_callback_info)
|
||||
{
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, gPlayer.disableSubtitle(), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value addSubtitle(napi_env env, napi_callback_info info)
|
||||
{
|
||||
PlaybackSubtitleSource source;
|
||||
size_t argc = 1;
|
||||
napi_value argv[1] = {nullptr};
|
||||
if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc < 1) return undefined(env);
|
||||
napi_valuetype type = napi_undefined;
|
||||
napi_typeof(env, argv[0], &type);
|
||||
if (type == napi_string) {
|
||||
size_t size = 0;
|
||||
napi_get_value_string_utf8(env, argv[0], nullptr, 0, &size);
|
||||
source.url.resize(size + 1);
|
||||
napi_get_value_string_utf8(env, argv[0], source.url.data(), source.url.size(), &size);
|
||||
source.url.resize(size);
|
||||
} else if (type == napi_object) {
|
||||
getNamedString(env, argv[0], "url", source.url);
|
||||
getNamedString(env, argv[0], "title", source.title);
|
||||
getNamedString(env, argv[0], "language", source.language);
|
||||
getNamedString(env, argv[0], "encoding", source.encoding);
|
||||
getNamedBool(env, argv[0], "forced", source.forced);
|
||||
getNamedBool(env, argv[0], "select", source.select);
|
||||
}
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, gPlayer.addSubtitle(source), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value removeSubtitle(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double id = -1;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, id) && gPlayer.removeSubtitle(static_cast<int>(id)), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setSubtitleDelay(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double seconds = 0;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, seconds) && gPlayer.setSubtitleDelay(seconds), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setSubtitleScale(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double scale = 1;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, scale) && gPlayer.setSubtitleScale(scale), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setSubtitlePosition(napi_env env, napi_callback_info info)
|
||||
{
|
||||
double position = 100;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getDoubleArg(env, info, position) && gPlayer.setSubtitlePosition(position), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value setAssOverride(napi_env env, napi_callback_info info)
|
||||
{
|
||||
std::string mode;
|
||||
napi_value result = nullptr;
|
||||
napi_get_boolean(env, getStringArg(env, info, mode) && gPlayer.setAssOverride(mode), &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value stop(napi_env env, napi_callback_info)
|
||||
{
|
||||
gPlayer.stop();
|
||||
@@ -155,6 +382,88 @@ napi_value getState(napi_env env, napi_callback_info)
|
||||
napi_set_named_property(env, object, "renderCount", value);
|
||||
napi_create_string_utf8(env, state.lastError.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, object, "lastError", value);
|
||||
napi_create_string_utf8(env, state.errorCode.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, object, "errorCode", value);
|
||||
napi_create_double(env, static_cast<double>(state.sessionId), &value);
|
||||
napi_set_named_property(env, object, "sessionId", value);
|
||||
napi_create_double(env, state.speed, &value);
|
||||
napi_set_named_property(env, object, "speed", value);
|
||||
napi_create_double(env, state.volume, &value);
|
||||
napi_set_named_property(env, object, "volume", value);
|
||||
napi_get_boolean(env, state.muted, &value);
|
||||
napi_set_named_property(env, object, "muted", value);
|
||||
napi_value network = nullptr;
|
||||
napi_create_object(env, &network);
|
||||
napi_create_string_utf8(env, state.network.protocol.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, network, "protocol", value);
|
||||
napi_create_string_utf8(env, state.network.host.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, network, "host", value);
|
||||
napi_create_string_utf8(env, state.network.networkState.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, network, "networkState", value);
|
||||
napi_create_int32(env, state.network.httpStatus, &value);
|
||||
napi_set_named_property(env, network, "httpStatus", value);
|
||||
napi_create_int32(env, state.network.redirectCount, &value);
|
||||
napi_set_named_property(env, network, "redirectCount", value);
|
||||
napi_get_boolean(env, state.network.buffering, &value);
|
||||
napi_set_named_property(env, network, "buffering", value);
|
||||
napi_create_double(env, state.network.cacheDuration, &value);
|
||||
napi_set_named_property(env, network, "cacheDuration", value);
|
||||
napi_create_int64(env, state.network.cacheBytes, &value);
|
||||
napi_set_named_property(env, network, "cacheBytes", value);
|
||||
napi_create_string_utf8(env, state.network.demuxer.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, network, "demuxer", value);
|
||||
napi_create_string_utf8(env, state.network.container.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, network, "container", value);
|
||||
napi_create_int64(env, state.network.currentBitrate, &value);
|
||||
napi_set_named_property(env, network, "currentBitrate", value);
|
||||
napi_set_named_property(env, object, "network", network);
|
||||
napi_value media = nullptr;
|
||||
napi_create_object(env, &media);
|
||||
napi_create_string_utf8(env, state.media.container.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
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_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_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_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_int32(env, state.media.trackCount, &value);
|
||||
napi_set_named_property(env, media, "trackCount", value);
|
||||
napi_create_int32(env, state.media.subtitleCount, &value);
|
||||
napi_set_named_property(env, media, "subtitleCount", value);
|
||||
napi_set_named_property(env, object, "media", media);
|
||||
napi_value tracks = nullptr;
|
||||
napi_create_array_with_length(env, state.tracks.size(), &tracks);
|
||||
for (size_t i = 0; i < state.tracks.size(); ++i) {
|
||||
const PlaybackTrack& track = state.tracks[i];
|
||||
napi_value item = nullptr;
|
||||
napi_create_object(env, &item);
|
||||
napi_create_int32(env, track.id, &value); napi_set_named_property(env, item, "id", value);
|
||||
napi_create_string_utf8(env, track.type.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "type", value);
|
||||
napi_create_string_utf8(env, track.title.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "title", value);
|
||||
napi_create_string_utf8(env, track.language.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "language", value);
|
||||
napi_create_string_utf8(env, track.codec.c_str(), NAPI_AUTO_LENGTH, &value); napi_set_named_property(env, item, "codec", value);
|
||||
napi_get_boolean(env, track.isDefault, &value); napi_set_named_property(env, item, "default", value);
|
||||
napi_get_boolean(env, track.forced, &value); napi_set_named_property(env, item, "forced", value);
|
||||
napi_get_boolean(env, track.selected, &value); napi_set_named_property(env, item, "selected", value);
|
||||
napi_get_boolean(env, track.external, &value); napi_set_named_property(env, item, "external", value);
|
||||
napi_set_element(env, tracks, i, item);
|
||||
}
|
||||
napi_set_named_property(env, object, "tracks", tracks);
|
||||
napi_create_string_utf8(env, state.requestedDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
napi_set_named_property(env, object, "requestedDecoder", value);
|
||||
napi_create_string_utf8(env, state.actualDecoder.c_str(), NAPI_AUTO_LENGTH, &value);
|
||||
@@ -240,7 +549,22 @@ napi_value Init(napi_env env, napi_value exports)
|
||||
{"setAudioEnabled", nullptr, setAudioEnabled, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"setHardwareDecode", nullptr, setHardwareDecode, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"pause", nullptr, pause, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"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},
|
||||
{"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},
|
||||
{"setMute", nullptr, setMute, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"selectAudioTrack", nullptr, selectAudioTrack, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"selectSubtitleTrack", nullptr, selectSubtitleTrack, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"disableSubtitle", nullptr, disableSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"addSubtitle", nullptr, addSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"removeSubtitle", nullptr, removeSubtitle, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"setSubtitleDelay", nullptr, setSubtitleDelay, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"setSubtitleScale", nullptr, setSubtitleScale, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"setSubtitlePosition", nullptr, setSubtitlePosition, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"setAssOverride", nullptr, setAssOverride, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"stop", nullptr, stop, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"release", nullptr, release, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"getState", nullptr, getState, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#undef LOG_DOMAIN
|
||||
#undef LOG_TAG
|
||||
@@ -20,12 +22,39 @@ constexpr uint64_t kDuration = 2;
|
||||
constexpr uint64_t kPause = 3;
|
||||
constexpr uint64_t kFps = 4;
|
||||
constexpr uint64_t kDropped = 5;
|
||||
constexpr uint64_t kTracks = 9;
|
||||
constexpr uint64_t kCacheDuration = 10;
|
||||
constexpr uint64_t kCacheState = 11;
|
||||
constexpr uint64_t kBuffering = 12;
|
||||
constexpr uint64_t kVolume = 13;
|
||||
constexpr uint64_t kMute = 14;
|
||||
constexpr uint64_t kSpeed = 15;
|
||||
|
||||
void logMpvError(const char* operation, int code)
|
||||
{
|
||||
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s failed: %{public}s (%{public}d)",
|
||||
operation, mpv_error_string(code), code);
|
||||
}
|
||||
|
||||
bool containsInsensitive(const std::string& value, const char* needle)
|
||||
{
|
||||
std::string lower = value;
|
||||
std::string target = needle ? needle : "";
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::transform(target.begin(), target.end(), target.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return !target.empty() && lower.find(target) != std::string::npos;
|
||||
}
|
||||
|
||||
bool supportedSubtitleEncoding(const std::string& encoding)
|
||||
{
|
||||
if (encoding.empty()) return true;
|
||||
std::string value = encoding;
|
||||
std::transform(value.begin(), value.end(), value.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return value == "utf-8" || value == "utf8" || value == "gbk" ||
|
||||
value == "gb18030" || value == "big5" || value == "shift-jis" ||
|
||||
value == "sjis";
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackEngine::PlaybackEngine()
|
||||
@@ -80,7 +109,7 @@ bool PlaybackEngine::initialize()
|
||||
initialized_.store(true);
|
||||
stopEvents_.store(false);
|
||||
startedAt_ = std::chrono::steady_clock::now();
|
||||
setState("PLAYER_CREATED");
|
||||
setState("IDLE");
|
||||
eventThread_ = std::thread(&PlaybackEngine::eventLoop, this);
|
||||
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "libmpv initialized");
|
||||
return true;
|
||||
@@ -88,6 +117,7 @@ bool PlaybackEngine::initialize()
|
||||
|
||||
bool PlaybackEngine::setAudioEnabled(bool enabled)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr)
|
||||
return false;
|
||||
int code = mpv_set_property_string(handle_, "aid", enabled ? "auto" : "no");
|
||||
@@ -101,6 +131,7 @@ bool PlaybackEngine::setAudioEnabled(bool enabled)
|
||||
|
||||
bool PlaybackEngine::setHardwareDecode(bool enabled)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr)
|
||||
return false;
|
||||
int code = mpv_set_property_string(handle_, "hwdec", enabled ? "ohcodec" : "no");
|
||||
@@ -133,35 +164,106 @@ void PlaybackEngine::observeProperties()
|
||||
mpv_observe_property(handle_, kHwdecCurrent, "hwdec-current", MPV_FORMAT_STRING);
|
||||
mpv_observe_property(handle_, kHwdecInterop, "hwdec-interop", MPV_FORMAT_STRING);
|
||||
mpv_observe_property(handle_, kDecoder, "decoder", MPV_FORMAT_STRING);
|
||||
mpv_observe_property(handle_, kTracks, "track-list", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(handle_, kCacheDuration, "demuxer-cache-duration", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(handle_, kCacheState, "demuxer-cache-state", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(handle_, kBuffering, "cache-buffering-state", MPV_FORMAT_INT64);
|
||||
mpv_observe_property(handle_, kVolume, "volume", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(handle_, kMute, "mute", MPV_FORMAT_FLAG);
|
||||
mpv_observe_property(handle_, kSpeed, "speed", MPV_FORMAT_DOUBLE);
|
||||
}
|
||||
|
||||
void PlaybackEngine::load(const std::string& url)
|
||||
{
|
||||
if (!initialize() || url.empty()) {
|
||||
setError(url.empty() ? "empty URL" : "player unavailable");
|
||||
return;
|
||||
PlaybackRequest request;
|
||||
request.url = url;
|
||||
load(request);
|
||||
}
|
||||
|
||||
bool PlaybackEngine::load(const PlaybackRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!initialize() || request.url.empty()) {
|
||||
setError(request.url.empty() ? "empty URL" : "player unavailable");
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
{
|
||||
std::lock_guard<std::mutex> stateLock(mutex_);
|
||||
snapshot_.position = 0.0;
|
||||
snapshot_.duration = 0.0;
|
||||
snapshot_.firstFrameMs = -1;
|
||||
snapshot_.renderCount = 0;
|
||||
snapshot_.lastError.clear();
|
||||
snapshot_.errorCode = "NONE";
|
||||
snapshot_.media = PlaybackMediaInfo{};
|
||||
snapshot_.network = PlaybackNetworkInfo{};
|
||||
snapshot_.tracks.clear();
|
||||
snapshot_.sessionId = ++sessionId_;
|
||||
awaitingStart_ = true;
|
||||
startedAt_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
const char* command[] = {"loadfile", url.c_str(), "replace", nullptr};
|
||||
if (!setHeadersLocked(request)) {
|
||||
setError("http-header-fields failed");
|
||||
return false;
|
||||
}
|
||||
if (mpv_set_property_string(handle_, "user-agent", request.userAgent.c_str()) < 0)
|
||||
setError("user-agent failed");
|
||||
if (mpv_set_property_string(handle_, "referrer", request.referer.c_str()) < 0)
|
||||
setError("referrer failed");
|
||||
mpv_set_property_string(handle_, "alang", request.preferredAudioLanguage.c_str());
|
||||
mpv_set_property_string(handle_, "slang", request.preferredSubtitleLanguage.c_str());
|
||||
std::string timeout = request.networkTimeout > 0 ? std::to_string(request.networkTimeout) : "0";
|
||||
mpv_set_property_string(handle_, "network-timeout", timeout.c_str());
|
||||
if (request.playbackSpeed > 0) {
|
||||
std::string speed = std::to_string(request.playbackSpeed);
|
||||
mpv_set_property_string(handle_, "speed", speed.c_str());
|
||||
snapshot_.speed = request.playbackSpeed;
|
||||
}
|
||||
std::string audioId = request.audioTrackId >= 0 ? std::to_string(request.audioTrackId) : "auto";
|
||||
mpv_set_property_string(handle_, "aid", audioId.c_str());
|
||||
if (request.subtitleTrackId >= 0) {
|
||||
std::string id = std::to_string(request.subtitleTrackId);
|
||||
mpv_set_property_string(handle_, "sid", id.c_str());
|
||||
} else if (request.subtitleEnabled == 0) {
|
||||
mpv_set_property_string(handle_, "sid", "no");
|
||||
} else {
|
||||
mpv_set_property_string(handle_, "sid", "auto");
|
||||
}
|
||||
const char* command[] = {"loadfile", request.url.c_str(), "replace", nullptr};
|
||||
int code = mpv_command(handle_, command);
|
||||
if (code < 0) {
|
||||
logMpvError("loadfile", code);
|
||||
setError(std::string("load failed: ") + mpv_error_string(code));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
setState("PREPARING");
|
||||
for (const auto& subtitle : request.subtitleFiles) {
|
||||
if (subtitle.url.empty()) continue;
|
||||
if (!supportedSubtitleEncoding(subtitle.encoding)) {
|
||||
setError("unsupported subtitle encoding");
|
||||
return false;
|
||||
}
|
||||
if (!subtitle.encoding.empty())
|
||||
mpv_set_property_string(handle_, "sub-codepage", subtitle.encoding.c_str());
|
||||
const char* flags = subtitle.select ? "select" : "auto";
|
||||
const char* subCommand[] = {"sub-add", subtitle.url.c_str(), flags,
|
||||
subtitle.title.c_str(), subtitle.language.c_str(), nullptr};
|
||||
mpv_command(handle_, subCommand);
|
||||
}
|
||||
if (request.startPositionMs >= 0) {
|
||||
std::string seconds = std::to_string(static_cast<double>(request.startPositionMs) / 1000.0);
|
||||
const char* seekCommand[] = {"seek", seconds.c_str(), "absolute", "exact", nullptr};
|
||||
mpv_command(handle_, seekCommand);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> stateLock(mutex_);
|
||||
snapshot_.state = "OPENING";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaybackEngine::play()
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) {
|
||||
setError("player unavailable");
|
||||
return;
|
||||
@@ -177,6 +279,7 @@ void PlaybackEngine::play()
|
||||
|
||||
void PlaybackEngine::pause()
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -189,8 +292,179 @@ void PlaybackEngine::pause()
|
||||
setState("PAUSED");
|
||||
}
|
||||
|
||||
void PlaybackEngine::togglePause()
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) return;
|
||||
int paused = 0;
|
||||
if (mpv_get_property(handle_, "pause", MPV_FORMAT_FLAG, &paused) < 0) {
|
||||
setError("toggle pause failed");
|
||||
return;
|
||||
}
|
||||
if (mpv_set_property_string(handle_, "pause", paused ? "no" : "yes") < 0) {
|
||||
setError("toggle pause failed");
|
||||
return;
|
||||
}
|
||||
setState(paused ? "PLAYING" : "PAUSED");
|
||||
}
|
||||
|
||||
bool PlaybackEngine::seekAbsolute(double seconds, bool exact, bool keyframe)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || !std::isfinite(seconds) || seconds < 0) return false;
|
||||
std::string amount = std::to_string(seconds);
|
||||
const char* mode = exact ? "exact" : (keyframe ? "keyframes" : "default");
|
||||
const char* command[] = {"seek", amount.c_str(), "absolute", mode, nullptr};
|
||||
int code = mpv_command(handle_, command);
|
||||
if (code < 0) {
|
||||
setError(std::string("seek failed: ") + mpv_error_string(code));
|
||||
return false;
|
||||
}
|
||||
setState("SEEKING");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::seekPercent(double percent, bool exact, bool keyframe)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || !std::isfinite(percent)) return false;
|
||||
percent = std::clamp(percent, 0.0, 100.0);
|
||||
std::string amount = std::to_string(percent);
|
||||
const char* mode = exact ? "exact" : (keyframe ? "keyframes" : "default");
|
||||
const char* command[] = {"seek", amount.c_str(), "absolute-percent", mode, nullptr};
|
||||
int code = mpv_command(handle_, command);
|
||||
if (code < 0) {
|
||||
setError(std::string("seek percent failed: ") + mpv_error_string(code));
|
||||
return false;
|
||||
}
|
||||
setState("SEEKING");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::setSpeed(double speed)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || !std::isfinite(speed) || speed <= 0) return false;
|
||||
std::string value = std::to_string(speed);
|
||||
if (mpv_set_property_string(handle_, "speed", value.c_str()) < 0) {
|
||||
setError("speed failed");
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.speed = speed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::setVolume(double volume)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || !std::isfinite(volume) || volume < 0) return false;
|
||||
std::string value = std::to_string(volume);
|
||||
if (mpv_set_property_string(handle_, "volume", value.c_str()) < 0) {
|
||||
setError("volume failed");
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.volume = volume;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::setMute(bool muted)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) return false;
|
||||
if (mpv_set_property_string(handle_, "mute", muted ? "yes" : "no") < 0) {
|
||||
setError("mute failed");
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.muted = muted;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::selectAudioTrack(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || id < 0) return false;
|
||||
std::string value = std::to_string(id);
|
||||
return mpv_set_property_string(handle_, "aid", value.c_str()) >= 0;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::selectSubtitleTrack(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || id < 0) return false;
|
||||
std::string value = std::to_string(id);
|
||||
return mpv_set_property_string(handle_, "sid", value.c_str()) >= 0;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::disableSubtitle()
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
return handle_ != nullptr && mpv_set_property_string(handle_, "sid", "no") >= 0;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::addSubtitle(const PlaybackSubtitleSource& source)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || source.url.empty()) return false;
|
||||
if (!supportedSubtitleEncoding(source.encoding)) {
|
||||
setError("unsupported subtitle encoding");
|
||||
return false;
|
||||
}
|
||||
if (!source.encoding.empty())
|
||||
mpv_set_property_string(handle_, "sub-codepage", source.encoding.c_str());
|
||||
const char* flags = source.select ? "select" : "auto";
|
||||
const char* command[] = {"sub-add", source.url.c_str(), flags, source.title.c_str(),
|
||||
source.language.c_str(), nullptr};
|
||||
int code = mpv_command(handle_, command);
|
||||
if (code < 0) setError(std::string("subtitle failed: ") + mpv_error_string(code));
|
||||
return code >= 0;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::removeSubtitle(int id)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || id < 0) return false;
|
||||
std::string value = std::to_string(id);
|
||||
const char* command[] = {"sub-remove", value.c_str(), nullptr};
|
||||
return mpv_command(handle_, command) >= 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::setAssOverride(const std::string& mode)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr || mode.empty()) return false;
|
||||
return mpv_set_property_string(handle_, "sub-ass-override", mode.c_str()) >= 0;
|
||||
}
|
||||
|
||||
void PlaybackEngine::seek(double delta)
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -218,6 +492,7 @@ void PlaybackEngine::seek(double delta)
|
||||
|
||||
void PlaybackEngine::stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -243,6 +518,7 @@ void PlaybackEngine::release()
|
||||
if (eventThread_.joinable()) {
|
||||
eventThread_.join();
|
||||
}
|
||||
std::lock_guard<std::mutex> apiLock(apiMutex_);
|
||||
if (handle_ != nullptr) {
|
||||
mpv_terminate_destroy(handle_);
|
||||
handle_ = nullptr;
|
||||
@@ -274,6 +550,173 @@ PlaybackSnapshot PlaybackEngine::snapshot() const
|
||||
return snapshot_;
|
||||
}
|
||||
|
||||
bool PlaybackEngine::setHeadersLocked(const PlaybackRequest& request)
|
||||
{
|
||||
std::vector<std::string> values;
|
||||
values.reserve(request.headers.size() + 1);
|
||||
for (const auto& header : request.headers) {
|
||||
if (!header.name.empty())
|
||||
values.push_back(header.name + ": " + header.value);
|
||||
}
|
||||
if (!request.cookies.empty())
|
||||
values.push_back("Cookie: " + request.cookies);
|
||||
std::vector<mpv_node> nodes(values.size());
|
||||
for (size_t i = 0; i < values.size(); ++i) {
|
||||
nodes[i].format = MPV_FORMAT_STRING;
|
||||
nodes[i].u.string = const_cast<char*>(values[i].c_str());
|
||||
}
|
||||
mpv_node_list list = {};
|
||||
list.num = static_cast<int>(nodes.size());
|
||||
list.values = nodes.empty() ? nullptr : nodes.data();
|
||||
mpv_node node = {};
|
||||
node.format = MPV_FORMAT_NODE_ARRAY;
|
||||
node.u.list = &list;
|
||||
int code = mpv_set_property(handle_, "http-header-fields", MPV_FORMAT_NODE, &node);
|
||||
if (code < 0) {
|
||||
logMpvError("http-header-fields", code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaybackEngine::refreshTracksLocked()
|
||||
{
|
||||
if (handle_ == nullptr) return;
|
||||
mpv_node node = {};
|
||||
if (mpv_get_property(handle_, "track-list", MPV_FORMAT_NODE, &node) < 0) return;
|
||||
snapshot_.tracks.clear();
|
||||
if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list != nullptr) {
|
||||
for (int i = 0; i < node.u.list->num; ++i) {
|
||||
const mpv_node& item = node.u.list->values[i];
|
||||
if (item.format != MPV_FORMAT_NODE_MAP || item.u.list == nullptr) continue;
|
||||
PlaybackTrack track;
|
||||
for (int j = 0; j < item.u.list->num; ++j) {
|
||||
const char* key = item.u.list->keys[j];
|
||||
const mpv_node& value = item.u.list->values[j];
|
||||
if (key == nullptr) continue;
|
||||
if (std::strcmp(key, "id") == 0 && value.format == MPV_FORMAT_INT64)
|
||||
track.id = static_cast<int>(value.u.int64);
|
||||
else if (std::strcmp(key, "type") == 0 && value.format == MPV_FORMAT_STRING)
|
||||
track.type = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "title") == 0 && value.format == MPV_FORMAT_STRING)
|
||||
track.title = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "lang") == 0 && value.format == MPV_FORMAT_STRING)
|
||||
track.language = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "codec") == 0 && value.format == MPV_FORMAT_STRING)
|
||||
track.codec = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "default") == 0 && value.format == MPV_FORMAT_FLAG)
|
||||
track.isDefault = value.u.flag != 0;
|
||||
else if (std::strcmp(key, "forced") == 0 && value.format == MPV_FORMAT_FLAG)
|
||||
track.forced = value.u.flag != 0;
|
||||
else if (std::strcmp(key, "selected") == 0 && value.format == MPV_FORMAT_FLAG)
|
||||
track.selected = value.u.flag != 0;
|
||||
else if (std::strcmp(key, "external") == 0 && value.format == MPV_FORMAT_FLAG)
|
||||
track.external = value.u.flag != 0;
|
||||
}
|
||||
snapshot_.tracks.push_back(std::move(track));
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
snapshot_.media.trackCount = static_cast<int>(snapshot_.tracks.size());
|
||||
snapshot_.media.subtitleCount = static_cast<int>(std::count_if(
|
||||
snapshot_.tracks.begin(), snapshot_.tracks.end(),
|
||||
[](const PlaybackTrack& track) { return track.type == "sub"; }));
|
||||
}
|
||||
|
||||
void PlaybackEngine::refreshMediaLocked()
|
||||
{
|
||||
if (handle_ == nullptr) return;
|
||||
auto getString = [this](const char* name) {
|
||||
char* value = mpv_get_property_string(handle_, name);
|
||||
std::string result = value ? value : "";
|
||||
if (value) mpv_free(value);
|
||||
return result;
|
||||
};
|
||||
snapshot_.media.container = getString("file-format");
|
||||
snapshot_.media.demuxer = getString("current-demuxer");
|
||||
snapshot_.media.videoCodec = getString("video-codec");
|
||||
snapshot_.media.audioCodec = getString("audio-codec");
|
||||
snapshot_.media.pixelFormat = getString("video-format");
|
||||
int64_t number = 0;
|
||||
if (mpv_get_property(handle_, "width", MPV_FORMAT_INT64, &number) >= 0) snapshot_.media.width = static_cast<int>(number);
|
||||
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_, "container-fps", MPV_FORMAT_DOUBLE, &snapshot_.media.fps) < 0) snapshot_.media.fps = 0;
|
||||
mpv_node node = {};
|
||||
if (mpv_get_property(handle_, "video-params", MPV_FORMAT_NODE, &node) >= 0) {
|
||||
if (node.format == MPV_FORMAT_NODE_MAP && node.u.list != nullptr) {
|
||||
for (int i = 0; i < node.u.list->num; ++i) {
|
||||
const char* key = node.u.list->keys[i];
|
||||
const mpv_node& value = node.u.list->values[i];
|
||||
if (!key) continue;
|
||||
if (std::strcmp(key, "pixelformat") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.pixelFormat = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "primaries") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorPrimaries = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "gamma") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorTransfer = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "colormatrix") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.colorSpace = value.u.string ? value.u.string : "";
|
||||
else if (std::strcmp(key, "bit-depth") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.bitDepth = static_cast<int>(value.u.int64);
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
}
|
||||
node = {};
|
||||
if (mpv_get_property(handle_, "audio-params", MPV_FORMAT_NODE, &node) >= 0) {
|
||||
if (node.format == MPV_FORMAT_NODE_MAP && node.u.list != nullptr) {
|
||||
for (int i = 0; i < node.u.list->num; ++i) {
|
||||
const char* key = node.u.list->keys[i];
|
||||
const mpv_node& value = node.u.list->values[i];
|
||||
if (!key) continue;
|
||||
if (std::strcmp(key, "samplerate") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.audioSampleRate = static_cast<int>(value.u.int64);
|
||||
else if (std::strcmp(key, "channels") == 0 && value.format == MPV_FORMAT_INT64) snapshot_.media.audioChannels = static_cast<int>(value.u.int64);
|
||||
else if (std::strcmp(key, "channel-layout") == 0 && value.format == MPV_FORMAT_STRING) snapshot_.media.audioChannelLayout = value.u.string ? value.u.string : "";
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
}
|
||||
refreshTracksLocked();
|
||||
refreshNetworkLocked();
|
||||
}
|
||||
|
||||
void PlaybackEngine::refreshNetworkLocked()
|
||||
{
|
||||
if (handle_ == nullptr) return;
|
||||
const std::string url = [&] {
|
||||
char* value = mpv_get_property_string(handle_, "stream-open-filename");
|
||||
std::string result = value ? value : "";
|
||||
if (value) mpv_free(value);
|
||||
return result;
|
||||
}();
|
||||
const size_t schemeEnd = url.find("://");
|
||||
if (schemeEnd != std::string::npos) {
|
||||
snapshot_.network.protocol = url.substr(0, schemeEnd);
|
||||
const size_t hostStart = schemeEnd + 3;
|
||||
const size_t hostEnd = url.find_first_of("/?#", hostStart);
|
||||
snapshot_.network.host = url.substr(hostStart, hostEnd == std::string::npos ? std::string::npos : hostEnd - hostStart);
|
||||
}
|
||||
int network = 0;
|
||||
if (mpv_get_property(handle_, "demuxer-via-network", MPV_FORMAT_FLAG, &network) >= 0)
|
||||
snapshot_.network.networkState = network ? "network" : "local";
|
||||
if (mpv_get_property(handle_, "demuxer-cache-duration", MPV_FORMAT_DOUBLE, &snapshot_.network.cacheDuration) < 0)
|
||||
snapshot_.network.cacheDuration = 0;
|
||||
int64_t buffering = 0;
|
||||
if (mpv_get_property(handle_, "cache-buffering-state", MPV_FORMAT_INT64, &buffering) >= 0)
|
||||
snapshot_.network.buffering = buffering > 0;
|
||||
mpv_node cache = {};
|
||||
if (mpv_get_property(handle_, "demuxer-cache-state", MPV_FORMAT_NODE, &cache) >= 0 &&
|
||||
cache.format == MPV_FORMAT_NODE_MAP && cache.u.list != nullptr) {
|
||||
for (int i = 0; i < cache.u.list->num; ++i) {
|
||||
if (std::strcmp(cache.u.list->keys[i], "total-bytes") == 0 &&
|
||||
cache.u.list->values[i].format == MPV_FORMAT_INT64)
|
||||
snapshot_.network.cacheBytes = cache.u.list->values[i].u.int64;
|
||||
}
|
||||
mpv_free_node_contents(&cache);
|
||||
}
|
||||
snapshot_.network.demuxer = snapshot_.media.demuxer;
|
||||
snapshot_.network.container = snapshot_.media.container;
|
||||
if (mpv_get_property(handle_, "video-bitrate", MPV_FORMAT_INT64, &snapshot_.network.currentBitrate) < 0)
|
||||
snapshot_.network.currentBitrate = 0;
|
||||
}
|
||||
|
||||
void PlaybackEngine::syncHwdecStatsLocked()
|
||||
{
|
||||
mpv_harmony_hwdec_stats stats = {};
|
||||
@@ -301,26 +744,47 @@ void PlaybackEngine::handleLogMessage(const mpv_event_log_message* message)
|
||||
{
|
||||
if (!message || !message->text)
|
||||
return;
|
||||
const char* text = message->text;
|
||||
if (std::strstr(text, "h264_ohcodec") || std::strstr(text, "hevc_ohcodec")) {
|
||||
const std::string text = redactSensitive(message->text);
|
||||
if (containsInsensitive(text, "http/1.") || containsInsensitive(text, "http error")) {
|
||||
std::istringstream stream(text);
|
||||
std::string token;
|
||||
while (stream >> token) {
|
||||
if (token.size() == 3 && std::isdigit(static_cast<unsigned char>(token[0])) &&
|
||||
std::isdigit(static_cast<unsigned char>(token[1])) &&
|
||||
std::isdigit(static_cast<unsigned char>(token[2]))) {
|
||||
const int status = std::atoi(token.c_str());
|
||||
if (status >= 100 && status <= 599) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.actualDecoder = std::strstr(text, "hevc_ohcodec") ? "hevc_ohcodec" : "h264_ohcodec";
|
||||
snapshot_.network.httpStatus = status;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (containsInsensitive(text, "redirect")) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++snapshot_.network.redirectCount;
|
||||
}
|
||||
const char* rawText = message->text;
|
||||
if (std::strstr(rawText, "h264_ohcodec") || std::strstr(rawText, "hevc_ohcodec")) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.actualDecoder = std::strstr(rawText, "hevc_ohcodec") ? "hevc_ohcodec" : "h264_ohcodec";
|
||||
snapshot_.hwdecActive = true;
|
||||
}
|
||||
if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0 &&
|
||||
(std::strstr(text, "fallback") || std::strstr(text, "falling back") ||
|
||||
std::strstr(text, "No hardware decoding"))) {
|
||||
(std::strstr(rawText, "fallback") || std::strstr(rawText, "falling back") ||
|
||||
std::strstr(rawText, "No hardware decoding"))) {
|
||||
bool requested = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
requested = snapshot_.hwdecRequested;
|
||||
}
|
||||
if (requested)
|
||||
mpv_harmony_hwdec_set_fallback(text);
|
||||
mpv_harmony_hwdec_set_fallback(text.c_str());
|
||||
}
|
||||
if (message->prefix && std::strncmp(message->prefix, "vd", 2) == 0) {
|
||||
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG,
|
||||
"mpv[%{public}s] %{public}s", message->prefix, text);
|
||||
"mpv[%{public}s] %{public}s", message->prefix, text.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,8 +798,9 @@ void PlaybackEngine::setError(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
snapshot_.state = "ERROR";
|
||||
snapshot_.lastError = error;
|
||||
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s", error.c_str());
|
||||
snapshot_.lastError = redactSensitive(error);
|
||||
snapshot_.errorCode = classifyError(snapshot_.lastError);
|
||||
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "%{public}s", snapshot_.lastError.c_str());
|
||||
}
|
||||
|
||||
void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property* property)
|
||||
@@ -361,6 +826,28 @@ void PlaybackEngine::updateProperty(uint64_t userdata, const mpv_event_property*
|
||||
case kDropped:
|
||||
if (property->format == MPV_FORMAT_INT64) snapshot_.dropped = *static_cast<int64_t*>(property->data);
|
||||
break;
|
||||
case kTracks:
|
||||
refreshTracksLocked();
|
||||
break;
|
||||
case kCacheDuration:
|
||||
case kCacheState:
|
||||
case kBuffering:
|
||||
refreshNetworkLocked();
|
||||
if (userdata == kBuffering && property->format == MPV_FORMAT_INT64) {
|
||||
const int64_t buffering = *static_cast<int64_t*>(property->data);
|
||||
if (buffering > 0) snapshot_.state = "BUFFERING";
|
||||
else if (snapshot_.state == "BUFFERING") snapshot_.state = "PLAYING";
|
||||
}
|
||||
break;
|
||||
case kVolume:
|
||||
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.volume = *static_cast<double*>(property->data);
|
||||
break;
|
||||
case kMute:
|
||||
if (property->format == MPV_FORMAT_FLAG) snapshot_.muted = *static_cast<int*>(property->data) != 0;
|
||||
break;
|
||||
case kSpeed:
|
||||
if (property->format == MPV_FORMAT_DOUBLE) snapshot_.speed = *static_cast<double*>(property->data);
|
||||
break;
|
||||
case kHwdecCurrent:
|
||||
if (property->format == MPV_FORMAT_STRING) {
|
||||
const char *value = *static_cast<char **>(property->data);
|
||||
@@ -407,15 +894,33 @@ void PlaybackEngine::eventLoop()
|
||||
updateProperty(event->reply_userdata, static_cast<mpv_event_property*>(event->data));
|
||||
break;
|
||||
case MPV_EVENT_START_FILE:
|
||||
setState("PREPARING");
|
||||
{
|
||||
auto* start = static_cast<mpv_event_start_file*>(event->data);
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
awaitingStart_ = true;
|
||||
if (start) playlistEntryId_ = start->playlist_entry_id;
|
||||
snapshot_.state = "OPENING";
|
||||
}
|
||||
break;
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
setState("READY");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
awaitingStart_ = false;
|
||||
refreshMediaLocked();
|
||||
snapshot_.state = "READY";
|
||||
}
|
||||
break;
|
||||
case MPV_EVENT_END_FILE: {
|
||||
auto* end = static_cast<mpv_event_end_file*>(event->data);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (end != nullptr && end->playlist_entry_id != 0 && playlistEntryId_ != 0 &&
|
||||
end->playlist_entry_id != playlistEntryId_) break;
|
||||
}
|
||||
if (end != nullptr && end->reason == MPV_END_FILE_REASON_ERROR) {
|
||||
setError(std::string("end-file: ") + mpv_error_string(end->error));
|
||||
} else if (end != nullptr && end->reason == MPV_END_FILE_REASON_EOF) {
|
||||
setState("ENDED");
|
||||
} else {
|
||||
setState("STOPPED");
|
||||
}
|
||||
@@ -428,3 +933,51 @@ 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:"};
|
||||
for (const char* key : keys) {
|
||||
size_t position = 0;
|
||||
while ((position = 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="};
|
||||
for (const char* key : queryKeys) {
|
||||
size_t position = 0;
|
||||
while ((position = result.find(key, position)) != std::string::npos) {
|
||||
const size_t end = result.find_first_of("& \r\n", position);
|
||||
result.replace(position, (end == std::string::npos ? result.size() : end) - position, std::string(key) + "[REDACTED]");
|
||||
position += std::strlen(key) + 10;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string PlaybackEngine::classifyError(const std::string& value)
|
||||
{
|
||||
if (containsInsensitive(value, "403") || containsInsensitive(value, "404") ||
|
||||
containsInsensitive(value, "500") || containsInsensitive(value, "http error"))
|
||||
return "HTTP";
|
||||
if (containsInsensitive(value, "tls") || containsInsensitive(value, "certificate") ||
|
||||
containsInsensitive(value, "ssl")) return "TLS";
|
||||
if (containsInsensitive(value, "not found") || containsInsensitive(value, "no such file")) return "FILE_NOT_FOUND";
|
||||
if (containsInsensitive(value, "permission") || containsInsensitive(value, "access denied")) return "PERMISSION";
|
||||
if (containsInsensitive(value, "subtitle") || containsInsensitive(value, "sub ")) return "SUBTITLE";
|
||||
if (containsInsensitive(value, "video codec") || containsInsensitive(value, "video decoder")) return "UNSUPPORTED_VIDEO_CODEC";
|
||||
if (containsInsensitive(value, "audio codec") || containsInsensitive(value, "audio decoder")) return "UNSUPPORTED_AUDIO_CODEC";
|
||||
if (containsInsensitive(value, "unsupported") && containsInsensitive(value, "container")) return "UNSUPPORTED_CONTAINER";
|
||||
if (containsInsensitive(value, "demux") || containsInsensitive(value, "format")) return "DEMUX";
|
||||
if (containsInsensitive(value, "decoder")) return "DECODER";
|
||||
if (containsInsensitive(value, "audio output")) return "AUDIO_OUTPUT";
|
||||
if (containsInsensitive(value, "video output")) return "VIDEO_OUTPUT";
|
||||
if (containsInsensitive(value, "seek")) return "SEEK";
|
||||
if (containsInsensitive(value, "timeout") || containsInsensitive(value, "connection") ||
|
||||
containsInsensitive(value, "network") || containsInsensitive(value, "dns")) return "NETWORK";
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -8,9 +8,91 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct PlaybackHeader {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct PlaybackSubtitleSource {
|
||||
std::string url;
|
||||
std::string title;
|
||||
std::string language;
|
||||
std::string encoding;
|
||||
bool forced = false;
|
||||
bool select = false;
|
||||
};
|
||||
|
||||
struct PlaybackRequest {
|
||||
std::string url;
|
||||
std::string title;
|
||||
int64_t startPositionMs = -1;
|
||||
std::string userAgent;
|
||||
std::string referer;
|
||||
std::string cookies;
|
||||
std::vector<PlaybackHeader> headers;
|
||||
std::vector<PlaybackSubtitleSource> subtitleFiles;
|
||||
std::string preferredAudioLanguage;
|
||||
std::string preferredSubtitleLanguage;
|
||||
int subtitleEnabled = -1;
|
||||
int audioTrackId = -1;
|
||||
int subtitleTrackId = -1;
|
||||
double playbackSpeed = 0.0;
|
||||
double networkTimeout = 0.0;
|
||||
};
|
||||
|
||||
struct PlaybackTrack {
|
||||
int id = 0;
|
||||
std::string type;
|
||||
std::string title;
|
||||
std::string language;
|
||||
std::string codec;
|
||||
bool isDefault = false;
|
||||
bool forced = false;
|
||||
bool selected = false;
|
||||
bool external = false;
|
||||
};
|
||||
|
||||
struct PlaybackMediaInfo {
|
||||
std::string container;
|
||||
std::string demuxer;
|
||||
std::string videoCodec;
|
||||
std::string audioCodec;
|
||||
std::string pixelFormat;
|
||||
std::string colorPrimaries;
|
||||
std::string colorTransfer;
|
||||
std::string colorSpace;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int bitDepth = 0;
|
||||
int audioSampleRate = 0;
|
||||
int audioChannels = 0;
|
||||
std::string audioChannelLayout;
|
||||
double fps = 0.0;
|
||||
int64_t videoBitrate = 0;
|
||||
int64_t audioBitrate = 0;
|
||||
int trackCount = 0;
|
||||
int subtitleCount = 0;
|
||||
};
|
||||
|
||||
struct PlaybackNetworkInfo {
|
||||
std::string protocol;
|
||||
std::string host;
|
||||
std::string networkState;
|
||||
int httpStatus = 0;
|
||||
int redirectCount = 0;
|
||||
bool buffering = false;
|
||||
double cacheDuration = 0.0;
|
||||
int64_t cacheBytes = 0;
|
||||
std::string demuxer;
|
||||
std::string container;
|
||||
int64_t currentBitrate = 0;
|
||||
};
|
||||
|
||||
struct PlaybackSnapshot {
|
||||
std::string state = "IDLE";
|
||||
uint64_t sessionId = 0;
|
||||
double position = 0.0;
|
||||
double duration = 0.0;
|
||||
double fps = 0.0;
|
||||
@@ -18,6 +100,13 @@ struct PlaybackSnapshot {
|
||||
int64_t firstFrameMs = -1;
|
||||
uint64_t renderCount = 0;
|
||||
std::string lastError;
|
||||
std::string errorCode = "NONE";
|
||||
PlaybackMediaInfo media;
|
||||
PlaybackNetworkInfo network;
|
||||
std::vector<PlaybackTrack> tracks;
|
||||
double speed = 1.0;
|
||||
double volume = 100.0;
|
||||
bool muted = false;
|
||||
std::string requestedDecoder = "software";
|
||||
std::string actualDecoder = "software";
|
||||
std::string hwdecBackend;
|
||||
@@ -43,9 +132,25 @@ public:
|
||||
bool setAudioEnabled(bool enabled);
|
||||
bool setHardwareDecode(bool enabled);
|
||||
void load(const std::string& url);
|
||||
bool load(const PlaybackRequest& request);
|
||||
void play();
|
||||
void pause();
|
||||
void togglePause();
|
||||
void seek(double delta);
|
||||
bool seekAbsolute(double seconds, bool exact, bool keyframe);
|
||||
bool seekPercent(double percent, bool exact, bool keyframe);
|
||||
bool setSpeed(double speed);
|
||||
bool setVolume(double volume);
|
||||
bool setMute(bool muted);
|
||||
bool selectAudioTrack(int id);
|
||||
bool selectSubtitleTrack(int id);
|
||||
bool disableSubtitle();
|
||||
bool addSubtitle(const PlaybackSubtitleSource& source);
|
||||
bool removeSubtitle(int id);
|
||||
bool setSubtitleDelay(double seconds);
|
||||
bool setSubtitleScale(double scale);
|
||||
bool setSubtitlePosition(double position);
|
||||
bool setAssOverride(const std::string& mode);
|
||||
void stop();
|
||||
void release();
|
||||
PlaybackSnapshot snapshot() const;
|
||||
@@ -61,13 +166,23 @@ private:
|
||||
void updateProperty(uint64_t userdata, const mpv_event_property* property);
|
||||
void syncHwdecStatsLocked();
|
||||
void handleLogMessage(const mpv_event_log_message* message);
|
||||
void refreshTracksLocked();
|
||||
void refreshMediaLocked();
|
||||
void refreshNetworkLocked();
|
||||
bool setHeadersLocked(const PlaybackRequest& request);
|
||||
static std::string redactSensitive(const std::string& value);
|
||||
static std::string classifyError(const std::string& value);
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
mutable std::mutex apiMutex_;
|
||||
PlaybackSnapshot snapshot_;
|
||||
mpv_handle* handle_ = nullptr;
|
||||
std::thread eventThread_;
|
||||
std::atomic<bool> stopEvents_{false};
|
||||
std::atomic<bool> initialized_{false};
|
||||
uint64_t sessionId_ = 0;
|
||||
int64_t playlistEntryId_ = 0;
|
||||
bool awaitingStart_ = false;
|
||||
std::chrono::steady_clock::time_point startedAt_;
|
||||
static constexpr uint64_t kHwdecCurrent = 6;
|
||||
static constexpr uint64_t kHwdecInterop = 7;
|
||||
|
||||
@@ -39,10 +39,78 @@ interface PlaybackState {
|
||||
nativeBufferImportFailure: number;
|
||||
surfacePresentCount: number;
|
||||
gpuInteropFrameCount: number;
|
||||
sessionId: number;
|
||||
errorCode: string;
|
||||
speed: number;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
network: NetworkState;
|
||||
media: MediaState;
|
||||
tracks: TrackState[];
|
||||
}
|
||||
|
||||
interface NetworkState {
|
||||
protocol: string;
|
||||
host: string;
|
||||
networkState: string;
|
||||
httpStatus: number;
|
||||
redirectCount: number;
|
||||
buffering: boolean;
|
||||
cacheDuration: number;
|
||||
cacheBytes: number;
|
||||
demuxer: string;
|
||||
container: string;
|
||||
currentBitrate: number;
|
||||
}
|
||||
|
||||
interface MediaState {
|
||||
container: string;
|
||||
demuxer: string;
|
||||
videoCodec: string;
|
||||
audioCodec: string;
|
||||
pixelFormat: string;
|
||||
width: number;
|
||||
height: number;
|
||||
fps: number;
|
||||
bitDepth: number;
|
||||
audioSampleRate: number;
|
||||
audioChannels: number;
|
||||
trackCount: number;
|
||||
subtitleCount: number;
|
||||
}
|
||||
|
||||
interface TrackState {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
language: string;
|
||||
codec: string;
|
||||
default: boolean;
|
||||
forced: boolean;
|
||||
selected: boolean;
|
||||
external: boolean;
|
||||
}
|
||||
|
||||
const LOCAL_TEST_URL: string = 'file:///data/storage/el2/base/haps/entry/files/sample.mp4';
|
||||
|
||||
interface HeaderInput {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function parseHeaders(value: string): HeaderInput[] {
|
||||
const result: HeaderInput[] = [];
|
||||
value.split('\n').forEach((line: string) => {
|
||||
const separator: number = line.indexOf(':');
|
||||
if (separator > 0) {
|
||||
const name: string = line.substring(0, separator).trim();
|
||||
const headerValue: string = line.substring(separator + 1).trim();
|
||||
if (name.length > 0) result.push({ name: name, value: headerValue });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct Index {
|
||||
@@ -50,6 +118,11 @@ struct Index {
|
||||
@State status: string = 'IDLE';
|
||||
@State audioEnabled: boolean = true;
|
||||
@State detail: string = 'Surface waiting';
|
||||
@State headersText: string = '';
|
||||
@State subtitleUrl: string = '';
|
||||
@State speedInput: string = '1';
|
||||
@State volumeInput: string = '100';
|
||||
@State subtitleDelayInput: string = '0';
|
||||
@State state: PlaybackState = {
|
||||
state: 'IDLE', position: 0, duration: 0, fps: 0, dropped: 0,
|
||||
firstFrameMs: -1, renderCount: 0, lastError: '', surface: 'WAITING', egl: 'WAITING', mpv: 'WAITING',
|
||||
@@ -60,7 +133,14 @@ struct Index {
|
||||
hwdecActive: false, hwdecBackend: '', hwdecFallbackReason: '', hwCapability: '',
|
||||
hardwareFrameCount: 0, softwareFrameCount: 0, hwFallbackCount: 0,
|
||||
nativeBufferImportCount: 0, nativeBufferImportFailure: 0, surfacePresentCount: 0,
|
||||
gpuInteropFrameCount: 0
|
||||
gpuInteropFrameCount: 0,
|
||||
sessionId: 0, errorCode: 'NONE', speed: 1, volume: 100, muted: false,
|
||||
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,
|
||||
trackCount: 0, subtitleCount: 0 },
|
||||
tracks: []
|
||||
};
|
||||
private poller: number = 0;
|
||||
|
||||
@@ -94,7 +174,9 @@ struct Index {
|
||||
this.url = LOCAL_TEST_URL;
|
||||
playbackLab.load(LOCAL_TEST_URL);
|
||||
})
|
||||
Button('Load URL').onClick(() => playbackLab.load(this.url))
|
||||
Button('Load URL').onClick(() => playbackLab.load({
|
||||
url: this.url, httpHeaders: parseHeaders(this.headersText)
|
||||
}))
|
||||
Button(this.audioEnabled ? 'Audio On' : 'Audio Off').onClick(() => {
|
||||
const enabled: boolean = !this.audioEnabled;
|
||||
if (playbackLab.setAudioEnabled(enabled) as boolean) this.audioEnabled = enabled;
|
||||
@@ -115,6 +197,29 @@ struct Index {
|
||||
Row({ space: 8 }) {
|
||||
Button('-10s').onClick(() => playbackLab.seek(-10))
|
||||
Button('+10s').onClick(() => playbackLab.seek(10))
|
||||
Button('Toggle').onClick(() => playbackLab.togglePause())
|
||||
}.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 }) {
|
||||
TextInput({ text: this.speedInput, placeholder: 'speed' }).width('25%')
|
||||
.onChange((value: string) => this.speedInput = value)
|
||||
Button('Set Speed').onClick(() => playbackLab.setSpeed(Number(this.speedInput)))
|
||||
TextInput({ text: this.volumeInput, placeholder: 'volume' }).width('25%')
|
||||
.onChange((value: string) => this.volumeInput = value)
|
||||
Button('Set Volume').onClick(() => playbackLab.setVolume(Number(this.volumeInput)))
|
||||
Button(this.state.muted ? 'Unmute' : 'Mute').onClick(() => playbackLab.setMute(!this.state.muted))
|
||||
}.width('100%')
|
||||
TextInput({ text: this.subtitleUrl, placeholder: 'external subtitle URL (SRT/ASS/WebVTT)' })
|
||||
.width('100%').onChange((value: string) => this.subtitleUrl = value)
|
||||
Row({ space: 8 }) {
|
||||
Button('Add Subtitle').onClick(() => playbackLab.addSubtitle({
|
||||
url: 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)
|
||||
Button('Set Delay').onClick(() => playbackLab.setSubtitleDelay(Number(this.subtitleDelayInput)))
|
||||
}.width('100%')
|
||||
Text(`position=${this.state.position.toFixed(2)} duration=${this.state.duration.toFixed(2)} fps=${this.state.fps.toFixed(2)}`)
|
||||
.fontSize(13).fontColor($r('app.color.muted')).width('100%')
|
||||
@@ -133,6 +238,21 @@ struct Index {
|
||||
Text(`fallback=${this.state.hwdecFallbackReason || 'none'} capability=${this.state.hwCapability || 'DEVICE TEST REQUIRED'}`)
|
||||
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(3)
|
||||
Text(detailLabel(this.detail)).fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
|
||||
Text('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)
|
||||
.fontSize(13).fontColor($r('app.color.muted')).width('100%').maxLines(2)
|
||||
ForEach(this.state.tracks, (track: TrackState) => {
|
||||
Row({ space: 6 }) {
|
||||
Text('track ' + track.id + ' ' + track.type + ' ' + (track.language || 'und') + ' ' + (track.codec || 'unknown') + (track.selected ? ' [selected]' : ''))
|
||||
.fontSize(12).fontColor($r('app.color.muted')).layoutWeight(1)
|
||||
if (track.type === 'audio') {
|
||||
Button('Use').onClick(() => playbackLab.selectAudioTrack(track.id))
|
||||
} else if (track.type === 'sub') {
|
||||
Button('Use').onClick(() => playbackLab.selectSubtitleTrack(track.id))
|
||||
}
|
||||
}.width('100%')
|
||||
}, (track: TrackState) => track.type + '-' + track.id)
|
||||
}
|
||||
.padding(16).width('100%').height('100%').backgroundColor($r('app.color.background'))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
#include "mpv/harmony_playback.h"
|
||||
#include "mpv/client.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <ctype.h>
|
||||
|
||||
struct mp_harmony_player {
|
||||
pthread_mutex_t lock;
|
||||
pthread_t events;
|
||||
int lock_ready;
|
||||
int initialized;
|
||||
int stop_events;
|
||||
mpv_handle *handle;
|
||||
uint64_t session_id;
|
||||
int64_t playlist_entry_id;
|
||||
int awaiting_start;
|
||||
mp_harmony_playback_snapshot snapshot;
|
||||
mp_harmony_track tracks[MP_HARMONY_MAX_TRACKS];
|
||||
size_t track_count;
|
||||
};
|
||||
|
||||
enum {
|
||||
PROP_TIME_POS = 1, PROP_DURATION, PROP_PAUSE, PROP_FPS, PROP_DROPPED,
|
||||
PROP_TRACKS, PROP_CACHE_DURATION, PROP_CACHE_STATE, PROP_BUFFERING,
|
||||
PROP_VOLUME, PROP_MUTE, PROP_SPEED,
|
||||
};
|
||||
|
||||
static void copy_text(char *dst, size_t size, const char *src)
|
||||
{
|
||||
if (!dst || size == 0) return;
|
||||
if (!src) src = "";
|
||||
snprintf(dst, size, "%.*s", (int)(size - 1), src);
|
||||
}
|
||||
|
||||
static int text_has(const char *text, const char *needle)
|
||||
{
|
||||
if (!text || !needle || !needle[0]) return 0;
|
||||
for (const char *p = text; *p; p++) {
|
||||
const char *a = p;
|
||||
const char *b = needle;
|
||||
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
|
||||
if (!*b) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int supported_subtitle_encoding(const char *encoding)
|
||||
{
|
||||
if (!encoding || !encoding[0]) return 1;
|
||||
return strcasecmp(encoding, "utf-8") == 0 || strcasecmp(encoding, "utf8") == 0 ||
|
||||
strcasecmp(encoding, "gbk") == 0 || strcasecmp(encoding, "gb18030") == 0 ||
|
||||
strcasecmp(encoding, "big5") == 0 || strcasecmp(encoding, "shift-jis") == 0 ||
|
||||
strcasecmp(encoding, "sjis") == 0;
|
||||
}
|
||||
|
||||
static void redact_text(char *dst, size_t size, const char *src)
|
||||
{
|
||||
size_t out = 0;
|
||||
const char *p = src ? src : "";
|
||||
while (*p && out + 1 < size) {
|
||||
size_t key_len = 0;
|
||||
if (strncasecmp(p, "authorization:", 14) == 0) key_len = 14;
|
||||
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;
|
||||
if (key_len) {
|
||||
size_t n = key_len;
|
||||
while (p[n] && p[n] != '\n' && p[n] != '\r') n++;
|
||||
if (out + key_len + 11 >= size) break;
|
||||
memcpy(dst + out, p, key_len); out += key_len;
|
||||
memcpy(dst + out, " [REDACTED]", 11); out += 11;
|
||||
p += n;
|
||||
continue;
|
||||
}
|
||||
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 ||
|
||||
strncasecmp(q, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0) {
|
||||
dst[out++] = *p++;
|
||||
while (*p && *p != '&' && *p != ' ' && *p != '\n' && out + 1 < size) p++;
|
||||
if (out + 10 >= size) break;
|
||||
memcpy(dst + out, "[REDACTED]", 10); out += 10;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dst[out++] = *p++;
|
||||
}
|
||||
dst[out] = '\0';
|
||||
}
|
||||
|
||||
static void set_error_locked(mp_harmony_player *p, mp_harmony_error_code code, const char *message)
|
||||
{
|
||||
p->snapshot.state = MP_HARMONY_ERROR;
|
||||
p->snapshot.error_code = code;
|
||||
redact_text(p->snapshot.error_message, sizeof(p->snapshot.error_message), message);
|
||||
}
|
||||
|
||||
static mp_harmony_error_code classify_error(int code, const char *message)
|
||||
{
|
||||
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"))
|
||||
return MP_HARMONY_ERROR_TLS;
|
||||
if (text_has(message, "not found") || text_has(message, "no such file"))
|
||||
return MP_HARMONY_ERROR_FILE_NOT_FOUND;
|
||||
if (text_has(message, "permission") || text_has(message, "access denied"))
|
||||
return MP_HARMONY_ERROR_PERMISSION;
|
||||
if (text_has(message, "subtitle") || text_has(message, "sub ")) return MP_HARMONY_ERROR_SUBTITLE;
|
||||
if (text_has(message, "video codec") || text_has(message, "video decoder"))
|
||||
return MP_HARMONY_ERROR_UNSUPPORTED_VIDEO_CODEC;
|
||||
if (text_has(message, "audio codec") || text_has(message, "audio decoder"))
|
||||
return MP_HARMONY_ERROR_UNSUPPORTED_AUDIO_CODEC;
|
||||
if (text_has(message, "unsupported") && text_has(message, "container"))
|
||||
return MP_HARMONY_ERROR_UNSUPPORTED_CONTAINER;
|
||||
if (text_has(message, "demux") || text_has(message, "format")) return MP_HARMONY_ERROR_DEMUX;
|
||||
if (text_has(message, "decoder")) return MP_HARMONY_ERROR_DECODER;
|
||||
if (text_has(message, "audio output")) return MP_HARMONY_ERROR_AUDIO_OUTPUT;
|
||||
if (text_has(message, "video output")) return MP_HARMONY_ERROR_VIDEO_OUTPUT;
|
||||
if (text_has(message, "seek")) return MP_HARMONY_ERROR_SEEK;
|
||||
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;
|
||||
}
|
||||
|
||||
static int set_property_string(mp_harmony_player *p, const char *name, const char *value)
|
||||
{
|
||||
int rc = mpv_set_property_string(p->handle, name, value ? value : "");
|
||||
if (rc < 0) {
|
||||
char message[MP_HARMONY_TEXT];
|
||||
snprintf(message, sizeof(message), "%s: %s", name, mpv_error_string(rc));
|
||||
set_error_locked(p, classify_error(rc, message), message);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void update_network_locked(mp_harmony_player *p)
|
||||
{
|
||||
char *url = mpv_get_property_string(p->handle, "stream-open-filename");
|
||||
if (!url) return;
|
||||
const char *scheme = strstr(url, "://");
|
||||
if (scheme) {
|
||||
size_t n = (size_t)(scheme - url);
|
||||
if (n >= sizeof(p->snapshot.network.protocol)) n = sizeof(p->snapshot.network.protocol) - 1;
|
||||
memcpy(p->snapshot.network.protocol, url, n); p->snapshot.network.protocol[n] = '\0';
|
||||
const char *host = scheme + 3;
|
||||
const char *end = strpbrk(host, "/?#");
|
||||
size_t hn = end ? (size_t)(end - host) : strlen(host);
|
||||
if (hn >= sizeof(p->snapshot.network.host)) hn = sizeof(p->snapshot.network.host) - 1;
|
||||
memcpy(p->snapshot.network.host, host, hn); p->snapshot.network.host[hn] = '\0';
|
||||
}
|
||||
mpv_free(url);
|
||||
int flag = 0;
|
||||
if (mpv_get_property(p->handle, "demuxer-via-network", MPV_FORMAT_FLAG, &flag) >= 0)
|
||||
copy_text(p->snapshot.network.network_state, sizeof(p->snapshot.network.network_state), flag ? "network" : "local");
|
||||
double duration = 0;
|
||||
if (mpv_get_property(p->handle, "demuxer-cache-duration", MPV_FORMAT_DOUBLE, &duration) >= 0)
|
||||
p->snapshot.network.cache_duration = duration;
|
||||
mpv_node cache = {0};
|
||||
if (mpv_get_property(p->handle, "demuxer-cache-state", MPV_FORMAT_NODE, &cache) >= 0) {
|
||||
if (cache.format == MPV_FORMAT_NODE_MAP && cache.u.list) {
|
||||
for (int i = 0; i < cache.u.list->num; i++) {
|
||||
const char *key = cache.u.list->keys[i];
|
||||
mpv_node *value = &cache.u.list->values[i];
|
||||
if (key && strcmp(key, "total-bytes") == 0 && value->format == MPV_FORMAT_INT64)
|
||||
p->snapshot.network.cache_bytes = value->u.int64;
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&cache);
|
||||
}
|
||||
int64_t buffering = 0;
|
||||
if (mpv_get_property(p->handle, "cache-buffering-state", MPV_FORMAT_INT64, &buffering) >= 0)
|
||||
p->snapshot.network.buffering = buffering > 0;
|
||||
char *demuxer = mpv_get_property_string(p->handle, "current-demuxer");
|
||||
char *container = mpv_get_property_string(p->handle, "file-format");
|
||||
copy_text(p->snapshot.network.demuxer, sizeof(p->snapshot.network.demuxer), demuxer);
|
||||
copy_text(p->snapshot.network.container, sizeof(p->snapshot.network.container), container);
|
||||
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)
|
||||
p->snapshot.network.current_bitrate = bitrate;
|
||||
}
|
||||
|
||||
static const char *node_string(mpv_node *node)
|
||||
{
|
||||
return node && node->format == MPV_FORMAT_STRING ? node->u.string : NULL;
|
||||
}
|
||||
|
||||
static int node_flag(mpv_node *node)
|
||||
{
|
||||
if (!node) return 0;
|
||||
if (node->format == MPV_FORMAT_FLAG) return node->u.flag != 0;
|
||||
return node->format == MPV_FORMAT_INT64 && node->u.int64 != 0;
|
||||
}
|
||||
|
||||
static int node_int(mpv_node *node)
|
||||
{
|
||||
if (!node) return 0;
|
||||
if (node->format == MPV_FORMAT_INT64) return (int)node->u.int64;
|
||||
if (node->format == MPV_FORMAT_DOUBLE) return (int)node->u.double_;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void refresh_tracks_locked(mp_harmony_player *p)
|
||||
{
|
||||
mpv_node node = {0};
|
||||
p->track_count = 0;
|
||||
if (mpv_get_property(p->handle, "track-list", MPV_FORMAT_NODE, &node) < 0) return;
|
||||
if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list) {
|
||||
for (int i = 0; i < node.u.list->num && p->track_count < MP_HARMONY_MAX_TRACKS; i++) {
|
||||
mpv_node *item = &node.u.list->values[i];
|
||||
if (item->format != MPV_FORMAT_NODE_MAP || !item->u.list) continue;
|
||||
mp_harmony_track *track = &p->tracks[p->track_count];
|
||||
memset(track, 0, sizeof(*track));
|
||||
for (int j = 0; j < item->u.list->num; j++) {
|
||||
const char *key = item->u.list->keys[j];
|
||||
mpv_node *value = &item->u.list->values[j];
|
||||
if (!key) continue;
|
||||
if (strcmp(key, "id") == 0) track->id = node_int(value);
|
||||
else if (strcmp(key, "type") == 0) {
|
||||
const char *s = node_string(value);
|
||||
track->type = s && strcmp(s, "audio") == 0 ? MP_HARMONY_TRACK_AUDIO :
|
||||
(s && strcmp(s, "sub") == 0 ? MP_HARMONY_TRACK_SUBTITLE : MP_HARMONY_TRACK_VIDEO);
|
||||
} else if (strcmp(key, "title") == 0) copy_text(track->title, sizeof(track->title), node_string(value));
|
||||
else if (strcmp(key, "lang") == 0) copy_text(track->language, sizeof(track->language), node_string(value));
|
||||
else if (strcmp(key, "codec") == 0) copy_text(track->codec, sizeof(track->codec), node_string(value));
|
||||
else if (strcmp(key, "default") == 0) track->is_default = node_flag(value);
|
||||
else if (strcmp(key, "forced") == 0) track->forced = node_flag(value);
|
||||
else if (strcmp(key, "selected") == 0) track->selected = node_flag(value);
|
||||
else if (strcmp(key, "external") == 0) track->external = node_flag(value);
|
||||
}
|
||||
p->track_count++;
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
p->snapshot.track_count = (int)p->track_count;
|
||||
p->snapshot.media.track_count = (int)p->track_count;
|
||||
p->snapshot.media.subtitle_count = 0;
|
||||
for (size_t i = 0; i < p->track_count; i++)
|
||||
if (p->tracks[i].type == MP_HARMONY_TRACK_SUBTITLE) p->snapshot.media.subtitle_count++;
|
||||
}
|
||||
|
||||
static void refresh_media_locked(mp_harmony_player *p)
|
||||
{
|
||||
char *v = mpv_get_property_string(p->handle, "file-format");
|
||||
copy_text(p->snapshot.media.container, sizeof(p->snapshot.media.container), v); if (v) mpv_free(v);
|
||||
v = mpv_get_property_string(p->handle, "current-demuxer");
|
||||
copy_text(p->snapshot.media.demuxer, sizeof(p->snapshot.media.demuxer), v); if (v) mpv_free(v);
|
||||
v = mpv_get_property_string(p->handle, "video-codec");
|
||||
copy_text(p->snapshot.media.video_codec, sizeof(p->snapshot.media.video_codec), v); if (v) mpv_free(v);
|
||||
v = mpv_get_property_string(p->handle, "audio-codec");
|
||||
copy_text(p->snapshot.media.audio_codec, sizeof(p->snapshot.media.audio_codec), v); if (v) mpv_free(v);
|
||||
v = mpv_get_property_string(p->handle, "video-format");
|
||||
copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), v); if (v) mpv_free(v);
|
||||
int64_t i64 = 0;
|
||||
if (mpv_get_property(p->handle, "width", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.width = (int)i64;
|
||||
if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.height = (int)i64;
|
||||
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, "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;
|
||||
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) {
|
||||
for (int j = 0; j < node.u.list->num; j++) {
|
||||
const char *key = node.u.list->keys[j]; mpv_node *value = &node.u.list->values[j];
|
||||
if (!key) continue;
|
||||
if (strcmp(key, "pixelformat") == 0) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), node_string(value));
|
||||
else if (strcmp(key, "primaries") == 0) copy_text(p->snapshot.media.color_primaries, sizeof(p->snapshot.media.color_primaries), node_string(value));
|
||||
else if (strcmp(key, "gamma") == 0) copy_text(p->snapshot.media.color_transfer, sizeof(p->snapshot.media.color_transfer), node_string(value));
|
||||
else if (strcmp(key, "colormatrix") == 0) copy_text(p->snapshot.media.color_space, sizeof(p->snapshot.media.color_space), node_string(value));
|
||||
else if (strcmp(key, "bit-depth") == 0) p->snapshot.media.bit_depth = node_int(value);
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
}
|
||||
memset(&node, 0, sizeof(node));
|
||||
if (mpv_get_property(p->handle, "audio-params", MPV_FORMAT_NODE, &node) >= 0) {
|
||||
if (node.format == MPV_FORMAT_NODE_MAP && node.u.list) {
|
||||
for (int j = 0; j < node.u.list->num; j++) {
|
||||
const char *key = node.u.list->keys[j]; mpv_node *value = &node.u.list->values[j];
|
||||
if (!key) continue;
|
||||
if (strcmp(key, "samplerate") == 0) p->snapshot.media.audio_sample_rate = node_int(value);
|
||||
else if (strcmp(key, "channels") == 0) p->snapshot.media.audio_channels = node_int(value);
|
||||
else if (strcmp(key, "channel-layout") == 0) copy_text(p->snapshot.media.audio_channel_layout, sizeof(p->snapshot.media.audio_channel_layout), node_string(value));
|
||||
}
|
||||
}
|
||||
mpv_free_node_contents(&node);
|
||||
}
|
||||
refresh_tracks_locked(p);
|
||||
update_network_locked(p);
|
||||
}
|
||||
|
||||
static void update_property_locked(mp_harmony_player *p, uint64_t id, mpv_event_property *property)
|
||||
{
|
||||
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_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_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;
|
||||
} else if (id == PROP_VOLUME && property->format == MPV_FORMAT_DOUBLE) p->snapshot.volume = (int)*(double *)property->data;
|
||||
else if (id == PROP_MUTE && property->format == MPV_FORMAT_FLAG) p->snapshot.muted = *(int *)property->data != 0;
|
||||
else if (id == PROP_SPEED && property->format == MPV_FORMAT_DOUBLE) p->snapshot.speed = *(double *)property->data;
|
||||
else if (id == PROP_TRACKS) refresh_tracks_locked(p);
|
||||
else if (id == PROP_CACHE_DURATION || id == PROP_CACHE_STATE || id == PROP_BUFFERING) {
|
||||
update_network_locked(p);
|
||||
if (id == PROP_BUFFERING && property->format == MPV_FORMAT_INT64) {
|
||||
int64_t buffering = *(int64_t *)property->data;
|
||||
if (buffering > 0) p->snapshot.state = MP_HARMONY_BUFFERING;
|
||||
else if (p->snapshot.state == MP_HARMONY_BUFFERING) p->snapshot.state = MP_HARMONY_PLAYING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_log_locked(mp_harmony_player *p, const mpv_event_log_message *log)
|
||||
{
|
||||
if (!log || !log->text) return;
|
||||
char redacted[MP_HARMONY_TEXT]; redact_text(redacted, sizeof(redacted), log->text);
|
||||
if (strstr(log->text, "HTTP/1.") || strstr(log->text, "http error")) {
|
||||
int status = 0;
|
||||
if (sscanf(log->text, "%*[^0-9]%d", &status) == 1 && status >= 100 && status <= 599)
|
||||
p->snapshot.network.http_status = status;
|
||||
}
|
||||
if (text_has(log->text, "redirect")) p->snapshot.network.redirect_count++;
|
||||
if (log->prefix && (strcmp(log->prefix, "cplayer") == 0 || strcmp(log->prefix, "ffmpeg") == 0) &&
|
||||
log->log_level <= MPV_LOG_LEVEL_ERROR)
|
||||
set_error_locked(p, classify_error(0, redacted), redacted);
|
||||
}
|
||||
|
||||
static void *event_loop(void *arg)
|
||||
{
|
||||
mp_harmony_player *p = arg;
|
||||
while (1) {
|
||||
pthread_mutex_lock(&p->lock);
|
||||
int stop = p->stop_events || !p->handle;
|
||||
mpv_handle *h = p->handle;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
if (stop) break;
|
||||
mpv_event *event = mpv_wait_event(h, 0.05);
|
||||
if (!event) continue;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (p->stop_events) { pthread_mutex_unlock(&p->lock); break; }
|
||||
switch (event->event_id) {
|
||||
case MPV_EVENT_START_FILE: {
|
||||
mpv_event_start_file *start = event->data;
|
||||
p->awaiting_start = 1;
|
||||
if (start) p->playlist_entry_id = start->playlist_entry_id;
|
||||
p->snapshot.state = MP_HARMONY_OPENING;
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
p->awaiting_start = 0; refresh_media_locked(p); p->snapshot.state = MP_HARMONY_READY;
|
||||
break;
|
||||
case MPV_EVENT_PROPERTY_CHANGE:
|
||||
update_property_locked(p, event->reply_userdata, event->data);
|
||||
break;
|
||||
case MPV_EVENT_LOG_MESSAGE:
|
||||
handle_log_locked(p, event->data);
|
||||
break;
|
||||
case MPV_EVENT_END_FILE: {
|
||||
mpv_event_end_file *end = event->data;
|
||||
if (end && end->playlist_entry_id && p->playlist_entry_id &&
|
||||
end->playlist_entry_id != p->playlist_entry_id) break;
|
||||
if (end && end->reason == MPV_END_FILE_REASON_ERROR) {
|
||||
const char *s = mpv_error_string(end->error);
|
||||
set_error_locked(p, classify_error(end->error, s), s);
|
||||
} else if (end && end->reason == MPV_END_FILE_REASON_EOF) p->snapshot.state = MP_HARMONY_ENDED;
|
||||
else p->snapshot.state = MP_HARMONY_STOPPED;
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_SHUTDOWN:
|
||||
p->snapshot.state = MP_HARMONY_RELEASED;
|
||||
pthread_mutex_unlock(&p->lock); return NULL;
|
||||
default: break;
|
||||
}
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int ensure_ready_locked(mp_harmony_player *p)
|
||||
{
|
||||
return p && p->initialized && p->handle ? 0 : -1;
|
||||
}
|
||||
|
||||
mp_harmony_player *mp_harmony_playback_create(void)
|
||||
{
|
||||
mp_harmony_player *p = calloc(1, sizeof(*p));
|
||||
if (!p) return NULL;
|
||||
if (pthread_mutex_init(&p->lock, NULL) != 0) { free(p); return NULL; }
|
||||
p->lock_ready = 1; p->snapshot.state = MP_HARMONY_IDLE;
|
||||
p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.speed = 1.0;
|
||||
return p;
|
||||
}
|
||||
|
||||
int mp_harmony_playback_initialize(mp_harmony_player *p)
|
||||
{
|
||||
if (!p || !p->lock_ready) return -1;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (p->initialized) { pthread_mutex_unlock(&p->lock); return 0; }
|
||||
p->handle = mpv_create();
|
||||
if (!p->handle) { set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "mpv_create failed"); pthread_mutex_unlock(&p->lock); return -1; }
|
||||
const char *options[][2] = {
|
||||
{"vo", "libmpv"}, {"terminal", "no"}, {"config", "no"}, {"hwdec", "no"},
|
||||
{"aid", "auto"}, {"cache", "auto"}, {"cache-pause", "yes"}, {"cookies", "no"},
|
||||
{"tls-verify", "yes"}, {NULL, NULL},
|
||||
};
|
||||
for (size_t i = 0; options[i][0]; i++) {
|
||||
if (mpv_set_option_string(p->handle, options[i][0], options[i][1]) < 0) {
|
||||
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, options[i][0]);
|
||||
mpv_terminate_destroy(p->handle); p->handle = NULL;
|
||||
pthread_mutex_unlock(&p->lock); return -1;
|
||||
}
|
||||
}
|
||||
if (mpv_initialize(p->handle) < 0) {
|
||||
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "mpv_initialize failed");
|
||||
mpv_terminate_destroy(p->handle); p->handle = NULL;
|
||||
pthread_mutex_unlock(&p->lock); return -1;
|
||||
}
|
||||
mpv_request_log_messages(p->handle, "debug");
|
||||
mpv_observe_property(p->handle, PROP_TIME_POS, "time-pos", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(p->handle, PROP_DURATION, "duration", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(p->handle, PROP_PAUSE, "pause", MPV_FORMAT_FLAG);
|
||||
mpv_observe_property(p->handle, PROP_FPS, "estimated-vf-fps", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(p->handle, PROP_DROPPED, "vo-drop-frame-count", MPV_FORMAT_INT64);
|
||||
mpv_observe_property(p->handle, PROP_TRACKS, "track-list", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(p->handle, PROP_CACHE_DURATION, "demuxer-cache-duration", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(p->handle, PROP_CACHE_STATE, "demuxer-cache-state", MPV_FORMAT_NODE);
|
||||
mpv_observe_property(p->handle, PROP_BUFFERING, "cache-buffering-state", MPV_FORMAT_INT64);
|
||||
mpv_observe_property(p->handle, PROP_VOLUME, "volume", MPV_FORMAT_DOUBLE);
|
||||
mpv_observe_property(p->handle, PROP_MUTE, "mute", MPV_FORMAT_FLAG);
|
||||
mpv_observe_property(p->handle, PROP_SPEED, "speed", MPV_FORMAT_DOUBLE);
|
||||
p->initialized = 1; p->stop_events = 0; p->session_id = 0; p->snapshot.state = MP_HARMONY_IDLE;
|
||||
if (pthread_create(&p->events, NULL, event_loop, p) != 0) {
|
||||
p->initialized = 0; mpv_terminate_destroy(p->handle); p->handle = NULL;
|
||||
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "event thread failed");
|
||||
pthread_mutex_unlock(&p->lock); return -1;
|
||||
}
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *r)
|
||||
{
|
||||
size_t count = r->http_header_count + (r->cookies && r->cookies[0] ? 1 : 0);
|
||||
if (count > MP_HARMONY_MAX_HEADERS) count = MP_HARMONY_MAX_HEADERS;
|
||||
mpv_node *values = calloc(count ? count : 1, sizeof(*values));
|
||||
char **owned = calloc(count ? count : 1, sizeof(*owned));
|
||||
if (!values || !owned) { free(values); free(owned); return -1; }
|
||||
size_t index = 0;
|
||||
for (size_t i = 0; r->http_headers && i < r->http_header_count && index < count; i++) {
|
||||
const char *name = r->http_headers[i].name ? r->http_headers[i].name : "";
|
||||
const char *value = r->http_headers[i].value ? r->http_headers[i].value : "";
|
||||
size_t n = strlen(name) + strlen(value) + 3;
|
||||
owned[index] = malloc(n);
|
||||
if (!owned[index]) continue;
|
||||
snprintf(owned[index], n, "%s: %s", name, value);
|
||||
values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++;
|
||||
}
|
||||
if (r->cookies && r->cookies[0] && index < count) {
|
||||
size_t n = strlen(r->cookies) + 9; owned[index] = malloc(n);
|
||||
if (owned[index]) { snprintf(owned[index], n, "Cookie: %s", r->cookies); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++; }
|
||||
}
|
||||
mpv_node_list list = {(int)index, values, NULL};
|
||||
mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u.list = &list};
|
||||
int rc = mpv_set_property(p->handle, "http-header-fields", MPV_FORMAT_NODE, &node);
|
||||
for (size_t i = 0; i < index; i++) free(owned[i]);
|
||||
free(owned); free(values);
|
||||
if (rc < 0) set_error_locked(p, classify_error(rc, "http-header-fields"), "http-header-fields failed");
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_request *r)
|
||||
{
|
||||
if (!p || !r || !r->url || !r->url[0]) return -1;
|
||||
if (mp_harmony_playback_initialize(p) < 0) return -1;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
|
||||
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.position = 0; p->snapshot.duration = 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; }
|
||||
{ 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");
|
||||
const char *args[] = {"loadfile", r->url, "replace", NULL};
|
||||
int rc = mpv_command(p->handle, args);
|
||||
if (rc < 0) set_error_locked(p, classify_error(rc, mpv_error_string(rc)), mpv_error_string(rc));
|
||||
if (rc >= 0 && r->start_position_ms >= 0) {
|
||||
char position[64];
|
||||
snprintf(position, sizeof(position), "%.3f", (double)r->start_position_ms / 1000.0);
|
||||
const char *seek[] = {"seek", position, "absolute", "exact", NULL};
|
||||
if (mpv_command(p->handle, seek) < 0)
|
||||
set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed");
|
||||
}
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
if (rc >= 0)
|
||||
for (size_t i = 0; i < r->subtitle_file_count; i++) mp_harmony_playback_add_subtitle(p, &r->subtitle_files[i]);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mp_harmony_playback_play(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "pause", "no"); if (r >= 0) p->snapshot.state = MP_HARMONY_PLAYING; pthread_mutex_unlock(&p->lock); return r; }
|
||||
int mp_harmony_playback_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_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe)
|
||||
{
|
||||
if (!p || value != value) return -1;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
|
||||
char amount[64]; snprintf(amount, sizeof(amount), "%.6f", value);
|
||||
const char *kind = mode == MP_HARMONY_SEEK_ABSOLUTE ? "absolute" : (mode == MP_HARMONY_SEEK_PERCENT ? "absolute-percent" : "relative");
|
||||
const char *flags = exact ? "exact" : (keyframe ? "keyframes" : "default");
|
||||
const char *args[] = {"seek", amount, kind, flags, NULL};
|
||||
int r = mpv_command(p->handle, args);
|
||||
if (r >= 0) p->snapshot.state = MP_HARMONY_SEEKING;
|
||||
else set_error_locked(p, MP_HARMONY_ERROR_SEEK, mpv_error_string(r));
|
||||
pthread_mutex_unlock(&p->lock); return r;
|
||||
}
|
||||
|
||||
int mp_harmony_playback_set_speed(mp_harmony_player *p, double speed) { if (!p || speed <= 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.4f", speed); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "speed", v); if (r >= 0) p->snapshot.speed = speed; pthread_mutex_unlock(&p->lock); return r; }
|
||||
int mp_harmony_playback_set_volume(mp_harmony_player *p, double volume) { if (!p || volume < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.3f", volume); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "volume", v); if (r >= 0) p->snapshot.volume = (int)volume; pthread_mutex_unlock(&p->lock); return r; }
|
||||
int mp_harmony_playback_set_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_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_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_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; }
|
||||
int mp_harmony_playback_get_network_info(mp_harmony_player *p, mp_harmony_network_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } update_network_locked(p); *info = p->snapshot.network; pthread_mutex_unlock(&p->lock); return 0; }
|
||||
int mp_harmony_playback_get_snapshot(mp_harmony_player *p, mp_harmony_playback_snapshot *snapshot) { if (!p || !snapshot) return -1; pthread_mutex_lock(&p->lock); *snapshot = p->snapshot; int r = ensure_ready_locked(p); pthread_mutex_unlock(&p->lock); return r; }
|
||||
|
||||
void mp_harmony_playback_destroy(mp_harmony_player *p)
|
||||
{
|
||||
if (!p) return;
|
||||
if (p->lock_ready) {
|
||||
pthread_mutex_lock(&p->lock);
|
||||
int active = p->initialized; p->stop_events = 1;
|
||||
if (p->handle) mpv_wakeup(p->handle);
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
if (active) pthread_join(p->events, NULL);
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (p->handle) mpv_terminate_destroy(p->handle);
|
||||
p->handle = NULL; p->initialized = 0; p->snapshot.state = MP_HARMONY_RELEASED;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
pthread_mutex_destroy(&p->lock);
|
||||
}
|
||||
free(p);
|
||||
}
|
||||
|
||||
const char *mp_harmony_playback_state_name(mp_harmony_state state)
|
||||
{
|
||||
static const char *names[] = {"IDLE", "OPENING", "BUFFERING", "READY", "PLAYING", "PAUSED", "SEEKING", "ENDED", "STOPPED", "ERROR", "RELEASED"};
|
||||
return state >= MP_HARMONY_IDLE && state <= MP_HARMONY_RELEASED ? names[state] : "UNKNOWN";
|
||||
}
|
||||
const char *mp_harmony_playback_error_name(mp_harmony_error_code error)
|
||||
{
|
||||
static const char *names[] = {"NONE", "NETWORK", "TLS", "HTTP", "DEMUX", "UNSUPPORTED_CONTAINER", "UNSUPPORTED_VIDEO_CODEC", "UNSUPPORTED_AUDIO_CODEC", "SUBTITLE", "DECODER", "AUDIO_OUTPUT", "VIDEO_OUTPUT", "SEEK", "FILE_NOT_FOUND", "PERMISSION", "UNKNOWN"};
|
||||
return error >= MP_HARMONY_ERROR_NONE && error <= MP_HARMONY_ERROR_UNKNOWN ? names[error] : "UNKNOWN";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include "mpv/harmony_playback.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
mp_harmony_playback_request request = {0};
|
||||
mp_harmony_playback_snapshot snapshot = {0};
|
||||
request.url = "file:///sample.mp4";
|
||||
(void)snapshot;
|
||||
return request.url == NULL;
|
||||
}
|
||||
Reference in new issue
Block a user