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..e9437362261a7 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -356,18 +356,40 @@ 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. If ```` is ``-1``, this will go exactly to the + previous frame. + + 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. + might make precise seeking slower. Also if the video is VFR, framestepping + using seeks will probably not work correctly except for the ``-1`` case. + + This does not work with audio-only playback. + +``frame-back-step`` + Calls ``frame-step`` with a value of ``-1`` and the ``seek`` flag. This does not work with audio-only playback. diff --git a/player/command.c b/player/command.c index db89ca4a90ca6..d78090adecd15 100644 --- a/player/command.c +++ b/player/command.c @@ -5684,13 +5684,16 @@ static void cmd_frame_step(void *p) { struct mp_cmd_ctx *cmd = p; struct MPContext *mpctx = cmd->mpctx; + bool backstep = *(bool *)cmd->priv; + int frames = backstep ? -1 : cmd->args[0].v.i; + int flags = backstep ? 1 : 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,27 +5701,14 @@ 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); -} - static void cmd_quit(void *p) { struct mp_cmd_ctx *cmd = p; @@ -6995,9 +6985,22 @@ 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-back-step", cmd_frame_back_step, .allow_auto_repeat = 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, + .priv = &(const bool){false}, + }, + { "frame-back-step", cmd_frame_step, + .priv = &(const int){true}, + .allow_auto_repeat = true, + }, { "playlist-next", cmd_playlist_next_prev, { {"flags", OPT_CHOICE(v.i, 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..ff2c00a7ca183 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, 0); set_pause_state(mpctx, true); } } @@ -258,6 +258,16 @@ 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. Use current_time if step_frames is -1. + int previous_frame = mpctx->num_past_frames - 1; + int offset = step_frames == -1 ? 0 : step_frames; + double pts = mpctx->past_frames[previous_frame].approx_duration * offset; + return current_time + pts; +} + static void mp_seek(MPContext *mpctx, struct seek_params seek) { struct MPOpts *opts = mpctx->opts; @@ -285,8 +295,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 +400,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 +456,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,