summaryrefslogtreecommitdiffstats
path: root/mpvcore/player
diff options
context:
space:
mode:
Diffstat (limited to 'mpvcore/player')
-rw-r--r--mpvcore/player/command.c3019
-rw-r--r--mpvcore/player/command.h50
-rw-r--r--mpvcore/player/lua/assdraw.lua98
-rw-r--r--mpvcore/player/lua/defaults.lua82
-rw-r--r--mpvcore/player/lua/osc.lua1288
-rw-r--r--mpvcore/player/mp_core.h356
-rw-r--r--mpvcore/player/mp_lua.c683
-rw-r--r--mpvcore/player/mp_lua.h14
-rw-r--r--mpvcore/player/mp_osd.h52
-rw-r--r--mpvcore/player/mplayer.c5079
-rw-r--r--mpvcore/player/screenshot.c405
-rw-r--r--mpvcore/player/screenshot.h46
-rw-r--r--mpvcore/player/timeline/tl_cue.c417
-rw-r--r--mpvcore/player/timeline/tl_edl.c393
-rw-r--r--mpvcore/player/timeline/tl_matroska.c575
15 files changed, 12557 insertions, 0 deletions
diff --git a/mpvcore/player/command.c b/mpvcore/player/command.c
new file mode 100644
index 0000000000..f1a2453ca9
--- /dev/null
+++ b/mpvcore/player/command.c
@@ -0,0 +1,3019 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <inttypes.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <time.h>
+
+#include <libavutil/avstring.h>
+#include <libavutil/common.h>
+
+#include "config.h"
+#include "talloc.h"
+#include "command.h"
+#include "mpvcore/mp_common.h"
+#include "mpvcore/input/input.h"
+#include "stream/stream.h"
+#include "demux/demux.h"
+#include "demux/stheader.h"
+#include "mpvcore/resolve.h"
+#include "mpvcore/playlist.h"
+#include "mpvcore/playlist_parser.h"
+#include "sub/sub.h"
+#include "sub/dec_sub.h"
+#include "mpvcore/m_option.h"
+#include "mpvcore/m_property.h"
+#include "mpvcore/m_config.h"
+#include "video/filter/vf.h"
+#include "video/decode/vd.h"
+#include "mp_osd.h"
+#include "video/out/vo.h"
+#include "video/csputils.h"
+#include "audio/mixer.h"
+#include "audio/out/ao.h"
+#include "audio/filter/af.h"
+#include "video/decode/dec_video.h"
+#include "audio/decode/dec_audio.h"
+#include "mpvcore/path.h"
+#include "stream/tv.h"
+#include "stream/stream_radio.h"
+#include "stream/pvr.h"
+#ifdef CONFIG_DVBIN
+#include "stream/dvbin.h"
+#endif
+#include "screenshot.h"
+#ifdef HAVE_SYS_MMAN_H
+#include <sys/mman.h>
+#endif
+
+#include "mp_core.h"
+#include "mp_lua.h"
+
+struct command_ctx {
+ int events;
+
+#define OVERLAY_MAX_ID 64
+ void *overlay_map[OVERLAY_MAX_ID];
+};
+
+static int edit_filters(struct MPContext *mpctx, 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 char *format_bitrate(int rate)
+{
+ return talloc_asprintf(NULL, "%d kbps", rate * 8 / 1000);
+}
+
+static char *format_delay(double time)
+{
+ return talloc_asprintf(NULL, "%d ms", ROUND(time * 1000));
+}
+
+// Property-option bridge.
+static int mp_property_generic_option(struct m_option *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ char *optname = prop->priv;
+ struct m_config_option *opt = m_config_get_co(mpctx->mconfig,
+ bstr0(optname));
+ void *valptr = opt->data;
+
+ switch (action) {
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)arg = *(opt->opt);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ m_option_copy(opt->opt, arg, valptr);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ m_option_copy(opt->opt, valptr, arg);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Playback speed (RW)
+static int mp_property_playback_speed(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double orig_speed = opts->playback_speed;
+ switch (action) {
+ case M_PROPERTY_SET: {
+ opts->playback_speed = *(double *) arg;
+ // Adjust time until next frame flip for nosound mode
+ mpctx->time_frame *= orig_speed / opts->playback_speed;
+ if (mpctx->sh_audio)
+ reinit_audio_chain(mpctx);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "x %6.2f", orig_speed);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// filename with path (RO)
+static int mp_property_path(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->filename)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(prop, action, arg, mpctx->filename);
+}
+
+static int mp_property_filename(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ 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);
+ int r = m_property_strdup_ro(prop, action, arg, f[0] ? f : filename);
+ talloc_free(filename);
+ return r;
+}
+
+static int mp_property_media_title(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ char *name = NULL;
+ if (mpctx->resolve_result)
+ name = mpctx->resolve_result->title;
+ if (name && name[0])
+ return m_property_strdup_ro(prop, action, arg, name);
+ if (mpctx->master_demuxer) {
+ name = demux_info_get(mpctx->master_demuxer, "title");
+ if (name && name[0])
+ return m_property_strdup_ro(prop, action, arg, name);
+ }
+ return mp_property_filename(prop, action, arg, mpctx);
+}
+
+static int mp_property_stream_path(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct stream *stream = mpctx->stream;
+ if (!stream || !stream->url)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(prop, action, arg, stream->url);
+}
+
+static int mp_property_stream_capture(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->stream)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ char *filename = *(char **)arg;
+ stream_set_capture_file(mpctx->stream, filename);
+ // fall through to mp_property_generic_option
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Demuxer name (RO)
+static int mp_property_demuxer(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_strdup_ro(prop, action, arg, demuxer->desc->name);
+}
+
+/// Position in the stream (RW)
+static int mp_property_stream_pos(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct stream *stream = mpctx->stream;
+ if (!stream)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int64_t *) arg = stream_tell(stream);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ stream_seek(stream, *(int64_t *) arg);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Stream start offset (RO)
+static int mp_property_stream_start(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ struct stream *stream = mpctx->stream;
+ if (!stream)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(prop, action, arg, stream->start_pos);
+}
+
+/// Stream end offset (RO)
+static int mp_property_stream_end(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct stream *stream = mpctx->stream;
+ if (!stream)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(prop, action, arg, stream->end_pos);
+}
+
+/// Stream length (RO)
+static int mp_property_stream_length(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ struct stream *stream = mpctx->stream;
+ if (!stream)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int64_ro(prop, action, arg,
+ stream->end_pos - stream->start_pos);
+}
+
+// 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(m_option_t *prop, int action, void *arg, double time)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(double *)arg = time;
+ 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 = *prop;
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Current stream position in seconds (RO)
+static int mp_property_stream_time_pos(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ double pts = demuxer->stream_pts;
+ if (pts == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(prop, action, arg, pts);
+}
+
+
+/// Media length in seconds (RO)
+static int mp_property_length(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ double len;
+
+ if (!(int) (len = get_time_length(mpctx)))
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(prop, action, arg, len);
+}
+
+static int mp_property_avsync(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->sh_audio || !mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+ if (mpctx->last_av_difference == MP_NOPTS_VALUE)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_double_ro(prop, action, arg, mpctx->last_av_difference);
+}
+
+/// Current position in percent (RW)
+static int mp_property_percent_pos(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->num_sources)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SET: ;
+ double pos = *(double *)arg;
+ queue_seek(mpctx, MPSEEK_FACTOR, pos / 100.0, 0);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(double *)arg = get_current_pos_ratio(mpctx, false) * 100.0;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "%d", get_percent_pos(mpctx));
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Current position in seconds (RW)
+static int mp_property_time_pos(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->num_sources)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, 0);
+ return M_PROPERTY_OK;
+ }
+ return property_time(prop, action, arg, get_current_time(mpctx));
+}
+
+static int mp_property_remaining(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ double len = get_time_length(mpctx);
+ double pos = get_current_time(mpctx);
+ double start = get_start_time(mpctx);
+
+ if (!(int)len)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return property_time(prop, action, arg, len - (pos - start));
+}
+
+/// Current chapter (RW)
+static int mp_property_chapter(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ int chapter = get_current_chapter(mpctx);
+ if (chapter < -1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *) arg = chapter;
+ 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: ;
+ int step_all;
+ if (action == M_PROPERTY_SWITCH) {
+ struct m_property_switch_arg *sarg = arg;
+ step_all = ROUND(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 >= 0 &&
+ 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 < -1)
+ chapter = -1;
+ if (chapter >= get_chapter_count(mpctx) && step_all > 0) {
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ } else {
+ mp_seek_chapter(mpctx, chapter);
+ }
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_list_chapters(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action == M_PROPERTY_GET) {
+ int count = get_chapter_count(mpctx);
+ int cur = mpctx->num_sources ? 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);
+ char *m1 = "> ", *m2 = " <";
+ if (n != cur)
+ m1 = m2 = "";
+ res = talloc_asprintf_append(res, " %s%s%s\n", m1, name, m2);
+ talloc_free(name);
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_edition(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ if (demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int edition = demuxer->edition;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *)arg = edition;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET: {
+ edition = *(int *)arg;
+ if (edition != demuxer->edition) {
+ opts->edition_id = edition;
+ mpctx->stop_play = PT_RESTART;
+ }
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE: {
+ struct m_option opt = {
+ .name = prop->name,
+ .type = CONF_TYPE_INT,
+ .flags = CONF_RANGE,
+ .min = 0,
+ .max = demuxer->num_editions - 1,
+ };
+ *(struct m_option *)arg = opt;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static struct mp_resolve_src *find_source(struct mp_resolve_result *res,
+ char *encid, char *url)
+{
+ if (res->num_srcs == 0)
+ return NULL;
+
+ int src = 0;
+ for (int n = 0; n < res->num_srcs; n++) {
+ char *s_url = res->srcs[n]->url;
+ char *s_encid = res->srcs[n]->encid;
+ if (url && s_url && strcmp(url, s_url) == 0) {
+ src = n;
+ break;
+ }
+ // Prefer source URL if possible; so continue in case encid isn't unique
+ if (encid && s_encid && strcmp(encid, s_encid) == 0)
+ src = n;
+ }
+ return res->srcs[src];
+}
+
+static int mp_property_quvi_format(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct mp_resolve_result *res = mpctx->resolve_result;
+ if (!res || !res->num_srcs)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct mp_resolve_src *cur = find_source(res, opts->quvi_format, res->url);
+ if (!cur)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(char **)arg = talloc_strdup(NULL, cur->encid);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET: {
+ mpctx->stop_play = PT_RESTART;
+ // Make it restart at the same position. This will have disastrous
+ // consequences if the stream is not arbitrarily seekable, but whatever.
+ m_config_backup_opt(mpctx->mconfig, "start");
+ opts->play_start = (struct m_rel_time) {
+ .type = REL_TIME_ABSOLUTE,
+ .pos = get_current_time(mpctx),
+ };
+ break;
+ }
+ case M_PROPERTY_SWITCH: {
+ struct m_property_switch_arg *sarg = arg;
+ int pos = 0;
+ for (int n = 0; n < res->num_srcs; n++) {
+ if (res->srcs[n] == cur) {
+ pos = n;
+ break;
+ }
+ }
+ pos += sarg->inc;
+ if (pos < 0 || pos >= res->num_srcs) {
+ if (sarg->wrap) {
+ pos = (res->num_srcs + pos) % res->num_srcs;
+ } else {
+ pos = av_clip(pos, 0, res->num_srcs);
+ }
+ }
+ char *fmt = res->srcs[pos]->encid;
+ return mp_property_quvi_format(prop, M_PROPERTY_SET, &fmt, mpctx);
+ }
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Number of titles in file
+static int mp_property_titles(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ unsigned int num_titles;
+ if (!demuxer || stream_control(demuxer->stream, STREAM_CTRL_GET_NUM_TITLES,
+ &num_titles) < 1)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg, num_titles);
+}
+
+/// Number of chapters in file
+static int mp_property_chapters(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->num_sources)
+ return M_PROPERTY_UNAVAILABLE;
+ int count = get_chapter_count(mpctx);
+ return m_property_int_ro(prop, action, arg, count);
+}
+
+static int mp_property_editions(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+ if (demuxer->num_editions <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg, demuxer->num_editions);
+}
+
+/// Current dvd angle (RW)
+static int mp_property_angle(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ int angle = -1;
+ int angles;
+
+ if (demuxer)
+ angle = demuxer_get_current_angle(demuxer);
+ if (angle < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ angles = demuxer_angles_count(demuxer);
+ if (angles <= 1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *) arg = angle;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ *(char **) arg = talloc_asprintf(NULL, "%d/%d", angle, angles);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET:
+ angle = demuxer_set_angle(demuxer, *(int *)arg);
+ if (angle >= 0) {
+ if (mpctx->sh_video)
+ resync_video_stream(mpctx->sh_video);
+
+ if (mpctx->sh_audio)
+ resync_audio_stream(mpctx->sh_audio);
+ }
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET_TYPE: {
+ struct m_option opt = {
+ .name = prop->name,
+ .type = CONF_TYPE_INT,
+ .flags = CONF_RANGE,
+ .min = 1,
+ .max = angles,
+ };
+ *(struct m_option *)arg = opt;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int tag_property(m_option_t *prop, int action, void *arg,
+ struct mp_tags *tags)
+{
+ static const m_option_t key_type =
+ {
+ "tags", NULL, CONF_TYPE_STRING, 0, 0, 0, NULL
+ };
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char **slist = NULL;
+ int num = 0;
+ for (int n = 0; n < tags->num_keys; n++) {
+ MP_TARRAY_APPEND(NULL, slist, num, tags->keys[n]);
+ MP_TARRAY_APPEND(NULL, slist, num, tags->values[n]);
+ }
+ MP_TARRAY_APPEND(NULL, slist, num, NULL);
+ *(char ***)arg = slist;
+ 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]);
+ }
+ *(char **)arg = res;
+ return res ? M_PROPERTY_OK : M_PROPERTY_UNAVAILABLE;
+ }
+ case M_PROPERTY_KEY_ACTION: {
+ struct m_property_action_arg *ka = arg;
+ char *meta = mp_tags_get_str(tags, ka->key);
+ 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 = key_type;
+ return M_PROPERTY_OK;
+ }
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Demuxer meta data
+static int mp_property_metadata(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return tag_property(prop, action, arg, demuxer->metadata);
+}
+
+static int mp_property_chapter_metadata(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ int chapter = get_current_chapter(mpctx);
+ if (!demuxer || chapter < 0)
+ return M_PROPERTY_UNAVAILABLE;
+
+ assert(chapter < demuxer->num_chapters);
+
+ return tag_property(prop, action, arg, demuxer->chapters[chapter].metadata);
+}
+
+static int mp_property_pause(m_option_t *prop, int action, void *arg,
+ void *ctx)
+{
+ MPContext *mpctx = ctx;
+
+ if (action == M_PROPERTY_SET) {
+ if (*(int *)arg) {
+ pause_player(mpctx);
+ } else {
+ unpause_player(mpctx);
+ }
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, ctx);
+}
+
+static int mp_property_cache(m_option_t *prop, int action, void *arg,
+ void *ctx)
+{
+ MPContext *mpctx = ctx;
+ int cache = mp_get_cache_percent(mpctx);
+ if (cache < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg, cache);
+}
+
+static int mp_property_clock(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ 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(prop, action, arg, outstr);
+ return M_PROPERTY_UNAVAILABLE;
+}
+
+/// Volume (RW)
+static int mp_property_volume(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ switch (action) {
+ case M_PROPERTY_GET:
+ mixer_getbothvolume(mpctx->mixer, arg);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ if (!mixer_audio_initialized(mpctx->mixer))
+ return M_PROPERTY_ERROR;
+ mixer_setvolume(mpctx->mixer, *(float *) arg, *(float *) arg);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SWITCH: {
+ if (!mixer_audio_initialized(mpctx->mixer))
+ return M_PROPERTY_ERROR;
+ struct m_property_switch_arg *sarg = arg;
+ if (sarg->inc <= 0)
+ mixer_decvolume(mpctx->mixer);
+ else
+ mixer_incvolume(mpctx->mixer);
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Mute (RW)
+static int mp_property_mute(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ switch (action) {
+ case M_PROPERTY_SET:
+ if (!mixer_audio_initialized(mpctx->mixer))
+ return M_PROPERTY_ERROR;
+ mixer_setmute(mpctx->mixer, *(int *) arg);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(int *)arg = mixer_getmute(mpctx->mixer);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_volrestore(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ switch (action) {
+ case M_PROPERTY_GET: {
+ char *s = mixer_get_volume_restore_data(mpctx->mixer);
+ *(char **)arg = s;
+ return s ? M_PROPERTY_OK : M_PROPERTY_UNAVAILABLE;
+ }
+ case M_PROPERTY_SET:
+ return M_PROPERTY_NOT_IMPLEMENTED;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Audio delay (RW)
+static int mp_property_audio_delay(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!(mpctx->sh_audio && mpctx->sh_video))
+ return M_PROPERTY_UNAVAILABLE;
+ float delay = mpctx->opts->audio_delay;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = format_delay(delay);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ mpctx->audio_delay = mpctx->opts->audio_delay = *(float *)arg;
+ mpctx->delay -= mpctx->audio_delay - delay;
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Audio codec tag (RO)
+static int mp_property_audio_format(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ const char *c = mpctx->sh_audio ? mpctx->sh_audio->gsh->codec : NULL;
+ return m_property_strdup_ro(prop, action, arg, c);
+}
+
+/// Audio codec name (RO)
+static int mp_property_audio_codec(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ const char *c = mpctx->sh_audio ? mpctx->sh_audio->gsh->decoder_desc : NULL;
+ return m_property_strdup_ro(prop, action, arg, c);
+}
+
+/// Audio bitrate (RO)
+static int mp_property_audio_bitrate(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->sh_audio)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = format_bitrate(mpctx->sh_audio->i_bps);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(int *)arg = mpctx->sh_audio->i_bps;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Samplerate (RO)
+static int mp_property_samplerate(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->sh_audio)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "%d kHz",
+ mpctx->sh_audio->samplerate / 1000);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(int *)arg = mpctx->sh_audio->samplerate;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Number of channels (RO)
+static int mp_property_channels(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->sh_audio)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **) arg = mp_chmap_to_str(&mpctx->sh_audio->channels);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_GET:
+ *(int *)arg = mpctx->sh_audio->channels.num;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+/// Balance (RW)
+static int mp_property_balance(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ float bal;
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ mixer_getbalance(mpctx->mixer, arg);
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT: {
+ char **str = arg;
+ mixer_getbalance(mpctx->mixer, &bal);
+ if (bal == 0.f)
+ *str = talloc_strdup(NULL, "center");
+ else if (bal == -1.f)
+ *str = talloc_strdup(NULL, "left only");
+ else if (bal == 1.f)
+ *str = talloc_strdup(NULL, "right only");
+ else {
+ unsigned right = (bal + 1.f) / 2.f * 100.f;
+ *str = talloc_asprintf(NULL, "left %d%%, right %d%%",
+ 100 - right, right);
+ }
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET:
+ mixer_setbalance(mpctx->mixer, *(float *)arg);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+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 (seen && !next) {
+ next = cur;
+ }
+ if (!seen || !track) {
+ prev = cur;
+ }
+ }
+ }
+ }
+ return direction > 0 ? next : prev;
+}
+
+static int property_switch_track(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx, enum stream_type type)
+{
+ if (!mpctx->num_sources)
+ return M_PROPERTY_UNAVAILABLE;
+ struct track *track = mpctx->current_track[type];
+
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *) arg = track ? track->user_tid : -2;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ if (!track)
+ *(char **) arg = talloc_strdup(NULL, "no");
+ else {
+ char *lang = track->lang;
+ if (!lang)
+ lang = mp_gtext("unknown");
+
+ 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);
+ }
+ return M_PROPERTY_OK;
+
+ case M_PROPERTY_SWITCH: {
+ struct m_property_switch_arg *sarg = arg;
+ mp_switch_track(mpctx, type,
+ track_next(mpctx, type, sarg->inc >= 0 ? +1 : -1, track));
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET:
+ mp_switch_track(mpctx, type, mp_track_by_tid(mpctx, type, *(int *)arg));
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+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(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action == M_PROPERTY_GET) {
+ 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;
+
+ bool selected = mpctx->current_track[track->type] == track;
+ res = talloc_asprintf_append(res, "%s: ",
+ track_type_name(track->type));
+ if (selected)
+ res = talloc_asprintf_append(res, "> ");
+ 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) ");
+ if (selected)
+ res = talloc_asprintf_append(res, "<");
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ res = talloc_asprintf_append(res, "\n");
+ }
+
+ struct demuxer *demuxer = mpctx->master_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_NOT_IMPLEMENTED;
+}
+
+/// Selected audio id (RW)
+static int mp_property_audio(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_switch_track(prop, action, arg, mpctx, STREAM_AUDIO);
+}
+
+/// Selected video id (RW)
+static int mp_property_video(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_switch_track(prop, action, arg, mpctx, STREAM_VIDEO);
+}
+
+static struct track *find_track_by_demuxer_id(MPContext *mpctx,
+ enum stream_type type,
+ int demuxer_id)
+{
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == type && track->demuxer_id == demuxer_id)
+ return track;
+ }
+ return NULL;
+}
+
+static int mp_property_program(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ demux_program_t prog;
+
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (!demuxer)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SWITCH:
+ case M_PROPERTY_SET:
+ if (action == M_PROPERTY_SET && arg)
+ prog.progid = *((int *) arg);
+ else
+ prog.progid = -1;
+ if (demux_control(demuxer, DEMUXER_CTRL_IDENTIFY_PROGRAM, &prog) ==
+ DEMUXER_CTRL_NOTIMPL)
+ return M_PROPERTY_ERROR;
+
+ if (prog.aid < 0 && prog.vid < 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR,
+ "Selected program contains no audio or video streams!\n");
+ return M_PROPERTY_ERROR;
+ }
+ mp_switch_track(mpctx, STREAM_VIDEO,
+ find_track_by_demuxer_id(mpctx, STREAM_VIDEO, prog.vid));
+ mp_switch_track(mpctx, STREAM_AUDIO,
+ find_track_by_demuxer_id(mpctx, STREAM_AUDIO, prog.aid));
+ mp_switch_track(mpctx, STREAM_SUB,
+ find_track_by_demuxer_id(mpctx, STREAM_VIDEO, prog.sid));
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+
+/// Fullscreen state (RW)
+static int mp_property_fullscreen(m_option_t *prop,
+ int action,
+ void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+ struct mp_vo_opts *opts = mpctx->video_out->opts;
+
+ if (action == M_PROPERTY_SET) {
+ int val = *(int *)arg;
+ opts->fullscreen = val;
+ if (mpctx->video_out->config_ok)
+ vo_control(mpctx->video_out, VOCTRL_FULLSCREEN, 0);
+ return opts->fullscreen == val ? M_PROPERTY_OK : M_PROPERTY_ERROR;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+#define VF_DEINTERLACE_LABEL "deinterlace"
+
+static const char *deint_filters[] = {
+#ifdef CONFIG_VF_LAVFI
+ "lavfi=yadif",
+#endif
+ "yadif",
+#if CONFIG_VAAPI_VPP
+ "vavpp",
+#endif
+ NULL
+};
+
+static int probe_deint_filters(struct MPContext *mpctx, const char *cmd)
+{
+ for (int n = 0; deint_filters[n]; n++) {
+ char filter[80];
+ // add a label so that removing the filter is easier
+ snprintf(filter, sizeof(filter), "@%s:%s", VF_DEINTERLACE_LABEL,
+ deint_filters[n]);
+ if (edit_filters(mpctx, STREAM_VIDEO, cmd, filter) >= 0)
+ return 0;
+ }
+ return -1;
+}
+
+static int get_deinterlacing(struct MPContext *mpctx)
+{
+ vf_instance_t *vf = mpctx->sh_video->vfilter;
+ int enabled = 0;
+ if (vf->control(vf, VFCTRL_GET_DEINTERLACE, &enabled) != CONTROL_OK)
+ enabled = -1;
+ if (enabled < 0) {
+ // vf_lavfi doesn't support VFCTRL_GET_DEINTERLACE
+ if (vf_find_by_label(vf, VF_DEINTERLACE_LABEL))
+ enabled = 1;
+ }
+ return enabled;
+}
+
+static void set_deinterlacing(struct MPContext *mpctx, bool enable)
+{
+ vf_instance_t *vf = mpctx->sh_video->vfilter;
+ if (vf_find_by_label(vf, VF_DEINTERLACE_LABEL)) {
+ if (!enable)
+ edit_filters(mpctx, STREAM_VIDEO, "del", "@" VF_DEINTERLACE_LABEL);
+ } else {
+ if ((get_deinterlacing(mpctx) > 0) != enable) {
+ int arg = enable;
+ if (vf->control(vf, VFCTRL_SET_DEINTERLACE, &arg) != CONTROL_OK)
+ probe_deint_filters(mpctx, "pre");
+ }
+ }
+ mpctx->opts->deinterlace = get_deinterlacing(mpctx) > 0;
+}
+
+static int mp_property_deinterlace(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->sh_video || !mpctx->sh_video->vfilter)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_GET:
+ *(int *)arg = get_deinterlacing(mpctx) > 0;
+ return M_PROPERTY_OK;
+ case M_PROPERTY_SET:
+ set_deinterlacing(mpctx, *(int *)arg);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+// Generic option + requires hard refresh to make changes take effect.
+static int video_refresh_property_helper(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ int r = mp_property_generic_option(prop, action, arg, mpctx);
+ if (action == M_PROPERTY_SET) {
+ if (mpctx->sh_video) {
+ reinit_video_filters(mpctx);
+ mp_force_video_refresh(mpctx);
+ }
+ }
+ return r;
+}
+
+static int mp_property_colormatrix(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action != M_PROPERTY_PRINT)
+ return video_refresh_property_helper(prop, action, arg, mpctx);
+
+ struct MPOpts *opts = mpctx->opts;
+
+ struct mp_csp_details vo_csp = {0};
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_GET_YUV_COLORSPACE, &vo_csp);
+
+ struct mp_image_params vd_csp = {0};
+ if (mpctx->sh_video)
+ vd_control(mpctx->sh_video, VDCTRL_GET_PARAMS, &vd_csp);
+
+ char *res = talloc_asprintf(NULL, "%s",
+ mp_csp_names[opts->requested_colorspace]);
+ if (!vo_csp.format) {
+ res = talloc_asprintf_append(res, " (VO: unknown)");
+ } else if (vo_csp.format != opts->requested_colorspace) {
+ res = talloc_asprintf_append(res, " (VO: %s)",
+ mp_csp_names[vo_csp.format]);
+ }
+ if (!vd_csp.colorspace) {
+ res = talloc_asprintf_append(res, " (VD: unknown)");
+ } else if (!vo_csp.format || vd_csp.colorspace != vo_csp.format) {
+ res = talloc_asprintf_append(res, " (VD: %s)",
+ mp_csp_names[vd_csp.colorspace]);
+ }
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_colormatrix_input_range(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (action != M_PROPERTY_PRINT)
+ return video_refresh_property_helper(prop, action, arg, mpctx);
+
+ struct MPOpts *opts = mpctx->opts;
+
+ struct mp_csp_details vo_csp = {0};
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_GET_YUV_COLORSPACE, &vo_csp );
+
+ struct mp_image_params vd_csp = {0};
+ if (mpctx->sh_video)
+ vd_control(mpctx->sh_video, VDCTRL_GET_PARAMS, &vd_csp);
+
+ char *res = talloc_asprintf(NULL, "%s",
+ mp_csp_levels_names[opts->requested_input_range]);
+ if (!vo_csp.levels_in) {
+ res = talloc_asprintf_append(res, " (VO: unknown)");
+ } else if (vo_csp.levels_in != opts->requested_input_range) {
+ res = talloc_asprintf_append(res, " (VO: %s)",
+ mp_csp_levels_names[vo_csp.levels_in]);
+ }
+ if (!vd_csp.colorlevels) {
+ res = talloc_asprintf_append(res, " (VD: unknown)");
+ } else if (!vo_csp.levels_in || vd_csp.colorlevels != vo_csp.levels_in) {
+ res = talloc_asprintf_append(res, " (VD: %s)",
+ mp_csp_levels_names[vd_csp.colorlevels]);
+ }
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+}
+
+static int mp_property_colormatrix_output_range(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (action != M_PROPERTY_PRINT)
+ return video_refresh_property_helper(prop, action, arg, mpctx);
+
+ struct MPOpts *opts = mpctx->opts;
+
+ int req = opts->requested_output_range;
+ struct mp_csp_details actual = {0};
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_GET_YUV_COLORSPACE, &actual);
+
+ char *res = talloc_asprintf(NULL, "%s", mp_csp_levels_names[req]);
+ if (!actual.levels_out) {
+ res = talloc_asprintf_append(res, " (Actual: unknown)");
+ } else if (actual.levels_out != req) {
+ res = talloc_asprintf_append(res, " (Actual: %s)",
+ mp_csp_levels_names[actual.levels_out]);
+ }
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+}
+
+// Update options which are managed through VOCTRL_GET/SET_PANSCAN.
+static int panscan_property_helper(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+
+ if (!mpctx->video_out
+ || vo_control(mpctx->video_out, VOCTRL_GET_PANSCAN, NULL) != VO_TRUE)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int r = mp_property_generic_option(prop, action, arg, mpctx);
+ if (action == M_PROPERTY_SET)
+ vo_control(mpctx->video_out, VOCTRL_SET_PANSCAN, NULL);
+ return r;
+}
+
+/// Helper to set vo flags.
+/** \ingroup PropertyImplHelper
+ */
+static int mp_property_vo_flag(m_option_t *prop, int action, void *arg,
+ int vo_ctrl, int *vo_var, MPContext *mpctx)
+{
+
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ if (action == M_PROPERTY_SET) {
+ if (*vo_var == !!*(int *) arg)
+ return M_PROPERTY_OK;
+ if (mpctx->video_out->config_ok)
+ vo_control(mpctx->video_out, vo_ctrl, 0);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Window always on top (RW)
+static int mp_property_ontop(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return mp_property_vo_flag(prop, action, arg, VOCTRL_ONTOP,
+ &mpctx->opts->vo.ontop, mpctx);
+}
+
+/// Show window borders (RW)
+static int mp_property_border(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return mp_property_vo_flag(prop, action, arg, VOCTRL_BORDER,
+ &mpctx->opts->vo.border, mpctx);
+}
+
+static int mp_property_framedrop(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+static int mp_property_video_color(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SET: {
+ if (set_video_colors(mpctx->sh_video, prop->name, *(int *) arg) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ break;
+ }
+ case M_PROPERTY_GET:
+ if (get_video_colors(mpctx->sh_video, prop->name, (int *)arg) <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ // Write new value to option variable
+ mp_property_generic_option(prop, M_PROPERTY_SET, arg, mpctx);
+ return M_PROPERTY_OK;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Video codec tag (RO)
+static int mp_property_video_format(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ const char *c = mpctx->sh_video ? mpctx->sh_video->gsh->codec : NULL;
+ return m_property_strdup_ro(prop, action, arg, c);
+}
+
+/// Video codec name (RO)
+static int mp_property_video_codec(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ const char *c = mpctx->sh_video ? mpctx->sh_video->gsh->decoder_desc : NULL;
+ return m_property_strdup_ro(prop, action, arg, c);
+}
+
+
+/// Video bitrate (RO)
+static int mp_property_video_bitrate(m_option_t *prop, int action,
+ void *arg, MPContext *mpctx)
+{
+ if (!mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = format_bitrate(mpctx->sh_video->i_bps);
+ return M_PROPERTY_OK;
+ }
+ return m_property_int_ro(prop, action, arg, mpctx->sh_video->i_bps);
+}
+
+/// Video display width (RO)
+static int mp_property_width(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct sh_video *sh = mpctx->sh_video;
+ if (!sh)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg,
+ sh->vf_input ? sh->vf_input->w : sh->disp_w);
+}
+
+/// Video display height (RO)
+static int mp_property_height(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct sh_video *sh = mpctx->sh_video;
+ if (!sh)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg,
+ sh->vf_input ? sh->vf_input->h : sh->disp_h);
+}
+
+static int property_vo_wh(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx, bool get_w)
+{
+ struct vo *vo = mpctx->video_out;
+ if (!mpctx->sh_video && !vo || !vo->hasframe)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_int_ro(prop, action, arg,
+ get_w ? vo->aspdat.prew : vo->aspdat.preh);
+}
+
+static int mp_property_dwidth(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_vo_wh(prop, action, arg, mpctx, true);
+}
+
+static int mp_property_dheight(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_vo_wh(prop, action, arg, mpctx, false);
+}
+
+static int mp_property_osd_w(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return m_property_int_ro(prop, action, arg, mpctx->osd->last_vo_res.w);
+}
+
+static int mp_property_osd_h(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return m_property_int_ro(prop, action, arg, mpctx->osd->last_vo_res.w);
+}
+
+static int mp_property_osd_par(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return m_property_double_ro(prop, action, arg,
+ mpctx->osd->last_vo_res.display_par);
+}
+
+/// Video fps (RO)
+static int mp_property_fps(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+ return m_property_float_ro(prop, action, arg, mpctx->sh_video->fps);
+}
+
+/// Video aspect (RO)
+static int mp_property_aspect(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+ if (!mpctx->sh_video)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_SET: {
+ mpctx->opts->movie_aspect = *(float *)arg;
+ reinit_video_filters(mpctx);
+ mp_force_video_refresh(mpctx);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET: {
+ float aspect = -1;
+ struct mp_image_params *params = sh_video->vf_input;
+ if (params && params->d_w && params->d_h) {
+ aspect = (float)params->d_w / params->d_h;
+ } else if (sh_video->disp_w && sh_video->disp_h) {
+ aspect = (float)sh_video->disp_w / sh_video->disp_h;
+ }
+ if (aspect <= 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(float *)arg = aspect;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+// For OSD and subtitle related properties using the generic option bridge.
+// - Fail as unavailable if no video is active
+// - Trigger OSD state update when property is set
+static int property_osd_helper(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+ if (action == M_PROPERTY_SET)
+ osd_changed_all(mpctx->osd);
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+/// Selected subtitles (RW)
+static int mp_property_sub(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_switch_track(prop, action, arg, mpctx, STREAM_SUB);
+}
+
+/// Subtitle delay (RW)
+static int mp_property_sub_delay(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+ switch (action) {
+ case M_PROPERTY_PRINT:
+ *(char **)arg = format_delay(opts->sub_delay);
+ return M_PROPERTY_OK;
+ }
+ return property_osd_helper(prop, action, arg, mpctx);
+}
+
+static int mp_property_sub_pos(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+ if (action == M_PROPERTY_PRINT) {
+ *(char **)arg = talloc_asprintf(NULL, "%d/100", opts->sub_pos);
+ return M_PROPERTY_OK;
+ }
+ return property_osd_helper(prop, action, arg, mpctx);
+}
+
+#ifdef CONFIG_TV
+
+static tvi_handle_t *get_tvh(struct MPContext *mpctx)
+{
+ if (!(mpctx->master_demuxer && mpctx->master_demuxer->type == DEMUXER_TYPE_TV))
+ return NULL;
+ return mpctx->master_demuxer->priv;
+}
+
+/// TV color settings (RW)
+static int mp_property_tv_color(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ tvi_handle_t *tvh = get_tvh(mpctx);
+ if (!tvh)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_SET:
+ return tv_set_color_options(tvh, prop->offset, *(int *) arg);
+ case M_PROPERTY_GET:
+ return tv_get_color_options(tvh, prop->offset, arg);
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+#endif
+
+static int mp_property_playlist_pos(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ struct playlist *pl = mpctx->playlist;
+ if (!pl->first)
+ return M_PROPERTY_UNAVAILABLE;
+
+ switch (action) {
+ case M_PROPERTY_GET: {
+ int pos = playlist_entry_to_index(pl, pl->current);
+ if (pos < 0)
+ return M_PROPERTY_UNAVAILABLE;
+ *(int *)arg = pos;
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_SET: {
+ struct playlist_entry *e = playlist_entry_from_index(pl, *(int *)arg);
+ if (!e)
+ return M_PROPERTY_ERROR;
+ mp_set_playlist_entry(mpctx, e);
+ return M_PROPERTY_OK;
+ }
+ case M_PROPERTY_GET_TYPE: {
+ struct m_option opt = {
+ .name = prop->name,
+ .type = CONF_TYPE_INT,
+ .flags = CONF_RANGE,
+ .min = 0,
+ .max = playlist_entry_count(pl) - 1,
+ };
+ *(struct m_option *)arg = opt;
+ return M_PROPERTY_OK;
+ }
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_playlist_count(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action == M_PROPERTY_GET) {
+ *(int *)arg = playlist_entry_count(mpctx->playlist);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+static int mp_property_playlist(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action == M_PROPERTY_GET) {
+ char *res = talloc_strdup(NULL, "");
+
+ for (struct playlist_entry *e = mpctx->playlist->first; e; e = e->next)
+ {
+ if (mpctx->playlist->current == e) {
+ res = talloc_asprintf_append(res, "> %s <\n", e->filename);
+ } else {
+ res = talloc_asprintf_append(res, "%s\n", e->filename);
+ }
+ }
+
+ *(char **)arg = res;
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+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, "]\n");
+ }
+ if (!res)
+ res = talloc_strdup(NULL, "(empty)");
+ return res;
+}
+
+static int property_filter(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx, enum stream_type mt)
+{
+ switch (action) {
+ case 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;
+ }
+ case M_PROPERTY_SET:
+ return set_filters(mpctx, mt, *(struct m_obj_settings **)arg) >= 0
+ ? M_PROPERTY_OK : M_PROPERTY_ERROR;
+ }
+ return mp_property_generic_option(prop, action, arg, mpctx);
+}
+
+static int mp_property_vf(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_filter(prop, action, arg, mpctx, STREAM_VIDEO);
+}
+
+static int mp_property_af(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ return property_filter(prop, action, arg, mpctx, STREAM_AUDIO);
+}
+
+static int mp_property_alias(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ const char *real_property = prop->priv;
+ int r = mp_property_do(real_property, action, arg, mpctx);
+ if (action == M_PROPERTY_GET_TYPE && r >= 0) {
+ // Fix the property name
+ struct m_option *type = arg;
+ type->name = prop->name;
+ }
+ return r;
+}
+
+static int mp_property_options(m_option_t *prop, int action, void *arg,
+ MPContext *mpctx)
+{
+ if (action != M_PROPERTY_KEY_ACTION)
+ return M_PROPERTY_NOT_IMPLEMENTED;
+
+ struct m_property_action_arg *ka = arg;
+
+ 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 (!(mpctx->initialized_flags & INITIALIZED_PLAYBACK) &&
+ !(opt->opt->flags & (M_OPT_PRE_PARSE | M_OPT_GLOBAL)))
+ {
+ m_option_copy(opt->opt, opt->data, ka->arg);
+ return M_PROPERTY_OK;
+ }
+ return M_PROPERTY_ERROR;
+ case M_PROPERTY_GET_TYPE:
+ *(struct m_option *)ka->arg = *opt->opt;
+ return M_PROPERTY_OK;
+ }
+
+ return M_PROPERTY_NOT_IMPLEMENTED;
+}
+
+// Use option-to-property-bridge. (The property and option have the same names.)
+#define M_OPTION_PROPERTY(name) \
+ {(name), mp_property_generic_option, &m_option_type_dummy, 0, 0, 0, (name)}
+
+// OPTION_PROPERTY(), but with a custom property handler. The custom handler
+// must let unknown operations fall back to mp_property_generic_option().
+#define M_OPTION_PROPERTY_CUSTOM(name, handler) \
+ {(name), (handler), &m_option_type_dummy, 0, 0, 0, (name)}
+#define M_OPTION_PROPERTY_CUSTOM_(name, handler, ...) \
+ {(name), (handler), &m_option_type_dummy, 0, 0, 0, (name), __VA_ARGS__}
+
+// Redirect a property name to another
+#define M_PROPERTY_ALIAS(name, real_property) \
+ {(name), mp_property_alias, &m_option_type_dummy, 0, 0, 0, (real_property)}
+
+/// All properties available in MPlayer.
+/** \ingroup Properties
+ */
+static const m_option_t mp_properties[] = {
+ // General
+ M_OPTION_PROPERTY("osd-level"),
+ M_OPTION_PROPERTY_CUSTOM("osd-scale", property_osd_helper),
+ M_OPTION_PROPERTY("loop"),
+ M_OPTION_PROPERTY_CUSTOM("speed", mp_property_playback_speed),
+ { "filename", mp_property_filename, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "path", mp_property_path, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "media-title", mp_property_media_title, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "stream-path", mp_property_stream_path, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ M_OPTION_PROPERTY_CUSTOM("stream-capture", mp_property_stream_capture),
+ { "demuxer", mp_property_demuxer, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "stream-pos", mp_property_stream_pos, CONF_TYPE_INT64,
+ M_OPT_MIN, 0, 0, NULL },
+ { "stream-start", mp_property_stream_start, CONF_TYPE_INT64,
+ M_OPT_MIN, 0, 0, NULL },
+ { "stream-end", mp_property_stream_end, CONF_TYPE_INT64,
+ M_OPT_MIN, 0, 0, NULL },
+ { "stream-length", mp_property_stream_length, CONF_TYPE_INT64,
+ M_OPT_MIN, 0, 0, NULL },
+ { "stream-time-pos", mp_property_stream_time_pos, CONF_TYPE_TIME,
+ M_OPT_MIN, 0, 0, NULL },
+ { "length", mp_property_length, CONF_TYPE_TIME,
+ M_OPT_MIN, 0, 0, NULL },
+ { "avsync", mp_property_avsync, CONF_TYPE_DOUBLE },
+ { "percent-pos", mp_property_percent_pos, CONF_TYPE_DOUBLE,
+ M_OPT_RANGE, 0, 100, NULL },
+ { "time-pos", mp_property_time_pos, CONF_TYPE_TIME,
+ M_OPT_MIN, 0, 0, NULL },
+ { "time-remaining", mp_property_remaining, CONF_TYPE_TIME },
+ { "chapter", mp_property_chapter, CONF_TYPE_INT,
+ M_OPT_MIN, -1, 0, NULL },
+ M_OPTION_PROPERTY_CUSTOM("edition", mp_property_edition),
+ M_OPTION_PROPERTY_CUSTOM("quvi-format", mp_property_quvi_format),
+ { "titles", mp_property_titles, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "chapters", mp_property_chapters, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "editions", mp_property_editions, CONF_TYPE_INT },
+ { "angle", mp_property_angle, &m_option_type_dummy },
+ { "metadata", mp_property_metadata, CONF_TYPE_STRING_LIST },
+ { "chapter-metadata", mp_property_chapter_metadata, CONF_TYPE_STRING_LIST },
+ M_OPTION_PROPERTY_CUSTOM("pause", mp_property_pause),
+ { "cache", mp_property_cache, CONF_TYPE_INT },
+ M_OPTION_PROPERTY("pts-association-mode"),
+ M_OPTION_PROPERTY("hr-seek"),
+ { "clock", mp_property_clock, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+
+ { "chapter-list", mp_property_list_chapters, CONF_TYPE_STRING },
+ { "track-list", property_list_tracks, CONF_TYPE_STRING },
+
+ { "playlist", mp_property_playlist, CONF_TYPE_STRING },
+ { "playlist-pos", mp_property_playlist_pos, CONF_TYPE_INT },
+ { "playlist-count", mp_property_playlist_count, CONF_TYPE_INT },
+
+ // Audio
+ { "volume", mp_property_volume, CONF_TYPE_FLOAT,
+ M_OPT_RANGE, 0, 100, NULL },
+ { "mute", mp_property_mute, CONF_TYPE_FLAG,
+ M_OPT_RANGE, 0, 1, NULL },
+ M_OPTION_PROPERTY_CUSTOM("audio-delay", mp_property_audio_delay),
+ { "audio-format", mp_property_audio_format, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "audio-codec", mp_property_audio_codec, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "audio-bitrate", mp_property_audio_bitrate, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "samplerate", mp_property_samplerate, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "channels", mp_property_channels, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ M_OPTION_PROPERTY_CUSTOM("aid", mp_property_audio),
+ { "balance", mp_property_balance, CONF_TYPE_FLOAT,
+ M_OPT_RANGE, -1, 1, NULL },
+ M_OPTION_PROPERTY_CUSTOM("volume-restore-data", mp_property_volrestore),
+
+ // Video
+ M_OPTION_PROPERTY_CUSTOM("fullscreen", mp_property_fullscreen),
+ { "deinterlace", mp_property_deinterlace, CONF_TYPE_FLAG,
+ M_OPT_RANGE, 0, 1, NULL },
+ M_OPTION_PROPERTY_CUSTOM("colormatrix", mp_property_colormatrix),
+ M_OPTION_PROPERTY_CUSTOM("colormatrix-input-range",
+ mp_property_colormatrix_input_range),
+ M_OPTION_PROPERTY_CUSTOM("colormatrix-output-range",
+ mp_property_colormatrix_output_range),
+ M_OPTION_PROPERTY_CUSTOM("ontop", mp_property_ontop),
+ M_OPTION_PROPERTY_CUSTOM("border", mp_property_border),
+ M_OPTION_PROPERTY_CUSTOM("framedrop", mp_property_framedrop),
+ M_OPTION_PROPERTY_CUSTOM("gamma", mp_property_video_color),
+ M_OPTION_PROPERTY_CUSTOM("brightness", mp_property_video_color),
+ M_OPTION_PROPERTY_CUSTOM("contrast", mp_property_video_color),
+ M_OPTION_PROPERTY_CUSTOM("saturation", mp_property_video_color),
+ M_OPTION_PROPERTY_CUSTOM("hue", mp_property_video_color),
+ M_OPTION_PROPERTY_CUSTOM("panscan", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-zoom", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-align-x", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-align-y", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-pan-x", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-pan-y", panscan_property_helper),
+ M_OPTION_PROPERTY_CUSTOM("video-unscaled", panscan_property_helper),
+ { "video-format", mp_property_video_format, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "video-codec", mp_property_video_codec, CONF_TYPE_STRING,
+ 0, 0, 0, NULL },
+ { "video-bitrate", mp_property_video_bitrate, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "width", mp_property_width, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "height", mp_property_height, CONF_TYPE_INT,
+ 0, 0, 0, NULL },
+ { "dwidth", mp_property_dwidth, CONF_TYPE_INT },
+ { "dheight", mp_property_dheight, CONF_TYPE_INT },
+ { "fps", mp_property_fps, CONF_TYPE_FLOAT,
+ 0, 0, 0, NULL },
+ { "aspect", mp_property_aspect, CONF_TYPE_FLOAT,
+ CONF_RANGE, -1, 10, NULL },
+ M_OPTION_PROPERTY_CUSTOM("vid", mp_property_video),
+ { "program", mp_property_program, CONF_TYPE_INT,
+ CONF_RANGE, -1, 65535, NULL },
+
+ { "osd-width", mp_property_osd_w, CONF_TYPE_INT },
+ { "osd-height", mp_property_osd_h, CONF_TYPE_INT },
+ { "osd-par", mp_property_osd_par, CONF_TYPE_DOUBLE },
+
+ // Subs
+ M_OPTION_PROPERTY_CUSTOM("sid", mp_property_sub),
+ M_OPTION_PROPERTY_CUSTOM("sub-delay", mp_property_sub_delay),
+ M_OPTION_PROPERTY_CUSTOM("sub-pos", mp_property_sub_pos),
+ M_OPTION_PROPERTY_CUSTOM("sub-visibility", property_osd_helper),
+ M_OPTION_PROPERTY_CUSTOM("sub-forced-only", property_osd_helper),
+ M_OPTION_PROPERTY_CUSTOM("sub-scale", property_osd_helper),
+#ifdef CONFIG_ASS
+ M_OPTION_PROPERTY_CUSTOM("ass-use-margins", property_osd_helper),
+ M_OPTION_PROPERTY_CUSTOM("ass-vsfilter-aspect-compat", property_osd_helper),
+ M_OPTION_PROPERTY_CUSTOM("ass-style-override", property_osd_helper),
+#endif
+
+ M_OPTION_PROPERTY_CUSTOM("vf*", mp_property_vf),
+ M_OPTION_PROPERTY_CUSTOM("af*", mp_property_af),
+
+#ifdef CONFIG_TV
+ { "tv-brightness", mp_property_tv_color, CONF_TYPE_INT,
+ M_OPT_RANGE, -100, 100, .offset = TV_COLOR_BRIGHTNESS },
+ { "tv-contrast", mp_property_tv_color, CONF_TYPE_INT,
+ M_OPT_RANGE, -100, 100, .offset = TV_COLOR_CONTRAST },
+ { "tv-saturation", mp_property_tv_color, CONF_TYPE_INT,
+ M_OPT_RANGE, -100, 100, .offset = TV_COLOR_SATURATION },
+ { "tv-hue", mp_property_tv_color, CONF_TYPE_INT,
+ M_OPT_RANGE, -100, 100, .offset = TV_COLOR_HUE },
+#endif
+
+ M_PROPERTY_ALIAS("video", "vid"),
+ M_PROPERTY_ALIAS("audio", "aid"),
+ M_PROPERTY_ALIAS("sub", "sid"),
+
+ { "options", mp_property_options, &m_option_type_dummy },
+
+ {0},
+};
+
+const struct m_option *mp_get_property_list(void)
+{
+ return mp_properties;
+}
+
+int mp_property_do(const char *name, int action, void *val,
+ struct MPContext *ctx)
+{
+ return m_property_do(mp_properties, name, action, val, ctx);
+}
+
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str)
+{
+ return m_properties_expand_string(mp_properties, str, mpctx);
+}
+
+void property_print_help(void)
+{
+ m_properties_print_help_list(mp_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 struct property_osd_display {
+ // property name
+ const char *name;
+ // name used on OSD
+ const char *osd_name;
+ // progressbar type
+ int osd_progbar;
+ // osd msg id if it must be shared
+ int osd_id;
+ // Needs special ways to display the new value (seeks are delayed)
+ int seek_msg, seek_bar;
+ // Free-form message (if NULL, osd_name or the property name is used)
+ const char *msg;
+ // Extra free-from message (just for volume)
+ const char *extra_msg;
+} property_osd_display[] = {
+ // general
+ { "loop", _("Loop") },
+ { "chapter", .seek_msg = OSD_SEEK_INFO_CHAPTER_TEXT,
+ .seek_bar = OSD_SEEK_INFO_BAR },
+ { "edition", .seek_msg = OSD_SEEK_INFO_EDITION },
+ { "pts-association-mode", "PTS association mode" },
+ { "hr-seek", "hr-seek" },
+ { "speed", _("Speed") },
+ { "clock", _("Clock") },
+ // audio
+ { "volume", _("Volume"),
+ .extra_msg = "${?mute==yes:(Muted)}", .osd_progbar = OSD_VOLUME },
+ { "mute", _("Mute") },
+ { "audio-delay", _("A-V delay") },
+ { "audio", _("Audio") },
+ { "balance", _("Balance"), .osd_progbar = OSD_BALANCE },
+ // video
+ { "panscan", _("Panscan"), .osd_progbar = OSD_PANSCAN },
+ { "ontop", _("Stay on top") },
+ { "border", _("Border") },
+ { "framedrop", _("Framedrop") },
+ { "deinterlace", _("Deinterlace") },
+ { "colormatrix", _("YUV colormatrix") },
+ { "colormatrix-input-range", _("YUV input range") },
+ { "colormatrix-output-range", _("RGB output range") },
+ { "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") },
+ { "sub-pos", _("Sub position") },
+ { "sub-delay", _("Sub delay"), .osd_id = OSD_MSG_SUB_DELAY },
+ { "sub-visibility", _("Subtitles") },
+ { "sub-forced-only", _("Forced sub only") },
+ { "sub-scale", _("Sub Scale")},
+ { "ass-vsfilter-aspect-compat", _("Subtitle VSFilter aspect compat")},
+ { "ass-style-override", _("ASS subtitle style override")},
+ { "vf*", _("Video filters"), .msg = "Video filters:\n${vf}"},
+ { "af*", _("Audio filters"), .msg = "Audio filters:\n${af}"},
+#ifdef CONFIG_TV
+ { "tv-brightness", _("Brightness"), .osd_progbar = OSD_BRIGHTNESS },
+ { "tv-hue", _("Hue"), .osd_progbar = OSD_HUE},
+ { "tv-saturation", _("Saturation"), .osd_progbar = OSD_SATURATION },
+ { "tv-contrast", _("Contrast"), .osd_progbar = OSD_CONTRAST },
+#endif
+ {0}
+};
+
+static void show_property_osd(MPContext *mpctx, const char *pname,
+ enum mp_on_osd osd_mode)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct m_option prop = {0};
+ struct property_osd_display *p;
+
+ if (mp_property_do(pname, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0)
+ return;
+
+ int osd_progbar = 0;
+ const char *osd_name = NULL;
+ const char *msg = NULL;
+ const char *extra_msg = NULL;
+
+ // look for the command
+ for (p = property_osd_display; p->name; p++) {
+ if (!strcmp(p->name, prop.name)) {
+ osd_progbar = p->seek_bar ? 1 : p->osd_progbar;
+ osd_name = p->seek_msg ? "" : mp_gtext(p->osd_name);
+ break;
+ }
+ }
+ if (!p->name)
+ p = NULL;
+
+ if (p) {
+ msg = p->msg;
+ extra_msg = p->extra_msg;
+ }
+
+ if (osd_mode != MP_ON_OSD_AUTO) {
+ osd_name = osd_name ? osd_name : prop.name;
+ if (!(osd_mode & MP_ON_OSD_MSG)) {
+ osd_name = NULL;
+ msg = NULL;
+ extra_msg = NULL;
+ }
+ osd_progbar = osd_progbar ? osd_progbar : ' ';
+ if (!(osd_mode & MP_ON_OSD_BAR))
+ osd_progbar = 0;
+ }
+
+ if (p && (p->seek_msg || p->seek_bar)) {
+ mpctx->add_osd_seek_info |=
+ (osd_name ? p->seek_msg : 0) | (osd_progbar ? p->seek_bar : 0);
+ return;
+ }
+
+ void *tmp = talloc_new(NULL);
+
+ if (!msg && osd_name)
+ msg = talloc_asprintf(tmp, "%s: ${%s}", osd_name, prop.name);
+
+ if (osd_progbar && (prop.flags & CONF_RANGE) == CONF_RANGE) {
+ bool ok = false;
+ if (prop.type == CONF_TYPE_INT) {
+ int i;
+ ok = mp_property_do(prop.name, M_PROPERTY_GET, &i, mpctx) > 0;
+ if (ok)
+ set_osd_bar(mpctx, osd_progbar, osd_name, prop.min, prop.max, i);
+ } else if (prop.type == CONF_TYPE_FLOAT) {
+ float f;
+ ok = mp_property_do(prop.name, M_PROPERTY_GET, &f, mpctx) > 0;
+ if (ok)
+ set_osd_bar(mpctx, osd_progbar, osd_name, prop.min, prop.max, f);
+ }
+ if (ok && osd_mode == MP_ON_OSD_AUTO && opts->osd_bar_visible)
+ msg = NULL;
+ }
+
+ char *osd_msg = NULL;
+ if (msg)
+ osd_msg = talloc_steal(tmp, mp_property_expand_string(mpctx, msg));
+ if (extra_msg) {
+ char *t = talloc_steal(tmp, mp_property_expand_string(mpctx, extra_msg));
+ osd_msg = talloc_asprintf(tmp, "%s%s%s", osd_msg ? osd_msg : "",
+ osd_msg && osd_msg[0] ? " " : "", t);
+ }
+
+ if (osd_msg && osd_msg[0]) {
+ int osd_id = 0;
+ if (p) {
+ int index = p - property_osd_display;
+ osd_id = p->osd_id ? p->osd_id : OSD_MSG_PROPERTY + index;
+ }
+ set_osd_tmsg(mpctx, osd_id, 1, opts->osd_duration, "%s", osd_msg);
+ }
+
+ talloc_free(tmp);
+}
+
+static const char *property_error_string(int error_value)
+{
+ switch (error_value) {
+ case M_PROPERTY_ERROR:
+ return "ERROR";
+ case M_PROPERTY_UNAVAILABLE:
+ return "PROPERTY_UNAVAILABLE";
+ case M_PROPERTY_NOT_IMPLEMENTED:
+ return "NOT_IMPLEMENTED";
+ case M_PROPERTY_UNKNOWN:
+ return "PROPERTY_UNKNOWN";
+ }
+ return "UNKNOWN";
+}
+
+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 *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);
+ } else {
+ m_option_free(co->opt, list);
+ *list = old_settings;
+ reinit_filters(mpctx, mediatype);
+ }
+
+ if (mediatype == STREAM_VIDEO)
+ mp_force_video_refresh(mpctx);
+
+ return success ? 0 : -1;
+}
+
+static int edit_filters(struct MPContext *mpctx, 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(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, 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_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration,
+ "Changing filters failed!");
+ }
+ }
+ return r;
+}
+
+#ifdef HAVE_SYS_MMAN_H
+
+static int ext2_sub_find(struct MPContext *mpctx, int id)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct sub_bitmaps *sub = &mpctx->osd->external2;
+ void *p = NULL;
+ if (id >= 0 && id < OVERLAY_MAX_ID)
+ p = cmd->overlay_map[id];
+ if (sub && p) {
+ for (int n = 0; n < sub->num_parts; n++) {
+ if (sub->parts[n].bitmap == p)
+ return n;
+ }
+ }
+ return -1;
+}
+
+static int ext2_sub_alloc(struct MPContext *mpctx)
+{
+ struct osd_state *osd = mpctx->osd;
+ struct sub_bitmaps *sub = &osd->external2;
+ struct sub_bitmap b = {0};
+ MP_TARRAY_APPEND(osd, sub->parts, sub->num_parts, b);
+ return sub->num_parts - 1;
+}
+
+static int overlay_add(struct MPContext *mpctx, int id, int x, int y,
+ char *file, int offset, char *fmt, int w, int h,
+ int stride)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct osd_state *osd = mpctx->osd;
+ if (strcmp(fmt, "bgra") != 0) {
+ MP_ERR(mpctx, "overlay_add: unsupported OSD format '%s'\n", fmt);
+ return -1;
+ }
+ if (id < 0 || id >= OVERLAY_MAX_ID) {
+ MP_ERR(mpctx, "overlay_add: invalid id %d\n", id);
+ return -1;
+ }
+ int fd = -1;
+ bool close_fd = true;
+ if (file[0] == '@') {
+ char *end;
+ fd = strtol(&file[1], &end, 10);
+ if (!file[1] || end[0])
+ fd = -1;
+ close_fd = false;
+ } else {
+ fd = open(file, O_RDONLY | O_BINARY);
+ }
+ void *p = mmap(NULL, h * stride, PROT_READ, MAP_SHARED, fd, offset);
+ if (fd >= 0 && close_fd)
+ close(fd);
+ if (!p) {
+ MP_ERR(mpctx, "overlay_add: could not open or map '%s'\n", file);
+ return -1;
+ }
+ int index = ext2_sub_find(mpctx, id);
+ if (index < 0)
+ index = ext2_sub_alloc(mpctx);
+ if (index < 0) {
+ munmap(p, h * stride);
+ return -1;
+ }
+ cmd->overlay_map[id] = p;
+ osd->external2.parts[index] = (struct sub_bitmap) {
+ .bitmap = p,
+ .stride = stride,
+ .x = x, .y = y,
+ .w = w, .h = h,
+ .dw = w, .dh = h,
+ };
+ osd->external2.bitmap_id = osd->external2.bitmap_pos_id = 1;
+ osd->external2.format = SUBBITMAP_RGBA;
+ osd->want_redraw = true;
+ return 0;
+}
+
+static void overlay_remove(struct MPContext *mpctx, int id)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct osd_state *osd = mpctx->osd;
+ int index = ext2_sub_find(mpctx, id);
+ if (index >= 0) {
+ struct sub_bitmaps *sub = &osd->external2;
+ struct sub_bitmap *part = &sub->parts[index];
+ munmap(part->bitmap, part->h * part->stride);
+ MP_TARRAY_REMOVE_AT(sub->parts, sub->num_parts, index);
+ cmd->overlay_map[id] = NULL;
+ sub->bitmap_id = sub->bitmap_pos_id = 1;
+ }
+}
+
+static void overlay_uninit(struct MPContext *mpctx)
+{
+ for (int id = 0; id < OVERLAY_MAX_ID; id++)
+ overlay_remove(mpctx, id);
+}
+
+#else
+
+static void overlay_uninit(struct MPContext *mpctx){}
+
+#endif
+
+// 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 == &m_option_type_flag || prop.type == &m_option_type_choice)
+ return false;
+
+ return true;
+}
+
+void run_command(MPContext *mpctx, mp_cmd_t *cmd)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int osd_duration = opts->osd_duration;
+ bool auto_osd = cmd->on_osd == MP_ON_OSD_AUTO;
+ bool msg_osd = auto_osd || (cmd->on_osd & MP_ON_OSD_MSG);
+ bool bar_osd = auto_osd || (cmd->on_osd & MP_ON_OSD_BAR);
+ bool msg_or_nobar_osd = msg_osd && !(auto_osd && opts->osd_bar_visible);
+ int osdl = msg_osd ? 1 : OSD_LEVEL_INVISIBLE;
+
+ if (!cmd->raw_args) {
+ for (int n = 0; n < cmd->nargs; n++) {
+ if (cmd->args[n].type.type == CONF_TYPE_STRING) {
+ cmd->args[n].v.s =
+ mp_property_expand_string(mpctx, cmd->args[n].v.s);
+ if (!cmd->args[n].v.s)
+ return;
+ talloc_steal(cmd, cmd->args[n].v.s);
+ }
+ }
+ }
+
+ switch (cmd->id) {
+ case MP_CMD_SEEK: {
+ double v = cmd->args[0].v.d * cmd->scale;
+ int abs = cmd->args[1].v.i;
+ int exact = cmd->args[2].v.i;
+ if (abs == 2) { // Absolute seek to a timestamp in seconds
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, v, exact);
+ set_osd_function(mpctx,
+ v > get_current_time(mpctx) ? OSD_FFW : OSD_REW);
+ } else if (abs) { /* Absolute seek by percentage */
+ queue_seek(mpctx, MPSEEK_FACTOR, v / 100.0, exact);
+ set_osd_function(mpctx, OSD_FFW); // Direction isn't set correctly
+ } else {
+ queue_seek(mpctx, MPSEEK_RELATIVE, v, exact);
+ set_osd_function(mpctx, (v > 0) ? OSD_FFW : OSD_REW);
+ }
+ if (bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (msg_or_nobar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ break;
+ }
+
+ case MP_CMD_SET: {
+ int r = mp_property_do(cmd->args[0].v.s, M_PROPERTY_SET_STRING,
+ cmd->args[1].v.s, mpctx);
+ if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) {
+ show_property_osd(mpctx, cmd->args[0].v.s, cmd->on_osd);
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration,
+ "Unknown property: '%s'", cmd->args[0].v.s);
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration,
+ "Failed to set property '%s' to '%s'",
+ cmd->args[0].v.s, cmd->args[1].v.s);
+ }
+ break;
+ }
+
+ case MP_CMD_ADD:
+ case MP_CMD_CYCLE:
+ {
+ struct m_property_switch_arg s = {
+ .inc = 1,
+ .wrap = cmd->id == MP_CMD_CYCLE,
+ };
+ if (cmd->args[1].v.d)
+ s.inc = cmd->args[1].v.d * cmd->scale;
+ char *property = cmd->args[0].v.s;
+ if (cmd->repeated && !check_property_autorepeat(property, mpctx)) {
+ mp_msg(MSGT_CPLAYER, MSGL_V,
+ "Dropping command '%.*s' from auto-repeated key.\n",
+ BSTR_P(cmd->original));
+ break;
+ }
+ int r = mp_property_do(property, M_PROPERTY_SWITCH, &s, mpctx);
+ if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) {
+ show_property_osd(mpctx, property, cmd->on_osd);
+ } else if (r == M_PROPERTY_UNKNOWN) {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration,
+ "Unknown property: '%s'", property);
+ } else if (r <= 0) {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration,
+ "Failed to increment property '%s' by %g",
+ property, s.inc);
+ }
+ break;
+ }
+
+ case MP_CMD_GET_PROPERTY: {
+ char *tmp;
+ int r = mp_property_do(cmd->args[0].v.s, M_PROPERTY_GET_STRING,
+ &tmp, mpctx);
+ if (r <= 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN,
+ "Failed to get value of property '%s'.\n",
+ cmd->args[0].v.s);
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "ANS_ERROR=%s\n",
+ property_error_string(r));
+ break;
+ }
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "ANS_%s=%s\n",
+ cmd->args[0].v.s, tmp);
+ talloc_free(tmp);
+ break;
+ }
+
+ case MP_CMD_SPEED_MULT: {
+ double v = cmd->args[0].v.d * cmd->scale;
+ v *= mpctx->opts->playback_speed;
+ mp_property_do("speed", M_PROPERTY_SET, &v, mpctx);
+ show_property_osd(mpctx, "speed", cmd->on_osd);
+ break;
+ }
+
+ case MP_CMD_FRAME_STEP:
+ add_step_frame(mpctx, 1);
+ break;
+
+ case MP_CMD_FRAME_BACK_STEP:
+ add_step_frame(mpctx, -1);
+ break;
+
+ case MP_CMD_QUIT:
+ mpctx->stop_play = PT_QUIT;
+ mpctx->quit_custom_rc = cmd->args[0].v.i;
+ mpctx->has_quit_custom_rc = true;
+ break;
+
+ case MP_CMD_QUIT_WATCH_LATER:
+ mp_write_watch_later_conf(mpctx);
+ mpctx->stop_play = PT_QUIT;
+ mpctx->quit_player_rc = 0;
+ break;
+
+ case MP_CMD_PLAYLIST_NEXT:
+ case MP_CMD_PLAYLIST_PREV:
+ {
+ int dir = cmd->id == MP_CMD_PLAYLIST_PREV ? -1 : +1;
+ int force = cmd->args[0].v.i;
+
+ struct playlist_entry *e = mp_next_file(mpctx, dir, force);
+ if (!e && !force)
+ break;
+ mpctx->playlist->current = e;
+ mpctx->playlist->current_was_replaced = false;
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ break;
+ }
+
+ case MP_CMD_SUB_STEP:
+ case MP_CMD_SUB_SEEK:
+ if (mpctx->osd->dec_sub) {
+ double a[2];
+ a[0] = mpctx->video_pts - mpctx->osd->video_offset + opts->sub_delay;
+ a[1] = cmd->args[0].v.i;
+ if (sub_control(mpctx->osd->dec_sub, SD_CTRL_SUB_STEP, a) > 0) {
+ if (cmd->id == MP_CMD_SUB_STEP) {
+ opts->sub_delay += a[0];
+ osd_changed_all(mpctx->osd);
+ set_osd_tmsg(mpctx, OSD_MSG_SUB_DELAY, osdl, osd_duration,
+ "Sub delay: %d ms", ROUND(opts->sub_delay * 1000));
+ } else {
+ // We can easily get stuck by failing to seek to the video
+ // frame which actually shows the sub first (because video
+ // frame PTS and sub PTS rarely match exactly). Add some
+ // rounding for the mess of it.
+ a[0] += 0.01 * (a[1] > 0 ? 1 : -1);
+ queue_seek(mpctx, MPSEEK_RELATIVE, a[0], 1);
+ set_osd_function(mpctx, (a[0] > 0) ? OSD_FFW : OSD_REW);
+ if (bar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR;
+ if (msg_or_nobar_osd)
+ mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT;
+ }
+ }
+ }
+ break;
+
+ case MP_CMD_OSD: {
+ int v = cmd->args[0].v.i;
+ int max = (opts->term_osd && !mpctx->video_out) ? MAX_TERM_OSD_LEVEL
+ : MAX_OSD_LEVEL;
+ if (opts->osd_level > max)
+ opts->osd_level = max;
+ if (v < 0)
+ opts->osd_level = (opts->osd_level + 1) % (max + 1);
+ else
+ opts->osd_level = v > max ? max : v;
+ if (msg_osd && opts->osd_level <= 1)
+ set_osd_tmsg(mpctx, OSD_MSG_OSD_STATUS, 0, osd_duration,
+ "OSD: %s", opts->osd_level ? "yes" : "no");
+ else
+ rm_osd_msg(mpctx, OSD_MSG_OSD_STATUS);
+ break;
+ }
+
+ case MP_CMD_PRINT_TEXT: {
+ mp_msg(MSGT_GLOBAL, MSGL_INFO, "%s\n", cmd->args[0].v.s);
+ break;
+ }
+
+ case MP_CMD_SHOW_TEXT: {
+ // if no argument supplied use default osd_duration, else <arg> ms.
+ set_osd_msg(mpctx, OSD_MSG_TEXT, cmd->args[2].v.i,
+ (cmd->args[1].v.i < 0 ? osd_duration : cmd->args[1].v.i),
+ "%s", cmd->args[0].v.s);
+ break;
+ }
+
+ case MP_CMD_LOADFILE: {
+ char *filename = cmd->args[0].v.s;
+ bool append = cmd->args[1].v.i;
+
+ if (!append)
+ playlist_clear(mpctx->playlist);
+
+ playlist_add(mpctx->playlist, playlist_entry_new(filename));
+
+ if (!append)
+ mp_set_playlist_entry(mpctx, mpctx->playlist->first);
+ break;
+ }
+
+ case MP_CMD_LOADLIST: {
+ char *filename = cmd->args[0].v.s;
+ bool append = cmd->args[1].v.i;
+ struct playlist *pl = playlist_parse_file(filename, opts);
+ if (pl) {
+ if (!append)
+ playlist_clear(mpctx->playlist);
+ playlist_transfer_entries(mpctx->playlist, pl);
+ talloc_free(pl);
+
+ if (!append && mpctx->playlist->first) {
+ struct playlist_entry *e =
+ mp_resume_playlist(mpctx->playlist, opts);
+ mp_set_playlist_entry(mpctx, e ? e : mpctx->playlist->first);
+ }
+ } else {
+ mp_tmsg(MSGT_CPLAYER, MSGL_ERR,
+ "\nUnable to load playlist %s.\n", filename);
+ }
+ break;
+ }
+
+ case MP_CMD_PLAYLIST_CLEAR: {
+ // Supposed to clear the playlist, except the currently played item.
+ if (mpctx->playlist->current_was_replaced)
+ mpctx->playlist->current = NULL;
+ while (mpctx->playlist->first) {
+ struct playlist_entry *e = mpctx->playlist->first;
+ if (e == mpctx->playlist->current) {
+ e = e->next;
+ if (!e)
+ break;
+ }
+ playlist_remove(mpctx->playlist, e);
+ }
+ break;
+ }
+
+ case MP_CMD_PLAYLIST_REMOVE: {
+ struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist,
+ cmd->args[0].v.i);
+ if (e) {
+ // Can't play a removed entry
+ if (mpctx->playlist->current == e)
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+ playlist_remove(mpctx->playlist, e);
+ }
+ break;
+ }
+
+ case MP_CMD_PLAYLIST_MOVE: {
+ 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) {
+ playlist_move(mpctx->playlist, e1, e2);
+ }
+ break;
+ }
+
+ case MP_CMD_STOP:
+ // Go back to the starting point.
+ mpctx->stop_play = PT_STOP;
+ break;
+
+ case MP_CMD_SHOW_PROGRESS:
+ mpctx->add_osd_seek_info |=
+ (msg_osd ? OSD_SEEK_INFO_TEXT : 0) |
+ (bar_osd ? OSD_SEEK_INFO_BAR : 0);
+ break;
+
+#ifdef CONFIG_RADIO
+ case MP_CMD_RADIO_STEP_CHANNEL:
+ if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) {
+ int v = cmd->args[0].v.i;
+ if (v > 0)
+ radio_step_channel(mpctx->stream, RADIO_CHANNEL_HIGHER);
+ else
+ radio_step_channel(mpctx->stream, RADIO_CHANNEL_LOWER);
+ if (radio_get_channel_name(mpctx->stream)) {
+ set_osd_tmsg(mpctx, OSD_MSG_RADIO_CHANNEL, osdl, osd_duration,
+ "Channel: %s",
+ radio_get_channel_name(mpctx->stream));
+ }
+ }
+ break;
+
+ case MP_CMD_RADIO_SET_CHANNEL:
+ if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) {
+ radio_set_channel(mpctx->stream, cmd->args[0].v.s);
+ if (radio_get_channel_name(mpctx->stream)) {
+ set_osd_tmsg(mpctx, OSD_MSG_RADIO_CHANNEL, osdl, osd_duration,
+ "Channel: %s",
+ radio_get_channel_name(mpctx->stream));
+ }
+ }
+ break;
+
+ case MP_CMD_RADIO_SET_FREQ:
+ if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO)
+ radio_set_freq(mpctx->stream, cmd->args[0].v.f);
+ break;
+
+ case MP_CMD_RADIO_STEP_FREQ:
+ if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO)
+ radio_step_freq(mpctx->stream, cmd->args[0].v.f);
+ break;
+#endif
+
+#ifdef CONFIG_TV
+ case MP_CMD_TV_START_SCAN:
+ if (get_tvh(mpctx))
+ tv_start_scan(get_tvh(mpctx), 1);
+ break;
+ case MP_CMD_TV_SET_FREQ:
+ if (get_tvh(mpctx))
+ tv_set_freq(get_tvh(mpctx), cmd->args[0].v.f * 16.0);
+#ifdef CONFIG_PVR
+ else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) {
+ pvr_set_freq(mpctx->stream, ROUND(cmd->args[0].v.f));
+ set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s",
+ pvr_get_current_channelname(mpctx->stream),
+ pvr_get_current_stationname(mpctx->stream));
+ }
+#endif /* CONFIG_PVR */
+ break;
+
+ case MP_CMD_TV_STEP_FREQ:
+ if (get_tvh(mpctx))
+ tv_step_freq(get_tvh(mpctx), cmd->args[0].v.f * 16.0);
+#ifdef CONFIG_PVR
+ else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) {
+ pvr_force_freq_step(mpctx->stream, ROUND(cmd->args[0].v.f));
+ set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: f %d",
+ pvr_get_current_channelname(mpctx->stream),
+ pvr_get_current_frequency(mpctx->stream));
+ }
+#endif /* CONFIG_PVR */
+ break;
+
+ case MP_CMD_TV_SET_NORM:
+ if (get_tvh(mpctx))
+ tv_set_norm(get_tvh(mpctx), cmd->args[0].v.s);
+ break;
+
+ case MP_CMD_TV_STEP_CHANNEL:
+ if (get_tvh(mpctx)) {
+ int v = cmd->args[0].v.i;
+ if (v > 0) {
+ tv_step_channel(get_tvh(mpctx), TV_CHANNEL_HIGHER);
+ } else {
+ tv_step_channel(get_tvh(mpctx), TV_CHANNEL_LOWER);
+ }
+ if (tv_channel_list) {
+ set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration,
+ "Channel: %s", tv_channel_current->name);
+ }
+ }
+#ifdef CONFIG_PVR
+ else if (mpctx->stream &&
+ mpctx->stream->type == STREAMTYPE_PVR) {
+ pvr_set_channel_step(mpctx->stream, cmd->args[0].v.i);
+ set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s",
+ pvr_get_current_channelname(mpctx->stream),
+ pvr_get_current_stationname(mpctx->stream));
+ }
+#endif /* CONFIG_PVR */
+#ifdef CONFIG_DVBIN
+ if (mpctx->stream->type == STREAMTYPE_DVB) {
+ int dir;
+ int v = cmd->args[0].v.i;
+
+ mpctx->last_dvb_step = v;
+ if (v > 0)
+ dir = DVB_CHANNEL_HIGHER;
+ else
+ dir = DVB_CHANNEL_LOWER;
+
+
+ if (dvb_step_channel(mpctx->stream, dir)) {
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->dvbin_reopen = 1;
+ }
+ }
+#endif /* CONFIG_DVBIN */
+ break;
+
+ case MP_CMD_TV_SET_CHANNEL:
+ if (get_tvh(mpctx)) {
+ tv_set_channel(get_tvh(mpctx), cmd->args[0].v.s);
+ if (tv_channel_list) {
+ set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration,
+ "Channel: %s", tv_channel_current->name);
+ }
+ }
+#ifdef CONFIG_PVR
+ else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) {
+ pvr_set_channel(mpctx->stream, cmd->args[0].v.s);
+ set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s",
+ pvr_get_current_channelname(mpctx->stream),
+ pvr_get_current_stationname(mpctx->stream));
+ }
+#endif /* CONFIG_PVR */
+ break;
+
+#ifdef CONFIG_DVBIN
+ case MP_CMD_DVB_SET_CHANNEL:
+ if (mpctx->stream->type == STREAMTYPE_DVB) {
+ mpctx->last_dvb_step = 1;
+
+ if (dvb_set_channel(mpctx->stream, cmd->args[1].v.i,
+ cmd->args[0].v.i)) {
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->dvbin_reopen = 1;
+ }
+ }
+ break;
+#endif /* CONFIG_DVBIN */
+
+ case MP_CMD_TV_LAST_CHANNEL:
+ if (get_tvh(mpctx)) {
+ tv_last_channel(get_tvh(mpctx));
+ if (tv_channel_list) {
+ set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration,
+ "Channel: %s", tv_channel_current->name);
+ }
+ }
+#ifdef CONFIG_PVR
+ else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) {
+ pvr_set_lastchannel(mpctx->stream);
+ set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s",
+ pvr_get_current_channelname(mpctx->stream),
+ pvr_get_current_stationname(mpctx->stream));
+ }
+#endif /* CONFIG_PVR */
+ break;
+
+ case MP_CMD_TV_STEP_NORM:
+ if (get_tvh(mpctx))
+ tv_step_norm(get_tvh(mpctx));
+ break;
+
+ case MP_CMD_TV_STEP_CHANNEL_LIST:
+ if (get_tvh(mpctx))
+ tv_step_chanlist(get_tvh(mpctx));
+ break;
+#endif /* CONFIG_TV */
+
+ case MP_CMD_SUB_ADD:
+ mp_add_subtitles(mpctx, cmd->args[0].v.s);
+ break;
+
+ case MP_CMD_SUB_REMOVE: {
+ struct track *sub = mp_track_by_tid(mpctx, STREAM_SUB, cmd->args[0].v.i);
+ if (sub)
+ mp_remove_track(mpctx, sub);
+ break;
+ }
+
+ case MP_CMD_SUB_RELOAD: {
+ struct track *sub = mp_track_by_tid(mpctx, STREAM_SUB, cmd->args[0].v.i);
+ if (sub && sub->is_external && sub->external_filename) {
+ struct track *nsub = mp_add_subtitles(mpctx, sub->external_filename);
+ if (nsub) {
+ mp_remove_track(mpctx, sub);
+ mp_switch_track(mpctx, nsub->type, nsub);
+ }
+ }
+ break;
+ }
+
+ case MP_CMD_SCREENSHOT:
+ screenshot_request(mpctx, cmd->args[0].v.i, cmd->args[1].v.i, msg_osd);
+ break;
+
+ case MP_CMD_SCREENSHOT_TO_FILE:
+ screenshot_to_file(mpctx, cmd->args[0].v.s, cmd->args[1].v.i, msg_osd);
+ break;
+
+ case MP_CMD_RUN:
+#ifndef __MINGW32__
+ if (!fork()) {
+ execl("/bin/sh", "sh", "-c", cmd->args[0].v.s, NULL);
+ exit(0);
+ }
+#endif
+ break;
+
+ case MP_CMD_KEYDOWN_EVENTS:
+ mp_input_put_key(mpctx->input, cmd->args[0].v.i);
+ break;
+
+ case MP_CMD_ENABLE_INPUT_SECTION:
+ mp_input_enable_section(mpctx->input, cmd->args[0].v.s,
+ cmd->args[1].v.i == 1 ? MP_INPUT_EXCLUSIVE : 0);
+ break;
+
+ case MP_CMD_DISABLE_INPUT_SECTION:
+ mp_input_disable_section(mpctx->input, cmd->args[0].v.s);
+ break;
+
+ case MP_CMD_VO_CMDLINE:
+ if (mpctx->video_out) {
+ char *s = cmd->args[0].v.s;
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "Setting vo cmd line to '%s'.\n",
+ s);
+ if (vo_control(mpctx->video_out, VOCTRL_SET_COMMAND_LINE, s) > 0) {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration, "vo='%s'", s);
+ } else {
+ set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration, "Failed!");
+ }
+ }
+ break;
+
+ case MP_CMD_AF:
+ edit_filters_osd(mpctx, STREAM_AUDIO, cmd->args[0].v.s,
+ cmd->args[1].v.s, msg_osd);
+ break;
+
+ case MP_CMD_VF:
+ edit_filters_osd(mpctx, STREAM_VIDEO, cmd->args[0].v.s,
+ cmd->args[1].v.s, msg_osd);
+ break;
+
+ case MP_CMD_SCRIPT_DISPATCH:
+ if (mpctx->lua_ctx) {
+#ifdef CONFIG_LUA
+ mp_lua_script_dispatch(mpctx, cmd->args[0].v.s, cmd->args[1].v.i,
+ cmd->key_up_follows ? "keyup_follows" : "press");
+#endif
+ }
+ break;
+
+#ifdef HAVE_SYS_MMAN_H
+ case MP_CMD_OVERLAY_ADD:
+ overlay_add(mpctx,
+ cmd->args[0].v.i, cmd->args[1].v.i, cmd->args[2].v.i,
+ cmd->args[3].v.s, cmd->args[4].v.i, cmd->args[5].v.s,
+ cmd->args[6].v.i, cmd->args[7].v.i, cmd->args[8].v.i);
+ break;
+
+ case MP_CMD_OVERLAY_REMOVE:
+ overlay_remove(mpctx, cmd->args[0].v.i);
+ break;
+#endif
+
+ case MP_CMD_COMMAND_LIST: {
+ for (struct mp_cmd *sub = cmd->args[0].v.p; sub; sub = sub->queue_next)
+ run_command(mpctx, sub);
+ break;
+ }
+
+ case MP_CMD_IGNORE:
+ break;
+
+ default:
+ mp_msg(MSGT_CPLAYER, MSGL_V,
+ "Received unknown cmd %s\n", cmd->name);
+ }
+
+ switch (cmd->pausing) {
+ case 1: // "pausing"
+ pause_player(mpctx);
+ break;
+ case 3: // "pausing_toggle"
+ if (opts->pause)
+ unpause_player(mpctx);
+ else
+ pause_player(mpctx);
+ break;
+ }
+}
+
+void command_uninit(struct MPContext *mpctx)
+{
+ overlay_uninit(mpctx);
+ talloc_free(mpctx->command_ctx);
+ mpctx->command_ctx = NULL;
+}
+
+void command_init(struct MPContext *mpctx)
+{
+ mpctx->command_ctx = talloc_zero(NULL, struct command_ctx);
+}
+
+// Notify that a property might have changed.
+void mp_notify_property(struct MPContext *mpctx, const char *property)
+{
+ mp_notify(mpctx, MP_EVENT_PROPERTY, (void *)property);
+}
+
+void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+ ctx->events |= 1u << event;
+}
+
+static void handle_script_event(struct MPContext *mpctx, const char *name,
+ const char *arg)
+{
+#ifdef CONFIG_LUA
+ mp_lua_event(mpctx, name, arg);
+#endif
+}
+
+void mp_flush_events(struct MPContext *mpctx)
+{
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ ctx->events |= (1u << MP_EVENT_TICK);
+
+ for (int n = 0; n < 16; n++) {
+ enum mp_event event = n;
+ unsigned mask = 1 << event;
+ if (ctx->events & mask) {
+ // The event handler could set event flags again; in this case let
+ // the next mp_flush_events() call handle it to avoid infinite loops.
+ ctx->events &= ~mask;
+ const char *name = NULL;
+ switch (event) {
+ case MP_EVENT_TICK: name = "tick"; break;
+ case MP_EVENT_TRACKS_CHANGED: name = "track-layout"; break;
+ case MP_EVENT_START_FILE: name = "start"; break;
+ case MP_EVENT_END_FILE: name = "end"; break;
+ default: ;
+ }
+ if (name)
+ handle_script_event(mpctx, name, "");
+ }
+ }
+}
diff --git a/mpvcore/player/command.h b/mpvcore/player/command.h
new file mode 100644
index 0000000000..d3469fc131
--- /dev/null
+++ b/mpvcore/player/command.h
@@ -0,0 +1,50 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_COMMAND_H
+#define MPLAYER_COMMAND_H
+
+struct MPContext;
+struct mp_cmd;
+
+void command_init(struct MPContext *mpctx);
+void command_uninit(struct MPContext *mpctx);
+
+void run_command(struct MPContext *mpctx, struct mp_cmd *cmd);
+char *mp_property_expand_string(struct MPContext *mpctx, const char *str);
+void property_print_help(void);
+int mp_property_do(const char* name, int action, void* val,
+ struct MPContext *mpctx);
+
+const struct m_option *mp_get_property_list(void);
+
+enum mp_event {
+ MP_EVENT_NONE,
+ MP_EVENT_TICK,
+ MP_EVENT_PROPERTY, // char*, property that is changed
+ MP_EVENT_TRACKS_CHANGED,
+ MP_EVENT_START_FILE,
+ MP_EVENT_END_FILE,
+};
+
+void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg);
+void mp_notify_property(struct MPContext *mpctx, const char *property);
+
+void mp_flush_events(struct MPContext *mpctx);
+
+#endif /* MPLAYER_COMMAND_H */
diff --git a/mpvcore/player/lua/assdraw.lua b/mpvcore/player/lua/assdraw.lua
new file mode 100644
index 0000000000..fc3b727f57
--- /dev/null
+++ b/mpvcore/player/lua/assdraw.lua
@@ -0,0 +1,98 @@
+local ass_mt = {}
+ass_mt.__index = ass_mt
+
+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 = math.pow(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.round_rect_cw(ass, x0, y0, x1, y1, r)
+ ass:move_to(x0 + r, y0)
+ ass:line_to(x1 - r, y0) -- top line
+ if r > 0 then
+ ass:bezier_curve(x1, y0, x1, y0, x1, y0 + r) -- top right corner
+ end
+ ass:line_to(x1, y1 - r) -- right line
+ if r > 0 then
+ ass:bezier_curve(x1, y1, x1, y1, x1 - r, y1) -- bottom right corner
+ end
+ ass:line_to(x0 + r, y1) -- bottom line
+ if r > 0 then
+ ass:bezier_curve(x0, y1, x0, y1, x0, y1 - r) -- bottom left corner
+ end
+ ass:line_to(x0, y0 + r) -- left line
+ if r > 0 then
+ ass:bezier_curve(x0, y0, x0, y0, x0 + r, y0) -- top left corner
+ end
+end
+
+return {ass_new = ass_new}
diff --git a/mpvcore/player/lua/defaults.lua b/mpvcore/player/lua/defaults.lua
new file mode 100644
index 0000000000..d24cda9cbe
--- /dev/null
+++ b/mpvcore/player/lua/defaults.lua
@@ -0,0 +1,82 @@
+
+local callbacks = {}
+-- each script has its own section, so that they don't conflict
+local default_section = "input_" .. 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}
+-- {key, callback, callback_down}
+-- key is the key string as used in input.conf, like "ctrl+a"
+-- callback is a Lua function that is called when the key binding is used.
+-- callback_down can be given too, and is called when a mouse button is pressed
+-- if the key is a mouse button. (The normal callback will be for mouse button
+-- down.)
+--
+-- 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)
+ local cfg = ""
+ for i = 1, #list do
+ local entry = list[i]
+ local key = entry[1]
+ local cb = entry[2]
+ local cb_down = entry[3]
+ if type(cb) == "function" then
+ callbacks[#callbacks + 1] = {press=cb, before_press=cb_down}
+ cfg = cfg .. key .. " script_dispatch " .. mp.script_name
+ .. " " .. #callbacks .. "\n"
+ else
+ cfg = cfg .. key .. " " .. cb .. "\n"
+ end
+ end
+ mp.input_define_section(section or default_section, cfg)
+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
+
+-- called by C on script_dispatch input command
+function mp_script_dispatch(id, event)
+ local cb = callbacks[id]
+ if cb then
+ if event == "press" and cb.press then
+ cb.press()
+ elseif event == "keyup_follows" and cb.before_press then
+ cb.before_press()
+ end
+ end
+end
+
+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("verbose", ...) end,
+ debug = function(...) return mp.log("debug", ...) end,
+}
+
+_G.print = mp.msg.info
+
+package.loaded["mp"] = mp
+package.loaded["mp.msg"] = mp.msg
+
+return {}
diff --git a/mpvcore/player/lua/osc.lua b/mpvcore/player/lua/osc.lua
new file mode 100644
index 0000000000..f105d10a9c
--- /dev/null
+++ b/mpvcore/player/lua/osc.lua
@@ -0,0 +1,1288 @@
+-- osc.lua
+
+local assdraw = require 'mp.assdraw'
+local msg = require 'mp.msg'
+
+--
+-- Parameters
+--
+
+-- default user option values
+-- do not touch, change them in plugin_osc.conf
+local user_opts = {
+ showwindowed = true, -- show OSC when windowed?
+ showfullscreen = true, -- show OSC when fullscreen?
+ scalewindowed = 1, -- scaling of the controller when windowed
+ scalefullscreen = 1, -- scaling of the controller when fullscreen
+ scaleforcedwindow = 2, -- scaling of the controller when rendered on a forced (dummy) 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)
+ 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, negative value disables autohide
+ fadeduration = 200, -- duration of fade out in ms, 0 = no fade
+ deadzonesize = 0, -- size of deadzone
+ minmousemove = 3, -- minimum amount of pixels the mouse has to move between ticks to make the OSC show up
+ seektooltip = false, -- display tooltip over the seekbar indicating time at mouse position
+ iamaprogrammer = false, -- use native mpv values and disable OSC internal playlist management (and some functions that depend on it)
+}
+
+local osc_param = {
+ osc_w = 550, -- width, height, corner-radius, padding of the OSC box
+ osc_h = 138,
+ osc_r = 10,
+ osc_p = 15,
+
+ -- calculated by osc_init()
+ playresy = 0, -- canvas size Y
+ playresx = 0, -- canvas size X
+ posX, posY = 0,0, -- position of the controler
+ pos_offsetX, pos_offsetY = 0,0, -- vertical/horizontal position offset for contents aligned at the borders of the box
+}
+
+local osc_styles = {
+ bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
+ smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20\\fnmpv-osd-symbols}",
+ smallButtonsLlabel = "{\\fs17\\fn" .. mp.property_get("options/osd-font") .. "}",
+ smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
+
+ elementDown = "{\\1c&H999999}",
+ timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
+ vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12}",
+ box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
+}
+
+-- 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 = true, -- if the right timcode should display total or remaining time
+ tc_ms = false, -- 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?
+ last_seek, -- last seek position, to avoid deadlocks by repeatedly seeking to the same position
+ last_mouseX, last_mouseY, -- last mouse position, to detect siginificant mouse movement
+ message_text,
+ message_timeout,
+}
+
+--
+-- User Settings Management
+--
+
+function val2str(val)
+ local strval = val
+ if type(val) == "boolean" then
+ if val then strval = "yes" else strval = "no" end
+ end
+
+ return strval
+end
+
+-- converts val to type of desttypeval
+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 not (tonumber(val) == nil) then
+ val = tonumber(val)
+ else
+ msg.error("Error: Can't convert " .. val .. " to number!")
+ val = nil
+ end
+ end
+ return val
+end
+
+-- Automagical config handling
+-- options: A table with options setable via config with assigned default values. The type of the default values is important for
+-- converting the values read from the config file back. Do not use "nil" as a default value!
+-- identifier: A simple indentifier string for the config file. Make sure this doesn't collide with other scripts.
+
+-- How does it work:
+-- Existance of the configfile will be checked, if it doesn't exist, the default values from the options table will be written in a new
+-- file, commented out. If it exits, the key/value pairs will be read, and values of keys that exist in the options table will overwrite
+-- their value. Keys that don't exist in the options table will be ignored, keys that don't exits in the config will keep their default
+-- value. The value's types will automatically be converted to the type used in the options table.
+function read_config(options, identifier)
+
+ local conffilename = "plugin_" .. identifier .. ".conf"
+ local conffile = mp.find_config_file(conffilename)
+ local f = io.open(conffile,"r")
+ if f == nil then
+ -- config not found
+ else
+ -- config exists, read values
+ local linecounter = 1
+ for line in f:lines() do
+ 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 options[key] == nil then
+ msg.warn(conffilename..":"..linecounter.." unknown key " .. key .. ", ignoring")
+ else
+ local convval = typeconv(options[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
+end
+
+-- read configfile
+read_config(user_opts, "osc")
+
+
+--
+-- Helperfunctions
+--
+
+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_element_hitbox(element)
+ return element.hitbox.x1, element.hitbox.y1, element.hitbox.x2, element.hitbox.y2
+end
+
+function mouse_hit(element)
+ local mX, mY = mp.get_mouse_pos()
+ local bX1, bY1, bX2, bY2 = get_element_hitbox(element)
+
+ 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
+
+function get_slider_value(element)
+ local fill_offsetV = element.metainfo.slider.border + element.metainfo.slider.gap
+ local paddingH = (element.h - (2*fill_offsetV)) / 2
+
+ local b_x1, b_x2 = element.hitbox.x1 + paddingH, element.hitbox.x2 - paddingH
+ local s_min, s_max = element.metainfo.slider.min, element.metainfo.slider.max
+
+ local pos = scale_value(b_x1, b_x2, s_min, s_max, mp.get_mouse_pos())
+
+ return limit_range(s_min, s_max, 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
+
+--
+-- 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_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 not (tracktable[n].type == "unkown") 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: "
+ local select_scale = 100
+ if #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 = "unkown", "", "{\\fscx" .. select_scale .. "\\fscy" .. select_scale .. "}○{\\fscx100\\fscy100}"
+ if not(track.language == nil) then lang = track.language end
+ if not(track.title == nil) then title = track.title end
+ if (track.id == tonumber(mp.property_get(type))) then
+ selected = "{\\fscx" .. select_scale .. "\\fscy" .. select_scale .. "}●{\\fscx100\\fscy100}"
+ 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.property_get(type) == "no") then
+ current_track_osc = 0
+ else
+ current_track_mpv = tonumber(mp.property_get(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.send_command("no-osd 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].language or "unkown") .. "] " .. (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.property_get(type)
+ if (track == "no" or track == nil) then
+ return 0
+ else
+ return tracks_mpv[type][tonumber(track)].osc_id
+ end
+end
+
+
+--
+-- Element Management
+--
+
+-- do not use this function, use the wrappers below
+function register_element(type, x, y, an, w, h, style, content, eventresponder, metainfo2)
+ -- type button, slider or box
+ -- x, y position
+ -- an alignment (see ASS standard)
+ -- w, h size of hitbox
+ -- style main style
+ -- content what the element should display, can be a string or a function(ass)
+ -- eventresponder A table containing functions mapped to events that shall be run on those events
+ -- metainfo A table containing additional parameters for the element
+
+ -- set default metainfo
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+ if metainfo.visible == nil then metainfo.visible = true end -- element visible at all?
+ if metainfo.enabled == nil then metainfo.enabled = true end -- element clickable?
+ if metainfo.styledown == nil then metainfo.styledown = true end -- should the element be styled with the elementDown style when clicked?
+ if metainfo.softrepeat == nil then metainfo.softrepeat = false end -- should the *_down event be executed with "hold for repeat" behaviour?
+ if metainfo.alpha1 == nil then metainfo.alpha1 = 0 end -- alpha1 of the element, 0 = opaque, 255 = transparent (primary fill alpha)
+ if metainfo.alpha2 == nil then metainfo.alpha2 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (secondary fill alpha)
+ if metainfo.alpha3 == nil then metainfo.alpha3 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (border alpha)
+ if metainfo.alpha4 == nil then metainfo.alpha4 = 255 end -- alpha1 of the element, 0 = opaque, 255 = transparent (shadow alpha)
+
+ if metainfo.visible then
+ local ass = assdraw.ass_new()
+
+ ass:append("{}") -- shitty hack to troll the new_event function into inserting a \n
+ ass:new_event()
+ ass:pos(x, y) -- positioning
+ ass:an(an)
+ ass:append(style) -- styling
+
+ -- if the element is supposed to be disabled, style it accordingly and kill the eventresponders
+ if metainfo.enabled == false then
+ metainfo.alpha1 = 136
+ eventresponder = nil
+ end
+
+ -- Calculate the hitbox
+ local bX1, bY1, bX2, bY2 = get_hitbox_coords(x, y, an, w, h)
+ local hitbox
+ if type == "slider" then
+ -- if it's a slider, cut the border and gap off, as those aren't of interest for eventhandling
+ local fill_offset = metainfo.slider.border + metainfo.slider.gap
+ hitbox = {x1 = bX1 + fill_offset, y1 = bY1 + fill_offset, x2 = bX2 - fill_offset, y2 = bY2 - fill_offset}
+ else
+ hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
+ end
+
+ local element = {
+ type = type,
+ elem_ass = ass,
+ hitbox = hitbox,
+ w = w,
+ h = h,
+ x = x,
+ y = y,
+ content = content,
+ eventresponder = eventresponder,
+ metainfo = metainfo,
+ }
+
+ table.insert(elements, element)
+ end
+end
+
+function register_button(x, y, an, w, h, style, content, eventresponder, metainfo)
+ register_element("button", x, y, an, w, h, style, content, eventresponder, metainfo)
+end
+
+function register_box(x, y, an, w, h, r, style, metainfo2)
+ local ass = assdraw.ass_new()
+ ass:draw_start()
+ ass:round_rect_cw(0, 0, w, h, r)
+ ass:draw_stop()
+
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+
+ metainfo.styledown = false
+
+ register_element("box", x, y, an, w, h, style, ass, nil, metainfo)
+end
+
+function register_slider(x, y, an, w, h, style, min, max, markerF, posF, eventresponder, metainfo2)
+ local metainfo = {}
+ if not (metainfo2 == nil) then metainfo = metainfo2 end
+ local slider1 = {}
+ if (metainfo.slider == nil) then metainfo.slider = slider1 end
+
+ -- defaults
+ if min == nil then metainfo.slider.min = 0 else metainfo.slider.min = min end
+ if max == nil then metainfo.slider.max = 100 else metainfo.slider.max = max end
+ if metainfo.slider.border == nil then metainfo.slider.border = 1 end
+ if metainfo.slider.gap == nil then metainfo.slider.gap = 2 end
+ if metainfo.slider.type == nil then metainfo.slider.type = "slider" end
+
+ metainfo.slider.markerF = markerF
+ metainfo.slider.posF = posF
+
+ -- prepare the box with markers
+ local ass = assdraw.ass_new()
+ local border, gap = metainfo.slider.border, metainfo.slider.gap
+ local fill_offsetV = border + gap -- Vertical offset between element outline and drag-area
+ local fill_offsetH = h / 2 -- Horizontal offset between element outline and drag-area
+
+ ass:draw_start()
+
+ -- the box
+ ass:rect_cw(0, 0, w, h);
+
+ -- the "hole"
+ ass:rect_ccw(border, border, w - border, h - border)
+
+ -- marker nibbles
+ if not (markerF == nil) and gap > 0 then
+ local markers = markerF()
+ for n = 1, #markers do
+ if (markers[n] > min) and (markers[n] < max) then
+
+ local coordL, coordR = fill_offsetH, (w - fill_offsetH)
+
+ local s = scale_value(min, max, coordL, coordR, markers[n])
+
+ if gap > 1 then
+ -- draw triangles
+ local a = gap / 0.5 --0.866
+ --top
+ ass:move_to(s - (a/2), border)
+ ass:line_to(s + (a/2), border)
+ ass:line_to(s, border + gap)
+
+ --bottom
+ ass:move_to(s - (a/2), h - border)
+ ass:line_to(s, h - border - gap)
+ ass:line_to(s + (a/2), h - border)
+
+ else
+ -- draw 1px nibbles
+ ass:rect_cw(s - 0.5, border, s + 0.5, border*2);
+ ass:rect_cw(s - 0.5, h - border*2, s + 0.5, h - border);
+ end
+
+ end
+ end
+ end
+
+ register_element("slider", x, y, an, w, h, style, ass, eventresponder, metainfo)
+end
+
+--
+-- Element Rendering
+--
+
+function render_elements(master_ass)
+
+ for n = 1, #elements do
+
+ local element = elements[n]
+ local elem_ass = assdraw.ass_new()
+ local elem_ass1 = element.elem_ass
+ elem_ass:merge(elem_ass1)
+
+ --alpha
+ local alpha1 = element.metainfo.alpha1
+ local alpha2 = element.metainfo.alpha2
+ local alpha3 = element.metainfo.alpha3
+ local alpha4 = element.metainfo.alpha4
+
+ if not(state.animation == nil) then
+ alpha1 = mult_alpha(element.metainfo.alpha1, state.animation)
+ alpha2 = mult_alpha(element.metainfo.alpha2, state.animation)
+ alpha3 = mult_alpha(element.metainfo.alpha3, state.animation)
+ alpha4 = mult_alpha(element.metainfo.alpha4, state.animation)
+ end
+
+ elem_ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}", alpha1, alpha2, alpha3, alpha4))
+
+
+ if state.active_element == n then
+
+ -- run render event functions
+ if not (element.eventresponder.render == nil) then
+ element.eventresponder.render(element)
+ end
+
+ if mouse_hit(element) then
+ -- mouse down styling
+ if element.metainfo.styledown then
+ elem_ass:append(osc_styles.elementDown)
+ end
+
+ if (element.metainfo.softrepeat == true) 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
+
+ if element.type == "slider" then
+
+ elem_ass:merge(element.content) -- ASS objects
+
+ -- draw pos marker
+ local pos = element.metainfo.slider.posF()
+
+ if not (pos == nil) then
+
+ pos = limit_range(element.metainfo.slider.min, element.metainfo.slider.max, pos)
+
+ local fill_offsetV = element.metainfo.slider.border + element.metainfo.slider.gap
+ local fill_offsetH = element.h/2
+
+ local coordL, coordR = fill_offsetH, (element.w - fill_offsetH)
+
+ local xp = scale_value(element.metainfo.slider.min, element.metainfo.slider.max, coordL, coordR, pos)
+
+ -- the filling, draw it only if positive
+ local innerH = element.h - (2*fill_offsetV)
+
+ if element.metainfo.slider.type == "bar" then
+ elem_ass:rect_cw(fill_offsetV, fill_offsetV, xp, element.h - fill_offsetV)
+ else
+ elem_ass:move_to(xp, fill_offsetV)
+ elem_ass:line_to(xp+(innerH/2), (innerH/2)+fill_offsetV)
+ elem_ass:line_to(xp, (innerH)+fill_offsetV)
+ elem_ass:line_to(xp-(innerH/2), (innerH/2)+fill_offsetV)
+ end
+ end
+
+ elem_ass:draw_stop()
+
+ -- add tooltip
+ if not (element.metainfo.slider.tooltipF == nil) then
+
+ if mouse_hit(element) then
+ local sliderpos = get_slider_value(element)
+ local tooltiplabel = element.metainfo.slider.tooltipF(sliderpos)
+ local s_min, s_max = element.metainfo.slider.min, element.metainfo.slider.max
+
+ local an = 2
+ if (sliderpos < (s_min + 10)) then
+ an = 1
+ elseif (sliderpos > (s_max - 10)) then
+ an = 3
+ end
+
+ elem_ass:new_event()
+ elem_ass:pos(mp.get_mouse_pos(), element.y - (element.h) - 0) -- positioning
+ elem_ass:an(an)
+ elem_ass:append(osc_styles.vidtitle) -- styling
+ elem_ass:append(tooltiplabel)
+
+ end
+ end
+
+
+
+ elseif element.type == "box" then
+ elem_ass:merge(element.content) -- ASS objects
+ elseif type(element.content) == "function" then
+ element.content(elem_ass) -- function objects
+ else
+ elem_ass:append(element.content) -- text objects
+ end
+
+ master_ass:merge(elem_ass)
+ end
+end
+
+--
+-- Message display
+--
+
+function show_message(text, duration)
+
+ if duration == nil then
+ duration = tonumber(mp.property_get("options/osd-duration")) / 1000
+ 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 and get the amount of lines along the way
+ local lines
+ text, lines = string.gsub(text, "\n", "\\N")
+
+ -- append a Zero-Width-Space to . and _ to enable linebreaking of long filenames
+ text = string.gsub(text, "%.", ".\226\128\139")
+ text = string.gsub(text, "_", "_\226\128\139")
+
+ -- scale the fontsize for longer multi-line output
+ local fontsize, outline = tonumber(mp.property_get("options/osd-font-size")), tonumber(mp.property_get("options/osd-border-size"))
+ if lines > 12 then
+ fontsize, outline = fontsize / 2, outline / 1.5
+ elseif lines > 8 then
+ fontsize, outline = fontsize / 1.5, outline / 1.25
+ end
+
+ local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
+
+ state.message_text = style .. text
+ state.message_timeout = mp.get_timer() + duration
+end
+
+function render_message(ass)
+ if not(state.message_timeout == nil) and not(state.message_text == nil) and state.message_timeout > mp.get_timer() then
+ ass:new_event()
+ ass:append(state.message_text)
+ else
+ state.message_text = nil
+ state.message_timeout = nil
+ end
+end
+
+--
+-- Initialisation and Layout
+--
+
+-- OSC INIT
+function osc_init()
+ -- kill old Elements
+ elements = {}
+
+ -- set canvas resolution according to display aspect and scaling setting
+ local baseResY = 720
+ local display_w, display_h, display_aspect = mp.get_screen_size()
+ local scale = 1
+
+ if (mp.property_get("video") == "no") then -- dummy/forced window
+ scale = user_opts.scaleforcedwindow
+ elseif (mp.property_get("fullscreen") == "yes") then
+ scale = user_opts.scalefullscreen
+ else
+ scale = user_opts.scalewindowed
+ end
+
+
+ if user_opts.vidscale then
+ osc_param.playresy = baseResY / scale
+ else
+ osc_param.playresy = display_h / scale
+ end
+ osc_param.playresx = osc_param.playresy * display_aspect
+
+ -- make sure the OSC actually fits into the video
+ if (osc_param.playresx < (osc_param.osc_w + (2 * osc_param.osc_p))) then
+ osc_param.playresy = (osc_param.osc_w + (2 * osc_param.osc_p)) / display_aspect
+ osc_param.playresx = osc_param.playresy * display_aspect
+ end
+
+ -- position of the controller according to video aspect and valignment
+ osc_param.posX = math.floor(get_align(user_opts.halign, osc_param.playresx, osc_param.osc_w, 0))
+ osc_param.posY = math.floor(get_align(user_opts.valign, osc_param.playresy, osc_param.osc_h, 0))
+
+ -- Some calculations on stuff we'll need
+ -- vertical/horizontal position offset for contents aligned at the borders of the box
+ osc_param.pos_offsetX, osc_param.pos_offsetY = (osc_param.osc_w - (2*osc_param.osc_p)) / 2, (osc_param.osc_h - (2*osc_param.osc_p)) / 2
+
+ -- fetch values
+ local osc_w, osc_h, osc_r, osc_p = osc_param.osc_w, osc_param.osc_h, osc_param.osc_r, osc_param.osc_p
+ local pos_offsetX, pos_offsetY = osc_param.pos_offsetX, osc_param.pos_offsetY
+ local posX, posY = osc_param.posX, osc_param.posY
+
+ --
+ -- Backround box
+ --
+
+ local metainfo = {}
+ metainfo.alpha1 = user_opts.boxalpha
+ metainfo.alpha3 = user_opts.boxalpha
+ register_box(posX, posY, 5, osc_w, osc_h, osc_r, osc_styles.box, metainfo)
+
+ --
+ -- Title row
+ --
+
+ local titlerowY = posY - pos_offsetY - 10
+
+ -- title
+ local contentF = function (ass)
+ local title = mp.property_get_string("media-title")
+ if not (title == nil) then
+
+ if #title > 80 then
+ title = string.format("{\\fscx%f}", (80 / #title) * 100) .. title
+ end
+
+ ass:append(title)
+ else
+ ass:append("mpv")
+ end
+ end
+
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function ()
+
+ local title = mp.property_get("media-title")
+ local pl_count = tonumber(mp.property_get("playlist-count"))
+
+ if pl_count > 1 then
+ local playlist_pos = countone(tonumber(mp.property_get("playlist-pos")))
+ title = "[" .. playlist_pos .. "/" .. pl_count .. "] " .. title
+ end
+
+ show_message(title)
+ end
+ eventresponder.mouse_btn2_up = function () show_message(mp.property_get("filename")) end
+
+ register_button(posX, titlerowY, 8, 496, 12, osc_styles.vidtitle, contentF, eventresponder, nil)
+
+ -- If we have more than one playlist entry, render playlist navigation buttons
+ local metainfo = {}
+ metainfo.visible = (tonumber(mp.property_get("playlist-count")) > 1)
+
+ -- playlist prev
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("playlist_prev weak") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("playlist"), 3) end
+ register_button(posX - pos_offsetX, titlerowY, 7, 12, 12, osc_styles.vidtitle, "◀", eventresponder, metainfo)
+
+ -- playlist next
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("playlist_next weak") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("playlist"), 3) end
+ register_button(posX + pos_offsetX, titlerowY, 9, 12, 12, osc_styles.vidtitle, "▶", eventresponder, metainfo)
+
+ --
+ -- Big buttons
+ --
+
+ local bigbuttonrowY = posY - pos_offsetY + 35
+ local bigbuttondistance = 60
+
+ --play/pause
+ local contentF = function (ass)
+ if mp.property_get("pause") == "yes" then
+ ass:append("\238\132\129")
+ else
+ ass:append("\238\128\130")
+ end
+ end
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("no-osd cycle pause") end
+ register_button(posX, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, contentF, eventresponder, nil)
+
+ --skipback
+ local metainfo = {}
+ metainfo.softrepeat = true
+
+ local eventresponder = {}
+ eventresponder.mouse_btn0_down = function () mp.send_command("no-osd seek -5 relative keyframes") end
+ eventresponder["shift+mouse_btn0_down"] = function () mp.send_command("no-osd frame_back_step") end
+ eventresponder.mouse_btn2_down = function () mp.send_command("no-osd seek -30 relative keyframes") end
+ register_button(posX - bigbuttondistance, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\128\132", eventresponder, metainfo)
+
+ --skipfrwd
+ local eventresponder = {}
+ eventresponder.mouse_btn0_down = function () mp.send_command("no-osd seek 10 relative keyframes") end
+ eventresponder["shift+mouse_btn0_down"] = function () mp.send_command("no-osd frame_step") end
+ eventresponder.mouse_btn2_down = function () mp.send_command("no-osd seek 60 relative keyframes") end
+ register_button(posX + bigbuttondistance, bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\128\133", eventresponder, metainfo)
+
+ --chapters
+ -- do we have any?
+ local metainfo = {}
+ metainfo.enabled = ((#mp.get_chapter_list()) > 0)
+
+ --prev
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add chapter -1") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("chapter-list"), 3) end
+ register_button(posX - (bigbuttondistance * 2), bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\132\132", eventresponder, metainfo)
+
+ --next
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add chapter 1") end
+ eventresponder["shift+mouse_btn0_up"] = function () show_message(mp.property_get("chapter-list"), 3) end
+ register_button(posX + (bigbuttondistance * 2), bigbuttonrowY, 5, 40, 40, osc_styles.bigButtons, "\238\132\133", eventresponder, metainfo)
+
+
+ --
+ -- Smaller buttons
+ --
+
+ if not (user_opts.iamaprogrammer) then
+ update_tracklist()
+ end
+
+ --cycle audio tracks
+
+ local metainfo = {}
+ local eventresponder = {}
+ local contentF
+
+ if not (user_opts.iamaprogrammer) then
+ metainfo.enabled = (#tracks_osc.audio > 0)
+
+ contentF = function (ass)
+ local aid = "–"
+ if not (get_track("audio") == 0) then
+ aid = get_track("audio")
+ end
+ ass:append("\238\132\134" .. osc_styles.smallButtonsLlabel .. " " .. aid .. "/" .. #tracks_osc.audio)
+ end
+
+ eventresponder.mouse_btn0_up = function () set_track("audio", 1) end
+ eventresponder.mouse_btn2_up = function () set_track("audio", -1) end
+ eventresponder["shift+mouse_btn0_down"] = function ()
+ show_message(get_tracklist("audio"), 2)
+ end
+ else
+ metainfo.enabled = true
+ contentF = function (ass)
+ local aid = mp.property_get("audio")
+
+ ass:append("\238\132\134" .. osc_styles.smallButtonsLlabel .. " " .. aid)
+ end
+
+ eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add audio 1") end
+ eventresponder.mouse_btn2_up = function () mp.send_command("osd-msg add audio -1") end
+ end
+
+ register_button(posX - pos_offsetX, bigbuttonrowY, 1, 70, 18, osc_styles.smallButtonsL, contentF, eventresponder, metainfo)
+
+
+ --cycle sub tracks
+
+ local metainfo = {}
+ local eventresponder = {}
+ local contentF
+
+ if not (user_opts.iamaprogrammer) then
+ metainfo.enabled = (#tracks_osc.sub > 0)
+
+ contentF = function (ass)
+ local sid = "–"
+ if not (get_track("sub") == 0) then
+ sid = get_track("sub")
+ end
+ ass:append("\238\132\135" .. osc_styles.smallButtonsLlabel .. " " .. sid .. "/" .. #tracks_osc.sub)
+ end
+
+ eventresponder.mouse_btn0_up = function () set_track("sub", 1) end
+ eventresponder.mouse_btn2_up = function () set_track("sub", -1) end
+ eventresponder["shift+mouse_btn0_down"] = function ()
+ show_message(get_tracklist("sub"), 2)
+ end
+ else
+ metainfo.enabled = true
+ contentF = function (ass)
+ local sid = mp.property_get("sub")
+
+ ass:append("\238\132\135" .. osc_styles.smallButtonsLlabel .. " " .. sid)
+ end
+
+ eventresponder.mouse_btn0_up = function () mp.send_command("osd-msg add sub 1") end
+ eventresponder.mouse_btn2_up = function () mp.send_command("osd-msg add sub -1") end
+ end
+ register_button(posX - pos_offsetX, bigbuttonrowY, 7, 70, 18, osc_styles.smallButtonsL, contentF, eventresponder, metainfo)
+
+
+ --toggle FS
+ local contentF = function (ass)
+ if mp.property_get("fullscreen") == "yes" then
+ ass:append("\238\132\137")
+ else
+ ass:append("\238\132\136")
+ end
+ end
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () mp.send_command("no-osd cycle fullscreen") end
+ register_button(posX+pos_offsetX, bigbuttonrowY, 6, 25, 25, osc_styles.smallButtonsR, contentF, eventresponder, nil)
+
+
+ --
+ -- Seekbar
+ --
+
+ local markerF = function ()
+ local duration = 0
+ if not (mp.property_get("length") == nil) then
+ duration = tonumber(mp.property_get("length"))
+ end
+
+ local chapters = mp.get_chapter_list()
+ local markers = {}
+ for n = 1, #chapters do
+ markers[n] = (chapters[n].time / duration * 100)
+ end
+ return markers
+ end
+
+ local posF = function ()
+ if mp.property_get("length") == nil then
+ return nil
+ else
+ return tonumber(mp.property_get("percent-pos"))
+ end
+ end
+
+ local tooltipF = function (pos)
+ if not (mp.property_get("length") == nil) then
+ duration = tonumber(mp.property_get("length"))
+ possec = duration * (pos / 100)
+ return mp.format_time(possec)
+ else
+ return nil
+ end
+ end
+
+ local metainfo = {}
+
+
+ metainfo.enabled = (not (mp.property_get("length") == nil)) and (tonumber(mp.property_get("length")) > 0)
+ metainfo.styledown = false
+ metainfo.slider = {}
+ metainfo.slider.border = 1
+ metainfo.slider.gap = 1 -- >1 will draw triangle markers
+ metainfo.slider.type = "slider" -- "bar" for old bar-style filling
+ if (user_opts.seektooltip) and (not (mp.property_get("length") == nil)) then
+ metainfo.slider.tooltipF = tooltipF
+ end
+
+ local eventresponder = {}
+ local sliderF = function (element)
+ local seek_to = get_slider_value(element)
+ -- ignore identical seeks
+ if not(state.last_seek == seek_to) then
+ mp.send_command(string.format("no-osd seek %f absolute-percent keyframes", seek_to))
+ state.last_seek = seek_to
+ end
+ end
+ eventresponder.render = sliderF
+ eventresponder.mouse_btn0_down = sliderF
+ register_slider(posX, posY+pos_offsetY-22, 2, pos_offsetX*2, 15, osc_styles.timecodes, 0, 100, markerF, posF, eventresponder, metainfo)
+
+ --
+ -- Timecodes + Volume
+ --
+
+ local bottomrowY = posY + pos_offsetY - 5
+
+ -- left (current pos)
+ local metainfo = {}
+ local eventresponder = {}
+
+ local contentF = function (ass)
+ if state.tc_ms then
+ ass:append(mp.property_get_string("time-pos/full"))
+ else
+ ass:append(mp.property_get_string("time-pos"))
+ end
+ end
+
+ eventresponder.mouse_btn0_up = function () state.tc_ms = not state.tc_ms end
+ register_button(posX - pos_offsetX, bottomrowY, 4, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo)
+
+ -- center (Cache)
+ local metainfo = {}
+ local eventresponder = {}
+
+ local contentF = function (ass)
+ local cache = mp.property_get("cache")
+ if not (cache == nil) then
+ cache = tonumber(mp.property_get("cache"))
+ if (cache < 45) then
+ ass:append("Cache: " .. (cache) .."%")
+ end
+ end
+ end
+ register_button(posX, bottomrowY, 5, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo)
+
+
+ -- right (total/remaining time)
+ -- do we have a usuable duration?
+ local metainfo = {}
+ metainfo.visible = (not (mp.property_get("length") == nil)) and (tonumber(mp.property_get("length")) > 0)
+
+ local contentF = function (ass)
+ if state.rightTC_trem == true then
+ if state.tc_ms then
+ ass:append("-" .. mp.property_get_string("time-remaining/full"))
+ else
+ ass:append("-" .. mp.property_get_string("time-remaining"))
+ end
+ else
+ if state.tc_ms then
+ ass:append(mp.property_get_string("length/full"))
+ else
+ ass:append(mp.property_get_string("length"))
+ end
+ end
+ end
+ local eventresponder = {}
+ eventresponder.mouse_btn0_up = function () state.rightTC_trem = not state.rightTC_trem end
+
+ register_button(posX + pos_offsetX, bottomrowY, 6, 110, 18, osc_styles.timecodes, contentF, eventresponder, metainfo)
+
+end
+
+--
+-- Other important stuff
+--
+
+
+function show_osc()
+
+ --remember last time of invocation (mouse move)
+ state.showtime = mp.get_timer()
+
+ state.osc_visible = true
+
+ if (user_opts.fadeduration > 0) then
+ state.anitype = nil
+ end
+
+end
+
+function hide_osc()
+ if (user_opts.fadeduration > 0) then
+ if not(state.osc_visible == false) then
+ state.anitype = "out"
+ end
+ else
+ state.osc_visible = false
+ end
+end
+
+function mouse_leave()
+ hide_osc()
+ -- reset mouse position
+ state.last_mouseX, state.last_mouseY = nil, nil
+end
+
+function request_init()
+ state.initREQ = true
+end
+
+function render()
+ local current_screen_sizeX, current_screen_sizeY = mp.get_screen_size()
+ local mouseX, mouseY = mp.get_mouse_pos()
+ local now = mp.get_timer()
+
+ -- check if display changed, if so request reinit
+ if not (state.mp_screen_sizeX == current_screen_sizeX and state.mp_screen_sizeY == current_screen_sizeY) then
+ request_init()
+ state.mp_screen_sizeX, state.mp_screen_sizeY = current_screen_sizeX, current_screen_sizeY
+ end
+
+ -- init management
+ if 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
+
+ -- autohide
+ if not (state.showtime == nil) and (user_opts.hidetimeout >= 0) and (state.showtime + (user_opts.hidetimeout/1000) < now) and (state.active_element == nil)
+ and not (mouseX >= osc_param.posX - (osc_param.osc_w / 2) and mouseX <= osc_param.posX + (osc_param.osc_w / 2)
+ and mouseY >= osc_param.posY - (osc_param.osc_h / 2) and mouseY <= osc_param.posY + (osc_param.osc_h / 2)) then
+ hide_osc()
+ end
+
+ -- fade animation
+ if not(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
+ state.osc_visible = true
+ state.animation = scale_value(state.anistart, (state.anistart + (user_opts.fadeduration/1000)), 255, 0, now)
+ elseif (state.anitype == "out") then --fade in
+ state.animation = scale_value(state.anistart, (state.anistart + (user_opts.fadeduration/1000)), 0, 255, now)
+ end
+
+ else
+ if (state.anitype == "out") then state.osc_visible = false end
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+ end
+ else
+ state.anistart = nil
+ state.animation = nil
+ state.anitype = nil
+ end
+
+ -- actual rendering
+ local ass = assdraw.ass_new()
+
+ -- Messages
+ render_message(ass)
+
+ -- actual OSC
+ if state.osc_visible then
+ render_elements(ass)
+ end
+
+ -- submit
+ local w, h, aspect = mp.get_screen_size()
+ mp.set_osd_ass(osc_param.playresy * aspect, osc_param.playresy, ass.text)
+
+ -- set mouse area
+ local area_y0, area_y1
+ if user_opts.valign > 0 then
+ -- deadzone above OSC
+ area_y0 = get_align(-1 + (2*user_opts.deadzonesize), osc_param.posY - (osc_param.osc_h / 2), 0, 0)
+ area_y1 = osc_param.playresy
+ else
+ -- deadzone below OSC
+ area_y0 = 0
+ area_y1 = (osc_param.posY + (osc_param.osc_h / 2))
+ + get_align(1 - (2*user_opts.deadzonesize), osc_param.playresy - (osc_param.posY + (osc_param.osc_h / 2)), 0, 0)
+ end
+
+ --mouse show/hide area
+ mp.set_mouse_area(0, area_y0, osc_param.playresx, area_y1, "showhide")
+
+ --mouse input area
+ if state.osc_visible then -- activate only when OSC is actually visible
+ mp.set_mouse_area(
+ osc_param.posX - (osc_param.osc_w / 2), osc_param.posY - (osc_param.osc_h / 2),
+ osc_param.posX + (osc_param.osc_w / 2), osc_param.posY + (osc_param.osc_h / 2),
+ "input")
+ mp.enable_key_bindings("input")
+ else
+ mp.disable_key_bindings("input")
+ end
+
+end
+
+--
+-- Eventhandling
+--
+
+function process_event(source, what)
+
+ if what == "down" then
+
+ for n = 1, #elements do
+
+ if not (elements[n].eventresponder == nil) then
+ if not (elements[n].eventresponder[source .. "_up"] == nil) or not (elements[n].eventresponder[source .. "_down"] == nil) then
+
+ if mouse_hit(elements[n]) then
+ state.active_element = n
+ state.active_event_source = source
+ -- fire the down event if the element has one
+ if not (elements[n].eventresponder[source .. "_" .. what] == nil) then
+ elements[n].eventresponder[source .. "_" .. what](elements[n])
+ end
+ end
+ end
+
+ end
+ end
+
+ elseif what == "up" then
+
+ if not (state.active_element == nil) then
+
+ local n = state.active_element
+
+ if n == 0 then
+ --click on background (does not work)
+ elseif n > 0 and not (elements[n].eventresponder[source .. "_" .. what] == nil) then
+
+ if mouse_hit(elements[n]) then
+ elements[n].eventresponder[source .. "_" .. what](elements[n])
+ end
+ end
+ end
+ state.active_element = nil
+ state.mouse_down_counter = 0
+ state.last_seek = nil
+
+ elseif source == "mouse_move" then
+ local mouseX, mouseY = mp.get_mouse_pos()
+ if (user_opts.minmousemove == 0) or
+ (not ((state.last_mouseX == nil) or (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
+
+ if not (state.active_element == nil) then
+
+ local n = state.active_element
+
+ if not (elements[n].eventresponder == nil) then
+ if not (elements[n].eventresponder[source] == nil) then
+ elements[n].eventresponder[source](elements[n])
+ end
+ end
+ end
+ end
+end
+
+-- called by mpv on every frame
+function tick()
+ if (mp.property_get("fullscreen") == "yes" and user_opts.showfullscreen) or (mp.property_get("fullscreen") == "no" and user_opts.showwindowed) then
+ render()
+ else
+ mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
+ end
+end
+
+function mp_event(name, arg)
+ if name == "tick" then
+ tick()
+ elseif name == "start" or name == "track-layout" then
+ request_init()
+ elseif name == "end" then
+ end
+end
+
+-- mouse show/hide bindings
+mp.set_key_bindings({
+ {"mouse_move", function(e) process_event("mouse_move", nil) end},
+ {"mouse_leave", mouse_leave},
+}, "showhide")
+mp.enable_key_bindings("showhide", "allow-vo-dragging|allow-hide-cursor")
+
+--mouse input bindings
+mp.set_key_bindings({
+ {"mouse_btn0", function(e) process_event("mouse_btn0", "up") end,
+ function(e) process_event("mouse_btn0", "down") end},
+ {"shift+mouse_btn0", function(e) process_event("shift+mouse_btn0", "up") end,
+ function(e) process_event("shift+mouse_btn0", "down") end},
+ {"mouse_btn2", function(e) process_event("mouse_btn2", "up") end,
+ function(e) process_event("mouse_btn2", "down") end},
+ {"mouse_btn0_dbl", "ignore"},
+ {"shift+mouse_btn0_dbl", "ignore"},
+ {"mouse_btn2_dbl", "ignore"},
+}, "input")
+mp.enable_key_bindings("input")
diff --git a/mpvcore/player/mp_core.h b/mpvcore/player/mp_core.h
new file mode 100644
index 0000000000..6d83be182a
--- /dev/null
+++ b/mpvcore/player/mp_core.h
@@ -0,0 +1,356 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_MP_CORE_H
+#define MPLAYER_MP_CORE_H
+
+#include <stdbool.h>
+
+#include "mpvcore/options.h"
+#include "demux/demux.h"
+
+// definitions used internally by the core player code
+
+#define INITIALIZED_VO 1
+#define INITIALIZED_AO 2
+#define INITIALIZED_GETCH2 8
+#define INITIALIZED_PLAYBACK 16
+#define INITIALIZED_LIBASS 32
+#define INITIALIZED_STREAM 64
+#define INITIALIZED_DEMUXER 512
+#define INITIALIZED_ACODEC 1024
+#define INITIALIZED_VCODEC 2048
+#define INITIALIZED_SUB 4096
+#define INITIALIZED_ALL 0xFFFF
+
+
+enum stop_play_reason {
+ KEEP_PLAYING = 0, // 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, clear playlist
+ PT_RESTART, // restart previous file
+ PT_QUIT, // stop playback, quit player
+};
+
+enum exit_reason {
+ EXIT_NONE,
+ EXIT_QUIT,
+ EXIT_PLAYED,
+ EXIT_ERROR,
+ EXIT_NOTPLAYED,
+ EXIT_SOMENOTPLAYED
+};
+
+struct timeline_part {
+ double start;
+ double source_start;
+ struct demuxer *source;
+};
+
+struct chapter {
+ double start;
+ char *name;
+};
+
+enum mp_osd_seek_info {
+ OSD_SEEK_INFO_BAR = 1,
+ OSD_SEEK_INFO_TEXT = 2,
+ OSD_SEEK_INFO_CHAPTER_TEXT = 4,
+ OSD_SEEK_INFO_EDITION = 8,
+};
+
+enum seek_type {
+ MPSEEK_NONE = 0,
+ MPSEEK_RELATIVE,
+ MPSEEK_ABSOLUTE,
+ MPSEEK_FACTOR,
+};
+
+struct track {
+ enum stream_type type;
+ // 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;
+
+ // Same as stream->demuxer_id. -1 if not set.
+ int demuxer_id;
+
+ char *title;
+ bool default_track;
+ bool attached_picture;
+ char *lang;
+
+ // If this track is from an external file (e.g. subtitle file).
+ bool is_external;
+ char *external_filename;
+ bool auto_loaded;
+
+ // If the track's stream changes with the timeline (ordered chapters).
+ bool under_timeline;
+
+ // Value can change if under_timeline==true.
+ struct demuxer *demuxer;
+ // Invariant: !stream || stream->demuxer == demuxer
+ struct sh_stream *stream;
+
+ // For external subtitles, which are read fully on init. Do not attempt
+ // to read packets from them.
+ bool preloaded;
+};
+
+enum {
+ MAX_NUM_VO_PTS = 100,
+};
+
+typedef struct MPContext {
+ struct mpv_global *global;
+ struct MPOpts *opts;
+ struct mp_log *log;
+ struct m_config *mconfig;
+ struct input_ctx *input;
+ struct osd_state *osd;
+ struct mp_osd_msg *osd_msg_stack;
+ char *terminal_osd_text;
+
+ 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_last_update;
+
+ struct playlist *playlist;
+ char *filename; // currently playing file
+ struct mp_resolve_result *resolve_result;
+ enum stop_play_reason stop_play;
+ unsigned int initialized_flags; // which subsystems have been initialized
+
+ // Return code to use with PT_QUIT
+ enum exit_reason quit_player_rc;
+ int quit_custom_rc;
+ bool has_quit_custom_rc;
+ bool error_playing;
+
+ int64_t shown_vframes, shown_aframes;
+
+ struct demuxer **sources;
+ int num_sources;
+
+ struct timeline_part *timeline;
+ int num_timeline_parts;
+ int timeline_part;
+ // NOTE: even if num_chapters==0, chapters being not NULL signifies presence
+ // of chapter metadata
+ struct chapter *chapters;
+ int num_chapters;
+ double video_offset;
+
+ struct stream *stream;
+ struct demuxer *demuxer;
+
+ struct track **tracks;
+ int num_tracks;
+
+ char *track_layout_hash;
+
+ // Selected tracks. NULL if no track selected.
+ struct track *current_track[STREAM_TYPE_COUNT];
+
+ struct sh_stream *sh[STREAM_TYPE_COUNT];
+ struct sh_audio *sh_audio; // same as sh[STREAM_AUDIO]->audio
+ struct sh_video *sh_video; // same as sh[STREAM_VIDEO]->video
+ struct sh_sub *sh_sub; // same as sh[STREAM_SUB]->sub
+
+ // Uses: accessing metadata (consider ordered chapters case, where the main
+ // demuxer defines metadata), or special purpose demuxers like TV.
+ struct demuxer *master_demuxer;
+
+ struct mixer *mixer;
+ struct ao *ao;
+ struct vo *video_out;
+
+ /* We're starting playback from scratch or after a seek. Show first
+ * video frame immediately and reinitialize sync. */
+ bool restart_playback;
+ /* Set if audio should be timed to start with video frame after seeking,
+ * not set when e.g. playing cover art */
+ bool sync_audio_to_video;
+ /* After playback restart (above) or audio stream change, adjust audio
+ * stream by cutting samples or adding silence at the beginning to make
+ * audio playback position match video position. */
+ bool syncing_audio;
+ bool hrseek_active;
+ bool hrseek_framedrop;
+ double hrseek_pts;
+ // 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 frame.
+ // In the audio-only case used as a timer since the last seek
+ // by the audio CPU usage meter.
+ double delay;
+ // AV sync: time until next frame should be shown
+ double time_frame;
+ // How long the last vo flip() call took. Used to adjust timing with
+ // the goal of making flip() calls finish (rather than start) at the
+ // specified time.
+ double last_vo_flip_duration;
+ // How much video timing has been changed to make it match the audio
+ // timeline. Used for status line information only.
+ double total_avsync_change;
+ // Total number of dropped frames that were "approved" to be dropped.
+ // Actual dropping depends on --framedrop and decoder internals.
+ int drop_frame_cnt;
+ // Number of frames dropped in a row.
+ int dropped_frames;
+ // 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;
+ double last_seek_pts;
+ // As video_pts, but is not reset when seeking away. (For the very short
+ // period of time until a new frame is decoded and shown.)
+ double last_vo_pts;
+ // Video PTS, or audio PTS if video has ended.
+ double playback_pts;
+ // Used to determine whether the video filter chain was rebuilt.
+ long last_vf_reconfig_count;
+
+ // History of video frames timestamps that were queued in the VO
+ // This includes even skipped frames during hr-seek
+ double vo_pts_history_pts[MAX_NUM_VO_PTS];
+ // Whether the PTS at vo_pts_history[n] is after a seek reset
+ uint64_t vo_pts_history_seek[MAX_NUM_VO_PTS];
+ uint64_t vo_pts_history_seek_ts;
+ uint64_t backstep_start_seek_ts;
+ bool backstep_active;
+
+ double audio_delay;
+
+ double last_heartbeat;
+ double last_metadata_update;
+
+ 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 (occasionally wrapping) microseconds. Used
+ // to turn a new time value to a delta from last time.
+ int64_t last_time;
+
+ // Used to communicate the parameters of a seek between parts
+ struct seek_params {
+ enum seek_type type;
+ double amount;
+ int exact; // -1 = disable, 0 = default, 1 = enable
+ // currently not set by commands, only used internally by seek()
+ int direction; // -1 = backward, 0 = default, 1 = forward
+ } 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;
+ double last_chapter_pts;
+
+ struct ass_library *ass_library;
+
+ int last_dvb_step;
+ int dvbin_reopen;
+
+ bool paused;
+ // 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;
+
+ // 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 lua_ctx *lua_ctx;
+} MPContext;
+
+
+// should not be global
+extern FILE *edl_fd;
+// These appear in options list
+extern int forced_subs_only;
+
+void uninit_player(struct MPContext *mpctx, unsigned int mask);
+void reinit_audio_chain(struct MPContext *mpctx);
+double playing_audio_pts(struct MPContext *mpctx);
+struct track *mp_add_subtitles(struct MPContext *mpctx, char *filename);
+int reinit_video_chain(struct MPContext *mpctx);
+int reinit_video_filters(struct MPContext *mpctx);
+int reinit_audio_filters(struct MPContext *mpctx);
+void pause_player(struct MPContext *mpctx);
+void unpause_player(struct MPContext *mpctx);
+void add_step_frame(struct MPContext *mpctx, int dir);
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ int exact);
+bool mp_seek_chapter(struct MPContext *mpctx, int chapter);
+double get_time_length(struct MPContext *mpctx);
+double get_start_time(struct MPContext *mpctx);
+double get_current_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);
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track);
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid);
+bool mp_remove_track(struct MPContext *mpctx, struct track *track);
+struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction,
+ bool force);
+int mp_get_cache_percent(struct MPContext *mpctx);
+void mp_write_watch_later_conf(struct MPContext *mpctx);
+void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e);
+struct playlist_entry *mp_resume_playlist(struct playlist *playlist,
+ struct MPOpts *opts);
+void mp_force_video_refresh(struct MPContext *mpctx);
+
+void mp_print_version(int always);
+
+// timeline/tl_matroska.c
+void build_ordered_chapter_timeline(struct MPContext *mpctx);
+// timeline/tl_edl.c
+void build_edl_timeline(struct MPContext *mpctx);
+// timeline/tl_cue.c
+void build_cue_timeline(struct MPContext *mpctx);
+
+#endif /* MPLAYER_MP_CORE_H */
diff --git a/mpvcore/player/mp_lua.c b/mpvcore/player/mp_lua.c
new file mode 100644
index 0000000000..9339329f17
--- /dev/null
+++ b/mpvcore/player/mp_lua.c
@@ -0,0 +1,683 @@
+#include <assert.h>
+#include <string.h>
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include "talloc.h"
+
+#include "mpvcore/mp_common.h"
+#include "mpvcore/m_property.h"
+#include "mpvcore/mp_msg.h"
+#include "mpvcore/m_option.h"
+#include "mpvcore/input/input.h"
+#include "mpvcore/path.h"
+#include "mpvcore/bstr.h"
+#include "osdep/timer.h"
+#include "sub/sub.h"
+#include "mp_core.h"
+#include "command.h"
+#include "mp_lua.h"
+
+// List of builtin modules and their contents as strings.
+// All these are generated from mpvcore/lua/*.lua
+static const char *builtin_lua_scripts[][2] = {
+ {"mp.defaults",
+# include "lua/defaults.inc"
+ },
+ {"mp.assdraw",
+# include "lua/assdraw.inc"
+ },
+ {"@osc",
+# include "lua/osc.inc"
+ },
+ {0}
+};
+
+// Represents a loaded script. Each has its own Lua state.
+struct script_ctx {
+ const char *name;
+ lua_State *state;
+ struct mp_log *log;
+ struct MPContext *mpctx;
+};
+
+struct lua_ctx {
+ struct script_ctx **scripts;
+ int num_scripts;
+};
+
+static struct script_ctx *find_script(struct lua_ctx *lctx, const char *name)
+{
+ for (int n = 0; n < lctx->num_scripts; n++) {
+ if (strcmp(lctx->scripts[n]->name, name) == 0)
+ return lctx->scripts[n];
+ }
+ return NULL;
+}
+
+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 wrap_cpcall(lua_State *L)
+{
+ lua_CFunction fn = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+ return fn(L);
+}
+
+// Call the given function fn under a Lua error handler (similar to lua_cpcall).
+// Pass the given number of args from the Lua stack to fn.
+// Returns 0 (and empty stack) on success.
+// Returns LUA_ERR[RUN|MEM|ERR] otherwise, with the error value on the stack.
+static int mp_cpcall(lua_State *L, lua_CFunction fn, int args)
+{
+ // Don't use lua_pushcfunction() - it allocates memory on Lua 5.1.
+ // Instead, emulate C closures by making wrap_cpcall call fn.
+ lua_pushlightuserdata(L, fn); // args... fn
+ // Will always succeed if mp_lua_init() set it up correctly.
+ lua_getfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // args... fn wrap_cpcall
+ lua_insert(L, -(args + 2)); // wrap_cpcall args... fn
+ return lua_pcall(L, args + 1, 0, 0);
+}
+
+static void report_error(lua_State *L)
+{
+ const char *err = lua_tostring(L, -1);
+ mp_msg(MSGT_CPLAYER, MSGL_WARN, "[lua] Error: %s\n",
+ err ? err : "[unknown]");
+ lua_pop(L, 1);
+}
+
+static void add_functions(struct script_ctx *ctx);
+
+static char *script_name_from_filename(void *talloc_ctx, struct lua_ctx *lctx,
+ const char *fname)
+{
+ fname = mp_basename(fname);
+ if (fname[0] == '@')
+ fname += 1;
+ char *name = talloc_strdup(talloc_ctx, fname);
+ // Drop .lua 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] = '_';
+ }
+ // Make unique (stupid but simple)
+ while (find_script(lctx, name))
+ name = talloc_strdup_append(name, "_");
+ return name;
+}
+
+static int load_file(struct script_ctx *ctx, const char *fname)
+{
+ int r = 0;
+ lua_State *L = ctx->state;
+ if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) {
+ report_error(L);
+ r = -1;
+ }
+ assert(lua_gettop(L) == 0);
+ return r;
+}
+
+static int load_builtin(lua_State *L)
+{
+ const char *name = luaL_checkstring(L, 1);
+ for (int n = 0; builtin_lua_scripts[n][0]; n++) {
+ if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
+ if (luaL_loadstring(L, builtin_lua_scripts[n][1]))
+ lua_error(L);
+ lua_call(L, 0, 1);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+// Execute "require " .. name
+static bool require(lua_State *L, const char *name)
+{
+ char buf[80];
+ // Lazy, but better than calling the "require" function manually
+ snprintf(buf, sizeof(buf), "require '%s'", name);
+ if (luaL_loadstring(L, buf) || lua_pcall(L, 0, 0, 0)) {
+ report_error(L);
+ return false;
+ }
+ return true;
+}
+
+static void mp_lua_load_script(struct MPContext *mpctx, const char *fname)
+{
+ struct lua_ctx *lctx = mpctx->lua_ctx;
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .mpctx = mpctx,
+ .name = script_name_from_filename(ctx, lctx, fname),
+ };
+ char *log_name = talloc_asprintf(ctx, "lua/%s", ctx->name);
+ ctx->log = mp_log_new(ctx, mpctx->log, log_name);
+
+ lua_State *L = ctx->state = luaL_newstate();
+ if (!L)
+ goto error_out;
+
+ // used by get_ctx()
+ lua_pushlightuserdata(L, ctx); // ctx
+ lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // -
+
+ lua_pushcfunction(L, wrap_cpcall); // closure
+ lua_setfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // -
+
+ luaL_openlibs(L);
+
+ lua_newtable(L); // mp
+ lua_pushvalue(L, -1); // mp mp
+ lua_setglobal(L, "mp"); // mp
+
+ add_functions(ctx); // mp
+
+ lua_pushstring(L, ctx->name); // mp name
+ lua_setfield(L, -2, "script_name"); // mp
+
+ lua_pop(L, 1); // -
+
+ // 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);
+
+ if (!require(L, "mp.defaults")) {
+ report_error(L);
+ goto error_out;
+ }
+
+ assert(lua_gettop(L) == 0);
+
+ if (fname[0] == '@') {
+ if (!require(L, fname))
+ goto error_out;
+ } else {
+ if (load_file(ctx, fname) < 0)
+ goto error_out;
+ }
+
+ MP_TARRAY_APPEND(lctx, lctx->scripts, lctx->num_scripts, ctx);
+ return;
+
+error_out:
+ if (ctx->state)
+ lua_close(ctx->state);
+ talloc_free(ctx);
+}
+
+static void kill_script(struct script_ctx *ctx)
+{
+ if (!ctx)
+ return;
+ struct lua_ctx *lctx = ctx->mpctx->lua_ctx;
+ lua_close(ctx->state);
+ for (int n = 0; n < lctx->num_scripts; n++) {
+ if (lctx->scripts[n] == ctx) {
+ MP_TARRAY_REMOVE_AT(lctx->scripts, lctx->num_scripts, n);
+ break;
+ }
+ }
+ talloc_free(ctx);
+}
+
+static const char *log_level[] = {
+ [MSGL_FATAL] = "fatal",
+ [MSGL_ERR] = "error",
+ [MSGL_WARN] = "warn",
+ [MSGL_INFO] = "info",
+ [MSGL_V] = "verbose",
+ [MSGL_DBG2] = "debug",
+};
+
+static int script_log(lua_State *L)
+{
+ struct script_ctx *ctx = get_ctx(L);
+
+ const char *level = luaL_checkstring(L, 1);
+ int msgl = -1;
+ for (int n = 0; n < MP_ARRAY_SIZE(log_level); n++) {
+ if (log_level[n] && strcasecmp(log_level[n], level) == 0) {
+ msgl = n;
+ break;
+ }
+ }
+ if (msgl < 0)
+ luaL_error(L, "Invalid log level '%s'", level);
+
+ 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_log(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
+ lua_pop(L, 1); // args... tostring
+ }
+ mp_msg_log(ctx->log, msgl, "\n");
+
+ return 0;
+}
+
+static int script_find_config_file(lua_State *L)
+{
+ const char *s = luaL_checkstring(L, 1);
+ char *path = mp_find_user_config_file(s);
+ if (path) {
+ lua_pushstring(L, path);
+ } else {
+ lua_pushnil(L);
+ }
+ talloc_free(path);
+ return 1;
+}
+
+static int run_event(lua_State *L)
+{
+ lua_getglobal(L, "mp_event"); // name arg mp_event
+ if (lua_isnil(L, -1))
+ return 0;
+ lua_insert(L, -3); // mp_event name arg
+ lua_call(L, 2, 0);
+ return 0;
+}
+
+void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg)
+{
+ // There is no proper subscription mechanism yet, so all scripts get it.
+ struct lua_ctx *lctx = mpctx->lua_ctx;
+ for (int n = 0; n < lctx->num_scripts; n++) {
+ struct script_ctx *ctx = lctx->scripts[n];
+ lua_State *L = ctx->state;
+ lua_pushstring(L, name);
+ if (arg) {
+ lua_pushstring(L, arg);
+ } else {
+ lua_pushnil(L);
+ }
+ if (mp_cpcall(L, run_event, 2) != 0)
+ report_error(L);
+ }
+}
+
+static int run_script_dispatch(lua_State *L)
+{
+ int id = lua_tointeger(L, 1);
+ const char *event = lua_tostring(L, 2);
+ lua_getglobal(L, "mp_script_dispatch");
+ if (lua_isnil(L, -1))
+ return 0;
+ lua_pushinteger(L, id);
+ lua_pushstring(L, event);
+ lua_call(L, 2, 0);
+ return 0;
+}
+
+void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
+ int id, char *event)
+{
+ struct script_ctx *ctx = find_script(mpctx->lua_ctx, script_name);
+ if (!ctx) {
+ mp_msg(MSGT_CPLAYER, MSGL_V,
+ "Can't find script '%s' when handling input.\n", script_name);
+ return;
+ }
+ lua_State *L = ctx->state;
+ lua_pushinteger(L, id);
+ lua_pushstring(L, event);
+ if (mp_cpcall(L, run_script_dispatch, 2) != 0)
+ report_error(L);
+}
+
+static int script_send_command(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ const char *s = luaL_checkstring(L, 1);
+
+ mp_cmd_t *cmd = mp_input_parse_cmd(mpctx->input, bstr0((char*)s), "<lua>");
+ if (!cmd)
+ luaL_error(L, "error parsing command");
+ mp_input_queue_cmd(mpctx->input, cmd);
+
+ return 0;
+}
+
+static int script_property_list(lua_State *L)
+{
+ const struct m_option *props = mp_get_property_list();
+ lua_newtable(L);
+ for (int i = 0; props[i].name; i++) {
+ lua_pushinteger(L, i + 1);
+ lua_pushstring(L, props[i].name);
+ lua_settable(L, -3);
+ }
+ return 1;
+}
+
+static int script_property_string(lua_State *L)
+{
+ const struct m_option *props = mp_get_property_list();
+ struct MPContext *mpctx = get_mpctx(L);
+ const char *name = luaL_checkstring(L, 1);
+ int type = lua_tointeger(L, lua_upvalueindex(1))
+ ? M_PROPERTY_PRINT : M_PROPERTY_GET_STRING;
+
+ char *result = NULL;
+ if (m_property_do(props, name, type, &result, mpctx) >= 0 && result) {
+ lua_pushstring(L, result);
+ talloc_free(result);
+ return 1;
+ }
+ if (type == M_PROPERTY_PRINT) {
+ lua_pushstring(L, "");
+ return 1;
+ }
+ return 0;
+}
+
+static int script_set_osd_ass(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ int res_x = luaL_checkinteger(L, 1);
+ int res_y = luaL_checkinteger(L, 2);
+ const char *text = luaL_checkstring(L, 3);
+ if (!mpctx->osd->external ||
+ strcmp(mpctx->osd->external, text) != 0 ||
+ mpctx->osd->external_res_x != res_x ||
+ mpctx->osd->external_res_y != res_y)
+ {
+ talloc_free(mpctx->osd->external);
+ mpctx->osd->external = talloc_strdup(mpctx->osd, text);
+ mpctx->osd->external_res_x = res_x;
+ mpctx->osd->external_res_y = res_y;
+ osd_changed(mpctx->osd, OSDTYPE_EXTERNAL);
+ }
+ return 0;
+}
+
+static int script_get_osd_resolution(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ int w, h;
+ osd_object_get_resolution(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
+ &w, &h);
+ lua_pushnumber(L, w);
+ lua_pushnumber(L, h);
+ return 2;
+}
+
+static int script_get_screen_size(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
+ double aspect = 1.0 * obj->vo_res.w / MPMAX(obj->vo_res.h, 1) /
+ obj->vo_res.display_par;
+ lua_pushnumber(L, obj->vo_res.w);
+ lua_pushnumber(L, obj->vo_res.h);
+ lua_pushnumber(L, aspect);
+ return 3;
+}
+
+static int script_get_mouse_pos(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ int px, py;
+ mp_input_get_mouse_pos(mpctx->input, &px, &py);
+ double sw, sh;
+ osd_object_get_scale_factor(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
+ &sw, &sh);
+ lua_pushnumber(L, px * sw);
+ lua_pushnumber(L, py * sh);
+ return 2;
+}
+
+static int script_get_timer(lua_State *L)
+{
+ lua_pushnumber(L, mp_time_sec());
+ return 1;
+}
+
+static int script_get_chapter_list(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ lua_newtable(L); // list
+ int num = get_chapter_count(mpctx);
+ for (int n = 0; n < num; n++) {
+ double time = chapter_start_time(mpctx, n);
+ char *name = chapter_display_name(mpctx, n);
+ lua_newtable(L); // list ch
+ lua_pushnumber(L, time); // list ch time
+ lua_setfield(L, -2, "time"); // list ch
+ lua_pushstring(L, name); // list ch name
+ lua_setfield(L, -2, "name"); // list ch
+ lua_pushinteger(L, n + 1); // list ch n1
+ lua_insert(L, -2); // list n1 ch
+ lua_settable(L, -3); // list
+ talloc_free(name);
+ }
+ return 1;
+}
+
+static const char *stream_type(enum stream_type t)
+{
+ switch (t) {
+ case STREAM_VIDEO: return "video";
+ case STREAM_AUDIO: return "audio";
+ case STREAM_SUB: return "sub";
+ default: return "unknown";
+ }
+}
+
+static int script_get_track_list(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ lua_newtable(L); // list
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ lua_newtable(L); // list track
+
+ lua_pushstring(L, stream_type(track->type));
+ lua_setfield(L, -2, "type");
+ lua_pushinteger(L, track->user_tid);
+ lua_setfield(L, -2, "id");
+ lua_pushboolean(L, track->default_track);
+ lua_setfield(L, -2, "default");
+ lua_pushboolean(L, track->attached_picture);
+ lua_setfield(L, -2, "attached_picture");
+ if (track->lang) {
+ lua_pushstring(L, track->lang);
+ lua_setfield(L, -2, "language");
+ }
+ if (track->title) {
+ lua_pushstring(L, track->title);
+ lua_setfield(L, -2, "title");
+ }
+ lua_pushboolean(L, track->is_external);
+ lua_setfield(L, -2, "external");
+ if (track->external_filename) {
+ lua_pushstring(L, track->external_filename);
+ lua_setfield(L, -2, "external_filename");
+ }
+ lua_pushboolean(L, track->auto_loaded);
+ lua_setfield(L, -2, "auto_loaded");
+
+ lua_pushinteger(L, n + 1); // list track n1
+ lua_insert(L, -2); // list n1 track
+ lua_settable(L, -3); // list
+ }
+ return 1;
+}
+
+static int script_input_define_section(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ char *section = (char *)luaL_checkstring(L, 1);
+ char *contents = (char *)luaL_checkstring(L, 2);
+ mp_input_define_section(mpctx->input, section, "<script>", contents, true);
+ return 0;
+}
+
+static int script_input_enable_section(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ char *section = (char *)luaL_checkstring(L, 1);
+ char *sflags = (char *)luaL_optstring(L, 2, "");
+ bstr bflags = bstr0(sflags);
+ int flags = 0;
+ while (bflags.len) {
+ bstr val;
+ bstr_split_tok(bflags, "|", &val, &bflags);
+ if (bstr_equals0(val, "allow-hide-cursor")) {
+ flags |= MP_INPUT_ALLOW_HIDE_CURSOR;
+ } else if (bstr_equals0(val, "allow-vo-dragging")) {
+ flags |= MP_INPUT_ALLOW_VO_DRAGGING;
+ } else if (bstr_equals0(val, "exclusive")) {
+ flags |= MP_INPUT_EXCLUSIVE;
+ } else {
+ luaL_error(L, "invalid flag: '%.*s'", BSTR_P(val));
+ }
+ }
+ mp_input_enable_section(mpctx->input, section, flags);
+ return 0;
+}
+
+static int script_input_disable_section(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+ char *section = (char *)luaL_checkstring(L, 1);
+ mp_input_disable_section(mpctx->input, section);
+ return 0;
+}
+
+static int script_input_set_section_mouse_area(lua_State *L)
+{
+ struct MPContext *mpctx = get_mpctx(L);
+
+ double sw, sh;
+ struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
+ osd_object_get_scale_factor(mpctx->osd, obj, &sw, &sh);
+
+ char *section = (char *)luaL_checkstring(L, 1);
+ int x0 = luaL_checkinteger(L, 2) / sw;
+ int y0 = luaL_checkinteger(L, 3) / sh;
+ int x1 = luaL_checkinteger(L, 4) / sw;
+ int y1 = luaL_checkinteger(L, 5) / sh;
+ 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;
+}
+
+struct fn_entry {
+ const char *name;
+ int (*fn)(lua_State *L);
+};
+
+#define FN_ENTRY(name) {#name, script_ ## name}
+
+static struct fn_entry fn_list[] = {
+ FN_ENTRY(log),
+ FN_ENTRY(find_config_file),
+ FN_ENTRY(send_command),
+ FN_ENTRY(property_list),
+ FN_ENTRY(set_osd_ass),
+ FN_ENTRY(get_osd_resolution),
+ FN_ENTRY(get_screen_size),
+ FN_ENTRY(get_mouse_pos),
+ FN_ENTRY(get_timer),
+ FN_ENTRY(get_chapter_list),
+ FN_ENTRY(get_track_list),
+ FN_ENTRY(input_define_section),
+ FN_ENTRY(input_enable_section),
+ FN_ENTRY(input_disable_section),
+ FN_ENTRY(input_set_section_mouse_area),
+ FN_ENTRY(format_time),
+};
+
+// On stack: mp table
+static void add_functions(struct script_ctx *ctx)
+{
+ lua_State *L = ctx->state;
+
+ for (int n = 0; n < MP_ARRAY_SIZE(fn_list); n++) {
+ lua_pushcfunction(L, fn_list[n].fn);
+ lua_setfield(L, -2, fn_list[n].name);
+ }
+
+ lua_pushinteger(L, 0);
+ lua_pushcclosure(L, script_property_string, 1);
+ lua_setfield(L, -2, "property_get");
+
+ lua_pushinteger(L, 1);
+ lua_pushcclosure(L, script_property_string, 1);
+ lua_setfield(L, -2, "property_get_string");
+}
+
+void mp_lua_init(struct MPContext *mpctx)
+{
+ mpctx->lua_ctx = talloc_zero(NULL, struct lua_ctx);
+ // Load scripts from options
+ if (mpctx->opts->lua_load_osc)
+ mp_lua_load_script(mpctx, "@osc");
+ char **files = mpctx->opts->lua_files;
+ for (int n = 0; files && files[n]; n++) {
+ if (files[n][0])
+ mp_lua_load_script(mpctx, files[n]);
+ }
+}
+
+void mp_lua_uninit(struct MPContext *mpctx)
+{
+ if (mpctx->lua_ctx) {
+ while (mpctx->lua_ctx->num_scripts)
+ kill_script(mpctx->lua_ctx->scripts[0]);
+ talloc_free(mpctx->lua_ctx);
+ mpctx->lua_ctx = NULL;
+ }
+}
diff --git a/mpvcore/player/mp_lua.h b/mpvcore/player/mp_lua.h
new file mode 100644
index 0000000000..050548e2d2
--- /dev/null
+++ b/mpvcore/player/mp_lua.h
@@ -0,0 +1,14 @@
+#ifndef MP_LUA_H
+#define MP_LUA_H
+
+#include <stdbool.h>
+
+struct MPContext;
+
+void mp_lua_init(struct MPContext *mpctx);
+void mp_lua_uninit(struct MPContext *mpctx);
+void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg);
+void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
+ int id, char *event);
+
+#endif
diff --git a/mpvcore/player/mp_osd.h b/mpvcore/player/mp_osd.h
new file mode 100644
index 0000000000..0b737f0c22
--- /dev/null
+++ b/mpvcore/player/mp_osd.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_MP_OSD_H
+#define MPLAYER_MP_OSD_H
+
+#include "compat/compiler.h"
+
+#define OSD_MSG_TEXT 1
+#define OSD_MSG_SUB_DELAY 2
+#define OSD_MSG_SPEED 3
+#define OSD_MSG_OSD_STATUS 4
+#define OSD_MSG_BAR 5
+#define OSD_MSG_PAUSE 6
+#define OSD_MSG_RADIO_CHANNEL 7
+#define OSD_MSG_TV_CHANNEL 8
+/// Base id for messages generated from the commmand to property bridge.
+#define OSD_MSG_PROPERTY 0x100
+#define OSD_MSG_SUB_BASE 0x1000
+
+#define MAX_OSD_LEVEL 3
+#define MAX_TERM_OSD_LEVEL 1
+#define OSD_LEVEL_INVISIBLE 4
+
+#define OSD_BAR_SEEK 256
+
+struct MPContext;
+
+void set_osd_bar(struct MPContext *mpctx, int type,const char* name,double min,double max,double val);
+void set_osd_msg(struct MPContext *mpctx, int id, int level, int time, const char* fmt, ...) PRINTF_ATTRIBUTE(5,6);
+void set_osd_tmsg(struct MPContext *mpctx, int id, int level, int time, const char* fmt, ...) PRINTF_ATTRIBUTE(5,6);
+void rm_osd_msg(struct MPContext *mpctx, int id);
+
+// osd_function is the symbol appearing in the video status, such as OSD_PLAY
+void set_osd_function(struct MPContext *mpctx, int osd_function);
+
+#endif /* MPLAYER_MP_OSD_H */
diff --git a/mpvcore/player/mplayer.c b/mpvcore/player/mplayer.c
new file mode 100644
index 0000000000..b92b38e050
--- /dev/null
+++ b/mpvcore/player/mplayer.c
@@ -0,0 +1,5079 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+#include <ctype.h>
+
+#ifdef PTW32_STATIC_LIB
+#include <pthread.h>
+#endif
+
+#include <libavutil/intreadwrite.h>
+#include <libavutil/attributes.h>
+#include <libavutil/md5.h>
+#include <libavutil/common.h>
+
+#include <libavcodec/version.h>
+
+#include "config.h"
+#include "talloc.h"
+
+#include "osdep/io.h"
+
+#if defined(__MINGW32__) || defined(__CYGWIN__)
+#include <windows.h>
+#endif
+#define WAKEUP_PERIOD 0.5
+#include <string.h>
+#include <unistd.h>
+
+// #include <sys/mman.h>
+#include <sys/types.h>
+#ifndef __MINGW32__
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#endif
+
+#include <sys/time.h>
+#include <sys/stat.h>
+
+#include <signal.h>
+#include <time.h>
+#include <fcntl.h>
+#include <limits.h>
+
+#include <errno.h>
+
+#include "mpvcore/mpv_global.h"
+#include "mpvcore/mp_msg.h"
+#include "mpvcore/av_log.h"
+
+
+#include "mpvcore/m_option.h"
+#include "mpvcore/m_config.h"
+#include "mpvcore/resolve.h"
+#include "mpvcore/m_property.h"
+
+#include "sub/find_subfiles.h"
+#include "sub/dec_sub.h"
+#include "sub/sd.h"
+
+#include "mp_osd.h"
+#include "video/out/vo.h"
+#include "screenshot.h"
+
+#include "sub/sub.h"
+#include "mpvcore/cpudetect.h"
+
+#ifdef CONFIG_X11
+#include "video/out/x11_common.h"
+#endif
+
+#ifdef CONFIG_COCOA
+#include "osdep/macosx_application.h"
+#endif
+
+#include "audio/out/ao.h"
+
+#include "mpvcore/codecs.h"
+
+#include "osdep/getch2.h"
+#include "osdep/timer.h"
+
+#include "mpvcore/input/input.h"
+#include "mpvcore/encode.h"
+
+#include "osdep/priority.h"
+
+#include "stream/tv.h"
+#include "stream/stream_radio.h"
+#ifdef CONFIG_DVBIN
+#include "stream/dvbin.h"
+#endif
+
+//**************************************************************************//
+// Playtree
+//**************************************************************************//
+#include "mpvcore/playlist.h"
+#include "mpvcore/playlist_parser.h"
+
+//**************************************************************************//
+// Config
+//**************************************************************************//
+#include "mpvcore/parser-cfg.h"
+#include "mpvcore/parser-mpcmd.h"
+
+//**************************************************************************//
+// Config file
+//**************************************************************************//
+
+#include "mpvcore/path.h"
+
+//**************************************************************************//
+//**************************************************************************//
+// Input media streaming & demultiplexer:
+//**************************************************************************//
+
+#include "stream/stream.h"
+#include "demux/demux.h"
+#include "demux/stheader.h"
+
+#include "audio/filter/af.h"
+#include "audio/decode/dec_audio.h"
+#include "video/decode/dec_video.h"
+#include "video/mp_image.h"
+#include "video/filter/vf.h"
+#include "video/decode/vd.h"
+
+#include "audio/mixer.h"
+
+#include "mp_core.h"
+#include "mpvcore/options.h"
+
+#include "mp_lua.h"
+
+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> specify subtitle file to use\n"
+" --playlist=<file> specify playlist file\n"
+"\n"
+" --list-options list all mpv options\n"
+"\n");
+
+static const char av_desync_help_text[] = _(
+"\n\n"
+" *************************************************\n"
+" **** Audio/Video desynchronisation detected! ****\n"
+" *************************************************\n\n"
+"This means either the audio or the video is played too slowly.\n"
+"Possible reasons, problems, workarounds:\n"
+"- Your system is simply too slow for this file.\n"
+" Transcode it to a lower bitrate file with tools like HandBrake.\n"
+"- Broken/buggy _audio_ driver.\n"
+" Experiment with different values for --autosync, 30 is a good start.\n"
+" If you have PulseAudio, try --ao=alsa .\n"
+"- Slow video output.\n"
+" Try a different -vo driver (-vo help for a list) or try -framedrop!\n"
+"- Playing a video file with --vo=opengl with higher FPS than the monitor.\n"
+" This is due to vsync limiting the framerate.\n"
+"- Playing from a slow network source.\n"
+" Download the file instead.\n"
+"- Try to find out whether audio or video is causing this by experimenting\n"
+" with --no-video and --no-audio.\n"
+"- If you swiched audio or video tracks, try seeking to force synchronization.\n"
+"If none of this helps you, file a bug report.\n\n");
+
+
+//**************************************************************************//
+//**************************************************************************//
+
+#include "sub/ass_mp.h"
+
+
+// ---
+
+#include "mpvcore/mp_common.h"
+#include "command.h"
+
+static void reset_subtitles(struct MPContext *mpctx);
+static void reinit_subs(struct MPContext *mpctx);
+static void handle_force_window(struct MPContext *mpctx, bool reconfig);
+
+static double get_relative_time(struct MPContext *mpctx)
+{
+ int64_t new_time = mp_time_us();
+ int64_t delta = new_time - mpctx->last_time;
+ mpctx->last_time = new_time;
+ return delta * 0.000001;
+}
+
+static double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t,
+ double fallback_time)
+{
+ double length = get_time_length(mpctx);
+ switch (t.type) {
+ case REL_TIME_ABSOLUTE:
+ return t.pos;
+ case REL_TIME_NEGATIVE:
+ if (length != 0)
+ return FFMAX(length - t.pos, 0.0);
+ break;
+ case REL_TIME_PERCENT:
+ if (length != 0)
+ return length * (t.pos / 100.0);
+ break;
+ case REL_TIME_CHAPTER:
+ if (chapter_start_time(mpctx, t.pos) >= 0)
+ return chapter_start_time(mpctx, t.pos);
+ break;
+ }
+ return fallback_time;
+}
+
+static double get_play_end_pts(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->play_end.type) {
+ return rel_time_to_abs(mpctx, opts->play_end, MP_NOPTS_VALUE);
+ } else if (opts->play_length.type) {
+ double startpts = get_start_time(mpctx);
+ double start = rel_time_to_abs(mpctx, opts->play_start, startpts);
+ double length = rel_time_to_abs(mpctx, opts->play_length, -1);
+ if (start != -1 && length != -1)
+ return start + length;
+ }
+ return MP_NOPTS_VALUE;
+}
+
+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 = "?";
+ const char *iid = NULL;
+ switch (t->type) {
+ case STREAM_VIDEO:
+ tname = "Video"; selopt = "vid"; langopt = NULL; iid = "VID";
+ break;
+ case STREAM_AUDIO:
+ tname = "Audio"; selopt = "aid"; langopt = "alang"; iid = "AID";
+ break;
+ case STREAM_SUB:
+ tname = "Subs"; selopt = "sid"; langopt = "slang"; iid = "SID";
+ break;
+ }
+ MP_INFO(mpctx, "[stream] %-5s %3s",
+ tname, mpctx->current_track[t->type] == t ? "(+)" : "");
+ MP_INFO(mpctx, " --%s=%d", selopt, t->user_tid);
+ if (t->lang && langopt)
+ MP_INFO(mpctx, " --%s=%s", langopt, t->lang);
+ if (t->default_track)
+ MP_INFO(mpctx, " (*)");
+ if (t->attached_picture)
+ MP_INFO(mpctx, " [P]");
+ if (t->title)
+ MP_INFO(mpctx, " '%s'", t->title);
+ const char *codec = s ? s->codec : NULL;
+ MP_INFO(mpctx, " (%s)", codec ? codec : "<unknown>");
+ if (t->is_external)
+ MP_INFO(mpctx, " (external)");
+ MP_INFO(mpctx, "\n");
+ // legacy compatibility
+ if (!iid)
+ return;
+ int id = t->user_tid;
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_ID=%d\n", iid, id);
+ if (t->title)
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_%d_NAME=%s\n", iid, id, t->title);
+ if (t->lang)
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_%d_LANG=%s\n", iid, id, t->lang);
+}
+
+static void print_file_properties(struct MPContext *mpctx, const char *filename)
+{
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_FILENAME=%s\n",
+ filename);
+ if (mpctx->sh_video) {
+ /* Assume FOURCC if all bytes >= 0x20 (' ') */
+ if (mpctx->sh_video->format >= 0x20202020)
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_FORMAT=%.4s\n", (char *)&mpctx->sh_video->format);
+ else
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_FORMAT=0x%08X\n", mpctx->sh_video->format);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_BITRATE=%d\n", mpctx->sh_video->i_bps * 8);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_WIDTH=%d\n", mpctx->sh_video->disp_w);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_HEIGHT=%d\n", mpctx->sh_video->disp_h);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_FPS=%5.3f\n", mpctx->sh_video->fps);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_VIDEO_ASPECT=%1.4f\n", mpctx->sh_video->aspect);
+ }
+ if (mpctx->sh_audio) {
+ /* Assume FOURCC if all bytes >= 0x20 (' ') */
+ if (mpctx->sh_audio->format >= 0x20202020)
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_AUDIO_FORMAT=%.4s\n", (char *)&mpctx->sh_audio->format);
+ else
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_AUDIO_FORMAT=%d\n", mpctx->sh_audio->format);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_AUDIO_BITRATE=%d\n", mpctx->sh_audio->i_bps * 8);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_AUDIO_RATE=%d\n", mpctx->sh_audio->samplerate);
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_AUDIO_NCH=%d\n", mpctx->sh_audio->channels.num);
+ }
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO,
+ "ID_LENGTH=%.2f\n", get_time_length(mpctx));
+ int chapter_count = get_chapter_count(mpctx);
+ if (chapter_count >= 0) {
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTERS=%d\n", chapter_count);
+ for (int i = 0; i < chapter_count; i++) {
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_ID=%d\n", i);
+ // print in milliseconds
+ double time = chapter_start_time(mpctx, i) * 1000.0;
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_%d_START=%"PRId64"\n",
+ i, (int64_t)(time < 0 ? -1 : time));
+ char *name = chapter_name(mpctx, i);
+ if (name) {
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_%d_NAME=%s\n", i,
+ name);
+ talloc_free(name);
+ }
+ }
+ }
+ struct demuxer *demuxer = mpctx->master_demuxer;
+ if (demuxer->num_editions > 1)
+ MP_INFO(mpctx, "Playing edition %d of %d (--edition=%d).\n",
+ demuxer->edition + 1, demuxer->num_editions, demuxer->edition);
+ 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]);
+ }
+}
+
+// Time used to seek external tracks to.
+static double get_main_demux_pts(struct MPContext *mpctx)
+{
+ double main_new_pos = MP_NOPTS_VALUE;
+ if (mpctx->demuxer) {
+ for (int n = 0; n < mpctx->demuxer->num_streams; n++) {
+ if (main_new_pos == MP_NOPTS_VALUE)
+ main_new_pos = demux_get_next_pts(mpctx->demuxer->streams[n]);
+ }
+ }
+ return main_new_pos;
+}
+
+static void set_demux_field(struct MPContext *mpctx, enum stream_type type,
+ struct sh_stream *s)
+{
+ mpctx->sh[type] = s;
+ // redundant fields for convenience access
+ switch(type) {
+ case STREAM_VIDEO: mpctx->sh_video = s ? s->video : NULL; break;
+ case STREAM_AUDIO: mpctx->sh_audio = s ? s->audio : NULL; break;
+ case STREAM_SUB: mpctx->sh_sub = s ? s->sub : NULL; break;
+ }
+}
+
+static void init_demux_stream(struct MPContext *mpctx, enum stream_type type)
+{
+ struct track *track = mpctx->current_track[type];
+ set_demux_field(mpctx, type, track ? track->stream : NULL);
+ struct sh_stream *stream = mpctx->sh[type];
+ if (stream) {
+ demuxer_switch_track(stream->demuxer, type, stream);
+ if (track->is_external) {
+ double pts = get_main_demux_pts(mpctx);
+ demux_seek(stream->demuxer, pts, SEEK_ABSOLUTE);
+ }
+ }
+}
+
+static void cleanup_demux_stream(struct MPContext *mpctx, enum stream_type type)
+{
+ struct sh_stream *stream = mpctx->sh[type];
+ if (stream)
+ demuxer_switch_track(stream->demuxer, type, NULL);
+ set_demux_field(mpctx, type, NULL);
+}
+
+// Switch the demuxers to current track selection. This is possibly important
+// for intialization: if something reads packets from the demuxer (like at least
+// reinit_audio_chain does, or when seeking), packets from the other streams
+// should be queued instead of discarded. So all streams should be enabled
+// before the first initialization function is called.
+static void preselect_demux_streams(struct MPContext *mpctx)
+{
+ // Disable all streams, just to be sure no unwanted streams are selected.
+ for (int n = 0; n < mpctx->num_sources; n++) {
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ struct track *track = mpctx->current_track[type];
+ if (!(track && track->demuxer == mpctx->sources[n] &&
+ demuxer_stream_is_selected(track->demuxer, track->stream)))
+ demuxer_switch_track(mpctx->sources[n], type, NULL);
+ }
+ }
+
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ struct track *track = mpctx->current_track[type];
+ if (track && track->stream)
+ demuxer_switch_track(track->stream->demuxer, type, track->stream);
+ }
+}
+
+static void uninit_subs(struct demuxer *demuxer)
+{
+ for (int i = 0; i < demuxer->num_streams; i++) {
+ struct sh_stream *sh = demuxer->streams[i];
+ if (sh->sub) {
+ sub_destroy(sh->sub->dec_sub);
+ sh->sub->dec_sub = NULL;
+ }
+ }
+}
+
+void uninit_player(struct MPContext *mpctx, unsigned int mask)
+{
+ mask &= mpctx->initialized_flags;
+
+ MP_DBG(mpctx, "\n*** uninit(0x%X)\n", mask);
+
+ if (mask & INITIALIZED_ACODEC) {
+ mpctx->initialized_flags &= ~INITIALIZED_ACODEC;
+ mixer_uninit_audio(mpctx->mixer);
+ if (mpctx->sh_audio)
+ uninit_audio(mpctx->sh_audio);
+ cleanup_demux_stream(mpctx, STREAM_AUDIO);
+ }
+
+ if (mask & INITIALIZED_SUB) {
+ mpctx->initialized_flags &= ~INITIALIZED_SUB;
+ if (mpctx->sh_sub)
+ sub_reset(mpctx->sh_sub->dec_sub);
+ cleanup_demux_stream(mpctx, STREAM_SUB);
+ mpctx->osd->dec_sub = NULL;
+ reset_subtitles(mpctx);
+ }
+
+ if (mask & INITIALIZED_LIBASS) {
+ mpctx->initialized_flags &= ~INITIALIZED_LIBASS;
+#ifdef CONFIG_ASS
+ if (mpctx->osd->ass_renderer)
+ ass_renderer_done(mpctx->osd->ass_renderer);
+ mpctx->osd->ass_renderer = NULL;
+ ass_clear_fonts(mpctx->ass_library);
+#endif
+ }
+
+ if (mask & INITIALIZED_VCODEC) {
+ mpctx->initialized_flags &= ~INITIALIZED_VCODEC;
+ if (mpctx->sh_video)
+ uninit_video(mpctx->sh_video);
+ cleanup_demux_stream(mpctx, STREAM_VIDEO);
+ mpctx->sync_audio_to_video = false;
+ }
+
+ if (mask & INITIALIZED_DEMUXER) {
+ mpctx->initialized_flags &= ~INITIALIZED_DEMUXER;
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ talloc_free(mpctx->tracks[i]);
+ }
+ mpctx->num_tracks = 0;
+ for (int t = 0; t < STREAM_TYPE_COUNT; t++)
+ mpctx->current_track[t] = NULL;
+ assert(!mpctx->sh_video && !mpctx->sh_audio && !mpctx->sh_sub);
+ mpctx->master_demuxer = NULL;
+ for (int i = 0; i < mpctx->num_sources; i++) {
+ uninit_subs(mpctx->sources[i]);
+ struct demuxer *demuxer = mpctx->sources[i];
+ if (demuxer->stream != mpctx->stream)
+ free_stream(demuxer->stream);
+ free_demuxer(demuxer);
+ }
+ talloc_free(mpctx->sources);
+ mpctx->sources = NULL;
+ mpctx->demuxer = NULL;
+ mpctx->num_sources = 0;
+ talloc_free(mpctx->timeline);
+ mpctx->timeline = NULL;
+ mpctx->num_timeline_parts = 0;
+ talloc_free(mpctx->chapters);
+ mpctx->chapters = NULL;
+ mpctx->num_chapters = 0;
+ mpctx->video_offset = 0;
+ }
+
+ // kill the cache process:
+ if (mask & INITIALIZED_STREAM) {
+ mpctx->initialized_flags &= ~INITIALIZED_STREAM;
+ if (mpctx->stream)
+ free_stream(mpctx->stream);
+ mpctx->stream = NULL;
+ }
+
+ if (mask & INITIALIZED_VO) {
+ mpctx->initialized_flags &= ~INITIALIZED_VO;
+ vo_destroy(mpctx->video_out);
+ mpctx->video_out = NULL;
+ }
+
+ // Must be after libvo uninit, as few vo drivers (svgalib) have tty code.
+ if (mask & INITIALIZED_GETCH2) {
+ mpctx->initialized_flags &= ~INITIALIZED_GETCH2;
+ MP_DBG(mpctx, "\n[[[uninit getch2]]]\n");
+ // restore terminal:
+ getch2_disable();
+ }
+
+ if (mask & INITIALIZED_AO) {
+ mpctx->initialized_flags &= ~INITIALIZED_AO;
+ if (mpctx->ao)
+ ao_uninit(mpctx->ao, mpctx->stop_play != AT_END_OF_FILE);
+ mpctx->ao = NULL;
+ }
+
+ if (mask & INITIALIZED_PLAYBACK)
+ mpctx->initialized_flags &= ~INITIALIZED_PLAYBACK;
+}
+
+static MP_NORETURN void exit_player(struct MPContext *mpctx,
+ enum exit_reason how)
+{
+ int rc;
+ uninit_player(mpctx, INITIALIZED_ALL);
+
+#ifdef CONFIG_ENCODING
+ encode_lavc_finish(mpctx->encode_lavc_ctx);
+ encode_lavc_free(mpctx->encode_lavc_ctx);
+#endif
+
+ mpctx->encode_lavc_ctx = NULL;
+
+#ifdef CONFIG_LUA
+ mp_lua_uninit(mpctx);
+#endif
+
+#if defined(__MINGW32__) || defined(__CYGWIN__)
+ timeEndPeriod(1);
+#endif
+
+#ifdef CONFIG_COCOA
+ cocoa_set_input_context(NULL);
+#endif
+
+ command_uninit(mpctx);
+
+ mp_input_uninit(mpctx->input);
+
+ osd_free(mpctx->osd);
+
+#ifdef CONFIG_ASS
+ ass_library_done(mpctx->ass_library);
+ mpctx->ass_library = NULL;
+#endif
+
+ if (how != EXIT_NONE) {
+ const char *reason;
+ switch (how) {
+ case EXIT_SOMENOTPLAYED:
+ case EXIT_PLAYED:
+ reason = "End of file";
+ break;
+ case EXIT_NOTPLAYED:
+ reason = "No files played";
+ break;
+ case EXIT_ERROR:
+ reason = "Fatal error";
+ break;
+ default:
+ reason = "Quit";
+ }
+ MP_INFO(mpctx, "\nExiting... (%s)\n", reason);
+ }
+
+ if (mpctx->has_quit_custom_rc) {
+ rc = mpctx->quit_custom_rc;
+ } else {
+ switch (how) {
+ case EXIT_ERROR:
+ rc = 1; break;
+ case EXIT_NOTPLAYED:
+ rc = 2; break;
+ case EXIT_SOMENOTPLAYED:
+ rc = 3; break;
+ default:
+ rc = 0;
+ }
+ }
+
+ // must be last since e.g. mp_msg uses option values
+ // that will be freed by this.
+
+ mp_msg_uninit(mpctx->global);
+ talloc_free(mpctx);
+
+#ifdef CONFIG_COCOA
+ terminate_cocoa_application();
+ // never reach here:
+ // terminate calls exit itself, just silence compiler warning
+ exit(0);
+#else
+ exit(rc);
+#endif
+}
+
+static void mk_config_dir(char *subdir)
+{
+ void *tmp = talloc_new(NULL);
+ char *confdir = talloc_steal(tmp, mp_find_user_config_file(""));
+ if (confdir) {
+ if (subdir)
+ confdir = mp_path_join(tmp, bstr0(confdir), bstr0(subdir));
+ mkdir(confdir, 0777);
+ }
+ talloc_free(tmp);
+}
+
+static int cfg_include(struct m_config *conf, char *filename, int flags)
+{
+ return m_config_parse_config_file(conf, filename, flags);
+}
+
+#define DEF_CONFIG "# Write your default config options here!\n\n\n"
+
+static bool parse_cfgfiles(struct MPContext *mpctx, m_config_t *conf)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *conffile;
+ int conffile_fd;
+ if (!opts->load_config)
+ return true;
+ if (!m_config_parse_config_file(conf, MPLAYER_CONFDIR "/mpv.conf", 0) < 0)
+ return false;
+ mk_config_dir(NULL);
+ if ((conffile = mp_find_user_config_file("config")) == NULL)
+ MP_ERR(mpctx, "mp_find_user_config_file(\"config\") problem\n");
+ else {
+ if ((conffile_fd = open(conffile, O_CREAT | O_EXCL | O_WRONLY,
+ 0666)) != -1) {
+ MP_INFO(mpctx, "Creating config file: %s\n", conffile);
+ write(conffile_fd, DEF_CONFIG, sizeof(DEF_CONFIG) - 1);
+ close(conffile_fd);
+ }
+ if (m_config_parse_config_file(conf, conffile, 0) < 0)
+ return false;
+ talloc_free(conffile);
+ }
+ return true;
+}
+
+// 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)
+
+#define PROFILE_CFG_PROTOCOL "protocol."
+
+static void load_per_protocol_config(m_config_t *conf, const char * const file)
+{
+ char *str;
+ char protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) + 1];
+ m_profile_t *p;
+
+ /* does filename actually uses a protocol ? */
+ if (!mp_is_url(bstr0(file)))
+ return;
+ str = strstr(file, "://");
+ if (!str)
+ return;
+
+ sprintf(protocol, "%s%s", PROFILE_CFG_PROTOCOL, file);
+ protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) - strlen(str)] = '\0';
+ p = m_config_get_profile0(conf, protocol);
+ if (p) {
+ mp_tmsg(MSGT_CPLAYER, MSGL_INFO,
+ "Loading protocol-related profile '%s'\n", protocol);
+ m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
+ }
+}
+
+#define PROFILE_CFG_EXTENSION "extension."
+
+static void load_per_extension_config(m_config_t *conf, const char * const file)
+{
+ char *str;
+ char extension[strlen(PROFILE_CFG_EXTENSION) + 8];
+ m_profile_t *p;
+
+ /* does filename actually have an extension ? */
+ str = strrchr(file, '.');
+ if (!str)
+ return;
+
+ sprintf(extension, PROFILE_CFG_EXTENSION);
+ strncat(extension, ++str, 7);
+ p = m_config_get_profile0(conf, extension);
+ if (p) {
+ mp_tmsg(MSGT_CPLAYER, MSGL_INFO,
+ "Loading extension-related profile '%s'\n", extension);
+ m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
+ }
+}
+
+#define PROFILE_CFG_VO "vo."
+#define PROFILE_CFG_AO "ao."
+
+static void load_per_output_config(m_config_t *conf, char *cfg, char *out)
+{
+ char profile[strlen(cfg) + strlen(out) + 1];
+ m_profile_t *p;
+
+ if (!out && !out[0])
+ return;
+
+ sprintf(profile, "%s%s", cfg, out);
+ p = m_config_get_profile0(conf, profile);
+ if (p) {
+ mp_tmsg(MSGT_CPLAYER, MSGL_INFO,
+ "Loading extension-related profile '%s'\n", profile);
+ m_config_set_profile(conf, p, FILE_LOCAL_FLAGS);
+ }
+}
+
+/**
+ * Tries to load a config file (in file local mode)
+ * @return 0 if file was not found, 1 otherwise
+ */
+static int try_load_config(m_config_t *conf, const char *file, int flags)
+{
+ if (!mp_path_exists(file))
+ return 0;
+ mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "Loading config '%s'\n", file);
+ m_config_parse_config_file(conf, file, flags);
+ return 1;
+}
+
+static void load_per_file_config(m_config_t *conf, const char * const file,
+ bool search_file_dir)
+{
+ char *confpath;
+ char cfg[MP_PATH_MAX];
+ const char *name;
+
+ if (strlen(file) > MP_PATH_MAX - 14) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN, "Filename is too long, "
+ "can not load file or directory specific config files\n");
+ return;
+ }
+ sprintf(cfg, "%s.conf", file);
+
+ name = mp_basename(cfg);
+ if (search_file_dir) {
+ char dircfg[MP_PATH_MAX];
+ strcpy(dircfg, cfg);
+ strcpy(dircfg + (name - cfg), "mpv.conf");
+ try_load_config(conf, dircfg, FILE_LOCAL_FLAGS);
+
+ if (try_load_config(conf, cfg, FILE_LOCAL_FLAGS))
+ return;
+ }
+
+ if ((confpath = mp_find_user_config_file(name)) != NULL) {
+ try_load_config(conf, confpath, FILE_LOCAL_FLAGS);
+
+ talloc_free(confpath);
+ }
+}
+
+#define MP_WATCH_LATER_CONF "watch_later"
+
+static char *get_playback_resume_config_filename(const char *fname,
+ struct MPOpts *opts)
+{
+ char *res = NULL;
+ void *tmp = talloc_new(NULL);
+ const char *realpath = fname;
+ bstr bfname = bstr0(fname);
+ if (!mp_is_url(bfname)) {
+ char *cwd = mp_getcwd(tmp);
+ if (!cwd)
+ goto exit;
+ realpath = mp_path_join(tmp, bstr0(cwd), bstr0(fname));
+ }
+#ifdef CONFIG_DVDREAD
+ if (bstr_startswith0(bfname, "dvd://"))
+ realpath = talloc_asprintf(tmp, "%s - %s", realpath, dvd_device);
+#endif
+#ifdef CONFIG_LIBBLURAY
+ if (bstr_startswith0(bfname, "br://") || bstr_startswith0(bfname, "bd://") ||
+ bstr_startswith0(bfname, "bluray://"))
+ realpath = talloc_asprintf(tmp, "%s - %s", realpath, bluray_device);
+#endif
+ 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]);
+
+ conf = talloc_asprintf(tmp, "%s/%s", MP_WATCH_LATER_CONF, conf);
+
+ res = mp_find_user_config_file(conf);
+
+exit:
+ talloc_free(tmp);
+ return res;
+}
+
+static const char *backup_properties[] = {
+ "osd-level",
+ //"loop",
+ "speed",
+ "edition",
+ "pause",
+ "volume-restore-data",
+ "audio-delay",
+ //"balance",
+ "fullscreen",
+ "colormatrix",
+ "colormatrix-input-range",
+ "colormatrix-output-range",
+ "ontop",
+ "border",
+ "gamma",
+ "brightness",
+ "contrast",
+ "saturation",
+ "hue",
+ "deinterlace",
+ "vf",
+ "af",
+ "panscan",
+ "aid",
+ "vid",
+ "sid",
+ "sub-delay",
+ "sub-pos",
+ "sub-visibility",
+ "sub-scale",
+ "ass-use-margins",
+ "ass-vsfilter-aspect-compat",
+ "ass-style-override",
+ 0
+};
+
+// Should follow what parser-cfg.c does/needs
+static bool needs_config_quoting(const char *s)
+{
+ for (int i = 0; s && s[i]; i++) {
+ unsigned char c = s[i];
+ if (!isprint(c) || isspace(c) || c == '#' || c == '\'' || c == '"')
+ return true;
+ }
+ return false;
+}
+
+void mp_write_watch_later_conf(struct MPContext *mpctx)
+{
+ void *tmp = talloc_new(NULL);
+ char *filename = mpctx->filename;
+ if (!filename)
+ goto exit;
+
+ double pos = get_current_time(mpctx);
+ if (pos == MP_NOPTS_VALUE)
+ goto exit;
+
+ mk_config_dir(MP_WATCH_LATER_CONF);
+
+ char *conffile = get_playback_resume_config_filename(mpctx->filename,
+ mpctx->opts);
+ talloc_steal(tmp, conffile);
+ if (!conffile)
+ goto exit;
+
+ MP_INFO(mpctx, "Saving state.\n");
+
+ FILE *file = fopen(conffile, "wb");
+ if (!file)
+ goto exit;
+ fprintf(file, "start=%f\n", pos);
+ for (int i = 0; backup_properties[i]; i++) {
+ const char *pname = backup_properties[i];
+ char *val = NULL;
+ int r = mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx);
+ if (r == M_PROPERTY_OK) {
+ 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);
+
+exit:
+ talloc_free(tmp);
+}
+
+static void load_playback_resume(m_config_t *conf, const char *file)
+{
+ char *fname = get_playback_resume_config_filename(file, conf->optstruct);
+ if (fname && mp_path_exists(fname)) {
+ // Never apply the saved start position to following files
+ m_config_backup_opt(conf, "start");
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "Resuming playback. This behavior can "
+ "be disabled with --no-resume-playback.\n");
+ try_load_config(conf, fname, M_SETOPT_PRESERVE_CMDLINE);
+ unlink(fname);
+ }
+ talloc_free(fname);
+}
+
+// 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_resume_playlist(struct playlist *playlist,
+ struct MPOpts *opts)
+{
+ if (!opts->position_resume)
+ return NULL;
+ for (struct playlist_entry *e = playlist->first; e; e = e->next) {
+ char *conf = get_playback_resume_config_filename(e->filename, opts);
+ bool exists = conf && mp_path_exists(conf);
+ talloc_free(conf);
+ if (exists)
+ return e;
+ }
+ return NULL;
+}
+
+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_ext(conf, params[n].name, params[n].value,
+ M_SETOPT_BACKUP);
+ }
+}
+
+/* When demux performs a blocking operation (network connection or
+ * cache filling) if the operation fails we use this function to check
+ * if it was interrupted by the user.
+ * The function returns whether it was interrupted. */
+static bool demux_was_interrupted(struct MPContext *mpctx)
+{
+ for (;;) {
+ if (mpctx->stop_play != KEEP_PLAYING
+ && mpctx->stop_play != AT_END_OF_FILE)
+ return true;
+ mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input, 0, 0);
+ if (!cmd)
+ break;
+ if (mp_input_is_abort_cmd(cmd->id))
+ run_command(mpctx, cmd);
+ mp_cmd_free(cmd);
+ }
+ return false;
+}
+
+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 = FFMAX(new_id, track->user_tid);
+ }
+ return new_id + 1;
+}
+
+// Map stream number (as used by libdvdread) to MPEG IDs (as used by demuxer).
+static int map_id_from_demuxer(struct demuxer *d, enum stream_type type, int id)
+{
+ if (d->stream->uncached_type == STREAMTYPE_DVD && type == STREAM_SUB)
+ id = id & 0x1F;
+ return id;
+}
+
+static struct track *add_stream_track(struct MPContext *mpctx,
+ struct sh_stream *stream,
+ bool under_timeline)
+{
+ for (int i = 0; i < mpctx->num_tracks; i++) {
+ struct track *track = mpctx->tracks[i];
+ if (track->stream == stream)
+ return track;
+ // DVD subtitle track that was added later
+ if (stream->type == STREAM_SUB && track->type == STREAM_SUB &&
+ map_id_from_demuxer(stream->demuxer, stream->type,
+ stream->demuxer_id) == track->demuxer_id
+ && !track->stream)
+ {
+ track->stream = stream;
+ track->demuxer_id = stream->demuxer_id;
+ // Initialize lazily selected track
+ bool selected = track == mpctx->current_track[STREAM_SUB];
+ demuxer_select_track(track->demuxer, stream, selected);
+ if (selected)
+ reinit_subs(mpctx);
+ 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,
+ .title = stream->title,
+ .default_track = stream->default_track,
+ .attached_picture = stream->attached_picture != NULL,
+ .lang = stream->lang,
+ .under_timeline = under_timeline,
+ .demuxer = stream->demuxer,
+ .stream = stream,
+ };
+ MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track);
+
+ if (stream->type == STREAM_SUB)
+ track->preloaded = !!stream->sub->track;
+
+ // Needed for DVD and Blu-ray.
+ if (!track->lang) {
+ struct stream_lang_req req = {
+ .type = track->type,
+ .id = map_id_from_demuxer(track->demuxer, track->type,
+ track->demuxer_id)
+ };
+ stream_control(track->demuxer->stream, STREAM_CTRL_GET_LANG, &req);
+ if (req.name[0])
+ track->lang = talloc_strdup(track, req.name);
+ }
+
+ demuxer_select_track(track->demuxer, stream, false);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return track;
+}
+
+static void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer)
+{
+ for (int n = 0; n < demuxer->num_streams; n++)
+ add_stream_track(mpctx, demuxer->streams[n], !!mpctx->timeline);
+}
+
+static void add_dvd_tracks(struct MPContext *mpctx)
+{
+#ifdef CONFIG_DVDREAD
+ struct demuxer *demuxer = mpctx->demuxer;
+ struct stream *stream = demuxer->stream;
+ struct stream_dvd_info_req info;
+ if (stream_control(stream, STREAM_CTRL_GET_DVD_INFO, &info) > 0) {
+ for (int n = 0; n < info.num_subs; n++) {
+ struct track *track = talloc_ptrtype(NULL, track);
+ *track = (struct track) {
+ .type = STREAM_SUB,
+ .user_tid = find_new_tid(mpctx, STREAM_SUB),
+ .demuxer_id = n,
+ .demuxer = mpctx->demuxer,
+ };
+ MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track);
+
+ struct stream_lang_req req = {.type = STREAM_SUB, .id = n};
+ stream_control(stream, STREAM_CTRL_GET_LANG, &req);
+ track->lang = talloc_strdup(track, req.name);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+ }
+ }
+ demuxer_enable_autoselect(demuxer);
+#endif
+}
+
+int mp_get_cache_percent(struct MPContext *mpctx)
+{
+ if (mpctx->stream) {
+ int64_t size = -1;
+ int64_t fill = -1;
+ stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_SIZE, &size);
+ stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_FILL, &fill);
+ if (size > 0 && fill >= 0)
+ return fill / (size / 100);
+ }
+ return -1;
+}
+
+static bool mp_get_cache_idle(struct MPContext *mpctx)
+{
+ int idle = 0;
+ if (mpctx->stream)
+ stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_IDLE, &idle);
+ return idle;
+}
+
+static void vo_update_window_title(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out)
+ return;
+ char *title = mp_property_expand_string(mpctx, mpctx->opts->wintitle);
+ if (!mpctx->video_out->window_title ||
+ strcmp(title, mpctx->video_out->window_title))
+ {
+ talloc_free(mpctx->video_out->window_title);
+ mpctx->video_out->window_title = talloc_steal(mpctx, title);
+ vo_control(mpctx->video_out, VOCTRL_UPDATE_WINDOW_TITLE, title);
+ } else {
+ talloc_free(title);
+ }
+}
+
+#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 int get_term_width(void)
+{
+ get_screen_size();
+ int width = screen_width > 0 ? screen_width : 80;
+#if defined(__MINGW32__) || defined(__CYGWIN__)
+ /* Windows command line is broken (MinGW's rxvt works, but we
+ * should not depend on that). */
+ width--;
+#endif
+ return width;
+}
+
+static void write_status_line(struct MPContext *mpctx, const char *line)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->slave_mode) {
+ mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s\n", line);
+ } else if (erase_to_end_of_line) {
+ mp_msg(MSGT_STATUSLINE, MSGL_STATUS,
+ "%s%s\r", line, erase_to_end_of_line);
+ } else {
+ int pos = strlen(line);
+ int width = get_term_width() - pos;
+ mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s%*s\r", line, width, "");
+ }
+}
+
+static void print_status(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ sh_video_t * const sh_video = mpctx->sh_video;
+
+ vo_update_window_title(mpctx);
+
+ if (opts->quiet)
+ return;
+
+ if (opts->status_msg) {
+ char *r = mp_property_expand_string(mpctx, opts->status_msg);
+ write_status_line(mpctx, r);
+ talloc_free(r);
+ return;
+ }
+
+ char *line = NULL;
+
+ // Playback status
+ if (mpctx->paused_for_cache && !opts->pause) {
+ saddf(&line, "(Buffering) ");
+ } else if (mpctx->paused) {
+ saddf(&line, "(Paused) ");
+ }
+
+ if (mpctx->sh_audio)
+ saddf(&line, "A");
+ if (mpctx->sh_video)
+ saddf(&line, "V");
+ saddf(&line, ": ");
+
+ // Playback position
+ double cur = get_current_time(mpctx);
+ sadd_hhmmssff(&line, cur, mpctx->opts->osd_fractions);
+
+ double len = get_time_length(mpctx);
+ if (len >= 0) {
+ saddf(&line, " / ");
+ sadd_hhmmssff(&line, len, mpctx->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->sh_audio && sh_video && mpctx->sync_audio_to_video) {
+ if (mpctx->last_av_difference != MP_NOPTS_VALUE)
+ saddf(&line, " A-V:%7.3f", mpctx->last_av_difference);
+ else
+ saddf(&line, " A-V: ???");
+ if (fabs(mpctx->total_avsync_change) > 0.05)
+ saddf(&line, " ct:%7.3f", mpctx->total_avsync_change);
+ }
+
+#ifdef CONFIG_ENCODING
+ 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
+#endif
+ {
+ // VO stats
+ if (sh_video && mpctx->drop_frame_cnt)
+ saddf(&line, " Late: %d", mpctx->drop_frame_cnt);
+ }
+
+ int cache = mp_get_cache_percent(mpctx);
+ if (cache >= 0)
+ saddf(&line, " Cache: %d%%", cache);
+
+ // end
+ write_status_line(mpctx, line);
+ talloc_free(line);
+}
+
+typedef struct mp_osd_msg mp_osd_msg_t;
+struct mp_osd_msg {
+ /// Previous message on the stack.
+ mp_osd_msg_t *prev;
+ /// Message text.
+ char *msg;
+ int id, level, started;
+ /// Display duration in seconds.
+ double time;
+ // Show full OSD for duration of message instead of msg
+ // (osd_show_progression command)
+ bool show_position;
+};
+
+// time is in ms
+static mp_osd_msg_t *add_osd_msg(struct MPContext *mpctx, int id, int level,
+ int time)
+{
+ rm_osd_msg(mpctx, id);
+ mp_osd_msg_t *msg = talloc_struct(mpctx, mp_osd_msg_t, {
+ .prev = mpctx->osd_msg_stack,
+ .msg = "",
+ .id = id,
+ .level = level,
+ .time = time / 1000.0,
+ });
+ mpctx->osd_msg_stack = msg;
+ return msg;
+}
+
+static void set_osd_msg_va(struct MPContext *mpctx, int id, int level, int time,
+ const char *fmt, va_list ap)
+{
+ if (level == OSD_LEVEL_INVISIBLE)
+ return;
+ mp_osd_msg_t *msg = add_osd_msg(mpctx, id, level, time);
+ msg->msg = talloc_vasprintf(msg, fmt, ap);
+}
+
+void set_osd_msg(struct MPContext *mpctx, int id, int level, int time,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ set_osd_msg_va(mpctx, id, level, time, fmt, ap);
+ va_end(ap);
+}
+
+void set_osd_tmsg(struct MPContext *mpctx, int id, int level, int time,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ set_osd_msg_va(mpctx, id, level, time, mp_gtext(fmt), ap);
+ va_end(ap);
+}
+
+/**
+ * \brief Remove a message from the OSD stack
+ *
+ * This function can be used to get rid of a message right away.
+ *
+ */
+
+void rm_osd_msg(struct MPContext *mpctx, int id)
+{
+ mp_osd_msg_t *msg, *last = NULL;
+
+ // Search for the msg
+ for (msg = mpctx->osd_msg_stack; msg && msg->id != id;
+ last = msg, msg = msg->prev) ;
+ if (!msg)
+ return;
+
+ // Detach it from the stack and free it
+ if (last)
+ last->prev = msg->prev;
+ else
+ mpctx->osd_msg_stack = msg->prev;
+ talloc_free(msg);
+}
+
+/**
+ * \brief Get the current message from the OSD stack.
+ *
+ * This function decrements the message timer and destroys the old ones.
+ * The message that should be displayed is returned (if any).
+ *
+ */
+
+static mp_osd_msg_t *get_osd_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ mp_osd_msg_t *msg, *prev, *last = NULL;
+ double now = mp_time_sec();
+ double diff;
+ char hidden_dec_done = 0;
+
+ if (mpctx->osd_visible && now >= mpctx->osd_visible) {
+ mpctx->osd_visible = 0;
+ mpctx->osd->progbar_type = -1; // disable
+ osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
+ }
+ if (mpctx->osd_function_visible && now >= mpctx->osd_function_visible) {
+ mpctx->osd_function_visible = 0;
+ mpctx->osd_function = 0;
+ }
+
+ if (!mpctx->osd_last_update)
+ mpctx->osd_last_update = now;
+ diff = now >= mpctx->osd_last_update ? now - mpctx->osd_last_update : 0;
+
+ mpctx->osd_last_update = now;
+
+ // Look for the first message in the stack with high enough level.
+ for (msg = mpctx->osd_msg_stack; msg; last = msg, msg = prev) {
+ prev = msg->prev;
+ if (msg->level > opts->osd_level && hidden_dec_done)
+ continue;
+ // The message has a high enough level or it is the first hidden one
+ // in both cases we decrement the timer or kill it.
+ if (!msg->started || msg->time > diff) {
+ if (msg->started)
+ msg->time -= diff;
+ else
+ msg->started = 1;
+ // display it
+ if (msg->level <= opts->osd_level)
+ return msg;
+ hidden_dec_done = 1;
+ continue;
+ }
+ // kill the message
+ talloc_free(msg);
+ if (last) {
+ last->prev = prev;
+ msg = last;
+ } else {
+ mpctx->osd_msg_stack = prev;
+ msg = NULL;
+ }
+ }
+ // Nothing found
+ return NULL;
+}
+
+// type: mp_osd_font_codepoints, ASCII, or OSD_BAR_*
+// name: fallback for terminal OSD
+void set_osd_bar(struct MPContext *mpctx, int type, const char *name,
+ double min, double max, double val)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->osd_level < 1 || !opts->osd_bar_visible)
+ return;
+
+ if (mpctx->video_out && opts->term_osd != 1) {
+ 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;
+ osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
+ return;
+ }
+
+ set_osd_msg(mpctx, OSD_MSG_BAR, 1, opts->osd_duration, "%s: %d %%",
+ name, ROUND(100 * (val - min) / (max - min)));
+}
+
+// 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) {
+ float new_value = (val - min) / (max - min);
+ if (new_value != mpctx->osd->progbar_value) {
+ mpctx->osd->progbar_value = new_value;
+ osd_changed(mpctx->osd, OSDTYPE_PROGBAR);
+ }
+ }
+}
+
+static void set_osd_bar_chapters(struct MPContext *mpctx, int type)
+{
+ struct osd_state *osd = mpctx->osd;
+ osd->progbar_num_stops = 0;
+ if (osd->progbar_type == type) {
+ double len = get_time_length(mpctx);
+ if (len > 0) {
+ 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(osd, osd->progbar_stops,
+ osd->progbar_num_stops, pos);
+ }
+ }
+ }
+ }
+}
+
+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;
+}
+
+/**
+ * \brief Display text subtitles on the OSD
+ */
+static void set_osd_subtitle(struct MPContext *mpctx, const char *text)
+{
+ if (!text)
+ text = "";
+ if (strcmp(mpctx->osd->sub_text, text) != 0) {
+ osd_set_sub(mpctx->osd, text);
+ if (!mpctx->video_out) {
+ rm_osd_msg(mpctx, OSD_MSG_SUB_BASE);
+ if (text && text[0])
+ set_osd_msg(mpctx, OSD_MSG_SUB_BASE, 1, INT_MAX, "%s", text);
+ }
+ }
+ if (!text[0])
+ rm_osd_msg(mpctx, OSD_MSG_SUB_BASE);
+}
+
+// sym == mpctx->osd_function
+static void saddf_osd_function_sym(char **buffer, int sym)
+{
+ char temp[10];
+ osd_get_function_sym(temp, sizeof(temp), sym);
+ saddf(buffer, "%s ", temp);
+}
+
+static void sadd_osd_status(char **buffer, struct MPContext *mpctx, bool full)
+{
+ bool fractions = mpctx->opts->osd_fractions;
+ int sym = mpctx->osd_function;
+ if (!sym) {
+ if (mpctx->paused_for_cache && !mpctx->opts->pause) {
+ sym = OSD_CLOCK;
+ } else if (mpctx->paused || mpctx->step_frames) {
+ sym = OSD_PAUSE;
+ } else {
+ sym = OSD_PLAY;
+ }
+ }
+ saddf_osd_function_sym(buffer, sym);
+ char *custom_msg = mpctx->opts->osd_status_msg;
+ if (custom_msg && full) {
+ char *text = mp_property_expand_string(mpctx, custom_msg);
+ *buffer = talloc_strdup_append(*buffer, text);
+ talloc_free(text);
+ } else {
+ sadd_hhmmssff(buffer, get_current_time(mpctx), fractions);
+ if (full) {
+ saddf(buffer, " / ");
+ sadd_hhmmssff(buffer, get_time_length(mpctx), fractions);
+ sadd_percentage(buffer, get_percent_pos(mpctx));
+ int cache = mp_get_cache_percent(mpctx);
+ if (cache >= 0)
+ saddf(buffer, " Cache: %d%%", cache);
+ }
+ }
+}
+
+// OSD messages initated 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) {
+ set_osd_bar(mpctx, OSD_BAR_SEEK, "Position", 0, 1,
+ av_clipf(get_current_pos_ratio(mpctx, false), 0, 1));
+ set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_TEXT) {
+ mp_osd_msg_t *msg = add_osd_msg(mpctx, OSD_MSG_TEXT, 1,
+ mpctx->opts->osd_duration);
+ msg->show_position = true;
+ }
+ if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CHAPTER_TEXT) {
+ char *chapter = chapter_display_name(mpctx, get_current_chapter(mpctx));
+ set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration,
+ "Chapter: %s", chapter);
+ talloc_free(chapter);
+ }
+ if ((mpctx->add_osd_seek_info & OSD_SEEK_INFO_EDITION)
+ && mpctx->master_demuxer)
+ {
+ set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration,
+ "Playing edition %d of %d.",
+ mpctx->master_demuxer->edition + 1,
+ mpctx->master_demuxer->num_editions);
+ }
+ mpctx->add_osd_seek_info = 0;
+}
+
+/**
+ * \brief Update the OSD message line.
+ *
+ * This function displays the current message on the vo OSD or on the term.
+ * If the stack is empty and the OSD level is high enough the timer
+ * is displayed (only on the vo OSD).
+ *
+ */
+
+static void update_osd_msg(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct osd_state *osd = mpctx->osd;
+
+ add_seek_osd_messages(mpctx);
+ update_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1,
+ av_clipf(get_current_pos_ratio(mpctx, false), 0, 1));
+
+ // Look if we have a msg
+ mp_osd_msg_t *msg = get_osd_msg(mpctx);
+ if (msg && !msg->show_position) {
+ if (mpctx->video_out && opts->term_osd != 1) {
+ osd_set_text(osd, msg->msg);
+ } else if (opts->term_osd) {
+ if (strcmp(mpctx->terminal_osd_text, msg->msg)) {
+ talloc_free(mpctx->terminal_osd_text);
+ mpctx->terminal_osd_text = talloc_strdup(mpctx, msg->msg);
+ // Multi-line message => clear what will be the second line
+ write_status_line(mpctx, "");
+ mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s%s\n", opts->term_osd_esc,
+ mpctx->terminal_osd_text);
+ print_status(mpctx);
+ }
+ }
+ return;
+ }
+
+ int osd_level = opts->osd_level;
+ if (msg && msg->show_position)
+ osd_level = 3;
+
+ if (mpctx->video_out && opts->term_osd != 1) {
+ // fallback on the timer
+ char *text = NULL;
+
+ if (osd_level >= 2)
+ sadd_osd_status(&text, mpctx, osd_level == 3);
+
+ osd_set_text(osd, text);
+ talloc_free(text);
+ return;
+ }
+
+ // Clear the term osd line
+ if (opts->term_osd && mpctx->terminal_osd_text[0]) {
+ mpctx->terminal_osd_text[0] = '\0';
+ mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s\n", opts->term_osd_esc);
+ }
+}
+
+static int build_afilter_chain(struct MPContext *mpctx)
+{
+ struct sh_audio *sh_audio = mpctx->sh_audio;
+ struct ao *ao = mpctx->ao;
+ struct MPOpts *opts = mpctx->opts;
+ int new_srate;
+ if (af_control_any_rev(sh_audio->afilter,
+ AF_CONTROL_PLAYBACK_SPEED | AF_CONTROL_SET,
+ &opts->playback_speed))
+ new_srate = sh_audio->samplerate;
+ else {
+ new_srate = sh_audio->samplerate * opts->playback_speed;
+ if (new_srate != ao->samplerate) {
+ // limits are taken from libaf/af_resample.c
+ if (new_srate < 8000)
+ new_srate = 8000;
+ if (new_srate > 192000)
+ new_srate = 192000;
+ opts->playback_speed = (double)new_srate / sh_audio->samplerate;
+ }
+ }
+ return init_audio_filters(sh_audio, new_srate,
+ &ao->samplerate, &ao->channels, &ao->format);
+}
+
+static int recreate_audio_filters(struct MPContext *mpctx)
+{
+ assert(mpctx->sh_audio);
+
+ // init audio filters:
+ if (!build_afilter_chain(mpctx)) {
+ MP_ERR(mpctx, "Couldn't find matching filter/ao format!\n");
+ return -1;
+ }
+
+ mixer_reinit_audio(mpctx->mixer, mpctx->ao, mpctx->sh_audio->afilter);
+
+ return 0;
+}
+
+int reinit_audio_filters(struct MPContext *mpctx)
+{
+ struct sh_audio *sh_audio = mpctx->sh_audio;
+ if (!sh_audio)
+ return -2;
+
+ af_uninit(mpctx->sh_audio->afilter);
+ if (af_init(mpctx->sh_audio->afilter) < 0)
+ return -1;
+ if (recreate_audio_filters(mpctx) < 0)
+ return -1;
+
+ return 0;
+}
+
+void reinit_audio_chain(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ init_demux_stream(mpctx, STREAM_AUDIO);
+ if (!mpctx->sh_audio) {
+ uninit_player(mpctx, INITIALIZED_AO);
+ goto no_audio;
+ }
+
+ if (!(mpctx->initialized_flags & INITIALIZED_ACODEC)) {
+ if (!init_best_audio_codec(mpctx->sh_audio, opts->audio_decoders))
+ goto init_error;
+ mpctx->initialized_flags |= INITIALIZED_ACODEC;
+ }
+
+ int ao_srate = opts->force_srate;
+ int ao_format = opts->audio_output_format;
+ struct mp_chmap ao_channels = {0};
+ if (mpctx->initialized_flags & INITIALIZED_AO) {
+ ao_srate = mpctx->ao->samplerate;
+ ao_format = mpctx->ao->format;
+ ao_channels = mpctx->ao->channels;
+ } else {
+ // Automatic downmix
+ if (mp_chmap_is_stereo(&opts->audio_output_channels) &&
+ !mp_chmap_is_stereo(&mpctx->sh_audio->channels))
+ {
+ mp_chmap_from_channels(&ao_channels, 2);
+ }
+ }
+
+ // Determine what the filter chain outputs. build_afilter_chain() also
+ // needs this for testing whether playback speed is changed by resampling
+ // or using a special filter.
+ if (!init_audio_filters(mpctx->sh_audio, // preliminary init
+ // input:
+ mpctx->sh_audio->samplerate,
+ // output:
+ &ao_srate, &ao_channels, &ao_format)) {
+ MP_ERR(mpctx, "Error at audio filter chain pre-init!\n");
+ goto init_error;
+ }
+
+ if (!(mpctx->initialized_flags & INITIALIZED_AO)) {
+ mpctx->initialized_flags |= INITIALIZED_AO;
+ mp_chmap_remove_useless_channels(&ao_channels,
+ &opts->audio_output_channels);
+ mpctx->ao = ao_init_best(mpctx->global, mpctx->input,
+ mpctx->encode_lavc_ctx, ao_srate, ao_format,
+ ao_channels);
+ struct ao *ao = mpctx->ao;
+ if (!ao) {
+ MP_ERR(mpctx, "Could not open/initialize audio device -> no sound.\n");
+ goto init_error;
+ }
+ ao->buffer.start = talloc_new(ao);
+ char *s = mp_audio_fmt_to_str(ao->samplerate, &ao->channels, ao->format);
+ MP_INFO(mpctx, "AO: [%s] %s\n", ao->driver->name, s);
+ talloc_free(s);
+ MP_VERBOSE(mpctx, "AO: Description: %s\n", ao->driver->description);
+ }
+
+ if (recreate_audio_filters(mpctx) < 0)
+ goto init_error;
+
+ mpctx->syncing_audio = true;
+ return;
+
+init_error:
+ uninit_player(mpctx, INITIALIZED_ACODEC | INITIALIZED_AO);
+ cleanup_demux_stream(mpctx, STREAM_AUDIO);
+no_audio:
+ mpctx->current_track[STREAM_AUDIO] = NULL;
+ MP_INFO(mpctx, "Audio: no audio\n");
+}
+
+
+// Return pts value corresponding to the end point of audio written to the
+// ao so far.
+static double written_audio_pts(struct MPContext *mpctx)
+{
+ sh_audio_t *sh_audio = mpctx->sh_audio;
+ if (!sh_audio)
+ return MP_NOPTS_VALUE;
+
+ double bps = sh_audio->channels.num * sh_audio->samplerate *
+ sh_audio->samplesize;
+
+ // first calculate the end pts of audio that has been output by decoder
+ double a_pts = sh_audio->pts;
+ if (a_pts == MP_NOPTS_VALUE)
+ return MP_NOPTS_VALUE;
+
+ // sh_audio->pts is the timestamp of the latest input packet with
+ // known pts that the decoder has decoded. sh_audio->pts_bytes is
+ // the amount of bytes the decoder has written after that timestamp.
+ a_pts += sh_audio->pts_bytes / bps;
+
+ // Now a_pts hopefully holds the pts for end of audio from decoder.
+ // Subtract data in buffers between decoder and audio out.
+
+ // Decoded but not filtered
+ a_pts -= sh_audio->a_buffer_len / bps;
+
+ // Data buffered in audio filters, measured in bytes of "missing" output
+ double buffered_output = af_calc_delay(sh_audio->afilter);
+
+ // Data that was ready for ao but was buffered because ao didn't fully
+ // accept everything to internal buffers yet
+ buffered_output += mpctx->ao->buffer.len;
+
+ // Filters divide audio length by playback_speed, so multiply by it
+ // to get the length in original units without speedup or slowdown
+ a_pts -= buffered_output * mpctx->opts->playback_speed / mpctx->ao->bps;
+
+ return a_pts + mpctx->video_offset;
+}
+
+// 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)
+ return pts;
+ return pts - mpctx->opts->playback_speed * ao_get_delay(mpctx->ao);
+}
+
+// When reading subtitles from a demuxer, and we read video or audio from the
+// demuxer, we should not explicitly read subtitle packets. (With external
+// subs, we have to.)
+static bool is_interleaved(struct MPContext *mpctx, struct track *track)
+{
+ if (track->is_external || !track->demuxer)
+ return false;
+
+ struct demuxer *demuxer = track->demuxer;
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ struct track *other = mpctx->current_track[type];
+ if (other && other != track && other->demuxer && other->demuxer == demuxer)
+ return true;
+ }
+ return false;
+}
+
+static void reset_subtitles(struct MPContext *mpctx)
+{
+ if (mpctx->sh_sub)
+ sub_reset(mpctx->sh_sub->dec_sub);
+ set_osd_subtitle(mpctx, NULL);
+ osd_changed(mpctx->osd, OSDTYPE_SUB);
+}
+
+static void update_subtitles(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!(mpctx->initialized_flags & INITIALIZED_SUB))
+ return;
+
+ struct track *track = mpctx->current_track[STREAM_SUB];
+ struct sh_sub *sh_sub = mpctx->sh_sub;
+ assert(track && sh_sub);
+ struct dec_sub *dec_sub = sh_sub->dec_sub;
+
+ if (mpctx->sh_video && mpctx->sh_video->vf_input) {
+ struct mp_image_params params = *mpctx->sh_video->vf_input;
+ sub_control(dec_sub, SD_CTRL_SET_VIDEO_PARAMS, &params);
+ }
+
+ mpctx->osd->video_offset = track->under_timeline ? mpctx->video_offset : 0;
+
+ double refpts_s = mpctx->playback_pts - mpctx->osd->video_offset;
+ double curpts_s = refpts_s + opts->sub_delay;
+
+ if (!track->preloaded) {
+ bool interleaved = is_interleaved(mpctx, track);
+
+ while (1) {
+ if (interleaved && !demux_has_packet(sh_sub->gsh))
+ break;
+ double subpts_s = demux_get_next_pts(sh_sub->gsh);
+ if (!demux_has_packet(sh_sub->gsh))
+ break;
+ if (subpts_s > curpts_s) {
+ mp_dbg(MSGT_CPLAYER, MSGL_DBG2,
+ "Sub early: c_pts=%5.3f s_pts=%5.3f\n",
+ curpts_s, subpts_s);
+ // Libass handled subs can be fed to it in advance
+ if (!sub_accept_packets_in_advance(dec_sub))
+ break;
+ // Try to avoid demuxing whole file at once
+ if (subpts_s > curpts_s + 1 && !interleaved)
+ break;
+ }
+ struct demux_packet *pkt = demux_read_packet(sh_sub->gsh);
+ mp_dbg(MSGT_CPLAYER, MSGL_V, "Sub: c_pts=%5.3f s_pts=%5.3f "
+ "duration=%5.3f len=%d\n", curpts_s, pkt->pts, pkt->duration,
+ pkt->len);
+ sub_decode(dec_sub, pkt);
+ talloc_free(pkt);
+ }
+ }
+
+ if (!mpctx->osd->render_bitmap_subs || !mpctx->video_out)
+ set_osd_subtitle(mpctx, sub_get_text(dec_sub, curpts_s));
+}
+
+static int check_framedrop(struct MPContext *mpctx, double frame_time)
+{
+ struct MPOpts *opts = mpctx->opts;
+ // check for frame-drop:
+ if (mpctx->sh_audio && !mpctx->ao->untimed &&
+ !demux_stream_eof(mpctx->sh_audio->gsh))
+ {
+ float delay = opts->playback_speed * ao_get_delay(mpctx->ao);
+ float d = delay - mpctx->delay;
+ if (frame_time < 0)
+ frame_time = mpctx->sh_video->fps > 0 ? 1.0 / mpctx->sh_video->fps : 0;
+ // we should avoid dropping too many frames in sequence unless we
+ // are too late. and we allow 100ms A-V delay here:
+ if (d < -mpctx->dropped_frames * frame_time - 0.100 && !mpctx->paused
+ && !mpctx->restart_playback) {
+ mpctx->drop_frame_cnt++;
+ mpctx->dropped_frames++;
+ return mpctx->opts->frame_dropping;
+ } else
+ mpctx->dropped_frames = 0;
+ }
+ return 0;
+}
+
+static double timing_sleep(struct MPContext *mpctx, double time_frame)
+{
+ // assume kernel HZ=100 for softsleep, works with larger HZ but with
+ // unnecessarily high CPU usage
+ struct MPOpts *opts = mpctx->opts;
+ double margin = opts->softsleep ? 0.011 : 0;
+ while (time_frame > margin) {
+ mp_sleep_us(1000000 * (time_frame - margin));
+ time_frame -= get_relative_time(mpctx);
+ }
+ if (opts->softsleep) {
+ if (time_frame < 0)
+ MP_WARN(mpctx, "Warning! Softsleep underflow!\n");
+ while (time_frame > 0)
+ time_frame -= get_relative_time(mpctx); // burn the CPU
+ }
+ return time_frame;
+}
+
+static void set_dvdsub_fake_extradata(struct dec_sub *dec_sub, struct stream *st,
+ int width, int height)
+{
+#ifdef CONFIG_DVDREAD
+ if (!st)
+ return;
+
+ struct stream_dvd_info_req info;
+ if (stream_control(st, STREAM_CTRL_GET_DVD_INFO, &info) < 0)
+ return;
+
+ struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS;
+ csp.int_bits_in = 8;
+ csp.int_bits_out = 8;
+ float cmatrix[3][4];
+ mp_get_yuv2rgb_coeffs(&csp, cmatrix);
+
+ if (width == 0 || height == 0) {
+ width = 720;
+ height = 480;
+ }
+
+ char *s = NULL;
+ s = talloc_asprintf_append(s, "size: %dx%d\n", width, height);
+ s = talloc_asprintf_append(s, "palette: ");
+ for (int i = 0; i < 16; i++) {
+ int color = info.palette[i];
+ int c[3] = {(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff};
+ mp_map_int_color(cmatrix, 8, c);
+ color = (c[2] << 16) | (c[1] << 8) | c[0];
+
+ if (i != 0)
+ talloc_asprintf_append(s, ", ");
+ s = talloc_asprintf_append(s, "%06x", color);
+ }
+ s = talloc_asprintf_append(s, "\n");
+
+ sub_set_extradata(dec_sub, s, strlen(s));
+ talloc_free(s);
+#endif
+}
+
+static void reinit_subs(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct track *track = mpctx->current_track[STREAM_SUB];
+
+ assert(!(mpctx->initialized_flags & INITIALIZED_SUB));
+
+ init_demux_stream(mpctx, STREAM_SUB);
+ if (!mpctx->sh_sub)
+ return;
+
+ if (!mpctx->sh_sub->dec_sub)
+ mpctx->sh_sub->dec_sub = sub_create(opts);
+
+ assert(track->demuxer);
+ // Lazily added DVD track - will be created on first sub packet
+ if (!track->stream)
+ return;
+
+ mpctx->initialized_flags |= INITIALIZED_SUB;
+
+ struct sh_sub *sh_sub = mpctx->sh_sub;
+ struct dec_sub *dec_sub = sh_sub->dec_sub;
+ assert(dec_sub);
+
+ if (!sub_is_initialized(dec_sub)) {
+ int w = mpctx->sh_video ? mpctx->sh_video->disp_w : 0;
+ int h = mpctx->sh_video ? mpctx->sh_video->disp_h : 0;
+ float fps = mpctx->sh_video ? mpctx->sh_video->fps : 25;
+
+ set_dvdsub_fake_extradata(dec_sub, track->demuxer->stream, w, h);
+ sub_set_video_res(dec_sub, w, h);
+ sub_set_video_fps(dec_sub, fps);
+ sub_set_ass_renderer(dec_sub, mpctx->osd->ass_library,
+ mpctx->osd->ass_renderer);
+ sub_init_from_sh(dec_sub, sh_sub);
+
+ // Don't do this if the file has video/audio streams. Don't do it even
+ // if it has only sub streams, because reading packets will change the
+ // demuxer position.
+ if (!track->preloaded && track->is_external) {
+ demux_seek(track->demuxer, 0, SEEK_ABSOLUTE);
+ track->preloaded = sub_read_all_packets(dec_sub, sh_sub);
+ }
+ }
+
+ mpctx->osd->dec_sub = dec_sub;
+
+ // Decides whether to use OSD path or normal subtitle rendering path.
+ mpctx->osd->render_bitmap_subs =
+ opts->ass_enabled || !sub_has_get_text(dec_sub);
+
+ reset_subtitles(mpctx);
+}
+
+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;
+}
+
+void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
+ struct track *track)
+{
+ assert(!track || track->type == type);
+
+ struct track *current = mpctx->current_track[type];
+ if (track == current)
+ return;
+
+ if (type == STREAM_VIDEO) {
+ int uninit = INITIALIZED_VCODEC;
+ if (!mpctx->opts->force_vo)
+ uninit |= mpctx->opts->fixed_vo && track ? 0 : INITIALIZED_VO;
+ uninit_player(mpctx, uninit);
+ } else if (type == STREAM_AUDIO) {
+ uninit_player(mpctx, INITIALIZED_AO | INITIALIZED_ACODEC);
+ } else if (type == STREAM_SUB) {
+ uninit_player(mpctx, INITIALIZED_SUB);
+ }
+
+ mpctx->current_track[type] = track;
+
+ int user_tid = track ? track->user_tid : -2;
+ if (type == STREAM_VIDEO) {
+ mpctx->opts->video_id = user_tid;
+ reinit_video_chain(mpctx);
+ mp_notify_property(mpctx, "vid");
+ } else if (type == STREAM_AUDIO) {
+ mpctx->opts->audio_id = user_tid;
+ reinit_audio_chain(mpctx);
+ mp_notify_property(mpctx, "aid");
+ } else if (type == STREAM_SUB) {
+ mpctx->opts->sub_id = user_tid;
+ reinit_subs(mpctx);
+ mp_notify_property(mpctx, "sid");
+ }
+
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = talloc_steal(mpctx, track_layout_hash(mpctx));
+}
+
+struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type,
+ int tid)
+{
+ if (tid == -1)
+ return mpctx->current_track[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->under_timeline)
+ return false;
+ if (!track->is_external)
+ return false;
+
+ if (mpctx->current_track[track->type] == track) {
+ mp_switch_track(mpctx, track->type, NULL);
+ if (mpctx->current_track[track->type] == track)
+ return false;
+ }
+
+ int index = 0;
+ while (index < mpctx->num_tracks && mpctx->tracks[index] != track)
+ index++;
+ assert(index < mpctx->num_tracks);
+ while (index + 1 < mpctx->num_tracks) {
+ mpctx->tracks[index] = mpctx->tracks[index + 1];
+ index++;
+ }
+ mpctx->num_tracks--;
+ talloc_free(track);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
+ return true;
+}
+
+/* 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 frame_time)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!mpctx->sh_audio || mpctx->syncing_audio)
+ return;
+
+ double a_pts = written_audio_pts(mpctx) - mpctx->delay;
+ double v_pts = mpctx->sh_video->pts;
+ double av_delay = a_pts - v_pts;
+ // Try to sync vo_flip() so it will *finish* at given time
+ av_delay += mpctx->last_vo_flip_duration;
+ av_delay -= mpctx->audio_delay; // This much pts difference is desired
+
+ double change = av_delay * 0.1;
+ double max_change = opts->default_max_pts_correction >= 0 ?
+ opts->default_max_pts_correction : frame_time * 0.1;
+ if (change < -max_change)
+ change = -max_change;
+ else if (change > max_change)
+ change = max_change;
+ mpctx->delay += change;
+ mpctx->total_avsync_change += change;
+}
+
+static int write_to_ao(struct MPContext *mpctx, void *data, int len, int flags,
+ double pts)
+{
+ if (mpctx->paused)
+ return 0;
+ struct ao *ao = mpctx->ao;
+ double bps = ao->bps / mpctx->opts->playback_speed;
+ ao->pts = pts;
+ int played = ao_play(mpctx->ao, data, len, flags);
+ if (played > 0) {
+ mpctx->shown_aframes += played / (af_fmt2bits(ao->format) / 8);
+ mpctx->delay += played / bps;
+ // Keep correct pts for remaining data - could be used to flush
+ // remaining buffer when closing ao.
+ ao->pts += played / bps;
+ return played;
+ }
+ return 0;
+}
+
+#define ASYNC_PLAY_DONE -3
+static int audio_start_sync(struct MPContext *mpctx, int playsize)
+{
+ struct ao *ao = mpctx->ao;
+ struct MPOpts *opts = mpctx->opts;
+ sh_audio_t * const sh_audio = mpctx->sh_audio;
+ int res;
+
+ // Timing info may not be set without
+ res = decode_audio(sh_audio, &ao->buffer, 1);
+ if (res < 0)
+ return res;
+
+ int bytes;
+ bool did_retry = false;
+ double written_pts;
+ double bps = ao->bps / opts->playback_speed;
+ bool hrseek = mpctx->hrseek_active; // audio only hrseek
+ mpctx->hrseek_active = false;
+ while (1) {
+ written_pts = written_audio_pts(mpctx);
+ double ptsdiff;
+ if (hrseek)
+ ptsdiff = written_pts - mpctx->hrseek_pts;
+ else
+ ptsdiff = written_pts - mpctx->sh_video->pts - mpctx->delay
+ - mpctx->audio_delay;
+ bytes = ptsdiff * bps;
+ bytes -= bytes % (ao->channels.num * af_fmt2bits(ao->format) / 8);
+
+ // ogg demuxers give packets without timing
+ if (written_pts <= 1 && sh_audio->pts == MP_NOPTS_VALUE) {
+ if (!did_retry) {
+ // Try to read more data to see packets that have pts
+ res = decode_audio(sh_audio, &ao->buffer, ao->bps);
+ if (res < 0)
+ return res;
+ did_retry = true;
+ continue;
+ }
+ bytes = 0;
+ }
+
+ if (fabs(ptsdiff) > 300 || isnan(ptsdiff)) // pts reset or just broken?
+ bytes = 0;
+
+ if (bytes > 0)
+ break;
+
+ mpctx->syncing_audio = false;
+ int a = FFMIN(-bytes, FFMAX(playsize, 20000));
+ res = decode_audio(sh_audio, &ao->buffer, a);
+ bytes += ao->buffer.len;
+ if (bytes >= 0) {
+ memmove(ao->buffer.start,
+ ao->buffer.start + ao->buffer.len - bytes, bytes);
+ ao->buffer.len = bytes;
+ if (res < 0)
+ return res;
+ return decode_audio(sh_audio, &ao->buffer, playsize);
+ }
+ ao->buffer.len = 0;
+ if (res < 0)
+ return res;
+ }
+ if (hrseek)
+ // Don't add silence in audio-only case even if position is too late
+ return 0;
+ int fillbyte = 0;
+ if ((ao->format & AF_FORMAT_SIGN_MASK) == AF_FORMAT_US)
+ fillbyte = 0x80;
+ if (bytes >= playsize) {
+ /* This case could fall back to the one below with
+ * bytes = playsize, but then silence would keep accumulating
+ * in a_out_buffer if the AO accepts less data than it asks for
+ * in playsize. */
+ char *p = malloc(playsize);
+ memset(p, fillbyte, playsize);
+ write_to_ao(mpctx, p, playsize, 0, written_pts - bytes / bps);
+ free(p);
+ return ASYNC_PLAY_DONE;
+ }
+ mpctx->syncing_audio = false;
+ decode_audio_prepend_bytes(&ao->buffer, bytes, fillbyte);
+ return decode_audio(sh_audio, &ao->buffer, playsize);
+}
+
+static int fill_audio_out_buffers(struct MPContext *mpctx, double endpts)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct ao *ao = mpctx->ao;
+ int playsize;
+ int playflags = 0;
+ bool audio_eof = false;
+ bool partial_fill = false;
+ sh_audio_t * const sh_audio = mpctx->sh_audio;
+ bool modifiable_audio_format = !(ao->format & AF_FORMAT_SPECIAL_MASK);
+ int unitsize = ao->channels.num * af_fmt2bits(ao->format) / 8;
+
+ if (mpctx->paused)
+ playsize = 1; // just initialize things (audio pts at least)
+ else
+ playsize = ao_get_space(ao);
+
+ // Coming here with hrseek_active still set means audio-only
+ if (!mpctx->sh_video || !mpctx->sync_audio_to_video)
+ mpctx->syncing_audio = false;
+ if (!opts->initial_audio_sync || !modifiable_audio_format) {
+ mpctx->syncing_audio = false;
+ mpctx->hrseek_active = false;
+ }
+
+ int res;
+ if (mpctx->syncing_audio || mpctx->hrseek_active)
+ res = audio_start_sync(mpctx, playsize);
+ else
+ res = decode_audio(sh_audio, &ao->buffer, playsize);
+
+ if (res < 0) { // EOF, error or format change
+ if (res == -2) {
+ /* The format change isn't handled too gracefully. A more precise
+ * implementation would require draining buffered old-format audio
+ * while displaying video, then doing the output format switch.
+ */
+ if (!mpctx->opts->gapless_audio)
+ uninit_player(mpctx, INITIALIZED_AO);
+ reinit_audio_chain(mpctx);
+ return -1;
+ } else if (res == ASYNC_PLAY_DONE)
+ return 0;
+ else if (demux_stream_eof(mpctx->sh_audio->gsh))
+ audio_eof = true;
+ }
+
+ if (endpts != MP_NOPTS_VALUE && modifiable_audio_format) {
+ double bytes = (endpts - written_audio_pts(mpctx) + mpctx->audio_delay)
+ * ao->bps / opts->playback_speed;
+ if (playsize > bytes) {
+ playsize = FFMAX(bytes, 0);
+ playflags |= AOPLAY_FINAL_CHUNK;
+ audio_eof = true;
+ partial_fill = true;
+ }
+ }
+
+ assert(ao->buffer.len % unitsize == 0);
+ if (playsize > ao->buffer.len) {
+ partial_fill = true;
+ playsize = ao->buffer.len;
+ if (audio_eof)
+ playflags |= AOPLAY_FINAL_CHUNK;
+ }
+ playsize -= playsize % unitsize;
+ if (!playsize)
+ return partial_fill && audio_eof ? -2 : -partial_fill;
+
+ // play audio:
+
+ int played = write_to_ao(mpctx, ao->buffer.start, playsize, playflags,
+ written_audio_pts(mpctx));
+ assert(played % unitsize == 0);
+ ao->buffer_playable_size = playsize - played;
+
+ if (played > 0) {
+ ao->buffer.len -= played;
+ memmove(ao->buffer.start, ao->buffer.start + played, ao->buffer.len);
+ } else if (!mpctx->paused && audio_eof && ao_get_delay(ao) < .04) {
+ // Sanity check to avoid hanging in case current ao doesn't output
+ // partial chunks and doesn't check for AOPLAY_FINAL_CHUNK
+ return -2;
+ }
+
+ return -partial_fill;
+}
+
+static void update_fps(struct MPContext *mpctx)
+{
+#ifdef CONFIG_ENCODING
+ struct sh_video *sh_video = mpctx->sh_video;
+ if (mpctx->encode_lavc_ctx && sh_video)
+ encode_lavc_set_video_fps(mpctx->encode_lavc_ctx, sh_video->fps);
+#endif
+}
+
+static void recreate_video_filters(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct sh_video *sh_video = mpctx->sh_video;
+ assert(sh_video);
+
+ vf_uninit_filter_chain(sh_video->vfilter);
+
+ char *vf_arg[] = {
+ "_oldargs_", (char *)mpctx->video_out, NULL
+ };
+ sh_video->vfilter = vf_open_filter(opts, NULL, "vo", vf_arg);
+
+ sh_video->vfilter = append_filters(sh_video->vfilter, opts->vf_settings);
+
+ struct vf_instance *vf = sh_video->vfilter;
+ mpctx->osd->render_subs_in_filter
+ = vf->control(vf, VFCTRL_INIT_OSD, NULL) == VO_TRUE;
+}
+
+int reinit_video_filters(struct MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+
+ if (!sh_video)
+ return -2;
+
+ recreate_video_filters(mpctx);
+ video_reinit_vo(sh_video);
+
+ return sh_video->vf_initialized > 0 ? 0 : -1;
+}
+
+int reinit_video_chain(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ assert(!(mpctx->initialized_flags & INITIALIZED_VCODEC));
+ init_demux_stream(mpctx, STREAM_VIDEO);
+ sh_video_t *sh_video = mpctx->sh_video;
+ if (!sh_video)
+ goto no_video;
+
+ MP_VERBOSE(mpctx, "[V] fourcc:0x%X size:%dx%d fps:%5.3f\n",
+ mpctx->sh_video->format,
+ mpctx->sh_video->disp_w, mpctx->sh_video->disp_h,
+ mpctx->sh_video->fps);
+ if (opts->force_fps)
+ mpctx->sh_video->fps = opts->force_fps;
+ update_fps(mpctx);
+
+ if (!mpctx->sh_video->fps && !opts->force_fps && !opts->correct_pts) {
+ MP_ERR(mpctx, "FPS not specified in the "
+ "header or invalid, use the -fps option.\n");
+ }
+
+ double ar = -1.0;
+ //================== Init VIDEO (codec & libvo) ==========================
+ if (!opts->fixed_vo || !(mpctx->initialized_flags & INITIALIZED_VO)) {
+ mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input,
+ mpctx->encode_lavc_ctx);
+ if (!mpctx->video_out) {
+ MP_FATAL(mpctx, "Error opening/initializing "
+ "the selected video_out (-vo) device.\n");
+ goto err_out;
+ }
+ mpctx->mouse_cursor_visible = true;
+ mpctx->initialized_flags |= INITIALIZED_VO;
+ }
+
+ // dynamic allocation only to make stheader.h lighter
+ talloc_free(sh_video->hwdec_info);
+ sh_video->hwdec_info = talloc_zero(sh_video, struct mp_hwdec_info);
+ vo_control(mpctx->video_out, VOCTRL_GET_HWDEC_INFO, sh_video->hwdec_info);
+
+ vo_update_window_title(mpctx);
+
+ if (stream_control(mpctx->sh_video->gsh->demuxer->stream,
+ STREAM_CTRL_GET_ASPECT_RATIO, &ar) != STREAM_UNSUPPORTED)
+ mpctx->sh_video->stream_aspect = ar;
+
+ recreate_video_filters(mpctx);
+
+ init_best_video_codec(sh_video, opts->video_decoders);
+
+ if (!sh_video->initialized)
+ goto err_out;
+
+ mpctx->initialized_flags |= INITIALIZED_VCODEC;
+
+ bool saver_state = opts->pause || !opts->stop_screensaver;
+ vo_control(mpctx->video_out, saver_state ? VOCTRL_RESTORE_SCREENSAVER
+ : VOCTRL_KILL_SCREENSAVER, NULL);
+
+ vo_control(mpctx->video_out, mpctx->paused ? VOCTRL_PAUSE
+ : VOCTRL_RESUME, NULL);
+
+ sh_video->last_pts = MP_NOPTS_VALUE;
+ sh_video->num_buffered_pts = 0;
+ sh_video->next_frame_time = 0;
+ mpctx->last_vf_reconfig_count = 0;
+ mpctx->restart_playback = true;
+ mpctx->sync_audio_to_video = !sh_video->gsh->attached_picture;
+ mpctx->delay = 0;
+ mpctx->vo_pts_history_seek_ts++;
+
+ vo_seek_reset(mpctx->video_out);
+ reset_subtitles(mpctx);
+
+ return 1;
+
+err_out:
+no_video:
+ uninit_player(mpctx, INITIALIZED_VCODEC | (opts->force_vo ? 0 : INITIALIZED_VO));
+ cleanup_demux_stream(mpctx, STREAM_VIDEO);
+ handle_force_window(mpctx, true);
+ MP_INFO(mpctx, "Video: no video\n");
+ return 0;
+}
+
+// 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;
+
+ // If not paused, the next frame should come soon enough.
+ if (opts->pause && mpctx->last_vo_pts != MP_NOPTS_VALUE)
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, mpctx->last_vo_pts, 1);
+}
+
+static void add_frame_pts(struct MPContext *mpctx, double pts)
+{
+ if (pts == MP_NOPTS_VALUE || mpctx->hrseek_framedrop) {
+ mpctx->vo_pts_history_seek_ts++; // mark discontinuity
+ return;
+ }
+ for (int n = MAX_NUM_VO_PTS - 1; n >= 1; n--) {
+ mpctx->vo_pts_history_seek[n] = mpctx->vo_pts_history_seek[n - 1];
+ mpctx->vo_pts_history_pts[n] = mpctx->vo_pts_history_pts[n - 1];
+ }
+ mpctx->vo_pts_history_seek[0] = mpctx->vo_pts_history_seek_ts;
+ mpctx->vo_pts_history_pts[0] = pts;
+}
+
+static double find_previous_pts(struct MPContext *mpctx, double pts)
+{
+ for (int n = 0; n < MAX_NUM_VO_PTS - 1; n++) {
+ if (pts == mpctx->vo_pts_history_pts[n] &&
+ mpctx->vo_pts_history_seek[n] != 0 &&
+ mpctx->vo_pts_history_seek[n] == mpctx->vo_pts_history_seek[n + 1])
+ {
+ return mpctx->vo_pts_history_pts[n + 1];
+ }
+ }
+ return MP_NOPTS_VALUE;
+}
+
+static double get_last_frame_pts(struct MPContext *mpctx)
+{
+ if (mpctx->vo_pts_history_seek[0] == mpctx->vo_pts_history_seek_ts)
+ return mpctx->vo_pts_history_pts[0];
+ return MP_NOPTS_VALUE;
+}
+
+static bool filter_output_queued_frame(struct MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+ struct vo *video_out = mpctx->video_out;
+
+ struct mp_image *img = vf_chain_output_queued_frame(sh_video->vfilter);
+ if (img)
+ vo_queue_image(video_out, img);
+ talloc_free(img);
+
+ return !!img;
+}
+
+static bool load_next_vo_frame(struct MPContext *mpctx, bool eof)
+{
+ if (vo_get_buffered_frame(mpctx->video_out, eof) >= 0)
+ return true;
+ if (filter_output_queued_frame(mpctx))
+ return true;
+ return false;
+}
+
+static void init_filter_params(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct sh_video *sh_video = mpctx->sh_video;
+
+ // Note that the video decoder already initializes the filter chain. This
+ // might recreate the chain a second time, which is not very elegant, but
+ // allows us to test whether enabling deinterlacing works with the current
+ // video format and other filters.
+ if (sh_video->vf_initialized != 1)
+ return;
+
+ if (sh_video->vf_reconfig_count <= mpctx->last_vf_reconfig_count) {
+ if (opts->deinterlace >= 0) {
+ mp_property_do("deinterlace", M_PROPERTY_SET, &opts->deinterlace,
+ mpctx);
+ }
+ }
+ // Setting filter params has to be "stable" (no change if params already
+ // set) - checking the reconfig count is just an optimization.
+ mpctx->last_vf_reconfig_count = sh_video->vf_reconfig_count;
+}
+
+static void filter_video(struct MPContext *mpctx, struct mp_image *frame)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+
+ init_filter_params(mpctx);
+
+ frame->pts = sh_video->pts;
+ mp_image_set_params(frame, sh_video->vf_input);
+ vf_filter_frame(sh_video->vfilter, frame);
+ filter_output_queued_frame(mpctx);
+}
+
+
+static struct demux_packet *video_read_frame(struct MPContext *mpctx)
+{
+ sh_video_t *sh_video = mpctx->sh_video;
+ demuxer_t *demuxer = sh_video->gsh->demuxer;
+ float pts1 = sh_video->last_pts;
+
+ struct demux_packet *pkt = demux_read_packet(sh_video->gsh);
+ if (!pkt)
+ return NULL; // EOF
+
+ if (pkt->pts != MP_NOPTS_VALUE)
+ sh_video->last_pts = pkt->pts;
+
+ float frame_time = sh_video->fps > 0 ? 1.0f / sh_video->fps : 0;
+
+ // override frame_time for variable/unknown FPS formats:
+ if (!mpctx->opts->force_fps) {
+ double next_pts = demux_get_next_pts(sh_video->gsh);
+ double d = next_pts == MP_NOPTS_VALUE ? sh_video->last_pts - pts1
+ : next_pts - sh_video->last_pts;
+ if (d >= 0) {
+ if (demuxer->type == DEMUXER_TYPE_TV) {
+ if (d > 0)
+ sh_video->fps = 1.0f / d;
+ frame_time = d;
+ } else {
+ if ((int)sh_video->fps <= 1)
+ frame_time = d;
+ }
+ }
+ }
+
+ sh_video->pts = sh_video->last_pts;
+ sh_video->next_frame_time = frame_time;
+ return pkt;
+}
+
+static double update_video_nocorrect_pts(struct MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+ double frame_time = 0;
+ while (1) {
+ // In nocorrect-pts mode there is no way to properly time these frames
+ if (load_next_vo_frame(mpctx, false))
+ break;
+ frame_time = sh_video->next_frame_time;
+ if (mpctx->restart_playback)
+ frame_time = 0;
+ struct demux_packet *pkt = video_read_frame(mpctx);
+ if (!pkt)
+ return -1;
+ if (mpctx->sh_audio)
+ mpctx->delay -= frame_time;
+ // video_read_frame can change fps (e.g. for ASF video)
+ update_fps(mpctx);
+ int framedrop_type = check_framedrop(mpctx, frame_time);
+
+ void *decoded_frame = decode_video(sh_video, pkt, framedrop_type,
+ sh_video->pts);
+ talloc_free(pkt);
+ if (decoded_frame) {
+ filter_video(mpctx, decoded_frame);
+ }
+ break;
+ }
+ return frame_time;
+}
+
+static double update_video_attached_pic(struct MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+
+ // Try to decode the picture multiple times, until it is displayed.
+ if (mpctx->video_out->hasframe)
+ return -1;
+
+ struct mp_image *decoded_frame =
+ decode_video(sh_video, sh_video->gsh->attached_picture, 0, 0);
+ if (decoded_frame)
+ filter_video(mpctx, decoded_frame);
+ load_next_vo_frame(mpctx, true);
+ mpctx->sh_video->pts = MP_NOPTS_VALUE;
+ return 0;
+}
+
+static void determine_frame_pts(struct MPContext *mpctx)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->user_pts_assoc_mode)
+ sh_video->pts_assoc_mode = opts->user_pts_assoc_mode;
+ else if (sh_video->pts_assoc_mode == 0) {
+ if (mpctx->sh_video->gsh->demuxer->timestamp_type == TIMESTAMP_TYPE_PTS
+ && sh_video->codec_reordered_pts != MP_NOPTS_VALUE)
+ sh_video->pts_assoc_mode = 1;
+ else
+ sh_video->pts_assoc_mode = 2;
+ } else {
+ int probcount1 = sh_video->num_reordered_pts_problems;
+ int probcount2 = sh_video->num_sorted_pts_problems;
+ if (sh_video->pts_assoc_mode == 2) {
+ int tmp = probcount1;
+ probcount1 = probcount2;
+ probcount2 = tmp;
+ }
+ if (probcount1 >= probcount2 * 1.5 + 2) {
+ sh_video->pts_assoc_mode = 3 - sh_video->pts_assoc_mode;
+ MP_VERBOSE(mpctx, "Switching to pts association mode "
+ "%d.\n", sh_video->pts_assoc_mode);
+ }
+ }
+ sh_video->pts = sh_video->pts_assoc_mode == 1 ?
+ sh_video->codec_reordered_pts : sh_video->sorted_pts;
+}
+
+static double update_video(struct MPContext *mpctx, double endpts)
+{
+ struct sh_video *sh_video = mpctx->sh_video;
+ struct vo *video_out = mpctx->video_out;
+ sh_video->vfilter->control(sh_video->vfilter, VFCTRL_SET_OSD_OBJ,
+ mpctx->osd); // for vf_sub
+ if (!mpctx->opts->correct_pts)
+ return update_video_nocorrect_pts(mpctx);
+
+ if (sh_video->gsh->attached_picture)
+ return update_video_attached_pic(mpctx);
+
+ double pts;
+
+ while (1) {
+ if (load_next_vo_frame(mpctx, false))
+ break;
+ pts = MP_NOPTS_VALUE;
+ struct demux_packet *pkt = NULL;
+ while (1) {
+ pkt = demux_read_packet(mpctx->sh_video->gsh);
+ if (!pkt || pkt->len)
+ break;
+ /* Packets with size 0 are assumed to not correspond to frames,
+ * but to indicate the absence of a frame in formats like AVI
+ * that must have packets at fixed timecode intervals. */
+ talloc_free(pkt);
+ }
+ if (pkt)
+ pts = pkt->pts;
+ if (pts != MP_NOPTS_VALUE)
+ pts += mpctx->video_offset;
+ if (pts >= mpctx->hrseek_pts - .005)
+ mpctx->hrseek_framedrop = false;
+ int framedrop_type = mpctx->hrseek_active && mpctx->hrseek_framedrop ?
+ 1 : check_framedrop(mpctx, -1);
+ struct mp_image *decoded_frame =
+ decode_video(sh_video, pkt, framedrop_type, pts);
+ talloc_free(pkt);
+ if (decoded_frame) {
+ determine_frame_pts(mpctx);
+ filter_video(mpctx, decoded_frame);
+ } else if (!pkt) {
+ if (!load_next_vo_frame(mpctx, true))
+ return -1;
+ }
+ break;
+ }
+
+ if (!video_out->frame_loaded)
+ return 0;
+
+ pts = video_out->next_pts;
+ if (pts == MP_NOPTS_VALUE) {
+ MP_ERR(mpctx, "Video pts after filters MISSING\n");
+ // Try to use decoder pts from before filters
+ pts = sh_video->pts;
+ if (pts == MP_NOPTS_VALUE)
+ pts = sh_video->last_pts;
+ }
+ if (endpts == MP_NOPTS_VALUE || pts < endpts)
+ add_frame_pts(mpctx, pts);
+ if (mpctx->hrseek_active && pts < mpctx->hrseek_pts - .005) {
+ vo_skip_frame(video_out);
+ return 0;
+ }
+ mpctx->hrseek_active = false;
+ sh_video->pts = pts;
+ if (sh_video->last_pts == MP_NOPTS_VALUE)
+ sh_video->last_pts = sh_video->pts;
+ else if (sh_video->last_pts > sh_video->pts) {
+ MP_WARN(mpctx, "Decreasing video pts: %f < %f\n",
+ sh_video->pts, sh_video->last_pts);
+ /* If the difference in pts is small treat it as jitter around the
+ * right value (possibly caused by incorrect timestamp ordering) and
+ * just show this frame immediately after the last one.
+ * Treat bigger differences as timestamp resets and start counting
+ * timing of later frames from the position of this one. */
+ if (sh_video->last_pts - sh_video->pts > 0.5)
+ sh_video->last_pts = sh_video->pts;
+ else
+ sh_video->pts = sh_video->last_pts;
+ } else if (sh_video->pts >= sh_video->last_pts + 60) {
+ // Assume a PTS difference >= 60 seconds is a discontinuity.
+ MP_WARN(mpctx, "Jump in video pts: %f -> %f\n",
+ sh_video->last_pts, sh_video->pts);
+ sh_video->last_pts = sh_video->pts;
+ }
+ double frame_time = sh_video->pts - sh_video->last_pts;
+ sh_video->last_pts = sh_video->pts;
+ if (mpctx->sh_audio)
+ mpctx->delay -= frame_time;
+ return frame_time;
+}
+
+void pause_player(struct MPContext *mpctx)
+{
+ mp_notify_property(mpctx, "pause");
+
+ mpctx->opts->pause = 1;
+
+ if (mpctx->video_out)
+ vo_control(mpctx->video_out, VOCTRL_RESTORE_SCREENSAVER, NULL);
+
+ if (mpctx->paused)
+ return;
+ mpctx->paused = true;
+ mpctx->step_frames = 0;
+ mpctx->time_frame -= get_relative_time(mpctx);
+ mpctx->osd_function = 0;
+ mpctx->paused_for_cache = false;
+
+ if (mpctx->video_out && mpctx->sh_video && mpctx->video_out->config_ok)
+ vo_control(mpctx->video_out, VOCTRL_PAUSE, NULL);
+
+ if (mpctx->ao && mpctx->sh_audio)
+ ao_pause(mpctx->ao); // pause audio, keep data if possible
+
+ // Only print status if there's actually a file being played.
+ if (mpctx->num_sources)
+ print_status(mpctx);
+
+ if (!mpctx->opts->quiet)
+ mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_PAUSED\n");
+}
+
+void unpause_player(struct MPContext *mpctx)
+{
+ mp_notify_property(mpctx, "pause");
+
+ mpctx->opts->pause = 0;
+
+ if (mpctx->video_out && mpctx->opts->stop_screensaver)
+ vo_control(mpctx->video_out, VOCTRL_KILL_SCREENSAVER, NULL);
+
+ if (!mpctx->paused)
+ return;
+ // Don't actually unpause while cache is loading.
+ if (mpctx->paused_for_cache)
+ return;
+ mpctx->paused = false;
+ mpctx->osd_function = 0;
+
+ if (mpctx->ao && mpctx->sh_audio)
+ ao_resume(mpctx->ao);
+ if (mpctx->video_out && mpctx->sh_video && mpctx->video_out->config_ok)
+ vo_control(mpctx->video_out, VOCTRL_RESUME, NULL); // resume video
+ (void)get_relative_time(mpctx); // ignore time that passed during pause
+}
+
+static void draw_osd(struct MPContext *mpctx)
+{
+ struct vo *vo = mpctx->video_out;
+
+ mpctx->osd->vo_pts = mpctx->video_pts;
+ vo_draw_osd(vo, mpctx->osd);
+}
+
+static bool redraw_osd(struct MPContext *mpctx)
+{
+ struct vo *vo = mpctx->video_out;
+ if (vo_redraw_frame(vo) < 0)
+ return false;
+
+ draw_osd(mpctx);
+
+ vo_flip_page(vo, 0, -1);
+ return true;
+}
+
+void add_step_frame(struct MPContext *mpctx, int dir)
+{
+ if (!mpctx->sh_video)
+ return;
+ if (dir > 0) {
+ mpctx->step_frames += 1;
+ unpause_player(mpctx);
+ } else if (dir < 0) {
+ if (!mpctx->backstep_active && !mpctx->hrseek_active) {
+ mpctx->backstep_active = true;
+ mpctx->backstep_start_seek_ts = mpctx->vo_pts_history_seek_ts;
+ pause_player(mpctx);
+ }
+ }
+}
+
+static void seek_reset(struct MPContext *mpctx, bool reset_ao, bool reset_ac)
+{
+ if (mpctx->sh_video) {
+ resync_video_stream(mpctx->sh_video);
+ vo_seek_reset(mpctx->video_out);
+ if (mpctx->sh_video->vf_initialized == 1)
+ vf_chain_seek_reset(mpctx->sh_video->vfilter);
+ mpctx->sh_video->num_buffered_pts = 0;
+ mpctx->sh_video->last_pts = MP_NOPTS_VALUE;
+ mpctx->sh_video->pts = MP_NOPTS_VALUE;
+ mpctx->video_pts = MP_NOPTS_VALUE;
+ mpctx->delay = 0;
+ mpctx->time_frame = 0;
+ }
+
+ if (mpctx->sh_audio && reset_ac) {
+ resync_audio_stream(mpctx->sh_audio);
+ if (reset_ao)
+ ao_reset(mpctx->ao);
+ mpctx->ao->buffer.len = mpctx->ao->buffer_playable_size;
+ mpctx->sh_audio->a_buffer_len = 0;
+ }
+
+ reset_subtitles(mpctx);
+
+ mpctx->restart_playback = true;
+ mpctx->hrseek_active = false;
+ mpctx->hrseek_framedrop = false;
+ mpctx->total_avsync_change = 0;
+ mpctx->drop_frame_cnt = 0;
+ mpctx->dropped_frames = 0;
+ mpctx->playback_pts = MP_NOPTS_VALUE;
+
+#ifdef CONFIG_ENCODING
+ encode_lavc_discontinuity(mpctx->encode_lavc_ctx);
+#endif
+}
+
+static bool timeline_set_part(struct MPContext *mpctx, int i, bool force)
+{
+ struct timeline_part *p = mpctx->timeline + mpctx->timeline_part;
+ struct timeline_part *n = mpctx->timeline + i;
+ mpctx->timeline_part = i;
+ mpctx->video_offset = n->start - n->source_start;
+ if (n->source == p->source && !force)
+ return false;
+ enum stop_play_reason orig_stop_play = mpctx->stop_play;
+ if (!mpctx->sh_video && mpctx->stop_play == KEEP_PLAYING)
+ mpctx->stop_play = AT_END_OF_FILE; // let audio uninit drain data
+ uninit_player(mpctx, INITIALIZED_VCODEC | (mpctx->opts->fixed_vo ? 0 : INITIALIZED_VO) | (mpctx->opts->gapless_audio ? 0 : INITIALIZED_AO) | INITIALIZED_ACODEC | INITIALIZED_SUB);
+ mpctx->stop_play = orig_stop_play;
+
+ mpctx->demuxer = n->source;
+ mpctx->stream = mpctx->demuxer->stream;
+
+ // While another timeline was active, the selection of active tracks might
+ // have been changed - possibly we need to update this source.
+ for (int x = 0; x < mpctx->num_tracks; x++) {
+ struct track *track = mpctx->tracks[x];
+ if (track->under_timeline) {
+ track->demuxer = mpctx->demuxer;
+ track->stream = demuxer_stream_by_demuxer_id(track->demuxer,
+ track->type,
+ track->demuxer_id);
+ }
+ }
+ preselect_demux_streams(mpctx);
+
+ return true;
+}
+
+// Given pts, switch playback to the corresponding part.
+// Return offset within that part.
+static double timeline_set_from_time(struct MPContext *mpctx, double pts,
+ bool *need_reset)
+{
+ if (pts < 0)
+ pts = 0;
+ for (int i = 0; i < mpctx->num_timeline_parts; i++) {
+ struct timeline_part *p = mpctx->timeline + i;
+ if (pts < (p + 1)->start) {
+ *need_reset = timeline_set_part(mpctx, i, false);
+ return pts - p->start + p->source_start;
+ }
+ }
+ return -1;
+}
+
+
+// return -1 if seek failed (non-seekable stream?), 0 otherwise
+static int seek(MPContext *mpctx, struct seek_params seek,
+ bool timeline_fallthrough)
+{
+ struct MPOpts *opts = mpctx->opts;
+ uint64_t prev_seek_ts = mpctx->vo_pts_history_seek_ts;
+
+ if (!mpctx->demuxer)
+ return -1;
+
+ if (mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = KEEP_PLAYING;
+ bool hr_seek = mpctx->demuxer->accurate_seek && opts->correct_pts;
+ hr_seek &= seek.exact >= 0 && seek.type != MPSEEK_FACTOR;
+ hr_seek &= (opts->hr_seek == 0 && seek.type == MPSEEK_ABSOLUTE) ||
+ opts->hr_seek > 0 || seek.exact > 0;
+ if (seek.type == MPSEEK_FACTOR || seek.amount < 0 ||
+ (seek.type == MPSEEK_ABSOLUTE && seek.amount < mpctx->last_chapter_pts))
+ mpctx->last_chapter_seek = -2;
+ if (seek.type == MPSEEK_FACTOR) {
+ double len = get_time_length(mpctx);
+ if (len > 0 && !mpctx->demuxer->ts_resets_possible) {
+ seek.amount = seek.amount * len + get_start_time(mpctx);
+ seek.type = MPSEEK_ABSOLUTE;
+ }
+ }
+ if ((mpctx->demuxer->accurate_seek || mpctx->timeline)
+ && seek.type == MPSEEK_RELATIVE) {
+ seek.type = MPSEEK_ABSOLUTE;
+ seek.direction = seek.amount > 0 ? 1 : -1;
+ seek.amount += get_current_time(mpctx);
+ }
+
+ /* At least the liba52 decoder wants to read from the input stream
+ * during initialization, so reinit must be done after the demux_seek()
+ * call that clears possible stream EOF. */
+ bool need_reset = false;
+ double demuxer_amount = seek.amount;
+ if (mpctx->timeline) {
+ demuxer_amount = timeline_set_from_time(mpctx, seek.amount,
+ &need_reset);
+ if (demuxer_amount == -1) {
+ assert(!need_reset);
+ mpctx->stop_play = AT_END_OF_FILE;
+ // Clear audio from current position
+ if (mpctx->sh_audio && !timeline_fallthrough) {
+ ao_reset(mpctx->ao);
+ mpctx->sh_audio->a_buffer_len = 0;
+ }
+ return -1;
+ }
+ }
+ if (need_reset) {
+ reinit_video_chain(mpctx);
+ reinit_subs(mpctx);
+ }
+
+ int demuxer_style = 0;
+ switch (seek.type) {
+ case MPSEEK_FACTOR:
+ demuxer_style |= SEEK_ABSOLUTE | SEEK_FACTOR;
+ break;
+ case MPSEEK_ABSOLUTE:
+ demuxer_style |= SEEK_ABSOLUTE;
+ break;
+ }
+ if (hr_seek || seek.direction < 0)
+ demuxer_style |= SEEK_BACKWARD;
+ else if (seek.direction > 0)
+ demuxer_style |= SEEK_FORWARD;
+ if (hr_seek || opts->mkv_subtitle_preroll)
+ demuxer_style |= SEEK_SUBPREROLL;
+
+ if (hr_seek)
+ demuxer_amount -= opts->hr_seek_demuxer_offset;
+ int seekresult = demux_seek(mpctx->demuxer, demuxer_amount, demuxer_style);
+ if (seekresult == 0) {
+ if (need_reset) {
+ reinit_audio_chain(mpctx);
+ seek_reset(mpctx, !timeline_fallthrough, false);
+ }
+ return -1;
+ }
+
+ // If audio or demuxer subs come from different files, seek them too:
+ bool have_external_tracks = false;
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ struct track *track = mpctx->current_track[type];
+ have_external_tracks |= track && track->is_external && track->demuxer;
+ }
+ if (have_external_tracks) {
+ double main_new_pos;
+ if (seek.type == MPSEEK_ABSOLUTE) {
+ main_new_pos = seek.amount - mpctx->video_offset;
+ } else {
+ main_new_pos = get_main_demux_pts(mpctx);
+ }
+ for (int type = 0; type < STREAM_TYPE_COUNT; type++) {
+ struct track *track = mpctx->current_track[type];
+ if (track && track->is_external && track->demuxer)
+ demux_seek(track->demuxer, main_new_pos, SEEK_ABSOLUTE);
+ }
+ }
+
+ if (need_reset)
+ reinit_audio_chain(mpctx);
+ /* If we just reinitialized audio it doesn't need to be reset,
+ * and resetting could lose audio some decoders produce during init. */
+ seek_reset(mpctx, !timeline_fallthrough, !need_reset);
+
+ if (timeline_fallthrough) {
+ // Important if video reinit happens.
+ mpctx->vo_pts_history_seek_ts = prev_seek_ts;
+ } else {
+ mpctx->vo_pts_history_seek_ts++;
+ mpctx->backstep_active = false;
+ }
+
+ /* Use the target time as "current position" for further relative
+ * seeks etc until a new video frame has been decoded */
+ if (seek.type == MPSEEK_ABSOLUTE) {
+ mpctx->video_pts = seek.amount;
+ mpctx->last_seek_pts = seek.amount;
+ } else
+ mpctx->last_seek_pts = MP_NOPTS_VALUE;
+
+ // The hr_seek==false case is for skipping frames with PTS before the
+ // current timeline chapter start. It's not really known where the demuxer
+ // level seek will end up, so the hrseek mechanism is abused to skip all
+ // frames before chapter start by setting hrseek_pts to the chapter start.
+ // It does nothing when the seek is inside of the current chapter, and
+ // seeking past the chapter is handled elsewhere.
+ if (hr_seek || mpctx->timeline) {
+ mpctx->hrseek_active = true;
+ mpctx->hrseek_framedrop = true;
+ mpctx->hrseek_pts = hr_seek ? seek.amount
+ : mpctx->timeline[mpctx->timeline_part].start;
+ }
+
+ mpctx->start_timestamp = mp_time_sec();
+
+ return 0;
+}
+
+void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount,
+ int exact)
+{
+ struct seek_params *seek = &mpctx->seek;
+ switch (type) {
+ case MPSEEK_RELATIVE:
+ if (seek->type == MPSEEK_FACTOR)
+ return; // Well... not common enough to bother doing better
+ seek->amount += amount;
+ seek->exact = FFMAX(seek->exact, exact);
+ if (seek->type == MPSEEK_NONE)
+ seek->exact = exact;
+ if (seek->type == MPSEEK_ABSOLUTE)
+ return;
+ if (seek->amount == 0) {
+ *seek = (struct seek_params){ 0 };
+ return;
+ }
+ seek->type = MPSEEK_RELATIVE;
+ return;
+ case MPSEEK_ABSOLUTE:
+ case MPSEEK_FACTOR:
+ *seek = (struct seek_params) {
+ .type = type,
+ .amount = amount,
+ .exact = exact,
+ };
+ return;
+ case MPSEEK_NONE:
+ *seek = (struct seek_params){ 0 };
+ return;
+ }
+ abort();
+}
+
+static void execute_queued_seek(struct MPContext *mpctx)
+{
+ if (mpctx->seek.type) {
+ seek(mpctx, mpctx->seek, false);
+ mpctx->seek = (struct seek_params){0};
+ }
+}
+
+double get_time_length(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return 0;
+
+ if (mpctx->timeline)
+ return mpctx->timeline[mpctx->num_timeline_parts].start;
+
+ double len = demuxer_get_time_length(demuxer);
+ if (len >= 0)
+ return len;
+
+ // Unknown
+ return 0;
+}
+
+/* If there are timestamps from stream level then use those (for example
+ * DVDs can have consistent times there while the MPEG-level timestamps
+ * reset). */
+double get_current_time(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return 0;
+ if (demuxer->stream_pts != MP_NOPTS_VALUE)
+ return demuxer->stream_pts;
+ if (mpctx->playback_pts != MP_NOPTS_VALUE)
+ return mpctx->playback_pts;
+ if (mpctx->last_seek_pts != MP_NOPTS_VALUE)
+ return mpctx->last_seek_pts;
+ return 0;
+}
+
+double get_start_time(struct MPContext *mpctx)
+{
+ struct demuxer *demuxer = mpctx->demuxer;
+ if (!demuxer)
+ return 0;
+ return demuxer_get_start_time(demuxer);
+}
+
+// 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 = get_start_time(mpctx);
+ double len = get_time_length(mpctx);
+ if (use_range) {
+ double startpos = rel_time_to_abs(mpctx, mpctx->opts->play_start,
+ MP_NOPTS_VALUE);
+ double endpos = get_play_end_pts(mpctx);
+ if (endpos == MP_NOPTS_VALUE || endpos > start + len)
+ endpos = start + len;
+ if (startpos == MP_NOPTS_VALUE || startpos < start)
+ startpos = start;
+ if (endpos < startpos)
+ endpos = startpos;
+ start = startpos;
+ len = endpos - startpos;
+ }
+ double pos = get_current_time(mpctx);
+ if (len > 0 && !demuxer->ts_resets_possible) {
+ ans = av_clipf((pos - start) / len, 0, 1);
+ } else {
+ int64_t size = (demuxer->movi_end - demuxer->movi_start);
+ int64_t fpos = demuxer->filepos > 0 ?
+ demuxer->filepos : stream_tell(demuxer->stream);
+ if (size > 0)
+ ans = av_clipf((double)(fpos - demuxer->movi_start) / 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;
+}
+
+int get_percent_pos(struct MPContext *mpctx)
+{
+ return av_clip(get_current_pos_ratio(mpctx, false) * 100, 0, 100);
+}
+
+// -2 is no chapters, -1 is before first chapter
+int get_current_chapter(struct MPContext *mpctx)
+{
+ double current_pts = get_current_time(mpctx);
+ if (mpctx->chapters) {
+ int i;
+ for (i = 1; i < mpctx->num_chapters; i++)
+ if (current_pts < mpctx->chapters[i].start)
+ break;
+ return FFMAX(mpctx->last_chapter_seek, i - 1);
+ }
+ if (mpctx->master_demuxer)
+ return FFMAX(mpctx->last_chapter_seek,
+ demuxer_get_current_chapter(mpctx->master_demuxer, current_pts));
+ return -2;
+}
+
+char *chapter_display_name(struct MPContext *mpctx, int chapter)
+{
+ char *name = chapter_name(mpctx, chapter);
+ char *dname = name;
+ 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);
+ }
+ if (dname != name)
+ talloc_free(name);
+ return dname;
+}
+
+// returns NULL if chapter name unavailable
+char *chapter_name(struct MPContext *mpctx, int chapter)
+{
+ if (mpctx->chapters) {
+ if (chapter < 0 || chapter >= mpctx->num_chapters)
+ return NULL;
+ return talloc_strdup(NULL, mpctx->chapters[chapter].name);
+ }
+ if (mpctx->master_demuxer)
+ return demuxer_chapter_name(mpctx->master_demuxer, chapter);
+ return NULL;
+}
+
+// returns the start of the chapter in seconds (-1 if unavailable)
+double chapter_start_time(struct MPContext *mpctx, int chapter)
+{
+ if (chapter == -1)
+ return get_start_time(mpctx);
+ if (mpctx->chapters)
+ return mpctx->chapters[chapter].start;
+ if (mpctx->master_demuxer)
+ return demuxer_chapter_time(mpctx->master_demuxer, chapter);
+ return -1;
+}
+
+int get_chapter_count(struct MPContext *mpctx)
+{
+ if (mpctx->chapters)
+ return mpctx->num_chapters;
+ if (mpctx->master_demuxer)
+ return demuxer_chapter_count(mpctx->master_demuxer);
+ return 0;
+}
+
+// Seek to a given chapter. Tries to queue the seek, but might seek immediately
+// in some cases. Returns success, no matter if seek is queued or immediate.
+bool mp_seek_chapter(struct MPContext *mpctx, int chapter)
+{
+ int num = get_chapter_count(mpctx);
+ if (num == 0)
+ return false;
+ if (chapter < -1 || chapter >= num)
+ return false;
+
+ mpctx->last_chapter_seek = -2;
+
+ double pts;
+ if (chapter == -1) {
+ pts = get_start_time(mpctx);
+ goto do_seek;
+ } else if (mpctx->chapters) {
+ pts = mpctx->chapters[chapter].start;
+ goto do_seek;
+ } else if (mpctx->master_demuxer) {
+ int res = demuxer_seek_chapter(mpctx->master_demuxer, chapter, &pts);
+ if (res >= 0) {
+ if (pts == -1) {
+ // for DVD/BD - seek happened via stream layer
+ seek_reset(mpctx, true, true);
+ mpctx->seek = (struct seek_params){0};
+ return true;
+ }
+ chapter = res;
+ goto do_seek;
+ }
+ }
+ return false;
+
+do_seek:
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, pts, 0);
+ mpctx->last_chapter_seek = chapter;
+ mpctx->last_chapter_pts = pts;
+ return true;
+}
+
+static void update_avsync(struct MPContext *mpctx)
+{
+ if (!mpctx->sh_audio || !mpctx->sh_video)
+ return;
+
+ double a_pos = playing_audio_pts(mpctx);
+
+ mpctx->last_av_difference = a_pos - mpctx->video_pts - mpctx->audio_delay;
+ if (mpctx->time_frame > 0)
+ mpctx->last_av_difference +=
+ mpctx->time_frame * mpctx->opts->playback_speed;
+ if (a_pos == MP_NOPTS_VALUE || mpctx->video_pts == MP_NOPTS_VALUE)
+ mpctx->last_av_difference = MP_NOPTS_VALUE;
+ if (mpctx->last_av_difference > 0.5 && mpctx->drop_frame_cnt > 50
+ && !mpctx->drop_message_shown) {
+ MP_WARN(mpctx, "%s", mp_gtext(av_desync_help_text));
+ mpctx->drop_message_shown = true;
+ }
+}
+
+static bool handle_osd_redraw(struct MPContext *mpctx)
+{
+ if (!mpctx->video_out || !mpctx->video_out->config_ok)
+ return false;
+ bool want_redraw = vo_get_want_redraw(mpctx->video_out);
+ if (mpctx->video_out->driver->draw_osd)
+ want_redraw |= mpctx->osd->want_redraw;
+ mpctx->osd->want_redraw = false;
+ if (want_redraw) {
+ if (redraw_osd(mpctx))
+ return true;
+ }
+ return false;
+}
+
+static void handle_metadata_update(struct MPContext *mpctx)
+{
+ if (mp_time_sec() > mpctx->last_metadata_update + 2) {
+ demux_info_update(mpctx->demuxer);
+ mpctx->last_metadata_update = mp_time_sec();
+ }
+}
+
+static void handle_pause_on_low_cache(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int cache = mp_get_cache_percent(mpctx);
+ bool idle = mp_get_cache_idle(mpctx);
+ if (mpctx->paused && mpctx->paused_for_cache) {
+ if (cache < 0 || cache >= opts->stream_cache_min_percent || idle) {
+ mpctx->paused_for_cache = false;
+ if (!opts->pause)
+ unpause_player(mpctx);
+ }
+ } else {
+ if (cache >= 0 && cache <= opts->stream_cache_pause && !idle) {
+ bool prev_paused_user = opts->pause;
+ pause_player(mpctx);
+ mpctx->paused_for_cache = true;
+ opts->pause = prev_paused_user;
+ }
+ }
+}
+
+static void handle_heartbeat_cmd(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->heartbeat_cmd && !mpctx->paused) {
+ double now = mp_time_sec();
+ if (now - mpctx->last_heartbeat > opts->heartbeat_interval) {
+ mpctx->last_heartbeat = now;
+ system(opts->heartbeat_cmd);
+ }
+ }
+}
+
+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;
+
+ 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 =
+ mp_time_sec() + opts->cursor_autohide_delay / 1000.0;
+ mouse_cursor_visible = true;
+ }
+
+ if (mp_time_sec() >= mpctx->mouse_timer)
+ 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_input_and_seek_coalesce(struct MPContext *mpctx)
+{
+ mp_flush_events(mpctx);
+
+ mp_cmd_t *cmd;
+ while ((cmd = mp_input_get_cmd(mpctx->input, 0, 1)) != NULL) {
+ /* Allow running consecutive seek commands to combine them,
+ * but execute the seek before running other commands.
+ * If the user seeks continuously (keeps arrow key down)
+ * try to finish showing a frame from one location before doing
+ * another seek (which could lead to unchanging display). */
+ if ((mpctx->seek.type && cmd->id != MP_CMD_SEEK) ||
+ (mpctx->restart_playback && cmd->id == MP_CMD_SEEK &&
+ mp_time_sec() - mpctx->start_timestamp < 0.3))
+ break;
+ cmd = mp_input_get_cmd(mpctx->input, 0, 0);
+ run_command(mpctx, cmd);
+ mp_cmd_free(cmd);
+ if (mpctx->stop_play)
+ break;
+ }
+}
+
+static void handle_backstep(struct MPContext *mpctx)
+{
+ if (!mpctx->backstep_active)
+ return;
+
+ double current_pts = mpctx->last_vo_pts;
+ mpctx->backstep_active = false;
+ bool demuxer_ok = mpctx->demuxer && mpctx->demuxer->accurate_seek;
+ if (demuxer_ok && mpctx->sh_video && current_pts != MP_NOPTS_VALUE) {
+ double seek_pts = find_previous_pts(mpctx, current_pts);
+ if (seek_pts != MP_NOPTS_VALUE) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, seek_pts, 1);
+ } else {
+ double last = get_last_frame_pts(mpctx);
+ if (last != MP_NOPTS_VALUE && last >= current_pts &&
+ mpctx->backstep_start_seek_ts != mpctx->vo_pts_history_seek_ts)
+ {
+ MP_ERR(mpctx, "Backstep failed.\n");
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, current_pts, 1);
+ } else if (!mpctx->hrseek_active) {
+ MP_VERBOSE(mpctx, "Start backstep indexing.\n");
+ // Force it to index the video up until current_pts.
+ // The whole point is getting frames _before_ that PTS,
+ // so apply an arbitrary offset. (In theory the offset
+ // has to be large enough to reach the previous frame.)
+ seek(mpctx, (struct seek_params){
+ .type = MPSEEK_ABSOLUTE,
+ .amount = current_pts - 1.0,
+ }, false);
+ // Don't leave hr-seek mode. If all goes right, hr-seek
+ // mode is cancelled as soon as the frame before
+ // current_pts is found during hr-seeking.
+ // Note that current_pts should be part of the index,
+ // otherwise we can't find the previous frame, so set the
+ // seek target an arbitrary amount of time after it.
+ if (mpctx->hrseek_active) {
+ mpctx->hrseek_pts = current_pts + 10.0;
+ mpctx->hrseek_framedrop = false;
+ mpctx->backstep_active = true;
+ }
+ } else {
+ mpctx->backstep_active = true;
+ }
+ }
+ }
+}
+
+static void handle_sstep(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->step_sec > 0 && !mpctx->stop_play && !mpctx->paused &&
+ !mpctx->restart_playback)
+ {
+ set_osd_function(mpctx, OSD_FFW);
+ queue_seek(mpctx, MPSEEK_RELATIVE, opts->step_sec, 0);
+ }
+}
+
+static void handle_keep_open(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (opts->keep_open && mpctx->stop_play == AT_END_OF_FILE) {
+ mpctx->stop_play = KEEP_PLAYING;
+ mpctx->playback_pts = mpctx->last_vo_pts;
+ pause_player(mpctx);
+ }
+}
+
+// Execute a forceful refresh of the VO window, if it hasn't had a valid frame
+// for a while. The problem is that a VO with no valid frame (vo->hasframe==0)
+// doesn't redraw video and doesn't OSD interaction. So screw it, hard.
+static void handle_force_window(struct MPContext *mpctx, bool reconfig)
+{
+ // Don't interfere with real video playback
+ if (mpctx->sh_video)
+ return;
+
+ struct vo *vo = mpctx->video_out;
+ if (!vo)
+ return;
+
+ if (!vo->config_ok || reconfig) {
+ MP_INFO(mpctx, "Creating non-video VO window.\n");
+ // Pick whatever works
+ int config_format = 0;
+ for (int fmt = IMGFMT_START; fmt < IMGFMT_END; fmt++) {
+ if (vo->driver->query_format(vo, fmt)) {
+ config_format = fmt;
+ break;
+ }
+ }
+ int w = 960;
+ int h = 480;
+ struct mp_image_params p = {
+ .imgfmt = config_format,
+ .w = w, .h = h,
+ .d_w = w, .d_h = h,
+ };
+ vo_reconfig(vo, &p, 0);
+ redraw_osd(mpctx);
+ }
+}
+
+static double get_wakeup_period(struct MPContext *mpctx)
+{
+ /* Even if we can immediately wake up in response to most input events,
+ * there are some timers which are not registered to the event loop
+ * and need to be checked periodically (like automatic mouse cursor hiding).
+ * OSD content updates behave similarly. Also some uncommon input devices
+ * may not have proper FD event support.
+ */
+ double sleeptime = WAKEUP_PERIOD;
+
+#ifndef HAVE_POSIX_SELECT
+ // No proper file descriptor event handling; keep waking up to poll input
+ sleeptime = FFMIN(sleeptime, 0.02);
+#endif
+
+ if (mpctx->video_out)
+ if (mpctx->video_out->wakeup_period > 0)
+ sleeptime = FFMIN(sleeptime, mpctx->video_out->wakeup_period);
+
+ return sleeptime;
+}
+
+static void run_playloop(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ bool full_audio_buffers = false;
+ bool audio_left = false, video_left = false;
+ double endpts = get_play_end_pts(mpctx);
+ bool end_is_chapter = false;
+ double sleeptime = get_wakeup_period(mpctx);
+ bool was_restart = mpctx->restart_playback;
+ bool new_frame_shown = false;
+
+#ifdef CONFIG_ENCODING
+ if (encode_lavc_didfail(mpctx->encode_lavc_ctx)) {
+ mpctx->stop_play = PT_QUIT;
+ return;
+ }
+#endif
+
+ // Add tracks that were added by the demuxer later (e.g. MPEG)
+ if (!mpctx->timeline && mpctx->demuxer)
+ add_demuxer_tracks(mpctx, mpctx->demuxer);
+
+ if (mpctx->timeline) {
+ double end = mpctx->timeline[mpctx->timeline_part + 1].start;
+ if (endpts == MP_NOPTS_VALUE || end < endpts) {
+ endpts = end;
+ end_is_chapter = true;
+ }
+ }
+
+ if (opts->chapterrange[1] > 0) {
+ int cur_chapter = get_current_chapter(mpctx);
+ if (cur_chapter != -1 && cur_chapter + 1 > opts->chapterrange[1])
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ }
+
+ if (mpctx->sh_audio && !mpctx->restart_playback && !mpctx->ao->untimed) {
+ int status = fill_audio_out_buffers(mpctx, endpts);
+ full_audio_buffers = status >= 0;
+ // Not at audio stream EOF yet
+ audio_left = status > -2;
+ }
+
+ if (mpctx->video_out) {
+ vo_check_events(mpctx->video_out);
+ handle_cursor_autohide(mpctx);
+ }
+
+ double buffered_audio = -1;
+ while (mpctx->sh_video) { // never loops, for "break;" only
+ struct vo *vo = mpctx->video_out;
+ update_fps(mpctx);
+
+ video_left = vo->hasframe || vo->frame_loaded;
+ if (!vo->frame_loaded && (!mpctx->paused || mpctx->restart_playback)) {
+ double frame_time = update_video(mpctx, endpts);
+ mp_dbg(MSGT_AVSYNC, MSGL_DBG2, "*** ftime=%5.3f ***\n", frame_time);
+ if (mpctx->sh_video->vf_initialized < 0) {
+ MP_FATAL(mpctx, "\nFATAL: Could not initialize video filters "
+ "(-vf) or video output (-vo).\n");
+ int uninit = INITIALIZED_VCODEC;
+ if (!opts->force_vo)
+ uninit |= INITIALIZED_VO;
+ uninit_player(mpctx, uninit);
+ mpctx->current_track[STREAM_VIDEO] = NULL;
+ if (!mpctx->current_track[STREAM_AUDIO])
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->error_playing = true;
+ handle_force_window(mpctx, true);
+ break;
+ }
+ video_left = frame_time >= 0;
+ if (video_left && !mpctx->restart_playback) {
+ mpctx->time_frame += frame_time / opts->playback_speed;
+ adjust_sync(mpctx, frame_time);
+ }
+ if (!video_left) {
+ mpctx->delay = 0;
+ mpctx->last_av_difference = 0;
+ }
+ }
+
+ if (endpts != MP_NOPTS_VALUE)
+ video_left &= mpctx->sh_video->pts < endpts;
+
+ handle_heartbeat_cmd(mpctx);
+
+ if (!video_left || (mpctx->paused && !mpctx->restart_playback))
+ break;
+ if (!vo->frame_loaded) {
+ sleeptime = 0;
+ break;
+ }
+
+ mpctx->time_frame -= get_relative_time(mpctx);
+ if (full_audio_buffers && !mpctx->restart_playback) {
+ buffered_audio = ao_get_delay(mpctx->ao);
+ mp_dbg(MSGT_AVSYNC, MSGL_DBG2, "delay=%f\n", buffered_audio);
+
+ if (opts->autosync) {
+ /* Smooth reported playback position from AO by averaging
+ * it with the value expected based on previus 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). */
+ float predicted = (mpctx->delay / opts->playback_speed +
+ mpctx->time_frame);
+ float difference = buffered_audio - predicted;
+ buffered_audio = predicted + difference / opts->autosync;
+ }
+
+ mpctx->time_frame = (buffered_audio -
+ mpctx->delay / opts->playback_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->untimed)
+ mpctx->time_frame = 0;
+ }
+
+ double vsleep = mpctx->time_frame - vo->flip_queue_offset;
+ if (vsleep > 0.050) {
+ sleeptime = FFMIN(sleeptime, vsleep - 0.040);
+ break;
+ }
+ sleeptime = 0;
+
+ //=================== FLIP PAGE (VIDEO BLT): ======================
+
+ vo_new_frame_imminent(vo);
+ struct sh_video *sh_video = mpctx->sh_video;
+ mpctx->video_pts = sh_video->pts;
+ mpctx->last_vo_pts = mpctx->video_pts;
+ mpctx->playback_pts = mpctx->video_pts;
+ update_subtitles(mpctx);
+ update_osd_msg(mpctx);
+ draw_osd(mpctx);
+
+ mpctx->time_frame -= get_relative_time(mpctx);
+ mpctx->time_frame -= vo->flip_queue_offset;
+ if (mpctx->time_frame > 0.001)
+ mpctx->time_frame = timing_sleep(mpctx, mpctx->time_frame);
+ mpctx->time_frame += vo->flip_queue_offset;
+
+ int64_t t2 = mp_time_us();
+ /* Playing with playback speed it's possible to get pathological
+ * cases with mpctx->time_frame negative enough to cause an
+ * overflow in pts_us calculation, thus the FFMAX. */
+ double time_frame = FFMAX(mpctx->time_frame, -1);
+ int64_t pts_us = mpctx->last_time + time_frame * 1e6;
+ int duration = -1;
+ double pts2 = vo->next_pts2;
+ if (pts2 != MP_NOPTS_VALUE && opts->correct_pts &&
+ !mpctx->restart_playback) {
+ // expected A/V sync correction is ignored
+ double diff = (pts2 - mpctx->video_pts);
+ diff /= opts->playback_speed;
+ if (mpctx->time_frame < 0)
+ diff += mpctx->time_frame;
+ if (diff < 0)
+ diff = 0;
+ if (diff > 10)
+ diff = 10;
+ duration = diff * 1e6;
+ }
+ vo_flip_page(vo, pts_us | 1, duration);
+
+ mpctx->last_vo_flip_duration = (mp_time_us() - t2) * 0.000001;
+ if (vo->driver->flip_page_timed) {
+ // No need to adjust sync based on flip speed
+ mpctx->last_vo_flip_duration = 0;
+ // For print_status - VO call finishing early is OK for sync
+ mpctx->time_frame -= get_relative_time(mpctx);
+ }
+ mpctx->shown_vframes++;
+ if (mpctx->restart_playback) {
+ if (mpctx->sync_audio_to_video) {
+ mpctx->syncing_audio = true;
+ if (mpctx->sh_audio)
+ fill_audio_out_buffers(mpctx, endpts);
+ mpctx->restart_playback = false;
+ }
+ mpctx->time_frame = 0;
+ get_relative_time(mpctx);
+ }
+ update_avsync(mpctx);
+ print_status(mpctx);
+ screenshot_flip(mpctx);
+ new_frame_shown = true;
+
+ break;
+ } // video
+
+ video_left &= mpctx->sync_audio_to_video; // force no-video semantics
+
+ if (mpctx->sh_audio && (mpctx->restart_playback ? !video_left :
+ mpctx->ao->untimed && (mpctx->delay <= 0 ||
+ !video_left))) {
+ int status = fill_audio_out_buffers(mpctx, endpts);
+ full_audio_buffers = status >= 0 && !mpctx->ao->untimed;
+ // Not at audio stream EOF yet
+ audio_left = status > -2;
+ }
+ if (!video_left)
+ mpctx->restart_playback = false;
+ if (mpctx->sh_audio && buffered_audio == -1)
+ buffered_audio = mpctx->paused ? 0 : ao_get_delay(mpctx->ao);
+
+ update_osd_msg(mpctx);
+
+ // The cache status is part of the status line. Possibly update it.
+ if (mpctx->paused && mp_get_cache_percent(mpctx) >= 0)
+ print_status(mpctx);
+
+ if (!video_left && (!mpctx->paused || was_restart)) {
+ double a_pos = 0;
+ if (mpctx->sh_audio) {
+ a_pos = (written_audio_pts(mpctx) -
+ mpctx->opts->playback_speed * buffered_audio);
+ }
+ mpctx->playback_pts = a_pos;
+ print_status(mpctx);
+ }
+
+ update_subtitles(mpctx);
+
+ /* 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.
+ *
+ * When all audio has been written to output driver, stay in the
+ * main loop handling commands until it has been mostly consumed,
+ * except in the gapless case, where the next file will be started
+ * while audio from the current one still remains to be played.
+ *
+ * We want this check to trigger if we seeked to this position,
+ * but not if we paused at it with audio possibly still buffered in
+ * the AO. There's currently no working way to check buffered audio
+ * inside AO while paused. Thus the "was_restart" check below, which
+ * should trigger after seek only, when we know there's no audio
+ * buffered.
+ */
+ if ((mpctx->sh_audio || mpctx->sh_video) && !audio_left && !video_left
+ && (opts->gapless_audio || buffered_audio < 0.05)
+ && (!mpctx->paused || was_restart)) {
+ if (end_is_chapter) {
+ seek(mpctx, (struct seek_params){
+ .type = MPSEEK_ABSOLUTE,
+ .amount = mpctx->timeline[mpctx->timeline_part+1].start
+ }, true);
+ } else
+ mpctx->stop_play = AT_END_OF_FILE;
+ sleeptime = 0;
+ }
+
+ if (!mpctx->stop_play && !mpctx->restart_playback) {
+
+ // If no more video is available, one frame means one playloop iteration.
+ // Otherwise, one frame means one video frame.
+ if (!video_left)
+ new_frame_shown = true;
+
+ if (opts->playing_msg && !mpctx->playing_msg_shown && new_frame_shown) {
+ mpctx->playing_msg_shown = true;
+ char *msg = mp_property_expand_string(mpctx, opts->playing_msg);
+ MP_INFO(mpctx, "%s\n", msg);
+ talloc_free(msg);
+ }
+
+ if (mpctx->max_frames >= 0) {
+ if (new_frame_shown)
+ mpctx->max_frames--;
+ if (mpctx->max_frames <= 0)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ }
+
+ if (mpctx->step_frames > 0 && !mpctx->paused) {
+ if (new_frame_shown)
+ mpctx->step_frames--;
+ if (mpctx->step_frames == 0)
+ pause_player(mpctx);
+ }
+
+ }
+
+ if (!mpctx->stop_play) {
+ double audio_sleep = 9;
+ if (mpctx->sh_audio && !mpctx->paused) {
+ if (mpctx->ao->untimed) {
+ if (!video_left)
+ audio_sleep = 0;
+ } else if (full_audio_buffers) {
+ audio_sleep = buffered_audio - 0.050;
+ // Keep extra safety margin if the buffers are large
+ if (audio_sleep > 0.100)
+ audio_sleep = FFMAX(audio_sleep - 0.200, 0.100);
+ else
+ audio_sleep = FFMAX(audio_sleep, 0.020);
+ } else
+ audio_sleep = 0.020;
+ }
+ sleeptime = FFMIN(sleeptime, audio_sleep);
+ if (sleeptime > 0) {
+ if (handle_osd_redraw(mpctx))
+ sleeptime = 0;
+ }
+ if (sleeptime > 0)
+ mp_input_get_cmd(mpctx->input, sleeptime * 1000, true);
+ }
+
+ handle_metadata_update(mpctx);
+
+ handle_pause_on_low_cache(mpctx);
+
+ handle_input_and_seek_coalesce(mpctx);
+
+ handle_backstep(mpctx);
+
+ handle_sstep(mpctx);
+
+ handle_keep_open(mpctx);
+
+ handle_force_window(mpctx, false);
+
+ execute_queued_seek(mpctx);
+}
+
+static bool attachment_is_font(struct demux_attachment *att)
+{
+ if (!att->name || !att->type || !att->data || !att->data_size)
+ return false;
+ // match against MIME types
+ if (strcmp(att->type, "application/x-truetype-font") == 0
+ || strcmp(att->type, "application/x-font") == 0)
+ return true;
+ // fallback: match against file extension
+ if (strlen(att->name) > 4) {
+ char *ext = att->name + strlen(att->name) - 4;
+ if (strcasecmp(ext, ".ttf") == 0 || strcasecmp(ext, ".ttc") == 0
+ || strcasecmp(ext, ".otf") == 0)
+ return true;
+ }
+ return false;
+}
+
+// Result numerically higher => better match. 0 == no match.
+static int match_lang(char **langs, char *lang)
+{
+ for (int idx = 0; langs && langs[idx]; idx++) {
+ if (lang && strcmp(langs[idx], lang) == 0)
+ return INT_MAX - idx;
+ }
+ 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:
+ * 0) track matches tid (always wins)
+ * 1) track is external
+ * 1b) track was passed explicitly (is not an auto-loaded subtitle)
+ * 2) earlier match in lang list
+ * 3) track is marked default
+ * 4) lower track number
+ * If select_fallback is not set, 4) 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).
+ */
+// Return whether t1 is preferred over t2
+static bool compare_track(struct track *t1, struct track *t2, char **langs)
+{
+ if (t1->is_external != t2->is_external)
+ return t1->is_external;
+ if (t1->auto_loaded != t2->auto_loaded)
+ return !t1->auto_loaded;
+ int l1 = match_lang(langs, t1->lang), l2 = match_lang(langs, t2->lang);
+ if (l1 != l2)
+ return l1 > l2;
+ if (t1->default_track != t2->default_track)
+ return t1->default_track;
+ if (t1->attached_picture != t2->attached_picture)
+ return !t1->attached_picture;
+ return t1->user_tid <= t2->user_tid;
+}
+static struct track *select_track(struct MPContext *mpctx,
+ enum stream_type type, int tid, char **langs)
+{
+ if (tid == -2)
+ return NULL;
+ bool select_fallback = type == STREAM_VIDEO || type == STREAM_AUDIO;
+ struct track *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)
+ return track;
+ if (!pick || compare_track(track, pick, langs))
+ pick = track;
+ }
+ if (pick && !select_fallback && !pick->is_external
+ && !match_lang(langs, pick->lang) && !pick->default_track)
+ pick = NULL;
+ if (pick && pick->attached_picture && !mpctx->opts->audio_display)
+ pick = NULL;
+ return pick;
+}
+
+// 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".
+ if (opts->video_id >= 0)
+ mpctx->opts->video_id = -1;
+ if (opts->audio_id >= 0)
+ mpctx->opts->audio_id = -1;
+ if (opts->sub_id >= 0)
+ mpctx->opts->sub_id = -1;
+ talloc_free(mpctx->track_layout_hash);
+ mpctx->track_layout_hash = NULL;
+ }
+ talloc_free(h);
+}
+
+static int read_keys(void *ctx, int fd)
+{
+ if (getch2(ctx))
+ return MP_INPUT_NOTHING;
+ return MP_INPUT_DEAD;
+}
+
+static void init_input(struct MPContext *mpctx)
+{
+ mpctx->input = mp_input_init(mpctx->global);
+ if (mpctx->opts->slave_mode)
+ mp_input_add_cmd_fd(mpctx->input, 0, USE_FD0_CMD_SELECT, MP_INPUT_SLAVE_CMD_FUNC, NULL);
+ else if (mpctx->opts->consolecontrols)
+ mp_input_add_key_fd(mpctx->input, 0, 1, read_keys, NULL, mpctx->input);
+ // Set the libstream interrupt callback
+ stream_set_interrupt_callback(mp_input_check_interrupt, mpctx->input);
+
+#ifdef CONFIG_COCOA
+ cocoa_set_input_context(mpctx->input);
+#endif
+}
+
+static void open_subtitles_from_options(struct MPContext *mpctx)
+{
+ // after reading video params we should load subtitles because
+ // we know fps so now we can adjust subtitle time to ~6 seconds AST
+ // check .sub
+ if (mpctx->opts->sub_name) {
+ for (int i = 0; mpctx->opts->sub_name[i] != NULL; ++i)
+ mp_add_subtitles(mpctx, mpctx->opts->sub_name[i]);
+ }
+ if (mpctx->opts->sub_auto) { // auto load sub file ...
+ char **tmp = find_text_subtitles(mpctx->opts, mpctx->filename);
+ int nsub = MP_TALLOC_ELEMS(tmp);
+ for (int i = 0; i < nsub; i++) {
+ char *filename = tmp[i];
+ for (int n = 0; n < mpctx->num_sources; n++) {
+ if (strcmp(mpctx->sources[n]->stream->url, filename) == 0)
+ goto skip;
+ }
+ struct track *track = mp_add_subtitles(mpctx, filename);
+ if (track)
+ track->auto_loaded = true;
+ skip:;
+ }
+ talloc_free(tmp);
+ }
+}
+
+static struct track *open_external_file(struct MPContext *mpctx, char *filename,
+ char *demuxer_name, int stream_cache,
+ enum stream_type filter)
+{
+ struct MPOpts *opts = mpctx->opts;
+ if (!filename)
+ return NULL;
+ char *disp_filename = filename;
+ if (strncmp(disp_filename, "memory://", 9) == 0)
+ disp_filename = "memory://"; // avoid noise
+ struct stream *stream = stream_open(filename, mpctx->opts);
+ if (!stream)
+ goto err_out;
+ stream_enable_cache_percent(&stream, stream_cache,
+ opts->stream_cache_def_size,
+ opts->stream_cache_min_percent,
+ opts->stream_cache_seek_min_percent);
+ struct demuxer_params params = {
+ .ass_library = mpctx->ass_library, // demux_libass requires it
+ };
+ struct demuxer *demuxer =
+ demux_open(stream, demuxer_name, &params, mpctx->opts);
+ if (!demuxer) {
+ free_stream(stream);
+ goto err_out;
+ }
+ struct track *first = NULL;
+ for (int n = 0; n < demuxer->num_streams; n++) {
+ struct sh_stream *sh = demuxer->streams[n];
+ if (sh->type == filter) {
+ struct track *t = add_stream_track(mpctx, sh, false);
+ t->is_external = true;
+ t->title = talloc_strdup(t, disp_filename);
+ t->external_filename = talloc_strdup(t, filename);
+ first = t;
+ }
+ }
+ if (!first) {
+ free_demuxer(demuxer);
+ free_stream(stream);
+ MP_WARN(mpctx, "No streams added from file %s.\n",
+ disp_filename);
+ goto err_out;
+ }
+ MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, demuxer);
+ return first;
+
+err_out:
+ MP_ERR(mpctx, "Can not open external file %s.\n",
+ disp_filename);
+ return false;
+}
+
+static void open_audiofiles_from_options(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ open_external_file(mpctx, opts->audio_stream, opts->audio_demuxer_name,
+ opts->audio_stream_cache, STREAM_AUDIO);
+}
+
+struct track *mp_add_subtitles(struct MPContext *mpctx, char *filename)
+{
+ struct MPOpts *opts = mpctx->opts;
+ return open_external_file(mpctx, filename, opts->sub_demuxer_name, 0,
+ STREAM_SUB);
+}
+
+static void open_subtitles_from_resolve(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct mp_resolve_result *res = mpctx->resolve_result;
+ if (!res)
+ return;
+ for (int n = 0; n < res->num_subs; n++) {
+ struct mp_resolve_sub *sub = res->subs[n];
+ char *s = talloc_strdup(NULL, sub->url);
+ if (!s)
+ s = talloc_asprintf(NULL, "memory://%s", sub->data);
+ struct track *t =
+ open_external_file(mpctx, s, opts->sub_demuxer_name, 0, STREAM_SUB);
+ talloc_free(s);
+ if (t)
+ t->lang = talloc_strdup(t, sub->lang);
+ }
+}
+
+static void print_timeline(struct MPContext *mpctx)
+{
+ if (mpctx->timeline) {
+ int part_count = mpctx->num_timeline_parts;
+ MP_VERBOSE(mpctx, "Timeline contains %d parts from %d "
+ "sources. Total length %.3f seconds.\n", part_count,
+ mpctx->num_sources, mpctx->timeline[part_count].start);
+ MP_VERBOSE(mpctx, "Source files:\n");
+ for (int i = 0; i < mpctx->num_sources; i++)
+ MP_VERBOSE(mpctx, "%d: %s\n", i,
+ mpctx->sources[i]->filename);
+ MP_VERBOSE(mpctx, "Timeline parts: (number, start, "
+ "source_start, source):\n");
+ for (int i = 0; i < part_count; i++) {
+ struct timeline_part *p = mpctx->timeline + i;
+ MP_VERBOSE(mpctx, "%3d %9.3f %9.3f %p/%s\n", i, p->start,
+ p->source_start, p->source, p->source->filename);
+ }
+ MP_VERBOSE(mpctx, "END %9.3f\n",
+ mpctx->timeline[part_count].start);
+ }
+}
+
+static void add_subtitle_fonts_from_sources(struct MPContext *mpctx)
+{
+#ifdef CONFIG_ASS
+ if (mpctx->opts->ass_enabled) {
+ for (int j = 0; j < mpctx->num_sources; j++) {
+ struct demuxer *d = mpctx->sources[j];
+ for (int i = 0; i < d->num_attachments; i++) {
+ struct demux_attachment *att = d->attachments + i;
+ if (mpctx->opts->use_embedded_fonts && attachment_is_font(att))
+ ass_add_font(mpctx->ass_library, att->name, att->data,
+ att->data_size);
+ }
+ }
+ }
+#endif
+}
+
+static void init_sub_renderer(struct MPContext *mpctx)
+{
+#ifdef CONFIG_ASS
+ assert(!(mpctx->initialized_flags & INITIALIZED_LIBASS));
+ assert(!mpctx->osd->ass_renderer);
+
+ mpctx->osd->ass_renderer = ass_renderer_init(mpctx->osd->ass_library);
+ if (mpctx->osd->ass_renderer) {
+ mp_ass_configure_fonts(mpctx->osd->ass_renderer,
+ mpctx->opts->sub_text_style);
+ }
+ mpctx->initialized_flags |= INITIALIZED_LIBASS;
+#endif
+}
+
+static struct mp_resolve_result *resolve_url(const char *filename,
+ struct MPOpts *opts)
+{
+ if (!mp_is_url(bstr0(filename)))
+ return NULL;
+#if defined(CONFIG_LIBQUVI) || defined(CONFIG_LIBQUVI9)
+ return mp_resolve_quvi(filename, opts);
+#else
+ return NULL;
+#endif
+}
+
+static void print_resolve_contents(struct mp_log *log,
+ struct mp_resolve_result *res)
+{
+ mp_msg_log(log, MSGL_V, "Resolve:\n");
+ mp_msg_log(log, MSGL_V, " title: %s\n", res->title);
+ mp_msg_log(log, MSGL_V, " url: %s\n", res->url);
+ for (int n = 0; n < res->num_srcs; n++) {
+ mp_msg_log(log, MSGL_V, " source %d:\n", n);
+ if (res->srcs[n]->url)
+ mp_msg_log(log, MSGL_V, " url: %s\n", res->srcs[n]->url);
+ if (res->srcs[n]->encid)
+ mp_msg_log(log, MSGL_V, " encid: %s\n", res->srcs[n]->encid);
+ }
+ for (int n = 0; n < res->num_subs; n++) {
+ mp_msg_log(log, MSGL_V, " subtitle %d:\n", n);
+ if (res->subs[n]->url)
+ mp_msg_log(log, MSGL_V, " url: %s\n", res->subs[n]->url);
+ if (res->subs[n]->lang)
+ mp_msg_log(log, MSGL_V, " lang: %s\n", res->subs[n]->lang);
+ if (res->subs[n]->data) {
+ mp_msg_log(log, MSGL_V, " data: %zd bytes\n",
+ strlen(res->subs[n]->data));
+ }
+ }
+ if (res->playlist) {
+ mp_msg_log(log, MSGL_V, " playlist with %d entries\n",
+ playlist_entry_count(res->playlist));
+ }
+}
+
+// Waiting for the slave master to send us a new file to play.
+static void idle_loop(struct MPContext *mpctx)
+{
+ // ================= idle loop (STOP state) =========================
+ bool need_reinit = true;
+ while (mpctx->opts->player_idle_mode && !mpctx->playlist->current
+ && mpctx->stop_play != PT_QUIT)
+ {
+ if (need_reinit)
+ handle_force_window(mpctx, true);
+ need_reinit = false;
+ int uninit = INITIALIZED_AO;
+ if (!mpctx->opts->force_vo)
+ uninit |= INITIALIZED_VO;
+ uninit_player(mpctx, uninit);
+ handle_force_window(mpctx, false);
+ if (mpctx->video_out)
+ vo_check_events(mpctx->video_out);
+ update_osd_msg(mpctx);
+ handle_osd_redraw(mpctx);
+ mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input,
+ get_wakeup_period(mpctx) * 1000,
+ false);
+ if (cmd)
+ run_command(mpctx, cmd);
+ mp_cmd_free(cmd);
+ mp_flush_events(mpctx);
+ }
+}
+
+static void stream_dump(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ char *filename = opts->stream_dump;
+ stream_t *stream = mpctx->stream;
+ assert(stream && filename);
+
+ stream_set_capture_file(stream, filename);
+
+ while (mpctx->stop_play == KEEP_PLAYING && !stream->eof) {
+ if (!opts->quiet && ((stream->pos / (1024 * 1024)) % 2) == 1) {
+ uint64_t pos = stream->pos - stream->start_pos;
+ uint64_t end = stream->end_pos - stream->start_pos;
+ char *line = talloc_asprintf(NULL, "Dumping %lld/%lld...",
+ (long long int)pos, (long long int)end);
+ write_status_line(mpctx, line);
+ talloc_free(line);
+ }
+ stream_fill_buffer(stream);
+ for (;;) {
+ mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input, 0, false);
+ if (!cmd)
+ break;
+ run_command(mpctx, cmd);
+ talloc_free(cmd);
+ }
+ }
+}
+
+// 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)
+{
+ if (mpctx->demuxer->playlist->first) {
+ playlist_transfer_entries(mpctx->playlist, mpctx->demuxer->playlist);
+ if (mpctx->playlist->current)
+ playlist_remove(mpctx->playlist, mpctx->playlist->current);
+ } else {
+ MP_WARN(mpctx, "Empty playlist!\n");
+ }
+}
+
+// Start playing the current playlist entry.
+// Handle initialization and deinitialization.
+static void play_current_file(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ double playback_start = -1e100;
+
+ mpctx->initialized_flags |= INITIALIZED_PLAYBACK;
+
+ mp_notify(mpctx, MP_EVENT_START_FILE, NULL);
+ mp_flush_events(mpctx);
+
+ mpctx->stop_play = 0;
+ mpctx->filename = NULL;
+ mpctx->shown_aframes = 0;
+ mpctx->shown_vframes = 0;
+
+ if (mpctx->playlist->current)
+ mpctx->filename = mpctx->playlist->current->filename;
+
+ if (!mpctx->filename)
+ goto terminate_playback;
+
+#ifdef CONFIG_ENCODING
+ encode_lavc_discontinuity(mpctx->encode_lavc_ctx);
+#endif
+
+ mpctx->add_osd_seek_info &= OSD_SEEK_INFO_EDITION;
+
+ 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);
+ }
+ }
+ }
+ }
+
+ load_per_protocol_config(mpctx->mconfig, mpctx->filename);
+ load_per_extension_config(mpctx->mconfig, mpctx->filename);
+ load_per_file_config(mpctx->mconfig, mpctx->filename, opts->use_filedir_conf);
+
+ if (opts->vo.video_driver_list)
+ load_per_output_config(mpctx->mconfig, PROFILE_CFG_VO,
+ opts->vo.video_driver_list[0].name);
+ if (opts->audio_driver_list)
+ load_per_output_config(mpctx->mconfig, PROFILE_CFG_AO,
+ opts->audio_driver_list[0].name);
+
+ if (opts->position_resume)
+ load_playback_resume(mpctx->mconfig, mpctx->filename);
+
+ load_per_file_options(mpctx->mconfig, mpctx->playlist->current->params,
+ mpctx->playlist->current->num_params);
+
+ // We must enable getch2 here to be able to interrupt network connection
+ // or cache filling
+ if (opts->consolecontrols && !opts->slave_mode) {
+ if (mpctx->initialized_flags & INITIALIZED_GETCH2)
+ MP_WARN(mpctx, "WARNING: getch2_init called twice!\n");
+ else
+ getch2_enable(); // prepare stdin for hotkeys...
+ mpctx->initialized_flags |= INITIALIZED_GETCH2;
+ MP_DBG(mpctx, "\n[[[init getch2]]]\n");
+ }
+
+#ifdef CONFIG_ASS
+ if (opts->ass_style_override)
+ ass_set_style_overrides(mpctx->ass_library, opts->ass_force_style_list);
+#endif
+
+ MP_INFO(mpctx, "Playing: %s\n", mpctx->filename);
+
+ //============ Open & Sync STREAM --- fork cache2 ====================
+
+ assert(mpctx->stream == NULL);
+ assert(mpctx->demuxer == NULL);
+ assert(mpctx->sh_audio == NULL);
+ assert(mpctx->sh_video == NULL);
+ assert(mpctx->sh_sub == NULL);
+
+ char *stream_filename = mpctx->filename;
+ mpctx->resolve_result = resolve_url(stream_filename, opts);
+ if (mpctx->resolve_result) {
+ print_resolve_contents(mpctx->log, mpctx->resolve_result);
+ if (mpctx->resolve_result->playlist) {
+ transfer_playlist(mpctx, mpctx->resolve_result->playlist);
+ goto terminate_playback;
+ }
+ stream_filename = mpctx->resolve_result->url;
+ }
+ mpctx->stream = stream_open(stream_filename, opts);
+ if (!mpctx->stream) { // error...
+ demux_was_interrupted(mpctx);
+ goto terminate_playback;
+ }
+ mpctx->initialized_flags |= INITIALIZED_STREAM;
+
+ mpctx->stream->start_pos += opts->seek_to_byte;
+
+ if (opts->stream_dump && opts->stream_dump[0]) {
+ stream_dump(mpctx);
+ goto terminate_playback;
+ }
+
+ // CACHE2: initial prefill: 20% later: 5% (should be set by -cacheopts)
+ int res = stream_enable_cache_percent(&mpctx->stream,
+ opts->stream_cache_size,
+ opts->stream_cache_def_size,
+ opts->stream_cache_min_percent,
+ opts->stream_cache_seek_min_percent);
+ if (res == 0)
+ if (demux_was_interrupted(mpctx))
+ goto terminate_playback;
+
+ stream_set_capture_file(mpctx->stream, opts->stream_capture);
+
+#ifdef CONFIG_DVBIN
+goto_reopen_demuxer: ;
+#endif
+
+ //============ Open DEMUXERS --- DETECT file type =======================
+
+ mpctx->audio_delay = opts->audio_delay;
+
+ mpctx->demuxer = demux_open(mpctx->stream, opts->demuxer_name, NULL, opts);
+ mpctx->master_demuxer = mpctx->demuxer;
+ if (!mpctx->demuxer) {
+ MP_ERR(mpctx, "Failed to recognize file format.\n");
+ goto terminate_playback;
+ }
+
+ MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, mpctx->demuxer);
+
+ mpctx->initialized_flags |= INITIALIZED_DEMUXER;
+
+ if (mpctx->demuxer->playlist) {
+ if (mpctx->demuxer->stream->safe_origin || opts->load_unsafe_playlists) {
+ transfer_playlist(mpctx, mpctx->demuxer->playlist);
+ } else {
+ MP_ERR(mpctx, "\nThis looks like a playlist, but playlist support "
+ "will not be used automatically.\nThe main problem with "
+ "playlist safety is that playlist entries can be arbitrary,\n"
+ "and an attacker could make mpv poke around in your local "
+ "filesystem or network.\nUse --playlist=file or the "
+ "--load-unsafe-playlists option to load them anyway.\n");
+ }
+ goto terminate_playback;
+ }
+
+ if (mpctx->demuxer->matroska_data.ordered_chapters)
+ build_ordered_chapter_timeline(mpctx);
+
+ if (mpctx->demuxer->type == DEMUXER_TYPE_EDL)
+ build_edl_timeline(mpctx);
+
+ if (mpctx->demuxer->type == DEMUXER_TYPE_CUE)
+ build_cue_timeline(mpctx);
+
+ print_timeline(mpctx);
+
+ if (mpctx->timeline) {
+ // With Matroska, the "master" file usually dictates track layout etc.
+ // On the contrary, the EDL and CUE demuxers are empty wrappers, as
+ // well as Matroska ordered chapter playlist-like files.
+ for (int n = 0; n < mpctx->num_timeline_parts; n++) {
+ if (mpctx->timeline[n].source == mpctx->demuxer)
+ goto main_is_ok;
+ }
+ mpctx->demuxer = mpctx->timeline[0].source;
+ main_is_ok: ;
+ }
+ add_dvd_tracks(mpctx);
+ add_demuxer_tracks(mpctx, mpctx->demuxer);
+
+ mpctx->timeline_part = 0;
+ if (mpctx->timeline)
+ timeline_set_part(mpctx, mpctx->timeline_part, true);
+
+ add_subtitle_fonts_from_sources(mpctx);
+ // libass seems to misbehave if fonts are changed while a renderer
+ // exists, so we (re)create the renderer after fonts are set.
+ init_sub_renderer(mpctx);
+
+ open_subtitles_from_options(mpctx);
+ open_subtitles_from_resolve(mpctx);
+ open_audiofiles_from_options(mpctx);
+
+ check_previous_track_selection(mpctx);
+
+ mpctx->current_track[STREAM_VIDEO] =
+ select_track(mpctx, STREAM_VIDEO, mpctx->opts->video_id, NULL);
+ mpctx->current_track[STREAM_AUDIO] =
+ select_track(mpctx, STREAM_AUDIO, mpctx->opts->audio_id,
+ mpctx->opts->audio_lang);
+ mpctx->current_track[STREAM_SUB] =
+ select_track(mpctx, STREAM_SUB, mpctx->opts->sub_id,
+ mpctx->opts->sub_lang);
+
+ demux_info_print(mpctx->master_demuxer);
+ print_file_properties(mpctx, mpctx->filename);
+
+ preselect_demux_streams(mpctx);
+
+#ifdef CONFIG_ENCODING
+ if (mpctx->encode_lavc_ctx && mpctx->current_track[STREAM_VIDEO])
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, AVMEDIA_TYPE_VIDEO);
+ if (mpctx->encode_lavc_ctx && mpctx->current_track[STREAM_AUDIO])
+ encode_lavc_expect_stream(mpctx->encode_lavc_ctx, AVMEDIA_TYPE_AUDIO);
+#endif
+
+ reinit_video_chain(mpctx);
+ reinit_audio_chain(mpctx);
+ reinit_subs(mpctx);
+
+ //================ SETUP STREAMS ==========================
+
+ if (opts->force_fps && mpctx->sh_video) {
+ mpctx->sh_video->fps = opts->force_fps;
+ MP_INFO(mpctx, "FPS forced to be %5.3f.\n", mpctx->sh_video->fps);
+ }
+
+ //==================== START PLAYING =======================
+
+ if (!mpctx->sh_video && !mpctx->sh_audio) {
+ MP_FATAL(mpctx, "No video or audio streams selected.\n");
+#ifdef CONFIG_DVBIN
+ if (mpctx->stream->type == STREAMTYPE_DVB) {
+ int dir;
+ int v = mpctx->last_dvb_step;
+ if (v > 0)
+ dir = DVB_CHANNEL_HIGHER;
+ else
+ dir = DVB_CHANNEL_LOWER;
+
+ if (dvb_step_channel(mpctx->stream, dir)) {
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ mpctx->dvbin_reopen = 1;
+ }
+ }
+#endif
+ goto terminate_playback;
+ }
+
+ MP_VERBOSE(mpctx, "Starting playback...\n");
+
+ mpctx->drop_frame_cnt = 0;
+ mpctx->dropped_frames = 0;
+ mpctx->max_frames = opts->play_frames;
+
+ if (mpctx->max_frames == 0) {
+ mpctx->stop_play = PT_NEXT_ENTRY;
+ goto terminate_playback;
+ }
+
+ mpctx->time_frame = 0;
+ mpctx->drop_message_shown = 0;
+ mpctx->restart_playback = true;
+ mpctx->video_pts = 0;
+ mpctx->last_vo_pts = MP_NOPTS_VALUE;
+ mpctx->last_seek_pts = 0;
+ mpctx->playback_pts = MP_NOPTS_VALUE;
+ mpctx->hrseek_active = false;
+ mpctx->hrseek_framedrop = false;
+ mpctx->step_frames = 0;
+ mpctx->backstep_active = false;
+ mpctx->total_avsync_change = 0;
+ mpctx->last_chapter_seek = -2;
+ mpctx->playing_msg_shown = false;
+ mpctx->paused = false;
+ mpctx->paused_for_cache = false;
+ mpctx->seek = (struct seek_params){ 0 };
+
+ // If there's a timeline force an absolute seek to initialize state
+ double startpos = rel_time_to_abs(mpctx, opts->play_start, -1);
+ if (startpos != -1 || mpctx->timeline) {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, startpos, 0);
+ execute_queued_seek(mpctx);
+ }
+ if (startpos == -1 && mpctx->resolve_result &&
+ mpctx->resolve_result->start_time > 0)
+ {
+ queue_seek(mpctx, MPSEEK_ABSOLUTE, mpctx->resolve_result->start_time, 0);
+ execute_queued_seek(mpctx);
+ }
+ if (opts->chapterrange[0] > 0) {
+ if (mp_seek_chapter(mpctx, opts->chapterrange[0] - 1))
+ execute_queued_seek(mpctx);
+ }
+
+ get_relative_time(mpctx); // reset current delta
+
+ if (mpctx->opts->pause)
+ pause_player(mpctx);
+
+ playback_start = mp_time_sec();
+ mpctx->error_playing = false;
+ while (!mpctx->stop_play)
+ run_playloop(mpctx);
+
+ MP_VERBOSE(mpctx, "EOF code: %d \n", mpctx->stop_play);
+
+#ifdef CONFIG_DVBIN
+ if (mpctx->dvbin_reopen) {
+ mpctx->stop_play = 0;
+ uninit_player(mpctx, INITIALIZED_ALL - (INITIALIZED_STREAM | INITIALIZED_GETCH2 | (opts->fixed_vo ? INITIALIZED_VO : 0)));
+ mpctx->dvbin_reopen = 0;
+ goto goto_reopen_demuxer;
+ }
+#endif
+
+terminate_playback: // don't jump here after ao/vo/getch initialization!
+
+ if (mpctx->stop_play == KEEP_PLAYING)
+ mpctx->stop_play = AT_END_OF_FILE;
+
+ if (opts->position_save_on_quit && mpctx->stop_play == PT_QUIT)
+ mp_write_watch_later_conf(mpctx);
+
+ if (mpctx->step_frames)
+ opts->pause = 1;
+
+ MP_INFO(mpctx, "\n");
+
+ // time to uninit all, except global stuff:
+ int uninitialize_parts = INITIALIZED_ALL;
+ if (opts->fixed_vo)
+ uninitialize_parts -= INITIALIZED_VO;
+ if ((opts->gapless_audio && mpctx->stop_play == AT_END_OF_FILE) ||
+ mpctx->encode_lavc_ctx)
+ uninitialize_parts -= INITIALIZED_AO;
+ uninit_player(mpctx, uninitialize_parts);
+
+ // xxx handle this as INITIALIZED_CONFIG?
+ if (mpctx->stop_play != PT_RESTART)
+ m_config_restore_backups(mpctx->mconfig);
+
+ mpctx->filename = NULL;
+ talloc_free(mpctx->resolve_result);
+ mpctx->resolve_result = NULL;
+
+ // Played/paused for longer than 3 seconds -> ok
+ bool playback_short = mpctx->stop_play == AT_END_OF_FILE &&
+ (playback_start < 0 || mp_time_sec() - playback_start < 3.0);
+ bool init_failed = mpctx->stop_play == AT_END_OF_FILE &&
+ (mpctx->shown_aframes == 0 && mpctx->shown_vframes == 0);
+ if (mpctx->playlist->current && !mpctx->playlist->current_was_replaced) {
+ mpctx->playlist->current->playback_short = playback_short;
+ mpctx->playlist->current->init_failed = init_failed;
+ }
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+ mp_notify(mpctx, MP_EVENT_END_FILE, NULL);
+ mp_flush_events(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) {
+ // Don't jump to files that would immediately go to next file anyway
+ while (next && next->playback_short)
+ next = next->prev;
+ // Always allow jumping to first file
+ if (!next && mpctx->opts->loop_times < 0)
+ next = mpctx->playlist->first;
+ }
+ if (!next && mpctx->opts->loop_times >= 0) {
+ if (direction > 0) {
+ if (mpctx->opts->shuffle)
+ playlist_shuffle(mpctx->playlist);
+ next = mpctx->playlist->first;
+ if (next && mpctx->opts->loop_times > 1) {
+ mpctx->opts->loop_times--;
+ if (mpctx->opts->loop_times == 1)
+ mpctx->opts->loop_times = -1;
+ }
+ } else {
+ next = mpctx->playlist->last;
+ // Don't jump to files that would immediately go to next file anyway
+ while (next && next->playback_short)
+ next = next->prev;
+ }
+ if (!force && next && next->init_failed) {
+ // Don't endless loop if no file in playlist is playable
+ bool all_failed = true;
+ struct playlist_entry *cur;
+ for (cur = mpctx->playlist->first; cur; cur = cur->next) {
+ all_failed &= cur->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.
+static void play_files(struct MPContext *mpctx)
+{
+ mpctx->quit_player_rc = EXIT_NONE;
+ for (;;) {
+ idle_loop(mpctx);
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ mpctx->error_playing = true;
+ play_current_file(mpctx);
+ if (mpctx->error_playing) {
+ if (!mpctx->quit_player_rc) {
+ mpctx->quit_player_rc = EXIT_NOTPLAYED;
+ } else if (mpctx->quit_player_rc == EXIT_PLAYED) {
+ mpctx->quit_player_rc = EXIT_SOMENOTPLAYED;
+ }
+ } else if (mpctx->quit_player_rc == EXIT_NOTPLAYED) {
+ mpctx->quit_player_rc = EXIT_SOMENOTPLAYED;
+ } else {
+ mpctx->quit_player_rc = EXIT_PLAYED;
+ }
+ if (mpctx->stop_play == PT_QUIT)
+ break;
+
+ if (!mpctx->stop_play || mpctx->stop_play == AT_END_OF_FILE)
+ mpctx->stop_play = PT_NEXT_ENTRY;
+
+ struct playlist_entry *new_entry = NULL;
+
+ if (mpctx->stop_play == PT_NEXT_ENTRY) {
+ new_entry = mp_next_file(mpctx, +1, false);
+ } else if (mpctx->stop_play == PT_CURRENT_ENTRY) {
+ new_entry = mpctx->playlist->current;
+ } else if (mpctx->stop_play == PT_RESTART) {
+ // The same as PT_CURRENT_ENTRY, unless we decide that the current
+ // playlist entry can be removed during playback.
+ new_entry = mpctx->playlist->current;
+ } else { // PT_STOP
+ playlist_clear(mpctx->playlist);
+ }
+
+ mpctx->playlist->current = new_entry;
+ mpctx->playlist->current_was_replaced = false;
+ mpctx->stop_play = 0;
+
+ if (!mpctx->playlist->current && !mpctx->opts->player_idle_mode)
+ break;
+ }
+}
+
+// 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(playlist_entry_to_index(mpctx->playlist, e) >= 0);
+ mpctx->playlist->current = e;
+ mpctx->playlist->current_was_replaced = false;
+ mpctx->stop_play = PT_CURRENT_ENTRY;
+}
+
+void mp_print_version(int always)
+{
+ int v = always ? MSGL_INFO : MSGL_V;
+ mp_msg(MSGT_CPLAYER, v,
+ "%s (C) 2000-2013 mpv/MPlayer/mplayer2 projects\n built on %s\n", mplayer_version, mplayer_builddate);
+ print_libav_versions(v);
+ mp_msg(MSGT_CPLAYER, v, "\n");
+}
+
+static bool handle_help_options(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ int opt_exit = 0;
+ if (opts->audio_decoders && strcmp(opts->audio_decoders, "help") == 0) {
+ struct mp_decoder_list *list = mp_audio_decoder_list();
+ mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Audio decoders:", list);
+ talloc_free(list);
+ opt_exit = 1;
+ }
+ if (opts->video_decoders && strcmp(opts->video_decoders, "help") == 0) {
+ struct mp_decoder_list *list = mp_video_decoder_list();
+ mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Video decoders:", list);
+ talloc_free(list);
+ opt_exit = 1;
+ }
+#ifdef CONFIG_X11
+ if (opts->vo.fstype_list && strcmp(opts->vo.fstype_list[0], "help") == 0) {
+ fstype_help();
+ mp_msg(MSGT_FIXME, MSGL_FIXME, "\n");
+ opt_exit = 1;
+ }
+#endif
+ if ((opts->demuxer_name && strcmp(opts->demuxer_name, "help") == 0) ||
+ (opts->audio_demuxer_name && strcmp(opts->audio_demuxer_name, "help") == 0) ||
+ (opts->sub_demuxer_name && strcmp(opts->sub_demuxer_name, "help") == 0)) {
+ demuxer_help();
+ MP_INFO(mpctx, "\n");
+ opt_exit = 1;
+ }
+ if (opts->list_properties) {
+ property_print_help();
+ opt_exit = 1;
+ }
+#ifdef CONFIG_ENCODING
+ if (encode_lavc_showhelp(mpctx->opts))
+ opt_exit = 1;
+#endif
+ return opt_exit;
+}
+
+#ifdef PTW32_STATIC_LIB
+static void detach_ptw32(void)
+{
+ pthread_win32_thread_detach_np();
+ pthread_win32_process_detach_np();
+}
+#endif
+
+static void osdep_preinit(int *p_argc, char ***p_argv)
+{
+ char *enable_talloc = getenv("MPV_LEAK_REPORT");
+ if (*p_argc > 1 && (strcmp((*p_argv)[1], "-leak-report") == 0 ||
+ strcmp((*p_argv)[1], "--leak-report") == 0))
+ enable_talloc = "1";
+ if (enable_talloc && strcmp(enable_talloc, "1") == 0)
+ talloc_enable_leak_report();
+
+#ifdef __MINGW32__
+ mp_get_converted_argv(p_argc, p_argv);
+#endif
+
+#ifdef PTW32_STATIC_LIB
+ pthread_win32_process_attach_np();
+ pthread_win32_thread_attach_np();
+ atexit(detach_ptw32);
+#endif
+
+#if defined(__MINGW32__) || defined(__CYGWIN__)
+ // stop Windows from showing all kinds of annoying error dialogs
+ SetErrorMode(0x8003);
+#endif
+
+ load_termcap(NULL); // load key-codes
+
+ mp_time_init();
+}
+
+/* This preprocessor directive is a hack to generate a mplayer-nomain.o object
+ * file for some tools to link against. */
+#ifndef DISABLE_MAIN
+static int mpv_main(int argc, char *argv[])
+{
+ osdep_preinit(&argc, &argv);
+
+ if (argc >= 1) {
+ argc--;
+ argv++;
+ }
+
+ struct MPContext *mpctx = talloc(NULL, MPContext);
+ *mpctx = (struct MPContext){
+ .last_dvb_step = 1,
+ .terminal_osd_text = talloc_strdup(mpctx, ""),
+ .playlist = talloc_struct(mpctx, struct playlist, {0}),
+ };
+
+ // Create the config context and register the options
+ mpctx->mconfig = m_config_new(mpctx, sizeof(struct MPOpts),
+ &mp_default_opts, mp_opts);
+ mpctx->opts = mpctx->mconfig->optstruct;
+ mpctx->mconfig->includefunc = cfg_include;
+ mpctx->mconfig->use_profiles = true;
+
+ struct MPOpts *opts = mpctx->opts;
+
+
+ mpctx->global = talloc_zero(mpctx, struct mpv_global);
+ mpctx->global->opts = opts;
+
+ // Nothing must call mp_msg() before this
+ mp_msg_init(mpctx->global);
+ mpctx->log = mp_log_new(mpctx, mpctx->global->log, "!cplayer");
+
+ init_libav();
+ GetCpuCaps(&gCpuCaps);
+ screenshot_init(mpctx);
+ mpctx->mixer = mixer_init(mpctx, opts);
+ command_init(mpctx);
+
+ // Preparse the command line
+ m_config_preparse_command_line(mpctx->mconfig, argc, argv);
+
+ mp_print_version(false);
+
+ if (!parse_cfgfiles(mpctx, mpctx->mconfig))
+ exit_player(mpctx, EXIT_ERROR);
+
+ int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist,
+ argc, argv);
+ if (r < 0) {
+ if (r <= M_OPT_EXIT) {
+ exit_player(mpctx, EXIT_NONE);
+ } else {
+ exit_player(mpctx, EXIT_ERROR);
+ }
+ }
+
+ if (handle_help_options(mpctx))
+ exit_player(mpctx, EXIT_NONE);
+
+ MP_VERBOSE(mpctx, "Configuration: " CONFIGURATION "\n");
+ MP_VERBOSE(mpctx, "Command line:");
+ for (int i = 0; i < argc; i++)
+ MP_VERBOSE(mpctx, " '%s'", argv[i]);
+ MP_VERBOSE(mpctx, "\n");
+
+ if (!mpctx->playlist->first && !opts->player_idle_mode) {
+ mp_print_version(true);
+ MP_INFO(mpctx, "%s", mp_gtext(mp_help_text));
+ exit_player(mpctx, EXIT_NONE);
+ }
+
+#ifdef CONFIG_PRIORITY
+ set_priority();
+#endif
+
+ init_input(mpctx);
+
+#ifdef CONFIG_ENCODING
+ if (opts->encode_output.file && *opts->encode_output.file) {
+ mpctx->encode_lavc_ctx = encode_lavc_init(&opts->encode_output);
+ if(!mpctx->encode_lavc_ctx) {
+ mp_msg(MSGT_VO, MSGL_INFO, "Encoding initialization failed.");
+ exit_player(mpctx, EXIT_ERROR);
+ }
+ m_config_set_option0(mpctx->mconfig, "vo", "lavc");
+ m_config_set_option0(mpctx->mconfig, "ao", "lavc");
+ m_config_set_option0(mpctx->mconfig, "fixed-vo", "yes");
+ m_config_set_option0(mpctx->mconfig, "force-window", "no");
+ m_config_set_option0(mpctx->mconfig, "gapless-audio", "yes");
+ mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE);
+ }
+#endif
+
+#ifdef CONFIG_ASS
+ mpctx->ass_library = mp_ass_init(opts);
+#else
+ MP_WARN(mpctx, "Compiled without libass.\n");
+ MP_WARN(mpctx, "There will be no OSD and no text subs.\n");
+#endif
+
+ mpctx->osd = osd_create(opts, mpctx->ass_library);
+
+ if (opts->force_vo) {
+ opts->fixed_vo = 1;
+ mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input,
+ mpctx->encode_lavc_ctx);
+ if (!mpctx->video_out) {
+ MP_FATAL(mpctx, "Error opening/initializing "
+ "the selected video_out (-vo) device.\n");
+ exit_player(mpctx, EXIT_ERROR);
+ }
+ mpctx->mouse_cursor_visible = true;
+ mpctx->initialized_flags |= INITIALIZED_VO;
+ }
+
+#ifdef CONFIG_LUA
+ // Lua user scripts can call arbitrary functions. Load them at a point
+ // where this is safe.
+ mp_lua_init(mpctx);
+#endif
+
+ if (opts->shuffle)
+ playlist_shuffle(mpctx->playlist);
+
+ mpctx->playlist->current = mp_resume_playlist(mpctx->playlist, opts);
+ if (!mpctx->playlist->current)
+ mpctx->playlist->current = mpctx->playlist->first;
+
+ play_files(mpctx);
+
+ exit_player(mpctx, mpctx->stop_play == PT_QUIT ? EXIT_QUIT : mpctx->quit_player_rc);
+
+ return 1;
+}
+
+int main(int argc, char *argv[])
+{
+#ifdef CONFIG_COCOA
+ return cocoa_main(mpv_main, argc, argv);
+#else
+ return mpv_main(argc, argv);
+#endif
+}
+
+#endif /* DISABLE_MAIN */
diff --git a/mpvcore/player/screenshot.c b/mpvcore/player/screenshot.c
new file mode 100644
index 0000000000..bafb3012fb
--- /dev/null
+++ b/mpvcore/player/screenshot.c
@@ -0,0 +1,405 @@
+/*
+ * This file is part of mplayer2.
+ *
+ * mplayer2 is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mplayer2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mplayer2; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "config.h"
+
+#include "osdep/io.h"
+
+#include "talloc.h"
+#include "screenshot.h"
+#include "mp_core.h"
+#include "command.h"
+#include "mpvcore/bstr.h"
+#include "mpvcore/mp_msg.h"
+#include "mp_osd.h"
+#include "mpvcore/path.h"
+#include "video/mp_image.h"
+#include "video/decode/dec_video.h"
+#include "video/filter/vf.h"
+#include "video/out/vo.h"
+#include "video/image_writer.h"
+#include "sub/sub.h"
+
+#include "video/csputils.h"
+
+#define MODE_FULL_WINDOW 1
+#define MODE_SUBTITLES 2
+
+typedef struct screenshot_ctx {
+ struct MPContext *mpctx;
+
+ int mode;
+ bool each_frame;
+ bool osd;
+
+ int frameno;
+} screenshot_ctx;
+
+void screenshot_init(struct MPContext *mpctx)
+{
+ mpctx->screenshot_ctx = talloc(mpctx, screenshot_ctx);
+ *mpctx->screenshot_ctx = (screenshot_ctx) {
+ .mpctx = mpctx,
+ .frameno = 1,
+ };
+}
+
+#define SMSG_OK 0
+#define SMSG_ERR 1
+
+static void screenshot_msg(screenshot_ctx *ctx, int status, const char *msg,
+ ...) PRINTF_ATTRIBUTE(3,4);
+
+static void screenshot_msg(screenshot_ctx *ctx, 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(MSGT_CPLAYER, status == SMSG_ERR ? MSGL_ERR : MSGL_INFO, "%s\n", s);
+ if (ctx->osd) {
+ set_osd_tmsg(ctx->mpctx, OSD_MSG_TEXT, 1, ctx->mpctx->opts->osd_duration,
+ "%s", s);
+ }
+
+ talloc_free(s);
+}
+
+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);
+}
+
+#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')
+ template = "shot%n";
+
+ 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 = mp_basename(mpctx->filename);
+ if (video_file) {
+ char *name = video_file;
+ if (fmt == 'F')
+ name = stripext(res, video_file);
+ append_filename(&res, name);
+ }
+ break;
+ }
+ case 'p':
+ case 'P': {
+ char *t = mp_format_time(get_current_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_current_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);
+ return talloc_asprintf_append(res, ".%s", file_ext);
+
+error_exit:
+ talloc_free(res);
+ return NULL;
+}
+
+static char *gen_fname(screenshot_ctx *ctx, const char *file_ext)
+{
+ 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) {
+ screenshot_msg(ctx, SMSG_ERR, "Invalid screenshot filename "
+ "template! Fix or remove the --screenshot-template "
+ "option.");
+ return NULL;
+ }
+
+ if (!mp_path_exists(fname))
+ return fname;
+
+ if (sequence == prev_sequence) {
+ screenshot_msg(ctx, SMSG_ERR, "Can't save screenshot, file '%s' "
+ "already exists!", fname);
+ talloc_free(fname);
+ return NULL;
+ }
+
+ talloc_free(fname);
+ }
+}
+
+static void add_subs(struct MPContext *mpctx, struct mp_image *image)
+{
+ int d_w = image->display_w ? image->display_w : image->w;
+ int d_h = image->display_h ? image->display_h : image->h;
+
+ double sar = (double)image->w / image->h;
+ double dar = (double)d_w / d_h;
+ struct mp_osd_res res = {
+ .w = image->w,
+ .h = image->h,
+ .display_par = sar / dar,
+ .video_par = dar / sar,
+ };
+
+ osd_draw_on_image(mpctx->osd, res, mpctx->osd->vo_pts,
+ OSD_DRAW_SUB_ONLY, image);
+}
+
+static void screenshot_save(struct MPContext *mpctx, struct mp_image *image)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ struct image_writer_opts *opts = mpctx->opts->screenshot_image_opts;
+
+ char *filename = gen_fname(ctx, image_writer_file_ext(opts));
+ if (filename) {
+ screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename);
+ if (!write_image(image, opts, filename))
+ screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!");
+ talloc_free(filename);
+ }
+}
+
+static struct mp_image *screenshot_get(struct MPContext *mpctx, int mode)
+{
+ struct mp_image *image = NULL;
+ if (mpctx->video_out && mpctx->video_out->config_ok) {
+ if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter)
+ mode = 0;
+
+ struct voctrl_screenshot_args args =
+ { .full_window = (mode == MODE_FULL_WINDOW) };
+
+ if (mpctx->sh_video && mpctx->sh_video->vfilter) {
+ struct vf_instance *vfilter = mpctx->sh_video->vfilter;
+ vfilter->control(vfilter, VFCTRL_SCREENSHOT, &args);
+ }
+
+ if (!args.out_image)
+ vo_control(mpctx->video_out, VOCTRL_SCREENSHOT, &args);
+
+ image = args.out_image;
+ if (image) {
+ if (mode == MODE_SUBTITLES && !args.has_osd)
+ add_subs(mpctx, image);
+ }
+ }
+ return image;
+}
+
+void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode,
+ bool osd)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+ struct image_writer_opts opts = *mpctx->opts->screenshot_image_opts;
+ bool old_osd = ctx->osd;
+ ctx->osd = osd;
+
+ if (mp_path_exists(filename)) {
+ screenshot_msg(ctx, SMSG_ERR, "Screenshot: file '%s' already exists.",
+ filename);
+ goto end;
+ }
+ char *ext = mp_splitext(filename, NULL);
+ if (ext)
+ opts.format = ext + 1; // omit '.'
+ struct mp_image *image = screenshot_get(mpctx, mode);
+ if (!image) {
+ screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed.");
+ goto end;
+ }
+ screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename);
+ if (!write_image(image, &opts, filename))
+ screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!");
+ talloc_free(image);
+
+end:
+ ctx->osd = old_osd;
+}
+
+void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame,
+ bool osd)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter)
+ mode = 0;
+
+ if (each_frame) {
+ ctx->each_frame = !ctx->each_frame;
+ if (!ctx->each_frame)
+ return;
+ } else {
+ ctx->each_frame = false;
+ }
+
+ ctx->mode = mode;
+ ctx->osd = osd;
+
+ struct mp_image *image = screenshot_get(mpctx, mode);
+
+ if (image) {
+ screenshot_save(mpctx, image);
+ } else {
+ screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed.");
+ }
+
+ talloc_free(image);
+}
+
+void screenshot_flip(struct MPContext *mpctx)
+{
+ screenshot_ctx *ctx = mpctx->screenshot_ctx;
+
+ if (!ctx->each_frame)
+ return;
+
+ ctx->each_frame = false;
+ screenshot_request(mpctx, ctx->mode, true, ctx->osd);
+}
diff --git a/mpvcore/player/screenshot.h b/mpvcore/player/screenshot.h
new file mode 100644
index 0000000000..1b12ac9b73
--- /dev/null
+++ b/mpvcore/player/screenshot.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of mplayer2.
+ *
+ * mplayer2 is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mplayer2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mplayer2; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_SCREENSHOT_H
+#define MPLAYER_SCREENSHOT_H
+
+#include <stdbool.h>
+
+struct MPContext;
+
+// One time initialization at program start.
+void screenshot_init(struct MPContext *mpctx);
+
+// Request a taking & saving a screenshot of the currently displayed frame.
+// mode: 0: -, 1: save the actual output window contents, 2: with subtitles.
+// each_frame: If set, this toggles per-frame screenshots, exactly like the
+// screenshot slave command (MP_CMD_SCREENSHOT).
+// osd: show status on OSD
+void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame,
+ bool osd);
+
+// filename: where to store the screenshot; doesn't try to find an alternate
+// name if the file already exists
+// mode, osd: same as in screenshot_request()
+void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode,
+ bool osd);
+
+// Called by the playback core code when a new frame is displayed.
+void screenshot_flip(struct MPContext *mpctx);
+
+#endif /* MPLAYER_SCREENSHOT_H */
diff --git a/mpvcore/player/timeline/tl_cue.c b/mpvcore/player/timeline/tl_cue.c
new file mode 100644
index 0000000000..e68b3349a5
--- /dev/null
+++ b/mpvcore/player/timeline/tl_cue.c
@@ -0,0 +1,417 @@
+/*
+ * This file is part of mplayer2.
+ *
+ * mplayer2 is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mplayer2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mplayer2; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <dirent.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <ctype.h>
+
+#include "talloc.h"
+
+#include "mpvcore/player/mp_core.h"
+#include "mpvcore/mp_msg.h"
+#include "demux/demux.h"
+#include "mpvcore/path.h"
+#include "mpvcore/bstr.h"
+#include "mpvcore/mp_common.h"
+#include "stream/stream.h"
+
+// used by demuxer_cue.c
+bool mp_probe_cue(struct bstr data);
+
+#define SECS_PER_CUE_FRAME (1.0/75.0)
+
+enum cue_command {
+ CUE_ERROR = -1, // not a valid CUE command, or an unknown extension
+ CUE_EMPTY, // line with whitespace only
+ CUE_UNUSED, // valid CUE command, but ignored by this code
+ CUE_FILE,
+ CUE_TRACK,
+ CUE_INDEX,
+ CUE_TITLE,
+};
+
+static const struct {
+ enum cue_command command;
+ const char *text;
+} cue_command_strings[] = {
+ { CUE_FILE, "FILE" },
+ { CUE_TRACK, "TRACK" },
+ { CUE_INDEX, "INDEX" },
+ { CUE_TITLE, "TITLE" },
+ { CUE_UNUSED, "CATALOG" },
+ { CUE_UNUSED, "CDTEXTFILE" },
+ { CUE_UNUSED, "FLAGS" },
+ { CUE_UNUSED, "ISRC" },
+ { CUE_UNUSED, "PERFORMER" },
+ { CUE_UNUSED, "POSTGAP" },
+ { CUE_UNUSED, "PREGAP" },
+ { CUE_UNUSED, "REM" },
+ { CUE_UNUSED, "SONGWRITER" },
+ { CUE_UNUSED, "MESSAGE" },
+ { -1 },
+};
+
+struct cue_track {
+ double pregap_start; // corresponds to INDEX 00
+ double start; // corresponds to INDEX 01
+ struct bstr filename;
+ int source;
+ struct bstr title;
+};
+
+static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params)
+{
+ struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data));
+ line = bstr_lstrip(line);
+ if (line.len == 0)
+ return CUE_EMPTY;
+ for (int n = 0; cue_command_strings[n].command != -1; n++) {
+ struct bstr name = bstr0(cue_command_strings[n].text);
+ if (bstr_startswith(line, name)) {
+ struct bstr rest = bstr_cut(line, name.len);
+ if (rest.len && !strchr(WHITESPACE, rest.start[0]))
+ continue;
+ if (out_params)
+ *out_params = rest;
+ return cue_command_strings[n].command;
+ }
+ }
+ return CUE_ERROR;
+}
+
+static bool eat_char(struct bstr *data, char ch)
+{
+ if (data->len && data->start[0] == ch) {
+ *data = bstr_cut(*data, 1);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+static struct bstr read_quoted(struct bstr *data)
+{
+ *data = bstr_lstrip(*data);
+ if (!eat_char(data, '"'))
+ return (struct bstr) {0};
+ int end = bstrchr(*data, '"');
+ if (end < 0)
+ return (struct bstr) {0};
+ struct bstr res = bstr_splice(*data, 0, end);
+ *data = bstr_cut(*data, end + 1);
+ return res;
+}
+
+// Read a 2 digit unsigned decimal integer.
+// Return -1 on failure.
+static int read_int_2(struct bstr *data)
+{
+ *data = bstr_lstrip(*data);
+ if (data->len && data->start[0] == '-')
+ return -1;
+ struct bstr s = *data;
+ int res = (int)bstrtoll(s, &s, 10);
+ if (data->len == s.len || data->len - s.len > 2)
+ return -1;
+ *data = s;
+ return res;
+}
+
+static double read_time(struct bstr *data)
+{
+ struct bstr s = *data;
+ bool ok = true;
+ double t1 = read_int_2(&s);
+ ok = eat_char(&s, ':') && ok;
+ double t2 = read_int_2(&s);
+ ok = eat_char(&s, ':') && ok;
+ double t3 = read_int_2(&s);
+ ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0;
+ return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0;
+}
+
+static struct bstr skip_utf8_bom(struct bstr data)
+{
+ return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data;
+}
+
+// Check if the text in data is most likely CUE data. This is used by the
+// demuxer code to check the file type.
+// data is the start of the probed file, possibly cut off at a random point.
+bool mp_probe_cue(struct bstr data)
+{
+ bool valid = false;
+ data = skip_utf8_bom(data);
+ for (;;) {
+ enum cue_command cmd = read_cmd(&data, NULL);
+ // End reached. Since the line was most likely cut off, don't use the
+ // result of the last parsing call.
+ if (data.len == 0)
+ break;
+ if (cmd == CUE_ERROR)
+ return false;
+ if (cmd != CUE_EMPTY)
+ valid = true;
+ }
+ return valid;
+}
+
+static void add_source(struct MPContext *mpctx, struct demuxer *d)
+{
+ MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, d);
+}
+
+static bool try_open(struct MPContext *mpctx, char *filename)
+{
+ struct bstr bfilename = bstr0(filename);
+ // Avoid trying to open itself or another .cue file. Best would be
+ // to check the result of demuxer auto-detection, but the demuxer
+ // API doesn't allow this without opening a full demuxer.
+ if (bstr_case_endswith(bfilename, bstr0(".cue"))
+ || bstrcasecmp(bstr0(mpctx->demuxer->filename), bfilename) == 0)
+ return false;
+
+ struct stream *s = stream_open(filename, mpctx->opts);
+ if (!s)
+ return false;
+ struct demuxer *d = demux_open(s, NULL, NULL, mpctx->opts);
+ // Since .bin files are raw PCM data with no headers, we have to explicitly
+ // open them. Also, try to avoid to open files that are most likely not .bin
+ // files, as that would only play noise. Checking the file extension is
+ // fragile, but it's about the only way we have.
+ // TODO: maybe also could check if the .bin file is a multiple of the Audio
+ // CD sector size (2352 bytes)
+ if (!d && bstr_case_endswith(bfilename, bstr0(".bin"))) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: Opening as BIN file!\n");
+ d = demux_open(s, "rawaudio", NULL, mpctx->opts);
+ }
+ if (d) {
+ add_source(mpctx, d);
+ return true;
+ }
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "Could not open source '%s'!\n", filename);
+ free_stream(s);
+ return false;
+}
+
+static bool open_source(struct MPContext *mpctx, struct bstr filename)
+{
+ void *ctx = talloc_new(NULL);
+ bool res = false;
+
+ struct bstr dirname = mp_dirname(mpctx->demuxer->filename);
+
+ struct bstr base_filename = bstr0(mp_basename(bstrdup0(ctx, filename)));
+ if (!base_filename.len) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN,
+ "CUE: Invalid audio filename in .cue file!\n");
+ } else {
+ char *fullname = mp_path_join(ctx, dirname, base_filename);
+ if (try_open(mpctx, fullname)) {
+ res = true;
+ goto out;
+ }
+ }
+
+ // Try an audio file with the same name as the .cue file (but different
+ // extension).
+ // Rationale: this situation happens easily if the audio file or both files
+ // are renamed.
+
+ struct bstr cuefile =
+ bstr_strip_ext(bstr0(mp_basename(mpctx->demuxer->filename)));
+
+ DIR *d = opendir(bstrdup0(ctx, dirname));
+ if (!d)
+ goto out;
+ struct dirent *de;
+ while ((de = readdir(d))) {
+ char *dename0 = de->d_name;
+ struct bstr dename = bstr0(dename0);
+ if (bstr_case_startswith(dename, cuefile)) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: No useful audio filename "
+ "in .cue file found, trying with '%s' instead!\n",
+ dename0);
+ if (try_open(mpctx, mp_path_join(ctx, dirname, dename))) {
+ res = true;
+ break;
+ }
+ }
+ }
+ closedir(d);
+
+out:
+ talloc_free(ctx);
+ if (!res)
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: Could not open audio file!\n");
+ return res;
+}
+
+// return length of the source in seconds, or -1 if unknown
+static double source_get_length(struct demuxer *demuxer)
+{
+ double get_time_ans;
+ // <= 0 means DEMUXER_CTRL_NOTIMPL or DEMUXER_CTRL_DONTKNOW
+ if (demuxer && demux_control(demuxer, DEMUXER_CTRL_GET_TIME_LENGTH,
+ (void *) &get_time_ans) > 0)
+ {
+ return get_time_ans;
+ } else {
+ return -1;
+ }
+}
+
+void build_cue_timeline(struct MPContext *mpctx)
+{
+ void *ctx = talloc_new(NULL);
+
+ struct bstr data = mpctx->demuxer->file_contents;
+ data = skip_utf8_bom(data);
+
+ struct cue_track *tracks = NULL;
+ size_t track_count = 0;
+
+ struct bstr filename = {0};
+ // Global metadata, and copied into new tracks.
+ struct cue_track proto_track = {0};
+ struct cue_track *cur_track = &proto_track;
+
+ while (data.len) {
+ struct bstr param;
+ switch (read_cmd(&data, &param)) {
+ case CUE_ERROR:
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: error parsing input file!\n");
+ goto out;
+ case CUE_TRACK: {
+ track_count++;
+ tracks = talloc_realloc(ctx, tracks, struct cue_track, track_count);
+ cur_track = &tracks[track_count - 1];
+ *cur_track = proto_track;
+ break;
+ }
+ case CUE_TITLE:
+ cur_track->title = read_quoted(&param);
+ break;
+ case CUE_INDEX: {
+ int type = read_int_2(&param);
+ double time = read_time(&param);
+ if (type == 1) {
+ cur_track->start = time;
+ cur_track->filename = filename;
+ } else if (type == 0) {
+ cur_track->pregap_start = time;
+ }
+ break;
+ }
+ case CUE_FILE:
+ // NOTE: FILE comes before TRACK, so don't use cur_track->filename
+ filename = read_quoted(&param);
+ break;
+ }
+ }
+
+ if (track_count == 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: no tracks found!\n");
+ goto out;
+ }
+
+ // Remove duplicate file entries. This might be too sophisticated, since
+ // CUE files usually use either separate files for every single track, or
+ // only one file for all tracks.
+
+ struct bstr *files = 0;
+ size_t file_count = 0;
+
+ for (size_t n = 0; n < track_count; n++) {
+ struct cue_track *track = &tracks[n];
+ track->source = -1;
+ for (size_t file = 0; file < file_count; file++) {
+ if (bstrcmp(files[file], track->filename) == 0) {
+ track->source = file;
+ break;
+ }
+ }
+ if (track->source == -1) {
+ file_count++;
+ files = talloc_realloc(ctx, files, struct bstr, file_count);
+ files[file_count - 1] = track->filename;
+ track->source = file_count - 1;
+ }
+ }
+
+ for (size_t i = 0; i < file_count; i++) {
+ if (!open_source(mpctx, files[i]))
+ goto out;
+ }
+
+ struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline,
+ track_count + 1);
+ struct chapter *chapters = talloc_array_ptrtype(NULL, chapters,
+ track_count);
+ double starttime = 0;
+ for (int i = 0; i < track_count; i++) {
+ struct demuxer *source = mpctx->sources[1 + tracks[i].source];
+ double duration;
+ if (i + 1 < track_count && tracks[i].source == tracks[i + 1].source) {
+ duration = tracks[i + 1].start - tracks[i].start;
+ } else {
+ duration = source_get_length(source);
+ // Two cases: 1) last track of a single-file cue, or 2) any track of
+ // a multi-file cue. We need to do this for 1) only because the
+ // timeline needs to be terminated with the length of the last
+ // track.
+ duration -= tracks[i].start;
+ }
+ if (duration < 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN,
+ "CUE: Can't get duration of source file!\n");
+ // xxx: do something more reasonable
+ duration = 0.0;
+ }
+ timeline[i] = (struct timeline_part) {
+ .start = starttime,
+ .source_start = tracks[i].start,
+ .source = source,
+ };
+ chapters[i] = (struct chapter) {
+ .start = timeline[i].start,
+ // might want to include other metadata here
+ .name = bstrdup0(chapters, tracks[i].title),
+ };
+ starttime += duration;
+ }
+
+ // apparently we need this to give the last part a non-zero length
+ timeline[track_count] = (struct timeline_part) {
+ .start = starttime,
+ // perhaps unused by the timeline code
+ .source_start = 0,
+ .source = timeline[0].source,
+ };
+
+ mpctx->timeline = timeline;
+ // the last part is not included it in the count
+ mpctx->num_timeline_parts = track_count + 1 - 1;
+ mpctx->chapters = chapters;
+ mpctx->num_chapters = track_count;
+
+out:
+ talloc_free(ctx);
+}
diff --git a/mpvcore/player/timeline/tl_edl.c b/mpvcore/player/timeline/tl_edl.c
new file mode 100644
index 0000000000..69e2402149
--- /dev/null
+++ b/mpvcore/player/timeline/tl_edl.c
@@ -0,0 +1,393 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <ctype.h>
+
+#include "talloc.h"
+
+#include "mpvcore/player/mp_core.h"
+#include "mpvcore/mp_msg.h"
+#include "demux/demux.h"
+#include "mpvcore/path.h"
+#include "mpvcore/bstr.h"
+#include "mpvcore/mp_common.h"
+#include "stream/stream.h"
+
+
+struct edl_source {
+ struct bstr id;
+ char *filename;
+ int lineno;
+};
+
+struct edl_time {
+ int64_t start;
+ int64_t end;
+ bool implied_start;
+ bool implied_end;
+};
+
+struct edl_part {
+ struct edl_time tl;
+ struct edl_time src;
+ int64_t duration;
+ int id;
+ int lineno;
+};
+
+static int find_edl_source(struct edl_source *sources, int num_sources,
+ struct bstr name)
+{
+ for (int i = 0; i < num_sources; i++)
+ if (!bstrcmp(sources[i].id, name))
+ return i;
+ return -1;
+}
+
+void build_edl_timeline(struct MPContext *mpctx)
+{
+ const struct bstr file_prefix = bstr0("<");
+ void *tmpmem = talloc_new(NULL);
+
+ struct bstr *lines = bstr_splitlines(tmpmem, mpctx->demuxer->file_contents);
+ int linec = MP_TALLOC_ELEMS(lines);
+ struct bstr header = bstr0("mplayer EDL file, version ");
+ if (!linec || !bstr_startswith(lines[0], header)) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Bad EDL header!\n");
+ goto out;
+ }
+ struct bstr version = bstr_strip(bstr_cut(lines[0], header.len));
+ if (bstrcmp(bstr0("2"), version)) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Unsupported EDL file version!\n");
+ goto out;
+ }
+ int num_sources = 0;
+ int num_parts = 0;
+ for (int i = 1; i < linec; i++) {
+ if (bstr_startswith(lines[i], file_prefix)) {
+ num_sources++;
+ } else {
+ int comment = bstrchr(lines[i], '#');
+ if (comment >= 0)
+ lines[i] = bstr_splice(lines[i], 0, comment);
+ if (bstr_strip(lines[i]).len)
+ num_parts++;
+ }
+ }
+ if (!num_parts) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "No parts in timeline!\n");
+ goto out;
+ }
+
+ // Parse source filename definitions
+
+ struct edl_source *edl_ids = talloc_array_ptrtype(tmpmem, edl_ids,
+ num_sources);
+ num_sources = 0;
+ for (int i = 1; i < linec; i++) {
+ struct bstr line = lines[i];
+ if (!bstr_startswith(line, file_prefix))
+ continue;
+ line = bstr_cut(line, file_prefix.len);
+ struct bstr id = bstr_split(line, WHITESPACE, &line);
+ if (find_edl_source(edl_ids, num_sources, id) >= 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Repeated ID on line %d!\n",
+ i+1);
+ goto out;
+ }
+ if (!isalpha(*id.start)) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Invalid ID on line %d!\n",
+ i+1);
+ goto out;
+ }
+ char *filename = mp_basename(bstrdup0(tmpmem, bstr_strip(line)));
+ if (!strlen(filename)) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR,
+ "EDL: Invalid filename on line %d!\n", i+1);
+ goto out;
+ }
+ struct bstr dirname = mp_dirname(mpctx->demuxer->filename);
+ char *fullname = mp_path_join(tmpmem, dirname, bstr0(filename));
+ edl_ids[num_sources++] = (struct edl_source){id, fullname, i+1};
+ }
+
+ // Parse timeline part definitions
+
+ struct edl_part *parts = talloc_array_ptrtype(tmpmem, parts, num_parts);
+ int total_parts = num_parts;
+ num_parts = 0;
+ for (int i = 1; i < linec; i++) {
+ struct bstr line = bstr_strip(lines[i]);
+ if (!line.len || bstr_startswith(line, file_prefix))
+ continue;
+ parts[num_parts] = (struct edl_part){{-1, -1}, {-1, -1}, 0, -1};
+ parts[num_parts].lineno = i + 1;
+ for (int s = 0; s < 2; s++) {
+ struct edl_time *p = !s ? &parts[num_parts].tl :
+ &parts[num_parts].src;
+ while (1) {
+ struct bstr t = bstr_split(line, WHITESPACE, &line);
+ if (!t.len) {
+ if (!s && num_parts < total_parts - 1) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: missing source "
+ "identifier on line %d (not last)!\n", i+1);
+ goto out;
+ }
+ break;
+ }
+ if (isalpha(*t.start)) {
+ if (s)
+ goto bad;
+ parts[num_parts].id = find_edl_source(edl_ids, num_sources,
+ t);
+ if (parts[num_parts].id < 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Undefined source "
+ "identifier on line %d!\n", i+1);
+ goto out;
+ }
+ break;
+ }
+ while (t.len) {
+ struct bstr next;
+ struct bstr arg = bstr_split(t, "+-", &next);
+ if (!arg.len) {
+ next = bstr_split(line, WHITESPACE, &line);
+ arg = bstr_split(next, "+-", &next);
+ }
+ if (!arg.len)
+ goto bad;
+ int64_t val;
+ if (!bstrcmp(arg, bstr0("*")))
+ val = -1;
+ else if (isdigit(*arg.start)) {
+ val = bstrtoll(arg, &arg, 10) * 1000000000;
+ if (arg.len && *arg.start == '.') {
+ int len = arg.len - 1;
+ arg = bstr_splice(arg, 1, 10);
+ int64_t val2 = bstrtoll(arg, &arg, 10);
+ if (arg.len)
+ goto bad;
+ for (; len < 9; len++)
+ val2 *= 10;
+ val += val2;
+ }
+ } else
+ goto bad;
+ int c = *t.start;
+ if (isdigit(c) || c == '*') {
+ if (val < 0)
+ p->implied_start = true;
+ else
+ p->start = val;
+ } else if (c == '-') {
+ if (val < 0)
+ p->implied_end = true;
+ else
+ p->end = val;
+ } else if (c == '+') {
+ if (val < 0)
+ goto bad;
+ if (val == 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: zero duration "
+ "on line %d!\n", i+1);
+ goto out;
+ }
+ parts[num_parts].duration = val;
+ } else
+ goto bad;
+ t = next;
+ }
+ }
+ }
+ num_parts++;
+ continue;
+ bad:
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Malformed line %d!\n", i+1);
+ goto out;
+ }
+
+ // Fill in implied start/stop/duration values
+
+ int64_t *times = talloc_zero_array(tmpmem, int64_t, num_sources);
+ while (1) {
+ int64_t time = 0;
+ for (int i = 0; i < num_parts; i++) {
+ for (int s = 0; s < 2; s++) {
+ struct edl_time *p = s ? &parts[i].tl : &parts[i].src;
+ if (!s && parts[i].id == -1)
+ continue;
+ int64_t *t = s ? &time : times + parts[i].id;
+ p->implied_start |= s && *t >= 0;
+ if (p->implied_start && p->start >= 0 && *t >= 0
+ && p->start != *t) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line "
+ "%d!\n", parts[i].lineno);
+ goto out;
+ }
+ if (p->start >= 0)
+ *t = p->start;
+ if (p->implied_start)
+ p->start = *t;
+ if (*t >= 0 && parts[i].duration)
+ *t += parts[i].duration;
+ else
+ *t = -1;
+ if (p->end >= 0)
+ *t = p->end;
+ }
+ }
+ for (int i = 0; i < num_sources; i++)
+ times[i] = -1;
+ time = -1;
+ for (int i = num_parts - 1; i >= 0; i--) {
+ for (int s = 0; s < 2; s++) {
+ struct edl_time *p = s ? &parts[i].tl : &parts[i].src;
+ if (!s && parts[i].id == -1)
+ continue;
+ int64_t *t = s ? &time : times + parts[i].id;
+ p->implied_end |= s && *t >= 0;
+ if (p->implied_end && p->end >= 0 && *t >=0 && p->end != *t) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line "
+ "%d!\n", parts[i].lineno);
+ goto out;
+ }
+ if (p->end >= 0)
+ *t = p->end;
+ if (p->implied_end)
+ p->end = *t;
+ if (*t >= 0 && parts[i].duration) {
+ *t -= parts[i].duration;
+ if (*t < 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Negative time "
+ "on line %d!\n", parts[i].lineno);
+ goto out;
+ }
+ } else
+ *t = -1;
+ if (p->start >= 0)
+ *t = p->start;
+ }
+ }
+ int missing_duration = -1;
+ int missing_srcstart = -1;
+ bool anything_done = false;
+ for (int i = 0; i < num_parts; i++) {
+ int64_t duration = parts[i].duration;
+ if (parts[i].tl.start >= 0 && parts[i].tl.end >= 0) {
+ int64_t duration2 = parts[i].tl.end - parts[i].tl.start;
+ if (duration && duration != duration2)
+ goto incons;
+ duration = duration2;
+ if (duration <= 0)
+ goto neg;
+ }
+ if (parts[i].src.start >= 0 && parts[i].src.end >= 0) {
+ int64_t duration2 = parts[i].src.end - parts[i].src.start;
+ if (duration && duration != duration2) {
+ incons:
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line "
+ "%d!\n", i+1);
+ goto out;
+ }
+ duration = duration2;
+ if (duration <= 0) {
+ neg:
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: duration <= 0 on "
+ "line %d!\n", parts[i].lineno);
+ goto out;
+ }
+ }
+ if (parts[i].id == -1)
+ continue;
+ if (!duration)
+ missing_duration = i;
+ else if (!parts[i].duration)
+ anything_done = true;
+ parts[i].duration = duration;
+ if (duration && parts[i].src.start < 0) {
+ if (parts[i].src.end < 0)
+ missing_srcstart = i;
+ else
+ parts[i].src.start = parts[i].src.end - duration;
+ }
+ }
+ if (!anything_done) {
+ if (missing_duration >= 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Could not determine "
+ "duration for line %d!\n",
+ parts[missing_duration].lineno);
+ goto out;
+ }
+ if (missing_srcstart >= 0) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: no source start time for "
+ "line %d!\n", parts[missing_srcstart].lineno);
+ goto out;
+ }
+ break;
+ }
+ }
+
+ // Open source files
+
+ struct demuxer **sources = talloc_array_ptrtype(NULL, sources,
+ num_sources + 1);
+ talloc_free(mpctx->sources);
+ mpctx->sources = sources;
+ sources[0] = mpctx->demuxer;
+ mpctx->num_sources = 1;
+
+ for (int i = 0; i < num_sources; i++) {
+ struct stream *s = stream_open(edl_ids[i].filename, mpctx->opts);
+ if (!s)
+ goto openfail;
+ struct demuxer *d = demux_open(s, NULL, NULL, mpctx->opts);
+ if (!d) {
+ free_stream(s);
+ openfail:
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Could not open source "
+ "file on line %d!\n", edl_ids[i].lineno);
+ goto out;
+ }
+ sources[mpctx->num_sources] = d;
+ mpctx->num_sources++;
+ }
+
+ // Write final timeline structure
+
+ struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline,
+ num_parts + 1);
+ int64_t starttime = 0;
+ for (int i = 0; i < num_parts; i++) {
+ timeline[i].start = starttime / 1e9;
+ starttime += parts[i].duration;
+ timeline[i].source_start = parts[i].src.start / 1e9;
+ timeline[i].source = sources[parts[i].id + 1];
+ }
+ if (parts[num_parts - 1].id != -1) {
+ timeline[num_parts].start = starttime / 1e9;
+ num_parts++;
+ }
+ mpctx->timeline = timeline;
+ mpctx->num_timeline_parts = num_parts - 1;
+
+ out:
+ talloc_free(tmpmem);
+}
diff --git a/mpvcore/player/timeline/tl_matroska.c b/mpvcore/player/timeline/tl_matroska.c
new file mode 100644
index 0000000000..5a96cfe5f1
--- /dev/null
+++ b/mpvcore/player/timeline/tl_matroska.c
@@ -0,0 +1,575 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <libavutil/common.h>
+
+#include "osdep/io.h"
+
+#include "talloc.h"
+
+#include "mpvcore/player/mp_core.h"
+#include "mpvcore/mp_msg.h"
+#include "demux/demux.h"
+#include "mpvcore/path.h"
+#include "mpvcore/bstr.h"
+#include "mpvcore/mp_common.h"
+#include "stream/stream.h"
+
+struct find_entry {
+ char *name;
+ int matchlen;
+ off_t size;
+};
+
+static int cmp_entry(const void *pa, const void *pb)
+{
+ const struct find_entry *a = pa, *b = pb;
+ // check "similar" filenames first
+ int matchdiff = b->matchlen - a->matchlen;
+ if (matchdiff)
+ return FFSIGN(matchdiff);
+ // check small files first
+ off_t sizediff = a->size - b->size;
+ if (sizediff)
+ return FFSIGN(sizediff);
+ return 0;
+}
+
+static char **find_files(const char *original_file, const char *suffix)
+{
+ void *tmpmem = talloc_new(NULL);
+ char *basename = mp_basename(original_file);
+ struct bstr directory = mp_dirname(original_file);
+ char **results = talloc_size(NULL, 0);
+ char *dir_zero = bstrdup0(tmpmem, directory);
+ DIR *dp = opendir(dir_zero);
+ if (!dp) {
+ talloc_free(tmpmem);
+ return results;
+ }
+ struct find_entry *entries = NULL;
+ struct dirent *ep;
+ int num_results = 0;
+ while ((ep = readdir(dp))) {
+ int suffix_offset = strlen(ep->d_name) - strlen(suffix);
+ // name must end with suffix
+ if (suffix_offset < 0 || strcmp(ep->d_name + suffix_offset, suffix))
+ continue;
+ // don't list the original name
+ if (!strcmp(ep->d_name, basename))
+ continue;
+
+ char *name = mp_path_join(results, directory, bstr0(ep->d_name));
+ char *s1 = ep->d_name;
+ char *s2 = basename;
+ int matchlen = 0;
+ while (*s1 && *s1++ == *s2++)
+ matchlen++;
+ // be a bit more fuzzy about matching the filename
+ matchlen = (matchlen + 3) / 5;
+
+ struct stat statbuf;
+ if (stat(name, &statbuf) != 0)
+ continue;
+ off_t size = statbuf.st_size;
+
+ entries = talloc_realloc(tmpmem, entries, struct find_entry,
+ num_results + 1);
+ entries[num_results] = (struct find_entry) { name, matchlen, size };
+ num_results++;
+ }
+ closedir(dp);
+ // NOTE: maybe should make it compare pointers instead
+ if (entries)
+ qsort(entries, num_results, sizeof(struct find_entry), cmp_entry);
+ results = talloc_realloc(NULL, results, char *, num_results);
+ for (int i = 0; i < num_results; i++) {
+ results[i] = entries[i].name;
+ }
+ talloc_free(tmpmem);
+ return results;
+}
+
+static int enable_cache(struct MPContext *mpctx, struct stream **stream,
+ struct demuxer **demuxer, struct demuxer_params *params)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (opts->stream_cache_size <= 0)
+ return 0;
+
+ char *filename = talloc_strdup(NULL, (*demuxer)->filename);
+ free_demuxer(*demuxer);
+ free_stream(*stream);
+
+ *stream = stream_open(filename, opts);
+ if (!*stream) {
+ talloc_free(filename);
+ return -1;
+ }
+
+ stream_enable_cache_percent(stream,
+ opts->stream_cache_size,
+ opts->stream_cache_def_size,
+ opts->stream_cache_min_percent,
+ opts->stream_cache_seek_min_percent);
+
+ *demuxer = demux_open(*stream, "mkv", params, opts);
+ if (!*demuxer) {
+ talloc_free(filename);
+ free_stream(*stream);
+ return -1;
+ }
+
+ talloc_free(filename);
+ return 1;
+}
+
+static bool has_source_request(struct matroska_segment_uid *uids,
+ int num_sources,
+ struct matroska_segment_uid *new_uid)
+{
+ for (int i = 0; i < num_sources; ++i) {
+ if (demux_matroska_uid_cmp(uids + i, new_uid))
+ return true;
+ }
+
+ return false;
+}
+
+// segment = get Nth segment of a multi-segment file
+static bool check_file_seg(struct MPContext *mpctx, struct demuxer ***sources,
+ int *num_sources, struct matroska_segment_uid **uids,
+ char *filename, int segment)
+{
+ bool was_valid = false;
+ struct demuxer_params params = {
+ .matroska_num_wanted_uids = *num_sources,
+ .matroska_wanted_uids = *uids,
+ .matroska_wanted_segment = segment,
+ .matroska_was_valid = &was_valid,
+ };
+ struct stream *s = stream_open(filename, mpctx->opts);
+ if (!s)
+ return false;
+ struct demuxer *d = demux_open(s, "mkv", &params, mpctx->opts);
+
+ if (!d) {
+ free_stream(s);
+ return was_valid;
+ }
+ if (d->type == DEMUXER_TYPE_MATROSKA) {
+ struct matroska_data *m = &d->matroska_data;
+
+ for (int i = 1; i < *num_sources; i++) {
+ struct matroska_segment_uid *uid = *uids + i;
+ if ((*sources)[i])
+ continue;
+ /* Accept the source if the segment uid matches and the edition
+ * either matches or isn't specified. */
+ if (!memcmp(uid->segment, m->uid.segment, 16) &&
+ (!uid->edition || uid->edition == m->uid.edition)) {
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "Match for source %d: %s\n",
+ i, d->filename);
+
+ for (int j = 0; j < m->num_ordered_chapters; j++) {
+ struct matroska_chapter *c = m->ordered_chapters + j;
+
+ if (!c->has_segment_uid)
+ continue;
+
+ if (has_source_request(*uids, *num_sources, &c->uid))
+ continue;
+
+ /* Set the requested segment. */
+ MP_TARRAY_GROW(NULL, *uids, *num_sources);
+ memcpy((*uids) + *num_sources, &c->uid, sizeof(c->uid));
+
+ /* Add a new source slot. */
+ MP_TARRAY_APPEND(NULL, *sources, *num_sources, NULL);
+ }
+
+ if (enable_cache(mpctx, &s, &d, &params) < 0)
+ continue;
+
+ (*sources)[i] = d;
+ return true;
+ }
+ }
+ }
+ free_demuxer(d);
+ free_stream(s);
+ return was_valid;
+}
+
+static void check_file(struct MPContext *mpctx, struct demuxer ***sources,
+ int *num_sources, struct matroska_segment_uid **uids,
+ char *filename, int first)
+{
+ for (int segment = first; ; segment++) {
+ if (!check_file_seg(mpctx, sources, num_sources,
+ uids, filename, segment))
+ break;
+ }
+}
+
+static bool missing(struct demuxer **sources, int num_sources)
+{
+ for (int i = 0; i < num_sources; i++) {
+ if (!sources[i])
+ return true;
+ }
+ return false;
+}
+
+static int find_ordered_chapter_sources(struct MPContext *mpctx,
+ struct demuxer ***sources,
+ int *num_sources,
+ struct matroska_segment_uid **uids)
+{
+ int num_filenames = 0;
+ char **filenames = NULL;
+ if (*num_sources > 1) {
+ char *main_filename = mpctx->demuxer->filename;
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "This file references data from "
+ "other sources.\n");
+ if (mpctx->demuxer->stream->uncached_type != STREAMTYPE_FILE) {
+ mp_msg(MSGT_CPLAYER, MSGL_WARN, "Playback source is not a "
+ "normal disk file. Will not search for related files.\n");
+ } else {
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "Will scan other files in the "
+ "same directory to find referenced sources.\n");
+ filenames = find_files(main_filename, ".mkv");
+ num_filenames = MP_TALLOC_ELEMS(filenames);
+ }
+ // Possibly get further segments appended to the first segment
+ check_file(mpctx, sources, num_sources, uids, main_filename, 1);
+ }
+
+ int old_source_count;
+ do {
+ old_source_count = *num_sources;
+ for (int i = 0; i < num_filenames; i++) {
+ if (!missing(*sources, *num_sources))
+ break;
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "Checking file %s\n", filenames[i]);
+ check_file(mpctx, sources, num_sources, uids, filenames[i], 0);
+ }
+ /* Loop while we have new sources to look for. */
+ } while (old_source_count != *num_sources);
+
+ talloc_free(filenames);
+ if (missing(*sources, *num_sources)) {
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "Failed to find ordered chapter part!\n"
+ "There will be parts MISSING from the video!\n");
+ int j = 1;
+ for (int i = 1; i < *num_sources; i++)
+ if ((*sources)[i]) {
+ struct matroska_segment_uid *source_uid = *uids + i;
+ struct matroska_segment_uid *target_uid = *uids + j;
+ (*sources)[j] = (*sources)[i];
+ memmove(target_uid, source_uid, sizeof(*source_uid));
+ j++;
+ }
+ *num_sources = j;
+ }
+ return *num_sources;
+}
+
+static int64_t add_timeline_part(struct MPOpts *opts,
+ struct demuxer *source,
+ struct timeline_part **timeline,
+ int *part_count,
+ uint64_t start,
+ uint64_t *last_end_time,
+ uint64_t *starttime)
+{
+ /* Only add a separate part if the time or file actually changes.
+ * Matroska files have chapter divisions that are redundant from
+ * timeline point of view because the same chapter structure is used
+ * both to specify the timeline and for normal chapter information.
+ * Removing a missing inserted external chapter can also cause this.
+ * We allow for a configurable fudge factor because of files which
+ * specify chapter end times that are one frame too early;
+ * we don't want to try seeking over a one frame gap. */
+ int64_t join_diff = start - *last_end_time;
+ if (*part_count == 0
+ || FFABS(join_diff) > opts->chapter_merge_threshold * 1e6
+ || source != (*timeline)[*part_count - 1].source) {
+ struct timeline_part new = {
+ .start = *starttime / 1e9,
+ .source_start = start / 1e9,
+ .source = source,
+ };
+ MP_TARRAY_APPEND(NULL, *timeline, *part_count, new);
+ } else if (*part_count > 0 && join_diff) {
+ /* Chapter was merged at an inexact boundary;
+ * adjust timestamps to match. */
+ mp_msg(MSGT_CPLAYER, MSGL_V, "Merging timeline part %d with "
+ "offset %g ms.\n", *part_count, join_diff / 1e6);
+ *starttime += join_diff;
+ return join_diff;
+ }
+
+ return 0;
+}
+
+static void account_missing_time(uint64_t *total_time,
+ uint64_t new_time,
+ const char *message)
+{
+ if (!new_time)
+ return;
+
+ *total_time += new_time;
+ mp_msg(MSGT_CPLAYER, MSGL_HINT,
+ "missing %"PRIu64" nanoseconds: %s\n",
+ new_time, message);
+}
+
+static void build_timeline_loop(struct MPOpts *opts,
+ struct demuxer **sources,
+ int num_sources,
+ int current_source,
+ uint64_t *starttime,
+ uint64_t *missing_time,
+ uint64_t *last_end_time,
+ struct timeline_part **timeline,
+ struct chapter *chapters,
+ int *part_count,
+ uint64_t skip,
+ uint64_t limit)
+{
+ uint64_t local_starttime = 0;
+ struct demuxer *source = sources[current_source];
+ struct matroska_data *m = &source->matroska_data;
+
+ for (int i = 0; i < m->num_ordered_chapters; i++) {
+ struct matroska_chapter *c = m->ordered_chapters + i;
+ uint64_t chapter_length = c->end - c->start;
+
+ /* Fill in the uid with the current one if one isn't requested. */
+ if (!c->has_segment_uid)
+ memcpy(&c->uid, &m->uid, sizeof(c->uid));
+
+ /* "Seek" to the end of the chapter. */
+ local_starttime += chapter_length;
+
+ /* If we're before the start time for the chapter, skip to the next
+ * one. */
+ if (local_starttime <= skip)
+ continue;
+
+ /* Look for the source for this chapter. */
+ for (int j = 0; j < num_sources; j++) {
+ struct demuxer *linked_source = sources[j];
+ struct matroska_data *linked_m = &linked_source->matroska_data;
+
+ /* Skip if the segment or edition isn't acceptable. */
+ if (!demux_matroska_uid_cmp(&c->uid, &linked_m->uid))
+ continue;
+
+ /* TODO: Add option to support recursive chapters when loading
+ * recursive ordered chapter editions? If so, more code will be
+ * needed to add chapters for external non-ordered segment loading
+ * as well since that part is not recursive. */
+ if (!limit) {
+ chapters[i].start = *starttime / 1e9;
+ chapters[i].name = talloc_strdup(chapters, c->name);
+ }
+
+ /* If we're the source or it's a non-ordered edition reference,
+ * just add a timeline part from the source. */
+ if (current_source == j || !linked_m->num_ordered_chapters) {
+ double source_full_length_seconds = demuxer_get_time_length(linked_source);
+ /* Some accuracy lost, but not enough to care. (Over one
+ * million parts, a nanosecond off here could add up to a
+ * millisecond and trigger a false-positive error message, but
+ * if that's your biggest problem at that point,
+ * congratulations. */
+ uint64_t source_full_length = source_full_length_seconds * 1e9;
+ uint64_t source_length = source_full_length - c->start;
+ int64_t join_diff = 0;
+
+ /* If the chapter starts after the end of a source, there's
+ * nothing we can get from it. Instead, mark the entire chapter
+ * as missing and make the chapter length 0. */
+ if (source_full_length <= c->start) {
+ account_missing_time(missing_time, chapter_length,
+ "referenced segment ends before the requested start time");
+ chapter_length = 0;
+ goto found;
+ }
+
+ /* If the source length starting at the chapter start is
+ * shorter than the chapter it is supposed to fill, add the gap
+ * to missing_time. Also, modify the chapter length to be what
+ * we actually have to avoid playing off the end of the file
+ * and not switching to the next source. */
+ if (source_length < chapter_length) {
+ account_missing_time(missing_time, chapter_length - source_length,
+ "referenced segment ends before the requested end time");
+ chapter_length = source_length;
+ }
+
+ join_diff = add_timeline_part(opts, linked_source, timeline, part_count,
+ c->start, last_end_time, starttime);
+
+ /* If we merged two chapters into a single part due to them
+ * being off by a few frames, we need to change the limit to
+ * avoid chopping the end of the intended chapter (the adding
+ * frames case) or showing extra content (the removing frames
+ * case). Also update chapter_length to incorporate the extra
+ * time. */
+ if (limit) {
+ limit += join_diff;
+ chapter_length += join_diff;
+ }
+ /* Otherwise, we have an ordered edition as the source. Since this
+ * can jump around all over the place, we need to build up the
+ * timeline parts for each of its chapters, but not add them as
+ * chapters. */
+ } else {
+ build_timeline_loop(opts, sources, num_sources, j, starttime,
+ missing_time, last_end_time, timeline,
+ chapters, part_count, c->start, c->end);
+ /* The loop call has added time as needed (we can't add it here
+ * due to 'join_diff' in the add_timeline_part function. Since
+ * the time has already been added as needed, the chapter has
+ * an effective 0 length at this point. */
+ chapter_length = 0;
+ }
+ *last_end_time = c->end;
+ goto found;
+ }
+
+ /* We're missing a part of the chapter, so add it to the accounting. */
+ account_missing_time(missing_time, chapter_length,
+ "the source for a chapter could not be found");
+ /* We don't have the source, but don't leave a gap in the timeline for
+ * the source. */
+ chapter_length = 0;
+ found:;
+ *starttime += chapter_length;
+ /* If we're after the limit on this chapter, stop here. */
+ if (limit && local_starttime >= limit) {
+ /* Back up the global start time by the overflow. */
+ *starttime -= local_starttime - limit;
+ break;
+ }
+ }
+
+ /* If we stopped before the limit, add up the missing time. */
+ if (local_starttime < limit)
+ account_missing_time(missing_time, limit - local_starttime,
+ "nested ordered chapter segment is shorter than the requested end time");
+}
+
+void build_ordered_chapter_timeline(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+
+ if (!opts->ordered_chapters) {
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, but "
+ "you have disabled support for them. Ignoring.\n");
+ return;
+ }
+
+ mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, will build "
+ "edit timeline.\n");
+
+ struct demuxer *demuxer = mpctx->demuxer;
+ struct matroska_data *m = &demuxer->matroska_data;
+
+ // +1 because sources/uid_map[0] is original file even if all chapters
+ // actually use other sources and need separate entries
+ struct demuxer **sources = talloc_array_ptrtype(NULL, sources,
+ m->num_ordered_chapters+1);
+ sources[0] = mpctx->demuxer;
+ struct matroska_segment_uid *uids = talloc_array_ptrtype(NULL, uids,
+ m->num_ordered_chapters + 1);
+ int num_sources = 1;
+ memcpy(uids[0].segment, m->uid.segment, 16);
+ uids[0].edition = 0;
+
+ for (int i = 0; i < m->num_ordered_chapters; i++) {
+ struct matroska_chapter *c = m->ordered_chapters + i;
+ /* If there isn't a segment uid, we are the source. If the segment uid
+ * is our segment uid and the edition matches. We can't accept the
+ * "don't care" edition value of 0 since the user may have requested a
+ * non-default edition. */
+ if (!c->has_segment_uid || demux_matroska_uid_cmp(&c->uid, &m->uid))
+ continue;
+
+ if (has_source_request(uids, num_sources, &c->uid))
+ continue;
+
+ memcpy(uids + num_sources, &c->uid, sizeof(c->uid));
+ sources[num_sources] = NULL;
+ num_sources++;
+ }
+
+ num_sources = find_ordered_chapter_sources(mpctx, &sources, &num_sources,
+ &uids);
+ talloc_free(uids);
+
+ struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, 0);
+ struct chapter *chapters = talloc_array_ptrtype(NULL, chapters, m->num_ordered_chapters);
+ uint64_t starttime = 0;
+ uint64_t missing_time = 0;
+ uint64_t last_end_time = 0;
+ int part_count = 0;
+ build_timeline_loop(opts, sources, num_sources, 0, &starttime,
+ &missing_time, &last_end_time, &timeline,
+ chapters, &part_count, 0, 0);
+
+ if (!part_count) {
+ // None of the parts come from the file itself???
+ talloc_free(sources);
+ talloc_free(timeline);
+ talloc_free(chapters);
+ return;
+ }
+
+ struct timeline_part new = {
+ .start = starttime / 1e9,
+ };
+ MP_TARRAY_APPEND(NULL, timeline, part_count, new);
+
+ /* Ignore anything less than a millisecond when reporting missing time. If
+ * users really notice less than a millisecond missing, maybe this can be
+ * revisited. */
+ if (missing_time >= 1e6)
+ mp_msg(MSGT_CPLAYER, MSGL_ERR, "There are %.3f seconds missing "
+ "from the timeline!\n", missing_time / 1e9);
+ talloc_free(mpctx->sources);
+ mpctx->sources = sources;
+ mpctx->num_sources = num_sources;
+ mpctx->timeline = timeline;
+ mpctx->num_timeline_parts = part_count - 1;
+ mpctx->num_chapters = m->num_ordered_chapters;
+ mpctx->chapters = chapters;
+}