1025 lines
56 KiB
C
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";
|
|
}
|