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

585 lines
35 KiB
C

#include "mpv/harmony_playback.h"
#include "mpv/client.h"
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
struct mp_harmony_player {
pthread_mutex_t lock;
pthread_t events;
int lock_ready;
int initialized;
int stop_events;
mpv_handle *handle;
uint64_t session_id;
int64_t playlist_entry_id;
int awaiting_start;
mp_harmony_playback_snapshot snapshot;
mp_harmony_track tracks[MP_HARMONY_MAX_TRACKS];
size_t track_count;
};
enum {
PROP_TIME_POS = 1, PROP_DURATION, PROP_PAUSE, PROP_FPS, PROP_DROPPED,
PROP_TRACKS, PROP_CACHE_DURATION, PROP_CACHE_STATE, PROP_BUFFERING,
PROP_VOLUME, PROP_MUTE, PROP_SPEED,
};
static void copy_text(char *dst, size_t size, const char *src)
{
if (!dst || size == 0) return;
if (!src) src = "";
snprintf(dst, size, "%.*s", (int)(size - 1), src);
}
static int text_has(const char *text, const char *needle)
{
if (!text || !needle || !needle[0]) return 0;
for (const char *p = text; *p; p++) {
const char *a = p;
const char *b = needle;
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) { a++; b++; }
if (!*b) return 1;
}
return 0;
}
static int supported_subtitle_encoding(const char *encoding)
{
if (!encoding || !encoding[0]) return 1;
return strcasecmp(encoding, "utf-8") == 0 || strcasecmp(encoding, "utf8") == 0 ||
strcasecmp(encoding, "gbk") == 0 || strcasecmp(encoding, "gb18030") == 0 ||
strcasecmp(encoding, "big5") == 0 || strcasecmp(encoding, "shift-jis") == 0 ||
strcasecmp(encoding, "sjis") == 0;
}
static void redact_text(char *dst, size_t size, const char *src)
{
size_t out = 0;
const char *p = src ? src : "";
while (*p && out + 1 < size) {
size_t key_len = 0;
if (strncasecmp(p, "authorization:", 14) == 0) key_len = 14;
else if (strncasecmp(p, "cookie:", 7) == 0) key_len = 7;
else if (strncasecmp(p, "proxy-authorization:", 21) == 0) key_len = 21;
else if (strncasecmp(p, "x-api-key:", 10) == 0) key_len = 10;
if (key_len) {
size_t n = key_len;
while (p[n] && p[n] != '\n' && p[n] != '\r') n++;
if (out + key_len + 11 >= size) break;
memcpy(dst + out, p, key_len); out += key_len;
memcpy(dst + out, " [REDACTED]", 11); out += 11;
p += n;
continue;
}
if (*p == '?' || *p == '&') {
const char *q = p + 1;
if (strncasecmp(q, "token=", 6) == 0 || strncasecmp(q, "auth=", 5) == 0 ||
strncasecmp(q, "key=", 4) == 0 || strncasecmp(q, "signature=", 10) == 0 ||
strncasecmp(q, "sig=", 4) == 0 || strncasecmp(q, "expires=", 8) == 0) {
dst[out++] = *p++;
while (*p && *p != '&' && *p != ' ' && *p != '\n' && out + 1 < size) p++;
if (out + 10 >= size) break;
memcpy(dst + out, "[REDACTED]", 10); out += 10;
continue;
}
}
dst[out++] = *p++;
}
dst[out] = '\0';
}
static void set_error_locked(mp_harmony_player *p, mp_harmony_error_code code, const char *message)
{
p->snapshot.state = MP_HARMONY_ERROR;
p->snapshot.error_code = code;
redact_text(p->snapshot.error_message, sizeof(p->snapshot.error_message), message);
}
static mp_harmony_error_code classify_error(int code, const char *message)
{
if (text_has(message, "403") || text_has(message, "404") || text_has(message, "500") ||
text_has(message, "http error")) return MP_HARMONY_ERROR_HTTP;
if (text_has(message, "tls") || text_has(message, "certificate") || text_has(message, "ssl"))
return MP_HARMONY_ERROR_TLS;
if (text_has(message, "not found") || text_has(message, "no such file"))
return MP_HARMONY_ERROR_FILE_NOT_FOUND;
if (text_has(message, "permission") || text_has(message, "access denied"))
return MP_HARMONY_ERROR_PERMISSION;
if (text_has(message, "subtitle") || text_has(message, "sub ")) return MP_HARMONY_ERROR_SUBTITLE;
if (text_has(message, "video codec") || text_has(message, "video decoder"))
return MP_HARMONY_ERROR_UNSUPPORTED_VIDEO_CODEC;
if (text_has(message, "audio codec") || text_has(message, "audio decoder"))
return MP_HARMONY_ERROR_UNSUPPORTED_AUDIO_CODEC;
if (text_has(message, "unsupported") && text_has(message, "container"))
return MP_HARMONY_ERROR_UNSUPPORTED_CONTAINER;
if (text_has(message, "demux") || text_has(message, "format")) return MP_HARMONY_ERROR_DEMUX;
if (text_has(message, "decoder")) return MP_HARMONY_ERROR_DECODER;
if (text_has(message, "audio output")) return MP_HARMONY_ERROR_AUDIO_OUTPUT;
if (text_has(message, "video output")) return MP_HARMONY_ERROR_VIDEO_OUTPUT;
if (text_has(message, "seek")) return MP_HARMONY_ERROR_SEEK;
if (text_has(message, "timeout") || text_has(message, "connection") ||
text_has(message, "network") || text_has(message, "dns"))
return MP_HARMONY_ERROR_NETWORK;
return code == MPV_ERROR_OPTION_ERROR ? MP_HARMONY_ERROR_UNKNOWN : MP_HARMONY_ERROR_NETWORK;
}
static int set_property_string(mp_harmony_player *p, const char *name, const char *value)
{
int rc = mpv_set_property_string(p->handle, name, value ? value : "");
if (rc < 0) {
char message[MP_HARMONY_TEXT];
snprintf(message, sizeof(message), "%s: %s", name, mpv_error_string(rc));
set_error_locked(p, classify_error(rc, message), message);
}
return rc;
}
static void update_network_locked(mp_harmony_player *p)
{
char *url = mpv_get_property_string(p->handle, "stream-open-filename");
if (!url) return;
const char *scheme = strstr(url, "://");
if (scheme) {
size_t n = (size_t)(scheme - url);
if (n >= sizeof(p->snapshot.network.protocol)) n = sizeof(p->snapshot.network.protocol) - 1;
memcpy(p->snapshot.network.protocol, url, n); p->snapshot.network.protocol[n] = '\0';
const char *host = scheme + 3;
const char *end = strpbrk(host, "/?#");
size_t hn = end ? (size_t)(end - host) : strlen(host);
if (hn >= sizeof(p->snapshot.network.host)) hn = sizeof(p->snapshot.network.host) - 1;
memcpy(p->snapshot.network.host, host, hn); p->snapshot.network.host[hn] = '\0';
}
mpv_free(url);
int flag = 0;
if (mpv_get_property(p->handle, "demuxer-via-network", MPV_FORMAT_FLAG, &flag) >= 0)
copy_text(p->snapshot.network.network_state, sizeof(p->snapshot.network.network_state), flag ? "network" : "local");
double duration = 0;
if (mpv_get_property(p->handle, "demuxer-cache-duration", MPV_FORMAT_DOUBLE, &duration) >= 0)
p->snapshot.network.cache_duration = duration;
mpv_node cache = {0};
if (mpv_get_property(p->handle, "demuxer-cache-state", MPV_FORMAT_NODE, &cache) >= 0) {
if (cache.format == MPV_FORMAT_NODE_MAP && cache.u.list) {
for (int i = 0; i < cache.u.list->num; i++) {
const char *key = cache.u.list->keys[i];
mpv_node *value = &cache.u.list->values[i];
if (key && strcmp(key, "total-bytes") == 0 && value->format == MPV_FORMAT_INT64)
p->snapshot.network.cache_bytes = value->u.int64;
}
}
mpv_free_node_contents(&cache);
}
int64_t buffering = 0;
if (mpv_get_property(p->handle, "cache-buffering-state", MPV_FORMAT_INT64, &buffering) >= 0)
p->snapshot.network.buffering = buffering > 0;
char *demuxer = mpv_get_property_string(p->handle, "current-demuxer");
char *container = mpv_get_property_string(p->handle, "file-format");
copy_text(p->snapshot.network.demuxer, sizeof(p->snapshot.network.demuxer), demuxer);
copy_text(p->snapshot.network.container, sizeof(p->snapshot.network.container), container);
if (demuxer) mpv_free(demuxer);
if (container) mpv_free(container);
int64_t bitrate = 0;
if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &bitrate) >= 0)
p->snapshot.network.current_bitrate = bitrate;
}
static const char *node_string(mpv_node *node)
{
return node && node->format == MPV_FORMAT_STRING ? node->u.string : NULL;
}
static int node_flag(mpv_node *node)
{
if (!node) return 0;
if (node->format == MPV_FORMAT_FLAG) return node->u.flag != 0;
return node->format == MPV_FORMAT_INT64 && node->u.int64 != 0;
}
static int node_int(mpv_node *node)
{
if (!node) return 0;
if (node->format == MPV_FORMAT_INT64) return (int)node->u.int64;
if (node->format == MPV_FORMAT_DOUBLE) return (int)node->u.double_;
return 0;
}
static void refresh_tracks_locked(mp_harmony_player *p)
{
mpv_node node = {0};
p->track_count = 0;
if (mpv_get_property(p->handle, "track-list", MPV_FORMAT_NODE, &node) < 0) return;
if (node.format == MPV_FORMAT_NODE_ARRAY && node.u.list) {
for (int i = 0; i < node.u.list->num && p->track_count < MP_HARMONY_MAX_TRACKS; i++) {
mpv_node *item = &node.u.list->values[i];
if (item->format != MPV_FORMAT_NODE_MAP || !item->u.list) continue;
mp_harmony_track *track = &p->tracks[p->track_count];
memset(track, 0, sizeof(*track));
for (int j = 0; j < item->u.list->num; j++) {
const char *key = item->u.list->keys[j];
mpv_node *value = &item->u.list->values[j];
if (!key) continue;
if (strcmp(key, "id") == 0) track->id = node_int(value);
else if (strcmp(key, "type") == 0) {
const char *s = node_string(value);
track->type = s && strcmp(s, "audio") == 0 ? MP_HARMONY_TRACK_AUDIO :
(s && strcmp(s, "sub") == 0 ? MP_HARMONY_TRACK_SUBTITLE : MP_HARMONY_TRACK_VIDEO);
} else if (strcmp(key, "title") == 0) copy_text(track->title, sizeof(track->title), node_string(value));
else if (strcmp(key, "lang") == 0) copy_text(track->language, sizeof(track->language), node_string(value));
else if (strcmp(key, "codec") == 0) copy_text(track->codec, sizeof(track->codec), node_string(value));
else if (strcmp(key, "default") == 0) track->is_default = node_flag(value);
else if (strcmp(key, "forced") == 0) track->forced = node_flag(value);
else if (strcmp(key, "selected") == 0) track->selected = node_flag(value);
else if (strcmp(key, "external") == 0) track->external = node_flag(value);
}
p->track_count++;
}
}
mpv_free_node_contents(&node);
p->snapshot.track_count = (int)p->track_count;
p->snapshot.media.track_count = (int)p->track_count;
p->snapshot.media.subtitle_count = 0;
for (size_t i = 0; i < p->track_count; i++)
if (p->tracks[i].type == MP_HARMONY_TRACK_SUBTITLE) p->snapshot.media.subtitle_count++;
}
static void refresh_media_locked(mp_harmony_player *p)
{
char *v = mpv_get_property_string(p->handle, "file-format");
copy_text(p->snapshot.media.container, sizeof(p->snapshot.media.container), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "current-demuxer");
copy_text(p->snapshot.media.demuxer, sizeof(p->snapshot.media.demuxer), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "video-codec");
copy_text(p->snapshot.media.video_codec, sizeof(p->snapshot.media.video_codec), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "audio-codec");
copy_text(p->snapshot.media.audio_codec, sizeof(p->snapshot.media.audio_codec), v); if (v) mpv_free(v);
v = mpv_get_property_string(p->handle, "video-format");
copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), v); if (v) mpv_free(v);
int64_t i64 = 0;
if (mpv_get_property(p->handle, "width", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.width = (int)i64;
if (mpv_get_property(p->handle, "height", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.height = (int)i64;
if (mpv_get_property(p->handle, "video-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.video_bitrate = i64;
if (mpv_get_property(p->handle, "audio-bitrate", MPV_FORMAT_INT64, &i64) >= 0) p->snapshot.media.audio_bitrate = i64;
if (mpv_get_property(p->handle, "container-fps", MPV_FORMAT_DOUBLE, &p->snapshot.media.fps) < 0)
p->snapshot.media.fps = 0;
if (mpv_get_property(p->handle, "duration", MPV_FORMAT_DOUBLE, &p->snapshot.duration) < 0)
p->snapshot.duration = 0;
mpv_node node = {0};
if (mpv_get_property(p->handle, "video-params", MPV_FORMAT_NODE, &node) >= 0) {
if (node.format == MPV_FORMAT_NODE_MAP && node.u.list) {
for (int j = 0; j < node.u.list->num; j++) {
const char *key = node.u.list->keys[j]; mpv_node *value = &node.u.list->values[j];
if (!key) continue;
if (strcmp(key, "pixelformat") == 0) copy_text(p->snapshot.media.pixel_format, sizeof(p->snapshot.media.pixel_format), node_string(value));
else if (strcmp(key, "primaries") == 0) copy_text(p->snapshot.media.color_primaries, sizeof(p->snapshot.media.color_primaries), node_string(value));
else if (strcmp(key, "gamma") == 0) copy_text(p->snapshot.media.color_transfer, sizeof(p->snapshot.media.color_transfer), node_string(value));
else if (strcmp(key, "colormatrix") == 0) copy_text(p->snapshot.media.color_space, sizeof(p->snapshot.media.color_space), node_string(value));
else if (strcmp(key, "bit-depth") == 0) p->snapshot.media.bit_depth = node_int(value);
}
}
mpv_free_node_contents(&node);
}
memset(&node, 0, sizeof(node));
if (mpv_get_property(p->handle, "audio-params", MPV_FORMAT_NODE, &node) >= 0) {
if (node.format == MPV_FORMAT_NODE_MAP && node.u.list) {
for (int j = 0; j < node.u.list->num; j++) {
const char *key = node.u.list->keys[j]; mpv_node *value = &node.u.list->values[j];
if (!key) continue;
if (strcmp(key, "samplerate") == 0) p->snapshot.media.audio_sample_rate = node_int(value);
else if (strcmp(key, "channels") == 0) p->snapshot.media.audio_channels = node_int(value);
else if (strcmp(key, "channel-layout") == 0) copy_text(p->snapshot.media.audio_channel_layout, sizeof(p->snapshot.media.audio_channel_layout), node_string(value));
}
}
mpv_free_node_contents(&node);
}
refresh_tracks_locked(p);
update_network_locked(p);
}
static void update_property_locked(mp_harmony_player *p, uint64_t id, mpv_event_property *property)
{
if (!property || !property->data) return;
if (id == PROP_TIME_POS && property->format == MPV_FORMAT_DOUBLE) p->snapshot.position = *(double *)property->data;
else if (id == PROP_DURATION && property->format == MPV_FORMAT_DOUBLE) p->snapshot.duration = *(double *)property->data;
else if (id == PROP_FPS && property->format == MPV_FORMAT_DOUBLE) p->snapshot.fps = *(double *)property->data;
else if (id == PROP_DROPPED && property->format == MPV_FORMAT_INT64) p->snapshot.dropped_frames = *(int64_t *)property->data;
else if (id == PROP_PAUSE && property->format == MPV_FORMAT_FLAG) {
if (*(int *)property->data) p->snapshot.state = MP_HARMONY_PAUSED;
else if (p->snapshot.state == MP_HARMONY_PAUSED || p->snapshot.state == MP_HARMONY_READY) p->snapshot.state = MP_HARMONY_PLAYING;
} else if (id == PROP_VOLUME && property->format == MPV_FORMAT_DOUBLE) p->snapshot.volume = (int)*(double *)property->data;
else if (id == PROP_MUTE && property->format == MPV_FORMAT_FLAG) p->snapshot.muted = *(int *)property->data != 0;
else if (id == PROP_SPEED && property->format == MPV_FORMAT_DOUBLE) p->snapshot.speed = *(double *)property->data;
else if (id == PROP_TRACKS) refresh_tracks_locked(p);
else if (id == PROP_CACHE_DURATION || id == PROP_CACHE_STATE || id == PROP_BUFFERING) {
update_network_locked(p);
if (id == PROP_BUFFERING && property->format == MPV_FORMAT_INT64) {
int64_t buffering = *(int64_t *)property->data;
if (buffering > 0) p->snapshot.state = MP_HARMONY_BUFFERING;
else if (p->snapshot.state == MP_HARMONY_BUFFERING) p->snapshot.state = MP_HARMONY_PLAYING;
}
}
}
static void handle_log_locked(mp_harmony_player *p, const mpv_event_log_message *log)
{
if (!log || !log->text) return;
char redacted[MP_HARMONY_TEXT]; redact_text(redacted, sizeof(redacted), log->text);
if (strstr(log->text, "HTTP/1.") || strstr(log->text, "http error")) {
int status = 0;
if (sscanf(log->text, "%*[^0-9]%d", &status) == 1 && status >= 100 && status <= 599)
p->snapshot.network.http_status = status;
}
if (text_has(log->text, "redirect")) p->snapshot.network.redirect_count++;
if (log->prefix && (strcmp(log->prefix, "cplayer") == 0 || strcmp(log->prefix, "ffmpeg") == 0) &&
log->log_level <= MPV_LOG_LEVEL_ERROR)
set_error_locked(p, classify_error(0, redacted), redacted);
}
static void *event_loop(void *arg)
{
mp_harmony_player *p = arg;
while (1) {
pthread_mutex_lock(&p->lock);
int stop = p->stop_events || !p->handle;
mpv_handle *h = p->handle;
pthread_mutex_unlock(&p->lock);
if (stop) break;
mpv_event *event = mpv_wait_event(h, 0.05);
if (!event) continue;
pthread_mutex_lock(&p->lock);
if (p->stop_events) { pthread_mutex_unlock(&p->lock); break; }
switch (event->event_id) {
case MPV_EVENT_START_FILE: {
mpv_event_start_file *start = event->data;
p->awaiting_start = 1;
if (start) p->playlist_entry_id = start->playlist_entry_id;
p->snapshot.state = MP_HARMONY_OPENING;
break;
}
case MPV_EVENT_FILE_LOADED:
p->awaiting_start = 0; refresh_media_locked(p); p->snapshot.state = MP_HARMONY_READY;
break;
case MPV_EVENT_PROPERTY_CHANGE:
update_property_locked(p, event->reply_userdata, event->data);
break;
case MPV_EVENT_LOG_MESSAGE:
handle_log_locked(p, event->data);
break;
case MPV_EVENT_END_FILE: {
mpv_event_end_file *end = event->data;
if (end && end->playlist_entry_id && p->playlist_entry_id &&
end->playlist_entry_id != p->playlist_entry_id) break;
if (end && end->reason == MPV_END_FILE_REASON_ERROR) {
const char *s = mpv_error_string(end->error);
set_error_locked(p, classify_error(end->error, s), s);
} else if (end && end->reason == MPV_END_FILE_REASON_EOF) p->snapshot.state = MP_HARMONY_ENDED;
else p->snapshot.state = MP_HARMONY_STOPPED;
break;
}
case MPV_EVENT_SHUTDOWN:
p->snapshot.state = MP_HARMONY_RELEASED;
pthread_mutex_unlock(&p->lock); return NULL;
default: break;
}
pthread_mutex_unlock(&p->lock);
}
return NULL;
}
static int ensure_ready_locked(mp_harmony_player *p)
{
return p && p->initialized && p->handle ? 0 : -1;
}
mp_harmony_player *mp_harmony_playback_create(void)
{
mp_harmony_player *p = calloc(1, sizeof(*p));
if (!p) return NULL;
if (pthread_mutex_init(&p->lock, NULL) != 0) { free(p); return NULL; }
p->lock_ready = 1; p->snapshot.state = MP_HARMONY_IDLE;
p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.speed = 1.0;
return p;
}
int mp_harmony_playback_initialize(mp_harmony_player *p)
{
if (!p || !p->lock_ready) return -1;
pthread_mutex_lock(&p->lock);
if (p->initialized) { pthread_mutex_unlock(&p->lock); return 0; }
p->handle = mpv_create();
if (!p->handle) { set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "mpv_create failed"); pthread_mutex_unlock(&p->lock); return -1; }
const char *options[][2] = {
{"vo", "libmpv"}, {"terminal", "no"}, {"config", "no"}, {"hwdec", "no"},
{"aid", "auto"}, {"cache", "auto"}, {"cache-pause", "yes"}, {"cookies", "no"},
{"tls-verify", "yes"}, {NULL, NULL},
};
for (size_t i = 0; options[i][0]; i++) {
if (mpv_set_option_string(p->handle, options[i][0], options[i][1]) < 0) {
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, options[i][0]);
mpv_terminate_destroy(p->handle); p->handle = NULL;
pthread_mutex_unlock(&p->lock); return -1;
}
}
if (mpv_initialize(p->handle) < 0) {
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "mpv_initialize failed");
mpv_terminate_destroy(p->handle); p->handle = NULL;
pthread_mutex_unlock(&p->lock); return -1;
}
mpv_request_log_messages(p->handle, "debug");
mpv_observe_property(p->handle, PROP_TIME_POS, "time-pos", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_DURATION, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_PAUSE, "pause", MPV_FORMAT_FLAG);
mpv_observe_property(p->handle, PROP_FPS, "estimated-vf-fps", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_DROPPED, "vo-drop-frame-count", MPV_FORMAT_INT64);
mpv_observe_property(p->handle, PROP_TRACKS, "track-list", MPV_FORMAT_NODE);
mpv_observe_property(p->handle, PROP_CACHE_DURATION, "demuxer-cache-duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_CACHE_STATE, "demuxer-cache-state", MPV_FORMAT_NODE);
mpv_observe_property(p->handle, PROP_BUFFERING, "cache-buffering-state", MPV_FORMAT_INT64);
mpv_observe_property(p->handle, PROP_VOLUME, "volume", MPV_FORMAT_DOUBLE);
mpv_observe_property(p->handle, PROP_MUTE, "mute", MPV_FORMAT_FLAG);
mpv_observe_property(p->handle, PROP_SPEED, "speed", MPV_FORMAT_DOUBLE);
p->initialized = 1; p->stop_events = 0; p->session_id = 0; p->snapshot.state = MP_HARMONY_IDLE;
if (pthread_create(&p->events, NULL, event_loop, p) != 0) {
p->initialized = 0; mpv_terminate_destroy(p->handle); p->handle = NULL;
set_error_locked(p, MP_HARMONY_ERROR_UNKNOWN, "event thread failed");
pthread_mutex_unlock(&p->lock); return -1;
}
pthread_mutex_unlock(&p->lock);
return 0;
}
static int set_headers_locked(mp_harmony_player *p, const mp_harmony_playback_request *r)
{
size_t count = r->http_header_count + (r->cookies && r->cookies[0] ? 1 : 0);
if (count > MP_HARMONY_MAX_HEADERS) count = MP_HARMONY_MAX_HEADERS;
mpv_node *values = calloc(count ? count : 1, sizeof(*values));
char **owned = calloc(count ? count : 1, sizeof(*owned));
if (!values || !owned) { free(values); free(owned); return -1; }
size_t index = 0;
for (size_t i = 0; r->http_headers && i < r->http_header_count && index < count; i++) {
const char *name = r->http_headers[i].name ? r->http_headers[i].name : "";
const char *value = r->http_headers[i].value ? r->http_headers[i].value : "";
size_t n = strlen(name) + strlen(value) + 3;
owned[index] = malloc(n);
if (!owned[index]) continue;
snprintf(owned[index], n, "%s: %s", name, value);
values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++;
}
if (r->cookies && r->cookies[0] && index < count) {
size_t n = strlen(r->cookies) + 9; owned[index] = malloc(n);
if (owned[index]) { snprintf(owned[index], n, "Cookie: %s", r->cookies); values[index].format = MPV_FORMAT_STRING; values[index].u.string = owned[index]; index++; }
}
mpv_node_list list = {(int)index, values, NULL};
mpv_node node = {.format = MPV_FORMAT_NODE_ARRAY, .u.list = &list};
int rc = mpv_set_property(p->handle, "http-header-fields", MPV_FORMAT_NODE, &node);
for (size_t i = 0; i < index; i++) free(owned[i]);
free(owned); free(values);
if (rc < 0) set_error_locked(p, classify_error(rc, "http-header-fields"), "http-header-fields failed");
return rc;
}
int mp_harmony_playback_load(mp_harmony_player *p, const mp_harmony_playback_request *r)
{
if (!p || !r || !r->url || !r->url[0]) return -1;
if (mp_harmony_playback_initialize(p) < 0) return -1;
pthread_mutex_lock(&p->lock);
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
p->session_id++; p->snapshot.session_id = p->session_id; p->snapshot.state = MP_HARMONY_OPENING;
p->snapshot.error_code = MP_HARMONY_ERROR_NONE; p->snapshot.error_message[0] = '\0';
p->snapshot.position = 0; p->snapshot.duration = 0;
p->snapshot.network.http_status = 0; p->snapshot.network.redirect_count = 0;
set_headers_locked(p, r);
set_property_string(p, "user-agent", r->user_agent ? r->user_agent : "");
set_property_string(p, "referrer", r->referer ? r->referer : "");
set_property_string(p, "alang", r->preferred_audio_language ? r->preferred_audio_language : "");
set_property_string(p, "slang", r->preferred_subtitle_language ? r->preferred_subtitle_language : "");
{ char timeout[32]; snprintf(timeout, sizeof(timeout), "%.3f", r->network_timeout > 0 ? r->network_timeout : 0.0); set_property_string(p, "network-timeout", timeout); }
if (r->playback_speed > 0) { char speed[32]; snprintf(speed, sizeof(speed), "%.4f", r->playback_speed); set_property_string(p, "speed", speed); p->snapshot.speed = r->playback_speed; }
{ char id[32]; snprintf(id, sizeof(id), "%d", r->audio_track_id); set_property_string(p, "aid", r->audio_track_id >= 0 ? id : "auto"); }
if (r->subtitle_track_id >= 0) { char id[32]; snprintf(id, sizeof(id), "%d", r->subtitle_track_id); set_property_string(p, "sid", id); }
else set_property_string(p, "sid", r->subtitle_enabled == 0 ? "no" : "auto");
const char *args[] = {"loadfile", r->url, "replace", NULL};
int rc = mpv_command(p->handle, args);
if (rc < 0) set_error_locked(p, classify_error(rc, mpv_error_string(rc)), mpv_error_string(rc));
if (rc >= 0 && r->start_position_ms >= 0) {
char position[64];
snprintf(position, sizeof(position), "%.3f", (double)r->start_position_ms / 1000.0);
const char *seek[] = {"seek", position, "absolute", "exact", NULL};
if (mpv_command(p->handle, seek) < 0)
set_error_locked(p, MP_HARMONY_ERROR_SEEK, "initial seek failed");
}
pthread_mutex_unlock(&p->lock);
if (rc >= 0)
for (size_t i = 0; i < r->subtitle_file_count; i++) mp_harmony_playback_add_subtitle(p, &r->subtitle_files[i]);
return rc;
}
int mp_harmony_playback_play(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "pause", "no"); if (r >= 0) p->snapshot.state = MP_HARMONY_PLAYING; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_pause(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "pause", "yes"); if (r >= 0) p->snapshot.state = MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_toggle_pause(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } int paused = 0; mpv_get_property(p->handle, "pause", MPV_FORMAT_FLAG, &paused); int r = set_property_string(p, "pause", paused ? "no" : "yes"); if (r >= 0) p->snapshot.state = paused ? MP_HARMONY_PLAYING : MP_HARMONY_PAUSED; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_stop(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : mpv_command_string(p->handle, "stop"); if (r >= 0) p->snapshot.state = MP_HARMONY_STOPPED; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_seek(mp_harmony_player *p, mp_harmony_seek_mode mode, double value, int exact, int keyframe)
{
if (!p || value != value) return -1;
pthread_mutex_lock(&p->lock);
if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; }
char amount[64]; snprintf(amount, sizeof(amount), "%.6f", value);
const char *kind = mode == MP_HARMONY_SEEK_ABSOLUTE ? "absolute" : (mode == MP_HARMONY_SEEK_PERCENT ? "absolute-percent" : "relative");
const char *flags = exact ? "exact" : (keyframe ? "keyframes" : "default");
const char *args[] = {"seek", amount, kind, flags, NULL};
int r = mpv_command(p->handle, args);
if (r >= 0) p->snapshot.state = MP_HARMONY_SEEKING;
else set_error_locked(p, MP_HARMONY_ERROR_SEEK, mpv_error_string(r));
pthread_mutex_unlock(&p->lock); return r;
}
int mp_harmony_playback_set_speed(mp_harmony_player *p, double speed) { if (!p || speed <= 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.4f", speed); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "speed", v); if (r >= 0) p->snapshot.speed = speed; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_volume(mp_harmony_player *p, double volume) { if (!p || volume < 0) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%.3f", volume); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "volume", v); if (r >= 0) p->snapshot.volume = (int)volume; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_mute(mp_harmony_player *p, int muted) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "mute", muted ? "yes" : "no"); if (r >= 0) p->snapshot.muted = muted != 0; pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_select_audio(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "aid", v); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_select_subtitle(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", v); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_disable_subtitle(mp_harmony_player *p) { if (!p) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sid", "no"); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_add_subtitle(mp_harmony_player *p, const mp_harmony_subtitle_source *s) { if (!p || !s || !s->url || !s->url[0]) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } if (!supported_subtitle_encoding(s->encoding)) { set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, "unsupported subtitle encoding"); pthread_mutex_unlock(&p->lock); return -1; } if (s->encoding && s->encoding[0]) set_property_string(p, "sub-codepage", s->encoding); const char *flags = s->select ? "select" : "auto"; const char *args[] = {"sub-add", s->url, flags, s->title ? s->title : "", s->language ? s->language : "", NULL}; int r = mpv_command(p->handle, args); if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_remove_subtitle(mp_harmony_player *p, int id) { if (!p) return -1; pthread_mutex_lock(&p->lock); char v[32]; snprintf(v, sizeof(v), "%d", id); int r = -1; if (ensure_ready_locked(p) == 0) { const char *args[] = {"sub-remove", v, NULL}; r = mpv_command(p->handle, args); } if (r < 0) set_error_locked(p, MP_HARMONY_ERROR_SUBTITLE, mpv_error_string(r)); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_delay(mp_harmony_player *p, double seconds) { if (!p) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", seconds); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-delay", value); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_scale(mp_harmony_player *p, double scale) { if (!p || scale <= 0) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", scale); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-scale", value); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_subtitle_position(mp_harmony_player *p, double position) { if (!p || position < 0 || position > 100) return -1; pthread_mutex_lock(&p->lock); char value[32]; snprintf(value, sizeof(value), "%.4f", position); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-pos", value); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_set_ass_override(mp_harmony_player *p, const char *mode) { if (!p || !mode) return -1; pthread_mutex_lock(&p->lock); int r = ensure_ready_locked(p) < 0 ? -1 : set_property_string(p, "sub-ass-override", mode); pthread_mutex_unlock(&p->lock); return r; }
int mp_harmony_playback_get_tracks(mp_harmony_player *p, mp_harmony_track *tracks, size_t capacity, size_t *count) { if (!p || !count) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_tracks_locked(p); size_t n = p->track_count < capacity ? p->track_count : capacity; if (tracks && n) memcpy(tracks, p->tracks, n * sizeof(*tracks)); *count = p->track_count; pthread_mutex_unlock(&p->lock); return 0; }
int mp_harmony_playback_get_media_info(mp_harmony_player *p, mp_harmony_media_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } refresh_media_locked(p); *info = p->snapshot.media; pthread_mutex_unlock(&p->lock); return 0; }
int mp_harmony_playback_get_network_info(mp_harmony_player *p, mp_harmony_network_info *info) { if (!p || !info) return -1; pthread_mutex_lock(&p->lock); if (ensure_ready_locked(p) < 0) { pthread_mutex_unlock(&p->lock); return -1; } update_network_locked(p); *info = p->snapshot.network; pthread_mutex_unlock(&p->lock); return 0; }
int mp_harmony_playback_get_snapshot(mp_harmony_player *p, mp_harmony_playback_snapshot *snapshot) { if (!p || !snapshot) return -1; pthread_mutex_lock(&p->lock); *snapshot = p->snapshot; int r = ensure_ready_locked(p); pthread_mutex_unlock(&p->lock); return r; }
void mp_harmony_playback_destroy(mp_harmony_player *p)
{
if (!p) return;
if (p->lock_ready) {
pthread_mutex_lock(&p->lock);
int active = p->initialized; p->stop_events = 1;
if (p->handle) mpv_wakeup(p->handle);
pthread_mutex_unlock(&p->lock);
if (active) pthread_join(p->events, NULL);
pthread_mutex_lock(&p->lock);
if (p->handle) mpv_terminate_destroy(p->handle);
p->handle = NULL; p->initialized = 0; p->snapshot.state = MP_HARMONY_RELEASED;
pthread_mutex_unlock(&p->lock);
pthread_mutex_destroy(&p->lock);
}
free(p);
}
const char *mp_harmony_playback_state_name(mp_harmony_state state)
{
static const char *names[] = {"IDLE", "OPENING", "BUFFERING", "READY", "PLAYING", "PAUSED", "SEEKING", "ENDED", "STOPPED", "ERROR", "RELEASED"};
return state >= MP_HARMONY_IDLE && state <= MP_HARMONY_RELEASED ? names[state] : "UNKNOWN";
}
const char *mp_harmony_playback_error_name(mp_harmony_error_code error)
{
static const char *names[] = {"NONE", "NETWORK", "TLS", "HTTP", "DEMUX", "UNSUPPORTED_CONTAINER", "UNSUPPORTED_VIDEO_CODEC", "UNSUPPORTED_AUDIO_CODEC", "SUBTITLE", "DECODER", "AUDIO_OUTPUT", "VIDEO_OUTPUT", "SEEK", "FILE_NOT_FOUND", "PERMISSION", "UNKNOWN"};
return error >= MP_HARMONY_ERROR_NONE && error <= MP_HARMONY_ERROR_UNKNOWN ? names[error] : "UNKNOWN";
}