summaryrefslogtreecommitdiffstats
path: root/player
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--player/audio.c7
-rw-r--r--player/client.c2
-rw-r--r--player/command.c680
-rw-r--r--player/command.h1
-rw-r--r--player/configfiles.c7
-rw-r--r--player/core.h9
-rw-r--r--player/external_files.c8
-rw-r--r--player/external_files.h1
-rw-r--r--player/javascript/defaults.js81
-rw-r--r--player/loadfile.c78
-rw-r--r--player/lua.c13
-rw-r--r--player/lua/auto_profiles.lua28
-rw-r--r--player/lua/console.lua561
-rw-r--r--player/lua/defaults.lua24
-rw-r--r--player/lua/input.lua69
-rw-r--r--player/lua/meson.build3
-rw-r--r--player/lua/osc.lua94
-rw-r--r--player/lua/stats.lua316
-rw-r--r--player/main.c16
-rw-r--r--player/meson.build7
-rw-r--r--player/misc.c12
-rw-r--r--player/osd.c5
-rw-r--r--player/playloop.c31
-rw-r--r--player/screenshot.c11
-rw-r--r--player/sub.c83
-rw-r--r--player/video.c34
26 files changed, 1512 insertions, 669 deletions
diff --git a/player/audio.c b/player/audio.c
index ca17d33..da91dd4 100644
--- a/player/audio.c
+++ b/player/audio.c
@@ -175,6 +175,7 @@ void audio_update_volume(struct MPContext *mpctx)
float gain = MPMAX(opts->softvol_volume / 100.0, 0);
gain = pow(gain, 3);
gain *= compute_replaygain(mpctx);
+ gain *= db_gain(opts->softvol_gain);
if (opts->softvol_mute == 1)
gain = 0.0;
@@ -617,7 +618,7 @@ double playing_audio_pts(struct MPContext *mpctx)
double pts = written_audio_pts(mpctx);
if (pts == MP_NOPTS_VALUE || !mpctx->ao)
return pts;
- return pts - mpctx->audio_speed * ao_get_delay(mpctx->ao);
+ return pts - ao_get_delay(mpctx->ao);
}
// This garbage is needed for untimed AOs. These consume audio infinitely fast,
@@ -828,7 +829,8 @@ void audio_start_ao(struct MPContext *mpctx)
double pts = MP_NOPTS_VALUE;
if (!get_sync_pts(mpctx, &pts))
return;
- double apts = playing_audio_pts(mpctx); // (basically including mpctx->delay)
+ double apts = written_audio_pts(mpctx);
+ apts -= apts != MP_NOPTS_VALUE ? mpctx->audio_speed * ao_get_delay(mpctx->ao) : 0;
if (pts != MP_NOPTS_VALUE && apts != MP_NOPTS_VALUE && pts < apts &&
mpctx->video_status != STATUS_EOF)
{
@@ -844,6 +846,7 @@ void audio_start_ao(struct MPContext *mpctx)
}
MP_VERBOSE(mpctx, "starting audio playback\n");
+ ao_c->audio_started = true;
ao_start(ao_c->ao);
mpctx->audio_status = STATUS_PLAYING;
if (ao_c->out_eof) {
diff --git a/player/client.c b/player/client.c
index b35f20a..5087f89 100644
--- a/player/client.c
+++ b/player/client.c
@@ -844,7 +844,7 @@ int mp_client_send_event_dup(struct MPContext *mpctx, const char *client_name,
return mp_client_send_event(mpctx, client_name, 0, event, event_data.data);
}
-const static bool deprecated_events[] = {
+static const bool deprecated_events[] = {
[MPV_EVENT_IDLE] = true,
[MPV_EVENT_TICK] = true,
};
diff --git a/player/command.c b/player/command.c
index 8bff0cd..ff5ca35 100644
--- a/player/command.c
+++ b/player/command.c
@@ -55,7 +55,9 @@
#include "options/m_option.h"
#include "options/m_property.h"
#include "options/m_config_frontend.h"
+#include "options/parse_configfile.h"
#include "osdep/getpid.h"
+#include "video/out/gpu/context.h"
#include "video/out/vo.h"
#include "video/csputils.h"
#include "video/hwdec.h"
@@ -72,6 +74,7 @@
#include "osdep/io.h"
#include "osdep/subprocess.h"
+#include "osdep/terminal.h"
#include "core.h"
@@ -91,6 +94,8 @@ struct command_ctx {
char **warned_deprecated;
int num_warned_deprecated;
+ bool command_opts_processed;
+
struct overlay *overlays;
int num_overlays;
// One of these is in use by the OSD; the other one exists so that the
@@ -109,9 +114,9 @@ struct command_ctx {
char **script_props;
mpv_node udata;
+ mpv_node mdata;
double cached_window_scale;
- bool shared_script_warning;
};
static const struct m_option script_props_type = {
@@ -122,9 +127,14 @@ static const struct m_option udata_type = {
.type = CONF_TYPE_NODE
};
+static const struct m_option mdata_type = {
+ .type = CONF_TYPE_NODE
+};
+
struct overlay {
struct mp_image *source;
int x, y;
+ int dw, dh;
};
struct hook_handler {
@@ -137,12 +147,27 @@ struct hook_handler {
bool active; // hook is currently in progress (only 1 at a time for now)
};
-// U+279C HEAVY ROUND-TIPPED RIGHTWARDS ARROW
+enum load_action_type {
+ LOAD_TYPE_REPLACE,
+ LOAD_TYPE_INSERT_AT,
+ LOAD_TYPE_INSERT_NEXT,
+ LOAD_TYPE_APPEND,
+};
+
+struct load_action {
+ enum load_action_type type;
+ bool play;
+};
+
+// U+25CB WHITE CIRCLE
+// U+25CF BLACK CIRCLE
// U+00A0 NO-BREAK SPACE
-#define ARROW_SP "\342\236\234\302\240"
+#define WHITECIRCLE "\xe2\x97\x8b"
+#define BLACKCIRCLE "\xe2\x97\x8f"
+#define NBSP "\xc2\xa0"
-const char list_current[] = OSD_ASS_0 ARROW_SP OSD_ASS_1;
-const char list_normal[] = OSD_ASS_0 "{\\alpha&HFF}" ARROW_SP "{\\r}" OSD_ASS_1;
+const char list_current[] = BLACKCIRCLE NBSP;
+const char list_normal[] = WHITECIRCLE NBSP;
static int edit_filters(struct MPContext *mpctx, struct mp_log *log,
enum stream_type mediatype,
@@ -399,9 +424,9 @@ static int mp_property_playback_speed(void *ctx, struct m_property *prop,
int action, void *arg)
{
MPContext *mpctx = ctx;
- if (action == M_PROPERTY_PRINT) {
- double speed = mpctx->opts->playback_speed;
- *(char **)arg = talloc_asprintf(NULL, "%.2f", speed);
+ if (action == M_PROPERTY_PRINT || action == M_PROPERTY_FIXED_LEN_PRINT) {
+ *(char **)arg = mp_format_double(NULL, mpctx->opts->playback_speed, 2,
+ false, false, action != M_PROPERTY_FIXED_LEN_PRINT);
return M_PROPERTY_OK;
}
return mp_property_generic_option(mpctx, prop, action, arg);
@@ -419,8 +444,9 @@ static int mp_property_av_speed_correction(void *ctx, struct m_property *prop,
default: MP_ASSERT_UNREACHABLE();
}
- if (action == M_PROPERTY_PRINT) {
- *(char **)arg = talloc_asprintf(NULL, "%+.3g%%", (val - 1) * 100);
+ if (action == M_PROPERTY_PRINT || action == M_PROPERTY_FIXED_LEN_PRINT) {
+ *(char **)arg = mp_format_double(NULL, (val - 1) * 100, 2, true,
+ true, action != M_PROPERTY_FIXED_LEN_PRINT);
return M_PROPERTY_OK;
}
@@ -652,13 +678,9 @@ static int mp_property_avsync(void *ctx, struct m_property *prop,
MPContext *mpctx = ctx;
if (!mpctx->ao_chain || !mpctx->vo_chain)
return M_PROPERTY_UNAVAILABLE;
- if (action == M_PROPERTY_PRINT) {
- // Truncate anything < 1e-4 to avoid switching to scientific notation
- if (fabs(mpctx->last_av_difference) < 1e-4) {
- *(char **)arg = talloc_strdup(NULL, "0");
- } else {
- *(char **)arg = talloc_asprintf(NULL, "%+.2g", mpctx->last_av_difference);
- }
+ if (action == M_PROPERTY_PRINT || action == M_PROPERTY_FIXED_LEN_PRINT) {
+ *(char **)arg = mp_format_double(NULL, mpctx->last_av_difference, 4,
+ true, false, action != M_PROPERTY_FIXED_LEN_PRINT);
return M_PROPERTY_OK;
}
return m_property_double_ro(action, arg, mpctx->last_av_difference);
@@ -1355,6 +1377,18 @@ static int mp_property_core_idle(void *ctx, struct m_property *prop,
return m_property_bool_ro(action, arg, !mpctx->playback_active);
}
+static int mp_property_deinterlace(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo_chain *vo_c = mpctx->vo_chain;
+ if (!vo_c)
+ return M_PROPERTY_UNAVAILABLE;
+
+ bool deinterlace_active = mp_output_chain_deinterlace_active(vo_c->filter);
+ return m_property_bool_ro(action, arg, deinterlace_active);
+}
+
static int mp_property_idle(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -1624,6 +1658,28 @@ static int mp_property_volume(void *ctx, struct m_property *prop,
return mp_property_generic_option(mpctx, prop, action, arg);
}
+static int mp_property_volume_gain(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct MPOpts *opts = mpctx->opts;
+
+ switch (action) {
+ case M_PROPERTY_GET_CONSTRICTED_TYPE:
+ *(struct m_option *)arg = (struct m_option){
+ .type = CONF_TYPE_FLOAT,
+ .min = opts->softvol_gain_min,
+ .max = opts->softvol_gain_max,
+ };
+ return M_PROPERTY_OK;
+ case M_PROPERTY_PRINT:
+ *(char **)arg = talloc_asprintf(NULL, "%.1f", opts->softvol_gain);
+ return M_PROPERTY_OK;
+ }
+
+ return mp_property_generic_option(mpctx, prop, action, arg);
+}
+
static int mp_property_ao_volume(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -1758,8 +1814,7 @@ static int mp_property_audio_devices(void *ctx, struct m_property *prop,
static int mp_property_ao(void *ctx, struct m_property *p, int action, void *arg)
{
MPContext *mpctx = ctx;
- return m_property_strdup_ro(action, arg,
- mpctx->ao ? ao_get_name(mpctx->ao) : NULL);
+ return m_property_strdup_ro(action, arg, mpctx->ao ? ao_get_name(mpctx->ao) : NULL);
}
/// Audio delay (RW)
@@ -1774,28 +1829,6 @@ static int mp_property_audio_delay(void *ctx, struct m_property *prop,
return mp_property_generic_option(mpctx, prop, action, arg);
}
-/// Audio codec tag (RO)
-static int mp_property_audio_codec_name(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- MPContext *mpctx = ctx;
- struct track *track = mpctx->current_track[0][STREAM_AUDIO];
- const char *c = track && track->stream ? track->stream->codec->codec : NULL;
- return m_property_strdup_ro(action, arg, c);
-}
-
-/// Audio codec name (RO)
-static int mp_property_audio_codec(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- MPContext *mpctx = ctx;
- struct track *track = mpctx->current_track[0][STREAM_AUDIO];
- char desc[256] = "";
- if (track && track->dec)
- mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
- return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
-}
-
static int property_audiofmt(struct mp_aframe *fmt, int action, void *arg)
{
if (!fmt || !mp_aframe_config_is_valid(fmt))
@@ -2000,6 +2033,10 @@ static int get_track_entry(int item, int action, void *arg, void *ctx)
.unavailable = !decoder_desc[0]},
{"codec", SUB_PROP_STR(p.codec),
.unavailable = !p.codec},
+ {"codec-desc", SUB_PROP_STR(p.codec_desc),
+ .unavailable = !p.codec_desc},
+ {"codec-profile", SUB_PROP_STR(p.codec_profile),
+ .unavailable = !p.codec_profile},
{"demux-w", SUB_PROP_INT(p.disp_w), .unavailable = !p.disp_w},
{"demux-h", SUB_PROP_INT(p.disp_h), .unavailable = !p.disp_h},
{"demux-crop-x",SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
@@ -2016,6 +2053,7 @@ static int get_track_entry(int item, int action, void *arg, void *ctx)
{"demux-bitrate", SUB_PROP_INT(p.bitrate), .unavailable = p.bitrate <= 0},
{"demux-rotation", SUB_PROP_INT(p.rotate), .unavailable = p.rotate <= 0},
{"demux-par", SUB_PROP_DOUBLE(par), .unavailable = par <= 0},
+ {"format-name", SUB_PROP_STR(p.format_name), .unavailable = !p.format_name},
{"replaygain-track-peak", SUB_PROP_FLOAT(rg.track_peak),
.unavailable = !has_rg},
{"replaygain-track-gain", SUB_PROP_FLOAT(rg.track_gain),
@@ -2208,28 +2246,6 @@ static int mp_property_frame_count(void *ctx, struct m_property *prop,
return m_property_int_ro(action, arg, frames);
}
-/// Video codec tag (RO)
-static int mp_property_video_format(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- MPContext *mpctx = ctx;
- struct track *track = mpctx->current_track[0][STREAM_VIDEO];
- const char *c = track && track->stream ? track->stream->codec->codec : NULL;
- return m_property_strdup_ro(action, arg, c);
-}
-
-/// Video codec name (RO)
-static int mp_property_video_codec(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- MPContext *mpctx = ctx;
- struct track *track = mpctx->current_track[0][STREAM_VIDEO];
- char desc[256] = "";
- if (track && track->dec)
- mp_decoder_wrapper_get_desc(track->dec, desc, sizeof(desc));
- return m_property_strdup_ro(action, arg, desc[0] ? desc : NULL);
-}
-
static const char *get_aspect_ratio_name(double ratio)
{
// Depending on cropping/mastering exact ratio may differ.
@@ -2272,73 +2288,77 @@ static const char *get_aspect_ratio_name(double ratio)
#undef RATIO_CASE
}
-static int property_imgparams(struct mp_image_params p, int action, void *arg)
+static int property_imgparams(const struct mp_image_params *p, int action, void *arg)
{
- if (!p.imgfmt)
+ if (!p->imgfmt && !p->imgfmt_name)
return M_PROPERTY_UNAVAILABLE;
int d_w, d_h;
- mp_image_params_get_dsize(&p, &d_w, &d_h);
+ mp_image_params_get_dsize(p, &d_w, &d_h);
- struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(p.imgfmt);
int bpp = 0;
- for (int i = 0; i < desc.num_planes; i++)
- bpp += desc.bpp[i] >> (desc.xs[i] + desc.ys[i]);
+ enum pl_alpha_mode alpha = p->repr.alpha;
+ int fmt = p->hw_subfmt ? p->hw_subfmt : p->imgfmt;
+ if (fmt) {
+ struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(fmt);
+ for (int i = 0; i < desc.num_planes; i++)
+ bpp += desc.bpp[i] >> (desc.xs[i] + desc.ys[i]);
- // Alpha type is not supported by FFmpeg, so MP_ALPHA_AUTO may mean alpha
- // is of an unknown type, or simply not present. Normalize to AUTO=no alpha.
- if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (p.alpha != MP_ALPHA_AUTO)) {
- p.alpha =
- (desc.flags & MP_IMGFLAG_ALPHA) ? MP_ALPHA_STRAIGHT : MP_ALPHA_AUTO;
+ // Alpha type is not supported by FFmpeg, so PL_ALPHA_UNKNOWN may mean alpha
+ // is of an unknown type, or simply not present. Normalize to AUTO=no alpha.
+ if (!!(desc.flags & MP_IMGFLAG_ALPHA) != (alpha != PL_ALPHA_UNKNOWN))
+ alpha = (desc.flags & MP_IMGFLAG_ALPHA) ? PL_ALPHA_INDEPENDENT : PL_ALPHA_UNKNOWN;
}
- const struct pl_hdr_metadata *hdr = &p.color.hdr;
+ const struct pl_hdr_metadata *hdr = &p->color.hdr;
bool has_cie_y = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_CIE_Y);
bool has_hdr10 = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10);
bool has_hdr10plus = pl_hdr_metadata_contains(hdr, PL_HDR_METADATA_HDR10PLUS);
- bool has_crop = mp_rect_w(p.crop) > 0 && mp_rect_h(p.crop) > 0;
+ bool has_crop = mp_rect_w(p->crop) > 0 && mp_rect_h(p->crop) > 0;
const char *aspect_name = get_aspect_ratio_name(d_w / (double)d_h);
- const char *sar_name = get_aspect_ratio_name(p.w / (double)p.h);
+ const char *sar_name = get_aspect_ratio_name(p->w / (double)p->h);
+ const char *pixelformat_name = p->imgfmt_name ? p->imgfmt_name :
+ mp_imgfmt_to_name(p->imgfmt);
struct m_sub_property props[] = {
- {"pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.imgfmt))},
- {"hw-pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p.hw_subfmt)),
- .unavailable = !p.hw_subfmt},
+ {"pixelformat", SUB_PROP_STR(pixelformat_name)},
+ {"hw-pixelformat", SUB_PROP_STR(mp_imgfmt_to_name(p->hw_subfmt)),
+ .unavailable = !p->hw_subfmt},
{"average-bpp", SUB_PROP_INT(bpp),
.unavailable = !bpp},
- {"w", SUB_PROP_INT(p.w)},
- {"h", SUB_PROP_INT(p.h)},
+ {"w", SUB_PROP_INT(p->w)},
+ {"h", SUB_PROP_INT(p->h)},
{"dw", SUB_PROP_INT(d_w)},
{"dh", SUB_PROP_INT(d_h)},
- {"crop-x", SUB_PROP_INT(p.crop.x0), .unavailable = !has_crop},
- {"crop-y", SUB_PROP_INT(p.crop.y0), .unavailable = !has_crop},
- {"crop-w", SUB_PROP_INT(mp_rect_w(p.crop)), .unavailable = !has_crop},
- {"crop-h", SUB_PROP_INT(mp_rect_h(p.crop)), .unavailable = !has_crop},
+ {"crop-x", SUB_PROP_INT(p->crop.x0), .unavailable = !has_crop},
+ {"crop-y", SUB_PROP_INT(p->crop.y0), .unavailable = !has_crop},
+ {"crop-w", SUB_PROP_INT(mp_rect_w(p->crop)), .unavailable = !has_crop},
+ {"crop-h", SUB_PROP_INT(mp_rect_h(p->crop)), .unavailable = !has_crop},
{"aspect", SUB_PROP_FLOAT(d_w / (double)d_h)},
{"aspect-name", SUB_PROP_STR(aspect_name), .unavailable = !aspect_name},
- {"par", SUB_PROP_FLOAT(p.p_w / (double)p.p_h)},
- {"sar", SUB_PROP_FLOAT(p.w / (double)p.h)},
+ {"par", SUB_PROP_FLOAT(p->p_w / (double)p->p_h)},
+ {"sar", SUB_PROP_FLOAT(p->w / (double)p->h)},
{"sar-name", SUB_PROP_STR(sar_name), .unavailable = !sar_name},
{"colormatrix",
- SUB_PROP_STR(m_opt_choice_str(mp_csp_names, p.color.space))},
+ SUB_PROP_STR(m_opt_choice_str(pl_csp_names, p->repr.sys))},
{"colorlevels",
- SUB_PROP_STR(m_opt_choice_str(mp_csp_levels_names, p.color.levels))},
+ SUB_PROP_STR(m_opt_choice_str(pl_csp_levels_names, p->repr.levels))},
{"primaries",
- SUB_PROP_STR(m_opt_choice_str(mp_csp_prim_names, p.color.primaries))},
+ SUB_PROP_STR(m_opt_choice_str(pl_csp_prim_names, p->color.primaries))},
{"gamma",
- SUB_PROP_STR(m_opt_choice_str(mp_csp_trc_names, p.color.gamma))},
- {"sig-peak", SUB_PROP_FLOAT(p.color.hdr.max_luma / MP_REF_WHITE)},
+ SUB_PROP_STR(m_opt_choice_str(pl_csp_trc_names, p->color.transfer))},
+ {"sig-peak", SUB_PROP_FLOAT(p->color.hdr.max_luma / MP_REF_WHITE)},
{"light",
- SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p.color.light))},
+ SUB_PROP_STR(m_opt_choice_str(mp_csp_light_names, p->light))},
{"chroma-location",
- SUB_PROP_STR(m_opt_choice_str(mp_chroma_names, p.chroma_location))},
+ SUB_PROP_STR(m_opt_choice_str(pl_chroma_names, p->chroma_location))},
{"stereo-in",
- SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p.stereo3d))},
- {"rotate", SUB_PROP_INT(p.rotate)},
+ SUB_PROP_STR(m_opt_choice_str(mp_stereo3d_names, p->stereo3d))},
+ {"rotate", SUB_PROP_INT(p->rotate)},
{"alpha",
- SUB_PROP_STR(m_opt_choice_str(mp_alpha_names, p.alpha)),
+ SUB_PROP_STR(m_opt_choice_str(pl_alpha_names, alpha)),
// avoid using "auto" for "no", so just make it unavailable
- .unavailable = p.alpha == MP_ALPHA_AUTO},
+ .unavailable = alpha == PL_ALPHA_UNKNOWN},
{"min-luma", SUB_PROP_FLOAT(hdr->min_luma), .unavailable = !has_hdr10},
{"max-luma", SUB_PROP_FLOAT(hdr->max_luma), .unavailable = !has_hdr10},
{"max-cll", SUB_PROP_FLOAT(hdr->max_cll), .unavailable = !has_hdr10},
@@ -2384,7 +2404,24 @@ static int mp_property_vo_imgparams(void *ctx, struct m_property *prop,
if (valid != M_PROPERTY_VALID)
return valid;
- return property_imgparams(vo_get_current_params(vo), action, arg);
+ struct mp_image_params p = vo_get_current_params(vo);
+ return property_imgparams(&p, action, arg);
+}
+
+static int mp_property_tgt_imgparams(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!mpctx->video_out)
+ return M_PROPERTY_UNAVAILABLE;
+
+ int valid = m_property_read_sub_validate(ctx, prop, action, arg);
+ if (valid != M_PROPERTY_VALID)
+ return valid;
+
+ struct mp_image_params p = vo_get_target_params(vo);
+ return property_imgparams(&p, action, arg);
}
static int mp_property_dec_imgparams(void *ctx, struct m_property *prop,
@@ -2403,7 +2440,7 @@ static int mp_property_dec_imgparams(void *ctx, struct m_property *prop,
mp_decoder_wrapper_get_video_dec_params(vo_c->track->dec, &p);
if (!p.imgfmt)
return M_PROPERTY_UNAVAILABLE;
- return property_imgparams(p, action, arg);
+ return property_imgparams(&p, action, arg);
}
static int mp_property_vd_imgparams(void *ctx, struct m_property *prop,
@@ -2417,7 +2454,7 @@ static int mp_property_vd_imgparams(void *ctx, struct m_property *prop,
struct mp_codec_params *c =
track && track->stream ? track->stream->codec : NULL;
if (vo_c->filter->input_params.imgfmt) {
- return property_imgparams(vo_c->filter->input_params, action, arg);
+ return property_imgparams(&vo_c->filter->input_params, action, arg);
} else if (c && c->disp_w && c->disp_h) {
// Simplistic fallback for stupid scripts querying "width"/"height"
// before the first frame is decoded.
@@ -2591,6 +2628,26 @@ static int mp_property_hidpi_scale(void *ctx, struct m_property *prop,
return m_property_double_ro(action, arg, cmd->cached_window_scale);
}
+static void update_hidpi_window_scale(struct MPContext *mpctx, bool hidpi_scale)
+{
+ struct command_ctx *cmd = mpctx->command_ctx;
+ struct vo *vo = mpctx->video_out;
+ if (!vo || cmd->cached_window_scale <= 0)
+ return;
+
+ double scale = hidpi_scale ? cmd->cached_window_scale : 1 / cmd->cached_window_scale;
+
+ int s[2];
+ if (vo_control(vo, VOCTRL_GET_UNFS_WINDOW_SIZE, s) <= 0 || s[0] < 1 || s[1] < 1)
+ return;
+
+ s[0] *= scale;
+ s[1] *= scale;
+ if (s[0] <= 0 || s[1] <= 0)
+ return;
+ vo_control(vo, VOCTRL_SET_UNFS_WINDOW_SIZE, s);
+}
+
static int mp_property_focused(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -2739,8 +2796,15 @@ static int mp_property_perf_info(void *ctx, struct m_property *p, int action,
static int mp_property_vo(void *ctx, struct m_property *p, int action, void *arg)
{
MPContext *mpctx = ctx;
- return m_property_strdup_ro(action, arg,
- mpctx->video_out ? mpctx->video_out->driver->name : NULL);
+ return m_property_strdup_ro(action, arg, mpctx->video_out ?
+ mpctx->video_out->driver->name : NULL);
+}
+
+static int mp_property_gpu_context(void *ctx, struct m_property *p, int action, void *arg)
+{
+ MPContext *mpctx = ctx;
+ return m_property_strdup_ro(action, arg, mpctx->video_out ?
+ mpctx->video_out->context_name : NULL);
}
static int mp_property_osd_dim(void *ctx, struct m_property *prop,
@@ -2790,6 +2854,23 @@ static int mp_property_osd_ass(void *ctx, struct m_property *prop,
return m_property_read_sub(props, action, arg);
}
+static int mp_property_term_size(void *ctx, struct m_property *prop,
+ int action, void *arg)
+{
+ int w = -1, h = -1;
+ terminal_get_size(&w, &h);
+ if (w == -1 || h == -1)
+ return M_PROPERTY_UNAVAILABLE;
+
+ struct m_sub_property props[] = {
+ {"w", SUB_PROP_INT(w)},
+ {"h", SUB_PROP_INT(h)},
+ {0}
+ };
+
+ return m_property_read_sub(props, action, arg);
+}
+
static int mp_property_mouse_pos(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -2875,9 +2956,10 @@ static int mp_property_sub_delay(void *ctx, struct m_property *prop,
{
MPContext *mpctx = ctx;
struct MPOpts *opts = mpctx->opts;
+ int track_ind = *(int *)prop->priv;
switch (action) {
case M_PROPERTY_PRINT:
- *(char **)arg = format_delay(opts->subs_rend->sub_delay);
+ *(char **)arg = format_delay(opts->subs_shared->sub_delay[track_ind]);
return M_PROPERTY_OK;
}
return mp_property_generic_option(mpctx, prop, action, arg);
@@ -2902,8 +2984,9 @@ static int mp_property_sub_pos(void *ctx, struct m_property *prop,
{
MPContext *mpctx = ctx;
struct MPOpts *opts = mpctx->opts;
+ int track_ind = *(int *)prop->priv;
if (action == M_PROPERTY_PRINT) {
- *(char **)arg = talloc_asprintf(NULL, "%4.2f%%/100", opts->subs_rend->sub_pos);
+ *(char **)arg = talloc_asprintf(NULL, "%4.2f%%/100", opts->subs_shared->sub_pos[track_ind]);
return M_PROPERTY_OK;
}
return mp_property_generic_option(mpctx, prop, action, arg);
@@ -2932,11 +3015,14 @@ static int mp_property_sub_ass_extradata(void *ctx, struct m_property *prop,
return M_PROPERTY_NOT_IMPLEMENTED;
}
-static int get_sub_text(void *ctx, struct m_property *prop,
- int action, void *arg, int sub_index)
+static int mp_property_sub_text(void *ctx, struct m_property *prop,
+ int action, void *arg)
{
- int type = *(int *)prop->priv;
MPContext *mpctx = ctx;
+ const int *def = prop->priv;
+ int sub_index = def[0];
+ int type = def[1];
+
struct track *track = mpctx->current_track[sub_index][STREAM_SUB];
struct dec_sub *sub = track ? track->d_sub : NULL;
double pts = mpctx->playback_pts;
@@ -2958,18 +3044,6 @@ static int get_sub_text(void *ctx, struct m_property *prop,
return M_PROPERTY_NOT_IMPLEMENTED;
}
-static int mp_property_sub_text(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- return get_sub_text(ctx, prop, action, arg, 0);
-}
-
-static int mp_property_secondary_sub_text(void *ctx, struct m_property *prop,
- int action, void *arg)
-{
- return get_sub_text(ctx, prop, action, arg, 1);
-}
-
static struct sd_times get_times(void *ctx, struct m_property *prop,
int action, void *arg)
{
@@ -3239,7 +3313,7 @@ static int mp_property_packet_bitrate(void *ctx, struct m_property *prop,
if (rate < 1000) {
*(char **)arg = talloc_asprintf(NULL, "%d kbps", (int)rate);
} else {
- *(char **)arg = talloc_asprintf(NULL, "%.3f mbps", rate / 1000.0);
+ *(char **)arg = talloc_asprintf(NULL, "%.3f Mbps", rate / 1000.0);
}
return M_PROPERTY_OK;
}
@@ -3624,29 +3698,32 @@ static int mp_property_bindings(void *ctx, struct m_property *prop,
return M_PROPERTY_NOT_IMPLEMENTED;
}
-
-static int mp_property_script_props(void *ctx, struct m_property *prop,
- int action, void *arg)
+static int mp_property_mdata(void *ctx, struct m_property *prop,
+ int action, void *arg)
{
MPContext *mpctx = ctx;
- struct command_ctx *cmd = mpctx->command_ctx;
- if (!cmd->shared_script_warning) {
- MP_WARN(mpctx, "The shared-script-properties property is deprecated and will "
- "be removed in the future. Use the user-data property instead.\n");
- cmd->shared_script_warning = true;
- }
+ mpv_node *node = &mpctx->command_ctx->mdata;
+
switch (action) {
case M_PROPERTY_GET_TYPE:
- *(struct m_option *)arg = script_props_type;
+ *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE};
return M_PROPERTY_OK;
case M_PROPERTY_GET:
- m_option_copy(&script_props_type, arg, &cmd->script_props);
+ case M_PROPERTY_GET_NODE:
+ m_option_copy(&mdata_type, arg, node);
return M_PROPERTY_OK;
case M_PROPERTY_SET:
- m_option_copy(&script_props_type, &cmd->script_props, arg);
+ case M_PROPERTY_SET_NODE: {
+ m_option_copy(&mdata_type, node, arg);
+ talloc_steal(mpctx->command_ctx, node_get_alloc(node));
mp_notify_property(mpctx, prop->name);
+
+ struct vo *vo = mpctx->video_out;
+ if (vo)
+ vo_control(vo, VOCTRL_UPDATE_MENU, arg);
return M_PROPERTY_OK;
}
+ }
return M_PROPERTY_NOT_IMPLEMENTED;
}
@@ -3673,8 +3750,9 @@ static int do_op_udata(struct udata_ctx* ctx, int action, void *arg)
assert(node);
m_option_copy(&udata_type, arg, node);
return M_PROPERTY_OK;
+ case M_PROPERTY_FIXED_LEN_PRINT:
case M_PROPERTY_PRINT: {
- char *str = m_option_pretty_print(&udata_type, node);
+ char *str = m_option_pretty_print(&udata_type, node, action == M_PROPERTY_FIXED_LEN_PRINT);
*(char **)arg = str;
return str != NULL;
}
@@ -3781,7 +3859,7 @@ static int do_list_udata(int item, int action, void *arg, void *ctx)
{
struct udata_ctx nctx = *(struct udata_ctx*)ctx;
nctx.node = &nctx.node->u.list->values[item];
- nctx.ta_parent = &nctx.node->u.list;
+ nctx.ta_parent = nctx.node->u.list;
return do_op_udata(&nctx, action, arg);
}
@@ -3807,7 +3885,7 @@ static int mp_property_udata(void *ctx, struct m_property *prop,
.mpctx = mpctx,
.path = path,
.node = &mpctx->command_ctx->udata,
- .ta_parent = &mpctx->command_ctx,
+ .ta_parent = mpctx->command_ctx,
};
int ret = do_op_udata(&nctx, action, arg);
@@ -3885,6 +3963,7 @@ static const struct m_property mp_properties_base[] = {
{"clock", mp_property_clock},
{"seekable", mp_property_seekable},
{"partially-seekable", mp_property_partially_seekable},
+ {"deinterlace-active", mp_property_deinterlace},
{"idle-active", mp_property_idle},
{"window-id", mp_property_window_id},
@@ -3904,11 +3983,12 @@ static const struct m_property mp_properties_base[] = {
// Audio
{"mixer-active", mp_property_mixer_active},
{"volume", mp_property_volume},
+ {"volume-gain", mp_property_volume_gain},
{"ao-volume", mp_property_ao_volume},
{"ao-mute", mp_property_ao_mute},
{"audio-delay", mp_property_audio_delay},
- {"audio-codec-name", mp_property_audio_codec_name},
- {"audio-codec", mp_property_audio_codec},
+ M_PROPERTY_ALIAS("audio-codec-name", "current-tracks/audio/codec"),
+ M_PROPERTY_ALIAS("audio-codec", "current-tracks/audio/codec-desc"),
{"audio-params", mp_property_audio_params},
{"audio-out-params", mp_property_audio_out_params},
{"aid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_AUDIO}},
@@ -3917,12 +3997,13 @@ static const struct m_property mp_properties_base[] = {
{"current-ao", mp_property_ao},
// Video
+ {"video-target-params", mp_property_tgt_imgparams},
{"video-out-params", mp_property_vo_imgparams},
{"video-dec-params", mp_property_dec_imgparams},
{"video-params", mp_property_vd_imgparams},
- {"video-format", mp_property_video_format},
{"video-frame-info", mp_property_video_frame_info},
- {"video-codec", mp_property_video_codec},
+ M_PROPERTY_ALIAS("video-format", "current-tracks/video/codec"),
+ M_PROPERTY_ALIAS("video-codec", "current-tracks/video/codec-desc"),
M_PROPERTY_ALIAS("dwidth", "video-out-params/dw"),
M_PROPERTY_ALIAS("dheight", "video-out-params/dh"),
M_PROPERTY_ALIAS("width", "video-params/w"),
@@ -3932,6 +4013,7 @@ static const struct m_property mp_properties_base[] = {
{"vo-passes", mp_property_vo_passes},
{"perf-info", mp_property_perf_info},
{"current-vo", mp_property_vo},
+ {"current-gpu-context", mp_property_gpu_context},
{"container-fps", mp_property_fps},
{"estimated-vf-fps", mp_property_vf_fps},
{"video-aspect-override", mp_property_video_aspect_override},
@@ -3956,16 +4038,20 @@ static const struct m_property mp_properties_base[] = {
{"sid", property_switch_track, .priv = (void *)(const int[]){0, STREAM_SUB}},
{"secondary-sid", property_switch_track,
.priv = (void *)(const int[]){1, STREAM_SUB}},
- {"sub-delay", mp_property_sub_delay},
+ {"sub-delay", mp_property_sub_delay, .priv = (void *)&(const int){0}},
+ {"secondary-sub-delay", mp_property_sub_delay,
+ .priv = (void *)&(const int){1}},
{"sub-speed", mp_property_sub_speed},
- {"sub-pos", mp_property_sub_pos},
+ {"sub-pos", mp_property_sub_pos, .priv = (void *)&(const int){0}},
+ {"secondary-sub-pos", mp_property_sub_pos,
+ .priv = (void *)&(const int){1}},
{"sub-ass-extradata", mp_property_sub_ass_extradata},
{"sub-text", mp_property_sub_text,
- .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
- {"secondary-sub-text", mp_property_secondary_sub_text,
- .priv = (void *)&(const int){SD_TEXT_TYPE_PLAIN}},
+ .priv = (void *)&(const int[]){0, SD_TEXT_TYPE_PLAIN}},
+ {"secondary-sub-text", mp_property_sub_text,
+ .priv = (void *)&(const int[]){1, SD_TEXT_TYPE_PLAIN}},
{"sub-text-ass", mp_property_sub_text,
- .priv = (void *)&(const int){SD_TEXT_TYPE_ASS}},
+ .priv = (void *)&(const int[]){0, SD_TEXT_TYPE_ASS}},
{"sub-start", mp_property_sub_start,
.priv = (void *)&(const int){0}},
{"secondary-sub-start", mp_property_sub_start,
@@ -4020,8 +4106,10 @@ static const struct m_property mp_properties_base[] = {
{"command-list", mp_property_commands},
{"input-bindings", mp_property_bindings},
- {"shared-script-properties", mp_property_script_props},
+ {"menu-data", mp_property_mdata},
+
{"user-data", mp_property_udata},
+ {"term-size", mp_property_term_size},
M_PROPERTY_ALIAS("video", "vid"),
M_PROPERTY_ALIAS("audio", "aid"),
@@ -4054,17 +4142,18 @@ static const char *const *const mp_event_property_change[] = {
"secondary-sub-text", "audio-bitrate", "video-bitrate", "sub-bitrate",
"decoder-frame-drop-count", "frame-drop-count", "video-frame-info",
"vf-metadata", "af-metadata", "sub-start", "sub-end", "secondary-sub-start",
- "secondary-sub-end", "video-out-params", "video-dec-params", "video-params"),
+ "secondary-sub-end", "video-out-params", "video-dec-params", "video-params",
+ "deinterlace-active", "video-target-params"),
E(MP_EVENT_DURATION_UPDATE, "duration"),
E(MPV_EVENT_VIDEO_RECONFIG, "video-out-params", "video-params",
"video-format", "video-codec", "video-bitrate", "dwidth", "dheight",
"width", "height", "container-fps", "aspect", "aspect-name", "vo-configured", "current-vo",
- "video-dec-params", "osd-dimensions",
- "hwdec", "hwdec-current", "hwdec-interop"),
+ "video-dec-params", "osd-dimensions", "hwdec", "hwdec-current", "hwdec-interop",
+ "window-id", "track-list", "current-tracks"),
E(MPV_EVENT_AUDIO_RECONFIG, "audio-format", "audio-codec", "audio-bitrate",
- "samplerate", "channels", "audio", "volume", "mute",
- "current-ao", "audio-codec-name", "audio-params",
- "audio-out-params", "volume-max", "mixer-active"),
+ "samplerate", "channels", "audio", "volume", "volume-gain", "mute",
+ "current-ao", "audio-codec-name", "audio-params", "track-list", "current-tracks",
+ "audio-out-params", "volume-max", "volume-gain-min", "volume-gain-max", "mixer-active"),
E(MPV_EVENT_SEEK, "seeking", "core-idle", "eof-reached"),
E(MPV_EVENT_PLAYBACK_RESTART, "seeking", "core-idle", "eof-reached"),
E(MP_EVENT_METADATA_UPDATE, "metadata", "filtered-metadata", "media-title"),
@@ -4247,6 +4336,9 @@ static const struct property_osd_display {
{"volume", "Volume",
.msg = "Volume: ${?volume:${volume}% ${?mute==yes:(Muted)}}${!volume:${volume}}",
.osd_progbar = OSD_VOLUME, .marker = 100},
+ {"volume-gain", "Volume gain",
+ .msg = "Volume gain: ${?volume-gain:${volume-gain} dB ${?mute==yes:(Muted)}}${!volume-gain:${volume-gain}}",
+ .osd_progbar = OSD_VOLUME, .marker = 0},
{"ao-volume", "AO Volume",
.msg = "AO Volume: ${?ao-volume:${ao-volume}% ${?ao-mute==yes:(Muted)}}${!ao-volume:${ao-volume}}",
.osd_progbar = OSD_VOLUME, .marker = 100},
@@ -4273,7 +4365,9 @@ static const struct property_osd_display {
{"sub", "Subtitles"},
{"secondary-sid", "Secondary subtitles"},
{"sub-pos", "Sub position"},
+ {"secondary-sub-pos", "Secondary sub position"},
{"sub-delay", "Sub delay"},
+ {"secondary-sub-delay", "Secondary sub delay"},
{"sub-speed", "Sub speed"},
{"sub-visibility",
.msg = "Subtitles ${!sub-visibility==yes:hidden}"
@@ -4285,6 +4379,7 @@ static const struct property_osd_display {
{"sub-scale", "Sub Scale"},
{"sub-ass-vsfilter-aspect-compat", "Subtitle VSFilter aspect compat"},
{"sub-ass-override", "ASS subtitle style override"},
+ {"secondary-sub-ass-override", "Secondary sub ASS subtitle style override"},
{"vf", "Video filters", .msg = "Video filters:\n${vf}"},
{"af", "Audio filters", .msg = "Audio filters:\n${af}"},
{"ab-loop-a", "A-B loop start"},
@@ -4474,8 +4569,8 @@ static void recreate_overlays(struct MPContext *mpctx)
struct sub_bitmap b = {
.bitmap = s->planes[0],
.stride = s->stride[0],
- .w = s->w, .dw = s->w,
- .h = s->h, .dh = s->h,
+ .w = s->w, .dw = o->dw,
+ .h = s->h, .dh = o->dh,
.x = o->x,
.y = o->y,
};
@@ -4572,7 +4667,12 @@ static void cmd_overlay_add(void *pcmd)
int offset = cmd->args[4].v.i;
char *fmt = cmd->args[5].v.s;
int w = cmd->args[6].v.i, h = cmd->args[7].v.i, stride = cmd->args[8].v.i;
+ int dw = cmd->args[9].v.i, dh = cmd->args[10].v.i;
+ if (dw <= 0)
+ dw = w;
+ if (dh <= 0)
+ dh = h;
if (strcmp(fmt, "bgra") != 0) {
MP_ERR(mpctx, "overlay-add: unsupported OSD format '%s'\n", fmt);
goto error;
@@ -4589,6 +4689,8 @@ static void cmd_overlay_add(void *pcmd)
.source = mp_image_alloc(IMGFMT_BGRA, w, h),
.x = x,
.y = y,
+ .dw = dw,
+ .dh = dh,
};
if (!overlay.source)
goto error;
@@ -5408,15 +5510,22 @@ static void cmd_sub_step_seek(void *p)
a[1] = cmd->args[0].v.i;
if (sub_control(sub, SD_CTRL_SUB_STEP, a) > 0) {
if (step) {
- mpctx->opts->subs_rend->sub_delay -= a[0] - refpts;
+ mpctx->opts->subs_shared->sub_delay[track_ind] -= a[0] - refpts;
m_config_notify_change_opt_ptr_notify(mpctx->mconfig,
- &mpctx->opts->subs_rend->sub_delay);
- show_property_osd(mpctx, "sub-delay", cmd->on_osd);
+ &mpctx->opts->subs_shared->sub_delay[track_ind]);
+ show_property_osd(
+ mpctx,
+ track_ind == 0 ? "sub-delay" : "secondary-sub-delay",
+ cmd->on_osd);
} else {
// We can easily seek/step to the wrong subtitle line (because
- // video frame PTS and sub PTS rarely match exactly). Add an
- // arbitrary forward offset as a workaround.
- a[0] += SUB_SEEK_OFFSET;
+ // video frame PTS and sub PTS rarely match exactly).
+ // sub/sd_ass.c adds SUB_SEEK_OFFSET as a workaround, and we
+ // need an even bigger offset without a video.
+ if (!mpctx->current_track[0][STREAM_VIDEO] ||
+ mpctx->current_track[0][STREAM_VIDEO]->image) {
+ a[0] += SUB_SEEK_WITHOUT_VIDEO_OFFSET - SUB_SEEK_OFFSET;
+ }
mark_seek(mpctx);
queue_seek(mpctx, MPSEEK_ABSOLUTE, a[0], MPSEEK_EXACT,
MPSEEK_FLAG_DELAY);
@@ -5472,29 +5581,84 @@ static void cmd_expand_path(void *p)
};
}
+static void cmd_escape_ass(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ bstr dst = {0};
+
+ osd_mangle_ass(&dst, cmd->args[0].v.s, true);
+
+ cmd->result = (mpv_node){
+ .format = MPV_FORMAT_STRING,
+ .u.string = dst.len ? (char *)dst.start : talloc_strdup(NULL, ""),
+ };
+}
+
+static struct load_action get_load_action(struct MPContext *mpctx, int action_flag)
+{
+ switch (action_flag) {
+ case 0: // replace
+ return (struct load_action){LOAD_TYPE_REPLACE, .play = true};
+ case 1: // append
+ return (struct load_action){LOAD_TYPE_APPEND, .play = false};
+ case 2: // append-play
+ return (struct load_action){LOAD_TYPE_APPEND, .play = true};
+ case 3: // insert-next
+ return (struct load_action){LOAD_TYPE_INSERT_NEXT, .play = false};
+ case 4: // insert-next-play
+ return (struct load_action){LOAD_TYPE_INSERT_NEXT, .play = true};
+ case 5: // insert-at
+ return (struct load_action){LOAD_TYPE_INSERT_AT, .play = false};
+ case 6: // insert-at-play
+ return (struct load_action){LOAD_TYPE_INSERT_AT, .play = true};
+ default: // default: replace
+ return (struct load_action){LOAD_TYPE_REPLACE, .play = true};
+ }
+}
+
+static struct playlist_entry *get_insert_entry(struct MPContext *mpctx, struct load_action *action,
+ int insert_at_idx)
+{
+ switch (action->type) {
+ case LOAD_TYPE_INSERT_NEXT:
+ return playlist_get_next(mpctx->playlist, +1);
+ case LOAD_TYPE_INSERT_AT:
+ return playlist_entry_from_index(mpctx->playlist, insert_at_idx);
+ case LOAD_TYPE_REPLACE:
+ case LOAD_TYPE_APPEND:
+ default:
+ return NULL;
+ }
+}
+
static void cmd_loadfile(void *p)
{
struct mp_cmd_ctx *cmd = p;
struct MPContext *mpctx = cmd->mpctx;
char *filename = cmd->args[0].v.s;
- int append = cmd->args[1].v.i;
+ int action_flag = cmd->args[1].v.i;
+ int insert_at_idx = cmd->args[2].v.i;
+
+ struct load_action action = get_load_action(mpctx, action_flag);
- if (!append)
+ if (action.type == LOAD_TYPE_REPLACE)
playlist_clear(mpctx->playlist);
struct playlist_entry *entry = playlist_entry_new(filename);
- if (cmd->args[2].v.str_list) {
- char **pairs = cmd->args[2].v.str_list;
+ if (cmd->args[3].v.str_list) {
+ char **pairs = cmd->args[3].v.str_list;
for (int i = 0; pairs[i] && pairs[i + 1]; i += 2)
playlist_entry_add_param(entry, bstr0(pairs[i]), bstr0(pairs[i + 1]));
}
- playlist_add(mpctx->playlist, entry);
+
+ struct playlist_entry *at = get_insert_entry(mpctx, &action, insert_at_idx);
+ playlist_insert_at(mpctx->playlist, entry, at);
struct mpv_node *res = &cmd->result;
node_init(res, MPV_FORMAT_NODE_MAP, NULL);
node_map_add_int64(res, "playlist_entry_id", entry->id);
- if (!append || (append == 2 && !mpctx->playlist->current)) {
+ if (action.type == LOAD_TYPE_REPLACE || (action.play && !mpctx->playlist->current)) {
if (mpctx->opts->position_save_on_quit) // requested in issue #1148
mp_write_watch_later_conf(mpctx);
mp_set_playlist_entry(mpctx, entry);
@@ -5508,25 +5672,37 @@ static void cmd_loadlist(void *p)
struct mp_cmd_ctx *cmd = p;
struct MPContext *mpctx = cmd->mpctx;
char *filename = cmd->args[0].v.s;
- int append = cmd->args[1].v.i;
+ int action_flag = cmd->args[1].v.i;
+ int insert_at_idx = cmd->args[2].v.i;
+
+ struct load_action action = get_load_action(mpctx, action_flag);
struct playlist *pl = playlist_parse_file(filename, cmd->abort->cancel,
mpctx->global);
if (pl) {
prepare_playlist(mpctx, pl);
struct playlist_entry *new = pl->current;
- if (!append)
+ if (action.type == LOAD_TYPE_REPLACE)
playlist_clear(mpctx->playlist);
struct playlist_entry *first = playlist_entry_from_index(pl, 0);
int num_entries = pl->num_entries;
- playlist_append_entries(mpctx->playlist, pl);
+
+ struct playlist_entry *at = get_insert_entry(mpctx, &action, insert_at_idx);
+ if (at == NULL) {
+ playlist_append_entries(mpctx->playlist, pl);
+ } else {
+ int at_index = playlist_entry_to_index(mpctx->playlist, at);
+ playlist_transfer_entries_to(mpctx->playlist, at_index, pl);
+ }
talloc_free(pl);
if (!new)
new = playlist_get_first(mpctx->playlist);
- if ((!append || (append == 2 && !mpctx->playlist->current)) && new)
+ if ((action.type == LOAD_TYPE_REPLACE ||
+ (action.play && !mpctx->playlist->current)) && new) {
mp_set_playlist_entry(mpctx, new);
+ }
struct mpv_node *res = &cmd->result;
node_init(res, MPV_FORMAT_NODE_MAP, NULL);
@@ -5752,6 +5928,10 @@ static void cmd_track_reload(void *p)
}
struct track *nt = mpctx->tracks[nt_num];
+
+ if (!nt->lang)
+ nt->lang = mp_guess_lang_from_filename(nt, nt->external_filename);
+
mp_switch_track(mpctx, nt->type, nt, 0);
print_track_list(mpctx, "Reloaded:");
}
@@ -5787,7 +5967,7 @@ static void cmd_run(void *p)
char **args = talloc_zero_array(NULL, char *, cmd->num_args + 1);
for (int n = 0; n < cmd->num_args; n++)
args[n] = cmd->args[n].v.s;
- mp_msg_flush_status_line(mpctx->log);
+ mp_msg_flush_status_line(mpctx->log, true);
struct mp_subprocess_opts opts = {
.exe = args[0],
.args = args,
@@ -6176,7 +6356,7 @@ static void cmd_mouse(void *p)
if (button == -1) {// no button
if (pre_key)
- mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_put_key_artificial(mpctx->input, pre_key, 1);
mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
return;
}
@@ -6194,9 +6374,9 @@ static void cmd_mouse(void *p)
}
button += dbc ? MP_MBTN_DBL_BASE : MP_MBTN_BASE;
if (pre_key)
- mp_input_put_key_artificial(mpctx->input, pre_key);
+ mp_input_put_key_artificial(mpctx->input, pre_key, 1);
mp_input_set_mouse_pos_artificial(mpctx->input, x, y);
- mp_input_put_key_artificial(mpctx->input, button);
+ mp_input_put_key_artificial(mpctx->input, button, 1);
}
static void cmd_key(void *p)
@@ -6207,7 +6387,7 @@ static void cmd_key(void *p)
const char *key_name = cmd->args[0].v.s;
if (key_name[0] == '\0' && action == MP_KEY_STATE_UP) {
- mp_input_put_key_artificial(mpctx->input, MP_INPUT_RELEASE_ALL);
+ mp_input_put_key_artificial(mpctx->input, MP_INPUT_RELEASE_ALL, 1);
} else {
int code = mp_input_get_key_from_name(key_name);
if (code < 0) {
@@ -6215,7 +6395,8 @@ static void cmd_key(void *p)
cmd->success = false;
return;
}
- mp_input_put_key_artificial(mpctx->input, code | action);
+ double scale = action == 0 ? cmd->args[1].v.d : 1;
+ mp_input_put_key_artificial(mpctx->input, code | action, scale);
}
}
@@ -6248,6 +6429,32 @@ static void cmd_apply_profile(void *p)
}
}
+static void cmd_load_config_file(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *config_file = cmd->args[0].v.s;
+ int r = m_config_parse_config_file(mpctx->mconfig, mpctx->global,
+ config_file, NULL, 0);
+
+ if (r < 1) {
+ cmd->success = false;
+ return;
+ }
+
+ mp_notify_property(mpctx, "profile-list");
+}
+
+static void cmd_load_input_conf(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+
+ char *config_file = cmd->args[0].v.s;
+ cmd->success = mp_input_load_config_file(mpctx->input, config_file);
+}
+
static void cmd_load_script(void *p)
{
struct mp_cmd_ctx *cmd = p;
@@ -6349,6 +6556,26 @@ static void cmd_dump_cache_ab(void *p)
cmd->args[0].v.s);
}
+static void cmd_begin_vo_dragging(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct vo *vo = mpctx->video_out;
+
+ if (vo)
+ vo_control(vo, VOCTRL_BEGIN_DRAGGING, NULL);
+}
+
+static void cmd_context_menu(void *p)
+{
+ struct mp_cmd_ctx *cmd = p;
+ struct MPContext *mpctx = cmd->mpctx;
+ struct vo *vo = mpctx->video_out;
+
+ if (vo)
+ vo_control(vo, VOCTRL_SHOW_MENU, NULL);
+}
+
/* This array defines all known commands.
* The first field the command name used in libmpv and input.conf.
* The second field is the handler function (see mp_cmd_def.handler and
@@ -6474,6 +6701,8 @@ const struct mp_cmd_def mp_cmds[] = {
.is_noisy = true },
{ "expand-path", cmd_expand_path, { {"text", OPT_STRING(v.s)} },
.is_noisy = true },
+ { "escape-ass", cmd_escape_ass, { {"text", OPT_STRING(v.s)} },
+ .is_noisy = true },
{ "show-progress", cmd_show_progress, .allow_auto_repeat = true,
.is_noisy = true },
@@ -6600,8 +6829,13 @@ const struct mp_cmd_def mp_cmds[] = {
{"flags", OPT_CHOICE(v.i,
{"replace", 0},
{"append", 1},
- {"append-play", 2}),
+ {"append-play", 2},
+ {"insert-next", 3},
+ {"insert-next-play", 4},
+ {"insert-at", 5},
+ {"insert-at-play", 6}),
.flags = MP_CMD_OPT_ARG},
+ {"index", OPT_INT(v.i), OPTDEF_INT(-1)},
{"options", OPT_KEYVALUELIST(v.str_list), .flags = MP_CMD_OPT_ARG},
},
},
@@ -6611,8 +6845,13 @@ const struct mp_cmd_def mp_cmds[] = {
{"flags", OPT_CHOICE(v.i,
{"replace", 0},
{"append", 1},
- {"append-play", 2}),
+ {"append-play", 2},
+ {"insert-next", 3},
+ {"insert-next-play", 4},
+ {"insert-at", 5},
+ {"insert-at-play", 6}),
.flags = MP_CMD_OPT_ARG},
+ {"index", OPT_INT(v.i), OPTDEF_INT(-1)},
},
.spawn_thread = true,
.can_abort = true,
@@ -6740,7 +6979,9 @@ const struct mp_cmd_def mp_cmds[] = {
{"fmt", OPT_STRING(v.s)},
{"w", OPT_INT(v.i)},
{"h", OPT_INT(v.i)},
- {"stride", OPT_INT(v.i)}, }},
+ {"stride", OPT_INT(v.i)},
+ {"dw", OPT_INT(v.i), OPTDEF_INT(0)},
+ {"dh", OPT_INT(v.i), OPTDEF_INT(0)}, }},
{ "overlay-remove", cmd_overlay_remove, { {"id", OPT_INT(v.i)} } },
{ "osd-overlay", cmd_osd_overlay,
@@ -6769,7 +7010,8 @@ const struct mp_cmd_def mp_cmds[] = {
.flags = MP_CMD_OPT_ARG}}},
{ "keybind", cmd_key_bind, { {"name", OPT_STRING(v.s)},
{"cmd", OPT_STRING(v.s)} }},
- { "keypress", cmd_key, { {"name", OPT_STRING(v.s)} },
+ { "keypress", cmd_key, { {"name", OPT_STRING(v.s)},
+ {"scale", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(1)} },
.priv = &(const int){0}},
{ "keydown", cmd_key, { {"name", OPT_STRING(v.s)} },
.priv = &(const int){MP_KEY_STATE_DOWN}},
@@ -6782,6 +7024,10 @@ const struct mp_cmd_def mp_cmds[] = {
.flags = MP_CMD_OPT_ARG}, }
},
+ { "load-config-file", cmd_load_config_file, {{"filename", OPT_STRING(v.s)}} },
+
+ { "load-input-conf", cmd_load_input_conf, {{"filename", OPT_STRING(v.s)}} },
+
{ "load-script", cmd_load_script, {{"filename", OPT_STRING(v.s)}} },
{ "dump-cache", cmd_dump_cache, { {"start", OPT_TIME(v.d),
@@ -6800,6 +7046,10 @@ const struct mp_cmd_def mp_cmds[] = {
{ "ab-loop-align-cache", cmd_align_cache_ab },
+ { "begin-vo-dragging", cmd_begin_vo_dragging },
+
+ { "context-menu", cmd_context_menu },
+
{0}
};
@@ -6821,6 +7071,11 @@ void command_uninit(struct MPContext *mpctx)
mpctx->command_ctx = NULL;
}
+static int str_compare(const void *a, const void *b)
+{
+ return strcmp(*(const char **)a, *(const char **)b);
+}
+
void command_init(struct MPContext *mpctx)
{
struct command_ctx *ctx = talloc(NULL, struct command_ctx);
@@ -6835,6 +7090,11 @@ void command_init(struct MPContext *mpctx)
talloc_zero_array(ctx, struct m_property, num_base + num_opts + 1);
memcpy(ctx->properties, mp_properties_base, sizeof(mp_properties_base));
+ const char **prop_names = talloc_array(NULL, const char *, num_base);
+ for (int i = 0; i < num_base; ++i)
+ prop_names[i] = mp_properties_base[i].name;
+ qsort(prop_names, num_base, sizeof(const char *), str_compare);
+
int count = num_base;
for (int n = 0; n < num_opts; n++) {
struct m_config_option *co = m_config_get_co_index(mpctx->mconfig, n);
@@ -6868,14 +7128,18 @@ void command_init(struct MPContext *mpctx)
}
// The option might be covered by a manual property already.
- if (m_property_list_find(ctx->properties, prop.name))
+ if (bsearch(&prop.name, prop_names, num_base, sizeof(const char *), str_compare))
continue;
ctx->properties[count++] = prop;
}
+ node_init(&ctx->mdata, MPV_FORMAT_NODE_ARRAY, NULL);
+ talloc_steal(ctx, ctx->mdata.u.list);
+
node_init(&ctx->udata, MPV_FORMAT_NODE_MAP, NULL);
talloc_steal(ctx, ctx->udata.u.list);
+ talloc_free(prop_names);
}
static void command_event(struct MPContext *mpctx, int event, void *arg)
@@ -6891,6 +7155,9 @@ static void command_event(struct MPContext *mpctx, int event, void *arg)
if (event == MPV_EVENT_PLAYBACK_RESTART)
ctx->last_seek_time = mp_time_sec();
+ if (event == MPV_EVENT_END_FILE)
+ mp_msg_flush_status_line(mpctx->log, false);
+
if (event == MPV_EVENT_END_FILE || event == MPV_EVENT_FILE_LOADED) {
// Update chapters - does nothing if something else is visible.
set_osd_bar_chapters(mpctx, OSD_BAR_SEEK);
@@ -6921,6 +7188,27 @@ void handle_command_updates(struct MPContext *mpctx)
// Depends on polling demuxer wakeup callback notifications.
cache_dump_poll(mpctx);
+
+ // Potentially run the commands now (idle) instead of waiting for a file to load.
+ if (mpctx->stop_play == PT_STOP)
+ run_command_opts(mpctx);
+}
+
+void run_command_opts(struct MPContext *mpctx)
+{
+ struct MPOpts *opts = mpctx->opts;
+ struct command_ctx *ctx = mpctx->command_ctx;
+
+ if (!opts->input_commands || ctx->command_opts_processed)
+ return;
+
+ // Take easy way out and add these to the input queue.
+ for (int i = 0; opts->input_commands[i]; i++) {
+ struct mp_cmd *cmd = mp_input_parse_cmd(mpctx->input, bstr0(opts->input_commands[i]),
+ "the command line");
+ mp_input_queue_cmd(mpctx->input, cmd);
+ }
+ ctx->command_opts_processed = true;
}
void mp_notify(struct MPContext *mpctx, int event, void *arg)
@@ -6987,8 +7275,11 @@ void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
if (sub) {
int ret = sub_control(sub, SD_CTRL_UPDATE_OPTS,
(void *)(uintptr_t)flags);
- if (ret == CONTROL_OK && flags & (UPDATE_SUB_FILT | UPDATE_SUB_HARD))
+ if (ret == CONTROL_OK && flags & (UPDATE_SUB_FILT | UPDATE_SUB_HARD)) {
sub_redecode_cached_packets(sub);
+ if (track->selected)
+ reselect_demux_stream(mpctx, track, true);
+ }
}
}
osd_changed(mpctx->osd);
@@ -7016,13 +7307,15 @@ void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
mpctx->ipc_ctx = mp_init_ipc(mpctx->clients, mpctx->global);
}
- if (opt_ptr == &opts->vo->video_driver_list) {
+ if (opt_ptr == &opts->vo->video_driver_list ||
+ opt_ptr == &opts->ra_ctx_opts->context_name ||
+ opt_ptr == &opts->ra_ctx_opts->context_type) {
struct track *track = mpctx->current_track[0][STREAM_VIDEO];
uninit_video_out(mpctx);
handle_force_window(mpctx, true);
reinit_video_chain(mpctx);
if (track)
- reselect_demux_stream(mpctx, track, true);
+ queue_seek(mpctx, MPSEEK_RELATIVE, 0.0, MPSEEK_EXACT, 0);
mp_wakeup_core(mpctx);
}
@@ -7042,11 +7335,23 @@ void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
if (flags & UPDATE_LAVFI_COMPLEX)
update_lavfi_complex(mpctx);
+ if (flags & UPDATE_VIDEO) {
+ if (mpctx->video_out) {
+ vo_control(mpctx->video_out, VOCTRL_UPDATE_RENDER_OPTS, NULL);
+ mp_wakeup_core(mpctx);
+ }
+ }
+
if (opt_ptr == &opts->vo->android_surface_size) {
if (mpctx->video_out)
vo_control(mpctx->video_out, VOCTRL_EXTERNAL_RESIZE, NULL);
}
+ if (opt_ptr == &opts->input_commands) {
+ mpctx->command_ctx->command_opts_processed = false;
+ run_command_opts(mpctx);
+ }
+
if (opt_ptr == &opts->playback_speed) {
update_playback_speed(mpctx);
mp_wakeup_core(mpctx);
@@ -7103,6 +7408,9 @@ void mp_option_change_callback(void *ctx, struct m_config_option *co, int flags,
if (opt_ptr == &opts->vo->window_scale)
update_window_scale(mpctx);
+ if (opt_ptr == &opts->vo->hidpi_window_scale)
+ update_hidpi_window_scale(mpctx, opts->vo->hidpi_window_scale);
+
if (opt_ptr == &opts->cursor_autohide_delay)
mpctx->mouse_timer = 0;
diff --git a/player/command.h b/player/command.h
index 185b78f..31e3b32 100644
--- a/player/command.h
+++ b/player/command.h
@@ -70,6 +70,7 @@ void run_command(struct MPContext *mpctx, struct mp_cmd *cmd,
struct mp_abort_entry *abort,
void (*on_completion)(struct mp_cmd_ctx *cmd),
void *on_completion_priv);
+void run_command_opts(struct MPContext *mpctx);
void mp_cmd_ctx_complete(struct mp_cmd_ctx *cmd);
PRINTF_ATTRIBUTE(3, 4)
void mp_cmd_msg(struct mp_cmd_ctx *cmd, int status, const char *msg, ...);
diff --git a/player/configfiles.c b/player/configfiles.c
index 9441638..2b94308 100644
--- a/player/configfiles.c
+++ b/player/configfiles.c
@@ -251,6 +251,9 @@ static bool needs_config_quoting(const char *s)
static void write_filename(struct MPContext *mpctx, FILE *file, char *filename)
{
+ if (mpctx->opts->ignore_path_in_watch_later_config && !mp_is_url(bstr0(filename)))
+ filename = mp_basename(filename);
+
if (mpctx->opts->write_filename_in_watch_later_config) {
char write_name[1024] = {0};
for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++)
@@ -280,7 +283,7 @@ static void write_redirect(struct MPContext *mpctx, char *path)
static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path)
{
- if (mp_is_url(bstr0(path)))
+ if (mp_is_url(bstr0(path)) || mpctx->opts->ignore_path_in_watch_later_config)
return;
// Write redirect entries for the file's parent directories to allow
@@ -403,7 +406,7 @@ void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file)
talloc_free(fname);
}
- if (mp_is_url(bstr0(file)))
+ if (mp_is_url(bstr0(file)) || mpctx->opts->ignore_path_in_watch_later_config)
return;
void *ctx = talloc_new(NULL);
diff --git a/player/core.h b/player/core.h
index 8a49585..c44868c 100644
--- a/player/core.h
+++ b/player/core.h
@@ -120,6 +120,7 @@ struct track {
char *title;
bool default_track, forced_track, dependent_track;
bool visual_impaired_track, hearing_impaired_track;
+ bool forced_select; // if the track was selected because it is forced
bool image;
bool attached_picture;
char *lang;
@@ -131,6 +132,8 @@ struct track {
char *external_filename;
bool auto_loaded;
+ bool demuxer_ready; // if more packets should be read (subtitles only)
+
struct demuxer *demuxer;
// Invariant: !stream || stream->demuxer == demuxer
struct sh_stream *stream;
@@ -191,6 +194,8 @@ struct ao_chain {
double start_pts;
bool start_pts_known;
+ bool audio_started;
+
struct track *track;
struct mp_pin *filter_src;
struct mp_pin *dec_src;
@@ -402,6 +407,9 @@ typedef struct MPContext {
int last_chapter_seek;
bool last_chapter_flag;
+ /* Heuristic for potentially redrawing subs. */
+ bool redraw_subs;
+
bool paused; // internal pause state
bool playback_active; // not paused, restarting, loading, unloading
bool in_playloop;
@@ -621,6 +629,7 @@ void mp_load_builtin_scripts(struct MPContext *mpctx);
int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname);
// sub.c
+void redraw_subs(struct MPContext *mpctx);
void reset_subtitle_state(struct MPContext *mpctx);
void reinit_sub(struct MPContext *mpctx, struct track *track);
void reinit_sub_all(struct MPContext *mpctx);
diff --git a/player/external_files.c b/player/external_files.c
index e9a6081..2e00912 100644
--- a/player/external_files.c
+++ b/player/external_files.c
@@ -142,6 +142,14 @@ static struct bstr guess_lang_from_filename(struct bstr name, int *fn_start)
return (struct bstr){name.start + i + 1, n};
}
+char *mp_guess_lang_from_filename(void* ctx, const char *filename)
+{
+ bstr filename_no_ext = bstr_strip_ext(bstr0(filename));
+ int start = 0; // only used in append_dir_subtitles()
+ char *lang = bstrto0(ctx, guess_lang_from_filename(filename_no_ext, &start));
+ return lang;
+}
+
static void append_dir_subtitles(struct mpv_global *global, struct MPOpts *opts,
struct subfn **slist, int *nsub,
struct bstr path, const char *fname,
diff --git a/player/external_files.h b/player/external_files.h
index 20b37c3..5d42c55 100644
--- a/player/external_files.h
+++ b/player/external_files.h
@@ -34,5 +34,6 @@ struct subfn *find_external_files(struct mpv_global *global, const char *fname,
bool mp_might_be_subtitle_file(const char *filename);
void mp_update_subtitle_exts(struct MPOpts *opts);
+char *mp_guess_lang_from_filename(void *talloc_ctx, const char *filename);
#endif /* MPLAYER_FINDFILES_H */
diff --git a/player/javascript/defaults.js b/player/javascript/defaults.js
index d906ec2..9f130c9 100644
--- a/player/javascript/defaults.js
+++ b/player/javascript/defaults.js
@@ -177,28 +177,6 @@ mp.abort_async_command = function abort_async_command(id) {
mp._abort_async_command(id);
}
-// shared-script-properties - always an object, even if without properties
-function shared_script_property_set(name, val) {
- if (arguments.length > 1)
- return mp.commandv("change-list", "shared-script-properties", "append", "" + name + "=" + val);
- else
- return mp.commandv("change-list", "shared-script-properties", "remove", name);
-}
-
-function shared_script_property_get(name) {
- return mp.get_property_native("shared-script-properties")[name];
-}
-
-function shared_script_property_observe(name, cb) {
- return mp.observe_property("shared-script-properties", "native",
- function shared_props_cb(_name, val) { cb(name, val[name]) }
- );
-}
-
-mp.utils.shared_script_property_set = shared_script_property_set;
-mp.utils.shared_script_property_get = shared_script_property_get;
-mp.utils.shared_script_property_observe = shared_script_property_observe;
-
// osd-ass
var next_assid = 1;
mp.create_osd_overlay = function create_osd_overlay(format) {
@@ -268,10 +246,10 @@ mp.get_osd_margins = function get_osd_margins() {
// {cb: fn, forced: bool, maybe input: str, repeatable: bool, complex: bool}
var binds = new_cache();
-function dispatch_key_binding(name, state, key_name) {
+function dispatch_key_binding(name, state, key_name, key_text) {
var cb = binds[name] ? binds[name].cb : false;
if (cb) // "script-binding [<script_name>/]<name>" command was invoked
- cb(state, key_name);
+ cb(state, key_name, key_text);
}
var binds_tid = 0; // flush timer id. actual id's are always true-thy
@@ -329,11 +307,12 @@ function add_binding(forced, key, name, fn, opts) {
fn({event: "press", is_mouse: false});
});
var KEY_STATES = { u: "up", d: "down", r: "repeat", p: "press" };
- key_data.cb = function key_cb(state, key_name) {
+ key_data.cb = function key_cb(state, key_name, key_text) {
fn({
event: KEY_STATES[state[0]] || "unknown",
is_mouse: state[1] == "m",
- key_name: key_name || undefined
+ key_name: key_name || undefined,
+ key_text: key_text || undefined
});
}
} else {
@@ -665,6 +644,56 @@ function read_options(opts, id, on_update, conf_override) {
mp.options = { read_options: read_options };
/**********************************************************************
+* input
+*********************************************************************/
+mp.input = {
+ get: function(t) {
+ mp.commandv("script-message-to", "console", "get-input", mp.script_name,
+ JSON.stringify({
+ prompt: t.prompt,
+ default_text: t.default_text,
+ cursor_position: t.cursor_position,
+ id: t.id,
+ }));
+
+ mp.register_script_message("input-event", function (type, text, cursor_position) {
+ if (t[type]) {
+ var result = t[type](text, cursor_position);
+
+ if (type == "complete" && result) {
+ mp.commandv("script-message-to", "console", "complete",
+ JSON.stringify(result[0]), result[1]);
+ }
+ }
+
+ if (type == "closed") {
+ mp.unregister_script_message("input-event");
+ }
+ })
+
+ return true;
+ },
+ terminate: function () {
+ mp.commandv("script-message-to", "console", "disable");
+ },
+ log: function (message, style, terminal_style) {
+ mp.commandv("script-message-to", "console", "log", JSON.stringify({
+ text: message,
+ style: style,
+ terminal_style: terminal_style,
+ }));
+ },
+ log_error: function (message) {
+ mp.commandv("script-message-to", "console", "log",
+ JSON.stringify({ text: message, error: true }));
+ },
+ set_log: function (log) {
+ mp.commandv("script-message-to", "console", "set-log",
+ JSON.stringify(log));
+ }
+}
+
+/**********************************************************************
* various
*********************************************************************/
g.print = mp.msg.info; // convenient alias
diff --git a/player/loadfile.c b/player/loadfile.c
index 1d25dc3..7421a47 100644
--- a/player/loadfile.c
+++ b/player/loadfile.c
@@ -278,6 +278,8 @@ static void print_stream(struct MPContext *mpctx, struct track *t)
APPEND(b, " '%s'", t->title);
const char *codec = s ? s->codec->codec : NULL;
APPEND(b, " (%s", codec ? codec : "<unknown>");
+ if (s && s->codec->codec_profile)
+ APPEND(b, " [%s]", s->codec->codec_profile);
if (t->type == STREAM_VIDEO) {
if (s && s->codec->disp_w)
APPEND(b, " %dx%d", s->codec->disp_w, s->codec->disp_h);
@@ -483,9 +485,10 @@ static int match_lang(char **langs, const char *lang)
* Forced tracks are preferred when the user prefers not to display subtitles
*/
// Return whether t1 is preferred over t2
-static bool compare_track(struct track *t1, struct track *t2, char **langs,
- bool os_langs, struct MPOpts *opts, int preferred_program)
+static bool compare_track(struct track *t1, struct track *t2, char **langs, bool os_langs,
+ bool forced, struct MPOpts *opts, int preferred_program)
{
+ bool sub = t2->type == STREAM_SUB;
if (!opts->autoload_files && t1->is_external != t2->is_external)
return !t1->is_external;
bool ext1 = t1->is_external && !t1->no_default;
@@ -503,16 +506,17 @@ static bool compare_track(struct track *t1, struct track *t2, char **langs,
(t2->program_id == preferred_program))
return t1->program_id == preferred_program;
}
- int forced = t1->type == STREAM_SUB ? opts->subs_fallback_forced : 1;
- bool force_match = forced == 1 || (t1->forced_track && forced == 2) ||
- (!t1->forced_track && !forced);
int l1 = match_lang(langs, t1->lang), l2 = match_lang(langs, t2->lang);
if (!os_langs && l1 != l2)
- return l1 > l2 && force_match;
- if (t1->default_track != t2->default_track)
- return t1->default_track && force_match;
+ return l1 > l2;
+ if (forced)
+ return t1->forced_track;
+ if (sub && !t2->forced_select && t2->forced_track)
+ return !t1->forced_track;
+ if (t1->default_track != t2->default_track && !t2->forced_select)
+ return t1->default_track;
if (os_langs && l1 != l2)
- return l1 > l2 && force_match;
+ return l1 > l2;
if (t1->attached_picture != t2->attached_picture)
return !t1->attached_picture;
if (t1->stream && t2->stream && opts->hls_bitrate >= 0 &&
@@ -624,11 +628,7 @@ struct track *select_default_track(struct MPContext *mpctx, int order,
}
const char *audio_lang = get_audio_lang(mpctx);
bool sub = type == STREAM_SUB;
- bool fallback_forced = sub && opts->subs_fallback_forced;
- bool audio_matches = false;
- bool sub_fallback = false;
struct track *pick = NULL;
- struct track *forced_pick = NULL;
for (int n = 0; n < mpctx->num_tracks; n++) {
struct track *track = mpctx->tracks[n];
if (track->type != type)
@@ -643,45 +643,27 @@ struct track *select_default_track(struct MPContext *mpctx, int order,
continue;
if (duplicate_track(mpctx, order, type, track))
continue;
- if (!pick || compare_track(track, pick, langs, os_langs, mpctx->opts, preferred_program))
+ if (sub) {
+ // Subtitle specific auto-selecting crap.
+ bool audio_matches = mp_match_lang_single(audio_lang, track->lang);
+ bool forced = track->forced_track && (opts->subs_fallback_forced == 2 ||
+ (audio_matches && opts->subs_fallback_forced == 1));
+ bool lang_match = !os_langs && match_lang(langs, track->lang) > 0;
+ bool subs_fallback = (track->is_external && !track->no_default) || opts->subs_fallback == 2 ||
+ (opts->subs_fallback == 1 && track->default_track);
+ bool subs_matching_audio = (!match_lang(langs, audio_lang) || opts->subs_with_matching_audio == 2 ||
+ (opts->subs_with_matching_audio == 1 && track->forced_track));
+ if (subs_matching_audio && ((!pick && (forced || lang_match || subs_fallback)) ||
+ (pick && compare_track(track, pick, langs, os_langs, forced, mpctx->opts, preferred_program))))
+ {
+ pick = track;
+ pick->forced_select = forced;
+ }
+ } else if (!pick || compare_track(track, pick, langs, os_langs, false, mpctx->opts, preferred_program)) {
pick = track;
-
- // Autoselecting forced sub tracks requires the following:
- // 1. Matches the audio language or --subs-fallback-forced=always.
- // 2. Matches the users list of preferred languages or none were specified (i.e. slang was not set).
- // 3. A track *wasn't* already selected by slang previously or the track->lang matches pick->lang and isn't forced.
- bool valid_forced_slang = (os_langs || (mp_match_lang_single(pick->lang, track->lang) && !pick->forced_track) ||
- (match_lang(langs, track->lang) && !match_lang(langs, pick->lang)));
- bool audio_lang_match = mp_match_lang_single(audio_lang, track->lang);
- if (fallback_forced && track->forced_track && valid_forced_slang && audio_lang_match &&
- (!forced_pick || compare_track(track, forced_pick, langs, os_langs, mpctx->opts, preferred_program)))
- {
- forced_pick = track;
}
}
- // If we found a forced track, use that.
- if (forced_pick)
- pick = forced_pick;
-
- // Clear out any picks for these special cases for subtitles
- if (pick) {
- audio_matches = mp_match_lang_single(pick->lang, audio_lang);
- sub_fallback = (pick->is_external && !pick->no_default) || opts->subs_fallback == 2 ||
- (opts->subs_fallback == 1 && pick->default_track);
- }
- if (pick && !forced_pick && sub && (!match_lang(langs, pick->lang) || os_langs) && !sub_fallback)
- pick = NULL;
- // Handle this after matching langs and selecting a fallback.
- if (pick && sub && (!opts->subs_with_matching_audio && audio_matches))
- pick = NULL;
- // Handle edge cases if we picked a track that doesn't match the --subs-fallback-force value
- if (pick && sub && ((!pick->forced_track && opts->subs_fallback_forced == 2) ||
- (pick->forced_track && !opts->subs_fallback_forced)))
- {
- pick = NULL;
- }
-
if (pick && pick->attached_picture && !mpctx->opts->audio_display)
pick = NULL;
if (pick && !opts->autoload_files && pick->is_external)
diff --git a/player/lua.c b/player/lua.c
index 41fd520..6354769 100644
--- a/player/lua.c
+++ b/player/lua.c
@@ -61,6 +61,9 @@ static const char * const builtin_lua_scripts[][2] = {
{"mp.assdraw",
# include "player/lua/assdraw.lua.inc"
},
+ {"mp.input",
+# include "player/lua/input.lua.inc"
+ },
{"mp.options",
# include "player/lua/options.lua.inc"
},
@@ -509,7 +512,7 @@ static int script_log(lua_State *L)
const char *s = lua_tostring(L, -1);
if (s == NULL)
return luaL_error(L, "Invalid argument");
- mp_msg(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
+ mp_msg(ctx->log, msgl, (i == 2 ? "%s" : " %s"), s);
lua_pop(L, 1); // args... tostring
}
mp_msg(ctx->log, msgl, "\n");
@@ -1184,11 +1187,11 @@ static int script_format_json(lua_State *L, void *tmp)
char *dst = talloc_strdup(tmp, "");
if (json_write(&dst, &node) >= 0) {
lua_pushstring(L, dst);
- lua_pushnil(L);
- } else {
- lua_pushnil(L);
- lua_pushstring(L, "error");
+ return 1;
}
+
+ lua_pushnil(L);
+ lua_pushstring(L, "error");
return 2;
}
diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua
index 9dca878..a0f5802 100644
--- a/player/lua/auto_profiles.lua
+++ b/player/lua/auto_profiles.lua
@@ -164,8 +164,8 @@ local function compile_cond(name, s)
return chunk
end
-local function load_profiles()
- for i, v in ipairs(mp.get_property_native("profile-list")) do
+local function load_profiles(profiles_property)
+ for _, v in ipairs(profiles_property) do
local cond = v["profile-cond"]
if cond and #cond > 0 then
local profile = {
@@ -182,17 +182,25 @@ local function load_profiles()
end
end
-load_profiles()
+mp.observe_property("profile-list", "native", function (_, profiles_property)
+ profiles = {}
+ watched_properties = {}
+ cached_properties = {}
+ properties_to_profiles = {}
+ mp.unobserve_property(on_property_change)
-if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
- -- make it exit immediately
- _G.mp_event_loop = function() end
- return
-end
+ load_profiles(profiles_property)
+
+ if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
+ -- make it exit immediately
+ _G.mp_event_loop = function() end
+ return
+ end
+
+ on_idle() -- re-evaluate all profiles immediately
+end)
mp.register_idle(on_idle)
for _, name in ipairs({"on_load", "on_preloaded", "on_before_start_file"}) do
mp.add_hook(name, 50, on_hook)
end
-
-on_idle() -- re-evaluate all profiles immediately
diff --git a/player/lua/console.lua b/player/lua/console.lua
index 44e9436..bbfaf47 100644
--- a/player/lua/console.lua
+++ b/player/lua/console.lua
@@ -27,11 +27,13 @@ local opts = {
-- multiplied by "scale".
font_size = 16,
border_size = 1,
+ case_sensitive = true,
-- Remove duplicate entries in history as to only keep the latest one.
history_dedup = true,
-- The ratio of font height to font width.
-- Adjusts table width of completion suggestions.
- font_hw_ratio = 2.0,
+ -- Values in the range 1.8..2.5 make sense for common monospace fonts.
+ font_hw_ratio = 'auto',
}
function detect_platform()
@@ -48,6 +50,7 @@ end
local platform = detect_platform()
if platform == 'windows' then
opts.font = 'Consolas'
+ opts.case_sensitive = false
elseif platform == 'darwin' then
opts.font = 'Menlo'
else
@@ -66,11 +69,21 @@ local styles = {
-- cccc66 cc9966 cc99cc 537bd2
debug = '{\\1c&Ha09f93&}',
- verbose = '{\\1c&H99cc99&}',
+ v = '{\\1c&H99cc99&}',
warn = '{\\1c&H66ccff&}',
error = '{\\1c&H7a77f2&}',
fatal = '{\\1c&H5791f9&\\b1}',
suggestion = '{\\1c&Hcc99cc&}',
+ selected_suggestion = '{\\1c&H2fbdfa&\\b1}',
+}
+
+local terminal_styles = {
+ debug = '\027[1;30m',
+ v = '\027[32m',
+ warn = '\027[33m',
+ error = '\027[31m',
+ fatal = '\027[1;31m',
+ selected_suggestion = '\027[7m',
}
local repl_active = false
@@ -78,15 +91,26 @@ local insert_mode = false
local pending_update = false
local line = ''
local cursor = 1
-local history = {}
+local default_prompt = '>'
+local prompt = default_prompt
+local default_id = 'default'
+local id = default_id
+local histories = {[id] = {}}
+local history = histories[id]
local history_pos = 1
-local log_buffer = {}
-local suggestion_buffer = {}
+local log_buffers = {[id] = {}}
local key_bindings = {}
local global_margins = { t = 0, b = 0 }
+local input_caller
+local suggestion_buffer = {}
+local selected_suggestion_index
+local completion_start_position
+local completion_append
local file_commands = {}
local path_separator = platform == 'windows' and '\\' or '/'
+local completion_old_line
+local completion_old_cursor
local update_timer = nil
update_timer = mp.add_periodic_timer(0.05, function()
@@ -107,9 +131,88 @@ mp.observe_property("user-data/osc/margins", "native", function(_, val)
update()
end)
+do
+ local width_length_ratio = 0.5
+ local osd_width, osd_height = 100, 100
+
+ ---Update osd resolution if valid
+ local function update_osd_resolution()
+ local dim = mp.get_property_native('osd-dimensions')
+ if not dim or dim.w == 0 or dim.h == 0 then
+ return
+ end
+ osd_width = dim.w
+ osd_height = dim.h
+ end
+
+ local text_osd = mp.create_osd_overlay('ass-events')
+ text_osd.compute_bounds, text_osd.hidden = true, true
+
+ local function measure_bounds(ass_text)
+ update_osd_resolution()
+ text_osd.res_x, text_osd.res_y = osd_width, osd_height
+ text_osd.data = ass_text
+ local res = text_osd:update()
+ return res.x0, res.y0, res.x1, res.y1
+ end
+
+ ---Measure text width and normalize to a font size of 1
+ ---text has to be ass safe
+ local function normalized_text_width(text, size, horizontal)
+ local align, rotation = horizontal and 7 or 1, horizontal and 0 or -90
+ local template = '{\\pos(0,0)\\rDefault\\blur0\\bord0\\shad0\\q2\\an%s\\fs%s\\fn%s\\frz%s}%s'
+ local x1, y1 = nil, nil
+ size = size / 0.8
+ -- prevent endless loop
+ local repetitions_left = 5
+ repeat
+ size = size * 0.8
+ local ass = assdraw.ass_new()
+ ass.text = template:format(align, size, opts.font, rotation, text)
+ _, _, x1, y1 = measure_bounds(ass.text)
+ repetitions_left = repetitions_left - 1
+ -- make sure nothing got clipped
+ until (x1 and x1 < osd_width and y1 < osd_height) or repetitions_left == 0
+ local width = (repetitions_left == 0 and not x1) and 0 or (horizontal and x1 or y1)
+ return width / size, horizontal and osd_width or osd_height
+ end
+
+ local function fit_on_osd(text)
+ local estimated_width = #text * width_length_ratio
+ if osd_width >= osd_height then
+ -- Fill the osd as much as possible, bigger is more accurate.
+ return math.min(osd_width / estimated_width, osd_height), true
+ else
+ return math.min(osd_height / estimated_width, osd_width), false
+ end
+ end
+
+ local measured_font_hw_ratio = nil
+ function get_font_hw_ratio()
+ local font_hw_ratio = tonumber(opts.font_hw_ratio)
+ if font_hw_ratio then
+ return font_hw_ratio
+ end
+ if not measured_font_hw_ratio then
+ local alphabet = 'abcdefghijklmnopqrstuvwxyz'
+ local text = alphabet:rep(3)
+ update_osd_resolution()
+ local size, horizontal = fit_on_osd(text)
+ local normalized_width = normalized_text_width(text, size * 0.9, horizontal)
+ measured_font_hw_ratio = #text / normalized_width * 0.95
+ end
+ return measured_font_hw_ratio
+ end
+end
+
-- Add a line to the log buffer (which is limited to 100 lines)
-function log_add(style, text)
- log_buffer[#log_buffer + 1] = { style = style, text = text }
+function log_add(text, style, terminal_style)
+ local log_buffer = log_buffers[id]
+ log_buffer[#log_buffer + 1] = {
+ text = text,
+ style = style or '',
+ terminal_style = terminal_style or '',
+ }
if #log_buffer > 100 then
table.remove(log_buffer, 1)
end
@@ -126,19 +229,7 @@ end
-- Escape a string for verbatim display on the OSD
function ass_escape(str)
- -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
- -- it isn't followed by a recognised character, so add a zero-width
- -- non-breaking space
- str = str:gsub('\\', '\\\239\187\191')
- str = str:gsub('{', '\\{')
- str = str:gsub('}', '\\}')
- -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
- -- consecutive newlines
- str = str:gsub('\n', '\239\187\191\\N')
- -- Turn leading spaces into hard spaces to prevent ASS from stripping them
- str = str:gsub('\\N ', '\\N\\h')
- str = str:gsub('^ ', '\\h')
- return str
+ return mp.command_native({'escape-ass', str})
end
-- Takes a list of strings, a max width in characters and
@@ -206,7 +297,9 @@ function format_table(list, width_max, rows_max)
local spaces = math.floor((width_max - width_total) / (column_count - 1))
spaces = math.max(spaces_min, math.min(spaces_max, spaces))
- local spacing = column_count > 1 and string.format('%' .. spaces .. 's', ' ') or ''
+ local spacing = column_count > 1
+ and ass_escape(string.format('%' .. spaces .. 's', ' '))
+ or ''
local rows = {}
for row = 1, row_count do
@@ -217,12 +310,17 @@ function format_table(list, width_max, rows_max)
-- more then 99 leads to 'invalid format (width or precision too long)'
local format_string = column == column_count and '%s'
or '%-' .. math.min(column_widths[column], 99) .. 's'
- columns[column] = string.format(format_string, list[i])
+ columns[column] = ass_escape(string.format(format_string, list[i]))
+
+ if i == selected_suggestion_index then
+ columns[column] = styles.selected_suggestion .. columns[column]
+ .. '{\\b0}'.. styles.suggestion
+ end
end
-- first row is at the bottom
rows[row_count - row + 1] = table.concat(columns, spacing)
end
- return table.concat(rows, '\n'), row_count
+ return table.concat(rows, ass_escape('\n')), row_count
end
local function print_to_terminal()
@@ -233,13 +331,19 @@ local function print_to_terminal()
end
local log = ''
- for _, log_line in ipairs(log_buffer) do
- log = log .. log_line.text
+ for _, log_line in ipairs(log_buffers[id]) do
+ log = log .. log_line.terminal_style .. log_line.text .. '\027[0m'
end
- local suggestions = table.concat(suggestion_buffer, '\t')
- if suggestions ~= '' then
- suggestions = suggestions .. '\n'
+ local suggestions = ''
+ for i, suggestion in ipairs(suggestion_buffer) do
+ if i == selected_suggestion_index then
+ suggestions = suggestions .. terminal_styles.selected_suggestion ..
+ suggestion .. '\027[0m'
+ else
+ suggestions = suggestions .. suggestion
+ end
+ suggestions = suggestions .. (i < #suggestion_buffer and '\t' or '\n')
end
local before_cur = line:sub(1, cursor - 1)
@@ -249,22 +353,18 @@ local function print_to_terminal()
after_cur = ' '
end
- mp.osd_message(log .. suggestions .. '> ' .. before_cur .. '\027[7m' ..
- after_cur:sub(1, 1) .. '\027[0m' .. after_cur:sub(2), 999)
+ mp.osd_message(log .. suggestions .. prompt .. ' ' .. before_cur ..
+ '\027[7m' .. after_cur:sub(1, 1) .. '\027[0m' ..
+ after_cur:sub(2), 999)
end
-- Render the REPL and console as an ASS OSD
function update()
pending_update = false
- -- Print to the terminal when there is no VO. Check both vo-configured so
- -- it works with --force-window --idle and no video tracks, and whether
- -- there is a video track so that the condition doesn't become true while
- -- switching VO at runtime, making mp.osd_message() print to the VO's OSD.
- -- This issue does not happen when switching VO without any video track
- -- regardless of the condition used.
- if not mp.get_property_native('vo-configured')
- and not mp.get_property('current-tracks/video') then
+ -- Unlike vo-configured, current-vo doesn't become falsy while switching VO,
+ -- which would print the log to the OSD.
+ if not mp.get_property('current-vo') then
print_to_terminal()
return
end
@@ -300,8 +400,9 @@ function update()
-- thin as possible and make it appear to be 1px wide by giving it 0.5px
-- horizontal borders.
local cheight = opts.font_size * 8
- local cglyph = '{\\r' ..
- '\\1a&H44&\\3a&H44&\\4a&H99&' ..
+ local cglyph = '{\\rDefault' ..
+ (mp.get_property_native('focused') == false
+ and '\\alpha&HFF&' or '\\1a&H44&\\3a&H44&\\4a&H99&') ..
'\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
'\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
@@ -317,12 +418,13 @@ function update()
local screeny_factor = (1 - global_margins.t - global_margins.b)
local lines_max = math.ceil(screeny * screeny_factor / opts.font_size - 1.5)
-- Estimate how many characters fit in one line
- local width_max = math.ceil(screenx / opts.font_size * opts.font_hw_ratio)
+ local width_max = math.ceil(screenx / opts.font_size * get_font_hw_ratio())
local suggestions, rows = format_table(suggestion_buffer, width_max, lines_max)
- local suggestion_ass = style .. styles.suggestion .. ass_escape(suggestions)
+ local suggestion_ass = style .. styles.suggestion .. suggestions
local log_ass = ''
+ local log_buffer = log_buffers[id]
local log_messages = #log_buffer
local log_max_lines = math.max(0, lines_max - rows)
if log_max_lines < log_messages then
@@ -339,7 +441,7 @@ function update()
if #suggestions > 0 then
ass:append(suggestion_ass .. '\\N')
end
- ass:append(style .. '> ' .. before_cur)
+ ass:append(style .. ass_escape(prompt) .. ' ' .. before_cur)
ass:append(cglyph)
ass:append(style .. after_cur)
@@ -348,7 +450,7 @@ function update()
ass:new_event()
ass:an(1)
ass:pos(2, screeny - 2 - global_margins.b * screeny)
- ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
+ ass:append(style .. '{\\alpha&HFF&}' .. ass_escape(prompt) .. ' ' .. before_cur)
ass:append(cglyph)
ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
@@ -362,12 +464,28 @@ function set_active(active)
repl_active = true
insert_mode = false
mp.enable_key_bindings('console-input', 'allow-hide-cursor+allow-vo-dragging')
- mp.enable_messages('terminal-default')
define_key_bindings()
+
+ if not input_caller then
+ prompt = default_prompt
+ id = default_id
+ history = histories[id]
+ history_pos = #history + 1
+ mp.enable_messages('terminal-default')
+ end
else
repl_active = false
+ suggestion_buffer = {}
undefine_key_bindings()
mp.enable_messages('silent:terminal-default')
+
+ if input_caller then
+ mp.commandv('script-message-to', input_caller, 'input-event',
+ 'closed', line, cursor)
+ input_caller = nil
+ line = ''
+ cursor = 1
+ end
collectgarbage()
end
update()
@@ -429,6 +547,16 @@ function len_utf8(str)
return len
end
+local function handle_edit()
+ suggestion_buffer = {}
+ update()
+
+ if input_caller then
+ mp.commandv('script-message-to', input_caller, 'input-event', 'edited',
+ line)
+ end
+end
+
-- Insert a character at the current cursor position (any_unicode)
function handle_char_input(c)
if insert_mode then
@@ -437,8 +565,7 @@ function handle_char_input(c)
line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
end
cursor = cursor + #c
- suggestion_buffer = {}
- update()
+ handle_edit()
end
-- Remove the character behind the cursor (Backspace)
@@ -447,16 +574,14 @@ function handle_backspace()
local prev = prev_utf8(line, cursor)
line = line:sub(1, prev - 1) .. line:sub(cursor)
cursor = prev
- suggestion_buffer = {}
- update()
+ handle_edit()
end
-- Remove the character in front of the cursor (Del)
function handle_del()
if cursor > line:len() then return end
line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
- suggestion_buffer = {}
- update()
+ handle_edit()
end
-- Toggle insert mode (Ins)
@@ -467,12 +592,14 @@ end
-- Move the cursor to the next character (Right)
function next_char(amount)
cursor = next_utf8(line, cursor)
+ suggestion_buffer = {}
update()
end
-- Move the cursor to the previous character (Left)
function prev_char(amount)
cursor = prev_utf8(line, cursor)
+ suggestion_buffer = {}
update()
end
@@ -482,8 +609,7 @@ function clear()
cursor = 1
insert_mode = false
history_pos = #history + 1
- suggestion_buffer = {}
- update()
+ handle_edit()
end
-- Close the REPL if the current line is empty, otherwise delete the next
@@ -521,7 +647,8 @@ function help_command(param)
end
end
if not cmd then
- log_add(styles.error, 'No command matches "' .. param .. '"!')
+ log_add('No command matches "' .. param .. '"!\n', styles.error,
+ terminal_styles.error)
return
end
output = output .. 'Command "' .. cmd.name .. '"\n'
@@ -536,7 +663,7 @@ function help_command(param)
output = output .. 'This command supports variable arguments.\n'
end
end
- log_add('', output)
+ log_add(output)
end
-- Add a line to the history and deduplicate
@@ -556,20 +683,25 @@ end
-- Run the current command and clear the line (Enter)
function handle_enter()
- if line == '' then
+ if line == '' and input_caller == nil then
return
end
- if history[#history] ~= line then
+ if history[#history] ~= line and line ~= '' then
history_add(line)
end
- -- match "help [<text>]", return <text> or "", strip all whitespace
- local help = line:match('^%s*help%s+(.-)%s*$') or
- (line:match('^%s*help$') and '')
- if help then
- help_command(help)
+ if input_caller then
+ mp.commandv('script-message-to', input_caller, 'input-event', 'submit',
+ line)
else
- mp.command(line)
+ -- match "help [<text>]", return <text> or "", strip all whitespace
+ local help = line:match('^%s*help%s+(.-)%s*$') or
+ (line:match('^%s*help$') and '')
+ if help then
+ help_command(help)
+ else
+ mp.command(line)
+ end
end
clear()
@@ -607,6 +739,7 @@ function go_history(new_pos)
end
cursor = line:len() + 1
insert_mode = false
+ suggestion_buffer = {}
update()
end
@@ -632,6 +765,7 @@ function prev_word()
-- string in order to do a "backwards" find. This wouldn't be as annoying
-- to do if Lua didn't insist on 1-based indexing.
cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
+ suggestion_buffer = {}
update()
end
@@ -639,6 +773,7 @@ end
-- the next word. (Ctrl+Right)
function next_word()
cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
+ suggestion_buffer = {}
update()
end
@@ -659,19 +794,21 @@ local function command_list_and_help()
end
local function property_list()
- local option_info = {
- 'name', 'type', 'set-from-commandline', 'set-locally', 'default-value',
- 'min', 'max', 'choices',
- }
-
local properties = mp.get_property_native('property-list')
+ for _, sub_property in pairs({'video', 'audio', 'sub', 'sub2'}) do
+ properties[#properties + 1] = 'current-tracks/' .. sub_property
+ end
+
for _, option in ipairs(mp.get_property_native('options')) do
properties[#properties + 1] = 'options/' .. option
properties[#properties + 1] = 'file-local-options/' .. option
properties[#properties + 1] = 'option-info/' .. option
- for _, sub_property in ipairs(option_info) do
+ for _, sub_property in pairs({
+ 'name', 'type', 'set-from-commandline', 'set-locally',
+ 'default-value', 'min', 'max', 'choices',
+ }) do
properties[#properties + 1] = 'option-info/' .. option .. '/' ..
sub_property
end
@@ -789,7 +926,7 @@ function build_completers()
{ pattern = '^%s*change[-_]list%s+"?([%w_-]+)"?%s+"()%a*$', list = list_option_verb_list, append = '" ' },
{ pattern = '^%s*([av]f)%s+()%a*$', list = list_option_verb_list, append = ' ' },
{ pattern = '^%s*([av]f)%s+"()%a*$', list = list_option_verb_list, append = '" ' },
- { pattern = '${=?()[%w_/-]*$', list = property_list, append = '}' },
+ { pattern = '${[=>]?()[%w_/-]*$', list = property_list, append = '}' },
}
for _, command in pairs({'set', 'add', 'cycle', 'cycle[-_]values', 'multiply'}) do
@@ -820,32 +957,6 @@ function build_completers()
return completers
end
--- Use 'list' to find possible tab-completions for 'part.'
--- Returns a list of all potential completions and the longest
--- common prefix of all the matching list items.
-function complete_match(part, list)
- local completions = {}
- local prefix = nil
-
- for _, candidate in ipairs(list) do
- if candidate:sub(1, part:len()) == part then
- if prefix and prefix ~= candidate then
- local prefix_len = part:len()
- while prefix:sub(1, prefix_len + 1)
- == candidate:sub(1, prefix_len + 1) do
- prefix_len = prefix_len + 1
- end
- prefix = candidate:sub(1, prefix_len)
- else
- prefix = candidate
- end
- completions[#completions + 1] = candidate
- end
- end
-
- return completions, prefix
-end
-
function common_prefix_length(s1, s2)
local common_count = 0
for i = 1, #s1 do
@@ -873,8 +984,101 @@ function max_overlap_length(s1, s2)
return 0
end
+-- If str starts with the first or last characters of prefix, strip them.
+local function strip_common_characters(str, prefix)
+ return str:sub(1 + math.max(
+ common_prefix_length(prefix, str),
+ max_overlap_length(prefix, str)))
+end
+
+-- Find the longest common case-sensitive prefix of the entries in "list".
+local function find_common_prefix(list)
+ local prefix = list[1]
+
+ for i = 2, #list do
+ prefix = prefix:sub(1, common_prefix_length(prefix, list[i]))
+ end
+
+ return prefix
+end
+
+-- Return the entries of "list" beginning with "part" and the longest common
+-- prefix of the matches.
+local function complete_match(part, list)
+ local completions = {}
+
+ for _, candidate in pairs(list) do
+ if candidate:sub(1, part:len()) == part then
+ completions[#completions + 1] = candidate
+ end
+ end
+
+ local prefix = find_common_prefix(completions)
+
+ if opts.case_sensitive then
+ return completions, prefix
+ end
+
+ completions = {}
+ local lower_case_completions = {}
+ local lower_case_part = part:lower()
+
+ for _, candidate in pairs(list) do
+ if candidate:sub(1, part:len()):lower() == lower_case_part then
+ completions[#completions + 1] = candidate
+ lower_case_completions[#lower_case_completions + 1] = candidate:lower()
+ end
+ end
+
+ local lower_case_prefix = find_common_prefix(lower_case_completions)
+
+ -- Behave like GNU readline with completion-ignore-case On.
+ -- part = 'fooBA', completions = {'foobarbaz', 'fooBARqux'} =>
+ -- prefix = 'fooBARqux', lower_case_prefix = 'foobar', return 'fooBAR'
+ if prefix then
+ return completions, prefix:sub(1, lower_case_prefix:len())
+ end
+
+ -- part = 'fooba', completions = {'fooBARbaz', 'fooBarqux'} =>
+ -- prefix = nil, lower_case_prefix ='foobar', return 'fooBAR'
+ if lower_case_prefix then
+ return completions, completions[1]:sub(1, lower_case_prefix:len())
+ end
+
+ return {}, part
+end
+
+local function cycle_through_suggestions(backwards)
+ selected_suggestion_index = selected_suggestion_index + (backwards and -1 or 1)
+
+ if selected_suggestion_index > #suggestion_buffer then
+ selected_suggestion_index = 1
+ elseif selected_suggestion_index < 1 then
+ selected_suggestion_index = #suggestion_buffer
+ end
+
+ local before_cur = line:sub(1, completion_start_position - 1) ..
+ suggestion_buffer[selected_suggestion_index] .. completion_append
+ line = before_cur .. strip_common_characters(line:sub(cursor), completion_append)
+ cursor = before_cur:len() + 1
+ update()
+end
+
-- Complete the option or property at the cursor (TAB)
-function complete()
+function complete(backwards)
+ if #suggestion_buffer > 0 then
+ cycle_through_suggestions(backwards)
+ return
+ end
+
+ if input_caller then
+ completion_old_line = line
+ completion_old_cursor = cursor
+ mp.commandv('script-message-to', input_caller, 'input-event',
+ 'complete', line:sub(1, cursor - 1))
+ return
+ end
+
local before_cur = line:sub(1, cursor - 1)
local after_cur = line:sub(cursor)
@@ -882,47 +1086,56 @@ function complete()
for _, completer in ipairs(build_completers()) do
-- Completer patterns should return the start of the word to be
-- completed as the first capture.
- local s, s2 = before_cur:match(completer.pattern)
- if not s then
+ local s2
+ completion_start_position, s2 = before_cur:match(completer.pattern)
+ if not completion_start_position then
-- Multiple input commands can be separated by semicolons, so all
-- completions that are anchored at the start of the string with
-- '^' can start from a semicolon as well. Replace ^ with ; and try
-- to match again.
- s, s2 = before_cur:match(completer.pattern:gsub('^^', ';'))
+ completion_start_position, s2 =
+ before_cur:match(completer.pattern:gsub('^^', ';'))
end
- if s then
+ if completion_start_position then
local hint
if s2 then
- hint = s
- s = s2
+ hint = completion_start_position
+ completion_start_position = s2
+ end
+
+ -- Expand ~ in file completion.
+ if completer.list == file_list and hint:find('^~' .. path_separator) then
+ local home = mp.command_native({'expand-path', '~/'})
+ before_cur = before_cur:sub(1, completion_start_position - #hint - 1) ..
+ home ..
+ before_cur:sub(completion_start_position - #hint + 1)
+ hint = home .. hint:sub(2)
+ completion_start_position = completion_start_position + #home - 1
end
-- If the completer's pattern found a word, check the completer's
-- list for possible completions
- local part = before_cur:sub(s)
+ local part = before_cur:sub(completion_start_position)
local completions, prefix = complete_match(part, completer.list(hint))
if #completions > 0 then
-- If there was only one full match from the list, add
-- completer.append to the final string. This is normally a
-- space or a quotation mark followed by a space.
- local after_cur_index = 1
+ completion_append = completer.append or ''
if #completions == 1 then
- local append = completer.append or ''
- prefix = prefix .. append
-
- -- calculate offset into after_cur
- local prefix_len = common_prefix_length(append, after_cur)
- local overlap_size = max_overlap_length(append, after_cur)
- after_cur_index = math.max(prefix_len, overlap_size) + 1
+ prefix = prefix .. completion_append
+ after_cur = strip_common_characters(after_cur, completion_append)
else
table.sort(completions)
suggestion_buffer = completions
+ selected_suggestion_index = 0
end
-- Insert the completion and update
- before_cur = before_cur:sub(1, s - 1) .. prefix
+ before_cur = before_cur:sub(1, completion_start_position - 1) ..
+ prefix
cursor = before_cur:len() + 1
- line = before_cur .. after_cur:sub(after_cur_index)
+ line = before_cur .. after_cur
update()
return
end
@@ -933,12 +1146,14 @@ end
-- Move the cursor to the beginning of the line (HOME)
function go_home()
cursor = 1
+ suggestion_buffer = {}
update()
end
-- Move the cursor to the end of the line (END)
function go_end()
cursor = line:len() + 1
+ suggestion_buffer = {}
update()
end
@@ -950,7 +1165,7 @@ function del_word()
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
line = before_cur .. after_cur
cursor = before_cur:len() + 1
- update()
+ handle_edit()
end
-- Delete from the cursor to the end of the word (Ctrl+Del)
@@ -962,25 +1177,25 @@ function del_next_word()
after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
line = before_cur .. after_cur
- update()
+ handle_edit()
end
-- Delete from the cursor to the end of the line (Ctrl+K)
function del_to_eol()
line = line:sub(1, cursor - 1)
- update()
+ handle_edit()
end
-- Delete from the cursor back to the start of the line (Ctrl+U)
function del_to_start()
line = line:sub(cursor)
cursor = 1
- update()
+ handle_edit()
end
-- Empty the log buffer of all messages (Ctrl+L)
function clear_log_buffer()
- log_buffer = {}
+ log_buffers[id] = {}
update()
end
@@ -1047,7 +1262,7 @@ function paste(clip)
local after_cur = line:sub(cursor)
line = before_cur .. text .. after_cur
cursor = cursor + text:len()
- update()
+ handle_edit()
end
-- List of input bindings. This is a weird mashup between common GUI text-input
@@ -1087,6 +1302,7 @@ function get_bindings()
{ 'alt+f', next_word },
{ 'tab', complete },
{ 'ctrl+i', complete },
+ { 'shift+tab', function() complete(true) end },
{ 'ctrl+a', go_home },
{ 'home', go_home },
{ 'ctrl+e', go_end },
@@ -1151,16 +1367,105 @@ mp.add_key_binding(nil, 'enable', function()
set_active(true)
end)
+mp.register_script_message('disable', function()
+ set_active(false)
+end)
+
-- Add a script-message to show the REPL and fill it with the provided text
mp.register_script_message('type', function(text, cursor_pos)
show_and_type(text, cursor_pos)
end)
+mp.register_script_message('get-input', function (script_name, args)
+ if repl_active then
+ return
+ end
+
+ input_caller = script_name
+ args = utils.parse_json(args)
+ prompt = args.prompt or default_prompt
+ line = args.default_text or ''
+ cursor = tonumber(args.cursor_position) or line:len() + 1
+ id = args.id or script_name .. prompt
+ if histories[id] == nil then
+ histories[id] = {}
+ log_buffers[id] = {}
+ end
+ history = histories[id]
+ history_pos = #history + 1
+
+ set_active(true)
+ mp.commandv('script-message-to', input_caller, 'input-event', 'opened')
+end)
+
+mp.register_script_message('log', function (message)
+ -- input.get's edited handler is invoked after submit, so avoid modifying
+ -- the default log.
+ if input_caller == nil then
+ return
+ end
+
+ message = utils.parse_json(message)
+
+ log_add(message.text .. '\n',
+ message.error and styles.error or message.style,
+ message.error and terminal_styles.error or message.terminal_style)
+end)
+
+mp.register_script_message('set-log', function (log)
+ if input_caller == nil then
+ return
+ end
+
+ log = utils.parse_json(log)
+ log_buffers[id] = {}
+
+ for i = 1, #log do
+ if type(log[i]) == 'table' then
+ log[i].text = log[i].text .. '\n'
+ log[i].style = log[i].style or ''
+ log[i].terminal_style = log[i].terminal_style or ''
+ log_buffers[id][i] = log[i]
+ else
+ log_buffers[id][i] = {
+ text = log[i] .. '\n',
+ style = '',
+ terminal_style = '',
+ }
+ end
+ end
+
+ update()
+end)
+
+mp.register_script_message('complete', function(list, start_pos)
+ if line ~= completion_old_line or cursor ~= completion_old_cursor then
+ return
+ end
+
+ local completions, prefix = complete_match(line:sub(start_pos, cursor),
+ utils.parse_json(list))
+ local before_cur = line:sub(1, start_pos - 1) .. prefix
+ local after_cur = line:sub(cursor)
+ cursor = before_cur:len() + 1
+ line = before_cur .. after_cur
+
+ if #completions > 1 then
+ suggestion_buffer = completions
+ selected_suggestion_index = 0
+ completion_start_position = start_pos
+ completion_append = ''
+ end
+
+ update()
+end)
+
-- Redraw the REPL when the OSD size changes. This is needed because the
-- PlayRes of the OSD will need to be adjusted.
mp.observe_property('osd-width', 'native', update)
mp.observe_property('osd-height', 'native', update)
mp.observe_property('display-hidpi-scale', 'native', update)
+mp.observe_property('focused', 'native', update)
-- Enable log messages. In silent mode, mpv will queue log messages in a buffer
-- until enable_messages is called again without the silent: prefix.
@@ -1185,20 +1490,8 @@ mp.register_event('log-message', function(e)
if e.level == 'trace' then return end
-- Use color for debug/v/warn/error/fatal messages.
- local style = ''
- if e.level == 'debug' then
- style = styles.debug
- elseif e.level == 'v' then
- style = styles.verbose
- elseif e.level == 'warn' then
- style = styles.warn
- elseif e.level == 'error' then
- style = styles.error
- elseif e.level == 'fatal' then
- style = styles.fatal
- end
-
- log_add(style, '[' .. e.prefix .. '] ' .. e.text)
+ log_add('[' .. e.prefix .. '] ' .. e.text, styles[e.level],
+ terminal_styles[e.level])
end)
collectgarbage()
diff --git a/player/lua/defaults.lua b/player/lua/defaults.lua
index 233d1d6..baa3a24 100644
--- a/player/lua/defaults.lua
+++ b/player/lua/defaults.lua
@@ -809,28 +809,4 @@ function mp_utils.subprocess_detached(t)
mp.commandv("run", unpack(t.args))
end
-function mp_utils.shared_script_property_set(name, value)
- if value ~= nil then
- -- no such thing as change-list with mpv_node, so build a string value
- mp.commandv("change-list", "shared-script-properties", "append",
- name .. "=" .. value)
- else
- mp.commandv("change-list", "shared-script-properties", "remove", name)
- end
-end
-
-function mp_utils.shared_script_property_get(name)
- local map = mp.get_property_native("shared-script-properties")
- return map and map[name]
-end
-
--- cb(name, value) on change and on init
-function mp_utils.shared_script_property_observe(name, cb)
- -- it's _very_ wasteful to observe the mpv core "super" property for every
- -- shared sub-property, but then again you shouldn't use this
- mp.observe_property("shared-script-properties", "native", function(_, val)
- cb(name, val and val[name])
- end)
-end
-
return {}
diff --git a/player/lua/input.lua b/player/lua/input.lua
new file mode 100644
index 0000000..24283e4
--- /dev/null
+++ b/player/lua/input.lua
@@ -0,0 +1,69 @@
+--[[
+This file is part of mpv.
+
+mpv is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+mpv is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+]]
+
+local utils = require "mp.utils"
+local input = {}
+
+function input.get(t)
+ mp.commandv("script-message-to", "console", "get-input",
+ mp.get_script_name(), utils.format_json({
+ prompt = t.prompt,
+ default_text = t.default_text,
+ cursor_position = t.cursor_position,
+ id = t.id,
+ }))
+
+ mp.register_script_message("input-event", function (type, text, cursor_position)
+ if t[type] then
+ local suggestions, completion_start_position = t[type](text, cursor_position)
+
+ if type == "complete" and suggestions then
+ mp.commandv("script-message-to", "console", "complete",
+ utils.format_json(suggestions), completion_start_position)
+ end
+ end
+
+ if type == "closed" then
+ mp.unregister_script_message("input-event")
+ end
+ end)
+
+ return true
+end
+
+function input.terminate()
+ mp.commandv("script-message-to", "console", "disable")
+end
+
+function input.log(message, style, terminal_style)
+ mp.commandv("script-message-to", "console", "log", utils.format_json({
+ text = message,
+ style = style,
+ terminal_style = terminal_style,
+ }))
+end
+
+function input.log_error(message)
+ mp.commandv("script-message-to", "console", "log",
+ utils.format_json({ text = message, error = true }))
+end
+
+function input.set_log(log)
+ mp.commandv("script-message-to", "console", "set-log", utils.format_json(log))
+end
+
+return input
diff --git a/player/lua/meson.build b/player/lua/meson.build
index 362c87c..1d87938 100644
--- a/player/lua/meson.build
+++ b/player/lua/meson.build
@@ -1,5 +1,6 @@
lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua',
- 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua']
+ 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua',
+ 'input.lua']
foreach file: lua_files
lua_file = custom_target(file,
input: join_paths(source_root, 'player', 'lua', file),
diff --git a/player/lua/osc.lua b/player/lua/osc.lua
index 45a5d90..3ba1890 100644
--- a/player/lua/osc.lua
+++ b/player/lua/osc.lua
@@ -38,6 +38,7 @@ local user_opts = {
seekrangeseparate = true, -- whether the seekranges overlay on the bar-style seekbar
seekrangealpha = 200, -- transparency of seekranges
seekbarkeyframes = true, -- use keyframes when dragging the seekbar
+ scrollcontrols = true, -- allow scrolling when hovering certain OSC elements
title = "${media-title}", -- string compatible with property-expansion
-- to be shown as OSC title
tooltipborder = 1, -- border of tooltip in bottom/topbar
@@ -51,6 +52,7 @@ local user_opts = {
boxvideo = false, -- apply osc_param.video_margins to video
windowcontrols = "auto", -- whether to show window controls
windowcontrols_alignment = "right", -- which side to show window controls on
+ windowcontrols_title = "${media-title}", -- same as title but for windowcontrols
greenandgrumpy = false, -- disable santa hat
livemarkers = true, -- update seekbar chapter markers on duration change
chapters_osd = true, -- whether to show chapters OSD on next/prev
@@ -125,6 +127,7 @@ local state = {
input_enabled = true,
showhide_enabled = false,
windowcontrols_buttons = false,
+ windowcontrols_title = false,
dmx_cache = 0,
using_video_margins = false,
border = true,
@@ -410,10 +413,10 @@ function set_track(type, next)
mp.commandv("set", type, new_track_mpv)
- if new_track_osc == 0 then
+ if new_track_osc == 0 then
show_message(nicetypes[type] .. " Track: none")
else
- show_message(nicetypes[type] .. " Track: "
+ show_message(nicetypes[type] .. " Track: "
.. new_track_osc .. "/" .. #tracks_osc[type]
.. " [".. (tracks_osc[type][new_track_osc].lang or "unknown") .."] "
.. (tracks_osc[type][new_track_osc].title or ""))
@@ -436,7 +439,7 @@ end
function window_controls_enabled()
val = user_opts.windowcontrols
if val == "auto" then
- return not state.border
+ return not (state.border and state.title_bar)
else
return val ~= "no"
end
@@ -952,10 +955,7 @@ function show_message(text, duration)
-- may slow down massively on huge input
text = string.sub(text, 0, 4000)
- -- replace actual linebreaks with ASS linebreaks
- text = string.gsub(text, "\n", "\\N")
-
- state.message_text = text
+ state.message_text = mp.command_native({"escape-ass", text})
if not state.message_hide_timer then
state.message_hide_timer = mp.add_timeout(0, request_tick)
@@ -1158,10 +1158,9 @@ function window_controls(topbar)
-- Window Title
ne = new_element("wctitle", "button")
ne.content = function ()
- local title = mp.command_native({"expand-text", user_opts.title})
- -- escape ASS, and strip newlines and trailing slashes
- title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
- return not (title == "") and title or "mpv"
+ local title = mp.command_native({"expand-text", user_opts.windowcontrols_title})
+ title = title:gsub("\n", " ")
+ return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv"
end
local left_pad = 5
local right_pad = 10
@@ -1789,9 +1788,8 @@ function osc_init()
ne.content = function ()
local title = state.forced_title or
mp.command_native({"expand-text", user_opts.title})
- -- escape ASS, and strip newlines and trailing slashes
- title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{","\\{")
- return not (title == "") and title or "mpv"
+ title = title:gsub("\n", " ")
+ return title ~= "" and mp.command_native({"escape-ass", title}) or "mpv"
end
ne.eventresponder["mbtn_left_up"] = function ()
@@ -1937,10 +1935,13 @@ function osc_init()
function () set_track("audio", -1) end
ne.eventresponder["shift+mbtn_left_down"] =
function () show_message(get_tracklist("audio"), 2) end
- ne.eventresponder["wheel_down_press"] =
- function () set_track("audio", 1) end
- ne.eventresponder["wheel_up_press"] =
- function () set_track("audio", -1) end
+
+ if user_opts.scrollcontrols then
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("audio", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("audio", -1) end
+ end
--cy_sub
ne = new_element("cy_sub", "button")
@@ -1960,10 +1961,13 @@ function osc_init()
function () set_track("sub", -1) end
ne.eventresponder["shift+mbtn_left_down"] =
function () show_message(get_tracklist("sub"), 2) end
- ne.eventresponder["wheel_down_press"] =
- function () set_track("sub", 1) end
- ne.eventresponder["wheel_up_press"] =
- function () set_track("sub", -1) end
+
+ if user_opts.scrollcontrols then
+ ne.eventresponder["wheel_down_press"] =
+ function () set_track("sub", 1) end
+ ne.eventresponder["wheel_up_press"] =
+ function () set_track("sub", -1) end
+ end
--tog_fs
ne = new_element("tog_fs", "button")
@@ -2053,10 +2057,13 @@ function osc_init()
"absolute-percent", "exact") end
ne.eventresponder["reset"] =
function (element) element.state.lastseek = nil end
- ne.eventresponder["wheel_up_press"] =
- function () mp.commandv("osd-auto", "seek", 10) end
- ne.eventresponder["wheel_down_press"] =
- function () mp.commandv("osd-auto", "seek", -10) end
+
+ if user_opts.scrollcontrols then
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "seek", 10) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "seek", -10) end
+ end
-- tc_left (current pos)
@@ -2140,10 +2147,12 @@ function osc_init()
ne.eventresponder["mbtn_left_up"] =
function () mp.commandv("cycle", "mute") end
- ne.eventresponder["wheel_up_press"] =
- function () mp.commandv("osd-auto", "add", "volume", 5) end
- ne.eventresponder["wheel_down_press"] =
- function () mp.commandv("osd-auto", "add", "volume", -5) end
+ if user_opts.scrollcontrols then
+ ne.eventresponder["wheel_up_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", 5) end
+ ne.eventresponder["wheel_down_press"] =
+ function () mp.commandv("osd-auto", "add", "volume", -5) end
+ end
-- load layout
@@ -2439,6 +2448,18 @@ function render()
if osc_param.areas["window-controls-title"] then
for _,cords in ipairs(osc_param.areas["window-controls-title"]) do
+ if state.osc_visible then -- activate only when OSC is actually visible
+ set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "window-controls-title")
+ end
+ if state.osc_visible ~= state.windowcontrols_title then
+ if state.osc_visible then
+ mp.enable_key_bindings("window-controls-title", "allow-vo-dragging")
+ else
+ mp.disable_key_bindings("window-controls-title", "allow-vo-dragging")
+ end
+ state.windowcontrols_title = state.osc_visible
+ end
+
if mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2) then
mouse_over_osc = true
end
@@ -2709,7 +2730,7 @@ function update_duration_watch()
if want_watch ~= duration_watched then
if want_watch then
- mp.observe_property("duration", nil, on_duration)
+ mp.observe_property("duration", "native", on_duration)
else
mp.unobserve_property(on_duration)
end
@@ -2722,8 +2743,8 @@ update_duration_watch()
mp.register_event("shutdown", shutdown)
mp.register_event("start-file", request_init)
-mp.observe_property("track-list", nil, request_init)
-mp.observe_property("playlist", nil, request_init)
+mp.observe_property("track-list", "native", request_init)
+mp.observe_property("playlist", "native", request_init)
mp.observe_property("chapter-list", "native", function(_, list)
list = list or {} -- safety, shouldn't return nil
table.sort(list, function(a, b) return a.time < b.time end)
@@ -2760,6 +2781,12 @@ mp.observe_property("border", "bool",
request_init_resize()
end
)
+mp.observe_property("title-bar", "bool",
+ function(name, val)
+ state.title_bar = val
+ request_init_resize()
+ end
+)
mp.observe_property("window-maximized", "bool",
function(name, val)
state.maximized = val
@@ -2915,3 +2942,4 @@ mp.register_script_message("osc-idlescreen", idlescreen_visibility)
set_virt_mouse_area(0, 0, 0, 0, "input")
set_virt_mouse_area(0, 0, 0, 0, "window-controls")
+set_virt_mouse_area(0, 0, 0, 0, "window-controls-title")
diff --git a/player/lua/stats.lua b/player/lua/stats.lua
index 16e8b68..3d093c7 100644
--- a/player/lua/stats.lua
+++ b/player/lua/stats.lua
@@ -30,6 +30,8 @@ local o = {
print_perfdata_passes = false, -- when true, print the full information about all passes
filter_params_max_length = 100, -- a filter list longer than this many characters will be shown one filter per line instead
show_frame_info = false, -- whether to show the current frame info
+ term_width_limit = -1, -- overwrites the terminal width
+ term_height_limit = -1, -- overwrites the terminal height
debug = false,
-- Graph options and style
@@ -83,6 +85,15 @@ local o = {
}
options.read_options(o)
+o.term_width_limit = tonumber(o.term_width_limit) or -1
+o.term_height_limit = tonumber(o.term_height_limit) or -1
+if o.term_width_limit < 0 then
+ o.term_width_limit = nil
+end
+if o.term_height_limit < 0 then
+ o.term_height_limit = nil
+end
+
local format = string.format
local max = math.max
local min = math.min
@@ -118,9 +129,6 @@ local function graph_add_value(graph, value)
graph.max = max(graph.max, value)
end
--- "\\<U+2060>" in UTF-8 (U+2060 is WORD-JOINER)
-local ESC_BACKSLASH = "\\" .. string.char(0xE2, 0x81, 0xA0)
-
local function no_ASS(t)
if not o.use_ass then
return t
@@ -128,16 +136,7 @@ local function no_ASS(t)
-- mp.osd_message supports ass-escape using osd-ass-cc/{0|1}
return ass_stop .. t .. ass_start
else
- -- mp.set_osd_ass doesn't support ass-escape. roll our own.
- -- similar to mpv's sub/osd_libass.c:mangle_ass(...), excluding
- -- space after newlines because no_ASS is not used with multi-line.
- -- space at the beginning is replaced with "\\h" because it matters
- -- at the beginning of a line, and we can't know where our output
- -- ends up. no issue if it ends up at the middle of a line.
- return tostring(t)
- :gsub("\\", ESC_BACKSLASH)
- :gsub("{", "\\{")
- :gsub("^ ", "\\h")
+ return mp.command_native({"escape-ass", tostring(t)})
end
end
@@ -222,7 +221,7 @@ local function generate_graph(values, i, len, v_max, v_avg, scale, x_tics)
local bg_box = format("{\\bord0.5}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0",
o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max)
- return format("%s{\\r}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s",
+ return format("%s{\\r}{\\rDefault}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s",
o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style())
end
@@ -277,7 +276,13 @@ local function sorted_keys(t, comp_fn)
return keys
end
-local function append_perfdata(s, dedicated_page, print_passes)
+local function scroll_hint()
+ local hint = format("(hint: scroll with %s/%s)", o.key_scroll_up, o.key_scroll_down)
+ if not o.use_ass then return " " .. hint end
+ return format(" {\\fs%s}%s{\\fs%s}", o.font_size * 0.66, hint, o.font_size)
+end
+
+local function append_perfdata(header, s, dedicated_page, print_passes)
local vo_p = mp.get_property_native("vo-passes")
if not vo_p then
return
@@ -318,11 +323,12 @@ local function append_perfdata(s, dedicated_page, print_passes)
-- ensure that the fixed title is one element and every scrollable line is
-- also one single element.
- s[#s+1] = format("%s%s%s%s{\\fs%s}%s%s{\\fs%s}",
+ local h = dedicated_page and header or s
+ h[#h+1] = format("%s%s%s%s{\\fs%s}%s{\\fs%s}%s",
dedicated_page and "" or o.nl, dedicated_page and "" or o.indent,
b("Frame Timings:"), o.prefix_sep, o.font_size * 0.66,
- "(last/average/peak μs)",
- dedicated_page and " (hint: scroll with ↑↓)" or "", o.font_size)
+ "(last/average/peak μs)", o.font_size,
+ dedicated_page and scroll_hint() or "")
for _,frame in ipairs(sorted_keys(vo_p)) do -- ensure fixed display order
local data = vo_p[frame]
@@ -363,11 +369,6 @@ local function append_perfdata(s, dedicated_page, print_passes)
end
end
-local function ellipsis(s, maxlen)
- if not maxlen or s:len() <= maxlen then return s end
- return s:sub(1, maxlen - 3) .. "..."
-end
-
-- command prefix tokens to strip - includes generic property commands
local cmd_prefixes = {
osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1,
@@ -419,7 +420,7 @@ local function keyname_cells(k)
return klen
end
-local function get_kbinfo_lines(width)
+local function get_kbinfo_lines()
-- active keys: only highest priority of each key, and not our (stats) keys
local bindings = mp.get_property_native("input-bindings", {})
local active = {} -- map: key-name -> bind-info
@@ -482,8 +483,6 @@ local function get_kbinfo_lines(width)
or format("{\\q2\\fn%s}%s {\\fn%s}{\\fs%d\\u1}",
o.font_mono, kspaces, o.font, 1.3*o.font_size)
local spost = term and "" or format("{\\u0\\fs%d}", o.font_size)
- local _, itabs = o.indent:gsub("\t", "")
- local cutoff = term and (width or 79) - o.indent:len() - itabs * 7 - spre:len()
-- create the display lines
local info_lines = {}
@@ -497,38 +496,25 @@ local function get_kbinfo_lines(width)
if bind.comment then
bind.cmd = bind.cmd .. " # " .. bind.comment
end
- append(info_lines, ellipsis(bind.cmd, cutoff),
- { prefix = kpre .. no_ASS(align_right(bind.key)) .. kpost })
+ append(info_lines, bind.cmd, { prefix = kpre .. no_ASS(align_right(bind.key)) .. kpost })
end
return info_lines
end
-local function append_general_perfdata(s, offset)
- local perf_info = mp.get_property_native("perf-info") or {}
- local count = 0
- for _, data in ipairs(perf_info) do
- count = count + 1
- end
- offset = max(1, min((offset or 1), count))
-
- local i = 0
- for _, data in ipairs(perf_info) do
- i = i + 1
- if i >= offset then
- append(s, data.text or data.value, {prefix="["..tostring(i).."] "..data.name..":"})
-
- if o.plot_perfdata and o.use_ass and data.value then
- buf = perf_buffers[data.name]
- if not buf then
- buf = {0, pos = 1, len = 50, max = 0}
- perf_buffers[data.name] = buf
- end
- graph_add_value(buf, data.value)
- s[#s+1] = generate_graph(buf, buf.pos, buf.len, buf.max, nil, 0.8, 1)
+local function append_general_perfdata(s)
+ for i, data in ipairs(mp.get_property_native("perf-info") or {}) do
+ append(s, data.text or data.value, {prefix="["..tostring(i).."] "..data.name..":"})
+
+ if o.plot_perfdata and o.use_ass and data.value then
+ buf = perf_buffers[data.name]
+ if not buf then
+ buf = {0, pos = 1, len = 50, max = 0}
+ perf_buffers[data.name] = buf
end
+ graph_add_value(buf, data.value)
+ s[#s] = s[#s] .. generate_graph(buf, buf.pos, buf.len, buf.max, nil, 0.8, 1)
end
end
- return offset
end
local function append_display_sync(s)
@@ -806,6 +792,7 @@ local function append_img_params(s, r, ro)
end
local indent = o.prefix_sep .. o.prefix_sep
+ r = ro or r
local pixel_format = r["hw-pixelformat"] or r["pixelformat"]
append(s, pixel_format, {prefix="Format:"})
@@ -828,7 +815,7 @@ local function append_fps(s, prop, eprop)
local unit = prop == "display-fps" and " Hz" or " fps"
local suffix = single and "" or " (specified)"
local esuffix = single and "" or " (estimated)"
- local prefix = prop == "display-fps" and "Refresh Rate:" or "Frame rate:"
+ local prefix = prop == "display-fps" and "Refresh Rate:" or "Frame Rate:"
local nl = o.nl
local indent = o.indent
@@ -855,6 +842,8 @@ local function add_video_out(s)
append(s, vo, {prefix_sep="", nl="", indent=""})
append_property(s, "display-names", {prefix_sep="", prefix="(", suffix=")",
no_prefix_markup=true, nl="", indent=" "})
+ append(s, mp.get_property_native("current-gpu-context"),
+ {prefix="Context:", nl="", indent=o.prefix_sep .. o.prefix_sep})
append_property(s, "avsync", {prefix="A-V:"})
append_fps(s, "display-fps", "estimated-display-fps")
if append_property(s, "decoder-frame-drop-count",
@@ -862,9 +851,9 @@ local function add_video_out(s)
append_property(s, "frame-drop-count", {suffix=" (output)", nl="", indent=""})
end
append_display_sync(s)
- append_perfdata(s, false, o.print_perfdata_passes)
+ append_perfdata(nil, s, false, o.print_perfdata_passes)
- if mp.get_property_native("deinterlace") then
+ if mp.get_property_native("deinterlace-active") then
append_property(s, "deinterlace", {prefix="Deinterlacing:"})
end
@@ -902,12 +891,11 @@ local function add_video(s)
return
end
- local osd_dims = mp.get_property_native("osd-dimensions")
- local scaled_width = osd_dims["w"] - osd_dims["ml"] - osd_dims["mr"]
- local scaled_height = osd_dims["h"] - osd_dims["mt"] - osd_dims["mb"]
-
append(s, "", {prefix=o.nl .. o.nl .. "Video:", nl="", indent=""})
- if append_property(s, "video-codec", {prefix_sep="", nl="", indent=""}) then
+ local track = mp.get_property_native("current-tracks/video")
+ if track and append(s, track["codec-desc"], {prefix_sep="", nl="", indent=""}) then
+ append(s, track["codec-profile"], {prefix="[", nl="", indent=" ", prefix_sep="",
+ no_prefix_markup=true, suffix="]"})
append_property(s, "hwdec-current", {prefix="HW:", nl="",
indent=o.prefix_sep .. o.prefix_sep,
no_prefix_markup=false, suffix=""}, {no=true, [""]=true})
@@ -947,19 +935,39 @@ end
local function add_audio(s)
local r = mp.get_property_native("audio-params")
-- in case of e.g. lavfi-complex there can be no input audio, only output
- if not r then
- r = mp.get_property_native("audio-out-params")
- end
+ local ro = mp.get_property_native("audio-out-params") or r
+ r = r or ro
if not r then
return
end
+ local merge = function(r, ro, prop)
+ local a = r[prop] or ro[prop]
+ local b = ro[prop] or r[prop]
+ return (a == b or a == nil) and a or (a .. " ➜ " .. b)
+ end
+
append(s, "", {prefix=o.nl .. o.nl .. "Audio:", nl="", indent=""})
- append_property(s, "audio-codec", {prefix_sep="", nl="", indent=""})
- local cc = append(s, r["channel-count"], {prefix="Channels:"})
- append(s, r["format"], {prefix="Format:", nl=cc and "" or o.nl,
+ local track = mp.get_property_native("current-tracks/audio")
+ if track then
+ append(s, track["codec-desc"], {prefix_sep="", nl="", indent=""})
+ append(s, track["codec-profile"], {prefix="[", nl="", indent=" ", prefix_sep="",
+ no_prefix_markup=true, suffix="]"})
+ end
+ append_property(s, "current-ao", {prefix="AO:", nl="",
+ indent=o.prefix_sep .. o.prefix_sep})
+ local dev = append_property(s, "audio-device", {prefix="Device:"})
+ local ao_mute = mp.get_property_native("ao-mute") and " (Muted)" or ""
+ append_property(s, "ao-volume", {prefix="AO Volume:", suffix="%" .. ao_mute,
+ nl=dev and "" or o.nl,
+ indent=dev and o.prefix_sep .. o.prefix_sep})
+ if math.abs(mp.get_property_native("audio-delay")) > 1e-6 then
+ append_property(s, "audio-delay", {prefix="A-V delay:"})
+ end
+ local cc = append(s, merge(r, ro, "channel-count"), {prefix="Channels:"})
+ append(s, merge(r, ro, "format"), {prefix="Format:", nl=cc and "" or o.nl,
indent=cc and o.prefix_sep .. o.prefix_sep})
- append(s, r["samplerate"], {prefix="Sample Rate:", suffix=" Hz"})
+ append(s, merge(r, ro, "samplerate"), {prefix="Sample Rate:", suffix=" Hz"})
append_property(s, "packet-audio-bitrate", {prefix="Bitrate:", suffix=" kbps"})
append_filters(s, "af", "Filters:")
end
@@ -987,6 +995,91 @@ local function eval_ass_formatting()
end
end
+-- assumptions:
+-- s is composed of SGR escape sequences and/or printable UTF8 sequences
+-- printable codepoints occupy one terminal cell (we don't have wcwidth)
+-- tabstops are 8, 16, 24..., and the output starts at 0 or a tabstop
+-- note: if maxwidth <= 2 and s doesn't fit: the result is "..." (more than 2)
+function term_ellipsis(s, maxwidth)
+ local TAB, ESC, SGR_END = 9, 27, ("m"):byte()
+ local width, ellipsis = 0, "..."
+ local fit_len, in_sgr
+
+ for i = 1, #s do
+ local x = s:byte(i)
+
+ if in_sgr then
+ in_sgr = x ~= SGR_END
+ elseif x == ESC then
+ in_sgr = true
+ ellipsis = "\27[0m..." -- ensure SGR reset
+ elseif x < 128 or x >= 192 then -- non UTF8-continuation
+ -- tab adds till the next stop, else add 1
+ width = width + (x == TAB and 8 - width % 8 or 1)
+
+ if fit_len == nil and width > maxwidth - 3 then
+ fit_len = i - 1 -- adding "..." still fits maxwidth
+ end
+ if width > maxwidth then
+ return s:sub(1, fit_len) .. ellipsis
+ end
+ end
+ end
+
+ return s
+end
+
+local function term_ellipsis_array(arr, from, to, max_width)
+ for i = from, to do
+ arr[i] = term_ellipsis(arr[i], max_width)
+ end
+ return arr
+end
+
+-- split str into a table
+-- example: local t = split(s, "\n")
+-- plain: whether pat is a plain string (default false - pat is a pattern)
+local function split(str, pat, plain)
+ local init = 1
+ local r, i, find, sub = {}, 1, string.find, string.sub
+ repeat
+ local f0, f1 = find(str, pat, init, plain)
+ r[i], i = sub(str, init, f0 and f0 - 1), i+1
+ init = f0 and f1 + 1
+ until f0 == nil
+ return r
+end
+
+-- Composes the output with header and scrollable content
+-- Returns string of the finished page and the actually chosen offset
+--
+-- header : table of the header where each entry is one line
+-- content : table of the content where each entry is one line
+-- apply_scroll: scroll the content
+local function finalize_page(header, content, apply_scroll)
+ local term_size = mp.get_property_native("term-size", {})
+ local term_width = o.term_width_limit or term_size.w or 80
+ local term_height = o.term_height_limit or term_size.h or 24
+ local from, to = 1, #content
+ if apply_scroll and term_height > 0 then
+ -- Up to 40 lines for libass because it can put a big performance toll on
+ -- libass to process many lines which end up outside (below) the screen.
+ -- In the terminal reduce height by 2 for the status line (can be more then one line)
+ local max_content_lines = (o.use_ass and 40 or term_height - 2) - #header
+ -- in the terminal the scrolling should stop once the last line is visible
+ local max_offset = o.use_ass and #content or #content - max_content_lines + 1
+ from = max(1, min((pages[curr_page].offset or 1), max_offset))
+ to = min(#content, from + max_content_lines - 1)
+ pages[curr_page].offset = from
+ end
+ local output = table.concat(header) .. table.concat(content, "", from, to)
+ if not o.use_ass and term_width > 0 then
+ local t = split(output, "\n", true)
+ -- limit width for the terminal
+ output = table.concat(term_ellipsis_array(t, 1, #t, term_width), "\n")
+ end
+ return output, from
+end
-- Returns an ASS string with "normal" stats
local function default_stats()
@@ -997,70 +1090,44 @@ local function default_stats()
add_video_out(stats)
add_video(stats)
add_audio(stats)
- return table.concat(stats)
-end
-
-local function scroll_vo_stats(stats, fixed_items, offset)
- local ret = {}
- local count = #stats - fixed_items
- offset = max(1, min((offset or 1), count))
-
- for i, line in pairs(stats) do
- if i <= fixed_items or i >= fixed_items + offset then
- ret[#ret+1] = stats[i]
- end
- end
- return ret, offset
+ return finalize_page({}, stats, false)
end
-- Returns an ASS string with extended VO stats
local function vo_stats()
- local stats = {}
+ local header, content = {}, {}
eval_ass_formatting()
- add_header(stats)
-
- -- first line (title) added next is considered fixed
- local fixed_items = #stats + 1
- append_perfdata(stats, true, true)
-
- local page = pages[o.key_page_2]
- stats, page.offset = scroll_vo_stats(stats, fixed_items, page.offset)
- return table.concat(stats)
+ add_header(header)
+ append_perfdata(header, content, true, true)
+ header = {table.concat(header)}
+ return finalize_page(header, content, true)
end
local kbinfo_lines = nil
-local function keybinding_info(after_scroll)
+local function keybinding_info(after_scroll, bindlist)
local header = {}
local page = pages[o.key_page_4]
eval_ass_formatting()
add_header(header)
- append(header, "", {prefix=format("%s: {\\fs%s}%s{\\fs%s}", page.desc,
- o.font_size * 0.66, "(hint: scroll with ↑↓)", o.font_size), nl="",
- indent=""})
+ append(header, "", {prefix=format("%s:%s", page.desc, scroll_hint()), nl="", indent=""})
+ header = {table.concat(header)}
if not kbinfo_lines or not after_scroll then
- kbinfo_lines = get_kbinfo_lines()
+ kbinfo_lines = get_kbinfo_lines(o.term_width_limit)
end
- -- up to 20 lines for the terminal - so that mpv can also print
- -- the status line without scrolling, and up to 40 lines for libass
- -- because it can put a big performance toll on libass to process
- -- many lines which end up outside (below) the screen.
- local term = not o.use_ass
- local nlines = #kbinfo_lines
- page.offset = max(1, min((page.offset or 1), term and nlines - 20 or nlines))
- local maxline = min(nlines, page.offset + (term and 20 or 40))
- return table.concat(header) ..
- table.concat(kbinfo_lines, "", page.offset, maxline)
+
+ return finalize_page(header, kbinfo_lines, not bindlist)
end
local function perf_stats()
- local stats = {}
+ local header, content = {}, {}
eval_ass_formatting()
- add_header(stats)
+ add_header(header)
local page = pages[o.key_page_0]
- append(stats, "", {prefix=page.desc .. ":", nl="", indent=""})
- page.offset = append_general_perfdata(stats, page.offset)
- return table.concat(stats)
+ append(header, "", {prefix=format("%s:%s", page.desc, scroll_hint()), nl="", indent=""})
+ append_general_perfdata(content)
+ header = {table.concat(header)}
+ return finalize_page(header, content, true)
end
local function opt_time(t)
@@ -1076,18 +1143,18 @@ local function cache_stats()
eval_ass_formatting()
add_header(stats)
- append(stats, "", {prefix="Cache info:", nl="", indent=""})
+ append(stats, "", {prefix="Cache Info:", nl="", indent=""})
local info = mp.get_property_native("demuxer-cache-state")
if info == nil then
append(stats, "Unavailable.", {})
- return table.concat(stats)
+ return finalize_page({}, stats, false)
end
local a = info["reader-pts"]
local b = info["cache-end"]
- append(stats, opt_time(a) .. " - " .. opt_time(b), {prefix = "Packet queue:"})
+ append(stats, opt_time(a) .. " - " .. opt_time(b), {prefix = "Packet Queue:"})
local r = nil
if a ~= nil and b ~= nil then
@@ -1101,7 +1168,7 @@ local function cache_stats()
nil, 0.8, 1)
r_graph = o.prefix_sep .. r_graph
end
- append(stats, opt_time(r), {prefix = "Read-ahead:", suffix = r_graph})
+ append(stats, opt_time(r), {prefix = "Readahead:", suffix = r_graph})
-- These states are not necessarily exclusive. They're about potentially
-- separate mechanisms, whose states may be decoupled.
@@ -1140,17 +1207,17 @@ local function cache_stats()
else
fc = "(disabled)"
end
- append(stats, fc, {prefix = "Disk cache:"})
+ append(stats, fc, {prefix = "Disk Cache:"})
- append(stats, info["debug-low-level-seeks"], {prefix = "Media seeks:"})
- append(stats, info["debug-byte-level-seeks"], {prefix = "Stream seeks:"})
+ append(stats, info["debug-low-level-seeks"], {prefix = "Media Seeks:"})
+ append(stats, info["debug-byte-level-seeks"], {prefix = "Stream Seeks:"})
append(stats, "", {prefix=o.nl .. o.nl .. "Ranges:", nl="", indent=""})
append(stats, info["bof-cached"] and "yes" or "no",
- {prefix = "Start cached:"})
+ {prefix = "Start Cached:"})
append(stats, info["eof-cached"] and "yes" or "no",
- {prefix = "End cached:"})
+ {prefix = "End Cached:"})
local ranges = info["seekable-ranges"] or {}
for n, r in ipairs(ranges) do
@@ -1159,7 +1226,7 @@ local function cache_stats()
{prefix = format("Range %s:", n)})
end
- return table.concat(stats)
+ return finalize_page({}, stats, false)
end
-- Record 1 sample of cache statistics.
@@ -1188,8 +1255,8 @@ pages = {
[o.key_page_1] = { f = default_stats, desc = "Default" },
[o.key_page_2] = { f = vo_stats, desc = "Extended Frame Timings", scroll = true },
[o.key_page_3] = { f = cache_stats, desc = "Cache Statistics" },
- [o.key_page_4] = { f = keybinding_info, desc = "Active key bindings", scroll = true },
- [o.key_page_0] = { f = perf_stats, desc = "Internal performance info", scroll = true },
+ [o.key_page_4] = { f = keybinding_info, desc = "Active Key Bindings", scroll = true },
+ [o.key_page_0] = { f = perf_stats, desc = "Internal Performance Info", scroll = true },
}
@@ -1409,9 +1476,8 @@ if o.bindlist ~= "no" then
mp.add_timeout(0, function() -- wait for all other scripts to finish init
o.ass_formatting = false
o.no_ass_indent = " "
- eval_ass_formatting()
- io.write(pages[o.key_page_4].desc .. ":" ..
- table.concat(get_kbinfo_lines(width)) .. "\n")
+ o.term_size = { w = width , h = 0}
+ io.write(keybinding_info(false, true) .. "\n")
mp.command("quit")
end)
end
diff --git a/player/main.c b/player/main.c
index 27cf9b4..48d29b5 100644
--- a/player/main.c
+++ b/player/main.c
@@ -71,7 +71,7 @@ static const char def_config[] =
;
#if HAVE_COCOA
-#include "osdep/macosx_events.h"
+#include "osdep/mac/app_bridge.h"
#endif
#ifndef FULLCONFIG
@@ -184,16 +184,17 @@ void mp_destroy(struct MPContext *mpctx)
cocoa_set_input_context(NULL);
#endif
- if (cas_terminal_owner(mpctx, mpctx)) {
- terminal_uninit();
- cas_terminal_owner(mpctx, NULL);
- }
-
mp_input_uninit(mpctx->input);
uninit_libav(mpctx->global);
mp_msg_uninit(mpctx->global);
+
+ if (cas_terminal_owner(mpctx, mpctx)) {
+ terminal_uninit();
+ cas_terminal_owner(mpctx, NULL);
+ }
+
assert(!mpctx->num_abort_list);
talloc_free(mpctx->abort_list);
mp_mutex_destroy(&mpctx->abort_lock);
@@ -389,7 +390,7 @@ int mp_initialize(struct MPContext *mpctx, char **options)
MP_STATS(mpctx, "start init");
#if HAVE_COCOA
- mpv_handle *ctx = mp_new_client(mpctx->clients, "osx");
+ mpv_handle *ctx = mp_new_client(mpctx->clients, "mac");
cocoa_set_mpv_handle(ctx);
#endif
@@ -419,7 +420,6 @@ int mp_initialize(struct MPContext *mpctx, char **options)
int mpv_main(int argc, char *argv[])
{
- mp_thread_set_name("mpv");
struct MPContext *mpctx = mp_create();
if (!mpctx)
return 1;
diff --git a/player/meson.build b/player/meson.build
index dc334b8..be1e812 100644
--- a/player/meson.build
+++ b/player/meson.build
@@ -1,10 +1,11 @@
subdir('javascript')
subdir('lua')
-# Meson doesn't allow having multiple build targets with the same name in the same file.
-# Just generate the com in here for windows builds.
-if win32 and get_option('cplayer')
+# Older versions of meson don't allow multiple build targets with the same name in the same
+# file. Generate it here for compatibility reasons for windows.
+if win32 and get_option('cplayer') and meson.version().version_compare('< 1.3.0')
wrapper_sources= '../osdep/win32-console-wrapper.c'
executable('mpv', wrapper_sources, c_args: '-municode', link_args: '-municode',
name_suffix: 'com', install: true)
+ warning('mpv.com executable will be generated in the player subdirectory.')
endif
diff --git a/player/misc.c b/player/misc.c
index b91d52a..1b265e1 100644
--- a/player/misc.c
+++ b/player/misc.c
@@ -147,7 +147,12 @@ double get_track_seek_offset(struct MPContext *mpctx, struct track *track)
if (track->type == STREAM_AUDIO)
return -opts->audio_delay;
if (track->type == STREAM_SUB)
- return -opts->subs_rend->sub_delay;
+ {
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ if (mpctx->current_track[n][STREAM_SUB] == track)
+ return -opts->subs_shared->sub_delay[n];
+ }
+ }
}
return 0;
}
@@ -247,7 +252,8 @@ void error_on_track(struct MPContext *mpctx, struct track *track)
if (track->type == STREAM_VIDEO)
MP_INFO(mpctx, "Video: no video\n");
if (mpctx->opts->stop_playback_on_init_failure ||
- !(mpctx->vo_chain || mpctx->ao_chain))
+ (!mpctx->current_track[0][STREAM_AUDIO] &&
+ !mpctx->current_track[0][STREAM_VIDEO]))
{
if (!mpctx->stop_play)
mpctx->stop_play = PT_ERROR;
@@ -317,7 +323,7 @@ void merge_playlist_files(struct playlist *pl)
edl = talloc_strdup_append_buffer(edl, e->filename);
}
playlist_clear(pl);
- playlist_add_file(pl, edl);
+ playlist_append_file(pl, edl);
talloc_free(edl);
}
diff --git a/player/osd.c b/player/osd.c
index dc03229..96ab287 100644
--- a/player/osd.c
+++ b/player/osd.c
@@ -112,7 +112,7 @@ static void term_osd_update_title(struct MPContext *mpctx)
void term_osd_set_subs(struct MPContext *mpctx, const char *text)
{
- if (mpctx->video_out || !text || !mpctx->opts->subs_rend->sub_visibility)
+ if (mpctx->video_out || !text || !mpctx->opts->subs_shared->sub_visibility[0])
text = ""; // disable
if (strcmp(mpctx->term_osd_subs ? mpctx->term_osd_subs : "", text) == 0)
return;
@@ -190,10 +190,9 @@ static char *get_term_status_msg(struct MPContext *mpctx)
saddf(&line, ": ");
// Playback position
- double speed = opts->term_remaining_playtime ? mpctx->video_speed : 1;
sadd_hhmmssff(&line, get_playback_time(mpctx), opts->osd_fractions);
saddf(&line, " / ");
- sadd_hhmmssff(&line, get_time_length(mpctx) / speed, opts->osd_fractions);
+ sadd_hhmmssff(&line, get_time_length(mpctx), opts->osd_fractions);
sadd_percentage(&line, get_percent_pos(mpctx));
diff --git a/player/playloop.c b/player/playloop.c
index 60596da..12239d6 100644
--- a/player/playloop.c
+++ b/player/playloop.c
@@ -419,6 +419,7 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
update_ab_loop_clip(mpctx);
mpctx->current_seek = seek;
+ redraw_subs(mpctx);
}
// This combines consecutive seek requests.
@@ -665,6 +666,9 @@ static void handle_osd_redraw(struct MPContext *mpctx)
if (!want_redraw)
return;
vo_redraw(mpctx->video_out);
+ // Even though we just redrew, it may need to be done again for certain
+ // cases of subtitles on an image.
+ redraw_subs(mpctx);
}
static void clear_underruns(struct MPContext *mpctx)
@@ -799,6 +803,22 @@ int get_cache_buffering_percentage(struct MPContext *mpctx)
return mpctx->demuxer ? mpctx->cache_buffer : -1;
}
+static void handle_update_subtitles(struct MPContext *mpctx)
+{
+ if (mpctx->video_status == STATUS_EOF) {
+ update_subtitles(mpctx, mpctx->playback_pts);
+ return;
+ }
+
+ for (int n = 0; n < mpctx->num_tracks; n++) {
+ struct track *track = mpctx->tracks[n];
+ if (track->type == STREAM_SUB && !track->demuxer_ready) {
+ update_subtitles(mpctx, mpctx->playback_pts);
+ break;
+ }
+ }
+}
+
static void handle_cursor_autohide(struct MPContext *mpctx)
{
struct MPOpts *opts = mpctx->opts;
@@ -1030,8 +1050,12 @@ int handle_force_window(struct MPContext *mpctx, bool force)
break;
}
}
+
+ // Use a 16:9 aspect ratio so that fullscreen on a 16:9 screen will not
+ // have vertical margins, which can lead to a different size or position
+ // of subtitles than with 16:9 videos.
int w = 960;
- int h = 480;
+ int h = 540;
struct mp_image_params p = {
.imgfmt = config_format,
.w = w, .h = h,
@@ -1132,6 +1156,7 @@ static void handle_playback_restart(struct MPContext *mpctx)
mpctx->hrseek_active = false;
mpctx->restart_complete = true;
mpctx->current_seek = (struct seek_params){0};
+ run_command_opts(mpctx);
handle_playback_time(mpctx);
mp_notify(mpctx, MPV_EVENT_PLAYBACK_RESTART, NULL);
update_core_idle_state(mpctx);
@@ -1224,8 +1249,8 @@ void run_playloop(struct MPContext *mpctx)
handle_dummy_ticks(mpctx);
update_osd_msg(mpctx);
- if (mpctx->video_status == STATUS_EOF)
- update_subtitles(mpctx, mpctx->playback_pts);
+
+ handle_update_subtitles(mpctx);
handle_each_frame_screenshot(mpctx);
diff --git a/player/screenshot.c b/player/screenshot.c
index e4d0912..aa637e6 100644
--- a/player/screenshot.c
+++ b/player/screenshot.c
@@ -77,7 +77,8 @@ static char *stripext(void *talloc_ctx, const char *s)
}
static bool write_screenshot(struct mp_cmd_ctx *cmd, struct mp_image *img,
- const char *filename, struct image_writer_opts *opts)
+ const char *filename, struct image_writer_opts *opts,
+ bool overwrite)
{
struct MPContext *mpctx = cmd->mpctx;
struct image_writer_opts *gopts = mpctx->opts->screenshot_image_opts;
@@ -88,7 +89,7 @@ static bool write_screenshot(struct mp_cmd_ctx *cmd, struct mp_image *img,
mp_core_unlock(mpctx);
bool ok = img && write_image(img, &opts_copy, filename, mpctx->global,
- mpctx->screenshot_ctx->log);
+ mpctx->screenshot_ctx->log, overwrite);
mp_core_lock(mpctx);
@@ -166,7 +167,7 @@ static char *create_fname(struct MPContext *mpctx, char *template,
goto error_exit;
char fmtstr[] = {'%', '0', digits, 'd', '\0'};
res = talloc_asprintf_append(res, fmtstr, *frameno);
- if (*frameno < 100000 - 1) {
+ if (*frameno < INT_MAX - 1) {
(*frameno) += 1;
(*sequence) += 1;
}
@@ -496,7 +497,7 @@ void cmd_screenshot_to_file(void *p)
cmd->success = false;
return;
}
- cmd->success = write_screenshot(cmd, image, filename, &opts);
+ cmd->success = write_screenshot(cmd, image, filename, &opts, true);
talloc_free(image);
}
@@ -537,7 +538,7 @@ void cmd_screenshot(void *p)
if (image) {
char *filename = gen_fname(cmd, image_writer_file_ext(opts));
if (filename) {
- cmd->success = write_screenshot(cmd, image, filename, NULL);
+ cmd->success = write_screenshot(cmd, image, filename, NULL, false);
if (cmd->success) {
node_init(res, MPV_FORMAT_NODE_MAP, NULL);
node_map_add_string(res, "filename", filename);
diff --git a/player/sub.c b/player/sub.c
index f3e42fe..65e5732 100644
--- a/player/sub.c
+++ b/player/sub.c
@@ -54,6 +54,19 @@ static void reset_subtitles(struct MPContext *mpctx, struct track *track)
term_osd_set_subs(mpctx, NULL);
}
+// Only matters for subs on an image.
+void redraw_subs(struct MPContext *mpctx)
+{
+ for (int n = 0; n < num_ptracks[STREAM_SUB]; n++) {
+ if (mpctx->current_track[n][STREAM_SUB] &&
+ mpctx->current_track[n][STREAM_SUB]->d_sub)
+ {
+ mpctx->redraw_subs = true;
+ break;
+ }
+ }
+}
+
void reset_subtitle_state(struct MPContext *mpctx)
{
for (int n = 0; n < mpctx->num_tracks; n++)
@@ -100,33 +113,43 @@ static bool update_subtitle(struct MPContext *mpctx, double video_pts,
sub_preload(dec_sub);
}
- if (!sub_read_packets(dec_sub, video_pts, mpctx->paused))
- return false;
+ bool packets_read = false;
+ bool sub_updated = false;
+ sub_read_packets(dec_sub, video_pts, mpctx->paused, &packets_read, &sub_updated);
- // Handle displaying subtitles on terminal; never done for secondary subs
- if (mpctx->current_track[0][STREAM_SUB] == track && !mpctx->video_out) {
- char *text = sub_get_text(dec_sub, video_pts, SD_TEXT_TYPE_PLAIN);
- term_osd_set_subs(mpctx, text);
- talloc_free(text);
- }
+ double osd_pts = osd_get_force_video_pts(mpctx->osd);
+
+ // Check if we need to update subtitles for these special cases. Always
+ // update on discontinuities like seeking or a new file.
+ if (sub_updated || mpctx->redraw_subs || osd_pts == MP_NOPTS_VALUE) {
+ // Always force a redecode of all packets if we have a refresh.
+ if (mpctx->redraw_subs)
+ sub_redecode_cached_packets(dec_sub);
+
+ // Handle displaying subtitles on terminal; never done for secondary subs
+ if (mpctx->current_track[0][STREAM_SUB] == track && !mpctx->video_out) {
+ char *text = sub_get_text(dec_sub, video_pts, SD_TEXT_TYPE_PLAIN);
+ term_osd_set_subs(mpctx, text);
+ talloc_free(text);
+ }
- // Handle displaying subtitles on VO with no video being played. This is
- // quite different, because normally subtitles are redrawn on new video
- // frames, using the video frames' timestamps.
- if (mpctx->video_out && mpctx->video_status == STATUS_EOF &&
- (mpctx->opts->subs_rend->sub_past_video_end ||
- !mpctx->current_track[0][STREAM_VIDEO] ||
- mpctx->current_track[0][STREAM_VIDEO]->image)) {
- if (osd_get_force_video_pts(mpctx->osd) != video_pts) {
- osd_set_force_video_pts(mpctx->osd, video_pts);
- osd_query_and_reset_want_redraw(mpctx->osd);
- vo_redraw(mpctx->video_out);
- // Force an arbitrary minimum FPS
- mp_set_timeout(mpctx, 0.1);
+ // Handle displaying subtitles on VO with no video being played. This is
+ // quite different, because normally subtitles are redrawn on new video
+ // frames, using the video frames' timestamps.
+ if (mpctx->video_out && mpctx->video_status == STATUS_EOF &&
+ (mpctx->opts->subs_rend->sub_past_video_end ||
+ !mpctx->current_track[0][STREAM_VIDEO] ||
+ mpctx->current_track[0][STREAM_VIDEO]->image)) {
+ if (osd_pts != video_pts) {
+ osd_set_force_video_pts(mpctx->osd, video_pts);
+ osd_query_and_reset_want_redraw(mpctx->osd);
+ vo_redraw(mpctx->video_out);
+ }
}
}
- return true;
+ mpctx->redraw_subs = false;
+ return packets_read;
}
// Return true if the subtitles for the given PTS are ready; false if the player
@@ -199,12 +222,20 @@ void reinit_sub(struct MPContext *mpctx, struct track *track)
sub_select(track->d_sub, true);
int order = get_order(mpctx, track);
osd_set_sub(mpctx->osd, order, track->d_sub);
- sub_control(track->d_sub, SD_CTRL_SET_TOP, &order);
// When paused we have to wait for packets to be available.
- // So just retry until we get a packet in this case.
- if (mpctx->playback_initialized)
- while (!update_subtitles(mpctx, mpctx->playback_pts) && mpctx->paused);
+ // Retry on a timeout until we get a packet. If still not successful,
+ // then queue it for later in the playloop (but this will have a delay).
+ if (mpctx->playback_initialized) {
+ track->demuxer_ready = false;
+ int64_t end = mp_time_ns() + MP_TIME_MS_TO_NS(50);
+ while (!track->demuxer_ready && mp_time_ns() < end)
+ track->demuxer_ready = update_subtitles(mpctx, mpctx->playback_pts) ||
+ !mpctx->paused;
+ if (!track->demuxer_ready)
+ mp_wakeup_core(mpctx);
+
+ }
}
void reinit_sub_all(struct MPContext *mpctx)
diff --git a/player/video.c b/player/video.c
index 48a3165..f0372b6 100644
--- a/player/video.c
+++ b/player/video.c
@@ -120,6 +120,7 @@ void reset_video_state(struct MPContext *mpctx)
mpctx->drop_message_shown = 0;
mpctx->display_sync_drift_dir = 0;
mpctx->display_sync_error = 0;
+ mpctx->display_sync_active = 0;
mpctx->video_status = mpctx->vo_chain ? STATUS_SYNCING : STATUS_EOF;
}
@@ -129,9 +130,9 @@ void uninit_video_out(struct MPContext *mpctx)
uninit_video_chain(mpctx);
if (mpctx->video_out) {
vo_destroy(mpctx->video_out);
+ mpctx->video_out = NULL;
mp_notify(mpctx, MPV_EVENT_VIDEO_RECONFIG, NULL);
}
- mpctx->video_out = NULL;
}
static void vo_chain_uninit(struct vo_chain *vo_c)
@@ -343,10 +344,9 @@ static void adjust_sync(struct MPContext *mpctx, double v_pts, double frame_time
{
struct MPOpts *opts = mpctx->opts;
- if (mpctx->audio_status == STATUS_EOF)
+ if (mpctx->audio_status != STATUS_PLAYING)
return;
- mpctx->delay -= frame_time;
double a_pts = written_audio_pts(mpctx) + opts->audio_delay - mpctx->delay;
double av_delay = a_pts - v_pts;
@@ -388,7 +388,9 @@ static void handle_new_frame(struct MPContext *mpctx)
}
}
mpctx->time_frame += frame_time / mpctx->video_speed;
- if (frame_time)
+ if (mpctx->ao_chain && mpctx->ao_chain->audio_started)
+ mpctx->delay -= frame_time;
+ if (mpctx->video_status >= STATUS_PLAYING)
adjust_sync(mpctx, pts, frame_time);
MP_TRACE(mpctx, "frametime=%5.3f\n", frame_time);
}
@@ -644,8 +646,9 @@ static void update_av_diff(struct MPContext *mpctx, double offset)
if (mpctx->vo_chain && mpctx->vo_chain->is_sparse)
return;
- double a_pos = playing_audio_pts(mpctx);
+ double a_pos = written_audio_pts(mpctx);
if (a_pos != MP_NOPTS_VALUE && mpctx->video_pts != MP_NOPTS_VALUE) {
+ a_pos -= mpctx->audio_speed * ao_get_delay(mpctx->ao);
mpctx->last_av_difference = a_pos - mpctx->video_pts
+ opts->audio_delay + offset;
}
@@ -1041,19 +1044,6 @@ static void apply_video_crop(struct MPContext *mpctx, struct vo *vo)
}
}
-static bool video_reconfig_needed(const struct mp_image_params *p1,
- const struct mp_image_params *p2)
-{
- return p1->imgfmt != p2->imgfmt ||
- p1->hw_subfmt != p2->hw_subfmt ||
- p1->w != p2->w || p1->h != p2->h ||
- p1->p_w != p2->p_w || p1->p_h != p2->p_h ||
- p1->force_window != p2->force_window ||
- p1->rotate != p2->rotate ||
- p1->stereo3d != p2->stereo3d ||
- !mp_rect_equals(&p1->crop, &p2->crop);
-}
-
void write_video(struct MPContext *mpctx)
{
struct MPOpts *opts = mpctx->opts;
@@ -1176,10 +1166,12 @@ void write_video(struct MPContext *mpctx)
// Filter output is different from VO input?
struct mp_image_params *p = &mpctx->next_frames[0]->params;
- if (!vo->params || video_reconfig_needed(p, vo->params)) {
+ if (!vo->params || !mp_image_params_static_equal(p, vo->params)) {
// Changing config deletes the current frame; wait until it's finished.
- if (vo_still_displaying(vo))
+ if (vo_still_displaying(vo)) {
+ vo_request_wakeup_on_done(vo);
return;
+ }
const struct vo_driver *info = mpctx->video_out->driver;
char extra[20] = {0};
@@ -1257,7 +1249,7 @@ void write_video(struct MPContext *mpctx)
diff /= mpctx->video_speed;
if (mpctx->time_frame < 0)
diff += mpctx->time_frame;
- frame->duration = MPCLAMP(diff, 0, 10) * 1e9;
+ frame->duration = MP_TIME_S_TO_NS(MPCLAMP(diff, 0, 10));
}
mpctx->video_pts = mpctx->next_frames[0]->pts;