From 38b53ebc42da61adc3c37af39e96a551e04a1876 Mon Sep 17 00:00:00 2001 From: Dudemanguy Date: Fri, 24 Jan 2025 13:56:07 -0600 Subject: [PATCH] command: allow frame-step to go through multiple frames and/or seek Previously, the default behavior of frame-step and frame-back-step is to play forward 1 frame or seek back 1 frame. We keep this behavior but introduce additional flags to control the exact behavior of the frame stepping. The first argument simply specifies how many frames to go through. The second argument specifies whether to play video to step through frames or to seek to step through frames. Playing through the video to step through frames only works going forwards (otherwise it will always seek). In theory we could use backwards playback for this, but that can be decided later. As a minor note, MPSEEK_BACKSTEP is renamed to MPSEEK_FRAMESTEP since forward seeks can use this as well. Fixes #10128. --- DOCS/interface-changes/frame-step.txt | 1 + DOCS/man/input.rst | 36 ++++++++++++++++++++------ player/command.c | 37 +++++++++++++++++---------- player/core.h | 4 +-- player/playloop.c | 28 +++++++++++++------- 5 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 DOCS/interface-changes/frame-step.txt diff --git a/DOCS/interface-changes/frame-step.txt b/DOCS/interface-changes/frame-step.txt new file mode 100644 index 0000000000000..c57b30eb94c0b --- /dev/null +++ b/DOCS/interface-changes/frame-step.txt @@ -0,0 +1 @@ +- add optional `frames` and `flags` arguments to `frame-step` command controlling the direction and amount of frames mpv steps through diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 267c4585d0081..5eb3de2fb31e6 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -356,21 +356,41 @@ Playback Control events that have already been displayed, or are within a short prefetch range. See `Cache`_ for details on how to control the available prefetch range. -``frame-step`` - Play one frame, then pause. Does nothing with audio-only playback. -``frame-back-step`` - Go back by one frame, then pause. Note that this can be very slow (it tries - to be precise, not fast), and sometimes fails to behave as expected. How - well this works depends on whether precise seeking works correctly (e.g. - see the ``--hr-seek-demuxer-offset`` option). Video filters or other video +``frame-step [] []`` + Go forward or backwards by a given amount of frames. If ```` is + omitted, the value is assumed to be ``1``. + + The second argument consists of flags controlling the frameskip mode: + + play (default) + Play the video forward by the desired amount of frames and then pause. + This only works with a positive value (i.e. frame stepping forwards). + seek + Perform a very exact seek that attempts to seek by the desired amount + of frames. + + Note that the default frameskip mode, play, is more accurate but can be + slow depending on how many frames you are skipping (i.e. skipping forward + 100 frames will play 100 frames of video before stopping). This mode only + works when going forwards. Frame stepping back always performs a seek. + + When using seek mode, this can still be very slow (it tries to be precise, + not fast), and sometimes fails to behave as expected. How well this works + depends on whether precise seeking works correctly (e.g. see the + ``--hr-seek-demuxer-offset`` option). Video filters or other video post-processing that modifies timing of frames (e.g. deinterlacing) should - usually work, but might make backstepping silently behave incorrectly in + usually work, but might make framestepping silently behave incorrectly in corner cases. Using ``--hr-seek-framedrop=no`` should help, although it might make precise seeking slower. This does not work with audio-only playback. +``frame-back-step`` + Calls ``frame-step`` with a value of ``-1``. + + This does not work with audio-only playback. + ``stop []`` Stop playback and clear playlist. With default settings, this is essentially like ``quit``. Useful for the client API: playback can be diff --git a/player/command.c b/player/command.c index db89ca4a90ca6..3f8f4c027003c 100644 --- a/player/command.c +++ b/player/command.c @@ -5684,13 +5684,15 @@ static void cmd_frame_step(void *p) { struct mp_cmd_ctx *cmd = p; struct MPContext *mpctx = cmd->mpctx; + int frames = cmd->args[0].v.i; + int flags = cmd->args[1].v.i; - if (!mpctx->playback_initialized) { + if (!mpctx->playback_initialized || frames == 0) { cmd->success = false; return; } - if (cmd->cmd->is_up_down) { + if (frames > 0 && !flags && cmd->cmd->is_up_down) { if (cmd->cmd->is_up) { if (mpctx->step_frames < 1) set_pause_state(mpctx, true); @@ -5698,25 +5700,23 @@ static void cmd_frame_step(void *p) if (cmd->cmd->repeated) { set_pause_state(mpctx, false); } else { - add_step_frame(mpctx, 1); + add_step_frame(mpctx, frames, flags); } } } else { - add_step_frame(mpctx, 1); + add_step_frame(mpctx, frames, flags); } } static void cmd_frame_back_step(void *p) { struct mp_cmd_ctx *cmd = p; - struct MPContext *mpctx = cmd->mpctx; - - if (!mpctx->playback_initialized) { - cmd->success = false; - return; - } - - add_step_frame(mpctx, -1); + struct mp_cmd_arg frames = {.v.i = -1}; + struct mp_cmd_arg flags = {.v.i = 0}; + cmd->args = talloc_realloc(cmd, cmd->args, struct mp_cmd_arg, 2); + cmd->args[0] = frames; + cmd->args[1] = flags; + cmd_frame_step(cmd); } static void cmd_quit(void *p) @@ -6995,8 +6995,17 @@ const struct mp_cmd_def mp_cmds[] = { { "stop", cmd_stop, { {"flags", OPT_FLAGS(v.i, {"keep-playlist", 1}), .flags = MP_CMD_OPT_ARG} } }, - { "frame-step", cmd_frame_step, .allow_auto_repeat = true, - .on_updown = true }, + { "frame-step", cmd_frame_step, + { + {"frames", OPT_INT(v.i), OPTDEF_INT(1)}, + {"flags", OPT_CHOICE(v.i, + {"play", 0}, + {"seek", 1}), + .flags = MP_CMD_OPT_ARG}, + }, + .allow_auto_repeat = true, + .on_updown = true + }, { "frame-back-step", cmd_frame_back_step, .allow_auto_repeat = true }, { "playlist-next", cmd_playlist_next_prev, { diff --git a/player/core.h b/player/core.h index 8f1bbfe99ffdc..f2434434601ad 100644 --- a/player/core.h +++ b/player/core.h @@ -69,7 +69,7 @@ enum seek_type { MPSEEK_RELATIVE, MPSEEK_ABSOLUTE, MPSEEK_FACTOR, - MPSEEK_BACKSTEP, + MPSEEK_FRAMESTEP, MPSEEK_CHAPTER, }; @@ -599,7 +599,7 @@ void reset_playback_state(struct MPContext *mpctx); void set_pause_state(struct MPContext *mpctx, bool user_pause); void update_internal_pause_state(struct MPContext *mpctx); void update_core_idle_state(struct MPContext *mpctx); -void add_step_frame(struct MPContext *mpctx, int dir); +void add_step_frame(struct MPContext *mpctx, int dir, bool use_seek); void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount, enum seek_precision exact, int flags); double get_time_length(struct MPContext *mpctx); diff --git a/player/playloop.c b/player/playloop.c index 8cfa44f1ac981..9281887a7ff37 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -203,16 +203,16 @@ void update_screensaver_state(struct MPContext *mpctx) : VOCTRL_KILL_SCREENSAVER, NULL); } -void add_step_frame(struct MPContext *mpctx, int dir) +void add_step_frame(struct MPContext *mpctx, int dir, bool use_seek) { if (!mpctx->vo_chain) return; - if (dir > 0) { - mpctx->step_frames += 1; + if (dir > 0 && !use_seek) { + mpctx->step_frames += dir; set_pause_state(mpctx, false); - } else if (dir < 0) { + } else { if (!mpctx->hrseek_active) { - queue_seek(mpctx, MPSEEK_BACKSTEP, 0, MPSEEK_VERY_EXACT, 0); + queue_seek(mpctx, MPSEEK_FRAMESTEP, dir, MPSEEK_VERY_EXACT, MPSEEK_FLAG_DELAY); set_pause_state(mpctx, true); } } @@ -258,6 +258,15 @@ void reset_playback_state(struct MPContext *mpctx) update_core_idle_state(mpctx); } +static double calculate_framestep_pts(MPContext *mpctx, double current_time, + int step_frames) +{ + // Crude guess at the pts. + int previous_frame = mpctx->num_past_frames - 1; + double pts = mpctx->past_frames[previous_frame].approx_duration * step_frames; + return current_time + pts; +} + static void mp_seek(MPContext *mpctx, struct seek_params seek) { struct MPOpts *opts = mpctx->opts; @@ -285,8 +294,9 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek) case MPSEEK_ABSOLUTE: seek_pts = seek.amount; break; - case MPSEEK_BACKSTEP: - seek_pts = current_time; + case MPSEEK_FRAMESTEP: + seek_pts = calculate_framestep_pts(mpctx, current_time, + (int)seek.amount); hr_seek_very_exact = true; break; case MPSEEK_RELATIVE: @@ -389,7 +399,7 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek) if (hr_seek) { mpctx->hrseek_active = true; - mpctx->hrseek_backstep = seek.type == MPSEEK_BACKSTEP; + mpctx->hrseek_backstep = seek.type == MPSEEK_FRAMESTEP && seek.amount == -1; mpctx->hrseek_pts = seek_pts * mpctx->play_dir; // allow decoder to drop frames before hrseek_pts @@ -445,7 +455,7 @@ void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount, return; case MPSEEK_ABSOLUTE: case MPSEEK_FACTOR: - case MPSEEK_BACKSTEP: + case MPSEEK_FRAMESTEP: case MPSEEK_CHAPTER: *seek = (struct seek_params) { .type = type,