summaryrefslogtreecommitdiffstats
path: root/player
diff options
context:
space:
mode:
Diffstat (limited to 'player')
-rw-r--r--player/audio.c985
-rw-r--r--player/client.c2248
-rw-r--r--player/client.h58
-rw-r--r--player/command.c7149
-rw-r--r--player/command.h123
-rw-r--r--player/configfiles.c472
-rw-r--r--player/core.h644
-rw-r--r--player/external_files.c359
-rw-r--r--player/external_files.h38
-rw-r--r--player/javascript.c1262
-rw-r--r--player/javascript/defaults.js782
-rw-r--r--player/javascript/meson.build6
-rw-r--r--player/loadfile.c2066
-rw-r--r--player/lua.c1341
-rw-r--r--player/lua/assdraw.lua160
-rw-r--r--player/lua/auto_profiles.lua198
-rw-r--r--player/lua/console.lua1204
-rw-r--r--player/lua/defaults.lua836
-rw-r--r--player/lua/meson.build10
-rw-r--r--player/lua/options.lua164
-rw-r--r--player/lua/osc.lua2917
-rw-r--r--player/lua/stats.lua1417
-rw-r--r--player/lua/ytdl_hook.lua1191
-rw-r--r--player/main.c467
-rw-r--r--player/meson.build10
-rw-r--r--player/misc.c334
-rw-r--r--player/osd.c580
-rw-r--r--player/playloop.c1291
-rw-r--r--player/screenshot.c611
-rw-r--r--player/screenshot.h46
-rw-r--r--player/scripting.c462
-rw-r--r--player/sub.c214
-rw-r--r--player/video.c1324
33 files changed, 30969 insertions, 0 deletions
diff --git a/player/audio.c b/player/audio.c
new file mode 100644
index 0000000..ca17d33
--- /dev/null
+++ b/player/audio.c
@@ -0,0 +1,985 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "common/encode.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "osdep/timer.h"
+
+#include "audio/format.h"
+#include "audio/out/ao.h"
+#include "demux/demux.h"
+#include "filters/f_async_queue.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+
+#include "core.h"
+#include "command.h"
+
+enum {
+ AD_OK = 0,
+ AD_EOF = -2,
+ AD_WAIT = -4,
+};
+
+static void ao_process(struct mp_filter *f);
+
+static void update_speed_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return;
+
+ double speed = mpctx->opts->playback_speed;
+ double resample = mpctx->speed_factor_a;
+ double drop = 1.0;
+
+ if (!mpctx->opts->pitch_correction) {
+ resample *= speed;
+ speed = 1.0;
+ }
+
+ if (mpctx->display_sync_active) {
+ switch (mpctx->video_out->opts->video_sync) {
+ case VS_DISP_ADROP:
+ drop *= speed * resample;
+ resample = speed = 1.0;
+ break;
+ case VS_DISP_TEMPO:
+ speed = mpctx->audio_speed;
+ resample = 1.0;
+ break;
+ }
+ }
+
+ mp_output_chain_set_audio_speed(ao_c->filter, speed, resample, drop);
+}
+
+static int recreate_audio_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(ao_c);
+
+ if (!mp_output_chain_update_filters(ao_c->filter, mpctx->opts->af_settings))
+ goto fail;
+
+ update_speed_filters(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ return 0;
+
+fail:
+ MP_ERR(mpctx, "Audio filter initialized failed!\n");
+ return -1;
+}
+
+int reinit_audio_filters(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return 0;
+
+ double delay = mp_output_get_measured_total_delay(ao_c->filter);
+
+ if (recreate_audio_filters(mpctx) < 0)
+ return -1;
+
+ double ndelay = mp_output_get_measured_total_delay(ao_c->filter);
+
+ // Only force refresh if the amount of dropped buffered data is going to
+ // cause "issues" for the A/V sync logic.
+ if (mpctx->audio_status == STATUS_PLAYING && delay - ndelay >= 0.2)
+ issue_refresh_seek(mpctx, MPSEEK_EXACT);
+ return 1;
+}
+
+static double db_gain(double db)
+{
+ return pow(10.0, db/20.0);
+}
+
+static float compute_replaygain(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ float rgain = 1.0;
+
+ struct replaygain_data *rg = NULL;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ if (track)
+ rg = track->stream->codec->replaygain_data;
+ if (opts->rgain_mode && rg) {
+ MP_VERBOSE(mpctx, "Replaygain: Track=%f/%f Album=%f/%f\n",
+ rg->track_gain, rg->track_peak,
+ rg->album_gain, rg->album_peak);
+
+ float gain, peak;
+ if (opts->rgain_mode == 1) {
+ gain = rg->track_gain;
+ peak = rg->track_peak;
+ } else {
+ gain = rg->album_gain;
+ peak = rg->album_peak;
+ }
+
+ gain += opts->rgain_preamp;
+ rgain = db_gain(gain);
+
+ MP_VERBOSE(mpctx, "Applying replay-gain: %f\n", rgain);
+
+ if (!opts->rgain_clip) { // clipping prevention
+ rgain = MPMIN(rgain, 1.0 / peak);
+ MP_VERBOSE(mpctx, "...with clipping prevention: %f\n", rgain);
+ }
+ } else if (opts->rgain_fallback) {
+ rgain = db_gain(opts->rgain_fallback);
+ MP_VERBOSE(mpctx, "Applying fallback gain: %f\n", rgain);
+ }
+
+ return rgain;
+}
+
+// Called when opts->softvol_volume or opts->softvol_mute were changed.
+void audio_update_volume(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c || !ao_c->ao)
+ return;
+
+ float gain = MPMAX(opts->softvol_volume / 100.0, 0);
+ gain = pow(gain, 3);
+ gain *= compute_replaygain(mpctx);
+ if (opts->softvol_mute == 1)
+ gain = 0.0;
+
+ ao_set_gain(ao_c->ao, gain);
+}
+
+// Call this if opts->playback_speed or mpctx->speed_factor_* change.
+void update_playback_speed(struct MPContext *mpctx)
+{
+ mpctx->audio_speed = mpctx->opts->playback_speed * mpctx->speed_factor_a;
+ mpctx->video_speed = mpctx->opts->playback_speed * mpctx->speed_factor_v;
+
+ update_speed_filters(mpctx);
+}
+
+static bool has_video_track(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain && mpctx->vo_chain->is_coverart)
+ return false;
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == STREAM_VIDEO && !track->attached_picture && !track->image)
+ return true;
+ }
+
+ return false;
+}
+
+static void ao_chain_reset_state(struct ao_chain *ao_c)
+{
+ ao_c->last_out_pts = MP_NOPTS_VALUE;
+ ao_c->out_eof = false;
+ ao_c->start_pts_known = false;
+ ao_c->start_pts = MP_NOPTS_VALUE;
+ ao_c->untimed_throttle = false;
+ ao_c->underrun = false;
+}
+
+void reset_audio_state(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain) {
+ ao_chain_reset_state(mpctx->ao_chain);
+ struct track *t = mpctx->ao_chain->track;
+ if (t && t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ }
+ mpctx->audio_status = mpctx->ao_chain ? STATUS_SYNCING : STATUS_EOF;
+ mpctx->delay = 0;
+ mpctx->logged_async_diff = -1;
+}
+
+void uninit_audio_out(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (ao_c) {
+ ao_c->ao_queue = NULL;
+ TA_FREEP(&ao_c->queue_filter);
+ ao_c->ao = NULL;
+ }
+ if (mpctx->ao) {
+ // Note: with gapless_audio, stop_play is not correctly set
+ if ((mpctx->opts->gapless_audio || mpctx->stop_play == AT_END_OF_FILE) &&
+ ao_is_playing(mpctx->ao) && !get_internal_paused(mpctx))
+ {
+ MP_VERBOSE(mpctx, "draining left over audio\n");
+ ao_drain(mpctx->ao);
+ }
+ ao_uninit(mpctx->ao);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+ }
+ mpctx->ao = NULL;
+ TA_FREEP(&mpctx->ao_filter_fmt);
+}
+
+static void ao_chain_uninit(struct ao_chain *ao_c)
+{
+ struct track *track = ao_c->track;
+ if (track) {
+ assert(track->ao_c == ao_c);
+ track->ao_c = NULL;
+ if (ao_c->dec_src)
+ assert(track->dec->f->pins[0] == ao_c->dec_src);
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+
+ if (ao_c->filter_src)
+ mp_pin_disconnect(ao_c->filter_src);
+
+ talloc_free(ao_c->filter->f);
+ talloc_free(ao_c->ao_filter);
+ talloc_free(ao_c);
+}
+
+void uninit_audio_chain(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain) {
+ ao_chain_uninit(mpctx->ao_chain);
+ mpctx->ao_chain = NULL;
+
+ mpctx->audio_status = STATUS_EOF;
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+ }
+}
+
+static char *audio_config_to_str_buf(char *buf, size_t buf_sz, int rate,
+ int format, struct mp_chmap channels)
+{
+ char ch[128];
+ mp_chmap_to_str_buf(ch, sizeof(ch), &channels);
+ char *hr_ch = mp_chmap_to_str_hr(&channels);
+ if (strcmp(hr_ch, ch) != 0)
+ mp_snprintf_cat(ch, sizeof(ch), " (%s)", hr_ch);
+ snprintf(buf, buf_sz, "%dHz %s %dch %s", rate,
+ ch, channels.num, af_fmt_to_str(format));
+ return buf;
+}
+
+// Decide whether on a format change, we should reinit the AO.
+static bool keep_weak_gapless_format(struct mp_aframe *old, struct mp_aframe* new)
+{
+ bool res = false;
+ struct mp_aframe *new_mod = mp_aframe_new_ref(new);
+ MP_HANDLE_OOM(new_mod);
+
+ // If the sample formats are compatible (== libswresample generally can
+ // convert them), keep the AO. On other changes, recreate it.
+
+ int old_fmt = mp_aframe_get_format(old);
+ int new_fmt = mp_aframe_get_format(new);
+
+ if (af_format_conversion_score(old_fmt, new_fmt) == INT_MIN)
+ goto done; // completely incompatible formats
+
+ if (!mp_aframe_set_format(new_mod, old_fmt))
+ goto done;
+
+ res = mp_aframe_config_equals(old, new_mod);
+
+done:
+ talloc_free(new_mod);
+ return res;
+}
+
+static void ao_chain_set_ao(struct ao_chain *ao_c, struct ao *ao)
+{
+ if (ao_c->ao != ao) {
+ assert(!ao_c->ao);
+ ao_c->ao = ao;
+ ao_c->ao_queue = ao_get_queue(ao_c->ao);
+ ao_c->queue_filter = mp_async_queue_create_filter(ao_c->ao_filter,
+ MP_PIN_IN, ao_c->ao_queue);
+ mp_async_queue_set_notifier(ao_c->queue_filter, ao_c->ao_filter);
+ // Make sure filtering never stops with frames stuck in access filter.
+ mp_filter_set_high_priority(ao_c->queue_filter, true);
+ audio_update_volume(ao_c->mpctx);
+ }
+
+ if (ao_c->filter->ao_needs_update)
+ mp_output_chain_set_ao(ao_c->filter, ao_c->ao);
+
+ mp_filter_wakeup(ao_c->ao_filter);
+}
+
+static int reinit_audio_filters_and_output(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(ao_c);
+ struct track *track = ao_c->track;
+
+ assert(ao_c->filter->ao_needs_update);
+
+ // The "ideal" filter output format
+ struct mp_aframe *out_fmt = mp_aframe_new_ref(ao_c->filter->output_aformat);
+ MP_HANDLE_OOM(out_fmt);
+
+ if (!mp_aframe_config_is_valid(out_fmt)) {
+ talloc_free(out_fmt);
+ goto init_error;
+ }
+
+ if (af_fmt_is_pcm(mp_aframe_get_format(out_fmt))) {
+ if (opts->force_srate)
+ mp_aframe_set_rate(out_fmt, opts->force_srate);
+ if (opts->audio_output_format)
+ mp_aframe_set_format(out_fmt, opts->audio_output_format);
+ if (opts->audio_output_channels.num_chmaps == 1)
+ mp_aframe_set_chmap(out_fmt, &opts->audio_output_channels.chmaps[0]);
+ }
+
+ // Weak gapless audio: if the filter output format is the same as the
+ // previous one, keep the AO and don't reinit anything.
+ // Strong gapless: always keep the AO
+ if ((mpctx->ao_filter_fmt && mpctx->ao && opts->gapless_audio < 0 &&
+ keep_weak_gapless_format(mpctx->ao_filter_fmt, out_fmt)) ||
+ (mpctx->ao && opts->gapless_audio > 0))
+ {
+ ao_chain_set_ao(ao_c, mpctx->ao);
+ talloc_free(out_fmt);
+ return 0;
+ }
+
+ // Wait until all played.
+ if (mpctx->ao && ao_is_playing(mpctx->ao)) {
+ talloc_free(out_fmt);
+ return 0;
+ }
+ // Format change during syncing. Force playback start early, then wait.
+ if (ao_c->ao_queue && mp_async_queue_get_frames(ao_c->ao_queue) &&
+ mpctx->audio_status == STATUS_SYNCING)
+ {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ talloc_free(out_fmt);
+ return 0;
+ }
+ if (mpctx->audio_status == STATUS_READY) {
+ talloc_free(out_fmt);
+ return 0;
+ }
+
+ uninit_audio_out(mpctx);
+
+ int out_rate = mp_aframe_get_rate(out_fmt);
+ int out_format = mp_aframe_get_format(out_fmt);
+ struct mp_chmap out_channels = {0};
+ mp_aframe_get_chmap(out_fmt, &out_channels);
+
+ int ao_flags = 0;
+ bool spdif_fallback = af_fmt_is_spdif(out_format) &&
+ ao_c->spdif_passthrough;
+
+ if (opts->ao_null_fallback && !spdif_fallback)
+ ao_flags |= AO_INIT_NULL_FALLBACK;
+
+ if (opts->audio_stream_silence)
+ ao_flags |= AO_INIT_STREAM_SILENCE;
+
+ if (opts->audio_exclusive)
+ ao_flags |= AO_INIT_EXCLUSIVE;
+
+ if (af_fmt_is_pcm(out_format)) {
+ if (!opts->audio_output_channels.set ||
+ opts->audio_output_channels.auto_safe)
+ ao_flags |= AO_INIT_SAFE_MULTICHANNEL_ONLY;
+
+ mp_chmap_sel_list(&out_channels,
+ opts->audio_output_channels.chmaps,
+ opts->audio_output_channels.num_chmaps);
+ }
+
+ if (!has_video_track(mpctx))
+ ao_flags |= AO_INIT_MEDIA_ROLE_MUSIC;
+
+ mpctx->ao_filter_fmt = out_fmt;
+
+ mpctx->ao = ao_init_best(mpctx->global, ao_flags, mp_wakeup_core_cb,
+ mpctx, mpctx->encode_lavc_ctx, out_rate,
+ out_format, out_channels);
+
+ int ao_rate = 0;
+ int ao_format = 0;
+ struct mp_chmap ao_channels = {0};
+ if (mpctx->ao)
+ ao_get_format(mpctx->ao, &ao_rate, &ao_format, &ao_channels);
+
+ // Verify passthrough format was not changed.
+ if (mpctx->ao && af_fmt_is_spdif(out_format)) {
+ if (out_rate != ao_rate || out_format != ao_format ||
+ !mp_chmap_equals(&out_channels, &ao_channels))
+ {
+ MP_ERR(mpctx, "Passthrough format unsupported.\n");
+ ao_uninit(mpctx->ao);
+ mpctx->ao = NULL;
+ }
+ }
+
+ if (!mpctx->ao) {
+ // If spdif was used, try to fallback to PCM.
+ if (spdif_fallback && ao_c->track && ao_c->track->dec) {
+ MP_VERBOSE(mpctx, "Falling back to PCM output.\n");
+ ao_c->spdif_passthrough = false;
+ ao_c->spdif_failed = true;
+ mp_decoder_wrapper_set_spdif_flag(ao_c->track->dec, false);
+ if (!mp_decoder_wrapper_reinit(ao_c->track->dec))
+ goto init_error;
+ reset_audio_state(mpctx);
+ mp_output_chain_reset_harder(ao_c->filter);
+ mp_wakeup_core(mpctx); // reinit with new format next time
+ return 0;
+ }
+
+ MP_ERR(mpctx, "Could not open/initialize audio device -> no sound.\n");
+ mpctx->error_playing = MPV_ERROR_AO_INIT_FAILED;
+ goto init_error;
+ }
+
+ char tmp[192];
+ MP_INFO(mpctx, "AO: [%s] %s\n", ao_get_name(mpctx->ao),
+ audio_config_to_str_buf(tmp, sizeof(tmp), ao_rate, ao_format,
+ ao_channels));
+ MP_VERBOSE(mpctx, "AO: Description: %s\n", ao_get_description(mpctx->ao));
+ update_window_title(mpctx, true);
+
+ ao_c->ao_resume_time =
+ opts->audio_wait_open > 0 ? mp_time_sec() + opts->audio_wait_open : 0;
+
+ bool eof = mpctx->audio_status == STATUS_EOF;
+ ao_set_paused(mpctx->ao, get_internal_paused(mpctx), eof);
+
+ ao_chain_set_ao(ao_c, mpctx->ao);
+
+ audio_update_volume(mpctx);
+
+ // Almost nonsensical hack to deal with certain format change scenarios.
+ if (mpctx->audio_status == STATUS_PLAYING)
+ ao_start(mpctx->ao);
+
+ mp_wakeup_core(mpctx);
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ return 0;
+
+init_error:
+ uninit_audio_chain(mpctx);
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+ return -1;
+}
+
+int init_audio_decoder(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->dec);
+ if (!track->stream)
+ goto init_error;
+
+ track->dec = mp_decoder_wrapper_create(mpctx->filter_root, track->stream);
+ if (!track->dec)
+ goto init_error;
+
+ if (track->ao_c)
+ mp_decoder_wrapper_set_spdif_flag(track->dec, true);
+
+ if (!mp_decoder_wrapper_reinit(track->dec))
+ goto init_error;
+
+ return 1;
+
+init_error:
+ if (track->sink)
+ mp_pin_disconnect(track->sink);
+ track->sink = NULL;
+ error_on_track(mpctx, track);
+ return 0;
+}
+
+void reinit_audio_chain(struct MPContext *mpctx)
+{
+ struct track *track = NULL;
+ track = mpctx->current_track[0][STREAM_AUDIO];
+ if (!track || !track->stream) {
+ if (!mpctx->encode_lavc_ctx)
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+ return;
+ }
+ reinit_audio_chain_src(mpctx, track);
+}
+
+static const struct mp_filter_info ao_filter = {
+ .name = "ao",
+ .process = ao_process,
+};
+
+// (track=NULL creates a blank chain, used for lavfi-complex)
+void reinit_audio_chain_src(struct MPContext *mpctx, struct track *track)
+{
+ assert(!mpctx->ao_chain);
+
+ mp_notify(mpctx, MPV_EVENT_AUDIO_RECONFIG, NULL);
+
+ struct ao_chain *ao_c = talloc_zero(NULL, struct ao_chain);
+ mpctx->ao_chain = ao_c;
+ ao_c->mpctx = mpctx;
+ ao_c->log = mpctx->log;
+ ao_c->filter =
+ mp_output_chain_create(mpctx->filter_root, MP_OUTPUT_CHAIN_AUDIO);
+ ao_c->spdif_passthrough = true;
+ ao_c->last_out_pts = MP_NOPTS_VALUE;
+ ao_c->delay = mpctx->opts->audio_delay;
+
+ ao_c->ao_filter = mp_filter_create(mpctx->filter_root, &ao_filter);
+ if (!ao_c->filter || !ao_c->ao_filter)
+ goto init_error;
+ ao_c->ao_filter->priv = ao_c;
+
+ mp_filter_add_pin(ao_c->ao_filter, MP_PIN_IN, "in");
+ mp_pin_connect(ao_c->ao_filter->pins[0], ao_c->filter->f->pins[1]);
+
+ if (track) {
+ ao_c->track = track;
+ track->ao_c = ao_c;
+ if (!init_audio_decoder(mpctx, track))
+ goto init_error;
+ ao_c->dec_src = track->dec->f->pins[0];
+ mp_pin_connect(ao_c->filter->f->pins[0], ao_c->dec_src);
+ }
+
+ reset_audio_state(mpctx);
+
+ if (recreate_audio_filters(mpctx) < 0)
+ goto init_error;
+
+ if (mpctx->ao)
+ audio_update_volume(mpctx);
+
+ mp_wakeup_core(mpctx);
+ return;
+
+init_error:
+ uninit_audio_chain(mpctx);
+ uninit_audio_out(mpctx);
+ error_on_track(mpctx, track);
+}
+
+// Return pts value corresponding to the start point of audio written to the
+// ao queue so far.
+double written_audio_pts(struct MPContext *mpctx)
+{
+ return mpctx->ao_chain ? mpctx->ao_chain->last_out_pts : MP_NOPTS_VALUE;
+}
+
+// Return pts value corresponding to currently playing audio.
+double playing_audio_pts(struct MPContext *mpctx)
+{
+ double pts = written_audio_pts(mpctx);
+ if (pts == MP_NOPTS_VALUE || !mpctx->ao)
+ return pts;
+ return pts - mpctx->audio_speed * ao_get_delay(mpctx->ao);
+}
+
+// This garbage is needed for untimed AOs. These consume audio infinitely fast,
+// so try keeping approximate A/V sync by blocking audio transfer as needed.
+static void update_throttle(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ bool new_throttle = mpctx->audio_status == STATUS_PLAYING &&
+ mpctx->delay > 0 && ao_c && ao_c->ao &&
+ ao_untimed(ao_c->ao) &&
+ mpctx->video_status != STATUS_EOF;
+ if (ao_c && new_throttle != ao_c->untimed_throttle) {
+ ao_c->untimed_throttle = new_throttle;
+ mp_wakeup_core(mpctx);
+ mp_filter_wakeup(ao_c->ao_filter);
+ }
+}
+
+static void ao_process(struct mp_filter *f)
+{
+ struct ao_chain *ao_c = f->priv;
+ struct MPContext *mpctx = ao_c->mpctx;
+
+ if (!ao_c->queue_filter) {
+ // This will eventually lead to the creation of the AO + queue, due
+ // to how f_output_chain and AO management works.
+ mp_pin_out_request_data(f->ppins[0]);
+ // Check for EOF with no data case, which is a mess because everything
+ // hates us.
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type == MP_FRAME_EOF) {
+ MP_VERBOSE(mpctx, "got EOF with no data before it\n");
+ ao_c->out_eof = true;
+ mpctx->audio_status = STATUS_DRAINING;
+ mp_wakeup_core(mpctx);
+ } else if (frame.type) {
+ mp_pin_out_unread(f->ppins[0], frame);
+ }
+ return;
+ }
+
+ // Due to mp_async_queue_set_notifier() this function is called when the
+ // queue becomes full. This affects state changes in the normal playloop,
+ // so wake it up. But avoid redundant wakeups during normal playback.
+ if (mpctx->audio_status != STATUS_PLAYING &&
+ mp_async_queue_is_full(ao_c->ao_queue))
+ mp_wakeup_core(mpctx);
+
+ if (mpctx->audio_status == STATUS_SYNCING && !ao_c->start_pts_known)
+ return;
+
+ if (ao_c->untimed_throttle)
+ return;
+
+ if (!mp_pin_can_transfer_data(ao_c->queue_filter->pins[0], f->ppins[0]))
+ return;
+
+ struct mp_frame frame = mp_pin_out_read(f->ppins[0]);
+ if (frame.type == MP_FRAME_AUDIO) {
+ struct mp_aframe *af = frame.data;
+
+ double endpts = get_play_end_pts(mpctx);
+ if (endpts != MP_NOPTS_VALUE) {
+ endpts *= mpctx->play_dir;
+ // Avoid decoding and discarding the entire rest of the file.
+ if (mp_aframe_get_pts(af) >= endpts) {
+ mp_pin_out_unread(f->ppins[0], frame);
+ if (!ao_c->out_eof) {
+ ao_c->out_eof = true;
+ mp_pin_in_write(ao_c->queue_filter->pins[0], MP_EOF_FRAME);
+ }
+ return;
+ }
+ }
+ double startpts = mpctx->audio_status == STATUS_SYNCING ?
+ ao_c->start_pts : MP_NOPTS_VALUE;
+ mp_aframe_clip_timestamps(af, startpts, endpts);
+
+ int samples = mp_aframe_get_size(af);
+ if (!samples) {
+ mp_filter_internal_mark_progress(f);
+ mp_frame_unref(&frame);
+ return;
+ }
+
+ ao_c->out_eof = false;
+
+ if (mpctx->audio_status == STATUS_DRAINING ||
+ mpctx->audio_status == STATUS_EOF)
+ {
+ // If a new frame comes decoder/filter EOF, we should preferably
+ // call get_sync_pts() again, which (at least in obscure situations)
+ // may require us to wait a while until the sync PTS is known. Our
+ // code sucks and can't deal with that, so jump through a hoop to
+ // get things done in the correct order.
+ mp_pin_out_unread(f->ppins[0], frame);
+ ao_c->start_pts_known = false;
+ mpctx->audio_status = STATUS_SYNCING;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "new audio frame after EOF\n");
+ return;
+ }
+
+ mpctx->shown_aframes += samples;
+ double real_samplerate = mp_aframe_get_rate(af) / mpctx->audio_speed;
+ if (mpctx->video_status != STATUS_EOF)
+ mpctx->delay += samples / real_samplerate;
+ ao_c->last_out_pts = mp_aframe_end_pts(af);
+ update_throttle(mpctx);
+
+ // Gapless case: the AO is still playing from previous file. It makes
+ // no sense to wait, and in fact the "full queue" event we're waiting
+ // for may never happen, so start immediately.
+ // If the new audio starts "later" (big video sync offset), transfer
+ // of data is stopped somewhere else.
+ if (mpctx->audio_status == STATUS_SYNCING && ao_is_playing(ao_c->ao)) {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "previous audio still playing; continuing\n");
+ }
+
+ mp_pin_in_write(ao_c->queue_filter->pins[0], frame);
+ } else if (frame.type == MP_FRAME_EOF) {
+ MP_VERBOSE(mpctx, "audio filter EOF\n");
+
+ ao_c->out_eof = true;
+ mp_wakeup_core(mpctx);
+
+ mp_pin_in_write(ao_c->queue_filter->pins[0], frame);
+ mp_filter_internal_mark_progress(f);
+ } else {
+ mp_frame_unref(&frame);
+ }
+}
+
+void reload_audio_output(struct MPContext *mpctx)
+{
+ if (!mpctx->ao)
+ return;
+
+ ao_reset(mpctx->ao);
+ uninit_audio_out(mpctx);
+ reinit_audio_filters(mpctx); // mostly to issue refresh seek
+
+ struct ao_chain *ao_c = mpctx->ao_chain;
+
+ if (ao_c) {
+ reset_audio_state(mpctx);
+ mp_output_chain_reset_harder(ao_c->filter);
+ }
+
+ // Whether we can use spdif might have changed. If we failed to use spdif
+ // in the previous initialization, try it with spdif again (we'll fallback
+ // to PCM again if necessary).
+ if (ao_c && ao_c->track) {
+ struct mp_decoder_wrapper *dec = ao_c->track->dec;
+ if (dec && ao_c->spdif_failed) {
+ ao_c->spdif_passthrough = true;
+ ao_c->spdif_failed = false;
+ mp_decoder_wrapper_set_spdif_flag(ao_c->track->dec, true);
+ if (!mp_decoder_wrapper_reinit(dec)) {
+ MP_ERR(mpctx, "Error reinitializing audio.\n");
+ error_on_track(mpctx, ao_c->track);
+ }
+ }
+ }
+
+ mp_wakeup_core(mpctx);
+}
+
+// Returns audio start pts for seeking or video sync.
+// Returns false if PTS is not known yet.
+static bool get_sync_pts(struct MPContext *mpctx, double *pts)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ *pts = MP_NOPTS_VALUE;
+
+ if (!opts->initial_audio_sync)
+ return true;
+
+ bool sync_to_video = mpctx->vo_chain && mpctx->video_status != STATUS_EOF &&
+ !mpctx->vo_chain->is_sparse;
+
+ if (sync_to_video) {
+ if (mpctx->video_status < STATUS_READY)
+ return false; // wait until we know a video PTS
+ if (mpctx->video_pts != MP_NOPTS_VALUE)
+ *pts = mpctx->video_pts - opts->audio_delay;
+ } else if (mpctx->hrseek_active) {
+ *pts = mpctx->hrseek_pts;
+ } else {
+ // If audio-only is enabled mid-stream during playback, sync accordingly.
+ *pts = mpctx->playback_pts;
+ }
+
+ return true;
+}
+
+// Look whether audio can be started yet - if audio has to start some time
+// after video.
+// Caller needs to ensure mpctx->restart_complete is OK
+void audio_start_ao(struct MPContext *mpctx)
+{
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c || !ao_c->ao || mpctx->audio_status != STATUS_READY)
+ return;
+ double pts = MP_NOPTS_VALUE;
+ if (!get_sync_pts(mpctx, &pts))
+ return;
+ double apts = playing_audio_pts(mpctx); // (basically including mpctx->delay)
+ if (pts != MP_NOPTS_VALUE && apts != MP_NOPTS_VALUE && pts < apts &&
+ mpctx->video_status != STATUS_EOF)
+ {
+ double diff = (apts - pts) / mpctx->opts->playback_speed;
+ if (!get_internal_paused(mpctx))
+ mp_set_timeout(mpctx, diff);
+ if (mpctx->logged_async_diff != diff) {
+ MP_VERBOSE(mpctx, "delaying audio start %f vs. %f, diff=%f\n",
+ apts, pts, diff);
+ mpctx->logged_async_diff = diff;
+ }
+ return;
+ }
+
+ MP_VERBOSE(mpctx, "starting audio playback\n");
+ ao_start(ao_c->ao);
+ mpctx->audio_status = STATUS_PLAYING;
+ if (ao_c->out_eof) {
+ mpctx->audio_status = STATUS_DRAINING;
+ MP_VERBOSE(mpctx, "audio draining\n");
+ }
+ ao_c->underrun = false;
+ mpctx->logged_async_diff = -1;
+ mp_wakeup_core(mpctx);
+}
+
+void fill_audio_out_buffers(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->ao && ao_query_and_reset_events(mpctx->ao, AO_EVENT_RELOAD))
+ reload_audio_output(mpctx);
+
+ if (mpctx->ao && ao_query_and_reset_events(mpctx->ao,
+ AO_EVENT_INITIAL_UNBLOCK))
+ ao_unblock(mpctx->ao);
+
+ update_throttle(mpctx);
+
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ if (!ao_c)
+ return;
+
+ if (ao_c->filter->failed_output_conversion) {
+ error_on_track(mpctx, ao_c->track);
+ return;
+ }
+
+ if (ao_c->filter->ao_needs_update) {
+ if (reinit_audio_filters_and_output(mpctx) < 0)
+ return;
+ }
+
+ if (mpctx->vo_chain && ao_c->track && ao_c->track->dec &&
+ mp_decoder_wrapper_get_pts_reset(ao_c->track->dec))
+ {
+ MP_WARN(mpctx, "Reset playback due to audio timestamp reset.\n");
+ reset_playback_state(mpctx);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->audio_status == STATUS_SYNCING) {
+ double pts;
+ bool ok = get_sync_pts(mpctx, &pts);
+
+ // If the AO is still playing from the previous file (due to gapless),
+ // but if video is active, this may not work if audio starts later than
+ // video, and gapless has no advantages anyway. So block doing anything
+ // until the old audio is fully played.
+ // (Buggy if AO underruns.)
+ if (mpctx->ao && ao_is_playing(mpctx->ao) &&
+ mpctx->video_status != STATUS_EOF) {
+ MP_VERBOSE(mpctx, "blocked, waiting for old audio to play\n");
+ ok = false;
+ }
+
+ if (ao_c->start_pts_known != ok || ao_c->start_pts != pts) {
+ ao_c->start_pts_known = ok;
+ ao_c->start_pts = pts;
+ mp_filter_wakeup(ao_c->ao_filter);
+ }
+
+ if (ao_c->ao && mp_async_queue_is_full(ao_c->ao_queue)) {
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "audio ready\n");
+ } else if (ao_c->out_eof) {
+ // Force playback start early.
+ mpctx->audio_status = STATUS_READY;
+ mp_wakeup_core(mpctx);
+ MP_VERBOSE(mpctx, "audio ready (and EOF)\n");
+ }
+ }
+
+ if (ao_c->ao && !ao_is_playing(ao_c->ao) && !ao_c->underrun &&
+ (mpctx->audio_status == STATUS_PLAYING ||
+ mpctx->audio_status == STATUS_DRAINING))
+ {
+ // Should be playing, but somehow isn't.
+
+ if (ao_c->out_eof && !mp_async_queue_get_frames(ao_c->ao_queue)) {
+ MP_VERBOSE(mpctx, "AO signaled EOF (while in state %s)\n",
+ mp_status_str(mpctx->audio_status));
+ mpctx->audio_status = STATUS_EOF;
+ mp_wakeup_core(mpctx);
+ // stops untimed AOs, stops pull AOs from streaming silence
+ ao_reset(ao_c->ao);
+ } else {
+ if (!ao_c->ao_underrun) {
+ MP_WARN(mpctx, "Audio device underrun detected.\n");
+ ao_c->ao_underrun = true;
+ mp_wakeup_core(mpctx);
+ ao_c->underrun = true;
+ }
+
+ // Wait until buffers are filled before recovering underrun.
+ if (ao_c->out_eof || mp_async_queue_is_full(ao_c->ao_queue)) {
+ MP_VERBOSE(mpctx, "restarting audio after underrun\n");
+ ao_start(mpctx->ao_chain->ao);
+ ao_c->ao_underrun = false;
+ ao_c->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+ }
+ }
+
+ if (mpctx->audio_status == STATUS_PLAYING && ao_c->out_eof) {
+ mpctx->audio_status = STATUS_DRAINING;
+ MP_VERBOSE(mpctx, "audio draining\n");
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->audio_status == STATUS_DRAINING) {
+ // Wait until the AO has played all queued data. In the gapless case,
+ // we trigger EOF immediately, and let it play asynchronously.
+ if (!ao_c->ao || (!ao_is_playing(ao_c->ao) ||
+ (opts->gapless_audio && !ao_untimed(ao_c->ao))))
+ {
+ MP_VERBOSE(mpctx, "audio EOF reached\n");
+ mpctx->audio_status = STATUS_EOF;
+ mp_wakeup_core(mpctx);
+ }
+ }
+
+ if (mpctx->restart_complete)
+ audio_start_ao(mpctx); // in case it got delayed
+}
+
+// Drop data queued for output, or which the AO is currently outputting.
+void clear_audio_output_buffers(struct MPContext *mpctx)
+{
+ if (mpctx->ao)
+ ao_reset(mpctx->ao);
+}
diff --git a/player/client.c b/player/client.c
new file mode 100644
index 0000000..b35f20a
--- /dev/null
+++ b/player/client.c
@@ -0,0 +1,2248 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <math.h>
+#include <stdatomic.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/global.h"
+#include "input/input.h"
+#include "input/cmd.h"
+#include "misc/ctype.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/rendezvous.h"
+#include "misc/thread_tools.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "options/path.h"
+#include "options/parse_configfile.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "osdep/io.h"
+#include "stream/stream.h"
+
+#include "command.h"
+#include "core.h"
+#include "client.h"
+
+/*
+ * Locking hierarchy:
+ *
+ * MPContext > mp_client_api.lock > mpv_handle.lock > * > mpv_handle.wakeup_lock
+ *
+ * MPContext strictly speaking has no locks, and instead is implicitly managed
+ * by MPContext.dispatch, which basically stops the playback thread at defined
+ * points in order to let clients access it in a synchronized manner. Since
+ * MPContext code accesses the client API, it's on top of the lock hierarchy.
+ *
+ */
+
+struct mp_client_api {
+ struct MPContext *mpctx;
+
+ mp_mutex lock;
+
+ // -- protected by lock
+
+ struct mpv_handle **clients;
+ int num_clients;
+ bool shutting_down; // do not allow new clients
+ bool have_terminator; // a client took over the role of destroying the core
+ bool terminate_core_thread; // make libmpv core thread exit
+ // This is incremented whenever the clients[] array above changes. This is
+ // used to safely unlock mp_client_api.lock while iterating the list of
+ // clients.
+ uint64_t clients_list_change_ts;
+ int64_t id_alloc;
+
+ struct mp_custom_protocol *custom_protocols;
+ int num_custom_protocols;
+
+ struct mpv_render_context *render_context;
+};
+
+struct observe_property {
+ // -- immutable
+ struct mpv_handle *owner;
+ char *name;
+ int id; // ==mp_get_property_id(name)
+ uint64_t event_mask; // ==mp_get_property_event_mask(name)
+ int64_t reply_id;
+ mpv_format format;
+ const struct m_option *type;
+ // -- protected by owner->lock
+ size_t refcount;
+ uint64_t change_ts; // logical timestamp incremented on each change
+ uint64_t value_ts; // logical timestamp for value contents
+ bool value_valid;
+ union m_option_value value;
+ uint64_t value_ret_ts; // logical timestamp of value returned to user
+ union m_option_value value_ret;
+ bool waiting_for_hook; // flag for draining old property changes on a hook
+};
+
+struct mpv_handle {
+ // -- immutable
+ char name[MAX_CLIENT_NAME];
+ struct mp_log *log;
+ struct MPContext *mpctx;
+ struct mp_client_api *clients;
+ int64_t id;
+
+ // -- not thread-safe
+ struct mpv_event *cur_event;
+ struct mpv_event_property cur_property_event;
+ struct observe_property *cur_property;
+
+ mp_mutex lock;
+
+ mp_mutex wakeup_lock;
+ mp_cond wakeup;
+
+ // -- protected by wakeup_lock
+ bool need_wakeup;
+ void (*wakeup_cb)(void *d);
+ void *wakeup_cb_ctx;
+ int wakeup_pipe[2];
+
+ // -- protected by lock
+
+ uint64_t event_mask;
+ bool queued_wakeup;
+
+ mpv_event *events; // ringbuffer of max_events entries
+ int max_events; // allocated number of entries in events
+ int first_event; // events[first_event] is the first readable event
+ int num_events; // number of readable events
+ int reserved_events; // number of entries reserved for replies
+ size_t async_counter; // pending other async events
+ bool choked; // recovering from queue overflow
+ bool destroying; // pending destruction; no API accesses allowed
+ bool hook_pending; // hook events are returned after draining properties
+
+ struct observe_property **properties;
+ int num_properties;
+ bool has_pending_properties; // (maybe) new property events (producer side)
+ bool new_property_events; // new property events (consumer side)
+ int cur_property_index; // round-robin for property events (consumer side)
+ uint64_t property_event_masks; // or-ed together event masks of all properties
+ // This is incremented whenever the properties[] array above changes. This
+ // is used to safely unlock mpv_handle.lock while reading a property. If
+ // the counter didn't change between unlock and relock, then it will assume
+ // the array did not change.
+ uint64_t properties_change_ts;
+
+ bool fuzzy_initialized; // see scripting.c wait_loaded()
+ bool is_weak; // can not keep core alive on its own
+ struct mp_log_buffer *messages;
+ int messages_level;
+};
+
+static bool gen_log_message_event(struct mpv_handle *ctx);
+static bool gen_property_change_event(struct mpv_handle *ctx);
+static void notify_property_events(struct mpv_handle *ctx, int event);
+
+// Must be called with prop->owner->lock held.
+static void prop_unref(struct observe_property *prop)
+{
+ if (!prop)
+ return;
+
+ assert(prop->refcount > 0);
+ prop->refcount -= 1;
+ if (!prop->refcount)
+ talloc_free(prop);
+}
+
+void mp_clients_init(struct MPContext *mpctx)
+{
+ mpctx->clients = talloc_ptrtype(NULL, mpctx->clients);
+ *mpctx->clients = (struct mp_client_api) {
+ .mpctx = mpctx,
+ };
+ mpctx->global->client_api = mpctx->clients;
+ mp_mutex_init(&mpctx->clients->lock);
+}
+
+void mp_clients_destroy(struct MPContext *mpctx)
+{
+ if (!mpctx->clients)
+ return;
+ assert(mpctx->clients->num_clients == 0);
+
+ // The API user is supposed to call mpv_render_context_free(). It's simply
+ // not allowed not to do this.
+ if (mpctx->clients->render_context) {
+ MP_FATAL(mpctx, "Broken API use: mpv_render_context_free() not called.\n");
+ abort();
+ }
+
+ mp_mutex_destroy(&mpctx->clients->lock);
+ talloc_free(mpctx->clients);
+ mpctx->clients = NULL;
+}
+
+// Test for "fuzzy" initialization of all clients. That is, all clients have
+// at least called mpv_wait_event() at least once since creation (or exited).
+bool mp_clients_all_initialized(struct MPContext *mpctx)
+{
+ bool all_ok = true;
+ mp_mutex_lock(&mpctx->clients->lock);
+ for (int n = 0; n < mpctx->clients->num_clients; n++) {
+ struct mpv_handle *ctx = mpctx->clients->clients[n];
+ mp_mutex_lock(&ctx->lock);
+ all_ok &= ctx->fuzzy_initialized;
+ mp_mutex_unlock(&ctx->lock);
+ }
+ mp_mutex_unlock(&mpctx->clients->lock);
+ return all_ok;
+}
+
+static struct mpv_handle *find_client_id(struct mp_client_api *clients, int64_t id)
+{
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (clients->clients[n]->id == id)
+ return clients->clients[n];
+ }
+ return NULL;
+}
+
+static struct mpv_handle *find_client(struct mp_client_api *clients,
+ const char *name)
+{
+ if (name[0] == '@') {
+ char *end;
+ errno = 0;
+ long long int id = strtoll(name + 1, &end, 10);
+ if (errno || end[0])
+ return NULL;
+ return find_client_id(clients, id);
+ }
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (strcmp(clients->clients[n]->name, name) == 0)
+ return clients->clients[n];
+ }
+
+ return NULL;
+}
+
+bool mp_client_id_exists(struct MPContext *mpctx, int64_t id)
+{
+ mp_mutex_lock(&mpctx->clients->lock);
+ bool r = find_client_id(mpctx->clients, id);
+ mp_mutex_unlock(&mpctx->clients->lock);
+ return r;
+}
+
+struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name)
+{
+ mp_mutex_lock(&clients->lock);
+
+ char nname[MAX_CLIENT_NAME];
+ for (int n = 1; n < 1000; n++) {
+ if (!name)
+ name = "client";
+ snprintf(nname, sizeof(nname) - 3, "%s", name); // - space for number
+ for (int i = 0; nname[i]; i++)
+ nname[i] = mp_isalnum(nname[i]) ? nname[i] : '_';
+ if (n > 1)
+ mp_snprintf_cat(nname, sizeof(nname), "%d", n);
+ if (!find_client(clients, nname))
+ break;
+ nname[0] = '\0';
+ }
+
+ if (!nname[0] || clients->shutting_down) {
+ mp_mutex_unlock(&clients->lock);
+ return NULL;
+ }
+
+ int num_events = 1000;
+
+ struct mpv_handle *client = talloc_ptrtype(NULL, client);
+ *client = (struct mpv_handle){
+ .log = mp_log_new(client, clients->mpctx->log, nname),
+ .mpctx = clients->mpctx,
+ .clients = clients,
+ .id = ++(clients->id_alloc),
+ .cur_event = talloc_zero(client, struct mpv_event),
+ .events = talloc_array(client, mpv_event, num_events),
+ .max_events = num_events,
+ .event_mask = (1ULL << INTERNAL_EVENT_BASE) - 1, // exclude internal events
+ .wakeup_pipe = {-1, -1},
+ };
+ mp_mutex_init(&client->lock);
+ mp_mutex_init(&client->wakeup_lock);
+ mp_cond_init(&client->wakeup);
+
+ snprintf(client->name, sizeof(client->name), "%s", nname);
+
+ clients->clients_list_change_ts += 1;
+ MP_TARRAY_APPEND(clients, clients->clients, clients->num_clients, client);
+
+ if (clients->num_clients == 1 && !clients->mpctx->is_cli)
+ client->fuzzy_initialized = true;
+
+ mp_mutex_unlock(&clients->lock);
+
+ mpv_request_event(client, MPV_EVENT_TICK, 0);
+
+ return client;
+}
+
+void mp_client_set_weak(struct mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ ctx->is_weak = true;
+ mp_mutex_unlock(&ctx->lock);
+}
+
+const char *mpv_client_name(mpv_handle *ctx)
+{
+ return ctx->name;
+}
+
+int64_t mpv_client_id(mpv_handle *ctx)
+{
+ return ctx->id;
+}
+
+struct mp_log *mp_client_get_log(struct mpv_handle *ctx)
+{
+ return ctx->log;
+}
+
+struct mpv_global *mp_client_get_global(struct mpv_handle *ctx)
+{
+ return ctx->mpctx->global;
+}
+
+static void wakeup_client(struct mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (!ctx->need_wakeup) {
+ ctx->need_wakeup = true;
+ mp_cond_broadcast(&ctx->wakeup);
+ if (ctx->wakeup_cb)
+ ctx->wakeup_cb(ctx->wakeup_cb_ctx);
+ if (ctx->wakeup_pipe[0] != -1)
+ (void)write(ctx->wakeup_pipe[1], &(char){0}, 1);
+ }
+ mp_mutex_unlock(&ctx->wakeup_lock);
+}
+
+// Note: the caller has to deal with sporadic wakeups.
+static int wait_wakeup(struct mpv_handle *ctx, int64_t end)
+{
+ int r = 0;
+ mp_mutex_unlock(&ctx->lock);
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (!ctx->need_wakeup)
+ r = mp_cond_timedwait_until(&ctx->wakeup, &ctx->wakeup_lock, end);
+ if (r == 0)
+ ctx->need_wakeup = false;
+ mp_mutex_unlock(&ctx->wakeup_lock);
+ mp_mutex_lock(&ctx->lock);
+ return r;
+}
+
+void mpv_set_wakeup_callback(mpv_handle *ctx, void (*cb)(void *d), void *d)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ ctx->wakeup_cb = cb;
+ ctx->wakeup_cb_ctx = d;
+ if (ctx->wakeup_cb)
+ ctx->wakeup_cb(ctx->wakeup_cb_ctx);
+ mp_mutex_unlock(&ctx->wakeup_lock);
+}
+
+static void lock_core(mpv_handle *ctx)
+{
+ mp_dispatch_lock(ctx->mpctx->dispatch);
+}
+
+static void unlock_core(mpv_handle *ctx)
+{
+ mp_dispatch_unlock(ctx->mpctx->dispatch);
+}
+
+void mpv_wait_async_requests(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ while (ctx->reserved_events || ctx->async_counter)
+ wait_wakeup(ctx, INT64_MAX);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// Send abort signal to all matching work items.
+// If type==0, destroy all of the matching ctx.
+// If ctx==0, destroy all.
+static void abort_async(struct MPContext *mpctx, mpv_handle *ctx,
+ int type, uint64_t id)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+
+ // Destroy all => ensure any newly appearing work is aborted immediately.
+ if (ctx == NULL)
+ mpctx->abort_all = true;
+
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ struct mp_abort_entry *abort = mpctx->abort_list[n];
+ if (!ctx || (abort->client == ctx && (!type ||
+ (abort->client_work_type == type && abort->client_work_id == id))))
+ {
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+ }
+
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+static void get_thread_id(void *ptr)
+{
+ *(mp_thread_id *)ptr = mp_thread_current_id();
+}
+
+static void mp_destroy_client(mpv_handle *ctx, bool terminate)
+{
+ if (!ctx)
+ return;
+
+ struct MPContext *mpctx = ctx->mpctx;
+ struct mp_client_api *clients = ctx->clients;
+
+ MP_DBG(ctx, "Exiting...\n");
+
+ if (terminate)
+ mpv_command(ctx, (const char*[]){"quit", NULL});
+
+ mp_mutex_lock(&ctx->lock);
+
+ ctx->destroying = true;
+
+ for (int n = 0; n < ctx->num_properties; n++)
+ prop_unref(ctx->properties[n]);
+ ctx->num_properties = 0;
+ ctx->properties_change_ts += 1;
+
+ prop_unref(ctx->cur_property);
+ ctx->cur_property = NULL;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ abort_async(mpctx, ctx, 0, 0);
+
+ // reserved_events equals the number of asynchronous requests that weren't
+ // yet replied. In order to avoid that trying to reply to a removed client
+ // causes a crash, block until all asynchronous requests were served.
+ mpv_wait_async_requests(ctx);
+
+ osd_set_external_remove_owner(mpctx->osd, ctx);
+ mp_input_remove_sections_by_owner(mpctx->input, ctx->name);
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ if (clients->clients[n] == ctx) {
+ clients->clients_list_change_ts += 1;
+ MP_TARRAY_REMOVE_AT(clients->clients, clients->num_clients, n);
+ while (ctx->num_events) {
+ talloc_free(ctx->events[ctx->first_event].data);
+ ctx->first_event = (ctx->first_event + 1) % ctx->max_events;
+ ctx->num_events--;
+ }
+ mp_msg_log_buffer_destroy(ctx->messages);
+ mp_cond_destroy(&ctx->wakeup);
+ mp_mutex_destroy(&ctx->wakeup_lock);
+ mp_mutex_destroy(&ctx->lock);
+ if (ctx->wakeup_pipe[0] != -1) {
+ close(ctx->wakeup_pipe[0]);
+ close(ctx->wakeup_pipe[1]);
+ }
+ talloc_free(ctx);
+ ctx = NULL;
+ break;
+ }
+ }
+ assert(!ctx);
+
+ if (mpctx->is_cli) {
+ terminate = false;
+ } else {
+ // If the last strong mpv_handle got destroyed, destroy the core.
+ bool has_strong_ref = false;
+ for (int n = 0; n < clients->num_clients; n++)
+ has_strong_ref |= !clients->clients[n]->is_weak;
+ if (!has_strong_ref)
+ terminate = true;
+
+ // Reserve the right to destroy mpctx for us.
+ if (clients->have_terminator)
+ terminate = false;
+ clients->have_terminator |= terminate;
+ }
+
+ // mp_shutdown_clients() sleeps to avoid wasting CPU.
+ // mp_hook_test_completion() also relies on this a bit.
+ mp_wakeup_core(mpctx);
+
+ mp_mutex_unlock(&clients->lock);
+
+ // Note that even if num_clients==0, having set have_terminator keeps mpctx
+ // and the core thread alive.
+ if (terminate) {
+ // Make sure the core stops playing files etc. Being able to lock the
+ // dispatch queue requires that the core thread is still active.
+ mp_dispatch_lock(mpctx->dispatch);
+ mpctx->stop_play = PT_QUIT;
+ mp_dispatch_unlock(mpctx->dispatch);
+
+ mp_thread_id playthread;
+ mp_dispatch_run(mpctx->dispatch, get_thread_id, &playthread);
+
+ // Ask the core thread to stop.
+ mp_mutex_lock(&clients->lock);
+ clients->terminate_core_thread = true;
+ mp_mutex_unlock(&clients->lock);
+ mp_wakeup_core(mpctx);
+
+ // Blocking wait for all clients and core thread to terminate.
+ mp_thread_join_id(playthread);
+
+ mp_destroy(mpctx);
+ }
+}
+
+void mpv_destroy(mpv_handle *ctx)
+{
+ mp_destroy_client(ctx, false);
+}
+
+void mpv_terminate_destroy(mpv_handle *ctx)
+{
+ mp_destroy_client(ctx, true);
+}
+
+// Can be called on the core thread only. Idempotent.
+// Also happens to take care of shutting down any async work.
+void mp_shutdown_clients(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ // Forcefully abort async work after 2 seconds of waiting.
+ double abort_time = mp_time_sec() + 2;
+
+ mp_mutex_lock(&clients->lock);
+
+ // Prevent that new clients can appear.
+ clients->shutting_down = true;
+
+ // Wait until we can terminate.
+ while (clients->num_clients || mpctx->outstanding_async ||
+ !(mpctx->is_cli || clients->terminate_core_thread))
+ {
+ mp_mutex_unlock(&clients->lock);
+
+ double left = abort_time - mp_time_sec();
+ if (left >= 0) {
+ mp_set_timeout(mpctx, left);
+ } else {
+ // Forcefully abort any ongoing async work. This is quite rude and
+ // probably not what everyone wants, so it happens only after a
+ // timeout.
+ abort_async(mpctx, NULL, 0, 0);
+ }
+
+ mp_client_broadcast_event(mpctx, MPV_EVENT_SHUTDOWN, NULL);
+ mp_wait_events(mpctx);
+
+ mp_mutex_lock(&clients->lock);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+bool mp_is_shutting_down(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+ mp_mutex_lock(&clients->lock);
+ bool res = clients->shutting_down;
+ mp_mutex_unlock(&clients->lock);
+ return res;
+}
+
+static MP_THREAD_VOID core_thread(void *p)
+{
+ struct MPContext *mpctx = p;
+
+ mp_thread_set_name("core");
+
+ while (!mpctx->initialized && mpctx->stop_play != PT_QUIT)
+ mp_idle(mpctx);
+
+ if (mpctx->initialized)
+ mp_play_files(mpctx);
+
+ // This actually waits until all clients are gone before actually
+ // destroying mpctx. Actual destruction is done by whatever destroys
+ // the last mpv_handle.
+ mp_shutdown_clients(mpctx);
+
+ MP_THREAD_RETURN();
+}
+
+mpv_handle *mpv_create(void)
+{
+ struct MPContext *mpctx = mp_create();
+ if (!mpctx)
+ return NULL;
+
+ m_config_set_profile(mpctx->mconfig, "libmpv", 0);
+
+ mpv_handle *ctx = mp_new_client(mpctx->clients, "main");
+ if (!ctx) {
+ mp_destroy(mpctx);
+ return NULL;
+ }
+
+ mp_thread thread;
+ if (mp_thread_create(&thread, core_thread, mpctx) != 0) {
+ ctx->clients->have_terminator = true; // avoid blocking
+ mpv_terminate_destroy(ctx);
+ mp_destroy(mpctx);
+ return NULL;
+ }
+
+ return ctx;
+}
+
+mpv_handle *mpv_create_client(mpv_handle *ctx, const char *name)
+{
+ if (!ctx)
+ return mpv_create();
+ mpv_handle *new = mp_new_client(ctx->mpctx->clients, name);
+ if (new)
+ mpv_wait_event(new, 0); // set fuzzy_initialized
+ return new;
+}
+
+mpv_handle *mpv_create_weak_client(mpv_handle *ctx, const char *name)
+{
+ mpv_handle *new = mpv_create_client(ctx, name);
+ if (new)
+ mp_client_set_weak(new);
+ return new;
+}
+
+int mpv_initialize(mpv_handle *ctx)
+{
+ lock_core(ctx);
+ int res = mp_initialize(ctx->mpctx, NULL) ? MPV_ERROR_INVALID_PARAMETER : 0;
+ mp_wakeup_core(ctx->mpctx);
+ unlock_core(ctx);
+ return res;
+}
+
+// set ev->data to a new copy of the original data
+// (done only for message types that are broadcast)
+static void dup_event_data(struct mpv_event *ev)
+{
+ switch (ev->event_id) {
+ case MPV_EVENT_CLIENT_MESSAGE: {
+ struct mpv_event_client_message *src = ev->data;
+ struct mpv_event_client_message *msg =
+ talloc_zero(NULL, struct mpv_event_client_message);
+ for (int n = 0; n < src->num_args; n++) {
+ MP_TARRAY_APPEND(msg, msg->args, msg->num_args,
+ talloc_strdup(msg, src->args[n]));
+ }
+ ev->data = msg;
+ break;
+ }
+ case MPV_EVENT_START_FILE:
+ ev->data = talloc_memdup(NULL, ev->data, sizeof(mpv_event_start_file));
+ break;
+ case MPV_EVENT_END_FILE:
+ ev->data = talloc_memdup(NULL, ev->data, sizeof(mpv_event_end_file));
+ break;
+ default:
+ // Doesn't use events with memory allocation.
+ if (ev->data)
+ abort();
+ }
+}
+
+// Reserve an entry in the ring buffer. This can be used to guarantee that the
+// reply can be made, even if the buffer becomes congested _after_ sending
+// the request.
+// Returns an error code if the buffer is full.
+static int reserve_reply(struct mpv_handle *ctx)
+{
+ int res = MPV_ERROR_EVENT_QUEUE_FULL;
+ mp_mutex_lock(&ctx->lock);
+ if (ctx->reserved_events + ctx->num_events < ctx->max_events && !ctx->choked)
+ {
+ ctx->reserved_events++;
+ res = 0;
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return res;
+}
+
+static int append_event(struct mpv_handle *ctx, struct mpv_event event, bool copy)
+{
+ if (ctx->num_events + ctx->reserved_events >= ctx->max_events)
+ return -1;
+ if (copy)
+ dup_event_data(&event);
+ ctx->events[(ctx->first_event + ctx->num_events) % ctx->max_events] = event;
+ ctx->num_events++;
+ wakeup_client(ctx);
+ if (event.event_id == MPV_EVENT_SHUTDOWN)
+ ctx->event_mask &= ctx->event_mask & ~(1ULL << MPV_EVENT_SHUTDOWN);
+ return 0;
+}
+
+static int send_event(struct mpv_handle *ctx, struct mpv_event *event, bool copy)
+{
+ mp_mutex_lock(&ctx->lock);
+ uint64_t mask = 1ULL << event->event_id;
+ if (ctx->property_event_masks & mask)
+ notify_property_events(ctx, event->event_id);
+ int r;
+ if (!(ctx->event_mask & mask)) {
+ r = 0;
+ } else if (ctx->choked) {
+ r = -1;
+ } else {
+ r = append_event(ctx, *event, copy);
+ if (r < 0) {
+ MP_ERR(ctx, "Too many events queued.\n");
+ ctx->choked = true;
+ }
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return r;
+}
+
+// Send a reply; the reply must have been previously reserved with
+// reserve_reply (otherwise, use send_event()).
+static void send_reply(struct mpv_handle *ctx, uint64_t userdata,
+ struct mpv_event *event)
+{
+ event->reply_userdata = userdata;
+ mp_mutex_lock(&ctx->lock);
+ // If this fails, reserve_reply() probably wasn't called.
+ assert(ctx->reserved_events > 0);
+ ctx->reserved_events--;
+ if (append_event(ctx, *event, false) < 0)
+ MP_ASSERT_UNREACHABLE();
+ mp_mutex_unlock(&ctx->lock);
+}
+
+void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ };
+ send_event(clients->clients[n], &event_data, true);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+// Like mp_client_broadcast_event(), but can be called from any thread.
+// Avoid using this.
+void mp_client_broadcast_event_external(struct mp_client_api *api, int event,
+ void *data)
+{
+ struct MPContext *mpctx = api->mpctx;
+
+ mp_client_broadcast_event(mpctx, event, data);
+ mp_wakeup_core(mpctx);
+}
+
+// If client_name == NULL, then broadcast and free the event.
+int mp_client_send_event(struct MPContext *mpctx, const char *client_name,
+ uint64_t reply_userdata, int event, void *data)
+{
+ if (!client_name) {
+ mp_client_broadcast_event(mpctx, event, data);
+ talloc_free(data);
+ return 0;
+ }
+
+ struct mp_client_api *clients = mpctx->clients;
+ int r = 0;
+
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ .reply_userdata = reply_userdata,
+ };
+
+ mp_mutex_lock(&clients->lock);
+
+ struct mpv_handle *ctx = find_client(clients, client_name);
+ if (ctx) {
+ r = send_event(ctx, &event_data, false);
+ } else {
+ r = -1;
+ talloc_free(data);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+
+ return r;
+}
+
+int mp_client_send_event_dup(struct MPContext *mpctx, const char *client_name,
+ int event, void *data)
+{
+ if (!client_name) {
+ mp_client_broadcast_event(mpctx, event, data);
+ return 0;
+ }
+
+ struct mpv_event event_data = {
+ .event_id = event,
+ .data = data,
+ };
+
+ dup_event_data(&event_data);
+ return mp_client_send_event(mpctx, client_name, 0, event, event_data.data);
+}
+
+const static bool deprecated_events[] = {
+ [MPV_EVENT_IDLE] = true,
+ [MPV_EVENT_TICK] = true,
+};
+
+int mpv_request_event(mpv_handle *ctx, mpv_event_id event, int enable)
+{
+ if (!mpv_event_name(event) || enable < 0 || enable > 1)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (event == MPV_EVENT_SHUTDOWN && !enable)
+ return MPV_ERROR_INVALID_PARAMETER;
+ assert(event < (int)INTERNAL_EVENT_BASE); // excluded above; they have no name
+ mp_mutex_lock(&ctx->lock);
+ uint64_t bit = 1ULL << event;
+ ctx->event_mask = enable ? ctx->event_mask | bit : ctx->event_mask & ~bit;
+ if (enable && event < MP_ARRAY_SIZE(deprecated_events) &&
+ deprecated_events[event])
+ {
+ MP_WARN(ctx, "The '%s' event is deprecated and will be removed.\n",
+ mpv_event_name(event));
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return 0;
+}
+
+// Set waiting_for_hook==true for all possibly pending properties.
+static void set_wait_for_hook_flags(mpv_handle *ctx)
+{
+ for (int n = 0; n < ctx->num_properties; n++) {
+ struct observe_property *prop = ctx->properties[n];
+
+ if (prop->value_ret_ts != prop->change_ts)
+ prop->waiting_for_hook = true;
+ }
+}
+
+// Return whether any property still has waiting_for_hook set.
+static bool check_for_for_hook_flags(mpv_handle *ctx)
+{
+ for (int n = 0; n < ctx->num_properties; n++) {
+ if (ctx->properties[n]->waiting_for_hook)
+ return true;
+ }
+ return false;
+}
+
+mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout)
+{
+ mpv_event *event = ctx->cur_event;
+
+ mp_mutex_lock(&ctx->lock);
+
+ if (!ctx->fuzzy_initialized)
+ mp_wakeup_core(ctx->clients->mpctx);
+ ctx->fuzzy_initialized = true;
+
+ if (timeout < 0)
+ timeout = 1e20;
+
+ int64_t deadline = mp_time_ns_add(mp_time_ns(), timeout);
+
+ *event = (mpv_event){0};
+ talloc_free_children(event);
+
+ while (1) {
+ if (ctx->queued_wakeup)
+ deadline = 0;
+ // Recover from overflow.
+ if (ctx->choked && !ctx->num_events) {
+ ctx->choked = false;
+ event->event_id = MPV_EVENT_QUEUE_OVERFLOW;
+ break;
+ }
+ struct mpv_event *ev =
+ ctx->num_events ? &ctx->events[ctx->first_event] : NULL;
+ if (ev && ev->event_id == MPV_EVENT_HOOK) {
+ // Give old property notifications priority over hooks. This is a
+ // guarantee given to clients to simplify their logic. New property
+ // changes after this are treated normally, so
+ if (!ctx->hook_pending) {
+ ctx->hook_pending = true;
+ set_wait_for_hook_flags(ctx);
+ }
+ if (check_for_for_hook_flags(ctx)) {
+ ev = NULL; // delay
+ } else {
+ ctx->hook_pending = false;
+ }
+ }
+ if (ev) {
+ *event = *ev;
+ ctx->first_event = (ctx->first_event + 1) % ctx->max_events;
+ ctx->num_events--;
+ talloc_steal(event, event->data);
+ break;
+ }
+ // If there's a changed property, generate change event (never queued).
+ if (gen_property_change_event(ctx))
+ break;
+ // Pop item from message queue, and return as event.
+ if (gen_log_message_event(ctx))
+ break;
+ int r = wait_wakeup(ctx, deadline);
+ if (r == ETIMEDOUT)
+ break;
+ }
+ ctx->queued_wakeup = false;
+
+ mp_mutex_unlock(&ctx->lock);
+
+ return event;
+}
+
+void mpv_wakeup(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->lock);
+ ctx->queued_wakeup = true;
+ wakeup_client(ctx);
+ mp_mutex_unlock(&ctx->lock);
+}
+
+// map client API types to internal types
+static const struct m_option type_conv[] = {
+ [MPV_FORMAT_STRING] = { .type = CONF_TYPE_STRING },
+ [MPV_FORMAT_FLAG] = { .type = CONF_TYPE_FLAG },
+ [MPV_FORMAT_INT64] = { .type = CONF_TYPE_INT64 },
+ [MPV_FORMAT_DOUBLE] = { .type = CONF_TYPE_DOUBLE },
+ [MPV_FORMAT_NODE] = { .type = CONF_TYPE_NODE },
+};
+
+static const struct m_option *get_mp_type(mpv_format format)
+{
+ if ((unsigned)format >= MP_ARRAY_SIZE(type_conv))
+ return NULL;
+ if (!type_conv[format].type)
+ return NULL;
+ return &type_conv[format];
+}
+
+// for read requests - MPV_FORMAT_OSD_STRING special handling
+static const struct m_option *get_mp_type_get(mpv_format format)
+{
+ if (format == MPV_FORMAT_OSD_STRING)
+ format = MPV_FORMAT_STRING; // it's string data, just other semantics
+ return get_mp_type(format);
+}
+
+// move src->dst, and do implicit conversion if possible (conversions to or
+// from strings are handled otherwise)
+static bool conv_node_to_format(void *dst, mpv_format dst_fmt, mpv_node *src)
+{
+ if (dst_fmt == src->format) {
+ const struct m_option *type = get_mp_type(dst_fmt);
+ memcpy(dst, &src->u, type->type->size);
+ return true;
+ }
+ if (dst_fmt == MPV_FORMAT_DOUBLE && src->format == MPV_FORMAT_INT64) {
+ *(double *)dst = src->u.int64;
+ return true;
+ }
+ if (dst_fmt == MPV_FORMAT_INT64 && src->format == MPV_FORMAT_DOUBLE) {
+ if (src->u.double_ > (double)INT64_MIN &&
+ src->u.double_ < (double)INT64_MAX)
+ {
+ *(int64_t *)dst = src->u.double_;
+ return true;
+ }
+ }
+ return false;
+}
+
+void mpv_free_node_contents(mpv_node *node)
+{
+ static const struct m_option type = { .type = CONF_TYPE_NODE };
+ m_option_free(&type, node);
+}
+
+int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ const struct m_option *type = get_mp_type(format);
+ if (!type)
+ return MPV_ERROR_OPTION_FORMAT;
+ struct mpv_node tmp;
+ if (format != MPV_FORMAT_NODE) {
+ tmp.format = format;
+ memcpy(&tmp.u, data, type->type->size);
+ data = &tmp;
+ }
+ lock_core(ctx);
+ int err = m_config_set_option_node(ctx->mpctx->mconfig, bstr0(name), data, 0);
+ unlock_core(ctx);
+ switch (err) {
+ case M_OPT_MISSING_PARAM:
+ case M_OPT_INVALID:
+ return MPV_ERROR_OPTION_ERROR;
+ case M_OPT_OUT_OF_RANGE:
+ return MPV_ERROR_OPTION_FORMAT;
+ case M_OPT_UNKNOWN:
+ return MPV_ERROR_OPTION_NOT_FOUND;
+ default:
+ if (err >= 0)
+ return 0;
+ return MPV_ERROR_OPTION_ERROR;
+ }
+}
+
+int mpv_set_option_string(mpv_handle *ctx, const char *name, const char *data)
+{
+ return mpv_set_option(ctx, name, MPV_FORMAT_STRING, &data);
+}
+
+// Run a command in the playback thread.
+static void run_locked(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data)
+{
+ mp_dispatch_lock(ctx->mpctx->dispatch);
+ fn(fn_data);
+ mp_dispatch_unlock(ctx->mpctx->dispatch);
+}
+
+// Run a command asynchronously. It's the responsibility of the caller to
+// actually send the reply. This helper merely saves a small part of the
+// required boilerplate to do so.
+// fn: callback to execute the request
+// fn_data: opaque caller-defined argument for fn. This will be automatically
+// freed with talloc_free(fn_data).
+static int run_async(mpv_handle *ctx, void (*fn)(void *fn_data), void *fn_data)
+{
+ int err = reserve_reply(ctx);
+ if (err < 0) {
+ talloc_free(fn_data);
+ return err;
+ }
+ mp_dispatch_enqueue(ctx->mpctx->dispatch, fn, fn_data);
+ return 0;
+}
+
+struct cmd_request {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd;
+ int status;
+ struct mpv_node *res;
+ struct mp_waiter completion;
+};
+
+static void cmd_complete(struct mp_cmd_ctx *cmd)
+{
+ struct cmd_request *req = cmd->on_completion_priv;
+
+ req->status = cmd->success ? 0 : MPV_ERROR_COMMAND;
+ if (req->res) {
+ *req->res = cmd->result;
+ cmd->result = (mpv_node){0};
+ }
+
+ // Unblock the waiting thread (especially for async commands).
+ mp_waiter_wakeup(&req->completion, 0);
+}
+
+static int run_client_command(mpv_handle *ctx, struct mp_cmd *cmd, mpv_node *res)
+{
+ if (!cmd)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!ctx->mpctx->initialized) {
+ talloc_free(cmd);
+ return MPV_ERROR_UNINITIALIZED;
+ }
+
+ cmd->sender = ctx->name;
+
+ struct cmd_request req = {
+ .mpctx = ctx->mpctx,
+ .cmd = cmd,
+ .res = res,
+ .completion = MP_WAITER_INITIALIZER,
+ };
+
+ bool async = cmd->flags & MP_ASYNC_CMD;
+
+ lock_core(ctx);
+ if (async) {
+ run_command(ctx->mpctx, cmd, NULL, NULL, NULL);
+ } else {
+ struct mp_abort_entry *abort = NULL;
+ if (cmd->def->can_abort) {
+ abort = talloc_zero(NULL, struct mp_abort_entry);
+ abort->client = ctx;
+ }
+ run_command(ctx->mpctx, cmd, abort, cmd_complete, &req);
+ }
+ unlock_core(ctx);
+
+ if (!async)
+ mp_waiter_wait(&req.completion);
+
+ return req.status;
+}
+
+int mpv_command(mpv_handle *ctx, const char **args)
+{
+ return run_client_command(ctx, mp_input_parse_cmd_strv(ctx->log, args), NULL);
+}
+
+int mpv_command_node(mpv_handle *ctx, mpv_node *args, mpv_node *result)
+{
+ struct mpv_node rn = {.format = MPV_FORMAT_NONE};
+ int r = run_client_command(ctx, mp_input_parse_cmd_node(ctx->log, args), &rn);
+ if (result && r >= 0)
+ *result = rn;
+ return r;
+}
+
+int mpv_command_ret(mpv_handle *ctx, const char **args, mpv_node *result)
+{
+ struct mpv_node rn = {.format = MPV_FORMAT_NONE};
+ int r = run_client_command(ctx, mp_input_parse_cmd_strv(ctx->log, args), &rn);
+ if (result && r >= 0)
+ *result = rn;
+ return r;
+}
+
+int mpv_command_string(mpv_handle *ctx, const char *args)
+{
+ return run_client_command(ctx,
+ mp_input_parse_cmd(ctx->mpctx->input, bstr0((char*)args), ctx->name), NULL);
+}
+
+struct async_cmd_request {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void async_cmd_complete(struct mp_cmd_ctx *cmd)
+{
+ struct async_cmd_request *req = cmd->on_completion_priv;
+
+ struct mpv_event_command *data = talloc_zero(NULL, struct mpv_event_command);
+ data->result = cmd->result;
+ cmd->result = (mpv_node){0};
+ talloc_steal(data, node_get_alloc(&data->result));
+
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_COMMAND_REPLY,
+ .data = data,
+ .error = cmd->success ? 0 : MPV_ERROR_COMMAND,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+
+ talloc_free(req);
+}
+
+static void async_cmd_fn(void *data)
+{
+ struct async_cmd_request *req = data;
+
+ struct mp_cmd *cmd = req->cmd;
+ ta_set_parent(cmd, NULL);
+ req->cmd = NULL;
+
+ struct mp_abort_entry *abort = NULL;
+ if (cmd->def->can_abort) {
+ abort = talloc_zero(NULL, struct mp_abort_entry);
+ abort->client = req->reply_ctx;
+ abort->client_work_type = MPV_EVENT_COMMAND_REPLY;
+ abort->client_work_id = req->userdata;
+ }
+
+ // This will synchronously or asynchronously call cmd_complete (depending
+ // on the command).
+ run_command(req->mpctx, cmd, abort, async_cmd_complete, req);
+}
+
+static int run_async_cmd(mpv_handle *ctx, uint64_t ud, struct mp_cmd *cmd)
+{
+ if (!cmd)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!ctx->mpctx->initialized) {
+ talloc_free(cmd);
+ return MPV_ERROR_UNINITIALIZED;
+ }
+
+ cmd->sender = ctx->name;
+
+ struct async_cmd_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct async_cmd_request){
+ .mpctx = ctx->mpctx,
+ .cmd = talloc_steal(req, cmd),
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+ return run_async(ctx, async_cmd_fn, req);
+}
+
+int mpv_command_async(mpv_handle *ctx, uint64_t ud, const char **args)
+{
+ return run_async_cmd(ctx, ud, mp_input_parse_cmd_strv(ctx->log, args));
+}
+
+int mpv_command_node_async(mpv_handle *ctx, uint64_t ud, mpv_node *args)
+{
+ return run_async_cmd(ctx, ud, mp_input_parse_cmd_node(ctx->log, args));
+}
+
+void mpv_abort_async_command(mpv_handle *ctx, uint64_t reply_userdata)
+{
+ abort_async(ctx->mpctx, ctx, MPV_EVENT_COMMAND_REPLY, reply_userdata);
+}
+
+static int translate_property_error(int errc)
+{
+ switch (errc) {
+ case M_PROPERTY_OK: return 0;
+ case M_PROPERTY_ERROR: return MPV_ERROR_PROPERTY_ERROR;
+ case M_PROPERTY_UNAVAILABLE: return MPV_ERROR_PROPERTY_UNAVAILABLE;
+ case M_PROPERTY_NOT_IMPLEMENTED: return MPV_ERROR_PROPERTY_ERROR;
+ case M_PROPERTY_UNKNOWN: return MPV_ERROR_PROPERTY_NOT_FOUND;
+ case M_PROPERTY_INVALID_FORMAT: return MPV_ERROR_PROPERTY_FORMAT;
+ // shouldn't happen
+ default: return MPV_ERROR_PROPERTY_ERROR;
+ }
+}
+
+struct setproperty_request {
+ struct MPContext *mpctx;
+ const char *name;
+ int format;
+ void *data;
+ int status;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void setproperty_fn(void *arg)
+{
+ struct setproperty_request *req = arg;
+ const struct m_option *type = get_mp_type(req->format);
+
+ struct mpv_node *node;
+ struct mpv_node tmp;
+ if (req->format == MPV_FORMAT_NODE) {
+ node = req->data;
+ } else {
+ tmp.format = req->format;
+ memcpy(&tmp.u, req->data, type->type->size);
+ node = &tmp;
+ }
+
+ int err = mp_property_do(req->name, M_PROPERTY_SET_NODE, node, req->mpctx);
+
+ req->status = translate_property_error(err);
+
+ if (req->reply_ctx) {
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_SET_PROPERTY_REPLY,
+ .error = req->status,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+ talloc_free(req);
+ }
+}
+
+int mpv_set_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ if (!ctx->mpctx->initialized) {
+ int r = mpv_set_option(ctx, name, format, data);
+ if (r == MPV_ERROR_OPTION_NOT_FOUND &&
+ mp_get_property_id(ctx->mpctx, name) >= 0)
+ return MPV_ERROR_PROPERTY_UNAVAILABLE;
+ switch (r) {
+ case MPV_ERROR_SUCCESS: return MPV_ERROR_SUCCESS;
+ case MPV_ERROR_OPTION_FORMAT: return MPV_ERROR_PROPERTY_FORMAT;
+ case MPV_ERROR_OPTION_NOT_FOUND: return MPV_ERROR_PROPERTY_NOT_FOUND;
+ default: return MPV_ERROR_PROPERTY_ERROR;
+ }
+ }
+ if (!get_mp_type(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct setproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = name,
+ .format = format,
+ .data = data,
+ };
+ run_locked(ctx, setproperty_fn, &req);
+ return req.status;
+}
+
+int mpv_del_property(mpv_handle *ctx, const char *name)
+{
+ const char* args[] = { "del", name, NULL };
+ return mpv_command(ctx, args);
+}
+
+int mpv_set_property_string(mpv_handle *ctx, const char *name, const char *data)
+{
+ return mpv_set_property(ctx, name, MPV_FORMAT_STRING, &data);
+}
+
+static void free_prop_set_req(void *ptr)
+{
+ struct setproperty_request *req = ptr;
+ const struct m_option *type = get_mp_type(req->format);
+ m_option_free(type, req->data);
+}
+
+int mpv_set_property_async(mpv_handle *ctx, uint64_t ud, const char *name,
+ mpv_format format, void *data)
+{
+ const struct m_option *type = get_mp_type(format);
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!type)
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct setproperty_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct setproperty_request){
+ .mpctx = ctx->mpctx,
+ .name = talloc_strdup(req, name),
+ .format = format,
+ .data = talloc_zero_size(req, type->type->size),
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+
+ m_option_copy(type, req->data, data);
+ talloc_set_destructor(req, free_prop_set_req);
+
+ return run_async(ctx, setproperty_fn, req);
+}
+
+struct getproperty_request {
+ struct MPContext *mpctx;
+ const char *name;
+ mpv_format format;
+ void *data;
+ int status;
+ struct mpv_handle *reply_ctx;
+ uint64_t userdata;
+};
+
+static void free_prop_data(void *ptr)
+{
+ struct mpv_event_property *prop = ptr;
+ const struct m_option *type = get_mp_type_get(prop->format);
+ m_option_free(type, prop->data);
+}
+
+static void getproperty_fn(void *arg)
+{
+ struct getproperty_request *req = arg;
+ const struct m_option *type = get_mp_type_get(req->format);
+
+ union m_option_value xdata = m_option_value_default;
+ void *data = req->data ? req->data : &xdata;
+
+ int err = -1;
+ switch (req->format) {
+ case MPV_FORMAT_OSD_STRING:
+ err = mp_property_do(req->name, M_PROPERTY_PRINT, data, req->mpctx);
+ break;
+ case MPV_FORMAT_STRING: {
+ char *s = NULL;
+ err = mp_property_do(req->name, M_PROPERTY_GET_STRING, &s, req->mpctx);
+ if (err == M_PROPERTY_OK)
+ *(char **)data = s;
+ break;
+ }
+ case MPV_FORMAT_NODE:
+ case MPV_FORMAT_FLAG:
+ case MPV_FORMAT_INT64:
+ case MPV_FORMAT_DOUBLE: {
+ struct mpv_node node = {{0}};
+ err = mp_property_do(req->name, M_PROPERTY_GET_NODE, &node, req->mpctx);
+ if (err == M_PROPERTY_NOT_IMPLEMENTED) {
+ // Go through explicit string conversion. Same reasoning as on the
+ // GET code path.
+ char *s = NULL;
+ err = mp_property_do(req->name, M_PROPERTY_GET_STRING, &s,
+ req->mpctx);
+ if (err != M_PROPERTY_OK)
+ break;
+ node.format = MPV_FORMAT_STRING;
+ node.u.string = s;
+ } else if (err <= 0)
+ break;
+ if (req->format == MPV_FORMAT_NODE) {
+ *(struct mpv_node *)data = node;
+ } else {
+ if (!conv_node_to_format(data, req->format, &node)) {
+ err = M_PROPERTY_INVALID_FORMAT;
+ mpv_free_node_contents(&node);
+ }
+ }
+ break;
+ }
+ default:
+ abort();
+ }
+
+ req->status = translate_property_error(err);
+
+ if (req->reply_ctx) {
+ struct mpv_event_property *prop = talloc_ptrtype(NULL, prop);
+ *prop = (struct mpv_event_property){
+ .name = talloc_steal(prop, (char *)req->name),
+ .format = req->format,
+ .data = talloc_size(prop, type->type->size),
+ };
+ // move data
+ memcpy(prop->data, &xdata, type->type->size);
+ talloc_set_destructor(prop, free_prop_data);
+ struct mpv_event reply = {
+ .event_id = MPV_EVENT_GET_PROPERTY_REPLY,
+ .data = prop,
+ .error = req->status,
+ };
+ send_reply(req->reply_ctx, req->userdata, &reply);
+ talloc_free(req);
+ }
+}
+
+int mpv_get_property(mpv_handle *ctx, const char *name, mpv_format format,
+ void *data)
+{
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!data)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (!get_mp_type_get(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct getproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = name,
+ .format = format,
+ .data = data,
+ };
+ run_locked(ctx, getproperty_fn, &req);
+ return req.status;
+}
+
+char *mpv_get_property_string(mpv_handle *ctx, const char *name)
+{
+ char *str = NULL;
+ mpv_get_property(ctx, name, MPV_FORMAT_STRING, &str);
+ return str;
+}
+
+char *mpv_get_property_osd_string(mpv_handle *ctx, const char *name)
+{
+ char *str = NULL;
+ mpv_get_property(ctx, name, MPV_FORMAT_OSD_STRING, &str);
+ return str;
+}
+
+int mpv_get_property_async(mpv_handle *ctx, uint64_t ud, const char *name,
+ mpv_format format)
+{
+ if (!ctx->mpctx->initialized)
+ return MPV_ERROR_UNINITIALIZED;
+ if (!get_mp_type_get(format))
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ struct getproperty_request *req = talloc_ptrtype(NULL, req);
+ *req = (struct getproperty_request){
+ .mpctx = ctx->mpctx,
+ .name = talloc_strdup(req, name),
+ .format = format,
+ .reply_ctx = ctx,
+ .userdata = ud,
+ };
+ return run_async(ctx, getproperty_fn, req);
+}
+
+static void property_free(void *p)
+{
+ struct observe_property *prop = p;
+
+ assert(prop->refcount == 0);
+
+ if (prop->type) {
+ m_option_free(prop->type, &prop->value);
+ m_option_free(prop->type, &prop->value_ret);
+ }
+}
+
+int mpv_observe_property(mpv_handle *ctx, uint64_t userdata,
+ const char *name, mpv_format format)
+{
+ const struct m_option *type = get_mp_type_get(format);
+ if (format != MPV_FORMAT_NONE && !type)
+ return MPV_ERROR_PROPERTY_FORMAT;
+ // Explicitly disallow this, because it would require a special code path.
+ if (format == MPV_FORMAT_OSD_STRING)
+ return MPV_ERROR_PROPERTY_FORMAT;
+
+ mp_mutex_lock(&ctx->lock);
+ assert(!ctx->destroying);
+ struct observe_property *prop = talloc_ptrtype(ctx, prop);
+ talloc_set_destructor(prop, property_free);
+ *prop = (struct observe_property){
+ .owner = ctx,
+ .name = talloc_strdup(prop, name),
+ .id = mp_get_property_id(ctx->mpctx, name),
+ .event_mask = mp_get_property_event_mask(name),
+ .reply_id = userdata,
+ .format = format,
+ .type = type,
+ .change_ts = 1, // force initial event
+ .refcount = 1,
+ .value = m_option_value_default,
+ .value_ret = m_option_value_default,
+ };
+ ctx->properties_change_ts += 1;
+ MP_TARRAY_APPEND(ctx, ctx->properties, ctx->num_properties, prop);
+ ctx->property_event_masks |= prop->event_mask;
+ ctx->new_property_events = true;
+ ctx->cur_property_index = 0;
+ ctx->has_pending_properties = true;
+ mp_mutex_unlock(&ctx->lock);
+ mp_wakeup_core(ctx->mpctx);
+ return 0;
+}
+
+int mpv_unobserve_property(mpv_handle *ctx, uint64_t userdata)
+{
+ mp_mutex_lock(&ctx->lock);
+ int count = 0;
+ for (int n = ctx->num_properties - 1; n >= 0; n--) {
+ struct observe_property *prop = ctx->properties[n];
+ // Perform actual removal of the property lazily to avoid creating
+ // dangling pointers and such.
+ if (prop->reply_id == userdata) {
+ prop_unref(prop);
+ ctx->properties_change_ts += 1;
+ MP_TARRAY_REMOVE_AT(ctx->properties, ctx->num_properties, n);
+ ctx->cur_property_index = 0;
+ count++;
+ }
+ }
+ mp_mutex_unlock(&ctx->lock);
+ return count;
+}
+
+static bool property_shared_prefix(const char *a0, const char *b0)
+{
+ bstr a = bstr0(a0);
+ bstr b = bstr0(b0);
+
+ // Treat options and properties as equivalent.
+ bstr_eatstart0(&a, "options/");
+ bstr_eatstart0(&b, "options/");
+
+ // Compare the potentially-common portion
+ if (memcmp(a.start, b.start, MPMIN(a.len, b.len)))
+ return false;
+
+ // If lengths were equal, we're done
+ if (a.len == b.len)
+ return true;
+
+ // Check for a slash in the first non-common byte of the longer string
+ if (a.len > b.len)
+ return a.start[b.len] == '/';
+ else
+ return b.start[a.len] == '/';
+}
+
+// Broadcast that a property has changed.
+void mp_client_property_change(struct MPContext *mpctx, const char *name)
+{
+ struct mp_client_api *clients = mpctx->clients;
+ int id = mp_get_property_id(mpctx, name);
+ bool any_pending = false;
+
+ mp_mutex_lock(&clients->lock);
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_handle *client = clients->clients[n];
+ mp_mutex_lock(&client->lock);
+ for (int i = 0; i < client->num_properties; i++) {
+ if (client->properties[i]->id == id &&
+ property_shared_prefix(name, client->properties[i]->name)) {
+ client->properties[i]->change_ts += 1;
+ client->has_pending_properties = true;
+ any_pending = true;
+ }
+ }
+ mp_mutex_unlock(&client->lock);
+ }
+
+ mp_mutex_unlock(&clients->lock);
+
+ // If we're inside mp_dispatch_queue_process(), this will cause the playloop
+ // to be re-run (to get mp_client_send_property_changes() called). If we're
+ // inside the normal playloop, this does nothing, but the latter function
+ // will be called at the end of the playloop anyway.
+ if (any_pending)
+ mp_dispatch_adjust_timeout(mpctx->dispatch, 0);
+}
+
+// Mark properties as changed in reaction to specific events.
+// Called with ctx->lock held.
+static void notify_property_events(struct mpv_handle *ctx, int event)
+{
+ uint64_t mask = 1ULL << event;
+ for (int i = 0; i < ctx->num_properties; i++) {
+ if (ctx->properties[i]->event_mask & mask) {
+ ctx->properties[i]->change_ts += 1;
+ ctx->has_pending_properties = true;
+ }
+ }
+
+ // Same as in mp_client_property_change().
+ if (ctx->has_pending_properties)
+ mp_dispatch_adjust_timeout(ctx->mpctx->dispatch, 0);
+}
+
+// Call with ctx->lock held (only). May temporarily drop the lock.
+static void send_client_property_changes(struct mpv_handle *ctx)
+{
+ uint64_t cur_ts = ctx->properties_change_ts;
+
+ ctx->has_pending_properties = false;
+
+ for (int n = 0; n < ctx->num_properties; n++) {
+ struct observe_property *prop = ctx->properties[n];
+
+ if (prop->value_ts == prop->change_ts)
+ continue;
+
+ bool changed = false;
+ if (prop->format) {
+ const struct m_option *type = prop->type;
+ union m_option_value val = m_option_value_default;
+ struct getproperty_request req = {
+ .mpctx = ctx->mpctx,
+ .name = prop->name,
+ .format = prop->format,
+ .data = &val,
+ };
+
+ // Temporarily unlock and read the property. The very important
+ // thing is that property getters can do whatever they want, _and_
+ // that they may wait on the client API user thread (if vo_libmpv
+ // or similar things are involved).
+ prop->refcount += 1; // keep prop alive (esp. prop->name)
+ ctx->async_counter += 1; // keep ctx alive
+ mp_mutex_unlock(&ctx->lock);
+ getproperty_fn(&req);
+ mp_mutex_lock(&ctx->lock);
+ ctx->async_counter -= 1;
+ prop_unref(prop);
+
+ // Set if observed properties was changed or something similar
+ // => start over, retry next time.
+ if (cur_ts != ctx->properties_change_ts || ctx->destroying) {
+ m_option_free(type, &val);
+ mp_wakeup_core(ctx->mpctx);
+ ctx->has_pending_properties = true;
+ break;
+ }
+ assert(prop->refcount > 0);
+
+ bool val_valid = req.status >= 0;
+ changed = prop->value_valid != val_valid;
+ if (prop->value_valid && val_valid)
+ changed = !equal_mpv_value(&prop->value, &val, prop->format);
+ if (prop->value_ts == 0)
+ changed = true; // initial event
+
+ prop->value_valid = val_valid;
+ if (changed && val_valid) {
+ // move val to prop->value
+ m_option_free(type, &prop->value);
+ memcpy(&prop->value, &val, type->type->size);
+ memset(&val, 0, type->type->size);
+ }
+
+ m_option_free(prop->type, &val);
+ } else {
+ changed = true;
+ }
+
+ if (prop->waiting_for_hook)
+ ctx->new_property_events = true; // make sure to wakeup
+
+ // Avoid retriggering the change event if the property didn't change,
+ // and the previous value was actually returned to the client.
+ if (!changed && prop->value_ret_ts == prop->value_ts) {
+ prop->value_ret_ts = prop->change_ts; // no change => no event
+ prop->waiting_for_hook = false;
+ } else {
+ ctx->new_property_events = true;
+ }
+
+ prop->value_ts = prop->change_ts;
+ }
+
+ if (ctx->destroying || ctx->new_property_events)
+ wakeup_client(ctx);
+}
+
+void mp_client_send_property_changes(struct MPContext *mpctx)
+{
+ struct mp_client_api *clients = mpctx->clients;
+
+ mp_mutex_lock(&clients->lock);
+ uint64_t cur_ts = clients->clients_list_change_ts;
+
+ for (int n = 0; n < clients->num_clients; n++) {
+ struct mpv_handle *ctx = clients->clients[n];
+
+ mp_mutex_lock(&ctx->lock);
+ if (!ctx->has_pending_properties || ctx->destroying) {
+ mp_mutex_unlock(&ctx->lock);
+ continue;
+ }
+ // Keep ctx->lock locked (unlock order does not matter).
+ mp_mutex_unlock(&clients->lock);
+ send_client_property_changes(ctx);
+ mp_mutex_unlock(&ctx->lock);
+ mp_mutex_lock(&clients->lock);
+ if (cur_ts != clients->clients_list_change_ts) {
+ // List changed; need to start over. Do it in the next iteration.
+ mp_wakeup_core(mpctx);
+ break;
+ }
+ }
+
+ mp_mutex_unlock(&clients->lock);
+}
+
+// Set ctx->cur_event to a generated property change event, if there is any
+// outstanding property.
+static bool gen_property_change_event(struct mpv_handle *ctx)
+{
+ if (!ctx->mpctx->initialized)
+ return false;
+
+ while (1) {
+ if (ctx->cur_property_index >= ctx->num_properties) {
+ ctx->new_property_events &= ctx->num_properties > 0;
+ if (!ctx->new_property_events)
+ break;
+ ctx->new_property_events = false;
+ ctx->cur_property_index = 0;
+ }
+
+ struct observe_property *prop = ctx->properties[ctx->cur_property_index++];
+
+ if (prop->value_ts == prop->change_ts && // not a stale value?
+ prop->value_ret_ts != prop->value_ts) // other value than last time?
+ {
+ prop->value_ret_ts = prop->value_ts;
+ prop->waiting_for_hook = false;
+ prop_unref(ctx->cur_property);
+ ctx->cur_property = prop;
+ prop->refcount += 1;
+
+ if (prop->value_valid)
+ m_option_copy(prop->type, &prop->value_ret, &prop->value);
+
+ ctx->cur_property_event = (struct mpv_event_property){
+ .name = prop->name,
+ .format = prop->value_valid ? prop->format : 0,
+ .data = prop->value_valid ? &prop->value_ret : NULL,
+ };
+ *ctx->cur_event = (struct mpv_event){
+ .event_id = MPV_EVENT_PROPERTY_CHANGE,
+ .reply_userdata = prop->reply_id,
+ .data = &ctx->cur_property_event,
+ };
+ return true;
+ }
+ }
+
+ return false;
+}
+
+int mpv_hook_add(mpv_handle *ctx, uint64_t reply_userdata,
+ const char *name, int priority)
+{
+ lock_core(ctx);
+ mp_hook_add(ctx->mpctx, ctx->name, ctx->id, name, reply_userdata, priority);
+ unlock_core(ctx);
+ return 0;
+}
+
+int mpv_hook_continue(mpv_handle *ctx, uint64_t id)
+{
+ lock_core(ctx);
+ int r = mp_hook_continue(ctx->mpctx, ctx->id, id);
+ unlock_core(ctx);
+ return r;
+}
+
+int mpv_load_config_file(mpv_handle *ctx, const char *filename)
+{
+ lock_core(ctx);
+ int r = m_config_parse_config_file(ctx->mpctx->mconfig, ctx->mpctx->global, filename, NULL, 0);
+ unlock_core(ctx);
+ if (r == 0)
+ return MPV_ERROR_INVALID_PARAMETER;
+ if (r < 0)
+ return MPV_ERROR_OPTION_ERROR;
+ return 0;
+}
+
+static void msg_wakeup(void *p)
+{
+ mpv_handle *ctx = p;
+ wakeup_client(ctx);
+}
+
+// Undocumented: if min_level starts with "silent:", then log messages are not
+// returned to the API user, but are stored until logging is enabled normally
+// again by calling this without "silent:". (Using a different level will
+// flush it, though.)
+int mpv_request_log_messages(mpv_handle *ctx, const char *min_level)
+{
+ bstr blevel = bstr0(min_level);
+ bool silent = bstr_eatstart0(&blevel, "silent:");
+
+ int level = -1;
+ for (int n = 0; n < MSGL_MAX + 1; n++) {
+ if (mp_log_levels[n] && bstr_equals0(blevel, mp_log_levels[n])) {
+ level = n;
+ break;
+ }
+ }
+ if (bstr_equals0(blevel, "terminal-default"))
+ level = MP_LOG_BUFFER_MSGL_TERM;
+
+ if (level < 0 && strcmp(min_level, "no") != 0)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ mp_mutex_lock(&ctx->lock);
+ if (level < 0 || level != ctx->messages_level) {
+ mp_msg_log_buffer_destroy(ctx->messages);
+ ctx->messages = NULL;
+ }
+ if (level >= 0) {
+ if (!ctx->messages) {
+ int size = level >= MSGL_V ? 10000 : 1000;
+ ctx->messages = mp_msg_log_buffer_new(ctx->mpctx->global, size,
+ level, msg_wakeup, ctx);
+ ctx->messages_level = level;
+ }
+ mp_msg_log_buffer_set_silent(ctx->messages, silent);
+ }
+ wakeup_client(ctx);
+ mp_mutex_unlock(&ctx->lock);
+ return 0;
+}
+
+// Set ctx->cur_event to a generated log message event, if any available.
+static bool gen_log_message_event(struct mpv_handle *ctx)
+{
+ if (ctx->messages) {
+ struct mp_log_buffer_entry *msg =
+ mp_msg_log_buffer_read(ctx->messages);
+ if (msg) {
+ struct mpv_event_log_message *cmsg =
+ talloc_ptrtype(ctx->cur_event, cmsg);
+ talloc_steal(cmsg, msg);
+ *cmsg = (struct mpv_event_log_message){
+ .prefix = msg->prefix,
+ .level = mp_log_levels[msg->level],
+ .log_level = mp_mpv_log_levels[msg->level],
+ .text = msg->text,
+ };
+ *ctx->cur_event = (struct mpv_event){
+ .event_id = MPV_EVENT_LOG_MESSAGE,
+ .data = cmsg,
+ };
+ return true;
+ }
+ }
+ return false;
+}
+
+int mpv_get_wakeup_pipe(mpv_handle *ctx)
+{
+ mp_mutex_lock(&ctx->wakeup_lock);
+ if (ctx->wakeup_pipe[0] == -1) {
+ if (mp_make_wakeup_pipe(ctx->wakeup_pipe) >= 0)
+ (void)write(ctx->wakeup_pipe[1], &(char){0}, 1);
+ }
+ int fd = ctx->wakeup_pipe[0];
+ mp_mutex_unlock(&ctx->wakeup_lock);
+ return fd;
+}
+
+unsigned long mpv_client_api_version(void)
+{
+ return MPV_CLIENT_API_VERSION;
+}
+
+int mpv_event_to_node(mpv_node *dst, mpv_event *event)
+{
+ *dst = (mpv_node){0};
+
+ node_init(dst, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_string(dst, "event", mpv_event_name(event->event_id));
+
+ if (event->error < 0)
+ node_map_add_string(dst, "error", mpv_error_string(event->error));
+
+ if (event->reply_userdata)
+ node_map_add_int64(dst, "id", event->reply_userdata);
+
+ switch (event->event_id) {
+
+ case MPV_EVENT_START_FILE: {
+ mpv_event_start_file *esf = event->data;
+
+ node_map_add_int64(dst, "playlist_entry_id", esf->playlist_entry_id);
+ break;
+ }
+
+ case MPV_EVENT_END_FILE: {
+ mpv_event_end_file *eef = event->data;
+
+ const char *reason;
+ switch (eef->reason) {
+ case MPV_END_FILE_REASON_EOF: reason = "eof"; break;
+ case MPV_END_FILE_REASON_STOP: reason = "stop"; break;
+ case MPV_END_FILE_REASON_QUIT: reason = "quit"; break;
+ case MPV_END_FILE_REASON_ERROR: reason = "error"; break;
+ case MPV_END_FILE_REASON_REDIRECT: reason = "redirect"; break;
+ default:
+ reason = "unknown";
+ }
+ node_map_add_string(dst, "reason", reason);
+
+ node_map_add_int64(dst, "playlist_entry_id", eef->playlist_entry_id);
+
+ if (eef->playlist_insert_id) {
+ node_map_add_int64(dst, "playlist_insert_id", eef->playlist_insert_id);
+ node_map_add_int64(dst, "playlist_insert_num_entries",
+ eef->playlist_insert_num_entries);
+ }
+
+ if (eef->reason == MPV_END_FILE_REASON_ERROR)
+ node_map_add_string(dst, "file_error", mpv_error_string(eef->error));
+ break;
+ }
+
+ case MPV_EVENT_LOG_MESSAGE: {
+ mpv_event_log_message *msg = event->data;
+
+ node_map_add_string(dst, "prefix", msg->prefix);
+ node_map_add_string(dst, "level", msg->level);
+ node_map_add_string(dst, "text", msg->text);
+ break;
+ }
+
+ case MPV_EVENT_CLIENT_MESSAGE: {
+ mpv_event_client_message *msg = event->data;
+
+ struct mpv_node *args = node_map_add(dst, "args", MPV_FORMAT_NODE_ARRAY);
+ for (int n = 0; n < msg->num_args; n++) {
+ struct mpv_node *sn = node_array_add(args, MPV_FORMAT_NONE);
+ sn->format = MPV_FORMAT_STRING;
+ sn->u.string = (char *)msg->args[n];
+ }
+ break;
+ }
+
+ case MPV_EVENT_PROPERTY_CHANGE: {
+ mpv_event_property *prop = event->data;
+
+ node_map_add_string(dst, "name", prop->name);
+
+ switch (prop->format) {
+ case MPV_FORMAT_NODE:
+ *node_map_add(dst, "data", MPV_FORMAT_NONE) =
+ *(struct mpv_node *)prop->data;
+ break;
+ case MPV_FORMAT_DOUBLE:
+ node_map_add_double(dst, "data", *(double *)prop->data);
+ break;
+ case MPV_FORMAT_FLAG:
+ node_map_add_flag(dst, "data", *(int *)prop->data);
+ break;
+ case MPV_FORMAT_STRING:
+ node_map_add_string(dst, "data", *(char **)prop->data);
+ break;
+ default: ;
+ }
+ break;
+ }
+
+ case MPV_EVENT_COMMAND_REPLY: {
+ mpv_event_command *cmd = event->data;
+
+ *node_map_add(dst, "result", MPV_FORMAT_NONE) = cmd->result;
+ break;
+ }
+
+ case MPV_EVENT_HOOK: {
+ mpv_event_hook *hook = event->data;
+
+ node_map_add_int64(dst, "hook_id", hook->id);
+ break;
+ }
+
+ }
+ return 0;
+}
+
+static const char *const err_table[] = {
+ [-MPV_ERROR_SUCCESS] = "success",
+ [-MPV_ERROR_EVENT_QUEUE_FULL] = "event queue full",
+ [-MPV_ERROR_NOMEM] = "memory allocation failed",
+ [-MPV_ERROR_UNINITIALIZED] = "core not uninitialized",
+ [-MPV_ERROR_INVALID_PARAMETER] = "invalid parameter",
+ [-MPV_ERROR_OPTION_NOT_FOUND] = "option not found",
+ [-MPV_ERROR_OPTION_FORMAT] = "unsupported format for accessing option",
+ [-MPV_ERROR_OPTION_ERROR] = "error setting option",
+ [-MPV_ERROR_PROPERTY_NOT_FOUND] = "property not found",
+ [-MPV_ERROR_PROPERTY_FORMAT] = "unsupported format for accessing property",
+ [-MPV_ERROR_PROPERTY_UNAVAILABLE] = "property unavailable",
+ [-MPV_ERROR_PROPERTY_ERROR] = "error accessing property",
+ [-MPV_ERROR_COMMAND] = "error running command",
+ [-MPV_ERROR_LOADING_FAILED] = "loading failed",
+ [-MPV_ERROR_AO_INIT_FAILED] = "audio output initialization failed",
+ [-MPV_ERROR_VO_INIT_FAILED] = "video output initialization failed",
+ [-MPV_ERROR_NOTHING_TO_PLAY] = "no audio or video data played",
+ [-MPV_ERROR_UNKNOWN_FORMAT] = "unrecognized file format",
+ [-MPV_ERROR_UNSUPPORTED] = "not supported",
+ [-MPV_ERROR_NOT_IMPLEMENTED] = "operation not implemented",
+ [-MPV_ERROR_GENERIC] = "something happened",
+};
+
+const char *mpv_error_string(int error)
+{
+ error = -error;
+ if (error < 0)
+ error = 0;
+ const char *name = NULL;
+ if (error < MP_ARRAY_SIZE(err_table))
+ name = err_table[error];
+ return name ? name : "unknown error";
+}
+
+static const char *const event_table[] = {
+ [MPV_EVENT_NONE] = "none",
+ [MPV_EVENT_SHUTDOWN] = "shutdown",
+ [MPV_EVENT_LOG_MESSAGE] = "log-message",
+ [MPV_EVENT_GET_PROPERTY_REPLY] = "get-property-reply",
+ [MPV_EVENT_SET_PROPERTY_REPLY] = "set-property-reply",
+ [MPV_EVENT_COMMAND_REPLY] = "command-reply",
+ [MPV_EVENT_START_FILE] = "start-file",
+ [MPV_EVENT_END_FILE] = "end-file",
+ [MPV_EVENT_FILE_LOADED] = "file-loaded",
+ [MPV_EVENT_IDLE] = "idle",
+ [MPV_EVENT_TICK] = "tick",
+ [MPV_EVENT_CLIENT_MESSAGE] = "client-message",
+ [MPV_EVENT_VIDEO_RECONFIG] = "video-reconfig",
+ [MPV_EVENT_AUDIO_RECONFIG] = "audio-reconfig",
+ [MPV_EVENT_SEEK] = "seek",
+ [MPV_EVENT_PLAYBACK_RESTART] = "playback-restart",
+ [MPV_EVENT_PROPERTY_CHANGE] = "property-change",
+ [MPV_EVENT_QUEUE_OVERFLOW] = "event-queue-overflow",
+ [MPV_EVENT_HOOK] = "hook",
+};
+
+const char *mpv_event_name(mpv_event_id event)
+{
+ if ((unsigned)event >= MP_ARRAY_SIZE(event_table))
+ return NULL;
+ return event_table[event];
+}
+
+void mpv_free(void *data)
+{
+ talloc_free(data);
+}
+
+int64_t mpv_get_time_ns(mpv_handle *ctx)
+{
+ return mp_time_ns();
+}
+
+int64_t mpv_get_time_us(mpv_handle *ctx)
+{
+ return mp_time_ns() / 1000;
+}
+
+#include "video/out/libmpv.h"
+
+static void do_kill(void *ptr)
+{
+ struct MPContext *mpctx = ptr;
+
+ struct track *track = mpctx->vo_chain ? mpctx->vo_chain->track : NULL;
+ uninit_video_out(mpctx);
+ if (track) {
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ error_on_track(mpctx, track);
+ }
+}
+
+// Used by vo_libmpv to (a)synchronously uninitialize video.
+void kill_video_async(struct mp_client_api *client_api)
+{
+ struct MPContext *mpctx = client_api->mpctx;
+ mp_dispatch_enqueue(mpctx->dispatch, do_kill, mpctx);
+}
+
+// Used by vo_libmpv to set the current render context.
+bool mp_set_main_render_context(struct mp_client_api *client_api,
+ struct mpv_render_context *ctx, bool active)
+{
+ assert(ctx);
+
+ mp_mutex_lock(&client_api->lock);
+ bool is_set = !!client_api->render_context;
+ bool is_same = client_api->render_context == ctx;
+ // Can set if it doesn't remove another existing ctx.
+ bool res = is_same || !is_set;
+ if (res)
+ client_api->render_context = active ? ctx : NULL;
+ mp_mutex_unlock(&client_api->lock);
+ return res;
+}
+
+// Used by vo_libmpv. Relies on guarantees by mp_render_context_acquire().
+struct mpv_render_context *
+mp_client_api_acquire_render_context(struct mp_client_api *ca)
+{
+ struct mpv_render_context *res = NULL;
+ mp_mutex_lock(&ca->lock);
+ if (ca->render_context && mp_render_context_acquire(ca->render_context))
+ res = ca->render_context;
+ mp_mutex_unlock(&ca->lock);
+ return res;
+}
+
+// stream_cb
+
+struct mp_custom_protocol {
+ char *protocol;
+ void *user_data;
+ mpv_stream_cb_open_ro_fn open_fn;
+};
+
+int mpv_stream_cb_add_ro(mpv_handle *ctx, const char *protocol, void *user_data,
+ mpv_stream_cb_open_ro_fn open_fn)
+{
+ if (!open_fn)
+ return MPV_ERROR_INVALID_PARAMETER;
+
+ struct mp_client_api *clients = ctx->clients;
+ int r = 0;
+ mp_mutex_lock(&clients->lock);
+ for (int n = 0; n < clients->num_custom_protocols; n++) {
+ struct mp_custom_protocol *proto = &clients->custom_protocols[n];
+ if (strcmp(proto->protocol, protocol) == 0) {
+ r = MPV_ERROR_INVALID_PARAMETER;
+ break;
+ }
+ }
+ if (stream_has_proto(protocol))
+ r = MPV_ERROR_INVALID_PARAMETER;
+ if (r >= 0) {
+ struct mp_custom_protocol proto = {
+ .protocol = talloc_strdup(clients, protocol),
+ .user_data = user_data,
+ .open_fn = open_fn,
+ };
+ MP_TARRAY_APPEND(clients, clients->custom_protocols,
+ clients->num_custom_protocols, proto);
+ }
+ mp_mutex_unlock(&clients->lock);
+ return r;
+}
+
+bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
+ void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn)
+{
+ struct mp_client_api *clients = g->client_api;
+ bool found = false;
+ mp_mutex_lock(&clients->lock);
+ for (int n = 0; n < clients->num_custom_protocols; n++) {
+ struct mp_custom_protocol *proto = &clients->custom_protocols[n];
+ if (strcmp(proto->protocol, protocol) == 0) {
+ *out_user_data = proto->user_data;
+ *out_fn = proto->open_fn;
+ found = true;
+ break;
+ }
+ }
+ mp_mutex_unlock(&clients->lock);
+ return found;
+}
diff --git a/player/client.h b/player/client.h
new file mode 100644
index 0000000..ddc568e
--- /dev/null
+++ b/player/client.h
@@ -0,0 +1,58 @@
+#ifndef MP_CLIENT_H_
+#define MP_CLIENT_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+#include "libmpv/stream_cb.h"
+#include "misc/bstr.h"
+
+struct MPContext;
+struct mpv_handle;
+struct mp_client_api;
+struct mp_log;
+struct mpv_global;
+
+// Includes space for \0
+#define MAX_CLIENT_NAME 64
+
+void mp_clients_init(struct MPContext *mpctx);
+void mp_clients_destroy(struct MPContext *mpctx);
+void mp_shutdown_clients(struct MPContext *mpctx);
+bool mp_is_shutting_down(struct MPContext *mpctx);
+bool mp_clients_all_initialized(struct MPContext *mpctx);
+
+bool mp_client_id_exists(struct MPContext *mpctx, int64_t id);
+void mp_client_broadcast_event(struct MPContext *mpctx, int event, void *data);
+int mp_client_send_event(struct MPContext *mpctx, const char *client_name,
+ uint64_t reply_userdata, int event, void *data);
+int mp_client_send_event_dup(struct MPContext *mpctx, const char *client_name,
+ int event, void *data);
+void mp_client_property_change(struct MPContext *mpctx, const char *name);
+void mp_client_send_property_changes(struct MPContext *mpctx);
+
+struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name);
+void mp_client_set_weak(struct mpv_handle *ctx);
+struct mp_log *mp_client_get_log(struct mpv_handle *ctx);
+struct mpv_global *mp_client_get_global(struct mpv_handle *ctx);
+
+void mp_client_broadcast_event_external(struct mp_client_api *api, int event,
+ void *data);
+
+// m_option.c
+void *node_get_alloc(struct mpv_node *node);
+
+// for vo_libmpv.c
+struct osd_state;
+struct mpv_render_context;
+bool mp_set_main_render_context(struct mp_client_api *client_api,
+ struct mpv_render_context *ctx, bool active);
+struct mpv_render_context *
+mp_client_api_acquire_render_context(struct mp_client_api *ca);
+void kill_video_async(struct mp_client_api *client_api);
+
+bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
+ void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn);
+
+#endif
diff --git a/player/command.c b/player/command.c
new file mode 100644
index 0000000..8bff0cd
--- /dev/null
+++ b/player/command.c
@@ -0,0 +1,7149 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <float.h>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <time.h>
+#include <math.h>
+#include <sys/types.h>
+
+#include <ass/ass.h>
+#include <libavutil/avstring.h>
+#include <libavutil/common.h>
+
+#include "mpv_talloc.h"
+#include "client.h"
+#include "external_files.h"
+#include "common/av_common.h"
+#include "common/codecs.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "filters/f_decoder_wrapper.h"
+#include "command.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "common/common.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "stream/stream.h"
+#include "demux/demux.h"
+#include "demux/stheader.h"
+#include "common/playlist.h"
+#include "sub/dec_sub.h"
+#include "sub/osd.h"
+#include "sub/sd.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "options/m_config_frontend.h"
+#include "osdep/getpid.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+#include "video/hwdec.h"
+#include "audio/aframe.h"
+#include "audio/format.h"
+#include "audio/out/ao.h"
+#include "video/out/bitmap_packer.h"
+#include "options/path.h"
+#include "screenshot.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/thread_pool.h"
+#include "misc/thread_tools.h"
+
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+
+#include "core.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+struct command_ctx {
+ // All properties, terminated with a {0} item.
+ struct m_property *properties;
+
+ double last_seek_time;
+ double last_seek_pts;
+ double marked_pts;
+ bool marked_permanent;
+
+ char **warned_deprecated;
+ int num_warned_deprecated;
+
+ struct overlay *overlays;
+ int num_overlays;
+ // One of these is in use by the OSD; the other one exists so that the
+ // bitmap list can be manipulated without additional synchronization.
+ struct sub_bitmaps overlay_osd[2];
+ int overlay_osd_current;
+ struct bitmap_packer *overlay_packer;
+
+ struct hook_handler **hooks;
+ int num_hooks;
+ int64_t hook_seq; // for hook_handler.seq
+
+ struct ao_hotplug *hotplug;
+
+ struct mp_cmd_ctx *cache_dump_cmd; // in progress cache dumping
+
+ char **script_props;
+ mpv_node udata;
+
+ double cached_window_scale;
+ bool shared_script_warning;
+};
+
+static const struct m_option script_props_type = {
+ .type = &m_option_type_keyvalue_list
+};
+
+static const struct m_option udata_type = {
+ .type = CONF_TYPE_NODE
+};
+
+struct overlay {
+ struct mp_image *source;
+ int x, y;
+};
+
+struct hook_handler {
+ char *client; // client mpv_handle name (for logging)
+ int64_t client_id; // client mpv_handle ID
+ char *type; // kind of hook, e.g. "on_load"
+ uint64_t user_id; // user-chosen ID
+ int priority; // priority for global hook order
+ int64_t seq; // unique ID, != 0, also for fixed order on equal priorities
+ bool active; // hook is currently in progress (only 1 at a time for now)
+};
+
+// U+279C HEAVY ROUND-TIPPED RIGHTWARDS ARROW
+// U+00A0 NO-BREAK SPACE
+#define ARROW_SP "\342\236\234\302\240"
+
+const char list_current[] = OSD_ASS_0 ARROW_SP OSD_ASS_1;
+const char list_normal[] = OSD_ASS_0 "{\\alpha&HFF}" ARROW_SP "{\\r}" OSD_ASS_1;
+
+static int edit_filters(struct MPContext *mpctx, struct mp_log *log,
+ enum stream_type mediatype,
+ const char *cmd, const char *arg);
+static int set_filters(struct MPContext *mpctx, enum stream_type mediatype,
+ struct m_obj_settings *new_chain);
+
+static bool is_property_set(int action, void *val);
+
+static void hook_remove(struct MPContext *mpctx, struct hook_handler *h)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ if (cmd->hooks[n] == h) {
+ talloc_free(cmd->hooks[n]);
+ MP_TARRAY_REMOVE_AT(cmd->hooks, cmd->num_hooks, n);
+ return;
+ }
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+bool mp_hook_test_completion(struct MPContext *mpctx, char *type)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (h->active && strcmp(h->type, type) == 0) {
+ if (!mp_client_id_exists(mpctx, h->client_id)) {
+ MP_WARN(mpctx, "client removed during hook handling\n");
+ hook_remove(mpctx, h);
+ break;
+ }
+ return false;
+ }
+ }
+ return true;
+}
+
+static int invoke_hook_handler(struct MPContext *mpctx, struct hook_handler *h)
+{
+ MP_VERBOSE(mpctx, "Running hook: %s/%s\n", h->client, h->type);
+ h->active = true;
+
+ uint64_t reply_id = 0;
+ mpv_event_hook *m = talloc_ptrtype(NULL, m);
+ *m = (mpv_event_hook){
+ .name = talloc_strdup(m, h->type),
+ .id = h->seq,
+ },
+ reply_id = h->user_id;
+ char *name = mp_tprintf(22, "@%"PRIi64, h->client_id);
+ int r = mp_client_send_event(mpctx, name, reply_id, MPV_EVENT_HOOK, m);
+ if (r < 0) {
+ MP_WARN(mpctx, "Sending hook command failed. Removing hook.\n");
+ hook_remove(mpctx, h);
+ mp_wakeup_core(mpctx); // repeat next iteration to finish
+ }
+ return r;
+}
+
+static int run_next_hook_handler(struct MPContext *mpctx, char *type, int index)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ for (int n = index; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (strcmp(h->type, type) == 0)
+ return invoke_hook_handler(mpctx, h);
+ }
+
+ mp_wakeup_core(mpctx); // finished hook
+ return 0;
+}
+
+// Start processing script/client API hooks. This is asynchronous, and the
+// caller needs to use mp_hook_test_completion() to check whether they're done.
+void mp_hook_start(struct MPContext *mpctx, char *type)
+{
+ while (run_next_hook_handler(mpctx, type, 0) < 0) {
+ // We can repeat this until all broken clients have been removed, and
+ // hook processing is successfully started.
+ }
+}
+
+int mp_hook_continue(struct MPContext *mpctx, int64_t client_id, uint64_t id)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ for (int n = 0; n < cmd->num_hooks; n++) {
+ struct hook_handler *h = cmd->hooks[n];
+ if (h->client_id == client_id && h->seq == id) {
+ if (!h->active)
+ break;
+ h->active = false;
+ return run_next_hook_handler(mpctx, h->type, n + 1);
+ }
+ }
+
+ MP_ERR(mpctx, "invalid hook API usage\n");
+ return MPV_ERROR_INVALID_PARAMETER;
+}
+
+static int compare_hook(const void *pa, const void *pb)
+{
+ struct hook_handler **h1 = (void *)pa;
+ struct hook_handler **h2 = (void *)pb;
+ if ((*h1)->priority != (*h2)->priority)
+ return (*h1)->priority - (*h2)->priority;
+ return (*h1)->seq - (*h2)->seq;
+}
+
+void mp_hook_add(struct MPContext *mpctx, char *client, int64_t client_id,
+ const char *name, uint64_t user_id, int pri)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct hook_handler *h = talloc_ptrtype(cmd, h);
+ int64_t seq = ++cmd->hook_seq;
+ *h = (struct hook_handler){
+ .client = talloc_strdup(h, client),
+ .client_id = client_id,
+ .type = talloc_strdup(h, name),
+ .user_id = user_id,
+ .priority = pri,
+ .seq = seq,
+ };
+ MP_TARRAY_APPEND(cmd, cmd->hooks, cmd->num_hooks, h);
+ qsort(cmd->hooks, cmd->num_hooks, sizeof(cmd->hooks[0]), compare_hook);
+}
+
+// Call before a seek, in order to allow revert-seek to undo the seek.
+void mark_seek(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ double now = mp_time_sec();
+ if (now > cmd->last_seek_time + 2.0 || cmd->last_seek_pts == MP_NOPTS_VALUE)
+ cmd->last_seek_pts = get_current_time(mpctx);
+ cmd->last_seek_time = now;
+}
+
+static char *skip_n_lines(char *text, int lines)
+{
+ while (text && lines > 0) {
+ char *next = strchr(text, '\n');
+ text = next ? next + 1 : NULL;
+ lines--;
+ }
+ return text;
+}
+
+static int count_lines(char *text)
+{
+ int count = 0;
+ while (text) {
+ char *next = strchr(text, '\n');
+ if (!next || (next[0] == '\n' && !next[1]))
+ break;
+ text = next + 1;
+ count++;
+ }
+ return count;
+}
+
+// Given a huge string separated by new lines, attempts to cut off text above
+// the current line to keep the line visible, and below to keep rendering
+// performance up. pos gives the current line (0 for the first line).
+// "text" might be returned as is, or it can be freed and a new allocation is
+// returned.
+// This is only a heuristic - we can't deal with line breaking.
+static char *cut_osd_list(struct MPContext *mpctx, char *text, int pos)
+{
+ int screen_h, font_h;
+ osd_get_text_size(mpctx->osd, &screen_h, &font_h);
+ int max_lines = screen_h / MPMAX(font_h, 1) - 1;
+
+ if (!text || max_lines < 5)
+ return text;
+
+ int count = count_lines(text);
+ if (count <= max_lines)
+ return text;
+
+ char *new = talloc_strdup(NULL, "");
+
+ int start = MPMAX(pos - max_lines / 2, 0);
+ if (start == 1)
+ start = 0; // avoid weird transition when pad_h becomes visible
+ int pad_h = start > 0;
+
+ int space = max_lines - pad_h - 1;
+ int pad_t = count - start > space;
+ if (!pad_t)
+ start = count - space;
+
+ if (pad_h) {
+ new = talloc_asprintf_append_buffer(new, "\342\206\221 (%d hidden items)\n",
+ start);
+ }
+
+ char *head = skip_n_lines(text, start);
+ if (!head) {
+ talloc_free(new);
+ return text;
+ }
+
+ int lines_shown = max_lines - pad_h - pad_t;
+ char *tail = skip_n_lines(head, lines_shown);
+ new = talloc_asprintf_append_buffer(new, "%.*s",
+ (int)(tail ? tail - head : strlen(head)), head);
+ if (pad_t) {
+ new = talloc_asprintf_append_buffer(new, "\342\206\223 (%d hidden items)\n",
+ count - start - lines_shown + 1);
+ }
+
+ talloc_free(text);
+ return new;
+}
+
+static char *format_delay(double time)
+{
+ return talloc_asprintf(NULL, "%d ms", (int)lrint(time * 1000));
+}
+
+// Property-option bridge. (Maps the property to the option with the same name.)
+static int mp_property_generic_option(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct m_config_option *opt =
+ m_config_get_co(mpctx->mconfig, bstr0(prop->name));
+
+ if (!opt)
+ return M_PROPERTY_UNKNOWN;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = *(opt->opt);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ if (!opt->data)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ m_option_copy(opt->opt, arg, opt->data);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ if (m_config_set_option_raw(mpctx->mconfig, opt, arg, 0) < 0)
+ return M_PROPERTY_ERROR;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Playback speed (RW)
+static int mp_property_playback_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ double speed = mpctx->opts->playback_speed;
+ *(char **)arg = talloc_asprintf(NULL, "%.2f", speed);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_av_speed_correction(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ char *type = prop->priv;
+ double val = 0;
+ switch (type[0]) {
+ case 'a': val = mpctx->speed_factor_a; break;
+ case 'v': val = mpctx->speed_factor_v; break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_asprintf(NULL, "%+.3g%%", (val - 1) * 100);
+ return M_PROPERTY_OK;
+ }
+
+ return m_property_double_ro(action, arg, val);
+}
+
+static int mp_property_display_sync_active(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, mpctx->display_sync_active);
+}
+
+static int mp_property_pid(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // 32 bit on linux/windows - which C99 `int' is not guaranteed to hold
+ return m_property_int64_ro(action, arg, mp_getpid());
+}
+
+/// filename with path (RO)
+static int mp_property_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, mpctx->filename);
+}
+
+static int mp_property_filename(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ char *filename = talloc_strdup(NULL, mpctx->filename);
+ if (mp_is_url(bstr0(filename)))
+ mp_url_unescape_inplace(filename);
+ char *f = (char *)mp_basename(filename);
+ if (!f[0])
+ f = filename;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *ka = arg;
+ if (strcmp(ka->key, "no-ext") == 0) {
+ action = ka->action;
+ arg = ka->arg;
+ bstr root;
+ if (mp_splitext(f, &root))
+ f = bstrto0(filename, root);
+ }
+ }
+ int r = m_property_strdup_ro(action, arg, f);
+ talloc_free(filename);
+ return r;
+}
+
+static int mp_property_stream_open_filename(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->stream_open_filename || !mpctx->playing)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_SET: {
+ if (mpctx->demuxer)
+ return M_PROPERTY_ERROR;
+ mpctx->stream_open_filename =
+ talloc_strdup(mpctx->stream_open_filename, *(char **)arg);
+ mp_notify_property(mpctx, prop->name);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ case M_PROPERTY_GET:
+ return m_property_strdup_ro(action, arg, mpctx->stream_open_filename);
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_file_size(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int64_t size = mpctx->demuxer->filesize;
+ if (size < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = format_file_size(size);
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, size);
+}
+
+static const char *find_non_filename_media_title(MPContext *mpctx)
+{
+ const char *name = mpctx->opts->media_title;
+ if (name && name[0])
+ return name;
+ if (mpctx->demuxer) {
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "service_name");
+ if (name && name[0])
+ return name;
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "title");
+ if (name && name[0])
+ return name;
+ name = mp_tags_get_str(mpctx->demuxer->metadata, "icy-title");
+ if (name && name[0])
+ return name;
+ }
+ if (mpctx->playing && mpctx->playing->title)
+ return mpctx->playing->title;
+ return NULL;
+}
+
+static int mp_property_media_title(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const char *name = find_non_filename_media_title(mpctx);
+ if (name && name[0])
+ return m_property_strdup_ro(action, arg, name);
+ return mp_property_filename(ctx, prop, action, arg);
+}
+
+static int mp_property_stream_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer || !mpctx->demuxer->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, mpctx->demuxer->filename);
+}
+
+/// Demuxer name (RO)
+static int mp_property_demuxer(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(action, arg, demuxer->desc->name);
+}
+
+static int mp_property_file_format(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ const char *name = demuxer->filetype ? demuxer->filetype : demuxer->desc->name;
+ return m_property_strdup_ro(action, arg, name);
+}
+
+static int mp_property_stream_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer || demuxer->filepos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(action, arg, demuxer->filepos);
+}
+
+/// Stream end offset (RO)
+static int mp_property_stream_end(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_file_size(ctx, prop, action, arg);
+}
+
+// Does some magic to handle "<name>/full" as time formatted with milliseconds.
+// Assumes prop is the type of the actual property.
+static int property_time(int action, void *arg, double time)
+{
+ if (time == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ const struct m_option time_type = {.type = CONF_TYPE_TIME};
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(double *)arg = time;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = time_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+
+ if (strcmp(ka->key, "full") != 0)
+ return M_PROPERTY_UNKNOWN;
+
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ *(double *)ka->arg = time;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)ka->arg = mp_format_time(time, true);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = time_type;
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_duration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double len = get_time_length(mpctx);
+
+ if (len < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, len);
+}
+
+static int mp_property_avsync(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->ao_chain || !mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ if (action == M_PROPERTY_PRINT) {
+ // Truncate anything < 1e-4 to avoid switching to scientific notation
+ if (fabs(mpctx->last_av_difference) < 1e-4) {
+ *(char **)arg = talloc_strdup(NULL, "0");
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "%+.2g", mpctx->last_av_difference);
+ }
+ return M_PROPERTY_OK;
+ }
+ return m_property_double_ro(action, arg, mpctx->last_av_difference);
+}
+
+static int mp_property_total_avsync_change(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->ao_chain || !mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ if (mpctx->total_avsync_change == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, mpctx->total_avsync_change);
+}
+
+static int mp_property_frame_drop_dec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_decoder_wrapper *dec = mpctx->vo_chain && mpctx->vo_chain->track
+ ? mpctx->vo_chain->track->dec : NULL;
+ if (!dec)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg,
+ mp_decoder_wrapper_get_frames_dropped(dec));
+}
+
+static int mp_property_mistimed_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain || !mpctx->display_sync_active)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, mpctx->mistimed_frames_total);
+}
+
+static int mp_property_vsync_ratio(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain || !mpctx->display_sync_active)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int vsyncs = 0, frames = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ int vsync = mpctx->past_frames[n].num_vsyncs;
+ if (vsync < 0)
+ break;
+ vsyncs += vsync;
+ frames += 1;
+ }
+
+ if (!frames)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, vsyncs / (double)frames);
+}
+
+static int mp_property_frame_drop_vo(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, vo_get_drop_count(mpctx->video_out));
+}
+
+static int mp_property_vo_delayed_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, vo_get_delayed_count(mpctx->video_out));
+}
+
+/// Current position in percent (RW)
+static int mp_property_percent_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ double pos = *(double *)arg;
+ queue_seek(mpctx, MPSEEK_FACTOR, pos / 100.0, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ double pos = get_current_pos_ratio(mpctx, false) * 100.0;
+ if (pos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(double *)arg = pos;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_DOUBLE,
+ .min = 0,
+ .max = 100,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ int pos = get_percent_pos(mpctx);
+ if (pos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = talloc_asprintf(NULL, "%d", pos);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_time_start(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // minor backwards-compat.
+ return property_time(action, arg, 0);
+}
+
+/// Current position in seconds (RW)
+static int mp_property_time_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ return property_time(action, arg, get_current_time(mpctx));
+}
+
+/// Current audio pts in seconds (R)
+static int mp_property_audio_pts(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized || mpctx->audio_status < STATUS_PLAYING ||
+ mpctx->audio_status >= STATUS_EOF)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, playing_audio_pts(mpctx));
+}
+
+static bool time_remaining(MPContext *mpctx, double *remaining)
+{
+ double len = get_time_length(mpctx);
+ double playback = get_playback_time(mpctx);
+
+ if (playback == MP_NOPTS_VALUE || len <= 0)
+ return false;
+
+ *remaining = len - playback;
+
+ return len >= 0;
+}
+
+static int mp_property_remaining(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double remaining;
+ if (!time_remaining(ctx, &remaining))
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(action, arg, remaining);
+}
+
+static int mp_property_playtime_remaining(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double remaining;
+ if (!time_remaining(mpctx, &remaining))
+ return M_PROPERTY_UNAVAILABLE;
+
+ double speed = mpctx->video_speed;
+ return property_time(action, arg, remaining / speed);
+}
+
+static int mp_property_playback_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, MPSEEK_DEFAULT, 0);
+ return M_PROPERTY_OK;
+ }
+ return property_time(action, arg, get_playback_time(mpctx));
+}
+
+/// Current chapter (RW)
+static int mp_property_chapter(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int chapter = get_current_chapter(mpctx);
+ int num = get_chapter_count(mpctx);
+ if (chapter < -1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *) arg = chapter;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_INT,
+ .min = -1,
+ .max = num - 1,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ *(char **) arg = chapter_display_name(mpctx, chapter);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SWITCH:
+ case M_PROPERTY_SET: ;
+ mark_seek(mpctx);
+ int step_all;
+ if (action == M_PROPERTY_SWITCH) {
+ struct m_property_switch_arg *sarg = arg;
+ step_all = lrint(sarg->inc);
+ // Check threshold for relative backward seeks
+ if (mpctx->opts->chapter_seek_threshold >= 0 && step_all < 0) {
+ double current_chapter_start =
+ chapter_start_time(mpctx, chapter);
+ // If we are far enough into a chapter, seek back to the
+ // beginning of current chapter instead of previous one
+ if (current_chapter_start != MP_NOPTS_VALUE &&
+ get_current_time(mpctx) - current_chapter_start >
+ mpctx->opts->chapter_seek_threshold)
+ {
+ step_all++;
+ }
+ }
+ } else // Absolute set
+ step_all = *(int *)arg - chapter;
+ chapter += step_all;
+ if (chapter < 0) // avoid using -1 if first chapter starts at 0
+ chapter = (chapter_start_time(mpctx, 0) <= 0) ? 0 : -1;
+ if (chapter >= num && step_all > 0) {
+ if (mpctx->opts->keep_open) {
+ seek_to_last_frame(mpctx);
+ } else {
+ // semi-broken file; ignore for user convenience
+ if (action == M_PROPERTY_SWITCH && num < 2)
+ return M_PROPERTY_UNAVAILABLE;
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mp_wakeup_core(mpctx);
+ }
+ } else {
+ double pts = chapter_start_time(mpctx, chapter);
+ if (pts != MP_NOPTS_VALUE) {
+ queue_seek(mpctx, MPSEEK_CHAPTER, pts, MPSEEK_DEFAULT, 0);
+ mpctx->last_chapter_seek = chapter;
+ mpctx->last_chapter_flag = true;
+ }
+ }
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_chapter_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ char *name = chapter_name(mpctx, item);
+ double time = chapter_start_time(mpctx, item);
+ struct m_sub_property props[] = {
+ {"title", SUB_PROP_STR(name)},
+ {"time", {.type = CONF_TYPE_TIME}, {.time = time}},
+ {0}
+ };
+
+ int r = m_property_read_sub(props, action, arg);
+ return r;
+}
+
+static int parse_node_chapters(struct MPContext *mpctx,
+ struct mpv_node *given_chapters)
+{
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (given_chapters->format != MPV_FORMAT_NODE_ARRAY)
+ return M_PROPERTY_ERROR;
+
+ double len = get_time_length(mpctx);
+
+ talloc_free(mpctx->chapters);
+ mpctx->num_chapters = 0;
+ mpctx->chapters = talloc_array(NULL, struct demux_chapter, 0);
+
+ for (int n = 0; n < given_chapters->u.list->num; n++) {
+ struct mpv_node *chapter_data = &given_chapters->u.list->values[n];
+
+ if (chapter_data->format != MPV_FORMAT_NODE_MAP)
+ continue;
+
+ mpv_node_list *chapter_data_elements = chapter_data->u.list;
+
+ double time = -1;
+ char *title = 0;
+
+ for (int e = 0; e < chapter_data_elements->num; e++) {
+ struct mpv_node *chapter_data_element =
+ &chapter_data_elements->values[e];
+ char *key = chapter_data_elements->keys[e];
+ switch (chapter_data_element->format) {
+ case MPV_FORMAT_INT64:
+ if (strcmp(key, "time") == 0)
+ time = (double)chapter_data_element->u.int64;
+ break;
+ case MPV_FORMAT_DOUBLE:
+ if (strcmp(key, "time") == 0)
+ time = chapter_data_element->u.double_;
+ break;
+ case MPV_FORMAT_STRING:
+ if (strcmp(key, "title") == 0)
+ title = chapter_data_element->u.string;
+ break;
+ }
+ }
+
+ if (time >= 0 && time < len) {
+ struct demux_chapter new = {
+ .pts = time,
+ .metadata = talloc_zero(mpctx->chapters, struct mp_tags),
+ };
+ if (title)
+ mp_tags_set_str(new.metadata, "title", title);
+ MP_TARRAY_APPEND(NULL, mpctx->chapters, mpctx->num_chapters, new);
+ }
+ }
+
+ mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL);
+ mp_notify_property(mpctx, "chapter-list");
+
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_list_chapters(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int count = get_chapter_count(mpctx);
+ switch (action) {
+ case M_PROPERTY_PRINT: {
+ int cur = mpctx->playback_initialized ? get_current_chapter(mpctx) : -1;
+ char *res = NULL;
+ int n;
+
+ if (count < 1) {
+ res = talloc_asprintf_append(res, "No chapters.");
+ }
+
+ for (n = 0; n < count; n++) {
+ char *name = chapter_display_name(mpctx, n);
+ double t = chapter_start_time(mpctx, n);
+ char* time = mp_format_time(t, false);
+ res = talloc_asprintf_append(res, "%s", time);
+ talloc_free(time);
+ const char *m = n == cur ? list_current : list_normal;
+ res = talloc_asprintf_append(res, " %s%s\n", m, name);
+ talloc_free(name);
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ struct mpv_node *given_chapters = arg;
+ return parse_node_chapters(mpctx, given_chapters);
+ }
+ }
+ return m_property_read_list(action, arg, count, get_chapter_entry, mpctx);
+}
+
+static int mp_property_current_edition(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer || demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, demuxer->edition);
+}
+
+static int mp_property_edition(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ char *name = NULL;
+
+ if (!demuxer)
+ return mp_property_generic_option(mpctx, prop, action, arg);
+
+ int ed = demuxer->edition;
+
+ if (demuxer->num_editions <= 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET_CONSTRICTED_TYPE: {
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_INT,
+ .min = 0,
+ .max = demuxer->num_editions - 1,
+ };
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ if (ed < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ name = mp_tags_get_str(demuxer->editions[ed].metadata, "title");
+ if (name) {
+ *(char **) arg = talloc_strdup(NULL, name);
+ } else {
+ *(char **) arg = talloc_asprintf(NULL, "%d", ed + 1);
+ }
+ return M_PROPERTY_OK;
+ }
+ default:
+ return mp_property_generic_option(mpctx, prop, action, arg);
+ }
+}
+
+static int get_edition_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ struct demuxer *demuxer = mpctx->demuxer;
+ struct demux_edition *ed = &demuxer->editions[item];
+
+ char *title = mp_tags_get_str(ed->metadata, "title");
+
+ struct m_sub_property props[] = {
+ {"id", SUB_PROP_INT(item)},
+ {"title", SUB_PROP_STR(title),
+ .unavailable = !title},
+ {"default", SUB_PROP_BOOL(ed->default_edition)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int property_list_editions(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_PRINT) {
+ char *res = NULL;
+
+ struct demux_edition *editions = demuxer->editions;
+ int num_editions = demuxer->num_editions;
+ int current = demuxer->edition;
+
+ if (!num_editions)
+ res = talloc_asprintf_append(res, "No editions.");
+
+ for (int n = 0; n < num_editions; n++) {
+ struct demux_edition *ed = &editions[n];
+
+ res = talloc_strdup_append(res, n == current ? list_current
+ : list_normal);
+ res = talloc_asprintf_append(res, "%d: ", n);
+ char *title = mp_tags_get_str(ed->metadata, "title");
+ if (!title)
+ title = "unnamed";
+ res = talloc_asprintf_append(res, "'%s'\n", title);
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return m_property_read_list(action, arg, demuxer->num_editions,
+ get_edition_entry, mpctx);
+}
+
+/// Number of chapters in file
+static int mp_property_chapters(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ int count = get_chapter_count(mpctx);
+ return m_property_int_ro(action, arg, count);
+}
+
+static int mp_property_editions(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ if (demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, demuxer->num_editions);
+}
+
+static int get_tag_entry(int item, int action, void *arg, void *ctx)
+{
+ struct mp_tags *tags = ctx;
+
+ struct m_sub_property props[] = {
+ {"key", SUB_PROP_STR(tags->keys[item])},
+ {"value", SUB_PROP_STR(tags->values[item])},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+// tags can be NULL for M_PROPERTY_GET_TYPE. (In all other cases, tags must be
+// provided, even for M_PROPERTY_KEY_ACTION GET_TYPE sub-actions.)
+static int tag_property(int action, void *arg, struct mp_tags *tags)
+{
+ switch (action) {
+ case M_PROPERTY_GET_NODE: // same as GET, because type==mpv_node
+ case M_PROPERTY_GET: {
+ mpv_node_list *list = talloc_zero(NULL, mpv_node_list);
+ mpv_node node = {
+ .format = MPV_FORMAT_NODE_MAP,
+ .u.list = list,
+ };
+ list->num = tags->num_keys;
+ list->values = talloc_array(list, mpv_node, list->num);
+ list->keys = talloc_array(list, char*, list->num);
+ for (int n = 0; n < tags->num_keys; n++) {
+ list->keys[n] = talloc_strdup(list, tags->keys[n]);
+ list->values[n] = (struct mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = talloc_strdup(list, tags->values[n]),
+ };
+ }
+ *(mpv_node*)arg = node;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE: {
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT: {
+ char *res = NULL;
+ for (int n = 0; n < tags->num_keys; n++) {
+ res = talloc_asprintf_append_buffer(res, "%s: %s\n",
+ tags->keys[n], tags->values[n]);
+ }
+ if (!res)
+ res = talloc_strdup(NULL, "(empty)");
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ if (bstr_equals0(key, "list")) {
+ struct m_property_action_arg nka = *ka;
+ nka.key = rem;
+ return m_property_read_list(action, &nka, tags->num_keys,
+ get_tag_entry, tags);
+ }
+ // Direct access without this prefix is allowed for compatibility.
+ bstr k = bstr0(ka->key);
+ bstr_eatstart0(&k, "by-key/");
+ char *meta = mp_tags_get_bstr(tags, k);
+ if (!meta)
+ return M_PROPERTY_UNKNOWN;
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ *(char **)ka->arg = talloc_strdup(NULL, meta);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = (struct m_option){
+ .type = CONF_TYPE_STRING,
+ };
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Demuxer meta data
+static int mp_property_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return tag_property(action, arg, demuxer->metadata);
+}
+
+static int mp_property_filtered_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->filtered_tags)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return tag_property(action, arg, mpctx->filtered_tags);
+}
+
+static int mp_property_chapter_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int chapter = get_current_chapter(mpctx);
+ if (chapter < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return tag_property(action, arg, mpctx->chapters[chapter].metadata);
+}
+
+static int mp_property_filter_metadata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const char *type = prop->priv;
+
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ struct mp_tags *metadata = NULL;
+ struct mp_output_chain *chain = NULL;
+ if (strcmp(type, "vf") == 0) {
+ chain = mpctx->vo_chain ? mpctx->vo_chain->filter : NULL;
+ } else if (strcmp(type, "af") == 0) {
+ chain = mpctx->ao_chain ? mpctx->ao_chain->filter : NULL;
+ }
+ if (!chain)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (ka->action != M_PROPERTY_GET_TYPE) {
+ struct mp_filter_command cmd = {
+ .type = MP_FILTER_COMMAND_GET_META,
+ .res = &metadata,
+ };
+ mp_output_chain_command(chain, mp_tprintf(80, "%.*s", BSTR_P(key)),
+ &cmd);
+
+ if (!metadata)
+ return M_PROPERTY_ERROR;
+ }
+
+ int res;
+ if (strlen(rem)) {
+ struct m_property_action_arg next_ka = *ka;
+ next_ka.key = rem;
+ res = tag_property(M_PROPERTY_KEY_ACTION, &next_ka, metadata);
+ } else {
+ res = tag_property(ka->action, ka->arg, metadata);
+ }
+ talloc_free(metadata);
+ return res;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_core_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !mpctx->playback_active);
+}
+
+static int mp_property_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, mpctx->stop_play == PT_STOP);
+}
+
+static int mp_property_window_id(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ int64_t wid;
+ if (!vo || vo_control(vo, VOCTRL_GET_WINDOW_ID, &wid) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(action, arg, wid);
+}
+
+static int mp_property_eof_reached(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ bool eof = mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF;
+ return m_property_bool_ro(action, arg, eof);
+}
+
+static int mp_property_seeking(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, !mpctx->restart_complete);
+}
+
+static int mp_property_playback_abort(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !mpctx->playing || mpctx->stop_play);
+}
+
+static int mp_property_cache_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ uint64_t val = s.bytes_per_second;
+
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_strdup_append(format_file_size(val), "/s");
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, val);
+}
+
+static int mp_property_demuxer_cache_duration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_duration < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, s.ts_duration);
+}
+
+static int mp_property_demuxer_cache_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_end == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, s.ts_end);
+}
+
+static int mp_property_demuxer_cache_idle(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ return m_property_bool_ro(action, arg, s.idle);
+}
+
+static int mp_property_demuxer_cache_state(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_GET_TYPE) {
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ }
+ if (action != M_PROPERTY_GET)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ struct mpv_node *r = (struct mpv_node *)arg;
+ node_init(r, MPV_FORMAT_NODE_MAP, NULL);
+
+ if (s.ts_end != MP_NOPTS_VALUE)
+ node_map_add_double(r, "cache-end", s.ts_end);
+
+ if (s.ts_reader != MP_NOPTS_VALUE)
+ node_map_add_double(r, "reader-pts", s.ts_reader);
+
+ if (s.ts_duration >= 0)
+ node_map_add_double(r, "cache-duration", s.ts_duration);
+
+ node_map_add_flag(r, "eof", s.eof);
+ node_map_add_flag(r, "underrun", s.underrun);
+ node_map_add_flag(r, "idle", s.idle);
+ node_map_add_int64(r, "total-bytes", s.total_bytes);
+ node_map_add_int64(r, "fw-bytes", s.fw_bytes);
+ if (s.file_cache_bytes >= 0)
+ node_map_add_int64(r, "file-cache-bytes", s.file_cache_bytes);
+ if (s.bytes_per_second > 0)
+ node_map_add_int64(r, "raw-input-rate", s.bytes_per_second);
+ if (s.seeking != MP_NOPTS_VALUE)
+ node_map_add_double(r, "debug-seeking", s.seeking);
+ node_map_add_int64(r, "debug-low-level-seeks", s.low_level_seeks);
+ node_map_add_int64(r, "debug-byte-level-seeks", s.byte_level_seeks);
+ if (s.ts_last != MP_NOPTS_VALUE)
+ node_map_add_double(r, "debug-ts-last", s.ts_last);
+
+ node_map_add_flag(r, "bof-cached", s.bof_cached);
+ node_map_add_flag(r, "eof-cached", s.eof_cached);
+
+ struct mpv_node *ranges =
+ node_map_add(r, "seekable-ranges", MPV_FORMAT_NODE_ARRAY);
+ for (int n = s.num_seek_ranges - 1; n >= 0; n--) {
+ struct demux_seek_range *range = &s.seek_ranges[n];
+ struct mpv_node *sub = node_array_add(ranges, MPV_FORMAT_NODE_MAP);
+ node_map_add_double(sub, "start", range->start);
+ node_map_add_double(sub, "end", range->end);
+ }
+
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_demuxer_start_time(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_double_ro(action, arg, mpctx->demuxer->start_time);
+}
+
+static int mp_property_paused_for_cache(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playback_initialized)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->paused_for_cache);
+}
+
+static int mp_property_cache_buffering(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int state = get_cache_buffering_percentage(mpctx);
+ if (state < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(action, arg, state);
+}
+
+static int mp_property_demuxer_is_network(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_bool_ro(action, arg, mpctx->demuxer->is_network);
+}
+
+
+static int mp_property_clock(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ char outstr[6];
+ time_t t = time(NULL);
+ struct tm *tmp = localtime(&t);
+
+ if ((tmp != NULL) && (strftime(outstr, sizeof(outstr), "%H:%M", tmp) == 5))
+ return m_property_strdup_ro(action, arg, outstr);
+ return M_PROPERTY_UNAVAILABLE;
+}
+
+static int mp_property_seekable(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->demuxer->seekable);
+}
+
+static int mp_property_partially_seekable(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_bool_ro(action, arg, mpctx->demuxer->partially_seekable);
+}
+
+static int mp_property_mixer_active(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg, !!mpctx->ao);
+}
+
+/// Volume (RW)
+static int mp_property_volume(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+
+ switch (action) {
+ case M_PROPERTY_GET_CONSTRICTED_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_FLOAT,
+ .min = 0,
+ .max = opts->softvol_max,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "%i", (int)opts->softvol_volume);
+ return M_PROPERTY_OK;
+ }
+
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_ao_volume(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct ao *ao = mpctx->ao;
+ if (!ao)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ float vol = *(float *)arg;
+ if (ao_control(ao, AOCONTROL_SET_VOLUME, &vol) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ if (ao_control(ao, AOCONTROL_GET_VOLUME, arg) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_FLOAT,
+ .min = 0,
+ .max = 100,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ float vol = 0;
+ if (ao_control(ao, AOCONTROL_GET_VOLUME, &vol) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = talloc_asprintf(NULL, "%.f", vol);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_ao_mute(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct ao *ao = mpctx->ao;
+ if (!ao)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ bool value = *(int *)arg;
+ if (ao_control(ao, AOCONTROL_SET_MUTE, &value) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ bool value = false;
+ if (ao_control(ao, AOCONTROL_GET_MUTE, &value) != CONTROL_OK)
+ return M_PROPERTY_UNAVAILABLE;
+ *(int *)arg = value;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_BOOL};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_device_entry(int item, int action, void *arg, void *ctx)
+{
+ struct ao_device_list *list = ctx;
+ struct ao_device_desc *entry = &list->devices[item];
+
+ struct m_sub_property props[] = {
+ {"name", SUB_PROP_STR(entry->name)},
+ {"description", SUB_PROP_STR(entry->desc)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static void create_hotplug(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ if (!cmd->hotplug) {
+ cmd->hotplug = ao_hotplug_create(mpctx->global, mp_wakeup_core_cb,
+ mpctx);
+ }
+}
+
+static int mp_property_audio_device(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (action == M_PROPERTY_PRINT) {
+ create_hotplug(mpctx);
+
+ char *name = NULL;
+ if (mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &name) < 1)
+ name = NULL;
+
+ struct ao_device_list *list = ao_hotplug_get_device_list(cmd->hotplug, mpctx->ao);
+ for (int n = 0; n < list->num_devices; n++) {
+ struct ao_device_desc *dev = &list->devices[n];
+ if (dev->name && name && strcmp(dev->name, name) == 0) {
+ *(char **)arg = talloc_strdup(NULL, dev->desc ? dev->desc : "?");
+ talloc_free(name);
+ return M_PROPERTY_OK;
+ }
+ }
+
+ talloc_free(name);
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_audio_devices(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ create_hotplug(mpctx);
+
+ struct ao_device_list *list = ao_hotplug_get_device_list(cmd->hotplug, mpctx->ao);
+ return m_property_read_list(action, arg, list->num_devices,
+ get_device_entry, list);
+}
+
+static int mp_property_ao(void *ctx, struct m_property *p, int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_strdup_ro(action, arg,
+ mpctx->ao ? ao_get_name(mpctx->ao) : NULL);
+}
+
+/// Audio delay (RW)
+static int mp_property_audio_delay(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = format_delay(mpctx->opts->audio_delay);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Audio codec tag (RO)
+static int mp_property_audio_codec_name(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ const char *c = track && track->stream ? track->stream->codec->codec : NULL;
+ return m_property_strdup_ro(action, arg, c);
+}
+
+/// Audio codec name (RO)
+static int mp_property_audio_codec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_AUDIO];
+ char desc[256] = "";
+ if (track && track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
+ return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
+}
+
+static int property_audiofmt(struct mp_aframe *fmt, int action, void *arg)
+{
+ if (!fmt || !mp_aframe_config_is_valid(fmt))
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct mp_chmap chmap = {0};
+ mp_aframe_get_chmap(fmt, &chmap);
+
+ struct m_sub_property props[] = {
+ {"samplerate", SUB_PROP_INT(mp_aframe_get_rate(fmt))},
+ {"channel-count", SUB_PROP_INT(chmap.num)},
+ {"channels", SUB_PROP_STR(mp_chmap_to_str(&chmap))},
+ {"hr-channels", SUB_PROP_STR(mp_chmap_to_str_hr(&chmap))},
+ {"format", SUB_PROP_STR(af_fmt_to_str(mp_aframe_get_format(fmt)))},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_audio_params(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return property_audiofmt(mpctx->ao_chain ?
+ mpctx->ao_chain->filter->input_aformat : NULL, action, arg);
+}
+
+static int mp_property_audio_out_params(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_aframe *frame = NULL;
+ if (mpctx->ao) {
+ frame = mp_aframe_create();
+ int samplerate;
+ int format;
+ struct mp_chmap channels;
+ ao_get_format(mpctx->ao, &samplerate, &format, &channels);
+ mp_aframe_set_rate(frame, samplerate);
+ mp_aframe_set_format(frame, format);
+ mp_aframe_set_chmap(frame, &channels);
+ }
+ int r = property_audiofmt(frame, action, arg);
+ talloc_free(frame);
+ return r;
+}
+
+static struct track* track_next(struct MPContext *mpctx, enum stream_type type,
+ int direction, struct track *track)
+{
+ assert(direction == -1 || direction == +1);
+ struct track *prev = NULL, *next = NULL;
+ bool seen = track == NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *cur = mpctx->tracks[n];
+ if (cur->type == type) {
+ if (cur == track) {
+ seen = true;
+ } else if (!cur->selected) {
+ if (seen && !next) {
+ next = cur;
+ }
+ if (!seen || !track) {
+ prev = cur;
+ }
+ }
+ }
+ }
+ return direction > 0 ? next : prev;
+}
+
+static int property_switch_track(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ const int *def = prop->priv;
+ int order = def[0];
+ enum stream_type type = def[1];
+
+ struct track *track = mpctx->current_track[order][type];
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ if (mpctx->playback_initialized) {
+ *(int *)arg = track ? track->user_tid : -2;
+ } else {
+ *(int *)arg = mpctx->opts->stream_id[order][type];
+ }
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ if (track) {
+ char *lang = track->lang;
+ if (!lang && type != STREAM_VIDEO) {
+ lang = "unknown";
+ } else if (!lang) {
+ lang = "";
+ }
+
+ if (track->title) {
+ *(char **)arg = talloc_asprintf(NULL, "(%d) %s (\"%s\")",
+ track->user_tid, lang, track->title);
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "(%d) %s",
+ track->user_tid, lang);
+ }
+ } else {
+ const char *msg = "no";
+ if (!mpctx->playback_initialized &&
+ mpctx->opts->stream_id[order][type] == -1)
+ msg = "auto";
+ *(char **) arg = talloc_strdup(NULL, msg);
+ }
+ return M_PROPERTY_OK;
+
+ case M_PROPERTY_SWITCH: {
+ if (mpctx->playback_initialized) {
+ struct m_property_switch_arg *sarg = arg;
+ do {
+ track = track_next(mpctx, type, sarg->inc >= 0 ? +1 : -1, track);
+ mp_switch_track_n(mpctx, order, type, track, FLAG_MARK_SELECTION);
+ } while (mpctx->current_track[order][type] != track);
+ print_track_list(mpctx, "Track switched:");
+ } else {
+ // Simply cycle between "no" and "auto". It's possible that this does
+ // not always do what the user means, but keep the complexity low.
+ mark_track_selection(mpctx, order, type,
+ mpctx->opts->stream_id[order][type] == -1 ? -2 : -1);
+ }
+ return M_PROPERTY_OK;
+ }
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int track_channels(struct track *track)
+{
+ return track->stream ? track->stream->codec->channels.num : 0;
+}
+
+static int get_track_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ struct track *track = mpctx->tracks[item];
+
+ struct mp_codec_params p =
+ track->stream ? *track->stream->codec : (struct mp_codec_params){0};
+
+ char decoder_desc[256] = {0};
+ if (track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, decoder_desc, sizeof(decoder_desc));
+
+ bool has_rg = track->stream && track->stream->codec->replaygain_data;
+ struct replaygain_data rg = has_rg ? *track->stream->codec->replaygain_data
+ : (struct replaygain_data){0};
+
+ double par = 0.0;
+ if (p.par_h)
+ par = p.par_w / (double) p.par_h;
+
+ int order = -1;
+ if (track->selected) {
+ for (int i = 0; i < num_ptracks[track->type]; i++) {
+ if (mpctx->current_track[i][track->type] == track) {
+ order = i;
+ break;
+ }
+ }
+ }
+
+ bool has_crop = mp_rect_w(p.crop) > 0 && mp_rect_h(p.crop) > 0;
+ struct m_sub_property props[] = {
+ {"id", SUB_PROP_INT(track->user_tid)},
+ {"type", SUB_PROP_STR(stream_type_name(track->type)),
+ .unavailable = !stream_type_name(track->type)},
+ {"src-id", SUB_PROP_INT(track->demuxer_id),
+ .unavailable = track->demuxer_id == -1},
+ {"title", SUB_PROP_STR(track->title),
+ .unavailable = !track->title},
+ {"lang", SUB_PROP_STR(track->lang),
+ .unavailable = !track->lang},
+ {"audio-channels", SUB_PROP_INT(track_channels(track)),
+ .unavailable = track_channels(track) <= 0},
+ {"image", SUB_PROP_BOOL(track->image)},
+ {"albumart", SUB_PROP_BOOL(track->attached_picture)},
+ {"default", SUB_PROP_BOOL(track->default_track)},
+ {"forced", SUB_PROP_BOOL(track->forced_track)},
+ {"dependent", SUB_PROP_BOOL(track->dependent_track)},
+ {"visual-impaired", SUB_PROP_BOOL(track->visual_impaired_track)},
+ {"hearing-impaired", SUB_PROP_BOOL(track->hearing_impaired_track)},
+ {"external", SUB_PROP_BOOL(track->is_external)},
+ {"selected", SUB_PROP_BOOL(track->selected)},
+ {"main-selection", SUB_PROP_INT(order), .unavailable = order < 0},
+ {"external-filename", SUB_PROP_STR(track->external_filename),
+ .unavailable = !track->external_filename},
+ {"ff-index", SUB_PROP_INT(track->ff_index)},
+ {"hls-bitrate", SUB_PROP_INT(track->hls_bitrate),
+ .unavailable = !track->hls_bitrate},
+ {"program-id", SUB_PROP_INT(track->program_id),
+ .unavailable = track->program_id < 0},
+ {"decoder-desc", SUB_PROP_STR(decoder_desc),
+ .unavailable = !decoder_desc[0]},
+ {"codec", SUB_PROP_STR(p.codec),
+ .unavailable = !p.codec},
+ {"demux-w", SUB_PROP_INT(p.disp_w), .unavailable = !p.disp_w},
+ {"demux-h", SUB_PROP_INT(p.disp_h), .unavailable = !p.disp_h},
+ {"demux-crop-x",SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
+ {"demux-crop-y",SUB_PROP_INT(p.crop.y0), .unavailable = !has_crop},
+ {"demux-crop-w",SUB_PROP_INT(mp_rect_w(p.crop)), .unavailable = !has_crop},
+ {"demux-crop-h",SUB_PROP_INT(mp_rect_h(p.crop)), .unavailable = !has_crop},
+ {"demux-channel-count", SUB_PROP_INT(p.channels.num),
+ .unavailable = !p.channels.num},
+ {"demux-channels", SUB_PROP_STR(mp_chmap_to_str(&p.channels)),
+ .unavailable = !p.channels.num},
+ {"demux-samplerate", SUB_PROP_INT(p.samplerate),
+ .unavailable = !p.samplerate},
+ {"demux-fps", SUB_PROP_DOUBLE(p.fps), .unavailable = p.fps <= 0},
+ {"demux-bitrate", SUB_PROP_INT(p.bitrate), .unavailable = p.bitrate <= 0},
+ {"demux-rotation", SUB_PROP_INT(p.rotate), .unavailable = p.rotate <= 0},
+ {"demux-par", SUB_PROP_DOUBLE(par), .unavailable = par <= 0},
+ {"replaygain-track-peak", SUB_PROP_FLOAT(rg.track_peak),
+ .unavailable = !has_rg},
+ {"replaygain-track-gain", SUB_PROP_FLOAT(rg.track_gain),
+ .unavailable = !has_rg},
+ {"replaygain-album-peak", SUB_PROP_FLOAT(rg.album_peak),
+ .unavailable = !has_rg},
+ {"replaygain-album-gain", SUB_PROP_FLOAT(rg.album_gain),
+ .unavailable = !has_rg},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static const char *track_type_name(enum stream_type t)
+{
+ switch (t) {
+ case STREAM_VIDEO: return "Video";
+ case STREAM_AUDIO: return "Audio";
+ case STREAM_SUB: return "Sub";
+ }
+ return NULL;
+}
+
+static int property_list_tracks(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ char *res = NULL;
+
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+
+ res = talloc_asprintf_append(res, "%s: ",
+ track_type_name(track->type));
+ res = talloc_strdup_append(res,
+ track->selected ? list_current : list_normal);
+ res = talloc_asprintf_append(res, "(%d) ", track->user_tid);
+ if (track->title)
+ res = talloc_asprintf_append(res, "'%s' ", track->title);
+ if (track->lang)
+ res = talloc_asprintf_append(res, "(%s) ", track->lang);
+ if (track->is_external)
+ res = talloc_asprintf_append(res, "(external) ");
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (demuxer && demuxer->num_editions > 1)
+ res = talloc_asprintf_append(res, "\nEdition: %d of %d\n",
+ demuxer->edition + 1,
+ demuxer->num_editions);
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return m_property_read_list(action, arg, mpctx->num_tracks,
+ get_track_entry, mpctx);
+}
+
+static int property_current_tracks(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ if (action != M_PROPERTY_KEY_ACTION)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int type = -1;
+ int order = 0;
+
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+
+ if (bstr_equals0(key, "video")) {
+ type = STREAM_VIDEO;
+ } else if (bstr_equals0(key, "audio")) {
+ type = STREAM_AUDIO;
+ } else if (bstr_equals0(key, "sub")) {
+ type = STREAM_SUB;
+ } else if (bstr_equals0(key, "sub2")) {
+ type = STREAM_SUB;
+ order = 1;
+ }
+
+ if (type < 0)
+ return M_PROPERTY_UNKNOWN;
+
+ struct track *t = mpctx->current_track[order][type];
+
+ if (!t && mpctx->lavfi) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (mpctx->tracks[n]->type == type && mpctx->tracks[n]->selected) {
+ t = mpctx->tracks[n];
+ break;
+ }
+ }
+ }
+
+ if (!t)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int index = -1;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (mpctx->tracks[n] == t) {
+ index = n;
+ break;
+ }
+ }
+ assert(index >= 0);
+
+ char *name = mp_tprintf(80, "track-list/%d/%s", index, rem);
+ return mp_property_do(name, ka->action, ka->arg, ctx);
+}
+
+static int mp_property_hwdec_current(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_decoder_wrapper *dec = track ? track->dec : NULL;
+
+ if (!dec)
+ return M_PROPERTY_UNAVAILABLE;
+
+ char *current = NULL;
+ mp_decoder_wrapper_control(dec, VDCTRL_GET_HWDEC, &current);
+ if (!current || !current[0])
+ current = "no";
+ return m_property_strdup_ro(action, arg, current);
+}
+
+static int mp_property_hwdec_interop(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out || !mpctx->video_out->hwdec_devs)
+ return M_PROPERTY_UNAVAILABLE;
+
+ char *names = hwdec_devices_get_names(mpctx->video_out->hwdec_devs);
+ int res = m_property_strdup_ro(action, arg, names);
+ talloc_free(names);
+ return res;
+}
+
+static int get_frame_count(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return -1;
+ if (!mpctx->vo_chain)
+ return -1;
+ double len = get_time_length(mpctx);
+ double fps = mpctx->vo_chain->filter->container_fps;
+ if (len < 0 || fps <= 0)
+ return 0;
+
+ return len * fps;
+}
+
+static int mp_property_frame_number(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int frames = get_frame_count(mpctx);
+ if (frames < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg,
+ lrint(get_current_pos_ratio(mpctx, false) * frames));
+}
+
+static int mp_property_frame_count(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int frames = get_frame_count(mpctx);
+ if (frames < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_int_ro(action, arg, frames);
+}
+
+/// Video codec tag (RO)
+static int mp_property_video_format(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ const char *c = track && track->stream ? track->stream->codec->codec : NULL;
+ return m_property_strdup_ro(action, arg, c);
+}
+
+/// Video codec name (RO)
+static int mp_property_video_codec(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ char desc[256] = "";
+ if (track && track->dec)
+ mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
+ return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
+}
+
+static const char *get_aspect_ratio_name(double ratio)
+{
+ // Depending on cropping/mastering exact ratio may differ.
+#define RATIO_THRESH 0.025
+#define RATIO_CASE(ref, name) \
+ if (fabs(ratio - (ref)) < RATIO_THRESH) \
+ return name; \
+
+ // https://en.wikipedia.org/wiki/Aspect_ratio_(image)
+ RATIO_CASE(9.0 / 16.0, "Vertical")
+ RATIO_CASE(1.0, "Square");
+ RATIO_CASE(19.0 / 16.0, "Movietone Ratio");
+ RATIO_CASE(5.0 / 4.0, "5:4");
+ RATIO_CASE(4.0 / 3.0, "4:3");
+ RATIO_CASE(11.0 / 8.0, "Academy Ratio");
+ RATIO_CASE(1.43, "IMAX Ratio");
+ RATIO_CASE(3.0 / 2.0, "VistaVision Ratio");
+ RATIO_CASE(16.0 / 10.0, "16:10");
+ RATIO_CASE(5.0 / 3.0, "35mm Widescreen Ratio");
+ RATIO_CASE(16.0 / 9.0, "16:9");
+ RATIO_CASE(7.0 / 4.0, "Early 35mm Widescreen Ratio");
+ RATIO_CASE(1.85, "Academy Flat");
+ RATIO_CASE(256.0 / 135.0, "SMPTE/DCI Ratio");
+ RATIO_CASE(2.0, "Univisium");
+ RATIO_CASE(2.208, "70mm film");
+ RATIO_CASE(2.35, "Scope");
+ RATIO_CASE(2.39, "Panavision");
+ RATIO_CASE(2.55, "Original CinemaScope");
+ RATIO_CASE(2.59, "Full-frame Cinerama");
+ RATIO_CASE(24.0 / 9.0, "Full-frame Super 16mm");
+ RATIO_CASE(2.76, "Ultra Panavision 70");
+ RATIO_CASE(32.0 / 9.0, "32:9");
+ RATIO_CASE(3.6, "Ultra-WideScreen 3.6");
+ RATIO_CASE(4.0, "Polyvision");
+ RATIO_CASE(12.0, "Circle-Vision 360°");
+
+ return NULL;
+
+#undef RATIO_THRESH
+#undef RATIO_CASE
+}
+
+static int property_imgparams(struct mp_image_params p, int action, void *arg)
+{
+ if (!p.imgfmt)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int d_w, d_h;
+ mp_image_params_get_dsize(&p, &d_w, &d_h);
+
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p.imgfmt);
+ int bpp = 0;
+ for (int i = 0; i < desc.num_planes; i++)
+ bpp += desc.bpp[i] >> (desc.xs[i] + desc.ys[i]);
+
+ // Alpha type is not supported by FFmpeg, so MP_ALPHA_AUTO may mean alpha
+ // is of an unknown type, or simply not present. Normalize to AUTO=no alpha.
+ if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (p.alpha != MP_ALPHA_AUTO)) {
+ p.alpha =
+ (desc.flags & MP_IMGFLAG_ALPHA) ? MP_ALPHA_STRAIGHT : MP_ALPHA_AUTO;
+ }
+
+ const struct pl_hdr_metadata *hdr = &p.color.hdr;
+ bool has_cie_y = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_CIE_Y);
+ bool has_hdr10 = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10);
+ bool has_hdr10plus = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10PLUS);
+
+ bool has_crop = mp_rect_w(p.crop) > 0 && mp_rect_h(p.crop) > 0;
+ const char *aspect_name = get_aspect_ratio_name(d_w / (double)d_h);
+ const char *sar_name = get_aspect_ratio_name(p.w / (double)p.h);
+ struct m_sub_property props[] = {
+ {"pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.imgfmt))},
+ {"hw-pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.hw_subfmt)),
+ .unavailable = !p.hw_subfmt},
+ {"average-bpp", SUB_PROP_INT(bpp),
+ .unavailable = !bpp},
+ {"w", SUB_PROP_INT(p.w)},
+ {"h", SUB_PROP_INT(p.h)},
+ {"dw", SUB_PROP_INT(d_w)},
+ {"dh", SUB_PROP_INT(d_h)},
+ {"crop-x", SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
+ {"crop-y", SUB_PROP_INT(p.crop.y0), .unavailable = !has_crop},
+ {"crop-w", SUB_PROP_INT(mp_rect_w(p.crop)), .unavailable = !has_crop},
+ {"crop-h", SUB_PROP_INT(mp_rect_h(p.crop)), .unavailable = !has_crop},
+ {"aspect", SUB_PROP_FLOAT(d_w / (double)d_h)},
+ {"aspect-name", SUB_PROP_STR(aspect_name), .unavailable = !aspect_name},
+ {"par", SUB_PROP_FLOAT(p.p_w / (double)p.p_h)},
+ {"sar", SUB_PROP_FLOAT(p.w / (double)p.h)},
+ {"sar-name", SUB_PROP_STR(sar_name), .unavailable = !sar_name},
+ {"colormatrix",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_names, p.color.space))},
+ {"colorlevels",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_levels_names, p.color.levels))},
+ {"primaries",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_prim_names, p.color.primaries))},
+ {"gamma",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_trc_names, p.color.gamma))},
+ {"sig-peak", SUB_PROP_FLOAT(p.color.hdr.max_luma / MP_REF_WHITE)},
+ {"light",
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.color.light))},
+ {"chroma-location",
+ SUB_PROP_STR(m_opt_choice_str(mp_chroma_names, p.chroma_location))},
+ {"stereo-in",
+ SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p.stereo3d))},
+ {"rotate", SUB_PROP_INT(p.rotate)},
+ {"alpha",
+ SUB_PROP_STR(m_opt_choice_str(mp_alpha_names, p.alpha)),
+ // avoid using "auto" for "no", so just make it unavailable
+ .unavailable = p.alpha == MP_ALPHA_AUTO},
+ {"min-luma", SUB_PROP_FLOAT(hdr->min_luma), .unavailable = !has_hdr10},
+ {"max-luma", SUB_PROP_FLOAT(hdr->max_luma), .unavailable = !has_hdr10},
+ {"max-cll", SUB_PROP_FLOAT(hdr->max_cll), .unavailable = !has_hdr10},
+ {"max-fall", SUB_PROP_FLOAT(hdr->max_fall), .unavailable = !has_hdr10},
+ {"scene-max-r", SUB_PROP_FLOAT(hdr->scene_max[0]), .unavailable = !has_hdr10plus},
+ {"scene-max-g", SUB_PROP_FLOAT(hdr->scene_max[1]), .unavailable = !has_hdr10plus},
+ {"scene-max-b", SUB_PROP_FLOAT(hdr->scene_max[2]), .unavailable = !has_hdr10plus},
+ {"scene-avg", SUB_PROP_FLOAT(hdr->scene_avg), .unavailable = !has_hdr10plus},
+ {"max-pq-y", SUB_PROP_FLOAT(hdr->max_pq_y), .unavailable = !has_cie_y},
+ {"avg-pq-y", SUB_PROP_FLOAT(hdr->avg_pq_y), .unavailable = !has_cie_y},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static struct mp_image_params get_video_out_params(struct MPContext *mpctx)
+{
+ if (!mpctx->vo_chain)
+ return (struct mp_image_params){0};
+
+ struct mp_image_params o_params = mpctx->vo_chain->filter->output_params;
+ if (mpctx->video_out) {
+ struct m_geometry *gm = &mpctx->video_out->opts->video_crop;
+ if (gm->xy_valid || (gm->wh_valid && (gm->w > 0 || gm->h > 0)))
+ {
+ m_rect_apply(&o_params.crop, o_params.w, o_params.h, gm);
+ }
+ }
+
+ return o_params;
+}
+
+static int mp_property_vo_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ return property_imgparams(vo_get_current_params(vo), action, arg);
+}
+
+static int mp_property_dec_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_image_params p = {0};
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ if (!vo_c || !vo_c->track)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ mp_decoder_wrapper_get_video_dec_params(vo_c->track->dec, &p);
+ if (!p.imgfmt)
+ return M_PROPERTY_UNAVAILABLE;
+ return property_imgparams(p, action, arg);
+}
+
+static int mp_property_vd_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ if (!vo_c)
+ return M_PROPERTY_UNAVAILABLE;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_codec_params *c =
+ track && track->stream ? track->stream->codec : NULL;
+ if (vo_c->filter->input_params.imgfmt) {
+ return property_imgparams(vo_c->filter->input_params, action, arg);
+ } else if (c && c->disp_w && c->disp_h) {
+ // Simplistic fallback for stupid scripts querying "width"/"height"
+ // before the first frame is decoded.
+ struct m_sub_property props[] = {
+ {"w", SUB_PROP_INT(c->disp_w)},
+ {"h", SUB_PROP_INT(c->disp_h)},
+ {0}
+ };
+ return m_property_read_sub(props, action, arg);
+ }
+ return M_PROPERTY_UNAVAILABLE;
+}
+
+static int mp_property_video_frame_info(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ struct mp_image *f = vo_get_current_frame(mpctx->video_out);
+ if (!f)
+ return M_PROPERTY_UNAVAILABLE;
+
+ const char *pict_types[] = {0, "I", "P", "B"};
+ const char *pict_type = f->pict_type >= 1 && f->pict_type <= 3
+ ? pict_types[f->pict_type] : NULL;
+
+ struct m_sub_property props[] = {
+ {"picture-type", SUB_PROP_STR(pict_type), .unavailable = !pict_type},
+ {"interlaced", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_INTERLACED))},
+ {"tff", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_TOP_FIRST))},
+ {"repeat", SUB_PROP_BOOL(!!(f->fields & MP_IMGFIELD_REPEAT_FIRST))},
+ {0}
+ };
+
+ talloc_free(f);
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_current_window_scale(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct mp_image_params params = get_video_out_params(mpctx);
+ int vid_w, vid_h;
+ mp_image_params_get_dsize(&params, &vid_w, &vid_h);
+ if (vid_w < 1 || vid_h < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (params.rotate % 180 == 90 && (vo->driver->caps & VO_CAP_ROTATE90))
+ MPSWAP(int, vid_w, vid_h);
+
+ if (vo->monitor_par < 1) {
+ vid_h = MPCLAMP(vid_h / vo->monitor_par, 1, 16000);
+ } else {
+ vid_w = MPCLAMP(vid_w * vo->monitor_par, 1, 16000);
+ }
+
+ if (action == M_PROPERTY_SET) {
+ // Also called by update_window_scale as a NULL property.
+ double scale = *(double *)arg;
+ int s[2] = {vid_w * scale, vid_h * scale};
+ if (s[0] <= 0 || s[1] <= 0)
+ return M_PROPERTY_INVALID_FORMAT;
+ vo_control(vo, VOCTRL_SET_UNFS_WINDOW_SIZE, s);
+ return M_PROPERTY_OK;
+ }
+
+ int s[2];
+ if (vo_control(vo, VOCTRL_GET_UNFS_WINDOW_SIZE, s) <= 0 ||
+ s[0] < 1 || s[1] < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ double xs = (double)s[0] / vid_w;
+ double ys = (double)s[1] / vid_h;
+ return m_property_double_ro(action, arg, (xs + ys) / 2);
+}
+
+static void update_window_scale(struct MPContext *mpctx)
+{
+ double scale = mpctx->opts->vo->window_scale;
+ mp_property_current_window_scale(mpctx, (struct m_property *)NULL,
+ M_PROPERTY_SET, (void*)&scale);
+}
+
+static int mp_property_display_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ double fps = mpctx->video_out ? vo_get_display_fps(mpctx->video_out) : 0;
+ switch (action) {
+ case M_PROPERTY_GET:
+ if (fps <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, fps);
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_DOUBLE};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_estimated_display_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ double interval = vo_get_estimated_vsync_interval(vo) / 1e9;
+ if (interval <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, 1.0 / interval);
+}
+
+static int mp_property_vsync_jitter(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ double stddev = vo_get_estimated_vsync_jitter(vo);
+ if (stddev < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, stddev);
+}
+
+static int mp_property_display_resolution(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ int res[2];
+ if (vo_control(vo, VOCTRL_GET_DISPLAY_RES, &res) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ if (strcmp(prop->name, "display-width") == 0) {
+ return m_property_int_ro(action, arg, res[0]);
+ } else {
+ return m_property_int_ro(action, arg, res[1]);
+ }
+}
+
+static int mp_property_hidpi_scale(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+ if (!cmd->cached_window_scale) {
+ double scale = 0;
+ if (vo_control(vo, VOCTRL_GET_HIDPI_SCALE, &scale) < 1 || !scale)
+ scale = -1;
+ cmd->cached_window_scale = scale;
+ }
+ if (cmd->cached_window_scale < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, cmd->cached_window_scale);
+}
+
+static int mp_property_focused(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ bool focused;
+ if (vo_control(vo, VOCTRL_GET_FOCUSED, &focused) < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return m_property_bool_ro(action, arg, focused);
+}
+
+static int mp_property_display_names(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ char** display_names;
+ if (vo_control(vo, VOCTRL_GET_DISPLAY_NAMES, &display_names) < 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ *(char ***)arg = display_names;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_vo_configured(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_bool_ro(action, arg,
+ mpctx->video_out && mpctx->video_out->config_ok);
+}
+
+static void get_frame_perf(struct mpv_node *node, struct mp_frame_perf *perf)
+{
+ for (int i = 0; i < perf->count; i++) {
+ struct mp_pass_perf *data = &perf->perf[i];
+ struct mpv_node *pass = node_array_add(node, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(pass, "desc", perf->desc[i]);
+ node_map_add(pass, "last", MPV_FORMAT_INT64)->u.int64 = data->last;
+ node_map_add(pass, "avg", MPV_FORMAT_INT64)->u.int64 = data->avg;
+ node_map_add(pass, "peak", MPV_FORMAT_INT64)->u.int64 = data->peak;
+ node_map_add(pass, "count", MPV_FORMAT_INT64)->u.int64 = data->count;
+ struct mpv_node *samples = node_map_add(pass, "samples", MPV_FORMAT_NODE_ARRAY);
+ for (int n = 0; n < data->count; n++)
+ node_array_add(samples, MPV_FORMAT_INT64)->u.int64 = data->samples[n];
+ }
+}
+
+static char *asprint_perf(char *res, struct mp_frame_perf *perf)
+{
+ for (int i = 0; i < perf->count; i++) {
+ struct mp_pass_perf *pass = &perf->perf[i];
+ res = talloc_asprintf_append(res,
+ "- %s: last %dus avg %dus peak %dus\n", perf->desc[i],
+ (int)pass->last/1000, (int)pass->avg/1000, (int)pass->peak/1000);
+ }
+
+ return res;
+}
+
+static int mp_property_vo_passes(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ // Return early, to avoid having to go through a completely unnecessary VOCTRL
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ case M_PROPERTY_GET:
+ break;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ default:
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ }
+
+ struct voctrl_performance_data *data = talloc_ptrtype(NULL, data);
+ if (vo_control(mpctx->video_out, VOCTRL_PERFORMANCE_DATA, data) <= 0) {
+ talloc_free(data);
+ return M_PROPERTY_UNAVAILABLE;
+ }
+
+ switch (action) {
+ case M_PROPERTY_PRINT: {
+ char *res = NULL;
+ res = talloc_asprintf_append(res, "fresh:\n");
+ res = asprint_perf(res, &data->fresh);
+ res = talloc_asprintf_append(res, "\nredraw:\n");
+ res = asprint_perf(res, &data->redraw);
+ *(char **)arg = res;
+ break;
+ }
+
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ node_init(&node, MPV_FORMAT_NODE_MAP, NULL);
+ struct mpv_node *fresh = node_map_add(&node, "fresh", MPV_FORMAT_NODE_ARRAY);
+ struct mpv_node *redraw = node_map_add(&node, "redraw", MPV_FORMAT_NODE_ARRAY);
+ get_frame_perf(fresh, &data->fresh);
+ get_frame_perf(redraw, &data->redraw);
+ *(struct mpv_node *)arg = node;
+ break;
+ }
+ }
+
+ talloc_free(data);
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_perf_info(void *ctx, struct m_property *p, int action,
+ void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ stats_global_query(mpctx->global, (struct mpv_node *)arg);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_vo(void *ctx, struct m_property *p, int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_strdup_ro(action, arg,
+ mpctx->video_out ? mpctx->video_out->driver->name : NULL);
+}
+
+static int mp_property_osd_dim(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd);
+
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ vo_res = (struct mp_osd_res){0};
+
+ double aspect = 1.0 * vo_res.w / MPMAX(vo_res.h, 1) /
+ (vo_res.display_par ? vo_res.display_par : 1);
+
+ struct m_sub_property props[] = {
+ {"w", SUB_PROP_INT(vo_res.w)},
+ {"h", SUB_PROP_INT(vo_res.h)},
+ {"par", SUB_PROP_DOUBLE(vo_res.display_par)},
+ {"aspect", SUB_PROP_DOUBLE(aspect)},
+ {"mt", SUB_PROP_INT(vo_res.mt)},
+ {"mb", SUB_PROP_INT(vo_res.mb)},
+ {"ml", SUB_PROP_INT(vo_res.ml)},
+ {"mr", SUB_PROP_INT(vo_res.mr)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_osd_sym(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ char temp[20];
+ get_current_osd_sym(mpctx, temp, sizeof(temp));
+ return m_property_strdup_ro(action, arg, temp);
+}
+
+static int mp_property_osd_ass(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct m_sub_property props[] = {
+ {"0", SUB_PROP_STR(OSD_ASS_0)},
+ {"1", SUB_PROP_STR(OSD_ASS_1)},
+ {0}
+ };
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_mouse_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+
+ case M_PROPERTY_GET: {
+ struct mpv_node node;
+ int x, y, hover;
+ mp_input_get_mouse_pos(mpctx->input, &x, &y, &hover);
+
+ node_init(&node, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(&node, "x", x);
+ node_map_add_int64(&node, "y", y);
+ node_map_add_flag(&node, "hover", hover);
+ *(struct mpv_node *)arg = node;
+
+ return M_PROPERTY_OK;
+ }
+ }
+
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Video fps (RO)
+static int mp_property_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ float fps = mpctx->vo_chain ? mpctx->vo_chain->filter->container_fps : 0;
+ if (fps < 0.1 || !isfinite(fps))
+ return M_PROPERTY_UNAVAILABLE;;
+ return m_property_float_ro(action, arg, fps);
+}
+
+static int mp_property_vf_fps(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->vo_chain)
+ return M_PROPERTY_UNAVAILABLE;
+ double avg = calc_average_frame_duration(mpctx);
+ if (avg <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, 1.0 / avg);
+}
+
+#define doubles_equal(x, y) (fabs((x) - (y)) <= 0.001)
+
+static int mp_property_video_aspect_override(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ double aspect_ratio;
+ mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &aspect_ratio);
+
+ if (doubles_equal(aspect_ratio, 2.35 / 1.0))
+ *(char **)arg = talloc_asprintf(NULL, "2.35:1");
+ else if (doubles_equal(aspect_ratio, 16.0 / 9.0))
+ *(char **)arg = talloc_asprintf(NULL, "16:9");
+ else if (doubles_equal(aspect_ratio, 16.0 / 10.0))
+ *(char **)arg = talloc_asprintf(NULL, "16:10");
+ else if (doubles_equal(aspect_ratio, 4.0 / 3.0))
+ *(char **)arg = talloc_asprintf(NULL, "4:3");
+ else if (doubles_equal(aspect_ratio, -1.0))
+ *(char **)arg = talloc_asprintf(NULL, "Original");
+ else
+ *(char **)arg = talloc_asprintf(NULL, "%.3f", aspect_ratio);
+
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Subtitle delay (RW)
+static int mp_property_sub_delay(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = format_delay(opts->subs_rend->sub_delay);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+/// Subtitle speed (RW)
+static int mp_property_sub_speed(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg =
+ talloc_asprintf(NULL, "%4.1f%%", 100 * opts->subs_rend->sub_speed);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_sub_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_asprintf(NULL, "%4.2f%%/100", opts->subs_rend->sub_pos);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_sub_ass_extradata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[0][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ if (!sub)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *data = sub_ass_get_extradata(sub);
+ if (!data)
+ return M_PROPERTY_UNAVAILABLE;
+ *(char **)arg = data;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg, int sub_index)
+{
+ int type = *(int *)prop->priv;
+ MPContext *mpctx = ctx;
+ struct track *track = mpctx->current_track[sub_index][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double pts = mpctx->playback_pts;
+ if (!sub || pts == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *text = sub_get_text(sub, pts, type);
+ if (!text)
+ text = talloc_strdup(NULL, "");
+ *(char **)arg = text;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return get_sub_text(ctx, prop, action, arg, 0);
+}
+
+static int mp_property_secondary_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return get_sub_text(ctx, prop, action, arg, 1);
+}
+
+static struct sd_times get_times(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+ MPContext *mpctx = ctx;
+ int track_ind = *(int *)prop->priv;
+ struct track *track = mpctx->current_track[track_ind][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double pts = mpctx->playback_pts;
+ if (!sub || pts == MP_NOPTS_VALUE)
+ return res;
+ return sub_get_times(sub, pts);
+}
+
+static int mp_property_sub_start(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double start = get_times(ctx, prop, action, arg).start;
+ if (start == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, start);
+}
+
+
+static int mp_property_sub_end(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ double end = get_times(ctx, prop, action, arg).end;
+ if (end == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(action, arg, end);
+}
+
+static int mp_property_playlist_current_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ *(int *)arg = playlist_entry_to_index(pl, pl->current);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ pl->current = playlist_entry_from_index(pl, *(int *)arg);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_playlist_playing_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+ return m_property_int_ro(action, arg,
+ playlist_entry_to_index(pl, mpctx->playing));
+}
+
+static int mp_property_playlist_pos_x(void *ctx, struct m_property *prop,
+ int action, void *arg, int base)
+{
+ MPContext *mpctx = ctx;
+ struct playlist *pl = mpctx->playlist;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ int pos = playlist_entry_to_index(pl, pl->current);
+ *(int *)arg = pos < 0 ? -1 : pos + base;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ int pos = *(int *)arg - base;
+ if (pos >= 0 && playlist_entry_to_index(pl, pl->current) == pos)
+ return M_PROPERTY_OK;
+ mp_set_playlist_entry(mpctx, playlist_entry_from_index(pl, pos));
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_INT};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_CONSTRICTED_TYPE: {
+ struct m_option opt = {
+ .type = CONF_TYPE_INT,
+ .min = base,
+ .max = playlist_entry_count(pl) - 1 + base,
+ };
+ *(struct m_option *)arg = opt;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_playlist_pos(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_playlist_pos_x(ctx, prop, action, arg, 0);
+}
+
+static int mp_property_playlist_pos_1(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return mp_property_playlist_pos_x(ctx, prop, action, arg, 1);
+}
+
+static int get_playlist_entry(int item, int action, void *arg, void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist, item);
+ if (!e)
+ return M_PROPERTY_ERROR;
+
+ bool current = mpctx->playlist->current == e;
+ bool playing = mpctx->playing == e;
+ struct m_sub_property props[] = {
+ {"filename", SUB_PROP_STR(e->filename)},
+ {"current", SUB_PROP_BOOL(1), .unavailable = !current},
+ {"playing", SUB_PROP_BOOL(1), .unavailable = !playing},
+ {"title", SUB_PROP_STR(e->title), .unavailable = !e->title},
+ {"id", SUB_PROP_INT64(e->id)},
+ {"playlist-path", SUB_PROP_STR(e->playlist_path), .unavailable = !e->playlist_path},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_playlist_path(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (!mpctx->playlist->current)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct playlist_entry *e = mpctx->playlist->current;
+ return m_property_strdup_ro(action, arg, e->playlist_path);
+}
+
+static int mp_property_playlist(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_PRINT) {
+ struct playlist *pl = mpctx->playlist;
+ char *res = talloc_strdup(NULL, "");
+
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ char *p = e->title;
+ if (!p) {
+ p = e->filename;
+ if (!mp_is_url(bstr0(p))) {
+ char *s = mp_basename(e->filename);
+ if (s[0])
+ p = s;
+ }
+ }
+ const char *m = pl->current == e ? list_current : list_normal;
+ res = talloc_asprintf_append(res, "%s%s\n", m, p);
+ }
+
+ *(char **)arg =
+ cut_osd_list(mpctx, res, playlist_entry_to_index(pl, pl->current));
+ return M_PROPERTY_OK;
+ }
+
+ return m_property_read_list(action, arg, playlist_entry_count(mpctx->playlist),
+ get_playlist_entry, mpctx);
+}
+
+static char *print_obj_osd_list(struct m_obj_settings *list)
+{
+ char *res = NULL;
+ for (int n = 0; list && list[n].name; n++) {
+ res = talloc_asprintf_append(res, "%s [", list[n].name);
+ for (int i = 0; list[n].attribs && list[n].attribs[i]; i += 2) {
+ res = talloc_asprintf_append(res, "%s%s=%s", i > 0 ? " " : "",
+ list[n].attribs[i],
+ list[n].attribs[i + 1]);
+ }
+ res = talloc_asprintf_append(res, "]");
+ if (!list[n].enabled)
+ res = talloc_strdup_append(res, " (disabled)");
+ res = talloc_strdup_append(res, "\n");
+ }
+ if (!res)
+ res = talloc_strdup(NULL, "(empty)");
+ return res;
+}
+
+static int property_filter(struct m_property *prop, int action, void *arg,
+ MPContext *mpctx, enum stream_type mt)
+{
+ if (action == M_PROPERTY_PRINT) {
+ struct m_config_option *opt = m_config_get_co(mpctx->mconfig,
+ bstr0(prop->name));
+ *(char **)arg = print_obj_osd_list(*(struct m_obj_settings **)opt->data);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_vf(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return property_filter(prop, action, arg, ctx, STREAM_VIDEO);
+}
+
+static int mp_property_af(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return property_filter(prop, action, arg, ctx, STREAM_AUDIO);
+}
+
+static int mp_property_ab_loop(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ double val;
+ if (mp_property_generic_option(mpctx, prop, M_PROPERTY_GET, &val) < 1)
+ return M_PROPERTY_ERROR;
+
+ return property_time(action, arg, val);
+ }
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
+static int mp_property_packet_bitrate(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ int type = (uintptr_t)prop->priv & ~0x100;
+ bool old = (uintptr_t)prop->priv & 0x100;
+
+ struct demuxer *demuxer = NULL;
+ if (mpctx->current_track[0][type])
+ demuxer = mpctx->current_track[0][type]->demuxer;
+ if (!demuxer)
+ demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ double r[STREAM_TYPE_COUNT];
+ demux_get_bitrate_stats(demuxer, r);
+ if (r[type] < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ // r[type] is in bytes/second -> bits
+ double rate = r[type] * 8;
+
+ // Same story, but used kilobits for some reason.
+ if (old)
+ return m_property_int64_ro(action, arg, llrint(rate / 1000.0));
+
+ if (action == M_PROPERTY_PRINT) {
+ rate /= 1000;
+ if (rate < 1000) {
+ *(char **)arg = talloc_asprintf(NULL, "%d kbps", (int)rate);
+ } else {
+ *(char **)arg = talloc_asprintf(NULL, "%.3f mbps", rate / 1000.0);
+ }
+ return M_PROPERTY_OK;
+ }
+ return m_property_int64_ro(action, arg, llrint(rate));
+}
+
+static int mp_property_cwd(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *cwd = mp_getcwd(NULL);
+ if (!cwd)
+ return M_PROPERTY_ERROR;
+ *(char **)arg = cwd;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_protocols(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = stream_get_proto_list();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_keylist(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = mp_get_key_list();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int get_decoder_entry(int item, int action, void *arg, void *ctx)
+{
+ struct mp_decoder_list *codecs = ctx;
+ struct mp_decoder_entry *c = &codecs->entries[item];
+
+ struct m_sub_property props[] = {
+ {"codec", SUB_PROP_STR(c->codec)},
+ {"driver" , SUB_PROP_STR(c->decoder)},
+ {"description", SUB_PROP_STR(c->desc)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
+static int mp_property_decoders(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct mp_decoder_list *codecs = talloc_zero(NULL, struct mp_decoder_list);
+ struct mp_decoder_list *v = talloc_steal(codecs, video_decoder_list());
+ struct mp_decoder_list *a = talloc_steal(codecs, audio_decoder_list());
+ mp_append_decoders(codecs, v);
+ mp_append_decoders(codecs, a);
+ int r = m_property_read_list(action, arg, codecs->num_entries,
+ get_decoder_entry, codecs);
+ talloc_free(codecs);
+ return r;
+}
+
+static int mp_property_encoders(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct mp_decoder_list *codecs = talloc_zero(NULL, struct mp_decoder_list);
+ mp_add_lavc_encoders(codecs);
+ int r = m_property_read_list(action, arg, codecs->num_entries,
+ get_decoder_entry, codecs);
+ talloc_free(codecs);
+ return r;
+}
+
+static int mp_property_lavf_demuxers(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char ***)arg = mp_get_lavf_demuxers();
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_version(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, mpv_version);
+}
+
+static int mp_property_configuration(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, CONFIGURATION);
+}
+
+static int mp_property_ffmpeg(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, av_version_info());
+}
+
+static int mp_property_libass_version(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_int64_ro(action, arg, ass_library_version());
+}
+
+static int mp_property_platform(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ return m_property_strdup_ro(action, arg, PLATFORM);
+}
+
+static int mp_property_alias(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ const char *real_property = prop->priv;
+ return mp_property_do(real_property, action, arg, ctx);
+}
+
+static int mp_property_deprecated_alias(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ const char *real_property = prop->priv;
+ for (int n = 0; n < cmd->num_warned_deprecated; n++) {
+ if (strcmp(cmd->warned_deprecated[n], prop->name) == 0)
+ goto done;
+ }
+ MP_WARN(mpctx, "Warning: property '%s' was replaced with '%s' and "
+ "might be removed in the future.\n", prop->name, real_property);
+ MP_TARRAY_APPEND(cmd, cmd->warned_deprecated, cmd->num_warned_deprecated,
+ (char *)prop->name);
+
+done:
+ return mp_property_do(real_property, action, arg, ctx);
+}
+
+static int access_options(struct m_property_action_arg *ka, bool local,
+ MPContext *mpctx)
+{
+ struct m_config_option *opt = m_config_get_co(mpctx->mconfig,
+ bstr0(ka->key));
+ if (!opt)
+ return M_PROPERTY_UNKNOWN;
+ if (!opt->data)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (ka->action) {
+ case M_PROPERTY_GET:
+ m_option_copy(opt->opt, ka->arg, opt->data);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET: {
+ if (local && !mpctx->playing)
+ return M_PROPERTY_ERROR;
+ int flags = local ? M_SETOPT_BACKUP : 0;
+ int r = m_config_set_option_raw(mpctx->mconfig, opt, ka->arg, flags);
+ mp_wakeup_core(mpctx);
+ return r < 0 ? M_PROPERTY_ERROR : M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = *opt->opt;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int access_option_list(int action, void *arg, bool local, MPContext *mpctx)
+{
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(char ***)arg = m_config_list_options(NULL, mpctx->mconfig);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION:
+ return access_options(arg, local, mpctx);
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_options(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return access_option_list(action, arg, false, mpctx);
+}
+
+static int mp_property_local_options(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return access_option_list(action, arg, true, mpctx);
+}
+
+static int mp_property_option_info(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ bstr key;
+ char *rem;
+ m_property_split_path(ka->key, &key, &rem);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, key);
+ if (!co)
+ return M_PROPERTY_UNKNOWN;
+ const struct m_option *opt = co->opt;
+
+ union m_option_value def = m_option_value_default;
+ const void *def_ptr = m_config_get_co_default(mpctx->mconfig, co);
+ if (def_ptr && opt->type->size > 0)
+ memcpy(&def, def_ptr, opt->type->size);
+
+ bool has_minmax = opt->min < opt->max &&
+ (opt->type->flags & M_OPT_TYPE_USES_RANGE);
+ char **choices = NULL;
+
+ if (opt->type == &m_option_type_choice) {
+ const struct m_opt_choice_alternatives *alt = opt->priv;
+ int num = 0;
+ for ( ; alt->name; alt++)
+ MP_TARRAY_APPEND(NULL, choices, num, alt->name);
+ MP_TARRAY_APPEND(NULL, choices, num, NULL);
+ }
+ if (opt->type == &m_option_type_obj_settings_list) {
+ const struct m_obj_list *objs = opt->priv;
+ int num = 0;
+ for (int n = 0; ; n++) {
+ struct m_obj_desc desc = {0};
+ if (!objs->get_desc(&desc, n))
+ break;
+ MP_TARRAY_APPEND(NULL, choices, num, (char *)desc.name);
+ }
+ MP_TARRAY_APPEND(NULL, choices, num, NULL);
+ }
+
+ struct m_sub_property props[] = {
+ {"name", SUB_PROP_STR(co->name)},
+ {"type", SUB_PROP_STR(opt->type->name)},
+ {"set-from-commandline", SUB_PROP_BOOL(co->is_set_from_cmdline)},
+ {"set-locally", SUB_PROP_BOOL(co->is_set_locally)},
+ {"default-value", *opt, def},
+ {"min", SUB_PROP_DOUBLE(opt->min),
+ .unavailable = !(has_minmax && opt->min != DBL_MIN)},
+ {"max", SUB_PROP_DOUBLE(opt->max),
+ .unavailable = !(has_minmax && opt->max != DBL_MAX)},
+ {"choices", .type = {.type = CONF_TYPE_STRING_LIST},
+ .value = {.string_list = choices}, .unavailable = !choices},
+ {0}
+ };
+
+ struct m_property_action_arg next_ka = *ka;
+ next_ka.key = rem;
+ int r = m_property_read_sub(props, M_PROPERTY_KEY_ACTION, &next_ka);
+ talloc_free(choices);
+ return r;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_list(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ struct MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_STRING_LIST};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ char **list = NULL;
+ int num = 0;
+ for (int n = 0; cmd->properties[n].name; n++) {
+ MP_TARRAY_APPEND(NULL, list, num,
+ talloc_strdup(NULL, cmd->properties[n].name));
+ }
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ *(char ***)arg = list;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_profile_list(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ *(struct mpv_node *)arg = m_config_get_profiles(mpctx->mconfig);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_commands(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ struct mpv_node *root = arg;
+ node_init(root, MPV_FORMAT_NODE_ARRAY, NULL);
+
+ for (int n = 0; mp_cmds[n].name; n++) {
+ const struct mp_cmd_def *cmd = &mp_cmds[n];
+ struct mpv_node *entry = node_array_add(root, MPV_FORMAT_NODE_MAP);
+
+ node_map_add_string(entry, "name", cmd->name);
+
+ struct mpv_node *args =
+ node_map_add(entry, "args", MPV_FORMAT_NODE_ARRAY);
+ for (int i = 0; i < MP_CMD_DEF_MAX_ARGS; i++) {
+ const struct m_option *a = &cmd->args[i];
+ if (!a->type)
+ break;
+ struct mpv_node *ae = node_array_add(args, MPV_FORMAT_NODE_MAP);
+ node_map_add_string(ae, "name", a->name);
+ node_map_add_string(ae, "type", a->type->name);
+ node_map_add_flag(ae, "optional", a->flags & MP_CMD_OPT_ARG);
+ }
+
+ node_map_add_flag(entry, "vararg", cmd->vararg);
+ }
+
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_bindings(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET: {
+ *(struct mpv_node *)arg = mp_input_get_bindings(mpctx->input);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+static int mp_property_script_props(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (!cmd->shared_script_warning) {
+ MP_WARN(mpctx, "The shared-script-properties property is deprecated and will "
+ "be removed in the future. Use the user-data property instead.\n");
+ cmd->shared_script_warning = true;
+ }
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = script_props_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ m_option_copy(&script_props_type, arg, &cmd->script_props);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ m_option_copy(&script_props_type, &cmd->script_props, arg);
+ mp_notify_property(mpctx, prop->name);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int do_list_udata(int item, int action, void *arg, void *ctx);
+
+struct udata_ctx {
+ MPContext *mpctx;
+ const char *path;
+ mpv_node *node;
+ void *ta_parent;
+};
+
+static int do_op_udata(struct udata_ctx* ctx, int action, void *arg)
+{
+ MPContext *mpctx = ctx->mpctx;
+ mpv_node *node = ctx->node;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = udata_type;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ case M_PROPERTY_GET_NODE: // same as GET, because type==mpv_node
+ assert(node);
+ m_option_copy(&udata_type, arg, node);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ char *str = m_option_pretty_print(&udata_type, node);
+ *(char **)arg = str;
+ return str != NULL;
+ }
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SET_NODE:
+ assert(node);
+ m_option_copy(&udata_type, node, arg);
+ talloc_steal(ctx->ta_parent, node_get_alloc(node));
+ mp_notify_property(mpctx, ctx->path);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_KEY_ACTION: {
+ assert(node);
+
+ // If we're operating on an array, sub-object access is handled by m_property_read_list
+ if (node->format == MPV_FORMAT_NODE_ARRAY)
+ return m_property_read_list(action, arg, node->u.list->num, &do_list_udata, ctx);
+
+ // Sub-objects only make sense for arrays and maps
+ if (node->format != MPV_FORMAT_NODE_MAP)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ struct m_property_action_arg *act = arg;
+
+ // See if the next layer down will also be a sub-object access
+ bstr key;
+ char *rem;
+ bool has_split = m_property_split_path(act->key, &key, &rem);
+
+ if (!has_split && act->action == M_PROPERTY_DELETE) {
+ // Find the object we're looking for
+ int i;
+ for (i = 0; i < node->u.list->num; i++) {
+ if (bstr_equals0(key, node->u.list->keys[i]))
+ break;
+ }
+
+ // Return if it didn't exist
+ if (i == node->u.list->num)
+ return M_PROPERTY_UNKNOWN;
+
+ // Delete the item
+ m_option_free(&udata_type, &node->u.list->values[i]);
+ talloc_free(node->u.list->keys[i]);
+
+ // Shift the remaining items back
+ for (i++; i < node->u.list->num; i++) {
+ node->u.list->values[i - 1] = node->u.list->values[i];
+ node->u.list->keys[i - 1] = node->u.list->keys[i];
+ }
+
+ // And decrement the count
+ node->u.list->num--;
+
+ return M_PROPERTY_OK;
+ }
+
+ // Look up the next level down
+ mpv_node *cnode = node_map_bget(node, key);
+
+ if (!cnode) {
+ switch (act->action) {
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SET_NODE: {
+ // If we're doing a set, and the key doesn't exist, create it.
+ // If we're recursing another layer down, make it an empty map;
+ // otherwise, make it NONE, since we'll be overwriting it at the next level.
+ cnode = node_map_badd(node, key, has_split ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NONE);
+ if (!cnode)
+ return M_PROPERTY_ERROR;
+ break;
+ case M_PROPERTY_GET_TYPE:
+ // Nonexistent keys have type NODE, so they can be overwritten
+ *(struct m_option *)act->arg = udata_type;
+ return M_PROPERTY_OK;
+ default:
+ // We can't perform any other options on nonexistent keys
+ return M_PROPERTY_UNKNOWN;
+ }
+ }
+ }
+
+ struct udata_ctx nctx = *ctx;
+ nctx.node = cnode;
+ nctx.ta_parent = node_get_alloc(node);
+
+ // If we're going down another level, set up a new key-action.
+ if (has_split) {
+ struct m_property_action_arg sub_act = {
+ .key = rem,
+ .action = act->action,
+ .arg = act->arg,
+ };
+
+ return do_op_udata(&nctx, M_PROPERTY_KEY_ACTION, &sub_act);
+ } else {
+ return do_op_udata(&nctx, act->action, act->arg);
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int do_list_udata(int item, int action, void *arg, void *ctx)
+{
+ struct udata_ctx nctx = *(struct udata_ctx*)ctx;
+ nctx.node = &nctx.node->u.list->values[item];
+ nctx.ta_parent = &nctx.node->u.list;
+
+ return do_op_udata(&nctx, action, arg);
+}
+
+static int mp_property_udata(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ // The root of udata is a shared map; don't allow overwriting
+ // or deleting the whole thing
+ if (action == M_PROPERTY_SET || action == M_PROPERTY_SET_NODE ||
+ action == M_PROPERTY_DELETE)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ char *path = NULL;
+ if (action == M_PROPERTY_KEY_ACTION) {
+ struct m_property_action_arg *act = arg;
+ if (act->action == M_PROPERTY_SET || act->action == M_PROPERTY_SET_NODE)
+ path = talloc_asprintf(NULL, "%s/%s", prop->name, act->key);
+ }
+
+ struct MPContext *mpctx = ctx;
+ struct udata_ctx nctx = {
+ .mpctx = mpctx,
+ .path = path,
+ .node = &mpctx->command_ctx->udata,
+ .ta_parent = &mpctx->command_ctx,
+ };
+
+ int ret = do_op_udata(&nctx, action, arg);
+
+ talloc_free(path);
+
+ return ret;
+}
+
+// Redirect a property name to another
+#define M_PROPERTY_ALIAS(name, real_property) \
+ {(name), mp_property_alias, .priv = (real_property)}
+
+#define M_PROPERTY_DEPRECATED_ALIAS(name, real_property) \
+ {(name), mp_property_deprecated_alias, .priv = (real_property)}
+
+// Base list of properties. This does not include option-mapped properties.
+static const struct m_property mp_properties_base[] = {
+ // General
+ {"pid", mp_property_pid},
+ {"speed", mp_property_playback_speed},
+ {"audio-speed-correction", mp_property_av_speed_correction, .priv = "a"},
+ {"video-speed-correction", mp_property_av_speed_correction, .priv = "v"},
+ {"display-sync-active", mp_property_display_sync_active},
+ {"filename", mp_property_filename},
+ {"stream-open-filename", mp_property_stream_open_filename},
+ {"file-size", mp_property_file_size},
+ {"path", mp_property_path},
+ {"media-title", mp_property_media_title},
+ {"stream-path", mp_property_stream_path},
+ {"current-demuxer", mp_property_demuxer},
+ {"file-format", mp_property_file_format},
+ {"stream-pos", mp_property_stream_pos},
+ {"stream-end", mp_property_stream_end},
+ {"duration", mp_property_duration},
+ {"avsync", mp_property_avsync},
+ {"total-avsync-change", mp_property_total_avsync_change},
+ {"mistimed-frame-count", mp_property_mistimed_frame_count},
+ {"vsync-ratio", mp_property_vsync_ratio},
+ {"display-width", mp_property_display_resolution},
+ {"display-height", mp_property_display_resolution},
+ {"decoder-frame-drop-count", mp_property_frame_drop_dec},
+ {"frame-drop-count", mp_property_frame_drop_vo},
+ {"vo-delayed-frame-count", mp_property_vo_delayed_frame_count},
+ {"percent-pos", mp_property_percent_pos},
+ {"time-start", mp_property_time_start},
+ {"time-pos", mp_property_time_pos},
+ {"time-remaining", mp_property_remaining},
+ {"audio-pts", mp_property_audio_pts},
+ {"playtime-remaining", mp_property_playtime_remaining},
+ {"playback-time", mp_property_playback_time},
+ {"chapter", mp_property_chapter},
+ {"edition", mp_property_edition},
+ {"current-edition", mp_property_current_edition},
+ {"chapters", mp_property_chapters},
+ {"editions", mp_property_editions},
+ {"metadata", mp_property_metadata},
+ {"filtered-metadata", mp_property_filtered_metadata},
+ {"chapter-metadata", mp_property_chapter_metadata},
+ {"vf-metadata", mp_property_filter_metadata, .priv = "vf"},
+ {"af-metadata", mp_property_filter_metadata, .priv = "af"},
+ {"core-idle", mp_property_core_idle},
+ {"eof-reached", mp_property_eof_reached},
+ {"seeking", mp_property_seeking},
+ {"playback-abort", mp_property_playback_abort},
+ {"cache-speed", mp_property_cache_speed},
+ {"demuxer-cache-duration", mp_property_demuxer_cache_duration},
+ {"demuxer-cache-time", mp_property_demuxer_cache_time},
+ {"demuxer-cache-idle", mp_property_demuxer_cache_idle},
+ {"demuxer-start-time", mp_property_demuxer_start_time},
+ {"demuxer-cache-state", mp_property_demuxer_cache_state},
+ {"cache-buffering-state", mp_property_cache_buffering},
+ {"paused-for-cache", mp_property_paused_for_cache},
+ {"demuxer-via-network", mp_property_demuxer_is_network},
+ {"clock", mp_property_clock},
+ {"seekable", mp_property_seekable},
+ {"partially-seekable", mp_property_partially_seekable},
+ {"idle-active", mp_property_idle},
+ {"window-id", mp_property_window_id},
+
+ {"chapter-list", mp_property_list_chapters},
+ {"track-list", property_list_tracks},
+ {"current-tracks", property_current_tracks},
+ {"edition-list", property_list_editions},
+
+ {"playlist", mp_property_playlist},
+ {"playlist-path", mp_property_playlist_path},
+ {"playlist-pos", mp_property_playlist_pos},
+ {"playlist-pos-1", mp_property_playlist_pos_1},
+ {"playlist-current-pos", mp_property_playlist_current_pos},
+ {"playlist-playing-pos", mp_property_playlist_playing_pos},
+ M_PROPERTY_ALIAS("playlist-count", "playlist/count"),
+
+ // Audio
+ {"mixer-active", mp_property_mixer_active},
+ {"volume", mp_property_volume},
+ {"ao-volume", mp_property_ao_volume},
+ {"ao-mute", mp_property_ao_mute},
+ {"audio-delay", mp_property_audio_delay},
+ {"audio-codec-name", mp_property_audio_codec_name},
+ {"audio-codec", mp_property_audio_codec},
+ {"audio-params", mp_property_audio_params},
+ {"audio-out-params", mp_property_audio_out_params},
+ {"aid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_AUDIO}},
+ {"audio-device", mp_property_audio_device},
+ {"audio-device-list", mp_property_audio_devices},
+ {"current-ao", mp_property_ao},
+
+ // Video
+ {"video-out-params", mp_property_vo_imgparams},
+ {"video-dec-params", mp_property_dec_imgparams},
+ {"video-params", mp_property_vd_imgparams},
+ {"video-format", mp_property_video_format},
+ {"video-frame-info", mp_property_video_frame_info},
+ {"video-codec", mp_property_video_codec},
+ M_PROPERTY_ALIAS("dwidth", "video-out-params/dw"),
+ M_PROPERTY_ALIAS("dheight", "video-out-params/dh"),
+ M_PROPERTY_ALIAS("width", "video-params/w"),
+ M_PROPERTY_ALIAS("height", "video-params/h"),
+ {"current-window-scale", mp_property_current_window_scale},
+ {"vo-configured", mp_property_vo_configured},
+ {"vo-passes", mp_property_vo_passes},
+ {"perf-info", mp_property_perf_info},
+ {"current-vo", mp_property_vo},
+ {"container-fps", mp_property_fps},
+ {"estimated-vf-fps", mp_property_vf_fps},
+ {"video-aspect-override", mp_property_video_aspect_override},
+ {"vid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_VIDEO}},
+ {"hwdec-current", mp_property_hwdec_current},
+ {"hwdec-interop", mp_property_hwdec_interop},
+
+ {"estimated-frame-count", mp_property_frame_count},
+ {"estimated-frame-number", mp_property_frame_number},
+
+ {"osd-dimensions", mp_property_osd_dim},
+ M_PROPERTY_ALIAS("osd-width", "osd-dimensions/w"),
+ M_PROPERTY_ALIAS("osd-height", "osd-dimensions/h"),
+ M_PROPERTY_ALIAS("osd-par", "osd-dimensions/par"),
+
+ {"osd-sym-cc", mp_property_osd_sym},
+ {"osd-ass-cc", mp_property_osd_ass},
+
+ {"mouse-pos", mp_property_mouse_pos},
+
+ // Subs
+ {"sid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_SUB}},
+ {"secondary-sid", property_switch_track,
+ .priv = (void *)(const int[]){1, STREAM_SUB}},
+ {"sub-delay", mp_property_sub_delay},
+ {"sub-speed", mp_property_sub_speed},
+ {"sub-pos", mp_property_sub_pos},
+ {"sub-ass-extradata", mp_property_sub_ass_extradata},
+ {"sub-text", mp_property_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
+ {"secondary-sub-text", mp_property_secondary_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
+ {"sub-text-ass", mp_property_sub_text,
+ .priv = (void *)&(const int){SD_TEXT_TYPE_ASS}},
+ {"sub-start", mp_property_sub_start,
+ .priv = (void *)&(const int){0}},
+ {"secondary-sub-start", mp_property_sub_start,
+ .priv = (void *)&(const int){1}},
+ {"sub-end", mp_property_sub_end,
+ .priv = (void *)&(const int){0}},
+ {"secondary-sub-end", mp_property_sub_end,
+ .priv = (void *)&(const int){1}},
+
+ {"vf", mp_property_vf},
+ {"af", mp_property_af},
+
+ {"ab-loop-a", mp_property_ab_loop},
+ {"ab-loop-b", mp_property_ab_loop},
+
+#define PROPERTY_BITRATE(name, old, type) \
+ {name, mp_property_packet_bitrate, (void *)(uintptr_t)((type)|(old?0x100:0))}
+ PROPERTY_BITRATE("packet-video-bitrate", true, STREAM_VIDEO),
+ PROPERTY_BITRATE("packet-audio-bitrate", true, STREAM_AUDIO),
+ PROPERTY_BITRATE("packet-sub-bitrate", true, STREAM_SUB),
+
+ PROPERTY_BITRATE("video-bitrate", false, STREAM_VIDEO),
+ PROPERTY_BITRATE("audio-bitrate", false, STREAM_AUDIO),
+ PROPERTY_BITRATE("sub-bitrate", false, STREAM_SUB),
+
+ {"focused", mp_property_focused},
+ {"display-names", mp_property_display_names},
+ {"display-fps", mp_property_display_fps},
+ {"estimated-display-fps", mp_property_estimated_display_fps},
+ {"vsync-jitter", mp_property_vsync_jitter},
+ {"display-hidpi-scale", mp_property_hidpi_scale},
+
+ {"working-directory", mp_property_cwd},
+
+ {"protocol-list", mp_property_protocols},
+ {"decoder-list", mp_property_decoders},
+ {"encoder-list", mp_property_encoders},
+ {"demuxer-lavf-list", mp_property_lavf_demuxers},
+ {"input-key-list", mp_property_keylist},
+
+ {"mpv-version", mp_property_version},
+ {"mpv-configuration", mp_property_configuration},
+ {"ffmpeg-version", mp_property_ffmpeg},
+ {"libass-version", mp_property_libass_version},
+ {"platform", mp_property_platform},
+
+ {"options", mp_property_options},
+ {"file-local-options", mp_property_local_options},
+ {"option-info", mp_property_option_info},
+ {"property-list", mp_property_list},
+ {"profile-list", mp_profile_list},
+ {"command-list", mp_property_commands},
+ {"input-bindings", mp_property_bindings},
+
+ {"shared-script-properties", mp_property_script_props},
+ {"user-data", mp_property_udata},
+
+ M_PROPERTY_ALIAS("video", "vid"),
+ M_PROPERTY_ALIAS("audio", "aid"),
+ M_PROPERTY_ALIAS("sub", "sid"),
+
+ // compatibility
+ M_PROPERTY_ALIAS("colormatrix", "video-params/colormatrix"),
+ M_PROPERTY_ALIAS("colormatrix-input-range", "video-params/colorlevels"),
+ M_PROPERTY_ALIAS("colormatrix-primaries", "video-params/primaries"),
+ M_PROPERTY_ALIAS("colormatrix-gamma", "video-params/gamma"),
+
+ M_PROPERTY_DEPRECATED_ALIAS("sub-forced-only-cur", "sub-forced-events-only"),
+};
+
+// Each entry describes which properties an event (possibly) changes.
+#define E(x, ...) [x] = (const char*const[]){__VA_ARGS__, NULL}
+static const char *const *const mp_event_property_change[] = {
+ E(MPV_EVENT_START_FILE, "*"),
+ E(MPV_EVENT_END_FILE, "*"),
+ E(MPV_EVENT_FILE_LOADED, "*"),
+ E(MP_EVENT_CHANGE_ALL, "*"),
+ E(MP_EVENT_TRACKS_CHANGED, "track-list", "current-tracks"),
+ E(MP_EVENT_TRACK_SWITCHED, "track-list", "current-tracks"),
+ E(MPV_EVENT_IDLE, "*"),
+ E(MPV_EVENT_TICK, "time-pos", "audio-pts", "stream-pos", "avsync",
+ "percent-pos", "time-remaining", "playtime-remaining", "playback-time",
+ "estimated-vf-fps", "total-avsync-change", "audio-speed-correction",
+ "video-speed-correction", "vo-delayed-frame-count", "mistimed-frame-count",
+ "vsync-ratio", "estimated-display-fps", "vsync-jitter", "sub-text",
+ "secondary-sub-text", "audio-bitrate", "video-bitrate", "sub-bitrate",
+ "decoder-frame-drop-count", "frame-drop-count", "video-frame-info",
+ "vf-metadata", "af-metadata", "sub-start", "sub-end", "secondary-sub-start",
+ "secondary-sub-end", "video-out-params", "video-dec-params", "video-params"),
+ E(MP_EVENT_DURATION_UPDATE, "duration"),
+ E(MPV_EVENT_VIDEO_RECONFIG, "video-out-params", "video-params",
+ "video-format", "video-codec", "video-bitrate", "dwidth", "dheight",
+ "width", "height", "container-fps", "aspect", "aspect-name", "vo-configured", "current-vo",
+ "video-dec-params", "osd-dimensions",
+ "hwdec", "hwdec-current", "hwdec-interop"),
+ E(MPV_EVENT_AUDIO_RECONFIG, "audio-format", "audio-codec", "audio-bitrate",
+ "samplerate", "channels", "audio", "volume", "mute",
+ "current-ao", "audio-codec-name", "audio-params",
+ "audio-out-params", "volume-max", "mixer-active"),
+ E(MPV_EVENT_SEEK, "seeking", "core-idle", "eof-reached"),
+ E(MPV_EVENT_PLAYBACK_RESTART, "seeking", "core-idle", "eof-reached"),
+ E(MP_EVENT_METADATA_UPDATE, "metadata", "filtered-metadata", "media-title"),
+ E(MP_EVENT_CHAPTER_CHANGE, "chapter", "chapter-metadata"),
+ E(MP_EVENT_CACHE_UPDATE,
+ "demuxer-cache-duration", "demuxer-cache-idle", "paused-for-cache",
+ "demuxer-cache-time", "cache-buffering-state", "cache-speed",
+ "demuxer-cache-state"),
+ E(MP_EVENT_WIN_RESIZE, "current-window-scale", "osd-width", "osd-height",
+ "osd-par", "osd-dimensions"),
+ E(MP_EVENT_WIN_STATE, "display-names", "display-fps", "display-width",
+ "display-height"),
+ E(MP_EVENT_WIN_STATE2, "display-hidpi-scale"),
+ E(MP_EVENT_FOCUS, "focused"),
+ E(MP_EVENT_CHANGE_PLAYLIST, "playlist", "playlist-pos", "playlist-pos-1",
+ "playlist-count", "playlist/count", "playlist-current-pos",
+ "playlist-playing-pos"),
+ E(MP_EVENT_INPUT_PROCESSED, "mouse-pos"),
+ E(MP_EVENT_CORE_IDLE, "core-idle", "eof-reached"),
+};
+#undef E
+
+// If there is no prefix, return length+1 (avoids matching full name as prefix).
+static int prefix_len(const char *p)
+{
+ const char *end = strchr(p, '/');
+ return end ? end - p : strlen(p) + 1;
+}
+
+static bool match_property(const char *a, const char *b)
+{
+ if (strcmp(a, "*") == 0)
+ return true;
+ // Give options and properties the same ID each, so change notifications
+ // work both way.
+ if (strncmp(a, "options/", 8) == 0)
+ a += 8;
+ if (strncmp(b, "options/", 8) == 0)
+ b += 8;
+ int len_a = prefix_len(a);
+ int len_b = prefix_len(b);
+ return strncmp(a, b, MPMIN(len_a, len_b)) == 0;
+}
+
+// Return a bitset of events which change the property.
+uint64_t mp_get_property_event_mask(const char *name)
+{
+ uint64_t mask = 0;
+ for (int n = 0; n < MP_ARRAY_SIZE(mp_event_property_change); n++) {
+ const char *const *const list = mp_event_property_change[n];
+ for (int i = 0; list && list[i]; i++) {
+ if (match_property(list[i], name))
+ mask |= 1ULL << n;
+ }
+ }
+ return mask;
+}
+
+// Return an ID for the property. It might not be unique, but is good enough
+// for property change handling. Return -1 if property unknown.
+int mp_get_property_id(struct MPContext *mpctx, const char *name)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ for (int n = 0; ctx->properties[n].name; n++) {
+ if (match_property(ctx->properties[n].name, name))
+ return n;
+ }
+ return -1;
+}
+
+static bool is_property_set(int action, void *val)
+{
+ switch (action) {
+ case M_PROPERTY_SET:
+ case M_PROPERTY_SWITCH:
+ case M_PROPERTY_SET_STRING:
+ case M_PROPERTY_SET_NODE:
+ case M_PROPERTY_MULTIPLY:
+ return true;
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *key = val;
+ return is_property_set(key->action, key->arg);
+ }
+ default:
+ return false;
+ }
+}
+
+int mp_property_do(const char *name, int action, void *val,
+ struct MPContext *ctx)
+{
+ struct command_ctx *cmd = ctx->command_ctx;
+ int r = m_property_do(ctx->log, cmd->properties, name, action, val, ctx);
+
+ if (mp_msg_test(ctx->log, MSGL_V) && is_property_set(action, val)) {
+ struct m_option ot = {0};
+ void *data = val;
+ switch (action) {
+ case M_PROPERTY_SET_NODE:
+ ot.type = &m_option_type_node;
+ break;
+ case M_PROPERTY_SET_STRING:
+ ot.type = &m_option_type_string;
+ data = &val;
+ break;
+ }
+ char *t = ot.type ? m_option_print(&ot, data) : NULL;
+ MP_VERBOSE(ctx, "Set property: %s%s%s -> %d\n",
+ name, t ? "=" : "", t ? t : "", r);
+ talloc_free(t);
+ }
+ return r;
+}
+
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ return m_properties_expand_string(ctx->properties, str, mpctx);
+}
+
+// Before expanding properties, parse C-style escapes like "\n"
+char *mp_property_expand_escaped_string(struct MPContext *mpctx, const char *str)
+{
+ void *tmp = talloc_new(NULL);
+ bstr strb = bstr0(str);
+ bstr dst = {0};
+ while (strb.len) {
+ if (!mp_append_escaped_string(tmp, &dst, &strb)) {
+ talloc_free(tmp);
+ return talloc_strdup(NULL, "(broken escape sequences)");
+ }
+ // pass " through literally
+ if (!bstr_eatstart0(&strb, "\""))
+ break;
+ bstr_xappend(tmp, &dst, bstr0("\""));
+ }
+ char *r = mp_property_expand_string(mpctx, dst.start);
+ talloc_free(tmp);
+ return r;
+}
+
+void property_print_help(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ m_properties_print_help_list(mpctx->log, ctx->properties);
+}
+
+/* List of default ways to show a property on OSD.
+ *
+ * If osd_progbar is set, a bar showing the current position between min/max
+ * values of the property is shown. In this case osd_msg is only used for
+ * terminal output if there is no video; it'll be a label shown together with
+ * percentage.
+ */
+static const struct property_osd_display {
+ // property name
+ const char *name;
+ // name used on OSD
+ const char *osd_name;
+ // progressbar type
+ int osd_progbar;
+ // Needs special ways to display the new value (seeks are delayed)
+ int seek_msg, seek_bar;
+ // Show a marker thing on OSD bar. Ignored if osd_progbar==0.
+ float marker;
+ // Free-form message (if NULL, osd_name or the property name is used)
+ const char *msg;
+} property_osd_display[] = {
+ // general
+ {"loop-playlist", "Loop"},
+ {"loop-file", "Loop current file"},
+ {"chapter",
+ .seek_msg = OSD_SEEK_INFO_CHAPTER_TEXT,
+ .seek_bar = OSD_SEEK_INFO_BAR},
+ {"hr-seek", "hr-seek"},
+ {"speed", "Speed"},
+ {"clock", "Clock"},
+ {"edition", "Edition"},
+ // audio
+ {"volume", "Volume",
+ .msg = "Volume: ${?volume:${volume}% ${?mute==yes:(Muted)}}${!volume:${volume}}",
+ .osd_progbar = OSD_VOLUME, .marker = 100},
+ {"ao-volume", "AO Volume",
+ .msg = "AO Volume: ${?ao-volume:${ao-volume}% ${?ao-mute==yes:(Muted)}}${!ao-volume:${ao-volume}}",
+ .osd_progbar = OSD_VOLUME, .marker = 100},
+ {"mute", "Mute"},
+ {"ao-mute", "AO Mute"},
+ {"audio-delay", "A-V delay"},
+ {"audio", "Audio"},
+ // video
+ {"panscan", "Panscan", .osd_progbar = OSD_PANSCAN},
+ {"taskbar-progress", "Progress in taskbar"},
+ {"snap-window", "Snap to screen edges"},
+ {"ontop", "Stay on top"},
+ {"on-all-workspaces", "Visibility on all workspaces"},
+ {"border", "Border"},
+ {"framedrop", "Framedrop"},
+ {"deinterlace", "Deinterlace"},
+ {"gamma", "Gamma", .osd_progbar = OSD_BRIGHTNESS },
+ {"brightness", "Brightness", .osd_progbar = OSD_BRIGHTNESS},
+ {"contrast", "Contrast", .osd_progbar = OSD_CONTRAST},
+ {"saturation", "Saturation", .osd_progbar = OSD_SATURATION},
+ {"hue", "Hue", .osd_progbar = OSD_HUE},
+ {"angle", "Angle"},
+ // subs
+ {"sub", "Subtitles"},
+ {"secondary-sid", "Secondary subtitles"},
+ {"sub-pos", "Sub position"},
+ {"sub-delay", "Sub delay"},
+ {"sub-speed", "Sub speed"},
+ {"sub-visibility",
+ .msg = "Subtitles ${!sub-visibility==yes:hidden}"
+ "${?sub-visibility==yes:visible${?sub==no: (but no subtitles selected)}}"},
+ {"secondary-sub-visibility",
+ .msg = "Secondary Subtitles ${!secondary-sub-visibility==yes:hidden}"
+ "${?secondary-sub-visibility==yes:visible${?secondary-sid==no: (but no secondary subtitles selected)}}"},
+ {"sub-forced-events-only", "Forced sub only"},
+ {"sub-scale", "Sub Scale"},
+ {"sub-ass-vsfilter-aspect-compat", "Subtitle VSFilter aspect compat"},
+ {"sub-ass-override", "ASS subtitle style override"},
+ {"vf", "Video filters", .msg = "Video filters:\n${vf}"},
+ {"af", "Audio filters", .msg = "Audio filters:\n${af}"},
+ {"ab-loop-a", "A-B loop start"},
+ {"ab-loop-b", .msg = "A-B loop: ${ab-loop-a} - ${ab-loop-b}"
+ "${?=ab-loop-count==0: (disabled)}"},
+ {"audio-device", "Audio device"},
+ {"hwdec", .msg = "Hardware decoding: ${hwdec-current}"},
+ {"video-aspect-override", "Aspect ratio override"},
+ // By default, don't display the following properties on OSD
+ {"pause", NULL},
+ {"fullscreen", NULL},
+ {"window-minimized", NULL},
+ {"window-maximized", NULL},
+ {0}
+};
+
+static void show_property_osd(MPContext *mpctx, const char *name, int osd_mode)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct property_osd_display disp = {.name = name, .osd_name = name};
+
+ if (!osd_mode)
+ return;
+
+ // look for the command
+ for (const struct property_osd_display *p = property_osd_display; p->name; p++)
+ {
+ if (!strcmp(p->name, name)) {
+ disp = *p;
+ break;
+ }
+ }
+
+ if (osd_mode == MP_ON_OSD_AUTO) {
+ osd_mode =
+ ((disp.msg || disp.osd_name || disp.seek_msg) ? MP_ON_OSD_MSG : 0) |
+ ((disp.osd_progbar || disp.seek_bar) ? MP_ON_OSD_BAR : 0);
+ }
+
+ if (!disp.osd_progbar)
+ disp.osd_progbar = ' ';
+
+ if (!disp.osd_name)
+ disp.osd_name = name;
+
+ if (disp.seek_msg || disp.seek_bar) {
+ mpctx->add_osd_seek_info |=
+ (osd_mode & MP_ON_OSD_MSG ? disp.seek_msg : 0) |
+ (osd_mode & MP_ON_OSD_BAR ? disp.seek_bar : 0);
+ return;
+ }
+
+ struct m_option prop = {0};
+ mp_property_do(name, M_PROPERTY_GET_CONSTRICTED_TYPE, &prop, mpctx);
+ if ((osd_mode & MP_ON_OSD_BAR)) {
+ if (prop.type == CONF_TYPE_INT && prop.min < prop.max) {
+ int n = prop.min;
+ if (disp.osd_progbar)
+ n = disp.marker;
+ int i;
+ if (mp_property_do(name, M_PROPERTY_GET, &i, mpctx) > 0)
+ set_osd_bar(mpctx, disp.osd_progbar, prop.min, prop.max, n, i);
+ } else if (prop.type == CONF_TYPE_FLOAT && prop.min < prop.max) {
+ float n = prop.min;
+ if (disp.osd_progbar)
+ n = disp.marker;
+ float f;
+ if (mp_property_do(name, M_PROPERTY_GET, &f, mpctx) > 0)
+ set_osd_bar(mpctx, disp.osd_progbar, prop.min, prop.max, n, f);
+ }
+ }
+
+ if (osd_mode & MP_ON_OSD_MSG) {
+ void *tmp = talloc_new(NULL);
+
+ const char *msg = disp.msg;
+ if (!msg)
+ msg = talloc_asprintf(tmp, "%s: ${%s}", disp.osd_name, name);
+
+ char *osd_msg = talloc_steal(tmp, mp_property_expand_string(mpctx, msg));
+
+ if (osd_msg && osd_msg[0])
+ set_osd_msg(mpctx, 1, opts->osd_duration, "%s", osd_msg);
+
+ talloc_free(tmp);
+ }
+}
+
+static bool reinit_filters(MPContext *mpctx, enum stream_type mediatype)
+{
+ switch (mediatype) {
+ case STREAM_VIDEO:
+ return reinit_video_filters(mpctx) >= 0;
+ case STREAM_AUDIO:
+ return reinit_audio_filters(mpctx) >= 0;
+ }
+ return false;
+}
+
+static const char *const filter_opt[STREAM_TYPE_COUNT] = {
+ [STREAM_VIDEO] = "vf",
+ [STREAM_AUDIO] = "af",
+};
+
+static int set_filters(struct MPContext *mpctx, enum stream_type mediatype,
+ struct m_obj_settings *new_chain)
+{
+ bstr option = bstr0(filter_opt[mediatype]);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, option);
+ if (!co)
+ return -1;
+
+ struct m_obj_settings **list = co->data;
+ struct m_obj_settings *old_settings = *list;
+ *list = NULL;
+ m_option_copy(co->opt, list, &new_chain);
+
+ bool success = reinit_filters(mpctx, mediatype);
+
+ if (success) {
+ m_option_free(co->opt, &old_settings);
+ m_config_notify_change_opt_ptr(mpctx->mconfig, list);
+ } else {
+ m_option_free(co->opt, list);
+ *list = old_settings;
+ }
+
+ return success ? 0 : -1;
+}
+
+static int edit_filters(struct MPContext *mpctx, struct mp_log *log,
+ enum stream_type mediatype,
+ const char *cmd, const char *arg)
+{
+ bstr option = bstr0(filter_opt[mediatype]);
+ struct m_config_option *co = m_config_get_co(mpctx->mconfig, option);
+ if (!co)
+ return -1;
+
+ // The option parser is used to modify the filter list itself.
+ char optname[20];
+ snprintf(optname, sizeof(optname), "%.*s-%s", BSTR_P(option), cmd);
+
+ struct m_obj_settings *new_chain = NULL;
+ m_option_copy(co->opt, &new_chain, co->data);
+
+ int r = m_option_parse(log, co->opt, bstr0(optname), bstr0(arg), &new_chain);
+ if (r >= 0)
+ r = set_filters(mpctx, mediatype, new_chain);
+
+ m_option_free(co->opt, &new_chain);
+
+ return r >= 0 ? 0 : -1;
+}
+
+static int edit_filters_osd(struct MPContext *mpctx, enum stream_type mediatype,
+ const char *cmd, const char *arg, bool on_osd)
+{
+ int r = edit_filters(mpctx, mpctx->log, mediatype, cmd, arg);
+ if (on_osd) {
+ if (r >= 0) {
+ const char *prop = filter_opt[mediatype];
+ show_property_osd(mpctx, prop, MP_ON_OSD_MSG);
+ } else {
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration,
+ "Changing filters failed!");
+ }
+ }
+ return r;
+}
+
+static void recreate_overlays(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ int overlay_next = !cmd->overlay_osd_current;
+ struct sub_bitmaps *new = &cmd->overlay_osd[overlay_next];
+ new->format = SUBBITMAP_BGRA;
+ new->change_id = 1;
+
+ bool valid = false;
+
+ new->num_parts = 0;
+ for (int n = 0; n < cmd->num_overlays; n++) {
+ struct overlay *o = &cmd->overlays[n];
+ if (o->source) {
+ struct mp_image *s = o->source;
+ struct sub_bitmap b = {
+ .bitmap = s->planes[0],
+ .stride = s->stride[0],
+ .w = s->w, .dw = s->w,
+ .h = s->h, .dh = s->h,
+ .x = o->x,
+ .y = o->y,
+ };
+ MP_TARRAY_APPEND(cmd, new->parts, new->num_parts, b);
+ }
+ }
+
+ if (!cmd->overlay_packer)
+ cmd->overlay_packer = talloc_zero(cmd, struct bitmap_packer);
+
+ cmd->overlay_packer->padding = 1; // assume bilinear scaling
+ packer_set_size(cmd->overlay_packer, new->num_parts);
+
+ for (int n = 0; n < new->num_parts; n++)
+ cmd->overlay_packer->in[n] = (struct pos){new->parts[n].w, new->parts[n].h};
+
+ if (packer_pack(cmd->overlay_packer) < 0 || new->num_parts == 0)
+ goto done;
+
+ struct pos bb[2];
+ packer_get_bb(cmd->overlay_packer, bb);
+
+ new->packed_w = bb[1].x;
+ new->packed_h = bb[1].y;
+
+ if (!new->packed || new->packed->w < new->packed_w ||
+ new->packed->h < new->packed_h)
+ {
+ talloc_free(new->packed);
+ new->packed = mp_image_alloc(IMGFMT_BGRA, cmd->overlay_packer->w,
+ cmd->overlay_packer->h);
+ if (!new->packed)
+ goto done;
+ }
+
+ if (!mp_image_make_writeable(new->packed))
+ goto done;
+
+ // clear padding
+ mp_image_clear(new->packed, 0, 0, new->packed->w, new->packed->h);
+
+ for (int n = 0; n < new->num_parts; n++) {
+ struct sub_bitmap *b = &new->parts[n];
+ struct pos pos = cmd->overlay_packer->result[n];
+
+ int stride = new->packed->stride[0];
+ void *pdata = (uint8_t *)new->packed->planes[0] + pos.y * stride + pos.x * 4;
+ memcpy_pic(pdata, b->bitmap, b->w * 4, b->h, stride, b->stride);
+
+ b->bitmap = pdata;
+ b->stride = stride;
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ }
+
+ valid = true;
+done:
+ if (!valid) {
+ new->format = SUBBITMAP_EMPTY;
+ new->num_parts = 0;
+ }
+
+ osd_set_external2(mpctx->osd, new);
+ mp_wakeup_core(mpctx);
+ cmd->overlay_osd_current = overlay_next;
+}
+
+// Set overlay with the given ID to the contents as described by "new".
+static void replace_overlay(struct MPContext *mpctx, int id, struct overlay *new)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ assert(id >= 0);
+ if (id >= cmd->num_overlays) {
+ MP_TARRAY_GROW(cmd, cmd->overlays, id);
+ while (cmd->num_overlays <= id)
+ cmd->overlays[cmd->num_overlays++] = (struct overlay){0};
+ }
+
+ struct overlay *ptr = &cmd->overlays[id];
+
+ talloc_free(ptr->source);
+ *ptr = *new;
+
+ recreate_overlays(mpctx);
+}
+
+static void cmd_overlay_add(void *pcmd)
+{
+ struct mp_cmd_ctx *cmd = pcmd;
+ struct MPContext *mpctx = cmd->mpctx;
+ int id = cmd->args[0].v.i, x = cmd->args[1].v.i, y = cmd->args[2].v.i;
+ char *file = cmd->args[3].v.s;
+ int offset = cmd->args[4].v.i;
+ char *fmt = cmd->args[5].v.s;
+ int w = cmd->args[6].v.i, h = cmd->args[7].v.i, stride = cmd->args[8].v.i;
+
+ if (strcmp(fmt, "bgra") != 0) {
+ MP_ERR(mpctx, "overlay-add: unsupported OSD format '%s'\n", fmt);
+ goto error;
+ }
+ if (id < 0 || id >= 64) { // arbitrary upper limit
+ MP_ERR(mpctx, "overlay-add: invalid id %d\n", id);
+ goto error;
+ }
+ if (w <= 0 || h <= 0 || stride < w * 4 || (stride % 4)) {
+ MP_ERR(mpctx, "overlay-add: inconsistent parameters\n");
+ goto error;
+ }
+ struct overlay overlay = {
+ .source = mp_image_alloc(IMGFMT_BGRA, w, h),
+ .x = x,
+ .y = y,
+ };
+ if (!overlay.source)
+ goto error;
+ int fd = -1;
+ bool close_fd = true;
+ void *p = NULL;
+ if (file[0] == '@') {
+ char *end;
+ fd = strtol(&file[1], &end, 10);
+ if (!file[1] || end[0])
+ fd = -1;
+ close_fd = false;
+ } else if (file[0] == '&') {
+ char *end;
+ unsigned long long addr = strtoull(&file[1], &end, 0);
+ if (!file[1] || end[0])
+ addr = 0;
+ p = (void *)(uintptr_t)addr;
+ } else {
+ fd = open(file, O_RDONLY | O_BINARY | O_CLOEXEC);
+ }
+ int map_size = 0;
+ if (fd >= 0) {
+ map_size = offset + h * stride;
+ void *m = mmap(NULL, map_size, PROT_READ, MAP_SHARED, fd, 0);
+ if (close_fd)
+ close(fd);
+ if (m && m != MAP_FAILED)
+ p = m;
+ }
+ if (!p) {
+ MP_ERR(mpctx, "overlay-add: could not open or map '%s'\n", file);
+ talloc_free(overlay.source);
+ goto error;
+ }
+ memcpy_pic(overlay.source->planes[0], (char *)p + offset, w * 4, h,
+ overlay.source->stride[0], stride);
+ if (map_size)
+ munmap(p, map_size);
+
+ replace_overlay(mpctx, id, &overlay);
+ return;
+error:
+ cmd->success = false;
+}
+
+static void cmd_overlay_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *cmdctx = mpctx->command_ctx;
+ int id = cmd->args[0].v.i;
+ if (id >= 0 && id < cmdctx->num_overlays)
+ replace_overlay(mpctx, id, &(struct overlay){0});
+}
+
+static void overlay_uninit(struct MPContext *mpctx)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ if (!mpctx->osd)
+ return;
+ for (int id = 0; id < cmd->num_overlays; id++)
+ replace_overlay(mpctx, id, &(struct overlay){0});
+ osd_set_external2(mpctx->osd, NULL);
+ for (int n = 0; n < 2; n++)
+ mp_image_unrefp(&cmd->overlay_osd[n].packed);
+}
+
+static void cmd_osd_overlay(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ double rc[4] = {0};
+
+ struct osd_external_ass ov = {
+ .owner = cmd->cmd->sender,
+ .id = cmd->args[0].v.i64,
+ .format = cmd->args[1].v.i,
+ .data = cmd->args[2].v.s,
+ .res_x = cmd->args[3].v.i,
+ .res_y = cmd->args[4].v.i,
+ .z = cmd->args[5].v.i,
+ .hidden = cmd->args[6].v.b,
+ .out_rc = cmd->args[7].v.b ? rc : NULL,
+ };
+
+ osd_set_external(mpctx->osd, &ov);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+
+ // (An empty rc uses INFINITY, avoid in JSON, just leave it unset.)
+ if (rc[0] < rc[2] && rc[1] < rc[3]) {
+ node_map_add_double(res, "x0", rc[0]);
+ node_map_add_double(res, "y0", rc[1]);
+ node_map_add_double(res, "x1", rc[2]);
+ node_map_add_double(res, "y1", rc[3]);
+ }
+
+ mp_wakeup_core(mpctx);
+}
+
+static struct track *find_track_with_url(struct MPContext *mpctx, int type,
+ const char *url)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track && track->type == type && track->is_external &&
+ strcmp(track->external_filename, url) == 0)
+ return track;
+ }
+ return NULL;
+}
+
+// Whether this property should react to key events generated by auto-repeat.
+static bool check_property_autorepeat(char *property, struct MPContext *mpctx)
+{
+ struct m_option prop = {0};
+ if (mp_property_do(property, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0)
+ return true;
+
+ // This is a heuristic at best.
+ if (prop.type->flags & M_OPT_TYPE_CHOICE)
+ return false;
+
+ return true;
+}
+
+// Whether changes to this property (add/cycle cmds) benefit from cmd->scale
+static bool check_property_scalable(char *property, struct MPContext *mpctx)
+{
+ struct m_option prop = {0};
+ if (mp_property_do(property, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0)
+ return true;
+
+ // These properties are backed by a floating-point number
+ return prop.type == &m_option_type_float ||
+ prop.type == &m_option_type_double ||
+ prop.type == &m_option_type_time ||
+ prop.type == &m_option_type_aspect;
+}
+
+static void show_property_status(struct mp_cmd_ctx *cmd, const char *name, int r)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct MPOpts *opts = mpctx->opts;
+ int osd_duration = opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) {
+ show_property_osd(mpctx, name, cmd->on_osd);
+ if (r == M_PROPERTY_UNAVAILABLE)
+ cmd->success = false;
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown property: '%s'", name);
+ cmd->success = false;
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Failed to set property '%s'",
+ name);
+ cmd->success = false;
+ }
+}
+
+static void change_property_cmd(struct mp_cmd_ctx *cmd,
+ const char *name, int action, void *arg)
+{
+ int r = mp_property_do(name, action, arg, cmd->mpctx);
+ show_property_status(cmd, name, r);
+}
+
+static void cmd_cycle_values(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int first = 0, dir = 1;
+
+ if (strcmp(cmd->args[first].v.s, "!reverse") == 0) {
+ first += 1;
+ dir = -1;
+ }
+
+ const char *name = cmd->args[first].v.s;
+ first += 1;
+
+ if (first >= cmd->num_args) {
+ MP_ERR(mpctx, "cycle-values command does not have any value arguments.\n");
+ cmd->success = false;
+ return;
+ }
+
+ struct m_option prop = {0};
+ int r = mp_property_do(name, M_PROPERTY_GET_TYPE, &prop, mpctx);
+ if (r <= 0) {
+ show_property_status(cmd, name, r);
+ return;
+ }
+
+ union m_option_value curval = m_option_value_default;
+ r = mp_property_do(name, M_PROPERTY_GET, &curval, mpctx);
+ if (r <= 0) {
+ show_property_status(cmd, name, r);
+ return;
+ }
+
+ int current = -1;
+ for (int n = first; n < cmd->num_args; n++) {
+ union m_option_value val = m_option_value_default;
+ if (m_option_parse(mpctx->log, &prop, bstr0(name),
+ bstr0(cmd->args[n].v.s), &val) < 0)
+ continue;
+
+ if (m_option_equal(&prop, &curval, &val))
+ current = n;
+
+ m_option_free(&prop, &val);
+
+ if (current >= 0)
+ break;
+ }
+
+ m_option_free(&prop, &curval);
+
+ if (current >= 0) {
+ current += dir;
+ if (current < first)
+ current = cmd->num_args - 1;
+ if (current >= cmd->num_args)
+ current = first;
+ } else {
+ MP_VERBOSE(mpctx, "Current value not found. Picking default.\n");
+ current = dir > 0 ? first : cmd->num_args - 1;
+ }
+
+ change_property_cmd(cmd, name, M_PROPERTY_SET_STRING, cmd->args[current].v.s);
+}
+
+struct cmd_list_ctx {
+ struct MPContext *mpctx;
+
+ // actual list command
+ struct mp_cmd_ctx *parent;
+
+ bool current_valid;
+ mp_thread_id current_tid;
+ bool completed_recursive;
+
+ // list of sub commands yet to run
+ struct mp_cmd **sub;
+ int num_sub;
+};
+
+static void continue_cmd_list(struct cmd_list_ctx *list);
+
+static void on_cmd_list_sub_completion(struct mp_cmd_ctx *cmd)
+{
+ struct cmd_list_ctx *list = cmd->on_completion_priv;
+
+ if (list->current_valid && mp_thread_id_equal(list->current_tid, mp_thread_current_id())) {
+ list->completed_recursive = true;
+ } else {
+ continue_cmd_list(list);
+ }
+}
+
+static void continue_cmd_list(struct cmd_list_ctx *list)
+{
+ while (list->parent->args[0].v.p) {
+ struct mp_cmd *sub = list->parent->args[0].v.p;
+ list->parent->args[0].v.p = sub->queue_next;
+
+ ta_set_parent(sub, NULL);
+
+ if (sub->flags & MP_ASYNC_CMD) {
+ // We run it "detached" (fire & forget)
+ run_command(list->mpctx, sub, NULL, NULL, NULL);
+ } else {
+ // Run the next command once this one completes.
+
+ list->completed_recursive = false;
+ list->current_valid = true;
+ list->current_tid = mp_thread_current_id();
+
+ run_command(list->mpctx, sub, NULL, on_cmd_list_sub_completion, list);
+
+ list->current_valid = false;
+
+ // run_command() either recursively calls the completion function,
+ // or lets the command continue run in the background. If it was
+ // completed recursively, we can just continue our loop. Otherwise
+ // the completion handler will invoke this loop again elsewhere.
+ // We could unconditionally call continue_cmd_list() in the handler
+ // instead, but then stack depth would grow with list length.
+ if (!list->completed_recursive)
+ return;
+ }
+ }
+
+ mp_cmd_ctx_complete(list->parent);
+ talloc_free(list);
+}
+
+static void cmd_list(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ cmd->completed = false;
+
+ struct cmd_list_ctx *list = talloc_zero(NULL, struct cmd_list_ctx);
+ list->mpctx = cmd->mpctx;
+ list->parent = p;
+
+ continue_cmd_list(list);
+}
+
+const struct mp_cmd_def mp_cmd_list = { "list", cmd_list, .exec_async = true };
+
+// Signal that the command is complete now. This also deallocates cmd.
+// You must call this function in a state where the core is locked for the
+// current thread (e.g. from the main thread, or from within mp_dispatch_lock()).
+// Completion means the command is finished, even if it errored or never ran.
+// Keep in mind that calling this can execute further user command that can
+// change arbitrary state (due to cmd_list).
+void mp_cmd_ctx_complete(struct mp_cmd_ctx *cmd)
+{
+ cmd->completed = true;
+ if (!cmd->success)
+ mpv_free_node_contents(&cmd->result);
+ if (cmd->on_completion)
+ cmd->on_completion(cmd);
+ if (cmd->abort)
+ mp_abort_remove(cmd->mpctx, cmd->abort);
+ mpv_free_node_contents(&cmd->result);
+ talloc_free(cmd);
+}
+
+static void run_command_on_worker_thread(void *p)
+{
+ struct mp_cmd_ctx *ctx = p;
+ struct MPContext *mpctx = ctx->mpctx;
+
+ mp_core_lock(mpctx);
+
+ bool exec_async = ctx->cmd->def->exec_async;
+ ctx->cmd->def->handler(ctx);
+ if (!exec_async)
+ mp_cmd_ctx_complete(ctx);
+
+ mpctx->outstanding_async -= 1;
+ if (!mpctx->outstanding_async && mp_is_shutting_down(mpctx))
+ mp_wakeup_core(mpctx);
+
+ mp_core_unlock(mpctx);
+}
+
+// Run the given command. Upon command completion, on_completion is called. This
+// can happen within the function, or for async commands, some time after the
+// function returns (the caller is supposed to be able to handle both cases). In
+// both cases, the callback will be called while the core is locked (i.e. you
+// can access the core freely).
+// If abort is non-NULL, then the caller creates the abort object. It must have
+// been allocated with talloc. run_command() will register/unregister/destroy
+// it. Must not be set if cmd->def->can_abort==false.
+// on_completion_priv is copied to mp_cmd_ctx.on_completion_priv and can be
+// accessed from the completion callback.
+// The completion callback is invoked exactly once. If it's NULL, it's ignored.
+// Ownership of cmd goes to the caller.
+void run_command(struct MPContext *mpctx, struct mp_cmd *cmd,
+ struct mp_abort_entry *abort,
+ void (*on_completion)(struct mp_cmd_ctx *cmd),
+ void *on_completion_priv)
+{
+ struct mp_cmd_ctx *ctx = talloc(NULL, struct mp_cmd_ctx);
+ *ctx = (struct mp_cmd_ctx){
+ .mpctx = mpctx,
+ .cmd = talloc_steal(ctx, cmd),
+ .args = cmd->args,
+ .num_args = cmd->nargs,
+ .priv = cmd->def->priv,
+ .abort = talloc_steal(ctx, abort),
+ .success = true,
+ .completed = true,
+ .on_completion = on_completion,
+ .on_completion_priv = on_completion_priv,
+ };
+
+ if (!ctx->abort && cmd->def->can_abort)
+ ctx->abort = talloc_zero(ctx, struct mp_abort_entry);
+
+ assert(cmd->def->can_abort == !!ctx->abort);
+
+ if (ctx->abort) {
+ ctx->abort->coupled_to_playback |= cmd->def->abort_on_playback_end;
+ mp_abort_add(mpctx, ctx->abort);
+ }
+
+ struct MPOpts *opts = mpctx->opts;
+ ctx->on_osd = cmd->flags & MP_ON_OSD_FLAGS;
+ bool auto_osd = ctx->on_osd == MP_ON_OSD_AUTO;
+ ctx->msg_osd = auto_osd || (ctx->on_osd & MP_ON_OSD_MSG);
+ ctx->bar_osd = auto_osd || (ctx->on_osd & MP_ON_OSD_BAR);
+ ctx->seek_msg_osd = auto_osd ? opts->osd_on_seek & 2 : ctx->msg_osd;
+ ctx->seek_bar_osd = auto_osd ? opts->osd_on_seek & 1 : ctx->bar_osd;
+
+ bool noise = cmd->def->is_noisy || cmd->mouse_move;
+ mp_cmd_dump(mpctx->log, noise ? MSGL_TRACE : MSGL_DEBUG, "Run command:", cmd);
+
+ if (cmd->flags & MP_EXPAND_PROPERTIES) {
+ for (int n = 0; n < cmd->nargs; n++) {
+ if (cmd->args[n].type->type == CONF_TYPE_STRING) {
+ char *s = mp_property_expand_string(mpctx, cmd->args[n].v.s);
+ if (!s) {
+ ctx->success = false;
+ mp_cmd_ctx_complete(ctx);
+ return;
+ }
+ talloc_free(cmd->args[n].v.s);
+ cmd->args[n].v.s = s;
+ }
+ }
+ }
+
+ if (cmd->def->spawn_thread) {
+ mpctx->outstanding_async += 1; // prevent that core disappears
+ if (!mp_thread_pool_queue(mpctx->thread_pool,
+ run_command_on_worker_thread, ctx))
+ {
+ mpctx->outstanding_async -= 1;
+ ctx->success = false;
+ mp_cmd_ctx_complete(ctx);
+ }
+ } else {
+ bool exec_async = cmd->def->exec_async;
+ cmd->def->handler(ctx);
+ if (!exec_async)
+ mp_cmd_ctx_complete(ctx);
+ }
+}
+
+// When a command shows a message. status is the level (e.g. MSGL_INFO), and
+// msg+vararg is as in printf (don't include a trailing "\n").
+void mp_cmd_msg(struct mp_cmd_ctx *cmd, int status, const char *msg, ...)
+{
+ va_list ap;
+ char *s;
+
+ va_start(ap, msg);
+ s = talloc_vasprintf(NULL, msg, ap);
+ va_end(ap);
+
+ MP_MSG(cmd->mpctx, status, "%s\n", s);
+ if (cmd->msg_osd && status <= MSGL_INFO)
+ set_osd_msg(cmd->mpctx, 1, cmd->mpctx->opts->osd_duration, "%s", s);
+
+ talloc_free(s);
+}
+
+static void cmd_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ double v = cmd->args[0].v.d * cmd->cmd->scale;
+ int abs = cmd->args[1].v.i & 3;
+ enum seek_precision precision = MPSEEK_DEFAULT;
+ switch (((cmd->args[2].v.i | cmd->args[1].v.i) >> 3) & 3) {
+ case 1: precision = MPSEEK_KEYFRAME; break;
+ case 2: precision = MPSEEK_EXACT; break;
+ }
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ mark_seek(mpctx);
+ switch (abs) {
+ case 0: { // Relative seek
+ queue_seek(mpctx, MPSEEK_RELATIVE, v, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, (v > 0) ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 1: { // Absolute seek by percentage
+ double ratio = v / 100.0;
+ double cur_pos = get_current_pos_ratio(mpctx, false);
+ queue_seek(mpctx, MPSEEK_FACTOR, ratio, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, cur_pos < ratio ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 2: { // Absolute seek to a timestamp in seconds
+ if (v < 0) {
+ // Seek from end
+ double len = get_time_length(mpctx);
+ if (len < 0) {
+ cmd->success = false;
+ return;
+ }
+ v = MPMAX(0, len + v);
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, v, precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx,
+ v > get_current_time(mpctx) ? OSD_FFW : OSD_REW);
+ break;
+ }
+ case 3: { // Relative seek by percentage
+ queue_seek(mpctx, MPSEEK_FACTOR,
+ get_current_pos_ratio(mpctx, false) + v / 100.0,
+ precision, MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, v > 0 ? OSD_FFW : OSD_REW);
+ break;
+ }}
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+}
+
+static void cmd_revert_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *cmdctx = mpctx->command_ctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ double oldpts = cmdctx->last_seek_pts;
+ if (cmdctx->marked_pts != MP_NOPTS_VALUE)
+ oldpts = cmdctx->marked_pts;
+ if (cmd->args[0].v.i & 3) {
+ cmdctx->marked_pts = get_current_time(mpctx);
+ cmdctx->marked_permanent = cmd->args[0].v.i & 1;
+ } else if (oldpts != MP_NOPTS_VALUE) {
+ if (!cmdctx->marked_permanent) {
+ cmdctx->marked_pts = MP_NOPTS_VALUE;
+ cmdctx->last_seek_pts = get_current_time(mpctx);
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, oldpts, MPSEEK_EXACT,
+ MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, OSD_REW);
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ } else {
+ cmd->success = false;
+ }
+}
+
+static void cmd_set(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ change_property_cmd(cmd, cmd->args[0].v.s,
+ M_PROPERTY_SET_STRING, cmd->args[1].v.s);
+}
+
+static void cmd_del(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ const char *name = cmd->args[0].v.s;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+ int osd_duration = mpctx->opts->osd_duration;
+
+ int r = mp_property_do(name, M_PROPERTY_DELETE, NULL, mpctx);
+
+ if (r == M_PROPERTY_OK) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Deleted property: '%s'", name);
+ cmd->success = true;
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown property: '%s'", name);
+ cmd->success = false;
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Failed to set property '%s'",
+ name);
+ cmd->success = false;
+ }
+}
+
+static void cmd_change_list(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *name = cmd->args[0].v.s;
+ char *op = cmd->args[1].v.s;
+ char *value = cmd->args[2].v.s;
+ int osd_duration = mpctx->opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ struct m_option prop = {0};
+ if (mp_property_do(name, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown option: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ const struct m_option_type *type = prop.type;
+ bool found = false;
+ for (int i = 0; type->actions && type->actions[i].name; i++) {
+ const struct m_option_action *action = &type->actions[i];
+ if (strcmp(action->name, op) == 0)
+ found = true;
+ }
+ if (!found) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Unknown action: '%s'", op);
+ cmd->success = false;
+ return;
+ }
+
+ union m_option_value val = m_option_value_default;
+ if (mp_property_do(name, M_PROPERTY_GET, &val, mpctx) <= 0) {
+ set_osd_msg(mpctx, osdl, osd_duration, "Could not read: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ char *optname = mp_tprintf(80, "%s-%s", name, op); // the dirty truth
+ int r = m_option_parse(mpctx->log, &prop, bstr0(optname), bstr0(value), &val);
+ if (r >= 0 && mp_property_do(name, M_PROPERTY_SET, &val, mpctx) <= 0)
+ r = -1;
+ m_option_free(&prop, &val);
+ if (r < 0) {
+ set_osd_msg(mpctx, osdl, osd_duration,
+ "Failed setting option: '%s'", name);
+ cmd->success = false;
+ return;
+ }
+
+ show_property_osd(mpctx, name, cmd->on_osd);
+}
+
+static void cmd_add_cycle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool is_cycle = !!cmd->priv;
+
+ char *property = cmd->args[0].v.s;
+ if (cmd->cmd->repeated && !check_property_autorepeat(property, mpctx) &&
+ !(cmd->cmd->flags & MP_ALLOW_REPEAT) /* "repeatable" prefix */ )
+ {
+ MP_VERBOSE(mpctx, "Dropping command '%s' from auto-repeated key.\n",
+ cmd->cmd->original);
+ return;
+ }
+
+ double scale = 1;
+ int scale_units = cmd->cmd->scale_units;
+ if (check_property_scalable(property, mpctx)) {
+ scale = cmd->cmd->scale;
+ scale_units = 1;
+ }
+
+ for (int i = 0; i < scale_units; i++) {
+ struct m_property_switch_arg s = {
+ .inc = cmd->args[1].v.d * scale,
+ .wrap = is_cycle,
+ };
+ change_property_cmd(cmd, property, M_PROPERTY_SWITCH, &s);
+ if (!cmd->success)
+ return;
+ }
+}
+
+static void cmd_multiply(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ change_property_cmd(cmd, cmd->args[0].v.s,
+ M_PROPERTY_MULTIPLY, &cmd->args[1].v.d);
+}
+
+static void cmd_frame_step(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ if (cmd->cmd->is_up_down) {
+ if (cmd->cmd->is_up) {
+ if (mpctx->step_frames < 1)
+ set_pause_state(mpctx, true);
+ } else {
+ if (cmd->cmd->repeated) {
+ set_pause_state(mpctx, false);
+ } else {
+ add_step_frame(mpctx, 1);
+ }
+ }
+ } else {
+ add_step_frame(mpctx, 1);
+ }
+}
+
+static void cmd_frame_back_step(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ add_step_frame(mpctx, -1);
+}
+
+static void cmd_quit(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool write_watch_later = *(bool *)cmd->priv;
+ if (write_watch_later || mpctx->opts->position_save_on_quit)
+ mp_write_watch_later_conf(mpctx);
+ mpctx->stop_play = PT_QUIT;
+ mpctx->quit_custom_rc = cmd->args[0].v.i;
+ mpctx->has_quit_custom_rc = true;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_next_prev(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int dir = *(int *)cmd->priv;
+ int force = cmd->args[0].v.i;
+
+ struct playlist_entry *e = mp_next_file(mpctx, dir, force);
+ if (!e && !force) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_set_playlist_entry(mpctx, e);
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_playlist_next_prev_playlist(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int direction = *(int *)cmd->priv;
+
+ struct playlist_entry *entry =
+ playlist_get_first_in_next_playlist(mpctx->playlist, direction);
+
+ if (!entry && mpctx->opts->loop_times != 1 && mpctx->playlist->current) {
+ entry = direction > 0 ? playlist_get_first(mpctx->playlist)
+ : playlist_get_last(mpctx->playlist);
+
+ if (entry && entry->playlist_path &&
+ mpctx->playlist->current->playlist_path &&
+ strcmp(entry->playlist_path,
+ mpctx->playlist->current->playlist_path) == 0)
+ entry = NULL;
+
+ if (direction > 0 && entry && mpctx->opts->loop_times > 1) {
+ mpctx->opts->loop_times--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->loop_times);
+ }
+
+ if (direction < 0)
+ entry = playlist_get_first_in_same_playlist(
+ entry, mpctx->playlist->current->playlist_path);
+ }
+
+ if (!entry) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_set_playlist_entry(mpctx, entry);
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_playlist_play_index(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct playlist *pl = mpctx->playlist;
+ int pos = cmd->args[0].v.i;
+
+ if (pos == -2)
+ pos = playlist_entry_to_index(pl, pl->current);
+
+ mp_set_playlist_entry(mpctx, playlist_entry_from_index(pl, pos));
+ if (cmd->on_osd & MP_ON_OSD_MSG)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_CURRENT_FILE;
+}
+
+static void cmd_sub_step_seek(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ bool step = *(bool *)cmd->priv;
+ int track_ind = cmd->args[1].v.i;
+
+ if (!mpctx->playback_initialized) {
+ cmd->success = false;
+ return;
+ }
+
+ struct track *track = mpctx->current_track[track_ind][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ double refpts = get_current_time(mpctx);
+ if (sub && refpts != MP_NOPTS_VALUE) {
+ double a[2];
+ a[0] = refpts;
+ a[1] = cmd->args[0].v.i;
+ if (sub_control(sub, SD_CTRL_SUB_STEP, a) > 0) {
+ if (step) {
+ mpctx->opts->subs_rend->sub_delay -= a[0] - refpts;
+ m_config_notify_change_opt_ptr_notify(mpctx->mconfig,
+ &mpctx->opts->subs_rend->sub_delay);
+ show_property_osd(mpctx, "sub-delay", cmd->on_osd);
+ } else {
+ // We can easily seek/step to the wrong subtitle line (because
+ // video frame PTS and sub PTS rarely match exactly). Add an
+ // arbitrary forward offset as a workaround.
+ a[0] += SUB_SEEK_OFFSET;
+ mark_seek(mpctx);
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, a[0], MPSEEK_EXACT,
+ MPSEEK_FLAG_DELAY);
+ set_osd_function(mpctx, (a[0] > refpts) ? OSD_FFW : OSD_REW);
+ if (cmd->seek_bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (cmd->seek_msg_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ }
+ }
+ }
+}
+
+static void cmd_print_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ MP_INFO(mpctx, "%s\n", cmd->args[0].v.s);
+}
+
+static void cmd_show_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int osd_duration = mpctx->opts->osd_duration;
+
+ // if no argument supplied use default osd_duration, else <arg> ms.
+ set_osd_msg(mpctx, cmd->args[2].v.i,
+ (cmd->args[1].v.i < 0 ? osd_duration : cmd->args[1].v.i),
+ "%s", cmd->args[0].v.s);
+}
+
+static void cmd_expand_text(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ cmd->result = (mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = mp_property_expand_string(mpctx, cmd->args[0].v.s)
+ };
+}
+
+static void cmd_expand_path(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ cmd->result = (mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = mp_get_user_path(NULL, mpctx->global, cmd->args[0].v.s)
+ };
+}
+
+static void cmd_loadfile(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *filename = cmd->args[0].v.s;
+ int append = cmd->args[1].v.i;
+
+ if (!append)
+ playlist_clear(mpctx->playlist);
+
+ struct playlist_entry *entry = playlist_entry_new(filename);
+ if (cmd->args[2].v.str_list) {
+ char **pairs = cmd->args[2].v.str_list;
+ for (int i = 0; pairs[i] && pairs[i + 1]; i += 2)
+ playlist_entry_add_param(entry, bstr0(pairs[i]), bstr0(pairs[i + 1]));
+ }
+ playlist_add(mpctx->playlist, entry);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "playlist_entry_id", entry->id);
+
+ if (!append || (append == 2 && !mpctx->playlist->current)) {
+ if (mpctx->opts->position_save_on_quit) // requested in issue #1148
+ mp_write_watch_later_conf(mpctx);
+ mp_set_playlist_entry(mpctx, entry);
+ }
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_loadlist(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char *filename = cmd->args[0].v.s;
+ int append = cmd->args[1].v.i;
+
+ struct playlist *pl = playlist_parse_file(filename, cmd->abort->cancel,
+ mpctx->global);
+ if (pl) {
+ prepare_playlist(mpctx, pl);
+ struct playlist_entry *new = pl->current;
+ if (!append)
+ playlist_clear(mpctx->playlist);
+ struct playlist_entry *first = playlist_entry_from_index(pl, 0);
+ int num_entries = pl->num_entries;
+ playlist_append_entries(mpctx->playlist, pl);
+ talloc_free(pl);
+
+ if (!new)
+ new = playlist_get_first(mpctx->playlist);
+
+ if ((!append || (append == 2 && !mpctx->playlist->current)) && new)
+ mp_set_playlist_entry(mpctx, new);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ if (num_entries) {
+ node_map_add_int64(res, "playlist_entry_id", first->id);
+ node_map_add_int64(res, "num_entries", num_entries);
+ }
+
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+ } else {
+ MP_ERR(mpctx, "Unable to load playlist %s.\n", filename);
+ cmd->success = false;
+ }
+}
+
+static void cmd_playlist_clear(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ // Supposed to clear the playlist, except the currently played item.
+ if (mpctx->playlist->current_was_replaced)
+ mpctx->playlist->current = NULL;
+ playlist_clear_except_current(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[0].v.i);
+ if (cmd->args[0].v.i < 0)
+ e = mpctx->playlist->current;
+ if (!e) {
+ cmd->success = false;
+ return;
+ }
+
+ // Can't play a removed entry
+ if (mpctx->playlist->current == e && !mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ playlist_remove(mpctx->playlist, e);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_playlist_move(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ struct playlist_entry *e1 = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[0].v.i);
+ struct playlist_entry *e2 = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[1].v.i);
+ if (!e1) {
+ cmd->success = false;
+ return;
+ }
+
+ playlist_move(mpctx->playlist, e1, e2);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_playlist_shuffle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ playlist_shuffle(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_playlist_unshuffle(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ playlist_unshuffle(mpctx->playlist);
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+}
+
+static void cmd_stop(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int flags = cmd->args[0].v.i;
+
+ if (!(flags & 1))
+ playlist_clear(mpctx->playlist);
+
+ if (mpctx->opts->player_idle_mode < 2 &&
+ mpctx->opts->position_save_on_quit)
+ {
+ mp_write_watch_later_conf(mpctx);
+ }
+
+ if (mpctx->stop_play != PT_QUIT)
+ mpctx->stop_play = PT_STOP;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_show_progress(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpctx->add_osd_seek_info |=
+ (cmd->msg_osd ? OSD_SEEK_INFO_TEXT : 0) |
+ (cmd->bar_osd ? OSD_SEEK_INFO_BAR : 0);
+
+ // If we got neither (i.e. no-osd) force both like osd-auto.
+ if (!mpctx->add_osd_seek_info)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT | OSD_SEEK_INFO_BAR;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+}
+
+static void cmd_track_add(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+ bool is_albumart = type == STREAM_VIDEO &&
+ cmd->args[4].v.b;
+
+ if (mpctx->stop_play) {
+ cmd->success = false;
+ return;
+ }
+
+ if (cmd->args[1].v.i == 2) {
+ struct track *t = find_track_with_url(mpctx, type, cmd->args[0].v.s);
+ if (t) {
+ if (mpctx->playback_initialized) {
+ mp_switch_track(mpctx, t->type, t, FLAG_MARK_SELECTION);
+ print_track_list(mpctx, "Track switched:");
+ } else {
+ mark_track_selection(mpctx, 0, t->type, t->user_tid);
+ }
+ return;
+ }
+ }
+ int first = mp_add_external_file(mpctx, cmd->args[0].v.s, type,
+ cmd->abort->cancel, is_albumart);
+ if (first < 0) {
+ cmd->success = false;
+ return;
+ }
+
+ for (int n = first; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (cmd->args[1].v.i == 1) {
+ t->no_default = true;
+ } else if (n == first) {
+ if (mpctx->playback_initialized) {
+ mp_switch_track(mpctx, t->type, t, FLAG_MARK_SELECTION);
+ } else {
+ mark_track_selection(mpctx, 0, t->type, t->user_tid);
+ }
+ }
+ char *title = cmd->args[2].v.s;
+ if (title && title[0])
+ t->title = talloc_strdup(t, title);
+ char *lang = cmd->args[3].v.s;
+ if (lang && lang[0])
+ t->lang = talloc_strdup(t, lang);
+ }
+
+ if (mpctx->playback_initialized)
+ print_track_list(mpctx, "Track added:");
+}
+
+static void cmd_track_remove(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ struct track *t = mp_track_by_tid(mpctx, type, cmd->args[0].v.i);
+ if (!t) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_remove_track(mpctx, t);
+ if (mpctx->playback_initialized)
+ print_track_list(mpctx, "Track removed:");
+}
+
+static void cmd_track_reload(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ if (!mpctx->playback_initialized) {
+ MP_ERR(mpctx, "Cannot reload while not initialized.\n");
+ cmd->success = false;
+ return;
+ }
+
+ struct track *t = mp_track_by_tid(mpctx, type, cmd->args[0].v.i);
+ int nt_num = -1;
+
+ if (t && t->is_external && t->external_filename) {
+ char *filename = talloc_strdup(NULL, t->external_filename);
+ bool is_albumart = t->attached_picture;
+ mp_remove_track(mpctx, t);
+ nt_num = mp_add_external_file(mpctx, filename, type, cmd->abort->cancel,
+ is_albumart);
+ talloc_free(filename);
+ }
+
+ if (nt_num < 0) {
+ cmd->success = false;
+ return;
+ }
+
+ struct track *nt = mpctx->tracks[nt_num];
+ mp_switch_track(mpctx, nt->type, nt, 0);
+ print_track_list(mpctx, "Reloaded:");
+}
+
+static void cmd_rescan_external_files(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (mpctx->stop_play) {
+ cmd->success = false;
+ return;
+ }
+
+ autoload_external_files(mpctx, cmd->abort->cancel);
+ if (!cmd->args[0].v.i && mpctx->playback_initialized) {
+ // somewhat fuzzy and not ideal
+ struct track *a = select_default_track(mpctx, 0, STREAM_AUDIO);
+ if (a && a->is_external)
+ mp_switch_track(mpctx, STREAM_AUDIO, a, 0);
+ struct track *s = select_default_track(mpctx, 0, STREAM_SUB);
+ if (s && s->is_external)
+ mp_switch_track(mpctx, STREAM_SUB, s, 0);
+
+ print_track_list(mpctx, "Track list:");
+ }
+}
+
+static void cmd_run(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char **args = talloc_zero_array(NULL, char *, cmd->num_args + 1);
+ for (int n = 0; n < cmd->num_args; n++)
+ args[n] = cmd->args[n].v.s;
+ mp_msg_flush_status_line(mpctx->log);
+ struct mp_subprocess_opts opts = {
+ .exe = args[0],
+ .args = args,
+ .fds = { {0, .src_fd = 0}, {1, .src_fd = 1}, {2, .src_fd = 2} },
+ .num_fds = 3,
+ .detach = true,
+ };
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+ if (res.error < 0) {
+ mp_err(mpctx->log, "Starting subprocess failed: %s\n",
+ mp_subprocess_err_str(res.error));
+ }
+ talloc_free(args);
+}
+
+struct subprocess_fd_ctx {
+ struct mp_log *log;
+ void* talloc_ctx;
+ int64_t max_size;
+ int msgl;
+ bool capture;
+ bstr output;
+};
+
+static void subprocess_read(void *p, char *data, size_t size)
+{
+ struct subprocess_fd_ctx *ctx = p;
+ if (ctx->capture) {
+ if (ctx->output.len < ctx->max_size)
+ bstr_xappend(ctx->talloc_ctx, &ctx->output, (bstr){data, size});
+ } else {
+ mp_msg(ctx->log, ctx->msgl, "%.*s", (int)size, data);
+ }
+}
+
+static void subprocess_write(void *p)
+{
+ // Unused; we write a full buffer.
+}
+
+static void cmd_subprocess(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ char **args = cmd->args[0].v.str_list;
+ bool playback_only = cmd->args[1].v.b;
+ bool detach = cmd->args[5].v.b;
+ char **env = cmd->args[6].v.str_list;
+ bstr stdin_data = bstr0(cmd->args[7].v.s);
+ bool passthrough_stdin = cmd->args[8].v.b;
+
+ if (env && !env[0])
+ env = NULL; // do not actually set an empty environment
+
+ if (!args || !args[0]) {
+ MP_ERR(mpctx, "program name missing\n");
+ cmd->success = false;
+ return;
+ }
+
+ if (stdin_data.len && passthrough_stdin) {
+ MP_ERR(mpctx, "both stdin_data and passthrough_stdin set\n");
+ cmd->success = false;
+ return;
+ }
+
+ void *tmp = talloc_new(NULL);
+
+ struct mp_log *fdlog = mp_log_new(tmp, mpctx->log, cmd->cmd->sender);
+ struct subprocess_fd_ctx fdctx[3];
+ for (int fd = 0; fd < 3; fd++) {
+ fdctx[fd] = (struct subprocess_fd_ctx) {
+ .log = fdlog,
+ .talloc_ctx = tmp,
+ .max_size = cmd->args[2].v.i,
+ .msgl = fd == 2 ? MSGL_ERR : MSGL_INFO,
+ };
+ }
+ fdctx[1].capture = cmd->args[3].v.b;
+ fdctx[2].capture = cmd->args[4].v.b;
+
+ mp_mutex_lock(&mpctx->abort_lock);
+ cmd->abort->coupled_to_playback = playback_only;
+ mp_abort_recheck_locked(mpctx, cmd->abort);
+ mp_mutex_unlock(&mpctx->abort_lock);
+
+ mp_core_unlock(mpctx);
+
+ struct mp_subprocess_opts opts = {
+ .exe = args[0],
+ .args = args,
+ .env = env,
+ .cancel = cmd->abort->cancel,
+ .detach = detach,
+ .fds = {
+ {
+ .fd = 0, // stdin
+ .src_fd = passthrough_stdin ? 0 : -1,
+ },
+ },
+ .num_fds = 1,
+ };
+
+ // stdout, stderr
+ for (int fd = 1; fd < 3; fd++) {
+ bool capture = fdctx[fd].capture || !detach;
+ opts.fds[opts.num_fds++] = (struct mp_subprocess_fd){
+ .fd = fd,
+ .src_fd = capture ? -1 : fd,
+ .on_read = capture ? subprocess_read : NULL,
+ .on_read_ctx = &fdctx[fd],
+ };
+ }
+ // stdin
+ if (stdin_data.len) {
+ opts.fds[0] = (struct mp_subprocess_fd){
+ .fd = 0,
+ .src_fd = -1,
+ .on_write = subprocess_write,
+ .on_write_ctx = &fdctx[0],
+ .write_buf = &stdin_data,
+ };
+ }
+
+ struct mp_subprocess_result sres;
+ mp_subprocess2(&opts, &sres);
+ int status = sres.exit_status;
+ char *error = NULL;
+ if (sres.error < 0) {
+ error = (char *)mp_subprocess_err_str(sres.error);
+ status = sres.error;
+ }
+
+ mp_core_lock(mpctx);
+
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "status", status);
+ node_map_add_flag(res, "killed_by_us", status == MP_SUBPROCESS_EKILLED_BY_US);
+ node_map_add_string(res, "error_string", error ? error : "");
+ const char *sname[] = {NULL, "stdout", "stderr"};
+ for (int fd = 1; fd < 3; fd++) {
+ if (!fdctx[fd].capture)
+ continue;
+ struct mpv_byte_array *ba =
+ node_map_add(res, sname[fd], MPV_FORMAT_BYTE_ARRAY)->u.ba;
+ *ba = (struct mpv_byte_array){
+ .data = talloc_steal(ba, fdctx[fd].output.start),
+ .size = fdctx[fd].output.len,
+ };
+ }
+
+ talloc_free(tmp);
+}
+
+static void cmd_enable_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_enable_section(mpctx->input, cmd->args[0].v.s, cmd->args[1].v.i);
+}
+
+static void cmd_disable_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_disable_section(mpctx->input, cmd->args[0].v.s);
+}
+
+static void cmd_define_input_section(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ mp_input_define_section(mpctx->input, cmd->args[0].v.s, "<api>",
+ cmd->args[1].v.s, !cmd->args[2].v.i,
+ cmd->cmd->sender);
+}
+
+static void cmd_ab_loop(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int osd_duration = mpctx->opts->osd_duration;
+ int osdl = cmd->msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ double now = get_current_time(mpctx);
+ if (mpctx->opts->ab_loop[0] == MP_NOPTS_VALUE) {
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &now, mpctx);
+ show_property_osd(mpctx, "ab-loop-a", cmd->on_osd);
+ } else if (mpctx->opts->ab_loop[1] == MP_NOPTS_VALUE) {
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &now, mpctx);
+ show_property_osd(mpctx, "ab-loop-b", cmd->on_osd);
+ } else {
+ now = MP_NOPTS_VALUE;
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &now, mpctx);
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &now, mpctx);
+ set_osd_msg(mpctx, osdl, osd_duration, "Clear A-B loop");
+ }
+}
+
+static void cmd_align_cache_ab(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ if (!mpctx->demuxer)
+ return;
+
+ double a = demux_probe_cache_dump_target(mpctx->demuxer,
+ mpctx->opts->ab_loop[0], false);
+ double b = demux_probe_cache_dump_target(mpctx->demuxer,
+ mpctx->opts->ab_loop[1], true);
+
+ mp_property_do("ab-loop-a", M_PROPERTY_SET, &a, mpctx);
+ mp_property_do("ab-loop-b", M_PROPERTY_SET, &b, mpctx);
+
+ // Happens to cover both properties.
+ show_property_osd(mpctx, "ab-loop-b", cmd->on_osd);
+}
+
+static void cmd_drop_buffers(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ reset_playback_state(mpctx);
+
+ if (mpctx->demuxer)
+ demux_flush(mpctx->demuxer);
+}
+
+static void cmd_ao_reload(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ reload_audio_output(mpctx);
+}
+
+static void cmd_filter(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+ cmd->success = edit_filters_osd(mpctx, type, cmd->args[0].v.s,
+ cmd->args[1].v.s, cmd->msg_osd) >= 0;
+}
+
+static void cmd_filter_command(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int type = *(int *)cmd->priv;
+
+ struct mp_output_chain *chain = NULL;
+ if (type == STREAM_VIDEO)
+ chain = mpctx->vo_chain ? mpctx->vo_chain->filter : NULL;
+ if (type == STREAM_AUDIO)
+ chain = mpctx->ao_chain ? mpctx->ao_chain->filter : NULL;
+ if (!chain) {
+ cmd->success = false;
+ return;
+ }
+ struct mp_filter_command filter_cmd = {
+ .type = MP_FILTER_COMMAND_TEXT,
+ .target = cmd->args[3].v.s,
+ .cmd = cmd->args[1].v.s,
+ .arg = cmd->args[2].v.s,
+ };
+ cmd->success = mp_output_chain_command(chain, cmd->args[0].v.s, &filter_cmd);
+}
+
+static void cmd_script_binding(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct mp_cmd *incmd = cmd->cmd;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpv_event_client_message event = {0};
+ char *name = cmd->args[0].v.s;
+ if (!name || !name[0]) {
+ cmd->success = false;
+ return;
+ }
+
+ char *sep = strchr(name, '/');
+ char *target = NULL;
+ char space[MAX_CLIENT_NAME];
+ if (sep) {
+ snprintf(space, sizeof(space), "%.*s", (int)(sep - name), name);
+ target = space;
+ name = sep + 1;
+ }
+ char state[3] = {'p', incmd->is_mouse_button ? 'm' : '-'};
+ if (incmd->is_up_down)
+ state[0] = incmd->repeated ? 'r' : (incmd->is_up ? 'u' : 'd');
+ event.num_args = 5;
+ event.args = (const char*[5]){"key-binding", name, state,
+ incmd->key_name ? incmd->key_name : "",
+ incmd->key_text ? incmd->key_text : ""};
+ if (mp_client_send_event_dup(mpctx, target,
+ MPV_EVENT_CLIENT_MESSAGE, &event) < 0)
+ {
+ MP_VERBOSE(mpctx, "Can't find script '%s' when handling input.\n",
+ target ? target : "-");
+ cmd->success = false;
+ }
+}
+
+static void cmd_script_message_to(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mpv_event_client_message *event = talloc_ptrtype(NULL, event);
+ *event = (mpv_event_client_message){0};
+ for (int n = 1; n < cmd->num_args; n++) {
+ MP_TARRAY_APPEND(event, event->args, event->num_args,
+ talloc_strdup(event, cmd->args[n].v.s));
+ }
+ if (mp_client_send_event(mpctx, cmd->args[0].v.s, 0,
+ MPV_EVENT_CLIENT_MESSAGE, event) < 0)
+ {
+ MP_VERBOSE(mpctx, "Can't find script '%s' to send message to.\n",
+ cmd->args[0].v.s);
+ cmd->success = false;
+ }
+}
+
+static void cmd_script_message(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ const char **args = talloc_array(NULL, const char *, cmd->num_args);
+ mpv_event_client_message event = {.args = args};
+ for (int n = 0; n < cmd->num_args; n++)
+ event.args[event.num_args++] = cmd->args[n].v.s;
+ mp_client_broadcast_event(mpctx, MPV_EVENT_CLIENT_MESSAGE, &event);
+ talloc_free(args);
+}
+
+static void cmd_ignore(void *p)
+{
+}
+
+static void cmd_write_watch_later_config(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ mp_write_watch_later_conf(mpctx);
+}
+
+static void cmd_delete_watch_later_config(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *filename = cmd->args[0].v.s;
+ if (filename && !*filename)
+ filename = NULL;
+ mp_delete_watch_later_conf(mpctx, filename);
+}
+
+static void cmd_mouse(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int pre_key = 0;
+
+ const int x = cmd->args[0].v.i, y = cmd->args[1].v.i;
+ int button = cmd->args[2].v.i;
+
+ if (mpctx->video_out && mpctx->video_out->config_ok) {
+ int oldx, oldy, oldhover;
+ mp_input_get_mouse_pos(mpctx->input, &oldx, &oldy, &oldhover);
+ struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd);
+
+ // TODO: VOs don't send outside positions. should we abort if outside?
+ int hover = x >= 0 && y >= 0 && x < vo_res.w && y < vo_res.h;
+
+ if (vo_res.w && vo_res.h && hover != oldhover)
+ pre_key = hover ? MP_KEY_MOUSE_ENTER : MP_KEY_MOUSE_LEAVE;
+ }
+
+ if (button == -1) {// no button
+ if (pre_key)
+ mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
+ return;
+ }
+ if (button < 0 || button >= MP_KEY_MOUSE_BTN_COUNT) {// invalid button
+ MP_ERR(mpctx, "%d is not a valid mouse button number.\n", button);
+ cmd->success = false;
+ return;
+ }
+ const bool dbc = cmd->args[3].v.i;
+ if (dbc && button > (MP_MBTN_RIGHT - MP_MBTN_BASE)) {
+ MP_ERR(mpctx, "%d is not a valid mouse button for double-clicks.\n",
+ button);
+ cmd->success = false;
+ return;
+ }
+ button += dbc ? MP_MBTN_DBL_BASE : MP_MBTN_BASE;
+ if (pre_key)
+ mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
+ mp_input_put_key_artificial(mpctx->input, button);
+}
+
+static void cmd_key(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ int action = *(int *)cmd->priv;
+
+ const char *key_name = cmd->args[0].v.s;
+ if (key_name[0] == '\0' && action == MP_KEY_STATE_UP) {
+ mp_input_put_key_artificial(mpctx->input, MP_INPUT_RELEASE_ALL);
+ } else {
+ int code = mp_input_get_key_from_name(key_name);
+ if (code < 0) {
+ MP_ERR(mpctx, "%s is not a valid input name.\n", key_name);
+ cmd->success = false;
+ return;
+ }
+ mp_input_put_key_artificial(mpctx->input, code | action);
+ }
+}
+
+static void cmd_key_bind(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ int code = mp_input_get_key_from_name(cmd->args[0].v.s);
+ if (code < 0) {
+ MP_ERR(mpctx, "%s is not a valid input name.\n", cmd->args[0].v.s);
+ cmd->success = false;
+ return;
+ }
+ const char *target_cmd = cmd->args[1].v.s;
+ mp_input_bind_key(mpctx->input, code, bstr0(target_cmd));
+}
+
+static void cmd_apply_profile(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *profile = cmd->args[0].v.s;
+ int mode = cmd->args[1].v.i;
+ if (mode == 0) {
+ cmd->success = m_config_set_profile(mpctx->mconfig, profile, 0) >= 0;
+ } else {
+ cmd->success = m_config_restore_profile(mpctx->mconfig, profile) >= 0;
+ }
+}
+
+static void cmd_load_script(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *script = cmd->args[0].v.s;
+ int64_t id = mp_load_user_script(mpctx, script);
+ if (id > 0) {
+ struct mpv_node *res = &cmd->result;
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "client_id", id);
+ } else {
+ cmd->success = false;
+ }
+}
+
+static void cache_dump_poll(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ struct mp_cmd_ctx *cmd = ctx->cache_dump_cmd;
+
+ if (!cmd)
+ return;
+
+ // Can't close demuxer without stopping dumping.
+ assert(mpctx->demuxer);
+
+ if (mp_cancel_test(cmd->abort->cancel)) {
+ // Synchronous abort. In particular, the dump command shall not report
+ // completion to the user before the dump target file was closed.
+ demux_cache_dump_set(mpctx->demuxer, 0, 0, NULL);
+ assert(demux_cache_dump_get_status(mpctx->demuxer) <= 0);
+ }
+
+ int status = demux_cache_dump_get_status(mpctx->demuxer);
+ if (status <= 0) {
+ if (status < 0) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Cache dumping stopped due to error.");
+ cmd->success = false;
+ } else {
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping successfully ended.");
+ cmd->success = true;
+ }
+ ctx->cache_dump_cmd = NULL;
+ mp_cmd_ctx_complete(cmd);
+ }
+}
+
+void mp_abort_cache_dumping(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ if (ctx->cache_dump_cmd)
+ mp_cancel_trigger(ctx->cache_dump_cmd->abort->cancel);
+ cache_dump_poll(mpctx);
+ assert(!ctx->cache_dump_cmd); // synchronous abort, must have worked
+}
+
+static void run_dump_cmd(struct mp_cmd_ctx *cmd, double start, double end,
+ char *filename)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ mp_abort_cache_dumping(mpctx);
+
+ if (!mpctx->demuxer) {
+ mp_cmd_msg(cmd, MSGL_ERR, "No demuxer open.");
+ cmd->success = false;
+ mp_cmd_ctx_complete(cmd);
+ return;
+ }
+
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping started.");
+
+ if (!demux_cache_dump_set(mpctx->demuxer, start, end, filename)) {
+ mp_cmd_msg(cmd, MSGL_INFO, "Cache dumping stopped.");
+ mp_cmd_ctx_complete(cmd);
+ return;
+ }
+
+ ctx->cache_dump_cmd = cmd;
+ cache_dump_poll(mpctx);
+}
+
+static void cmd_dump_cache(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+
+ run_dump_cmd(cmd, cmd->args[0].v.d, cmd->args[1].v.d, cmd->args[2].v.s);
+}
+
+static void cmd_dump_cache_ab(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ run_dump_cmd(cmd, mpctx->opts->ab_loop[0], mpctx->opts->ab_loop[1],
+ cmd->args[0].v.s);
+}
+
+/* This array defines all known commands.
+ * The first field the command name used in libmpv and input.conf.
+ * The second field is the handler function (see mp_cmd_def.handler and
+ * run_command()).
+ * Then comes the definition of each argument. They are defined like options,
+ * except that the result is parsed into mp_cmd.args[] (thus the option variable
+ * is a field in the mp_cmd_arg union field). Arguments are optional if either
+ * defval is set (usually via OPTDEF_ macros), or the MP_CMD_OPT_ARG flag is
+ * set, or if it's the last argument and .vararg is set. If .vararg is set, the
+ * command has an arbitrary number of arguments, all using the type indicated by
+ * the last argument (they are appended to mp_cmd.args[] starting at the last
+ * argument's index).
+ * Arguments have names, which can be used by named argument functions, e.g. in
+ * Lua with mp.command_native().
+ */
+
+// This does not specify the real destination of the command parameter values,
+// it just provides a dummy for the OPT_ macros. The real destination is an
+// array item in mp_cmd.args[], using the index of the option definition.
+#define OPT_BASE_STRUCT struct mp_cmd_arg
+
+const struct mp_cmd_def mp_cmds[] = {
+ { "ignore", cmd_ignore, .is_ignore = true, .is_noisy = true, },
+
+ { "seek", cmd_seek,
+ {
+ {"target", OPT_TIME(v.d)},
+ {"flags", OPT_FLAGS(v.i,
+ {"relative", 4|0}, {"-", 4|0},
+ {"absolute-percent", 4|1},
+ {"absolute", 4|2},
+ {"relative-percent", 4|3},
+ {"keyframes", 32|8},
+ {"exact", 32|16}),
+ OPTDEF_INT(4|0)},
+ // backwards compatibility only
+ {"legacy", OPT_CHOICE(v.i,
+ {"unused", 0}, {"default-precise", 0},
+ {"keyframes", 32|8},
+ {"exact", 32|16}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ },
+ { "revert-seek", cmd_revert_seek,
+ { {"flags", OPT_FLAGS(v.i, {"mark", 2|0}, {"mark-permanent", 2|1}),
+ .flags = MP_CMD_OPT_ARG} },
+ },
+ { "quit", cmd_quit, { {"code", OPT_INT(v.i), .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const bool){0} },
+ { "quit-watch-later", cmd_quit, { {"code", OPT_INT(v.i),
+ .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const bool){1} },
+ { "stop", cmd_stop,
+ { {"flags", OPT_FLAGS(v.i, {"keep-playlist", 1}), .flags = MP_CMD_OPT_ARG} }
+ },
+ { "frame-step", cmd_frame_step, .allow_auto_repeat = true,
+ .on_updown = true },
+ { "frame-back-step", cmd_frame_back_step, .allow_auto_repeat = true },
+ { "playlist-next", cmd_playlist_next_prev,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"weak", 0},
+ {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){1},
+ },
+ { "playlist-prev", cmd_playlist_next_prev,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"weak", 0},
+ {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){-1},
+ },
+ { "playlist-next-playlist", cmd_playlist_next_prev_playlist,
+ .priv = &(const int){1} },
+ { "playlist-prev-playlist", cmd_playlist_next_prev_playlist,
+ .priv = &(const int){-1} },
+ { "playlist-play-index", cmd_playlist_play_index,
+ {
+ {"index", OPT_CHOICE(v.i, {"current", -2}, {"none", -1}),
+ M_RANGE(-1, INT_MAX)},
+ }
+ },
+ { "playlist-shuffle", cmd_playlist_shuffle, },
+ { "playlist-unshuffle", cmd_playlist_unshuffle, },
+ { "sub-step", cmd_sub_step_seek,
+ {
+ {"skip", OPT_INT(v.i)},
+ {"flags", OPT_CHOICE(v.i,
+ {"primary", 0},
+ {"secondary", 1}),
+ OPTDEF_INT(0)},
+ },
+ .allow_auto_repeat = true,
+ .priv = &(const bool){true}
+ },
+ { "sub-seek", cmd_sub_step_seek,
+ {
+ {"skip", OPT_INT(v.i)},
+ {"flags", OPT_CHOICE(v.i,
+ {"primary", 0},
+ {"secondary", 1}),
+ OPTDEF_INT(0)},
+ },
+ .allow_auto_repeat = true,
+ .priv = &(const bool){false}
+ },
+ { "print-text", cmd_print_text, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true, .allow_auto_repeat = true },
+ { "show-text", cmd_show_text,
+ {
+ {"text", OPT_STRING(v.s)},
+ {"duration", OPT_INT(v.i), OPTDEF_INT(-1)},
+ {"level", OPT_INT(v.i), .flags = MP_CMD_OPT_ARG},
+ },
+ .is_noisy = true, .allow_auto_repeat = true},
+ { "expand-text", cmd_expand_text, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true },
+ { "expand-path", cmd_expand_path, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true },
+ { "show-progress", cmd_show_progress, .allow_auto_repeat = true,
+ .is_noisy = true },
+
+ { "sub-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_SUB},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "audio-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_AUDIO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "video-add", cmd_track_add,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i, {"select", 0}, {"auto", 1}, {"cached", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"title", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"lang", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"albumart", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_VIDEO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "sub-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_SUB}, },
+ { "audio-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_AUDIO}, },
+ { "video-remove", cmd_track_remove, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_VIDEO}, },
+
+ { "sub-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_SUB},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "audio-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_AUDIO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+ { "video-reload", cmd_track_reload, { {"id", OPT_INT(v.i), OPTDEF_INT(-1)} },
+ .priv = &(const int){STREAM_VIDEO},
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "rescan-external-files", cmd_rescan_external_files,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"keep-selection", 1},
+ {"reselect", 0}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ .abort_on_playback_end = true,
+ },
+
+ { "screenshot", cmd_screenshot,
+ {
+ {"flags", OPT_FLAGS(v.i,
+ {"video", 4|0}, {"-", 4|0},
+ {"window", 4|1},
+ {"subtitles", 4|2},
+ {"each-frame", 8}),
+ OPTDEF_INT(4|2)},
+ // backwards compatibility
+ {"legacy", OPT_CHOICE(v.i,
+ {"unused", 0}, {"single", 0},
+ {"each-frame", 8}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ },
+ { "screenshot-to-file", cmd_screenshot_to_file,
+ {
+ {"filename", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"video", 0},
+ {"window", 1},
+ {"subtitles", 2}),
+ OPTDEF_INT(2)},
+ },
+ .spawn_thread = true,
+ },
+ { "screenshot-raw", cmd_screenshot_raw,
+ {
+ {"flags", OPT_CHOICE(v.i,
+ {"video", 0},
+ {"window", 1},
+ {"subtitles", 2}),
+ OPTDEF_INT(2)},
+ },
+ },
+ { "loadfile", cmd_loadfile,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"replace", 0},
+ {"append", 1},
+ {"append-play", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ {"options", OPT_KEYVALUELIST(v.str_list), .flags = MP_CMD_OPT_ARG},
+ },
+ },
+ { "loadlist", cmd_loadlist,
+ {
+ {"url", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i,
+ {"replace", 0},
+ {"append", 1},
+ {"append-play", 2}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ },
+ { "playlist-clear", cmd_playlist_clear },
+ { "playlist-remove", cmd_playlist_remove, {
+ {"index", OPT_CHOICE(v.i, {"current", -1}),
+ .flags = MP_CMD_OPT_ARG, M_RANGE(0, INT_MAX)}, }},
+ { "playlist-move", cmd_playlist_move, { {"index1", OPT_INT(v.i)},
+ {"index2", OPT_INT(v.i)}, }},
+ { "run", cmd_run, { {"command", OPT_STRING(v.s)},
+ {"args", OPT_STRING(v.s)}, },
+ .vararg = true,
+ },
+ { "subprocess", cmd_subprocess,
+ {
+ {"args", OPT_STRINGLIST(v.str_list)},
+ {"playback_only", OPT_BOOL(v.b), OPTDEF_INT(1)},
+ {"capture_size", OPT_BYTE_SIZE(v.i64), M_RANGE(0, INT_MAX),
+ OPTDEF_INT64(64 * 1024 * 1024)},
+ {"capture_stdout", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"capture_stderr", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"detach", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ {"env", OPT_STRINGLIST(v.str_list), .flags = MP_CMD_OPT_ARG},
+ {"stdin_data", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG},
+ {"passthrough_stdin", OPT_BOOL(v.b), .flags = MP_CMD_OPT_ARG},
+ },
+ .spawn_thread = true,
+ .can_abort = true,
+ },
+
+ { "set", cmd_set, {{"name", OPT_STRING(v.s)}, {"value", OPT_STRING(v.s)}}},
+ { "del", cmd_del, {{"name", OPT_STRING(v.s)}}},
+ { "change-list", cmd_change_list, { {"name", OPT_STRING(v.s)},
+ {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)} }},
+ { "add", cmd_add_cycle, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(1)}, },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ },
+ { "cycle", cmd_add_cycle, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_CYCLEDIR(v.d), OPTDEF_DOUBLE(1)}, },
+ .allow_auto_repeat = true,
+ .scalable = true,
+ .priv = "",
+ },
+ { "multiply", cmd_multiply, { {"name", OPT_STRING(v.s)},
+ {"value", OPT_DOUBLE(v.d)}},
+ .allow_auto_repeat = true},
+
+ { "cycle-values", cmd_cycle_values, { {"arg0", OPT_STRING(v.s)},
+ {"arg1", OPT_STRING(v.s)},
+ {"argN", OPT_STRING(v.s)}, },
+ .vararg = true},
+
+ { "enable-section", cmd_enable_input_section,
+ {
+ {"name", OPT_STRING(v.s)},
+ {"flags", OPT_FLAGS(v.i,
+ {"default", 0},
+ {"exclusive", MP_INPUT_EXCLUSIVE},
+ {"allow-hide-cursor", MP_INPUT_ALLOW_HIDE_CURSOR},
+ {"allow-vo-dragging", MP_INPUT_ALLOW_VO_DRAGGING}),
+ .flags = MP_CMD_OPT_ARG},
+ }
+ },
+ { "disable-section", cmd_disable_input_section,
+ {{"name", OPT_STRING(v.s)} }},
+ { "define-section", cmd_define_input_section,
+ {
+ {"name", OPT_STRING(v.s)},
+ {"contents", OPT_STRING(v.s)},
+ {"flags", OPT_CHOICE(v.i, {"default", 0}, {"force", 1}),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ },
+
+ { "ab-loop", cmd_ab_loop },
+
+ { "drop-buffers", cmd_drop_buffers, },
+
+ { "af", cmd_filter, { {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)}, },
+ .priv = &(const int){STREAM_AUDIO} },
+ { "vf", cmd_filter, { {"operation", OPT_STRING(v.s)},
+ {"value", OPT_STRING(v.s)}, },
+ .priv = &(const int){STREAM_VIDEO} },
+
+ { "af-command", cmd_filter_command,
+ {
+ {"label", OPT_STRING(v.s)},
+ {"command", OPT_STRING(v.s)},
+ {"argument", OPT_STRING(v.s)},
+ {"target", OPT_STRING(v.s), OPTDEF_STR("all"),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_AUDIO} },
+ { "vf-command", cmd_filter_command,
+ {
+ {"label", OPT_STRING(v.s)},
+ {"command", OPT_STRING(v.s)},
+ {"argument", OPT_STRING(v.s)},
+ {"target", OPT_STRING(v.s), OPTDEF_STR("all"),
+ .flags = MP_CMD_OPT_ARG},
+ },
+ .priv = &(const int){STREAM_VIDEO} },
+
+ { "ao-reload", cmd_ao_reload },
+
+ { "script-binding", cmd_script_binding, { {"name", OPT_STRING(v.s)} },
+ .allow_auto_repeat = true, .on_updown = true},
+
+ { "script-message", cmd_script_message, { {"args", OPT_STRING(v.s)} },
+ .vararg = true },
+ { "script-message-to", cmd_script_message_to, { {"target", OPT_STRING(v.s)},
+ {"args", OPT_STRING(v.s)} },
+ .vararg = true },
+
+ { "overlay-add", cmd_overlay_add, { {"id", OPT_INT(v.i)},
+ {"x", OPT_INT(v.i)},
+ {"y", OPT_INT(v.i)},
+ {"file", OPT_STRING(v.s)},
+ {"offset", OPT_INT(v.i)},
+ {"fmt", OPT_STRING(v.s)},
+ {"w", OPT_INT(v.i)},
+ {"h", OPT_INT(v.i)},
+ {"stride", OPT_INT(v.i)}, }},
+ { "overlay-remove", cmd_overlay_remove, { {"id", OPT_INT(v.i)} } },
+
+ { "osd-overlay", cmd_osd_overlay,
+ {
+ {"id", OPT_INT64(v.i64)},
+ {"format", OPT_CHOICE(v.i, {"none", 0}, {"ass-events", 1})},
+ {"data", OPT_STRING(v.s)},
+ {"res_x", OPT_INT(v.i), OPTDEF_INT(0)},
+ {"res_y", OPT_INT(v.i), OPTDEF_INT(720)},
+ {"z", OPT_INT(v.i), OPTDEF_INT(0)},
+ {"hidden", OPT_BOOL(v.b), OPTDEF_INT(0)},
+ {"compute_bounds", OPT_BOOL(v.b), OPTDEF_INT(0)},
+ },
+ .is_noisy = true,
+ },
+
+ { "write-watch-later-config", cmd_write_watch_later_config },
+ { "delete-watch-later-config", cmd_delete_watch_later_config,
+ {{"filename", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG} }},
+
+ { "mouse", cmd_mouse, { {"x", OPT_INT(v.i)},
+ {"y", OPT_INT(v.i)},
+ {"button", OPT_INT(v.i), OPTDEF_INT(-1)},
+ {"mode", OPT_CHOICE(v.i,
+ {"single", 0}, {"double", 1}),
+ .flags = MP_CMD_OPT_ARG}}},
+ { "keybind", cmd_key_bind, { {"name", OPT_STRING(v.s)},
+ {"cmd", OPT_STRING(v.s)} }},
+ { "keypress", cmd_key, { {"name", OPT_STRING(v.s)} },
+ .priv = &(const int){0}},
+ { "keydown", cmd_key, { {"name", OPT_STRING(v.s)} },
+ .priv = &(const int){MP_KEY_STATE_DOWN}},
+ { "keyup", cmd_key, { {"name", OPT_STRING(v.s), .flags = MP_CMD_OPT_ARG} },
+ .priv = &(const int){MP_KEY_STATE_UP}},
+
+ { "apply-profile", cmd_apply_profile, {
+ {"name", OPT_STRING(v.s)},
+ {"mode", OPT_CHOICE(v.i, {"apply", 0}, {"restore", 1}),
+ .flags = MP_CMD_OPT_ARG}, }
+ },
+
+ { "load-script", cmd_load_script, {{"filename", OPT_STRING(v.s)}} },
+
+ { "dump-cache", cmd_dump_cache, { {"start", OPT_TIME(v.d),
+ .flags = M_OPT_ALLOW_NO},
+ {"end", OPT_TIME(v.d),
+ .flags = M_OPT_ALLOW_NO},
+ {"filename", OPT_STRING(v.s)} },
+ .exec_async = true,
+ .can_abort = true,
+ },
+
+ { "ab-loop-dump-cache", cmd_dump_cache_ab, { {"filename", OPT_STRING(v.s)} },
+ .exec_async = true,
+ .can_abort = true,
+ },
+
+ { "ab-loop-align-cache", cmd_align_cache_ab },
+
+ {0}
+};
+
+#undef OPT_BASE_STRUCT
+#undef ARG
+
+void command_uninit(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ assert(!ctx->cache_dump_cmd); // closing the demuxer must have aborted it
+
+ overlay_uninit(mpctx);
+ ao_hotplug_destroy(ctx->hotplug);
+
+ m_option_free(&script_props_type, &ctx->script_props);
+
+ talloc_free(mpctx->command_ctx);
+ mpctx->command_ctx = NULL;
+}
+
+void command_init(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = talloc(NULL, struct command_ctx);
+ *ctx = (struct command_ctx){
+ .last_seek_pts = MP_NOPTS_VALUE,
+ };
+ mpctx->command_ctx = ctx;
+
+ int num_base = MP_ARRAY_SIZE(mp_properties_base);
+ int num_opts = m_config_get_co_count(mpctx->mconfig);
+ ctx->properties =
+ talloc_zero_array(ctx, struct m_property, num_base + num_opts + 1);
+ memcpy(ctx->properties, mp_properties_base, sizeof(mp_properties_base));
+
+ int count = num_base;
+ for (int n = 0; n < num_opts; n++) {
+ struct m_config_option *co = m_config_get_co_index(mpctx->mconfig, n);
+ assert(co->name[0]);
+ if (co->opt->flags & M_OPT_NOPROP)
+ continue;
+
+ struct m_property prop = {
+ .name = co->name,
+ .call = mp_property_generic_option,
+ .is_option = true,
+ };
+
+ if (co->opt->type == &m_option_type_alias) {
+ prop.priv = co->opt->priv;
+
+ prop.call = co->opt->deprecation_message ?
+ mp_property_deprecated_alias : mp_property_alias;
+
+ // Check whether this eventually arrives at a real option. If not,
+ // it's some CLI special handling thing. For example, "nosound" is
+ // mapped to "no-audio", which has CLI special-handling, and cannot
+ // be set as property.
+ struct m_config_option *co2 = co;
+ while (co2 && co2->opt->type == &m_option_type_alias) {
+ const char *alias = (const char *)co2->opt->priv;
+ co2 = m_config_get_co_raw(mpctx->mconfig, bstr0(alias));
+ }
+ if (!co2)
+ continue;
+ }
+
+ // The option might be covered by a manual property already.
+ if (m_property_list_find(ctx->properties, prop.name))
+ continue;
+
+ ctx->properties[count++] = prop;
+ }
+
+ node_init(&ctx->udata, MPV_FORMAT_NODE_MAP, NULL);
+ talloc_steal(ctx, ctx->udata.u.list);
+}
+
+static void command_event(struct MPContext *mpctx, int event, void *arg)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ if (event == MPV_EVENT_START_FILE) {
+ ctx->last_seek_pts = MP_NOPTS_VALUE;
+ ctx->marked_pts = MP_NOPTS_VALUE;
+ ctx->marked_permanent = false;
+ }
+
+ if (event == MPV_EVENT_PLAYBACK_RESTART)
+ ctx->last_seek_time = mp_time_sec();
+
+ if (event == MPV_EVENT_END_FILE || event == MPV_EVENT_FILE_LOADED) {
+ // Update chapters - does nothing if something else is visible.
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ }
+ if (event == MP_EVENT_WIN_STATE2)
+ ctx->cached_window_scale = 0;
+
+ if (event == MP_EVENT_METADATA_UPDATE) {
+ struct playlist_entry *const pe = mpctx->playing;
+ if (pe && !pe->title) {
+ const char *const name = find_non_filename_media_title(mpctx);
+ if (name && name[0]) {
+ pe->title = talloc_strdup(pe, name);
+ mp_notify_property(mpctx, "playlist");
+ }
+ }
+ }
+}
+
+void handle_command_updates(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ // This is a bit messy: ao_hotplug wakes up the player, and then we have
+ // to recheck the state. Then the client(s) will read the property.
+ if (ctx->hotplug && ao_hotplug_check_update(ctx->hotplug))
+ mp_notify_property(mpctx, "audio-device-list");
+
+ // Depends on polling demuxer wakeup callback notifications.
+ cache_dump_poll(mpctx);
+}
+
+void mp_notify(struct MPContext *mpctx, int event, void *arg)
+{
+ // The OSD can implicitly reference some properties.
+ mpctx->osd_idle_update = true;
+
+ command_event(mpctx, event, arg);
+
+ mp_client_broadcast_event(mpctx, event, arg);
+}
+
+static void update_priority(struct MPContext *mpctx)
+{
+#if HAVE_WIN32_DESKTOP
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->w32_priority > 0)
+ SetPriorityClass(GetCurrentProcess(), opts->w32_priority);
+#endif
+}
+
+static void update_track_switch(struct MPContext *mpctx, int order, int type)
+{
+ if (!mpctx->playback_initialized)
+ return;
+
+ int tid = mpctx->opts->stream_id[order][type];
+ struct track *track;
+ if (tid == -1) {
+ // If "auto" reset to default track selection
+ track = select_default_track(mpctx, order, type);
+ mark_track_selection(mpctx, order, type, -1);
+ } else {
+ track = mp_track_by_tid(mpctx, type, tid);
+ }
+ mp_switch_track_n(mpctx, order, type, track, (tid == -1) ? 0 : FLAG_MARK_SELECTION);
+ print_track_list(mpctx, "Track switched:");
+ mp_wakeup_core(mpctx);
+}
+
+void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
+ bool self_update)
+{
+ struct MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+ bool init = !co;
+ void *opt_ptr = init ? NULL : co->data; // NULL on start
+
+ if (co)
+ mp_notify_property(mpctx, co->name);
+ if (opt_ptr == &opts->media_title)
+ mp_notify(mpctx, MP_EVENT_METADATA_UPDATE, NULL);
+
+ if (self_update)
+ return;
+
+ if (flags & UPDATE_TERM)
+ mp_update_logging(mpctx, false);
+
+ if (flags & (UPDATE_OSD | UPDATE_SUB_FILT | UPDATE_SUB_HARD)) {
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ struct track *track = mpctx->current_track[n][STREAM_SUB];
+ struct dec_sub *sub = track ? track->d_sub : NULL;
+ if (sub) {
+ int ret = sub_control(sub, SD_CTRL_UPDATE_OPTS,
+ (void *)(uintptr_t)flags);
+ if (ret == CONTROL_OK && flags & (UPDATE_SUB_FILT | UPDATE_SUB_HARD))
+ sub_redecode_cached_packets(sub);
+ }
+ }
+ osd_changed(mpctx->osd);
+ }
+
+ if (flags & UPDATE_BUILTIN_SCRIPTS)
+ mp_load_builtin_scripts(mpctx);
+
+ if (flags & UPDATE_IMGPAR) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ if (track && track->dec) {
+ mp_decoder_wrapper_reset_params(track->dec);
+ mp_force_video_refresh(mpctx);
+ }
+ }
+
+ if (flags & UPDATE_INPUT)
+ mp_input_update_opts(mpctx->input);
+
+ if (flags & UPDATE_SUB_EXTS)
+ mp_update_subtitle_exts(mpctx->opts);
+
+ if (init || opt_ptr == &opts->ipc_path || opt_ptr == &opts->ipc_client) {
+ mp_uninit_ipc(mpctx->ipc_ctx);
+ mpctx->ipc_ctx = mp_init_ipc(mpctx->clients, mpctx->global);
+ }
+
+ if (opt_ptr == &opts->vo->video_driver_list) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ uninit_video_out(mpctx);
+ handle_force_window(mpctx, true);
+ reinit_video_chain(mpctx);
+ if (track)
+ reselect_demux_stream(mpctx, track, true);
+
+ mp_wakeup_core(mpctx);
+ }
+
+ if (flags & UPDATE_AUDIO)
+ reload_audio_output(mpctx);
+
+ if (flags & UPDATE_PRIORITY)
+ update_priority(mpctx);
+
+ if (flags & UPDATE_SCREENSAVER)
+ update_screensaver_state(mpctx);
+
+ if (flags & UPDATE_VOL)
+ audio_update_volume(mpctx);
+
+ if (flags & UPDATE_LAVFI_COMPLEX)
+ update_lavfi_complex(mpctx);
+
+ if (opt_ptr == &opts->vo->android_surface_size) {
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_EXTERNAL_RESIZE, NULL);
+ }
+
+ if (opt_ptr == &opts->playback_speed) {
+ update_playback_speed(mpctx);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (opt_ptr == &opts->play_dir) {
+ if (mpctx->play_dir != opts->play_dir) {
+ // Some weird things for play_dir if we're at EOF.
+ // 1. The option must be set before we seek.
+ // 2. queue_seek can change the stop_play value; always keep the old one.
+ int old_stop_play = mpctx->stop_play;
+ if (old_stop_play == AT_END_OF_FILE)
+ mpctx->play_dir = opts->play_dir;
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, get_current_time(mpctx),
+ MPSEEK_EXACT, 0);
+ if (old_stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = old_stop_play;
+ }
+ }
+
+ if (opt_ptr == &opts->edition_id) {
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (mpctx->playback_initialized && demuxer && demuxer->num_editions > 0) {
+ if (opts->edition_id != demuxer->edition) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ mp_wakeup_core(mpctx);
+ }
+ }
+ }
+
+ if (opt_ptr == &opts->pause)
+ set_pause_state(mpctx, opts->pause);
+
+ if (opt_ptr == &opts->audio_delay) {
+ if (mpctx->ao_chain) {
+ mpctx->delay += mpctx->opts->audio_delay - mpctx->ao_chain->delay;
+ mpctx->ao_chain->delay = mpctx->opts->audio_delay;
+ }
+ mp_wakeup_core(mpctx);
+ }
+
+ if (flags & UPDATE_HWDEC) {
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_decoder_wrapper *dec = track ? track->dec : NULL;
+ if (dec) {
+ mp_decoder_wrapper_control(dec, VDCTRL_REINIT, NULL);
+ double last_pts = mpctx->video_pts;
+ if (last_pts != MP_NOPTS_VALUE)
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, last_pts, MPSEEK_EXACT, 0);
+ }
+ }
+
+ if (opt_ptr == &opts->vo->window_scale)
+ update_window_scale(mpctx);
+
+ if (opt_ptr == &opts->cursor_autohide_delay)
+ mpctx->mouse_timer = 0;
+
+ if (flags & UPDATE_DVB_PROG) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ }
+
+ if (opt_ptr == &opts->ab_loop[0] || opt_ptr == &opts->ab_loop[1]) {
+ update_ab_loop_clip(mpctx);
+ // Update if visible
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ mp_wakeup_core(mpctx);
+ }
+
+ if (opt_ptr == &opts->vf_settings)
+ set_filters(mpctx, STREAM_VIDEO, opts->vf_settings);
+
+ if (opt_ptr == &opts->af_settings)
+ set_filters(mpctx, STREAM_AUDIO, opts->af_settings);
+
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int order = 0; order < num_ptracks[type]; order++) {
+ if (opt_ptr == &opts->stream_id[order][type])
+ update_track_switch(mpctx, order, type);
+ }
+ }
+
+ if (opt_ptr == &opts->vo->fullscreen && !opts->vo->fullscreen)
+ mpctx->mouse_event_ts--; // Show mouse cursor
+
+ if (opt_ptr == &opts->vo->taskbar_progress)
+ update_vo_playback_state(mpctx);
+
+ if (opt_ptr == &opts->image_display_duration && mpctx->vo_chain
+ && mpctx->vo_chain->is_sparse && !mpctx->ao_chain
+ && mpctx->video_status == STATUS_DRAINING)
+ mpctx->time_frame = opts->image_display_duration;
+}
+
+void mp_notify_property(struct MPContext *mpctx, const char *property)
+{
+ mp_client_property_change(mpctx, property);
+}
diff --git a/player/command.h b/player/command.h
new file mode 100644
index 0000000..185b78f
--- /dev/null
+++ b/player/command.h
@@ -0,0 +1,123 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_COMMAND_H
+#define MPLAYER_COMMAND_H
+
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+#include "osdep/compiler.h"
+
+struct MPContext;
+struct mp_cmd;
+struct mp_log;
+struct mpv_node;
+struct m_config_option;
+
+void command_init(struct MPContext *mpctx);
+void command_uninit(struct MPContext *mpctx);
+
+// Runtime context for a single command.
+struct mp_cmd_ctx {
+ struct MPContext *mpctx;
+ struct mp_cmd *cmd; // original command
+ // Fields from cmd (for convenience)
+ struct mp_cmd_arg *args;
+ int num_args;
+ const void *priv; // cmd->def->priv
+ // OSD control
+ int on_osd; // MP_ON_OSD_FLAGS;
+ bool msg_osd; // OSD message requested
+ bool bar_osd; // OSD bar requested
+ bool seek_msg_osd; // same as above, but for seek commands
+ bool seek_bar_osd;
+ // If mp_cmd_def.can_abort is set, this will be set.
+ struct mp_abort_entry *abort;
+ // Return values (to be set by command implementation, read by the
+ // completion callback).
+ bool success; // true by default
+ struct mpv_node result;
+ // Command handlers can set this to false if returning from the command
+ // handler does not complete the command. It stops the common command code
+ // from signaling the completion automatically, and you can call
+ // mp_cmd_ctx_complete() to invoke on_completion() properly (including all
+ // the bookkeeping).
+ /// (Note that in no case you can call mp_cmd_ctx_complete() from within
+ // the command handler, because it frees the mp_cmd_ctx.)
+ bool completed; // true by default
+ // This is managed by the common command code. For rules about how and where
+ // this is called see run_command() comments.
+ void (*on_completion)(struct mp_cmd_ctx *cmd);
+ void *on_completion_priv; // for free use by on_completion callback
+};
+
+void run_command(struct MPContext *mpctx, struct mp_cmd *cmd,
+ struct mp_abort_entry *abort,
+ void (*on_completion)(struct mp_cmd_ctx *cmd),
+ void *on_completion_priv);
+void mp_cmd_ctx_complete(struct mp_cmd_ctx *cmd);
+PRINTF_ATTRIBUTE(3, 4)
+void mp_cmd_msg(struct mp_cmd_ctx *cmd, int status, const char *msg, ...);
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str);
+char *mp_property_expand_escaped_string(struct MPContext *mpctx, const char *str);
+void property_print_help(struct MPContext *mpctx);
+int mp_property_do(const char* name, int action, void* val,
+ struct MPContext *mpctx);
+
+void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
+ bool self_update);
+
+void mp_notify(struct MPContext *mpctx, int event, void *arg);
+void mp_notify_property(struct MPContext *mpctx, const char *property);
+
+void handle_command_updates(struct MPContext *mpctx);
+
+int mp_get_property_id(struct MPContext *mpctx, const char *name);
+uint64_t mp_get_property_event_mask(const char *name);
+
+enum {
+ // Must start with the first unused positive value in enum mpv_event_id
+ // MPV_EVENT_* and MP_EVENT_* must not overlap.
+ INTERNAL_EVENT_BASE = 26,
+ MP_EVENT_CHANGE_ALL,
+ MP_EVENT_CACHE_UPDATE,
+ MP_EVENT_WIN_RESIZE,
+ MP_EVENT_WIN_STATE,
+ MP_EVENT_WIN_STATE2,
+ MP_EVENT_FOCUS,
+ MP_EVENT_CHANGE_PLAYLIST,
+ MP_EVENT_CORE_IDLE,
+ MP_EVENT_DURATION_UPDATE,
+ MP_EVENT_INPUT_PROCESSED,
+ MP_EVENT_TRACKS_CHANGED,
+ MP_EVENT_TRACK_SWITCHED,
+ MP_EVENT_METADATA_UPDATE,
+ MP_EVENT_CHAPTER_CHANGE,
+};
+
+bool mp_hook_test_completion(struct MPContext *mpctx, char *type);
+void mp_hook_start(struct MPContext *mpctx, char *type);
+int mp_hook_continue(struct MPContext *mpctx, int64_t client_id, uint64_t id);
+void mp_hook_add(struct MPContext *mpctx, char *client, int64_t client_id,
+ const char *name, uint64_t user_id, int pri);
+
+void mark_seek(struct MPContext *mpctx);
+
+void mp_abort_cache_dumping(struct MPContext *mpctx);
+
+#endif /* MPLAYER_COMMAND_H */
diff --git a/player/configfiles.c b/player/configfiles.c
new file mode 100644
index 0000000..9441638
--- /dev/null
+++ b/player/configfiles.c
@@ -0,0 +1,472 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <libavutil/md5.h>
+
+#include "mpv_talloc.h"
+
+#include "osdep/io.h"
+
+#include "common/global.h"
+#include "common/encode.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "options/path.h"
+#include "options/m_config.h"
+#include "options/m_config_frontend.h"
+#include "options/parse_configfile.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/m_property.h"
+
+#include "stream/stream.h"
+
+#include "core.h"
+#include "command.h"
+
+static void load_all_cfgfiles(struct MPContext *mpctx, char *section,
+ char *filename)
+{
+ char **cf = mp_find_all_config_files(NULL, mpctx->global, filename);
+ for (int i = 0; cf && cf[i]; i++)
+ m_config_parse_config_file(mpctx->mconfig, mpctx->global, cf[i], section, 0);
+ talloc_free(cf);
+}
+
+// This name is used in builtin.conf to force encoding defaults (like ao/vo).
+#define SECT_ENCODE "encoding"
+
+void mp_parse_cfgfiles(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mp_mk_user_dir(mpctx->global, "home", "");
+
+ char *p1 = mp_get_user_path(NULL, mpctx->global, "~~home/");
+ char *p2 = mp_get_user_path(NULL, mpctx->global, "~~old_home/");
+ if (strcmp(p1, p2) != 0 && mp_path_exists(p2)) {
+ MP_WARN(mpctx, "Warning, two config dirs found:\n %s (main)\n"
+ " %s (bogus)\nYou should merge or delete the second one.\n",
+ p1, p2);
+ }
+ talloc_free(p1);
+ talloc_free(p2);
+
+ char *section = NULL;
+ bool encoding = opts->encode_opts &&
+ opts->encode_opts->file && opts->encode_opts->file[0];
+ // In encoding mode, we don't want to apply normal config options.
+ // So we "divert" normal options into a separate section, and the diverted
+ // section is never used - unless maybe it's explicitly referenced from an
+ // encoding profile.
+ if (encoding)
+ section = "playback-default";
+
+ load_all_cfgfiles(mpctx, NULL, "encoding-profiles.conf");
+
+ load_all_cfgfiles(mpctx, section, "mpv.conf|config");
+
+ if (encoding)
+ m_config_set_profile(mpctx->mconfig, SECT_ENCODE, 0);
+}
+
+static int try_load_config(struct MPContext *mpctx, const char *file, int flags,
+ int msgl)
+{
+ if (!mp_path_exists(file))
+ return 0;
+ MP_MSG(mpctx, msgl, "Loading config '%s'\n", file);
+ m_config_parse_config_file(mpctx->mconfig, mpctx->global, file, NULL, flags);
+ return 1;
+}
+
+// Set options file-local, and don't set them if the user set them via the
+// command line.
+#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE)
+
+static void mp_load_per_file_config(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *confpath;
+ char cfg[512];
+ const char *file = mpctx->filename;
+
+ if (opts->use_filedir_conf) {
+ if (snprintf(cfg, sizeof(cfg), "%s.conf", file) >= sizeof(cfg)) {
+ MP_VERBOSE(mpctx, "Filename is too long, can not load file or "
+ "directory specific config files\n");
+ return;
+ }
+
+ char *name = mp_basename(cfg);
+
+ bstr dir = mp_dirname(cfg);
+ char *dircfg = mp_path_join_bstr(NULL, dir, bstr0("mpv.conf"));
+ try_load_config(mpctx, dircfg, FILE_LOCAL_FLAGS, MSGL_INFO);
+ talloc_free(dircfg);
+
+ if (try_load_config(mpctx, cfg, FILE_LOCAL_FLAGS, MSGL_INFO))
+ return;
+
+ if ((confpath = mp_find_config_file(NULL, mpctx->global, name))) {
+ try_load_config(mpctx, confpath, FILE_LOCAL_FLAGS, MSGL_INFO);
+
+ talloc_free(confpath);
+ }
+ }
+}
+
+static void mp_auto_load_profile(struct MPContext *mpctx, char *category,
+ bstr item)
+{
+ if (!item.len)
+ return;
+
+ char t[512];
+ snprintf(t, sizeof(t), "%s.%.*s", category, BSTR_P(item));
+ m_profile_t *p = m_config_get_profile0(mpctx->mconfig, t);
+ if (p) {
+ MP_INFO(mpctx, "Auto-loading profile '%s'\n", t);
+ m_config_set_profile(mpctx->mconfig, t, FILE_LOCAL_FLAGS);
+ }
+}
+
+void mp_load_auto_profiles(struct MPContext *mpctx)
+{
+ mp_auto_load_profile(mpctx, "protocol",
+ mp_split_proto(bstr0(mpctx->filename), NULL));
+ mp_auto_load_profile(mpctx, "extension",
+ bstr0(mp_splitext(mpctx->filename, NULL)));
+
+ mp_load_per_file_config(mpctx);
+}
+
+#define MP_WATCH_LATER_CONF "watch_later"
+
+static bool check_mtime(const char *f1, const char *f2)
+{
+ struct stat st1, st2;
+ if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
+ return false;
+ return st1.st_mtime == st2.st_mtime;
+}
+
+static bool copy_mtime(const char *f1, const char *f2)
+{
+ struct stat st1, st2;
+
+ if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0)
+ return false;
+
+ struct utimbuf ut = {
+ .actime = st2.st_atime, // we want to pass this through intact
+ .modtime = st1.st_mtime,
+ };
+
+ if (utime(f2, &ut) != 0)
+ return false;
+
+ return true;
+}
+
+static char *mp_get_playback_resume_dir(struct MPContext *mpctx)
+{
+ char *wl_dir = mpctx->opts->watch_later_dir;
+ if (wl_dir && wl_dir[0]) {
+ wl_dir = mp_get_user_path(mpctx, mpctx->global, wl_dir);
+ } else {
+ wl_dir = mp_find_user_file(mpctx, mpctx->global, "state", MP_WATCH_LATER_CONF);
+ }
+ return wl_dir;
+}
+
+static char *mp_get_playback_resume_config_filename(struct MPContext *mpctx,
+ const char *fname)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *res = NULL;
+ void *tmp = talloc_new(NULL);
+ const char *realpath = fname;
+ bstr bfname = bstr0(fname);
+ if (!mp_is_url(bfname)) {
+ if (opts->ignore_path_in_watch_later_config) {
+ realpath = mp_basename(fname);
+ } else {
+ char *cwd = mp_getcwd(tmp);
+ if (!cwd)
+ goto exit;
+ realpath = mp_path_join(tmp, cwd, fname);
+ }
+ }
+ uint8_t md5[16];
+ av_md5_sum(md5, realpath, strlen(realpath));
+ char *conf = talloc_strdup(tmp, "");
+ for (int i = 0; i < 16; i++)
+ conf = talloc_asprintf_append(conf, "%02X", md5[i]);
+
+ char *wl_dir = mp_get_playback_resume_dir(mpctx);
+ if (wl_dir && wl_dir[0])
+ res = mp_path_join(NULL, wl_dir, conf);
+
+exit:
+ talloc_free(tmp);
+ return res;
+}
+
+// Should follow what parser-cfg.c does/needs
+static bool needs_config_quoting(const char *s)
+{
+ if (s[0] == '%')
+ return true;
+ for (int i = 0; s[i]; i++) {
+ unsigned char c = s[i];
+ if (!mp_isprint(c) || mp_isspace(c) || c == '#' || c == '\'' || c == '"')
+ return true;
+ }
+ return false;
+}
+
+static void write_filename(struct MPContext *mpctx, FILE *file, char *filename)
+{
+ if (mpctx->opts->write_filename_in_watch_later_config) {
+ char write_name[1024] = {0};
+ for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++)
+ write_name[n] = (unsigned char)filename[n] < 32 ? '_' : filename[n];
+ fprintf(file, "# %s\n", write_name);
+ }
+}
+
+static void write_redirect(struct MPContext *mpctx, char *path)
+{
+ char *conffile = mp_get_playback_resume_config_filename(mpctx, path);
+ if (conffile) {
+ FILE *file = fopen(conffile, "wb");
+ if (file) {
+ fprintf(file, "# redirect entry\n");
+ write_filename(mpctx, file, path);
+ fclose(file);
+ }
+
+ if (mpctx->opts->position_check_mtime &&
+ !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile))
+ MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", path, conffile);
+
+ talloc_free(conffile);
+ }
+}
+
+static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path)
+{
+ if (mp_is_url(bstr0(path)))
+ return;
+
+ // Write redirect entries for the file's parent directories to allow
+ // resuming playback when playing parent directories whose entries are
+ // expanded only the first time they are "played". For example, if
+ // "/a/b/c.mkv" is the current entry, also create resume files for /a/b and
+ // /a, so that "mpv --directory-mode=lazy /a" resumes playback from
+ // /a/b/c.mkv even when b isn't the first directory in /a.
+ bstr dir = mp_dirname(path);
+ // There is no need to write a redirect entry for "/".
+ while (dir.len > 1 && dir.len < strlen(path)) {
+ path[dir.len] = '\0';
+ mp_path_strip_trailing_separator(path);
+ write_redirect(mpctx, path);
+ dir = mp_dirname(path);
+ }
+}
+
+void mp_write_watch_later_conf(struct MPContext *mpctx)
+{
+ struct playlist_entry *cur = mpctx->playing;
+ char *conffile = NULL;
+ void *ctx = talloc_new(NULL);
+
+ if (!cur)
+ goto exit;
+
+ char *path = mp_normalize_path(ctx, cur->filename);
+
+ struct demuxer *demux = mpctx->demuxer;
+
+ conffile = mp_get_playback_resume_config_filename(mpctx, path);
+ if (!conffile)
+ goto exit;
+
+ char *wl_dir = mp_get_playback_resume_dir(mpctx);
+ mp_mkdirp(wl_dir);
+
+ MP_INFO(mpctx, "Saving state.\n");
+
+ FILE *file = fopen(conffile, "wb");
+ if (!file) {
+ MP_WARN(mpctx, "Can't open %s for writing\n", conffile);
+ goto exit;
+ }
+
+ write_filename(mpctx, file, path);
+
+ bool write_start = true;
+ double pos = get_current_time(mpctx);
+
+ if ((demux && (!demux->seekable || demux->partially_seekable)) ||
+ pos == MP_NOPTS_VALUE)
+ {
+ write_start = false;
+ MP_INFO(mpctx, "Not seekable, or time unknown - not saving position.\n");
+ }
+ char **watch_later_options = mpctx->opts->watch_later_options;
+ for (int i = 0; watch_later_options && watch_later_options[i]; i++) {
+ char *pname = watch_later_options[i];
+ // Always save start if we have it in the array.
+ if (write_start && strcmp(pname, "start") == 0) {
+ fprintf(file, "%s=%f\n", pname, pos);
+ continue;
+ }
+ // Only store it if it's different from the initial value.
+ if (m_config_watch_later_backup_opt_changed(mpctx->mconfig, pname)) {
+ char *val = NULL;
+ mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx);
+ if (needs_config_quoting(val)) {
+ // e.g. '%6%STRING'
+ fprintf(file, "%s=%%%d%%%s\n", pname, (int)strlen(val), val);
+ } else {
+ fprintf(file, "%s=%s\n", pname, val);
+ }
+ talloc_free(val);
+ }
+ }
+ fclose(file);
+
+ if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(path)) &&
+ !copy_mtime(path, conffile))
+ {
+ MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", cur->filename,
+ conffile);
+ }
+
+ write_redirects_for_parent_dirs(mpctx, path);
+
+ // Also write redirect entries for a playlist that mpv expanded if the
+ // current entry is a URL, this is mostly useful for playing multiple
+ // archives of images, e.g. with mpv 1.zip 2.zip and quit-watch-later
+ // on 2.zip, write redirect entries for 2.zip, not just for the archive://
+ // URL.
+ if (cur->playlist_path && mp_is_url(bstr0(path))) {
+ char *playlist_path = mp_normalize_path(ctx, cur->playlist_path);
+ write_redirect(mpctx, playlist_path);
+ write_redirects_for_parent_dirs(mpctx, playlist_path);
+ }
+
+exit:
+ talloc_free(conffile);
+ talloc_free(ctx);
+}
+
+void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file)
+{
+ if (!file) {
+ struct playlist_entry *cur = mpctx->playing;
+ if (!cur)
+ return;
+ file = cur->filename;
+ if (!file)
+ return;
+ }
+
+ char *fname = mp_get_playback_resume_config_filename(mpctx, file);
+ if (fname) {
+ unlink(fname);
+ talloc_free(fname);
+ }
+
+ if (mp_is_url(bstr0(file)))
+ return;
+
+ void *ctx = talloc_new(NULL);
+ char *path = mp_normalize_path(ctx, file);
+
+ bstr dir = mp_dirname(path);
+ while (dir.len > 1 && dir.len < strlen(path)) {
+ path[dir.len] = '\0';
+ mp_path_strip_trailing_separator(path);
+ fname = mp_get_playback_resume_config_filename(mpctx, path);
+ if (fname) {
+ unlink(fname);
+ talloc_free(fname);
+ }
+ dir = mp_dirname(path);
+ }
+
+ talloc_free(ctx);
+}
+
+bool mp_load_playback_resume(struct MPContext *mpctx, const char *file)
+{
+ bool resume = false;
+ if (!mpctx->opts->position_resume)
+ return resume;
+ char *fname = mp_get_playback_resume_config_filename(mpctx, file);
+ if (fname && mp_path_exists(fname)) {
+ if (mpctx->opts->position_check_mtime &&
+ !mp_is_url(bstr0(file)) && !check_mtime(file, fname))
+ {
+ talloc_free(fname);
+ return resume;
+ }
+
+ // Never apply the saved start position to following files
+ m_config_backup_opt(mpctx->mconfig, "start");
+ MP_INFO(mpctx, "Resuming playback. This behavior can "
+ "be disabled with --no-resume-playback.\n");
+ try_load_config(mpctx, fname, M_SETOPT_PRESERVE_CMDLINE, MSGL_V);
+ resume = true;
+ }
+ talloc_free(fname);
+ return resume;
+}
+
+// Returns the first file that has a resume config.
+// Compared to hashing the playlist file or contents and managing separate
+// resume file for them, this is simpler, and also has the nice property
+// that appending to a playlist doesn't interfere with resuming (especially
+// if the playlist comes from the command line).
+struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx,
+ struct playlist *playlist)
+{
+ if (!mpctx->opts->position_resume)
+ return NULL;
+ for (int n = 0; n < playlist->num_entries; n++) {
+ struct playlist_entry *e = playlist->entries[n];
+ char *conf = mp_get_playback_resume_config_filename(mpctx, e->filename);
+ bool exists = conf && mp_path_exists(conf);
+ talloc_free(conf);
+ if (exists)
+ return e;
+ }
+ return NULL;
+}
+
diff --git a/player/core.h b/player/core.h
new file mode 100644
index 0000000..8a49585
--- /dev/null
+++ b/player/core.h
@@ -0,0 +1,644 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_MP_CORE_H
+#define MPLAYER_MP_CORE_H
+
+#include <stdatomic.h>
+#include <stdbool.h>
+
+#include "libmpv/client.h"
+
+#include "audio/aframe.h"
+#include "common/common.h"
+#include "filters/f_output_chain.h"
+#include "filters/filter.h"
+#include "options/options.h"
+#include "osdep/threads.h"
+#include "sub/osd.h"
+#include "video/mp_image.h"
+#include "video/out/vo.h"
+
+// definitions used internally by the core player code
+
+enum stop_play_reason {
+ KEEP_PLAYING = 0, // playback of a file is actually going on
+ // must be 0, numeric values of others do not matter
+ AT_END_OF_FILE, // file has ended, prepare to play next
+ // also returned on unrecoverable playback errors
+ PT_NEXT_ENTRY, // prepare to play next entry in playlist
+ PT_CURRENT_ENTRY, // prepare to play mpctx->playlist->current
+ PT_STOP, // stop playback / idle mode
+ PT_QUIT, // stop playback, quit player
+ PT_ERROR, // play next playlist entry (due to an error)
+};
+
+enum mp_osd_seek_info {
+ OSD_SEEK_INFO_BAR = 1,
+ OSD_SEEK_INFO_TEXT = 2,
+ OSD_SEEK_INFO_CHAPTER_TEXT = 4,
+ OSD_SEEK_INFO_CURRENT_FILE = 8,
+};
+
+
+enum {
+ // other constants
+ OSD_LEVEL_INVISIBLE = 4,
+ OSD_BAR_SEEK = 256,
+
+ MAX_NUM_VO_PTS = 100,
+};
+
+enum seek_type {
+ MPSEEK_NONE = 0,
+ MPSEEK_RELATIVE,
+ MPSEEK_ABSOLUTE,
+ MPSEEK_FACTOR,
+ MPSEEK_BACKSTEP,
+ MPSEEK_CHAPTER,
+};
+
+enum seek_precision {
+ // The following values are numerically sorted by increasing precision
+ MPSEEK_DEFAULT = 0,
+ MPSEEK_KEYFRAME,
+ MPSEEK_EXACT,
+ MPSEEK_VERY_EXACT,
+};
+
+enum seek_flags {
+ MPSEEK_FLAG_DELAY = 1 << 0, // give player chance to coalesce multiple seeks
+ MPSEEK_FLAG_NOFLUSH = 1 << 1, // keeping remaining data for seamless loops
+};
+
+struct seek_params {
+ enum seek_type type;
+ enum seek_precision exact;
+ double amount;
+ unsigned flags; // MPSEEK_FLAG_*
+};
+
+// Information about past video frames that have been sent to the VO.
+struct frame_info {
+ double pts;
+ double duration; // PTS difference to next frame
+ double approx_duration; // possibly fixed/smoothed out duration
+ double av_diff; // A/V diff at time of scheduling
+ int num_vsyncs; // scheduled vsyncs, if using display-sync
+};
+
+struct track {
+ enum stream_type type;
+
+ // Currently used for decoding.
+ bool selected;
+
+ // The type specific ID, also called aid (audio), sid (subs), vid (video).
+ // For UI purposes only; this ID doesn't have anything to do with any
+ // IDs coming from demuxers or container files.
+ int user_tid;
+
+ int demuxer_id; // same as stream->demuxer_id. -1 if not set.
+ int ff_index; // same as stream->ff_index, or 0.
+ int hls_bitrate; // same as stream->hls_bitrate. 0 if not set.
+ int program_id; // same as stream->program_id. -1 if not set.
+
+ char *title;
+ bool default_track, forced_track, dependent_track;
+ bool visual_impaired_track, hearing_impaired_track;
+ bool image;
+ bool attached_picture;
+ char *lang;
+
+ // If this track is from an external file (e.g. subtitle file).
+ bool is_external;
+ bool no_default; // pretend it's not external for auto-selection
+ bool no_auto_select;
+ char *external_filename;
+ bool auto_loaded;
+
+ struct demuxer *demuxer;
+ // Invariant: !stream || stream->demuxer == demuxer
+ struct sh_stream *stream;
+
+ // Current subtitle state (or cached state if selected==false).
+ struct dec_sub *d_sub;
+
+ // Current decoding state (NULL if selected==false)
+ struct mp_decoder_wrapper *dec;
+
+ // Where the decoded result goes to (one of them is not NULL if active)
+ struct vo_chain *vo_c;
+ struct ao_chain *ao_c;
+ struct mp_pin *sink;
+};
+
+// Summarizes video filtering and output.
+struct vo_chain {
+ struct mp_log *log;
+
+ struct mp_output_chain *filter;
+
+ struct vo *vo;
+
+ struct track *track;
+ struct mp_pin *filter_src;
+ struct mp_pin *dec_src;
+
+ // - video consists of a single picture, which should be shown only once
+ // - do not sync audio to video in any way
+ bool is_coverart;
+ // - video consists of sparse still images
+ bool is_sparse;
+ bool sparse_eof_signalled;
+
+ bool underrun;
+ bool underrun_signaled;
+};
+
+// Like vo_chain, for audio.
+struct ao_chain {
+ struct mp_log *log;
+ struct MPContext *mpctx;
+
+ bool spdif_passthrough, spdif_failed;
+
+ struct mp_output_chain *filter;
+
+ struct ao *ao;
+ struct mp_async_queue *ao_queue;
+ struct mp_filter *queue_filter;
+ struct mp_filter *ao_filter;
+ double ao_resume_time;
+
+ bool out_eof;
+ double last_out_pts;
+
+ double start_pts;
+ bool start_pts_known;
+
+ struct track *track;
+ struct mp_pin *filter_src;
+ struct mp_pin *dec_src;
+
+ double delay;
+ bool untimed_throttle;
+
+ bool ao_underrun; // last known AO state
+ bool underrun; // for cache pause logic
+};
+
+/* Note that playback can be paused, stopped, etc. at any time. While paused,
+ * playback restart is still active, because you want seeking to work even
+ * if paused.
+ * The main purpose of distinguishing these states is proper reinitialization
+ * of A/V sync.
+ */
+enum playback_status {
+ // code may compare status values numerically
+ STATUS_SYNCING, // seeking for a position to resume
+ STATUS_READY, // buffers full, playback can be started any time
+ STATUS_PLAYING, // normal playback
+ STATUS_DRAINING, // decoding has ended; still playing out queued buffers
+ STATUS_EOF, // playback has ended, or is disabled
+};
+
+const char *mp_status_str(enum playback_status st);
+
+extern const int num_ptracks[STREAM_TYPE_COUNT];
+
+// Maximum of all num_ptracks[] values.
+#define MAX_PTRACKS 2
+
+typedef struct MPContext {
+ bool initialized;
+ bool is_cli;
+ struct mpv_global *global;
+ struct MPOpts *opts;
+ struct mp_log *log;
+ struct stats_ctx *stats;
+ struct m_config *mconfig;
+ struct input_ctx *input;
+ struct mp_client_api *clients;
+ struct mp_dispatch_queue *dispatch;
+ struct mp_cancel *playback_abort;
+ // Number of asynchronous tasks that still need to finish until MPContext
+ // destruction is ok. It's implied that the async tasks call
+ // mp_wakeup_core() each time this is decremented.
+ // As using an atomic+wakeup would be racy, this is a normal integer, and
+ // mp_dispatch_lock must be called to change it.
+ int64_t outstanding_async;
+
+ struct mp_thread_pool *thread_pool; // for coarse I/O, often during loading
+
+ struct mp_log *statusline;
+ struct osd_state *osd;
+ char *term_osd_text;
+ char *term_osd_status;
+ char *term_osd_subs;
+ char *term_osd_contents;
+ char *term_osd_title;
+ char *last_window_title;
+ struct voctrl_playback_state vo_playback_state;
+
+ int add_osd_seek_info; // bitfield of enum mp_osd_seek_info
+ double osd_visible; // for the osd bar only
+ int osd_function;
+ double osd_function_visible;
+ double osd_msg_visible;
+ double osd_msg_next_duration;
+ double osd_last_update;
+ bool osd_force_update, osd_idle_update;
+ char *osd_msg_text;
+ bool osd_show_pos;
+ struct osd_progbar_state osd_progbar;
+
+ struct playlist *playlist;
+ struct playlist_entry *playing; // currently playing file
+ char *filename; // immutable copy of playing->filename (or NULL)
+ char *stream_open_filename;
+ char **playlist_paths; // used strictly for playlist validation
+ int playlist_paths_len;
+ enum stop_play_reason stop_play;
+ bool playback_initialized; // playloop can be run/is running
+ int error_playing;
+
+ // Return code to use with PT_QUIT
+ int quit_custom_rc;
+ bool has_quit_custom_rc;
+
+ // Global file statistics
+ int files_played; // played without issues (even if stopped by user)
+ int files_errored; // played, but errors happened at one point
+ int files_broken; // couldn't be played at all
+
+ // Current file statistics
+ int64_t shown_vframes, shown_aframes;
+
+ struct demux_chapter *chapters;
+ int num_chapters;
+
+ struct demuxer *demuxer;
+ struct mp_tags *filtered_tags;
+
+ struct track **tracks;
+ int num_tracks;
+
+ char *track_layout_hash;
+
+ // Selected tracks. NULL if no track selected.
+ // There can be num_ptracks[type] of the same STREAM_TYPE selected at once.
+ // Currently, this is used for the secondary subtitle track only.
+ struct track *current_track[MAX_PTRACKS][STREAM_TYPE_COUNT];
+
+ struct mp_filter *filter_root;
+
+ struct mp_filter *lavfi;
+ char *lavfi_graph;
+
+ struct ao *ao;
+ struct mp_aframe *ao_filter_fmt; // for weak gapless audio check
+ struct ao_chain *ao_chain;
+
+ struct vo_chain *vo_chain;
+
+ struct vo *video_out;
+ // next_frame[0] is the next frame, next_frame[1] the one after that.
+ // The +1 is for adding 1 additional frame in backstep mode.
+ struct mp_image *next_frames[VO_MAX_REQ_FRAMES + 1];
+ int num_next_frames;
+ struct mp_image *saved_frame; // for hrseek_lastframe and hrseek_backstep
+
+ enum playback_status video_status, audio_status;
+ bool restart_complete;
+ int play_dir;
+ // Factors to multiply with opts->playback_speed to get the total audio or
+ // video speed (usually 1.0, but can be set to by the sync code).
+ double speed_factor_v, speed_factor_a;
+ // Redundant values set from opts->playback_speed and speed_factor_*.
+ // update_playback_speed() updates them from the other fields.
+ double audio_speed, video_speed;
+ bool display_sync_active;
+ int display_sync_drift_dir;
+ // Timing error (in seconds) due to rounding on vsync boundaries
+ double display_sync_error;
+ // Number of mistimed frames.
+ int mistimed_frames_total;
+ bool hrseek_active; // skip all data until hrseek_pts
+ bool hrseek_lastframe; // drop everything until last frame reached
+ bool hrseek_backstep; // go to frame before seek target
+ double hrseek_pts;
+ struct seek_params current_seek;
+ bool ab_loop_clip; // clip to the "b" part of an A-B loop if available
+ // AV sync: the next frame should be shown when the audio out has this
+ // much (in seconds) buffered data left. Increased when more data is
+ // written to the ao, decreased when moving to the next video frame.
+ double delay;
+ // AV sync: time in seconds until next frame should be shown
+ double time_frame;
+ // How much video timing has been changed to make it match the audio
+ // timeline. Used for status line information only.
+ double total_avsync_change;
+ // A-V sync difference when last frame was displayed. Kept to display
+ // the same value if the status line is updated at a time where no new
+ // video frame is shown.
+ double last_av_difference;
+ /* timestamp of video frame currently visible on screen
+ * (or at least queued to be flipped by VO) */
+ double video_pts;
+ // Last seek target.
+ double last_seek_pts;
+ // Frame duration field from demuxer. Only used for duration of the last
+ // video frame.
+ double last_frame_duration;
+ // Video PTS, or audio PTS if video has ended.
+ double playback_pts;
+ // For logging only.
+ double logged_async_diff;
+
+ int last_chapter;
+
+ // Past timestamps etc.
+ // The newest frame is at index 0.
+ struct frame_info *past_frames;
+ int num_past_frames;
+
+ double last_idle_tick;
+ double next_cache_update;
+
+ double sleeptime; // number of seconds to sleep before next iteration
+
+ double mouse_timer;
+ unsigned int mouse_event_ts;
+ bool mouse_cursor_visible;
+
+ // used to prevent hanging in some error cases
+ double start_timestamp;
+
+ // Timestamp from the last time some timing functions read the
+ // current time, in nanoseconds.
+ // Used to turn a new time value to a delta from last time.
+ int64_t last_time;
+
+ struct seek_params seek;
+
+ /* Heuristic for relative chapter seeks: keep track which chapter
+ * the user wanted to go to, even if we aren't exactly within the
+ * boundaries of that chapter due to an inaccurate seek. */
+ int last_chapter_seek;
+ bool last_chapter_flag;
+
+ bool paused; // internal pause state
+ bool playback_active; // not paused, restarting, loading, unloading
+ bool in_playloop;
+
+ // step this many frames, then pause
+ int step_frames;
+ // Counted down each frame, stop playback if 0 is reached. (-1 = disable)
+ int max_frames;
+ bool playing_msg_shown;
+
+ bool paused_for_cache;
+ bool demux_underrun;
+ double cache_stop_time;
+ int cache_buffer;
+ double cache_update_pts;
+
+ // Set after showing warning about decoding being too slow for realtime
+ // playback rate. Used to avoid showing it multiple times.
+ bool drop_message_shown;
+
+ struct screenshot_ctx *screenshot_ctx;
+ struct command_ctx *command_ctx;
+ struct encode_lavc_context *encode_lavc_ctx;
+
+ struct mp_ipc_ctx *ipc_ctx;
+
+ int64_t builtin_script_ids[5];
+
+ mp_mutex abort_lock;
+
+ // --- The following fields are protected by abort_lock
+ struct mp_abort_entry **abort_list;
+ int num_abort_list;
+ bool abort_all; // during final termination
+
+ // --- Owned by MPContext
+ mp_thread open_thread;
+ bool open_active; // open_thread is a valid thread handle, all setup
+ atomic_bool open_done;
+ // --- All fields below are immutable while open_active is true.
+ // Otherwise, they're owned by MPContext.
+ struct mp_cancel *open_cancel;
+ char *open_url;
+ char *open_format;
+ int open_url_flags;
+ bool open_for_prefetch;
+ // --- All fields below are owned by open_thread, unless open_done was set
+ // to true.
+ struct demuxer *open_res_demuxer;
+ int open_res_error;
+} MPContext;
+
+// Contains information about an asynchronous work item, how it can be aborted,
+// and when. All fields are protected by MPContext.abort_lock.
+struct mp_abort_entry {
+ // General conditions.
+ bool coupled_to_playback; // trigger when playback is terminated
+ // Actual trigger to abort the work. Pointer immutable, owner may access
+ // without holding the abort_lock.
+ struct mp_cancel *cancel;
+ // For client API.
+ struct mpv_handle *client; // non-NULL if done by a client API user
+ int client_work_type; // client API type, e.h. MPV_EVENT_COMMAND_REPLY
+ uint64_t client_work_id; // client API user reply_userdata value
+ // (only valid if client_work_type set)
+};
+
+// audio.c
+void reset_audio_state(struct MPContext *mpctx);
+void reinit_audio_chain(struct MPContext *mpctx);
+int init_audio_decoder(struct MPContext *mpctx, struct track *track);
+int reinit_audio_filters(struct MPContext *mpctx);
+double playing_audio_pts(struct MPContext *mpctx);
+void fill_audio_out_buffers(struct MPContext *mpctx);
+double written_audio_pts(struct MPContext *mpctx);
+void clear_audio_output_buffers(struct MPContext *mpctx);
+void update_playback_speed(struct MPContext *mpctx);
+void uninit_audio_out(struct MPContext *mpctx);
+void uninit_audio_chain(struct MPContext *mpctx);
+void reinit_audio_chain_src(struct MPContext *mpctx, struct track *track);
+void audio_update_volume(struct MPContext *mpctx);
+void reload_audio_output(struct MPContext *mpctx);
+void audio_start_ao(struct MPContext *mpctx);
+
+// configfiles.c
+void mp_parse_cfgfiles(struct MPContext *mpctx);
+void mp_load_auto_profiles(struct MPContext *mpctx);
+bool mp_load_playback_resume(struct MPContext *mpctx, const char *file);
+void mp_write_watch_later_conf(struct MPContext *mpctx);
+void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file);
+struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx,
+ struct playlist *playlist);
+
+// loadfile.c
+void mp_abort_playback_async(struct MPContext *mpctx);
+void mp_abort_add(struct MPContext *mpctx, struct mp_abort_entry *abort);
+void mp_abort_remove(struct MPContext *mpctx, struct mp_abort_entry *abort);
+void mp_abort_recheck_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort);
+void mp_abort_trigger_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort);
+int mp_add_external_file(struct MPContext *mpctx, char *filename,
+ enum stream_type filter, struct mp_cancel *cancel,
+ bool cover_art);
+void mark_track_selection(struct MPContext *mpctx, int order,
+ enum stream_type type, int value);
+#define FLAG_MARK_SELECTION 1
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track, int flags);
+void mp_switch_track_n(struct MPContext *mpctx, int order,
+ enum stream_type type, struct track *track, int flags);
+void mp_deselect_track(struct MPContext *mpctx, struct track *track);
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid);
+void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer);
+bool mp_remove_track(struct MPContext *mpctx, struct track *track);
+struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
+ bool force);
+void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e);
+void mp_play_files(struct MPContext *mpctx);
+void update_demuxer_properties(struct MPContext *mpctx);
+void print_track_list(struct MPContext *mpctx, const char *msg);
+void reselect_demux_stream(struct MPContext *mpctx, struct track *track,
+ bool refresh_only);
+void prepare_playlist(struct MPContext *mpctx, struct playlist *pl);
+void autoload_external_files(struct MPContext *mpctx, struct mp_cancel *cancel);
+struct track *select_default_track(struct MPContext *mpctx, int order,
+ enum stream_type type);
+void prefetch_next(struct MPContext *mpctx);
+void update_lavfi_complex(struct MPContext *mpctx);
+
+// main.c
+int mp_initialize(struct MPContext *mpctx, char **argv);
+struct MPContext *mp_create(void);
+void mp_destroy(struct MPContext *mpctx);
+void mp_print_version(struct mp_log *log, int always);
+void mp_update_logging(struct MPContext *mpctx, bool preinit);
+void issue_refresh_seek(struct MPContext *mpctx, enum seek_precision min_prec);
+
+// misc.c
+double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t);
+double get_play_end_pts(struct MPContext *mpctx);
+double get_play_start_pts(struct MPContext *mpctx);
+bool get_ab_loop_times(struct MPContext *mpctx, double t[2]);
+void merge_playlist_files(struct playlist *pl);
+void update_content_type(struct MPContext *mpctx, struct track *track);
+void update_vo_playback_state(struct MPContext *mpctx);
+void update_window_title(struct MPContext *mpctx, bool force);
+void error_on_track(struct MPContext *mpctx, struct track *track);
+int stream_dump(struct MPContext *mpctx, const char *source_filename);
+double get_track_seek_offset(struct MPContext *mpctx, struct track *track);
+
+// osd.c
+void set_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double neutral, double val);
+bool set_osd_msg(struct MPContext *mpctx, int level, int time,
+ const char* fmt, ...) PRINTF_ATTRIBUTE(4,5);
+void set_osd_function(struct MPContext *mpctx, int osd_function);
+void term_osd_set_subs(struct MPContext *mpctx, const char *text);
+void get_current_osd_sym(struct MPContext *mpctx, char *buf, size_t buf_size);
+void set_osd_bar_chapters(struct MPContext *mpctx, int type);
+
+// playloop.c
+void mp_wait_events(struct MPContext *mpctx);
+void mp_set_timeout(struct MPContext *mpctx, double sleeptime);
+void mp_wakeup_core(struct MPContext *mpctx);
+void mp_wakeup_core_cb(void *ctx);
+void mp_core_lock(struct MPContext *mpctx);
+void mp_core_unlock(struct MPContext *mpctx);
+double get_relative_time(struct MPContext *mpctx);
+void reset_playback_state(struct MPContext *mpctx);
+void set_pause_state(struct MPContext *mpctx, bool user_pause);
+void update_internal_pause_state(struct MPContext *mpctx);
+void update_core_idle_state(struct MPContext *mpctx);
+void add_step_frame(struct MPContext *mpctx, int dir);
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ enum seek_precision exact, int flags);
+double get_time_length(struct MPContext *mpctx);
+double get_start_time(struct MPContext *mpctx, int dir);
+double get_current_time(struct MPContext *mpctx);
+double get_playback_time(struct MPContext *mpctx);
+int get_percent_pos(struct MPContext *mpctx);
+double get_current_pos_ratio(struct MPContext *mpctx, bool use_range);
+int get_current_chapter(struct MPContext *mpctx);
+char *chapter_display_name(struct MPContext *mpctx, int chapter);
+char *chapter_name(struct MPContext *mpctx, int chapter);
+double chapter_start_time(struct MPContext *mpctx, int chapter);
+int get_chapter_count(struct MPContext *mpctx);
+int get_cache_buffering_percentage(struct MPContext *mpctx);
+void execute_queued_seek(struct MPContext *mpctx);
+void run_playloop(struct MPContext *mpctx);
+void mp_idle(struct MPContext *mpctx);
+void idle_loop(struct MPContext *mpctx);
+int handle_force_window(struct MPContext *mpctx, bool force);
+void seek_to_last_frame(struct MPContext *mpctx);
+void update_screensaver_state(struct MPContext *mpctx);
+void update_ab_loop_clip(struct MPContext *mpctx);
+bool get_internal_paused(struct MPContext *mpctx);
+
+// scripting.c
+struct mp_script_args {
+ const struct mp_scripting *backend;
+ struct MPContext *mpctx;
+ struct mp_log *log;
+ struct mpv_handle *client;
+ const char *filename;
+ const char *path;
+};
+struct mp_scripting {
+ const char *name; // e.g. "lua script"
+ const char *file_ext; // e.g. "lua"
+ bool no_thread; // don't run load() on dedicated thread
+ int (*load)(struct mp_script_args *args);
+};
+bool mp_load_scripts(struct MPContext *mpctx);
+void mp_load_builtin_scripts(struct MPContext *mpctx);
+int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname);
+
+// sub.c
+void reset_subtitle_state(struct MPContext *mpctx);
+void reinit_sub(struct MPContext *mpctx, struct track *track);
+void reinit_sub_all(struct MPContext *mpctx);
+void uninit_sub(struct MPContext *mpctx, struct track *track);
+void uninit_sub_all(struct MPContext *mpctx);
+void update_osd_msg(struct MPContext *mpctx);
+bool update_subtitles(struct MPContext *mpctx, double video_pts);
+
+// video.c
+void reset_video_state(struct MPContext *mpctx);
+int init_video_decoder(struct MPContext *mpctx, struct track *track);
+void reinit_video_chain(struct MPContext *mpctx);
+void reinit_video_chain_src(struct MPContext *mpctx, struct track *track);
+int reinit_video_filters(struct MPContext *mpctx);
+void write_video(struct MPContext *mpctx);
+void mp_force_video_refresh(struct MPContext *mpctx);
+void uninit_video_out(struct MPContext *mpctx);
+void uninit_video_chain(struct MPContext *mpctx);
+double calc_average_frame_duration(struct MPContext *mpctx);
+
+#endif /* MPLAYER_MP_CORE_H */
diff --git a/player/external_files.c b/player/external_files.c
new file mode 100644
index 0000000..e9a6081
--- /dev/null
+++ b/player/external_files.c
@@ -0,0 +1,359 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <dirent.h>
+#include <string.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "misc/ctype.h"
+#include "misc/charset_conv.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "external_files.h"
+
+// Stolen from: vlc/-/blob/master/modules/meta_engine/folder.c#L40
+// sorted by priority (descending)
+static const char *const cover_files[] = {
+ "AlbumArt",
+ "Album",
+ "cover",
+ "front",
+ "AlbumArtSmall",
+ "Folder",
+ ".folder",
+ "thumb",
+ NULL
+};
+
+// Needed for mp_might_be_subtitle_file
+char **sub_exts;
+
+static bool test_ext_list(bstr ext, char **list)
+{
+ if (!list)
+ goto done;
+ for (int n = 0; list[n]; n++) {
+ if (bstrcasecmp(bstr0(list[n]), ext) == 0)
+ return true;
+ }
+done:
+ return false;
+}
+
+static int test_ext(MPOpts *opts, bstr ext)
+{
+ if (test_ext_list(ext, opts->sub_auto_exts))
+ return STREAM_SUB;
+ if (test_ext_list(ext, opts->audiofile_auto_exts))
+ return STREAM_AUDIO;
+ if (test_ext_list(ext, opts->coverart_auto_exts))
+ return STREAM_VIDEO;
+ return -1;
+}
+
+static int test_cover_filename(bstr fname)
+{
+ for (int n = 0; cover_files[n]; n++) {
+ if (bstrcasecmp(bstr0(cover_files[n]), fname) == 0) {
+ return MP_ARRAY_SIZE(cover_files) - n;
+ }
+ }
+ return 0;
+}
+
+bool mp_might_be_subtitle_file(const char *filename)
+{
+ return test_ext_list(bstr_get_ext(bstr0(filename)), sub_exts);
+}
+
+void mp_update_subtitle_exts(struct MPOpts *opts)
+{
+ sub_exts = opts->sub_auto_exts;
+}
+
+static int compare_sub_filename(const void *a, const void *b)
+{
+ const struct subfn *s1 = a;
+ const struct subfn *s2 = b;
+ return strcoll(s1->fname, s2->fname);
+}
+
+static int compare_sub_priority(const void *a, const void *b)
+{
+ const struct subfn *s1 = a;
+ const struct subfn *s2 = b;
+ if (s1->priority > s2->priority)
+ return -1;
+ if (s1->priority < s2->priority)
+ return 1;
+ return strcoll(s1->fname, s2->fname);
+}
+
+static struct bstr guess_lang_from_filename(struct bstr name, int *fn_start)
+{
+ if (name.len < 2)
+ return (struct bstr){NULL, 0};
+
+ int n = 0;
+ int i = name.len - 1;
+
+ char thing = '.';
+ if (name.start[i] == ')') {
+ thing = '(';
+ i--;
+ }
+ if (name.start[i] == ']') {
+ thing = '[';
+ i--;
+ }
+
+ while (i >= 0 && mp_isalpha(name.start[i])) {
+ n++;
+ if (n > 3)
+ return (struct bstr){NULL, 0};
+ i--;
+ }
+
+ if (n < 2 || i == 0 || name.start[i] != thing)
+ return (struct bstr){NULL, 0};
+
+ *fn_start = i;
+ return (struct bstr){name.start + i + 1, n};
+}
+
+static void append_dir_subtitles(struct mpv_global *global, struct MPOpts *opts,
+ struct subfn **slist, int *nsub,
+ struct bstr path, const char *fname,
+ int limit_fuzziness, int limit_type)
+{
+ void *tmpmem = talloc_new(NULL);
+ struct mp_log *log = mp_log_new(tmpmem, global->log, "find_files");
+
+ struct bstr f_fbname = bstr0(mp_basename(fname));
+ struct bstr f_fname = mp_iconv_to_utf8(log, f_fbname,
+ "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
+ struct bstr f_fname_noext = bstrdup(tmpmem, bstr_strip_ext(f_fname));
+ bstr_lower(f_fname_noext);
+ struct bstr f_fname_trim = bstr_strip(f_fname_noext);
+
+ if (f_fbname.start != f_fname.start)
+ talloc_steal(tmpmem, f_fname.start);
+
+ char *path0 = bstrdup0(tmpmem, path);
+
+ if (mp_is_url(bstr0(path0)))
+ goto out;
+
+ DIR *d = opendir(path0);
+ if (!d)
+ goto out;
+ mp_verbose(log, "Loading external files in %.*s\n", BSTR_P(path));
+ struct dirent *de;
+ while ((de = readdir(d))) {
+ void *tmpmem2 = talloc_new(tmpmem);
+ struct bstr den = bstr0(de->d_name);
+ struct bstr dename = mp_iconv_to_utf8(log, den,
+ "UTF-8-MAC", MP_NO_LATIN1_FALLBACK);
+ // retrieve various parts of the filename
+ struct bstr tmp_fname_noext = bstrdup(tmpmem2, bstr_strip_ext(dename));
+ bstr_lower(tmp_fname_noext);
+ struct bstr tmp_fname_ext = bstr_get_ext(dename);
+ struct bstr tmp_fname_trim = bstr_strip(tmp_fname_noext);
+
+ if (den.start != dename.start)
+ talloc_steal(tmpmem2, dename.start);
+
+ // check what it is (most likely)
+ int type = test_ext(opts, tmp_fname_ext);
+ char **langs = NULL;
+ int fuzz = -1;
+ switch (type) {
+ case STREAM_SUB:
+ langs = opts->stream_lang[type];
+ fuzz = opts->sub_auto;
+ break;
+ case STREAM_AUDIO:
+ langs = opts->stream_lang[type];
+ fuzz = opts->audiofile_auto;
+ break;
+ case STREAM_VIDEO:
+ fuzz = opts->coverart_auto;
+ break;
+ }
+
+ if (fuzz < 0 || (limit_type >= 0 && limit_type != type))
+ goto next_sub;
+
+ // we have a (likely) subtitle file
+ // higher prio -> auto-selection may prefer it (0 = not loaded)
+ int prio = 0;
+
+ if (bstrcmp(tmp_fname_trim, f_fname_trim) == 0)
+ prio |= 32; // exact movie name match
+
+ bstr lang = {0};
+ int start = 0;
+ lang = guess_lang_from_filename(tmp_fname_trim, &start);
+ if (bstr_startswith(tmp_fname_trim, f_fname_trim)) {
+ if (lang.len && start == f_fname_trim.len)
+ prio |= 16; // exact movie name + followed by lang
+
+ if (lang.len && fuzz >= 1)
+ prio |= 4; // matches the movie name + a language was matched
+
+ for (int n = 0; langs && langs[n]; n++) {
+ if (lang.len && bstr_case_startswith(lang, bstr0(langs[n]))) {
+ if (fuzz >= 1)
+ prio |= 8; // known language -> boost priority
+ break;
+ }
+ }
+ }
+
+ if (bstr_find(tmp_fname_trim, f_fname_trim) >= 0 && fuzz >= 1)
+ prio |= 2; // contains the movie name
+
+ if (type == STREAM_VIDEO && opts->coverart_whitelist && prio == 0)
+ prio = test_cover_filename(tmp_fname_trim);
+
+ // doesn't contain the movie name
+ // don't try in the mplayer subtitle directory
+ if (!limit_fuzziness && fuzz >= 2)
+ prio |= 1;
+
+ mp_trace(log, "Potential external file: \"%s\" Priority: %d\n",
+ de->d_name, prio);
+
+ if (prio) {
+ char *subpath = mp_path_join_bstr(*slist, path, dename);
+ if (mp_path_exists(subpath)) {
+ MP_TARRAY_GROW(NULL, *slist, *nsub);
+ struct subfn *sub = *slist + (*nsub)++;
+
+ // annoying and redundant
+ if (strncmp(subpath, "./", 2) == 0)
+ subpath += 2;
+
+ sub->type = type;
+ sub->priority = prio;
+ sub->fname = subpath;
+ sub->lang = lang.len ? bstrdup0(*slist, lang) : NULL;
+ } else
+ talloc_free(subpath);
+ }
+
+ next_sub:
+ talloc_free(tmpmem2);
+ }
+ closedir(d);
+
+ out:
+ talloc_free(tmpmem);
+}
+
+static bool case_endswith(const char *s, const char *end)
+{
+ size_t len = strlen(s);
+ size_t elen = strlen(end);
+ return len >= elen && strcasecmp(s + len - elen, end) == 0;
+}
+
+// Drop .sub file if .idx file exists.
+// Assumes slist is sorted by compare_sub_filename.
+static void filter_subidx(struct subfn **slist, int *nsub)
+{
+ const char *prev = NULL;
+ for (int n = 0; n < *nsub; n++) {
+ const char *fname = (*slist)[n].fname;
+ if (case_endswith(fname, ".idx")) {
+ prev = fname;
+ } else if (case_endswith(fname, ".sub")) {
+ if (prev && strncmp(prev, fname, strlen(fname) - 4) == 0)
+ (*slist)[n].priority = -1;
+ }
+ }
+ for (int n = *nsub - 1; n >= 0; n--) {
+ if ((*slist)[n].priority < 0)
+ MP_TARRAY_REMOVE_AT(*slist, *nsub, n);
+ }
+}
+
+static void load_paths(struct mpv_global *global, struct MPOpts *opts,
+ struct subfn **slist, int *nsubs, const char *fname,
+ char **paths, char *cfg_path, int type)
+{
+ for (int i = 0; paths && paths[i]; i++) {
+ char *expanded_path = mp_get_user_path(NULL, global, paths[i]);
+ char *path = mp_path_join_bstr(
+ *slist, mp_dirname(fname),
+ bstr0(expanded_path ? expanded_path : paths[i]));
+ append_dir_subtitles(global, opts, slist, nsubs, bstr0(path),
+ fname, 0, type);
+ talloc_free(expanded_path);
+ }
+
+ // Load subtitles in ~/.mpv/sub (or similar) limiting sub fuzziness
+ char *mp_subdir = mp_find_config_file(NULL, global, cfg_path);
+ if (mp_subdir) {
+ append_dir_subtitles(global, opts, slist, nsubs, bstr0(mp_subdir),
+ fname, 1, type);
+ }
+ talloc_free(mp_subdir);
+}
+
+// Return a list of subtitles and audio files found, sorted by priority.
+// Last element is terminated with a fname==NULL entry.
+struct subfn *find_external_files(struct mpv_global *global, const char *fname,
+ struct MPOpts *opts)
+{
+ struct subfn *slist = talloc_array_ptrtype(NULL, slist, 1);
+ int n = 0;
+
+ // Load subtitles from current media directory
+ append_dir_subtitles(global, opts, &slist, &n, mp_dirname(fname), fname, 0, -1);
+
+ // Load subtitles in dirs specified by sub-paths option
+ if (opts->sub_auto >= 0) {
+ load_paths(global, opts, &slist, &n, fname, opts->sub_paths, "sub",
+ STREAM_SUB);
+ }
+
+ if (opts->audiofile_auto >= 0) {
+ load_paths(global, opts, &slist, &n, fname, opts->audiofile_paths,
+ "audio", STREAM_AUDIO);
+ }
+
+ // Sort by name for filter_subidx()
+ qsort(slist, n, sizeof(*slist), compare_sub_filename);
+
+ filter_subidx(&slist, &n);
+
+ // Sort subs by priority and append them
+ qsort(slist, n, sizeof(*slist), compare_sub_priority);
+
+ struct subfn z = {0};
+ MP_TARRAY_APPEND(NULL, slist, n, z);
+
+ return slist;
+}
diff --git a/player/external_files.h b/player/external_files.h
new file mode 100644
index 0000000..20b37c3
--- /dev/null
+++ b/player/external_files.h
@@ -0,0 +1,38 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_FIND_SUBFILES_H
+#define MPLAYER_FIND_SUBFILES_H
+
+#include <stdbool.h>
+
+struct subfn {
+ int type; // STREAM_SUB/STREAM_AUDIO/STREAM_VIDEO(coverart)
+ int priority;
+ char *fname;
+ char *lang;
+};
+
+struct mpv_global;
+struct MPOpts;
+struct subfn *find_external_files(struct mpv_global *global, const char *fname,
+ struct MPOpts *opts);
+
+bool mp_might_be_subtitle_file(const char *filename);
+void mp_update_subtitle_exts(struct MPOpts *opts);
+
+#endif /* MPLAYER_FINDFILES_H */
diff --git a/player/javascript.c b/player/javascript.c
new file mode 100644
index 0000000..5be7277
--- /dev/null
+++ b/player/javascript.c
@@ -0,0 +1,1262 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <math.h>
+#include <stdint.h>
+
+#include <mujs.h>
+
+#include "osdep/io.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "options/m_property.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "core.h"
+#include "command.h"
+#include "client.h"
+#include "libmpv/client.h"
+
+// List of builtin modules and their contents as strings.
+// All these are generated from player/javascript/*.js
+static const char *const builtin_files[][3] = {
+ {"@/defaults.js",
+# include "player/javascript/defaults.js.inc"
+ },
+ {0}
+};
+
+// Represents a loaded script. Each has its own js state.
+struct script_ctx {
+ const char *filename;
+ const char *path; // NULL if single file
+ struct mpv_handle *client;
+ struct MPContext *mpctx;
+ struct mp_log *log;
+ char *last_error_str;
+ size_t js_malloc_size;
+ struct stats_ctx *stats;
+};
+
+static struct script_ctx *jctx(js_State *J)
+{
+ return (struct script_ctx *)js_getcontext(J);
+}
+
+static mpv_handle *jclient(js_State *J)
+{
+ return jctx(J)->client;
+}
+
+static void pushnode(js_State *J, mpv_node *node);
+static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx);
+static int jsL_checkint(js_State *J, int idx);
+static uint64_t jsL_checkuint64(js_State *J, int idx);
+
+/**********************************************************************
+ * conventions, MuJS notes and vm errors
+ *********************************************************************/
+// - push_foo functions are called from C and push a value to the vm stack.
+//
+// - JavaScript C functions are code which the vm can call as a js function.
+// By convention, script_bar and script__baz are js C functions. The former
+// is exposed to end users as bar, and _baz is for internal use.
+//
+// - js C functions get a fresh vm stack with their arguments, and may
+// manipulate their stack as they see fit. On exit, the vm considers the
+// top value of their stack as their return value, and GC the rest.
+//
+// - js C function's stack[0] is "this", and the rest (1, 2, ...) are the args.
+// On entry the stack has at least the number of args defined for the func,
+// padded with undefined if called with less, or bigger if called with more.
+//
+// - Almost all vm APIs (js_*) may throw an error - a longjmp to the last
+// recovery/catch point, which could skip releasing resources. This includes
+// js_try itself(!), except at the outer-most [1] js_try which is always
+// entering the try part (and the catch part if the try part throws).
+// The assumption should be that anything can throw and needs careful setup.
+// One such automated setup is the autofree mechanism. Details later.
+//
+// - Unless named s_foo, all the functions at this file (inc. init) which
+// touch the vm may throw, but either cleanup resources regardless (mostly
+// autofree) or leave allocated resources on caller-provided talloc context
+// which the caller should release, typically with autofree (e.g. makenode).
+//
+// - Functions named s_foo (safe foo) never throw if called at the outer-most
+// try-levels, or, inside JS C functions - never throw after allocating.
+// If they didn't throw then they return 0 on success, 1 on js-errors.
+//
+// [1] In practice the N outer-most (nested) tries are guaranteed to enter the
+// try/carch code, where N is the mujs try-stack size (64 with mujs 1.1.3).
+// But because we can't track try-level at (called-back) JS C functions,
+// it's only guaranteed when we know we're near the outer-most try level.
+
+/**********************************************************************
+ * mpv scripting API error handling
+ *********************************************************************/
+// - Errors may be thrown on some cases - the reason is at the exception.
+//
+// - Some APIs also set last error which can be fetched with mp.last_error(),
+// where empty string (false-y) is success, or an error string otherwise.
+//
+// - The rest of the APIs are guaranteed to return undefined on error or a
+// true-thy value on success and may or may not set last error.
+//
+// - push_success, push_failure, push_status and pushed_error set last error.
+
+// iserr as true indicates an error, and if so, str may indicate a reason.
+// Internally ctx->last_error_str is never NULL, and empty indicates success.
+static void set_last_error(struct script_ctx *ctx, bool iserr, const char *str)
+{
+ ctx->last_error_str[0] = 0;
+ if (!iserr)
+ return;
+ if (!str || !str[0])
+ str = "Error";
+ ctx->last_error_str = talloc_strdup_append(ctx->last_error_str, str);
+}
+
+// For use only by wrappers at defaults.js.
+// arg: error string. Use empty string to indicate success.
+static void script__set_last_error(js_State *J)
+{
+ const char *e = js_tostring(J, 1);
+ set_last_error(jctx(J), e[0], e);
+}
+
+// mp.last_error() . args: none. return the last error without modifying it.
+static void script_last_error(js_State *J)
+{
+ js_pushstring(J, jctx(J)->last_error_str);
+}
+
+// Generic success for APIs which don't return an actual value.
+static void push_success(js_State *J)
+{
+ set_last_error(jctx(J), 0, NULL);
+ js_pushboolean(J, true);
+}
+
+// Doesn't (intentionally) throw. Just sets last_error and pushes undefined
+static void push_failure(js_State *J, const char *str)
+{
+ set_last_error(jctx(J), 1, str);
+ js_pushundefined(J);
+}
+
+// Most of the scripting APIs are either sending some values and getting status
+// code in return, or requesting some value while providing a default in case an
+// error happened. These simplify the C code for that and always set last_error.
+
+static void push_status(js_State *J, int err)
+{
+ if (err >= 0) {
+ push_success(J);
+ } else {
+ push_failure(J, mpv_error_string(err));
+ }
+}
+
+ // If err is success then return 0, else push the item at def and return 1
+static bool pushed_error(js_State *J, int err, int def)
+{
+ bool iserr = err < 0;
+ set_last_error(jctx(J), iserr, iserr ? mpv_error_string(err) : NULL);
+ if (!iserr)
+ return false;
+
+ js_copy(J, def);
+ return true;
+}
+
+/**********************************************************************
+ * Autofree - care-free resource deallocation on vm errors, and otherwise
+ *********************************************************************/
+// - Autofree (af) functions are called with a talloc context argument which is
+// freed after the function exits - either normally or because it threw an
+// error, on the latter case it then re-throws the error after the cleanup.
+//
+// Autofree js C functions should have an additional void* talloc arg and
+// inserted into the vm using af_newcfunction, but otherwise used normally.
+//
+// To wrap an autofree function af_TARGET in C:
+// 1. Create a wrapper s_TARGET which does this:
+// if (js_try(J))
+// return 1;
+// *af = talloc_new(NULL);
+// af_TARGET(J, ..., *af);
+// js_endtry(J);
+// return 0;
+// 2. Use s_TARGET like so (frees if allocated, throws if {s,af}_TARGET threw):
+// void *af = NULL;
+// int r = s_TARGET(J, ..., &af); // use J, af where the callee expects.
+// talloc_free(af);
+// if (r)
+// js_throw(J);
+//
+// The reason that the allocation happens inside try/catch is that js_try
+// itself can throw (if it runs out of try-stack) and therefore the code
+// inside the try part is not reached - but neither is the catch part(!),
+// and instead it throws to the next outer catch - but before we've allocated
+// anything, hence no leaks on such case. If js_try did get entered, then the
+// allocation happened, and then if af_TARGET threw then s_TARGET will catch
+// it (and return 1) and we'll free if afterwards.
+
+// add_af_file, add_af_dir, add_af_mpv_alloc take a valid FILE*/DIR*/char* value
+// respectively, and fclose/closedir/mpv_free it when the parent is freed.
+
+static void destruct_af_file(void *p)
+{
+ fclose(*(FILE**)p);
+}
+
+static void add_af_file(void *parent, FILE *f)
+{
+ FILE **pf = talloc(parent, FILE*);
+ *pf = f;
+ talloc_set_destructor(pf, destruct_af_file);
+}
+
+static void destruct_af_dir(void *p)
+{
+ closedir(*(DIR**)p);
+}
+
+static void add_af_dir(void *parent, DIR *d)
+{
+ DIR **pd = talloc(parent, DIR*);
+ *pd = d;
+ talloc_set_destructor(pd, destruct_af_dir);
+}
+
+static void destruct_af_mpv_alloc(void *p)
+{
+ mpv_free(*(char**)p);
+}
+
+static void add_af_mpv_alloc(void *parent, char *ma)
+{
+ char **p = talloc(parent, char*);
+ *p = ma;
+ talloc_set_destructor(p, destruct_af_mpv_alloc);
+}
+
+static void destruct_af_mpv_node(void *p)
+{
+ mpv_free_node_contents((mpv_node*)p); // does nothing for MPV_FORMAT_NONE
+}
+
+// returns a new zeroed allocated struct mpv_node, and free it and its content
+// when the parent is freed.
+static mpv_node *new_af_mpv_node(void *parent)
+{
+ mpv_node *p = talloc_zero(parent, mpv_node); // .format == MPV_FORMAT_NONE
+ talloc_set_destructor(p, destruct_af_mpv_node);
+ return p;
+}
+
+// Prototype for autofree functions which can be called from inside the vm.
+typedef void (*af_CFunction)(js_State*, void*);
+
+// safely run autofree js c function directly
+static int s_run_af_jsc(js_State *J, af_CFunction fn, void **af)
+{
+ if (js_try(J))
+ return 1;
+ *af = talloc_new(NULL);
+ fn(J, *af);
+ js_endtry(J);
+ return 0;
+}
+
+// The trampoline function through which all autofree functions are called from
+// inside the vm. Obtains the target function address and autofree-call it.
+static void script__autofree(js_State *J)
+{
+ // The target function is at the "af_" property of this function instance.
+ js_currentfunction(J);
+ js_getproperty(J, -1, "af_");
+ af_CFunction fn = (af_CFunction)js_touserdata(J, -1, "af_fn");
+ js_pop(J, 2);
+
+ void *af = NULL;
+ int r = s_run_af_jsc(J, fn, &af);
+ talloc_free(af);
+ if (r)
+ js_throw(J);
+}
+
+// Identical to js_newcfunction, but the function is inserted with an autofree
+// wrapper, and its prototype should have the additional af argument.
+static void af_newcfunction(js_State *J, af_CFunction fn, const char *name,
+ int length)
+{
+ js_newcfunction(J, script__autofree, name, length);
+ js_pushnull(J); // a prototype for the userdata object
+ js_newuserdata(J, "af_fn", fn, NULL); // uses a "af_fn" verification tag
+ js_defproperty(J, -2, "af_", JS_READONLY | JS_DONTENUM | JS_DONTCONF);
+}
+
+/**********************************************************************
+ * Initialization and file loading
+ *********************************************************************/
+
+static const char *get_builtin_file(const char *name)
+{
+ for (int n = 0; builtin_files[n][0]; n++) {
+ if (strcmp(builtin_files[n][0], name) == 0)
+ return builtin_files[n][1];
+ }
+ return NULL;
+}
+
+// Push up to limit bytes of file fname: from builtin_files, else from the OS.
+static void af_push_file(js_State *J, const char *fname, int limit, void *af)
+{
+ char *filename = mp_get_user_path(af, jctx(J)->mpctx->global, fname);
+ MP_VERBOSE(jctx(J), "Reading file '%s'\n", filename);
+ if (limit < 0)
+ limit = INT_MAX - 1;
+
+ const char *builtin = get_builtin_file(filename);
+ if (builtin) {
+ js_pushlstring(J, builtin, MPMIN(limit, strlen(builtin)));
+ return;
+ }
+
+ FILE *f = fopen(filename, "rb");
+ if (!f)
+ js_error(J, "cannot open file: '%s'", filename);
+ add_af_file(af, f);
+
+ int len = MPMIN(limit, 32 * 1024); // initial allocation, size*2 strategy
+ int got = 0;
+ char *s = NULL;
+ while ((s = talloc_realloc(af, s, char, len))) {
+ int want = len - got;
+ int r = fread(s + got, 1, want, f);
+
+ if (feof(f) || (len == limit && r == want)) {
+ js_pushlstring(J, s, got + r);
+ return;
+ }
+ if (r != want)
+ js_error(J, "cannot read data from file: '%s'", filename);
+
+ got = got + r;
+ len = MPMIN(limit, len * 2);
+ }
+
+ js_error(J, "cannot allocate %d bytes for file: '%s'", len, filename);
+}
+
+// Safely run af_push_file.
+static int s_push_file(js_State *J, const char *fname, int limit, void **af)
+{
+ if (js_try(J))
+ return 1;
+ *af = talloc_new(NULL);
+ af_push_file(J, fname, limit, *af);
+ js_endtry(J);
+ return 0;
+}
+
+// Called directly, push up to limit bytes of file fname (from builtin/os).
+static void push_file_content(js_State *J, const char *fname, int limit)
+{
+ void *af = NULL;
+ int r = s_push_file(J, fname, limit, &af);
+ talloc_free(af);
+ if (r)
+ js_throw(J);
+}
+
+// utils.read_file(..). args: fname [,max]. returns [up to max] bytes as string.
+static void script_read_file(js_State *J)
+{
+ int limit = js_isundefined(J, 2) ? -1 : jsL_checkint(J, 2);
+ push_file_content(J, js_tostring(J, 1), limit);
+}
+
+// Runs a file with the caller's this, leaves the stack as is.
+static void run_file(js_State *J, const char *fname)
+{
+ MP_VERBOSE(jctx(J), "Loading file %s\n", fname);
+ push_file_content(J, fname, -1);
+ js_loadstring(J, fname, js_tostring(J, -1));
+ js_copy(J, 0); // use the caller's this
+ js_call(J, 0);
+ js_pop(J, 2); // result, file content
+}
+
+// The spec defines .name and .message for Error objects. Most engines also set
+// a very convenient .stack = name + message + trace, but MuJS instead sets
+// .stackTrace = trace only. Normalize by adding such .stack if required.
+// Run this before anything such that we can get traces on any following errors.
+static const char *norm_err_proto_js = "\
+ if (Error().stackTrace && !Error().stack) {\
+ Object.defineProperty(Error.prototype, 'stack', {\
+ get: function() {\
+ return this.name + ': ' + this.message + this.stackTrace;\
+ }\
+ });\
+ }\
+";
+
+static void add_functions(js_State*, struct script_ctx*);
+
+// args: none. called as script, setup and run the main script
+static void script__run_script(js_State *J)
+{
+ js_loadstring(J, "@/norm_err.js", norm_err_proto_js);
+ js_copy(J, 0);
+ js_pcall(J, 0);
+
+ struct script_ctx *ctx = jctx(J);
+ add_functions(J, ctx);
+ run_file(J, "@/defaults.js");
+ run_file(J, ctx->filename); // the main file to run
+
+ if (!js_hasproperty(J, 0, "mp_event_loop") || !js_iscallable(J, -1))
+ js_error(J, "no event loop function");
+ js_copy(J, 0);
+ js_call(J, 0); // mp_event_loop
+}
+
+// Safely set last error from stack top: stack trace or toString or generic.
+// May leave items on stack - the caller should detect and pop if it cares.
+static void s_top_to_last_error(struct script_ctx *ctx, js_State *J)
+{
+ set_last_error(ctx, 1, "unknown error");
+ if (js_try(J))
+ return;
+ if (js_isobject(J, -1))
+ js_hasproperty(J, -1, "stack"); // fetches it if exists
+ set_last_error(ctx, 1, js_tostring(J, -1));
+ js_endtry(J);
+}
+
+// MuJS can report warnings through this.
+static void report_handler(js_State *J, const char *msg)
+{
+ MP_WARN(jctx(J), "[JS] %s\n", msg);
+}
+
+// Safely setup the js vm for calling run_script.
+static int s_init_js(js_State *J, struct script_ctx *ctx)
+{
+ if (js_try(J))
+ return 1;
+ js_setcontext(J, ctx);
+ js_setreport(J, report_handler);
+ js_newcfunction(J, script__run_script, "run_script", 0);
+ js_pushglobal(J); // 'this' for script__run_script
+ js_endtry(J);
+ return 0;
+}
+
+static void *mp_js_alloc(void *actx, void *ptr, int size_)
+{
+ if (size_ < 0)
+ return NULL;
+
+ struct script_ctx* ctx = actx;
+ size_t size = size_, osize = 0;
+ if (ptr) // free/realloc
+ osize = ta_get_size(ptr);
+
+ void *ret = talloc_realloc_size(actx, ptr, size);
+
+ if (!size || ret) { // free / successful realloc/malloc
+ ctx->js_malloc_size = ctx->js_malloc_size - osize + size;
+ stats_size_value(ctx->stats, "mem", ctx->js_malloc_size);
+ }
+ return ret;
+}
+
+/**********************************************************************
+ * Initialization - booting the script
+ *********************************************************************/
+// s_load_javascript: (entry point) creates the js vm, runs the script, returns
+// on script exit or uncaught js errors. Never throws.
+// script__run_script: - loads the built in functions and vars into the vm
+// - runs the default file[s] and the main script file
+// - calls mp_event_loop, returns on script-exit or throws.
+//
+// Note: init functions don't need autofree. They can use ctx as a talloc
+// context and free normally. If they throw - ctx is freed right afterwards.
+static int s_load_javascript(struct mp_script_args *args)
+{
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .client = args->client,
+ .mpctx = args->mpctx,
+ .log = args->log,
+ .last_error_str = talloc_strdup(ctx, "Cannot initialize JavaScript"),
+ .filename = args->filename,
+ .path = args->path,
+ .js_malloc_size = 0,
+ .stats = stats_ctx_create(ctx, args->mpctx->global,
+ mp_tprintf(80, "script/%s", mpv_client_name(args->client))),
+ };
+
+ stats_register_thread_cputime(ctx->stats, "cpu");
+
+ js_Alloc alloc_fn = NULL;
+ void *actx = NULL;
+
+ if (args->mpctx->opts->js_memory_report) {
+ alloc_fn = mp_js_alloc;
+ actx = ctx;
+ }
+
+ int r = -1;
+ js_State *J = js_newstate(alloc_fn, actx, 0);
+ if (!J || s_init_js(J, ctx))
+ goto error_out;
+
+ set_last_error(ctx, 0, NULL);
+ if (js_pcall(J, 0)) { // script__run_script
+ s_top_to_last_error(ctx, J);
+ goto error_out;
+ }
+
+ r = 0;
+
+error_out:
+ if (r)
+ MP_FATAL(ctx, "%s\n", ctx->last_error_str);
+ if (J)
+ js_freestate(J);
+
+ talloc_free(ctx);
+ return r;
+}
+
+/**********************************************************************
+ * Main mp.* scripting APIs and helpers
+ *********************************************************************/
+// Return the index in opts of stack[idx] (or of def if undefined), else throws.
+static int checkopt(js_State *J, int idx, const char *def, const char *opts[],
+ const char *desc)
+{
+ const char *opt = js_isundefined(J, idx) ? def : js_tostring(J, idx);
+ for (int i = 0; opts[i]; i++) {
+ if (strcmp(opt, opts[i]) == 0)
+ return i;
+ }
+ js_error(J, "Invalid %s '%s'", desc, opt);
+}
+
+// args: level as string and a variable numbers of args to print. adds final \n
+static void script_log(js_State *J)
+{
+ const char *level = js_tostring(J, 1);
+ int msgl = mp_msg_find_level(level);
+ if (msgl < 0)
+ js_error(J, "Invalid log level '%s'", level);
+
+ struct mp_log *log = jctx(J)->log;
+ for (int top = js_gettop(J), i = 2; i < top; i++)
+ mp_msg(log, msgl, (i == 2 ? "%s" : " %s"), js_tostring(J, i));
+ mp_msg(log, msgl, "\n");
+ push_success(J);
+}
+
+static void script_find_config_file(js_State *J, void *af)
+{
+ const char *fname = js_tostring(J, 1);
+ char *path = mp_find_config_file(af, jctx(J)->mpctx->global, fname);
+ if (path) {
+ js_pushstring(J, path);
+ } else {
+ push_failure(J, "not found");
+ }
+}
+
+static void script__request_event(js_State *J)
+{
+ const char *event = js_tostring(J, 1);
+ bool enable = js_toboolean(J, 2);
+
+ for (int n = 0; n < 256; n++) {
+ // some n's may be missing ("holes"), returning NULL
+ const char *name = mpv_event_name(n);
+ if (name && strcmp(name, event) == 0) {
+ push_status(J, mpv_request_event(jclient(J), n, enable));
+ return;
+ }
+ }
+ push_failure(J, "Unknown event name");
+}
+
+static void script_enable_messages(js_State *J)
+{
+ const char *level = js_tostring(J, 1);
+ int e = mpv_request_log_messages(jclient(J), level);
+ if (e == MPV_ERROR_INVALID_PARAMETER)
+ js_error(J, "Invalid log level '%s'", level);
+ push_status(J, e);
+}
+
+// args - command [with arguments] as string
+static void script_command(js_State *J)
+{
+ push_status(J, mpv_command_string(jclient(J), js_tostring(J, 1)));
+}
+
+// args: strings of command and then variable number of arguments
+static void script_commandv(js_State *J)
+{
+ const char *argv[MP_CMD_MAX_ARGS + 1];
+ int length = js_gettop(J) - 1;
+ if (length >= MP_ARRAY_SIZE(argv))
+ js_error(J, "Too many arguments");
+
+ for (int i = 0; i < length; i++)
+ argv[i] = js_tostring(J, 1 + i);
+ argv[length] = NULL;
+ push_status(J, mpv_command(jclient(J), argv));
+}
+
+// args: name, string value
+static void script_set_property(js_State *J)
+{
+ int e = mpv_set_property_string(jclient(J), js_tostring(J, 1),
+ js_tostring(J, 2));
+ push_status(J, e);
+}
+
+// args: name, boolean
+static void script_set_property_bool(js_State *J)
+{
+ int v = js_toboolean(J, 2);
+ int e = mpv_set_property(jclient(J), js_tostring(J, 1), MPV_FORMAT_FLAG, &v);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_number(js_State *J)
+{
+ double result;
+ const char *name = js_tostring(J, 1);
+ int e = mpv_get_property(jclient(J), name, MPV_FORMAT_DOUBLE, &result);
+ if (!pushed_error(J, e, 2))
+ js_pushnumber(J, result);
+}
+
+// args: name, native value
+static void script_set_property_native(js_State *J, void *af)
+{
+ mpv_node node;
+ makenode(af, &node, J, 2);
+ mpv_handle *h = jclient(J);
+ int e = mpv_set_property(h, js_tostring(J, 1), MPV_FORMAT_NODE, &node);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property(js_State *J, void *af)
+{
+ mpv_handle *h = jclient(J);
+ char *res = NULL;
+ int e = mpv_get_property(h, js_tostring(J, 1), MPV_FORMAT_STRING, &res);
+ if (e >= 0)
+ add_af_mpv_alloc(af, res);
+ if (!pushed_error(J, e, 2))
+ js_pushstring(J, res);
+}
+
+// args: name
+static void script_del_property(js_State *J)
+{
+ int e = mpv_del_property(jclient(J), js_tostring(J, 1));
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_bool(js_State *J)
+{
+ int result;
+ mpv_handle *h = jclient(J);
+ int e = mpv_get_property(h, js_tostring(J, 1), MPV_FORMAT_FLAG, &result);
+ if (!pushed_error(J, e, 2))
+ js_pushboolean(J, result);
+}
+
+// args: name, number
+static void script_set_property_number(js_State *J)
+{
+ double v = js_tonumber(J, 2);
+ mpv_handle *h = jclient(J);
+ int e = mpv_set_property(h, js_tostring(J, 1), MPV_FORMAT_DOUBLE, &v);
+ push_status(J, e);
+}
+
+// args: name [,def]
+static void script_get_property_native(js_State *J, void *af)
+{
+ const char *name = js_tostring(J, 1);
+ mpv_handle *h = jclient(J);
+ mpv_node *presult_node = new_af_mpv_node(af);
+ int e = mpv_get_property(h, name, MPV_FORMAT_NODE, presult_node);
+ if (!pushed_error(J, e, 2))
+ pushnode(J, presult_node);
+}
+
+// args: name [,def]
+static void script_get_property_osd(js_State *J, void *af)
+{
+ const char *name = js_tostring(J, 1);
+ mpv_handle *h = jclient(J);
+ char *res = NULL;
+ int e = mpv_get_property(h, name, MPV_FORMAT_OSD_STRING, &res);
+ if (e >= 0)
+ add_af_mpv_alloc(af, res);
+ if (!pushed_error(J, e, 2))
+ js_pushstring(J, res);
+}
+
+// args: id, name, type
+static void script__observe_property(js_State *J)
+{
+ const char *fmts[] = {"none", "native", "bool", "string", "number", NULL};
+ const mpv_format mf[] = {MPV_FORMAT_NONE, MPV_FORMAT_NODE, MPV_FORMAT_FLAG,
+ MPV_FORMAT_STRING, MPV_FORMAT_DOUBLE};
+
+ mpv_format f = mf[checkopt(J, 3, "none", fmts, "observe type")];
+ int e = mpv_observe_property(jclient(J), jsL_checkuint64(J, 1),
+ js_tostring(J, 2),
+ f);
+ push_status(J, e);
+}
+
+// args: id
+static void script__unobserve_property(js_State *J)
+{
+ int e = mpv_unobserve_property(jclient(J), jsL_checkuint64(J, 1));
+ push_status(J, e);
+}
+
+// args: native (array of command and args, similar to commandv) [,def]
+static void script_command_native(js_State *J, void *af)
+{
+ mpv_node cmd;
+ makenode(af, &cmd, J, 1);
+ mpv_node *presult_node = new_af_mpv_node(af);
+ int e = mpv_command_node(jclient(J), &cmd, presult_node);
+ if (!pushed_error(J, e, 2))
+ pushnode(J, presult_node);
+}
+
+// args: async-command-id, native-command
+static void script__command_native_async(js_State *J, void *af)
+{
+ uint64_t id = jsL_checkuint64(J, 1);
+ struct mpv_node node;
+ makenode(af, &node, J, 2);
+ push_status(J, mpv_command_node_async(jclient(J), id, &node));
+}
+
+// args: async-command-id
+static void script__abort_async_command(js_State *J)
+{
+ mpv_abort_async_command(jclient(J), jsL_checkuint64(J, 1));
+ push_success(J);
+}
+
+// args: none, result in millisec
+static void script_get_time_ms(js_State *J)
+{
+ js_pushnumber(J, mpv_get_time_us(jclient(J)) / (double)(1000));
+}
+
+// push object with properties names (NULL terminated) with respective vals
+static void push_nums_obj(js_State *J, const char * const names[],
+ const double vals[])
+{
+ js_newobject(J);
+ for (int i = 0; names[i]; i++) {
+ js_pushnumber(J, vals[i]);
+ js_setproperty(J, -2, names[i]);
+ }
+}
+
+// args: input-section-name, x0, y0, x1, y1
+static void script_input_set_section_mouse_area(js_State *J)
+{
+ char *section = (char *)js_tostring(J, 1);
+ mp_input_set_section_mouse_area(jctx(J)->mpctx->input, section,
+ jsL_checkint(J, 2), jsL_checkint(J, 3), // x0, y0
+ jsL_checkint(J, 4), jsL_checkint(J, 5)); // x1, y1
+ push_success(J);
+}
+
+// args: time-in-ms [,format-string]
+static void script_format_time(js_State *J, void *af)
+{
+ double t = js_tonumber(J, 1);
+ const char *fmt = js_isundefined(J, 2) ? "%H:%M:%S" : js_tostring(J, 2);
+ char *r = talloc_steal(af, mp_format_time_fmt(fmt, t));
+ if (!r)
+ js_error(J, "Invalid time format string '%s'", fmt);
+ js_pushstring(J, r);
+}
+
+// TODO: untested
+static void script_get_wakeup_pipe(js_State *J)
+{
+ js_pushnumber(J, mpv_get_wakeup_pipe(jclient(J)));
+}
+
+// args: name (str), priority (int), id (uint)
+static void script__hook_add(js_State *J)
+{
+ const char *name = js_tostring(J, 1);
+ int pri = jsL_checkint(J, 2);
+ uint64_t id = jsL_checkuint64(J, 3);
+ push_status(J, mpv_hook_add(jclient(J), id, name, pri));
+}
+
+// args: id (uint)
+static void script__hook_continue(js_State *J)
+{
+ push_status(J, mpv_hook_continue(jclient(J), jsL_checkuint64(J, 1)));
+}
+
+/**********************************************************************
+ * mp.utils
+ *********************************************************************/
+
+// args: [path [,filter]]
+static void script_readdir(js_State *J, void *af)
+{
+ // 0 1 2 3
+ const char *filters[] = {"all", "files", "dirs", "normal", NULL};
+ const char *path = js_isundefined(J, 1) ? "." : js_tostring(J, 1);
+ int t = checkopt(J, 2, "normal", filters, "listing filter");
+
+ DIR *dir = opendir(path);
+ if (!dir) {
+ push_failure(J, "Cannot open dir");
+ return;
+ }
+ add_af_dir(af, dir);
+ set_last_error(jctx(J), 0, NULL);
+ js_newarray(J); // the return value
+ char *fullpath = talloc_strdup(af, "");
+ struct dirent *e;
+ int n = 0;
+ while ((e = readdir(dir))) {
+ char *name = e->d_name;
+ if (t) {
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+ continue;
+ if (fullpath)
+ fullpath[0] = '\0';
+ fullpath = talloc_asprintf_append(fullpath, "%s/%s", path, name);
+ struct stat st;
+ if (stat(fullpath, &st))
+ continue;
+ if (!(((t & 1) && S_ISREG(st.st_mode)) ||
+ ((t & 2) && S_ISDIR(st.st_mode))))
+ {
+ continue;
+ }
+ }
+ js_pushstring(J, name);
+ js_setindex(J, -2, n++);
+ }
+}
+
+static void script_file_info(js_State *J)
+{
+ const char *path = js_tostring(J, 1);
+
+ struct stat statbuf;
+ if (stat(path, &statbuf) != 0) {
+ push_failure(J, "Cannot stat path");
+ return;
+ }
+ // Clear last error
+ set_last_error(jctx(J), 0, NULL);
+
+ const char * stat_names[] = {
+ "mode", "size",
+ "atime", "mtime", "ctime", NULL
+ };
+ const double stat_values[] = {
+ statbuf.st_mode,
+ statbuf.st_size,
+ statbuf.st_atime,
+ statbuf.st_mtime,
+ statbuf.st_ctime
+ };
+ // Create an object and add all fields
+ push_nums_obj(J, stat_names, stat_values);
+
+ // Convenience booleans
+ js_pushboolean(J, S_ISREG(statbuf.st_mode));
+ js_setproperty(J, -2, "is_file");
+
+ js_pushboolean(J, S_ISDIR(statbuf.st_mode));
+ js_setproperty(J, -2, "is_dir");
+}
+
+
+static void script_split_path(js_State *J)
+{
+ const char *p = js_tostring(J, 1);
+ bstr fname = mp_dirname(p);
+ js_newarray(J);
+ js_pushlstring(J, fname.start, fname.len);
+ js_setindex(J, -2, 0);
+ js_pushstring(J, mp_basename(p));
+ js_setindex(J, -2, 1);
+}
+
+static void script_join_path(js_State *J, void *af)
+{
+ js_pushstring(J, mp_path_join(af, js_tostring(J, 1), js_tostring(J, 2)));
+}
+
+// args: is_append, prefixed file name, data (c-str)
+static void script__write_file(js_State *J, void *af)
+{
+ static const char *prefix = "file://";
+ bool append = js_toboolean(J, 1);
+ const char *fname = js_tostring(J, 2);
+ const char *data = js_tostring(J, 3);
+ const char *opstr = append ? "append" : "write";
+
+ if (strstr(fname, prefix) != fname) // simple protection for incorrect use
+ js_error(J, "File name must be prefixed with '%s'", prefix);
+ fname += strlen(prefix);
+ fname = mp_get_user_path(af, jctx(J)->mpctx->global, fname);
+ MP_VERBOSE(jctx(J), "%s file '%s'\n", opstr, fname);
+
+ FILE *f = fopen(fname, append ? "ab" : "wb");
+ if (!f)
+ js_error(J, "Cannot open (%s) file: '%s'", opstr, fname);
+ add_af_file(af, f);
+
+ int len = strlen(data); // limited by terminating null
+ int wrote = fwrite(data, 1, len, f);
+ if (len != wrote)
+ js_error(J, "Cannot %s to file: '%s'", opstr, fname);
+ js_pushboolean(J, 1); // success. doesn't touch last_error
+}
+
+// args: env var name
+static void script_getenv(js_State *J)
+{
+ const char *v = getenv(js_tostring(J, 1));
+ if (v) {
+ js_pushstring(J, v);
+ } else {
+ js_pushundefined(J);
+ }
+}
+
+// args: none
+static void script_get_env_list(js_State *J)
+{
+ js_newarray(J);
+ for (int n = 0; environ && environ[n]; n++) {
+ js_pushstring(J, environ[n]);
+ js_setindex(J, -2, n);
+ }
+}
+
+// args: as-filename, content-string, returns the compiled result as a function
+static void script_compile_js(js_State *J)
+{
+ js_loadstring(J, js_tostring(J, 1), js_tostring(J, 2));
+}
+
+// args: true = print info (with the warning report function - no info report)
+static void script__gc(js_State *J)
+{
+ js_gc(J, js_toboolean(J, 1) ? 1 : 0);
+ push_success(J);
+}
+
+/**********************************************************************
+ * Core functions: pushnode, makenode and the event loop backend
+ *********************************************************************/
+
+// pushes a js value/array/object from an mpv_node
+static void pushnode(js_State *J, mpv_node *node)
+{
+ int len;
+ switch (node->format) {
+ case MPV_FORMAT_NONE: js_pushnull(J); break;
+ case MPV_FORMAT_STRING: js_pushstring(J, node->u.string); break;
+ case MPV_FORMAT_INT64: js_pushnumber(J, node->u.int64); break;
+ case MPV_FORMAT_DOUBLE: js_pushnumber(J, node->u.double_); break;
+ case MPV_FORMAT_FLAG: js_pushboolean(J, node->u.flag); break;
+ case MPV_FORMAT_BYTE_ARRAY:
+ js_pushlstring(J, node->u.ba->data, node->u.ba->size);
+ break;
+ case MPV_FORMAT_NODE_ARRAY:
+ js_newarray(J);
+ len = node->u.list->num;
+ for (int n = 0; n < len; n++) {
+ pushnode(J, &node->u.list->values[n]);
+ js_setindex(J, -2, n);
+ }
+ break;
+ case MPV_FORMAT_NODE_MAP:
+ js_newobject(J);
+ len = node->u.list->num;
+ for (int n = 0; n < len; n++) {
+ pushnode(J, &node->u.list->values[n]);
+ js_setproperty(J, -2, node->u.list->keys[n]);
+ }
+ break;
+ default:
+ js_pushstring(J, "[UNSUPPORTED_MPV_FORMAT]");
+ break;
+ }
+}
+
+// For the object at stack index idx, extract the (own) property names into
+// keys array (and allocate it to accommodate) and return the number of keys.
+static int get_obj_properties(void *ta_ctx, char ***keys, js_State *J, int idx)
+{
+ int length = 0;
+ js_pushiterator(J, idx, 1);
+
+ *keys = talloc_new(ta_ctx);
+ const char *name;
+ while ((name = js_nextiterator(J, -1)))
+ MP_TARRAY_APPEND(ta_ctx, *keys, length, talloc_strdup(ta_ctx, name));
+
+ js_pop(J, 1); // the iterator
+ return length;
+}
+
+// true if we don't lose (too much) precision when casting to int64
+static bool same_as_int64(double d)
+{
+ // The range checks also validly filter inf and nan, so behavior is defined
+ return d >= INT64_MIN && d <= (double) INT64_MAX && d == (int64_t)d;
+}
+
+static int jsL_checkint(js_State *J, int idx)
+{
+ double d = js_tonumber(J, idx);
+ if (!(d >= INT_MIN && d <= INT_MAX))
+ js_error(J, "int out of range at index %d", idx);
+ return d;
+}
+
+static uint64_t jsL_checkuint64(js_State *J, int idx)
+{
+ double d = js_tonumber(J, idx);
+ if (!(d >= 0 && d <= (double) UINT64_MAX))
+ js_error(J, "uint64 out of range at index %d", idx);
+ return d;
+}
+
+// From the js stack value/array/object at index idx
+static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx)
+{
+ if (js_isundefined(J, idx) || js_isnull(J, idx)) {
+ dst->format = MPV_FORMAT_NONE;
+
+ } else if (js_isboolean(J, idx)) {
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = js_toboolean(J, idx);
+
+ } else if (js_isnumber(J, idx)) {
+ double val = js_tonumber(J, idx);
+ if (same_as_int64(val)) { // use int, because we can
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = val;
+ } else {
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = val;
+ }
+
+ } else if (js_isarray(J, idx)) {
+ dst->format = MPV_FORMAT_NODE_ARRAY;
+ dst->u.list = talloc(ta_ctx, struct mpv_node_list);
+ dst->u.list->keys = NULL;
+
+ int length = js_getlength(J, idx);
+ dst->u.list->num = length;
+ dst->u.list->values = talloc_array(ta_ctx, mpv_node, length);
+ for (int n = 0; n < length; n++) {
+ js_getindex(J, idx, n);
+ makenode(ta_ctx, &dst->u.list->values[n], J, -1);
+ js_pop(J, 1);
+ }
+
+ } else if (js_isobject(J, idx)) {
+ dst->format = MPV_FORMAT_NODE_MAP;
+ dst->u.list = talloc(ta_ctx, struct mpv_node_list);
+
+ int length = get_obj_properties(ta_ctx, &dst->u.list->keys, J, idx);
+ dst->u.list->num = length;
+ dst->u.list->values = talloc_array(ta_ctx, mpv_node, length);
+ for (int n = 0; n < length; n++) {
+ js_getproperty(J, idx, dst->u.list->keys[n]);
+ makenode(ta_ctx, &dst->u.list->values[n], J, -1);
+ js_pop(J, 1);
+ }
+
+ } else { // string, or anything else as string
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(ta_ctx, js_tostring(J, idx));
+ }
+}
+
+// args: wait in secs (infinite if negative) if mpv doesn't send events earlier.
+static void script_wait_event(js_State *J, void *af)
+{
+ double timeout = js_isnumber(J, 1) ? js_tonumber(J, 1) : -1;
+ mpv_event *event = mpv_wait_event(jclient(J), timeout);
+
+ mpv_node *rn = new_af_mpv_node(af);
+ mpv_event_to_node(rn, event);
+ pushnode(J, rn);
+}
+
+/**********************************************************************
+ * Script functions setup
+ *********************************************************************/
+#define FN_ENTRY(name, length) {#name, length, script_ ## name, NULL}
+#define AF_ENTRY(name, length) {#name, length, NULL, script_ ## name}
+struct fn_entry {
+ const char *name;
+ int length;
+ js_CFunction jsc_fn;
+ af_CFunction afc_fn;
+};
+
+// Names starting with underscore are wrapped at @defaults.js
+// FN_ENTRY is a normal js C function, AF_ENTRY is an autofree js C function.
+static const struct fn_entry main_fns[] = {
+ FN_ENTRY(log, 1),
+ AF_ENTRY(wait_event, 1),
+ FN_ENTRY(_request_event, 2),
+ AF_ENTRY(find_config_file, 1),
+ FN_ENTRY(command, 1),
+ FN_ENTRY(commandv, 0),
+ AF_ENTRY(command_native, 2),
+ AF_ENTRY(_command_native_async, 2),
+ FN_ENTRY(_abort_async_command, 1),
+ FN_ENTRY(del_property, 1),
+ FN_ENTRY(get_property_bool, 2),
+ FN_ENTRY(get_property_number, 2),
+ AF_ENTRY(get_property_native, 2),
+ AF_ENTRY(get_property, 2),
+ AF_ENTRY(get_property_osd, 2),
+ FN_ENTRY(set_property, 2),
+ FN_ENTRY(set_property_bool, 2),
+ FN_ENTRY(set_property_number, 2),
+ AF_ENTRY(set_property_native, 2),
+ FN_ENTRY(_observe_property, 3),
+ FN_ENTRY(_unobserve_property, 1),
+ FN_ENTRY(get_time_ms, 0),
+ AF_ENTRY(format_time, 2),
+ FN_ENTRY(enable_messages, 1),
+ FN_ENTRY(get_wakeup_pipe, 0),
+ FN_ENTRY(_hook_add, 3),
+ FN_ENTRY(_hook_continue, 1),
+ FN_ENTRY(input_set_section_mouse_area, 5),
+ FN_ENTRY(last_error, 0),
+ FN_ENTRY(_set_last_error, 1),
+ {0}
+};
+
+static const struct fn_entry utils_fns[] = {
+ AF_ENTRY(readdir, 2),
+ FN_ENTRY(file_info, 1),
+ FN_ENTRY(split_path, 1),
+ AF_ENTRY(join_path, 2),
+ FN_ENTRY(get_env_list, 0),
+
+ FN_ENTRY(read_file, 2),
+ AF_ENTRY(_write_file, 3),
+ FN_ENTRY(getenv, 1),
+ FN_ENTRY(compile_js, 2),
+ FN_ENTRY(_gc, 1),
+ {0}
+};
+
+// Adds an object <module> with the functions at e to the top object
+static void add_package_fns(js_State *J, const char *module,
+ const struct fn_entry *e)
+{
+ js_newobject(J);
+ for (int n = 0; e[n].name; n++) {
+ if (e[n].jsc_fn) {
+ js_newcfunction(J, e[n].jsc_fn, e[n].name, e[n].length);
+ } else {
+ af_newcfunction(J, e[n].afc_fn, e[n].name, e[n].length);
+ }
+ js_setproperty(J, -2, e[n].name);
+ }
+ js_setproperty(J, -2, module);
+}
+
+// Called directly, adds functions/vars to the caller's this.
+static void add_functions(js_State *J, struct script_ctx *ctx)
+{
+ js_copy(J, 0);
+ add_package_fns(J, "mp", main_fns);
+ js_getproperty(J, 0, "mp"); // + this mp
+ add_package_fns(J, "utils", utils_fns);
+
+ js_pushstring(J, mpv_client_name(ctx->client));
+ js_setproperty(J, -2, "script_name");
+
+ js_pushstring(J, ctx->filename);
+ js_setproperty(J, -2, "script_file");
+
+ if (ctx->path) {
+ js_pushstring(J, ctx->path);
+ js_setproperty(J, -2, "script_path");
+ }
+
+ js_pop(J, 2); // leave the stack as we got it
+}
+
+// main export of this file, used by cplayer to load js scripts
+const struct mp_scripting mp_scripting_js = {
+ .name = "js",
+ .file_ext = "js",
+ .load = s_load_javascript,
+};
diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js
new file mode 100644
index 0000000..d906ec2
--- /dev/null
+++ b/player/javascript/defaults.js
@@ -0,0 +1,782 @@
+"use strict";
+(function main_default_js(g) {
+// - g is the global object.
+// - User callbacks called without 'this', global only if callee is non-strict.
+// - The names of function expressions are not required, but are used in stack
+// traces. We name them where useful to show up (fname:#line always shows).
+
+mp.msg = { log: mp.log };
+mp.msg.verbose = mp.log.bind(null, "v");
+var levels = ["fatal", "error", "warn", "info", "debug", "trace"];
+levels.forEach(function(l) { mp.msg[l] = mp.log.bind(null, l) });
+
+// same as {} but without inherited stuff, e.g. o["toString"] doesn't exist.
+// used where we try to fetch items by keys which we don't absolutely trust.
+function new_cache() {
+ return Object.create(null, {});
+}
+
+/**********************************************************************
+ * event handlers, property observers, idle, client messages, hooks, async
+ *********************************************************************/
+var ehandlers = new_cache() // items of event-name: array of {maybe cb: fn}
+
+mp.register_event = function(name, fn) {
+ if (!ehandlers[name])
+ ehandlers[name] = [];
+ ehandlers[name] = ehandlers[name].concat([{cb: fn}]); // replaces the arr
+ return mp._request_event(name, true);
+}
+
+mp.unregister_event = function(fn) {
+ for (var name in ehandlers) {
+ ehandlers[name] = ehandlers[name].filter(function(h) {
+ if (h.cb != fn)
+ return true;
+ delete h.cb; // dispatch could have a ref to h
+ }); // replacing, not mutating the array
+ if (!ehandlers[name].length) {
+ delete ehandlers[name];
+ mp._request_event(name, false);
+ }
+ }
+}
+
+// call only pre-registered handlers, but not ones which got unregistered
+function dispatch_event(e) {
+ var handlers = ehandlers[e.event];
+ if (handlers) {
+ for (var len = handlers.length, i = 0; i < len; i++) {
+ var cb = handlers[i].cb; // 'handlers' won't mutate, but unregister
+ if (cb) // could remove cb from some items
+ cb(e);
+ }
+ }
+}
+
+// ----- idle observers -----
+var iobservers = [], // array of callbacks
+ ideleted = false;
+
+mp.register_idle = function(fn) {
+ iobservers.push(fn);
+}
+
+mp.unregister_idle = function(fn) {
+ iobservers.forEach(function(f, i) {
+ if (f == fn)
+ delete iobservers[i]; // -> same length but [more] sparse
+ });
+ ideleted = true;
+}
+
+function notify_idle_observers() {
+ // forEach and filter skip deleted items and newly added items
+ iobservers.forEach(function(f) { f() });
+ if (ideleted) {
+ iobservers = iobservers.filter(function() { return true });
+ ideleted = false;
+ }
+}
+
+// ----- property observers -----
+var next_oid = 1,
+ observers = new_cache(); // items of id: fn
+
+mp.observe_property = function(name, format, fn) {
+ var id = next_oid++;
+ observers[id] = fn;
+ return mp._observe_property(id, name, format || undefined); // allow null
+}
+
+mp.unobserve_property = function(fn) {
+ for (var id in observers) {
+ if (observers[id] == fn) {
+ delete observers[id];
+ mp._unobserve_property(id);
+ }
+ }
+}
+
+function notify_observer(e) {
+ var cb = observers[e.id];
+ if (cb)
+ cb(e.name, e.data);
+}
+
+// ----- Client messages -----
+var messages = new_cache(); // items of name: fn
+
+// overrides name. no libmpv API to reg/unreg specific messages.
+mp.register_script_message = function(name, fn) {
+ messages[name] = fn;
+}
+
+mp.unregister_script_message = function(name) {
+ delete messages[name];
+}
+
+function dispatch_message(ev) {
+ var cb = ev.args.length ? messages[ev.args[0]] : false;
+ if (cb)
+ cb.apply(null, ev.args.slice(1));
+}
+
+// ----- hooks -----
+var hooks = []; // array of callbacks, id is index+1
+
+function run_hook(ev) {
+ var state = 0; // 0:initial, 1:deferred, 2:continued
+ function do_cont() { return state = 2, mp._hook_continue(ev.hook_id) }
+
+ function err() { return mp.msg.error("hook already continued"), undefined }
+ function usr_defer() { return state == 2 ? err() : (state = 1, true) }
+ function usr_cont() { return state == 2 ? err() : do_cont() }
+
+ var cb = ev.id > 0 && hooks[ev.id - 1];
+ if (cb)
+ cb({ defer: usr_defer, cont: usr_cont });
+ return state == 0 ? do_cont() : true;
+}
+
+mp.add_hook = function add_hook(name, pri, fn) {
+ hooks.push(fn);
+ // 50 (scripting docs default priority) maps to 0 (default in C API docs)
+ return mp._hook_add(name, pri - 50, hooks.length);
+}
+
+// ----- async commands -----
+var async_callbacks = new_cache(); // items of id: fn
+var async_next_id = 1;
+
+mp.command_native_async = function command_native_async(node, cb) {
+ var id = async_next_id++;
+ cb = cb || function dummy() {};
+ if (!mp._command_native_async(id, node)) {
+ var le = mp.last_error();
+ setTimeout(cb, 0, false, undefined, le); /* callback async */
+ mp._set_last_error(le);
+ return undefined;
+ }
+ async_callbacks[id] = cb;
+ return id;
+}
+
+function async_command_handler(ev) {
+ var cb = async_callbacks[ev.id];
+ delete async_callbacks[ev.id];
+ if (ev.error)
+ cb(false, undefined, ev.error);
+ else
+ cb(true, ev.result, "");
+}
+
+mp.abort_async_command = function abort_async_command(id) {
+ // cb will be invoked regardless, possibly with the abort result
+ if (async_callbacks[id])
+ mp._abort_async_command(id);
+}
+
+// shared-script-properties - always an object, even if without properties
+function shared_script_property_set(name, val) {
+ if (arguments.length > 1)
+ return mp.commandv("change-list", "shared-script-properties", "append", "" + name + "=" + val);
+ else
+ return mp.commandv("change-list", "shared-script-properties", "remove", name);
+}
+
+function shared_script_property_get(name) {
+ return mp.get_property_native("shared-script-properties")[name];
+}
+
+function shared_script_property_observe(name, cb) {
+ return mp.observe_property("shared-script-properties", "native",
+ function shared_props_cb(_name, val) { cb(name, val[name]) }
+ );
+}
+
+mp.utils.shared_script_property_set = shared_script_property_set;
+mp.utils.shared_script_property_get = shared_script_property_get;
+mp.utils.shared_script_property_observe = shared_script_property_observe;
+
+// osd-ass
+var next_assid = 1;
+mp.create_osd_overlay = function create_osd_overlay(format) {
+ return {
+ format: format || "ass-events",
+ id: next_assid++,
+ data: "",
+ res_x: 0,
+ res_y: 720,
+ z: 0,
+
+ update: function ass_update() {
+ var cmd = {}; // shallow clone of `this', excluding methods
+ for (var k in this) {
+ if (typeof this[k] != "function")
+ cmd[k] = this[k];
+ }
+
+ cmd.name = "osd-overlay";
+ cmd.res_x = Math.round(this.res_x);
+ cmd.res_y = Math.round(this.res_y);
+
+ return mp.command_native(cmd);
+ },
+
+ remove: function ass_remove() {
+ mp.command_native({
+ name: "osd-overlay",
+ id: this.id,
+ format: "none",
+ data: "",
+ });
+ return mp.last_error() ? undefined : true;
+ },
+ };
+}
+
+// osd-ass legacy API
+mp.set_osd_ass = function set_osd_ass(res_x, res_y, data) {
+ if (!mp._legacy_overlay)
+ mp._legacy_overlay = mp.create_osd_overlay("ass-events");
+
+ var lo = mp._legacy_overlay;
+ if (lo.res_x == res_x && lo.res_y == res_y && lo.data == data)
+ return true;
+
+ mp._legacy_overlay.res_x = res_x;
+ mp._legacy_overlay.res_y = res_y;
+ mp._legacy_overlay.data = data;
+ return mp._legacy_overlay.update();
+}
+
+// the following return undefined on error, null passthrough, or legacy object
+mp.get_osd_size = function get_osd_size() {
+ var d = mp.get_property_native("osd-dimensions");
+ return d && {width: d.w, height: d.h, aspect: d.aspect};
+}
+mp.get_osd_margins = function get_osd_margins() {
+ var d = mp.get_property_native("osd-dimensions");
+ return d && {left: d.ml, right: d.mr, top: d.mt, bottom: d.mb};
+}
+
+/**********************************************************************
+ * key bindings
+ *********************************************************************/
+// binds: items of (binding) name which are objects of:
+// {cb: fn, forced: bool, maybe input: str, repeatable: bool, complex: bool}
+var binds = new_cache();
+
+function dispatch_key_binding(name, state, key_name) {
+ var cb = binds[name] ? binds[name].cb : false;
+ if (cb) // "script-binding [<script_name>/]<name>" command was invoked
+ cb(state, key_name);
+}
+
+var binds_tid = 0; // flush timer id. actual id's are always true-thy
+mp.flush_key_bindings = function flush_key_bindings() {
+ function prioritized_inputs(arr) {
+ return arr.sort(function(a, b) { return a.id - b.id })
+ .map(function(bind) { return bind.input });
+ }
+
+ var def = [], forced = [];
+ for (var n in binds)
+ if (binds[n].input)
+ (binds[n].forced ? forced : def).push(binds[n]);
+ // newer bindings for the same key override/hide older ones
+ def = prioritized_inputs(def);
+ forced = prioritized_inputs(forced);
+
+ var sect = "input_" + mp.script_name;
+ mp.commandv("define-section", sect, def.join("\n"), "default");
+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
+
+ sect = "input_forced_" + mp.script_name;
+ mp.commandv("define-section", sect, forced.join("\n"), "force");
+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
+
+ clearTimeout(binds_tid); // cancel future flush if called directly
+ binds_tid = 0;
+}
+
+function sched_bindings_flush() {
+ if (!binds_tid)
+ binds_tid = setTimeout(mp.flush_key_bindings, 0); // fires on idle
+}
+
+// name/opts maybe omitted. opts: object with optional bool members: repeatable,
+// complex, forced, or a string str which is evaluated as object {str: true}.
+var next_bid = 1;
+function add_binding(forced, key, name, fn, opts) {
+ if (typeof name == "function") { // as if "name" is not part of the args
+ opts = fn;
+ fn = name;
+ name = false;
+ }
+ var key_data = {forced: forced};
+ switch (typeof opts) { // merge opts into key_data
+ case "string": key_data[opts] = true; break;
+ case "object": for (var o in opts) key_data[o] = opts[o];
+ }
+ key_data.id = next_bid++;
+ if (!name)
+ name = "__keybinding" + key_data.id; // new unique binding name
+
+ if (key_data.complex) {
+ mp.register_script_message(name, function msg_cb() {
+ fn({event: "press", is_mouse: false});
+ });
+ var KEY_STATES = { u: "up", d: "down", r: "repeat", p: "press" };
+ key_data.cb = function key_cb(state, key_name) {
+ fn({
+ event: KEY_STATES[state[0]] || "unknown",
+ is_mouse: state[1] == "m",
+ key_name: key_name || undefined
+ });
+ }
+ } else {
+ mp.register_script_message(name, fn);
+ key_data.cb = function key_cb(state) {
+ // Emulate the semantics at input.c: mouse emits on up, kb on down.
+ // Also, key repeat triggers the binding again.
+ var e = state[0],
+ emit = (state[1] == "m") ? (e == "u") : (e == "d");
+ if (emit || e == "p" || e == "r" && key_data.repeatable)
+ fn();
+ }
+ }
+
+ if (key)
+ key_data.input = key + " script-binding " + mp.script_name + "/" + name;
+ binds[name] = key_data; // used by user and/or our (key) script-binding
+ sched_bindings_flush();
+}
+
+mp.add_key_binding = add_binding.bind(null, false);
+mp.add_forced_key_binding = add_binding.bind(null, true);
+
+mp.remove_key_binding = function(name) {
+ mp.unregister_script_message(name);
+ delete binds[name];
+ sched_bindings_flush();
+}
+
+/**********************************************************************
+ Timers: compatible HTML5 WindowTimers - set/clear Timeout/Interval
+ - Spec: https://www.w3.org/TR/html5/webappapis.html#timers
+ - Guaranteed to callback a-sync to [re-]insertion (event-loop wise).
+ - Guaranteed to callback by expiration order, or, if equal, by insertion order.
+ - Not guaranteed schedule accuracy, though intervals should have good average.
+ *********************************************************************/
+
+// pending 'timers' ordered by expiration: latest at index 0 (top fires first).
+// Earlier timers are quicker to handle - just push/pop or fewer items to shift.
+var next_tid = 1,
+ timers = [], // while in process_timers, just insertion-ordered (push)
+ tset_is_push = false, // signal set_timer that we're in process_timers
+ tcanceled = false, // or object of items timer-id: true
+ now = mp.get_time_ms; // just an alias
+
+function insert_sorted(arr, t) {
+ for (var i = arr.length - 1; i >= 0 && t.when >= arr[i].when; i--)
+ arr[i + 1] = arr[i]; // move up timers which fire earlier than t
+ arr[i + 1] = t; // i is -1 or fires later than t
+}
+
+// args (is "arguments"): fn_or_str [,duration [,user_arg1 [, user_arg2 ...]]]
+function set_timer(repeat, args) {
+ var fos = args[0],
+ duration = Math.max(0, (args[1] || 0)), // minimum and default are 0
+ t = {
+ id: next_tid++,
+ when: now() + duration,
+ interval: repeat ? duration : -1,
+ callback: (typeof fos == "function") ? fos : Function(fos),
+ args: (args.length < 3) ? false : [].slice.call(args, 2),
+ };
+
+ if (tset_is_push) {
+ timers.push(t);
+ } else {
+ insert_sorted(timers, t);
+ }
+ return t.id;
+}
+
+g.setTimeout = function setTimeout() { return set_timer(false, arguments) };
+g.setInterval = function setInterval() { return set_timer(true, arguments) };
+
+g.clearTimeout = g.clearInterval = function(id) {
+ if (id < next_tid) { // must ignore if not active timer id.
+ if (!tcanceled)
+ tcanceled = {};
+ tcanceled[id] = true;
+ }
+}
+
+// arr: ordered timers array. ret: -1: no timers, 0: due, positive: ms to wait
+function peek_wait(arr) {
+ return arr.length ? Math.max(0, arr[arr.length - 1].when - now()) : -1;
+}
+
+function peek_timers_wait() {
+ return peek_wait(timers); // must not be called while in process_timers
+}
+
+// Callback all due non-canceled timers which were inserted before calling us.
+// Returns wait in ms till the next timer (possibly 0), or -1 if nothing pends.
+function process_timers() {
+ var wait = peek_wait(timers);
+ if (wait != 0)
+ return wait;
+
+ var actives = timers; // only process those already inserted by now
+ timers = []; // we'll handle added new timers at the end of processing.
+ tset_is_push = true; // signal set_timer to just push-insert
+
+ do {
+ var t = actives.pop();
+ if (tcanceled && tcanceled[t.id])
+ continue;
+
+ if (t.args) {
+ t.callback.apply(null, t.args);
+ } else {
+ (0, t.callback)(); // faster, nicer stack trace than t.cb.call()
+ }
+
+ if (t.interval >= 0) {
+ // allow 20 ms delay/clock-resolution/gc before we skip and reset
+ t.when = Math.max(now() - 20, t.when + t.interval);
+ timers.push(t); // insertion order only
+ }
+ } while (peek_wait(actives) == 0);
+
+ // new 'timers' are insertion-ordered. remains of actives are fully ordered
+ timers.forEach(function(t) { insert_sorted(actives, t) });
+ timers = actives; // now we're fully ordered again, and with all timers
+ tset_is_push = false;
+ if (tcanceled) {
+ timers = timers.filter(function(t) { return !tcanceled[t.id] });
+ tcanceled = false;
+ }
+ return peek_wait(timers);
+}
+
+/**********************************************************************
+ CommonJS module/require
+
+ Spec: http://wiki.commonjs.org/wiki/Modules/1.1.1
+ - All the mandatory requirements are implemented, all the unit tests pass.
+ - The implementation makes the following exception:
+ - Allows the chars [~@:\\] in module id for meta-dir/builtin/dos-drive/UNC.
+
+ Implementation choices beyond the specification:
+ - A module may assign to module.exports (rather than only to exports).
+ - A module's 'this' is the global object, also if it sets strict mode.
+ - No 'global'/'self'. Users can do "this.global = this;" before require(..)
+ - A module has "privacy of its top scope", runs in its own function context.
+ - No id identity with symlinks - a valid choice which others make too.
+ - require("X") always maps to "X.js" -> require("foo.js") is file "foo.js.js".
+ - Global modules search paths are 'scripts/modules.js/' in mpv config dirs.
+ - A main script could e.g. require("./abc") to load a non-global module.
+ - Module id supports mpv path enhancements, e.g. ~/foo, ~~/bar, ~~desktop/baz
+ *********************************************************************/
+
+mp.module_paths = []; // global modules search paths
+if (mp.script_path !== undefined) // loaded as a directory
+ mp.module_paths.push(mp.utils.join_path(mp.script_path, "modules"));
+
+// Internal meta top-dirs. Users should not rely on these names.
+var MODULES_META = "~~modules",
+ SCRIPTDIR_META = "~~scriptdir", // relative script path -> meta absolute id
+ main_script = mp.utils.split_path(mp.script_file); // -> [ path, file ]
+
+function resolve_module_file(id) {
+ var sep = id.indexOf("/"),
+ base = id.substring(0, sep),
+ rest = id.substring(sep + 1) + ".js";
+
+ if (base == SCRIPTDIR_META)
+ return mp.utils.join_path(main_script[0], rest);
+
+ if (base == MODULES_META) {
+ for (var i = 0; i < mp.module_paths.length; i++) {
+ try {
+ var f = mp.utils.join_path(mp.module_paths[i], rest);
+ mp.utils.read_file(f, 1); // throws on any error
+ return f;
+ } catch (e) {}
+ }
+ throw(Error("Cannot find module file '" + rest + "'"));
+ }
+
+ return id + ".js";
+}
+
+// Delimiter '/', remove redundancies, prefix with modules meta-root if needed.
+// E.g. c:\x -> c:/x, or ./x//y/../z -> ./x/z, or utils/x -> ~~modules/utils/x .
+function canonicalize(id) {
+ var path = id.replace(/\\/g,"/").split("/"),
+ t = path[0],
+ base = [];
+
+ // if not strictly relative then must be top-level. figure out base/rest
+ if (t != "." && t != "..") {
+ // global module if it's not fs-root/home/dos-drive/builtin/meta-dir
+ if (!(t == "" || t == "~" || t[1] == ":" || t == "@" || t.match(/^~~/)))
+ path.unshift(MODULES_META); // add an explicit modules meta-root
+
+ if (id.match(/^\\\\/)) // simple UNC handling, preserve leading \\srv
+ path = ["\\\\" + path[2]].concat(path.slice(3)); // [ \\srv, shr..]
+
+ if (t[1] == ":" && t.length > 2) { // path: [ "c:relative", "path" ]
+ path[0] = t.substring(2);
+ path.unshift(t[0] + ":."); // -> [ "c:.", "relative", "path" ]
+ }
+ base = [path.shift()];
+ }
+
+ // path is now logically relative. base, if not empty, is its [meta] root.
+ // normalize the relative part - always id-based (spec Module Id, 1.3.6).
+ var cr = []; // canonicalized relative
+ for (var i = 0; i < path.length; i++) {
+ if (path[i] == "." || path[i] == "")
+ continue;
+ if (path[i] == ".." && cr.length && cr[cr.length - 1] != "..") {
+ cr.pop();
+ continue;
+ }
+ cr.push(path[i]);
+ }
+
+ if (!base.length && cr[0] != "..")
+ base = ["."]; // relative and not ../<stuff> so must start with ./
+ return base.concat(cr).join("/");
+}
+
+function resolve_module_id(base_id, new_id) {
+ new_id = canonicalize(new_id);
+ if (!new_id.match(/^\.\/|^\.\.\//)) // doesn't start with ./ or ../
+ return new_id; // not relative, we don't care about base_id
+
+ var combined = mp.utils.join_path(mp.utils.split_path(base_id)[0], new_id);
+ return canonicalize(combined);
+}
+
+var req_cache = new_cache(); // global for all instances of require
+
+// ret: a require function instance which uses base_id to resolve relative id's
+function new_require(base_id) {
+ return function require(id) {
+ id = resolve_module_id(base_id, id); // id is now top-level
+ if (req_cache[id])
+ return req_cache[id].exports;
+
+ var new_module = {id: id, exports: {}};
+ req_cache[id] = new_module;
+ try {
+ var filename = resolve_module_file(id);
+ // we need dedicated free vars + filename in traces + allow strict
+ var str = "mp._req = function(require, exports, module) {" +
+ mp.utils.read_file(filename) +
+ "\n;}";
+ mp.utils.compile_js(filename, str)(); // only runs the assignment
+ var tmp = mp._req; // we have mp._req, or else we'd have thrown
+ delete mp._req;
+ tmp.call(g, new_require(id), new_module.exports, new_module);
+ } catch (e) {
+ delete req_cache[id];
+ throw(e);
+ }
+
+ return new_module.exports;
+ };
+}
+
+g.require = new_require(SCRIPTDIR_META + "/" + main_script[1]);
+
+/**********************************************************************
+ * mp.options
+ *********************************************************************/
+function read_options(opts, id, on_update, conf_override) {
+ id = String(id ? id : mp.get_script_name());
+ mp.msg.debug("reading options for " + id);
+
+ var conf, fname = "~~/script-opts/" + id + ".conf";
+ try {
+ conf = arguments.length > 3 ? conf_override : mp.utils.read_file(fname);
+ } catch (e) {
+ mp.msg.verbose(fname + " not found.");
+ }
+
+ // data as config file lines array, or empty array
+ var data = conf ? conf.replace(/\r\n/g, "\n").split("\n") : [],
+ conf_len = data.length; // before we append script-opts below
+
+ // Append relevant script-opts as <key-sans-id>=<value> to data
+ var sopts = mp.get_property_native("options/script-opts"),
+ prefix = id + "-";
+ for (var key in sopts) {
+ if (key.indexOf(prefix) == 0)
+ data.push(key.substring(prefix.length) + "=" + sopts[key]);
+ }
+
+ // Update opts from data
+ data.forEach(function(line, i) {
+ if (line[0] == "#" || line.trim() == "")
+ return;
+
+ var key = line.substring(0, line.indexOf("=")),
+ val = line.substring(line.indexOf("=") + 1),
+ type = typeof opts[key],
+ info = i < conf_len ? fname + ":" + (i + 1) // 1-based line number
+ : "script-opts:" + prefix + key;
+
+ if (!opts.hasOwnProperty(key))
+ mp.msg.warn(info, "Ignoring unknown key '" + key + "'");
+ else if (type == "string")
+ opts[key] = val;
+ else if (type == "boolean" && (val == "yes" || val == "no"))
+ opts[key] = (val == "yes");
+ else if (type == "number" && val.trim() != "" && !isNaN(val))
+ opts[key] = Number(val);
+ else
+ mp.msg.error(info, "Error: can't convert '" + val + "' to " + type);
+ });
+
+ if (on_update) {
+ mp.observe_property("options/script-opts", "native", function(_n, _v) {
+ var saved = JSON.parse(JSON.stringify(opts)); // clone
+ var changelist = {}, changed = false;
+ read_options(opts, id, 0, conf); // re-apply orig-file + script-opts
+ for (var key in opts) {
+ if (opts[key] != saved[key]) // type always stays the same
+ changelist[key] = changed = true;
+ }
+ if (changed)
+ on_update(changelist);
+ });
+ }
+}
+
+mp.options = { read_options: read_options };
+
+/**********************************************************************
+ * various
+ *********************************************************************/
+g.print = mp.msg.info; // convenient alias
+mp.get_script_name = function() { return mp.script_name };
+mp.get_script_file = function() { return mp.script_file };
+mp.get_script_directory = function() { return mp.script_path };
+mp.get_time = function() { return mp.get_time_ms() / 1000 };
+mp.utils.getcwd = function() { return mp.get_property("working-directory") };
+mp.utils.getpid = function() { return mp.get_property_number("pid") }
+mp.utils.get_user_path =
+ function(p) { return mp.command_native(["expand-path", String(p)]) };
+mp.get_mouse_pos = function() { return mp.get_property_native("mouse-pos") };
+mp.utils.write_file = mp.utils._write_file.bind(null, false);
+mp.utils.append_file = mp.utils._write_file.bind(null, true);
+mp.dispatch_event = dispatch_event;
+mp.process_timers = process_timers;
+mp.notify_idle_observers = notify_idle_observers;
+mp.peek_timers_wait = peek_timers_wait;
+
+mp.get_opt = function(key, def) {
+ var v = mp.get_property_native("options/script-opts")[key];
+ return (typeof v != "undefined") ? v : def;
+}
+
+mp.osd_message = function osd_message(text, duration) {
+ mp.commandv("show_text", text, Math.round(1000 * (duration || -1)));
+}
+
+mp.utils.subprocess = function subprocess(t) {
+ var cmd = { name: "subprocess", capture_stdout: true };
+ var new_names = { cancellable: "playback_only", max_size: "capture_size" };
+ for (var k in t)
+ cmd[new_names[k] || k] = t[k];
+
+ var rv = mp.command_native(cmd);
+ if (mp.last_error()) /* typically on missing/incorrect args */
+ rv = { error_string: mp.last_error(), status: -1 };
+ if (rv.error_string)
+ rv.error = rv.error_string;
+ return rv;
+}
+
+mp.utils.subprocess_detached = function subprocess_detached(t) {
+ return mp.commandv.apply(null, ["run"].concat(t.args));
+}
+
+
+// ----- dump: like print, but expands objects/arrays recursively -----
+function replacer(k, v) {
+ var t = typeof v;
+ if (t == "function" || t == "undefined")
+ return "<" + t + ">";
+ if (Array.isArray(this) && t == "object" && v !== null) { // "safe" mode
+ if (this.indexOf(v) >= 0)
+ return "<VISITED>";
+ this.push(v);
+ }
+ return v;
+}
+
+function obj2str(v) {
+ try { // can process objects more than once, but throws on cycles
+ return JSON.stringify(v, replacer.bind(null), 2);
+ } catch (e) { // simple safe: exclude visited objects, even if not cyclic
+ return JSON.stringify(v, replacer.bind([]), 2);
+ }
+}
+
+g.dump = function dump() {
+ var toprint = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var v = arguments[i];
+ toprint.push((typeof v == "object") ? obj2str(v) : replacer(0, v));
+ }
+ print.apply(null, toprint);
+}
+
+/**********************************************************************
+ * main listeners and event loop
+ *********************************************************************/
+mp.keep_running = true;
+g.exit = function() { mp.keep_running = false }; // user-facing too
+mp.register_event("shutdown", g.exit);
+mp.register_event("property-change", notify_observer);
+mp.register_event("hook", run_hook);
+mp.register_event("command-reply", async_command_handler);
+mp.register_event("client-message", dispatch_message);
+mp.register_script_message("key-binding", dispatch_key_binding);
+
+g.mp_event_loop = function mp_event_loop() {
+ var wait = 0; // seconds
+ do { // distapch events as long as they arrive, then do the timers/idle
+ var e = mp.wait_event(wait);
+ if (e.event != "none") {
+ dispatch_event(e);
+ wait = 0; // poll the next one
+ } else {
+ wait = process_timers() / 1000;
+ if (wait != 0 && iobservers.length) {
+ notify_idle_observers(); // can add timers -> recalculate wait
+ wait = peek_timers_wait() / 1000;
+ }
+ }
+ } while (mp.keep_running);
+};
+
+
+// let the user extend us, e.g. by adding items to mp.module_paths
+var initjs = mp.find_config_file("init.js"); // ~~/init.js
+if (initjs)
+ require(initjs.slice(0, -3)); // remove ".js"
+else if ((initjs = mp.find_config_file(".init.js")))
+ mp.msg.warn("Use init.js instead of .init.js (ignoring " + initjs + ")");
+
+})(this)
diff --git a/player/javascript/meson.build b/player/javascript/meson.build
new file mode 100644
index 0000000..bfff4b4
--- /dev/null
+++ b/player/javascript/meson.build
@@ -0,0 +1,6 @@
+defaults_js = custom_target('defaults.js',
+ input: join_paths(source_root, 'player', 'javascript', 'defaults.js'),
+ output: 'defaults.js.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+)
+sources += defaults_js
diff --git a/player/loadfile.c b/player/loadfile.c
new file mode 100644
index 0000000..1d25dc3
--- /dev/null
+++ b/player/loadfile.c
@@ -0,0 +1,2066 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <strings.h>
+#include <inttypes.h>
+#include <assert.h>
+
+#include <libavutil/avutil.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/thread_pool.h"
+#include "misc/thread_tools.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+
+#include "client.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/global.h"
+#include "options/path.h"
+#include "options/m_config.h"
+#include "options/parse_configfile.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/m_property.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "common/stats.h"
+#include "input/input.h"
+#include "misc/language.h"
+
+#include "audio/out/ao.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/f_lavfi.h"
+#include "filters/filter_internal.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "external_files.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+#include "libmpv/client.h"
+
+// Called from the demuxer thread if a new packet is available, or other changes.
+static void wakeup_demux(void *pctx)
+{
+ struct MPContext *mpctx = pctx;
+ mp_wakeup_core(mpctx);
+}
+
+// Called by foreign threads when playback should be stopped and such.
+void mp_abort_playback_async(struct MPContext *mpctx)
+{
+ mp_cancel_trigger(mpctx->playback_abort);
+
+ mp_mutex_lock(&mpctx->abort_lock);
+
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ struct mp_abort_entry *abort = mpctx->abort_list[n];
+ if (abort->coupled_to_playback)
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Add it to the global list, and allocate required data structures.
+void mp_abort_add(struct MPContext *mpctx, struct mp_abort_entry *abort)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+ assert(!abort->cancel);
+ abort->cancel = mp_cancel_new(NULL);
+ MP_TARRAY_APPEND(NULL, mpctx->abort_list, mpctx->num_abort_list, abort);
+ mp_abort_recheck_locked(mpctx, abort);
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Remove Add it to the global list, and free/clear required data structures.
+// Does not deallocate the abort value itself.
+void mp_abort_remove(struct MPContext *mpctx, struct mp_abort_entry *abort)
+{
+ mp_mutex_lock(&mpctx->abort_lock);
+ for (int n = 0; n < mpctx->num_abort_list; n++) {
+ if (mpctx->abort_list[n] == abort) {
+ MP_TARRAY_REMOVE_AT(mpctx->abort_list, mpctx->num_abort_list, n);
+ TA_FREEP(&abort->cancel);
+ abort = NULL; // it's not free'd, just clear for the assert below
+ break;
+ }
+ }
+ assert(!abort); // should have been in the list
+ mp_mutex_unlock(&mpctx->abort_lock);
+}
+
+// Verify whether the abort needs to be signaled after changing certain fields
+// in abort.
+void mp_abort_recheck_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort)
+{
+ if ((abort->coupled_to_playback && mp_cancel_test(mpctx->playback_abort)) ||
+ mpctx->abort_all)
+ {
+ mp_abort_trigger_locked(mpctx, abort);
+ }
+}
+
+void mp_abort_trigger_locked(struct MPContext *mpctx,
+ struct mp_abort_entry *abort)
+{
+ mp_cancel_trigger(abort->cancel);
+}
+
+static void kill_demuxers_reentrant(struct MPContext *mpctx,
+ struct demuxer **demuxers, int num_demuxers)
+{
+ struct demux_free_async_state **items = NULL;
+ int num_items = 0;
+
+ for (int n = 0; n < num_demuxers; n++) {
+ struct demuxer *d = demuxers[n];
+
+ if (!demux_cancel_test(d)) {
+ // Make sure it is set if it wasn't yet.
+ demux_set_wakeup_cb(d, wakeup_demux, mpctx);
+
+ struct demux_free_async_state *item = demux_free_async(d);
+ if (item) {
+ MP_TARRAY_APPEND(NULL, items, num_items, item);
+ d = NULL;
+ }
+ }
+
+ demux_cancel_and_free(d);
+ }
+
+ if (!num_items)
+ return;
+
+ MP_DBG(mpctx, "Terminating demuxers...\n");
+
+ double end = mp_time_sec() + mpctx->opts->demux_termination_timeout;
+ bool force = false;
+ while (num_items) {
+ double wait = end - mp_time_sec();
+
+ for (int n = 0; n < num_items; n++) {
+ struct demux_free_async_state *item = items[n];
+ if (demux_free_async_finish(item)) {
+ items[n] = items[num_items - 1];
+ num_items -= 1;
+ n--;
+ goto repeat;
+ } else if (wait < 0) {
+ demux_free_async_force(item);
+ if (!force)
+ MP_VERBOSE(mpctx, "Forcefully terminating demuxers...\n");
+ force = true;
+ }
+ }
+
+ if (wait >= 0)
+ mp_set_timeout(mpctx, wait);
+ mp_idle(mpctx);
+ repeat:;
+ }
+
+ talloc_free(items);
+
+ MP_DBG(mpctx, "Done terminating demuxers.\n");
+}
+
+static void uninit_demuxer(struct MPContext *mpctx)
+{
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int r = 0; r < num_ptracks[t]; r++)
+ mpctx->current_track[r][t] = NULL;
+ }
+
+ talloc_free(mpctx->chapters);
+ mpctx->chapters = NULL;
+ mpctx->num_chapters = 0;
+
+ mp_abort_cache_dumping(mpctx);
+
+ struct demuxer **demuxers = NULL;
+ int num_demuxers = 0;
+
+ if (mpctx->demuxer)
+ MP_TARRAY_APPEND(NULL, demuxers, num_demuxers, mpctx->demuxer);
+ mpctx->demuxer = NULL;
+
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+
+ assert(!track->dec && !track->d_sub);
+ assert(!track->vo_c && !track->ao_c);
+ assert(!track->sink);
+
+ // Demuxers can be added in any order (if they appear mid-stream), and
+ // we can't know which tracks uses which, so here's some O(n^2) trash.
+ for (int n = 0; n < num_demuxers; n++) {
+ if (demuxers[n] == track->demuxer) {
+ track->demuxer = NULL;
+ break;
+ }
+ }
+ if (track->demuxer)
+ MP_TARRAY_APPEND(NULL, demuxers, num_demuxers, track->demuxer);
+
+ talloc_free(track);
+ }
+ mpctx->num_tracks = 0;
+
+ kill_demuxers_reentrant(mpctx, demuxers, num_demuxers);
+ talloc_free(demuxers);
+}
+
+#define APPEND(s, ...) mp_snprintf_cat(s, sizeof(s), __VA_ARGS__)
+
+static void print_stream(struct MPContext *mpctx, struct track *t)
+{
+ struct sh_stream *s = t->stream;
+ const char *tname = "?";
+ const char *selopt = "?";
+ const char *langopt = "?";
+ switch (t->type) {
+ case STREAM_VIDEO:
+ tname = "Video"; selopt = "vid"; langopt = NULL;
+ break;
+ case STREAM_AUDIO:
+ tname = "Audio"; selopt = "aid"; langopt = "alang";
+ break;
+ case STREAM_SUB:
+ tname = "Subs"; selopt = "sid"; langopt = "slang";
+ break;
+ }
+ char b[2048] = {0};
+ bool forced_only = false;
+ if (t->type == STREAM_SUB) {
+ bool forced_opt = mpctx->opts->subs_rend->sub_forced_events_only;
+ if (forced_opt)
+ forced_only = t->selected;
+ }
+ APPEND(b, " %3s %-5s", t->selected ? (forced_only ? "(*)" : "(+)") : "", tname);
+ APPEND(b, " --%s=%d", selopt, t->user_tid);
+ if (t->lang && langopt)
+ APPEND(b, " --%s=%s", langopt, t->lang);
+ if (t->default_track)
+ APPEND(b, " (*)");
+ if (t->forced_track)
+ APPEND(b, " (f)");
+ if (t->attached_picture)
+ APPEND(b, " [P]");
+ if (forced_only)
+ APPEND(b, " [F]");
+ if (t->title)
+ APPEND(b, " '%s'", t->title);
+ const char *codec = s ? s->codec->codec : NULL;
+ APPEND(b, " (%s", codec ? codec : "<unknown>");
+ if (t->type == STREAM_VIDEO) {
+ if (s && s->codec->disp_w)
+ APPEND(b, " %dx%d", s->codec->disp_w, s->codec->disp_h);
+ if (s && s->codec->fps)
+ APPEND(b, " %.3ffps", s->codec->fps);
+ } else if (t->type == STREAM_AUDIO) {
+ if (s && s->codec->channels.num)
+ APPEND(b, " %dch", s->codec->channels.num);
+ if (s && s->codec->samplerate)
+ APPEND(b, " %dHz", s->codec->samplerate);
+ }
+ APPEND(b, ")");
+ if (s && s->hls_bitrate > 0)
+ APPEND(b, " (%d kbps)", (s->hls_bitrate + 500) / 1000);
+ if (t->is_external)
+ APPEND(b, " (external)");
+ MP_INFO(mpctx, "%s\n", b);
+}
+
+void print_track_list(struct MPContext *mpctx, const char *msg)
+{
+ if (msg)
+ MP_INFO(mpctx, "%s\n", msg);
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ if (mpctx->tracks[n]->type == t)
+ print_stream(mpctx, mpctx->tracks[n]);
+ }
+}
+
+void update_demuxer_properties(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return;
+ demux_update(demuxer, get_current_time(mpctx));
+ int events = demuxer->events;
+ if ((events & DEMUX_EVENT_INIT) && demuxer->num_editions > 1) {
+ for (int n = 0; n < demuxer->num_editions; n++) {
+ struct demux_edition *edition = &demuxer->editions[n];
+ char b[128] = {0};
+ APPEND(b, " %3s --edition=%d",
+ n == demuxer->edition ? "(+)" : "", n);
+ char *name = mp_tags_get_str(edition->metadata, "title");
+ if (name)
+ APPEND(b, " '%s'", name);
+ if (edition->default_edition)
+ APPEND(b, " (*)");
+ MP_INFO(mpctx, "%s\n", b);
+ }
+ }
+ struct demuxer *tracks = mpctx->demuxer;
+ if (tracks->events & DEMUX_EVENT_STREAMS) {
+ add_demuxer_tracks(mpctx, tracks);
+ print_track_list(mpctx, NULL);
+ tracks->events &= ~DEMUX_EVENT_STREAMS;
+ }
+ if (events & DEMUX_EVENT_METADATA) {
+ struct mp_tags *info =
+ mp_tags_filtered(mpctx, demuxer->metadata, mpctx->opts->display_tags);
+ // prev is used to attempt to print changed tags only (to some degree)
+ struct mp_tags *prev = mpctx->filtered_tags;
+ int n_prev = 0;
+ bool had_output = false;
+ for (int n = 0; n < info->num_keys; n++) {
+ if (prev && n_prev < prev->num_keys) {
+ if (strcmp(prev->keys[n_prev], info->keys[n]) == 0) {
+ n_prev++;
+ if (strcmp(prev->values[n_prev - 1], info->values[n]) == 0)
+ continue;
+ }
+ }
+ struct mp_log *log = mp_log_new(NULL, mpctx->log, "!display-tags");
+ if (!had_output)
+ mp_info(log, "File tags:\n");
+ mp_info(log, " %s: %s\n", info->keys[n], info->values[n]);
+ had_output = true;
+ talloc_free(log);
+ }
+ talloc_free(mpctx->filtered_tags);
+ mpctx->filtered_tags = info;
+ mp_notify(mpctx, MP_EVENT_METADATA_UPDATE, NULL);
+ }
+ if (events & DEMUX_EVENT_DURATION)
+ mp_notify(mpctx, MP_EVENT_DURATION_UPDATE, NULL);
+ demuxer->events = 0;
+}
+
+// Enables or disables the stream for the given track, according to
+// track->selected.
+// With refresh_only=true, refreshes the stream if it's enabled.
+void reselect_demux_stream(struct MPContext *mpctx, struct track *track,
+ bool refresh_only)
+{
+ if (!track->stream)
+ return;
+ double pts = get_current_time(mpctx);
+ if (pts != MP_NOPTS_VALUE) {
+ pts += get_track_seek_offset(mpctx, track);
+ if (track->type == STREAM_SUB)
+ pts -= 10.0;
+ }
+ if (refresh_only)
+ demuxer_refresh_track(track->demuxer, track->stream, pts);
+ else
+ demuxer_select_track(track->demuxer, track->stream, pts, track->selected);
+}
+
+static void enable_demux_thread(struct MPContext *mpctx, struct demuxer *demux)
+{
+ if (mpctx->opts->demuxer_thread && !demux->fully_read) {
+ demux_set_wakeup_cb(demux, wakeup_demux, mpctx);
+ demux_start_thread(demux);
+ }
+}
+
+static int find_new_tid(struct MPContext *mpctx, enum stream_type t)
+{
+ int new_id = 0;
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+ if (track->type == t)
+ new_id = MPMAX(new_id, track->user_tid);
+ }
+ return new_id + 1;
+}
+
+static struct track *add_stream_track(struct MPContext *mpctx,
+ struct demuxer *demuxer,
+ struct sh_stream *stream)
+{
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+ if (track->stream == stream)
+ return track;
+ }
+
+ struct track *track = talloc_ptrtype(NULL, track);
+ *track = (struct track) {
+ .type = stream->type,
+ .user_tid = find_new_tid(mpctx, stream->type),
+ .demuxer_id = stream->demuxer_id,
+ .ff_index = stream->ff_index,
+ .hls_bitrate = stream->hls_bitrate,
+ .program_id = stream->program_id,
+ .title = stream->title,
+ .default_track = stream->default_track,
+ .forced_track = stream->forced_track,
+ .dependent_track = stream->dependent_track,
+ .visual_impaired_track = stream->visual_impaired_track,
+ .hearing_impaired_track = stream->hearing_impaired_track,
+ .image = stream->image,
+ .attached_picture = stream->attached_picture != NULL,
+ .lang = stream->lang,
+ .demuxer = demuxer,
+ .stream = stream,
+ };
+ MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return track;
+}
+
+void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer)
+{
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++)
+ add_stream_track(mpctx, demuxer, demux_get_stream(demuxer, n));
+}
+
+// Result numerically higher => better match. 0 == no match.
+static int match_lang(char **langs, const char *lang)
+{
+ if (!lang)
+ return 0;
+ for (int idx = 0; langs && langs[idx]; idx++) {
+ int score = mp_match_lang_single(langs[idx], lang);
+ if (score > 0)
+ return INT_MAX - (idx + 1) * LANGUAGE_SCORE_MAX + score - 1;
+ }
+ return 0;
+}
+
+/* Get the track wanted by the user.
+ * tid is the track ID requested by the user (-2: deselect, -1: default)
+ * lang is a string list, NULL is same as empty list
+ * Sort tracks based on the following criteria, and pick the first:
+ *0a) track matches tid (always wins)
+ * 0b) track is not from --external-file
+ * 1) track is external (no_default cancels this)
+ * 1b) track was passed explicitly (is not an auto-loaded subtitle)
+ * 1c) track matches the program ID of the video
+ * 2) earlier match in lang list but not if we're using os_langs
+ * 3a) track is marked forced and we're preferring forced tracks
+ * 3b) track is marked non-forced and we're preferring non-forced tracks
+ * 3c) track is marked default
+ * 3d) match in lang list with os_langs
+ * 4) attached picture, HLS bitrate
+ * 5) lower track number
+ * If select_fallback is not set, 5) is only used to determine whether a
+ * matching track is preferred over another track. Otherwise, always pick a
+ * track (if nothing else matches, return the track with lowest ID).
+ * Forced tracks are preferred when the user prefers not to display subtitles
+ */
+// Return whether t1 is preferred over t2
+static bool compare_track(struct track *t1, struct track *t2, char **langs,
+ bool os_langs, struct MPOpts *opts, int preferred_program)
+{
+ if (!opts->autoload_files && t1->is_external != t2->is_external)
+ return !t1->is_external;
+ bool ext1 = t1->is_external && !t1->no_default;
+ bool ext2 = t2->is_external && !t2->no_default;
+ if (ext1 != ext2) {
+ if (t1->attached_picture && t2->attached_picture
+ && opts->audio_display == 1)
+ return !ext1;
+ return ext1;
+ }
+ if (t1->auto_loaded != t2->auto_loaded)
+ return !t1->auto_loaded;
+ if (preferred_program != -1 && t1->program_id != -1 && t2->program_id != -1) {
+ if ((t1->program_id == preferred_program) !=
+ (t2->program_id == preferred_program))
+ return t1->program_id == preferred_program;
+ }
+ int forced = t1->type == STREAM_SUB ? opts->subs_fallback_forced : 1;
+ bool force_match = forced == 1 || (t1->forced_track && forced == 2) ||
+ (!t1->forced_track && !forced);
+ int l1 = match_lang(langs, t1->lang), l2 = match_lang(langs, t2->lang);
+ if (!os_langs && l1 != l2)
+ return l1 > l2 && force_match;
+ if (t1->default_track != t2->default_track)
+ return t1->default_track && force_match;
+ if (os_langs && l1 != l2)
+ return l1 > l2 && force_match;
+ if (t1->attached_picture != t2->attached_picture)
+ return !t1->attached_picture;
+ if (t1->stream && t2->stream && opts->hls_bitrate >= 0 &&
+ t1->stream->hls_bitrate != t2->stream->hls_bitrate)
+ {
+ bool t1_ok = t1->stream->hls_bitrate <= opts->hls_bitrate;
+ bool t2_ok = t2->stream->hls_bitrate <= opts->hls_bitrate;
+ if (t1_ok != t2_ok)
+ return t1_ok;
+ if (t1_ok && t2_ok)
+ return t1->stream->hls_bitrate > t2->stream->hls_bitrate;
+ return t1->stream->hls_bitrate < t2->stream->hls_bitrate;
+ }
+ return t1->user_tid <= t2->user_tid;
+}
+
+static bool duplicate_track(struct MPContext *mpctx, int order,
+ enum stream_type type, struct track *track)
+{
+ for (int i = 0; i < order; i++) {
+ if (mpctx->current_track[i][type] == track)
+ return true;
+ }
+ return false;
+}
+
+static bool append_lang(size_t *nb, char ***out, char *in)
+{
+ if (!in)
+ return false;
+ MP_TARRAY_GROW(NULL, *out, *nb + 1);
+ (*out)[(*nb)++] = in;
+ (*out)[*nb] = NULL;
+ ta_set_parent(in, *out);
+ return true;
+}
+
+static char **add_os_langs(void)
+{
+ size_t nb = 0;
+ char **out = NULL;
+ char **autos = mp_get_user_langs();
+ for (int i = 0; autos && autos[i]; i++) {
+ if (!append_lang(&nb, &out, autos[i]))
+ goto cleanup;
+ }
+
+cleanup:
+ talloc_free(autos);
+ return out;
+}
+
+static char **process_langs(char **in)
+{
+ size_t nb = 0;
+ char **out = NULL;
+ for (int i = 0; in && in[i]; i++) {
+ if (!append_lang(&nb, &out, talloc_strdup(NULL, in[i])))
+ break;
+ }
+ return out;
+}
+
+static const char *get_audio_lang(struct MPContext *mpctx)
+{
+ // If we have a single current audio track, this is simple.
+ if (mpctx->current_track[0][STREAM_AUDIO])
+ return mpctx->current_track[0][STREAM_AUDIO]->lang;
+
+ const char *ret = NULL;
+
+ // Otherwise, we may be using a filter with multiple inputs.
+ // Iterate over the tracks and find the ones in use.
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ const struct track *t = mpctx->tracks[i];
+ if (t->type != STREAM_AUDIO || !t->selected)
+ continue;
+
+ // If we have input in multiple audio languages, bail out;
+ // we don't have a meaningful single language.
+ // Partial matches (e.g. en-US vs en-GB) are acceptable here.
+ if (ret && t->lang && !mp_match_lang_single(t->lang, ret))
+ return NULL;
+
+ // We'll return the first non-null tag we see
+ if (!ret)
+ ret = t->lang;
+ }
+
+ return ret;
+}
+
+struct track *select_default_track(struct MPContext *mpctx, int order,
+ enum stream_type type)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int tid = opts->stream_id[order][type];
+ int preferred_program = (type != STREAM_VIDEO && mpctx->current_track[0][STREAM_VIDEO]) ?
+ mpctx->current_track[0][STREAM_VIDEO]->program_id : -1;
+ if (tid == -2)
+ return NULL;
+ char **langs = process_langs(opts->stream_lang[type]);
+ bool os_langs = false;
+ // Try to add OS languages if enabled by the user and we don't already have a lang from slang.
+ if (type == STREAM_SUB && (!langs || !strcmp(langs[0], "")) && opts->subs_match_os_language) {
+ talloc_free(langs);
+ langs = add_os_langs();
+ os_langs = true;
+ }
+ const char *audio_lang = get_audio_lang(mpctx);
+ bool sub = type == STREAM_SUB;
+ bool fallback_forced = sub && opts->subs_fallback_forced;
+ bool audio_matches = false;
+ bool sub_fallback = false;
+ struct track *pick = NULL;
+ struct track *forced_pick = NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+ if (track->user_tid == tid) {
+ pick = track;
+ goto cleanup;
+ }
+ if (tid >= 0)
+ continue;
+ if (track->no_auto_select)
+ continue;
+ if (duplicate_track(mpctx, order, type, track))
+ continue;
+ if (!pick || compare_track(track, pick, langs, os_langs, mpctx->opts, preferred_program))
+ pick = track;
+
+ // Autoselecting forced sub tracks requires the following:
+ // 1. Matches the audio language or --subs-fallback-forced=always.
+ // 2. Matches the users list of preferred languages or none were specified (i.e. slang was not set).
+ // 3. A track *wasn't* already selected by slang previously or the track->lang matches pick->lang and isn't forced.
+ bool valid_forced_slang = (os_langs || (mp_match_lang_single(pick->lang, track->lang) && !pick->forced_track) ||
+ (match_lang(langs, track->lang) && !match_lang(langs, pick->lang)));
+ bool audio_lang_match = mp_match_lang_single(audio_lang, track->lang);
+ if (fallback_forced && track->forced_track && valid_forced_slang && audio_lang_match &&
+ (!forced_pick || compare_track(track, forced_pick, langs, os_langs, mpctx->opts, preferred_program)))
+ {
+ forced_pick = track;
+ }
+ }
+
+ // If we found a forced track, use that.
+ if (forced_pick)
+ pick = forced_pick;
+
+ // Clear out any picks for these special cases for subtitles
+ if (pick) {
+ audio_matches = mp_match_lang_single(pick->lang, audio_lang);
+ sub_fallback = (pick->is_external && !pick->no_default) || opts->subs_fallback == 2 ||
+ (opts->subs_fallback == 1 && pick->default_track);
+ }
+ if (pick && !forced_pick && sub && (!match_lang(langs, pick->lang) || os_langs) && !sub_fallback)
+ pick = NULL;
+ // Handle this after matching langs and selecting a fallback.
+ if (pick && sub && (!opts->subs_with_matching_audio && audio_matches))
+ pick = NULL;
+ // Handle edge cases if we picked a track that doesn't match the --subs-fallback-force value
+ if (pick && sub && ((!pick->forced_track && opts->subs_fallback_forced == 2) ||
+ (pick->forced_track && !opts->subs_fallback_forced)))
+ {
+ pick = NULL;
+ }
+
+ if (pick && pick->attached_picture && !mpctx->opts->audio_display)
+ pick = NULL;
+ if (pick && !opts->autoload_files && pick->is_external)
+ pick = NULL;
+cleanup:
+ talloc_free(langs);
+ return pick;
+}
+
+static char *track_layout_hash(struct MPContext *mpctx)
+{
+ char *h = talloc_strdup(NULL, "");
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type != type)
+ continue;
+ h = talloc_asprintf_append_buffer(h, "%d-%d-%d-%d-%s\n", type,
+ track->user_tid, track->default_track, track->is_external,
+ track->lang ? track->lang : "");
+ }
+ }
+ return h;
+}
+
+// Normally, video/audio/sub track selection is persistent across files. This
+// code resets track selection if the new file has a different track layout.
+static void check_previous_track_selection(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->track_layout_hash)
+ return;
+
+ char *h = track_layout_hash(mpctx);
+ if (strcmp(h, mpctx->track_layout_hash) != 0) {
+ // Reset selection, but only if they're not "auto" or "off". The
+ // defaults are -1 (default selection), or -2 (off) for secondary tracks.
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ if (opts->stream_id[i][t] >= 0)
+ mark_track_selection(mpctx, i, t, i == 0 ? -1 : -2);
+ }
+ }
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = NULL;
+ }
+ talloc_free(h);
+}
+
+// Update the matching track selection user option to the given value.
+void mark_track_selection(struct MPContext *mpctx, int order,
+ enum stream_type type, int value)
+{
+ assert(order >= 0 && order < num_ptracks[type]);
+ mpctx->opts->stream_id[order][type] = value;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->stream_id[order][type]);
+}
+
+void mp_switch_track_n(struct MPContext *mpctx, int order, enum stream_type type,
+ struct track *track, int flags)
+{
+ assert(!track || track->type == type);
+ assert(type >= 0 && type < STREAM_TYPE_COUNT);
+ assert(order >= 0 && order < num_ptracks[type]);
+
+ // Mark the current track selection as explicitly user-requested. (This is
+ // different from auto-selection or disabling a track due to errors.)
+ if (flags & FLAG_MARK_SELECTION)
+ mark_track_selection(mpctx, order, type, track ? track->user_tid : -2);
+
+ // No decoder should be initialized yet.
+ if (!mpctx->demuxer)
+ return;
+
+ struct track *current = mpctx->current_track[order][type];
+ if (track == current)
+ return;
+
+ if (current && current->sink) {
+ MP_ERR(mpctx, "Can't disable input to complex filter.\n");
+ goto error;
+ }
+ if ((type == STREAM_VIDEO && mpctx->vo_chain && !mpctx->vo_chain->track) ||
+ (type == STREAM_AUDIO && mpctx->ao_chain && !mpctx->ao_chain->track))
+ {
+ MP_ERR(mpctx, "Can't switch away from complex filter output.\n");
+ goto error;
+ }
+
+ if (track && track->selected) {
+ // Track has been selected in a different order parameter.
+ MP_ERR(mpctx, "Track %d is already selected.\n", track->user_tid);
+ goto error;
+ }
+
+ if (order == 0) {
+ if (type == STREAM_VIDEO) {
+ uninit_video_chain(mpctx);
+ if (!track)
+ handle_force_window(mpctx, true);
+ } else if (type == STREAM_AUDIO) {
+ clear_audio_output_buffers(mpctx);
+ uninit_audio_chain(mpctx);
+ if (!track)
+ uninit_audio_out(mpctx);
+ }
+ }
+ if (type == STREAM_SUB)
+ uninit_sub(mpctx, current);
+
+ if (current) {
+ current->selected = false;
+ reselect_demux_stream(mpctx, current, false);
+ }
+
+ mpctx->current_track[order][type] = track;
+
+ if (track) {
+ track->selected = true;
+ reselect_demux_stream(mpctx, track, false);
+ }
+
+ if (type == STREAM_VIDEO && order == 0) {
+ reinit_video_chain(mpctx);
+ } else if (type == STREAM_AUDIO && order == 0) {
+ reinit_audio_chain(mpctx);
+ } else if (type == STREAM_SUB && order >= 0 && order <= 2) {
+ reinit_sub(mpctx, track);
+ }
+
+ mp_notify(mpctx, MP_EVENT_TRACK_SWITCHED, NULL);
+ mp_wakeup_core(mpctx);
+
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = talloc_steal(mpctx, track_layout_hash(mpctx));
+
+ return;
+error:
+ mark_track_selection(mpctx, order, type, -1);
+}
+
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track, int flags)
+{
+ mp_switch_track_n(mpctx, 0, type, track, flags);
+}
+
+void mp_deselect_track(struct MPContext *mpctx, struct track *track)
+{
+ if (track && track->selected) {
+ for (int t = 0; t < num_ptracks[track->type]; t++) {
+ if (mpctx->current_track[t][track->type] != track)
+ continue;
+ mp_switch_track_n(mpctx, t, track->type, NULL, 0);
+ mark_track_selection(mpctx, t, track->type, -1); // default
+ }
+ }
+}
+
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid)
+{
+ if (tid == -1)
+ return mpctx->current_track[0][type];
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == type && track->user_tid == tid)
+ return track;
+ }
+ return NULL;
+}
+
+bool mp_remove_track(struct MPContext *mpctx, struct track *track)
+{
+ if (!track->is_external)
+ return false;
+
+ mp_deselect_track(mpctx, track);
+ if (track->selected)
+ return false;
+
+ struct demuxer *d = track->demuxer;
+
+ int index = 0;
+ while (index < mpctx->num_tracks && mpctx->tracks[index] != track)
+ index++;
+ MP_TARRAY_REMOVE_AT(mpctx->tracks, mpctx->num_tracks, index);
+ talloc_free(track);
+
+ // Close the demuxer, unless there is still a track using it. These are
+ // all external tracks.
+ bool in_use = false;
+ for (int n = mpctx->num_tracks - 1; n >= 0 && !in_use; n--)
+ in_use |= mpctx->tracks[n]->demuxer == d;
+
+ if (!in_use)
+ demux_cancel_and_free(d);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return true;
+}
+
+// Add the given file as additional track. The filter argument controls how or
+// if tracks are auto-selected at any point.
+// To be run on a worker thread, locked (temporarily unlocks core).
+// cancel will generally be used to abort the loading process, but on success
+// the demuxer is changed to be slaved to mpctx->playback_abort instead.
+int mp_add_external_file(struct MPContext *mpctx, char *filename,
+ enum stream_type filter, struct mp_cancel *cancel,
+ bool cover_art)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!filename || mp_cancel_test(cancel))
+ return -1;
+
+ char *disp_filename = filename;
+ if (strncmp(disp_filename, "memory://", 9) == 0)
+ disp_filename = "memory://"; // avoid noise
+
+ struct demuxer_params params = {
+ .is_top_level = true,
+ .stream_flags = STREAM_ORIGIN_DIRECT,
+ };
+
+ switch (filter) {
+ case STREAM_SUB:
+ params.force_format = opts->sub_demuxer_name;
+ break;
+ case STREAM_AUDIO:
+ params.force_format = opts->audio_demuxer_name;
+ break;
+ }
+
+ mp_core_unlock(mpctx);
+
+ struct demuxer *demuxer =
+ demux_open_url(filename, &params, cancel, mpctx->global);
+ if (demuxer)
+ enable_demux_thread(mpctx, demuxer);
+
+ mp_core_lock(mpctx);
+
+ // The command could have overlapped with playback exiting. (We don't care
+ // if playback has started again meanwhile - weird, but not a problem.)
+ if (mpctx->stop_play)
+ goto err_out;
+
+ if (!demuxer)
+ goto err_out;
+
+ if (filter != STREAM_SUB && opts->rebase_start_time)
+ demux_set_ts_offset(demuxer, -demuxer->start_time);
+
+ bool has_any = false;
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++) {
+ struct sh_stream *sh = demux_get_stream(demuxer, n);
+ if (sh->type == filter || filter == STREAM_TYPE_COUNT) {
+ has_any = true;
+ break;
+ }
+ }
+
+ if (!has_any) {
+ char *tname = mp_tprintf(20, "%s ", stream_type_name(filter));
+ if (filter == STREAM_TYPE_COUNT)
+ tname = "";
+ MP_ERR(mpctx, "No %sstreams in file %s.\n", tname, disp_filename);
+ goto err_out;
+ }
+
+ int first_num = -1;
+ for (int n = 0; n < demux_get_num_stream(demuxer); n++) {
+ struct sh_stream *sh = demux_get_stream(demuxer, n);
+ struct track *t = add_stream_track(mpctx, demuxer, sh);
+ t->is_external = true;
+ if (sh->title && sh->title[0]) {
+ t->title = talloc_strdup(t, sh->title);
+ } else {
+ t->title = talloc_strdup(t, mp_basename(disp_filename));
+ }
+ t->external_filename = talloc_strdup(t, filename);
+ t->no_default = sh->type != filter;
+ t->no_auto_select = t->no_default;
+ // if we found video, and we are loading cover art, flag as such.
+ t->attached_picture = t->type == STREAM_VIDEO && cover_art;
+ if (first_num < 0 && (filter == STREAM_TYPE_COUNT || sh->type == filter))
+ first_num = mpctx->num_tracks - 1;
+ }
+
+ mp_cancel_set_parent(demuxer->cancel, mpctx->playback_abort);
+
+ return first_num;
+
+err_out:
+ demux_cancel_and_free(demuxer);
+ if (!mp_cancel_test(cancel))
+ MP_ERR(mpctx, "Can not open external file %s.\n", disp_filename);
+ return -1;
+}
+
+// to be run on a worker thread, locked (temporarily unlocks core)
+static void open_external_files(struct MPContext *mpctx, char **files,
+ enum stream_type filter)
+{
+ // Need a copy, because the option value could be mutated during iteration.
+ void *tmp = talloc_new(NULL);
+ files = mp_dup_str_array(tmp, files);
+
+ for (int n = 0; files && files[n]; n++)
+ // when given filter is set to video, we are loading up cover art
+ mp_add_external_file(mpctx, files[n], filter, mpctx->playback_abort,
+ filter == STREAM_VIDEO);
+
+ talloc_free(tmp);
+}
+
+// See mp_add_external_file() for meaning of cancel parameter.
+void autoload_external_files(struct MPContext *mpctx, struct mp_cancel *cancel)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->sub_auto < 0 && opts->audiofile_auto < 0 && opts->coverart_auto < 0)
+ return;
+ if (!opts->autoload_files || strcmp(mpctx->filename, "-") == 0)
+ return;
+
+ void *tmp = talloc_new(NULL);
+ struct subfn *list = find_external_files(mpctx->global, mpctx->filename, opts);
+ talloc_steal(tmp, list);
+
+ int sc[STREAM_TYPE_COUNT] = {0};
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ if (!mpctx->tracks[n]->attached_picture)
+ sc[mpctx->tracks[n]->type]++;
+ }
+
+ for (int i = 0; list && list[i].fname; i++) {
+ struct subfn *e = &list[i];
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (t->demuxer && strcmp(t->demuxer->filename, e->fname) == 0)
+ goto skip;
+ }
+ if (e->type == STREAM_SUB && !sc[STREAM_VIDEO] && !sc[STREAM_AUDIO])
+ goto skip;
+ if (e->type == STREAM_AUDIO && !sc[STREAM_VIDEO])
+ goto skip;
+ if (e->type == STREAM_VIDEO && (sc[STREAM_VIDEO] || !sc[STREAM_AUDIO]))
+ goto skip;
+
+ // when given filter is set to video, we are loading up cover art
+ int first = mp_add_external_file(mpctx, e->fname, e->type, cancel,
+ e->type == STREAM_VIDEO);
+ if (first < 0)
+ goto skip;
+
+ for (int n = first; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ t->auto_loaded = true;
+ if (!t->lang)
+ t->lang = talloc_strdup(t, e->lang);
+ }
+ skip:;
+ }
+
+ talloc_free(tmp);
+}
+
+// Do stuff to a newly loaded playlist. This includes any processing that may
+// be required after loading a playlist.
+void prepare_playlist(struct MPContext *mpctx, struct playlist *pl)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ pl->current = NULL;
+
+ if (opts->playlist_pos >= 0)
+ pl->current = playlist_entry_from_index(pl, opts->playlist_pos);
+
+ if (opts->shuffle)
+ playlist_shuffle(pl);
+
+ if (opts->merge_files)
+ merge_playlist_files(pl);
+
+ if (!pl->current)
+ pl->current = mp_check_playlist_resume(mpctx, pl);
+
+ if (!pl->current)
+ pl->current = playlist_get_first(pl);
+}
+
+// Replace the current playlist entry with playlist contents. Moves the entries
+// from the given playlist pl, so the entries don't actually need to be copied.
+static void transfer_playlist(struct MPContext *mpctx, struct playlist *pl,
+ int64_t *start_id, int *num_new_entries)
+{
+ if (pl->num_entries) {
+ prepare_playlist(mpctx, pl);
+ struct playlist_entry *new = pl->current;
+ *num_new_entries = pl->num_entries;
+ *start_id = playlist_transfer_entries(mpctx->playlist, pl);
+ // current entry is replaced
+ if (mpctx->playlist->current)
+ playlist_remove(mpctx->playlist, mpctx->playlist->current);
+ if (new)
+ mpctx->playlist->current = new;
+ } else {
+ MP_WARN(mpctx, "Empty playlist!\n");
+ }
+}
+
+static void process_hooks(struct MPContext *mpctx, char *name)
+{
+ mp_hook_start(mpctx, name);
+
+ while (!mp_hook_test_completion(mpctx, name)) {
+ mp_idle(mpctx);
+
+ // We have no idea what blocks a hook, so just do a full abort. This
+ // does nothing for hooks that happen outside of playback.
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+}
+
+// to be run on a worker thread, locked (temporarily unlocks core)
+static void load_chapters(struct MPContext *mpctx)
+{
+ struct demuxer *src = mpctx->demuxer;
+ bool free_src = false;
+ char *chapter_file = mpctx->opts->chapter_file;
+ if (chapter_file && chapter_file[0]) {
+ chapter_file = talloc_strdup(NULL, chapter_file);
+ mp_core_unlock(mpctx);
+ struct demuxer_params p = {.stream_flags = STREAM_ORIGIN_DIRECT};
+ struct demuxer *demux = demux_open_url(chapter_file, &p,
+ mpctx->playback_abort,
+ mpctx->global);
+ mp_core_lock(mpctx);
+ if (demux) {
+ src = demux;
+ free_src = true;
+ }
+ talloc_free(mpctx->chapters);
+ mpctx->chapters = NULL;
+ talloc_free(chapter_file);
+ }
+ if (src && !mpctx->chapters) {
+ talloc_free(mpctx->chapters);
+ mpctx->num_chapters = src->num_chapters;
+ mpctx->chapters = demux_copy_chapter_data(src->chapters, src->num_chapters);
+ if (mpctx->opts->rebase_start_time) {
+ for (int n = 0; n < mpctx->num_chapters; n++)
+ mpctx->chapters[n].pts -= src->start_time;
+ }
+ }
+ if (free_src)
+ demux_cancel_and_free(src);
+}
+
+static void load_per_file_options(m_config_t *conf,
+ struct playlist_param *params,
+ int params_count)
+{
+ for (int n = 0; n < params_count; n++) {
+ m_config_set_option_cli(conf, params[n].name, params[n].value,
+ M_SETOPT_BACKUP);
+ }
+}
+
+static MP_THREAD_VOID open_demux_thread(void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+
+ mp_thread_set_name("opener");
+
+ struct demuxer_params p = {
+ .force_format = mpctx->open_format,
+ .stream_flags = mpctx->open_url_flags,
+ .stream_record = true,
+ .is_top_level = true,
+ };
+ struct demuxer *demux =
+ demux_open_url(mpctx->open_url, &p, mpctx->open_cancel, mpctx->global);
+ mpctx->open_res_demuxer = demux;
+
+ if (demux) {
+ MP_VERBOSE(mpctx, "Opening done: %s\n", mpctx->open_url);
+
+ if (mpctx->open_for_prefetch && !demux->fully_read) {
+ int num_streams = demux_get_num_stream(demux);
+ for (int n = 0; n < num_streams; n++) {
+ struct sh_stream *sh = demux_get_stream(demux, n);
+ demuxer_select_track(demux, sh, MP_NOPTS_VALUE, true);
+ }
+
+ demux_set_wakeup_cb(demux, wakeup_demux, mpctx);
+ demux_start_thread(demux);
+ demux_start_prefetch(demux);
+ }
+ } else {
+ MP_VERBOSE(mpctx, "Opening failed or was aborted: %s\n", mpctx->open_url);
+
+ if (p.demuxer_failed) {
+ mpctx->open_res_error = MPV_ERROR_UNKNOWN_FORMAT;
+ } else {
+ mpctx->open_res_error = MPV_ERROR_LOADING_FAILED;
+ }
+ }
+
+ atomic_store(&mpctx->open_done, true);
+ mp_wakeup_core(mpctx);
+ MP_THREAD_RETURN();
+}
+
+static void cancel_open(struct MPContext *mpctx)
+{
+ if (mpctx->open_cancel)
+ mp_cancel_trigger(mpctx->open_cancel);
+
+ if (mpctx->open_active)
+ mp_thread_join(mpctx->open_thread);
+ mpctx->open_active = false;
+
+ if (mpctx->open_res_demuxer)
+ demux_cancel_and_free(mpctx->open_res_demuxer);
+ mpctx->open_res_demuxer = NULL;
+
+ TA_FREEP(&mpctx->open_cancel);
+ TA_FREEP(&mpctx->open_url);
+ TA_FREEP(&mpctx->open_format);
+
+ atomic_store(&mpctx->open_done, false);
+}
+
+// Setup all the field to open this url, and make sure a thread is running.
+static void start_open(struct MPContext *mpctx, char *url, int url_flags,
+ bool for_prefetch)
+{
+ cancel_open(mpctx);
+
+ assert(!mpctx->open_active);
+ assert(!mpctx->open_cancel);
+ assert(!mpctx->open_res_demuxer);
+ assert(!atomic_load(&mpctx->open_done));
+
+ mpctx->open_cancel = mp_cancel_new(NULL);
+ mpctx->open_url = talloc_strdup(NULL, url);
+ mpctx->open_format = talloc_strdup(NULL, mpctx->opts->demuxer_name);
+ mpctx->open_url_flags = url_flags;
+ mpctx->open_for_prefetch = for_prefetch && mpctx->opts->demuxer_thread;
+
+ if (mp_thread_create(&mpctx->open_thread, open_demux_thread, mpctx)) {
+ cancel_open(mpctx);
+ return;
+ }
+
+ mpctx->open_active = true;
+}
+
+static void open_demux_reentrant(struct MPContext *mpctx)
+{
+ char *url = mpctx->stream_open_filename;
+
+ if (mpctx->open_active) {
+ bool done = atomic_load(&mpctx->open_done);
+ bool failed = done && !mpctx->open_res_demuxer;
+ bool correct_url = strcmp(mpctx->open_url, url) == 0;
+
+ if (correct_url && !failed) {
+ MP_VERBOSE(mpctx, "Using prefetched/prefetching URL.\n");
+ } else if (correct_url && failed) {
+ MP_VERBOSE(mpctx, "Prefetched URL failed, retrying.\n");
+ cancel_open(mpctx);
+ } else {
+ if (done) {
+ MP_VERBOSE(mpctx, "Dropping finished prefetch of wrong URL.\n");
+ } else {
+ MP_VERBOSE(mpctx, "Aborting ongoing prefetch of wrong URL.\n");
+ }
+ cancel_open(mpctx);
+ }
+ }
+
+ if (!mpctx->open_active)
+ start_open(mpctx, url, mpctx->playing->stream_flags, false);
+
+ // User abort should cancel the opener now.
+ mp_cancel_set_parent(mpctx->open_cancel, mpctx->playback_abort);
+
+ while (!atomic_load(&mpctx->open_done)) {
+ mp_idle(mpctx);
+
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+
+ if (mpctx->open_res_demuxer) {
+ mpctx->demuxer = mpctx->open_res_demuxer;
+ mpctx->open_res_demuxer = NULL;
+ mp_cancel_set_parent(mpctx->demuxer->cancel, mpctx->playback_abort);
+ } else {
+ mpctx->error_playing = mpctx->open_res_error;
+ }
+
+ cancel_open(mpctx); // cleanup
+}
+
+void prefetch_next(struct MPContext *mpctx)
+{
+ if (!mpctx->opts->prefetch_open)
+ return;
+
+ struct playlist_entry *new_entry = mp_next_file(mpctx, +1, false);
+ if (new_entry && !mpctx->open_active && new_entry->filename) {
+ MP_VERBOSE(mpctx, "Prefetching: %s\n", new_entry->filename);
+ start_open(mpctx, new_entry->filename, new_entry->stream_flags, true);
+ }
+}
+
+static void clear_playlist_paths(struct MPContext *mpctx)
+{
+ TA_FREEP(&mpctx->playlist_paths);
+ mpctx->playlist_paths_len = 0;
+}
+
+static bool infinite_playlist_loading_loop(struct MPContext *mpctx, struct playlist *pl)
+{
+ if (pl->num_entries) {
+ struct playlist_entry *e = pl->entries[0];
+ for (int n = 0; n < mpctx->playlist_paths_len; n++) {
+ if (strcmp(mpctx->playlist_paths[n], e->filename) == 0) {
+ clear_playlist_paths(mpctx);
+ return true;
+ }
+ }
+ }
+ MP_TARRAY_APPEND(mpctx, mpctx->playlist_paths, mpctx->playlist_paths_len,
+ talloc_strdup(mpctx->playlist_paths, mpctx->filename));
+ return false;
+}
+
+// Destroy the complex filter, and remove the references to the filter pads.
+// (Call cleanup_deassociated_complex_filters() to close decoders/VO/AO
+// that are not connected anymore due to this.)
+static void deassociate_complex_filters(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ mpctx->tracks[n]->sink = NULL;
+ if (mpctx->vo_chain)
+ mpctx->vo_chain->filter_src = NULL;
+ if (mpctx->ao_chain)
+ mpctx->ao_chain->filter_src = NULL;
+ TA_FREEP(&mpctx->lavfi);
+ TA_FREEP(&mpctx->lavfi_graph);
+}
+
+// Close all decoders and sinks (AO/VO) that are not connected to either
+// a track or a filter pad.
+static void cleanup_deassociated_complex_filters(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (!(track->sink || track->vo_c || track->ao_c)) {
+ if (track->dec && !track->vo_c && !track->ao_c) {
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+ track->selected = false;
+ }
+ }
+
+ if (mpctx->vo_chain && !mpctx->vo_chain->dec_src &&
+ !mpctx->vo_chain->filter_src)
+ {
+ uninit_video_chain(mpctx);
+ }
+ if (mpctx->ao_chain && !mpctx->ao_chain->dec_src &&
+ !mpctx->ao_chain->filter_src)
+ {
+ uninit_audio_chain(mpctx);
+ }
+}
+
+static void kill_outputs(struct MPContext *mpctx, struct track *track)
+{
+ if (track->vo_c || track->ao_c) {
+ MP_VERBOSE(mpctx, "deselecting track %d for lavfi-complex option\n",
+ track->user_tid);
+ mp_switch_track(mpctx, track->type, NULL, 0);
+ }
+ assert(!(track->vo_c || track->ao_c));
+}
+
+// >0: changed, 0: no change, -1: error
+static int reinit_complex_filters(struct MPContext *mpctx, bool force_uninit)
+{
+ char *graph = mpctx->opts->lavfi_complex;
+ bool have_graph = graph && graph[0] && !force_uninit;
+ if (have_graph && mpctx->lavfi &&
+ strcmp(graph, mpctx->lavfi_graph) == 0 &&
+ !mp_filter_has_failed(mpctx->lavfi))
+ return 0;
+ if (!mpctx->lavfi && !have_graph)
+ return 0;
+
+ // Deassociate the old filter pads. We leave both sources (tracks) and
+ // sinks (AO/VO) "dangling", connected to neither track or filter pad.
+ // Later, we either reassociate them with new pads, or uninit them if
+ // they are still dangling. This avoids too interruptive actions like
+ // recreating the VO.
+ deassociate_complex_filters(mpctx);
+
+ bool success = false;
+ if (!have_graph) {
+ success = true; // normal full removal of graph
+ goto done;
+ }
+
+ struct mp_lavfi *l =
+ mp_lavfi_create_graph(mpctx->filter_root, 0, false, NULL, NULL, graph);
+ if (!l)
+ goto done;
+ mpctx->lavfi = l->f;
+ mpctx->lavfi_graph = talloc_strdup(NULL, graph);
+
+ mp_filter_set_error_handler(mpctx->lavfi, mpctx->filter_root);
+
+ for (int n = 0; n < mpctx->lavfi->num_pins; n++)
+ mp_pin_disconnect(mpctx->lavfi->pins[n]);
+
+ struct mp_pin *pad = mp_filter_get_named_pin(mpctx->lavfi, "vo");
+ if (pad && mp_pin_get_dir(pad) == MP_PIN_OUT) {
+ if (mpctx->vo_chain && mpctx->vo_chain->track)
+ kill_outputs(mpctx, mpctx->vo_chain->track);
+ if (!mpctx->vo_chain) {
+ reinit_video_chain_src(mpctx, NULL);
+ if (!mpctx->vo_chain)
+ goto done;
+ }
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(!vo_c->track);
+ vo_c->filter_src = pad;
+ mp_pin_connect(vo_c->filter->f->pins[0], vo_c->filter_src);
+ }
+
+ pad = mp_filter_get_named_pin(mpctx->lavfi, "ao");
+ if (pad && mp_pin_get_dir(pad) == MP_PIN_OUT) {
+ if (mpctx->ao_chain && mpctx->ao_chain->track)
+ kill_outputs(mpctx, mpctx->ao_chain->track);
+ if (!mpctx->ao_chain) {
+ reinit_audio_chain_src(mpctx, NULL);
+ if (!mpctx->ao_chain)
+ goto done;
+ }
+ struct ao_chain *ao_c = mpctx->ao_chain;
+ assert(!ao_c->track);
+ ao_c->filter_src = pad;
+ mp_pin_connect(ao_c->filter->f->pins[0], ao_c->filter_src);
+ }
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+
+ char label[32];
+ char prefix;
+ switch (track->type) {
+ case STREAM_VIDEO: prefix = 'v'; break;
+ case STREAM_AUDIO: prefix = 'a'; break;
+ default: continue;
+ }
+ snprintf(label, sizeof(label), "%cid%d", prefix, track->user_tid);
+
+ pad = mp_filter_get_named_pin(mpctx->lavfi, label);
+ if (!pad)
+ continue;
+ if (mp_pin_get_dir(pad) != MP_PIN_IN)
+ continue;
+ assert(!mp_pin_is_connected(pad));
+
+ assert(!track->sink);
+
+ kill_outputs(mpctx, track);
+
+ track->sink = pad;
+ track->selected = true;
+
+ if (!track->dec) {
+ if (track->type == STREAM_VIDEO && !init_video_decoder(mpctx, track))
+ goto done;
+ if (track->type == STREAM_AUDIO && !init_audio_decoder(mpctx, track))
+ goto done;
+ }
+
+ mp_pin_connect(track->sink, track->dec->f->pins[0]);
+ }
+
+ // Don't allow unconnected pins. Libavfilter would make the data flow a
+ // real pain anyway.
+ for (int n = 0; n < mpctx->lavfi->num_pins; n++) {
+ struct mp_pin *pin = mpctx->lavfi->pins[n];
+ if (!mp_pin_is_connected(pin)) {
+ MP_ERR(mpctx, "Pad %s is not connected to anything.\n",
+ mp_pin_get_name(pin));
+ goto done;
+ }
+ }
+
+ success = true;
+done:
+
+ if (!success)
+ deassociate_complex_filters(mpctx);
+
+ cleanup_deassociated_complex_filters(mpctx);
+
+ if (mpctx->playback_initialized) {
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reselect_demux_stream(mpctx, mpctx->tracks[n], false);
+ }
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return success ? 1 : -1;
+}
+
+void update_lavfi_complex(struct MPContext *mpctx)
+{
+ if (mpctx->playback_initialized) {
+ if (reinit_complex_filters(mpctx, false) != 0)
+ issue_refresh_seek(mpctx, MPSEEK_EXACT);
+ }
+}
+
+
+// Worker thread for loading external files and such. This is needed to avoid
+// freezing the core when waiting for network while loading these.
+static void load_external_opts_thread(void *p)
+{
+ void **a = p;
+ struct MPContext *mpctx = a[0];
+ struct mp_waiter *waiter = a[1];
+
+ mp_core_lock(mpctx);
+
+ load_chapters(mpctx);
+ open_external_files(mpctx, mpctx->opts->audio_files, STREAM_AUDIO);
+ open_external_files(mpctx, mpctx->opts->sub_name, STREAM_SUB);
+ open_external_files(mpctx, mpctx->opts->coverart_files, STREAM_VIDEO);
+ open_external_files(mpctx, mpctx->opts->external_files, STREAM_TYPE_COUNT);
+ autoload_external_files(mpctx, mpctx->playback_abort);
+
+ mp_waiter_wakeup(waiter, 0);
+ mp_wakeup_core(mpctx);
+ mp_core_unlock(mpctx);
+}
+
+static void load_external_opts(struct MPContext *mpctx)
+{
+ struct mp_waiter wait = MP_WAITER_INITIALIZER;
+
+ void *a[] = {mpctx, &wait};
+ if (!mp_thread_pool_queue(mpctx->thread_pool, load_external_opts_thread, a)) {
+ mpctx->stop_play = PT_ERROR;
+ return;
+ }
+
+ while (!mp_waiter_poll(&wait)) {
+ mp_idle(mpctx);
+
+ if (mpctx->stop_play)
+ mp_abort_playback_async(mpctx);
+ }
+
+ mp_waiter_wait(&wait);
+}
+
+// Start playing the current playlist entry.
+// Handle initialization and deinitialization.
+static void play_current_file(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ assert(mpctx->stop_play);
+ mpctx->stop_play = 0;
+
+ process_hooks(mpctx, "on_before_start_file");
+ if (mpctx->stop_play || !mpctx->playlist->current)
+ return;
+
+ mpv_event_start_file start_event = {
+ .playlist_entry_id = mpctx->playlist->current->id,
+ };
+ mpv_event_end_file end_event = {
+ .playlist_entry_id = start_event.playlist_entry_id,
+ };
+
+ mp_notify(mpctx, MPV_EVENT_START_FILE, &start_event);
+
+ mp_cancel_reset(mpctx->playback_abort);
+
+ mpctx->error_playing = MPV_ERROR_LOADING_FAILED;
+ mpctx->filename = NULL;
+ mpctx->shown_aframes = 0;
+ mpctx->shown_vframes = 0;
+ mpctx->last_chapter_seek = -2;
+ mpctx->last_chapter_flag = false;
+ mpctx->last_chapter = -2;
+ mpctx->paused = false;
+ mpctx->playing_msg_shown = false;
+ mpctx->max_frames = -1;
+ mpctx->video_speed = mpctx->audio_speed = opts->playback_speed;
+ mpctx->speed_factor_a = mpctx->speed_factor_v = 1.0;
+ mpctx->display_sync_error = 0.0;
+ mpctx->display_sync_active = false;
+ // let get_current_time() show 0 as start time (before playback_pts is set)
+ mpctx->last_seek_pts = 0.0;
+ mpctx->seek = (struct seek_params){ 0 };
+ mpctx->filter_root = mp_filter_create_root(mpctx->global);
+ mp_filter_graph_set_wakeup_cb(mpctx->filter_root, mp_wakeup_core_cb, mpctx);
+ mp_filter_graph_set_max_run_time(mpctx->filter_root, 0.1);
+
+ reset_playback_state(mpctx);
+
+ mpctx->playing = mpctx->playlist->current;
+ assert(mpctx->playing);
+ assert(mpctx->playing->filename);
+ mpctx->playing->reserved += 1;
+
+ mpctx->filename = talloc_strdup(NULL, mpctx->playing->filename);
+ mpctx->stream_open_filename = mpctx->filename;
+
+ mpctx->add_osd_seek_info &= OSD_SEEK_INFO_CURRENT_FILE;
+
+ if (opts->reset_options) {
+ for (int n = 0; opts->reset_options[n]; n++) {
+ const char *opt = opts->reset_options[n];
+ if (opt[0]) {
+ if (strcmp(opt, "all") == 0) {
+ m_config_backup_all_opts(mpctx->mconfig);
+ } else {
+ m_config_backup_opt(mpctx->mconfig, opt);
+ }
+ }
+ }
+ }
+
+ mp_load_auto_profiles(mpctx);
+
+ bool watch_later = mp_load_playback_resume(mpctx, mpctx->filename);
+
+ load_per_file_options(mpctx->mconfig, mpctx->playing->params,
+ mpctx->playing->num_params);
+
+ mpctx->max_frames = opts->play_frames;
+
+ handle_force_window(mpctx, false);
+
+ if (mpctx->playlist->num_entries > 1 ||
+ mpctx->playing->playlist_path)
+ MP_INFO(mpctx, "Playing: %s\n", mpctx->filename);
+
+ assert(mpctx->demuxer == NULL);
+
+ process_hooks(mpctx, "on_load");
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ if (opts->stream_dump && opts->stream_dump[0]) {
+ if (stream_dump(mpctx, mpctx->stream_open_filename) >= 0)
+ mpctx->error_playing = 1;
+ goto terminate_playback;
+ }
+
+ open_demux_reentrant(mpctx);
+ if (!mpctx->stop_play && !mpctx->demuxer) {
+ process_hooks(mpctx, "on_load_fail");
+ if (strcmp(mpctx->stream_open_filename, mpctx->filename) != 0 &&
+ !mpctx->stop_play)
+ {
+ mpctx->error_playing = MPV_ERROR_LOADING_FAILED;
+ open_demux_reentrant(mpctx);
+ }
+ }
+ if (!mpctx->demuxer || mpctx->stop_play)
+ goto terminate_playback;
+
+ if (mpctx->demuxer->playlist) {
+ if (watch_later)
+ mp_delete_watch_later_conf(mpctx, mpctx->filename);
+ struct playlist *pl = mpctx->demuxer->playlist;
+ playlist_populate_playlist_path(pl, mpctx->filename);
+ if (infinite_playlist_loading_loop(mpctx, pl)) {
+ mpctx->stop_play = PT_STOP;
+ MP_ERR(mpctx, "Infinite playlist loading loop detected.\n");
+ goto terminate_playback;
+ }
+ transfer_playlist(mpctx, pl, &end_event.playlist_insert_id,
+ &end_event.playlist_insert_num_entries);
+ mp_notify_property(mpctx, "playlist");
+ mpctx->error_playing = 2;
+ goto terminate_playback;
+ }
+
+ if (mpctx->opts->rebase_start_time)
+ demux_set_ts_offset(mpctx->demuxer, -mpctx->demuxer->start_time);
+ enable_demux_thread(mpctx, mpctx->demuxer);
+
+ add_demuxer_tracks(mpctx, mpctx->demuxer);
+
+ load_external_opts(mpctx);
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ check_previous_track_selection(mpctx);
+
+ process_hooks(mpctx, "on_preloaded");
+ if (mpctx->stop_play)
+ goto terminate_playback;
+
+ if (reinit_complex_filters(mpctx, false) < 0)
+ goto terminate_playback;
+
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ struct track *sel = NULL;
+ bool taken = (t == STREAM_VIDEO && mpctx->vo_chain) ||
+ (t == STREAM_AUDIO && mpctx->ao_chain);
+ if (!taken && opts->stream_auto_sel)
+ sel = select_default_track(mpctx, i, t);
+ mpctx->current_track[i][t] = sel;
+ }
+ }
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++) {
+ for (int i = 0; i < num_ptracks[t]; i++) {
+ // One track can strictly feed at most 1 decoder
+ struct track *track = mpctx->current_track[i][t];
+ if (track) {
+ if (track->type != STREAM_SUB &&
+ mpctx->encode_lavc_ctx &&
+ !encode_lavc_stream_type_ok(mpctx->encode_lavc_ctx,
+ track->type))
+ {
+ MP_WARN(mpctx, "Disabling %s (not supported by target "
+ "format).\n", stream_type_name(track->type));
+ mpctx->current_track[i][t] = NULL;
+ mark_track_selection(mpctx, i, t, -2); // disable
+ } else if (track->selected) {
+ MP_ERR(mpctx, "Track %d can't be selected twice.\n",
+ track->user_tid);
+ mpctx->current_track[i][t] = NULL;
+ mark_track_selection(mpctx, i, t, -2); // disable
+ } else {
+ track->selected = true;
+ }
+ }
+
+ // Revert selection of unselected tracks to default. This is needed
+ // because track properties have inconsistent behavior.
+ if (!track && opts->stream_id[i][t] >= 0)
+ mark_track_selection(mpctx, i, t, -1); // default
+ }
+ }
+
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reselect_demux_stream(mpctx, mpctx->tracks[n], false);
+
+ update_demuxer_properties(mpctx);
+
+ update_playback_speed(mpctx);
+
+ reinit_video_chain(mpctx);
+ reinit_audio_chain(mpctx);
+ reinit_sub_all(mpctx);
+
+ if (mpctx->encode_lavc_ctx) {
+ if (mpctx->vo_chain)
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, STREAM_VIDEO);
+ if (mpctx->ao_chain)
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, STREAM_AUDIO);
+ encode_lavc_set_metadata(mpctx->encode_lavc_ctx,
+ mpctx->demuxer->metadata);
+ }
+
+ if (!mpctx->vo_chain && !mpctx->ao_chain && opts->stream_auto_sel) {
+ MP_FATAL(mpctx, "No video or audio streams selected.\n");
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ goto terminate_playback;
+ }
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_coverart) {
+ MP_INFO(mpctx,
+ "Displaying cover art. Use --no-audio-display to prevent this.\n");
+ }
+
+ if (!mpctx->vo_chain)
+ handle_force_window(mpctx, true);
+
+ MP_VERBOSE(mpctx, "Starting playback...\n");
+
+ mpctx->playback_initialized = true;
+ mpctx->playing->playlist_prev_attempt = false;
+ mp_notify(mpctx, MPV_EVENT_FILE_LOADED, NULL);
+ update_screensaver_state(mpctx);
+ clear_playlist_paths(mpctx);
+
+ if (watch_later)
+ mp_delete_watch_later_conf(mpctx, mpctx->filename);
+
+ if (mpctx->max_frames == 0) {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->error_playing = 0;
+ goto terminate_playback;
+ }
+
+ if (opts->demuxer_cache_wait) {
+ demux_start_prefetch(mpctx->demuxer);
+
+ while (!mpctx->stop_play) {
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+ if (s.idle)
+ break;
+
+ mp_idle(mpctx);
+ }
+ }
+
+ // (Not get_play_start_pts(), which would always trigger a seek.)
+ double play_start_pts = rel_time_to_abs(mpctx, opts->play_start);
+
+ // Backward playback -> start from end by default.
+ if (play_start_pts == MP_NOPTS_VALUE && opts->play_dir < 0)
+ play_start_pts = get_start_time(mpctx, -1);
+
+ if (play_start_pts != MP_NOPTS_VALUE) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, play_start_pts, MPSEEK_DEFAULT, 0);
+ execute_queued_seek(mpctx);
+ }
+
+ update_internal_pause_state(mpctx);
+
+ mpctx->error_playing = 0;
+ mpctx->in_playloop = true;
+ while (!mpctx->stop_play)
+ run_playloop(mpctx);
+ mpctx->in_playloop = false;
+
+ MP_VERBOSE(mpctx, "EOF code: %d \n", mpctx->stop_play);
+
+terminate_playback:
+
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_ERROR;
+
+ if (mpctx->stop_play != AT_END_OF_FILE)
+ clear_audio_output_buffers(mpctx);
+
+ update_core_idle_state(mpctx);
+
+ if (mpctx->step_frames) {
+ opts->pause = true;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->pause);
+ }
+
+ process_hooks(mpctx, "on_unload");
+
+ // time to uninit all, except global stuff:
+ reinit_complex_filters(mpctx, true);
+ uninit_audio_chain(mpctx);
+ uninit_video_chain(mpctx);
+ uninit_sub_all(mpctx);
+ if (!opts->gapless_audio && !mpctx->encode_lavc_ctx)
+ uninit_audio_out(mpctx);
+
+ mpctx->playback_initialized = false;
+
+ uninit_demuxer(mpctx);
+
+ // Possibly stop ongoing async commands.
+ mp_abort_playback_async(mpctx);
+
+ m_config_restore_backups(mpctx->mconfig);
+
+ TA_FREEP(&mpctx->filter_root);
+ talloc_free(mpctx->filtered_tags);
+ mpctx->filtered_tags = NULL;
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ if (encode_lavc_didfail(mpctx->encode_lavc_ctx))
+ mpctx->stop_play = PT_ERROR;
+
+ if (mpctx->stop_play == PT_ERROR && !mpctx->error_playing)
+ mpctx->error_playing = MPV_ERROR_GENERIC;
+
+ bool nothing_played = !mpctx->shown_aframes && !mpctx->shown_vframes &&
+ mpctx->error_playing <= 0;
+ bool playlist_prev_continue = false;
+ switch (mpctx->stop_play) {
+ case PT_ERROR:
+ case AT_END_OF_FILE:
+ {
+ if (mpctx->error_playing == 0 && nothing_played)
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ if (mpctx->error_playing < 0) {
+ end_event.error = mpctx->error_playing;
+ end_event.reason = MPV_END_FILE_REASON_ERROR;
+ } else if (mpctx->error_playing == 2) {
+ end_event.reason = MPV_END_FILE_REASON_REDIRECT;
+ } else {
+ end_event.reason = MPV_END_FILE_REASON_EOF;
+ }
+ if (mpctx->playing) {
+ mpctx->playing->init_failed = nothing_played;
+ playlist_prev_continue = mpctx->playing->playlist_prev_attempt &&
+ nothing_played;
+ mpctx->playing->playlist_prev_attempt = false;
+ }
+ break;
+ }
+ // Note that error_playing is meaningless in these cases.
+ case PT_NEXT_ENTRY:
+ case PT_CURRENT_ENTRY:
+ case PT_STOP: end_event.reason = MPV_END_FILE_REASON_STOP; break;
+ case PT_QUIT: end_event.reason = MPV_END_FILE_REASON_QUIT; break;
+ };
+ mp_notify(mpctx, MPV_EVENT_END_FILE, &end_event);
+
+ MP_VERBOSE(mpctx, "finished playback, %s (reason %d)\n",
+ mpv_error_string(end_event.error), end_event.reason);
+ if (end_event.error == MPV_ERROR_UNKNOWN_FORMAT)
+ MP_ERR(mpctx, "Failed to recognize file format.\n");
+
+ if (mpctx->playing)
+ playlist_entry_unref(mpctx->playing);
+ mpctx->playing = NULL;
+ talloc_free(mpctx->filename);
+ mpctx->filename = NULL;
+ mpctx->stream_open_filename = NULL;
+
+ if (end_event.error < 0 && nothing_played) {
+ mpctx->files_broken++;
+ } else if (end_event.error < 0) {
+ mpctx->files_errored++;
+ } else {
+ mpctx->files_played++;
+ }
+
+ assert(mpctx->stop_play);
+
+ process_hooks(mpctx, "on_after_end_file");
+
+ if (playlist_prev_continue) {
+ struct playlist_entry *e = mp_next_file(mpctx, -1, false);
+ if (e) {
+ mp_set_playlist_entry(mpctx, e);
+ play_current_file(mpctx);
+ }
+ }
+}
+
+// Determine the next file to play. Note that if this function returns non-NULL,
+// it can have side-effects and mutate mpctx.
+// direction: -1 (previous) or +1 (next)
+// force: if true, don't skip playlist entries marked as failed
+struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
+ bool force)
+{
+ struct playlist_entry *next = playlist_get_next(mpctx->playlist, direction);
+ if (next && direction < 0 && !force)
+ next->playlist_prev_attempt = true;
+ if (!next && mpctx->opts->loop_times != 1) {
+ if (direction > 0) {
+ if (mpctx->opts->shuffle)
+ playlist_shuffle(mpctx->playlist);
+ next = playlist_get_first(mpctx->playlist);
+ if (next && mpctx->opts->loop_times > 1) {
+ mpctx->opts->loop_times--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig,
+ &mpctx->opts->loop_times);
+ }
+ } else {
+ next = playlist_get_last(mpctx->playlist);
+ }
+ bool ignore_failures = mpctx->opts->loop_times == -2;
+ if (!force && next && next->init_failed && !ignore_failures) {
+ // Don't endless loop if no file in playlist is playable
+ bool all_failed = true;
+ for (int n = 0; n < mpctx->playlist->num_entries; n++) {
+ all_failed &= mpctx->playlist->entries[n]->init_failed;
+ if (!all_failed)
+ break;
+ }
+ if (all_failed)
+ next = NULL;
+ }
+ }
+ return next;
+}
+
+// Play all entries on the playlist, starting from the current entry.
+// Return if all done.
+void mp_play_files(struct MPContext *mpctx)
+{
+ stats_register_thread_cputime(mpctx->stats, "thread");
+
+ // Wait for all scripts to load before possibly starting playback.
+ if (!mp_clients_all_initialized(mpctx)) {
+ MP_VERBOSE(mpctx, "Waiting for scripts...\n");
+ while (!mp_clients_all_initialized(mpctx))
+ mp_idle(mpctx);
+ mp_wakeup_core(mpctx); // avoid lost wakeups during waiting
+ MP_VERBOSE(mpctx, "Done loading scripts.\n");
+ }
+ // After above is finished; but even if it's skipped.
+ mp_msg_set_early_logging(mpctx->global, false);
+
+ prepare_playlist(mpctx, mpctx->playlist);
+
+ for (;;) {
+ idle_loop(mpctx);
+
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ if (mpctx->playlist->current)
+ play_current_file(mpctx);
+
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ struct playlist_entry *new_entry = NULL;
+ if (mpctx->stop_play == PT_NEXT_ENTRY || mpctx->stop_play == PT_ERROR ||
+ mpctx->stop_play == AT_END_OF_FILE)
+ {
+ new_entry = mp_next_file(mpctx, +1, false);
+ } else if (mpctx->stop_play == PT_CURRENT_ENTRY) {
+ new_entry = mpctx->playlist->current;
+ }
+
+ mpctx->playlist->current = new_entry;
+ mpctx->playlist->current_was_replaced = false;
+ mpctx->stop_play = new_entry ? PT_NEXT_ENTRY : PT_STOP;
+
+ if (!mpctx->playlist->current && mpctx->opts->player_idle_mode < 2)
+ break;
+ }
+
+ cancel_open(mpctx);
+
+ if (mpctx->encode_lavc_ctx) {
+ // Make sure all streams get finished.
+ uninit_audio_out(mpctx);
+ uninit_video_out(mpctx);
+
+ if (!encode_lavc_free(mpctx->encode_lavc_ctx))
+ mpctx->files_errored += 1;
+
+ mpctx->encode_lavc_ctx = NULL;
+ }
+}
+
+// Abort current playback and set the given entry to play next.
+// e must be on the mpctx->playlist.
+void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e)
+{
+ assert(!e || playlist_entry_to_index(mpctx->playlist, e) >= 0);
+ mpctx->playlist->current = e;
+ mpctx->playlist->current_was_replaced = false;
+ mp_notify(mpctx, MP_EVENT_CHANGE_PLAYLIST, NULL);
+ // Make it pick up the new entry.
+ if (mpctx->stop_play != PT_QUIT)
+ mpctx->stop_play = e ? PT_CURRENT_ENTRY : PT_STOP;
+ mp_wakeup_core(mpctx);
+}
diff --git a/player/lua.c b/player/lua.c
new file mode 100644
index 0000000..41fd520
--- /dev/null
+++ b/player/lua.c
@@ -0,0 +1,1341 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <math.h>
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+#include "options/m_property.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "options/m_option.h"
+#include "input/input.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "misc/json.h"
+#include "osdep/subprocess.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "core.h"
+#include "command.h"
+#include "client.h"
+#include "libmpv/client.h"
+
+// List of builtin modules and their contents as strings.
+// All these are generated from player/lua/*.lua
+static const char * const builtin_lua_scripts[][2] = {
+ {"mp.defaults",
+# include "player/lua/defaults.lua.inc"
+ },
+ {"mp.assdraw",
+# include "player/lua/assdraw.lua.inc"
+ },
+ {"mp.options",
+# include "player/lua/options.lua.inc"
+ },
+ {"@osc.lua",
+# include "player/lua/osc.lua.inc"
+ },
+ {"@ytdl_hook.lua",
+# include "player/lua/ytdl_hook.lua.inc"
+ },
+ {"@stats.lua",
+# include "player/lua/stats.lua.inc"
+ },
+ {"@console.lua",
+# include "player/lua/console.lua.inc"
+ },
+ {"@auto_profiles.lua",
+# include "player/lua/auto_profiles.lua.inc"
+ },
+ {0}
+};
+
+// Represents a loaded script. Each has its own Lua state.
+struct script_ctx {
+ const char *name;
+ const char *filename;
+ const char *path; // NULL if single file
+ lua_State *state;
+ struct mp_log *log;
+ struct mpv_handle *client;
+ struct MPContext *mpctx;
+ size_t lua_malloc_size;
+ lua_Alloc lua_allocf;
+ void *lua_alloc_ud;
+ struct stats_ctx *stats;
+};
+
+#if LUA_VERSION_NUM <= 501
+#define mp_cpcall lua_cpcall
+#define mp_lua_len lua_objlen
+#else
+// Curse whoever had this stupid idea. Curse whoever thought it would be a good
+// idea not to include an emulated lua_cpcall() even more.
+static int mp_cpcall (lua_State *L, lua_CFunction func, void *ud)
+{
+ lua_pushcfunction(L, func); // doesn't allocate in 5.2 (but does in 5.1)
+ lua_pushlightuserdata(L, ud);
+ return lua_pcall(L, 1, 0, 0);
+}
+#define mp_lua_len lua_rawlen
+#endif
+
+// Ensure that the given argument exists, even if it's nil. Can be used to
+// avoid confusing the last missing optional arg with the first temporary value
+// pushed to the stack.
+static void mp_lua_optarg(lua_State *L, int arg)
+{
+ while (arg > lua_gettop(L))
+ lua_pushnil(L);
+}
+
+// autofree: avoid leaks if a lua-error occurs between talloc new/free.
+// If a lua c-function does a new allocation (not tied to an existing context),
+// and an uncaught lua-error occurs before "free" - the allocation is leaked.
+
+// autofree lua C function: same as lua_CFunction but with these differences:
+// - It accepts an additional void* argument - a pre-initialized talloc context
+// which it can use, and which is freed with its children once the function
+// completes - regardless if a lua error occurred or not. If a lua error did
+// occur then it's re-thrown after the ctx is freed.
+// The stack/arguments/upvalues/return are the same as with lua_CFunction.
+// - It's inserted into the lua VM using af_pushc{function,closure} instead of
+// lua_pushc{function,closure}, which takes care of wrapping it with the
+// automatic talloc allocation + lua-error-handling + talloc release.
+// This requires using AF_ENTRY instead of FN_ENTRY at struct fn_entry.
+// - The autofree overhead per call is roughly two additional plain lua calls.
+// Typically that's up to 20% slower than plain new+free without "auto",
+// and at most about twice slower - compared to bare new+free lua_CFunction.
+// - The overhead of af_push* is one additional lua-c-closure with two upvalues.
+typedef int (*af_CFunction)(lua_State *L, void *ctx);
+
+static void af_pushcclosure(lua_State *L, af_CFunction fn, int n);
+#define af_pushcfunction(L, fn) af_pushcclosure((L), (fn), 0)
+
+
+// add_af_dir, add_af_mpv_alloc take a valid DIR*/char* value respectively,
+// and closedir/mpv_free it when the parent is freed.
+
+static void destruct_af_dir(void *p)
+{
+ closedir(*(DIR**)p);
+}
+
+static void add_af_dir(void *parent, DIR *d)
+{
+ DIR **pd = talloc(parent, DIR*);
+ *pd = d;
+ talloc_set_destructor(pd, destruct_af_dir);
+}
+
+static void destruct_af_mpv_alloc(void *p)
+{
+ mpv_free(*(char**)p);
+}
+
+static void add_af_mpv_alloc(void *parent, char *ma)
+{
+ char **p = talloc(parent, char*);
+ *p = ma;
+ talloc_set_destructor(p, destruct_af_mpv_alloc);
+}
+
+
+// Perform the equivalent of mpv_free_node_contents(node) when tmp is freed.
+static void steal_node_allocations(void *tmp, mpv_node *node)
+{
+ talloc_steal(tmp, node_get_alloc(node));
+}
+
+// lua_Alloc compatible. Serves only to track memory usage. This wraps the
+// existing allocator, partly because luajit requires the use of its internal
+// allocator on 64-bit platforms.
+static void *mp_lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize)
+{
+ struct script_ctx *ctx = ud;
+
+ // Ah, what the fuck, screw whoever introduced this to Lua 5.2.
+ if (!ptr)
+ osize = 0;
+
+ ptr = ctx->lua_allocf(ctx->lua_alloc_ud, ptr, osize, nsize);
+ if (nsize && !ptr)
+ return NULL; // allocation failed, so original memory left untouched
+
+ ctx->lua_malloc_size = ctx->lua_malloc_size - osize + nsize;
+ stats_size_value(ctx->stats, "mem", ctx->lua_malloc_size);
+
+ return ptr;
+}
+
+static struct script_ctx *get_ctx(lua_State *L)
+{
+ lua_getfield(L, LUA_REGISTRYINDEX, "ctx");
+ struct script_ctx *ctx = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ assert(ctx);
+ return ctx;
+}
+
+static struct MPContext *get_mpctx(lua_State *L)
+{
+ return get_ctx(L)->mpctx;
+}
+
+static int error_handler(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ if (luaL_loadstring(L, "return debug.traceback('', 3)") == 0) { // e fn|err
+ lua_call(L, 0, 1); // e backtrace
+ const char *tr = lua_tostring(L, -1);
+ MP_WARN(ctx, "%s\n", tr ? tr : "(unknown)");
+ }
+ lua_pop(L, 1); // e
+
+ return 1;
+}
+
+// Check client API error code:
+// if err >= 0, push "true" to the stack, and return 1
+// if err < 0, push nil and then the error string to the stack, and return 2
+static int check_error(lua_State *L, int err)
+{
+ if (err >= 0) {
+ lua_pushboolean(L, 1);
+ return 1;
+ }
+ lua_pushnil(L);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static void add_functions(struct script_ctx *ctx);
+
+static void load_file(lua_State *L, const char *fname)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ MP_DBG(ctx, "loading file %s\n", fname);
+ void *tmp = talloc_new(ctx);
+ // according to Lua manual chunkname should be '@' plus the filename
+ char *dispname = talloc_asprintf(tmp, "@%s", fname);
+ struct bstr s = stream_read_file(fname, tmp, ctx->mpctx->global, 100000000);
+ if (!s.start)
+ luaL_error(L, "Could not read file.\n");
+ if (luaL_loadbuffer(L, s.start, s.len, dispname))
+ lua_error(L);
+ lua_call(L, 0, 1);
+ talloc_free(tmp);
+}
+
+static int load_builtin(lua_State *L)
+{
+ const char *name = luaL_checkstring(L, 1);
+ char dispname[80];
+ snprintf(dispname, sizeof(dispname), "@%s", name);
+ for (int n = 0; builtin_lua_scripts[n][0]; n++) {
+ if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
+ const char *script = builtin_lua_scripts[n][1];
+ if (luaL_loadbuffer(L, script, strlen(script), dispname))
+ lua_error(L);
+ lua_call(L, 0, 1);
+ return 1;
+ }
+ }
+ luaL_error(L, "builtin module '%s' not found\n", name);
+ return 0;
+}
+
+// Execute "require " .. name
+static void require(lua_State *L, const char *name)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ MP_DBG(ctx, "loading %s\n", name);
+ // Lazy, but better than calling the "require" function manually
+ char buf[80];
+ snprintf(buf, sizeof(buf), "require '%s'", name);
+ if (luaL_loadstring(L, buf))
+ lua_error(L);
+ lua_call(L, 0, 0);
+}
+
+// Push the table of a module. If it doesn't exist, it's created.
+// The Lua script can call "require(module)" to "load" it.
+static void push_module_table(lua_State *L, const char *module)
+{
+ lua_getglobal(L, "package"); // package
+ lua_getfield(L, -1, "loaded"); // package loaded
+ lua_remove(L, -2); // loaded
+ lua_getfield(L, -1, module); // loaded module
+ if (lua_isnil(L, -1)) {
+ lua_pop(L, 1); // loaded
+ lua_newtable(L); // loaded module
+ lua_pushvalue(L, -1); // loaded module module
+ lua_setfield(L, -3, module); // loaded module
+ }
+ lua_remove(L, -2); // module
+}
+
+static int load_scripts(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *fname = ctx->filename;
+
+ require(L, "mp.defaults");
+
+ if (fname[0] == '@') {
+ require(L, fname);
+ } else {
+ load_file(L, fname);
+ }
+
+ lua_getglobal(L, "mp_event_loop"); // fn
+ if (lua_isnil(L, -1))
+ luaL_error(L, "no event loop function\n");
+ lua_call(L, 0, 0); // -
+
+ return 0;
+}
+
+static void fuck_lua(lua_State *L, const char *search_path, const char *extra)
+{
+ void *tmp = talloc_new(NULL);
+
+ lua_getglobal(L, "package"); // package
+ lua_getfield(L, -1, search_path); // package search_path
+ bstr path = bstr0(lua_tostring(L, -1));
+ char *newpath = talloc_strdup(tmp, "");
+
+ // Script-directory paths take priority.
+ if (extra) {
+ newpath = talloc_asprintf_append(newpath, "%s%s",
+ newpath[0] ? ";" : "",
+ mp_path_join(tmp, extra, "?.lua"));
+ }
+
+ // Unbelievable but true: Lua loads .lua files AND dynamic libraries from
+ // the working directory. This is highly security relevant.
+ // Lua scripts are still supposed to load globally installed libraries, so
+ // try to get by by filtering out any relative paths.
+ while (path.len) {
+ bstr item;
+ bstr_split_tok(path, ";", &item, &path);
+ if (mp_path_is_absolute(item)) {
+ newpath = talloc_asprintf_append(newpath, "%s%.*s",
+ newpath[0] ? ";" : "",
+ BSTR_P(item));
+ }
+ }
+
+ lua_pushstring(L, newpath); // package search_path newpath
+ lua_setfield(L, -3, search_path); // package search_path
+ lua_pop(L, 2); // -
+
+ talloc_free(tmp);
+}
+
+static int run_lua(lua_State *L)
+{
+ struct script_ctx *ctx = lua_touserdata(L, -1);
+ lua_pop(L, 1); // -
+
+ luaL_openlibs(L);
+
+ // used by get_ctx()
+ lua_pushlightuserdata(L, ctx); // ctx
+ lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // -
+
+ add_functions(ctx); // mp
+
+ push_module_table(L, "mp"); // mp
+
+ // "mp" is available by default, and no "require 'mp'" is needed
+ lua_pushvalue(L, -1); // mp mp
+ lua_setglobal(L, "mp"); // mp
+
+ lua_pushstring(L, ctx->name); // mp name
+ lua_setfield(L, -2, "script_name"); // mp
+
+ // used by pushnode()
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE"); // mp table
+ lua_setfield(L, -2, "UNKNOWN_TYPE"); // mp
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "MAP"); // mp table
+ lua_setfield(L, -2, "MAP"); // mp
+ lua_newtable(L); // mp table
+ lua_pushvalue(L, -1); // mp table table
+ lua_setfield(L, LUA_REGISTRYINDEX, "ARRAY"); // mp table
+ lua_setfield(L, -2, "ARRAY"); // mp
+
+ lua_pop(L, 1); // -
+
+ assert(lua_gettop(L) == 0);
+
+ // Add a preloader for each builtin Lua module
+ lua_getglobal(L, "package"); // package
+ assert(lua_type(L, -1) == LUA_TTABLE);
+ lua_getfield(L, -1, "preload"); // package preload
+ assert(lua_type(L, -1) == LUA_TTABLE);
+ for (int n = 0; builtin_lua_scripts[n][0]; n++) {
+ lua_pushcfunction(L, load_builtin); // package preload load_builtin
+ lua_setfield(L, -2, builtin_lua_scripts[n][0]);
+ }
+ lua_pop(L, 2); // -
+
+ assert(lua_gettop(L) == 0);
+
+ fuck_lua(L, "path", ctx->path);
+ fuck_lua(L, "cpath", NULL);
+ assert(lua_gettop(L) == 0);
+
+ // run this under an error handler that can do backtraces
+ lua_pushcfunction(L, error_handler); // errf
+ lua_pushcfunction(L, load_scripts); // errf fn
+ if (lua_pcall(L, 0, 0, -2)) { // errf [error]
+ const char *e = lua_tostring(L, -1);
+ MP_FATAL(ctx, "Lua error: %s\n", e ? e : "(unknown)");
+ }
+
+ return 0;
+}
+
+static int load_lua(struct mp_script_args *args)
+{
+ int r = -1;
+
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .mpctx = args->mpctx,
+ .client = args->client,
+ .name = mpv_client_name(args->client),
+ .log = args->log,
+ .filename = args->filename,
+ .path = args->path,
+ .stats = stats_ctx_create(ctx, args->mpctx->global,
+ mp_tprintf(80, "script/%s", mpv_client_name(args->client))),
+ };
+
+ stats_register_thread_cputime(ctx->stats, "cpu");
+
+ if (LUA_VERSION_NUM != 501 && LUA_VERSION_NUM != 502) {
+ MP_FATAL(ctx, "Only Lua 5.1 and 5.2 are supported.\n");
+ goto error_out;
+ }
+
+ lua_State *L = ctx->state = luaL_newstate();
+ if (!L) {
+ MP_FATAL(ctx, "Could not initialize Lua.\n");
+ goto error_out;
+ }
+
+ // Wrap the internal allocator with our version that does accounting
+ ctx->lua_allocf = lua_getallocf(L, &ctx->lua_alloc_ud);
+ lua_setallocf(L, mp_lua_alloc, ctx);
+
+ if (mp_cpcall(L, run_lua, ctx)) {
+ const char *err = "unknown error";
+ if (lua_type(L, -1) == LUA_TSTRING) // avoid allocation
+ err = lua_tostring(L, -1);
+ MP_FATAL(ctx, "Lua error: %s\n", err);
+ goto error_out;
+ }
+
+ r = 0;
+
+error_out:
+ if (ctx->state)
+ lua_close(ctx->state);
+ talloc_free(ctx);
+ return r;
+}
+
+static int check_loglevel(lua_State *L, int arg)
+{
+ const char *level = luaL_checkstring(L, arg);
+ int n = mp_msg_find_level(level);
+ if (n >= 0)
+ return n;
+ luaL_error(L, "Invalid log level '%s'", level);
+ abort();
+}
+
+static int script_log(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ int msgl = check_loglevel(L, 1);
+
+ int last = lua_gettop(L);
+ lua_getglobal(L, "tostring"); // args... tostring
+ for (int i = 2; i <= last; i++) {
+ lua_pushvalue(L, -1); // args... tostring tostring
+ lua_pushvalue(L, i); // args... tostring tostring args[i]
+ lua_call(L, 1, 1); // args... tostring str
+ const char *s = lua_tostring(L, -1);
+ if (s == NULL)
+ return luaL_error(L, "Invalid argument");
+ mp_msg(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
+ lua_pop(L, 1); // args... tostring
+ }
+ mp_msg(ctx->log, msgl, "\n");
+
+ return 0;
+}
+
+static int script_find_config_file(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ const char *s = luaL_checkstring(L, 1);
+ char *path = mp_find_config_file(NULL, mpctx->global, s);
+ if (path) {
+ lua_pushstring(L, path);
+ } else {
+ lua_pushnil(L);
+ }
+ talloc_free(path);
+ return 1;
+}
+
+static int script_get_script_directory(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ if (ctx->path) {
+ lua_pushstring(L, ctx->path);
+ return 1;
+ }
+ return 0;
+}
+
+static void pushnode(lua_State *L, mpv_node *node);
+
+static int script_raw_wait_event(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ mpv_event *event = mpv_wait_event(ctx->client, luaL_optnumber(L, 1, 1e20));
+
+ struct mpv_node rn;
+ mpv_event_to_node(&rn, event);
+ steal_node_allocations(tmp, &rn);
+
+ pushnode(L, &rn); // event
+
+ // return event
+ return 1;
+}
+
+static int script_request_event(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *event = luaL_checkstring(L, 1);
+ bool enable = lua_toboolean(L, 2);
+ // brute force event name -> id; stops working for events > assumed max
+ int event_id = -1;
+ for (int n = 0; n < 256; n++) {
+ const char *name = mpv_event_name(n);
+ if (name && strcmp(name, event) == 0) {
+ event_id = n;
+ break;
+ }
+ }
+ lua_pushboolean(L, mpv_request_event(ctx->client, event_id, enable) >= 0);
+ return 1;
+}
+
+static int script_enable_messages(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *level = luaL_checkstring(L, 1);
+ int r = mpv_request_log_messages(ctx->client, level);
+ if (r == MPV_ERROR_INVALID_PARAMETER)
+ luaL_error(L, "Invalid log level '%s'", level);
+ return check_error(L, r);
+}
+
+static int script_command(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *s = luaL_checkstring(L, 1);
+
+ return check_error(L, mpv_command_string(ctx->client, s));
+}
+
+static int script_commandv(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ int num = lua_gettop(L);
+ const char *args[50];
+ if (num + 1 > MP_ARRAY_SIZE(args))
+ luaL_error(L, "too many arguments");
+ for (int n = 1; n <= num; n++) {
+ const char *s = lua_tostring(L, n);
+ if (!s)
+ luaL_error(L, "argument %d is not a string", n);
+ args[n - 1] = s;
+ }
+ args[num] = NULL;
+ return check_error(L, mpv_command(ctx->client, args));
+}
+
+static int script_del_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+
+ return check_error(L, mpv_del_property(ctx->client, p));
+}
+
+static int script_set_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ const char *v = luaL_checkstring(L, 2);
+
+ return check_error(L, mpv_set_property_string(ctx->client, p, v));
+}
+
+static int script_set_property_bool(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ int v = lua_toboolean(L, 2);
+
+ return check_error(L, mpv_set_property(ctx->client, p, MPV_FORMAT_FLAG, &v));
+}
+
+static bool is_int(double d)
+{
+ int64_t v = d;
+ return d == (double)v;
+}
+
+static int script_set_property_number(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ double d = luaL_checknumber(L, 2);
+ // If the number might be an integer, then set it as integer. The mpv core
+ // will (probably) convert INT64 to DOUBLE when setting, but not the other
+ // way around.
+ int res;
+ if (is_int(d)) {
+ res = mpv_set_property(ctx->client, p, MPV_FORMAT_INT64, &(int64_t){d});
+ } else {
+ res = mpv_set_property(ctx->client, p, MPV_FORMAT_DOUBLE, &d);
+ }
+ return check_error(L, res);
+}
+
+static void makenode(void *tmp, mpv_node *dst, lua_State *L, int t)
+{
+ luaL_checkstack(L, 6, "makenode");
+
+ if (t < 0)
+ t = lua_gettop(L) + (t + 1);
+ switch (lua_type(L, t)) {
+ case LUA_TNIL:
+ dst->format = MPV_FORMAT_NONE;
+ break;
+ case LUA_TNUMBER: {
+ double d = lua_tonumber(L, t);
+ if (is_int(d)) {
+ dst->format = MPV_FORMAT_INT64;
+ dst->u.int64 = d;
+ } else {
+ dst->format = MPV_FORMAT_DOUBLE;
+ dst->u.double_ = d;
+ }
+ break;
+ }
+ case LUA_TBOOLEAN:
+ dst->format = MPV_FORMAT_FLAG;
+ dst->u.flag = !!lua_toboolean(L, t);
+ break;
+ case LUA_TSTRING: {
+ size_t len = 0;
+ char *s = (char *)lua_tolstring(L, t, &len);
+ bool has_zeros = !!memchr(s, 0, len);
+ if (has_zeros) {
+ mpv_byte_array *ba = talloc_zero(tmp, mpv_byte_array);
+ *ba = (mpv_byte_array){talloc_memdup(tmp, s, len), len};
+ dst->format = MPV_FORMAT_BYTE_ARRAY;
+ dst->u.ba = ba;
+ } else {
+ dst->format = MPV_FORMAT_STRING;
+ dst->u.string = talloc_strdup(tmp, s);
+ }
+ break;
+ }
+ case LUA_TTABLE: {
+ // Lua uses the same type for arrays and maps, so guess the correct one.
+ int format = MPV_FORMAT_NONE;
+ if (lua_getmetatable(L, t)) { // mt
+ lua_getfield(L, -1, "type"); // mt val
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ const char *type = lua_tostring(L, -1);
+ if (strcmp(type, "MAP") == 0) {
+ format = MPV_FORMAT_NODE_MAP;
+ } else if (strcmp(type, "ARRAY") == 0) {
+ format = MPV_FORMAT_NODE_ARRAY;
+ }
+ }
+ lua_pop(L, 2);
+ }
+ if (format == MPV_FORMAT_NONE) {
+ // If all keys are integers, and they're in sequence, take it
+ // as an array.
+ int count = 0;
+ for (int n = 1; ; n++) {
+ lua_pushinteger(L, n); // n
+ lua_gettable(L, t); // t[n]
+ bool empty = lua_isnil(L, -1); // t[n]
+ lua_pop(L, 1); // -
+ if (empty) {
+ count = n - 1;
+ break;
+ }
+ }
+ if (count > 0)
+ format = MPV_FORMAT_NODE_ARRAY;
+ lua_pushnil(L); // nil
+ while (lua_next(L, t) != 0) { // key value
+ count--;
+ lua_pop(L, 1); // key
+ if (count < 0) {
+ lua_pop(L, 1); // -
+ format = MPV_FORMAT_NODE_MAP;
+ break;
+ }
+ }
+ }
+ if (format == MPV_FORMAT_NONE)
+ format = MPV_FORMAT_NODE_ARRAY; // probably empty table; assume array
+ mpv_node_list *list = talloc_zero(tmp, mpv_node_list);
+ dst->format = format;
+ dst->u.list = list;
+ if (format == MPV_FORMAT_NODE_ARRAY) {
+ for (int n = 0; ; n++) {
+ lua_pushinteger(L, n + 1); // n1
+ lua_gettable(L, t); // t[n1]
+ if (lua_isnil(L, -1))
+ break;
+ MP_TARRAY_GROW(tmp, list->values, list->num);
+ makenode(tmp, &list->values[n], L, -1);
+ list->num++;
+ lua_pop(L, 1); // -
+ }
+ lua_pop(L, 1); // -
+ } else {
+ lua_pushnil(L); // nil
+ while (lua_next(L, t) != 0) { // key value
+ MP_TARRAY_GROW(tmp, list->values, list->num);
+ MP_TARRAY_GROW(tmp, list->keys, list->num);
+ makenode(tmp, &list->values[list->num], L, -1);
+ if (lua_type(L, -2) != LUA_TSTRING) {
+ luaL_error(L, "key must be a string, but got %s",
+ lua_typename(L, lua_type(L, -2)));
+ }
+ list->keys[list->num] = talloc_strdup(tmp, lua_tostring(L, -2));
+ list->num++;
+ lua_pop(L, 1); // key
+ }
+ }
+ break;
+ }
+ default:
+ // unknown type
+ luaL_error(L, "disallowed Lua type found: %s\n", lua_typename(L, t));
+ }
+}
+
+static int script_set_property_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *p = luaL_checkstring(L, 1);
+ struct mpv_node node;
+ makenode(tmp, &node, L, 2);
+ int res = mpv_set_property(ctx->client, p, MPV_FORMAT_NODE, &node);
+ return check_error(L, res);
+
+}
+
+static int script_get_property_base(lua_State *L, void *tmp, int is_osd)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+ int type = is_osd ? MPV_FORMAT_OSD_STRING : MPV_FORMAT_STRING;
+
+ char *result = NULL;
+ int err = mpv_get_property(ctx->client, name, type, &result);
+ if (err >= 0) {
+ add_af_mpv_alloc(tmp, result);
+ lua_pushstring(L, result);
+ return 1;
+ } else {
+ if (lua_isnoneornil(L, 2) && type == MPV_FORMAT_OSD_STRING) {
+ lua_pushstring(L, "");
+ } else {
+ lua_pushvalue(L, 2);
+ }
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static int script_get_property(lua_State *L, void *tmp)
+{
+ return script_get_property_base(L, tmp, 0);
+}
+
+static int script_get_property_osd(lua_State *L, void *tmp)
+{
+ return script_get_property_base(L, tmp, 1);
+}
+
+static int script_get_property_bool(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+
+ int result = 0;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_FLAG, &result);
+ if (err >= 0) {
+ lua_pushboolean(L, !!result);
+ return 1;
+ } else {
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static int script_get_property_number(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+
+ // Note: the mpv core will (hopefully) convert INT64 to DOUBLE
+ double result = 0;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_DOUBLE, &result);
+ if (err >= 0) {
+ lua_pushnumber(L, result);
+ return 1;
+ } else {
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+ }
+}
+
+static void pushnode(lua_State *L, mpv_node *node)
+{
+ luaL_checkstack(L, 6, "pushnode");
+
+ switch (node->format) {
+ case MPV_FORMAT_STRING:
+ lua_pushstring(L, node->u.string);
+ break;
+ case MPV_FORMAT_INT64:
+ lua_pushnumber(L, node->u.int64);
+ break;
+ case MPV_FORMAT_DOUBLE:
+ lua_pushnumber(L, node->u.double_);
+ break;
+ case MPV_FORMAT_NONE:
+ lua_pushnil(L);
+ break;
+ case MPV_FORMAT_FLAG:
+ lua_pushboolean(L, node->u.flag);
+ break;
+ case MPV_FORMAT_NODE_ARRAY:
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "ARRAY"); // table mt
+ lua_setmetatable(L, -2); // table
+ for (int n = 0; n < node->u.list->num; n++) {
+ pushnode(L, &node->u.list->values[n]); // table value
+ lua_rawseti(L, -2, n + 1); // table
+ }
+ break;
+ case MPV_FORMAT_NODE_MAP:
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "MAP"); // table mt
+ lua_setmetatable(L, -2); // table
+ for (int n = 0; n < node->u.list->num; n++) {
+ lua_pushstring(L, node->u.list->keys[n]); // table key
+ pushnode(L, &node->u.list->values[n]); // table key value
+ lua_rawset(L, -3);
+ }
+ break;
+ case MPV_FORMAT_BYTE_ARRAY:
+ lua_pushlstring(L, node->u.ba->data, node->u.ba->size);
+ break;
+ default:
+ // unknown value - what do we do?
+ // for now, set a unique dummy value
+ lua_newtable(L); // table
+ lua_getfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE");
+ lua_setmetatable(L, -2); // table
+ break;
+ }
+}
+
+static int script_get_property_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ const char *name = luaL_checkstring(L, 1);
+ mp_lua_optarg(L, 2);
+
+ mpv_node node;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_NODE, &node);
+ if (err >= 0) {
+ steal_node_allocations(tmp, &node);
+ pushnode(L, &node);
+ return 1;
+ }
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static mpv_format check_property_format(lua_State *L, int arg)
+{
+ if (lua_isnil(L, arg))
+ return MPV_FORMAT_NONE;
+ const char *fmts[] = {"none", "native", "bool", "string", "number", NULL};
+ switch (luaL_checkoption(L, arg, "none", fmts)) {
+ case 0: return MPV_FORMAT_NONE;
+ case 1: return MPV_FORMAT_NODE;
+ case 2: return MPV_FORMAT_FLAG;
+ case 3: return MPV_FORMAT_STRING;
+ case 4: return MPV_FORMAT_DOUBLE;
+ }
+ abort();
+}
+
+// It has a raw_ prefix, because there is a more high level API in defaults.lua.
+static int script_raw_observe_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ mpv_format format = check_property_format(L, 3);
+ return check_error(L, mpv_observe_property(ctx->client, id, name, format));
+}
+
+static int script_raw_unobserve_property(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ lua_pushnumber(L, mpv_unobserve_property(ctx->client, id));
+ return 1;
+}
+
+static int script_command_native(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ mp_lua_optarg(L, 2);
+ struct mpv_node node;
+ struct mpv_node result;
+ makenode(tmp, &node, L, 1);
+ int err = mpv_command_node(ctx->client, &node, &result);
+ if (err >= 0) {
+ steal_node_allocations(tmp, &result);
+ pushnode(L, &result);
+ return 1;
+ }
+ lua_pushvalue(L, 2);
+ lua_pushstring(L, mpv_error_string(err));
+ return 2;
+}
+
+static int script_raw_command_native_async(lua_State *L, void *tmp)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ struct mpv_node node;
+ makenode(tmp, &node, L, 2);
+ int res = mpv_command_node_async(ctx->client, id, &node);
+ return check_error(L, res);
+}
+
+static int script_raw_abort_async_command(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t id = luaL_checknumber(L, 1);
+ mpv_abort_async_command(ctx->client, id);
+ return 0;
+}
+
+static int script_get_time(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_pushnumber(L, mpv_get_time_us(ctx->client) / (double)(1000 * 1000));
+ return 1;
+}
+
+static int script_input_set_section_mouse_area(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+
+ char *section = (char *)luaL_checkstring(L, 1);
+ int x0 = luaL_checkinteger(L, 2);
+ int y0 = luaL_checkinteger(L, 3);
+ int x1 = luaL_checkinteger(L, 4);
+ int y1 = luaL_checkinteger(L, 5);
+ mp_input_set_section_mouse_area(mpctx->input, section, x0, y0, x1, y1);
+ return 0;
+}
+
+static int script_format_time(lua_State *L)
+{
+ double t = luaL_checknumber(L, 1);
+ const char *fmt = luaL_optstring(L, 2, "%H:%M:%S");
+ char *r = mp_format_time_fmt(fmt, t);
+ if (!r)
+ luaL_error(L, "Invalid time format string '%s'", fmt);
+ lua_pushstring(L, r);
+ talloc_free(r);
+ return 1;
+}
+
+static int script_get_wakeup_pipe(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_pushinteger(L, mpv_get_wakeup_pipe(ctx->client));
+ return 1;
+}
+
+static int script_raw_hook_add(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ uint64_t ud = luaL_checkinteger(L, 1);
+ const char *name = luaL_checkstring(L, 2);
+ int pri = luaL_checkinteger(L, 3);
+ return check_error(L, mpv_hook_add(ctx->client, ud, name, pri));
+}
+
+static int script_raw_hook_continue(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+ lua_Integer id = luaL_checkinteger(L, 1);
+ return check_error(L, mpv_hook_continue(ctx->client, id));
+}
+
+static int script_readdir(lua_State *L, void *tmp)
+{
+ // 0 1 2 3
+ const char *fmts[] = {"all", "files", "dirs", "normal", NULL};
+ const char *path = luaL_checkstring(L, 1);
+ int t = luaL_checkoption(L, 2, "normal", fmts);
+ DIR *dir = opendir(path);
+ if (!dir) {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ return 2;
+ }
+ add_af_dir(tmp, dir);
+ lua_newtable(L); // list
+ char *fullpath = talloc_strdup(tmp, "");
+ struct dirent *e;
+ int n = 0;
+ while ((e = readdir(dir))) {
+ char *name = e->d_name;
+ if (t) {
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
+ continue;
+ if (fullpath)
+ fullpath[0] = '\0';
+ fullpath = talloc_asprintf_append(fullpath, "%s/%s", path, name);
+ struct stat st;
+ if (stat(fullpath, &st))
+ continue;
+ if (!(((t & 1) && S_ISREG(st.st_mode)) ||
+ ((t & 2) && S_ISDIR(st.st_mode))))
+ continue;
+ }
+ lua_pushinteger(L, ++n); // list index
+ lua_pushstring(L, name); // list index name
+ lua_settable(L, -3); // list
+ }
+ return 1;
+}
+
+static int script_file_info(lua_State *L)
+{
+ const char *path = luaL_checkstring(L, 1);
+
+ struct stat statbuf;
+ if (stat(path, &statbuf) != 0) {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ return 2;
+ }
+
+ lua_newtable(L); // Result stat table
+
+ const char * stat_names[] = {
+ "mode", "size",
+ "atime", "mtime", "ctime", NULL
+ };
+ const lua_Number stat_values[] = {
+ statbuf.st_mode,
+ statbuf.st_size,
+ statbuf.st_atime,
+ statbuf.st_mtime,
+ statbuf.st_ctime
+ };
+
+ // Add all fields
+ for (int i = 0; stat_names[i]; i++) {
+ lua_pushnumber(L, stat_values[i]);
+ lua_setfield(L, -2, stat_names[i]);
+ }
+
+ // Convenience booleans
+ lua_pushboolean(L, S_ISREG(statbuf.st_mode));
+ lua_setfield(L, -2, "is_file");
+
+ lua_pushboolean(L, S_ISDIR(statbuf.st_mode));
+ lua_setfield(L, -2, "is_dir");
+
+ // Return table
+ return 1;
+}
+
+static int script_split_path(lua_State *L)
+{
+ const char *p = luaL_checkstring(L, 1);
+ bstr fname = mp_dirname(p);
+ lua_pushlstring(L, fname.start, fname.len);
+ lua_pushstring(L, mp_basename(p));
+ return 2;
+}
+
+static int script_join_path(lua_State *L, void *tmp)
+{
+ const char *p1 = luaL_checkstring(L, 1);
+ const char *p2 = luaL_checkstring(L, 2);
+ char *r = mp_path_join(tmp, p1, p2);
+ lua_pushstring(L, r);
+ return 1;
+}
+
+static int script_parse_json(lua_State *L, void *tmp)
+{
+ mp_lua_optarg(L, 2);
+ char *text = talloc_strdup(tmp, luaL_checkstring(L, 1));
+ bool trail = lua_toboolean(L, 2);
+ bool ok = false;
+ struct mpv_node node;
+ if (json_parse(tmp, &node, &text, MAX_JSON_DEPTH) >= 0) {
+ json_skip_whitespace(&text);
+ ok = !text[0] || trail;
+ }
+ if (ok) {
+ pushnode(L, &node);
+ lua_pushnil(L);
+ } else {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ }
+ lua_pushstring(L, text);
+ return 3;
+}
+
+static int script_format_json(lua_State *L, void *tmp)
+{
+ struct mpv_node node;
+ makenode(tmp, &node, L, 1);
+ char *dst = talloc_strdup(tmp, "");
+ if (json_write(&dst, &node) >= 0) {
+ lua_pushstring(L, dst);
+ lua_pushnil(L);
+ } else {
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
+ }
+ return 2;
+}
+
+static int script_get_env_list(lua_State *L)
+{
+ lua_newtable(L); // table
+ for (int n = 0; environ && environ[n]; n++) {
+ lua_pushstring(L, environ[n]); // table str
+ lua_rawseti(L, -2, n + 1); // table
+ }
+ return 1;
+}
+
+#define FN_ENTRY(name) {#name, script_ ## name, 0}
+#define AF_ENTRY(name) {#name, 0, script_ ## name}
+struct fn_entry {
+ const char *name;
+ int (*fn)(lua_State *L); // lua_CFunction
+ int (*af)(lua_State *L, void *); // af_CFunction
+};
+
+static const struct fn_entry main_fns[] = {
+ FN_ENTRY(log),
+ AF_ENTRY(raw_wait_event),
+ FN_ENTRY(request_event),
+ FN_ENTRY(find_config_file),
+ FN_ENTRY(get_script_directory),
+ FN_ENTRY(command),
+ FN_ENTRY(commandv),
+ AF_ENTRY(command_native),
+ AF_ENTRY(raw_command_native_async),
+ FN_ENTRY(raw_abort_async_command),
+ AF_ENTRY(get_property),
+ AF_ENTRY(get_property_osd),
+ FN_ENTRY(get_property_bool),
+ FN_ENTRY(get_property_number),
+ AF_ENTRY(get_property_native),
+ FN_ENTRY(del_property),
+ FN_ENTRY(set_property),
+ FN_ENTRY(set_property_bool),
+ FN_ENTRY(set_property_number),
+ AF_ENTRY(set_property_native),
+ FN_ENTRY(raw_observe_property),
+ FN_ENTRY(raw_unobserve_property),
+ FN_ENTRY(get_time),
+ FN_ENTRY(input_set_section_mouse_area),
+ FN_ENTRY(format_time),
+ FN_ENTRY(enable_messages),
+ FN_ENTRY(get_wakeup_pipe),
+ FN_ENTRY(raw_hook_add),
+ FN_ENTRY(raw_hook_continue),
+ {0}
+};
+
+static const struct fn_entry utils_fns[] = {
+ AF_ENTRY(readdir),
+ FN_ENTRY(file_info),
+ FN_ENTRY(split_path),
+ AF_ENTRY(join_path),
+ AF_ENTRY(parse_json),
+ AF_ENTRY(format_json),
+ FN_ENTRY(get_env_list),
+ {0}
+};
+
+typedef struct autofree_data {
+ af_CFunction target;
+ void *ctx;
+} autofree_data;
+
+/* runs the target autofree script_* function with the ctx argument */
+static int script_autofree_call(lua_State *L)
+{
+ // n*args &data
+ autofree_data *data = lua_touserdata(L, -1);
+ lua_pop(L, 1); // n*args
+ assert(data && data->target && data->ctx);
+ return data->target(L, data->ctx);
+}
+
+static int script_autofree_trampoline(lua_State *L)
+{
+ // n*args
+ autofree_data data = {
+ .target = lua_touserdata(L, lua_upvalueindex(2)), // fn
+ .ctx = NULL,
+ };
+ assert(data.target);
+
+ lua_pushvalue(L, lua_upvalueindex(1)); // n*args autofree_call (closure)
+ lua_insert(L, 1); // autofree_call n*args
+ lua_pushlightuserdata(L, &data); // autofree_call n*args &data
+
+ data.ctx = talloc_new(NULL);
+ int r = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); // m*retvals
+ talloc_free(data.ctx);
+
+ if (r)
+ lua_error(L);
+
+ return lua_gettop(L); // m (retvals)
+}
+
+static void af_pushcclosure(lua_State *L, af_CFunction fn, int n)
+{
+ // Instead of pushing a direct closure of fn with n upvalues, we push an
+ // autofree_trampoline closure with two upvalues:
+ // 1: autofree_call closure with the n upvalues given here.
+ // 2: fn
+ //
+ // when called the autofree_trampoline closure will pcall the autofree_call
+ // closure with the current lua call arguments and an additional argument
+ // which holds ctx and fn. the autofree_call closure (with the n upvalues
+ // given here) calls fn directly and provides it with the ctx C argument,
+ // so that fn sees the exact n upvalues and lua call arguments as intended,
+ // wrapped with ctx init/cleanup.
+
+ lua_pushcclosure(L, script_autofree_call, n);
+ lua_pushlightuserdata(L, fn);
+ lua_pushcclosure(L, script_autofree_trampoline, 2);
+}
+
+static void register_package_fns(lua_State *L, char *module,
+ const struct fn_entry *e)
+{
+ push_module_table(L, module); // modtable
+ for (int n = 0; e[n].name; n++) {
+ if (e[n].af) {
+ af_pushcclosure(L, e[n].af, 0); // modtable fn
+ } else {
+ lua_pushcclosure(L, e[n].fn, 0); // modtable fn
+ }
+ lua_setfield(L, -2, e[n].name); // modtable
+ }
+ lua_pop(L, 1); // -
+}
+
+static void add_functions(struct script_ctx *ctx)
+{
+ lua_State *L = ctx->state;
+
+ register_package_fns(L, "mp", main_fns);
+ register_package_fns(L, "mp.utils", utils_fns);
+}
+
+const struct mp_scripting mp_scripting_lua = {
+ .name = "lua",
+ .file_ext = "lua",
+ .load = load_lua,
+};
diff --git a/player/lua/assdraw.lua b/player/lua/assdraw.lua
new file mode 100644
index 0000000..06079d5
--- /dev/null
+++ b/player/lua/assdraw.lua
@@ -0,0 +1,160 @@
+local ass_mt = {}
+ass_mt.__index = ass_mt
+local c = 0.551915024494 -- circle approximation
+
+local function ass_new()
+ return setmetatable({ scale = 4, text = "" }, ass_mt)
+end
+
+function ass_mt.new_event(ass)
+ -- osd_libass.c adds an event per line
+ if #ass.text > 0 then
+ ass.text = ass.text .. "\n"
+ end
+end
+
+function ass_mt.draw_start(ass)
+ ass.text = string.format("%s{\\p%d}", ass.text, ass.scale)
+end
+
+function ass_mt.draw_stop(ass)
+ ass.text = ass.text .. "{\\p0}"
+end
+
+function ass_mt.coord(ass, x, y)
+ local scale = 2 ^ (ass.scale - 1)
+ local ix = math.ceil(x * scale)
+ local iy = math.ceil(y * scale)
+ ass.text = string.format("%s %d %d", ass.text, ix, iy)
+end
+
+function ass_mt.append(ass, s)
+ ass.text = ass.text .. s
+end
+
+function ass_mt.merge(ass1, ass2)
+ ass1.text = ass1.text .. ass2.text
+end
+
+function ass_mt.pos(ass, x, y)
+ ass:append(string.format("{\\pos(%f,%f)}", x, y))
+end
+
+function ass_mt.an(ass, an)
+ ass:append(string.format("{\\an%d}", an))
+end
+
+function ass_mt.move_to(ass, x, y)
+ ass:append(" m")
+ ass:coord(x, y)
+end
+
+function ass_mt.line_to(ass, x, y)
+ ass:append(" l")
+ ass:coord(x, y)
+end
+
+function ass_mt.bezier_curve(ass, x1, y1, x2, y2, x3, y3)
+ ass:append(" b")
+ ass:coord(x1, y1)
+ ass:coord(x2, y2)
+ ass:coord(x3, y3)
+end
+
+
+function ass_mt.rect_ccw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x0, y1)
+ ass:line_to(x1, y1)
+ ass:line_to(x1, y0)
+end
+
+function ass_mt.rect_cw(ass, x0, y0, x1, y1)
+ ass:move_to(x0, y0)
+ ass:line_to(x1, y0)
+ ass:line_to(x1, y1)
+ ass:line_to(x0, y1)
+end
+
+function ass_mt.hexagon_cw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ ass:move_to(x0 + r1, y0)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y0)
+ end
+ ass:line_to(x1, y0 + r2)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y1)
+ end
+ ass:line_to(x0 + r1, y1)
+ ass:line_to(x0, y0 + r1)
+end
+
+function ass_mt.hexagon_ccw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ ass:move_to(x0 + r1, y0)
+ ass:line_to(x0, y0 + r1)
+ ass:line_to(x0 + r1, y1)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y1)
+ end
+ ass:line_to(x1, y0 + r2)
+ if x0 ~= x1 then
+ ass:line_to(x1 - r2, y0)
+ end
+end
+
+function ass_mt.round_rect_cw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ local c1 = c * r1 -- circle approximation
+ local c2 = c * r2 -- circle approximation
+ ass:move_to(x0 + r1, y0)
+ ass:line_to(x1 - r2, y0) -- top line
+ if r2 > 0 then
+ ass:bezier_curve(x1 - r2 + c2, y0, x1, y0 + r2 - c2, x1, y0 + r2) -- top right corner
+ end
+ ass:line_to(x1, y1 - r2) -- right line
+ if r2 > 0 then
+ ass:bezier_curve(x1, y1 - r2 + c2, x1 - r2 + c2, y1, x1 - r2, y1) -- bottom right corner
+ end
+ ass:line_to(x0 + r1, y1) -- bottom line
+ if r1 > 0 then
+ ass:bezier_curve(x0 + r1 - c1, y1, x0, y1 - r1 + c1, x0, y1 - r1) -- bottom left corner
+ end
+ ass:line_to(x0, y0 + r1) -- left line
+ if r1 > 0 then
+ ass:bezier_curve(x0, y0 + r1 - c1, x0 + r1 - c1, y0, x0 + r1, y0) -- top left corner
+ end
+end
+
+function ass_mt.round_rect_ccw(ass, x0, y0, x1, y1, r1, r2)
+ if r2 == nil then
+ r2 = r1
+ end
+ local c1 = c * r1 -- circle approximation
+ local c2 = c * r2 -- circle approximation
+ ass:move_to(x0 + r1, y0)
+ if r1 > 0 then
+ ass:bezier_curve(x0 + r1 - c1, y0, x0, y0 + r1 - c1, x0, y0 + r1) -- top left corner
+ end
+ ass:line_to(x0, y1 - r1) -- left line
+ if r1 > 0 then
+ ass:bezier_curve(x0, y1 - r1 + c1, x0 + r1 - c1, y1, x0 + r1, y1) -- bottom left corner
+ end
+ ass:line_to(x1 - r2, y1) -- bottom line
+ if r2 > 0 then
+ ass:bezier_curve(x1 - r2 + c2, y1, x1, y1 - r2 + c2, x1, y1 - r2) -- bottom right corner
+ end
+ ass:line_to(x1, y0 + r2) -- right line
+ if r2 > 0 then
+ ass:bezier_curve(x1, y0 + r2 - c2, x1 - r2 + c2, y0, x1 - r2, y0) -- top right corner
+ end
+end
+
+return {ass_new = ass_new}
diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua
new file mode 100644
index 0000000..9dca878
--- /dev/null
+++ b/player/lua/auto_profiles.lua
@@ -0,0 +1,198 @@
+-- Note: anything global is accessible by profile condition expressions.
+
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+
+local profiles = {}
+local watched_properties = {} -- indexed by property name (used as a set)
+local cached_properties = {} -- property name -> last known raw value
+local properties_to_profiles = {} -- property name -> set of profiles using it
+local have_dirty_profiles = false -- at least one profile is marked dirty
+local pending_hooks = {} -- as set (keys only, meaningless values)
+
+-- Used during evaluation of the profile condition, and should contain the
+-- profile the condition is evaluated for.
+local current_profile = nil
+
+-- Cached set of all top-level mpv properities. Only used for extra validation.
+local property_set = {}
+for _, property in pairs(mp.get_property_native("property-list")) do
+ property_set[property] = true
+end
+
+local function evaluate(profile)
+ msg.verbose("Re-evaluating auto profile " .. profile.name)
+
+ current_profile = profile
+ local status, res = pcall(profile.cond)
+ current_profile = nil
+
+ if not status then
+ -- errors can be "normal", e.g. in case properties are unavailable
+ msg.verbose("Profile condition error on evaluating: " .. res)
+ res = false
+ end
+ res = not not res
+ if res ~= profile.status then
+ if res == true then
+ msg.info("Applying auto profile: " .. profile.name)
+ mp.commandv("apply-profile", profile.name)
+ elseif profile.status == true and profile.has_restore_opt then
+ msg.info("Restoring profile: " .. profile.name)
+ mp.commandv("apply-profile", profile.name, "restore")
+ end
+ end
+ profile.status = res
+ profile.dirty = false
+end
+
+local function on_property_change(name, val)
+ cached_properties[name] = val
+ -- Mark all profiles reading this property as dirty, so they get re-evaluated
+ -- the next time the script goes back to sleep.
+ local dependent_profiles = properties_to_profiles[name]
+ if dependent_profiles then
+ for profile, _ in pairs(dependent_profiles) do
+ assert(profile.cond) -- must be a profile table
+ profile.dirty = true
+ have_dirty_profiles = true
+ end
+ end
+end
+
+local function on_idle()
+ -- When events and property notifications stop, re-evaluate all dirty profiles.
+ if have_dirty_profiles then
+ for _, profile in ipairs(profiles) do
+ if profile.dirty then
+ evaluate(profile)
+ end
+ end
+ end
+ have_dirty_profiles = false
+ -- Release all hooks (the point was to wait until an idle event)
+ while true do
+ local h = next(pending_hooks)
+ if not h then
+ break
+ end
+ pending_hooks[h] = nil
+ h:cont()
+ end
+end
+
+local function on_hook(h)
+ h:defer()
+ pending_hooks[h] = true
+end
+
+function get(name, default)
+ -- Normally, we use the cached value only
+ if not watched_properties[name] then
+ watched_properties[name] = true
+ local res, err = mp.get_property_native(name)
+ -- Property has to not exist and the toplevel of property in the name must also
+ -- not have an existing match in the property set for this to be considered an error.
+ -- This allows things like user-data/test to still work.
+ if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then
+ msg.error("Property '" .. name .. "' was not found.")
+ return default
+ end
+ cached_properties[name] = res
+ mp.observe_property(name, "native", on_property_change)
+ end
+ -- The first time the property is read we need add it to the
+ -- properties_to_profiles table, which will be used to mark the profile
+ -- dirty if a property referenced by it changes.
+ if current_profile then
+ local map = properties_to_profiles[name]
+ if not map then
+ map = {}
+ properties_to_profiles[name] = map
+ end
+ map[current_profile] = true
+ end
+ local val = cached_properties[name]
+ if val == nil then
+ val = default
+ end
+ return val
+end
+
+local function magic_get(name)
+ -- Lua identifiers can't contain "-", so in order to match with mpv
+ -- property conventions, replace "_" to "-"
+ name = string.gsub(name, "_", "-")
+ return get(name, nil)
+end
+
+local evil_magic = {}
+setmetatable(evil_magic, {
+ __index = function(table, key)
+ -- interpret everything as property, unless it already exists as
+ -- a non-nil global value
+ local v = _G[key]
+ if type(v) ~= "nil" then
+ return v
+ end
+ return magic_get(key)
+ end,
+})
+
+p = {}
+setmetatable(p, {
+ __index = function(table, key)
+ return magic_get(key)
+ end,
+})
+
+local function compile_cond(name, s)
+ local code, chunkname = "return " .. s, "profile " .. name .. " condition"
+ local chunk, err
+ if setfenv then -- lua 5.1
+ chunk, err = loadstring(code, chunkname)
+ if chunk then
+ setfenv(chunk, evil_magic)
+ end
+ else -- lua 5.2
+ chunk, err = load(code, chunkname, "t", evil_magic)
+ end
+ if not chunk then
+ msg.error("Profile '" .. name .. "' condition: " .. err)
+ chunk = function() return false end
+ end
+ return chunk
+end
+
+local function load_profiles()
+ for i, v in ipairs(mp.get_property_native("profile-list")) do
+ local cond = v["profile-cond"]
+ if cond and #cond > 0 then
+ local profile = {
+ name = v.name,
+ cond = compile_cond(v.name, cond),
+ properties = {},
+ status = nil,
+ dirty = true, -- need re-evaluate
+ has_restore_opt = v["profile-restore"] and v["profile-restore"] ~= "default"
+ }
+ profiles[#profiles + 1] = profile
+ have_dirty_profiles = true
+ end
+ end
+end
+
+load_profiles()
+
+if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
+ -- make it exit immediately
+ _G.mp_event_loop = function() end
+ return
+end
+
+mp.register_idle(on_idle)
+for _, name in ipairs({"on_load", "on_preloaded", "on_before_start_file"}) do
+ mp.add_hook(name, 50, on_hook)
+end
+
+on_idle() -- re-evaluate all profiles immediately
diff --git a/player/lua/console.lua b/player/lua/console.lua
new file mode 100644
index 0000000..44e9436
--- /dev/null
+++ b/player/lua/console.lua
@@ -0,0 +1,1204 @@
+-- Copyright (C) 2019 the mpv developers
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+local utils = require 'mp.utils'
+local assdraw = require 'mp.assdraw'
+
+-- Default options
+local opts = {
+ -- All drawing is scaled by this value, including the text borders and the
+ -- cursor. Change it if you have a high-DPI display.
+ scale = 1,
+ -- Set the font used for the REPL and the console.
+ -- This has to be a monospaced font.
+ font = "",
+ -- Set the font size used for the REPL and the console. This will be
+ -- multiplied by "scale".
+ font_size = 16,
+ border_size = 1,
+ -- Remove duplicate entries in history as to only keep the latest one.
+ history_dedup = true,
+ -- The ratio of font height to font width.
+ -- Adjusts table width of completion suggestions.
+ font_hw_ratio = 2.0,
+}
+
+function detect_platform()
+ local platform = mp.get_property_native('platform')
+ if platform == 'darwin' or platform == 'windows' then
+ return platform
+ elseif os.getenv('WAYLAND_DISPLAY') then
+ return 'wayland'
+ end
+ return 'x11'
+end
+
+-- Pick a better default font for Windows and macOS
+local platform = detect_platform()
+if platform == 'windows' then
+ opts.font = 'Consolas'
+elseif platform == 'darwin' then
+ opts.font = 'Menlo'
+else
+ opts.font = 'monospace'
+end
+
+-- Apply user-set options
+require 'mp.options'.read_options(opts)
+
+local styles = {
+ -- Colors are stolen from base16 Eighties by Chris Kempson
+ -- and converted to BGR as is required by ASS.
+ -- 2d2d2d 393939 515151 697374
+ -- 939fa0 c8d0d3 dfe6e8 ecf0f2
+ -- 7a77f2 5791f9 66ccff 99cc99
+ -- cccc66 cc9966 cc99cc 537bd2
+
+ debug = '{\\1c&Ha09f93&}',
+ verbose = '{\\1c&H99cc99&}',
+ warn = '{\\1c&H66ccff&}',
+ error = '{\\1c&H7a77f2&}',
+ fatal = '{\\1c&H5791f9&\\b1}',
+ suggestion = '{\\1c&Hcc99cc&}',
+}
+
+local repl_active = false
+local insert_mode = false
+local pending_update = false
+local line = ''
+local cursor = 1
+local history = {}
+local history_pos = 1
+local log_buffer = {}
+local suggestion_buffer = {}
+local key_bindings = {}
+local global_margins = { t = 0, b = 0 }
+
+local file_commands = {}
+local path_separator = platform == 'windows' and '\\' or '/'
+
+local update_timer = nil
+update_timer = mp.add_periodic_timer(0.05, function()
+ if pending_update then
+ update()
+ else
+ update_timer:kill()
+ end
+end)
+update_timer:kill()
+
+mp.observe_property("user-data/osc/margins", "native", function(_, val)
+ if val then
+ global_margins = val
+ else
+ global_margins = { t = 0, b = 0 }
+ end
+ update()
+end)
+
+-- Add a line to the log buffer (which is limited to 100 lines)
+function log_add(style, text)
+ log_buffer[#log_buffer + 1] = { style = style, text = text }
+ if #log_buffer > 100 then
+ table.remove(log_buffer, 1)
+ end
+
+ if repl_active then
+ if not update_timer:is_enabled() then
+ update()
+ update_timer:resume()
+ else
+ pending_update = true
+ end
+ end
+end
+
+-- Escape a string for verbatim display on the OSD
+function ass_escape(str)
+ -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
+ -- it isn't followed by a recognised character, so add a zero-width
+ -- non-breaking space
+ str = str:gsub('\\', '\\\239\187\191')
+ str = str:gsub('{', '\\{')
+ str = str:gsub('}', '\\}')
+ -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
+ -- consecutive newlines
+ str = str:gsub('\n', '\239\187\191\\N')
+ -- Turn leading spaces into hard spaces to prevent ASS from stripping them
+ str = str:gsub('\\N ', '\\N\\h')
+ str = str:gsub('^ ', '\\h')
+ return str
+end
+
+-- Takes a list of strings, a max width in characters and
+-- optionally a max row count.
+-- The result contains at least one column.
+-- Rows are cut off from the top if rows_max is specified.
+-- returns a string containing the formatted table and the row count
+function format_table(list, width_max, rows_max)
+ if #list == 0 then
+ return '', 0
+ end
+
+ local spaces_min = 2
+ local spaces_max = 8
+ local list_size = #list
+ local column_count = 1
+ local row_count = list_size
+ local column_widths
+ -- total width without spacing
+ local width_total = 0
+
+ local list_widths = {}
+ for i, item in ipairs(list) do
+ list_widths[i] = len_utf8(item)
+ end
+
+ -- use as many columns as possible
+ for columns = 2, list_size do
+ local rows_lower_bound = math.min(rows_max, math.ceil(list_size / columns))
+ local rows_upper_bound = math.min(rows_max, list_size, math.ceil(list_size / (columns - 1) - 1))
+ for rows = rows_upper_bound, rows_lower_bound, -1 do
+ cw = {}
+ width_total = 0
+
+ -- find out width of each column
+ for column = 1, columns do
+ local width = 0
+ for row = 1, rows do
+ local i = row + (column - 1) * rows
+ local item_width = list_widths[i]
+ if not item_width then break end
+ if width < item_width then
+ width = item_width
+ end
+ end
+ cw[column] = width
+ width_total = width_total + width
+ if width_total + (columns - 1) * spaces_min > width_max then
+ break
+ end
+ end
+
+ if width_total + (columns - 1) * spaces_min <= width_max then
+ row_count = rows
+ column_count = columns
+ column_widths = cw
+ else
+ break
+ end
+ end
+ if width_total + (columns - 1) * spaces_min > width_max then
+ break
+ end
+ end
+
+ local spaces = math.floor((width_max - width_total) / (column_count - 1))
+ spaces = math.max(spaces_min, math.min(spaces_max, spaces))
+ local spacing = column_count > 1 and string.format('%' .. spaces .. 's', ' ') or ''
+
+ local rows = {}
+ for row = 1, row_count do
+ local columns = {}
+ for column = 1, column_count do
+ local i = row + (column - 1) * row_count
+ if i > #list then break end
+ -- more then 99 leads to 'invalid format (width or precision too long)'
+ local format_string = column == column_count and '%s'
+ or '%-' .. math.min(column_widths[column], 99) .. 's'
+ columns[column] = string.format(format_string, list[i])
+ end
+ -- first row is at the bottom
+ rows[row_count - row + 1] = table.concat(columns, spacing)
+ end
+ return table.concat(rows, '\n'), row_count
+end
+
+local function print_to_terminal()
+ -- Clear the log after closing the console.
+ if not repl_active then
+ mp.osd_message('')
+ return
+ end
+
+ local log = ''
+ for _, log_line in ipairs(log_buffer) do
+ log = log .. log_line.text
+ end
+
+ local suggestions = table.concat(suggestion_buffer, '\t')
+ if suggestions ~= '' then
+ suggestions = suggestions .. '\n'
+ end
+
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+ -- Ensure there is a character with inverted colors to print.
+ if after_cur == '' then
+ after_cur = ' '
+ end
+
+ mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' ..
+ after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999)
+end
+
+-- Render the REPL and console as an ASS OSD
+function update()
+ pending_update = false
+
+ -- Print to the terminal when there is no VO. Check both vo-configured so
+ -- it works with --force-window --idle and no video tracks, and whether
+ -- there is a video track so that the condition doesn't become true while
+ -- switching VO at runtime, making mp.osd_message() print to the VO's OSD.
+ -- This issue does not happen when switching VO without any video track
+ -- regardless of the condition used.
+ if not mp.get_property_native('vo-configured')
+ and not mp.get_property('current-tracks/video') then
+ print_to_terminal()
+ return
+ end
+
+ local dpi_scale = mp.get_property_native("display-hidpi-scale", 1.0)
+
+ dpi_scale = dpi_scale * opts.scale
+
+ local screenx, screeny, aspect = mp.get_osd_size()
+ screenx = screenx / dpi_scale
+ screeny = screeny / dpi_scale
+
+ -- Clear the OSD if the REPL is not active
+ if not repl_active then
+ mp.set_osd_ass(screenx, screeny, '')
+ return
+ end
+
+ local coordinate_top = math.floor(global_margins.t * screeny + 0.5)
+ local clipping_coordinates = '0,' .. coordinate_top .. ',' ..
+ screenx .. ',' .. screeny
+ local ass = assdraw.ass_new()
+ local has_shadow = mp.get_property('osd-back-color'):sub(2, 3) == '00'
+ local style = '{\\r' ..
+ '\\1a&H00&\\3a&H00&\\1c&Heeeeee&\\3c&H111111&' ..
+ (has_shadow and '\\4a&H99&\\4c&H000000&' or '') ..
+ '\\fn' .. opts.font .. '\\fs' .. opts.font_size ..
+ '\\bord' .. opts.border_size .. '\\xshad0\\yshad1\\fsp0\\q1' ..
+ '\\clip(' .. clipping_coordinates .. ')}'
+ -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
+ -- inline with the surrounding text, but it sets the advance to the width
+ -- of the drawing. So the cursor doesn't affect layout too much, make it as
+ -- thin as possible and make it appear to be 1px wide by giving it 0.5px
+ -- horizontal borders.
+ local cheight = opts.font_size * 8
+ local cglyph = '{\\r' ..
+ '\\1a&H44&\\3a&H44&\\4a&H99&' ..
+ '\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
+ '\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
+ 'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
+ '{\\p0}'
+ local before_cur = ass_escape(line:sub(1, cursor - 1))
+ local after_cur = ass_escape(line:sub(cursor))
+
+ -- Render log messages as ASS.
+ -- This will render at most screeny / font_size - 1 messages.
+
+ -- lines above the prompt
+ -- subtract 1.5 to account for the input line
+ local screeny_factor = (1 - global_margins.t - global_margins.b)
+ local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5)
+ -- Estimate how many characters fit in one line
+ local width_max = math.ceil(screenx / opts.font_size * opts.font_hw_ratio)
+
+ local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max)
+ local suggestion_ass = style .. styles.suggestion .. ass_escape(suggestions)
+
+ local log_ass = ''
+ local log_messages = #log_buffer
+ local log_max_lines = math.max(0, lines_max - rows)
+ if log_max_lines < log_messages then
+ log_messages = log_max_lines
+ end
+ for i = #log_buffer - log_messages + 1, #log_buffer do
+ log_ass = log_ass .. style .. log_buffer[i].style .. ass_escape(log_buffer[i].text)
+ end
+
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:append(log_ass .. '\\N')
+ if #suggestions > 0 then
+ ass:append(suggestion_ass .. '\\N')
+ end
+ ass:append(style .. '> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. after_cur)
+
+ -- Redraw the cursor with the REPL text invisible. This will make the
+ -- cursor appear in front of the text.
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2 - global_margins.b * screeny)
+ ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
+
+ mp.set_osd_ass(screenx, screeny, ass.text)
+end
+
+-- Set the REPL visibility ("enable", Esc)
+function set_active(active)
+ if active == repl_active then return end
+ if active then
+ repl_active = true
+ insert_mode = false
+ mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging')
+ mp.enable_messages('terminal-default')
+ define_key_bindings()
+ else
+ repl_active = false
+ undefine_key_bindings()
+ mp.enable_messages('silent:terminal-default')
+ collectgarbage()
+ end
+ update()
+end
+
+-- Show the repl if hidden and replace its contents with 'text'
+-- (script-message-to repl type)
+function show_and_type(text, cursor_pos)
+ text = text or ''
+ cursor_pos = tonumber(cursor_pos)
+
+ -- Save the line currently being edited, just in case
+ if line ~= text and line ~= '' and history[#history] ~= line then
+ history_add(line)
+ end
+
+ line = text
+ if cursor_pos ~= nil and cursor_pos >= 1
+ and cursor_pos <= line:len() + 1 then
+ cursor = math.floor(cursor_pos)
+ else
+ cursor = line:len() + 1
+ end
+ history_pos = #history + 1
+ insert_mode = false
+ if repl_active then
+ update()
+ else
+ set_active(true)
+ end
+end
+
+-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
+-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
+function next_utf8(str, pos)
+ if pos > str:len() then return pos end
+ repeat
+ pos = pos + 1
+ until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+-- As above, but finds the previous UTF-8 character in 'str' before 'pos'
+function prev_utf8(str, pos)
+ if pos <= 1 then return pos end
+ repeat
+ pos = pos - 1
+ until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+function len_utf8(str)
+ local len = 0
+ local pos = 1
+ while pos <= str:len() do
+ pos = next_utf8(str, pos)
+ len = len + 1
+ end
+ return len
+end
+
+-- Insert a character at the current cursor position (any_unicode)
+function handle_char_input(c)
+ if insert_mode then
+ line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
+ else
+ line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
+ end
+ cursor = cursor + #c
+ suggestion_buffer = {}
+ update()
+end
+
+-- Remove the character behind the cursor (Backspace)
+function handle_backspace()
+ if cursor <= 1 then return end
+ local prev = prev_utf8(line, cursor)
+ line = line:sub(1, prev - 1) .. line:sub(cursor)
+ cursor = prev
+ suggestion_buffer = {}
+ update()
+end
+
+-- Remove the character in front of the cursor (Del)
+function handle_del()
+ if cursor > line:len() then return end
+ line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
+ suggestion_buffer = {}
+ update()
+end
+
+-- Toggle insert mode (Ins)
+function handle_ins()
+ insert_mode = not insert_mode
+end
+
+-- Move the cursor to the next character (Right)
+function next_char(amount)
+ cursor = next_utf8(line, cursor)
+ update()
+end
+
+-- Move the cursor to the previous character (Left)
+function prev_char(amount)
+ cursor = prev_utf8(line, cursor)
+ update()
+end
+
+-- Clear the current line (Ctrl+C)
+function clear()
+ line = ''
+ cursor = 1
+ insert_mode = false
+ history_pos = #history + 1
+ suggestion_buffer = {}
+ update()
+end
+
+-- Close the REPL if the current line is empty, otherwise delete the next
+-- character (Ctrl+D)
+function maybe_exit()
+ if line == '' then
+ set_active(false)
+ else
+ handle_del()
+ end
+end
+
+function help_command(param)
+ local cmdlist = mp.get_property_native('command-list')
+ table.sort(cmdlist, function(c1, c2)
+ return c1.name < c2.name
+ end)
+ local output = ''
+ if param == '' then
+ output = 'Available commands:\n'
+ for _, cmd in ipairs(cmdlist) do
+ output = output .. ' ' .. cmd.name
+ end
+ output = output .. '\n'
+ output = output .. 'Use "help command" to show information about a command.\n'
+ output = output .. "ESC or Ctrl+d exits the console.\n"
+ else
+ local cmd = nil
+ for _, curcmd in ipairs(cmdlist) do
+ if curcmd.name:find(param, 1, true) then
+ cmd = curcmd
+ if curcmd.name == param then
+ break -- exact match
+ end
+ end
+ end
+ if not cmd then
+ log_add(styles.error, 'No command matches "' .. param .. '"!')
+ return
+ end
+ output = output .. 'Command "' .. cmd.name .. '"\n'
+ for _, arg in ipairs(cmd.args) do
+ output = output .. ' ' .. arg.name .. ' (' .. arg.type .. ')'
+ if arg.optional then
+ output = output .. ' (optional)'
+ end
+ output = output .. '\n'
+ end
+ if cmd.vararg then
+ output = output .. 'This command supports variable arguments.\n'
+ end
+ end
+ log_add('', output)
+end
+
+-- Add a line to the history and deduplicate
+function history_add(text)
+ if opts.history_dedup then
+ -- More recent entries are more likely to be repeated
+ for i = #history, 1, -1 do
+ if history[i] == text then
+ table.remove(history, i)
+ break
+ end
+ end
+ end
+
+ history[#history + 1] = text
+end
+
+-- Run the current command and clear the line (Enter)
+function handle_enter()
+ if line == '' then
+ return
+ end
+ if history[#history] ~= line then
+ history_add(line)
+ end
+
+ -- match "help [<text>]", return <text> or "", strip all whitespace
+ local help = line:match('^%s*help%s+(.-)%s*$') or
+ (line:match('^%s*help$') and '')
+ if help then
+ help_command(help)
+ else
+ mp.command(line)
+ end
+
+ clear()
+end
+
+-- Go to the specified position in the command history
+function go_history(new_pos)
+ local old_pos = history_pos
+ history_pos = new_pos
+
+ -- Restrict the position to a legal value
+ if history_pos > #history + 1 then
+ history_pos = #history + 1
+ elseif history_pos < 1 then
+ history_pos = 1
+ end
+
+ -- Do nothing if the history position didn't actually change
+ if history_pos == old_pos then
+ return
+ end
+
+ -- If the user was editing a non-history line, save it as the last history
+ -- entry. This makes it much less frustrating to accidentally hit Up/Down
+ -- while editing a line.
+ if old_pos == #history + 1 and line ~= '' and history[#history] ~= line then
+ history_add(line)
+ end
+
+ -- Now show the history line (or a blank line for #history + 1)
+ if history_pos <= #history then
+ line = history[history_pos]
+ else
+ line = ''
+ end
+ cursor = line:len() + 1
+ insert_mode = false
+ update()
+end
+
+-- Go to the specified relative position in the command history (Up, Down)
+function move_history(amount)
+ go_history(history_pos + amount)
+end
+
+-- Go to the first command in the command history (PgUp)
+function handle_pgup()
+ go_history(1)
+end
+
+-- Stop browsing history and start editing a blank line (PgDown)
+function handle_pgdown()
+ go_history(#history + 1)
+end
+
+-- Move to the start of the current word, or if already at the start, the start
+-- of the previous word. (Ctrl+Left)
+function prev_word()
+ -- This is basically the same as next_word() but backwards, so reverse the
+ -- string in order to do a "backwards" find. This wouldn't be as annoying
+ -- to do if Lua didn't insist on 1-based indexing.
+ cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
+ update()
+end
+
+-- Move to the end of the current word, or if already at the end, the end of
+-- the next word. (Ctrl+Right)
+function next_word()
+ cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
+ update()
+end
+
+local function command_list()
+ local commands = {}
+ for i, command in ipairs(mp.get_property_native('command-list')) do
+ commands[i] = command.name
+ end
+
+ return commands
+end
+
+local function command_list_and_help()
+ local commands = command_list()
+ commands[#commands + 1] = 'help'
+
+ return commands
+end
+
+local function property_list()
+ local option_info = {
+ 'name', 'type', 'set-from-commandline', 'set-locally', 'default-value',
+ 'min', 'max', 'choices',
+ }
+
+ local properties = mp.get_property_native('property-list')
+
+ for _, option in ipairs(mp.get_property_native('options')) do
+ properties[#properties + 1] = 'options/' .. option
+ properties[#properties + 1] = 'file-local-options/' .. option
+ properties[#properties + 1] = 'option-info/' .. option
+
+ for _, sub_property in ipairs(option_info) do
+ properties[#properties + 1] = 'option-info/' .. option .. '/' ..
+ sub_property
+ end
+ end
+
+ return properties
+end
+
+local function profile_list()
+ local profiles = {}
+
+ for i, profile in ipairs(mp.get_property_native('profile-list')) do
+ profiles[i] = profile.name
+ end
+
+ return profiles
+end
+
+local function list_option_list()
+ local options = {}
+
+ -- Don't log errors for renamed and removed properties.
+ -- (Just mp.enable_messages('fatal') still logs them to the terminal.)
+ local msg_level_backup = mp.get_property('msg-level')
+ mp.set_property('msg-level', msg_level_backup == '' and 'cplayer=no'
+ or msg_level_backup .. ',cplayer=no')
+
+ for _, option in pairs(mp.get_property_native('options')) do
+ if mp.get_property('option-info/' .. option .. '/type', ''):find(' list$') then
+ options[#options + 1] = option
+ end
+ end
+
+ mp.set_property('msg-level', msg_level_backup)
+
+ return options
+end
+
+local function list_option_verb_list(option)
+ local type = mp.get_property('option-info/' .. option .. '/type')
+
+ if type == 'Key/value list' then
+ return {'add', 'append', 'set', 'remove'}
+ end
+
+ if type == 'String list' or type == 'Object settings list' then
+ return {'add', 'append', 'clr', 'pre', 'set', 'remove', 'toggle'}
+ end
+
+ return {}
+end
+
+local function choice_list(option)
+ local info = mp.get_property_native('option-info/' .. option, {})
+
+ if info.type == 'Flag' then
+ return { 'no', 'yes' }
+ end
+
+ return info.choices or {}
+end
+
+local function find_commands_with_file_argument()
+ if #file_commands > 0 then
+ return file_commands
+ end
+
+ for _, command in pairs(mp.get_property_native('command-list')) do
+ if command.args[1] and
+ (command.args[1].name == 'filename' or command.args[1].name == 'url') then
+ file_commands[#file_commands + 1] = command.name
+ end
+ end
+
+ return file_commands
+end
+
+local function file_list(directory)
+ if directory == '' then
+ directory = '.'
+ end
+
+ local files = utils.readdir(directory, 'files') or {}
+
+ for _, dir in pairs(utils.readdir(directory, 'dirs') or {}) do
+ files[#files + 1] = dir .. path_separator
+ end
+
+ return files
+end
+
+-- List of tab-completions:
+-- pattern: A Lua pattern used in string:match. It should return the start
+-- position of the word to be completed in the first capture (using
+-- the empty parenthesis notation "()"). In patterns with 2
+-- captures, the first determines the completions, and the second is
+-- the start of the word to be completed.
+-- list: A function that returns a list of candidate completion values.
+-- append: An extra string to be appended to the end of a successful
+-- completion. It is only appended if 'list' contains exactly one
+-- match.
+function build_completers()
+ local completers = {
+ { pattern = '^%s*()[%w_-]*$', list = command_list_and_help, append = ' ' },
+ { pattern = '^%s*help%s+()[%w_-]*$', list = command_list },
+ { pattern = '^%s*set%s+"?([%w_-]+)"?%s+()%S*$', list = choice_list },
+ { pattern = '^%s*set%s+"?([%w_-]+)"?%s+"()%S*$', list = choice_list, append = '"' },
+ { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+()%S*$', list = choice_list, append = " " },
+ { pattern = '^%s*cycle[-_]values%s+"?([%w_-]+)"?.-%s+"()%S*$', list = choice_list, append = '" ' },
+ { pattern = '^%s*apply[-_]profile%s+"()%S*$', list = profile_list, append = '"' },
+ { pattern = '^%s*apply[-_]profile%s+()%S*$', list = profile_list },
+ { pattern = '^%s*change[-_]list%s+()[%w_-]*$', list = list_option_list, append = ' ' },
+ { pattern = '^%s*change[-_]list%s+()"[%w_-]*$', list = list_option_list, append = '" ' },
+ { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+()%a*$', list = list_option_verb_list, append = ' ' },
+ { pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+"()%a*$', list = list_option_verb_list, append = '" ' },
+ { pattern = '^%s*([av]f)%s+()%a*$', list = list_option_verb_list, append = ' ' },
+ { pattern = '^%s*([av]f)%s+"()%a*$', list = list_option_verb_list, append = '" ' },
+ { pattern = '${=?()[%w_/-]*$', list = property_list, append = '}' },
+ }
+
+ for _, command in pairs({'set', 'add', 'cycle', 'cycle[-_]values', 'multiply'}) do
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command .. '%s+()[%w_/-]*$',
+ list = property_list,
+ append = ' ',
+ }
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command .. '%s+"()[%w_/-]*$',
+ list = property_list,
+ append = '" ',
+ }
+ end
+
+
+ for _, command in pairs(find_commands_with_file_argument()) do
+ completers[#completers + 1] = {
+ pattern = '^%s*' .. command:gsub('-', '[-_]') ..
+ '%s+["\']?(.-)()[^' .. path_separator ..']*$',
+ list = file_list,
+ -- Unfortunately appending " here would append it everytime a
+ -- directory is fully completed, even if you intend to browse it
+ -- afterwards.
+ }
+ end
+
+ return completers
+end
+
+-- Use 'list' to find possible tab-completions for 'part.'
+-- Returns a list of all potential completions and the longest
+-- common prefix of all the matching list items.
+function complete_match(part, list)
+ local completions = {}
+ local prefix = nil
+
+ for _, candidate in ipairs(list) do
+ if candidate:sub(1, part:len()) == part then
+ if prefix and prefix ~= candidate then
+ local prefix_len = part:len()
+ while prefix:sub(1, prefix_len + 1)
+ == candidate:sub(1, prefix_len + 1) do
+ prefix_len = prefix_len + 1
+ end
+ prefix = candidate:sub(1, prefix_len)
+ else
+ prefix = candidate
+ end
+ completions[#completions + 1] = candidate
+ end
+ end
+
+ return completions, prefix
+end
+
+function common_prefix_length(s1, s2)
+ local common_count = 0
+ for i = 1, #s1 do
+ if s1:byte(i) ~= s2:byte(i) then
+ break
+ end
+ common_count = common_count + 1
+ end
+ return common_count
+end
+
+function max_overlap_length(s1, s2)
+ for s1_offset = 0, #s1 - 1 do
+ local match = true
+ for i = 1, #s1 - s1_offset do
+ if s1:byte(s1_offset + i) ~= s2:byte(i) then
+ match = false
+ break
+ end
+ end
+ if match then
+ return #s1 - s1_offset
+ end
+ end
+ return 0
+end
+
+-- Complete the option or property at the cursor (TAB)
+function complete()
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ -- Try the first completer that works
+ for _, completer in ipairs(build_completers()) do
+ -- Completer patterns should return the start of the word to be
+ -- completed as the first capture.
+ local s, s2 = before_cur:match(completer.pattern)
+ if not s then
+ -- Multiple input commands can be separated by semicolons, so all
+ -- completions that are anchored at the start of the string with
+ -- '^' can start from a semicolon as well. Replace ^ with ; and try
+ -- to match again.
+ s, s2 = before_cur:match(completer.pattern:gsub('^^', ';'))
+ end
+ if s then
+ local hint
+ if s2 then
+ hint = s
+ s = s2
+ end
+
+ -- If the completer's pattern found a word, check the completer's
+ -- list for possible completions
+ local part = before_cur:sub(s)
+ local completions, prefix = complete_match(part, completer.list(hint))
+ if #completions > 0 then
+ -- If there was only one full match from the list, add
+ -- completer.append to the final string. This is normally a
+ -- space or a quotation mark followed by a space.
+ local after_cur_index = 1
+ if #completions == 1 then
+ local append = completer.append or ''
+ prefix = prefix .. append
+
+ -- calculate offset into after_cur
+ local prefix_len = common_prefix_length(append, after_cur)
+ local overlap_size = max_overlap_length(append, after_cur)
+ after_cur_index = math.max(prefix_len, overlap_size) + 1
+ else
+ table.sort(completions)
+ suggestion_buffer = completions
+ end
+
+ -- Insert the completion and update
+ before_cur = before_cur:sub(1, s - 1) .. prefix
+ cursor = before_cur:len() + 1
+ line = before_cur .. after_cur:sub(after_cur_index)
+ update()
+ return
+ end
+ end
+ end
+end
+
+-- Move the cursor to the beginning of the line (HOME)
+function go_home()
+ cursor = 1
+ update()
+end
+
+-- Move the cursor to the end of the line (END)
+function go_end()
+ cursor = line:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
+function del_word()
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
+ line = before_cur .. after_cur
+ cursor = before_cur:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the end of the word (Ctrl+Del)
+function del_next_word()
+ if cursor > line:len() then return end
+
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
+ line = before_cur .. after_cur
+ update()
+end
+
+-- Delete from the cursor to the end of the line (Ctrl+K)
+function del_to_eol()
+ line = line:sub(1, cursor - 1)
+ update()
+end
+
+-- Delete from the cursor back to the start of the line (Ctrl+U)
+function del_to_start()
+ line = line:sub(cursor)
+ cursor = 1
+ update()
+end
+
+-- Empty the log buffer of all messages (Ctrl+L)
+function clear_log_buffer()
+ log_buffer = {}
+ update()
+end
+
+-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
+function get_clipboard(clip)
+ if platform == 'x11' then
+ local res = utils.subprocess({
+ args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'wayland' then
+ local res = utils.subprocess({
+ args = { 'wl-paste', clip and '-n' or '-np' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'windows' then
+ local res = utils.subprocess({
+ args = { 'powershell', '-NoProfile', '-Command', [[& {
+ Trap {
+ Write-Error -ErrorRecord $_
+ Exit 1
+ }
+
+ $clip = ""
+ if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
+ $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
+ } else {
+ Add-Type -AssemblyName PresentationCore
+ $clip = [Windows.Clipboard]::GetText()
+ }
+
+ $clip = $clip -Replace "`r",""
+ $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
+ [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
+ }]] },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'darwin' then
+ local res = utils.subprocess({
+ args = { 'pbpaste' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ end
+ return ''
+end
+
+-- Paste text from the window-system's clipboard. 'clip' determines whether the
+-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
+function paste(clip)
+ local text = get_clipboard(clip)
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+ line = before_cur .. text .. after_cur
+ cursor = cursor + text:len()
+ update()
+end
+
+-- List of input bindings. This is a weird mashup between common GUI text-input
+-- bindings and readline bindings.
+function get_bindings()
+ local bindings = {
+ { 'esc', function() set_active(false) end },
+ { 'ctrl+[', function() set_active(false) end },
+ { 'enter', handle_enter },
+ { 'kp_enter', handle_enter },
+ { 'shift+enter', function() handle_char_input('\n') end },
+ { 'ctrl+j', handle_enter },
+ { 'ctrl+m', handle_enter },
+ { 'bs', handle_backspace },
+ { 'shift+bs', handle_backspace },
+ { 'ctrl+h', handle_backspace },
+ { 'del', handle_del },
+ { 'shift+del', handle_del },
+ { 'ins', handle_ins },
+ { 'shift+ins', function() paste(false) end },
+ { 'mbtn_mid', function() paste(false) end },
+ { 'left', function() prev_char() end },
+ { 'ctrl+b', function() prev_char() end },
+ { 'right', function() next_char() end },
+ { 'ctrl+f', function() next_char() end },
+ { 'up', function() move_history(-1) end },
+ { 'ctrl+p', function() move_history(-1) end },
+ { 'wheel_up', function() move_history(-1) end },
+ { 'down', function() move_history(1) end },
+ { 'ctrl+n', function() move_history(1) end },
+ { 'wheel_down', function() move_history(1) end },
+ { 'wheel_left', function() end },
+ { 'wheel_right', function() end },
+ { 'ctrl+left', prev_word },
+ { 'alt+b', prev_word },
+ { 'ctrl+right', next_word },
+ { 'alt+f', next_word },
+ { 'tab', complete },
+ { 'ctrl+i', complete },
+ { 'ctrl+a', go_home },
+ { 'home', go_home },
+ { 'ctrl+e', go_end },
+ { 'end', go_end },
+ { 'pgup', handle_pgup },
+ { 'pgdwn', handle_pgdown },
+ { 'ctrl+c', clear },
+ { 'ctrl+d', maybe_exit },
+ { 'ctrl+k', del_to_eol },
+ { 'ctrl+l', clear_log_buffer },
+ { 'ctrl+u', del_to_start },
+ { 'ctrl+v', function() paste(true) end },
+ { 'meta+v', function() paste(true) end },
+ { 'ctrl+bs', del_word },
+ { 'ctrl+w', del_word },
+ { 'ctrl+del', del_next_word },
+ { 'alt+d', del_next_word },
+ { 'kp_dec', function() handle_char_input('.') end },
+ }
+
+ for i = 0, 9 do
+ bindings[#bindings + 1] =
+ {'kp' .. i, function() handle_char_input('' .. i) end}
+ end
+
+ return bindings
+end
+
+local function text_input(info)
+ if info.key_text and (info.event == "press" or info.event == "down"
+ or info.event == "repeat")
+ then
+ handle_char_input(info.key_text)
+ end
+end
+
+function define_key_bindings()
+ if #key_bindings > 0 then
+ return
+ end
+ for _, bind in ipairs(get_bindings()) do
+ -- Generate arbitrary name for removing the bindings later.
+ local name = "_console_" .. (#key_bindings + 1)
+ key_bindings[#key_bindings + 1] = name
+ mp.add_forced_key_binding(bind[1], name, bind[2], {repeatable = true})
+ end
+ mp.add_forced_key_binding("any_unicode", "_console_text", text_input,
+ {repeatable = true, complex = true})
+ key_bindings[#key_bindings + 1] = "_console_text"
+end
+
+function undefine_key_bindings()
+ for _, name in ipairs(key_bindings) do
+ mp.remove_key_binding(name)
+ end
+ key_bindings = {}
+end
+
+-- Add a global binding for enabling the REPL. While it's enabled, its bindings
+-- will take over and it can be closed with ESC.
+mp.add_key_binding(nil, 'enable', function()
+ set_active(true)
+end)
+
+-- Add a script-message to show the REPL and fill it with the provided text
+mp.register_script_message('type', function(text, cursor_pos)
+ show_and_type(text, cursor_pos)
+end)
+
+-- Redraw the REPL when the OSD size changes. This is needed because the
+-- PlayRes of the OSD will need to be adjusted.
+mp.observe_property('osd-width', 'native', update)
+mp.observe_property('osd-height', 'native', update)
+mp.observe_property('display-hidpi-scale', 'native', update)
+
+-- Enable log messages. In silent mode, mpv will queue log messages in a buffer
+-- until enable_messages is called again without the silent: prefix.
+mp.enable_messages('silent:terminal-default')
+
+mp.register_event('log-message', function(e)
+ -- Ignore log messages from the OSD because of paranoia, since writing them
+ -- to the OSD could generate more messages in an infinite loop.
+ if e.prefix:sub(1, 3) == 'osd' then return end
+
+ -- Ignore messages output by this script.
+ if e.prefix == mp.get_script_name() then return end
+
+ -- Ignore buffer overflow warning messages. Overflowed log messages would
+ -- have been offscreen anyway.
+ if e.prefix == 'overflow' then return end
+
+ -- Filter out trace-level log messages, even if the terminal-default log
+ -- level includes them. These aren't too useful for an on-screen display
+ -- without scrollback and they include messages that are generated from the
+ -- OSD display itself.
+ if e.level == 'trace' then return end
+
+ -- Use color for debug/v/warn/error/fatal messages.
+ local style = ''
+ if e.level == 'debug' then
+ style = styles.debug
+ elseif e.level == 'v' then
+ style = styles.verbose
+ elseif e.level == 'warn' then
+ style = styles.warn
+ elseif e.level == 'error' then
+ style = styles.error
+ elseif e.level == 'fatal' then
+ style = styles.fatal
+ end
+
+ log_add(style, '[' .. e.prefix .. '] ' .. e.text)
+end)
+
+collectgarbage()
diff --git a/player/lua/defaults.lua b/player/lua/defaults.lua
new file mode 100644
index 0000000..233d1d6
--- /dev/null
+++ b/player/lua/defaults.lua
@@ -0,0 +1,836 @@
+-- Compatibility shim for lua 5.2/5.3
+unpack = unpack or table.unpack
+
+-- these are used internally by lua.c
+mp.UNKNOWN_TYPE.info = "this value is inserted if the C type is not supported"
+mp.UNKNOWN_TYPE.type = "UNKNOWN_TYPE"
+
+mp.ARRAY.info = "native array"
+mp.ARRAY.type = "ARRAY"
+
+mp.MAP.info = "native map"
+mp.MAP.type = "MAP"
+
+function mp.get_script_name()
+ return mp.script_name
+end
+
+function mp.get_opt(key, def)
+ local opts = mp.get_property_native("options/script-opts")
+ local val = opts[key]
+ if val == nil then
+ val = def
+ end
+ return val
+end
+
+function mp.input_define_section(section, contents, flags)
+ if flags == nil or flags == "" then
+ flags = "default"
+ end
+ mp.commandv("define-section", section, contents, flags)
+end
+
+function mp.input_enable_section(section, flags)
+ if flags == nil then
+ flags = ""
+ end
+ mp.commandv("enable-section", section, flags)
+end
+
+function mp.input_disable_section(section)
+ mp.commandv("disable-section", section)
+end
+
+function mp.get_mouse_pos()
+ local m = mp.get_property_native("mouse-pos")
+ return m.x, m.y
+end
+
+-- For dispatching script-binding. This is sent as:
+-- script-message-to $script_name $binding_name $keystate
+-- The array is indexed by $binding_name, and has functions like this as value:
+-- fn($binding_name, $keystate)
+local dispatch_key_bindings = {}
+
+local message_id = 0
+local function reserve_binding()
+ message_id = message_id + 1
+ return "__keybinding" .. tostring(message_id)
+end
+
+local function dispatch_key_binding(name, state, key_name, key_text)
+ local fn = dispatch_key_bindings[name]
+ if fn then
+ fn(name, state, key_name, key_text)
+ end
+end
+
+-- "Old", deprecated API
+
+-- each script has its own section, so that they don't conflict
+local default_section = "input_dispatch_" .. mp.script_name
+
+-- Set the list of key bindings. These will override the user's bindings, so
+-- you should use this sparingly.
+-- A call to this function will remove all bindings previously set with this
+-- function. For example, set_key_bindings({}) would remove all script defined
+-- key bindings.
+-- Note: the bindings are not active by default. Use enable_key_bindings().
+--
+-- list is an array of key bindings, where each entry is an array as follow:
+-- {key, callback_press, callback_down, callback_up}
+-- key is the key string as used in input.conf, like "ctrl+a"
+--
+-- callback can be a string too, in which case the following will be added like
+-- an input.conf line: key .. " " .. callback
+-- (And callback_down is ignored.)
+function mp.set_key_bindings(list, section, flags)
+ local cfg = ""
+ for i = 1, #list do
+ local entry = list[i]
+ local key = entry[1]
+ local cb = entry[2]
+ local cb_down = entry[3]
+ local cb_up = entry[4]
+ if type(cb) ~= "string" then
+ local mangle = reserve_binding()
+ dispatch_key_bindings[mangle] = function(name, state)
+ local event = state:sub(1, 1)
+ local is_mouse = state:sub(2, 2) == "m"
+ local def = (is_mouse and "u") or "d"
+ if event == "r" then
+ return
+ end
+ if event == "p" and cb then
+ cb()
+ elseif event == "d" and cb_down then
+ cb_down()
+ elseif event == "u" and cb_up then
+ cb_up()
+ elseif event == def and cb then
+ cb()
+ end
+ end
+ cfg = cfg .. key .. " script-binding " ..
+ mp.script_name .. "/" .. mangle .. "\n"
+ else
+ cfg = cfg .. key .. " " .. cb .. "\n"
+ end
+ end
+ mp.input_define_section(section or default_section, cfg, flags)
+end
+
+function mp.enable_key_bindings(section, flags)
+ mp.input_enable_section(section or default_section, flags)
+end
+
+function mp.disable_key_bindings(section)
+ mp.input_disable_section(section or default_section)
+end
+
+function mp.set_mouse_area(x0, y0, x1, y1, section)
+ mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1)
+end
+
+-- "Newer" and more convenient API
+
+local key_bindings = {}
+local key_binding_counter = 0
+local key_bindings_dirty = false
+
+function mp.flush_keybindings()
+ if not key_bindings_dirty then
+ return
+ end
+ key_bindings_dirty = false
+
+ for i = 1, 2 do
+ local section, flags
+ local def = i == 1
+ if def then
+ section = "input_" .. mp.script_name
+ flags = "default"
+ else
+ section = "input_forced_" .. mp.script_name
+ flags = "force"
+ end
+ local bindings = {}
+ for k, v in pairs(key_bindings) do
+ if v.bind and v.forced ~= def then
+ bindings[#bindings + 1] = v
+ end
+ end
+ table.sort(bindings, function(a, b)
+ return a.priority < b.priority
+ end)
+ local cfg = ""
+ for _, v in ipairs(bindings) do
+ cfg = cfg .. v.bind .. "\n"
+ end
+ mp.input_define_section(section, cfg, flags)
+ -- TODO: remove the section if the script is stopped
+ mp.input_enable_section(section, "allow-hide-cursor+allow-vo-dragging")
+ end
+end
+
+local function add_binding(attrs, key, name, fn, rp)
+ if type(name) ~= "string" and name ~= nil then
+ rp = fn
+ fn = name
+ name = nil
+ end
+ rp = rp or ""
+ if name == nil then
+ name = reserve_binding()
+ end
+ local repeatable = rp == "repeatable" or rp["repeatable"]
+ if rp["forced"] then
+ attrs.forced = true
+ end
+ local key_cb, msg_cb
+ if not fn then
+ fn = function() end
+ end
+ if rp["complex"] then
+ local key_states = {
+ ["u"] = "up",
+ ["d"] = "down",
+ ["r"] = "repeat",
+ ["p"] = "press",
+ }
+ key_cb = function(name, state, key_name, key_text)
+ if key_text == "" then
+ key_text = nil
+ end
+ fn({
+ event = key_states[state:sub(1, 1)] or "unknown",
+ is_mouse = state:sub(2, 2) == "m",
+ key_name = key_name,
+ key_text = key_text,
+ })
+ end
+ msg_cb = function()
+ fn({event = "press", is_mouse = false})
+ end
+ else
+ key_cb = function(name, state)
+ -- Emulate the same semantics as input.c uses for most bindings:
+ -- For keyboard, "down" runs the command, "up" does nothing;
+ -- for mouse, "down" does nothing, "up" runs the command.
+ -- Also, key repeat triggers the binding again.
+ local event = state:sub(1, 1)
+ local is_mouse = state:sub(2, 2) == "m"
+ if event == "r" and not repeatable then
+ return
+ end
+ if is_mouse and (event == "u" or event == "p") then
+ fn()
+ elseif not is_mouse and (event == "d" or event == "r" or event == "p") then
+ fn()
+ end
+ end
+ msg_cb = fn
+ end
+ if key and #key > 0 then
+ attrs.bind = key .. " script-binding " .. mp.script_name .. "/" .. name
+ end
+ attrs.name = name
+ -- new bindings override old ones (but do not overwrite them)
+ key_binding_counter = key_binding_counter + 1
+ attrs.priority = key_binding_counter
+ key_bindings[name] = attrs
+ key_bindings_dirty = true
+ dispatch_key_bindings[name] = key_cb
+ mp.register_script_message(name, msg_cb)
+end
+
+function mp.add_key_binding(...)
+ add_binding({forced=false}, ...)
+end
+
+function mp.add_forced_key_binding(...)
+ add_binding({forced=true}, ...)
+end
+
+function mp.remove_key_binding(name)
+ key_bindings[name] = nil
+ dispatch_key_bindings[name] = nil
+ key_bindings_dirty = true
+ mp.unregister_script_message(name)
+end
+
+local timers = {}
+
+local timer_mt = {}
+timer_mt.__index = timer_mt
+
+function mp.add_timeout(seconds, cb, disabled)
+ local t = mp.add_periodic_timer(seconds, cb, disabled)
+ t.oneshot = true
+ return t
+end
+
+function mp.add_periodic_timer(seconds, cb, disabled)
+ local t = {
+ timeout = seconds,
+ cb = cb,
+ oneshot = false,
+ }
+ setmetatable(t, timer_mt)
+ if not disabled then
+ t:resume()
+ end
+ return t
+end
+
+function timer_mt.stop(t)
+ if timers[t] then
+ timers[t] = nil
+ t.next_deadline = t.next_deadline - mp.get_time()
+ end
+end
+
+function timer_mt.kill(t)
+ timers[t] = nil
+ t.next_deadline = nil
+end
+mp.cancel_timer = timer_mt.kill
+
+function timer_mt.resume(t)
+ if not timers[t] then
+ local timeout = t.next_deadline
+ if timeout == nil then
+ timeout = t.timeout
+ end
+ t.next_deadline = mp.get_time() + timeout
+ timers[t] = t
+ end
+end
+
+function timer_mt.is_enabled(t)
+ return timers[t] ~= nil
+end
+
+-- Return the timer that expires next.
+local function get_next_timer()
+ local best = nil
+ for t, _ in pairs(timers) do
+ if best == nil or t.next_deadline < best.next_deadline then
+ best = t
+ end
+ end
+ return best
+end
+
+function mp.get_next_timeout()
+ local timer = get_next_timer()
+ if not timer then
+ return
+ end
+ local now = mp.get_time()
+ return timer.next_deadline - now
+end
+
+-- Run timers that have met their deadline at the time of invocation.
+-- Return: time>0 in seconds till the next due timer, 0 if there are due timers
+-- (aborted to avoid infinite loop), or nil if no timers
+local function process_timers()
+ local t0 = nil
+ while true do
+ local timer = get_next_timer()
+ if not timer then
+ return
+ end
+ local now = mp.get_time()
+ local wait = timer.next_deadline - now
+ if wait > 0 then
+ return wait
+ else
+ if not t0 then
+ t0 = now -- first due callback: always executes, remember t0
+ elseif timer.next_deadline > t0 then
+ -- don't block forever with slow callbacks and endless timers.
+ -- we'll continue right after checking mpv events.
+ return 0
+ end
+
+ if timer.oneshot then
+ timer:kill()
+ else
+ timer.next_deadline = now + timer.timeout
+ end
+ timer.cb()
+ end
+ end
+end
+
+local messages = {}
+
+function mp.register_script_message(name, fn)
+ messages[name] = fn
+end
+
+function mp.unregister_script_message(name)
+ messages[name] = nil
+end
+
+local function message_dispatch(ev)
+ if #ev.args > 0 then
+ local handler = messages[ev.args[1]]
+ if handler then
+ handler(unpack(ev.args, 2))
+ end
+ end
+end
+
+local property_id = 0
+local properties = {}
+
+function mp.observe_property(name, t, cb)
+ local id = property_id + 1
+ property_id = id
+ properties[id] = cb
+ mp.raw_observe_property(id, name, t)
+end
+
+function mp.unobserve_property(cb)
+ for prop_id, prop_cb in pairs(properties) do
+ if cb == prop_cb then
+ properties[prop_id] = nil
+ mp.raw_unobserve_property(prop_id)
+ end
+ end
+end
+
+local function property_change(ev)
+ local prop = properties[ev.id]
+ if prop then
+ prop(ev.name, ev.data)
+ end
+end
+
+-- used by default event loop (mp_event_loop()) to decide when to quit
+mp.keep_running = true
+
+local event_handlers = {}
+
+function mp.register_event(name, cb)
+ local list = event_handlers[name]
+ if not list then
+ list = {}
+ event_handlers[name] = list
+ end
+ list[#list + 1] = cb
+ return mp.request_event(name, true)
+end
+
+function mp.unregister_event(cb)
+ for name, sub in pairs(event_handlers) do
+ local found = false
+ for i, e in ipairs(sub) do
+ if e == cb then
+ found = true
+ break
+ end
+ end
+ if found then
+ -- create a new array, just in case this function was called
+ -- from an event handler
+ local new = {}
+ for i = 1, #sub do
+ if sub[i] ~= cb then
+ new[#new + 1] = sub[i]
+ end
+ end
+ event_handlers[name] = new
+ if #new == 0 then
+ mp.request_event(name, false)
+ end
+ end
+ end
+end
+
+-- default handlers
+mp.register_event("shutdown", function() mp.keep_running = false end)
+mp.register_event("client-message", message_dispatch)
+mp.register_event("property-change", property_change)
+
+-- called before the event loop goes back to sleep
+local idle_handlers = {}
+
+function mp.register_idle(cb)
+ idle_handlers[#idle_handlers + 1] = cb
+end
+
+function mp.unregister_idle(cb)
+ local new = {}
+ for _, handler in ipairs(idle_handlers) do
+ if handler ~= cb then
+ new[#new + 1] = handler
+ end
+ end
+ idle_handlers = new
+end
+
+-- sent by "script-binding"
+mp.register_script_message("key-binding", dispatch_key_binding)
+
+mp.msg = {
+ log = mp.log,
+ fatal = function(...) return mp.log("fatal", ...) end,
+ error = function(...) return mp.log("error", ...) end,
+ warn = function(...) return mp.log("warn", ...) end,
+ info = function(...) return mp.log("info", ...) end,
+ verbose = function(...) return mp.log("v", ...) end,
+ debug = function(...) return mp.log("debug", ...) end,
+ trace = function(...) return mp.log("trace", ...) end,
+}
+
+_G.print = mp.msg.info
+
+package.loaded["mp"] = mp
+package.loaded["mp.msg"] = mp.msg
+
+function mp.wait_event(t)
+ local r = mp.raw_wait_event(t)
+ if r and r.file_error and not r.error then
+ -- compat; deprecated
+ r.error = r.file_error
+ end
+ return r
+end
+
+_G.mp_event_loop = function()
+ mp.dispatch_events(true)
+end
+
+local function call_event_handlers(e)
+ local handlers = event_handlers[e.event]
+ if handlers then
+ for _, handler in ipairs(handlers) do
+ handler(e)
+ end
+ end
+end
+
+mp.use_suspend = false
+
+local suspend_warned = false
+
+function mp.dispatch_events(allow_wait)
+ local more_events = true
+ if mp.use_suspend then
+ if not suspend_warned then
+ mp.msg.error("mp.use_suspend is now ignored.")
+ suspend_warned = true
+ end
+ end
+ while mp.keep_running do
+ local wait = 0
+ if not more_events then
+ wait = process_timers() or 1e20 -- infinity for all practical purposes
+ if wait ~= 0 then
+ local idle_called = nil
+ for _, handler in ipairs(idle_handlers) do
+ idle_called = true
+ handler()
+ end
+ if idle_called then
+ -- handlers don't complete in 0 time, and may modify timers
+ wait = mp.get_next_timeout() or 1e20
+ if wait < 0 then
+ wait = 0
+ end
+ end
+ end
+ if allow_wait ~= true then
+ return
+ end
+ end
+ local e = mp.wait_event(wait)
+ more_events = false
+ if e.event ~= "none" then
+ call_event_handlers(e)
+ more_events = true
+ end
+ end
+end
+
+mp.register_idle(mp.flush_keybindings)
+
+-- additional helpers
+
+function mp.osd_message(text, duration)
+ if not duration then
+ duration = "-1"
+ else
+ duration = tostring(math.floor(duration * 1000))
+ end
+ mp.commandv("show-text", text, duration)
+end
+
+local hook_table = {}
+
+local hook_mt = {}
+hook_mt.__index = hook_mt
+
+function hook_mt.cont(t)
+ if t._id == nil then
+ mp.msg.error("hook already continued")
+ else
+ mp.raw_hook_continue(t._id)
+ t._id = nil
+ end
+end
+
+function hook_mt.defer(t)
+ t._defer = true
+end
+
+mp.register_event("hook", function(ev)
+ local fn = hook_table[tonumber(ev.id)]
+ local hookobj = {
+ _id = ev.hook_id,
+ _defer = false,
+ }
+ setmetatable(hookobj, hook_mt)
+ if fn then
+ fn(hookobj)
+ end
+ if not hookobj._defer and hookobj._id ~= nil then
+ hookobj:cont()
+ end
+end)
+
+function mp.add_hook(name, pri, cb)
+ local id = #hook_table + 1
+ hook_table[id] = cb
+ -- The C API suggests using 0 for a neutral priority, but lua.rst suggests
+ -- 50 (?), so whatever.
+ mp.raw_hook_add(id, name, pri - 50)
+end
+
+local async_call_table = {}
+local async_next_id = 1
+
+function mp.command_native_async(node, cb)
+ local id = async_next_id
+ async_next_id = async_next_id + 1
+ cb = cb or function() end
+ local res, err = mp.raw_command_native_async(id, node)
+ if not res then
+ mp.add_timeout(0, function() cb(false, nil, err) end)
+ return res, err
+ end
+ local t = {cb = cb, id = id}
+ async_call_table[id] = t
+ return t
+end
+
+mp.register_event("command-reply", function(ev)
+ local id = tonumber(ev.id)
+ local t = async_call_table[id]
+ local cb = t.cb
+ t.id = nil
+ async_call_table[id] = nil
+ if ev.error then
+ cb(false, nil, ev.error)
+ else
+ cb(true, ev.result, nil)
+ end
+end)
+
+function mp.abort_async_command(t)
+ if t.id ~= nil then
+ mp.raw_abort_async_command(t.id)
+ end
+end
+
+local overlay_mt = {}
+overlay_mt.__index = overlay_mt
+local overlay_new_id = 0
+
+function mp.create_osd_overlay(format)
+ overlay_new_id = overlay_new_id + 1
+ local overlay = {
+ format = format,
+ id = overlay_new_id,
+ data = "",
+ res_x = 0,
+ res_y = 720,
+ }
+ setmetatable(overlay, overlay_mt)
+ return overlay
+end
+
+function overlay_mt.update(ov)
+ local cmd = {}
+ for k, v in pairs(ov) do
+ cmd[k] = v
+ end
+ cmd.name = "osd-overlay"
+ cmd.res_x = math.floor(cmd.res_x)
+ cmd.res_y = math.floor(cmd.res_y)
+ return mp.command_native(cmd)
+end
+
+function overlay_mt.remove(ov)
+ mp.command_native {
+ name = "osd-overlay",
+ id = ov.id,
+ format = "none",
+ data = "",
+ }
+end
+
+-- legacy API
+function mp.set_osd_ass(res_x, res_y, data)
+ if not mp._legacy_overlay then
+ mp._legacy_overlay = mp.create_osd_overlay("ass-events")
+ end
+ if mp._legacy_overlay.res_x ~= res_x or
+ mp._legacy_overlay.res_y ~= res_y or
+ mp._legacy_overlay.data ~= data
+ then
+ mp._legacy_overlay.res_x = res_x
+ mp._legacy_overlay.res_y = res_y
+ mp._legacy_overlay.data = data
+ mp._legacy_overlay:update()
+ end
+end
+
+function mp.get_osd_size()
+ local prop = mp.get_property_native("osd-dimensions")
+ return prop.w, prop.h, prop.aspect
+end
+
+function mp.get_osd_margins()
+ local prop = mp.get_property_native("osd-dimensions")
+ return prop.ml, prop.mt, prop.mr, prop.mb
+end
+
+local mp_utils = package.loaded["mp.utils"]
+
+function mp_utils.format_table(t, set)
+ if not set then
+ set = { [t] = true }
+ end
+ local res = "{"
+ -- pretty expensive but simple way to distinguish array and map parts of t
+ local keys = {}
+ local vals = {}
+ local arr = 0
+ for i = 1, #t do
+ if t[i] == nil then
+ break
+ end
+ keys[i] = i
+ vals[i] = t[i]
+ arr = i
+ end
+ for k, v in pairs(t) do
+ if not (type(k) == "number" and k >= 1 and k <= arr and keys[k]) then
+ keys[#keys + 1] = k
+ vals[#keys] = v
+ end
+ end
+ for i = 1, #keys do
+ if #res > 1 then
+ res = res .. ", "
+ end
+ if i > arr then
+ res = res .. mp_utils.to_string(keys[i], set) .. " = "
+ end
+ res = res .. mp_utils.to_string(vals[i], set)
+ end
+ res = res .. "}"
+ return res
+end
+
+function mp_utils.to_string(v, set)
+ if type(v) == "string" then
+ return "\"" .. v .. "\""
+ elseif type(v) == "table" then
+ if set then
+ if set[v] then
+ return "[cycle]"
+ end
+ set[v] = true
+ end
+ return mp_utils.format_table(v, set)
+ else
+ return tostring(v)
+ end
+end
+
+function mp_utils.getcwd()
+ return mp.get_property("working-directory")
+end
+
+function mp_utils.getpid()
+ return mp.get_property_number("pid")
+end
+
+function mp_utils.format_bytes_humanized(b)
+ local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}
+ local i = 1
+ while b >= 1024 do
+ b = b / 1024
+ i = i + 1
+ end
+ return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1))
+end
+
+function mp_utils.subprocess(t)
+ local cmd = {}
+ cmd.name = "subprocess"
+ cmd.capture_stdout = true
+ for k, v in pairs(t) do
+ if k == "cancellable" then
+ k = "playback_only"
+ elseif k == "max_size" then
+ k = "capture_size"
+ end
+ cmd[k] = v
+ end
+ local res, err = mp.command_native(cmd)
+ if res == nil then
+ -- an error usually happens only if parsing failed (or no args passed)
+ res = {error_string = err, status = -1}
+ end
+ if res.error_string ~= "" then
+ res.error = res.error_string
+ end
+ return res
+end
+
+function mp_utils.subprocess_detached(t)
+ mp.commandv("run", unpack(t.args))
+end
+
+function mp_utils.shared_script_property_set(name, value)
+ if value ~= nil then
+ -- no such thing as change-list with mpv_node, so build a string value
+ mp.commandv("change-list", "shared-script-properties", "append",
+ name .. "=" .. value)
+ else
+ mp.commandv("change-list", "shared-script-properties", "remove", name)
+ end
+end
+
+function mp_utils.shared_script_property_get(name)
+ local map = mp.get_property_native("shared-script-properties")
+ return map and map[name]
+end
+
+-- cb(name, value) on change and on init
+function mp_utils.shared_script_property_observe(name, cb)
+ -- it's _very_ wasteful to observe the mpv core "super" property for every
+ -- shared sub-property, but then again you shouldn't use this
+ mp.observe_property("shared-script-properties", "native", function(_, val)
+ cb(name, val and val[name])
+ end)
+end
+
+return {}
diff --git a/player/lua/meson.build b/player/lua/meson.build
new file mode 100644
index 0000000..362c87c
--- /dev/null
+++ b/player/lua/meson.build
@@ -0,0 +1,10 @@
+lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua',
+ 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua']
+foreach file: lua_files
+ lua_file = custom_target(file,
+ input: join_paths(source_root, 'player', 'lua', file),
+ output: file + '.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+ )
+ sources += lua_file
+endforeach
diff --git a/player/lua/options.lua b/player/lua/options.lua
new file mode 100644
index 0000000..b05b734
--- /dev/null
+++ b/player/lua/options.lua
@@ -0,0 +1,164 @@
+local msg = require 'mp.msg'
+
+-- converts val to type of desttypeval
+local function typeconv(desttypeval, val)
+ if type(desttypeval) == "boolean" then
+ if val == "yes" then
+ val = true
+ elseif val == "no" then
+ val = false
+ else
+ msg.error("Error: Can't convert '" .. val .. "' to boolean!")
+ val = nil
+ end
+ elseif type(desttypeval) == "number" then
+ if tonumber(val) ~= nil then
+ val = tonumber(val)
+ else
+ msg.error("Error: Can't convert '" .. val .. "' to number!")
+ val = nil
+ end
+ end
+ return val
+end
+
+-- performs a deep-copy of the given option value
+local function opt_copy(val)
+ return val -- no tables currently
+end
+
+-- compares the given option values for equality
+local function opt_equal(val1, val2)
+ return val1 == val2
+end
+
+-- performs a deep-copy of an entire option table
+local function opt_table_copy(opts)
+ local copy = {}
+ for key, value in pairs(opts) do
+ copy[key] = opt_copy(value)
+ end
+ return copy
+end
+
+
+local function read_options(options, identifier, on_update)
+ local option_types = opt_table_copy(options)
+ if identifier == nil then
+ identifier = mp.get_script_name()
+ end
+ msg.debug("reading options for " .. identifier)
+
+ -- read config file
+ local conffilename = "script-opts/" .. identifier .. ".conf"
+ local conffile = mp.find_config_file(conffilename)
+ if conffile == nil then
+ msg.debug(conffilename .. " not found.")
+ conffilename = "lua-settings/" .. identifier .. ".conf"
+ conffile = mp.find_config_file(conffilename)
+ if conffile then
+ msg.warn("lua-settings/ is deprecated, use directory script-opts/")
+ end
+ end
+ local f = conffile and io.open(conffile,"r")
+ if f == nil then
+ -- config not found
+ msg.debug(conffilename .. " not found.")
+ else
+ -- config exists, read values
+ msg.verbose("Opened config file " .. conffilename .. ".")
+ local linecounter = 1
+ for line in f:lines() do
+ if line:sub(#line) == "\r" then
+ line = line:sub(1, #line - 1)
+ end
+ if string.find(line, "#") == 1 then
+
+ else
+ local eqpos = string.find(line, "=")
+ if eqpos == nil then
+
+ else
+ local key = string.sub(line, 1, eqpos-1)
+ local val = string.sub(line, eqpos+1)
+
+ -- match found values with defaults
+ if option_types[key] == nil then
+ msg.warn(conffilename..":"..linecounter..
+ " unknown key '" .. key .. "', ignoring")
+ else
+ local convval = typeconv(option_types[key], val)
+ if convval == nil then
+ msg.error(conffilename..":"..linecounter..
+ " error converting value '" .. val ..
+ "' for key '" .. key .. "'")
+ else
+ options[key] = convval
+ end
+ end
+ end
+ end
+ linecounter = linecounter + 1
+ end
+ io.close(f)
+ end
+
+ --parse command-line options
+ local prefix = identifier.."-"
+ -- command line options are always applied on top of these
+ local conf_and_default_opts = opt_table_copy(options)
+
+ local function parse_opts(full, options)
+ for key, val in pairs(full) do
+ if string.find(key, prefix, 1, true) == 1 then
+ key = string.sub(key, string.len(prefix)+1)
+
+ -- match found values with defaults
+ if option_types[key] == nil then
+ msg.warn("script-opts: unknown key " .. key .. ", ignoring")
+ else
+ local convval = typeconv(option_types[key], val)
+ if convval == nil then
+ msg.error("script-opts: error converting value '" .. val ..
+ "' for key '" .. key .. "'")
+ else
+ options[key] = convval
+ end
+ end
+ end
+ end
+ end
+
+ --initial
+ parse_opts(mp.get_property_native("options/script-opts"), options)
+
+ --runtime updates
+ if on_update then
+ local last_opts = opt_table_copy(options)
+
+ mp.observe_property("options/script-opts", "native", function(name, val)
+ local new_opts = opt_table_copy(conf_and_default_opts)
+ parse_opts(val, new_opts)
+ local changelist = {}
+ for key, val in pairs(new_opts) do
+ if not opt_equal(last_opts[key], val) then
+ -- copy to user
+ options[key] = opt_copy(val)
+ changelist[key] = true
+ end
+ end
+ last_opts = new_opts
+ if next(changelist) ~= nil then
+ on_update(changelist)
+ end
+ end)
+ end
+
+end
+
+-- backwards compatibility with broken read_options export
+_G.read_options = read_options
+
+return {
+ read_options = read_options,
+}
diff --git a/player/lua/osc.lua b/player/lua/osc.lua
new file mode 100644
index 0000000..45a5d90
--- /dev/null
+++ b/player/lua/osc.lua
@@ -0,0 +1,2917 @@
+local assdraw = require 'mp.assdraw'
+local msg = require 'mp.msg'
+local opt = require 'mp.options'
+local utils = require 'mp.utils'
+
+--
+-- Parameters
+--
+-- default user option values
+-- do not touch, change them in osc.conf
+local user_opts = {
+ showwindowed = true, -- show OSC when windowed?
+ showfullscreen = true, -- show OSC when fullscreen?
+ idlescreen = true, -- show mpv logo on idle
+ scalewindowed = 1, -- scaling of the controller when windowed
+ scalefullscreen = 1, -- scaling of the controller when fullscreen
+ scaleforcedwindow = 2, -- scaling when rendered on a forced window
+ vidscale = true, -- scale the controller with the video?
+ valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom)
+ halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
+ barmargin = 0, -- vertical margin of top/bottombar
+ boxalpha = 80, -- alpha of the background box,
+ -- 0 (opaque) to 255 (fully transparent)
+ hidetimeout = 500, -- duration in ms until the OSC hides if no
+ -- mouse movement. enforced non-negative for the
+ -- user, but internally negative is "always-on".
+ fadeduration = 200, -- duration of fade out in ms, 0 = no fade
+ deadzonesize = 0.5, -- size of deadzone
+ minmousemove = 0, -- minimum amount of pixels the mouse has to
+ -- move between ticks to make the OSC show up
+ iamaprogrammer = false, -- use native mpv values and disable OSC
+ -- internal track list management (and some
+ -- functions that depend on it)
+ layout = "bottombar",
+ seekbarstyle = "bar", -- bar, diamond or knob
+ seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle
+ seekrangestyle = "inverted",-- bar, line, slider, inverted or none
+ seekrangeseparate = true, -- whether the seekranges overlay on the bar-style seekbar
+ seekrangealpha = 200, -- transparency of seekranges
+ seekbarkeyframes = true, -- use keyframes when dragging the seekbar
+ title = "${media-title}", -- string compatible with property-expansion
+ -- to be shown as OSC title
+ tooltipborder = 1, -- border of tooltip in bottom/topbar
+ timetotal = false, -- display total time instead of remaining time?
+ remaining_playtime = true, -- display the remaining time in playtime or video-time mode
+ -- playtime takes speed into account, whereas video-time doesn't
+ timems = false, -- display timecodes with milliseconds?
+ tcspace = 100, -- timecode spacing (compensate font size estimation)
+ visibility = "auto", -- only used at init to set visibility_mode(...)
+ boxmaxchars = 80, -- title crop threshold for box layout
+ boxvideo = false, -- apply osc_param.video_margins to video
+ windowcontrols = "auto", -- whether to show window controls
+ windowcontrols_alignment = "right", -- which side to show window controls on
+ greenandgrumpy = false, -- disable santa hat
+ livemarkers = true, -- update seekbar chapter markers on duration change
+ chapters_osd = true, -- whether to show chapters OSD on next/prev
+ playlist_osd = true, -- whether to show playlist OSD on next/prev
+ chapter_fmt = "Chapter: %s", -- chapter print format for seekbar-hover. "no" to disable
+ unicodeminus = false, -- whether to use the Unicode minus sign character
+}
+
+-- read options from config and command-line
+opt.read_options(user_opts, "osc", function(list) update_options(list) end)
+
+local osc_param = { -- calculated by osc_init()
+ playresy = 0, -- canvas size Y
+ playresx = 0, -- canvas size X
+ display_aspect = 1,
+ unscaled_y = 0,
+ areas = {},
+ video_margins = {
+ l = 0, r = 0, t = 0, b = 0, -- left/right/top/bottom
+ },
+}
+
+local osc_styles = {
+ bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
+ smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
+ smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" .. mp.get_property("options/osd-font") .. "}",
+ smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
+ topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
+
+ elementDown = "{\\1c&H999999}",
+ timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
+ vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+
+ topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
+ smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
+ timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs27}",
+ timePosBar = "{\\blur0\\bord".. user_opts.tooltipborder .."\\1c&HFFFFFF\\3c&H000000\\fs30}",
+ vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}",
+
+ wcButtons = "{\\1c&HFFFFFF\\fs24\\fnmpv-osd-symbols}",
+ wcTitle = "{\\1c&HFFFFFF\\fs24\\q2}",
+ wcBar = "{\\1c&H000000}",
+}
+
+-- internal states, do not touch
+local state = {
+ showtime, -- time of last invocation (last mouse move)
+ osc_visible = false,
+ anistart, -- time when the animation started
+ anitype, -- current type of animation
+ animation, -- current animation alpha
+ mouse_down_counter = 0, -- used for softrepeat
+ active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
+ active_event_source = nil, -- the "button" that issued the current event
+ rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
+ tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
+ mp_screen_sizeX, mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
+ initREQ = false, -- is a re-init request pending?
+ marginsREQ = false, -- is a margins update pending?
+ last_mouseX, last_mouseY, -- last mouse position, to detect significant mouse movement
+ mouse_in_window = false,
+ message_text,
+ message_hide_timer,
+ fullscreen = false,
+ tick_timer = nil,
+ tick_last_time = 0, -- when the last tick() was run
+ hide_timer = nil,
+ cache_state = nil,
+ idle = false,
+ enabled = true,
+ input_enabled = true,
+ showhide_enabled = false,
+ windowcontrols_buttons = false,
+ dmx_cache = 0,
+ using_video_margins = false,
+ border = true,
+ maximized = false,
+ osd = mp.create_osd_overlay("ass-events"),
+ chapter_list = {}, -- sorted by time
+}
+
+local window_control_box_width = 80
+local tick_delay = 0.03
+
+local is_december = os.date("*t").month == 12
+
+--
+-- Helperfunctions
+--
+
+function kill_animation()
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+end
+
+function set_osd(res_x, res_y, text, z)
+ if state.osd.res_x == res_x and
+ state.osd.res_y == res_y and
+ state.osd.data == text then
+ return
+ end
+ state.osd.res_x = res_x
+ state.osd.res_y = res_y
+ state.osd.data = text
+ state.osd.z = z
+ state.osd:update()
+end
+
+local margins_opts = {
+ {"l", "video-margin-ratio-left"},
+ {"r", "video-margin-ratio-right"},
+ {"t", "video-margin-ratio-top"},
+ {"b", "video-margin-ratio-bottom"},
+}
+
+-- scale factor for translating between real and virtual ASS coordinates
+function get_virt_scale_factor()
+ local w, h = mp.get_osd_size()
+ if w <= 0 or h <= 0 then
+ return 0, 0
+ end
+ return osc_param.playresx / w, osc_param.playresy / h
+end
+
+-- return mouse position in virtual ASS coordinates (playresx/y)
+function get_virt_mouse_pos()
+ if state.mouse_in_window then
+ local sx, sy = get_virt_scale_factor()
+ local x, y = mp.get_mouse_pos()
+ return x * sx, y * sy
+ else
+ return -1, -1
+ end
+end
+
+function set_virt_mouse_area(x0, y0, x1, y1, name)
+ local sx, sy = get_virt_scale_factor()
+ mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
+end
+
+function scale_value(x0, x1, y0, y1, val)
+ local m = (y1 - y0) / (x1 - x0)
+ local b = y0 - (m * x0)
+ return (m * val) + b
+end
+
+-- returns hitbox spanning coordinates (top left, bottom right corner)
+-- according to alignment
+function get_hitbox_coords(x, y, an, w, h)
+
+ local alignments = {
+ [1] = function () return x, y-h, x+w, y end,
+ [2] = function () return x-(w/2), y-h, x+(w/2), y end,
+ [3] = function () return x-w, y-h, x, y end,
+
+ [4] = function () return x, y-(h/2), x+w, y+(h/2) end,
+ [5] = function () return x-(w/2), y-(h/2), x+(w/2), y+(h/2) end,
+ [6] = function () return x-w, y-(h/2), x, y+(h/2) end,
+
+ [7] = function () return x, y, x+w, y+h end,
+ [8] = function () return x-(w/2), y, x+(w/2), y+h end,
+ [9] = function () return x-w, y, x, y+h end,
+ }
+
+ return alignments[an]()
+end
+
+function get_hitbox_coords_geo(geometry)
+ return get_hitbox_coords(geometry.x, geometry.y, geometry.an,
+ geometry.w, geometry.h)
+end
+
+function get_element_hitbox(element)
+ return element.hitbox.x1, element.hitbox.y1,
+ element.hitbox.x2, element.hitbox.y2
+end
+
+function mouse_hit(element)
+ return mouse_hit_coords(get_element_hitbox(element))
+end
+
+function mouse_hit_coords(bX1, bY1, bX2, bY2)
+ local mX, mY = get_virt_mouse_pos()
+ return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
+end
+
+function limit_range(min, max, val)
+ if val > max then
+ val = max
+ elseif val < min then
+ val = min
+ end
+ return val
+end
+
+-- translate value into element coordinates
+function get_slider_ele_pos_for(element, val)
+
+ local ele_pos = scale_value(
+ element.slider.min.value, element.slider.max.value,
+ element.slider.min.ele_pos, element.slider.max.ele_pos,
+ val)
+
+ return limit_range(
+ element.slider.min.ele_pos, element.slider.max.ele_pos,
+ ele_pos)
+end
+
+-- translates global (mouse) coordinates to value
+function get_slider_value_at(element, glob_pos)
+
+ local val = scale_value(
+ element.slider.min.glob_pos, element.slider.max.glob_pos,
+ element.slider.min.value, element.slider.max.value,
+ glob_pos)
+
+ return limit_range(
+ element.slider.min.value, element.slider.max.value,
+ val)
+end
+
+-- get value at current mouse position
+function get_slider_value(element)
+ return get_slider_value_at(element, get_virt_mouse_pos())
+end
+
+function countone(val)
+ if not user_opts.iamaprogrammer then
+ val = val + 1
+ end
+ return val
+end
+
+-- align: -1 .. +1
+-- frame: size of the containing area
+-- obj: size of the object that should be positioned inside the area
+-- margin: min. distance from object to frame (as long as -1 <= align <= +1)
+function get_align(align, frame, obj, margin)
+ return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
+end
+
+-- multiplies two alpha values, formular can probably be improved
+function mult_alpha(alphaA, alphaB)
+ return 255 - (((1-(alphaA/255)) * (1-(alphaB/255))) * 255)
+end
+
+function add_area(name, x1, y1, x2, y2)
+ -- create area if needed
+ if osc_param.areas[name] == nil then
+ osc_param.areas[name] = {}
+ end
+ table.insert(osc_param.areas[name], {x1=x1, y1=y1, x2=x2, y2=y2})
+end
+
+function ass_append_alpha(ass, alpha, modifier)
+ local ar = {}
+
+ for ai, av in pairs(alpha) do
+ av = mult_alpha(av, modifier)
+ if state.animation then
+ av = mult_alpha(av, state.animation)
+ end
+ ar[ai] = av
+ end
+
+ ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}",
+ ar[1], ar[2], ar[3], ar[4]))
+end
+
+function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
+ if hexagon then
+ ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
+ else
+ ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
+ end
+end
+
+function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
+ if hexagon then
+ ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
+ else
+ ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
+ end
+end
+
+
+--
+-- Tracklist Management
+--
+
+local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
+
+-- updates the OSC internal playlists, should be run each time the track-layout changes
+function update_tracklist()
+ local tracktable = mp.get_property_native("track-list", {})
+
+ -- by osc_id
+ tracks_osc = {}
+ tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
+ -- by mpv_id
+ tracks_mpv = {}
+ tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
+ for n = 1, #tracktable do
+ if tracktable[n].type ~= "unknown" then
+ local type = tracktable[n].type
+ local mpv_id = tonumber(tracktable[n].id)
+
+ -- by osc_id
+ table.insert(tracks_osc[type], tracktable[n])
+
+ -- by mpv_id
+ tracks_mpv[type][mpv_id] = tracktable[n]
+ tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
+ end
+ end
+end
+
+-- return a nice list of tracks of the given type (video, audio, sub)
+function get_tracklist(type)
+ local msg = "Available " .. nicetypes[type] .. " Tracks: "
+ if not tracks_osc or #tracks_osc[type] == 0 then
+ msg = msg .. "none"
+ else
+ for n = 1, #tracks_osc[type] do
+ local track = tracks_osc[type][n]
+ local lang, title, selected = "unknown", "", "○"
+ if track.lang ~= nil then lang = track.lang end
+ if track.title ~= nil then title = track.title end
+ if track.id == tonumber(mp.get_property(type)) then
+ selected = "●"
+ end
+ msg = msg.."\n"..selected.." "..n..": ["..lang.."] "..title
+ end
+ end
+ return msg
+end
+
+-- relatively change the track of given <type> by <next> tracks
+ --(+1 -> next, -1 -> previous)
+function set_track(type, next)
+ local current_track_mpv, current_track_osc
+ if mp.get_property(type) == "no" then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.get_property(type))
+ current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
+ end
+ local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
+ local new_track_mpv
+ if new_track_osc == 0 then
+ new_track_mpv = "no"
+ else
+ new_track_mpv = tracks_osc[type][new_track_osc].id
+ end
+
+ mp.commandv("set", type, new_track_mpv)
+
+ if new_track_osc == 0 then
+ show_message(nicetypes[type] .. " Track: none")
+ else
+ show_message(nicetypes[type] .. " Track: "
+ .. new_track_osc .. "/" .. #tracks_osc[type]
+ .. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
+ .. (tracks_osc[type][new_track_osc].title or ""))
+ end
+end
+
+-- get the currently selected track of <type>, OSC-style counted
+function get_track(type)
+ local track = mp.get_property(type)
+ if track ~= "no" and track ~= nil then
+ local tr = tracks_mpv[type][tonumber(track)]
+ if tr then
+ return tr.osc_id
+ end
+ end
+ return 0
+end
+
+-- WindowControl helpers
+function window_controls_enabled()
+ val = user_opts.windowcontrols
+ if val == "auto" then
+ return not state.border
+ else
+ return val ~= "no"
+ end
+end
+
+function window_controls_alignment()
+ return user_opts.windowcontrols_alignment
+end
+
+--
+-- Element Management
+--
+
+local elements = {}
+
+function prepare_elements()
+
+ -- remove elements without layout or invisible
+ local elements2 = {}
+ for n, element in pairs(elements) do
+ if element.layout ~= nil and element.visible then
+ table.insert(elements2, element)
+ end
+ end
+ elements = elements2
+
+ function elem_compare (a, b)
+ return a.layout.layer < b.layout.layer
+ end
+
+ table.sort(elements, elem_compare)
+
+
+ for _,element in pairs(elements) do
+
+ local elem_geo = element.layout.geometry
+
+ -- Calculate the hitbox
+ local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
+ element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
+
+ local style_ass = assdraw.ass_new()
+
+ -- prepare static elements
+ style_ass:append("{}") -- hack to troll new_event into inserting a \n
+ style_ass:new_event()
+ style_ass:pos(elem_geo.x, elem_geo.y)
+ style_ass:an(elem_geo.an)
+ style_ass:append(element.layout.style)
+
+ element.style_ass = style_ass
+
+ local static_ass = assdraw.ass_new()
+
+
+ if element.type == "box" then
+ --draw box
+ static_ass:draw_start()
+ ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
+ element.layout.box.radius, element.layout.box.hexagon)
+ static_ass:draw_stop()
+
+ elseif element.type == "slider" then
+ --draw static slider parts
+
+ local r1 = 0
+ local r2 = 0
+ local slider_lo = element.layout.slider
+ -- offset between element outline and drag-area
+ local foV = slider_lo.border + slider_lo.gap
+
+ -- calculate positions of min and max points
+ if slider_lo.stype ~= "bar" then
+ r1 = elem_geo.h / 2
+ element.slider.min.ele_pos = elem_geo.h / 2
+ element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
+ if slider_lo.stype == "diamond" then
+ r2 = (elem_geo.h - 2 * slider_lo.border) / 2
+ elseif slider_lo.stype == "knob" then
+ r2 = r1
+ end
+ else
+ element.slider.min.ele_pos =
+ slider_lo.border + slider_lo.gap
+ element.slider.max.ele_pos =
+ elem_geo.w - (slider_lo.border + slider_lo.gap)
+ end
+
+ element.slider.min.glob_pos =
+ element.hitbox.x1 + element.slider.min.ele_pos
+ element.slider.max.glob_pos =
+ element.hitbox.x1 + element.slider.max.ele_pos
+
+ -- -- --
+
+ static_ass:draw_start()
+
+ -- the box
+ ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1, slider_lo.stype == "diamond")
+
+ -- the "hole"
+ ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border,
+ elem_geo.w - slider_lo.border, elem_geo.h - slider_lo.border,
+ r2, slider_lo.stype == "diamond")
+
+ -- marker nibbles
+ if element.slider.markerF ~= nil and slider_lo.gap > 0 then
+ local markers = element.slider.markerF()
+ for _,marker in pairs(markers) do
+ if marker > element.slider.min.value and
+ marker < element.slider.max.value then
+
+ local s = get_slider_ele_pos_for(element, marker)
+
+ if slider_lo.gap > 1 then -- draw triangles
+
+ local a = slider_lo.gap / 0.5 --0.866
+
+ --top
+ if slider_lo.nibbles_top then
+ static_ass:move_to(s - (a / 2), slider_lo.border)
+ static_ass:line_to(s + (a / 2), slider_lo.border)
+ static_ass:line_to(s, foV)
+ end
+
+ --bottom
+ if slider_lo.nibbles_bottom then
+ static_ass:move_to(s - (a / 2),
+ elem_geo.h - slider_lo.border)
+ static_ass:line_to(s,
+ elem_geo.h - foV)
+ static_ass:line_to(s + (a / 2),
+ elem_geo.h - slider_lo.border)
+ end
+
+ else -- draw 2x1px nibbles
+
+ --top
+ if slider_lo.nibbles_top then
+ static_ass:rect_cw(s - 1, slider_lo.border,
+ s + 1, slider_lo.border + slider_lo.gap);
+ end
+
+ --bottom
+ if slider_lo.nibbles_bottom then
+ static_ass:rect_cw(s - 1,
+ elem_geo.h -slider_lo.border -slider_lo.gap,
+ s + 1, elem_geo.h - slider_lo.border);
+ end
+ end
+ end
+ end
+ end
+ end
+
+ element.static_ass = static_ass
+
+
+ -- if the element is supposed to be disabled,
+ -- style it accordingly and kill the eventresponders
+ if not element.enabled then
+ element.layout.alpha[1] = 136
+ element.eventresponder = nil
+ end
+ end
+end
+
+
+--
+-- Element Rendering
+--
+
+-- returns nil or a chapter element from the native property chapter-list
+function get_chapter(possec)
+ local cl = state.chapter_list -- sorted, get latest before possec, if any
+
+ for n=#cl,1,-1 do
+ if possec >= cl[n].time then
+ return cl[n]
+ end
+ end
+end
+
+function render_elements(master_ass)
+
+ -- when the slider is dragged or hovered and we have a target chapter name
+ -- then we use it instead of the normal title. we calculate it before the
+ -- render iterations because the title may be rendered before the slider.
+ state.forced_title = nil
+ local se, ae = state.slider_element, elements[state.active_element]
+ if user_opts.chapter_fmt ~= "no" and se and (ae == se or (not ae and mouse_hit(se))) then
+ local dur = mp.get_property_number("duration", 0)
+ if dur > 0 then
+ local possec = get_slider_value(se) * dur / 100 -- of mouse pos
+ local ch = get_chapter(possec)
+ if ch and ch.title and ch.title ~= "" then
+ state.forced_title = string.format(user_opts.chapter_fmt, ch.title)
+ end
+ end
+ end
+
+ for n=1, #elements do
+ local element = elements[n]
+
+ local style_ass = assdraw.ass_new()
+ style_ass:merge(element.style_ass)
+ ass_append_alpha(style_ass, element.layout.alpha, 0)
+
+ if element.eventresponder and (state.active_element == n) then
+
+ -- run render event functions
+ if element.eventresponder.render ~= nil then
+ element.eventresponder.render(element)
+ end
+
+ if mouse_hit(element) then
+ -- mouse down styling
+ if element.styledown then
+ style_ass:append(osc_styles.elementDown)
+ end
+
+ if element.softrepeat and state.mouse_down_counter >= 15
+ and state.mouse_down_counter % 5 == 0 then
+
+ element.eventresponder[state.active_event_source.."_down"](element)
+ end
+ state.mouse_down_counter = state.mouse_down_counter + 1
+ end
+
+ end
+
+ local elem_ass = assdraw.ass_new()
+
+ elem_ass:merge(style_ass)
+
+ if element.type ~= "button" then
+ elem_ass:merge(element.static_ass)
+ end
+
+ if element.type == "slider" then
+
+ local slider_lo = element.layout.slider
+ local elem_geo = element.layout.geometry
+ local s_min = element.slider.min.value
+ local s_max = element.slider.max.value
+
+ -- draw pos marker
+ local foH, xp
+ local pos = element.slider.posF()
+ local foV = slider_lo.border + slider_lo.gap
+ local innerH = elem_geo.h - (2 * foV)
+ local seekRanges = element.slider.seekRangesF()
+ local seekRangeLineHeight = innerH / 5
+
+ if slider_lo.stype ~= "bar" then
+ foH = elem_geo.h / 2
+ else
+ foH = slider_lo.border + slider_lo.gap
+ end
+
+ if pos then
+ xp = get_slider_ele_pos_for(element, pos)
+
+ if slider_lo.stype ~= "bar" then
+ local r = (user_opts.seekbarhandlesize * innerH) / 2
+ ass_draw_rr_h_cw(elem_ass, xp - r, foH - r,
+ xp + r, foH + r,
+ r, slider_lo.stype == "diamond")
+ else
+ local h = 0
+ if seekRanges and user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
+ h = seekRangeLineHeight
+ end
+ elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h)
+
+ if seekRanges and not user_opts.seekrangeseparate and slider_lo.rtype ~= "inverted" then
+ -- Punch holes for the seekRanges to be drawn later
+ for _,range in pairs(seekRanges) do
+ if range["start"] < pos then
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = xp
+
+ if pos > range["end"] then
+ pend = get_slider_ele_pos_for(element, range["end"])
+ end
+ elem_ass:rect_ccw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ end
+ end
+ end
+ end
+
+ if slider_lo.rtype == "slider" then
+ ass_draw_rr_h_cw(elem_ass, foH - innerH / 6, foH - innerH / 6,
+ xp, foH + innerH / 6,
+ innerH / 6, slider_lo.stype == "diamond", 0)
+ ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15,
+ elem_geo.w - foH + innerH / 15, foH + innerH / 15,
+ 0, slider_lo.stype == "diamond", innerH / 15)
+ for _,range in pairs(seekRanges or {}) do
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = get_slider_ele_pos_for(element, range["end"])
+ ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21,
+ pend, foH + innerH / 21,
+ innerH / 21, slider_lo.stype == "diamond")
+ end
+ end
+ end
+
+ if seekRanges then
+ if slider_lo.rtype ~= "inverted" then
+ elem_ass:draw_stop()
+ elem_ass:merge(element.style_ass)
+ ass_append_alpha(elem_ass, element.layout.alpha, user_opts.seekrangealpha)
+ elem_ass:merge(element.static_ass)
+ end
+
+ for _,range in pairs(seekRanges) do
+ local pstart = get_slider_ele_pos_for(element, range["start"])
+ local pend = get_slider_ele_pos_for(element, range["end"])
+
+ if slider_lo.rtype == "slider" then
+ ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21,
+ pend, foH + innerH / 21,
+ innerH / 21, slider_lo.stype == "diamond")
+ elseif slider_lo.rtype == "line" then
+ if slider_lo.stype == "bar" then
+ elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ else
+ ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8, foH - innerH / 8,
+ pend + innerH / 8, foH + innerH / 8,
+ innerH / 8, slider_lo.stype == "diamond")
+ end
+ elseif slider_lo.rtype == "bar" then
+ if slider_lo.stype ~= "bar" then
+ ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV,
+ pend + innerH / 2, foV + innerH,
+ innerH / 2, slider_lo.stype == "diamond")
+ elseif range["end"] >= (pos or 0) then
+ elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV)
+ else
+ elem_ass:rect_cw(pstart, elem_geo.h - foV - seekRangeLineHeight, pend, elem_geo.h - foV)
+ end
+ elseif slider_lo.rtype == "inverted" then
+ if slider_lo.stype ~= "bar" then
+ ass_draw_rr_h_ccw(elem_ass, pstart, (elem_geo.h / 2) - 1, pend,
+ (elem_geo.h / 2) + 1,
+ 1, slider_lo.stype == "diamond")
+ else
+ elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1, pend, (elem_geo.h / 2) + 1)
+ end
+ end
+ end
+ end
+
+ elem_ass:draw_stop()
+
+ -- add tooltip
+ if element.slider.tooltipF ~= nil then
+ if mouse_hit(element) then
+ local sliderpos = get_slider_value(element)
+ local tooltiplabel = element.slider.tooltipF(sliderpos)
+
+ local an = slider_lo.tooltip_an
+
+ local ty
+
+ if an == 2 then
+ ty = element.hitbox.y1 - slider_lo.border
+ else
+ ty = element.hitbox.y1 + elem_geo.h / 2
+ end
+
+ local tx = get_virt_mouse_pos()
+ if slider_lo.adjust_tooltip then
+ if an == 2 then
+ if sliderpos < (s_min + 3) then
+ an = an - 1
+ elseif sliderpos > (s_max - 3) then
+ an = an + 1
+ end
+ elseif sliderpos > (s_max+s_min) / 2 then
+ an = an + 1
+ tx = tx - 5
+ else
+ an = an - 1
+ tx = tx + 10
+ end
+ end
+
+ -- tooltip label
+ elem_ass:new_event()
+ elem_ass:pos(tx, ty)
+ elem_ass:an(an)
+ elem_ass:append(slider_lo.tooltip_style)
+ ass_append_alpha(elem_ass, slider_lo.alpha, 0)
+ elem_ass:append(tooltiplabel)
+
+ end
+ end
+
+ elseif element.type == "button" then
+
+ local buttontext
+ if type(element.content) == "function" then
+ buttontext = element.content() -- function objects
+ elseif element.content ~= nil then
+ buttontext = element.content -- text objects
+ end
+
+ local maxchars = element.layout.button.maxchars
+ if maxchars ~= nil and #buttontext > maxchars then
+ local max_ratio = 1.25 -- up to 25% more chars while shrinking
+ local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
+ if #buttontext > limit then
+ while (#buttontext > limit) do
+ buttontext = buttontext:gsub(".[\128-\191]*$", "")
+ end
+ buttontext = buttontext .. "..."
+ end
+ local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
+ local stretch = (maxchars/#buttontext)*100
+ buttontext = string.format("{\\fscx%f}",
+ (maxchars/#buttontext)*100) .. buttontext
+ end
+
+ elem_ass:append(buttontext)
+ end
+
+ master_ass:merge(elem_ass)
+ end
+end
+
+--
+-- Message display
+--
+
+-- pos is 1 based
+function limited_list(prop, pos)
+ local proplist = mp.get_property_native(prop, {})
+ local count = #proplist
+ if count == 0 then
+ return count, proplist
+ end
+
+ local fs = tonumber(mp.get_property('options/osd-font-size'))
+ local max = math.ceil(osc_param.unscaled_y*0.75 / fs)
+ if max % 2 == 0 then
+ max = max - 1
+ end
+ local delta = math.ceil(max / 2) - 1
+ local begi = math.max(math.min(pos - delta, count - max + 1), 1)
+ local endi = math.min(begi + max - 1, count)
+
+ local reslist = {}
+ for i=begi, endi do
+ local item = proplist[i]
+ item.current = (i == pos) and true or nil
+ table.insert(reslist, item)
+ end
+ return count, reslist
+end
+
+function get_playlist()
+ local pos = mp.get_property_number('playlist-pos', 0) + 1
+ local count, limlist = limited_list('playlist', pos)
+ if count == 0 then
+ return 'Empty playlist.'
+ end
+
+ local message = string.format('Playlist [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local title = v.title
+ local _, filename = utils.split_path(v.filename)
+ if title == nil then
+ title = filename
+ end
+ message = string.format('%s %s %s\n', message,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function get_chapterlist()
+ local pos = mp.get_property_number('chapter', 0) + 1
+ local count, limlist = limited_list('chapter-list', pos)
+ if count == 0 then
+ return 'No chapters.'
+ end
+
+ local message = string.format('Chapters [%d/%d]:\n', pos, count)
+ for i, v in ipairs(limlist) do
+ local time = mp.format_time(v.time)
+ local title = v.title
+ if title == nil then
+ title = string.format('Chapter %02d', i)
+ end
+ message = string.format('%s[%s] %s %s\n', message, time,
+ (v.current and '●' or '○'), title)
+ end
+ return message
+end
+
+function show_message(text, duration)
+
+ --print("text: "..text.." duration: " .. duration)
+ if duration == nil then
+ duration = tonumber(mp.get_property("options/osd-duration")) / 1000
+ elseif not type(duration) == "number" then
+ print("duration: " .. duration)
+ end
+
+ -- cut the text short, otherwise the following functions
+ -- may slow down massively on huge input
+ text = string.sub(text, 0, 4000)
+
+ -- replace actual linebreaks with ASS linebreaks
+ text = string.gsub(text, "\n", "\\N")
+
+ state.message_text = text
+
+ if not state.message_hide_timer then
+ state.message_hide_timer = mp.add_timeout(0, request_tick)
+ end
+ state.message_hide_timer:kill()
+ state.message_hide_timer.timeout = duration
+ state.message_hide_timer:resume()
+ request_tick()
+end
+
+function render_message(ass)
+ if state.message_hide_timer and state.message_hide_timer:is_enabled() and
+ state.message_text
+ then
+ local _, lines = string.gsub(state.message_text, "\\N", "")
+
+ local fontsize = tonumber(mp.get_property("options/osd-font-size"))
+ local outline = tonumber(mp.get_property("options/osd-border-size"))
+ local maxlines = math.ceil(osc_param.unscaled_y*0.75 / fontsize)
+ local counterscale = osc_param.playresy / osc_param.unscaled_y
+
+ fontsize = fontsize * counterscale / math.max(0.65 + math.min(lines/maxlines, 1), 1)
+ outline = outline * counterscale / math.max(0.75 + math.min(lines/maxlines, 1)/2, 1)
+
+ local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
+
+
+ ass:new_event()
+ ass:append(style .. state.message_text)
+ else
+ state.message_text = nil
+ end
+end
+
+--
+-- Initialisation and Layout
+--
+
+function new_element(name, type)
+ elements[name] = {}
+ elements[name].type = type
+
+ -- add default stuff
+ elements[name].eventresponder = {}
+ elements[name].visible = true
+ elements[name].enabled = true
+ elements[name].softrepeat = false
+ elements[name].styledown = (type == "button")
+ elements[name].state = {}
+
+ if type == "slider" then
+ elements[name].slider = {min = {value = 0}, max = {value = 100}}
+ end
+
+
+ return elements[name]
+end
+
+function add_layout(name)
+ if elements[name] ~= nil then
+ -- new layout
+ elements[name].layout = {}
+
+ -- set layout defaults
+ elements[name].layout.layer = 50
+ elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
+
+ if elements[name].type == "button" then
+ elements[name].layout.button = {
+ maxchars = nil,
+ }
+ elseif elements[name].type == "slider" then
+ -- slider defaults
+ elements[name].layout.slider = {
+ border = 1,
+ gap = 1,
+ nibbles_top = true,
+ nibbles_bottom = true,
+ stype = "slider",
+ adjust_tooltip = true,
+ tooltip_style = "",
+ tooltip_an = 2,
+ alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255},
+ }
+ elseif elements[name].type == "box" then
+ elements[name].layout.box = {radius = 0, hexagon = false}
+ end
+
+ return elements[name].layout
+ else
+ msg.error("Can't add_layout to element \""..name.."\", doesn't exist.")
+ end
+end
+
+-- Window Controls
+function window_controls(topbar)
+ local wc_geo = {
+ x = 0,
+ y = 30 + user_opts.barmargin,
+ an = 1,
+ w = osc_param.playresx,
+ h = 30,
+ }
+
+ local alignment = window_controls_alignment()
+ local controlbox_w = window_control_box_width
+ local titlebox_w = wc_geo.w - controlbox_w
+
+ -- Default alignment is "right"
+ local controlbox_left = wc_geo.w - controlbox_w
+ local titlebox_left = wc_geo.x
+ local titlebox_right = wc_geo.w - controlbox_w
+
+ if alignment == "left" then
+ controlbox_left = wc_geo.x
+ titlebox_left = wc_geo.x + controlbox_w
+ titlebox_right = wc_geo.w
+ end
+
+ add_area("window-controls",
+ get_hitbox_coords(controlbox_left, wc_geo.y, wc_geo.an,
+ controlbox_w, wc_geo.h))
+
+ local lo
+
+ -- Background Bar
+ new_element("wcbar", "box")
+ lo = add_layout("wcbar")
+ lo.geometry = wc_geo
+ lo.layer = 10
+ lo.style = osc_styles.wcBar
+ lo.alpha[1] = user_opts.boxalpha
+
+ local button_y = wc_geo.y - (wc_geo.h / 2)
+ local first_geo =
+ {x = controlbox_left + 5, y = button_y, an = 4, w = 25, h = 25}
+ local second_geo =
+ {x = controlbox_left + 30, y = button_y, an = 4, w = 25, h = 25}
+ local third_geo =
+ {x = controlbox_left + 55, y = button_y, an = 4, w = 25, h = 25}
+
+ -- Window control buttons use symbols in the custom mpv osd font
+ -- because the official unicode codepoints are sufficiently
+ -- exotic that a system might lack an installed font with them,
+ -- and libass will complain that they are not present in the
+ -- default font, even if another font with them is available.
+
+ -- Close: 🗙
+ ne = new_element("close", "button")
+ ne.content = "\238\132\149"
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("quit") end
+ lo = add_layout("close")
+ lo.geometry = alignment == "left" and first_geo or third_geo
+ lo.style = osc_styles.wcButtons
+
+ -- Minimize: 🗕
+ ne = new_element("minimize", "button")
+ ne.content = "\238\132\146"
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "window-minimized") end
+ lo = add_layout("minimize")
+ lo.geometry = alignment == "left" and second_geo or first_geo
+ lo.style = osc_styles.wcButtons
+
+ -- Maximize: 🗖 /🗗
+ ne = new_element("maximize", "button")
+ if state.maximized or state.fullscreen then
+ ne.content = "\238\132\148"
+ else
+ ne.content = "\238\132\147"
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ if state.fullscreen then
+ mp.commandv("cycle", "fullscreen")
+ else
+ mp.commandv("cycle", "window-maximized")
+ end
+ end
+ lo = add_layout("maximize")
+ lo.geometry = alignment == "left" and third_geo or second_geo
+ lo.style = osc_styles.wcButtons
+
+ -- deadzone below window controls
+ local sh_area_y0, sh_area_y1
+ sh_area_y0 = user_opts.barmargin
+ sh_area_y1 = wc_geo.y + get_align(1 - (2 * user_opts.deadzonesize),
+ osc_param.playresy - wc_geo.y, 0, 0)
+ add_area("showhide_wc", wc_geo.x, sh_area_y0, wc_geo.w, sh_area_y1)
+
+ if topbar then
+ -- The title is already there as part of the top bar
+ return
+ else
+ -- Apply boxvideo margins to the control bar
+ osc_param.video_margins.t = wc_geo.h / osc_param.playresy
+ end
+
+ -- Window Title
+ ne = new_element("wctitle", "button")
+ ne.content = function ()
+ local title = mp.command_native({"expand-text", user_opts.title})
+ -- escape ASS, and strip newlines and trailing slashes
+ title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
+ return not (title == "") and title or "mpv"
+ end
+ local left_pad = 5
+ local right_pad = 10
+ lo = add_layout("wctitle")
+ lo.geometry =
+ { x = titlebox_left + left_pad, y = wc_geo.y - 3, an = 1,
+ w = titlebox_w, h = wc_geo.h }
+ lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
+ osc_styles.wcTitle,
+ titlebox_left + left_pad, wc_geo.y - wc_geo.h,
+ titlebox_right - right_pad , wc_geo.y + wc_geo.h)
+
+ add_area("window-controls-title",
+ titlebox_left, 0, titlebox_right, wc_geo.h)
+end
+
+--
+-- Layouts
+--
+
+local layouts = {}
+
+-- Classic box layout
+layouts["box"] = function ()
+
+ local osc_geo = {
+ w = 550, -- width
+ h = 138, -- height
+ r = 10, -- corner-radius
+ p = 15, -- padding
+ }
+
+ -- make sure the OSC actually fits into the video
+ if osc_param.playresx < (osc_geo.w + (2 * osc_geo.p)) then
+ osc_param.playresy = (osc_geo.w + (2 * osc_geo.p)) / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
+ osc_geo.w, 0))
+ local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
+ osc_geo.h, 0))
+
+ -- position offset for contents aligned at the borders of the box
+ local pos_offsetX = (osc_geo.w - (2*osc_geo.p)) / 2
+ local pos_offsetY = (osc_geo.h - (2*osc_geo.p)) / 2
+
+ osc_param.areas = {} -- delete areas
+
+ -- area for active mouse input
+ add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
+
+ -- area for show/hide
+ local sh_area_y0, sh_area_y1
+ if user_opts.valign > 0 then
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
+ posY - (osc_geo.h / 2), 0, 0)
+ sh_area_y1 = osc_param.playresy
+ else
+ -- deadzone below OSC
+ sh_area_y0 = 0
+ sh_area_y1 = (posY + (osc_geo.h / 2)) +
+ get_align(1 - (2*user_opts.deadzonesize),
+ osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ -- fetch values
+ local osc_w, osc_h, osc_r, osc_p =
+ osc_geo.w, osc_geo.h, osc_geo.r, osc_geo.p
+
+ local lo
+
+ --
+ -- Background box
+ --
+
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+ lo.alpha[3] = user_opts.boxalpha
+ lo.box.radius = osc_r
+
+ --
+ -- Title row
+ --
+
+ local titlerowY = posY - pos_offsetY - 10
+
+ lo = add_layout("title")
+ lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
+ lo.style = osc_styles.vidtitle
+ lo.button.maxchars = user_opts.boxmaxchars
+
+ lo = add_layout("pl_prev")
+ lo.geometry =
+ {x = (posX - pos_offsetX), y = titlerowY, an = 7, w = 12, h = 12}
+ lo.style = osc_styles.topButtons
+
+ lo = add_layout("pl_next")
+ lo.geometry =
+ {x = (posX + pos_offsetX), y = titlerowY, an = 9, w = 12, h = 12}
+ lo.style = osc_styles.topButtons
+
+ --
+ -- Big buttons
+ --
+
+ local bigbtnrowY = posY - pos_offsetY + 35
+ local bigbtndist = 60
+
+ lo = add_layout("playpause")
+ lo.geometry =
+ {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("skipback")
+ lo.geometry =
+ {x = posX - bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("skipfrwd")
+ lo.geometry =
+ {x = posX + bigbtndist, y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("ch_prev")
+ lo.geometry =
+ {x = posX - (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("ch_next")
+ lo.geometry =
+ {x = posX + (bigbtndist * 2), y = bigbtnrowY, an = 5, w = 40, h = 40}
+ lo.style = osc_styles.bigButtons
+
+ lo = add_layout("cy_audio")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bigbtnrowY, an = 1, w = 70, h = 18}
+ lo.style = osc_styles.smallButtonsL
+
+ lo = add_layout("cy_sub")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bigbtnrowY, an = 7, w = 70, h = 18}
+ lo.style = osc_styles.smallButtonsL
+
+ lo = add_layout("tog_fs")
+ lo.geometry =
+ {x = posX+pos_offsetX - 25, y = bigbtnrowY, an = 4, w = 25, h = 25}
+ lo.style = osc_styles.smallButtonsR
+
+ lo = add_layout("volume")
+ lo.geometry =
+ {x = posX+pos_offsetX - (25 * 2) - osc_geo.p,
+ y = bigbtnrowY, an = 4, w = 25, h = 25}
+ lo.style = osc_styles.smallButtonsR
+
+ --
+ -- Seekbar
+ --
+
+ lo = add_layout("seekbar")
+ lo.geometry =
+ {x = posX, y = posY+pos_offsetY-22, an = 2, w = pos_offsetX*2, h = 15}
+ lo.style = osc_styles.timecodes
+ lo.slider.tooltip_style = osc_styles.vidtitle
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+
+ --
+ -- Timecodes + Cache
+ --
+
+ local bottomrowY = posY + pos_offsetY - 5
+
+ lo = add_layout("tc_left")
+ lo.geometry =
+ {x = posX - pos_offsetX, y = bottomrowY, an = 4, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+ lo = add_layout("tc_right")
+ lo.geometry =
+ {x = posX + pos_offsetX, y = bottomrowY, an = 6, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+ lo = add_layout("cache")
+ lo.geometry =
+ {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
+ lo.style = osc_styles.timecodes
+
+end
+
+-- slim box layout
+layouts["slimbox"] = function ()
+
+ local osc_geo = {
+ w = 660, -- width
+ h = 70, -- height
+ r = 10, -- corner-radius
+ }
+
+ -- make sure the OSC actually fits into the video
+ if osc_param.playresx < (osc_geo.w) then
+ osc_param.playresy = (osc_geo.w) / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
+ osc_geo.w, 0))
+ local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
+ osc_geo.h, 0))
+
+ osc_param.areas = {} -- delete areas
+
+ -- area for active mouse input
+ add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
+
+ -- area for show/hide
+ local sh_area_y0, sh_area_y1
+ if user_opts.valign > 0 then
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2*user_opts.deadzonesize),
+ posY - (osc_geo.h / 2), 0, 0)
+ sh_area_y1 = osc_param.playresy
+ else
+ -- deadzone below OSC
+ sh_area_y0 = 0
+ sh_area_y1 = (posY + (osc_geo.h / 2)) +
+ get_align(1 - (2*user_opts.deadzonesize),
+ osc_param.playresy - (posY + (osc_geo.h / 2)), 0, 0)
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ local lo
+
+ local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
+
+ -- styles
+ local styles = {
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+ timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
+ tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}",
+ }
+
+
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+ lo.alpha[3] = 0
+ if user_opts["seekbarstyle"] ~= "bar" then
+ lo.box.radius = osc_geo.r
+ lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
+ end
+
+
+ lo = add_layout("seekbar")
+ lo.geometry =
+ {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
+ lo.style = osc_styles.timecodes
+ lo.slider.border = 0
+ lo.slider.gap = 1.5
+ lo.slider.tooltip_style = styles.tooltip
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+ lo.slider.adjust_tooltip = false
+
+ --
+ -- Timecodes
+ --
+
+ lo = add_layout("tc_left")
+ lo.geometry =
+ {x = posX - (inner_w/2) + osc_geo.r, y = posY + 1,
+ an = 7, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+ lo = add_layout("tc_right")
+ lo.geometry =
+ {x = posX + (inner_w/2) - osc_geo.r, y = posY + 1,
+ an = 9, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+ -- Cache
+
+ lo = add_layout("cache")
+ lo.geometry =
+ {x = posX, y = posY + 1,
+ an = 8, w = tc_w, h = ele_h}
+ lo.style = styles.timecodes
+ lo.alpha[3] = user_opts.boxalpha
+
+
+end
+
+function bar_layout(direction)
+ local osc_geo = {
+ x = -2,
+ y,
+ an = (direction < 0) and 7 or 1,
+ w,
+ h = 56,
+ }
+
+ local padX = 9
+ local padY = 3
+ local buttonW = 27
+ local tcW = (state.tc_ms) and 170 or 110
+ if user_opts.tcspace >= 50 and user_opts.tcspace <= 200 then
+ -- adjust our hardcoded font size estimation
+ tcW = tcW * user_opts.tcspace / 100
+ end
+
+ local tsW = 90
+ local minW = (buttonW + padX)*5 + (tcW + padX)*4 + (tsW + padX)*2
+
+ -- Special topbar handling when window controls are present
+ local padwc_l
+ local padwc_r
+ if direction < 0 or not window_controls_enabled() then
+ padwc_l = 0
+ padwc_r = 0
+ elseif window_controls_alignment() == "left" then
+ padwc_l = window_control_box_width
+ padwc_r = 0
+ else
+ padwc_l = 0
+ padwc_r = window_control_box_width
+ end
+
+ if osc_param.display_aspect > 0 and osc_param.playresx < minW then
+ osc_param.playresy = minW / osc_param.display_aspect
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+ end
+
+ osc_geo.y = direction * (54 + user_opts.barmargin)
+ osc_geo.w = osc_param.playresx + 4
+ if direction < 0 then
+ osc_geo.y = osc_geo.y + osc_param.playresy
+ end
+
+ local line1 = osc_geo.y - direction * (9 + padY)
+ local line2 = osc_geo.y - direction * (36 + padY)
+
+ osc_param.areas = {}
+
+ add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
+ osc_geo.w, osc_geo.h))
+
+ local sh_area_y0, sh_area_y1
+ if direction > 0 then
+ -- deadzone below OSC
+ sh_area_y0 = user_opts.barmargin
+ sh_area_y1 = osc_geo.y + get_align(1 - (2 * user_opts.deadzonesize),
+ osc_param.playresy - osc_geo.y, 0, 0)
+ else
+ -- deadzone above OSC
+ sh_area_y0 = get_align(-1 + (2 * user_opts.deadzonesize), osc_geo.y, 0, 0)
+ sh_area_y1 = osc_param.playresy - user_opts.barmargin
+ end
+ add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
+
+ local lo, geo
+
+ -- Background bar
+ new_element("bgbox", "box")
+ lo = add_layout("bgbox")
+
+ lo.geometry = osc_geo
+ lo.layer = 10
+ lo.style = osc_styles.box
+ lo.alpha[1] = user_opts.boxalpha
+
+
+ -- Playlist prev/next
+ geo = { x = osc_geo.x + padX, y = line1,
+ an = 4, w = 18, h = 18 - padY }
+ lo = add_layout("pl_prev")
+ lo.geometry = geo
+ lo.style = osc_styles.topButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("pl_next")
+ lo.geometry = geo
+ lo.style = osc_styles.topButtonsBar
+
+ local t_l = geo.x + geo.w + padX
+
+ -- Cache
+ geo = { x = osc_geo.x + osc_geo.w - padX, y = geo.y,
+ an = 6, w = 150, h = geo.h }
+ lo = add_layout("cache")
+ lo.geometry = geo
+ lo.style = osc_styles.vidtitleBar
+
+ local t_r = geo.x - geo.w - padX*2
+
+ -- Title
+ geo = { x = t_l, y = geo.y, an = 4,
+ w = t_r - t_l, h = geo.h }
+ lo = add_layout("title")
+ lo.geometry = geo
+ lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}",
+ osc_styles.vidtitleBar,
+ geo.x, geo.y-geo.h, geo.w, geo.y+geo.h)
+
+
+ -- Playback control buttons
+ geo = { x = osc_geo.x + padX + padwc_l, y = line2, an = 4,
+ w = buttonW, h = 36 - padY*2}
+ lo = add_layout("playpause")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("ch_prev")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x + geo.w + padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("ch_next")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Left timecode
+ geo = { x = geo.x + geo.w + padX + tcW, y = geo.y, an = 6,
+ w = tcW, h = geo.h }
+ lo = add_layout("tc_left")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+
+ local sb_l = geo.x + padX
+
+ -- Fullscreen button
+ geo = { x = osc_geo.x + osc_geo.w - buttonW - padX - padwc_r, y = geo.y, an = 4,
+ w = buttonW, h = geo.h }
+ lo = add_layout("tog_fs")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Volume
+ geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("volume")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ -- Track selection buttons
+ geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
+ lo = add_layout("cy_sub")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+ geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
+ lo = add_layout("cy_audio")
+ lo.geometry = geo
+ lo.style = osc_styles.smallButtonsBar
+
+
+ -- Right timecode
+ geo = { x = geo.x - padX - tcW - 10, y = geo.y, an = geo.an,
+ w = tcW, h = geo.h }
+ lo = add_layout("tc_right")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+
+ local sb_r = geo.x - padX
+
+
+ -- Seekbar
+ geo = { x = sb_l, y = geo.y, an = geo.an,
+ w = math.max(0, sb_r - sb_l), h = geo.h }
+ new_element("bgbar1", "box")
+ lo = add_layout("bgbar1")
+
+ lo.geometry = geo
+ lo.layer = 15
+ lo.style = osc_styles.timecodesBar
+ lo.alpha[1] =
+ math.min(255, user_opts.boxalpha + (255 - user_opts.boxalpha)*0.8)
+ if user_opts["seekbarstyle"] ~= "bar" then
+ lo.box.radius = geo.h / 2
+ lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
+ end
+
+ lo = add_layout("seekbar")
+ lo.geometry = geo
+ lo.style = osc_styles.timecodesBar
+ lo.slider.border = 0
+ lo.slider.gap = 2
+ lo.slider.tooltip_style = osc_styles.timePosBar
+ lo.slider.tooltip_an = 5
+ lo.slider.stype = user_opts["seekbarstyle"]
+ lo.slider.rtype = user_opts["seekrangestyle"]
+
+ if direction < 0 then
+ osc_param.video_margins.b = osc_geo.h / osc_param.playresy
+ else
+ osc_param.video_margins.t = osc_geo.h / osc_param.playresy
+ end
+end
+
+layouts["bottombar"] = function()
+ bar_layout(-1)
+end
+
+layouts["topbar"] = function()
+ bar_layout(1)
+end
+
+-- Validate string type user options
+function validate_user_opts()
+ if layouts[user_opts.layout] == nil then
+ msg.warn("Invalid setting \""..user_opts.layout.."\" for layout")
+ user_opts.layout = "bottombar"
+ end
+
+ if user_opts.seekbarstyle ~= "bar" and
+ user_opts.seekbarstyle ~= "diamond" and
+ user_opts.seekbarstyle ~= "knob" then
+ msg.warn("Invalid setting \"" .. user_opts.seekbarstyle
+ .. "\" for seekbarstyle")
+ user_opts.seekbarstyle = "bar"
+ end
+
+ if user_opts.seekrangestyle ~= "bar" and
+ user_opts.seekrangestyle ~= "line" and
+ user_opts.seekrangestyle ~= "slider" and
+ user_opts.seekrangestyle ~= "inverted" and
+ user_opts.seekrangestyle ~= "none" then
+ msg.warn("Invalid setting \"" .. user_opts.seekrangestyle
+ .. "\" for seekrangestyle")
+ user_opts.seekrangestyle = "inverted"
+ end
+
+ if user_opts.seekrangestyle == "slider" and
+ user_opts.seekbarstyle == "bar" then
+ msg.warn("Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported")
+ user_opts.seekrangestyle = "inverted"
+ end
+
+ if user_opts.windowcontrols ~= "auto" and
+ user_opts.windowcontrols ~= "yes" and
+ user_opts.windowcontrols ~= "no" then
+ msg.warn("windowcontrols cannot be \"" ..
+ user_opts.windowcontrols .. "\". Ignoring.")
+ user_opts.windowcontrols = "auto"
+ end
+ if user_opts.windowcontrols_alignment ~= "right" and
+ user_opts.windowcontrols_alignment ~= "left" then
+ msg.warn("windowcontrols_alignment cannot be \"" ..
+ user_opts.windowcontrols_alignment .. "\". Ignoring.")
+ user_opts.windowcontrols_alignment = "right"
+ end
+end
+
+function update_options(list)
+ validate_user_opts()
+ request_tick()
+ visibility_mode(user_opts.visibility, true)
+ update_duration_watch()
+ request_init()
+end
+
+local UNICODE_MINUS = string.char(0xe2, 0x88, 0x92) -- UTF-8 for U+2212 MINUS SIGN
+
+-- OSC INIT
+function osc_init()
+ msg.debug("osc_init")
+
+ -- set canvas resolution according to display aspect and scaling setting
+ local baseResY = 720
+ local display_w, display_h, display_aspect = mp.get_osd_size()
+ local scale = 1
+
+ if mp.get_property("video") == "no" then -- dummy/forced window
+ scale = user_opts.scaleforcedwindow
+ elseif state.fullscreen then
+ scale = user_opts.scalefullscreen
+ else
+ scale = user_opts.scalewindowed
+ end
+
+ if user_opts.vidscale then
+ osc_param.unscaled_y = baseResY
+ else
+ osc_param.unscaled_y = display_h
+ end
+ osc_param.playresy = osc_param.unscaled_y / scale
+ if display_aspect > 0 then
+ osc_param.display_aspect = display_aspect
+ end
+ osc_param.playresx = osc_param.playresy * osc_param.display_aspect
+
+ -- stop seeking with the slider to prevent skipping files
+ state.active_element = nil
+
+ osc_param.video_margins = {l = 0, r = 0, t = 0, b = 0}
+
+ elements = {}
+
+ -- some often needed stuff
+ local pl_count = mp.get_property_number("playlist-count", 0)
+ local have_pl = (pl_count > 1)
+ local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
+ local have_ch = (mp.get_property_number("chapters", 0) > 0)
+ local loop = mp.get_property("loop-playlist", "no")
+
+ local ne
+
+ -- title
+ ne = new_element("title", "button")
+
+ ne.content = function ()
+ local title = state.forced_title or
+ mp.command_native({"expand-text", user_opts.title})
+ -- escape ASS, and strip newlines and trailing slashes
+ title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
+ return not (title == "") and title or "mpv"
+ end
+
+ ne.eventresponder["mbtn_left_up"] = function ()
+ local title = mp.get_property_osd("media-title")
+ if have_pl then
+ title = string.format("[%d/%d] %s", countone(pl_pos - 1),
+ pl_count, title)
+ end
+ show_message(title)
+ end
+
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(mp.get_property_osd("filename")) end
+
+ -- playlist buttons
+
+ -- prev
+ ne = new_element("pl_prev", "button")
+
+ ne.content = "\238\132\144"
+ ne.enabled = (pl_pos > 1) or (loop ~= "no")
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("playlist-prev", "weak")
+ if user_opts.playlist_osd then
+ show_message(get_playlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_playlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_playlist(), 3) end
+
+ --next
+ ne = new_element("pl_next", "button")
+
+ ne.content = "\238\132\129"
+ ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("playlist-next", "weak")
+ if user_opts.playlist_osd then
+ show_message(get_playlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_playlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_playlist(), 3) end
+
+
+ -- big buttons
+
+ --playpause
+ ne = new_element("playpause", "button")
+
+ ne.content = function ()
+ if mp.get_property("pause") == "yes" then
+ return ("\238\132\129")
+ else
+ return ("\238\128\130")
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "pause") end
+
+ --skipback
+ ne = new_element("skipback", "button")
+
+ ne.softrepeat = true
+ ne.content = "\238\128\132"
+ ne.eventresponder["mbtn_left_down"] =
+ function () mp.commandv("seek", -5, "relative", "keyframes") end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () mp.commandv("frame-back-step") end
+ ne.eventresponder["mbtn_right_down"] =
+ function () mp.commandv("seek", -30, "relative", "keyframes") end
+
+ --skipfrwd
+ ne = new_element("skipfrwd", "button")
+
+ ne.softrepeat = true
+ ne.content = "\238\128\133"
+ ne.eventresponder["mbtn_left_down"] =
+ function () mp.commandv("seek", 10, "relative", "keyframes") end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () mp.commandv("frame-step") end
+ ne.eventresponder["mbtn_right_down"] =
+ function () mp.commandv("seek", 60, "relative", "keyframes") end
+
+ --ch_prev
+ ne = new_element("ch_prev", "button")
+
+ ne.enabled = have_ch
+ ne.content = "\238\132\132"
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("add", "chapter", -1)
+ if user_opts.chapters_osd then
+ show_message(get_chapterlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_chapterlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_chapterlist(), 3) end
+
+ --ch_next
+ ne = new_element("ch_next", "button")
+
+ ne.enabled = have_ch
+ ne.content = "\238\132\133"
+ ne.eventresponder["mbtn_left_up"] =
+ function ()
+ mp.commandv("add", "chapter", 1)
+ if user_opts.chapters_osd then
+ show_message(get_chapterlist(), 3)
+ end
+ end
+ ne.eventresponder["shift+mbtn_left_up"] =
+ function () show_message(get_chapterlist(), 3) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () show_message(get_chapterlist(), 3) end
+
+ --
+ update_tracklist()
+
+ --cy_audio
+ ne = new_element("cy_audio", "button")
+
+ ne.enabled = (#tracks_osc.audio > 0)
+ ne.content = function ()
+ local aid = "–"
+ if get_track("audio") ~= 0 then
+ aid = get_track("audio")
+ end
+ return ("\238\132\134" .. osc_styles.smallButtonsLlabel
+ .. " " .. aid .. "/" .. #tracks_osc.audio)
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () set_track("audio", 1) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () set_track("audio", -1) end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () show_message(get_tracklist("audio"), 2) end
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("audio", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("audio", -1) end
+
+ --cy_sub
+ ne = new_element("cy_sub", "button")
+
+ ne.enabled = (#tracks_osc.sub > 0)
+ ne.content = function ()
+ local sid = "–"
+ if get_track("sub") ~= 0 then
+ sid = get_track("sub")
+ end
+ return ("\238\132\135" .. osc_styles.smallButtonsLlabel
+ .. " " .. sid .. "/" .. #tracks_osc.sub)
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () set_track("sub", 1) end
+ ne.eventresponder["mbtn_right_up"] =
+ function () set_track("sub", -1) end
+ ne.eventresponder["shift+mbtn_left_down"] =
+ function () show_message(get_tracklist("sub"), 2) end
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("sub", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("sub", -1) end
+
+ --tog_fs
+ ne = new_element("tog_fs", "button")
+ ne.content = function ()
+ if state.fullscreen then
+ return ("\238\132\137")
+ else
+ return ("\238\132\136")
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "fullscreen") end
+
+ --seekbar
+ ne = new_element("seekbar", "slider")
+
+ ne.enabled = mp.get_property("percent-pos") ~= nil
+ state.slider_element = ne.enabled and ne or nil -- used for forced_title
+ ne.slider.markerF = function ()
+ local duration = mp.get_property_number("duration", nil)
+ if duration ~= nil then
+ local chapters = mp.get_property_native("chapter-list", {})
+ local markers = {}
+ for n = 1, #chapters do
+ markers[n] = (chapters[n].time / duration * 100)
+ end
+ return markers
+ else
+ return {}
+ end
+ end
+ ne.slider.posF =
+ function () return mp.get_property_number("percent-pos", nil) end
+ ne.slider.tooltipF = function (pos)
+ local duration = mp.get_property_number("duration", nil)
+ if duration ~= nil and pos ~= nil then
+ possec = duration * (pos / 100)
+ return mp.format_time(possec)
+ else
+ return ""
+ end
+ end
+ ne.slider.seekRangesF = function()
+ if user_opts.seekrangestyle == "none" then
+ return nil
+ end
+ local cache_state = state.cache_state
+ if not cache_state then
+ return nil
+ end
+ local duration = mp.get_property_number("duration", nil)
+ if duration == nil or duration <= 0 then
+ return nil
+ end
+ local ranges = cache_state["seekable-ranges"]
+ if #ranges == 0 then
+ return nil
+ end
+ local nranges = {}
+ for _, range in pairs(ranges) do
+ nranges[#nranges + 1] = {
+ ["start"] = 100 * range["start"] / duration,
+ ["end"] = 100 * range["end"] / duration,
+ }
+ end
+ return nranges
+ end
+ ne.eventresponder["mouse_move"] = --keyframe seeking when mouse is dragged
+ function (element)
+ -- mouse move events may pile up during seeking and may still get
+ -- sent when the user is done seeking, so we need to throw away
+ -- identical seeks
+ local seekto = get_slider_value(element)
+ if element.state.lastseek == nil or
+ element.state.lastseek ~= seekto then
+ local flags = "absolute-percent"
+ if not user_opts.seekbarkeyframes then
+ flags = flags .. "+exact"
+ end
+ mp.commandv("seek", seekto, flags)
+ element.state.lastseek = seekto
+ end
+
+ end
+ ne.eventresponder["mbtn_left_down"] = --exact seeks on single clicks
+ function (element) mp.commandv("seek", get_slider_value(element),
+ "absolute-percent", "exact") end
+ ne.eventresponder["reset"] =
+ function (element) element.state.lastseek = nil end
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "seek", 10) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "seek", -10) end
+
+
+ -- tc_left (current pos)
+ ne = new_element("tc_left", "button")
+
+ ne.content = function ()
+ if state.tc_ms then
+ return (mp.get_property_osd("playback-time/full"))
+ else
+ return (mp.get_property_osd("playback-time"))
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] = function ()
+ state.tc_ms = not state.tc_ms
+ request_init()
+ end
+
+ -- tc_right (total/remaining time)
+ ne = new_element("tc_right", "button")
+
+ ne.visible = (mp.get_property_number("duration", 0) > 0)
+ ne.content = function ()
+ if state.rightTC_trem then
+ local minus = user_opts.unicodeminus and UNICODE_MINUS or "-"
+ local property = user_opts.remaining_playtime and "playtime-remaining"
+ or "time-remaining"
+ if state.tc_ms then
+ return (minus..mp.get_property_osd(property .. "/full"))
+ else
+ return (minus..mp.get_property_osd(property))
+ end
+ else
+ if state.tc_ms then
+ return (mp.get_property_osd("duration/full"))
+ else
+ return (mp.get_property_osd("duration"))
+ end
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () state.rightTC_trem = not state.rightTC_trem end
+
+ -- cache
+ ne = new_element("cache", "button")
+
+ ne.content = function ()
+ local cache_state = state.cache_state
+ if not (cache_state and cache_state["seekable-ranges"] and
+ #cache_state["seekable-ranges"] > 0) then
+ -- probably not a network stream
+ return ""
+ end
+ local dmx_cache = cache_state and cache_state["cache-duration"]
+ local thresh = math.min(state.dmx_cache * 0.05, 5) -- 5% or 5s
+ if dmx_cache and math.abs(dmx_cache - state.dmx_cache) >= thresh then
+ state.dmx_cache = dmx_cache
+ else
+ dmx_cache = state.dmx_cache
+ end
+ local min = math.floor(dmx_cache / 60)
+ local sec = math.floor(dmx_cache % 60) -- don't round e.g. 59.9 to 60
+ return "Cache: " .. (min > 0 and
+ string.format("%sm%02.0fs", min, sec) or
+ string.format("%3.0fs", sec))
+ end
+
+ -- volume
+ ne = new_element("volume", "button")
+
+ ne.content = function()
+ local volume = mp.get_property_number("volume", 0)
+ local mute = mp.get_property_native("mute")
+ local volicon = {"\238\132\139", "\238\132\140",
+ "\238\132\141", "\238\132\142"}
+ if volume == 0 or mute then
+ return "\238\132\138"
+ else
+ return volicon[math.min(4,math.ceil(volume / (100/3)))]
+ end
+ end
+ ne.eventresponder["mbtn_left_up"] =
+ function () mp.commandv("cycle", "mute") end
+
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", 5) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", -5) end
+
+
+ -- load layout
+ layouts[user_opts.layout]()
+
+ -- load window controls
+ if window_controls_enabled() then
+ window_controls(user_opts.layout == "topbar")
+ end
+
+ --do something with the elements
+ prepare_elements()
+
+ update_margins()
+end
+
+function reset_margins()
+ if state.using_video_margins then
+ for _, opt in ipairs(margins_opts) do
+ mp.set_property_number(opt[2], 0.0)
+ end
+ state.using_video_margins = false
+ end
+end
+
+function update_margins()
+ local margins = osc_param.video_margins
+
+ -- Don't use margins if it's visible only temporarily.
+ if not state.osc_visible or get_hidetimeout() >= 0 or
+ (state.fullscreen and not user_opts.showfullscreen) or
+ (not state.fullscreen and not user_opts.showwindowed)
+ then
+ margins = {l = 0, r = 0, t = 0, b = 0}
+ end
+
+ if user_opts.boxvideo then
+ -- check whether any margin option has a non-default value
+ local margins_used = false
+
+ if not state.using_video_margins then
+ for _, opt in ipairs(margins_opts) do
+ if mp.get_property_number(opt[2], 0.0) ~= 0.0 then
+ margins_used = true
+ end
+ end
+ end
+
+ if not margins_used then
+ for _, opt in ipairs(margins_opts) do
+ local v = margins[opt[1]]
+ if v ~= 0 or state.using_video_margins then
+ mp.set_property_number(opt[2], v)
+ state.using_video_margins = true
+ end
+ end
+ end
+ else
+ reset_margins()
+ end
+
+ mp.set_property_native("user-data/osc/margins", margins)
+end
+
+function shutdown()
+ reset_margins()
+ mp.del_property("user-data/osc")
+end
+
+--
+-- Other important stuff
+--
+
+
+function show_osc()
+ -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
+ if not state.enabled then return end
+
+ msg.trace("show_osc")
+ --remember last time of invocation (mouse move)
+ state.showtime = mp.get_time()
+
+ osc_visible(true)
+
+ if user_opts.fadeduration > 0 then
+ state.anitype = nil
+ end
+end
+
+function hide_osc()
+ msg.trace("hide_osc")
+ if not state.enabled then
+ -- typically hide happens at render() from tick(), but now tick() is
+ -- no-op and won't render again to remove the osc, so do that manually.
+ state.osc_visible = false
+ render_wipe()
+ elseif user_opts.fadeduration > 0 then
+ if state.osc_visible then
+ state.anitype = "out"
+ request_tick()
+ end
+ else
+ osc_visible(false)
+ end
+end
+
+function osc_visible(visible)
+ if state.osc_visible ~= visible then
+ state.osc_visible = visible
+ update_margins()
+ end
+ request_tick()
+end
+
+function pause_state(name, enabled)
+ state.paused = enabled
+ request_tick()
+end
+
+function cache_state(name, st)
+ state.cache_state = st
+ request_tick()
+end
+
+-- Request that tick() is called (which typically re-renders the OSC).
+-- The tick is then either executed immediately, or rate-limited if it was
+-- called a small time ago.
+function request_tick()
+ if state.tick_timer == nil then
+ state.tick_timer = mp.add_timeout(0, tick)
+ end
+
+ if not state.tick_timer:is_enabled() then
+ local now = mp.get_time()
+ local timeout = tick_delay - (now - state.tick_last_time)
+ if timeout < 0 then
+ timeout = 0
+ end
+ state.tick_timer.timeout = timeout
+ state.tick_timer:resume()
+ end
+end
+
+function mouse_leave()
+ if get_hidetimeout() >= 0 then
+ hide_osc()
+ end
+ -- reset mouse position
+ state.last_mouseX, state.last_mouseY = nil, nil
+ state.mouse_in_window = false
+end
+
+function request_init()
+ state.initREQ = true
+ request_tick()
+end
+
+-- Like request_init(), but also request an immediate update
+function request_init_resize()
+ request_init()
+ -- ensure immediate update
+ state.tick_timer:kill()
+ state.tick_timer.timeout = 0
+ state.tick_timer:resume()
+end
+
+function render_wipe()
+ msg.trace("render_wipe()")
+ state.osd.data = "" -- allows set_osd to immediately update on enable
+ state.osd:remove()
+end
+
+function render()
+ msg.trace("rendering")
+ local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
+ local mouseX, mouseY = get_virt_mouse_pos()
+ local now = mp.get_time()
+
+ -- check if display changed, if so request reinit
+ if state.mp_screen_sizeX ~= current_screen_sizeX
+ or state.mp_screen_sizeY ~= current_screen_sizeY then
+
+ request_init_resize()
+
+ state.mp_screen_sizeX = current_screen_sizeX
+ state.mp_screen_sizeY = current_screen_sizeY
+ end
+
+ -- init management
+ if state.active_element then
+ -- mouse is held down on some element - keep ticking and ignore initReq
+ -- till it's released, or else the mouse-up (click) will misbehave or
+ -- get ignored. that's because osc_init() recreates the osc elements,
+ -- but mouse handling depends on the elements staying unmodified
+ -- between mouse-down and mouse-up (using the index active_element).
+ request_tick()
+ elseif state.initREQ then
+ osc_init()
+ state.initREQ = false
+
+ -- store initial mouse position
+ if (state.last_mouseX == nil or state.last_mouseY == nil)
+ and not (mouseX == nil or mouseY == nil) then
+
+ state.last_mouseX, state.last_mouseY = mouseX, mouseY
+ end
+ end
+
+
+ -- fade animation
+ if state.anitype ~= nil then
+
+ if state.anistart == nil then
+ state.anistart = now
+ end
+
+ if now < state.anistart + (user_opts.fadeduration / 1000) then
+
+ if state.anitype == "in" then --fade in
+ osc_visible(true)
+ state.animation = scale_value(state.anistart,
+ (state.anistart + (user_opts.fadeduration / 1000)),
+ 255, 0, now)
+ elseif state.anitype == "out" then --fade out
+ state.animation = scale_value(state.anistart,
+ (state.anistart + (user_opts.fadeduration / 1000)),
+ 0, 255, now)
+ end
+
+ else
+ if state.anitype == "out" then
+ osc_visible(false)
+ end
+ kill_animation()
+ end
+ else
+ kill_animation()
+ end
+
+ --mouse show/hide area
+ for k,cords in pairs(osc_param.areas["showhide"]) do
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
+ end
+ if osc_param.areas["showhide_wc"] then
+ for k,cords in pairs(osc_param.areas["showhide_wc"]) do
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide_wc")
+ end
+ else
+ set_virt_mouse_area(0, 0, 0, 0, "showhide_wc")
+ end
+ do_enable_keybindings()
+
+ --mouse input area
+ local mouse_over_osc = false
+
+ for _,cords in ipairs(osc_param.areas["input"]) do
+ if state.osc_visible then -- activate only when OSC is actually visible
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
+ end
+ if state.osc_visible ~= state.input_enabled then
+ if state.osc_visible then
+ mp.enable_key_bindings("input")
+ else
+ mp.disable_key_bindings("input")
+ end
+ state.input_enabled = state.osc_visible
+ end
+
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+
+ if osc_param.areas["window-controls"] then
+ for _,cords in ipairs(osc_param.areas["window-controls"]) do
+ if state.osc_visible then -- activate only when OSC is actually visible
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls")
+ end
+ if state.osc_visible ~= state.windowcontrols_buttons then
+ if state.osc_visible then
+ mp.enable_key_bindings("window-controls")
+ else
+ mp.disable_key_bindings("window-controls")
+ end
+ state.windowcontrols_buttons = state.osc_visible
+ end
+
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+ end
+
+ if osc_param.areas["window-controls-title"] then
+ for _,cords in ipairs(osc_param.areas["window-controls-title"]) do
+ if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
+ mouse_over_osc = true
+ end
+ end
+ end
+
+ -- autohide
+ if state.showtime ~= nil and get_hidetimeout() >= 0 then
+ local timeout = state.showtime + (get_hidetimeout() / 1000) - now
+ if timeout <= 0 then
+ if state.active_element == nil and not mouse_over_osc then
+ hide_osc()
+ end
+ else
+ -- the timer is only used to recheck the state and to possibly run
+ -- the code above again
+ if not state.hide_timer then
+ state.hide_timer = mp.add_timeout(0, tick)
+ end
+ state.hide_timer.timeout = timeout
+ -- re-arm
+ state.hide_timer:kill()
+ state.hide_timer:resume()
+ end
+ end
+
+
+ -- actual rendering
+ local ass = assdraw.ass_new()
+
+ -- Messages
+ render_message(ass)
+
+ -- actual OSC
+ if state.osc_visible then
+ render_elements(ass)
+ end
+
+ -- submit
+ set_osd(osc_param.playresy * osc_param.display_aspect,
+ osc_param.playresy, ass.text, 1000)
+end
+
+--
+-- Eventhandling
+--
+
+local function element_has_action(element, action)
+ return element and element.eventresponder and
+ element.eventresponder[action]
+end
+
+function process_event(source, what)
+ local action = string.format("%s%s", source,
+ what and ("_" .. what) or "")
+
+ if what == "down" or what == "press" then
+
+ for n = 1, #elements do
+
+ if mouse_hit(elements[n]) and
+ elements[n].eventresponder and
+ (elements[n].eventresponder[source .. "_up"] or
+ elements[n].eventresponder[action]) then
+
+ if what == "down" then
+ state.active_element = n
+ state.active_event_source = source
+ end
+ -- fire the down or press event if the element has one
+ if element_has_action(elements[n], action) then
+ elements[n].eventresponder[action](elements[n])
+ end
+
+ end
+ end
+
+ elseif what == "up" then
+
+ if elements[state.active_element] then
+ local n = state.active_element
+
+ if n == 0 then
+ --click on background (does not work)
+ elseif element_has_action(elements[n], action) and
+ mouse_hit(elements[n]) then
+
+ elements[n].eventresponder[action](elements[n])
+ end
+
+ --reset active element
+ if element_has_action(elements[n], "reset") then
+ elements[n].eventresponder["reset"](elements[n])
+ end
+
+ end
+ state.active_element = nil
+ state.mouse_down_counter = 0
+
+ elseif source == "mouse_move" then
+
+ state.mouse_in_window = true
+
+ local mouseX, mouseY = get_virt_mouse_pos()
+ if user_opts.minmousemove == 0 or
+ ((state.last_mouseX ~= nil and state.last_mouseY ~= nil) and
+ ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove)
+ or (math.abs(mouseY - state.last_mouseY) >= user_opts.minmousemove)
+ )
+ ) then
+ show_osc()
+ end
+ state.last_mouseX, state.last_mouseY = mouseX, mouseY
+
+ local n = state.active_element
+ if element_has_action(elements[n], action) then
+ elements[n].eventresponder[action](elements[n])
+ end
+ end
+
+ -- ensure rendering after any (mouse) event - icons could change etc
+ request_tick()
+end
+
+
+local logo_lines = {
+ -- White border
+ "{\\c&HE5E5E5&\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 {\\p0}",
+ -- Purple fill
+ "{\\c&H682167&\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42{\\p0}",
+ -- Darker fill
+ "{\\c&H430142&\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}",
+ -- White fill
+ "{\\c&HDDDBDD&\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}",
+ -- Triangle
+ "{\\c&H691F69&\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}",
+}
+
+local santa_hat_lines = {
+ -- Pompoms
+ "{\\c&HC0C0C0&\\p6}m 500 -323 b 491 -322 481 -318 475 -311 465 -312 456 -319 446 -318 434 -314 427 -304 417 -297 410 -290 404 -282 395 -278 390 -274 387 -267 381 -265 377 -261 379 -254 384 -253 397 -244 409 -232 425 -228 437 -228 446 -218 457 -217 462 -216 466 -213 468 -209 471 -205 477 -203 482 -206 491 -211 499 -217 508 -222 532 -235 556 -249 576 -267 584 -272 584 -284 578 -290 569 -305 550 -312 533 -309 523 -310 515 -316 507 -321 505 -323 503 -323 500 -323{\\p0}",
+ "{\\c&HE0E0E0&\\p6}m 315 -260 b 286 -258 259 -240 246 -215 235 -210 222 -215 211 -211 204 -188 177 -176 172 -151 170 -139 163 -128 154 -121 143 -103 141 -81 143 -60 139 -46 125 -34 129 -17 132 -1 134 16 142 30 145 56 161 80 181 96 196 114 210 133 231 144 266 153 303 138 328 115 373 79 401 28 423 -24 446 -73 465 -123 483 -174 487 -199 467 -225 442 -227 421 -232 402 -242 384 -254 364 -259 342 -250 322 -260 320 -260 317 -261 315 -260{\\p0}",
+ -- Main cap
+ "{\\c&H0000F0&\\p6}m 1151 -523 b 1016 -516 891 -458 769 -406 693 -369 624 -319 561 -262 526 -252 465 -235 479 -187 502 -147 551 -135 588 -111 1115 165 1379 232 1909 761 1926 800 1952 834 1987 858 2020 883 2053 912 2065 952 2088 1000 2146 962 2139 919 2162 836 2156 747 2143 662 2131 615 2116 567 2122 517 2120 410 2090 306 2089 199 2092 147 2071 99 2034 64 1987 5 1928 -41 1869 -86 1777 -157 1712 -256 1629 -337 1578 -389 1521 -436 1461 -476 1407 -509 1343 -507 1284 -515 1240 -519 1195 -521 1151 -523{\\p0}",
+ -- Cap shadow
+ "{\\c&H0000AA&\\p6}m 1657 248 b 1658 254 1659 261 1660 267 1669 276 1680 284 1689 293 1695 302 1700 311 1707 320 1716 325 1726 330 1735 335 1744 347 1752 360 1761 371 1753 352 1754 331 1753 311 1751 237 1751 163 1751 90 1752 64 1752 37 1767 14 1778 -3 1785 -24 1786 -45 1786 -60 1786 -77 1774 -87 1760 -96 1750 -78 1751 -65 1748 -37 1750 -8 1750 20 1734 78 1715 134 1699 192 1694 211 1689 231 1676 246 1671 251 1661 255 1657 248 m 1909 541 b 1914 542 1922 549 1917 539 1919 520 1921 502 1919 483 1918 458 1917 433 1915 407 1930 373 1942 338 1947 301 1952 270 1954 238 1951 207 1946 214 1947 229 1945 239 1939 278 1936 318 1924 356 1923 362 1913 382 1912 364 1906 301 1904 237 1891 175 1887 150 1892 126 1892 101 1892 68 1893 35 1888 2 1884 -9 1871 -20 1859 -14 1851 -6 1854 9 1854 20 1855 58 1864 95 1873 132 1883 179 1894 225 1899 273 1908 362 1910 451 1909 541{\\p0}",
+ -- Brim and tip pompom
+ "{\\c&HF8F8F8&\\p6}m 626 -191 b 565 -155 486 -196 428 -151 387 -115 327 -101 304 -47 273 2 267 59 249 113 219 157 217 213 215 265 217 309 260 302 285 283 373 264 465 264 555 257 608 252 655 292 709 287 759 294 816 276 863 298 903 340 972 324 1012 367 1061 394 1125 382 1167 424 1213 462 1268 482 1322 506 1385 546 1427 610 1479 662 1510 690 1534 725 1566 752 1611 796 1664 830 1703 880 1740 918 1747 986 1805 1005 1863 991 1897 932 1916 880 1914 823 1945 777 1961 725 1979 673 1957 622 1938 575 1912 534 1862 515 1836 473 1790 417 1755 351 1697 305 1658 266 1633 216 1593 176 1574 138 1539 116 1497 110 1448 101 1402 77 1371 37 1346 -16 1295 15 1254 6 1211 -27 1170 -62 1121 -86 1072 -104 1027 -128 976 -133 914 -130 851 -137 794 -162 740 -181 679 -168 626 -191 m 2051 917 b 1971 932 1929 1017 1919 1091 1912 1149 1923 1214 1970 1254 2000 1279 2027 1314 2066 1325 2139 1338 2212 1295 2254 1238 2281 1203 2287 1158 2282 1116 2292 1061 2273 1006 2229 970 2206 941 2167 938 2138 918{\\p0}",
+}
+
+-- called by mpv on every frame
+function tick()
+ if state.marginsREQ == true then
+ update_margins()
+ state.marginsREQ = false
+ end
+
+ if not state.enabled then return end
+
+ if state.idle then
+
+ -- render idle message
+ msg.trace("idle message")
+ local _, _, display_aspect = mp.get_osd_size()
+ if display_aspect == 0 then
+ return
+ end
+ local display_h = 360
+ local display_w = display_h * display_aspect
+ -- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800
+ local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140
+ local line_prefix = ("{\\rDefault\\an7\\1a&H00&\\bord0\\shad0\\pos(%f,%f)}"):format(icon_x, icon_y)
+
+ local ass = assdraw.ass_new()
+ -- mpv logo
+ if user_opts.idlescreen then
+ for i, line in ipairs(logo_lines) do
+ ass:new_event()
+ ass:append(line_prefix .. line)
+ end
+ end
+
+ -- Santa hat
+ if is_december and user_opts.idlescreen and not user_opts.greenandgrumpy then
+ for i, line in ipairs(santa_hat_lines) do
+ ass:new_event()
+ ass:append(line_prefix .. line)
+ end
+ end
+
+ if user_opts.idlescreen then
+ ass:new_event()
+ ass:pos(display_w / 2, icon_y + 65)
+ ass:an(8)
+ ass:append("Drop files or URLs to play here.")
+ end
+ set_osd(display_w, display_h, ass.text, -1000)
+
+ if state.showhide_enabled then
+ mp.disable_key_bindings("showhide")
+ mp.disable_key_bindings("showhide_wc")
+ state.showhide_enabled = false
+ end
+
+
+ elseif state.fullscreen and user_opts.showfullscreen
+ or (not state.fullscreen and user_opts.showwindowed) then
+
+ -- render the OSC
+ render()
+ else
+ -- Flush OSD
+ render_wipe()
+ end
+
+ state.tick_last_time = mp.get_time()
+
+ if state.anitype ~= nil then
+ -- state.anistart can be nil - animation should now start, or it can
+ -- be a timestamp when it started. state.idle has no animation.
+ if not state.idle and
+ (not state.anistart or
+ mp.get_time() < 1 + state.anistart + user_opts.fadeduration/1000)
+ then
+ -- animating or starting, or still within 1s past the deadline
+ request_tick()
+ else
+ kill_animation()
+ end
+ end
+end
+
+function do_enable_keybindings()
+ if state.enabled then
+ if not state.showhide_enabled then
+ mp.enable_key_bindings("showhide", "allow-vo-dragging+allow-hide-cursor")
+ mp.enable_key_bindings("showhide_wc", "allow-vo-dragging+allow-hide-cursor")
+ end
+ state.showhide_enabled = true
+ end
+end
+
+function enable_osc(enable)
+ state.enabled = enable
+ if enable then
+ do_enable_keybindings()
+ else
+ hide_osc() -- acts immediately when state.enabled == false
+ if state.showhide_enabled then
+ mp.disable_key_bindings("showhide")
+ mp.disable_key_bindings("showhide_wc")
+ end
+ state.showhide_enabled = false
+ end
+end
+
+-- duration is observed for the sole purpose of updating chapter markers
+-- positions. live streams with chapters are very rare, and the update is also
+-- expensive (with request_init), so it's only observed when we have chapters
+-- and the user didn't disable the livemarkers option (update_duration_watch).
+function on_duration() request_init() end
+
+local duration_watched = false
+function update_duration_watch()
+ local want_watch = user_opts.livemarkers and
+ (mp.get_property_number("chapters", 0) or 0) > 0 and
+ true or false -- ensure it's a boolean
+
+ if want_watch ~= duration_watched then
+ if want_watch then
+ mp.observe_property("duration", nil, on_duration)
+ else
+ mp.unobserve_property(on_duration)
+ end
+ duration_watched = want_watch
+ end
+end
+
+validate_user_opts()
+update_duration_watch()
+
+mp.register_event("shutdown", shutdown)
+mp.register_event("start-file", request_init)
+mp.observe_property("track-list", nil, request_init)
+mp.observe_property("playlist", nil, request_init)
+mp.observe_property("chapter-list", "native", function(_, list)
+ list = list or {} -- safety, shouldn't return nil
+ table.sort(list, function(a, b) return a.time < b.time end)
+ state.chapter_list = list
+ update_duration_watch()
+ request_init()
+end)
+
+mp.register_script_message("osc-message", show_message)
+mp.register_script_message("osc-chapterlist", function(dur)
+ show_message(get_chapterlist(), dur)
+end)
+mp.register_script_message("osc-playlist", function(dur)
+ show_message(get_playlist(), dur)
+end)
+mp.register_script_message("osc-tracklist", function(dur)
+ local msg = {}
+ for k,v in pairs(nicetypes) do
+ table.insert(msg, get_tracklist(k))
+ end
+ show_message(table.concat(msg, '\n\n'), dur)
+end)
+
+mp.observe_property("fullscreen", "bool",
+ function(name, val)
+ state.fullscreen = val
+ state.marginsREQ = true
+ request_init_resize()
+ end
+)
+mp.observe_property("border", "bool",
+ function(name, val)
+ state.border = val
+ request_init_resize()
+ end
+)
+mp.observe_property("window-maximized", "bool",
+ function(name, val)
+ state.maximized = val
+ request_init_resize()
+ end
+)
+mp.observe_property("idle-active", "bool",
+ function(name, val)
+ state.idle = val
+ request_tick()
+ end
+)
+mp.observe_property("pause", "bool", pause_state)
+mp.observe_property("demuxer-cache-state", "native", cache_state)
+mp.observe_property("vo-configured", "bool", function(name, val)
+ request_tick()
+end)
+mp.observe_property("playback-time", "number", function(name, val)
+ request_tick()
+end)
+mp.observe_property("osd-dimensions", "native", function(name, val)
+ -- (we could use the value instead of re-querying it all the time, but then
+ -- we might have to worry about property update ordering)
+ request_init_resize()
+end)
+
+-- mouse show/hide bindings
+mp.set_key_bindings({
+ {"mouse_move", function(e) process_event("mouse_move", nil) end},
+ {"mouse_leave", mouse_leave},
+}, "showhide", "force")
+mp.set_key_bindings({
+ {"mouse_move", function(e) process_event("mouse_move", nil) end},
+ {"mouse_leave", mouse_leave},
+}, "showhide_wc", "force")
+do_enable_keybindings()
+
+--mouse input bindings
+mp.set_key_bindings({
+ {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
+ function(e) process_event("mbtn_left", "down") end},
+ {"shift+mbtn_left", function(e) process_event("shift+mbtn_left", "up") end,
+ function(e) process_event("shift+mbtn_left", "down") end},
+ {"mbtn_right", function(e) process_event("mbtn_right", "up") end,
+ function(e) process_event("mbtn_right", "down") end},
+ -- alias to shift_mbtn_left for single-handed mouse use
+ {"mbtn_mid", function(e) process_event("shift+mbtn_left", "up") end,
+ function(e) process_event("shift+mbtn_left", "down") end},
+ {"wheel_up", function(e) process_event("wheel_up", "press") end},
+ {"wheel_down", function(e) process_event("wheel_down", "press") end},
+ {"mbtn_left_dbl", "ignore"},
+ {"shift+mbtn_left_dbl", "ignore"},
+ {"mbtn_right_dbl", "ignore"},
+}, "input", "force")
+mp.enable_key_bindings("input")
+
+mp.set_key_bindings({
+ {"mbtn_left", function(e) process_event("mbtn_left", "up") end,
+ function(e) process_event("mbtn_left", "down") end},
+}, "window-controls", "force")
+mp.enable_key_bindings("window-controls")
+
+function get_hidetimeout()
+ if user_opts.visibility == "always" then
+ return -1 -- disable autohide
+ end
+ return user_opts.hidetimeout
+end
+
+function always_on(val)
+ if state.enabled then
+ if val then
+ show_osc()
+ else
+ hide_osc()
+ end
+ end
+end
+
+-- mode can be auto/always/never/cycle
+-- the modes only affect internal variables and not stored on its own.
+function visibility_mode(mode, no_osd)
+ if mode == "cycle" then
+ if not state.enabled then
+ mode = "auto"
+ elseif user_opts.visibility ~= "always" then
+ mode = "always"
+ else
+ mode = "never"
+ end
+ end
+
+ if mode == "auto" then
+ always_on(false)
+ enable_osc(true)
+ elseif mode == "always" then
+ enable_osc(true)
+ always_on(true)
+ elseif mode == "never" then
+ enable_osc(false)
+ else
+ msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
+ return
+ end
+
+ user_opts.visibility = mode
+ mp.set_property_native("user-data/osc/visibility", mode)
+
+ if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
+ mp.osd_message("OSC visibility: " .. mode)
+ end
+
+ -- Reset the input state on a mode change. The input state will be
+ -- recalculated on the next render cycle, except in 'never' mode where it
+ -- will just stay disabled.
+ mp.disable_key_bindings("input")
+ mp.disable_key_bindings("window-controls")
+ state.input_enabled = false
+
+ update_margins()
+ request_tick()
+end
+
+function idlescreen_visibility(mode, no_osd)
+ if mode == "cycle" then
+ if user_opts.idlescreen then
+ mode = "no"
+ else
+ mode = "yes"
+ end
+ end
+
+ if mode == "yes" then
+ user_opts.idlescreen = true
+ else
+ user_opts.idlescreen = false
+ end
+
+ mp.set_property_native("user-data/osc/idlescreen", user_opts.idlescreen)
+
+ if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
+ mp.osd_message("OSC logo visibility: " .. tostring(mode))
+ end
+
+ request_tick()
+end
+
+visibility_mode(user_opts.visibility, true)
+mp.register_script_message("osc-visibility", visibility_mode)
+mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
+
+mp.register_script_message("osc-idlescreen", idlescreen_visibility)
+
+set_virt_mouse_area(0, 0, 0, 0, "input")
+set_virt_mouse_area(0, 0, 0, 0, "window-controls")
diff --git a/player/lua/stats.lua b/player/lua/stats.lua
new file mode 100644
index 0000000..16e8b68
--- /dev/null
+++ b/player/lua/stats.lua
@@ -0,0 +1,1417 @@
+-- Display some stats.
+--
+-- Please consult the readme for information about usage and configuration:
+-- https://github.com/Argon-/mpv-stats
+--
+-- Please note: not every property is always available and therefore not always
+-- visible.
+
+local mp = require 'mp'
+local options = require 'mp.options'
+local utils = require 'mp.utils'
+
+-- Options
+local o = {
+ -- Default key bindings
+ key_page_1 = "1",
+ key_page_2 = "2",
+ key_page_3 = "3",
+ key_page_4 = "4",
+ key_page_0 = "0",
+ -- For pages which support scrolling
+ key_scroll_up = "UP",
+ key_scroll_down = "DOWN",
+ scroll_lines = 1,
+
+ duration = 4,
+ redraw_delay = 1, -- acts as duration in the toggling case
+ ass_formatting = true,
+ persistent_overlay = false, -- whether the stats can be overwritten by other output
+ print_perfdata_passes = false, -- when true, print the full information about all passes
+ filter_params_max_length = 100, -- a filter list longer than this many characters will be shown one filter per line instead
+ show_frame_info = false, -- whether to show the current frame info
+ debug = false,
+
+ -- Graph options and style
+ plot_perfdata = true,
+ plot_vsync_ratio = true,
+ plot_vsync_jitter = true,
+ plot_tonemapping_lut = false,
+ skip_frames = 5,
+ global_max = true,
+ flush_graph_data = true, -- clear data buffers when toggling
+ plot_bg_border_color = "0000FF",
+ plot_bg_color = "262626",
+ plot_color = "FFFFFF",
+
+ -- Text style
+ font = "sans-serif",
+ font_mono = "monospace", -- monospaced digits are sufficient
+ font_size = 8,
+ font_color = "FFFFFF",
+ border_size = 0.8,
+ border_color = "262626",
+ shadow_x_offset = 0.0,
+ shadow_y_offset = 0.0,
+ shadow_color = "000000",
+ alpha = "11",
+
+ -- Custom header for ASS tags to style the text output.
+ -- Specifying this will ignore the text style values above and just
+ -- use this string instead.
+ custom_header = "",
+
+ -- Text formatting
+ -- With ASS
+ ass_nl = "\\N",
+ ass_indent = "\\h\\h\\h\\h\\h",
+ ass_prefix_sep = "\\h\\h",
+ ass_b1 = "{\\b1}",
+ ass_b0 = "{\\b0}",
+ ass_it1 = "{\\i1}",
+ ass_it0 = "{\\i0}",
+ -- Without ASS
+ no_ass_nl = "\n",
+ no_ass_indent = "\t",
+ no_ass_prefix_sep = " ",
+ no_ass_b1 = "\027[1m",
+ no_ass_b0 = "\027[0m",
+ no_ass_it1 = "\027[3m",
+ no_ass_it0 = "\027[0m",
+
+ bindlist = "no", -- print page 4 to the terminal on startup and quit mpv
+}
+options.read_options(o)
+
+local format = string.format
+local max = math.max
+local min = math.min
+
+-- Function used to record performance data
+local recorder = nil
+-- Timer used for redrawing (toggling) and clearing the screen (oneshot)
+local display_timer = nil
+-- Timer used to update cache stats.
+local cache_recorder_timer = nil
+-- Current page and <page key>:<page function> mappings
+local curr_page = o.key_page_1
+local pages = {}
+local scroll_bound = false
+local tm_viz_prev = nil
+-- Save these sequences locally as we'll need them a lot
+local ass_start = mp.get_property_osd("osd-ass-cc/0")
+local ass_stop = mp.get_property_osd("osd-ass-cc/1")
+-- Ring buffers for the values used to construct a graph.
+-- .pos denotes the current position, .len the buffer length
+-- .max is the max value in the corresponding buffer
+local vsratio_buf, vsjitter_buf
+local function init_buffers()
+ vsratio_buf = {0, pos = 1, len = 50, max = 0}
+ vsjitter_buf = {0, pos = 1, len = 50, max = 0}
+end
+local cache_ahead_buf, cache_speed_buf
+local perf_buffers = {}
+
+local function graph_add_value(graph, value)
+ graph.pos = (graph.pos % graph.len) + 1
+ graph[graph.pos] = value
+ graph.max = max(graph.max, value)
+end
+
+-- "\\<U+2060>" in UTF-8 (U+2060 is WORD-JOINER)
+local ESC_BACKSLASH = "\\" .. string.char(0xE2, 0x81, 0xA0)
+
+local function no_ASS(t)
+ if not o.use_ass then
+ return t
+ elseif not o.persistent_overlay then
+ -- mp.osd_message supports ass-escape using osd-ass-cc/{0|1}
+ return ass_stop .. t .. ass_start
+ else
+ -- mp.set_osd_ass doesn't support ass-escape. roll our own.
+ -- similar to mpv's sub/osd_libass.c:mangle_ass(...), excluding
+ -- space after newlines because no_ASS is not used with multi-line.
+ -- space at the beginning is replaced with "\\h" because it matters
+ -- at the beginning of a line, and we can't know where our output
+ -- ends up. no issue if it ends up at the middle of a line.
+ return tostring(t)
+ :gsub("\\", ESC_BACKSLASH)
+ :gsub("{", "\\{")
+ :gsub("^ ", "\\h")
+ end
+end
+
+
+local function b(t)
+ return o.b1 .. t .. o.b0
+end
+
+
+local function it(t)
+ return o.it1 .. t .. o.it0
+end
+
+
+local function text_style()
+ if not o.use_ass then
+ return ""
+ end
+ if o.custom_header and o.custom_header ~= "" then
+ return o.custom_header
+ else
+ local has_shadow = mp.get_property('osd-back-color'):sub(2, 3) == '00'
+ return format("{\\r\\an7\\fs%d\\fn%s\\bord%f\\3c&H%s&" ..
+ "\\1c&H%s&\\1a&H%s&\\3a&H%s&" ..
+ (has_shadow and "\\4a&H%s&\\xshad%f\\yshad%f\\4c&H%s&}" or "}"),
+ o.font_size, o.font, o.border_size,
+ o.border_color, o.font_color, o.alpha, o.alpha, o.alpha,
+ o.shadow_x_offset, o.shadow_y_offset, o.shadow_color)
+ end
+end
+
+
+local function has_vo_window()
+ return mp.get_property_native("vo-configured") and mp.get_property_native("video-osd")
+end
+
+
+-- Generate a graph from the given values.
+-- Returns an ASS formatted vector drawing as string.
+--
+-- values: Array/table of numbers representing the data. Used like a ring buffer
+-- it will get iterated backwards `len` times starting at position `i`.
+-- i : Index of the latest data value in `values`.
+-- len : The length/amount of numbers in `values`.
+-- v_max : The maximum number in `values`. It is used to scale all data
+-- values to a range of 0 to `v_max`.
+-- v_avg : The average number in `values`. It is used to try and center graphs
+-- if possible. May be left as nil
+-- scale : A value that will be multiplied with all data values.
+-- x_tics: Horizontal width multiplier for the steps
+local function generate_graph(values, i, len, v_max, v_avg, scale, x_tics)
+ -- Check if at least one value exists
+ if not values[i] then
+ return ""
+ end
+
+ local x_max = (len - 1) * x_tics
+ local y_offset = o.border_size
+ local y_max = o.font_size * 0.66
+ local x = 0
+
+ if v_max > 0 then
+ -- try and center the graph if possible, but avoid going above `scale`
+ if v_avg and v_avg > 0 then
+ scale = min(scale, v_max / (2 * v_avg))
+ end
+ scale = scale * y_max / v_max
+ end -- else if v_max==0 then all values are 0 and scale doesn't matter
+
+ local s = {format("m 0 0 n %f %f l ", x, y_max - scale * values[i])}
+ i = ((i - 2) % len) + 1
+
+ for p = 1, len - 1 do
+ if values[i] then
+ x = x - x_tics
+ s[#s+1] = format("%f %f ", x, y_max - scale * values[i])
+ end
+ i = ((i - 2) % len) + 1
+ end
+
+ s[#s+1] = format("%f %f %f %f", x, y_max, 0, y_max)
+
+ local bg_box = format("{\\bord0.5}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0",
+ o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max)
+ return format("%s{\\r}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s",
+ o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style())
+end
+
+
+local function append(s, str, attr)
+ if not str then
+ return false
+ end
+ attr.prefix_sep = attr.prefix_sep or o.prefix_sep
+ attr.indent = attr.indent or o.indent
+ attr.nl = attr.nl or o.nl
+ attr.suffix = attr.suffix or ""
+ attr.prefix = attr.prefix or ""
+ attr.no_prefix_markup = attr.no_prefix_markup or false
+ attr.prefix = attr.no_prefix_markup and attr.prefix or b(attr.prefix)
+ s[#s+1] = format("%s%s%s%s%s%s", attr.nl, attr.indent,
+ attr.prefix, attr.prefix_sep, no_ASS(str), attr.suffix)
+ return true
+end
+
+
+-- Format and append a property.
+-- A property whose value is either `nil` or empty (hereafter called "invalid")
+-- is skipped and not appended.
+-- Returns `false` in case nothing was appended, otherwise `true`.
+--
+-- s : Table containing strings.
+-- prop : The property to query and format (based on its OSD representation).
+-- attr : Optional table to overwrite certain (formatting) attributes for
+-- this property.
+-- exclude: Optional table containing keys which are considered invalid values
+-- for this property. Specifying this will replace empty string as
+-- default invalid value (nil is always invalid).
+local function append_property(s, prop, attr, excluded)
+ excluded = excluded or {[""] = true}
+ local ret = mp.get_property_osd(prop)
+ if not ret or excluded[ret] then
+ if o.debug then
+ print("No value for property: " .. prop)
+ end
+ return false
+ end
+ return append(s, ret, attr)
+end
+
+local function sorted_keys(t, comp_fn)
+ local keys = {}
+ for k,_ in pairs(t) do
+ keys[#keys+1] = k
+ end
+ table.sort(keys, comp_fn)
+ return keys
+end
+
+local function append_perfdata(s, dedicated_page, print_passes)
+ local vo_p = mp.get_property_native("vo-passes")
+ if not vo_p then
+ return
+ end
+
+ local ds = mp.get_property_bool("display-sync-active", false)
+ local target_fps = ds and mp.get_property_number("display-fps", 0)
+ or mp.get_property_number("container-fps", 0)
+ if target_fps > 0 then target_fps = 1 / target_fps * 1e9 end
+
+ -- Sums of all last/avg/peak values
+ local last_s, avg_s, peak_s = {}, {}, {}
+ for frame, data in pairs(vo_p) do
+ last_s[frame], avg_s[frame], peak_s[frame] = 0, 0, 0
+ for _, pass in ipairs(data) do
+ last_s[frame] = last_s[frame] + pass["last"]
+ avg_s[frame] = avg_s[frame] + pass["avg"]
+ peak_s[frame] = peak_s[frame] + pass["peak"]
+ end
+ end
+
+ -- Pretty print measured time
+ local function pp(i)
+ -- rescale to microseconds for a saner display
+ return format("%5d", i / 1000)
+ end
+
+ -- Format n/m with a font weight based on the ratio
+ local function p(n, m)
+ local i = 0
+ if m > 0 then
+ i = tonumber(n) / m
+ end
+ -- Calculate font weight. 100 is minimum, 400 is normal, 700 bold, 900 is max
+ local w = (700 * math.sqrt(i)) + 200
+ return format("{\\b%d}%2d%%{\\b0}", w, i * 100)
+ end
+
+ -- ensure that the fixed title is one element and every scrollable line is
+ -- also one single element.
+ s[#s+1] = format("%s%s%s%s{\\fs%s}%s%s{\\fs%s}",
+ dedicated_page and "" or o.nl, dedicated_page and "" or o.indent,
+ b("Frame Timings:"), o.prefix_sep, o.font_size * 0.66,
+ "(last/average/peak μs)",
+ dedicated_page and " (hint: scroll with ↑↓)" or "", o.font_size)
+
+ for _,frame in ipairs(sorted_keys(vo_p)) do -- ensure fixed display order
+ local data = vo_p[frame]
+ local f = "%s%s%s{\\fn%s}%s / %s / %s %s%s{\\fn%s}%s%s%s"
+
+ if print_passes then
+ s[#s+1] = format("%s%s%s:", o.nl, o.indent,
+ b(frame:gsub("^%l", string.upper)))
+
+ for _, pass in ipairs(data) do
+ s[#s+1] = format(f, o.nl, o.indent, o.indent,
+ o.font_mono, pp(pass["last"]),
+ pp(pass["avg"]), pp(pass["peak"]),
+ o.prefix_sep .. "\\h\\h", p(pass["last"], last_s[frame]),
+ o.font, o.prefix_sep, o.prefix_sep, pass["desc"])
+
+ if o.plot_perfdata and o.use_ass then
+ -- use the same line that was already started for this iteration
+ s[#s] = s[#s] ..
+ generate_graph(pass["samples"], pass["count"],
+ pass["count"], pass["peak"],
+ pass["avg"], 0.9, 0.25)
+ end
+ end
+
+ -- Print sum of timing values as "Total"
+ s[#s+1] = format(f, o.nl, o.indent, o.indent,
+ o.font_mono, pp(last_s[frame]),
+ pp(avg_s[frame]), pp(peak_s[frame]),
+ o.prefix_sep, b("Total"), o.font, "", "", "")
+ else
+ -- for the simplified view, we just print the sum of each pass
+ s[#s+1] = format(f, o.nl, o.indent, o.indent, o.font_mono,
+ pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]),
+ "", "", o.font, o.prefix_sep, o.prefix_sep,
+ frame:gsub("^%l", string.upper))
+ end
+ end
+end
+
+local function ellipsis(s, maxlen)
+ if not maxlen or s:len() <= maxlen then return s end
+ return s:sub(1, maxlen - 3) .. "..."
+end
+
+-- command prefix tokens to strip - includes generic property commands
+local cmd_prefixes = {
+ osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1,
+ async=1, expand_properties=1, repeatable=1, set=1, add=1, multiply=1,
+ toggle=1, cycle=1, cycle_values=1, ["!reverse"]=1, change_list=1,
+}
+-- commands/writable-properties prefix sub-words (followed by -) to strip
+local name_prefixes = {
+ define=1, delete=1, enable=1, disable=1, dump=1, write=1, drop=1, revert=1,
+ ab=1, hr=1, secondary=1, current=1,
+}
+-- extract a command "subject" from a command string, by removing all
+-- generic prefix tokens and then returning the first interesting sub-word
+-- of the next token. For target-script name we also check another token.
+-- The tokenizer works fine for things we care about - valid mpv commands,
+-- properties and script names, possibly quoted, white-space[s]-separated.
+-- It's decent in practice, and worst case is "incorrect" subject.
+local function cmd_subject(cmd)
+ cmd = cmd:gsub(";.*", ""):gsub("%-", "_") -- only first cmd, s/-/_/
+ local TOKEN = '^%s*["\']?([%w_!]*)' -- captures+ends before (maybe) final "
+ local tok, sname, subw
+
+ repeat tok, cmd = cmd:match(TOKEN .. '["\']?(.*)')
+ until not cmd_prefixes[tok]
+ -- tok is the 1st non-generic command/property name token, cmd is the rest
+
+ sname = tok == "script_message_to" and cmd:match(TOKEN)
+ or tok == "script_binding" and cmd:match(TOKEN .. "/")
+ if sname and sname ~= "" then
+ return "script: " .. sname
+ end
+
+ -- return the first sub-word of tok which is not a useless prefix
+ repeat subw, tok = tok:match("([^_]*)_?(.*)")
+ until tok == "" or not name_prefixes[subw]
+ return subw:len() > 1 and subw or "[unknown]"
+end
+
+-- key names are valid UTF-8, ascii7 except maybe the last/only codepoint.
+-- we count codepoints and ignore wcwidth. no need for grapheme clusters.
+-- our error for alignment is at most one cell (if last CP is double-width).
+-- (if k was valid but arbitrary: we'd count all bytes <0x80 or >=0xc0)
+local function keyname_cells(k)
+ local klen = k:len()
+ if klen > 1 and k:byte(klen) >= 0x80 then -- last/only CP is not ascii7
+ repeat klen = klen-1
+ until klen == 1 or k:byte(klen) >= 0xc0 -- last CP begins at klen
+ end
+ return klen
+end
+
+local function get_kbinfo_lines(width)
+ -- active keys: only highest priority of each key, and not our (stats) keys
+ local bindings = mp.get_property_native("input-bindings", {})
+ local active = {} -- map: key-name -> bind-info
+ for _, bind in pairs(bindings) do
+ if bind.priority >= 0 and (
+ not active[bind.key] or
+ (active[bind.key].is_weak and not bind.is_weak) or
+ (bind.is_weak == active[bind.key].is_weak and
+ bind.priority > active[bind.key].priority)
+ ) and not bind.cmd:find("script-binding stats/__forced_", 1, true)
+ then
+ active[bind.key] = bind
+ end
+ end
+
+ -- make an array, find max key len, add sort keys (.subject/.mods[_count])
+ local ordered = {}
+ local kspaces = "" -- as many spaces as the longest key name
+ for _, bind in pairs(active) do
+ bind.subject = cmd_subject(bind.cmd)
+ if bind.subject ~= "ignore" then
+ ordered[#ordered+1] = bind
+ _,_, bind.mods = bind.key:find("(.*)%+.")
+ _, bind.mods_count = bind.key:gsub("%+.", "")
+ if bind.key:len() > kspaces:len() then
+ kspaces = string.rep(" ", bind.key:len())
+ end
+ end
+ end
+
+ local function align_right(key)
+ return kspaces:sub(keyname_cells(key)) .. key
+ end
+
+ -- sort by: subject, mod(ifier)s count, mods, key-len, lowercase-key, key
+ table.sort(ordered, function(a, b)
+ if a.subject ~= b.subject then
+ return a.subject < b.subject
+ elseif a.mods_count ~= b.mods_count then
+ return a.mods_count < b.mods_count
+ elseif a.mods ~= b.mods then
+ return a.mods < b.mods
+ elseif a.key:len() ~= b.key:len() then
+ return a.key:len() < b.key:len()
+ elseif a.key:lower() ~= b.key:lower() then
+ return a.key:lower() < b.key:lower()
+ else
+ return a.key > b.key -- only case differs, lowercase first
+ end
+ end)
+
+ -- key/subject pre/post formatting for terminal/ass.
+ -- key/subject alignment uses spaces (with mono font if ass)
+ -- word-wrapping is disabled for ass, or cut at 79 for the terminal
+ local LTR = string.char(0xE2, 0x80, 0x8E) -- U+200E Left To Right mark
+ local term = not o.use_ass
+ local kpre = term and "" or format("{\\q2\\fn%s}%s", o.font_mono, LTR)
+ local kpost = term and " " or format(" {\\fn%s}", o.font)
+ local spre = term and kspaces .. " "
+ or format("{\\q2\\fn%s}%s {\\fn%s}{\\fs%d\\u1}",
+ o.font_mono, kspaces, o.font, 1.3*o.font_size)
+ local spost = term and "" or format("{\\u0\\fs%d}", o.font_size)
+ local _, itabs = o.indent:gsub("\t", "")
+ local cutoff = term and (width or 79) - o.indent:len() - itabs * 7 - spre:len()
+
+ -- create the display lines
+ local info_lines = {}
+ local subject = nil
+ for _, bind in ipairs(ordered) do
+ if bind.subject ~= subject then -- new subject (title)
+ subject = bind.subject
+ append(info_lines, "", {})
+ append(info_lines, "", { prefix = spre .. subject .. spost })
+ end
+ if bind.comment then
+ bind.cmd = bind.cmd .. " # " .. bind.comment
+ end
+ append(info_lines, ellipsis(bind.cmd, cutoff),
+ { prefix = kpre .. no_ASS(align_right(bind.key)) .. kpost })
+ end
+ return info_lines
+end
+
+local function append_general_perfdata(s, offset)
+ local perf_info = mp.get_property_native("perf-info") or {}
+ local count = 0
+ for _, data in ipairs(perf_info) do
+ count = count + 1
+ end
+ offset = max(1, min((offset or 1), count))
+
+ local i = 0
+ for _, data in ipairs(perf_info) do
+ i = i + 1
+ if i >= offset then
+ append(s, data.text or data.value, {prefix="["..tostring(i).."] "..data.name..":"})
+
+ if o.plot_perfdata and o.use_ass and data.value then
+ buf = perf_buffers[data.name]
+ if not buf then
+ buf = {0, pos = 1, len = 50, max = 0}
+ perf_buffers[data.name] = buf
+ end
+ graph_add_value(buf, data.value)
+ s[#s+1] = generate_graph(buf, buf.pos, buf.len, buf.max, nil, 0.8, 1)
+ end
+ end
+ end
+ return offset
+end
+
+local function append_display_sync(s)
+ if not mp.get_property_bool("display-sync-active", false) then
+ return
+ end
+
+ local vspeed = append_property(s, "video-speed-correction", {prefix="DS:"})
+ if vspeed then
+ append_property(s, "audio-speed-correction",
+ {prefix="/", nl="", indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ else
+ append_property(s, "audio-speed-correction",
+ {prefix="DS:" .. o.prefix_sep .. " - / ", prefix_sep=""})
+ end
+
+ append_property(s, "mistimed-frame-count", {prefix="Mistimed:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep})
+ append_property(s, "vo-delayed-frame-count", {prefix="Delayed:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep})
+
+ -- As we need to plot some graphs we print jitter and ratio on their own lines
+ if not display_timer.oneshot and (o.plot_vsync_ratio or o.plot_vsync_jitter) and o.use_ass then
+ local ratio_graph = ""
+ local jitter_graph = ""
+ if o.plot_vsync_ratio then
+ ratio_graph = generate_graph(vsratio_buf, vsratio_buf.pos, vsratio_buf.len, vsratio_buf.max, nil, 0.8, 1)
+ end
+ if o.plot_vsync_jitter then
+ jitter_graph = generate_graph(vsjitter_buf, vsjitter_buf.pos, vsjitter_buf.len, vsjitter_buf.max, nil, 0.8, 1)
+ end
+ append_property(s, "vsync-ratio", {prefix="VSync Ratio:", suffix=o.prefix_sep .. ratio_graph})
+ append_property(s, "vsync-jitter", {prefix="VSync Jitter:", suffix=o.prefix_sep .. jitter_graph})
+ else
+ -- Since no graph is needed we can print ratio/jitter on the same line and save some space
+ local vr = append_property(s, "vsync-ratio", {prefix="VSync Ratio:"})
+ append_property(s, "vsync-jitter", {prefix="VSync Jitter:",
+ nl=vr and "" or o.nl,
+ indent=vr and o.prefix_sep .. o.prefix_sep})
+ end
+end
+
+
+local function append_filters(s, prop, prefix)
+ local length = 0
+ local filters = {}
+
+ for _,f in ipairs(mp.get_property_native(prop, {})) do
+ local n = f.name
+ if f.enabled ~= nil and not f.enabled then
+ n = n .. " (disabled)"
+ end
+
+ if f.label ~= nil then
+ n = "@" .. f.label .. ": " .. n
+ end
+
+ local p = {}
+ for _,key in ipairs(sorted_keys(f.params)) do
+ p[#p+1] = key .. "=" .. f.params[key]
+ end
+ if #p > 0 then
+ p = " [" .. table.concat(p, " ") .. "]"
+ else
+ p = ""
+ end
+
+ length = length + n:len() + p:len()
+ filters[#filters+1] = no_ASS(n) .. it(no_ASS(p))
+ end
+
+ if #filters > 0 then
+ local ret
+ if length < o.filter_params_max_length then
+ ret = table.concat(filters, ", ")
+ else
+ local sep = o.nl .. o.indent .. o.indent
+ ret = sep .. table.concat(filters, sep)
+ end
+ s[#s+1] = o.nl .. o.indent .. b(prefix) .. o.prefix_sep .. ret
+ end
+end
+
+
+local function add_header(s)
+ s[#s+1] = text_style()
+end
+
+
+local function add_file(s)
+ append(s, "", {prefix="File:", nl="", indent=""})
+ append_property(s, "filename", {prefix_sep="", nl="", indent=""})
+ if not (mp.get_property_osd("filename") == mp.get_property_osd("media-title")) then
+ append_property(s, "media-title", {prefix="Title:"})
+ end
+
+ local editions = mp.get_property_number("editions")
+ local edition = mp.get_property_number("current-edition")
+ local ed_cond = (edition and editions > 1)
+ if ed_cond then
+ append_property(s, "edition-list/" .. tostring(edition) .. "/title",
+ {prefix="Edition:"})
+ append_property(s, "edition-list/count",
+ {prefix="(" .. tostring(edition + 1) .. "/", suffix=")", nl="",
+ indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ end
+
+ local ch_index = mp.get_property_number("chapter")
+ if ch_index and ch_index >= 0 then
+ append_property(s, "chapter-list/" .. tostring(ch_index) .. "/title", {prefix="Chapter:",
+ nl=ed_cond and "" or o.nl})
+ append_property(s, "chapter-list/count",
+ {prefix="(" .. tostring(ch_index + 1) .. " /", suffix=")", nl="",
+ indent=" ", prefix_sep=" ", no_prefix_markup=true})
+ end
+
+ local fs = append_property(s, "file-size", {prefix="Size:"})
+ append_property(s, "file-format", {prefix="Format/Protocol:",
+ nl=fs and "" or o.nl,
+ indent=fs and o.prefix_sep .. o.prefix_sep})
+
+ local demuxer_cache = mp.get_property_native("demuxer-cache-state", {})
+ if demuxer_cache["fw-bytes"] then
+ demuxer_cache = demuxer_cache["fw-bytes"] -- returns bytes
+ else
+ demuxer_cache = 0
+ end
+ local demuxer_secs = mp.get_property_number("demuxer-cache-duration", 0)
+ if demuxer_cache + demuxer_secs > 0 then
+ append(s, utils.format_bytes_humanized(demuxer_cache), {prefix="Total Cache:"})
+ append(s, format("%.1f", demuxer_secs), {prefix="(", suffix=" sec)", nl="",
+ no_prefix_markup=true, prefix_sep="", indent=o.prefix_sep})
+ end
+end
+
+
+local function crop_noop(w, h, r)
+ return r["crop-x"] == 0 and r["crop-y"] == 0 and
+ r["crop-w"] == w and r["crop-h"] == h
+end
+
+
+local function crop_equal(r, ro)
+ return r["crop-x"] == ro["crop-x"] and r["crop-y"] == ro["crop-y"] and
+ r["crop-w"] == ro["crop-w"] and r["crop-h"] == ro["crop-h"]
+end
+
+
+local function append_resolution(s, r, prefix, w_prop, h_prop, video_res)
+ if not r then
+ return
+ end
+ w_prop = w_prop or "w"
+ h_prop = h_prop or "h"
+ if append(s, r[w_prop], {prefix=prefix}) then
+ append(s, r[h_prop], {prefix="x", nl="", indent=" ", prefix_sep=" ",
+ no_prefix_markup=true})
+ if r["aspect"] ~= nil and not video_res then
+ append(s, format("%.2f:1", r["aspect"]), {prefix="", nl="", indent="",
+ no_prefix_markup=true})
+ append(s, r["aspect-name"], {prefix="(", suffix=")", nl="", indent=" ",
+ prefix_sep="", no_prefix_markup=true})
+ end
+ if r["sar"] ~= nil and video_res then
+ append(s, format("%.2f:1", r["sar"]), {prefix="", nl="", indent="",
+ no_prefix_markup=true})
+ append(s, r["sar-name"], {prefix="(", suffix=")", nl="", indent=" ",
+ prefix_sep="", no_prefix_markup=true})
+ end
+ if r["s"] then
+ append(s, format("%.2f", r["s"]), {prefix="(", suffix="x)", nl="",
+ indent=o.prefix_sep, prefix_sep="",
+ no_prefix_markup=true})
+ end
+ -- We can skip crop if it is the same as video decoded resolution
+ if r["crop-w"] and (not video_res or
+ not crop_noop(r[w_prop], r[h_prop], r)) then
+ append(s, format("[x: %d, y: %d, w: %d, h: %d]",
+ r["crop-x"], r["crop-y"], r["crop-w"], r["crop-h"]),
+ {prefix="", nl="", indent="", no_prefix_markup=true})
+ end
+ end
+end
+
+
+local function pq_eotf(x)
+ if not x then
+ return x;
+ end
+
+ local PQ_M1 = 2610.0 / 4096 * 1.0 / 4
+ local PQ_M2 = 2523.0 / 4096 * 128
+ local PQ_C1 = 3424.0 / 4096
+ local PQ_C2 = 2413.0 / 4096 * 32
+ local PQ_C3 = 2392.0 / 4096 * 32
+
+ x = x ^ (1.0 / PQ_M2)
+ x = max(x - PQ_C1, 0.0) / (PQ_C2 - PQ_C3 * x)
+ x = x ^ (1.0 / PQ_M1)
+ x = x * 10000.0
+
+ return x
+end
+
+
+local function append_hdr(s, hdr, video_out)
+ if not hdr then
+ return
+ end
+
+ local function should_show(val)
+ return val and val ~= 203 and val > 0
+ end
+
+ -- If we are printing video out parameters it is just display, not mastering
+ local display_prefix = video_out and "Display:" or "Mastering display:"
+
+ local indent = ""
+
+ if should_show(hdr["max-cll"]) or should_show(hdr["max-luma"]) then
+ append(s, "", {prefix="HDR10:"})
+ if hdr["min-luma"] and should_show(hdr["max-luma"]) then
+ -- libplacebo uses close to zero values as "defined zero"
+ hdr["min-luma"] = hdr["min-luma"] <= 1e-6 and 0 or hdr["min-luma"]
+ append(s, format("%.2g / %.0f", hdr["min-luma"], hdr["max-luma"]),
+ {prefix=display_prefix, suffix=" cd/m²", nl="", indent=indent})
+ indent = o.prefix_sep .. o.prefix_sep
+ end
+ if should_show(hdr["max-cll"]) then
+ append(s, hdr["max-cll"], {prefix="MaxCLL:", suffix=" cd/m²", nl="",
+ indent=indent})
+ indent = o.prefix_sep .. o.prefix_sep
+ end
+ if hdr["max-fall"] and hdr["max-fall"] > 0 then
+ append(s, hdr["max-fall"], {prefix="MaxFALL:", suffix=" cd/m²", nl="",
+ indent=indent})
+ end
+ end
+
+ indent = o.prefix_sep .. o.prefix_sep
+
+ if hdr["scene-max-r"] or hdr["scene-max-g"] or
+ hdr["scene-max-b"] or hdr["scene-avg"] then
+ append(s, "", {prefix="HDR10+:"})
+ append(s, format("%.1f / %.1f / %.1f", hdr["scene-max-r"] or 0,
+ hdr["scene-max-g"] or 0, hdr["scene-max-b"] or 0),
+ {prefix="MaxRGB:", suffix=" cd/m²", nl="", indent=""})
+ append(s, format("%.1f", hdr["scene-avg"] or 0),
+ {prefix="Avg:", suffix=" cd/m²", nl="", indent=indent})
+ end
+
+ if hdr["max-pq-y"] and hdr["avg-pq-y"] then
+ append(s, "", {prefix="PQ(Y):"})
+ append(s, format("%.2f cd/m² (%.2f%% PQ)", pq_eotf(hdr["max-pq-y"]),
+ hdr["max-pq-y"] * 100), {prefix="Max:", nl="",
+ indent=""})
+ append(s, format("%.2f cd/m² (%.2f%% PQ)", pq_eotf(hdr["avg-pq-y"]),
+ hdr["avg-pq-y"] * 100), {prefix="Avg:", nl="",
+ indent=indent})
+ end
+end
+
+
+local function append_img_params(s, r, ro)
+ if not r then
+ return
+ end
+
+ append_resolution(s, r, "Resolution:", "w", "h", true)
+ if ro and (r["w"] ~= ro["dw"] or r["h"] ~= ro["dh"]) then
+ if ro["crop-w"] and (crop_noop(r["w"], r["h"], ro) or crop_equal(r, ro)) then
+ ro["crop-w"] = nil
+ end
+ append_resolution(s, ro, "Output Resolution:", "dw", "dh")
+ end
+
+ local indent = o.prefix_sep .. o.prefix_sep
+
+ local pixel_format = r["hw-pixelformat"] or r["pixelformat"]
+ append(s, pixel_format, {prefix="Format:"})
+ append(s, r["colorlevels"], {prefix="Levels:", nl="", indent=indent})
+ if r["chroma-location"] and r["chroma-location"] ~= "unknown" then
+ append(s, r["chroma-location"], {prefix="Chroma Loc:", nl="", indent=indent})
+ end
+
+ -- Group these together to save vertical space
+ append(s, r["colormatrix"], {prefix="Colormatrix:"})
+ append(s, r["primaries"], {prefix="Primaries:", nl="", indent=indent})
+ append(s, r["gamma"], {prefix="Transfer:", nl="", indent=indent})
+end
+
+
+local function append_fps(s, prop, eprop)
+ local fps = mp.get_property_osd(prop)
+ local efps = mp.get_property_osd(eprop)
+ local single = fps ~= "" and efps ~= "" and fps == efps
+ local unit = prop == "display-fps" and " Hz" or " fps"
+ local suffix = single and "" or " (specified)"
+ local esuffix = single and "" or " (estimated)"
+ local prefix = prop == "display-fps" and "Refresh Rate:" or "Frame rate:"
+ local nl = o.nl
+ local indent = o.indent
+
+ if fps ~= "" and append(s, fps, {prefix=prefix, suffix=unit .. suffix}) then
+ prefix = ""
+ nl = ""
+ indent = ""
+ end
+
+ if not single and efps ~= "" then
+ append(s, efps,
+ {prefix=prefix, suffix=unit .. esuffix, nl=nl, indent=indent})
+ end
+end
+
+
+local function add_video_out(s)
+ local vo = mp.get_property_native("current-vo")
+ if not vo then
+ return
+ end
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Display:", nl="", indent=""})
+ append(s, vo, {prefix_sep="", nl="", indent=""})
+ append_property(s, "display-names", {prefix_sep="", prefix="(", suffix=")",
+ no_prefix_markup=true, nl="", indent=" "})
+ append_property(s, "avsync", {prefix="A-V:"})
+ append_fps(s, "display-fps", "estimated-display-fps")
+ if append_property(s, "decoder-frame-drop-count",
+ {prefix="Dropped Frames:", suffix=" (decoder)"}) then
+ append_property(s, "frame-drop-count", {suffix=" (output)", nl="", indent=""})
+ end
+ append_display_sync(s)
+ append_perfdata(s, false, o.print_perfdata_passes)
+
+ if mp.get_property_native("deinterlace") then
+ append_property(s, "deinterlace", {prefix="Deinterlacing:"})
+ end
+
+ local scale = nil
+ if not mp.get_property_native("fullscreen") then
+ scale = mp.get_property_native("current-window-scale")
+ end
+
+ local r = mp.get_property_native("video-target-params")
+ if not r then
+ local osd_dims = mp.get_property_native("osd-dimensions")
+ local scaled_width = osd_dims["w"] - osd_dims["ml"] - osd_dims["mr"]
+ local scaled_height = osd_dims["h"] - osd_dims["mt"] - osd_dims["mb"]
+ append_resolution(s, {w=scaled_width, h=scaled_height, s=scale},
+ "Resolution:")
+ return
+ end
+
+ -- Add window scale
+ r["s"] = scale
+
+ append_img_params(s, r)
+ append_hdr(s, r, true)
+end
+
+
+local function add_video(s)
+ local r = mp.get_property_native("video-params")
+ local ro = mp.get_property_native("video-out-params")
+ -- in case of e.g. lavfi-complex there can be no input video, only output
+ if not r then
+ r = ro
+ end
+ if not r then
+ return
+ end
+
+ local osd_dims = mp.get_property_native("osd-dimensions")
+ local scaled_width = osd_dims["w"] - osd_dims["ml"] - osd_dims["mr"]
+ local scaled_height = osd_dims["h"] - osd_dims["mt"] - osd_dims["mb"]
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Video:", nl="", indent=""})
+ if append_property(s, "video-codec", {prefix_sep="", nl="", indent=""}) then
+ append_property(s, "hwdec-current", {prefix="HW:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep,
+ no_prefix_markup=false, suffix=""}, {no=true, [""]=true})
+ end
+ local has_prefix = false
+ if o.show_frame_info then
+ if append_property(s, "estimated-frame-number", {prefix="Frame:"}) then
+ append_property(s, "estimated-frame-count", {indent=" / ", nl="",
+ prefix_sep=""})
+ has_prefix = true
+ end
+ local frame_info = mp.get_property_native("video-frame-info")
+ if frame_info and frame_info["picture-type"] then
+ local attrs = has_prefix and {prefix="(", suffix=")", indent=" ", nl="",
+ prefix_sep="", no_prefix_markup=true}
+ or {prefix="Picture Type:"}
+ append(s, frame_info["picture-type"], attrs)
+ has_prefix = true
+ end
+ if frame_info and frame_info["interlaced"] then
+ local attrs = has_prefix and {indent=" ", nl="", prefix_sep=""}
+ or {prefix="Picture Type:"}
+ append(s, "Interlaced", attrs)
+ end
+ end
+
+ if mp.get_property_native("current-tracks/video/image") == false then
+ append_fps(s, "container-fps", "estimated-vf-fps")
+ end
+ append_img_params(s, r, ro)
+ append_hdr(s, ro)
+ append_property(s, "packet-video-bitrate", {prefix="Bitrate:", suffix=" kbps"})
+ append_filters(s, "vf", "Filters:")
+end
+
+
+local function add_audio(s)
+ local r = mp.get_property_native("audio-params")
+ -- in case of e.g. lavfi-complex there can be no input audio, only output
+ if not r then
+ r = mp.get_property_native("audio-out-params")
+ end
+ if not r then
+ return
+ end
+
+ append(s, "", {prefix=o.nl .. o.nl .. "Audio:", nl="", indent=""})
+ append_property(s, "audio-codec", {prefix_sep="", nl="", indent=""})
+ local cc = append(s, r["channel-count"], {prefix="Channels:"})
+ append(s, r["format"], {prefix="Format:", nl=cc and "" or o.nl,
+ indent=cc and o.prefix_sep .. o.prefix_sep})
+ append(s, r["samplerate"], {prefix="Sample Rate:", suffix=" Hz"})
+ append_property(s, "packet-audio-bitrate", {prefix="Bitrate:", suffix=" kbps"})
+ append_filters(s, "af", "Filters:")
+end
+
+
+-- Determine whether ASS formatting shall/can be used and set formatting sequences
+local function eval_ass_formatting()
+ o.use_ass = o.ass_formatting and has_vo_window()
+ if o.use_ass then
+ o.nl = o.ass_nl
+ o.indent = o.ass_indent
+ o.prefix_sep = o.ass_prefix_sep
+ o.b1 = o.ass_b1
+ o.b0 = o.ass_b0
+ o.it1 = o.ass_it1
+ o.it0 = o.ass_it0
+ else
+ o.nl = o.no_ass_nl
+ o.indent = o.no_ass_indent
+ o.prefix_sep = o.no_ass_prefix_sep
+ o.b1 = o.no_ass_b1
+ o.b0 = o.no_ass_b0
+ o.it1 = o.no_ass_it1
+ o.it0 = o.no_ass_it0
+ end
+end
+
+
+-- Returns an ASS string with "normal" stats
+local function default_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+ add_file(stats)
+ add_video_out(stats)
+ add_video(stats)
+ add_audio(stats)
+ return table.concat(stats)
+end
+
+local function scroll_vo_stats(stats, fixed_items, offset)
+ local ret = {}
+ local count = #stats - fixed_items
+ offset = max(1, min((offset or 1), count))
+
+ for i, line in pairs(stats) do
+ if i <= fixed_items or i >= fixed_items + offset then
+ ret[#ret+1] = stats[i]
+ end
+ end
+ return ret, offset
+end
+
+-- Returns an ASS string with extended VO stats
+local function vo_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+
+ -- first line (title) added next is considered fixed
+ local fixed_items = #stats + 1
+ append_perfdata(stats, true, true)
+
+ local page = pages[o.key_page_2]
+ stats, page.offset = scroll_vo_stats(stats, fixed_items, page.offset)
+ return table.concat(stats)
+end
+
+local kbinfo_lines = nil
+local function keybinding_info(after_scroll)
+ local header = {}
+ local page = pages[o.key_page_4]
+ eval_ass_formatting()
+ add_header(header)
+ append(header, "", {prefix=format("%s: {\\fs%s}%s{\\fs%s}", page.desc,
+ o.font_size * 0.66, "(hint: scroll with ↑↓)", o.font_size), nl="",
+ indent=""})
+
+ if not kbinfo_lines or not after_scroll then
+ kbinfo_lines = get_kbinfo_lines()
+ end
+ -- up to 20 lines for the terminal - so that mpv can also print
+ -- the status line without scrolling, and up to 40 lines for libass
+ -- because it can put a big performance toll on libass to process
+ -- many lines which end up outside (below) the screen.
+ local term = not o.use_ass
+ local nlines = #kbinfo_lines
+ page.offset = max(1, min((page.offset or 1), term and nlines - 20 or nlines))
+ local maxline = min(nlines, page.offset + (term and 20 or 40))
+ return table.concat(header) ..
+ table.concat(kbinfo_lines, "", page.offset, maxline)
+end
+
+local function perf_stats()
+ local stats = {}
+ eval_ass_formatting()
+ add_header(stats)
+ local page = pages[o.key_page_0]
+ append(stats, "", {prefix=page.desc .. ":", nl="", indent=""})
+ page.offset = append_general_perfdata(stats, page.offset)
+ return table.concat(stats)
+end
+
+local function opt_time(t)
+ if type(t) == type(1.1) then
+ return mp.format_time(t)
+ end
+ return "?"
+end
+
+-- Returns an ASS string with stats about the demuxer cache etc.
+local function cache_stats()
+ local stats = {}
+
+ eval_ass_formatting()
+ add_header(stats)
+ append(stats, "", {prefix="Cache info:", nl="", indent=""})
+
+ local info = mp.get_property_native("demuxer-cache-state")
+ if info == nil then
+ append(stats, "Unavailable.", {})
+ return table.concat(stats)
+ end
+
+ local a = info["reader-pts"]
+ local b = info["cache-end"]
+
+ append(stats, opt_time(a) .. " - " .. opt_time(b), {prefix = "Packet queue:"})
+
+ local r = nil
+ if a ~= nil and b ~= nil then
+ r = b - a
+ end
+
+ local r_graph = nil
+ if not display_timer.oneshot and o.use_ass then
+ r_graph = generate_graph(cache_ahead_buf, cache_ahead_buf.pos,
+ cache_ahead_buf.len, cache_ahead_buf.max,
+ nil, 0.8, 1)
+ r_graph = o.prefix_sep .. r_graph
+ end
+ append(stats, opt_time(r), {prefix = "Read-ahead:", suffix = r_graph})
+
+ -- These states are not necessarily exclusive. They're about potentially
+ -- separate mechanisms, whose states may be decoupled.
+ local state = "reading"
+ local seek_ts = info["debug-seeking"]
+ if seek_ts ~= nil then
+ state = "seeking (to " .. mp.format_time(seek_ts) .. ")"
+ elseif info["eof"] == true then
+ state = "eof"
+ elseif info["underrun"] then
+ state = "underrun"
+ elseif info["idle"] == true then
+ state = "inactive"
+ end
+ append(stats, state, {prefix = "State:"})
+
+ local speed = info["raw-input-rate"] or 0
+ local speed_graph = nil
+ if not display_timer.oneshot and o.use_ass then
+ speed_graph = generate_graph(cache_speed_buf, cache_speed_buf.pos,
+ cache_speed_buf.len, cache_speed_buf.max,
+ nil, 0.8, 1)
+ speed_graph = o.prefix_sep .. speed_graph
+ end
+ append(stats, utils.format_bytes_humanized(speed) .. "/s", {prefix="Speed:",
+ suffix=speed_graph})
+
+ append(stats, utils.format_bytes_humanized(info["total-bytes"]),
+ {prefix = "Total RAM:"})
+ append(stats, utils.format_bytes_humanized(info["fw-bytes"]),
+ {prefix = "Forward RAM:"})
+
+ local fc = info["file-cache-bytes"]
+ if fc ~= nil then
+ fc = utils.format_bytes_humanized(fc)
+ else
+ fc = "(disabled)"
+ end
+ append(stats, fc, {prefix = "Disk cache:"})
+
+ append(stats, info["debug-low-level-seeks"], {prefix = "Media seeks:"})
+ append(stats, info["debug-byte-level-seeks"], {prefix = "Stream seeks:"})
+
+ append(stats, "", {prefix=o.nl .. o.nl .. "Ranges:", nl="", indent=""})
+
+ append(stats, info["bof-cached"] and "yes" or "no",
+ {prefix = "Start cached:"})
+ append(stats, info["eof-cached"] and "yes" or "no",
+ {prefix = "End cached:"})
+
+ local ranges = info["seekable-ranges"] or {}
+ for n, r in ipairs(ranges) do
+ append(stats, mp.format_time(r["start"]) .. " - " ..
+ mp.format_time(r["end"]),
+ {prefix = format("Range %s:", n)})
+ end
+
+ return table.concat(stats)
+end
+
+-- Record 1 sample of cache statistics.
+-- (Unlike record_data(), this does not return a function, but runs directly.)
+local function record_cache_stats()
+ local info = mp.get_property_native("demuxer-cache-state")
+ if info == nil then
+ return
+ end
+
+ local a = info["reader-pts"]
+ local b = info["cache-end"]
+ if a ~= nil and b ~= nil then
+ graph_add_value(cache_ahead_buf, b - a)
+ end
+
+ graph_add_value(cache_speed_buf, info["raw-input-rate"] or 0)
+end
+
+cache_recorder_timer = mp.add_periodic_timer(0.25, record_cache_stats)
+cache_recorder_timer:kill()
+
+-- Current page and <page key>:<page function> mapping
+curr_page = o.key_page_1
+pages = {
+ [o.key_page_1] = { f = default_stats, desc = "Default" },
+ [o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings", scroll = true },
+ [o.key_page_3] = { f = cache_stats, desc = "Cache Statistics" },
+ [o.key_page_4] = { f = keybinding_info, desc = "Active key bindings", scroll = true },
+ [o.key_page_0] = { f = perf_stats, desc = "Internal performance info", scroll = true },
+}
+
+
+-- Returns a function to record vsratio/jitter with the specified `skip` value
+local function record_data(skip)
+ init_buffers()
+ skip = max(skip, 0)
+ local i = skip
+ return function()
+ if i < skip then
+ i = i + 1
+ return
+ else
+ i = 0
+ end
+
+ if o.plot_vsync_jitter then
+ local r = mp.get_property_number("vsync-jitter", nil)
+ if r then
+ vsjitter_buf.pos = (vsjitter_buf.pos % vsjitter_buf.len) + 1
+ vsjitter_buf[vsjitter_buf.pos] = r
+ vsjitter_buf.max = max(vsjitter_buf.max, r)
+ end
+ end
+
+ if o.plot_vsync_ratio then
+ local r = mp.get_property_number("vsync-ratio", nil)
+ if r then
+ vsratio_buf.pos = (vsratio_buf.pos % vsratio_buf.len) + 1
+ vsratio_buf[vsratio_buf.pos] = r
+ vsratio_buf.max = max(vsratio_buf.max, r)
+ end
+ end
+ end
+end
+
+-- Call the function for `page` and print it to OSD
+local function print_page(page, after_scroll)
+ -- the page functions assume we start in ass-enabled mode.
+ -- that's true for mp.set_osd_ass, but not for mp.osd_message.
+ local ass_content = pages[page].f(after_scroll)
+ if o.persistent_overlay then
+ mp.set_osd_ass(0, 0, ass_content)
+ else
+ mp.osd_message((o.use_ass and ass_start or "") .. ass_content,
+ display_timer.oneshot and o.duration or o.redraw_delay + 1)
+ end
+end
+
+
+local function clear_screen()
+ if o.persistent_overlay then mp.set_osd_ass(0, 0, "") else mp.osd_message("", 0) end
+end
+
+local function scroll_delta(d)
+ if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end
+ pages[curr_page].offset = (pages[curr_page].offset or 1) + d
+ print_page(curr_page, true)
+end
+local function scroll_up() scroll_delta(-o.scroll_lines) end
+local function scroll_down() scroll_delta(o.scroll_lines) end
+
+local function reset_scroll_offsets()
+ for _, page in pairs(pages) do
+ page.offset = nil
+ end
+end
+local function bind_scroll()
+ if not scroll_bound then
+ mp.add_forced_key_binding(o.key_scroll_up, "__forced_"..o.key_scroll_up, scroll_up, {repeatable=true})
+ mp.add_forced_key_binding(o.key_scroll_down, "__forced_"..o.key_scroll_down, scroll_down, {repeatable=true})
+ scroll_bound = true
+ end
+end
+local function unbind_scroll()
+ if scroll_bound then
+ mp.remove_key_binding("__forced_"..o.key_scroll_up)
+ mp.remove_key_binding("__forced_"..o.key_scroll_down)
+ scroll_bound = false
+ end
+end
+local function update_scroll_bindings(k)
+ if pages[k].scroll then
+ bind_scroll()
+ else
+ unbind_scroll()
+ end
+end
+
+-- Add keybindings for every page
+local function add_page_bindings()
+ local function a(k)
+ return function()
+ reset_scroll_offsets()
+ update_scroll_bindings(k)
+ curr_page = k
+ print_page(k)
+ if display_timer.oneshot then display_timer:kill() ; display_timer:resume() end
+ end
+ end
+ for k, _ in pairs(pages) do
+ mp.add_forced_key_binding(k, "__forced_"..k, a(k), {repeatable=true})
+ end
+ update_scroll_bindings(curr_page)
+end
+
+
+-- Remove keybindings for every page
+local function remove_page_bindings()
+ for k, _ in pairs(pages) do
+ mp.remove_key_binding("__forced_"..k)
+ end
+ unbind_scroll()
+end
+
+
+local function process_key_binding(oneshot)
+ reset_scroll_offsets()
+ -- Stats are already being displayed
+ if display_timer:is_enabled() then
+ -- Previous and current keys were oneshot -> restart timer
+ if display_timer.oneshot and oneshot then
+ display_timer:kill()
+ print_page(curr_page)
+ display_timer:resume()
+ -- Previous and current keys were toggling -> end toggling
+ elseif not display_timer.oneshot and not oneshot then
+ display_timer:kill()
+ cache_recorder_timer:stop()
+ if tm_viz_prev ~= nil then
+ mp.set_property_native("tone-mapping-visualize", tm_viz_prev)
+ tm_viz_prev = nil
+ end
+ clear_screen()
+ remove_page_bindings()
+ if recorder then
+ mp.unobserve_property(recorder)
+ recorder = nil
+ end
+ end
+ -- No stats are being displayed yet
+ else
+ if not oneshot and (o.plot_vsync_jitter or o.plot_vsync_ratio) then
+ recorder = record_data(o.skip_frames)
+ -- Rely on the fact that "vsync-ratio" is updated at the same time.
+ -- Using "none" to get a sample any time, even if it does not change.
+ -- Will stop working if "vsync-jitter" property change notification
+ -- changes, but it's fine for an internal script.
+ mp.observe_property("vsync-jitter", "none", recorder)
+ end
+ if not oneshot and o.plot_tonemapping_lut then
+ tm_viz_prev = mp.get_property_native("tone-mapping-visualize")
+ mp.set_property_native("tone-mapping-visualize", true)
+ end
+ if not oneshot then
+ cache_ahead_buf = {0, pos = 1, len = 50, max = 0}
+ cache_speed_buf = {0, pos = 1, len = 50, max = 0}
+ cache_recorder_timer:resume()
+ end
+ display_timer:kill()
+ display_timer.oneshot = oneshot
+ display_timer.timeout = oneshot and o.duration or o.redraw_delay
+ add_page_bindings()
+ print_page(curr_page)
+ display_timer:resume()
+ end
+end
+
+
+-- Create the timer used for redrawing (toggling) or clearing the screen (oneshot)
+-- The duration here is not important and always set in process_key_binding()
+display_timer = mp.add_periodic_timer(o.duration,
+ function()
+ if display_timer.oneshot then
+ display_timer:kill() ; clear_screen() ; remove_page_bindings()
+ else
+ print_page(curr_page)
+ end
+ end)
+display_timer:kill()
+
+-- Single invocation key binding
+mp.add_key_binding(nil, "display-stats", function() process_key_binding(true) end,
+ {repeatable=true})
+
+-- Toggling key binding
+mp.add_key_binding(nil, "display-stats-toggle", function() process_key_binding(false) end,
+ {repeatable=false})
+
+-- Single invocation bindings without key, can be used in input.conf to create
+-- bindings for a specific page: "e script-binding stats/display-page-2"
+for k, _ in pairs(pages) do
+ mp.add_key_binding(nil, "display-page-" .. k,
+ function()
+ curr_page = k
+ process_key_binding(true)
+ end, {repeatable=true})
+end
+
+-- Reprint stats immediately when VO was reconfigured, only when toggled
+mp.register_event("video-reconfig",
+ function()
+ if display_timer:is_enabled() then
+ print_page(curr_page)
+ end
+ end)
+
+-- --script-opts=stats-bindlist=[-]{yes|<TERM-WIDTH>}
+if o.bindlist ~= "no" then
+ mp.command("no-osd set really-quiet yes")
+ if o.bindlist:sub(1, 1) == "-" then
+ o.bindlist = o.bindlist:sub(2)
+ o.no_ass_b0 = ""
+ o.no_ass_b1 = ""
+ end
+ local width = max(40, math.floor(tonumber(o.bindlist) or 79))
+ mp.add_timeout(0, function() -- wait for all other scripts to finish init
+ o.ass_formatting = false
+ o.no_ass_indent = " "
+ eval_ass_formatting()
+ io.write(pages[o.key_page_4].desc .. ":" ..
+ table.concat(get_kbinfo_lines(width)) .. "\n")
+ mp.command("quit")
+ end)
+end
diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua
new file mode 100644
index 0000000..3161da6
--- /dev/null
+++ b/player/lua/ytdl_hook.lua
@@ -0,0 +1,1191 @@
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+local options = require 'mp.options'
+
+local o = {
+ exclude = "",
+ try_ytdl_first = false,
+ use_manifests = false,
+ all_formats = false,
+ force_all_formats = true,
+ thumbnails = "none",
+ ytdl_path = "",
+}
+
+local ytdl = {
+ path = "",
+ paths_to_search = {"yt-dlp", "yt-dlp_x86", "youtube-dl"},
+ searched = false,
+ blacklisted = {}
+}
+
+options.read_options(o, nil, function()
+ ytdl.blacklisted = {} -- reparse o.exclude next time
+ ytdl.searched = false
+end)
+
+local chapter_list = {}
+local playlist_cookies = {}
+
+function Set (t)
+ local set = {}
+ for _, v in pairs(t) do set[v] = true end
+ return set
+end
+
+-- ?: surrogate (keep in mind that there is no lazy evaluation)
+function iif(cond, if_true, if_false)
+ if cond then
+ return if_true
+ end
+ return if_false
+end
+
+-- youtube-dl JSON name to mpv tag name
+local tag_list = {
+ ["uploader"] = "uploader",
+ ["channel_url"] = "channel_url",
+ -- these titles tend to be a bit too long, so hide them on the terminal
+ -- (default --display-tags does not include this name)
+ ["description"] = "ytdl_description",
+ -- "title" is handled by force-media-title
+ -- tags don't work with all_formats=yes
+}
+
+local safe_protos = Set {
+ "http", "https", "ftp", "ftps",
+ "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
+ "data"
+}
+
+-- For some sites, youtube-dl returns the audio codec (?) only in the "ext" field.
+local ext_map = {
+ ["mp3"] = "mp3",
+ ["opus"] = "opus",
+}
+
+local codec_map = {
+ -- src pattern = mpv codec
+ ["vtt"] = "webvtt",
+ ["opus"] = "opus",
+ ["vp9"] = "vp9",
+ ["avc1%..*"] = "h264",
+ ["av01%..*"] = "av1",
+ ["mp4a%..*"] = "aac",
+}
+
+-- Codec name as reported by youtube-dl mapped to mpv internal codec names.
+-- Fun fact: mpv will not really use the codec, but will still try to initialize
+-- the codec on track selection (just to scrap it), meaning it's only a hint,
+-- but one that may make initialization fail. On the other hand, if the codec
+-- is valid but completely different from the actual media, nothing bad happens.
+local function map_codec_to_mpv(codec)
+ if codec == nil then
+ return nil
+ end
+ for k, v in pairs(codec_map) do
+ local s, e = codec:find(k)
+ if s == 1 and e == #codec then
+ return v
+ end
+ end
+ return nil
+end
+
+local function platform_is_windows()
+ return mp.get_property_native("platform") == "windows"
+end
+
+local function exec(args)
+ msg.debug("Running: " .. table.concat(args, " "))
+
+ return mp.command_native({
+ name = "subprocess",
+ args = args,
+ capture_stdout = true,
+ capture_stderr = true,
+ })
+end
+
+-- return true if it was explicitly set on the command line
+local function option_was_set(name)
+ return mp.get_property_bool("option-info/" ..name.. "/set-from-commandline",
+ false)
+end
+
+-- return true if the option was set locally
+local function option_was_set_locally(name)
+ return mp.get_property_bool("option-info/" ..name.. "/set-locally", false)
+end
+
+-- youtube-dl may set special http headers for some sites (user-agent, cookies)
+local function set_http_headers(http_headers)
+ if not http_headers then
+ return
+ end
+ local headers = {}
+ local useragent = http_headers["User-Agent"]
+ if useragent and not option_was_set("user-agent") then
+ mp.set_property("file-local-options/user-agent", useragent)
+ end
+ local additional_fields = {"Cookie", "Referer", "X-Forwarded-For"}
+ for idx, item in pairs(additional_fields) do
+ local field_value = http_headers[item]
+ if field_value then
+ headers[#headers + 1] = item .. ": " .. field_value
+ end
+ end
+ if #headers > 0 and not option_was_set("http-header-fields") then
+ mp.set_property_native("file-local-options/http-header-fields", headers)
+ end
+end
+
+local special_cookie_field_names = Set {
+ "expires", "max-age", "domain", "path"
+}
+
+-- parse single-line Set-Cookie syntax
+local function parse_cookies(cookies_line)
+ if not cookies_line then
+ return {}
+ end
+ local cookies = {}
+ local cookie = {}
+ for stem in cookies_line:gmatch('[^;]+') do
+ stem = stem:gsub("^%s*(.-)%s*$", "%1")
+ local name, value = stem:match('^(.-)=(.+)$')
+ if name and name ~= "" and value then
+ local cmp_name = name:lower()
+ if special_cookie_field_names[cmp_name] then
+ cookie[cmp_name] = value
+ else
+ if cookie.name and cookie.value then
+ table.insert(cookies, cookie)
+ end
+ cookie = {
+ name = name,
+ value = value,
+ }
+ end
+ end
+ end
+ if cookie.name and cookie.value then
+ local cookie_key = cookie.domain .. ":" .. cookie.name
+ cookies[cookie_key] = cookie
+ end
+ return cookies
+end
+
+-- serialize cookies for avformat
+local function serialize_cookies_for_avformat(cookies)
+ local result = ''
+ for _, cookie in pairs(cookies) do
+ local cookie_str = ('%s=%s; '):format(cookie.name, cookie.value)
+ for k, v in pairs(cookie) do
+ if k ~= "name" and k ~= "value" then
+ cookie_str = cookie_str .. ('%s=%s; '):format(k, v)
+ end
+ end
+ result = result .. cookie_str .. '\r\n'
+ end
+ return result
+end
+
+-- set file-local cookies, preserving existing ones
+local function set_cookies(cookies)
+ if not cookies or cookies == "" then
+ return
+ end
+
+ local option_key = "file-local-options/stream-lavf-o"
+ local stream_opts = mp.get_property_native(option_key, {})
+ local existing_cookies = parse_cookies(stream_opts["cookies"])
+
+ local new_cookies = parse_cookies(cookies)
+ for cookie_key, cookie in pairs(new_cookies) do
+ if not existing_cookies[cookie_key] then
+ existing_cookies[cookie_key] = cookie
+ end
+ end
+
+ stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies)
+ mp.set_property_native(option_key, stream_opts)
+end
+
+local function append_libav_opt(props, name, value)
+ if not props then
+ props = {}
+ end
+
+ if name and value and not props[name] then
+ props[name] = value
+ end
+
+ return props
+end
+
+local function edl_escape(url)
+ return "%" .. string.len(url) .. "%" .. url
+end
+
+local function url_is_safe(url)
+ local proto = type(url) == "string" and url:match("^(%a[%w+.-]*):") or nil
+ local safe = proto and safe_protos[proto]
+ if not safe then
+ msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
+ end
+ return safe
+end
+
+local function time_to_secs(time_string)
+ local ret
+
+ local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
+ if a ~= nil then
+ ret = (a*3600 + b*60 + c)
+ else
+ a, b = time_string:match("(%d%d?):(%d%d)")
+ if a ~= nil then
+ ret = (a*60 + b)
+ end
+ end
+
+ return ret
+end
+
+local function extract_chapters(data, video_length)
+ local ret = {}
+
+ for line in data:gmatch("[^\r\n]+") do
+ local time = time_to_secs(line)
+ if time and (time < video_length) then
+ table.insert(ret, {time = time, title = line})
+ end
+ end
+ table.sort(ret, function(a, b) return a.time < b.time end)
+ return ret
+end
+
+local function is_blacklisted(url)
+ if o.exclude == "" then return false end
+ if #ytdl.blacklisted == 0 then
+ for match in o.exclude:gmatch('%|?([^|]+)') do
+ ytdl.blacklisted[#ytdl.blacklisted + 1] = match
+ end
+ end
+ if #ytdl.blacklisted > 0 then
+ url = url:match('https?://(.+)')
+ for _, exclude in ipairs(ytdl.blacklisted) do
+ if url:match(exclude) then
+ msg.verbose('URL matches excluded substring. Skipping.')
+ return true
+ end
+ end
+ end
+ return false
+end
+
+local function parse_yt_playlist(url, json)
+ -- return 0-based index to use with --playlist-start
+
+ if not json.extractor or
+ (json.extractor ~= "youtube:tab" and
+ json.extractor ~= "youtube:playlist") then
+ return nil
+ end
+
+ local query = url:match("%?.+")
+ if not query then return nil end
+
+ local args = {}
+ for arg, param in query:gmatch("(%a+)=([^&?]+)") do
+ if arg and param then
+ args[arg] = param
+ end
+ end
+
+ local maybe_idx = tonumber(args["index"])
+
+ -- if index matches v param it's probably the requested item
+ if maybe_idx and #json.entries >= maybe_idx and
+ json.entries[maybe_idx].id == args["v"] then
+ msg.debug("index matches requested video")
+ return maybe_idx - 1
+ end
+
+ -- if there's no index or it doesn't match, look for video
+ for i = 1, #json.entries do
+ if json.entries[i].id == args["v"] then
+ msg.debug("found requested video in index " .. (i - 1))
+ return i - 1
+ end
+ end
+
+ msg.debug("requested video not found in playlist")
+ -- if item isn't on the playlist, give up
+ return nil
+end
+
+local function make_absolute_url(base_url, url)
+ if url:find("https?://") == 1 then return url end
+
+ local proto, domain, rest =
+ base_url:match("(https?://)([^/]+/)(.*)/?")
+ local segs = {}
+ rest:gsub("([^/]+)", function(c) table.insert(segs, c) end)
+ url:gsub("([^/]+)", function(c) table.insert(segs, c) end)
+ local resolved_url = {}
+ for i, v in ipairs(segs) do
+ if v == ".." then
+ table.remove(resolved_url)
+ elseif v ~= "." then
+ table.insert(resolved_url, v)
+ end
+ end
+ return proto .. domain ..
+ table.concat(resolved_url, "/")
+end
+
+local function join_url(base_url, fragment)
+ local res = ""
+ if base_url and fragment.path then
+ res = make_absolute_url(base_url, fragment.path)
+ elseif fragment.url then
+ res = fragment.url
+ end
+ return res
+end
+
+local function edl_track_joined(fragments, protocol, is_live, base)
+ if type(fragments) ~= "table" or not fragments[1] then
+ msg.debug("No fragments to join into EDL")
+ return nil
+ end
+
+ local edl = "edl://"
+ local offset = 1
+ local parts = {}
+
+ if protocol == "http_dash_segments" and not is_live then
+ msg.debug("Using dash")
+ local args = ""
+
+ -- assume MP4 DASH initialization segment
+ if not fragments[1].duration and #fragments > 1 then
+ msg.debug("Using init segment")
+ args = args .. ",init=" .. edl_escape(join_url(base, fragments[1]))
+ offset = 2
+ end
+
+ table.insert(parts, "!mp4_dash" .. args)
+
+ -- Check remaining fragments for duration;
+ -- if not available in all, give up.
+ for i = offset, #fragments do
+ if not fragments[i].duration then
+ msg.verbose("EDL doesn't support fragments " ..
+ "without duration with MP4 DASH")
+ return nil
+ end
+ end
+ end
+
+ for i = offset, #fragments do
+ local fragment = fragments[i]
+ if not url_is_safe(join_url(base, fragment)) then
+ return nil
+ end
+ table.insert(parts, edl_escape(join_url(base, fragment)))
+ if fragment.duration then
+ parts[#parts] =
+ parts[#parts] .. ",length="..fragment.duration
+ end
+ end
+ return edl .. table.concat(parts, ";") .. ";"
+end
+
+local function has_native_dash_demuxer()
+ local demuxers = mp.get_property_native("demuxer-lavf-list", {})
+ for _, v in ipairs(demuxers) do
+ if v == "dash" then
+ return true
+ end
+ end
+ return false
+end
+
+local function valid_manifest(json)
+ local reqfmt = json["requested_formats"] and json["requested_formats"][1] or {}
+ if not reqfmt["manifest_url"] and not json["manifest_url"] then
+ return false
+ end
+ local proto = reqfmt["protocol"] or json["protocol"] or ""
+ return (proto == "http_dash_segments" and has_native_dash_demuxer()) or
+ proto:find("^m3u8")
+end
+
+local function as_integer(v, def)
+ def = def or 0
+ local num = math.floor(tonumber(v) or def)
+ if num > -math.huge and num < math.huge then
+ return num
+ end
+ return def
+end
+
+local function tags_to_edl(json)
+ local tags = {}
+ for json_name, mp_name in pairs(tag_list) do
+ local v = json[json_name]
+ if v then
+ tags[#tags + 1] = mp_name .. "=" .. edl_escape(tostring(v))
+ end
+ end
+ if #tags == 0 then
+ return nil
+ end
+ return "!global_tags," .. table.concat(tags, ",")
+end
+
+-- Convert a format list from youtube-dl to an EDL URL, or plain URL.
+-- json: full json blob by youtube-dl
+-- formats: format list by youtube-dl
+-- use_all_formats: if=true, then formats is the full format list, and the
+-- function will attempt to return them as delay-loaded tracks
+-- See res table initialization in the function for result type.
+local function formats_to_edl(json, formats, use_all_formats)
+ local res = {
+ -- the media URL, which may be EDL
+ url = nil,
+ -- for use_all_formats=true: whether any muxed formats are present, and
+ -- at the same time the separate EDL parts don't have both audio/video
+ muxed_needed = false,
+ }
+
+ local default_formats = {}
+ local requested_formats = json["requested_formats"] or json["requested_downloads"]
+ if use_all_formats and requested_formats then
+ for _, track in ipairs(requested_formats) do
+ local id = track["format_id"]
+ if id then
+ default_formats[id] = true
+ end
+ end
+ end
+
+ local duration = as_integer(json["duration"])
+ local single_url = nil
+ local streams = {}
+
+ local tbr_only = true
+ for index, track in ipairs(formats) do
+ tbr_only = tbr_only and track["tbr"] and
+ (not track["abr"]) and (not track["vbr"])
+ end
+
+ local has_requested_video = false
+ local has_requested_audio = false
+ -- Web players with quality selection always show the highest quality
+ -- option at the top. Since tracks are usually listed with the first
+ -- track at the top, that should also be the highest quality track.
+ -- yt-dlp/youtube-dl sorts it's formats from worst to best.
+ -- Iterate in reverse to get best track first.
+ for index = #formats, 1, -1 do
+ local track = formats[index]
+ local edl_track = nil
+ edl_track = edl_track_joined(track.fragments,
+ track.protocol, json.is_live,
+ track.fragment_base_url)
+ if not edl_track and not url_is_safe(track.url) then
+ msg.error("No safe URL or supported fragmented stream available")
+ return nil
+ end
+
+ local is_default = default_formats[track["format_id"]]
+ local tracks = {}
+ -- "none" means it is not a video
+ -- nil means it is unknown
+ if (o.force_all_formats or track.vcodec) and track.vcodec ~= "none" then
+ tracks[#tracks + 1] = {
+ media_type = "video",
+ codec = map_codec_to_mpv(track.vcodec),
+ }
+ if is_default then
+ has_requested_video = true
+ end
+ end
+ if (o.force_all_formats or track.acodec) and track.acodec ~= "none" then
+ tracks[#tracks + 1] = {
+ media_type = "audio",
+ codec = map_codec_to_mpv(track.acodec) or
+ ext_map[track.ext],
+ }
+ if is_default then
+ has_requested_audio = true
+ end
+ end
+
+ local url = edl_track or track.url
+ local hdr = {"!new_stream", "!no_clip", "!no_chapters"}
+ local skip = #tracks == 0
+ local params = ""
+
+ if use_all_formats then
+ for _, sub in ipairs(tracks) do
+ -- A single track that is either audio or video. Delay load it.
+ local props = ""
+ if sub.media_type == "video" then
+ props = props .. ",w=" .. as_integer(track.width)
+ .. ",h=" .. as_integer(track.height)
+ .. ",fps=" .. as_integer(track.fps)
+ elseif sub.media_type == "audio" then
+ props = props .. ",samplerate=" .. as_integer(track.asr)
+ end
+ hdr[#hdr + 1] = "!delay_open,media_type=" .. sub.media_type ..
+ ",codec=" .. (sub.codec or "null") .. props
+
+ -- Add bitrate information etc. for better user selection.
+ local byterate = 0
+ local rates = {"tbr", "vbr", "abr"}
+ if #tracks > 1 then
+ rates = {({video = "vbr", audio = "abr"})[sub.media_type]}
+ end
+ if tbr_only then
+ rates = {"tbr"}
+ end
+ for _, f in ipairs(rates) do
+ local br = as_integer(track[f])
+ if br > 0 then
+ byterate = math.floor(br * 1000 / 8)
+ break
+ end
+ end
+ local title = track.format or track.format_note or ""
+ if #tracks > 1 then
+ if #title > 0 then
+ title = title .. " "
+ end
+ title = title .. "muxed-" .. index
+ end
+ local flags = {}
+ if is_default then
+ flags[#flags + 1] = "default"
+ end
+ hdr[#hdr + 1] = "!track_meta,title=" ..
+ edl_escape(title) .. ",byterate=" .. byterate ..
+ iif(#flags > 0, ",flags=" .. table.concat(flags, "+"), "")
+ end
+
+ if duration > 0 then
+ params = params .. ",length=" .. duration
+ end
+ end
+
+ if not skip then
+ hdr[#hdr + 1] = edl_escape(url) .. params
+
+ streams[#streams + 1] = table.concat(hdr, ";")
+ -- In case there is only 1 of these streams.
+ -- Note: assumes it has no important EDL headers
+ single_url = url
+ end
+ end
+
+ local tags = tags_to_edl(json)
+
+ -- Merge all tracks into a single virtual file, but avoid EDL if it's
+ -- only a single track without metadata (i.e. redundant).
+ if #streams == 1 and single_url and not tags then
+ res.url = single_url
+ elseif #streams > 0 then
+ if tags then
+ -- not a stream; just for the sake of concatenating the EDL string
+ streams[#streams + 1] = tags
+ end
+ res.url = "edl://" .. table.concat(streams, ";")
+ else
+ return nil
+ end
+
+ if has_requested_audio ~= has_requested_video then
+ local not_req_prop = has_requested_video and "aid" or "vid"
+ if mp.get_property(not_req_prop) == "auto" then
+ mp.set_property("file-local-options/" .. not_req_prop, "no")
+ end
+ end
+
+ return res
+end
+
+local function add_single_video(json)
+ local streamurl = ""
+ local format_info = ""
+ local max_bitrate = 0
+ local requested_formats = json["requested_formats"] or json["requested_downloads"]
+ local all_formats = json["formats"]
+ local has_requested_formats = requested_formats and #requested_formats > 0
+ local http_headers = has_requested_formats
+ and requested_formats[1].http_headers
+ or json.http_headers
+ local cookies = has_requested_formats
+ and requested_formats[1].cookies
+ or json.cookies
+
+ if o.use_manifests and valid_manifest(json) then
+ -- prefer manifest_url if present
+ format_info = "manifest"
+
+ local mpd_url = requested_formats and
+ requested_formats[1]["manifest_url"] or json["manifest_url"]
+ if not mpd_url then
+ msg.error("No manifest URL found in JSON data.")
+ return
+ elseif not url_is_safe(mpd_url) then
+ return
+ end
+
+ streamurl = mpd_url
+
+ if requested_formats then
+ for _, track in pairs(requested_formats) do
+ max_bitrate = (track.tbr and track.tbr > max_bitrate) and
+ track.tbr or max_bitrate
+ end
+ elseif json.tbr then
+ max_bitrate = json.tbr > max_bitrate and json.tbr or max_bitrate
+ end
+ end
+
+ if streamurl == "" then
+ -- possibly DASH/split tracks
+ local res = nil
+
+ -- Not having requested_formats usually hints to HLS master playlist
+ -- usage, which we don't want to split off, at least not yet.
+ if (all_formats and o.all_formats) and
+ (has_requested_formats or o.force_all_formats)
+ then
+ format_info = "all_formats (separate)"
+ res = formats_to_edl(json, all_formats, true)
+ -- Note: since we don't delay-load muxed streams, use normal stream
+ -- selection if we have to use muxed streams.
+ if res and res.muxed_needed then
+ res = nil
+ end
+ end
+
+ if not res and has_requested_formats then
+ format_info = "youtube-dl (separate)"
+ res = formats_to_edl(json, requested_formats, false)
+ end
+
+ if res then
+ streamurl = res.url
+ end
+ end
+
+ if streamurl == "" and json.url then
+ format_info = "youtube-dl (single)"
+ local edl_track = nil
+ edl_track = edl_track_joined(json.fragments, json.protocol,
+ json.is_live, json.fragment_base_url)
+
+ if not edl_track and not url_is_safe(json.url) then
+ return
+ end
+ -- normal video or single track
+ streamurl = edl_track or json.url
+ end
+
+ if streamurl == "" then
+ msg.error("No URL found in JSON data.")
+ return
+ end
+
+ set_http_headers(http_headers)
+
+ msg.verbose("format selection: " .. format_info)
+ msg.debug("streamurl: " .. streamurl)
+
+ mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
+
+ if mp.get_property("force-media-title", "") == "" then
+ mp.set_property("file-local-options/force-media-title", json.title)
+ end
+
+ -- set hls-bitrate for dash track selection
+ if max_bitrate > 0 and
+ not option_was_set("hls-bitrate") and
+ not option_was_set_locally("hls-bitrate") then
+ mp.set_property_native('file-local-options/hls-bitrate', max_bitrate*1000)
+ end
+
+ -- add subtitles
+ if json.requested_subtitles ~= nil then
+ local subs = {}
+ for lang, info in pairs(json.requested_subtitles) do
+ subs[#subs + 1] = {lang = lang or "-", info = info}
+ end
+ table.sort(subs, function(a, b) return a.lang < b.lang end)
+ for _, e in ipairs(subs) do
+ local lang, sub_info = e.lang, e.info
+ msg.verbose("adding subtitle ["..lang.."]")
+
+ local sub = nil
+
+ if sub_info.data ~= nil then
+ sub = "memory://"..sub_info.data
+ elseif sub_info.url ~= nil and
+ url_is_safe(sub_info.url) then
+ sub = sub_info.url
+ end
+
+ if sub ~= nil then
+ local edl = "edl://!no_clip;!delay_open,media_type=sub"
+ local codec = map_codec_to_mpv(sub_info.ext)
+ if codec then
+ edl = edl .. ",codec=" .. codec
+ end
+ edl = edl .. ";" .. edl_escape(sub)
+ local title = sub_info.name or sub_info.ext
+ mp.commandv("sub-add", edl, "auto", title, lang)
+ else
+ msg.verbose("No subtitle data/url for ["..lang.."]")
+ end
+ end
+ end
+
+ -- add thumbnails
+ if (o.thumbnails == 'all' or o.thumbnails == 'best') and json.thumbnails ~= nil then
+ local thumb = nil
+ local thumb_height = -1
+ local thumb_preference = nil
+
+ for i = #json.thumbnails, 1, -1 do
+ local thumb_info = json.thumbnails[i]
+ if thumb_info.url ~= nil then
+ if o.thumbnails == 'all' then
+ msg.verbose("adding thumbnail")
+ mp.commandv("video-add", thumb_info.url, "auto")
+ thumb_height = 0
+ elseif (thumb_preference ~= nil and (thumb_info.preference or -math.huge) > thumb_preference) or
+ (thumb_preference == nil and ((thumb_info.height or 0) > thumb_height)) then
+ thumb = thumb_info.url
+ thumb_height = thumb_info.height or 0
+ thumb_preference = thumb_info.preference
+ end
+ end
+ end
+
+ if thumb ~= nil then
+ msg.verbose("adding thumbnail")
+ mp.commandv("video-add", thumb, "auto")
+ elseif thumb_height == -1 then
+ msg.verbose("No thumbnail url")
+ end
+ end
+
+ -- add chapters
+ if json.chapters then
+ msg.debug("Adding pre-parsed chapters")
+ for i = 1, #json.chapters do
+ local chapter = json.chapters[i]
+ local title = chapter.title or ""
+ if title == "" then
+ title = string.format('Chapter %02d', i)
+ end
+ table.insert(chapter_list, {time=chapter.start_time, title=title})
+ end
+ elseif json.description ~= nil and json.duration ~= nil then
+ chapter_list = extract_chapters(json.description, json.duration)
+ end
+
+ -- set start time
+ if json.start_time or json.section_start and
+ not option_was_set("start") and
+ not option_was_set_locally("start") then
+ local start_time = json.start_time or json.section_start
+ msg.debug("Setting start to: " .. start_time .. " secs")
+ mp.set_property("file-local-options/start", start_time)
+ end
+
+ -- set end time
+ if json.end_time or json.section_end and
+ not option_was_set("end") and
+ not option_was_set_locally("end") then
+ local end_time = json.end_time or json.section_end
+ msg.debug("Setting end to: " .. end_time .. " secs")
+ mp.set_property("file-local-options/end", end_time)
+ end
+
+ -- set aspect ratio for anamorphic video
+ if json.stretched_ratio ~= nil and
+ not option_was_set("video-aspect-override") then
+ mp.set_property('file-local-options/video-aspect-override', json.stretched_ratio)
+ end
+
+ local stream_opts = mp.get_property_native("file-local-options/stream-lavf-o", {})
+
+ -- for rtmp
+ if json.protocol == "rtmp" then
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_tcurl", streamurl)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_pageurl", json.page_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_playpath", json.play_path)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_swfverify", json.player_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_swfurl", json.player_url)
+ stream_opts = append_libav_opt(stream_opts,
+ "rtmp_app", json.app)
+ end
+
+ if json.proxy and json.proxy ~= "" then
+ stream_opts = append_libav_opt(stream_opts,
+ "http_proxy", json.proxy)
+ end
+
+ if cookies and cookies ~= "" then
+ local existing_cookies = parse_cookies(stream_opts["cookies"])
+ local new_cookies = parse_cookies(cookies)
+ for cookie_key, cookie in pairs(new_cookies) do
+ existing_cookies[cookie_key] = cookie
+ end
+ stream_opts["cookies"] = serialize_cookies_for_avformat(existing_cookies)
+ end
+
+ mp.set_property_native("file-local-options/stream-lavf-o", stream_opts)
+end
+
+local function check_version(ytdl_path)
+ local command = {
+ name = "subprocess",
+ capture_stdout = true,
+ args = {ytdl_path, "--version"}
+ }
+ local version_string = mp.command_native(command).stdout
+ local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)")
+
+ -- sanity check
+ if tonumber(year) < 2000 or tonumber(month) > 12 or
+ tonumber(day) > 31 then
+ return
+ end
+ local version_ts = os.time{year=year, month=month, day=day}
+ if os.difftime(os.time(), version_ts) > 60*60*24*90 then
+ msg.warn("It appears that your youtube-dl version is severely out of date.")
+ end
+end
+
+function run_ytdl_hook(url)
+ local start_time = os.clock()
+
+ -- strip ytdl://
+ if url:find("ytdl://") == 1 then
+ url = url:sub(8)
+ end
+
+ local format = mp.get_property("options/ytdl-format")
+ local raw_options = mp.get_property_native("options/ytdl-raw-options")
+ local allsubs = true
+ local proxy = nil
+ local use_playlist = false
+
+ local command = {
+ ytdl.path, "--no-warnings", "-J", "--flat-playlist",
+ "--sub-format", "ass/srt/best"
+ }
+
+ -- Checks if video option is "no", change format accordingly,
+ -- but only if user didn't explicitly set one
+ if mp.get_property("options/vid") == "no" and #format == 0 then
+ format = "bestaudio/best"
+ msg.verbose("Video disabled. Only using audio")
+ end
+
+ if format == "" then
+ format = "bestvideo+bestaudio/best"
+ end
+
+ if format ~= "ytdl" then
+ table.insert(command, "--format")
+ table.insert(command, format)
+ end
+
+ for param, arg in pairs(raw_options) do
+ table.insert(command, "--" .. param)
+ if arg ~= "" then
+ table.insert(command, arg)
+ end
+ if (param == "sub-lang" or param == "sub-langs" or param == "srt-lang") and (arg ~= "") then
+ allsubs = false
+ elseif param == "proxy" and arg ~= "" then
+ proxy = arg
+ elseif param == "yes-playlist" then
+ use_playlist = true
+ end
+ end
+
+ if allsubs == true then
+ table.insert(command, "--all-subs")
+ end
+ if not use_playlist then
+ table.insert(command, "--no-playlist")
+ end
+ table.insert(command, "--")
+ table.insert(command, url)
+
+ local result
+ if ytdl.searched then
+ result = exec(command)
+ else
+ local separator = platform_is_windows() and ";" or ":"
+ if o.ytdl_path:match("[^" .. separator .. "]") then
+ ytdl.paths_to_search = {}
+ for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
+ table.insert(ytdl.paths_to_search, path)
+ end
+ end
+
+ for _, path in pairs(ytdl.paths_to_search) do
+ -- search for youtube-dl in mpv's config dir
+ local exesuf = platform_is_windows() and not path:lower():match("%.exe$") and ".exe" or ""
+ local ytdl_cmd = mp.find_config_file(path .. exesuf)
+ if ytdl_cmd then
+ msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
+ ytdl.path = ytdl_cmd
+ command[1] = ytdl.path
+ result = exec(command)
+ break
+ else
+ msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
+ command[1] = path
+ result = exec(command)
+ if result.error_string == "init" then
+ msg.verbose("youtube-dl with path " .. path .. " not found in PATH or not enough permissions")
+ else
+ msg.verbose("Found youtube-dl with path " .. path .. " in PATH")
+ ytdl.path = path
+ break
+ end
+ end
+ end
+
+ ytdl.searched = true
+ end
+
+ if result.killed_by_us then
+ return
+ end
+
+ local json = result.stdout
+ local parse_err = nil
+
+ if result.status ~= 0 or json == "" then
+ json = nil
+ elseif json then
+ json, parse_err = utils.parse_json(json)
+ end
+
+ if json == nil then
+ msg.verbose("status:", result.status)
+ msg.verbose("reason:", result.error_string)
+ msg.verbose("stdout:", result.stdout)
+ msg.verbose("stderr:", result.stderr)
+
+ -- trim our stderr to avoid spurious newlines
+ ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1")
+ msg.error(ytdl_err)
+ local err = "youtube-dl failed: "
+ if result.error_string and result.error_string == "init" then
+ err = err .. "not found or not enough permissions"
+ elseif parse_err then
+ err = err .. "failed to parse JSON data: " .. parse_err
+ else
+ err = err .. "unexpected error occurred"
+ end
+ msg.error(err)
+ if parse_err or string.find(ytdl_err, "yt%-dl%.org/bug") then
+ check_version(ytdl.path)
+ end
+ return
+ end
+
+ msg.verbose("youtube-dl succeeded!")
+ msg.debug('ytdl parsing took '..os.clock()-start_time..' seconds')
+
+ json["proxy"] = json["proxy"] or proxy
+
+ -- what did we get?
+ if json["direct"] then
+ -- direct URL, nothing to do
+ msg.verbose("Got direct URL")
+ return
+ elseif json["_type"] == "playlist" or
+ json["_type"] == "multi_video" then
+ -- a playlist
+
+ if #json.entries == 0 then
+ msg.warn("Got empty playlist, nothing to play.")
+ return
+ end
+
+ local self_redirecting_url =
+ json.entries[1]["_type"] ~= "url_transparent" and
+ json.entries[1]["webpage_url"] and
+ json.entries[1]["webpage_url"] == json["webpage_url"]
+
+
+ -- some funky guessing to detect multi-arc videos
+ if self_redirecting_url and #json.entries > 1
+ and json.entries[1].protocol == "m3u8_native"
+ and json.entries[1].url then
+ msg.verbose("multi-arc video detected, building EDL")
+
+ local playlist = edl_track_joined(json.entries)
+
+ msg.debug("EDL: " .. playlist)
+
+ if not playlist then
+ return
+ end
+
+ -- can't change the http headers for each entry, so use the 1st
+ set_http_headers(json.entries[1].http_headers)
+ set_cookies(json.entries[1].cookies or json.cookies)
+
+ mp.set_property("stream-open-filename", playlist)
+ if json.title and mp.get_property("force-media-title", "") == "" then
+ mp.set_property("file-local-options/force-media-title",
+ json.title)
+ end
+
+ -- there might not be subs for the first segment
+ local entry_wsubs = nil
+ for i, entry in pairs(json.entries) do
+ if entry.requested_subtitles ~= nil then
+ entry_wsubs = i
+ break
+ end
+ end
+
+ if entry_wsubs ~= nil and
+ json.entries[entry_wsubs].duration ~= nil then
+ for j, req in pairs(json.entries[entry_wsubs].requested_subtitles) do
+ local subfile = "edl://"
+ for i, entry in pairs(json.entries) do
+ if entry.requested_subtitles ~= nil and
+ entry.requested_subtitles[j] ~= nil and
+ url_is_safe(entry.requested_subtitles[j].url) then
+ subfile = subfile..edl_escape(entry.requested_subtitles[j].url)
+ else
+ subfile = subfile..edl_escape("memory://WEBVTT")
+ end
+ subfile = subfile..",length="..entry.duration..";"
+ end
+ msg.debug(j.." sub EDL: "..subfile)
+ mp.commandv("sub-add", subfile, "auto", req.ext, j)
+ end
+ end
+
+ elseif self_redirecting_url and #json.entries == 1 then
+ msg.verbose("Playlist with single entry detected.")
+ add_single_video(json.entries[1])
+ else
+ local playlist_index = parse_yt_playlist(url, json)
+ local playlist = {"#EXTM3U"}
+ for i, entry in pairs(json.entries) do
+ local site = entry.url
+ local title = entry.title
+
+ if title ~= nil then
+ title = string.gsub(title, '%s+', ' ')
+ table.insert(playlist, "#EXTINF:0," .. title)
+ end
+
+ --[[ some extractors will still return the full info for
+ all clips in the playlist and the URL will point
+ directly to the file in that case, which we don't
+ want so get the webpage URL instead, which is what
+ we want, but only if we aren't going to trigger an
+ infinite loop
+ --]]
+ if entry["webpage_url"] and not self_redirecting_url then
+ site = entry["webpage_url"]
+ end
+
+ local playlist_url = nil
+
+ -- links without protocol as returned by --flat-playlist
+ if not site:find("://") then
+ -- youtube extractor provides only IDs,
+ -- others come prefixed with the extractor name and ":"
+ local prefix = site:find(":") and "ytdl://" or
+ "https://youtu.be/"
+ playlist_url = prefix .. site
+ elseif url_is_safe(site) then
+ playlist_url = site
+ end
+
+ if playlist_url then
+ table.insert(playlist, playlist_url)
+ -- save the cookies in a table for the playlist hook
+ playlist_cookies[playlist_url] = entry.cookies or json.cookies
+ end
+
+ end
+
+ if use_playlist and
+ not option_was_set("playlist-start") and playlist_index then
+ mp.set_property_number("playlist-start", playlist_index)
+ end
+
+ mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
+ end
+
+ else -- probably a video
+ add_single_video(json)
+ end
+ msg.debug('script running time: '..os.clock()-start_time..' seconds')
+end
+
+if not o.try_ytdl_first then
+ mp.add_hook("on_load", 10, function ()
+ msg.verbose('ytdl:// hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if url:find("ytdl://") ~= 1 then
+ msg.verbose('not a ytdl:// url')
+ return
+ end
+ run_ytdl_hook(url)
+ end)
+end
+
+mp.add_hook("on_load", 20, function ()
+ msg.verbose('playlist hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if playlist_cookies[url] then
+ set_cookies(playlist_cookies[url])
+ end
+end)
+
+mp.add_hook(o.try_ytdl_first and "on_load" or "on_load_fail", 10, function()
+ msg.verbose('full hook')
+ local url = mp.get_property("stream-open-filename", "")
+ if url:find("ytdl://") ~= 1 and
+ not ((url:find("https?://") == 1) and not is_blacklisted(url)) then
+ return
+ end
+ run_ytdl_hook(url)
+end)
+
+mp.add_hook("on_preloaded", 10, function ()
+ if next(chapter_list) ~= nil then
+ msg.verbose("Setting chapters")
+
+ mp.set_property_native("chapter-list", chapter_list)
+ chapter_list = {}
+ end
+end)
diff --git a/player/main.c b/player/main.c
new file mode 100644
index 0000000..27cf9b4
--- /dev/null
+++ b/player/main.c
@@ -0,0 +1,467 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+#include <string.h>
+#include <locale.h>
+
+#include "config.h"
+
+#include <libplacebo/config.h>
+
+#include "mpv_talloc.h"
+
+#include "misc/dispatch.h"
+#include "misc/thread_pool.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/threads.h"
+#include "osdep/timer.h"
+#include "osdep/main-fn.h"
+
+#include "common/av_log.h"
+#include "common/codecs.h"
+#include "common/encode.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/m_property.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "common/stats.h"
+#include "common/global.h"
+#include "filters/f_decoder_wrapper.h"
+#include "options/parse_configfile.h"
+#include "options/parse_commandline.h"
+#include "common/playlist.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "input/input.h"
+
+#include "audio/out/ao.h"
+#include "misc/thread_tools.h"
+#include "sub/osd.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "client.h"
+#include "command.h"
+#include "screenshot.h"
+
+static const char def_config[] =
+#include "etc/builtin.conf.inc"
+;
+
+#if HAVE_COCOA
+#include "osdep/macosx_events.h"
+#endif
+
+#ifndef FULLCONFIG
+#define FULLCONFIG "(missing)\n"
+#endif
+
+enum exit_reason {
+ EXIT_NONE,
+ EXIT_NORMAL,
+ EXIT_ERROR,
+};
+
+const char mp_help_text[] =
+"Usage: mpv [options] [url|path/]filename\n"
+"\n"
+"Basic options:\n"
+" --start=<time> seek to given (percent, seconds, or hh:mm:ss) position\n"
+" --no-audio do not play sound\n"
+" --no-video do not play video\n"
+" --fs fullscreen playback\n"
+" --sub-file=<file> specify subtitle file to use\n"
+" --playlist=<file> specify playlist file\n"
+"\n"
+" --list-options list all mpv options\n"
+" --h=<string> print options which contain the given string in their name\n"
+"\n";
+
+static mp_static_mutex terminal_owner_lock = MP_STATIC_MUTEX_INITIALIZER;
+static struct MPContext *terminal_owner;
+
+static bool cas_terminal_owner(struct MPContext *old, struct MPContext *new)
+{
+ mp_mutex_lock(&terminal_owner_lock);
+ bool r = terminal_owner == old;
+ if (r)
+ terminal_owner = new;
+ mp_mutex_unlock(&terminal_owner_lock);
+ return r;
+}
+
+void mp_update_logging(struct MPContext *mpctx, bool preinit)
+{
+ bool had_log_file = mp_msg_has_log_file(mpctx->global);
+
+ mp_msg_update_msglevels(mpctx->global, mpctx->opts);
+
+ bool enable = mpctx->opts->use_terminal;
+ bool enabled = cas_terminal_owner(mpctx, mpctx);
+ if (enable != enabled) {
+ if (enable && cas_terminal_owner(NULL, mpctx)) {
+ terminal_init();
+ enabled = true;
+ } else if (!enable) {
+ terminal_uninit();
+ cas_terminal_owner(mpctx, NULL);
+ }
+ }
+
+ if (mp_msg_has_log_file(mpctx->global) && !had_log_file) {
+ // for log-file=... in config files.
+ // we did flush earlier messages, but they were in a cyclic buffer, so
+ // the version might have been overwritten. ensure we have it.
+ mp_print_version(mpctx->log, false);
+ }
+
+ if (enabled && !preinit && mpctx->opts->consolecontrols)
+ terminal_setup_getch(mpctx->input);
+}
+
+void mp_print_version(struct mp_log *log, int always)
+{
+ int v = always ? MSGL_INFO : MSGL_V;
+ mp_msg(log, v, "%s %s\n", mpv_version, mpv_copyright);
+ if (strcmp(mpv_builddate, "UNKNOWN"))
+ mp_msg(log, v, " built on %s\n", mpv_builddate);
+ mp_msg(log, v, "libplacebo version: %s\n", PL_VERSION);
+ check_library_versions(log, v);
+ mp_msg(log, v, "\n");
+ // Only in verbose mode.
+ if (!always) {
+ mp_msg(log, MSGL_V, "Configuration: " CONFIGURATION "\n");
+ mp_msg(log, MSGL_V, "List of enabled features: %s\n", FULLCONFIG);
+ #ifdef NDEBUG
+ mp_msg(log, MSGL_V, "Built with NDEBUG.\n");
+ #endif
+ }
+}
+
+void mp_destroy(struct MPContext *mpctx)
+{
+ mp_shutdown_clients(mpctx);
+
+ mp_uninit_ipc(mpctx->ipc_ctx);
+ mpctx->ipc_ctx = NULL;
+
+ uninit_audio_out(mpctx);
+ uninit_video_out(mpctx);
+
+ // If it's still set here, it's an error.
+ encode_lavc_free(mpctx->encode_lavc_ctx);
+ mpctx->encode_lavc_ctx = NULL;
+
+ command_uninit(mpctx);
+
+ mp_clients_destroy(mpctx);
+
+ osd_free(mpctx->osd);
+
+#if HAVE_COCOA
+ cocoa_set_input_context(NULL);
+#endif
+
+ if (cas_terminal_owner(mpctx, mpctx)) {
+ terminal_uninit();
+ cas_terminal_owner(mpctx, NULL);
+ }
+
+ mp_input_uninit(mpctx->input);
+
+ uninit_libav(mpctx->global);
+
+ mp_msg_uninit(mpctx->global);
+ assert(!mpctx->num_abort_list);
+ talloc_free(mpctx->abort_list);
+ mp_mutex_destroy(&mpctx->abort_lock);
+ talloc_free(mpctx->mconfig); // destroy before dispatch
+ talloc_free(mpctx);
+}
+
+static bool handle_help_options(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct mp_log *log = mpctx->log;
+ if (opts->ao_opts->audio_device &&
+ strcmp(opts->ao_opts->audio_device, "help") == 0)
+ {
+ ao_print_devices(mpctx->global, log, mpctx->ao);
+ return true;
+ }
+ if (opts->property_print_help) {
+ property_print_help(mpctx);
+ return true;
+ }
+ if (encode_lavc_showhelp(log, opts->encode_opts))
+ return true;
+ return false;
+}
+
+static int cfg_include(void *ctx, char *filename, int flags)
+{
+ struct MPContext *mpctx = ctx;
+ char *fname = mp_get_user_path(NULL, mpctx->global, filename);
+ int r = m_config_parse_config_file(mpctx->mconfig, mpctx->global, fname, NULL, flags);
+ talloc_free(fname);
+ return r;
+}
+
+// We mostly care about LC_NUMERIC, and how "." vs. "," is treated,
+// Other locale stuff might break too, but probably isn't too bad.
+static bool check_locale(void)
+{
+ char *name = setlocale(LC_NUMERIC, NULL);
+ return !name || strcmp(name, "C") == 0 || strcmp(name, "C.UTF-8") == 0;
+}
+
+struct MPContext *mp_create(void)
+{
+ if (!check_locale()) {
+ // Normally, we never print anything (except if the "terminal" option
+ // is enabled), so this is an exception.
+ fprintf(stderr, "Non-C locale detected. This is not supported.\n"
+ "Call 'setlocale(LC_NUMERIC, \"C\");' in your code.\n");
+ return NULL;
+ }
+
+ char *enable_talloc = getenv("MPV_LEAK_REPORT");
+ if (!enable_talloc)
+ enable_talloc = HAVE_TA_LEAK_REPORT ? "1" : "0";
+ if (strcmp(enable_talloc, "1") == 0)
+ talloc_enable_leak_report();
+
+ mp_time_init();
+
+ struct MPContext *mpctx = talloc(NULL, MPContext);
+ *mpctx = (struct MPContext){
+ .last_chapter = -2,
+ .term_osd_contents = talloc_strdup(mpctx, ""),
+ .osd_progbar = { .type = -1 },
+ .playlist = talloc_zero(mpctx, struct playlist),
+ .dispatch = mp_dispatch_create(mpctx),
+ .playback_abort = mp_cancel_new(mpctx),
+ .thread_pool = mp_thread_pool_create(mpctx, 0, 1, 30),
+ .stop_play = PT_NEXT_ENTRY,
+ .play_dir = 1,
+ };
+
+ mp_mutex_init(&mpctx->abort_lock);
+
+ mpctx->global = talloc_zero(mpctx, struct mpv_global);
+
+ stats_global_init(mpctx->global);
+
+ // Nothing must call mp_msg*() and related before this
+ mp_msg_init(mpctx->global);
+ mpctx->log = mp_log_new(mpctx, mpctx->global->log, "!cplayer");
+ mpctx->statusline = mp_log_new(mpctx, mpctx->log, "!statusline");
+
+ mpctx->stats = stats_ctx_create(mpctx, mpctx->global, "main");
+
+ // Create the config context and register the options
+ mpctx->mconfig = m_config_new(mpctx, mpctx->log, &mp_opt_root);
+ mpctx->opts = mpctx->mconfig->optstruct;
+ mpctx->global->config = mpctx->mconfig->shadow;
+ mpctx->mconfig->includefunc = cfg_include;
+ mpctx->mconfig->includefunc_ctx = mpctx;
+ mpctx->mconfig->use_profiles = true;
+ mpctx->mconfig->is_toplevel = true;
+ mpctx->mconfig->global = mpctx->global;
+ m_config_parse(mpctx->mconfig, "", bstr0(def_config), NULL, 0);
+
+ mpctx->input = mp_input_init(mpctx->global, mp_wakeup_core_cb, mpctx);
+ screenshot_init(mpctx);
+ command_init(mpctx);
+ init_libav(mpctx->global);
+ mp_clients_init(mpctx);
+ mpctx->osd = osd_create(mpctx->global);
+
+#if HAVE_COCOA
+ cocoa_set_input_context(mpctx->input);
+#endif
+
+ char *verbose_env = getenv("MPV_VERBOSE");
+ if (verbose_env)
+ mpctx->opts->verbose = atoi(verbose_env);
+
+ mp_cancel_trigger(mpctx->playback_abort);
+
+ return mpctx;
+}
+
+// Finish mpctx initialization. This must be done after setting up all options.
+// Some of the initializations depend on the options, and can't be changed or
+// undone later.
+// If options is not NULL, apply them as command line player arguments.
+// Returns: 0 on success, -1 on error, 1 if exiting normally (e.g. help).
+int mp_initialize(struct MPContext *mpctx, char **options)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ assert(!mpctx->initialized);
+
+ // Preparse the command line, so we can init the terminal early.
+ if (options) {
+ m_config_preparse_command_line(mpctx->mconfig, mpctx->global,
+ &opts->verbose, options);
+ }
+
+ mp_init_paths(mpctx->global, opts);
+ mp_msg_set_early_logging(mpctx->global, true);
+ mp_update_logging(mpctx, true);
+
+ if (options) {
+ MP_VERBOSE(mpctx, "Command line options:");
+ for (int i = 0; options[i]; i++)
+ MP_VERBOSE(mpctx, " '%s'", options[i]);
+ MP_VERBOSE(mpctx, "\n");
+ }
+
+ mp_print_version(mpctx->log, false);
+
+ mp_parse_cfgfiles(mpctx);
+
+ if (options) {
+ int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist,
+ mpctx->global, options);
+ if (r < 0)
+ return r == M_OPT_EXIT ? 1 : -1;
+ }
+
+ if (opts->operation_mode == 1) {
+ m_config_set_profile(mpctx->mconfig, "builtin-pseudo-gui",
+ M_SETOPT_NO_OVERWRITE);
+ m_config_set_profile(mpctx->mconfig, "pseudo-gui", 0);
+ }
+
+ // Backup the default settings, which should not be stored in the resume
+ // config files. This explicitly includes values set by config files and
+ // the command line.
+ m_config_backup_watch_later_opts(mpctx->mconfig);
+
+ mp_input_load_config(mpctx->input);
+
+ // From this point on, all mpctx members are initialized.
+ mpctx->initialized = true;
+ mpctx->mconfig->option_change_callback = mp_option_change_callback;
+ mpctx->mconfig->option_change_callback_ctx = mpctx;
+ m_config_set_update_dispatch_queue(mpctx->mconfig, mpctx->dispatch);
+ // Run all update handlers.
+ mp_option_change_callback(mpctx, NULL, UPDATE_OPTS_MASK, false);
+
+ if (handle_help_options(mpctx))
+ return 1; // help
+
+ check_library_versions(mp_null_log, 0);
+
+ if (!mpctx->playlist->num_entries && !opts->player_idle_mode &&
+ options)
+ {
+ // nothing to play
+ mp_print_version(mpctx->log, true);
+ MP_INFO(mpctx, "%s", mp_help_text);
+ return 1;
+ }
+
+ MP_STATS(mpctx, "start init");
+
+#if HAVE_COCOA
+ mpv_handle *ctx = mp_new_client(mpctx->clients, "osx");
+ cocoa_set_mpv_handle(ctx);
+#endif
+
+ if (opts->encode_opts->file && opts->encode_opts->file[0]) {
+ mpctx->encode_lavc_ctx = encode_lavc_init(mpctx->global);
+ if(!mpctx->encode_lavc_ctx) {
+ MP_INFO(mpctx, "Encoding initialization failed.\n");
+ return -1;
+ }
+ m_config_set_profile(mpctx->mconfig, "encoding", 0);
+ mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
+ }
+
+ mp_load_scripts(mpctx);
+
+ if (opts->force_vo == 2 && handle_force_window(mpctx, false) < 0)
+ return -1;
+
+ // Needed to properly enter _initial_ idle mode if playlist empty.
+ if (mpctx->opts->player_idle_mode && !mpctx->playlist->num_entries)
+ mpctx->stop_play = PT_STOP;
+
+ MP_STATS(mpctx, "end init");
+
+ return 0;
+}
+
+int mpv_main(int argc, char *argv[])
+{
+ mp_thread_set_name("mpv");
+ struct MPContext *mpctx = mp_create();
+ if (!mpctx)
+ return 1;
+
+ mpctx->is_cli = true;
+
+ char **options = argv && argv[0] ? argv + 1 : NULL; // skips program name
+ int r = mp_initialize(mpctx, options);
+ if (r == 0)
+ mp_play_files(mpctx);
+
+ int rc = 0;
+ const char *reason = NULL;
+ if (r < 0) {
+ reason = "Fatal error";
+ rc = 1;
+ } else if (r > 0) {
+ // nothing
+ } else if (mpctx->stop_play == PT_QUIT) {
+ reason = "Quit";
+ } else if (mpctx->files_played) {
+ if (mpctx->files_errored || mpctx->files_broken) {
+ reason = "Some errors happened";
+ rc = 3;
+ } else {
+ reason = "End of file";
+ }
+ } else if (mpctx->files_broken && !mpctx->files_errored) {
+ reason = "Errors when loading file";
+ rc = 2;
+ } else if (mpctx->files_errored) {
+ reason = "Interrupted by error";
+ rc = 2;
+ } else {
+ reason = "No files played";
+ }
+
+ if (reason)
+ MP_INFO(mpctx, "Exiting... (%s)\n", reason);
+ if (mpctx->has_quit_custom_rc)
+ rc = mpctx->quit_custom_rc;
+
+ mp_destroy(mpctx);
+ return rc;
+}
diff --git a/player/meson.build b/player/meson.build
new file mode 100644
index 0000000..dc334b8
--- /dev/null
+++ b/player/meson.build
@@ -0,0 +1,10 @@
+subdir('javascript')
+subdir('lua')
+
+# Meson doesn't allow having multiple build targets with the same name in the same file.
+# Just generate the com in here for windows builds.
+if win32 and get_option('cplayer')
+ wrapper_sources= '../osdep/win32-console-wrapper.c'
+ executable('mpv', wrapper_sources, c_args: '-municode', link_args: '-municode',
+ name_suffix: 'com', install: true)
+endif
diff --git a/player/misc.c b/player/misc.c
new file mode 100644
index 0000000..b91d52a
--- /dev/null
+++ b/player/misc.c
@@ -0,0 +1,334 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "osdep/io.h"
+#include "osdep/timer.h"
+#include "osdep/threads.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_property.h"
+#include "options/m_config.h"
+#include "common/common.h"
+#include "common/global.h"
+#include "common/encode.h"
+#include "common/playlist.h"
+#include "input/input.h"
+
+#include "audio/out/ao.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+
+const int num_ptracks[STREAM_TYPE_COUNT] = {
+ [STREAM_VIDEO] = 1,
+ [STREAM_AUDIO] = 1,
+ [STREAM_SUB] = 2,
+};
+
+double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t)
+{
+ double length = get_time_length(mpctx);
+ // Relative times are an offset to the start of the file.
+ double start = 0;
+ if (mpctx->demuxer && !mpctx->opts->rebase_start_time)
+ start = mpctx->demuxer->start_time;
+
+ switch (t.type) {
+ case REL_TIME_ABSOLUTE:
+ return t.pos;
+ case REL_TIME_RELATIVE:
+ if (t.pos >= 0) {
+ return start + t.pos;
+ } else {
+ if (length >= 0)
+ return start + MPMAX(length + t.pos, 0.0);
+ }
+ break;
+ case REL_TIME_PERCENT:
+ if (length >= 0)
+ return start + length * (t.pos / 100.0);
+ break;
+ case REL_TIME_CHAPTER:
+ return chapter_start_time(mpctx, t.pos); // already absolute time
+ }
+
+ return MP_NOPTS_VALUE;
+}
+
+static double get_play_end_pts_setting(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double end = rel_time_to_abs(mpctx, opts->play_end);
+ double length = rel_time_to_abs(mpctx, opts->play_length);
+ if (length != MP_NOPTS_VALUE) {
+ double start = get_play_start_pts(mpctx);
+ if (end == MP_NOPTS_VALUE || start + length < end)
+ end = start + length;
+ }
+ return end;
+}
+
+// Return absolute timestamp against which currently playing media should be
+// clipped. Returns MP_NOPTS_VALUE if no clipping should happen.
+double get_play_end_pts(struct MPContext *mpctx)
+{
+ double end = get_play_end_pts_setting(mpctx);
+ double ab[2];
+ if (mpctx->ab_loop_clip && get_ab_loop_times(mpctx, ab)) {
+ if (end == MP_NOPTS_VALUE || end > ab[1])
+ end = ab[1];
+ }
+ return end;
+}
+
+// Get the absolute PTS at which playback should start.
+// Never returns MP_NOPTS_VALUE.
+double get_play_start_pts(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double res = rel_time_to_abs(mpctx, opts->play_start);
+ if (res == MP_NOPTS_VALUE)
+ res = get_start_time(mpctx, mpctx->play_dir);
+ return res;
+}
+
+// Get timestamps to use for AB-loop. Returns false iff any of the timestamps
+// are invalid and/or AB-loops are currently disabled, and set t[] to either
+// the user options or NOPTS on best effort basis.
+bool get_ab_loop_times(struct MPContext *mpctx, double t[2])
+{
+ struct MPOpts *opts = mpctx->opts;
+ int dir = mpctx->play_dir;
+
+ t[0] = opts->ab_loop[0];
+ t[1] = opts->ab_loop[1];
+
+ if (!opts->ab_loop_count)
+ return false;
+
+ if (t[0] == MP_NOPTS_VALUE || t[1] == MP_NOPTS_VALUE || t[0] == t[1])
+ return false;
+
+ if (t[0] * dir > t[1] * dir)
+ MPSWAP(double, t[0], t[1]);
+
+ return true;
+}
+
+double get_track_seek_offset(struct MPContext *mpctx, struct track *track)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (track->selected) {
+ if (track->type == STREAM_AUDIO)
+ return -opts->audio_delay;
+ if (track->type == STREAM_SUB)
+ return -opts->subs_rend->sub_delay;
+ }
+ return 0;
+}
+
+void issue_refresh_seek(struct MPContext *mpctx, enum seek_precision min_prec)
+{
+ // let queued seeks execute at a slightly later point
+ if (mpctx->seek.type) {
+ mp_wakeup_core(mpctx);
+ return;
+ }
+ // repeat currently ongoing seeks
+ if (mpctx->current_seek.type) {
+ mpctx->seek = mpctx->current_seek;
+ mp_wakeup_core(mpctx);
+ return;
+ }
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, get_current_time(mpctx), min_prec, 0);
+}
+
+void update_content_type(struct MPContext *mpctx, struct track *track)
+{
+ enum mp_content_type content_type;
+ if (!track || !track->vo_c) {
+ content_type = MP_CONTENT_NONE;
+ } else if (track->image) {
+ content_type = MP_CONTENT_IMAGE;
+ } else {
+ content_type = MP_CONTENT_VIDEO;
+ }
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_CONTENT_TYPE, &content_type);
+}
+
+void update_vo_playback_state(struct MPContext *mpctx)
+{
+ if (mpctx->video_out && mpctx->video_out->config_ok) {
+ struct voctrl_playback_state oldstate = mpctx->vo_playback_state;
+ struct voctrl_playback_state newstate = {
+ .taskbar_progress = mpctx->opts->vo->taskbar_progress,
+ .playing = mpctx->playing,
+ .paused = mpctx->paused,
+ .percent_pos = get_percent_pos(mpctx),
+ };
+
+ if (oldstate.taskbar_progress != newstate.taskbar_progress ||
+ oldstate.playing != newstate.playing ||
+ oldstate.paused != newstate.paused ||
+ oldstate.percent_pos != newstate.percent_pos)
+ {
+ // Don't update progress bar if it was and still is hidden
+ if ((oldstate.playing && oldstate.taskbar_progress) ||
+ (newstate.playing && newstate.taskbar_progress))
+ {
+ vo_control_async(mpctx->video_out,
+ VOCTRL_UPDATE_PLAYBACK_STATE, &newstate);
+ }
+ mpctx->vo_playback_state = newstate;
+ }
+ } else {
+ mpctx->vo_playback_state = (struct voctrl_playback_state){ 0 };
+ }
+}
+
+void update_window_title(struct MPContext *mpctx, bool force)
+{
+ if (!mpctx->video_out && !mpctx->ao) {
+ talloc_free(mpctx->last_window_title);
+ mpctx->last_window_title = NULL;
+ return;
+ }
+ char *title = mp_property_expand_string(mpctx, mpctx->opts->wintitle);
+ if (!mpctx->last_window_title || force ||
+ strcmp(title, mpctx->last_window_title) != 0)
+ {
+ talloc_free(mpctx->last_window_title);
+ mpctx->last_window_title = talloc_steal(mpctx, title);
+
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_UPDATE_WINDOW_TITLE, title);
+
+ if (mpctx->ao) {
+ ao_control(mpctx->ao, AOCONTROL_UPDATE_STREAM_TITLE, title);
+ }
+ } else {
+ talloc_free(title);
+ }
+}
+
+void error_on_track(struct MPContext *mpctx, struct track *track)
+{
+ if (!track || !track->selected)
+ return;
+ mp_deselect_track(mpctx, track);
+ if (track->type == STREAM_AUDIO)
+ MP_INFO(mpctx, "Audio: no audio\n");
+ if (track->type == STREAM_VIDEO)
+ MP_INFO(mpctx, "Video: no video\n");
+ if (mpctx->opts->stop_playback_on_init_failure ||
+ !(mpctx->vo_chain || mpctx->ao_chain))
+ {
+ if (!mpctx->stop_play)
+ mpctx->stop_play = PT_ERROR;
+ if (mpctx->error_playing >= 0)
+ mpctx->error_playing = MPV_ERROR_NOTHING_TO_PLAY;
+ }
+ mp_wakeup_core(mpctx);
+}
+
+int stream_dump(struct MPContext *mpctx, const char *source_filename)
+{
+ struct MPOpts *opts = mpctx->opts;
+ stream_t *stream = stream_create(source_filename,
+ STREAM_ORIGIN_DIRECT | STREAM_READ,
+ mpctx->playback_abort, mpctx->global);
+ if (!stream)
+ return -1;
+
+ int64_t size = stream_get_size(stream);
+
+ FILE *dest = fopen(opts->stream_dump, "wb");
+ if (!dest) {
+ MP_ERR(mpctx, "Error opening dump file: %s\n", mp_strerror(errno));
+ return -1;
+ }
+
+ bool ok = true;
+
+ while (mpctx->stop_play == KEEP_PLAYING && ok) {
+ if (!opts->quiet && ((stream->pos / (1024 * 1024)) % 2) == 1) {
+ uint64_t pos = stream->pos;
+ MP_MSG(mpctx, MSGL_STATUS, "Dumping %lld/%lld...",
+ (long long int)pos, (long long int)size);
+ }
+ uint8_t buf[4096];
+ int len = stream_read(stream, buf, sizeof(buf));
+ if (!len) {
+ ok &= stream->eof;
+ break;
+ }
+ ok &= fwrite(buf, len, 1, dest) == 1;
+ mp_wakeup_core(mpctx); // don't actually sleep
+ mp_idle(mpctx); // but process input
+ }
+
+ ok &= fclose(dest) == 0;
+ free_stream(stream);
+ return ok ? 0 : -1;
+}
+
+void merge_playlist_files(struct playlist *pl)
+{
+ if (!pl->num_entries)
+ return;
+ char *edl = talloc_strdup(NULL, "edl://");
+ for (int n = 0; n < pl->num_entries; n++) {
+ struct playlist_entry *e = pl->entries[n];
+ if (n)
+ edl = talloc_strdup_append_buffer(edl, ";");
+ // Escape if needed
+ if (e->filename[strcspn(e->filename, "=%,;\n")] ||
+ bstr_strip(bstr0(e->filename)).len != strlen(e->filename))
+ {
+ // %length%
+ edl = talloc_asprintf_append_buffer(edl, "%%%zd%%", strlen(e->filename));
+ }
+ edl = talloc_strdup_append_buffer(edl, e->filename);
+ }
+ playlist_clear(pl);
+ playlist_add_file(pl, edl);
+ talloc_free(edl);
+}
+
+const char *mp_status_str(enum playback_status st)
+{
+ switch (st) {
+ case STATUS_SYNCING: return "syncing";
+ case STATUS_READY: return "ready";
+ case STATUS_PLAYING: return "playing";
+ case STATUS_DRAINING: return "draining";
+ case STATUS_EOF: return "eof";
+ default: return "bug";
+ }
+}
diff --git a/player/osd.c b/player/osd.c
new file mode 100644
index 0000000..dc03229
--- /dev/null
+++ b/player/osd.c
@@ -0,0 +1,580 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "options/m_property.h"
+#include "filters/f_decoder_wrapper.h"
+#include "common/encode.h"
+
+#include "osdep/terminal.h"
+#include "osdep/timer.h"
+
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+
+#define saddf(var, ...) (*(var) = talloc_asprintf_append((*var), __VA_ARGS__))
+
+// append time in the hh:mm:ss format (plus fractions if wanted)
+static void sadd_hhmmssff(char **buf, double time, bool fractions)
+{
+ char *s = mp_format_time(time, fractions);
+ *buf = talloc_strdup_append(*buf, s);
+ talloc_free(s);
+}
+
+static void sadd_percentage(char **buf, int percent) {
+ if (percent >= 0)
+ *buf = talloc_asprintf_append(*buf, " (%d%%)", percent);
+}
+
+static char *join_lines(void *ta_ctx, char **parts, int num_parts)
+{
+ char *res = talloc_strdup(ta_ctx, "");
+ for (int n = 0; n < num_parts; n++)
+ res = talloc_asprintf_append(res, "%s%s", n ? "\n" : "", parts[n]);
+ return res;
+}
+
+static void term_osd_update(struct MPContext *mpctx)
+{
+ int num_parts = 0;
+ char *parts[3] = {0};
+
+ if (!mpctx->opts->use_terminal)
+ return;
+
+ if (mpctx->term_osd_subs && mpctx->term_osd_subs[0])
+ parts[num_parts++] = mpctx->term_osd_subs;
+ if (mpctx->term_osd_text && mpctx->term_osd_text[0])
+ parts[num_parts++] = mpctx->term_osd_text;
+ if (mpctx->term_osd_status && mpctx->term_osd_status[0])
+ parts[num_parts++] = mpctx->term_osd_status;
+
+ char *s = join_lines(mpctx, parts, num_parts);
+
+ if (strcmp(mpctx->term_osd_contents, s) == 0 &&
+ mp_msg_has_status_line(mpctx->global))
+ {
+ talloc_free(s);
+ } else {
+ talloc_free(mpctx->term_osd_contents);
+ mpctx->term_osd_contents = s;
+ mp_msg(mpctx->statusline, MSGL_STATUS, "%s", s);
+ }
+}
+
+static void term_osd_update_title(struct MPContext *mpctx)
+{
+ if (!mpctx->opts->use_terminal)
+ return;
+
+ char *s = mp_property_expand_escaped_string(mpctx, mpctx->opts->term_title);
+ if (bstr_equals(bstr0(s), bstr0(mpctx->term_osd_title))) {
+ talloc_free(s);
+ return;
+ }
+
+ mp_msg_set_term_title(mpctx->statusline, s);
+ mpctx->term_osd_title = talloc_steal(mpctx, s);
+}
+
+void term_osd_set_subs(struct MPContext *mpctx, const char *text)
+{
+ if (mpctx->video_out || !text || !mpctx->opts->subs_rend->sub_visibility)
+ text = ""; // disable
+ if (strcmp(mpctx->term_osd_subs ? mpctx->term_osd_subs : "", text) == 0)
+ return;
+ talloc_free(mpctx->term_osd_subs);
+ mpctx->term_osd_subs = talloc_strdup(mpctx, text);
+ term_osd_update(mpctx);
+}
+
+static void term_osd_set_text_lazy(struct MPContext *mpctx, const char *text)
+{
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if ((video_osd && mpctx->opts->term_osd != 1) || !text)
+ text = ""; // disable
+ talloc_free(mpctx->term_osd_text);
+ mpctx->term_osd_text = talloc_strdup(mpctx, text);
+}
+
+static void term_osd_set_status_lazy(struct MPContext *mpctx, const char *text)
+{
+ talloc_free(mpctx->term_osd_status);
+ mpctx->term_osd_status = talloc_strdup(mpctx, text);
+}
+
+static void add_term_osd_bar(struct MPContext *mpctx, char **line, int width)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (width < 5)
+ return;
+
+ int pos = get_current_pos_ratio(mpctx, false) * (width - 3);
+ pos = MPCLAMP(pos, 0, width - 3);
+
+ bstr chars = bstr0(opts->term_osd_bar_chars);
+ bstr parts[5];
+ for (int n = 0; n < 5; n++)
+ parts[n] = bstr_split_utf8(chars, &chars);
+
+ saddf(line, "\r%.*s", BSTR_P(parts[0]));
+ for (int n = 0; n < pos; n++)
+ saddf(line, "%.*s", BSTR_P(parts[1]));
+ saddf(line, "%.*s", BSTR_P(parts[2]));
+ for (int n = 0; n < width - 3 - pos; n++)
+ saddf(line, "%.*s", BSTR_P(parts[3]));
+ saddf(line, "%.*s", BSTR_P(parts[4]));
+}
+
+static bool is_busy(struct MPContext *mpctx)
+{
+ return !mpctx->restart_complete && mp_time_sec() - mpctx->start_timestamp > 0.3;
+}
+
+static char *get_term_status_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->status_msg)
+ return mp_property_expand_escaped_string(mpctx, opts->status_msg);
+
+ char *line = NULL;
+
+ // Playback status
+ if (is_busy(mpctx)) {
+ saddf(&line, "(...) ");
+ } else if (mpctx->paused_for_cache && !opts->pause) {
+ saddf(&line, "(Buffering) ");
+ } else if (mpctx->paused) {
+ saddf(&line, "(Paused) ");
+ }
+
+ if (mpctx->ao_chain)
+ saddf(&line, "A");
+ if (mpctx->vo_chain)
+ saddf(&line, "V");
+ saddf(&line, ": ");
+
+ // Playback position
+ double speed = opts->term_remaining_playtime ? mpctx->video_speed : 1;
+ sadd_hhmmssff(&line, get_playback_time(mpctx), opts->osd_fractions);
+ saddf(&line, " / ");
+ sadd_hhmmssff(&line, get_time_length(mpctx) / speed, opts->osd_fractions);
+
+ sadd_percentage(&line, get_percent_pos(mpctx));
+
+ // other
+ if (opts->playback_speed != 1)
+ saddf(&line, " x%4.2f", opts->playback_speed);
+
+ // A-V sync
+ if (mpctx->ao_chain && mpctx->vo_chain && !mpctx->vo_chain->is_sparse) {
+ saddf(&line, " A-V:%7.3f", mpctx->last_av_difference);
+ if (fabs(mpctx->total_avsync_change) > 0.05)
+ saddf(&line, " ct:%7.3f", mpctx->total_avsync_change);
+ }
+
+ double position = get_current_pos_ratio(mpctx, true);
+ char lavcbuf[80];
+ if (encode_lavc_getstatus(mpctx->encode_lavc_ctx, lavcbuf, sizeof(lavcbuf),
+ position) >= 0)
+ {
+ // encoding stats
+ saddf(&line, " %s", lavcbuf);
+ } else {
+ // VO stats
+ if (mpctx->vo_chain) {
+ if (mpctx->display_sync_active) {
+ char *r = mp_property_expand_string(mpctx,
+ "${?vsync-ratio:${vsync-ratio}}");
+ if (r[0]) {
+ saddf(&line, " DS: %s/%"PRId64, r,
+ vo_get_delayed_count(mpctx->video_out));
+ }
+ talloc_free(r);
+ }
+ int64_t c = vo_get_drop_count(mpctx->video_out);
+ struct mp_decoder_wrapper *dec = mpctx->vo_chain->track
+ ? mpctx->vo_chain->track->dec : NULL;
+ int dropped_frames =
+ dec ? mp_decoder_wrapper_get_frames_dropped(dec) : 0;
+ if (c > 0 || dropped_frames > 0) {
+ saddf(&line, " Dropped: %"PRId64, c);
+ if (dropped_frames)
+ saddf(&line, "/%d", dropped_frames);
+ }
+ }
+ }
+
+ if (mpctx->demuxer && demux_is_network_cached(mpctx->demuxer)) {
+ saddf(&line, " Cache: ");
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ if (s.ts_duration < 0) {
+ saddf(&line, "???");
+ } else if (s.ts_duration < 10) {
+ saddf(&line, "%2.1fs", s.ts_duration);
+ } else {
+ saddf(&line, "%2ds", (int)s.ts_duration);
+ }
+ int64_t cache_size = s.fw_bytes;
+ if (cache_size > 0) {
+ if (cache_size >= 1024 * 1024) {
+ saddf(&line, "/%lldMB", (long long)(cache_size / 1024 / 1024));
+ } else {
+ saddf(&line, "/%lldKB", (long long)(cache_size / 1024));
+ }
+ }
+ }
+
+ return line;
+}
+
+static void term_osd_print_status_lazy(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ term_osd_update_title(mpctx);
+ update_window_title(mpctx, false);
+ update_vo_playback_state(mpctx);
+
+ if (!opts->use_terminal)
+ return;
+
+ if (opts->quiet || !mpctx->playback_initialized || !mpctx->playing_msg_shown)
+ {
+ if (!mpctx->playing)
+ term_osd_set_status_lazy(mpctx, "");
+ return;
+ }
+
+ char *line = get_term_status_msg(mpctx);
+
+ if (opts->term_osd_bar) {
+ saddf(&line, "\n");
+ int w = 80, h = 24;
+ terminal_get_size(&w, &h);
+ add_term_osd_bar(mpctx, &line, w);
+ }
+
+ term_osd_set_status_lazy(mpctx, line);
+ talloc_free(line);
+}
+
+static bool set_osd_msg_va(struct MPContext *mpctx, int level, int time,
+ const char *fmt, va_list ap)
+{
+ if (level > mpctx->opts->osd_level)
+ return false;
+
+ talloc_free(mpctx->osd_msg_text);
+ mpctx->osd_msg_text = talloc_vasprintf(mpctx, fmt, ap);
+ mpctx->osd_show_pos = false;
+ mpctx->osd_msg_next_duration = time / 1000.0;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+ if (mpctx->osd_msg_next_duration <= 0)
+ mpctx->osd_msg_visible = mp_time_sec();
+ return true;
+}
+
+bool set_osd_msg(struct MPContext *mpctx, int level, int time,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ bool r = set_osd_msg_va(mpctx, level, time, fmt, ap);
+ va_end(ap);
+ return r;
+}
+
+// type: mp_osd_font_codepoints, ASCII, or OSD_BAR_*
+void set_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double neutral, double val)
+{
+ struct MPOpts *opts = mpctx->opts;
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if (opts->osd_level < 1 || !opts->osd_bar_visible || !video_osd)
+ return;
+
+ mpctx->osd_visible = mp_time_sec() + opts->osd_duration / 1000.0;
+ mpctx->osd_progbar.type = type;
+ mpctx->osd_progbar.value = (val - min) / (max - min);
+ mpctx->osd_progbar.num_stops = 0;
+ if (neutral > min && neutral < max) {
+ float pos = (neutral - min) / (max - min);
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, pos);
+ }
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ mp_wakeup_core(mpctx);
+}
+
+// Update a currently displayed bar of the same type, without resetting the
+// timer.
+static void update_osd_bar(struct MPContext *mpctx, int type,
+ double min, double max, double val)
+{
+ if (mpctx->osd_progbar.type != type)
+ return;
+
+ float new_value = (val - min) / (max - min);
+ if (new_value != mpctx->osd_progbar.value) {
+ mpctx->osd_progbar.value = new_value;
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ }
+}
+
+void set_osd_bar_chapters(struct MPContext *mpctx, int type)
+{
+ if (mpctx->osd_progbar.type != type)
+ return;
+
+ mpctx->osd_progbar.num_stops = 0;
+ double len = get_time_length(mpctx);
+ if (len > 0) {
+ // Always render the loop points, even if they're incomplete.
+ double ab[2];
+ bool valid = get_ab_loop_times(mpctx, ab);
+ for (int n = 0; n < 2; n++) {
+ if (ab[n] != MP_NOPTS_VALUE) {
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, ab[n] / len);
+ }
+ }
+ if (!valid) {
+ int num = get_chapter_count(mpctx);
+ for (int n = 0; n < num; n++) {
+ double time = chapter_start_time(mpctx, n);
+ if (time >= 0) {
+ float pos = time / len;
+ MP_TARRAY_APPEND(mpctx, mpctx->osd_progbar.stops,
+ mpctx->osd_progbar.num_stops, pos);
+ }
+ }
+ }
+ }
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ mp_wakeup_core(mpctx);
+}
+
+// osd_function is the symbol appearing in the video status, such as OSD_PLAY
+void set_osd_function(struct MPContext *mpctx, int osd_function)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mpctx->osd_function = osd_function;
+ mpctx->osd_function_visible = mp_time_sec() + opts->osd_duration / 1000.0;
+ mpctx->osd_force_update = true;
+ mp_wakeup_core(mpctx);
+}
+
+void get_current_osd_sym(struct MPContext *mpctx, char *buf, size_t buf_size)
+{
+ int sym = mpctx->osd_function;
+ if (!sym) {
+ if (is_busy(mpctx) || (mpctx->paused_for_cache && !mpctx->opts->pause)) {
+ sym = OSD_CLOCK;
+ } else if (mpctx->paused || mpctx->step_frames) {
+ sym = OSD_PAUSE;
+ } else {
+ sym = OSD_PLAY;
+ }
+ }
+ osd_get_function_sym(buf, buf_size, sym);
+}
+
+static void sadd_osd_status(char **buffer, struct MPContext *mpctx, int level)
+{
+ assert(level >= 0 && level <= 3);
+ if (level == 0)
+ return;
+ char *msg = mpctx->opts->osd_msg[level - 1];
+
+ if (msg && msg[0]) {
+ char *text = mp_property_expand_escaped_string(mpctx, msg);
+ *buffer = talloc_strdup_append(*buffer, text);
+ talloc_free(text);
+ } else if (level >= 2) {
+ bool fractions = mpctx->opts->osd_fractions;
+ char sym[10];
+ get_current_osd_sym(mpctx, sym, sizeof(sym));
+ saddf(buffer, "%s ", sym);
+ char *custom_msg = mpctx->opts->osd_status_msg;
+ if (custom_msg && level == 3) {
+ char *text = mp_property_expand_escaped_string(mpctx, custom_msg);
+ *buffer = talloc_strdup_append(*buffer, text);
+ talloc_free(text);
+ } else {
+ sadd_hhmmssff(buffer, get_playback_time(mpctx), fractions);
+ if (level == 3) {
+ saddf(buffer, " / ");
+ sadd_hhmmssff(buffer, get_time_length(mpctx), fractions);
+ sadd_percentage(buffer, get_percent_pos(mpctx));
+ }
+ }
+ }
+}
+
+// OSD messages initiated by seeking commands are added lazily with this
+// function, because multiple successive seek commands can be coalesced.
+static void add_seek_osd_messages(struct MPContext *mpctx)
+{
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_BAR) {
+ double pos = get_current_pos_ratio(mpctx, false);
+ set_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, 0, MPCLAMP(pos, 0, 1));
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_TEXT) {
+ // Never in term-osd mode
+ bool video_osd = mpctx->video_out && mpctx->opts->video_osd;
+ if (video_osd && mpctx->opts->term_osd != 1) {
+ if (set_osd_msg(mpctx, 1, mpctx->opts->osd_duration, ""))
+ mpctx->osd_show_pos = true;
+ }
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CHAPTER_TEXT) {
+ char *chapter = chapter_display_name(mpctx, get_current_chapter(mpctx));
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration,
+ "Chapter: %s", chapter);
+ talloc_free(chapter);
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CURRENT_FILE) {
+ if (mpctx->filename) {
+ set_osd_msg(mpctx, 1, mpctx->opts->osd_duration, "%s",
+ mpctx->filename);
+ }
+ }
+ mpctx->add_osd_seek_info = 0;
+}
+
+// Update the OSD text (both on VO and terminal status line).
+void update_osd_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct osd_state *osd = mpctx->osd;
+
+ double now = mp_time_sec();
+
+ if (!mpctx->osd_force_update) {
+ // Assume nothing is going on at all.
+ if (!mpctx->osd_idle_update)
+ return;
+
+ double delay = 0.050; // update the OSD at most this often
+ double diff = now - mpctx->osd_last_update;
+ if (diff < delay) {
+ mp_set_timeout(mpctx, delay - diff);
+ return;
+ }
+ }
+ mpctx->osd_force_update = false;
+ mpctx->osd_idle_update = false;
+ mpctx->osd_last_update = now;
+
+ if (mpctx->osd_visible) {
+ double sleep = mpctx->osd_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ mpctx->osd_visible = 0;
+ mpctx->osd_progbar.type = -1; // disable
+ osd_set_progbar(mpctx->osd, &mpctx->osd_progbar);
+ }
+ }
+
+ if (mpctx->osd_function_visible) {
+ double sleep = mpctx->osd_function_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ mpctx->osd_function_visible = 0;
+ mpctx->osd_function = 0;
+ }
+ }
+
+ if (mpctx->osd_msg_next_duration > 0) {
+ // This is done to avoid cutting the OSD message short if slow commands
+ // are executed between setting the OSD message and showing it.
+ mpctx->osd_msg_visible = now + mpctx->osd_msg_next_duration;
+ mpctx->osd_msg_next_duration = 0;
+ }
+
+ if (mpctx->osd_msg_visible) {
+ double sleep = mpctx->osd_msg_visible - now;
+ if (sleep > 0) {
+ mp_set_timeout(mpctx, sleep);
+ mpctx->osd_idle_update = true;
+ } else {
+ talloc_free(mpctx->osd_msg_text);
+ mpctx->osd_msg_text = NULL;
+ mpctx->osd_msg_visible = 0;
+ mpctx->osd_show_pos = false;
+ }
+ }
+
+ add_seek_osd_messages(mpctx);
+
+ if (mpctx->osd_progbar.type == OSD_BAR_SEEK) {
+ double pos = get_current_pos_ratio(mpctx, false);
+ update_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, MPCLAMP(pos, 0, 1));
+ }
+
+ term_osd_set_text_lazy(mpctx, mpctx->osd_msg_text);
+ term_osd_print_status_lazy(mpctx);
+ term_osd_update(mpctx);
+
+ if (!opts->video_osd)
+ return;
+
+ int osd_level = opts->osd_level;
+ if (mpctx->osd_show_pos)
+ osd_level = 3;
+
+ char *text = NULL;
+ sadd_osd_status(&text, mpctx, osd_level);
+ if (mpctx->osd_msg_text && mpctx->osd_msg_text[0]) {
+ text = talloc_asprintf_append(text, "%s%s", text ? "\n" : "",
+ mpctx->osd_msg_text);
+ }
+ osd_set_text(osd, text);
+ talloc_free(text);
+}
diff --git a/player/playloop.c b/player/playloop.c
new file mode 100644
index 0000000..60596da
--- /dev/null
+++ b/player/playloop.c
@@ -0,0 +1,1291 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "client.h"
+#include "command.h"
+#include "core.h"
+#include "mpv_talloc.h"
+#include "screenshot.h"
+
+#include "audio/out/ao.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "common/msg.h"
+#include "common/playlist.h"
+#include "common/stats.h"
+#include "demux/demux.h"
+#include "filters/f_decoder_wrapper.h"
+#include "filters/filter_internal.h"
+#include "input/input.h"
+#include "misc/dispatch.h"
+#include "options/m_config_frontend.h"
+#include "options/m_property.h"
+#include "options/options.h"
+#include "osdep/terminal.h"
+#include "osdep/timer.h"
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "sub/osd.h"
+#include "video/out/vo.h"
+
+// Wait until mp_wakeup_core() is called, since the last time
+// mp_wait_events() was called.
+void mp_wait_events(struct MPContext *mpctx)
+{
+ mp_client_send_property_changes(mpctx);
+
+ stats_event(mpctx->stats, "iterations");
+
+ bool sleeping = mpctx->sleeptime > 0;
+ if (sleeping)
+ MP_STATS(mpctx, "start sleep");
+
+ mp_dispatch_queue_process(mpctx->dispatch, mpctx->sleeptime);
+
+ mpctx->sleeptime = INFINITY;
+
+ if (sleeping)
+ MP_STATS(mpctx, "end sleep");
+}
+
+// Set the timeout used when the playloop goes to sleep. This means the
+// playloop will re-run as soon as the timeout elapses (or earlier).
+// mp_set_timeout(c, 0) is essentially equivalent to mp_wakeup_core(c).
+void mp_set_timeout(struct MPContext *mpctx, double sleeptime)
+{
+ if (mpctx->sleeptime > sleeptime) {
+ mpctx->sleeptime = sleeptime;
+ int64_t abstime = mp_time_ns_add(mp_time_ns(), sleeptime);
+ mp_dispatch_adjust_timeout(mpctx->dispatch, abstime);
+ }
+}
+
+// Cause the playloop to run. This can be called from any thread. If called
+// from within the playloop itself, it will be run immediately again, instead
+// of going to sleep in the next mp_wait_events().
+void mp_wakeup_core(struct MPContext *mpctx)
+{
+ mp_dispatch_interrupt(mpctx->dispatch);
+}
+
+// Opaque callback variant of mp_wakeup_core().
+void mp_wakeup_core_cb(void *ctx)
+{
+ struct MPContext *mpctx = ctx;
+ mp_wakeup_core(mpctx);
+}
+
+void mp_core_lock(struct MPContext *mpctx)
+{
+ mp_dispatch_lock(mpctx->dispatch);
+}
+
+void mp_core_unlock(struct MPContext *mpctx)
+{
+ mp_dispatch_unlock(mpctx->dispatch);
+}
+
+// Process any queued user input.
+static void mp_process_input(struct MPContext *mpctx)
+{
+ int processed = 0;
+ for (;;) {
+ mp_cmd_t *cmd = mp_input_read_cmd(mpctx->input);
+ if (!cmd)
+ break;
+ run_command(mpctx, cmd, NULL, NULL, NULL);
+ processed = 1;
+ }
+ mp_set_timeout(mpctx, mp_input_get_delay(mpctx->input));
+ if (processed)
+ mp_notify(mpctx, MP_EVENT_INPUT_PROCESSED, NULL);
+}
+
+double get_relative_time(struct MPContext *mpctx)
+{
+ int64_t new_time = mp_time_ns();
+ int64_t delta = new_time - mpctx->last_time;
+ mpctx->last_time = new_time;
+ return delta * 1e-9;
+}
+
+void update_core_idle_state(struct MPContext *mpctx)
+{
+ bool eof = mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF;
+ bool active = !mpctx->paused && mpctx->restart_complete &&
+ !mpctx->stop_play && mpctx->in_playloop && !eof;
+
+ if (mpctx->playback_active != active) {
+ mpctx->playback_active = active;
+
+ update_screensaver_state(mpctx);
+
+ mp_notify(mpctx, MP_EVENT_CORE_IDLE, NULL);
+ }
+}
+
+bool get_internal_paused(struct MPContext *mpctx)
+{
+ return mpctx->opts->pause || mpctx->paused_for_cache;
+}
+
+// The value passed here is the new value for mpctx->opts->pause
+void set_pause_state(struct MPContext *mpctx, bool user_pause)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ opts->pause = user_pause;
+
+ bool internal_paused = get_internal_paused(mpctx);
+ if (internal_paused != mpctx->paused) {
+ mpctx->paused = internal_paused;
+
+ if (mpctx->ao) {
+ bool eof = mpctx->audio_status == STATUS_EOF;
+ ao_set_paused(mpctx->ao, internal_paused, eof);
+ }
+
+ if (mpctx->video_out)
+ vo_set_paused(mpctx->video_out, internal_paused);
+
+ mpctx->osd_function = 0;
+ mpctx->osd_force_update = true;
+
+ mp_wakeup_core(mpctx);
+
+ if (internal_paused) {
+ mpctx->step_frames = 0;
+ mpctx->time_frame -= get_relative_time(mpctx);
+ } else {
+ (void)get_relative_time(mpctx); // ignore time that passed during pause
+ }
+ }
+
+ update_core_idle_state(mpctx);
+
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->pause);
+}
+
+void update_internal_pause_state(struct MPContext *mpctx)
+{
+ set_pause_state(mpctx, mpctx->opts->pause);
+}
+
+void update_screensaver_state(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out)
+ return;
+
+ bool saver_state = (!mpctx->playback_active || !mpctx->opts->stop_screensaver) &&
+ mpctx->opts->stop_screensaver != 2;
+ vo_control_async(mpctx->video_out, saver_state ? VOCTRL_RESTORE_SCREENSAVER
+ : VOCTRL_KILL_SCREENSAVER, NULL);
+}
+
+void add_step_frame(struct MPContext *mpctx, int dir)
+{
+ if (!mpctx->vo_chain)
+ return;
+ if (dir > 0) {
+ mpctx->step_frames += 1;
+ set_pause_state(mpctx, false);
+ } else if (dir < 0) {
+ if (!mpctx->hrseek_active) {
+ queue_seek(mpctx, MPSEEK_BACKSTEP, 0, MPSEEK_VERY_EXACT, 0);
+ set_pause_state(mpctx, true);
+ }
+ }
+}
+
+// Clear some playback-related fields on file loading or after seeks.
+void reset_playback_state(struct MPContext *mpctx)
+{
+ mp_filter_reset(mpctx->filter_root);
+
+ reset_video_state(mpctx);
+ reset_audio_state(mpctx);
+ reset_subtitle_state(mpctx);
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ // (Often, but not always, this is redundant and also done elsewhere.)
+ if (t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ if (t->d_sub)
+ sub_set_play_dir(t->d_sub, mpctx->play_dir);
+ }
+
+ // May need unpause first
+ if (mpctx->paused_for_cache)
+ update_internal_pause_state(mpctx);
+
+ mpctx->hrseek_active = false;
+ mpctx->hrseek_lastframe = false;
+ mpctx->hrseek_backstep = false;
+ mpctx->current_seek = (struct seek_params){0};
+ mpctx->playback_pts = MP_NOPTS_VALUE;
+ mpctx->step_frames = 0;
+ mpctx->ab_loop_clip = true;
+ mpctx->restart_complete = false;
+ mpctx->paused_for_cache = false;
+ mpctx->cache_buffer = 100;
+ mpctx->cache_update_pts = MP_NOPTS_VALUE;
+
+ encode_lavc_discontinuity(mpctx->encode_lavc_ctx);
+
+ update_internal_pause_state(mpctx);
+ update_core_idle_state(mpctx);
+}
+
+static void mp_seek(MPContext *mpctx, struct seek_params seek)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->demuxer || !seek.type || seek.amount == MP_NOPTS_VALUE)
+ return;
+
+ if (seek.type == MPSEEK_CHAPTER) {
+ mpctx->last_chapter_flag = false;
+ seek.type = MPSEEK_ABSOLUTE;
+ } else {
+ mpctx->last_chapter_seek = -2;
+ }
+
+ bool hr_seek_very_exact = seek.exact == MPSEEK_VERY_EXACT;
+ double current_time = get_playback_time(mpctx);
+ if (current_time == MP_NOPTS_VALUE && seek.type == MPSEEK_RELATIVE)
+ return;
+ if (current_time == MP_NOPTS_VALUE)
+ current_time = 0;
+ double seek_pts = MP_NOPTS_VALUE;
+ int demux_flags = 0;
+
+ switch (seek.type) {
+ case MPSEEK_ABSOLUTE:
+ seek_pts = seek.amount;
+ break;
+ case MPSEEK_BACKSTEP:
+ seek_pts = current_time;
+ hr_seek_very_exact = true;
+ break;
+ case MPSEEK_RELATIVE:
+ demux_flags = seek.amount > 0 ? SEEK_FORWARD : 0;
+ seek_pts = current_time + seek.amount;
+ break;
+ case MPSEEK_FACTOR: ;
+ double len = get_time_length(mpctx);
+ if (len >= 0)
+ seek_pts = seek.amount * len;
+ break;
+ default: MP_ASSERT_UNREACHABLE();
+ }
+
+ double demux_pts = seek_pts;
+
+ bool hr_seek = seek.exact != MPSEEK_KEYFRAME && seek_pts != MP_NOPTS_VALUE &&
+ (seek.exact >= MPSEEK_EXACT || opts->hr_seek == 1 ||
+ (opts->hr_seek >= 0 && seek.type == MPSEEK_ABSOLUTE) ||
+ (opts->hr_seek == 2 && (!mpctx->vo_chain || mpctx->vo_chain->is_sparse)));
+
+ // Under certain circumstances, prefer SEEK_FACTOR.
+ if (seek.type == MPSEEK_FACTOR && !hr_seek &&
+ (mpctx->demuxer->ts_resets_possible || seek_pts == MP_NOPTS_VALUE))
+ {
+ demux_pts = seek.amount;
+ demux_flags |= SEEK_FACTOR;
+ }
+
+ int play_dir = opts->play_dir;
+ if (play_dir < 0)
+ demux_flags |= SEEK_SATAN;
+
+ if (hr_seek) {
+ double hr_seek_offset = opts->hr_seek_demuxer_offset;
+ // Always try to compensate for possibly bad demuxers in "special"
+ // situations where we need more robustness from the hr-seek code, even
+ // if the user doesn't use --hr-seek-demuxer-offset.
+ // The value is arbitrary, but should be "good enough" in most situations.
+ if (hr_seek_very_exact)
+ hr_seek_offset = MPMAX(hr_seek_offset, 0.5); // arbitrary
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ double offset = 0;
+ if (!mpctx->tracks[n]->is_external)
+ offset += get_track_seek_offset(mpctx, mpctx->tracks[n]);
+ hr_seek_offset = MPMAX(hr_seek_offset, -offset);
+ }
+ demux_pts -= hr_seek_offset * play_dir;
+ demux_flags = (demux_flags | SEEK_HR) & ~SEEK_FORWARD;
+ // For HR seeks in backward playback mode, the correct seek rounding
+ // direction is forward instead of backward.
+ if (play_dir < 0)
+ demux_flags |= SEEK_FORWARD;
+ }
+
+ if (!mpctx->demuxer->seekable)
+ demux_flags |= SEEK_CACHED;
+
+ demux_flags |= SEEK_BLOCK;
+
+ if (!demux_seek(mpctx->demuxer, demux_pts, demux_flags)) {
+ if (!mpctx->demuxer->seekable) {
+ MP_ERR(mpctx, "Cannot seek in this stream.\n");
+ MP_ERR(mpctx, "You can force it with '--force-seekable=yes'.\n");
+ }
+ return;
+ }
+
+ mpctx->play_dir = play_dir;
+
+ // Seek external, extra files too:
+ for (int t = 0; t < mpctx->num_tracks; t++) {
+ struct track *track = mpctx->tracks[t];
+ if (track->selected && track->is_external && track->demuxer) {
+ double main_new_pos = demux_pts;
+ if (!hr_seek || track->is_external)
+ main_new_pos += get_track_seek_offset(mpctx, track);
+ if (demux_flags & SEEK_FACTOR)
+ main_new_pos = seek_pts;
+ demux_seek(track->demuxer, main_new_pos,
+ demux_flags & (SEEK_SATAN | SEEK_BLOCK));
+ }
+ }
+
+ if (!(seek.flags & MPSEEK_FLAG_NOFLUSH))
+ clear_audio_output_buffers(mpctx);
+
+ reset_playback_state(mpctx);
+
+ demux_block_reading(mpctx->demuxer, false);
+ for (int t = 0; t < mpctx->num_tracks; t++) {
+ struct track *track = mpctx->tracks[t];
+ if (track->selected && track->demuxer)
+ demux_block_reading(track->demuxer, false);
+ }
+
+ /* Use the target time as "current position" for further relative
+ * seeks etc until a new video frame has been decoded */
+ mpctx->last_seek_pts = seek_pts;
+
+ if (hr_seek) {
+ mpctx->hrseek_active = true;
+ mpctx->hrseek_backstep = seek.type == MPSEEK_BACKSTEP;
+ mpctx->hrseek_pts = seek_pts * mpctx->play_dir;
+
+ // allow decoder to drop frames before hrseek_pts
+ bool hrseek_framedrop = !hr_seek_very_exact && opts->hr_seek_framedrop;
+
+ MP_VERBOSE(mpctx, "hr-seek, skipping to %f%s%s\n", mpctx->hrseek_pts,
+ hrseek_framedrop ? "" : " (no framedrop)",
+ mpctx->hrseek_backstep ? " (backstep)" : "");
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ struct mp_decoder_wrapper *dec = track->dec;
+ if (dec && hrseek_framedrop)
+ mp_decoder_wrapper_set_start_pts(dec, mpctx->hrseek_pts);
+ }
+ }
+
+ if (mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = KEEP_PLAYING;
+
+ mpctx->start_timestamp = mp_time_sec();
+ mp_wakeup_core(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_SEEK, NULL);
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+
+ update_ab_loop_clip(mpctx);
+
+ mpctx->current_seek = seek;
+}
+
+// This combines consecutive seek requests.
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ enum seek_precision exact, int flags)
+{
+ struct seek_params *seek = &mpctx->seek;
+
+ mp_wakeup_core(mpctx);
+
+ if (mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = KEEP_PLAYING;
+
+ switch (type) {
+ case MPSEEK_RELATIVE:
+ seek->flags |= flags;
+ if (seek->type == MPSEEK_FACTOR)
+ return; // Well... not common enough to bother doing better
+ seek->amount += amount;
+ seek->exact = MPMAX(seek->exact, exact);
+ if (seek->type == MPSEEK_NONE)
+ seek->exact = exact;
+ if (seek->type == MPSEEK_ABSOLUTE)
+ return;
+ seek->type = MPSEEK_RELATIVE;
+ return;
+ case MPSEEK_ABSOLUTE:
+ case MPSEEK_FACTOR:
+ case MPSEEK_BACKSTEP:
+ case MPSEEK_CHAPTER:
+ *seek = (struct seek_params) {
+ .type = type,
+ .amount = amount,
+ .exact = exact,
+ .flags = flags,
+ };
+ return;
+ case MPSEEK_NONE:
+ *seek = (struct seek_params){ 0 };
+ return;
+ }
+ MP_ASSERT_UNREACHABLE();
+}
+
+void execute_queued_seek(struct MPContext *mpctx)
+{
+ if (mpctx->seek.type) {
+ bool queued_hr_seek = mpctx->seek.exact != MPSEEK_KEYFRAME;
+ // Let explicitly imprecise seeks cancel precise seeks:
+ if (mpctx->hrseek_active && !queued_hr_seek)
+ mpctx->start_timestamp = -1e9;
+ // If the user seeks continuously (keeps arrow key down) try to finish
+ // showing a frame from one location before doing another seek (instead
+ // of never updating the screen).
+ if ((mpctx->seek.flags & MPSEEK_FLAG_DELAY) &&
+ mp_time_sec() - mpctx->start_timestamp < 0.3)
+ {
+ // Wait until a video frame is available and has been shown.
+ if (mpctx->video_status < STATUS_PLAYING)
+ return;
+ // On A/V hr-seeks, always wait for the full result, to avoid corner
+ // cases when seeking past EOF (we want it to determine that EOF
+ // actually happened, instead of overwriting it with the new seek).
+ if (mpctx->hrseek_active && queued_hr_seek && mpctx->vo_chain &&
+ mpctx->ao_chain && !mpctx->restart_complete)
+ return;
+ }
+ mp_seek(mpctx, mpctx->seek);
+ mpctx->seek = (struct seek_params){0};
+ }
+}
+
+// NOPTS (i.e. <0) if unknown
+double get_time_length(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ return demuxer && demuxer->duration >= 0 ? demuxer->duration : MP_NOPTS_VALUE;
+}
+
+// Return approximate PTS of first frame played. This can be completely wrong
+// for a number of reasons in a number of situations.
+double get_start_time(struct MPContext *mpctx, int dir)
+{
+ double res = 0;
+ if (mpctx->demuxer) {
+ if (!mpctx->opts->rebase_start_time)
+ res += mpctx->demuxer->start_time;
+ if (dir < 0)
+ res += MPMAX(mpctx->demuxer->duration, 0);
+ }
+ return res;
+}
+
+double get_current_time(struct MPContext *mpctx)
+{
+ if (!mpctx->demuxer)
+ return MP_NOPTS_VALUE;
+ if (mpctx->playback_pts != MP_NOPTS_VALUE)
+ return mpctx->playback_pts * mpctx->play_dir;
+ return mpctx->last_seek_pts;
+}
+
+double get_playback_time(struct MPContext *mpctx)
+{
+ double cur = get_current_time(mpctx);
+ // During seeking, the time corresponds to the last seek time - apply some
+ // cosmetics to it.
+ if (cur != MP_NOPTS_VALUE && mpctx->playback_pts == MP_NOPTS_VALUE) {
+ double length = get_time_length(mpctx);
+ if (length >= 0)
+ cur = MPCLAMP(cur, 0, length);
+ }
+ return cur;
+}
+
+// Return playback position in 0.0-1.0 ratio, or -1 if unknown.
+double get_current_pos_ratio(struct MPContext *mpctx, bool use_range)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return -1;
+ double ans = -1;
+ double start = 0;
+ double len = get_time_length(mpctx);
+ if (use_range) {
+ double startpos = get_play_start_pts(mpctx);
+ double endpos = get_play_end_pts(mpctx);
+ if (endpos > MPMAX(0, len))
+ endpos = MPMAX(0, len);
+ if (endpos < startpos)
+ endpos = startpos;
+ start = startpos;
+ len = endpos - startpos;
+ }
+ double pos = get_current_time(mpctx);
+ if (len > 0)
+ ans = MPCLAMP((pos - start) / len, 0, 1);
+ if (ans < 0) {
+ int64_t size = demuxer->filesize;
+ if (size > 0 && demuxer->filepos >= 0)
+ ans = MPCLAMP(demuxer->filepos / (double)size, 0, 1);
+ }
+ if (use_range) {
+ if (mpctx->opts->play_frames > 0)
+ ans = MPMAX(ans, 1.0 -
+ mpctx->max_frames / (double) mpctx->opts->play_frames);
+ }
+ return ans;
+}
+
+// 0-100, -1 if unknown
+int get_percent_pos(struct MPContext *mpctx)
+{
+ double pos = get_current_pos_ratio(mpctx, false);
+ return pos < 0 ? -1 : (int)round(pos * 100);
+}
+
+// -2 is no chapters, -1 is before first chapter
+int get_current_chapter(struct MPContext *mpctx)
+{
+ if (!mpctx->num_chapters)
+ return -2;
+ double current_pts = get_current_time(mpctx);
+ int i;
+ for (i = 0; i < mpctx->num_chapters; i++)
+ if (current_pts < mpctx->chapters[i].pts)
+ break;
+ return mpctx->last_chapter_flag ?
+ mpctx->last_chapter_seek : MPMAX(mpctx->last_chapter_seek, i - 1);
+}
+
+char *chapter_display_name(struct MPContext *mpctx, int chapter)
+{
+ char *name = chapter_name(mpctx, chapter);
+ char *dname = NULL;
+ if (name) {
+ dname = talloc_asprintf(NULL, "(%d) %s", chapter + 1, name);
+ } else if (chapter < -1) {
+ dname = talloc_strdup(NULL, "(unavailable)");
+ } else {
+ int chapter_count = get_chapter_count(mpctx);
+ if (chapter_count <= 0)
+ dname = talloc_asprintf(NULL, "(%d)", chapter + 1);
+ else
+ dname = talloc_asprintf(NULL, "(%d) of %d", chapter + 1,
+ chapter_count);
+ }
+ return dname;
+}
+
+// returns NULL if chapter name unavailable
+char *chapter_name(struct MPContext *mpctx, int chapter)
+{
+ if (chapter < 0 || chapter >= mpctx->num_chapters)
+ return NULL;
+ return mp_tags_get_str(mpctx->chapters[chapter].metadata, "title");
+}
+
+// returns the start of the chapter in seconds (NOPTS if unavailable)
+double chapter_start_time(struct MPContext *mpctx, int chapter)
+{
+ if (chapter == -1)
+ return 0;
+ if (chapter >= 0 && chapter < mpctx->num_chapters)
+ return mpctx->chapters[chapter].pts;
+ return MP_NOPTS_VALUE;
+}
+
+int get_chapter_count(struct MPContext *mpctx)
+{
+ return mpctx->num_chapters;
+}
+
+// If the current playback position (or seek target) falls before the B
+// position, actually make playback loop when reaching the B point. The
+// intention is that you can seek out of the ab-loop range.
+void update_ab_loop_clip(struct MPContext *mpctx)
+{
+ double pts = get_current_time(mpctx);
+ double ab[2];
+ mpctx->ab_loop_clip = pts != MP_NOPTS_VALUE &&
+ get_ab_loop_times(mpctx, ab) &&
+ pts * mpctx->play_dir <= ab[1] * mpctx->play_dir;
+}
+
+static void handle_osd_redraw(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ return;
+ // If we're playing normally, let OSD be redrawn naturally as part of
+ // video display.
+ if (!mpctx->paused) {
+ if (mpctx->sleeptime < 0.1 && mpctx->video_status == STATUS_PLAYING)
+ return;
+ }
+ // Don't redraw immediately during a seek (makes it significantly slower).
+ bool use_video = mpctx->vo_chain && !mpctx->vo_chain->is_sparse;
+ if (use_video && mp_time_sec() - mpctx->start_timestamp < 0.1) {
+ mp_set_timeout(mpctx, 0.1);
+ return;
+ }
+ bool want_redraw = osd_query_and_reset_want_redraw(mpctx->osd) ||
+ vo_want_redraw(mpctx->video_out);
+ if (!want_redraw)
+ return;
+ vo_redraw(mpctx->video_out);
+}
+
+static void clear_underruns(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain && mpctx->ao_chain->underrun) {
+ mpctx->ao_chain->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+
+ if (mpctx->vo_chain && mpctx->vo_chain->underrun) {
+ mpctx->vo_chain->underrun = false;
+ mp_wakeup_core(mpctx);
+ }
+}
+
+static void handle_update_cache(struct MPContext *mpctx)
+{
+ bool force_update = false;
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->demuxer || mpctx->encode_lavc_ctx) {
+ clear_underruns(mpctx);
+ return;
+ }
+
+ double now = mp_time_sec();
+
+ struct demux_reader_state s;
+ demux_get_reader_state(mpctx->demuxer, &s);
+
+ mpctx->demux_underrun |= s.underrun;
+
+ int cache_buffer = 100;
+ bool use_pause_on_low_cache = opts->cache_pause && mpctx->play_dir > 0;
+
+ if (!mpctx->restart_complete) {
+ // Audio or video is restarting, and initial buffering is enabled. Make
+ // sure we actually restart them in paused mode, so no audio gets
+ // dropped and video technically doesn't start yet.
+ use_pause_on_low_cache &= opts->cache_pause_initial &&
+ (mpctx->video_status == STATUS_READY ||
+ mpctx->audio_status == STATUS_READY);
+ }
+
+ bool is_low = use_pause_on_low_cache && !s.idle &&
+ s.ts_duration < opts->cache_pause_wait;
+
+ // Enter buffering state only if there actually was an underrun (or if
+ // initial caching before playback restart is used).
+ bool need_wait = is_low;
+ if (is_low && !mpctx->paused_for_cache && mpctx->restart_complete) {
+ // Wait only if an output underrun was registered. (Or if there is no
+ // underrun detection.)
+ bool output_underrun = false;
+
+ if (mpctx->ao_chain)
+ output_underrun |= mpctx->ao_chain->underrun;
+ if (mpctx->vo_chain)
+ output_underrun |= mpctx->vo_chain->underrun;
+
+ // Output underruns could be sporadic (unrelated to demuxer buffer state
+ // and for example caused by slow decoding), so use a past demuxer
+ // underrun as indication that the underrun was possibly due to a
+ // demuxer underrun.
+ need_wait = mpctx->demux_underrun && output_underrun;
+ }
+
+ // Let the underrun flag "stick" around until the cache has fully recovered.
+ // See logic where demux_underrun is used.
+ if (!is_low)
+ mpctx->demux_underrun = false;
+
+ if (mpctx->paused_for_cache != need_wait) {
+ mpctx->paused_for_cache = need_wait;
+ update_internal_pause_state(mpctx);
+ force_update = true;
+ if (mpctx->paused_for_cache)
+ mpctx->cache_stop_time = now;
+ }
+
+ if (!mpctx->paused_for_cache)
+ clear_underruns(mpctx);
+
+ if (mpctx->paused_for_cache) {
+ cache_buffer =
+ 100 * MPCLAMP(s.ts_duration / opts->cache_pause_wait, 0, 0.99);
+ mp_set_timeout(mpctx, 0.2);
+ }
+
+ // Also update cache properties.
+ bool busy = !s.idle;
+ if (fabs(mpctx->cache_update_pts - mpctx->playback_pts) >= 1.0)
+ busy = true;
+ if (busy || mpctx->next_cache_update > 0) {
+ if (mpctx->next_cache_update <= now) {
+ mpctx->next_cache_update = busy ? now + 0.25 : 0;
+ force_update = true;
+ }
+ if (mpctx->next_cache_update > 0)
+ mp_set_timeout(mpctx, mpctx->next_cache_update - now);
+ }
+
+ if (mpctx->cache_buffer != cache_buffer) {
+ if ((mpctx->cache_buffer == 100) != (cache_buffer == 100)) {
+ if (cache_buffer < 100) {
+ MP_VERBOSE(mpctx, "Enter buffering (buffer went from %d%% -> %d%%) [%fs].\n",
+ mpctx->cache_buffer, cache_buffer, s.ts_duration);
+ } else {
+ double t = now - mpctx->cache_stop_time;
+ MP_VERBOSE(mpctx, "End buffering (waited %f secs) [%fs].\n",
+ t, s.ts_duration);
+ }
+ } else {
+ MP_VERBOSE(mpctx, "Still buffering (buffer went from %d%% -> %d%%) [%fs].\n",
+ mpctx->cache_buffer, cache_buffer, s.ts_duration);
+ }
+ mpctx->cache_buffer = cache_buffer;
+ force_update = true;
+ }
+
+ if (s.eof && !busy)
+ prefetch_next(mpctx);
+
+ if (force_update) {
+ mpctx->cache_update_pts = mpctx->playback_pts;
+ mp_notify(mpctx, MP_EVENT_CACHE_UPDATE, NULL);
+ }
+}
+
+int get_cache_buffering_percentage(struct MPContext *mpctx)
+{
+ return mpctx->demuxer ? mpctx->cache_buffer : -1;
+}
+
+static void handle_cursor_autohide(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+
+ if (!vo)
+ return;
+
+ bool mouse_cursor_visible = mpctx->mouse_cursor_visible;
+ double now = mp_time_sec();
+
+ unsigned mouse_event_ts = mp_input_get_mouse_event_counter(mpctx->input);
+ if (mpctx->mouse_event_ts != mouse_event_ts) {
+ mpctx->mouse_event_ts = mouse_event_ts;
+ mpctx->mouse_timer = now + opts->cursor_autohide_delay / 1000.0;
+ mouse_cursor_visible = true;
+ }
+
+ if (mpctx->mouse_timer > now) {
+ mp_set_timeout(mpctx, mpctx->mouse_timer - now);
+ } else {
+ mouse_cursor_visible = false;
+ }
+
+ if (opts->cursor_autohide_delay == -1)
+ mouse_cursor_visible = true;
+
+ if (opts->cursor_autohide_delay == -2)
+ mouse_cursor_visible = false;
+
+ if (opts->cursor_autohide_fs && !opts->vo->fullscreen)
+ mouse_cursor_visible = true;
+
+ if (mouse_cursor_visible != mpctx->mouse_cursor_visible)
+ vo_control(vo, VOCTRL_SET_CURSOR_VISIBILITY, &mouse_cursor_visible);
+ mpctx->mouse_cursor_visible = mouse_cursor_visible;
+}
+
+static void handle_vo_events(struct MPContext *mpctx)
+{
+ struct vo *vo = mpctx->video_out;
+ int events = vo ? vo_query_and_reset_events(vo, VO_EVENTS_USER) : 0;
+ if (events & VO_EVENT_RESIZE)
+ mp_notify(mpctx, MP_EVENT_WIN_RESIZE, NULL);
+ if (events & VO_EVENT_WIN_STATE)
+ mp_notify(mpctx, MP_EVENT_WIN_STATE, NULL);
+ if (events & VO_EVENT_DPI)
+ mp_notify(mpctx, MP_EVENT_WIN_STATE2, NULL);
+ if (events & VO_EVENT_FOCUS)
+ mp_notify(mpctx, MP_EVENT_FOCUS, NULL);
+}
+
+static void handle_sstep(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (mpctx->stop_play || !mpctx->restart_complete)
+ return;
+
+ if (opts->step_sec > 0 && !mpctx->paused) {
+ set_osd_function(mpctx, OSD_FFW);
+ queue_seek(mpctx, MPSEEK_RELATIVE, opts->step_sec, MPSEEK_DEFAULT, 0);
+ }
+
+ if (mpctx->video_status >= STATUS_EOF) {
+ if (mpctx->max_frames >= 0 && !mpctx->stop_play)
+ mpctx->stop_play = AT_END_OF_FILE; // force EOF even if audio left
+ if (mpctx->step_frames > 0 && !mpctx->paused)
+ set_pause_state(mpctx, true);
+ }
+}
+
+static void handle_loop_file(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->stop_play != AT_END_OF_FILE)
+ return;
+
+ double target = MP_NOPTS_VALUE;
+ enum seek_precision prec = MPSEEK_DEFAULT;
+
+ double ab[2];
+ if (get_ab_loop_times(mpctx, ab) && mpctx->ab_loop_clip) {
+ if (opts->ab_loop_count > 0) {
+ opts->ab_loop_count--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->ab_loop_count);
+ }
+ target = ab[0];
+ prec = MPSEEK_EXACT;
+ } else if (opts->loop_file) {
+ if (opts->loop_file > 0) {
+ opts->loop_file--;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &opts->loop_file);
+ }
+ target = get_start_time(mpctx, mpctx->play_dir);
+ }
+
+ if (target != MP_NOPTS_VALUE) {
+ if (!mpctx->shown_aframes && !mpctx->shown_vframes) {
+ MP_WARN(mpctx, "No media data to loop.\n");
+ return;
+ }
+
+ mpctx->stop_play = KEEP_PLAYING;
+ set_osd_function(mpctx, OSD_FFW);
+ mark_seek(mpctx);
+
+ // Assumes execute_queued_seek() happens before next audio/video is
+ // attempted to be decoded or filtered.
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, target, prec, MPSEEK_FLAG_NOFLUSH);
+ }
+}
+
+void seek_to_last_frame(struct MPContext *mpctx)
+{
+ if (!mpctx->vo_chain)
+ return;
+ if (mpctx->hrseek_lastframe) // exit if we already tried this
+ return;
+ MP_VERBOSE(mpctx, "seeking to last frame...\n");
+ // Approximately seek close to the end of the file.
+ // Usually, it will seek some seconds before end.
+ double end = MP_NOPTS_VALUE;
+ if (mpctx->play_dir > 0) {
+ end = get_play_end_pts(mpctx);
+ if (end == MP_NOPTS_VALUE)
+ end = get_time_length(mpctx);
+ } else {
+ end = get_start_time(mpctx, 1);
+ }
+ mp_seek(mpctx, (struct seek_params){
+ .type = MPSEEK_ABSOLUTE,
+ .amount = end,
+ .exact = MPSEEK_VERY_EXACT,
+ });
+ // Make it exact: stop seek only if last frame was reached.
+ if (mpctx->hrseek_active) {
+ mpctx->hrseek_pts = INFINITY * mpctx->play_dir;
+ mpctx->hrseek_lastframe = true;
+ }
+}
+
+static void handle_keep_open(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->keep_open && mpctx->stop_play == AT_END_OF_FILE &&
+ (opts->keep_open == 2 ||
+ (!playlist_get_next(mpctx->playlist, 1) && opts->loop_times == 1)))
+ {
+ mpctx->stop_play = KEEP_PLAYING;
+ if (mpctx->vo_chain) {
+ if (!vo_has_frame(mpctx->video_out)) { // EOF not reached normally
+ seek_to_last_frame(mpctx);
+ mpctx->audio_status = STATUS_EOF;
+ mpctx->video_status = STATUS_EOF;
+ }
+ }
+ if (opts->keep_open_pause) {
+ if (mpctx->ao && ao_is_playing(mpctx->ao))
+ return;
+ set_pause_state(mpctx, true);
+ }
+ }
+}
+
+static void handle_chapter_change(struct MPContext *mpctx)
+{
+ int chapter = get_current_chapter(mpctx);
+ if (chapter != mpctx->last_chapter) {
+ mpctx->last_chapter = chapter;
+ mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL);
+ }
+}
+
+// Execute a forceful refresh of the VO window. This clears the window from
+// the previous video. It also creates/destroys the VO on demand.
+// It tries to make the change only in situations where the window is
+// definitely needed or not needed, or if the force parameter is set (the
+// latter also decides whether to clear an existing window, because there's
+// no way to know if this has already been done or not).
+int handle_force_window(struct MPContext *mpctx, bool force)
+{
+ // True if we're either in idle mode, or loading of the file has finished.
+ // It's also set via force in some stages during file loading.
+ bool act = mpctx->stop_play || mpctx->playback_initialized || force;
+
+ // On the other hand, if a video track is selected, but no video is ever
+ // decoded on it, then create the window.
+ bool stalled_video = mpctx->playback_initialized && mpctx->restart_complete &&
+ mpctx->video_status == STATUS_EOF && mpctx->vo_chain &&
+ !mpctx->video_out->config_ok;
+
+ // Don't interfere with real video playback
+ if (mpctx->vo_chain && !stalled_video)
+ return 0;
+
+ if (!mpctx->opts->force_vo) {
+ if (act && !mpctx->vo_chain)
+ uninit_video_out(mpctx);
+ return 0;
+ }
+
+ if (mpctx->opts->force_vo != 2 && !act)
+ return 0;
+
+ if (!mpctx->video_out) {
+ struct vo_extra ex = {
+ .input_ctx = mpctx->input,
+ .osd = mpctx->osd,
+ .encode_lavc_ctx = mpctx->encode_lavc_ctx,
+ .wakeup_cb = mp_wakeup_core_cb,
+ .wakeup_ctx = mpctx,
+ };
+ mpctx->video_out = init_best_video_out(mpctx->global, &ex);
+ if (!mpctx->video_out)
+ goto err;
+ mpctx->mouse_cursor_visible = true;
+ }
+
+ if (!mpctx->video_out->config_ok || force) {
+ struct vo *vo = mpctx->video_out;
+ // Pick whatever works
+ int config_format = 0;
+ uint8_t fmts[IMGFMT_END - IMGFMT_START] = {0};
+ vo_query_formats(vo, fmts);
+ for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
+ if (fmts[fmt - IMGFMT_START]) {
+ config_format = fmt;
+ break;
+ }
+ }
+ int w = 960;
+ int h = 480;
+ struct mp_image_params p = {
+ .imgfmt = config_format,
+ .w = w, .h = h,
+ .p_w = 1, .p_h = 1,
+ .force_window = true,
+ };
+ if (vo_reconfig(vo, &p) < 0)
+ goto err;
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ update_content_type(mpctx, track);
+ update_screensaver_state(mpctx);
+ vo_set_paused(vo, true);
+ vo_redraw(vo);
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+
+ return 0;
+
+err:
+ mpctx->opts->force_vo = 0;
+ m_config_notify_change_opt_ptr(mpctx->mconfig, &mpctx->opts->force_vo);
+ uninit_video_out(mpctx);
+ MP_FATAL(mpctx, "Error opening/initializing the VO window.\n");
+ return -1;
+}
+
+// Potentially needed by some Lua scripts, which assume TICK always comes.
+static void handle_dummy_ticks(struct MPContext *mpctx)
+{
+ if ((mpctx->video_status != STATUS_PLAYING &&
+ mpctx->video_status != STATUS_DRAINING) ||
+ mpctx->paused)
+ {
+ if (mp_time_sec() - mpctx->last_idle_tick > 0.050) {
+ mpctx->last_idle_tick = mp_time_sec();
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+ }
+ }
+}
+
+// Update current playback time.
+static void handle_playback_time(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain &&
+ !mpctx->vo_chain->is_sparse &&
+ mpctx->video_status >= STATUS_PLAYING &&
+ mpctx->video_status < STATUS_EOF)
+ {
+ mpctx->playback_pts = mpctx->video_pts;
+ } else if (mpctx->audio_status >= STATUS_PLAYING &&
+ mpctx->audio_status < STATUS_EOF)
+ {
+ mpctx->playback_pts = playing_audio_pts(mpctx);
+ } else if (mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF)
+ {
+ double apts = playing_audio_pts(mpctx);
+ double vpts = mpctx->video_pts;
+ double mpts = MP_PTS_MAX(apts, vpts);
+ if (mpts != MP_NOPTS_VALUE)
+ mpctx->playback_pts = mpts;
+ }
+}
+
+// We always make sure audio and video buffers are filled before actually
+// starting playback. This code handles starting them at the same time.
+static void handle_playback_restart(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->audio_status < STATUS_READY ||
+ mpctx->video_status < STATUS_READY)
+ return;
+
+ handle_update_cache(mpctx);
+
+ if (mpctx->video_status == STATUS_READY) {
+ mpctx->video_status = STATUS_PLAYING;
+ get_relative_time(mpctx);
+ mp_wakeup_core(mpctx);
+ MP_DBG(mpctx, "starting video playback\n");
+ }
+
+ if (mpctx->audio_status == STATUS_READY) {
+ // If a new seek is queued while the current one finishes, don't
+ // actually play the audio, but resume seeking immediately.
+ if (mpctx->seek.type && mpctx->video_status == STATUS_PLAYING) {
+ handle_playback_time(mpctx);
+ mpctx->seek.flags &= ~MPSEEK_FLAG_DELAY; // immediately
+ execute_queued_seek(mpctx);
+ return;
+ }
+
+ audio_start_ao(mpctx);
+ }
+
+ if (!mpctx->restart_complete) {
+ mpctx->hrseek_active = false;
+ mpctx->restart_complete = true;
+ mpctx->current_seek = (struct seek_params){0};
+ handle_playback_time(mpctx);
+ mp_notify(mpctx, MPV_EVENT_PLAYBACK_RESTART, NULL);
+ update_core_idle_state(mpctx);
+ if (!mpctx->playing_msg_shown) {
+ if (opts->playing_msg && opts->playing_msg[0]) {
+ char *msg =
+ mp_property_expand_escaped_string(mpctx, opts->playing_msg);
+ struct mp_log *log = mp_log_new(NULL, mpctx->log, "!term-msg");
+ mp_info(log, "%s\n", msg);
+ talloc_free(log);
+ talloc_free(msg);
+ }
+ if (opts->osd_playing_msg && opts->osd_playing_msg[0]) {
+ char *msg =
+ mp_property_expand_escaped_string(mpctx, opts->osd_playing_msg);
+ set_osd_msg(mpctx, 1, opts->osd_playing_msg_duration ?
+ opts->osd_playing_msg_duration : opts->osd_duration,
+ "%s", msg);
+ talloc_free(msg);
+ }
+ }
+ mpctx->playing_msg_shown = true;
+ mp_wakeup_core(mpctx);
+ update_ab_loop_clip(mpctx);
+ MP_VERBOSE(mpctx, "playback restart complete @ %f, audio=%s, video=%s%s\n",
+ mpctx->playback_pts, mp_status_str(mpctx->audio_status),
+ mp_status_str(mpctx->video_status),
+ get_internal_paused(mpctx) ? " (paused)" : "");
+
+ // To avoid strange effects when using relative seeks, especially if
+ // there are no proper audio & video timestamps (seeks after EOF).
+ double length = get_time_length(mpctx);
+ if (mpctx->last_seek_pts != MP_NOPTS_VALUE && length >= 0)
+ mpctx->last_seek_pts = MPCLAMP(mpctx->last_seek_pts, 0, length);
+
+ // Continuous seeks past EOF => treat as EOF instead of repeating seek.
+ if (mpctx->seek.type == MPSEEK_RELATIVE && mpctx->seek.amount > 0 &&
+ mpctx->video_status == STATUS_EOF &&
+ mpctx->audio_status == STATUS_EOF)
+ mpctx->seek = (struct seek_params){0};
+ }
+}
+
+static void handle_eof(struct MPContext *mpctx)
+{
+ if (mpctx->seek.type)
+ return; // for proper keep-open operation
+
+ /* Don't quit while paused and we're displaying the last video frame. On the
+ * other hand, if we don't have a video frame, then the user probably seeked
+ * outside of the video, and we do want to quit. */
+ bool prevent_eof =
+ mpctx->paused && mpctx->video_out && vo_has_frame(mpctx->video_out);
+ /* It's possible for the user to simultaneously switch both audio
+ * and video streams to "disabled" at runtime. Handle this by waiting
+ * rather than immediately stopping playback due to EOF.
+ */
+ if ((mpctx->ao_chain || mpctx->vo_chain) && !prevent_eof &&
+ mpctx->audio_status == STATUS_EOF &&
+ mpctx->video_status == STATUS_EOF &&
+ !mpctx->stop_play)
+ {
+ mpctx->stop_play = AT_END_OF_FILE;
+ }
+}
+
+void run_playloop(struct MPContext *mpctx)
+{
+ if (encode_lavc_didfail(mpctx->encode_lavc_ctx)) {
+ mpctx->stop_play = PT_ERROR;
+ return;
+ }
+
+ update_demuxer_properties(mpctx);
+
+ handle_cursor_autohide(mpctx);
+ handle_vo_events(mpctx);
+ handle_command_updates(mpctx);
+
+ if (mpctx->lavfi && mp_filter_has_failed(mpctx->lavfi))
+ mpctx->stop_play = AT_END_OF_FILE;
+
+ fill_audio_out_buffers(mpctx);
+ write_video(mpctx);
+
+ handle_playback_restart(mpctx);
+
+ handle_playback_time(mpctx);
+
+ handle_dummy_ticks(mpctx);
+
+ update_osd_msg(mpctx);
+ if (mpctx->video_status == STATUS_EOF)
+ update_subtitles(mpctx, mpctx->playback_pts);
+
+ handle_each_frame_screenshot(mpctx);
+
+ handle_eof(mpctx);
+
+ handle_loop_file(mpctx);
+
+ handle_keep_open(mpctx);
+
+ handle_sstep(mpctx);
+
+ update_core_idle_state(mpctx);
+
+ execute_queued_seek(mpctx);
+
+ if (mpctx->stop_play)
+ return;
+
+ handle_osd_redraw(mpctx);
+
+ if (mp_filter_graph_run(mpctx->filter_root))
+ mp_wakeup_core(mpctx);
+
+ mp_wait_events(mpctx);
+
+ handle_update_cache(mpctx);
+
+ mp_process_input(mpctx);
+
+ handle_chapter_change(mpctx);
+
+ handle_force_window(mpctx, false);
+}
+
+void mp_idle(struct MPContext *mpctx)
+{
+ handle_dummy_ticks(mpctx);
+ mp_wait_events(mpctx);
+ mp_process_input(mpctx);
+ handle_command_updates(mpctx);
+ handle_update_cache(mpctx);
+ handle_cursor_autohide(mpctx);
+ handle_vo_events(mpctx);
+ update_osd_msg(mpctx);
+ handle_osd_redraw(mpctx);
+}
+
+// Waiting for the slave master to send us a new file to play.
+void idle_loop(struct MPContext *mpctx)
+{
+ // ================= idle loop (STOP state) =========================
+ bool need_reinit = true;
+ while (mpctx->opts->player_idle_mode && mpctx->stop_play == PT_STOP) {
+ if (need_reinit) {
+ uninit_audio_out(mpctx);
+ handle_force_window(mpctx, true);
+ mp_wakeup_core(mpctx);
+ mp_notify(mpctx, MPV_EVENT_IDLE, NULL);
+ need_reinit = false;
+ }
+ mp_idle(mpctx);
+ }
+}
diff --git a/player/screenshot.c b/player/screenshot.c
new file mode 100644
index 0000000..e4d0912
--- /dev/null
+++ b/player/screenshot.c
@@ -0,0 +1,611 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <libavcodec/avcodec.h>
+
+#include "common/global.h"
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+#include "screenshot.h"
+#include "core.h"
+#include "command.h"
+#include "input/cmd.h"
+#include "misc/bstr.h"
+#include "misc/dispatch.h"
+#include "misc/node.h"
+#include "misc/thread_tools.h"
+#include "common/msg.h"
+#include "options/path.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+#include "video/out/vo.h"
+#include "video/image_writer.h"
+#include "video/sws_utils.h"
+#include "sub/osd.h"
+
+#include "video/csputils.h"
+
+#define MODE_FULL_WINDOW 1
+#define MODE_SUBTITLES 2
+
+typedef struct screenshot_ctx {
+ struct MPContext *mpctx;
+ struct mp_log *log;
+
+ // Command to repeat in each-frame mode.
+ struct mp_cmd *each_frame;
+
+ int frameno;
+ uint64_t last_frame_count;
+} screenshot_ctx;
+
+void screenshot_init(struct MPContext *mpctx)
+{
+ mpctx->screenshot_ctx = talloc(mpctx, screenshot_ctx);
+ *mpctx->screenshot_ctx = (screenshot_ctx) {
+ .mpctx = mpctx,
+ .frameno = 1,
+ .log = mp_log_new(mpctx, mpctx->log, "screenshot")
+ };
+}
+
+static char *stripext(void *talloc_ctx, const char *s)
+{
+ const char *end = strrchr(s, '.');
+ if (!end)
+ end = s + strlen(s);
+ return talloc_asprintf(talloc_ctx, "%.*s", (int)(end - s), s);
+}
+
+static bool write_screenshot(struct mp_cmd_ctx *cmd, struct mp_image *img,
+ const char *filename, struct image_writer_opts *opts)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ struct image_writer_opts *gopts = mpctx->opts->screenshot_image_opts;
+ struct image_writer_opts opts_copy = opts ? *opts : *gopts;
+
+ mp_cmd_msg(cmd, MSGL_V, "Starting screenshot: '%s'", filename);
+
+ mp_core_unlock(mpctx);
+
+ bool ok = img && write_image(img, &opts_copy, filename, mpctx->global,
+ mpctx->screenshot_ctx->log);
+
+ mp_core_lock(mpctx);
+
+ if (ok) {
+ mp_cmd_msg(cmd, MSGL_INFO, "Screenshot: '%s'", filename);
+ } else {
+ mp_cmd_msg(cmd, MSGL_ERR, "Error writing screenshot!");
+ }
+ return ok;
+}
+
+#ifdef _WIN32
+#define ILLEGAL_FILENAME_CHARS "?\"/\\<>*|:"
+#else
+#define ILLEGAL_FILENAME_CHARS "/"
+#endif
+
+// Replace all characters disallowed in filenames with '_' and return the newly
+// allocated result string.
+static char *sanitize_filename(void *talloc_ctx, const char *s)
+{
+ char *res = talloc_strdup(talloc_ctx, s);
+ char *cur = res;
+ while (*cur) {
+ if (strchr(ILLEGAL_FILENAME_CHARS, *cur) || ((unsigned char)*cur) < 32)
+ *cur = '_';
+ cur++;
+ }
+ return res;
+}
+
+static void append_filename(char **s, const char *f)
+{
+ char *append = sanitize_filename(NULL, f);
+ *s = talloc_strdup_append(*s, append);
+ talloc_free(append);
+}
+
+static char *create_fname(struct MPContext *mpctx, char *template,
+ const char *file_ext, int *sequence, int *frameno)
+{
+ char *res = talloc_strdup(NULL, ""); //empty string, non-NULL context
+
+ time_t raw_time = time(NULL);
+ struct tm *local_time = localtime(&raw_time);
+
+ if (!template || *template == '\0')
+ return NULL;
+
+ for (;;) {
+ char *next = strchr(template, '%');
+ if (!next)
+ break;
+ res = talloc_strndup_append(res, template, next - template);
+ template = next + 1;
+ char fmt = *template++;
+ switch (fmt) {
+ case '#':
+ case '0':
+ case 'n': {
+ int digits = '4';
+ if (fmt == '#') {
+ if (!*sequence) {
+ *frameno = 1;
+ }
+ fmt = *template++;
+ }
+ if (fmt == '0') {
+ digits = *template++;
+ if (digits < '0' || digits > '9')
+ goto error_exit;
+ fmt = *template++;
+ }
+ if (fmt != 'n')
+ goto error_exit;
+ char fmtstr[] = {'%', '0', digits, 'd', '\0'};
+ res = talloc_asprintf_append(res, fmtstr, *frameno);
+ if (*frameno < 100000 - 1) {
+ (*frameno) += 1;
+ (*sequence) += 1;
+ }
+ break;
+ }
+ case 'f':
+ case 'F': {
+ char *video_file = NULL;
+ if (mpctx->filename)
+ video_file = mp_basename(mpctx->filename);
+
+ if (!video_file)
+ video_file = "NO_FILE";
+
+ char *name = video_file;
+ if (fmt == 'F')
+ name = stripext(res, video_file);
+ append_filename(&res, name);
+ break;
+ }
+ case 'x':
+ case 'X': {
+ char *fallback = "";
+ if (fmt == 'X') {
+ if (template[0] != '{')
+ goto error_exit;
+ char *end = strchr(template, '}');
+ if (!end)
+ goto error_exit;
+ fallback = talloc_strndup(res, template + 1, end - template - 1);
+ template = end + 1;
+ }
+ if (!mpctx->filename || mp_is_url(bstr0(mpctx->filename))) {
+ res = talloc_strdup_append(res, fallback);
+ } else {
+ bstr dir = mp_dirname(mpctx->filename);
+ if (!bstr_equals0(dir, "."))
+ res = talloc_asprintf_append(res, "%.*s", BSTR_P(dir));
+ }
+ break;
+ }
+ case 'p':
+ case 'P': {
+ char *t = mp_format_time(get_playback_time(mpctx), fmt == 'P');
+ append_filename(&res, t);
+ talloc_free(t);
+ break;
+ }
+ case 'w': {
+ char tfmt = *template;
+ if (!tfmt)
+ goto error_exit;
+ template++;
+ char fmtstr[] = {'%', tfmt, '\0'};
+ char *s = mp_format_time_fmt(fmtstr, get_playback_time(mpctx));
+ if (!s)
+ goto error_exit;
+ append_filename(&res, s);
+ talloc_free(s);
+ break;
+ }
+ case 't': {
+ char tfmt = *template;
+ if (!tfmt)
+ goto error_exit;
+ template++;
+ char fmtstr[] = {'%', tfmt, '\0'};
+ char buffer[80];
+ if (strftime(buffer, sizeof(buffer), fmtstr, local_time) == 0)
+ buffer[0] = '\0';
+ append_filename(&res, buffer);
+ break;
+ }
+ case '{': {
+ char *end = strchr(template, '}');
+ if (!end)
+ goto error_exit;
+ struct bstr prop = bstr_splice(bstr0(template), 0, end - template);
+ char *tmp = talloc_asprintf(NULL, "${%.*s}", BSTR_P(prop));
+ char *s = mp_property_expand_string(mpctx, tmp);
+ talloc_free(tmp);
+ if (s)
+ append_filename(&res, s);
+ talloc_free(s);
+ template = end + 1;
+ break;
+ }
+ case '%':
+ res = talloc_strdup_append(res, "%");
+ break;
+ default:
+ goto error_exit;
+ }
+ }
+
+ res = talloc_strdup_append(res, template);
+ res = talloc_asprintf_append(res, ".%s", file_ext);
+ char *fname = mp_get_user_path(NULL, mpctx->global, res);
+ talloc_free(res);
+ return fname;
+
+error_exit:
+ talloc_free(res);
+ return NULL;
+}
+
+static char *gen_fname(struct mp_cmd_ctx *cmd, const char *file_ext)
+{
+ struct MPContext *mpctx = cmd->mpctx;
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ int sequence = 0;
+ for (;;) {
+ int prev_sequence = sequence;
+ char *fname = create_fname(ctx->mpctx,
+ ctx->mpctx->opts->screenshot_template,
+ file_ext,
+ &sequence,
+ &ctx->frameno);
+
+ if (!fname) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Invalid screenshot filename "
+ "template! Fix or remove the --screenshot-template "
+ "option.");
+ return NULL;
+ }
+
+ char *dir = ctx->mpctx->opts->screenshot_dir;
+ if (dir && dir[0]) {
+ void *t = fname;
+ dir = mp_get_user_path(t, ctx->mpctx->global, dir);
+ fname = mp_path_join(NULL, dir, fname);
+
+ mp_mkdirp(dir);
+
+ talloc_free(t);
+ }
+
+ char *full_dir = bstrto0(fname, mp_dirname(fname));
+ if (!mp_path_exists(full_dir)) {
+ mp_mkdirp(full_dir);
+ }
+
+ if (!mp_path_exists(fname))
+ return fname;
+
+ if (sequence == prev_sequence) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Can't save screenshot, file '%s' "
+ "already exists!", fname);
+ talloc_free(fname);
+ return NULL;
+ }
+
+ talloc_free(fname);
+ }
+}
+
+static void add_osd(struct MPContext *mpctx, struct mp_image *image, int mode)
+{
+ bool window = mode == MODE_FULL_WINDOW;
+ struct mp_osd_res res = window ? osd_get_vo_res(mpctx->video_out->osd) :
+ osd_res_from_image_params(&image->params);
+ if (mode == MODE_SUBTITLES || window) {
+ osd_draw_on_image(mpctx->osd, res, mpctx->video_pts,
+ OSD_DRAW_SUB_ONLY, image);
+ }
+ if (window) {
+ osd_draw_on_image(mpctx->osd, res, mpctx->video_pts,
+ OSD_DRAW_OSD_ONLY, image);
+ }
+}
+
+static struct mp_image *screenshot_get(struct MPContext *mpctx, int mode,
+ bool high_depth)
+{
+ struct mp_image *image = NULL;
+ const struct image_writer_opts *imgopts = mpctx->opts->screenshot_image_opts;
+ if (mode == MODE_SUBTITLES && osd_get_render_subs_in_filter(mpctx->osd))
+ mode = 0;
+
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ return NULL;
+
+ vo_wait_frame(mpctx->video_out); // important for each-frame mode
+
+ bool use_sw = mpctx->opts->screenshot_sw;
+ bool window = mode == MODE_FULL_WINDOW;
+ struct voctrl_screenshot ctrl = {
+ .scaled = window,
+ .subs = mode != 0,
+ .osd = window,
+ .high_bit_depth = high_depth && imgopts->high_bit_depth,
+ .native_csp = image_writer_flexible_csp(imgopts),
+ };
+ if (!use_sw)
+ vo_control(mpctx->video_out, VOCTRL_SCREENSHOT, &ctrl);
+ image = ctrl.res;
+
+ if (!use_sw && !image && window)
+ vo_control(mpctx->video_out, VOCTRL_SCREENSHOT_WIN, &image);
+
+ if (!image) {
+ use_sw = true;
+ MP_VERBOSE(mpctx->screenshot_ctx, "Falling back to software screenshot.\n");
+ image = vo_get_current_frame(mpctx->video_out);
+ }
+
+ // vo_get_current_frame() can return a hardware frame, which we have to download first.
+ if (image && image->fmt.flags & MP_IMGFLAG_HWACCEL) {
+ struct mp_image *nimage = mp_image_hw_download(image, NULL);
+ talloc_free(image);
+ if (!nimage)
+ return NULL;
+ image = nimage;
+ }
+
+ if (use_sw && image && window) {
+ if (mp_image_crop_valid(&image->params) &&
+ (mp_rect_w(image->params.crop) != image->w ||
+ mp_rect_h(image->params.crop) != image->h))
+ {
+ struct mp_image *nimage = mp_image_new_ref(image);
+ if (!nimage) {
+ MP_ERR(mpctx->screenshot_ctx, "mp_image_new_ref failed!\n");
+ return NULL;
+ }
+ mp_image_crop_rc(nimage, image->params.crop);
+ talloc_free(image);
+ image = nimage;
+ }
+ struct mp_osd_res res = osd_get_vo_res(mpctx->video_out->osd);
+ struct mp_osd_res image_res = osd_res_from_image_params(&image->params);
+ if (!osd_res_equals(res, image_res)) {
+ struct mp_image *nimage = mp_image_alloc(image->imgfmt, res.w, res.h);
+ if (!nimage) {
+ talloc_free(image);
+ return NULL;
+ }
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+ mp_sws_scale(sws, nimage, image);
+ talloc_free(image);
+ talloc_free(sws);
+ image = nimage;
+ }
+ }
+
+ if (!image)
+ return NULL;
+
+ if (use_sw && mode != 0)
+ add_osd(mpctx, image, mode);
+ mp_image_params_guess_csp(&image->params);
+ return image;
+}
+
+struct mp_image *convert_image(struct mp_image *image, int destfmt,
+ struct mpv_global *global, struct mp_log *log)
+{
+ int d_w, d_h;
+ mp_image_params_get_dsize(&image->params, &d_w, &d_h);
+
+ struct mp_image_params p = {
+ .imgfmt = destfmt,
+ .w = d_w,
+ .h = d_h,
+ .p_w = 1,
+ .p_h = 1,
+ };
+ mp_image_params_guess_csp(&p);
+
+ if (mp_image_params_equal(&p, &image->params))
+ return mp_image_new_ref(image);
+
+ struct mp_image *dst = mp_image_alloc(p.imgfmt, p.w, p.h);
+ if (!dst) {
+ mp_err(log, "Out of memory.\n");
+ return NULL;
+ }
+ mp_image_copy_attributes(dst, image);
+
+ dst->params = p;
+
+ struct mp_sws_context *sws = mp_sws_alloc(NULL);
+ sws->log = log;
+ if (global)
+ mp_sws_enable_cmdline_opts(sws, global);
+ bool ok = mp_sws_scale(sws, dst, image) >= 0;
+ talloc_free(sws);
+
+ if (!ok) {
+ mp_err(log, "Error when converting image.\n");
+ talloc_free(dst);
+ return NULL;
+ }
+
+ return dst;
+}
+
+// mode is the same as in screenshot_get()
+static struct mp_image *screenshot_get_rgb(struct MPContext *mpctx, int mode)
+{
+ struct mp_image *mpi = screenshot_get(mpctx, mode, false);
+ if (!mpi)
+ return NULL;
+ struct mp_image *res = convert_image(mpi, IMGFMT_BGR0, mpctx->global,
+ mpctx->log);
+ talloc_free(mpi);
+ return res;
+}
+
+void cmd_screenshot_to_file(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ const char *filename = cmd->args[0].v.s;
+ int mode = cmd->args[1].v.i;
+ struct image_writer_opts opts = *mpctx->opts->screenshot_image_opts;
+
+ char *ext = mp_splitext(filename, NULL);
+ int format = image_writer_format_from_ext(ext);
+ if (format)
+ opts.format = format;
+ bool high_depth = image_writer_high_depth(&opts);
+ struct mp_image *image = screenshot_get(mpctx, mode, high_depth);
+ if (!image) {
+ mp_cmd_msg(cmd, MSGL_ERR, "Taking screenshot failed.");
+ cmd->success = false;
+ return;
+ }
+ cmd->success = write_screenshot(cmd, image, filename, &opts);
+ talloc_free(image);
+}
+
+void cmd_screenshot(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct mpv_node *res = &cmd->result;
+ int mode = cmd->args[0].v.i & 3;
+ bool each_frame_toggle = (cmd->args[0].v.i | cmd->args[1].v.i) & 8;
+ bool each_frame_mode = cmd->args[0].v.i & 16;
+
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (mode == MODE_SUBTITLES && osd_get_render_subs_in_filter(mpctx->osd))
+ mode = 0;
+
+ if (!each_frame_mode) {
+ if (each_frame_toggle) {
+ if (ctx->each_frame) {
+ TA_FREEP(&ctx->each_frame);
+ return;
+ }
+ ctx->each_frame = talloc_steal(ctx, mp_cmd_clone(cmd->cmd));
+ ctx->each_frame->args[0].v.i |= 16;
+ } else {
+ TA_FREEP(&ctx->each_frame);
+ }
+ }
+
+ cmd->success = false;
+
+ struct image_writer_opts *opts = mpctx->opts->screenshot_image_opts;
+ bool high_depth = image_writer_high_depth(opts);
+
+ struct mp_image *image = screenshot_get(mpctx, mode, high_depth);
+
+ if (image) {
+ char *filename = gen_fname(cmd, image_writer_file_ext(opts));
+ if (filename) {
+ cmd->success = write_screenshot(cmd, image, filename, NULL);
+ if (cmd->success) {
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_string(res, "filename", filename);
+ }
+ }
+ talloc_free(filename);
+ } else {
+ mp_cmd_msg(cmd, MSGL_ERR, "Taking screenshot failed.");
+ }
+
+ talloc_free(image);
+}
+
+void cmd_screenshot_raw(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct mpv_node *res = &cmd->result;
+
+ struct mp_image *img = screenshot_get_rgb(mpctx, cmd->args[0].v.i);
+ if (!img) {
+ cmd->success = false;
+ return;
+ }
+
+ node_init(res, MPV_FORMAT_NODE_MAP, NULL);
+ node_map_add_int64(res, "w", img->w);
+ node_map_add_int64(res, "h", img->h);
+ node_map_add_int64(res, "stride", img->stride[0]);
+ node_map_add_string(res, "format", "bgr0");
+ struct mpv_byte_array *ba =
+ node_map_add(res, "data", MPV_FORMAT_BYTE_ARRAY)->u.ba;
+ *ba = (struct mpv_byte_array){
+ .data = img->planes[0],
+ .size = img->stride[0] * img->h,
+ };
+ talloc_steal(ba, img);
+}
+
+static void screenshot_fin(struct mp_cmd_ctx *cmd)
+{
+ void **a = cmd->on_completion_priv;
+ struct MPContext *mpctx = a[0];
+ struct mp_waiter *waiter = a[1];
+
+ mp_waiter_wakeup(waiter, 0);
+ mp_wakeup_core(mpctx);
+}
+
+void handle_each_frame_screenshot(struct MPContext *mpctx)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (!ctx->each_frame)
+ return;
+
+ if (ctx->last_frame_count == mpctx->shown_vframes)
+ return;
+ ctx->last_frame_count = mpctx->shown_vframes;
+
+ struct mp_waiter wait = MP_WAITER_INITIALIZER;
+ void *a[] = {mpctx, &wait};
+ run_command(mpctx, mp_cmd_clone(ctx->each_frame), NULL, screenshot_fin, a);
+
+ // Block (in a reentrant way) until the screenshot was written. Otherwise,
+ // we could pile up screenshot requests forever.
+ while (!mp_waiter_poll(&wait))
+ mp_idle(mpctx);
+
+ mp_waiter_wait(&wait);
+}
diff --git a/player/screenshot.h b/player/screenshot.h
new file mode 100644
index 0000000..97abc79
--- /dev/null
+++ b/player/screenshot.h
@@ -0,0 +1,46 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MPLAYER_SCREENSHOT_H
+#define MPLAYER_SCREENSHOT_H
+
+#include <stdbool.h>
+
+struct MPContext;
+struct mp_image;
+struct mp_log;
+struct mpv_global;
+
+// One time initialization at program start.
+void screenshot_init(struct MPContext *mpctx);
+
+// Called by the playback core on each iteration.
+void handle_each_frame_screenshot(struct MPContext *mpctx);
+
+/* Return the image converted to the given format. If the pixel aspect ratio is
+ * not 1:1, the image is scaled as well. Returns NULL on failure.
+ * If global!=NULL, use command line scaler options etc.
+ */
+struct mp_image *convert_image(struct mp_image *image, int destfmt,
+ struct mpv_global *global, struct mp_log *log);
+
+// Handlers for the user-facing commands.
+void cmd_screenshot(void *p);
+void cmd_screenshot_to_file(void *p);
+void cmd_screenshot_raw(void *p);
+
+#endif /* MPLAYER_SCREENSHOT_H */
diff --git a/player/scripting.c b/player/scripting.c
new file mode 100644
index 0000000..0b20081
--- /dev/null
+++ b/player/scripting.c
@@ -0,0 +1,462 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+#include "osdep/threads.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "options/parse_configfile.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "core.h"
+#include "client.h"
+#include "libmpv/client.h"
+#include "libmpv/render.h"
+#include "libmpv/stream_cb.h"
+
+extern const struct mp_scripting mp_scripting_lua;
+extern const struct mp_scripting mp_scripting_cplugin;
+extern const struct mp_scripting mp_scripting_js;
+extern const struct mp_scripting mp_scripting_run;
+
+static const struct mp_scripting *const scripting_backends[] = {
+#if HAVE_LUA
+ &mp_scripting_lua,
+#endif
+#if HAVE_CPLUGINS
+ &mp_scripting_cplugin,
+#endif
+#if HAVE_JAVASCRIPT
+ &mp_scripting_js,
+#endif
+ &mp_scripting_run,
+ NULL
+};
+
+static char *script_name_from_filename(void *talloc_ctx, const char *fname)
+{
+ fname = mp_basename(fname);
+ if (fname[0] == '@')
+ fname += 1;
+ char *name = talloc_strdup(talloc_ctx, fname);
+ // Drop file extension
+ char *dot = strrchr(name, '.');
+ if (dot)
+ *dot = '\0';
+ // Turn it into a safe identifier - this is used with e.g. dispatching
+ // input via: "send scriptname ..."
+ for (int n = 0; name[n]; n++) {
+ char c = name[n];
+ if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
+ !(c >= '0' && c <= '9'))
+ name[n] = '_';
+ }
+ return talloc_asprintf(talloc_ctx, "%s", name);
+}
+
+static void run_script(struct mp_script_args *arg)
+{
+ char *name = talloc_asprintf(NULL, "%s/%s", arg->backend->name,
+ mpv_client_name(arg->client));
+ mp_thread_set_name(name);
+ talloc_free(name);
+
+ if (arg->backend->load(arg) < 0)
+ MP_ERR(arg, "Could not load %s script %s\n", arg->backend->name, arg->filename);
+
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+}
+
+static MP_THREAD_VOID script_thread(void *p)
+{
+ struct mp_script_args *arg = p;
+ run_script(arg);
+
+ MP_THREAD_RETURN();
+}
+
+static int64_t mp_load_script(struct MPContext *mpctx, const char *fname)
+{
+ char *ext = mp_splitext(fname, NULL);
+ if (ext && strcasecmp(ext, "disable") == 0)
+ return 0;
+
+ void *tmp = talloc_new(NULL);
+
+ const char *path = NULL;
+ char *script_name = NULL;
+ const struct mp_scripting *backend = NULL;
+
+ struct stat s;
+ if (!stat(fname, &s) && S_ISDIR(s.st_mode)) {
+ path = fname;
+ fname = NULL;
+
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ char *filename = mp_tprintf(80, "main.%s", b->file_ext);
+ fname = mp_path_join(tmp, path, filename);
+ if (!stat(fname, &s) && S_ISREG(s.st_mode)) {
+ backend = b;
+ break;
+ }
+ talloc_free((void *)fname);
+ fname = NULL;
+ }
+
+ if (!fname) {
+ MP_ERR(mpctx, "Cannot find main.* for any supported scripting "
+ "backend in: %s\n", path);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ script_name = talloc_strdup(tmp, path);
+ mp_path_strip_trailing_separator(script_name);
+ script_name = mp_basename(script_name);
+ } else {
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ if (ext && strcasecmp(ext, b->file_ext) == 0) {
+ backend = b;
+ break;
+ }
+ }
+ script_name = script_name_from_filename(tmp, fname);
+ }
+
+ if (!backend) {
+ MP_ERR(mpctx, "Can't load unknown script: %s\n", fname);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ struct mp_script_args *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_script_args){
+ .mpctx = mpctx,
+ .filename = talloc_strdup(arg, fname),
+ .path = talloc_strdup(arg, path),
+ .backend = backend,
+ // Create the client before creating the thread; otherwise a race
+ // condition could happen, where MPContext is destroyed while the
+ // thread tries to create the client.
+ .client = mp_new_client(mpctx->clients, script_name),
+ };
+
+ talloc_free(tmp);
+ fname = NULL; // might have been freed so don't touch anymore
+
+ if (!arg->client) {
+ MP_ERR(mpctx, "Failed to create client for script: %s\n", arg->filename);
+ talloc_free(arg);
+ return -1;
+ }
+
+ mp_client_set_weak(arg->client);
+ arg->log = mp_client_get_log(arg->client);
+ int64_t id = mpv_client_id(arg->client);
+
+ MP_DBG(arg, "Loading %s script %s...\n", backend->name, arg->filename);
+
+ if (backend->no_thread) {
+ run_script(arg);
+ } else {
+ mp_thread thread;
+ if (mp_thread_create(&thread, script_thread, arg)) {
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+ return -1;
+ }
+ mp_thread_detach(thread);
+ }
+
+ return id;
+}
+
+int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname)
+{
+ char *path = mp_get_user_path(NULL, mpctx->global, fname);
+ int64_t ret = mp_load_script(mpctx, path);
+ talloc_free(path);
+ return ret;
+}
+
+static int compare_filename(const void *pa, const void *pb)
+{
+ char *a = (char *)pa;
+ char *b = (char *)pb;
+ return strcmp(a, b);
+}
+
+static char **list_script_files(void *talloc_ctx, char *path)
+{
+ char **files = NULL;
+ int count = 0;
+ DIR *dp = opendir(path);
+ if (!dp)
+ return NULL;
+ struct dirent *ep;
+ while ((ep = readdir(dp))) {
+ if (ep->d_name[0] != '.') {
+ char *fname = mp_path_join(talloc_ctx, path, ep->d_name);
+ struct stat s;
+ if (!stat(fname, &s) && (S_ISREG(s.st_mode) || S_ISDIR(s.st_mode)))
+ MP_TARRAY_APPEND(talloc_ctx, files, count, fname);
+ }
+ }
+ closedir(dp);
+ if (files)
+ qsort(files, count, sizeof(char *), compare_filename);
+ MP_TARRAY_APPEND(talloc_ctx, files, count, NULL);
+ return files;
+}
+
+static void load_builtin_script(struct MPContext *mpctx, int slot, bool enable,
+ const char *fname)
+{
+ assert(slot < MP_ARRAY_SIZE(mpctx->builtin_script_ids));
+ int64_t *pid = &mpctx->builtin_script_ids[slot];
+ if (*pid > 0 && !mp_client_id_exists(mpctx, *pid))
+ *pid = 0; // died
+ if ((*pid > 0) != enable) {
+ if (enable) {
+ *pid = mp_load_script(mpctx, fname);
+ } else {
+ char *name = mp_tprintf(22, "@%"PRIi64, *pid);
+ mp_client_send_event(mpctx, name, 0, MPV_EVENT_SHUTDOWN, NULL);
+ }
+ }
+}
+
+void mp_load_builtin_scripts(struct MPContext *mpctx)
+{
+ load_builtin_script(mpctx, 0, mpctx->opts->lua_load_osc, "@osc.lua");
+ load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua");
+ load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua");
+ load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
+ load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
+ "@auto_profiles.lua");
+}
+
+bool mp_load_scripts(struct MPContext *mpctx)
+{
+ bool ok = true;
+
+ // Load scripts from options
+ char **files = mpctx->opts->script_files;
+ for (int n = 0; files && files[n]; n++) {
+ if (files[n][0])
+ ok &= mp_load_user_script(mpctx, files[n]) >= 0;
+ }
+ if (!mpctx->opts->auto_load_scripts)
+ return ok;
+
+ // Load all scripts
+ void *tmp = talloc_new(NULL);
+ char **scriptsdir = mp_find_all_config_files(tmp, mpctx->global, "scripts");
+ for (int i = 0; scriptsdir && scriptsdir[i]; i++) {
+ files = list_script_files(tmp, scriptsdir[i]);
+ for (int n = 0; files && files[n]; n++)
+ ok &= mp_load_script(mpctx, files[n]) >= 0;
+ }
+ talloc_free(tmp);
+
+ return ok;
+}
+
+#if HAVE_CPLUGINS
+
+#if !HAVE_WIN32
+#include <dlfcn.h>
+#endif
+
+#define MPV_DLOPEN_FN "mpv_open_cplugin"
+typedef int (*mpv_open_cplugin)(mpv_handle *handle);
+
+static void init_sym_table(struct mp_script_args *args, void *lib) {
+#define INIT_SYM(name) \
+ { \
+ void **sym = (void **)dlsym(lib, "pfn_" #name); \
+ if (sym) { \
+ if (*sym && *sym != &name) \
+ MP_ERR(args, "Overriding already set function " #name "\n"); \
+ *sym = &name; \
+ } \
+ }
+
+ INIT_SYM(mpv_client_api_version);
+ INIT_SYM(mpv_error_string);
+ INIT_SYM(mpv_free);
+ INIT_SYM(mpv_client_name);
+ INIT_SYM(mpv_client_id);
+ INIT_SYM(mpv_create);
+ INIT_SYM(mpv_initialize);
+ INIT_SYM(mpv_destroy);
+ INIT_SYM(mpv_terminate_destroy);
+ INIT_SYM(mpv_create_client);
+ INIT_SYM(mpv_create_weak_client);
+ INIT_SYM(mpv_load_config_file);
+ INIT_SYM(mpv_get_time_us);
+ INIT_SYM(mpv_free_node_contents);
+ INIT_SYM(mpv_set_option);
+ INIT_SYM(mpv_set_option_string);
+ INIT_SYM(mpv_command);
+ INIT_SYM(mpv_command_node);
+ INIT_SYM(mpv_command_ret);
+ INIT_SYM(mpv_command_string);
+ INIT_SYM(mpv_command_async);
+ INIT_SYM(mpv_command_node_async);
+ INIT_SYM(mpv_abort_async_command);
+ INIT_SYM(mpv_set_property);
+ INIT_SYM(mpv_set_property_string);
+ INIT_SYM(mpv_del_property);
+ INIT_SYM(mpv_set_property_async);
+ INIT_SYM(mpv_get_property);
+ INIT_SYM(mpv_get_property_string);
+ INIT_SYM(mpv_get_property_osd_string);
+ INIT_SYM(mpv_get_property_async);
+ INIT_SYM(mpv_observe_property);
+ INIT_SYM(mpv_unobserve_property);
+ INIT_SYM(mpv_event_name);
+ INIT_SYM(mpv_event_to_node);
+ INIT_SYM(mpv_request_event);
+ INIT_SYM(mpv_request_log_messages);
+ INIT_SYM(mpv_wait_event);
+ INIT_SYM(mpv_wakeup);
+ INIT_SYM(mpv_set_wakeup_callback);
+ INIT_SYM(mpv_wait_async_requests);
+ INIT_SYM(mpv_hook_add);
+ INIT_SYM(mpv_hook_continue);
+ INIT_SYM(mpv_get_wakeup_pipe);
+
+ INIT_SYM(mpv_render_context_create);
+ INIT_SYM(mpv_render_context_set_parameter);
+ INIT_SYM(mpv_render_context_get_info);
+ INIT_SYM(mpv_render_context_set_update_callback);
+ INIT_SYM(mpv_render_context_update);
+ INIT_SYM(mpv_render_context_render);
+ INIT_SYM(mpv_render_context_report_swap);
+ INIT_SYM(mpv_render_context_free);
+
+ INIT_SYM(mpv_stream_cb_add_ro);
+
+#undef INIT_SYM
+}
+
+static int load_cplugin(struct mp_script_args *args)
+{
+ void *lib = dlopen(args->filename, RTLD_NOW | RTLD_LOCAL);
+ if (!lib)
+ goto error;
+ // Note: once loaded, we never unload, as unloading the libraries linked to
+ // the plugin can cause random serious problems.
+ mpv_open_cplugin sym = (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN);
+ if (!sym)
+ goto error;
+
+ init_sym_table(args, lib);
+
+ return sym(args->client) ? -1 : 0;
+error: ;
+ char *err = dlerror();
+ if (err)
+ MP_ERR(args, "C plugin error: '%s'\n", err);
+ return -1;
+}
+
+const struct mp_scripting mp_scripting_cplugin = {
+ .name = "cplugin",
+ #if HAVE_WIN32
+ .file_ext = "dll",
+ #else
+ .file_ext = "so",
+ #endif
+ .load = load_cplugin,
+};
+
+#endif
+
+static int load_run(struct mp_script_args *args)
+{
+ // The arg->client object might die and with it args->log, so duplicate it.
+ args->log = mp_log_new(args, args->log, NULL);
+
+ int fds[2];
+ if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds))
+ return -1;
+ args->client = NULL; // ownership lost
+
+ char *fdopt = fds[1] >= 0 ? mp_tprintf(80, "--mpv-ipc-fd=%d:%d", fds[0], fds[1])
+ : mp_tprintf(80, "--mpv-ipc-fd=%d", fds[0]);
+
+ struct mp_subprocess_opts opts = {
+ .exe = (char *)args->filename,
+ .args = (char *[]){(char *)args->filename, fdopt, NULL},
+ .fds = {
+ // Keep terminal stuff
+ {.fd = 0, .src_fd = 0,},
+ {.fd = 1, .src_fd = 1,},
+ {.fd = 2, .src_fd = 2,},
+ // Just hope these don't step over each other (e.g. fds[1] could be
+ // below 4, if the std FDs are missing).
+ {.fd = fds[0], .src_fd = fds[0], },
+ {.fd = fds[1], .src_fd = fds[1], },
+ },
+ .num_fds = fds[1] >= 0 ? 5 : 4,
+ .detach = true,
+ };
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+
+ // Closing these will (probably) make the client exit, if it really died.
+ // They _should_ be CLOEXEC, but are not, because
+ // posix_spawn_file_actions_adddup2() may not clear the CLOEXEC flag
+ // properly if by coincidence fd==src_fd.
+ close(fds[0]);
+ if (fds[1] >= 0)
+ close(fds[1]);
+
+ if (res.error < 0) {
+ MP_ERR(args, "Starting '%s' failed: %s\n", args->filename,
+ mp_subprocess_err_str(res.error));
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct mp_scripting mp_scripting_run = {
+ .name = "ipc",
+ .file_ext = "run",
+ .no_thread = true,
+ .load = load_run,
+};
diff --git a/player/sub.c b/player/sub.c
new file mode 100644
index 0000000..f3e42fe
--- /dev/null
+++ b/player/sub.c
@@ -0,0 +1,214 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "common/global.h"
+
+#include "stream/stream.h"
+#include "sub/dec_sub.h"
+#include "demux/demux.h"
+#include "video/mp_image.h"
+
+#include "core.h"
+
+// 0: primary sub, 1: secondary sub, -1: not selected
+static int get_order(struct MPContext *mpctx, struct track *track)
+{
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ if (mpctx->current_track[n][STREAM_SUB] == track)
+ return n;
+ }
+ return -1;
+}
+
+static void reset_subtitles(struct MPContext *mpctx, struct track *track)
+{
+ if (track->d_sub) {
+ sub_reset(track->d_sub);
+ sub_set_play_dir(track->d_sub, mpctx->play_dir);
+ }
+ term_osd_set_subs(mpctx, NULL);
+}
+
+void reset_subtitle_state(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ reset_subtitles(mpctx, mpctx->tracks[n]);
+ term_osd_set_subs(mpctx, NULL);
+}
+
+void uninit_sub(struct MPContext *mpctx, struct track *track)
+{
+ if (track && track->d_sub) {
+ reset_subtitles(mpctx, track);
+ sub_select(track->d_sub, false);
+ int order = get_order(mpctx, track);
+ osd_set_sub(mpctx->osd, order, NULL);
+ sub_destroy(track->d_sub);
+ track->d_sub = NULL;
+ }
+}
+
+void uninit_sub_all(struct MPContext *mpctx)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++)
+ uninit_sub(mpctx, mpctx->tracks[n]);
+}
+
+static bool update_subtitle(struct MPContext *mpctx, double video_pts,
+ struct track *track)
+{
+ struct dec_sub *dec_sub = track ? track->d_sub : NULL;
+
+ if (!dec_sub || video_pts == MP_NOPTS_VALUE)
+ return true;
+
+ if (mpctx->vo_chain) {
+ struct mp_image_params params = mpctx->vo_chain->filter->input_params;
+ if (params.imgfmt)
+ sub_control(dec_sub, SD_CTRL_SET_VIDEO_PARAMS, &params);
+ }
+
+ if (track->demuxer->fully_read && sub_can_preload(dec_sub)) {
+ // Assume fully_read implies no interleaved audio/video streams.
+ // (Reading packets will change the demuxer position.)
+ demux_seek(track->demuxer, 0, 0);
+ sub_preload(dec_sub);
+ }
+
+ if (!sub_read_packets(dec_sub, video_pts, mpctx->paused))
+ return false;
+
+ // Handle displaying subtitles on terminal; never done for secondary subs
+ if (mpctx->current_track[0][STREAM_SUB] == track && !mpctx->video_out) {
+ char *text = sub_get_text(dec_sub, video_pts, SD_TEXT_TYPE_PLAIN);
+ term_osd_set_subs(mpctx, text);
+ talloc_free(text);
+ }
+
+ // Handle displaying subtitles on VO with no video being played. This is
+ // quite different, because normally subtitles are redrawn on new video
+ // frames, using the video frames' timestamps.
+ if (mpctx->video_out && mpctx->video_status == STATUS_EOF &&
+ (mpctx->opts->subs_rend->sub_past_video_end ||
+ !mpctx->current_track[0][STREAM_VIDEO] ||
+ mpctx->current_track[0][STREAM_VIDEO]->image)) {
+ if (osd_get_force_video_pts(mpctx->osd) != video_pts) {
+ osd_set_force_video_pts(mpctx->osd, video_pts);
+ osd_query_and_reset_want_redraw(mpctx->osd);
+ vo_redraw(mpctx->video_out);
+ // Force an arbitrary minimum FPS
+ mp_set_timeout(mpctx, 0.1);
+ }
+ }
+
+ return true;
+}
+
+// Return true if the subtitles for the given PTS are ready; false if the player
+// should wait for new demuxer data, and then should retry.
+bool update_subtitles(struct MPContext *mpctx, double video_pts)
+{
+ bool ok = true;
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++)
+ ok &= update_subtitle(mpctx, video_pts, mpctx->current_track[n][STREAM_SUB]);
+ return ok;
+}
+
+static struct attachment_list *get_all_attachments(struct MPContext *mpctx)
+{
+ struct attachment_list *list = talloc_zero(NULL, struct attachment_list);
+ struct demuxer *prev_demuxer = NULL;
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *t = mpctx->tracks[n];
+ if (!t->demuxer || prev_demuxer == t->demuxer)
+ continue;
+ prev_demuxer = t->demuxer;
+ for (int i = 0; i < t->demuxer->num_attachments; i++) {
+ struct demux_attachment *att = &t->demuxer->attachments[i];
+ struct demux_attachment copy = {
+ .name = talloc_strdup(list, att->name),
+ .type = talloc_strdup(list, att->type),
+ .data = talloc_memdup(list, att->data, att->data_size),
+ .data_size = att->data_size,
+ };
+ MP_TARRAY_APPEND(list, list->entries, list->num_entries, copy);
+ }
+ }
+ return list;
+}
+
+static bool init_subdec(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->d_sub);
+
+ if (!track->demuxer || !track->stream)
+ return false;
+
+ track->d_sub = sub_create(mpctx->global, track,
+ get_all_attachments(mpctx),
+ get_order(mpctx, track));
+ if (!track->d_sub)
+ return false;
+
+ struct track *vtrack = mpctx->current_track[0][STREAM_VIDEO];
+ struct mp_codec_params *v_c =
+ vtrack && vtrack->stream ? vtrack->stream->codec : NULL;
+ double fps = v_c ? v_c->fps : 25;
+ sub_control(track->d_sub, SD_CTRL_SET_VIDEO_DEF_FPS, &fps);
+
+ return true;
+}
+
+void reinit_sub(struct MPContext *mpctx, struct track *track)
+{
+ if (!track || !track->stream || track->stream->type != STREAM_SUB)
+ return;
+
+ assert(!track->d_sub);
+
+ if (!init_subdec(mpctx, track)) {
+ error_on_track(mpctx, track);
+ return;
+ }
+
+ sub_select(track->d_sub, true);
+ int order = get_order(mpctx, track);
+ osd_set_sub(mpctx->osd, order, track->d_sub);
+ sub_control(track->d_sub, SD_CTRL_SET_TOP, &order);
+
+ // When paused we have to wait for packets to be available.
+ // So just retry until we get a packet in this case.
+ if (mpctx->playback_initialized)
+ while (!update_subtitles(mpctx, mpctx->playback_pts) && mpctx->paused);
+}
+
+void reinit_sub_all(struct MPContext *mpctx)
+{
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++)
+ reinit_sub(mpctx, mpctx->current_track[n][STREAM_SUB]);
+}
diff --git a/player/video.c b/player/video.c
new file mode 100644
index 0000000..48a3165
--- /dev/null
+++ b/player/video.c
@@ -0,0 +1,1324 @@
+/*
+ * 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 Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "common/common.h"
+#include "common/encode.h"
+#include "options/m_property.h"
+#include "osdep/timer.h"
+
+#include "audio/out/ao.h"
+#include "audio/format.h"
+#include "demux/demux.h"
+#include "stream/stream.h"
+#include "sub/osd.h"
+#include "video/hwdec.h"
+#include "filters/f_decoder_wrapper.h"
+#include "video/out/vo.h"
+
+#include "core.h"
+#include "command.h"
+#include "screenshot.h"
+
+enum {
+ // update_video() - code also uses: <0 error, 0 eof, >0 progress
+ VD_ERROR = -1,
+ VD_EOF = 0, // end of file - no new output
+ VD_PROGRESS = 1, // progress, but no output; repeat call with no waiting
+ VD_NEW_FRAME = 2, // the call produced a new frame
+ VD_WAIT = 3, // no EOF, but no output; wait until wakeup
+};
+
+static const char av_desync_help_text[] =
+"\n"
+"Audio/Video desynchronisation detected! Possible reasons include too slow\n"
+"hardware, temporary CPU spikes, broken drivers, and broken files. Audio\n"
+"position will not match to the video (see A-V status field).\n"
+"Consider trying `--profile=fast` and/or `--hwdec=auto-safe` as they may help.\n"
+"\n";
+
+static bool recreate_video_filters(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(vo_c);
+
+ return mp_output_chain_update_filters(vo_c->filter, opts->vf_settings);
+}
+
+int reinit_video_filters(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c)
+ return 0;
+
+ if (!recreate_video_filters(mpctx))
+ return -1;
+
+ mp_force_video_refresh(mpctx);
+
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+
+ return 0;
+}
+
+static void vo_chain_reset_state(struct vo_chain *vo_c)
+{
+ vo_seek_reset(vo_c->vo);
+ vo_c->underrun = false;
+ vo_c->underrun_signaled = false;
+}
+
+void reset_video_state(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain) {
+ vo_chain_reset_state(mpctx->vo_chain);
+ struct track *t = mpctx->vo_chain->track;
+ if (t && t->dec)
+ mp_decoder_wrapper_set_play_dir(t->dec, mpctx->play_dir);
+ }
+
+ for (int n = 0; n < mpctx->num_next_frames; n++)
+ mp_image_unrefp(&mpctx->next_frames[n]);
+ mpctx->num_next_frames = 0;
+ mp_image_unrefp(&mpctx->saved_frame);
+
+ mpctx->delay = 0;
+ mpctx->time_frame = 0;
+ mpctx->video_pts = MP_NOPTS_VALUE;
+ mpctx->last_frame_duration = 0;
+ mpctx->num_past_frames = 0;
+ mpctx->total_avsync_change = 0;
+ mpctx->last_av_difference = 0;
+ mpctx->mistimed_frames_total = 0;
+ mpctx->drop_message_shown = 0;
+ mpctx->display_sync_drift_dir = 0;
+ mpctx->display_sync_error = 0;
+
+ mpctx->video_status = mpctx->vo_chain ? STATUS_SYNCING : STATUS_EOF;
+}
+
+void uninit_video_out(struct MPContext *mpctx)
+{
+ uninit_video_chain(mpctx);
+ if (mpctx->video_out) {
+ vo_destroy(mpctx->video_out);
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+ mpctx->video_out = NULL;
+}
+
+static void vo_chain_uninit(struct vo_chain *vo_c)
+{
+ struct track *track = vo_c->track;
+ if (track) {
+ assert(track->vo_c == vo_c);
+ track->vo_c = NULL;
+ if (vo_c->dec_src)
+ assert(track->dec->f->pins[0] == vo_c->dec_src);
+ talloc_free(track->dec->f);
+ track->dec = NULL;
+ }
+
+ if (vo_c->filter_src)
+ mp_pin_disconnect(vo_c->filter_src);
+
+ talloc_free(vo_c->filter->f);
+ talloc_free(vo_c);
+ // this does not free the VO
+}
+
+void uninit_video_chain(struct MPContext *mpctx)
+{
+ if (mpctx->vo_chain) {
+ reset_video_state(mpctx);
+ vo_chain_uninit(mpctx->vo_chain);
+ mpctx->vo_chain = NULL;
+
+ mpctx->video_status = STATUS_EOF;
+
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+}
+
+int init_video_decoder(struct MPContext *mpctx, struct track *track)
+{
+ assert(!track->dec);
+ if (!track->stream)
+ goto err_out;
+
+ struct mp_filter *parent = mpctx->filter_root;
+
+ // If possible, set this as parent so the decoder gets the hwdec and DR
+ // interfaces.
+ // Note: We rely on being able to get rid of all references to the VO by
+ // destroying the VO chain. Thus, decoders not linked to vo_chain
+ // must not use the hwdec context.
+ if (track->vo_c)
+ parent = track->vo_c->filter->f;
+
+ track->dec = mp_decoder_wrapper_create(parent, track->stream);
+ if (!track->dec)
+ goto err_out;
+
+ if (!mp_decoder_wrapper_reinit(track->dec))
+ goto err_out;
+
+ return 1;
+
+err_out:
+ if (track->sink)
+ mp_pin_disconnect(track->sink);
+ track->sink = NULL;
+ error_on_track(mpctx, track);
+ return 0;
+}
+
+void reinit_video_chain(struct MPContext *mpctx)
+{
+ struct track *track = mpctx->current_track[0][STREAM_VIDEO];
+ if (!track || !track->stream) {
+ error_on_track(mpctx, track);
+ return;
+ }
+ reinit_video_chain_src(mpctx, track);
+}
+
+static void filter_update_subtitles(void *ctx, double pts)
+{
+ struct MPContext *mpctx = ctx;
+
+ if (osd_get_render_subs_in_filter(mpctx->osd))
+ update_subtitles(mpctx, pts);
+}
+
+// (track=NULL creates a blank chain, used for lavfi-complex)
+void reinit_video_chain_src(struct MPContext *mpctx, struct track *track)
+{
+ assert(!mpctx->vo_chain);
+
+ if (!mpctx->video_out) {
+ struct vo_extra ex = {
+ .input_ctx = mpctx->input,
+ .osd = mpctx->osd,
+ .encode_lavc_ctx = mpctx->encode_lavc_ctx,
+ .wakeup_cb = mp_wakeup_core_cb,
+ .wakeup_ctx = mpctx,
+ };
+ mpctx->video_out = init_best_video_out(mpctx->global, &ex);
+ if (!mpctx->video_out) {
+ MP_FATAL(mpctx, "Error opening/initializing "
+ "the selected video_out (--vo) device.\n");
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ goto err_out;
+ }
+ mpctx->mouse_cursor_visible = true;
+ }
+
+ update_window_title(mpctx, true);
+
+ struct vo_chain *vo_c = talloc_zero(NULL, struct vo_chain);
+ mpctx->vo_chain = vo_c;
+ vo_c->log = mpctx->log;
+ vo_c->vo = mpctx->video_out;
+ vo_c->filter =
+ mp_output_chain_create(mpctx->filter_root, MP_OUTPUT_CHAIN_VIDEO);
+ mp_output_chain_set_vo(vo_c->filter, vo_c->vo);
+ vo_c->filter->update_subtitles = filter_update_subtitles;
+ vo_c->filter->update_subtitles_ctx = mpctx;
+
+ if (track) {
+ vo_c->track = track;
+ track->vo_c = vo_c;
+ if (!init_video_decoder(mpctx, track))
+ goto err_out;
+
+ vo_c->dec_src = track->dec->f->pins[0];
+ vo_c->filter->container_fps =
+ mp_decoder_wrapper_get_container_fps(track->dec);
+ vo_c->is_coverart = !!track->attached_picture;
+ vo_c->is_sparse = track->stream->still_image || vo_c->is_coverart;
+
+ if (vo_c->is_coverart)
+ mp_decoder_wrapper_set_coverart_flag(track->dec, true);
+
+ track->vo_c = vo_c;
+ vo_c->track = track;
+
+ mp_pin_connect(vo_c->filter->f->pins[0], vo_c->dec_src);
+ }
+
+ if (!recreate_video_filters(mpctx))
+ goto err_out;
+
+ update_content_type(mpctx, track);
+ update_screensaver_state(mpctx);
+
+ vo_set_paused(vo_c->vo, get_internal_paused(mpctx));
+
+ reset_video_state(mpctx);
+ term_osd_set_subs(mpctx, NULL);
+
+ return;
+
+err_out:
+ uninit_video_chain(mpctx);
+ error_on_track(mpctx, track);
+ handle_force_window(mpctx, true);
+}
+
+// Try to refresh the video by doing a precise seek to the currently displayed
+// frame. This can go wrong in all sorts of ways, so use sparingly.
+void mp_force_video_refresh(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c)
+ return;
+
+ // If not paused, the next frame should come soon enough.
+ if (opts->pause || mpctx->time_frame >= 0.5 ||
+ mpctx->video_status == STATUS_EOF)
+ {
+ issue_refresh_seek(mpctx, MPSEEK_VERY_EXACT);
+ }
+}
+
+static void check_framedrop(struct MPContext *mpctx, struct vo_chain *vo_c)
+{
+ struct MPOpts *opts = mpctx->opts;
+ // check for frame-drop:
+ if (mpctx->video_status == STATUS_PLAYING && !mpctx->paused &&
+ mpctx->audio_status == STATUS_PLAYING && !ao_untimed(mpctx->ao) &&
+ vo_c->track && vo_c->track->dec && (opts->frame_dropping & 2))
+ {
+ float fps = vo_c->filter->container_fps;
+ // it's a crappy heuristic; avoid getting upset by incorrect fps
+ if (fps <= 20 || fps >= 500)
+ return;
+ double frame_time = 1.0 / fps;
+ // try to drop as many frames as we appear to be behind
+ mp_decoder_wrapper_set_frame_drops(vo_c->track->dec,
+ MPCLAMP((mpctx->last_av_difference - 0.010) / frame_time, 0, 100));
+ }
+}
+
+/* Modify video timing to match the audio timeline. There are two main
+ * reasons this is needed. First, video and audio can start from different
+ * positions at beginning of file or after a seek (MPlayer starts both
+ * immediately even if they have different pts). Second, the file can have
+ * audio timestamps that are inconsistent with the duration of the audio
+ * packets, for example two consecutive timestamp values differing by
+ * one second but only a packet with enough samples for half a second
+ * of playback between them.
+ */
+static void adjust_sync(struct MPContext *mpctx, double v_pts, double frame_time)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (mpctx->audio_status == STATUS_EOF)
+ return;
+
+ mpctx->delay -= frame_time;
+ double a_pts = written_audio_pts(mpctx) + opts->audio_delay - mpctx->delay;
+ double av_delay = a_pts - v_pts;
+
+ double change = av_delay * 0.1;
+ double factor = fabs(av_delay) < 0.3 ? 0.1 : 0.4;
+ double max_change = opts->default_max_pts_correction >= 0 ?
+ opts->default_max_pts_correction : frame_time * factor;
+ if (change < -max_change)
+ change = -max_change;
+ else if (change > max_change)
+ change = max_change;
+ mpctx->delay += change;
+ mpctx->total_avsync_change += change;
+
+ if (mpctx->display_sync_active)
+ mpctx->total_avsync_change = 0;
+}
+
+// Make the frame at position 0 "known" to the playback logic. This must happen
+// only once for each frame, so this function has to be called carefully.
+// Generally, if position 0 gets a new frame, this must be called.
+static void handle_new_frame(struct MPContext *mpctx)
+{
+ assert(mpctx->num_next_frames >= 1);
+
+ double frame_time = 0;
+ double pts = mpctx->next_frames[0]->pts;
+ bool is_sparse = mpctx->vo_chain && mpctx->vo_chain->is_sparse;
+
+ if (mpctx->video_pts != MP_NOPTS_VALUE) {
+ frame_time = pts - mpctx->video_pts;
+ double tolerance = mpctx->demuxer->ts_resets_possible &&
+ !is_sparse ? 5 : 1e4;
+ if (frame_time <= 0 || frame_time >= tolerance) {
+ // Assume a discontinuity.
+ MP_WARN(mpctx, "Invalid video timestamp: %f -> %f\n",
+ mpctx->video_pts, pts);
+ frame_time = 0;
+ }
+ }
+ mpctx->time_frame += frame_time / mpctx->video_speed;
+ if (frame_time)
+ adjust_sync(mpctx, pts, frame_time);
+ MP_TRACE(mpctx, "frametime=%5.3f\n", frame_time);
+}
+
+// Remove the first frame in mpctx->next_frames
+static void shift_frames(struct MPContext *mpctx)
+{
+ if (mpctx->num_next_frames < 1)
+ return;
+ talloc_free(mpctx->next_frames[0]);
+ for (int n = 0; n < mpctx->num_next_frames - 1; n++)
+ mpctx->next_frames[n] = mpctx->next_frames[n + 1];
+ mpctx->num_next_frames -= 1;
+}
+
+static bool use_video_lookahead(struct MPContext *mpctx)
+{
+ return mpctx->video_out &&
+ !(mpctx->video_out->driver->caps & VO_CAP_NORETAIN) &&
+ !(mpctx->opts->untimed || mpctx->video_out->driver->untimed) &&
+ !mpctx->opts->video_latency_hacks;
+}
+
+static int get_req_frames(struct MPContext *mpctx, bool eof)
+{
+ // On EOF, drain all frames.
+ if (eof)
+ return 1;
+
+ if (!use_video_lookahead(mpctx))
+ return 1;
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_sparse)
+ return 1;
+
+ // Normally require at least 2 frames, so we can compute a frame duration.
+ int min = 2;
+
+ // On the first frame, output a new frame as quickly as possible.
+ if (mpctx->video_pts == MP_NOPTS_VALUE)
+ return min;
+
+ int req = vo_get_num_req_frames(mpctx->video_out);
+ return MPCLAMP(req, min, MP_ARRAY_SIZE(mpctx->next_frames) - 1);
+}
+
+// Whether it's fine to call add_new_frame() now.
+static bool needs_new_frame(struct MPContext *mpctx)
+{
+ return mpctx->num_next_frames < get_req_frames(mpctx, false);
+}
+
+// Queue a frame to mpctx->next_frames[]. Call only if needs_new_frame() signals ok.
+static void add_new_frame(struct MPContext *mpctx, struct mp_image *frame)
+{
+ assert(mpctx->num_next_frames < MP_ARRAY_SIZE(mpctx->next_frames));
+ assert(frame);
+ mpctx->next_frames[mpctx->num_next_frames++] = frame;
+ if (mpctx->num_next_frames == 1)
+ handle_new_frame(mpctx);
+}
+
+// Enough video filtered already to push one frame to the VO?
+// Set eof to true if no new frames are to be expected.
+static bool have_new_frame(struct MPContext *mpctx, bool eof)
+{
+ return mpctx->num_next_frames >= get_req_frames(mpctx, eof);
+}
+
+// Fill mpctx->next_frames[] with a newly filtered or decoded image.
+// logical_eof: is set to true if there is EOF after currently queued frames
+// returns VD_* code
+static int video_output_image(struct MPContext *mpctx, bool *logical_eof)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ bool hrseek = false;
+ double hrseek_pts = mpctx->hrseek_pts;
+ double tolerance = mpctx->hrseek_backstep ? 0 : .005;
+ if (mpctx->video_status == STATUS_SYNCING) {
+ hrseek = mpctx->hrseek_active;
+ // playback_pts is normally only set when audio and video have started
+ // playing normally. If video is in syncing mode, then this must mean
+ // video was just enabled via track switching - skip to current time.
+ if (!hrseek && mpctx->playback_pts != MP_NOPTS_VALUE) {
+ hrseek = true;
+ hrseek_pts = mpctx->playback_pts;
+ }
+ }
+
+ if (vo_c->is_coverart) {
+ *logical_eof = true;
+ if (vo_has_frame(mpctx->video_out))
+ return VD_EOF;
+ hrseek = false;
+ }
+
+ if (have_new_frame(mpctx, false))
+ return VD_NEW_FRAME;
+
+ // Get a new frame if we need one.
+ int r = VD_PROGRESS;
+ if (needs_new_frame(mpctx)) {
+ // Filter a new frame.
+ struct mp_image *img = NULL;
+ struct mp_frame frame = mp_pin_out_read(vo_c->filter->f->pins[1]);
+ if (frame.type == MP_FRAME_NONE) {
+ r = vo_c->filter->got_output_eof ? VD_EOF : VD_WAIT;
+ } else if (frame.type == MP_FRAME_EOF) {
+ r = VD_EOF;
+ } else if (frame.type == MP_FRAME_VIDEO) {
+ img = frame.data;
+ } else {
+ MP_ERR(mpctx, "unexpected frame type %s\n",
+ mp_frame_type_str(frame.type));
+ mp_frame_unref(&frame);
+ return VD_ERROR;
+ }
+ if (img) {
+ double endpts = get_play_end_pts(mpctx);
+ if (endpts != MP_NOPTS_VALUE)
+ endpts *= mpctx->play_dir;
+ if ((endpts != MP_NOPTS_VALUE && img->pts >= endpts) ||
+ mpctx->max_frames == 0)
+ {
+ mp_pin_out_unread(vo_c->filter->f->pins[1], frame);
+ img = NULL;
+ r = VD_EOF;
+ } else if (hrseek && (img->pts < hrseek_pts - tolerance ||
+ mpctx->hrseek_lastframe))
+ {
+ /* just skip - but save in case it was the last frame */
+ mp_image_setrefp(&mpctx->saved_frame, img);
+ } else {
+ if (hrseek && mpctx->hrseek_backstep) {
+ if (mpctx->saved_frame) {
+ add_new_frame(mpctx, mpctx->saved_frame);
+ mpctx->saved_frame = NULL;
+ } else {
+ MP_WARN(mpctx, "Backstep failed.\n");
+ }
+ mpctx->hrseek_backstep = false;
+ }
+ mp_image_unrefp(&mpctx->saved_frame);
+ add_new_frame(mpctx, img);
+ img = NULL;
+ }
+ talloc_free(img);
+ }
+ }
+
+ if (!hrseek)
+ mp_image_unrefp(&mpctx->saved_frame);
+
+ if (r == VD_EOF) {
+ // If hr-seek went past EOF, use the last frame.
+ if (mpctx->saved_frame)
+ add_new_frame(mpctx, mpctx->saved_frame);
+ mpctx->saved_frame = NULL;
+ *logical_eof = true;
+ }
+
+ return have_new_frame(mpctx, r <= 0) ? VD_NEW_FRAME : r;
+}
+
+static bool check_for_hwdec_fallback(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c->filter->failed_output_conversion || !vo_c->track || !vo_c->track->dec)
+ return false;
+
+ if (mp_decoder_wrapper_control(vo_c->track->dec,
+ VDCTRL_FORCE_HWDEC_FALLBACK, NULL) != CONTROL_OK)
+ return false;
+
+ mp_output_chain_reset_harder(vo_c->filter);
+ return true;
+}
+
+static bool check_for_forced_eof(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+
+ if (!vo_c->track || !vo_c->track->dec)
+ return false;
+
+ struct mp_decoder_wrapper *dec = vo_c->track->dec;
+ bool forced_eof = false;
+
+ mp_decoder_wrapper_control(dec, VDCTRL_CHECK_FORCED_EOF, &forced_eof);
+ return forced_eof;
+}
+
+/* Update avsync before a new video frame is displayed. Actually, this can be
+ * called arbitrarily often before the actual display.
+ * This adjusts the time of the next video frame */
+static void update_avsync_before_frame(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+
+ if (mpctx->video_status < STATUS_READY) {
+ mpctx->time_frame = 0;
+ } else if (mpctx->display_sync_active || vo->opts->video_sync == VS_NONE) {
+ // don't touch the timing
+ } else if (mpctx->audio_status == STATUS_PLAYING &&
+ mpctx->video_status == STATUS_PLAYING &&
+ !ao_untimed(mpctx->ao))
+ {
+ double buffered_audio = ao_get_delay(mpctx->ao);
+
+ double predicted = mpctx->delay / mpctx->video_speed +
+ mpctx->time_frame;
+ double difference = buffered_audio - predicted;
+ MP_STATS(mpctx, "value %f audio-diff", difference);
+
+ if (opts->autosync) {
+ /* Smooth reported playback position from AO by averaging
+ * it with the value expected based on previous value and
+ * time elapsed since then. May help smooth video timing
+ * with audio output that have inaccurate position reporting.
+ * This is badly implemented; the behavior of the smoothing
+ * now undesirably depends on how often this code runs
+ * (mainly depends on video frame rate). */
+ buffered_audio = predicted + difference / opts->autosync;
+ }
+
+ mpctx->time_frame = buffered_audio - mpctx->delay / mpctx->video_speed;
+ } else {
+ /* If we're more than 200 ms behind the right playback
+ * position, don't try to speed up display of following
+ * frames to catch up; continue with default speed from
+ * the current frame instead.
+ * If untimed is set always output frames immediately
+ * without sleeping.
+ */
+ if (mpctx->time_frame < -0.2 || opts->untimed || vo->driver->untimed)
+ mpctx->time_frame = 0;
+ }
+}
+
+// Update the A/V sync difference when a new video frame is being shown.
+static void update_av_diff(struct MPContext *mpctx, double offset)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ mpctx->last_av_difference = 0;
+
+ if (mpctx->audio_status != STATUS_PLAYING ||
+ mpctx->video_status != STATUS_PLAYING)
+ return;
+
+ if (mpctx->vo_chain && mpctx->vo_chain->is_sparse)
+ return;
+
+ double a_pos = playing_audio_pts(mpctx);
+ if (a_pos != MP_NOPTS_VALUE && mpctx->video_pts != MP_NOPTS_VALUE) {
+ mpctx->last_av_difference = a_pos - mpctx->video_pts
+ + opts->audio_delay + offset;
+ }
+
+ if (fabs(mpctx->last_av_difference) > 0.5 && !mpctx->drop_message_shown) {
+ MP_WARN(mpctx, "%s", av_desync_help_text);
+ mpctx->drop_message_shown = true;
+ }
+}
+
+double calc_average_frame_duration(struct MPContext *mpctx)
+{
+ double total = 0;
+ int num = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ double dur = mpctx->past_frames[n].approx_duration;
+ if (dur <= 0)
+ continue;
+ total += dur;
+ num += 1;
+ }
+ return num > 0 ? total / num : 0;
+}
+
+// Find a speed factor such that the display FPS is an integer multiple of the
+// effective video FPS. If this is not possible, try to do it for multiples,
+// which still leads to an improved end result.
+// Both parameters are durations in seconds.
+static double calc_best_speed(double vsync, double frame,
+ double max_change, int max_factor)
+{
+ double ratio = frame / vsync;
+ for (int factor = 1; factor <= max_factor; factor++) {
+ double scale = ratio * factor / rint(ratio * factor);
+ if (fabs(scale - 1) <= max_change)
+ return scale;
+ }
+ return -1;
+}
+
+static double find_best_speed(struct MPContext *mpctx, double vsync)
+{
+ double total = 0;
+ int num = 0;
+ for (int n = 0; n < mpctx->num_past_frames; n++) {
+ double dur = mpctx->past_frames[n].approx_duration;
+ if (dur <= 0)
+ continue;
+ double best = calc_best_speed(vsync, dur / mpctx->opts->playback_speed,
+ mpctx->opts->sync_max_video_change / 100,
+ mpctx->opts->sync_max_factor);
+ if (best <= 0)
+ continue;
+ total += best;
+ num++;
+ }
+ // If it doesn't work, play at normal speed.
+ return num > 0 ? total / num : 1;
+}
+
+static bool using_spdif_passthrough(struct MPContext *mpctx)
+{
+ if (mpctx->ao_chain && mpctx->ao_chain->ao) {
+ int samplerate;
+ int format;
+ struct mp_chmap channels;
+ ao_get_format(mpctx->ao_chain->ao, &samplerate, &format, &channels);
+ return !af_fmt_is_pcm(format);
+ }
+ return false;
+}
+
+// Compute the relative audio speed difference by taking A/V dsync into account.
+static double compute_audio_drift(struct MPContext *mpctx, double vsync)
+{
+ // Least-squares linear regression, using relative real time for x, and
+ // audio desync for y. Assume speed didn't change for the frames we're
+ // looking at for simplicity. This also should actually use the realtime
+ // (minus paused time) for x, but use vsync scheduling points instead.
+ if (mpctx->num_past_frames <= 10)
+ return NAN;
+ int num = mpctx->num_past_frames - 1;
+ double sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
+ double x = 0;
+ for (int n = 0; n < num; n++) {
+ struct frame_info *frame = &mpctx->past_frames[n + 1];
+ if (frame->num_vsyncs < 0)
+ return NAN;
+ double y = frame->av_diff;
+ sum_x += x;
+ sum_y += y;
+ sum_xy += x * y;
+ sum_xx += x * x;
+ x -= frame->num_vsyncs * vsync;
+ }
+ return (sum_x * sum_y - num * sum_xy) / (sum_x * sum_x - num * sum_xx);
+}
+
+static void adjust_audio_drift_compensation(struct MPContext *mpctx, double vsync)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int mode = mpctx->video_out->opts->video_sync;
+
+ if ((mode != VS_DISP_RESAMPLE && mode != VS_DISP_TEMPO) ||
+ mpctx->audio_status != STATUS_PLAYING)
+ {
+ mpctx->speed_factor_a = mpctx->speed_factor_v;
+ return;
+ }
+
+ // Try to smooth out audio timing drifts. This can happen if either
+ // video isn't playing at expected speed, or audio is not playing at
+ // the requested speed. Both are unavoidable.
+ // The audio desync is made up of 2 parts: 1. drift due to rounding
+ // errors and imperfect information, and 2. an offset, due to
+ // unaligned audio/video start, or disruptive events halting audio
+ // or video for a small time.
+ // Instead of trying to be clever, just apply an awfully dumb drift
+ // compensation with a constant factor, which does what we want. In
+ // theory we could calculate the exact drift compensation needed,
+ // but it likely would be wrong anyway, and we'd run into the same
+ // issues again, except with more complex code.
+ // 1 means drifts to positive, -1 means drifts to negative
+ double max_drift = vsync / 2;
+ double av_diff = mpctx->last_av_difference;
+ int new = mpctx->display_sync_drift_dir;
+ if (av_diff * -mpctx->display_sync_drift_dir >= 0)
+ new = 0;
+ if (fabs(av_diff) > max_drift)
+ new = av_diff >= 0 ? 1 : -1;
+
+ bool change = mpctx->display_sync_drift_dir != new;
+ if (new || change) {
+ if (change)
+ MP_VERBOSE(mpctx, "Change display sync audio drift: %d\n", new);
+ mpctx->display_sync_drift_dir = new;
+
+ double max_correct = opts->sync_max_audio_change / 100;
+ double audio_factor = 1 + max_correct * -mpctx->display_sync_drift_dir;
+
+ if (new == 0) {
+ // If we're resetting, actually try to be clever and pick a speed
+ // which compensates the general drift we're getting.
+ double drift = compute_audio_drift(mpctx, vsync);
+ if (isnormal(drift)) {
+ // other = will be multiplied with audio_factor for final speed
+ double other = mpctx->opts->playback_speed * mpctx->speed_factor_v;
+ audio_factor = (mpctx->audio_speed - drift) / other;
+ MP_VERBOSE(mpctx, "Compensation factor: %f\n", audio_factor);
+ }
+ }
+
+ audio_factor = MPCLAMP(audio_factor, 1 - max_correct, 1 + max_correct);
+ mpctx->speed_factor_a = audio_factor * mpctx->speed_factor_v;
+ }
+}
+
+// Manipulate frame timing for display sync, or do nothing for normal timing.
+static void handle_display_sync_frame(struct MPContext *mpctx,
+ struct vo_frame *frame)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct vo *vo = mpctx->video_out;
+ int mode = vo->opts->video_sync;
+
+ if (!mpctx->display_sync_active) {
+ mpctx->display_sync_error = 0.0;
+ mpctx->display_sync_drift_dir = 0;
+ }
+
+ mpctx->display_sync_active = false;
+
+ if (!VS_IS_DISP(mode))
+ return;
+ bool resample = mode == VS_DISP_RESAMPLE || mode == VS_DISP_RESAMPLE_VDROP ||
+ mode == VS_DISP_RESAMPLE_NONE;
+ bool drop = mode == VS_DISP_VDROP || mode == VS_DISP_RESAMPLE ||
+ mode == VS_DISP_ADROP || mode == VS_DISP_RESAMPLE_VDROP ||
+ mode == VS_DISP_TEMPO;
+ drop &= frame->can_drop;
+
+ if (resample && using_spdif_passthrough(mpctx))
+ return;
+
+ double vsync = vo_get_vsync_interval(vo) / 1e9;
+ if (vsync <= 0)
+ return;
+
+ double approx_duration = MPMAX(0, mpctx->past_frames[0].approx_duration);
+ double adjusted_duration = approx_duration / opts->playback_speed;
+ if (adjusted_duration > 0.5)
+ return;
+
+ mpctx->speed_factor_v = 1.0;
+ if (mode != VS_DISP_VDROP)
+ mpctx->speed_factor_v = find_best_speed(mpctx, vsync);
+
+ // Determine for how many vsyncs a frame should be displayed. This can be
+ // e.g. 2 for 30hz on a 60hz display. It can also be 0 if the video
+ // framerate is higher than the display framerate.
+ // We use the speed-adjusted (i.e. real) frame duration for this.
+ double frame_duration = adjusted_duration / mpctx->speed_factor_v;
+ double ratio = (frame_duration + mpctx->display_sync_error) / vsync;
+ int num_vsyncs = MPMAX(lrint(ratio), 0);
+ double prev_error = mpctx->display_sync_error;
+ mpctx->display_sync_error += frame_duration - num_vsyncs * vsync;
+
+ MP_TRACE(mpctx, "s=%f vsyncs=%d dur=%f ratio=%f err=%.20f (%f/%f)\n",
+ mpctx->speed_factor_v, num_vsyncs, adjusted_duration, ratio,
+ mpctx->display_sync_error, mpctx->display_sync_error / vsync,
+ mpctx->display_sync_error / frame_duration);
+
+ double av_diff = mpctx->last_av_difference;
+ MP_STATS(mpctx, "value %f avdiff", av_diff);
+
+ // Intended number of additional display frames to drop (<0) or repeat (>0)
+ int drop_repeat = 0;
+
+ // If we are too far ahead/behind, attempt to drop/repeat frames.
+ // Tolerate some desync to avoid frame dropping due to jitter.
+ if (drop && fabs(av_diff) >= 0.020 && fabs(av_diff) / vsync >= 1)
+ drop_repeat = -av_diff / vsync; // round towards 0
+
+ // We can only drop all frames at most. We can repeat much more frames,
+ // but we still limit it to 10 times the original frames to avoid that
+ // corner cases or exceptional situations cause too much havoc.
+ drop_repeat = MPCLAMP(drop_repeat, -num_vsyncs, num_vsyncs * 10);
+ num_vsyncs += drop_repeat;
+
+ // Always show the first frame.
+ if (mpctx->num_past_frames <= 1 && num_vsyncs < 1)
+ num_vsyncs = 1;
+
+ // Estimate the video position, so we can calculate a good A/V difference
+ // value below. This is used to estimate A/V drift.
+ double time_left = vo_get_delay(vo);
+
+ // We also know that the timing is (necessarily) off, because we have to
+ // align frame timings on the vsync boundaries. This is unavoidable, and
+ // for the sake of the A/V sync calculations we pretend it's perfect.
+ time_left += prev_error;
+ // Likewise, we know sync is off, but is going to be compensated.
+ time_left += drop_repeat * vsync;
+
+ // If syncing took too long, disregard timing of the first frame.
+ if (mpctx->num_past_frames == 2 && time_left < 0) {
+ vo_discard_timing_info(vo);
+ time_left = 0;
+ }
+
+ if (drop_repeat) {
+ mpctx->mistimed_frames_total += 1;
+ MP_STATS(mpctx, "mistimed");
+ }
+
+ mpctx->total_avsync_change = 0;
+ update_av_diff(mpctx, time_left * opts->playback_speed);
+
+ mpctx->past_frames[0].num_vsyncs = num_vsyncs;
+ mpctx->past_frames[0].av_diff = mpctx->last_av_difference;
+
+ if (resample || mode == VS_DISP_ADROP || mode == VS_DISP_TEMPO) {
+ adjust_audio_drift_compensation(mpctx, vsync);
+ } else {
+ mpctx->speed_factor_a = 1.0;
+ }
+
+ // A bad guess, only needed when reverting to audio sync.
+ mpctx->time_frame = time_left;
+
+ frame->vsync_interval = vsync;
+ frame->vsync_offset = -prev_error;
+ frame->ideal_frame_duration = frame_duration;
+ frame->ideal_frame_vsync = (-prev_error / frame_duration) * approx_duration;
+ frame->ideal_frame_vsync_duration = (vsync / frame_duration) * approx_duration;
+ frame->num_vsyncs = num_vsyncs;
+ frame->display_synced = true;
+ frame->approx_duration = approx_duration;
+
+ // Adjust frame virtual vsyncs by the repeat count
+ if (drop_repeat > 0)
+ frame->ideal_frame_vsync_duration /= drop_repeat;
+
+ mpctx->display_sync_active = true;
+ // Try to avoid audio underruns that may occur if we update
+ // the playback speed while in the STATUS_SYNCING state.
+ if (mpctx->video_status != STATUS_SYNCING)
+ update_playback_speed(mpctx);
+
+ MP_STATS(mpctx, "value %f aspeed", mpctx->speed_factor_a - 1);
+ MP_STATS(mpctx, "value %f vspeed", mpctx->speed_factor_v - 1);
+}
+
+static void schedule_frame(struct MPContext *mpctx, struct vo_frame *frame)
+{
+ handle_display_sync_frame(mpctx, frame);
+
+ if (mpctx->num_past_frames > 1 &&
+ ((mpctx->past_frames[1].num_vsyncs >= 0) != mpctx->display_sync_active))
+ {
+ MP_VERBOSE(mpctx, "Video sync mode %s.\n",
+ mpctx->display_sync_active ? "enabled" : "disabled");
+ }
+
+ if (!mpctx->display_sync_active) {
+ mpctx->speed_factor_a = 1.0;
+ mpctx->speed_factor_v = 1.0;
+ update_playback_speed(mpctx);
+
+ update_av_diff(mpctx, mpctx->time_frame > 0 ?
+ mpctx->time_frame * mpctx->video_speed : 0);
+ }
+}
+
+// Determine the mpctx->past_frames[0] frame duration.
+static void calculate_frame_duration(struct MPContext *mpctx)
+{
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ assert(mpctx->num_past_frames >= 1 && mpctx->num_next_frames >= 1);
+
+ double demux_duration = vo_c->filter->container_fps > 0
+ ? 1.0 / vo_c->filter->container_fps : -1;
+ double duration = demux_duration;
+
+ if (mpctx->num_next_frames >= 2) {
+ double pts0 = mpctx->next_frames[0]->pts;
+ double pts1 = mpctx->next_frames[1]->pts;
+ if (pts0 != MP_NOPTS_VALUE && pts1 != MP_NOPTS_VALUE && pts1 >= pts0)
+ duration = pts1 - pts0;
+ }
+
+ // The following code tries to compensate for rounded Matroska timestamps
+ // by "unrounding" frame durations, or if not possible, approximating them.
+ // These formats usually round on 1ms. Some muxers do this incorrectly,
+ // and might go off by 1ms more, and compensate for it later by an equal
+ // rounding error into the opposite direction.
+ double tolerance = 0.001 * 3 + 0.0001;
+
+ double total = 0;
+ int num_dur = 0;
+ for (int n = 1; n < mpctx->num_past_frames; n++) {
+ // Eliminate likely outliers using a really dumb heuristic.
+ double dur = mpctx->past_frames[n].duration;
+ if (dur <= 0 || fabs(dur - duration) >= tolerance)
+ break;
+ total += dur;
+ num_dur += 1;
+ }
+ double approx_duration = num_dur > 0 ? total / num_dur : duration;
+
+ // Try if the demuxer frame rate fits - if so, just take it.
+ if (demux_duration > 0) {
+ // Note that even if each timestamp is within rounding tolerance, it
+ // could literally not add up (e.g. if demuxer FPS is rounded itself).
+ if (fabs(duration - demux_duration) < tolerance &&
+ fabs(total - demux_duration * num_dur) < tolerance &&
+ (num_dur >= 16 || num_dur >= mpctx->num_past_frames - 4))
+ {
+ approx_duration = demux_duration;
+ }
+ }
+
+ mpctx->past_frames[0].duration = duration;
+ mpctx->past_frames[0].approx_duration = approx_duration;
+
+ MP_STATS(mpctx, "value %f frame-duration", MPMAX(0, duration));
+ MP_STATS(mpctx, "value %f frame-duration-approx", MPMAX(0, approx_duration));
+}
+
+static void apply_video_crop(struct MPContext *mpctx, struct vo *vo)
+{
+ for (int n = 0; n < mpctx->num_next_frames; n++) {
+ struct m_geometry *gm = &vo->opts->video_crop;
+ struct mp_image_params p = mpctx->next_frames[n]->params;
+ if (gm->xy_valid || (gm->wh_valid && (gm->w > 0 || gm->h > 0)))
+ {
+ m_rect_apply(&p.crop, p.w, p.h, gm);
+ }
+
+ if (p.crop.x1 == 0 && p.crop.y1 == 0)
+ return;
+
+ if (!mp_image_crop_valid(&p)) {
+ char *str = m_option_type_rect.print(NULL, gm);
+ MP_WARN(vo, "Ignoring invalid --video-crop=%s for %dx%d image\n",
+ str, p.w, p.h);
+ talloc_free(str);
+ *gm = (struct m_geometry){0};
+ mp_property_do("video-crop", M_PROPERTY_SET, gm, mpctx);
+ return;
+ }
+ mpctx->next_frames[n]->params.crop = p.crop;
+ }
+}
+
+static bool video_reconfig_needed(const struct mp_image_params *p1,
+ const struct mp_image_params *p2)
+{
+ return p1->imgfmt != p2->imgfmt ||
+ p1->hw_subfmt != p2->hw_subfmt ||
+ p1->w != p2->w || p1->h != p2->h ||
+ p1->p_w != p2->p_w || p1->p_h != p2->p_h ||
+ p1->force_window != p2->force_window ||
+ p1->rotate != p2->rotate ||
+ p1->stereo3d != p2->stereo3d ||
+ !mp_rect_equals(&p1->crop, &p2->crop);
+}
+
+void write_video(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->vo_chain)
+ return;
+ struct track *track = mpctx->vo_chain->track;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ struct vo *vo = vo_c->vo;
+
+ if (vo_c->filter->reconfig_happened) {
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ vo_c->filter->reconfig_happened = false;
+ }
+
+ // Actual playback starts when both audio and video are ready.
+ if (mpctx->video_status == STATUS_READY)
+ return;
+
+ if (mpctx->paused && mpctx->video_status >= STATUS_READY)
+ return;
+
+ bool logical_eof = false;
+ int r = video_output_image(mpctx, &logical_eof);
+ MP_TRACE(mpctx, "video_output_image: r=%d/eof=%d/st=%s\n", r, logical_eof,
+ mp_status_str(mpctx->video_status));
+
+ if (r < 0)
+ goto error;
+
+ if (r == VD_WAIT) {
+ // Heuristic to detect underruns.
+ if (mpctx->video_status == STATUS_PLAYING && !vo_still_displaying(vo) &&
+ !vo_c->underrun_signaled)
+ {
+ vo_c->underrun = true;
+ vo_c->underrun_signaled = true;
+ }
+ // Demuxer will wake us up for more packets to decode.
+ return;
+ }
+
+ if (r == VD_EOF) {
+ if (check_for_hwdec_fallback(mpctx))
+ return;
+ if (check_for_forced_eof(mpctx)) {
+ uninit_video_chain(mpctx);
+ handle_force_window(mpctx, true);
+ return;
+ }
+ if (vo_c->filter->failed_output_conversion)
+ goto error;
+
+ mpctx->delay = 0;
+ mpctx->last_av_difference = 0;
+
+ if (mpctx->video_status <= STATUS_PLAYING) {
+ mpctx->video_status = STATUS_DRAINING;
+ get_relative_time(mpctx);
+ if (vo_c->is_sparse && !mpctx->ao_chain) {
+ MP_VERBOSE(mpctx, "assuming this is an image\n");
+ mpctx->time_frame += opts->image_display_duration;
+ } else if (mpctx->last_frame_duration > 0) {
+ MP_VERBOSE(mpctx, "using demuxer frame duration for last frame\n");
+ mpctx->time_frame += mpctx->last_frame_duration;
+ } else {
+ mpctx->time_frame = 0;
+ }
+ // Encode mode can't honor this; it'll only delay finishing.
+ if (mpctx->encode_lavc_ctx)
+ mpctx->time_frame = 0;
+ }
+
+ // Wait for the VO to signal actual EOF, then exit if the frame timer
+ // has expired.
+ bool has_frame = vo_has_frame(vo); // maybe not configured
+ if (mpctx->video_status == STATUS_DRAINING &&
+ (vo_is_ready_for_frame(vo, -1) || !has_frame))
+ {
+ mpctx->time_frame -= get_relative_time(mpctx);
+ mp_set_timeout(mpctx, mpctx->time_frame);
+ if (mpctx->time_frame <= 0 || !has_frame) {
+ MP_VERBOSE(mpctx, "video EOF reached\n");
+ mpctx->video_status = STATUS_EOF;
+ }
+ }
+
+ // Avoid pointlessly spamming the logs every frame.
+ if (!vo_c->is_sparse || !vo_c->sparse_eof_signalled) {
+ MP_DBG(mpctx, "video EOF (status=%d)\n", mpctx->video_status);
+ vo_c->sparse_eof_signalled = vo_c->is_sparse;
+ }
+ return;
+ }
+
+ if (mpctx->video_status > STATUS_PLAYING)
+ mpctx->video_status = STATUS_PLAYING;
+
+ if (r != VD_NEW_FRAME) {
+ mp_wakeup_core(mpctx); // Decode more in next iteration.
+ return;
+ }
+
+ if (logical_eof && !mpctx->num_past_frames && mpctx->num_next_frames == 1 &&
+ use_video_lookahead(mpctx) && !vo_c->is_sparse)
+ {
+ // Too much danger to accidentally mark video as sparse when e.g.
+ // seeking exactly to the last frame, so as a heuristic, do this only
+ // if it looks like the "first" video frame (unreliable, but often
+ // works out well). Helps with seeking with single-image video tracks,
+ // as well as detecting whether as video track is really an image.
+ if (mpctx->next_frames[0]->pts == 0) {
+ MP_VERBOSE(mpctx, "assuming single-image video stream\n");
+ vo_c->is_sparse = true;
+ }
+ }
+
+ // Inject vo crop to notify and reconfig if needed
+ apply_video_crop(mpctx, vo);
+
+ // Filter output is different from VO input?
+ struct mp_image_params *p = &mpctx->next_frames[0]->params;
+ if (!vo->params || video_reconfig_needed(p, vo->params)) {
+ // Changing config deletes the current frame; wait until it's finished.
+ if (vo_still_displaying(vo))
+ return;
+
+ const struct vo_driver *info = mpctx->video_out->driver;
+ char extra[20] = {0};
+ if (p->p_w != p->p_h) {
+ int d_w, d_h;
+ mp_image_params_get_dsize(p, &d_w, &d_h);
+ snprintf(extra, sizeof(extra), " => %dx%d", d_w, d_h);
+ }
+ char sfmt[20] = {0};
+ if (p->hw_subfmt)
+ snprintf(sfmt, sizeof(sfmt), "[%s]", mp_imgfmt_to_name(p->hw_subfmt));
+ MP_INFO(mpctx, "VO: [%s] %dx%d%s %s%s\n",
+ info->name, p->w, p->h, extra, mp_imgfmt_to_name(p->imgfmt), sfmt);
+ MP_VERBOSE(mpctx, "VO: Description: %s\n", info->description);
+
+ int vo_r = vo_reconfig2(vo, mpctx->next_frames[0]);
+ if (vo_r < 0) {
+ mpctx->error_playing = MPV_ERROR_VO_INIT_FAILED;
+ goto error;
+ }
+ mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
+ }
+
+ mpctx->time_frame -= get_relative_time(mpctx);
+ update_avsync_before_frame(mpctx);
+
+ // Enforce timing subtitles to video frames.
+ osd_set_force_video_pts(mpctx->osd, MP_NOPTS_VALUE);
+
+ if (!update_subtitles(mpctx, mpctx->next_frames[0]->pts)) {
+ MP_VERBOSE(mpctx, "Video frame delayed due to waiting on subtitles.\n");
+ return;
+ }
+
+ double time_frame = MPMAX(mpctx->time_frame, -1);
+ int64_t pts = mp_time_ns() + (int64_t)(time_frame * 1e9);
+
+ // wait until VO wakes us up to get more frames
+ // (NB: in theory, the 1st frame after display sync mode change uses the
+ // wrong waiting mode)
+ if (!vo_is_ready_for_frame(vo, mpctx->display_sync_active ? -1 : pts))
+ return;
+
+ assert(mpctx->num_next_frames >= 1);
+
+ if (mpctx->num_past_frames >= MAX_NUM_VO_PTS)
+ mpctx->num_past_frames--;
+ MP_TARRAY_INSERT_AT(mpctx, mpctx->past_frames, mpctx->num_past_frames, 0,
+ (struct frame_info){0});
+ mpctx->past_frames[0] = (struct frame_info){
+ .pts = mpctx->next_frames[0]->pts,
+ .num_vsyncs = -1,
+ };
+ calculate_frame_duration(mpctx);
+
+ int req = vo_get_num_req_frames(mpctx->video_out);
+ assert(req >= 1 && req <= VO_MAX_REQ_FRAMES);
+ struct vo_frame dummy = {
+ .pts = pts,
+ .duration = -1,
+ .still = mpctx->step_frames > 0,
+ .can_drop = opts->frame_dropping & 1,
+ .num_frames = MPMIN(mpctx->num_next_frames, req),
+ .num_vsyncs = 1,
+ };
+ for (int n = 0; n < dummy.num_frames; n++)
+ dummy.frames[n] = mpctx->next_frames[n];
+ struct vo_frame *frame = vo_frame_ref(&dummy);
+
+ double diff = mpctx->past_frames[0].approx_duration;
+ if (opts->untimed || vo->driver->untimed)
+ diff = -1; // disable frame dropping and aspects of frame timing
+ if (diff >= 0) {
+ // expected A/V sync correction is ignored
+ diff /= mpctx->video_speed;
+ if (mpctx->time_frame < 0)
+ diff += mpctx->time_frame;
+ frame->duration = MPCLAMP(diff, 0, 10) * 1e9;
+ }
+
+ mpctx->video_pts = mpctx->next_frames[0]->pts;
+ mpctx->last_frame_duration =
+ mpctx->next_frames[0]->pkt_duration / mpctx->video_speed;
+
+ shift_frames(mpctx);
+
+ schedule_frame(mpctx, frame);
+
+ mpctx->osd_force_update = true;
+ update_osd_msg(mpctx);
+
+ vo_queue_frame(vo, frame);
+
+ check_framedrop(mpctx, vo_c);
+
+ // The frames were shifted down; "initialize" the new first entry.
+ if (mpctx->num_next_frames >= 1)
+ handle_new_frame(mpctx);
+
+ mpctx->shown_vframes++;
+ if (mpctx->video_status < STATUS_PLAYING) {
+ mpctx->video_status = STATUS_READY;
+ // After a seek, make sure to wait until the first frame is visible.
+ if (!opts->video_latency_hacks) {
+ vo_wait_frame(vo);
+ MP_VERBOSE(mpctx, "first video frame after restart shown\n");
+ }
+ }
+
+ mp_notify(mpctx, MPV_EVENT_TICK, NULL);
+
+ // hr-seek past EOF -> returns last frame, but terminates playback. The
+ // early EOF is needed to trigger the exit before the next seek is executed.
+ // Always using early EOF breaks other cases, like images.
+ if (logical_eof && !mpctx->num_next_frames && mpctx->ao_chain)
+ mpctx->video_status = STATUS_EOF;
+
+ if (mpctx->video_status != STATUS_EOF) {
+ if (mpctx->step_frames > 0) {
+ mpctx->step_frames--;
+ if (!mpctx->step_frames)
+ set_pause_state(mpctx, true);
+ }
+ if (mpctx->max_frames == 0 && !mpctx->stop_play)
+ mpctx->stop_play = AT_END_OF_FILE;
+ if (mpctx->max_frames > 0)
+ mpctx->max_frames--;
+ }
+
+ vo_c->underrun_signaled = false;
+
+ if (mpctx->video_status == STATUS_EOF || mpctx->stop_play)
+ mp_wakeup_core(mpctx);
+ return;
+
+error:
+ MP_FATAL(mpctx, "Could not initialize video chain.\n");
+ uninit_video_chain(mpctx);
+ error_on_track(mpctx, track);
+ handle_force_window(mpctx, true);
+ mp_wakeup_core(mpctx);
+}