Files
Velix-MPV-Core/player/harmony_playback.c
T

1025 lines
56 KiB
C

/*
* This file is part of mpv.
*
* mpv is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the FSF.
*/
#include "mpv/harmony_playback.h"
#include "mpv/client.h"
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <math.h>
#include <time.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;
char current_url[MP_HARMONY_URL];
int64_t playlist_entry_id;
int awaiting_start;
int64_t pending_start_position_ms;
int64_t perf_origin_ns;
int64_t buffering_start_ns;
int seek_active;
char tls_ca_file[MP_HARMONY_URL];
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 int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *request);
static int add_subtitle_locked(mp_harmony_player *p, const mp_harmony_subtitle_source *source);
enum mp_harmony_internal_timeline_event {
MP_HARMONY_INTERNAL_RENDER_CONTEXT_READY = 1,
MP_HARMONY_INTERNAL_FIRST_VIDEO_FRAME,
MP_HARMONY_INTERNAL_FIRST_RENDER_BEGIN,
MP_HARMONY_INTERNAL_FIRST_RENDER_END,
MP_HARMONY_INTERNAL_FIRST_SWAP,
MP_HARMONY_INTERNAL_AUDIO_RENDERER_START,
MP_HARMONY_INTERNAL_FIRST_AUDIO_CALLBACK,
MP_HARMONY_INTERNAL_SEEK_FIRST_FRAME,
MP_HARMONY_INTERNAL_SEEK_FIRST_SWAP,
};
/* Private renderer bridge; intentionally absent from the public header. */
MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *player);
MP_HARMONY_EXPORT int mp_harmony_playback_internal_mark_timeline(
mp_harmony_player *player, int event);
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 int64_t clock_now_ns(void)
{
struct timespec now = {0};
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return 0;
return (int64_t)now.tv_sec * 1000000000LL + now.tv_nsec;
}
static void reset_timeline(mp_harmony_playback_timeline *timeline)
{
if (!timeline) return;
int64_t *values = (int64_t *)timeline;
for (size_t i = 0; i < sizeof(*timeline) / sizeof(*values); i++) values[i] = -1;
}
static void reset_metrics(mp_harmony_playback_metrics *metrics)
{
if (!metrics) return;
memset(metrics, 0, sizeof(*metrics));
metrics->network_wait_ms = -1;
metrics->late_frames = -1;
metrics->mistimed_frames = -1;
metrics->decoder_frames = -1;
}
static int64_t elapsed_ms_locked(const mp_harmony_player *p)
{
if (!p || p->perf_origin_ns <= 0) return -1;
int64_t now = clock_now_ns();
return now > p->perf_origin_ns ? (now - p->perf_origin_ns) / 1000000LL : 0;
}
static void set_once(int64_t *value, int64_t timestamp)
{
if (value && *value < 0 && timestamp >= 0) *value = timestamp;
}
static int url_is_network(const char *url)
{
return url && (strncasecmp(url, "http://", 7) == 0 || strncasecmp(url, "https://", 8) == 0);
}
static void mark_timeline_locked(mp_harmony_player *p, int event)
{
if (!p) return;
int64_t elapsed = elapsed_ms_locked(p);
mp_harmony_playback_timeline *t = &p->snapshot.timeline;
switch (event) {
case MP_HARMONY_INTERNAL_RENDER_CONTEXT_READY:
set_once(&t->render_context_ready_ms, elapsed); break;
case MP_HARMONY_INTERNAL_FIRST_VIDEO_FRAME:
set_once(&t->first_video_frame_available_ms, elapsed);
if (p->seek_active) set_once(&t->seek_first_frame_ms, elapsed);
break;
case MP_HARMONY_INTERNAL_FIRST_RENDER_BEGIN:
set_once(&t->first_render_begin_ms, elapsed); break;
case MP_HARMONY_INTERNAL_FIRST_RENDER_END:
set_once(&t->first_render_end_ms, elapsed); break;
case MP_HARMONY_INTERNAL_FIRST_SWAP:
set_once(&t->first_swap_ms, elapsed);
set_once(&t->startup_total_ms, elapsed);
if (p->seek_active) {
set_once(&t->seek_first_swap_ms, elapsed);
set_once(&t->seek_first_visible_frame_ms, elapsed);
if (t->seek_request_ms >= 0)
set_once(&t->seek_recovery_ms, elapsed - t->seek_request_ms);
p->seek_active = 0;
}
break;
case MP_HARMONY_INTERNAL_AUDIO_RENDERER_START:
set_once(&t->audio_renderer_start_ms, elapsed); break;
case MP_HARMONY_INTERNAL_FIRST_AUDIO_CALLBACK:
set_once(&t->first_audio_callback_ms, elapsed); break;
case MP_HARMONY_INTERNAL_SEEK_FIRST_FRAME:
set_once(&t->seek_first_frame_ms, elapsed); break;
case MP_HARMONY_INTERNAL_SEEK_FIRST_SWAP:
set_once(&t->seek_first_swap_ms, elapsed);
set_once(&t->seek_first_visible_frame_ms, elapsed);
if (t->seek_request_ms >= 0)
set_once(&t->seek_recovery_ms, elapsed - t->seek_request_ms);
p->seek_active = 0;
break;
default: break;
}
}
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;
/* The current OHOS matrix has no iconv feature; accept UTF-8 only so
* unsupported input cannot silently fall back to mojibake. */
return strcasecmp(encoding, "utf-8") == 0 || strcasecmp(encoding, "utf8") == 0;
}
static int supported_ass_override(const char *mode)
{
return mode && (strcmp(mode, "no") == 0 || strcmp(mode, "yes") == 0 ||
strcmp(mode, "force") == 0 || strcmp(mode, "scale") == 0 ||
strcmp(mode, "strip") == 0);
}
static int is_cookie_header(const char *name)
{
return name && strcasecmp(name, "cookie") == 0;
}
static int valid_header_name(const char *value)
{
if (!value || !value[0]) return 0;
for (const unsigned char *p = (const unsigned char *)value; *p; p++)
if (*p <= 0x20 || *p >= 0x7f || *p == ':') return 0;
return 1;
}
static int valid_header_value(const char *value)
{
if (!value) return 1;
for (const unsigned char *p = (const unsigned char *)value; *p; p++)
if (*p == '\r' || *p == '\n' || (*p < 0x20 && *p != '\t')) return 0;
return 1;
}
static void redact_text(char *dst, size_t size, const char *src)
{
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;
else if (strncasecmp(p, "x-emby-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "x-auth-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "x-access-token:", 15) == 0) key_len = 15;
else if (strncasecmp(p, "x-api-token:", 12) == 0) key_len = 12;
else if (strncasecmp(p, "access-token:", 13) == 0) key_len = 13;
else if (strncasecmp(p, "auth-token:", 11) == 0) key_len = 11;
else if (strncasecmp(p, "token:", 6) == 0) key_len = 6;
else if (strncasecmp(p, "api-key:", 8) == 0) key_len = 8;
else if (strncasecmp(p, "apikey:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "secret:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "signature:", 10) == 0) key_len = 10;
else if (strncasecmp(p, "sig:", 4) == 0) key_len = 4;
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, "access_token=", 13) == 0 ||
strncasecmp(q, "auth=", 5) == 0 || strncasecmp(q, "key=", 4) == 0 ||
strncasecmp(q, "api_key=", 8) == 0 || strncasecmp(q, "apikey=", 7) == 0 ||
strncasecmp(q, "signature=", 10) == 0 || strncasecmp(q, "x-amz-signature=", 16) == 0 ||
strncasecmp(q, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0 ||
strncasecmp(q, "auth_token=", 11) == 0 || strncasecmp(q, "access-token=", 13) == 0 ||
strncasecmp(q, "api-key=", 8) == 0 || strncasecmp(q, "x-emby-token=", 13) == 0) {
dst[out++] = *p++;
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);
p->snapshot.error.code = code;
copy_text(p->snapshot.error.raw_message, sizeof(p->snapshot.error.raw_message),
p->snapshot.error_message);
}
static mp_harmony_error_code classify_error(int code, const char *message)
{
(void)code;
if (text_has(message, "403") || text_has(message, "404") || text_has(message, "500") ||
text_has(message, "http error")) return MP_HARMONY_ERROR_HTTP;
if (text_has(message, "tls") || text_has(message, "certificate") || text_has(message, "ssl"))
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;
if (code == MPV_ERROR_LOADING_FAILED) return MP_HARMONY_ERROR_NETWORK;
return MP_HARMONY_ERROR_UNKNOWN;
}
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, "demux-bitrate", MPV_FORMAT_INT64, &bitrate) >= 0)
p->snapshot.network.current_bitrate = bitrate;
p->snapshot.metrics.buffering = p->snapshot.network.buffering;
p->snapshot.metrics.cache_duration = p->snapshot.network.cache_duration;
p->snapshot.metrics.cache_bytes = p->snapshot.network.cache_bytes;
p->snapshot.metrics.current_bitrate = p->snapshot.network.current_bitrate;
}
static const char *node_string(mpv_node *node)
{
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};
if (mpv_get_property(p->handle, "track-list", MPV_FORMAT_NODE, &node) < 0) return;
p->track_count = 0;
if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list) {
for (int i = 0; i < node.u.list->num && p->track_count < MP_HARMONY_MAX_TRACKS; i++) {
mpv_node *item = &node.u.list->values[i];
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");
if (v && v[0]) copy_text(p->snapshot.media.container, sizeof(p->snapshot.media.container), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "current-demuxer");
if (v && v[0]) copy_text(p->snapshot.media.demuxer, sizeof(p->snapshot.media.demuxer), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "video-codec");
if (v && v[0]) copy_text(p->snapshot.media.video_codec, sizeof(p->snapshot.media.video_codec), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "audio-codec");
if (v && v[0]) copy_text(p->snapshot.media.audio_codec, sizeof(p->snapshot.media.audio_codec), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "video-format");
if (v && v[0]) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), v); if (v) mpv_free(v);
int64_t i64 = 0;
if (mpv_get_property(p->handle, "width", MPV_FORMAT_INT64, &i64) >= 0) {
p->snapshot.media.width = (int)i64;
p->snapshot.media.video_width = (int)i64;
}
if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) {
p->snapshot.media.height = (int)i64;
p->snapshot.media.video_height = (int)i64;
}
if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.video_bitrate = i64;
if (mpv_get_property(p->handle, "audio-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.audio_bitrate = i64;
if (mpv_get_property(p->handle, "demux-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.bitrate = i64;
if (mpv_get_property(p->handle, "container-fps", MPV_FORMAT_DOUBLE, &p->snapshot.media.fps) < 0)
p->snapshot.media.fps = 0;
double duration = 0;
if (mpv_get_property(p->handle, "duration", MPV_FORMAT_DOUBLE, &duration) >= 0)
p->snapshot.duration = duration;
p->snapshot.media.duration = p->snapshot.duration;
mpv_node node = {0};
if (mpv_get_property(p->handle, "video-params", MPV_FORMAT_NODE, &node) >= 0) {
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));
copy_text(p->snapshot.media.color_matrix, sizeof(p->snapshot.media.color_matrix), node_string(value));
}
else if (strcmp(key, "bit-depth") == 0) p->snapshot.media.bit_depth = node_int(value);
}
}
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;
p->snapshot.media.duration = p->snapshot.duration;
}
else if (id == PROP_FPS && property->format == MPV_FORMAT_DOUBLE) p->snapshot.fps = *(double *)property->data;
else if (id == PROP_DROPPED && property->format == MPV_FORMAT_INT64) {
p->snapshot.dropped_frames = *(int64_t *)property->data;
p->snapshot.metrics.dropped_frames = p->snapshot.dropped_frames;
}
else if (id == PROP_PAUSE && property->format == MPV_FORMAT_FLAG) {
if (*(int *)property->data) p->snapshot.state = MP_HARMONY_PAUSED;
else if (p->snapshot.state == MP_HARMONY_PAUSED || p->snapshot.state == MP_HARMONY_READY) p->snapshot.state = MP_HARMONY_PLAYING;
} 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) {
int was_buffering = p->snapshot.metrics.buffering;
update_network_locked(p);
int is_buffering = p->snapshot.metrics.buffering;
if (is_buffering != was_buffering) {
if (is_buffering) {
p->snapshot.metrics.buffering_enter_count++;
p->buffering_start_ns = clock_now_ns();
p->snapshot.state = MP_HARMONY_BUFFERING;
} else {
p->snapshot.metrics.buffering_exit_count++;
if (p->buffering_start_ns > 0) {
int64_t duration = (clock_now_ns() - p->buffering_start_ns) / 1000000LL;
p->snapshot.metrics.buffering_total_ms += duration;
if (duration > p->snapshot.metrics.buffering_max_single_ms)
p->snapshot.metrics.buffering_max_single_ms = duration;
}
p->buffering_start_ns = 0;
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.") || text_has(log->text, "http error")) {
int status = 0;
const char *http = strstr(log->text, "HTTP/");
if (http && sscanf(http, "HTTP/%*[^ ] %d", &status) == 1 && status >= 100 && status <= 599) {
p->snapshot.network.http_status = status;
if (status == 206) p->snapshot.network.range_supported = 1;
set_once(&p->snapshot.timeline.first_byte_ms, elapsed_ms_locked(p));
if (p->snapshot.timeline.network_open_begin_ms >= 0) {
set_once(&p->snapshot.timeline.network_open_end_ms, elapsed_ms_locked(p));
p->snapshot.metrics.network_wait_ms =
p->snapshot.timeline.first_byte_ms - p->snapshot.timeline.network_open_begin_ms;
}
}
}
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;
set_once(&p->snapshot.timeline.start_file_ms, elapsed_ms_locked(p));
if (url_is_network(p->current_url))
set_once(&p->snapshot.timeline.network_open_begin_ms, elapsed_ms_locked(p));
p->snapshot.state = MP_HARMONY_OPENING;
break;
}
case MPV_EVENT_FILE_LOADED:
{
char *loaded_url = mpv_get_property_string(p->handle, "stream-open-filename");
int matches = !loaded_url || !loaded_url[0] || !p->current_url[0] ||
strcmp(loaded_url, p->current_url) == 0;
if (loaded_url) mpv_free(loaded_url);
if (!matches) break;
p->awaiting_start = 0;
set_once(&p->snapshot.timeline.demux_ready_ms, elapsed_ms_locked(p));
set_once(&p->snapshot.timeline.file_loaded_ms, elapsed_ms_locked(p));
if (p->snapshot.timeline.network_open_begin_ms >= 0) {
set_once(&p->snapshot.timeline.network_open_end_ms, elapsed_ms_locked(p));
if (p->snapshot.timeline.first_byte_ms >= 0)
p->snapshot.metrics.network_wait_ms =
p->snapshot.timeline.first_byte_ms - p->snapshot.timeline.network_open_begin_ms;
else
p->snapshot.metrics.network_wait_ms =
p->snapshot.timeline.network_open_end_ms - p->snapshot.timeline.network_open_begin_ms;
}
refresh_media_locked(p);
if (p->pending_start_position_ms >= 0) {
char position[64];
snprintf(position, sizeof(position), "%.3f", (double)p->pending_start_position_ms / 1000.0);
const char *seek[] = {"seek", position, "absolute", "exact", NULL};
int rc = mpv_command(p->handle, seek);
p->pending_start_position_ms = -1;
if (rc < 0) {
p->snapshot.network.seek_result[0] = '\0';
set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed");
} else {
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), "ok");
}
}
if (p->snapshot.state != MP_HARMONY_ERROR) p->snapshot.state = MP_HARMONY_READY;
break;
}
case MPV_EVENT_PROPERTY_CHANGE:
update_property_locked(p, event->reply_userdata, event->data);
break;
case MPV_EVENT_VIDEO_RECONFIG:
set_once(&p->snapshot.timeline.video_reconfig_ms, elapsed_ms_locked(p));
set_once(&p->snapshot.timeline.video_decoder_ready_ms, elapsed_ms_locked(p));
if (p->seek_active) set_once(&p->snapshot.timeline.seek_video_reconfig_ms, elapsed_ms_locked(p));
break;
case MPV_EVENT_AUDIO_RECONFIG:
set_once(&p->snapshot.timeline.audio_reconfig_ms, elapsed_ms_locked(p));
set_once(&p->snapshot.timeline.audio_decoder_ready_ms, elapsed_ms_locked(p));
break;
case MPV_EVENT_SEEK:
if (p->seek_active) set_once(&p->snapshot.timeline.seek_demux_ms, elapsed_ms_locked(p));
break;
case MPV_EVENT_PLAYBACK_RESTART:
set_once(&p->snapshot.timeline.playback_restart_ms, elapsed_ms_locked(p));
if (p->seek_active) {
set_once(&p->snapshot.timeline.seek_playback_restart_ms, elapsed_ms_locked(p));
if (p->snapshot.timeline.seek_request_ms >= 0)
set_once(&p->snapshot.timeline.seek_recovery_ms,
elapsed_ms_locked(p) - p->snapshot.timeline.seek_request_ms);
}
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);
mp_harmony_error_code error = classify_error(end->error, s);
if (p->snapshot.network.http_status >= 400 && p->snapshot.network.http_status <= 599)
error = MP_HARMONY_ERROR_HTTP;
/* Keep a more specific FFmpeg/TLS/HTTP diagnostic emitted before
* the generic end-file reason instead of replacing it with
* "loading failed". */
if (p->snapshot.error_code == MP_HARMONY_ERROR_NONE ||
p->snapshot.network.http_status >= 400)
set_error_locked(p, error, s);
else
p->snapshot.state = MP_HARMONY_ERROR;
} 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; p->snapshot.volume = 100;
p->snapshot.subtitle_scale = 1.0; p->snapshot.subtitle_position = 100.0;
reset_metrics(&p->snapshot.metrics);
reset_timeline(&p->snapshot.timeline);
p->pending_start_position_ms = -1;
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; }
int64_t create_begin = clock_now_ns();
p->handle = mpv_create();
if (create_begin > 0 && p->handle)
p->snapshot.timeline.mpv_create_ms = (clock_now_ns() - create_begin) / 1000000LL;
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"}, {"sub-fonts-dir", "/system/fonts"}, {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 (p->tls_ca_file[0] && mpv_set_option_string(p->handle, "tls-ca-file", p->tls_ca_file) < 0) {
set_error_locked(p, MP_HARMONY_ERROR_TLS, "tls-ca-file option failed");
mpv_terminate_destroy(p->handle); p->handle = NULL;
pthread_mutex_unlock(&p->lock); return -1;
}
int64_t initialize_begin = clock_now_ns();
int initialize_result = mpv_initialize(p->handle);
if (initialize_begin > 0 && initialize_result >= 0)
p->snapshot.timeline.mpv_initialize_ms = (clock_now_ns() - initialize_begin) / 1000000LL;
if (initialize_result < 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, "frame-drop-count", MPV_FORMAT_INT64);
mpv_observe_property(p->handle, PROP_TRACKS, "track-list", MPV_FORMAT_NODE);
mpv_observe_property(p->handle, PROP_CACHE_DURATION, "demuxer-cache-duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_CACHE_STATE, "demuxer-cache-state", MPV_FORMAT_NODE);
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)
{
if (!r || (r->http_header_count && !r->http_headers)) return -1;
if (!valid_header_value(r->cookies)) return -1;
size_t count = 0;
for (size_t i = 0; i < r->http_header_count; i++) {
const char *name = r->http_headers[i].name;
const char *value = r->http_headers[i].value;
if (name && name[0] && !valid_header_name(name)) return -1;
if (!valid_header_value(value)) return -1;
if (name && name[0] && !(r->cookies && r->cookies[0] && is_cookie_header(name))) count++;
}
if (r->cookies && r->cookies[0]) count++;
if (count > MP_HARMONY_MAX_HEADERS) return -1;
mpv_node *values = calloc(count ? count : 1, sizeof(*values));
char **owned = calloc(count ? count : 1, sizeof(*owned));
if (!values || !owned) { free(values); free(owned); return -1; }
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 : "";
if (!name[0] || (r->cookies && r->cookies[0] && is_cookie_header(name))) continue;
size_t n = strlen(name) + strlen(value) + 3;
owned[index] = malloc(n);
if (!owned[index]) {
for (size_t j = 0; j < index; j++) free(owned[j]);
free(owned); free(values); return -1;
}
snprintf(owned[index], n, "%s: %s", name, value);
values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++;
}
if (r->cookies && r->cookies[0] && index < count) {
size_t n = strlen(r->cookies) + 9; owned[index] = malloc(n);
if (!owned[index]) {
for (size_t j = 0; j < index; j++) free(owned[j]);
free(owned); free(values); return -1;
}
snprintf(owned[index], n, "Cookie: %s", r->cookies); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++;
}
mpv_node_list list = {(int)index, values, NULL};
mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u.list = &list};
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 (r->tls_ca_file && strlen(r->tls_ca_file) >= sizeof(p->tls_ca_file)) return -1;
if (r->http_header_count > MP_HARMONY_MAX_HEADERS ||
r->subtitle_file_count > MP_HARMONY_MAX_SUBTITLES ||
(r->playback_speed != 0.0 &&
(!isfinite(r->playback_speed) || r->playback_speed <= 0)) ||
(r->network_timeout != 0.0 &&
(!isfinite(r->network_timeout) || r->network_timeout < 0))) return -1;
copy_text(p->tls_ca_file, sizeof(p->tls_ca_file), r->tls_ca_file);
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.error.code = MP_HARMONY_ERROR_NONE; p->snapshot.error.raw_message[0] = '\0';
p->snapshot.position = 0; p->snapshot.duration = 0;
p->snapshot.subtitle_delay_ms = 0;
p->snapshot.subtitle_scale = 1.0;
p->snapshot.subtitle_position = 100.0;
p->snapshot.media = (mp_harmony_media_info){0};
p->snapshot.network = (mp_harmony_network_info){0};
int64_t mpv_create_ms = p->snapshot.timeline.mpv_create_ms;
int64_t mpv_initialize_ms = p->snapshot.timeline.mpv_initialize_ms;
reset_metrics(&p->snapshot.metrics);
reset_timeline(&p->snapshot.timeline);
p->snapshot.timeline.mpv_create_ms = mpv_create_ms;
p->snapshot.timeline.mpv_initialize_ms = mpv_initialize_ms;
p->perf_origin_ns = clock_now_ns();
p->snapshot.timeline.session_setup_ms = 0;
p->buffering_start_ns = 0;
p->seek_active = 0;
p->snapshot.track_count = 0; p->track_count = 0; p->awaiting_start = 1;
p->pending_start_position_ms = r->start_position_ms >= 0 ? r->start_position_ms : -1;
copy_text(p->current_url, sizeof(p->current_url), r->url);
p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0;
p->snapshot.network.range_supported = -1; p->snapshot.network.seek_result[0] = '\0';
if (r->subtitle_file_count && !r->subtitle_files) {
set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "subtitle list is null");
pthread_mutex_unlock(&p->lock); return -1;
}
for (size_t i = 0; i < r->subtitle_file_count; i++) {
if (!r->subtitle_files[i].url || !r->subtitle_files[i].url[0]) continue;
if (!supported_subtitle_encoding(r->subtitle_files[i].encoding)) {
set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding");
pthread_mutex_unlock(&p->lock); return -1;
}
}
if (set_headers_locked(p, r) < 0) {
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "http-header-fields failed");
pthread_mutex_unlock(&p->lock); return -1;
}
if (!valid_header_value(r->user_agent) || !valid_header_value(r->referer) ||
!valid_header_value(r->title) || !valid_header_value(r->url)) {
set_error_locked(p, MP_HARMONY_ERROR_HTTP, "invalid header or URL text");
pthread_mutex_unlock(&p->lock); return -1;
}
if (set_property_string(p, "user-agent", r->user_agent ? r->user_agent : "") < 0 ||
set_property_string(p, "referrer", r->referer ? r->referer : "") < 0 ||
set_property_string(p, "force-media-title", r->title ? r->title : "") < 0 ||
set_property_string(p, "alang", r->preferred_audio_language ? r->preferred_audio_language : "") < 0 ||
set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : "") < 0 ||
set_property_string(p, "tls-ca-file", r->tls_ca_file ? r->tls_ca_file : "") < 0) {
pthread_mutex_unlock(&p->lock); return -1;
}
{ char timeout[32]; snprintf(timeout, sizeof(timeout), "%.3f", r->network_timeout > 0 ? r->network_timeout : 0.0); if (set_property_string(p, "network-timeout", timeout) < 0) { pthread_mutex_unlock(&p->lock); return -1; } }
if (r->playback_speed > 0) { char speed[32]; snprintf(speed, sizeof(speed), "%.4f", r->playback_speed); if (set_property_string(p, "speed", speed) < 0) { pthread_mutex_unlock(&p->lock); return -1; } p->snapshot.speed = r->playback_speed; }
{ char id[32]; snprintf(id, sizeof(id), "%d", r->audio_track_id); if (set_property_string(p, "aid", r->audio_track_id >= 0 ? id : "auto") < 0) { pthread_mutex_unlock(&p->lock); return -1; } }
if (r->subtitle_track_id >= 0) { char id[32]; snprintf(id, sizeof(id), "%d", r->subtitle_track_id); if (set_property_string(p, "sid", id) < 0) { pthread_mutex_unlock(&p->lock); return -1; } }
else if (set_property_string(p, "sid", r->subtitle_enabled == 0 ? "no" : "auto") < 0) { pthread_mutex_unlock(&p->lock); return -1; }
const char *args[] = {"loadfile", r->url, "replace", NULL};
int rc = mpv_command(p->handle, args);
set_once(&p->snapshot.timeline.load_command_ms, elapsed_ms_locked(p));
if (rc < 0) set_error_locked(p, classify_error(rc, mpv_error_string(rc)), mpv_error_string(rc));
if (rc >= 0) {
for (size_t i = 0; i < r->subtitle_file_count; i++) {
if (r->subtitle_files[i].url && r->subtitle_files[i].url[0] &&
add_subtitle_locked(p, &r->subtitle_files[i]) < 0) {
rc = -1;
break;
}
}
}
pthread_mutex_unlock(&p->lock);
return rc;
}
int mp_harmony_playback_play(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "pause", "no"); if (r >= 0) p->snapshot.state = MP_HARMONY_PLAYING; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_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 = -1; if (ensure_ready_locked(p) == 0) { r = mpv_command_string(p->handle, "stop"); if (r >= 0) { mp_harmony_playback_request empty = {0}; set_headers_locked(p, &empty); set_property_string(p, "user-agent", ""); set_property_string(p, "referrer", ""); set_property_string(p, "force-media-title", ""); p->pending_start_position_ms = -1; p->snapshot.state = MP_HARMONY_STOPPED; } } pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe)
{
if (!p || !isfinite(value) || mode < MP_HARMONY_SEEK_RELATIVE || mode > MP_HARMONY_SEEK_PERCENT) return -1;
pthread_mutex_lock(&p->lock);
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
p->seek_active = 1;
p->snapshot.timeline.seek_request_ms = elapsed_ms_locked(p);
p->snapshot.timeline.seek_command_ms = -1;
p->snapshot.timeline.seek_demux_ms = -1;
p->snapshot.timeline.seek_video_reconfig_ms = -1;
p->snapshot.timeline.seek_playback_restart_ms = -1;
p->snapshot.timeline.seek_first_frame_ms = -1;
p->snapshot.timeline.seek_first_swap_ms = -1;
p->snapshot.timeline.seek_command_latency_ms = -1;
p->snapshot.timeline.seek_recovery_ms = -1;
p->snapshot.timeline.seek_first_visible_frame_ms = -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" : NULL);
const char *args[] = {"seek", amount, kind, flags, NULL};
int r = mpv_command(p->handle, args);
p->snapshot.timeline.seek_command_ms = elapsed_ms_locked(p);
if (p->snapshot.timeline.seek_request_ms >= 0 && p->snapshot.timeline.seek_command_ms >= 0)
p->snapshot.timeline.seek_command_latency_ms =
p->snapshot.timeline.seek_command_ms - p->snapshot.timeline.seek_request_ms;
if (r >= 0) {
p->snapshot.state = MP_HARMONY_SEEKING;
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), "ok");
} else {
char redacted[MP_HARMONY_TEXT];
redact_text(redacted, sizeof(redacted), mpv_error_string(r));
copy_text(p->snapshot.network.seek_result, sizeof(p->snapshot.network.seek_result), redacted);
set_error_locked(p, MP_HARMONY_ERROR_SEEK, redacted);
p->seek_active = 0;
}
pthread_mutex_unlock(&p->lock); return r;
}
int mp_harmony_playback_set_speed(mp_harmony_player *p, double speed) { if (!p || !isfinite(speed) || speed <= 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.4f", speed); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "speed", v); if (r >= 0) p->snapshot.speed = speed; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_volume(mp_harmony_player *p, double volume) { if (!p || !isfinite(volume) || volume < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.3f", volume); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "volume", v); if (r >= 0) p->snapshot.volume = (int)volume; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_mute(mp_harmony_player *p, int muted) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "mute", muted ? "yes" : "no"); if (r >= 0) p->snapshot.muted = muted != 0; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_audio_enabled(mp_harmony_player *p, int enabled) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", enabled ? "auto" : "no"); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_select_audio(mp_harmony_player *p, int id) { if (!p || id < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", v); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_select_subtitle(mp_harmony_player *p, int id) { if (!p || id < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", v); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_disable_subtitle(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", "no"); pthread_mutex_unlock(&p->lock); return r; }
static int add_subtitle_locked(mp_harmony_player *p, const mp_harmony_subtitle_source *s)
{
if (!s || !s->url || !s->url[0]) return -1;
if (ensure_ready_locked(p) < 0) return -1;
if (!supported_subtitle_encoding(s->encoding)) {
set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding");
return -1;
}
if (s->encoding && s->encoding[0] && set_property_string(p, "sub-codepage", s->encoding) < 0) {
return -1;
}
char flags[64];
snprintf(flags, sizeof(flags), "%s%s%s", s->select ? "select" : "auto",
s->forced ? "+forced" : "", s->is_default ? "+default" : "");
const char *args[] = {"sub-add", s->url, flags, s->title ? s->title : "",
s->language ? s->language : "", NULL};
int r = mpv_command(p->handle, args);
if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r));
return r;
}
int mp_harmony_playback_add_subtitle(mp_harmony_player *p, const mp_harmony_subtitle_source *s)
{
if (!p || !s || !s->url || !s->url[0]) return -1;
pthread_mutex_lock(&p->lock);
int r = add_subtitle_locked(p, s);
pthread_mutex_unlock(&p->lock);
return r;
}
int mp_harmony_playback_remove_subtitle(mp_harmony_player *p, int id) { if (!p || id < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = -1; if (ensure_ready_locked(p) == 0) { const char *args[] = {"sub-remove", v, NULL}; r = mpv_command(p->handle, args); } if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *p, double seconds) { if (!p || !isfinite(seconds)) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", seconds); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-delay", value); if (r >= 0) p->snapshot.subtitle_delay_ms = (int64_t)(seconds * 1000.0); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *p, double scale) { if (!p || !isfinite(scale) || scale <= 0) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", scale); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-scale", value); if (r >= 0) p->snapshot.subtitle_scale = scale; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_position(mp_harmony_player *p, double position) { if (!p || !isfinite(position) || position < 0 || position > 100) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", position); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-pos", value); if (r >= 0) p->snapshot.subtitle_position = position; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_ass_override(mp_harmony_player *p, const char *mode) { if (!p || !supported_ass_override(mode)) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-ass-override", mode); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_get_tracks(mp_harmony_player *p, mp_harmony_track *tracks, size_t capacity, size_t *count) { if (!p || !count) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_tracks_locked(p); size_t n = p->track_count < capacity ? p->track_count : capacity; if (tracks && n) memcpy(tracks, p->tracks, n * sizeof(*tracks)); *count = p->track_count; pthread_mutex_unlock(&p->lock); return 0; }
int mp_harmony_playback_get_media_info(mp_harmony_player *p, mp_harmony_media_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_media_locked(p); *info = p->snapshot.media; pthread_mutex_unlock(&p->lock); return 0; }
int mp_harmony_playback_get_network_info(mp_harmony_player *p, mp_harmony_network_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } update_network_locked(p); *info = p->snapshot.network; pthread_mutex_unlock(&p->lock); return 0; }
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);
}
MP_HARMONY_EXPORT mpv_handle *mp_harmony_playback_internal_handle(mp_harmony_player *p)
{
if (!p) return NULL;
pthread_mutex_lock(&p->lock);
mpv_handle *handle = p->handle;
pthread_mutex_unlock(&p->lock);
return handle;
}
MP_HARMONY_EXPORT int mp_harmony_playback_internal_mark_timeline(mp_harmony_player *p, int event)
{
if (!p) return -1;
pthread_mutex_lock(&p->lock);
if (ensure_ready_locked(p) < 0) {
pthread_mutex_unlock(&p->lock);
return -1;
}
mark_timeline_locked(p, event);
pthread_mutex_unlock(&p->lock);
return 0;
}
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";
}