diff options
Diffstat (limited to 'player')
-rw-r--r-- | player/audio.c | 7 | ||||
-rw-r--r-- | player/client.c | 2 | ||||
-rw-r--r-- | player/command.c | 680 | ||||
-rw-r--r-- | player/command.h | 1 | ||||
-rw-r--r-- | player/configfiles.c | 7 | ||||
-rw-r--r-- | player/core.h | 9 | ||||
-rw-r--r-- | player/external_files.c | 8 | ||||
-rw-r--r-- | player/external_files.h | 1 | ||||
-rw-r--r-- | player/javascript/defaults.js | 81 | ||||
-rw-r--r-- | player/loadfile.c | 78 | ||||
-rw-r--r-- | player/lua.c | 13 | ||||
-rw-r--r-- | player/lua/auto_profiles.lua | 28 | ||||
-rw-r--r-- | player/lua/console.lua | 561 | ||||
-rw-r--r-- | player/lua/defaults.lua | 24 | ||||
-rw-r--r-- | player/lua/input.lua | 69 | ||||
-rw-r--r-- | player/lua/meson.build | 3 | ||||
-rw-r--r-- | player/lua/osc.lua | 94 | ||||
-rw-r--r-- | player/lua/stats.lua | 316 | ||||
-rw-r--r-- | player/main.c | 16 | ||||
-rw-r--r-- | player/meson.build | 7 | ||||
-rw-r--r-- | player/misc.c | 12 | ||||
-rw-r--r-- | player/osd.c | 5 | ||||
-rw-r--r-- | player/playloop.c | 31 | ||||
-rw-r--r-- | player/screenshot.c | 11 | ||||
-rw-r--r-- | player/sub.c | 83 | ||||
-rw-r--r-- | player/video.c | 34 |
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; |