Skip to content

Commit

Permalink
command: allow frame-step to go through multiple frames and/or seek
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Dudemanguy committed Jan 28, 2025
1 parent 57962e9 commit 9661a38
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 40 deletions.
1 change: 1 addition & 0 deletions DOCS/interface-changes/frame-step.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add optional `frames` and `flags` arguments to `frame-step` command controlling the direction and amount of frames mpv steps through
40 changes: 31 additions & 9 deletions DOCS/man/input.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 [<frames>] [<flags>]``
Go forward or backwards by a given amount of frames. If ``<frames>`` 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 ``<frames>`` 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.

Expand Down
43 changes: 23 additions & 20 deletions player/command.c
Original file line number Diff line number Diff line change
Expand Up @@ -5679,41 +5679,31 @@ 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);
} else {
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;
Expand Down Expand Up @@ -6990,9 +6980,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,
Expand Down
4 changes: 2 additions & 2 deletions player/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ enum seek_type {
MPSEEK_RELATIVE,
MPSEEK_ABSOLUTE,
MPSEEK_FACTOR,
MPSEEK_BACKSTEP,
MPSEEK_FRAMESTEP,
MPSEEK_CHAPTER,
};

Expand Down Expand Up @@ -601,7 +601,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);
Expand Down
29 changes: 20 additions & 9 deletions player/playloop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 9661a38

Please sign in to comment.