Implement backwards playback

See manpage additions. This is a huge hack. You can bet there are shit
tons of bugs. It's literally forcing square pegs into round holes.
Hopefully, the manpage wall of text makes it clear enough that the whole
shit can easily crash and burn. (Although it shouldn't literally crash.
That would be a bug. It possibly _could_ start a fire by entering some
sort of endless loop, not a literal one, just something where it tries
to do work without making progress.)

(Some obvious bugs I simply ignored for this initial version, but
there's a number of potential bugs I can't even imagine. Normal playback
should remain completely unaffected, though.)

How this works is also described in the manpage. Basically, we demux in
reverse, then we decode in reverse, then we render in reverse.

The decoding part is the simplest: just reorder the decoder output. This
weirdly integrates with the timeline/ordered chapter code, which also
has special requirements on feeding the packets to the decoder in a
non-straightforward way (it doesn't conflict, although a bugmessmass
breaks correct slicing of segments, so EDL/ordered chapter playback is
broken in backward direction).

Backward demuxing is pretty involved. In theory, it could be much
easier: simply iterating the usual demuxer output backward. But this
just doesn't fit into our code, so there's a cthulhu nightmare of shit.
To be specific, each stream (audio, video) is reversed separately. At
least this means we can do backward playback within cached content (for
example, you could play backwards in a live stream; on that note, it
disables prefetching, which would lead to losing new live video, but
this could be avoided).

The fuckmess also meant that I didn't bother trying to support
subtitles. Subtitles are a problem because they're "sparse" streams.
They need to be "passively" demuxed: you don't try to read a subtitle
packet, you demux audio and video, and then look whether there was a
subtitle packet. This means to get subtitles for a time range, you need
to know that you demuxed video and audio over this range, which becomes
pretty messy when you demux audio and video backwards separately.

Backward display is the most weird (and potentially buggy) part. To
avoid that we need to touch a LOT of timing code, we negate all
timestamps. The basic idea is that due to the navigation, all
comparisons and subtractions of timestamps keep working, and you don't
need to touch every single of them to "reverse" them.

E.g.:

    bool before = pts_a < pts_b;

would need to be:

    bool before = forward
        ? pts_a < pts_b
        : pts_a > pts_b;

or:

    bool before = pts_a * dir < pts_b * dir;

or if you, as it's implemented now, just do this after decoding:

    pts_a *= dir;
    pts_b *= dir;

and then in the normal timing/renderer code:

    bool before = pts_a < pts_b;

Consequently, we don't need many changes in the latter code. But some
assumptions inhererently true for forward playback may have been broken
anyway. What is mainly needed is fixing places where values are passed
between positive and negative "domains". For example, seeking and
timestamp user display always uses positive timestamps. The main mess is
that it's not obvious which domain a given variable should or does use.

Well, in my tests with a single file, it suddenly started to work when I
did this. I'm honestly surprised that it did, and that I didn't have to
change a single line in the timing code past decoder (just something
minor to make external/cached text subtitles display). I committed it
immediately while avoiding thinking about it. But there really likely
are subtle problems of all sorts.

As far as I'm aware, gstreamer also supports backward playback. When I
looked at this years ago, I couldn't find a way to actually try this,
and I didn't revisit it now. Back then I also read talk slides from the
person who implemented it, and I'm not sure if and which ideas I might
have taken from it. It's possible that the timestamp reversal is
inspired by it, but I didn't check. (I think it claimed that it could
avoid large changes by changing a sign?)

VapourSynth has some sort of reverse function, which provides a backward
view on a video. The function itself is trivial to implement, as
VapourSynth aims to provide random access to video by frame numbers (so
you just request decreasing frame numbers). From what I remember, it
wasn't exactly fluid, but it worked. It's implemented by creating an
index, and seeking to the target on demand, and a bunch of caching. mpv
could use it, but it would either require using VapourSynth as demuxer
and decoder for everything, or replacing the current file every time
something is supposed to be played backwards.

FFmpeg's libavfilter has reversal filters for audio and video. These
require buffering the entire media data of the file, and don't really
fit into mpv's architecture. It could be used by playing a libavfilter
graph that also demuxes, but that's like VapourSynth but worse.
This commit is contained in:
wm4 2019-05-18 02:10:51 +02:00
parent fc4e59f25d
commit b9d351f02a
22 changed files with 874 additions and 43 deletions

View File

@ -366,6 +366,197 @@ Playback Control
of them fails. This doesn't affect playback of audio-only or video-only
files.
``--play-direction=<forward|backward>``
Control the playback direction (default: forward). Setting ``backward``
will attempt to play the file in reverse direction, with decreasing
playback time. If this is set on playback starts, playback will start from
the end of the file. If this is changed at during playback, a hr-seek will
be issued to change the direction.
The rest of this option description pertains to the ``backward`` mode.
.. note::
Backward playback is extremely fragile. It may not always work, is much
slower than forward playback, and breaks certain other features. How
well it works depends mainly on the file being played. Generally, it
will show good results (or results at all) only if the stars align.
mpv, as well as most media formats, were designed for forward playback
only. Backward playback is bolted on top of mpv, and tries to make a medium
effort to make backward playback work. Depending on your use-case, another
tool may work much better.
Backward playback is not exactly a 1st class feature. Implementation
tradeoffs were made, that are bad for backward playback, but in turn do not
cause disadvantages for normal playback. Various possible optimizations are
not implemented in order to keep the complexity down. Normally, a media
player is highly pipelined (future data is prepared in separate threads, so
it is available in realtime when the next stage needs it), but backward
playback will essentially stall the pipeline at various random points.
For example, for intra-only codecs are trivially backward playable, and
tools built around them may make efficient use of them (consider video
editors or camera viewers). mpv won't be efficient in this case, because it
uses its generic backward playback algorithm, that on top of it is not very
optimized.
If you just want to quickly go backward through the video and just show
"keyframes", just use forward playback, and hold down the left cursor key
(which on CLI with default config sends many small relative seek commands).
The implementation consists of mostly 3 parts:
- Backward demuxing. This relies on the demuxer cache, so the demuxer cache
should (or must, didn't test it) be enabled, and its size will affect
performance. If the cache is too small or too large, quadratic runtime
behavior may result.
- Backward decoding. The decoder library used (libavcodec) does not support
this. It is emulated by feeding bits of data in forward, putting the
result in a queue, returning the queue data to the VO in reverse, and
then starting over at an earlier position. This can require buffering an
extreme amount of decoded data, and also completely breaks pipelining.
- Backward output. This is relatively simple, because the decoder returns
the frames in the needed order. However, this may cause various problems
because very basic assumptions are broken (such as time going forward).
Also, some filtering becomes impossible. Deinterlacing filters will not
work.
Known problems:
- It's fragile. If anything doesn't work, random non-useful behavior may
occur. In simple cases, the player will just play nonsense and artifacts.
In other cases, it may get stuck or heat the CPU. (Exceeding memory usage
significantly beyond the user-set limits would be a bug, though.)
- Performance and resource usage isn't good. In part this is inherent to
backward playback of normal media formats, and in parts due to
implementation choices and tradeoffs.
- This is extremely reliant on good demuxer behavior. Although backward
demuxing requires no special demuxer support, it is required that the
demuxer performs seeks reliably, fulfills some specific requirements
about packet metadata, and has deterministic behavior.
- Starting playback exactly from the end may or may not work, depending on
seeking behavior and file duration detection.
- Some container formats, audio, and video codecs are not supported due to
their behavior. There is no list, and the player usually does not detect
them. Certain live streams (including TV captures) may exhibit problems
in particular, as well as some lossy audio codecs. h264 intra-refresh is
known not to work due to problems with libavcodec.
- Function with EDL/mkv ordered chapters is obviously broken.
- Backward demuxing of subtitles is not supported. Subtitle display still
works for some external text subtitle formats. (These are fully read into
memory, and only backward display is needed.) Text subtitles that are
cached in the subtitle renderer also have a chance to be displayed
correctly.
- Some features dealing with playback of broken or hard to deal with files
will be disabled (such as timestamp correction).
- If demuxer low level seeks (i.e. seeking the actual demuxer instead of
just within the demuxer cache) are performed by backward playback, the
created seek ranges may not join, because not enough overlap is achieved.
- Trying to use this with hardware video decoding will probably exhaust all
your GPU memory and then crash a thing or two.
- Stream recording and encoding are broken.
- Relative seeks may behave weird. Small seeks backward (towards smaller
time, i.e. ``seek -1``) may not really seek properly, and audio will
remain muted for a while. Using hr-seek is recommended, which should have
none of these problems.
- Some things are just weird. For example, while seek commands manipulate
playback time in the expected way (provided they work correctly), the
framestep commands are transposed. Backstepping will perform very
expensive work to step forward by 1 frame.
Tuning:
- Remove all ``--vf``/``--af`` filters you have set. Disable deinterlacing.
Disable idiotic nonsense like SPDIF passthrough.
- Increasing ``--video-reversal-buffer`` might help if reversal queue
overflow is reported, which may happen in high bitrate video, or video
with large GOP.
- The demuxer cache is essential for backward demuxing. If it's too small,
a queue overflow will be logged, and backward playback cannot continue,
or it performs too many low level seeks. If it's too large, implementation
tradeoffs may cause general performance issues. Use ``--demuxer-max-bytes``
to potentially increase the amount of packets the demuxer layer can queue
for reverse demuxing (basically it's the ``--video-reversal-buffer``
equivalent for the demuxer layer).
- ``--demuxer-backward-playback-step`` also factors into how many seeks may
be performed, and whether backward demuxing could break due to queue
overflow.
- Setting ``--demuxer-cache-wait`` may be useful to cache the entire file
into the demuxer cache. Set ``--demuxer-max-bytes`` to a large size to
make sure it can read the entire cache; ``--demuxer-max-back-bytes``
should also be set to a large size to prevent that tries to trim the
cache.
- If audio artifacts are audible, even though the AO does not underrun,
increasing ``--audio-reversal-buffer`` might help in some cases.
``--video-reversal-buffer=<bytesize>``, ``--audio-reversal-buffer=<bytesize>``
For backward decoding. Backward decoding decodes forward in steps, and then
reverses the decoder output. These options control the approximate maximum
amount of bytes that can be buffered. The main use of this is to avoid
unbounded resource usage; during normal backward playback, it's not supposed
to hit the limit, and if it does, it will drop frames and complain about it.
This does not work correctly if video hardware decoding is used. The video
frame size will not include the referenced GPU and driver memory.
How large the queue size needs to be depends entirely on the way the media
was encoded. Audio typically requires a very small buffer, while video can
require excessively large buffers.
(Technically, this allows the last frame to exceed the limit. Also, this
does not account for other buffered frames, such as inside the decoder or
the video output.)
This does not affect demuxer cache behavior at all.
See ``--list-options`` for defaults and value range. ``<bytesize>`` options
accept suffixes such as ``KiB`` and ``MiB``.
``--video-backward-overlap=<auto|number>``, ``--audio-backward-overlap=<auto|number>``
Number of overlapping packets to use for backward decoding (default: auto).
Backward decoding works by forward decoding in small steps. Some codecs
cannot restart decoding from any packet (even if it's marked as seek point),
which becomes noticeable with backward decoding (in theory this is a problem
with seeking too, but ``--hr-seek-demuxer-offset`` can fix it for seeking).
In particular, MDCT based audio codecs are affected.
The solution is to feed a previous packet to the decoder each time, and then
discard the output. This option controls how many packets to feed. The
``auto`` choice is currently hardcoded to 1 for audio, and 0 for video.
``--video-backward-overlap`` was intended to handle intra-refresh video, but
which does not work since libavcodec silently drops frames even with
``--vd-lavc-show-all``, and it's too messy to accurately guess which frames
have been dropped.
``--demuxer-backward-playback-step=<seconds>``
Number of seconds the demuxer should seek back to get new packets during
backward playback (default: 60). This is useful for tuning backward
playback, see ``--play-direction`` for details.
Setting this to a very low value or 0 may make the player think seeking is
broken, or may make it perform multiple seeks.
Program Behavior
----------------

View File

@ -520,6 +520,56 @@ bool mp_aframe_set_silence(struct mp_aframe *f, int offset, int samples)
return true;
}
bool mp_aframe_reverse(struct mp_aframe *f)
{
int format = mp_aframe_get_format(f);
size_t bps = af_fmt_to_bytes(format);
if (!af_fmt_is_pcm(format) || bps > 16)
return false;
uint8_t **d = mp_aframe_get_data_rw(f);
if (!d)
return false;
int planes = mp_aframe_get_planes(f);
int samples = mp_aframe_get_size(f);
int channels = mp_aframe_get_channels(f);
size_t sstride = mp_aframe_get_sstride(f);
int plane_samples = channels;
if (af_fmt_is_planar(format))
plane_samples = 1;
for (int p = 0; p < planes; p++) {
for (int n = 0; n < samples / 2; n++) {
int s1_offset = n * sstride;
int s2_offset = (samples - 1 - n) * sstride;
for (int c = 0; c < plane_samples; c++) {
// Nobody said it'd be fast.
char tmp[16];
uint8_t *s1 = d[p] + s1_offset + c * bps;
uint8_t *s2 = d[p] + s2_offset + c * bps;
memcpy(tmp, s2, bps);
memcpy(s2, s1, bps);
memcpy(s1, tmp, bps);
}
}
}
return true;
}
int mp_aframe_approx_byte_size(struct mp_aframe *frame)
{
// God damn, AVFrame is too fucking annoying. Just go with the size that
// allocating a new frame would use.
int planes = mp_aframe_get_planes(frame);
size_t sstride = mp_aframe_get_sstride(frame);
int samples = frame->av_frame->nb_samples;
int plane_size = MP_ALIGN_UP(sstride * MPMAX(samples, 1), 32);
return plane_size * planes + sizeof(*frame);
}
struct mp_aframe_pool {
AVBufferPool *avpool;
int element_size;

View File

@ -51,6 +51,10 @@ int mp_aframe_get_planes(struct mp_aframe *frame);
int mp_aframe_get_total_plane_samples(struct mp_aframe *frame);
size_t mp_aframe_get_sstride(struct mp_aframe *frame);
bool mp_aframe_reverse(struct mp_aframe *frame);
int mp_aframe_approx_byte_size(struct mp_aframe *frame);
char *mp_aframe_format_str_buf(char *buf, size_t buf_size, struct mp_aframe *fmt);
#define mp_aframe_format_str(fmt) mp_aframe_format_str_buf((char[32]){0}, 32, (fmt))

View File

@ -88,6 +88,9 @@ struct demux_opts {
int seekable_cache;
int create_ccs;
char *record_file;
int video_back_preroll;
int audio_back_preroll;
double back_seek_size;
};
#define OPT_BASE_STRUCT struct demux_opts
@ -110,6 +113,12 @@ const struct m_sub_options demux_conf = {
({"auto", -1}, {"no", 0}, {"yes", 1})),
OPT_FLAG("sub-create-cc-track", create_ccs, 0),
OPT_STRING("stream-record", record_file, 0),
OPT_CHOICE_OR_INT("video-backward-overlap", video_back_preroll, 0, 0,
1024, ({"auto", -1})),
OPT_CHOICE_OR_INT("audio-backward-overlap", audio_back_preroll, 0, 0,
1024, ({"auto", -1})),
OPT_DOUBLE("demuxer-backward-playback-step", back_seek_size, M_OPT_MIN,
.min = 0),
{0}
},
.size = sizeof(struct demux_opts),
@ -121,6 +130,9 @@ const struct m_sub_options demux_conf = {
.min_secs_cache = 10.0 * 60 * 60,
.seekable_cache = -1,
.access_references = 1,
.video_back_preroll = -1,
.audio_back_preroll = -1,
.back_seek_size = 60,
},
};
@ -183,6 +195,15 @@ struct demux_internal {
// file (or if the demuxer was just opened).
bool after_seek_to_start;
// Demuxing backwards. Since demuxer implementations don't support this
// directly, it is emulated by seeking backwards for every packet run. Also,
// packets between keyframes are demuxed forwards (you can't decode that
// stuff otherwise), which adds complexity on top of it.
bool back_demuxing;
// For backward demuxing: back-step seek needs to be triggered.
bool need_back_seek;
bool tracks_switched; // thread needs to inform demuxer of this
bool seeking; // there's a seek queued
@ -322,6 +343,27 @@ struct demux_stream {
int64_t last_ret_pos;
double last_ret_dts;
// Backwards demuxing.
// pos/dts of the previous keyframe packet returned; valid if
// back_range_started or back_restarting are set.
int64_t back_restart_pos;
double back_restart_dts;
bool back_restarting; // searching keyframe before restart pos
// Current PTS lower bound for back demuxing.
double back_seek_pos;
// pos/dts of the packet to resume demuxing from when another stream caused
// a seek backward to get more packets. reader_head will be reset to this
// packet as soon as it's encountered again.
int64_t back_resume_pos;
double back_resume_dts;
bool back_resuming; // resuming mode (above fields are valid/used)
// Set to true if the first packet (keyframe) of a range was returned.
bool back_range_started;
// Number of packets at start of range yet to return. -1 is used for BOF.
int back_range_min;
// Static packet preroll count.
int back_preroll;
// for closed captions (demuxer_feed_caption)
struct sh_stream *cc;
bool ignore_eof; // ignore stream in underrun detection
@ -352,6 +394,9 @@ static void demuxer_sort_chapters(demuxer_t *demuxer);
static void *demux_thread(void *pctx);
static void update_cache(struct demux_internal *in);
static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp);
static struct demux_packet *advance_reader_head(struct demux_stream *ds);
static bool queue_seek(struct demux_internal *in, double seek_pts, int flags,
bool clear_back_state);
#if 0
// very expensive check for redundant cached queue state
@ -693,7 +738,8 @@ static void ds_clear_reader_queue_state(struct demux_stream *ds)
ds->need_wakeup = true;
}
static void ds_clear_reader_state(struct demux_stream *ds)
static void ds_clear_reader_state(struct demux_stream *ds,
bool clear_back_state)
{
ds_clear_reader_queue_state(ds);
@ -704,6 +750,18 @@ static void ds_clear_reader_state(struct demux_stream *ds)
ds->attached_picture_added = false;
ds->last_ret_pos = -1;
ds->last_ret_dts = MP_NOPTS_VALUE;
if (clear_back_state) {
ds->back_restart_pos = -1;
ds->back_restart_dts = MP_NOPTS_VALUE;
ds->back_restarting = false;
ds->back_seek_pos = MP_NOPTS_VALUE;
ds->back_resume_pos = -1;
ds->back_resume_dts = MP_NOPTS_VALUE;
ds->back_resuming = false;
ds->back_range_started = false;
ds->back_range_min = 0;
}
}
// Call if the observed reader state on this stream somehow changes. The wakeup
@ -728,7 +786,7 @@ static void update_stream_selection_state(struct demux_internal *in,
ds->eof = false;
ds->refreshing = false;
ds_clear_reader_state(ds);
ds_clear_reader_state(ds, true);
// We still have to go over the whole stream list to update ds->eager for
// other streams too, because they depend on other stream's selections.
@ -859,6 +917,8 @@ static void demux_add_sh_stream_locked(struct demux_internal *in,
};
talloc_set_destructor(sh->ds, ds_destroy);
struct demux_stream *ds = sh->ds;
if (!sh->codec->codec)
sh->codec->codec = "";
@ -887,6 +947,19 @@ static void demux_add_sh_stream_locked(struct demux_internal *in,
mp_tags_replace(sh->ds->tags_init->sh, sh->tags);
mp_packet_tags_setref(&sh->ds->tags_reader, sh->ds->tags_init);
switch (ds->type) {
case STREAM_AUDIO:
ds->back_preroll = in->opts->audio_back_preroll;
if (ds->back_preroll < 0)
ds->back_preroll = 1; // auto
break;
case STREAM_VIDEO:
ds->back_preroll = in->opts->video_back_preroll;
if (ds->back_preroll < 0)
ds->back_preroll = 0; // auto
break;
}
in->events |= DEMUX_EVENT_STREAMS;
if (in->wakeup_cb)
in->wakeup_cb(in->wakeup_cb_ctx);
@ -1159,7 +1232,241 @@ void demuxer_feed_caption(struct sh_stream *stream, demux_packet_t *dp)
dp->dts = MP_ADD_PTS(dp->dts, -in->ts_offset);
add_packet_locked(sh, dp);
pthread_mutex_unlock(&in->lock);
}
static void perform_backward_seek(struct demux_internal *in)
{
double target = MP_NOPTS_VALUE;
for (int n = 0; n < in->num_streams; n++) {
struct demux_stream *ds = in->streams[n]->ds;
if (ds->reader_head && !ds->back_restarting && !ds->back_resuming &&
ds->eager)
{
ds->back_resuming = true;
ds->back_resume_pos = ds->reader_head->pos;
ds->back_resume_dts = ds->reader_head->dts;
}
target = MP_PTS_MIN(target, ds->back_seek_pos);
}
target = PTS_OR_DEF(target, in->d_thread->start_time);
target -= in->opts->back_seek_size;
MP_VERBOSE(in, "triggering backward seek to get more packets\n");
queue_seek(in, target, SEEK_SATAN, false);
in->reading = true;
}
// Search for a packet to resume demuxing from.
// from_cache: if true, this was called trying to go backwards in the cache;
// if false, this is from a hard seek before the back_restart_pos
// The implementation of this function is quite awkward, because the packet
// queue is a singly linked list without back links, while it needs to search
// backwards.
// This is the core of backward demuxing.
static void find_backward_restart_pos(struct demux_stream *ds, bool from_cache)
{
struct demux_internal *in = ds->in;
assert(ds->back_restarting);
if (!ds->reader_head)
return; // no packets yet
struct demux_packet *first = ds->reader_head;
struct demux_packet *last = ds->queue->tail;
assert(last);
if ((ds->global_correct_dts && last->dts < ds->back_restart_dts) ||
(ds->global_correct_pos && last->pos < ds->back_restart_pos))
return; // restart pos not reached yet
// The target we're searching for is apparently before the start of the queue.
if ((ds->global_correct_dts && first->dts > ds->back_restart_dts) ||
(ds->global_correct_pos && first->pos > ds->back_restart_pos))
{
// If this function was called for trying to backstep within the packet
// cache, the cache probably got pruned past the target (reader_head is
// being moved backwards, so nothing stops it from pruning packets
// before that). Just make the caller seek.
if (from_cache) {
in->need_back_seek = true;
return;
}
// The demuxer probably seeked to the wrong position, or broke dts/pos
// determinism assumptions?
MP_ERR(in, "Demuxer did not seek correctly.\n");
return;
}
// Packet at back_restart_pos. (Note: we don't actually need it, only the
// packet immediately before it. But same effort.)
struct demux_packet *back_restart = NULL;
for (struct demux_packet *cur = first; cur; cur = cur->next) {
if ((ds->global_correct_dts && cur->dts == ds->back_restart_dts) ||
(ds->global_correct_pos && cur->pos == ds->back_restart_pos))
{
back_restart = cur;
break;
}
}
if (!back_restart) {
// The packet should have been in the searched range; maybe dts/pos
// determinism assumptions were broken.
MP_ERR(in, "Demuxer not cooperating.\n");
return;
}
if (!ds->reader_head->keyframe)
MP_WARN(in, "Queue not starting on keyframe.\n");
// Find where to restart demuxing. It's usually the last keyframe packet
// before restart_pos, but might be up to back_preroll packets earlier.
struct demux_packet *last_keyframe = NULL;
struct demux_packet *last_preroll = NULL;
// Keep this packet at back_preroll packets before last_keyframe.
struct demux_packet *pre_packet = ds->reader_head;
int pre_packet_offset = ds->back_preroll;
// (Normally, we'd just iterate backwards, but no back links.)
for (struct demux_packet *cur = ds->reader_head;
cur != back_restart;
cur = cur->next)
{
if (cur->keyframe) {
last_keyframe = cur;
last_preroll = pre_packet;
}
if (pre_packet_offset) {
pre_packet_offset--;
} else {
pre_packet = pre_packet->next;
}
}
if (!last_keyframe) {
// Note: assume this holds true. You could think of various reasons why
// this might break.
if (ds->queue->is_bof) {
MP_VERBOSE(in, "BOF for stream %d\n", ds->index);
ds->back_restarting = false;
ds->back_range_started = false;
ds->back_range_min = -1;
ds->need_wakeup = true;
wakeup_ds(ds);
return;
}
goto resume_earlier;
}
int got_preroll = 0;
for (struct demux_packet *cur = last_preroll;
cur != last_keyframe;
cur = cur->next)
got_preroll++;
if (got_preroll < ds->back_preroll && !ds->queue->is_bof)
goto resume_earlier;
// (Round preroll down to last_keyframe in the worst case.)
while (!last_preroll->keyframe)
last_preroll = last_preroll->next;
// Skip reader_head from previous keyframe to current one.
// Or if preroll is involved, the first preroll packet.
while (ds->reader_head != last_preroll) {
if (!advance_reader_head(ds))
assert(0); // last_preroll must be in list
}
ds->back_restarting = false;
ds->back_range_started = false;
ds->back_range_min = got_preroll + 1;
ds->need_wakeup = true;
wakeup_ds(ds);
return;
resume_earlier:
// If an earlier seek didn't land at an early enough position, we need to
// try to seek even earlier. Usually this will happen with large
// back_preroll values, because the initial back seek does not take them
// into account. We don't really know how much we need to seek, so add some
// random value to the previous seek value. Not ideal.
if (!from_cache && ds->back_seek_pos != MP_NOPTS_VALUE)
ds->back_seek_pos -= 1.0;
in->need_back_seek = true;
}
// Process that one or multiple packets were added.
static void back_demux_see_packets(struct demux_stream *ds)
{
struct demux_internal *in = ds->in;
if (!ds->selected || !in->back_demuxing)
return;
assert(!(ds->back_resuming && ds->back_restarting));
if (!ds->global_correct_dts && !ds->global_correct_pos) {
MP_ERR(in, "Can't demux backward due to demuxer problems.\n");
return;
}
while (ds->back_resuming && ds->reader_head) {
struct demux_packet *head = ds->reader_head;
if ((ds->global_correct_dts && head->dts == ds->back_resume_dts) ||
(ds->global_correct_pos && head->pos == ds->back_resume_pos))
{
ds->back_resuming = false;
ds->need_wakeup = true;
wakeup_ds(ds); // probably
break;
}
advance_reader_head(ds);
}
if (ds->back_restarting)
find_backward_restart_pos(ds, false);
}
// Resume demuxing from an earlier position for backward playback. May trigger
// a seek.
static void step_backwards(struct demux_stream *ds)
{
struct demux_internal *in = ds->in;
assert(in->back_demuxing);
assert(!ds->back_restarting);
ds->back_restarting = true;
// Move to start of queue. This is inefficient, because we need to iterate
// the entire fucking packet queue just to update the fw_* stats. But as
// long as we don't have demux_packet.prev links or a complete index, it's
// the thing to do.
// Note: if the buffer forward is much larger than the one backward, it
// would be worth looping until the previous reader_head and decrementing
// fw_packs/fw_bytes - you could skip the full recompute_buffers().
ds->reader_head = ds->queue->head;
in->fw_bytes -= ds->fw_bytes;
recompute_buffers(ds);
in->fw_bytes += ds->fw_bytes;
// Exclude weird special-cases (incomplete pruning? broken seeks?)
while (ds->reader_head && !ds->reader_head->keyframe)
advance_reader_head(ds);
find_backward_restart_pos(ds, true);
}
// Add the keyframe to the end of the index. Not all packets are actually added.
@ -1353,6 +1660,9 @@ static void attempt_range_joining(struct demux_internal *in)
MP_VERBOSE(in, "ranges joined!\n");
for (int n = 0; n < in->num_streams; n++)
back_demux_see_packets(in->streams[n]->ds);
failed:
clear_cached_range(in, next);
free_empty_cached_ranges(in);
@ -1560,6 +1870,8 @@ static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp)
}
}
back_demux_see_packets(ds);
wakeup_ds(ds);
}
@ -1578,13 +1890,19 @@ static bool read_packet(struct demux_internal *in)
bool read_more = false, prefetch_more = false, refresh_more = false;
for (int n = 0; n < in->num_streams; n++) {
struct demux_stream *ds = in->streams[n]->ds;
read_more |= ds->eager && !ds->reader_head;
if (ds->eager) {
read_more |= !ds->reader_head;
if (in->back_demuxing)
read_more |= ds->back_restarting || ds->back_resuming;
}
refresh_more |= ds->refreshing;
if (ds->eager && ds->queue->last_ts != MP_NOPTS_VALUE &&
in->min_secs > 0 && ds->base_ts != MP_NOPTS_VALUE &&
ds->queue->last_ts >= ds->base_ts)
ds->queue->last_ts >= ds->base_ts &&
!in->back_demuxing)
prefetch_more |= ds->queue->last_ts - ds->base_ts < in->min_secs;
}
MP_TRACE(in, "bytes=%zd, read_more=%d prefetch_more=%d, refresh_more=%d\n",
in->fw_bytes, read_more, prefetch_more, refresh_more);
if (in->fw_bytes >= in->max_bytes) {
@ -1604,6 +1922,8 @@ static bool read_packet(struct demux_internal *in)
ds->refreshing ? " (refreshing)" : "");
}
}
if (in->back_demuxing)
MP_ERR(in, "Backward playback is likely stuck/broken now.\n");
}
for (int n = 0; n < in->num_streams; n++) {
struct demux_stream *ds = in->streams[n]->ds;
@ -1798,6 +2118,10 @@ static bool thread_work(struct demux_internal *in)
execute_trackswitch(in);
return true;
}
if (in->need_back_seek) {
perform_backward_seek(in);
return true;
}
if (in->seeking) {
execute_seek(in);
return true;
@ -1876,6 +2200,11 @@ static int dequeue_packet(struct demux_stream *ds, struct demux_packet **res)
if (in->blocked)
return 0;
if (ds->back_resuming || ds->back_restarting) {
assert(in->back_demuxing);
return 0;
}
if (ds->sh->attached_picture) {
ds->eof = true;
if (ds->attached_picture_added)
@ -1895,8 +2224,27 @@ static int dequeue_packet(struct demux_stream *ds, struct demux_packet **res)
pthread_cond_signal(&in->wakeup); // possibly read more
}
bool eof = !ds->reader_head && ds->eof;
if (in->back_demuxing) {
// Subtitles not supported => EOF.
if (!ds->eager)
return -1;
// Next keyframe (or EOF) was reached => step back.
if (ds->back_range_started && !ds->back_range_min &&
((ds->reader_head && ds->reader_head->keyframe) || eof))
{
step_backwards(ds);
if (ds->back_restarting)
return 0;
}
eof = ds->back_range_min < 0;
}
ds->need_wakeup = !ds->reader_head;
if (!ds->reader_head) {
if (!ds->reader_head || eof) {
if (!ds->eager) {
// Non-eager streams temporarily return EOF. If they returned 0,
// the reader would have to wait for new packets, which does not
@ -1904,7 +2252,7 @@ static int dequeue_packet(struct demux_stream *ds, struct demux_packet **res)
// streams.
return -1;
}
return ds->eof ? -1 : 0;
return eof ? -1 : 0;
}
struct demux_packet *pkt = advance_reader_head(ds);
@ -1916,6 +2264,23 @@ static int dequeue_packet(struct demux_stream *ds, struct demux_packet **res)
abort();
pkt->next = NULL;
if (ds->in->back_demuxing) {
if (ds->back_range_min)
ds->back_range_min -= 1;
if (ds->back_range_min) {
pkt->back_preroll = true;
} else if (pkt->keyframe) {
// For next backward adjust action.
ds->back_restart_dts = pkt->dts;
ds->back_restart_pos = pkt->pos;
}
if (!ds->back_range_started) {
pkt->back_restart = true;
ds->back_range_started = true;
}
ds->back_seek_pos = MP_PTS_MIN(ds->back_seek_pos, pkt->pts);
}
double ts = PTS_OR_DEF(pkt->dts, pkt->pts);
if (ts != MP_NOPTS_VALUE)
ds->base_ts = ts;
@ -2544,13 +2909,15 @@ struct demuxer *demux_open_url(const char *url,
}
// called locked, from user thread only
static void clear_reader_state(struct demux_internal *in)
static void clear_reader_state(struct demux_internal *in,
bool clear_back_state)
{
for (int n = 0; n < in->num_streams; n++)
ds_clear_reader_state(in->streams[n]->ds);
ds_clear_reader_state(in->streams[n]->ds, clear_back_state);
in->warned_queue_overflow = false;
in->d_user->filepos = -1; // implicitly synchronized
in->blocked = false;
in->need_back_seek = false;
assert(in->fw_bytes == 0);
}
@ -2561,7 +2928,7 @@ void demux_flush(demuxer_t *demuxer)
assert(demuxer == in->d_user);
pthread_mutex_lock(&demuxer->in->lock);
clear_reader_state(in);
clear_reader_state(in, true);
for (int n = 0; n < in->num_ranges; n++)
clear_cached_range(in, in->ranges[n]);
free_empty_cached_ranges(in);
@ -2802,12 +3169,20 @@ int demux_seek(demuxer_t *demuxer, double seek_pts, int flags)
{
struct demux_internal *in = demuxer->in;
assert(demuxer == in->d_user);
int res = 0;
pthread_mutex_lock(&in->lock);
int res = queue_seek(in, seek_pts, flags, true);
pthread_cond_signal(&in->wakeup);
pthread_mutex_unlock(&in->lock);
return res;
}
static bool queue_seek(struct demux_internal *in, double seek_pts, int flags,
bool clear_back_state)
{
if (seek_pts == MP_NOPTS_VALUE)
goto done;
return false;
MP_VERBOSE(in, "queuing seek to %f%s\n", seek_pts,
in->seeking ? " (cascade)" : "");
@ -2818,26 +3193,35 @@ int demux_seek(demuxer_t *demuxer, double seek_pts, int flags)
bool require_cache = flags & SEEK_CACHED;
flags &= ~(unsigned)SEEK_CACHED;
bool set_backwards = flags & SEEK_SATAN;
flags &= ~(unsigned)SEEK_SATAN;
// For HR seeks, the correct seek rounding direction is forward instead of
// backward.
if (set_backwards && (flags & SEEK_HR))
flags |= SEEK_FORWARD;
struct demux_cached_range *cache_target =
find_cache_seek_target(in, seek_pts, flags);
if (!cache_target) {
if (require_cache) {
MP_VERBOSE(demuxer, "Cached seek not possible.\n");
goto done;
MP_VERBOSE(in, "Cached seek not possible.\n");
return false;
}
if (!demuxer->seekable) {
MP_WARN(demuxer, "Cannot seek in this file.\n");
goto done;
if (!in->d_thread->seekable) {
MP_WARN(in, "Cannot seek in this file.\n");
return false;
}
}
clear_reader_state(in);
clear_reader_state(in, clear_back_state);
in->eof = false;
in->last_eof = false;
in->idle = true;
in->reading = false;
in->back_demuxing = set_backwards;
if (cache_target) {
execute_cache_seek(in, cache_target, seek_pts, flags);
@ -2855,12 +3239,7 @@ int demux_seek(demuxer_t *demuxer, double seek_pts, int flags)
if (!in->threading && in->seeking)
execute_seek(in);
res = 1;
done:
pthread_cond_signal(&in->wakeup);
pthread_mutex_unlock(&in->lock);
return res;
return true;
}
struct sh_stream *demuxer_stream_by_demuxer_id(struct demuxer *d,

View File

@ -58,6 +58,7 @@ struct demux_reader_state {
#define SEEK_FORWARD (1 << 2) // prefer later time if not exact
// (if unset, prefer earlier time)
#define SEEK_CACHED (1 << 3) // allow packet cache seeks only
#define SEEK_SATAN (1 << 4) // enable backward demuxing
#define SEEK_HR (1 << 5) // hr-seek (this is a weak hint only)
// Strictness of the demuxer open format check.

View File

@ -129,6 +129,8 @@ void demux_packet_copy_attribs(struct demux_packet *dst, struct demux_packet *sr
dst->start = src->start;
dst->end = src->end;
dst->codec = src->codec;
dst->back_restart = src->back_restart;
dst->back_preroll = src->back_preroll;
dst->keyframe = src->keyframe;
dst->stream = src->stream;
mp_packet_tags_setref(&dst->metadata, src->metadata);

View File

@ -36,6 +36,10 @@ typedef struct demux_packet {
bool keyframe;
// backward playback
bool back_restart; // restart point (reverse and return previous frames)
bool back_preroll; // initial discarded frame for smooth decoder reinit
// segmentation (ordered chapters, EDL)
bool segmented;
struct mp_codec_params *codec; // set to non-NULL iff segmented is set

View File

@ -87,6 +87,13 @@ struct priv {
double start, end;
struct demux_packet *new_segment;
struct mp_frame packet;
bool packet_fed;
int preroll_discard;
size_t reverse_queue_byte_size;
struct mp_frame *reverse_queue;
int num_reverse_queue;
bool reverse_queue_complete;
struct mp_frame decoded_coverart;
int coverart_returned; // 0: no, 1: coverart frame itself, 2: EOF returned
@ -108,11 +115,19 @@ static void reset_decoder(struct priv *p)
p->public.pts_reset = false;
p->packets_without_output = 0;
mp_frame_unref(&p->packet);
p->packet_fed = false;
p->preroll_discard = 0;
talloc_free(p->new_segment);
p->new_segment = NULL;
p->start = p->end = MP_NOPTS_VALUE;
p->coverart_returned = 0;
for (int n = 0; n < p->num_reverse_queue; n++)
mp_frame_unref(&p->reverse_queue[n]);
p->num_reverse_queue = 0;
p->reverse_queue_byte_size = 0;
p->reverse_queue_complete = false;
if (p->decoder)
mp_filter_reset(p->decoder->f);
}
@ -307,18 +322,22 @@ static void process_video_frame(struct priv *p, struct mp_image *mpi)
struct MPOpts *opts = p->opt_cache->opts;
m_config_cache_update(p->opt_cache);
int dir = p->public.play_dir;
// Note: the PTS is reordered, but the DTS is not. Both should be monotonic.
double pts = mpi->pts;
double dts = mpi->dts;
if (pts != MP_NOPTS_VALUE) {
if (pts < p->codec_pts)
pts *= dir;
if (pts < p->codec_pts && dir > 0)
p->num_codec_pts_problems++;
p->codec_pts = mpi->pts;
}
if (dts != MP_NOPTS_VALUE) {
if (dts <= p->codec_dts)
dts *= dir;
if (dts <= p->codec_dts && dir > 0)
p->num_codec_dts_problems++;
p->codec_dts = mpi->dts;
}
@ -401,8 +420,12 @@ void mp_decoder_wrapper_get_video_dec_params(struct mp_decoder_wrapper *d,
static void process_audio_frame(struct priv *p, struct mp_aframe *aframe)
{
double dir = p->public.play_dir;
double frame_pts = mp_aframe_get_pts(aframe);
if (frame_pts != MP_NOPTS_VALUE) {
frame_pts *= dir;
if (p->pts != MP_NOPTS_VALUE)
MP_STATS(p, "value %f audio-pts-err", p->pts - frame_pts);
@ -429,7 +452,10 @@ static void process_audio_frame(struct priv *p, struct mp_aframe *aframe)
mp_aframe_set_pts(aframe, p->pts);
if (p->pts != MP_NOPTS_VALUE)
p->pts += mp_aframe_duration(aframe);
p->pts += mp_aframe_duration(aframe) * dir;
if (dir < 0)
mp_aframe_set_pts(aframe, p->pts);
}
@ -445,8 +471,9 @@ static bool is_new_segment(struct priv *p, struct mp_frame frame)
if (frame.type != MP_FRAME_PACKET)
return false;
struct demux_packet *pkt = frame.data;
return pkt->segmented && (pkt->start != p->start || pkt->end != p->end ||
pkt->codec != p->codec);
return (pkt->segmented && (pkt->start != p->start || pkt->end != p->end ||
pkt->codec != p->codec)) ||
(p->public.play_dir < 0 && pkt->back_restart && p->packet_fed);
}
static void feed_packet(struct priv *p)
@ -466,6 +493,9 @@ static void feed_packet(struct priv *p)
}
}
if (!p->packet.type)
return;
// Flush current data if the packet is a new segment.
if (is_new_segment(p, p->packet)) {
assert(!p->new_segment);
@ -474,7 +504,8 @@ static void feed_packet(struct priv *p)
}
assert(p->packet.type == MP_FRAME_PACKET || p->packet.type == MP_FRAME_EOF);
struct demux_packet *packet = p->packet.data;
struct demux_packet *packet =
p->packet.type == MP_FRAME_PACKET ? p->packet.data : NULL;
// For video framedropping, including parts of the hr-seek logic.
if (p->decoder->control) {
@ -488,7 +519,7 @@ static void feed_packet(struct priv *p)
if (p->public.attempt_framedrops)
framedrop_type = 1;
if (start_pts != MP_NOPTS_VALUE && packet &&
if (start_pts != MP_NOPTS_VALUE && packet && p->public.play_dir > 0 &&
packet->pts < start_pts - .005 && !p->has_broken_packet_pts)
framedrop_type = 2;
@ -511,8 +542,12 @@ static void feed_packet(struct priv *p)
if (p->first_packet_pdts == MP_NOPTS_VALUE)
p->first_packet_pdts = pkt_pdts;
if (packet && packet->back_preroll)
p->preroll_discard += 1;
mp_pin_in_write(p->decoder->f->pins[0], p->packet);
p->packet = MP_NO_FRAME;
p->packet_fed = true;
p->packets_without_output += 1;
}
@ -549,6 +584,10 @@ static bool process_decoded_frame(struct priv *p, struct mp_frame *frame)
double pts = mp_aframe_get_pts(aframe);
if (pts != MP_NOPTS_VALUE && p->start != MP_NOPTS_VALUE)
segment_ended = pts >= p->end;
if (p->public.play_dir < 0 && !mp_aframe_reverse(aframe))
MP_ERR(p, "Couldn't reverse audio frame.\n");
if (mp_aframe_get_size(aframe) == 0)
mp_frame_unref(frame);
} else {
@ -558,6 +597,35 @@ static bool process_decoded_frame(struct priv *p, struct mp_frame *frame)
return segment_ended;
}
static void enqueue_backward_frame(struct priv *p, struct mp_frame frame)
{
bool eof = frame.type == MP_FRAME_EOF;
if (!eof) {
struct MPOpts *opts = p->opt_cache->opts;
uint64_t queue_size = 0;
switch (p->header->type) {
case STREAM_VIDEO: queue_size = opts->video_reverse_size; break;
case STREAM_AUDIO: queue_size = opts->audio_reverse_size; break;
}
if (p->reverse_queue_byte_size >= queue_size) {
MP_ERR(p, "Reversal queue overflow, discarding frame.\n");
mp_frame_unref(&frame);
return;
}
p->reverse_queue_byte_size += mp_frame_approx_size(frame);
}
// Note: EOF (really BOF) is propagated, but not reversed.
MP_TARRAY_INSERT_AT(p, p->reverse_queue, p->num_reverse_queue,
eof ? 0 : p->num_reverse_queue, frame);
p->reverse_queue_complete = eof;
}
static void read_frame(struct priv *p)
{
struct mp_pin *pin = p->f->ppins[0];
@ -576,6 +644,14 @@ static void read_frame(struct priv *p)
return;
}
if (p->reverse_queue_complete && p->num_reverse_queue) {
struct mp_frame frame = p->reverse_queue[p->num_reverse_queue - 1];
p->num_reverse_queue -= 1;
mp_pin_in_write(pin, frame);
return;
}
p->reverse_queue_complete = false;
struct mp_frame frame = mp_pin_out_read(p->decoder->f->pins[1]);
if (!frame.type)
return;
@ -593,23 +669,47 @@ static void read_frame(struct priv *p)
}
p->packets_without_output = 0;
if (p->preroll_discard > 0 && frame.type != MP_FRAME_EOF) {
p->preroll_discard -= 1;
mp_frame_unref(&frame);
mp_filter_internal_mark_progress(p->f);
return;
}
bool segment_ended = process_decoded_frame(p, &frame);
if (p->public.play_dir < 0 && frame.type) {
enqueue_backward_frame(p, frame);
frame = MP_NO_FRAME;
}
// If there's a new segment, start it as soon as we're drained/finished.
if (segment_ended && p->new_segment) {
struct demux_packet *new_segment = p->new_segment;
p->new_segment = NULL;
struct mp_frame *reverse_queue = p->reverse_queue;
int num_reverse_queue = p->num_reverse_queue;
p->reverse_queue = NULL;
p->num_reverse_queue = 0;
reset_decoder(p);
if (p->codec != new_segment->codec) {
p->codec = new_segment->codec;
if (!mp_decoder_wrapper_reinit(&p->public))
mp_filter_internal_mark_failed(p->f);
if (new_segment->segmented) {
if (p->codec != new_segment->codec) {
p->codec = new_segment->codec;
if (!mp_decoder_wrapper_reinit(&p->public))
mp_filter_internal_mark_failed(p->f);
}
p->start = new_segment->start;
p->end = new_segment->end;
}
p->start = new_segment->start;
p->end = new_segment->end;
assert(!p->reverse_queue);
p->reverse_queue = reverse_queue;
p->num_reverse_queue = num_reverse_queue;
p->reverse_queue_complete = p->num_reverse_queue > 0;
p->packet = MAKE_FRAME(MP_FRAME_PACKET, new_segment);
mp_filter_internal_mark_progress(p->f);
@ -655,6 +755,8 @@ struct mp_decoder_wrapper *mp_decoder_wrapper_create(struct mp_filter *parent,
p->codec = p->header->codec;
w->f = f;
w->play_dir = 1;
struct MPOpts *opts = p->opt_cache->opts;
mp_filter_add_pin(f, MP_PIN_OUT, "out");

View File

@ -37,6 +37,7 @@ struct mp_decoder_wrapper {
// Can be set by user.
struct mp_recorder_sink *recorder_sink;
int play_dir;
// --- for STREAM_VIDEO

View File

@ -14,6 +14,7 @@ struct frame_handler {
void *(*new_ref)(void *data);
double (*get_pts)(void *data);
void (*set_pts)(void *data, double pts);
int (*approx_size)(void *data);
AVFrame *(*new_av_ref)(void *data);
void *(*from_av_ref)(AVFrame *data);
void (*free)(void *data);
@ -34,6 +35,11 @@ static void video_set_pts(void *data, double pts)
((struct mp_image *)data)->pts = pts;
}
static int video_approx_size(void *data)
{
return mp_image_approx_byte_size(data);
}
static AVFrame *video_new_av_ref(void *data)
{
return mp_image_to_av_frame(data);
@ -59,6 +65,11 @@ static void audio_set_pts(void *data, double pts)
mp_aframe_set_pts(data, pts);
}
static int audio_approx_size(void *data)
{
return mp_aframe_approx_byte_size(data);
}
static AVFrame *audio_new_av_ref(void *data)
{
return mp_aframe_to_avframe(data);
@ -88,6 +99,7 @@ static const struct frame_handler frame_handlers[] = {
.new_ref = video_ref,
.get_pts = video_get_pts,
.set_pts = video_set_pts,
.approx_size = video_approx_size,
.new_av_ref = video_new_av_ref,
.from_av_ref = video_from_av_ref,
.free = talloc_free,
@ -98,6 +110,7 @@ static const struct frame_handler frame_handlers[] = {
.new_ref = audio_ref,
.get_pts = audio_get_pts,
.set_pts = audio_set_pts,
.approx_size = audio_approx_size,
.new_av_ref = audio_new_av_ref,
.from_av_ref = audio_from_av_ref,
.free = talloc_free,
@ -160,6 +173,13 @@ void mp_frame_set_pts(struct mp_frame frame, double pts)
frame_handlers[frame.type].set_pts(frame.data, pts);
}
int mp_frame_approx_size(struct mp_frame frame)
{
if (frame_handlers[frame.type].approx_size)
return frame_handlers[frame.type].approx_size(frame.data);
return 0;
}
AVFrame *mp_frame_to_av(struct mp_frame frame, struct AVRational *tb)
{
if (!frame_handlers[frame.type].new_av_ref)

View File

@ -45,6 +45,9 @@ struct mp_frame mp_frame_ref(struct mp_frame frame);
double mp_frame_get_pts(struct mp_frame frame);
void mp_frame_set_pts(struct mp_frame frame, double pts);
// Estimation of total size in bytes. This is for buffering purposes.
int mp_frame_approx_size(struct mp_frame frame);
struct AVFrame;
struct AVRational;
struct AVFrame *mp_frame_to_av(struct mp_frame frame, struct AVRational *tb);

View File

@ -405,6 +405,11 @@ const m_option_t mp_opts[] = {
OPT_REL_TIME("end", play_end, 0),
OPT_REL_TIME("length", play_length, 0),
OPT_CHOICE("play-direction", play_dir, 0,
({"forward", 1}, {"backward", -1})),
OPT_BYTE_SIZE("video-reversal-buffer", video_reverse_size, 0, 0, (size_t)-1),
OPT_BYTE_SIZE("audio-reversal-buffer", audio_reverse_size, 0, 0, (size_t)-1),
OPT_FLAG("rebase-start-time", rebase_start_time, 0),
OPT_TIME("ab-loop-a", ab_loop[0], 0, .min = MP_NOPTS_VALUE),
@ -937,6 +942,9 @@ const struct MPOpts mp_default_opts = {
.audiofile_auto = -1,
.osd_bar_visible = 1,
.screenshot_template = "mpv-shot%n",
.play_dir = 1,
.video_reverse_size = 1 * 1024 * 1024 * 1024,
.audio_reverse_size = 64 * 1024 * 1024,
.audio_output_channels = {
.set = 1,

View File

@ -224,6 +224,7 @@ typedef struct MPOpts {
struct m_rel_time play_start;
struct m_rel_time play_end;
struct m_rel_time play_length;
int play_dir;
int rebase_start_time;
int play_frames;
double ab_loop[2];
@ -252,6 +253,8 @@ typedef struct MPOpts {
int prefetch_open;
char *audio_demuxer_name;
char *sub_demuxer_name;
int64_t video_reverse_size;
int64_t audio_reverse_size;
int cache_pause;
int cache_pause_initial;

View File

@ -465,6 +465,19 @@ static int mp_property_playback_speed(void *ctx, struct m_property *prop,
return mp_property_generic_option(mpctx, prop, action, arg);
}
static int mp_property_play_direction(void *ctx, struct m_property *prop,
int action, void *arg)
{
MPContext *mpctx = ctx;
if (action == M_PROPERTY_SET) {
if (mpctx->play_dir != *(int *)arg) {
queue_seek(mpctx, MPSEEK_ABSOLUTE, get_current_time(mpctx),
MPSEEK_EXACT, 0);
}
}
return mp_property_generic_option(mpctx, prop, action, arg);
}
static int mp_property_av_speed_correction(void *ctx, struct m_property *prop,
int action, void *arg)
{
@ -3561,6 +3574,8 @@ static const struct m_property mp_properties_base[] = {
{"property-list", mp_property_list},
{"profile-list", mp_profile_list},
{"play-direction", mp_property_play_direction},
M_PROPERTY_ALIAS("video", "vid"),
M_PROPERTY_ALIAS("audio", "aid"),
M_PROPERTY_ALIAS("sub", "sid"),

View File

@ -326,6 +326,7 @@ typedef struct MPContext {
enum playback_status video_status, audio_status;
bool restart_complete;
int play_dir;
// Factors to multiply with opts->playback_speed to get the total audio or
// video speed (usually 1.0, but can be set to by the sync code).
double speed_factor_v, speed_factor_a;

View File

@ -1551,6 +1551,11 @@ static void play_current_file(struct MPContext *mpctx)
}
double play_start_pts = get_play_start_pts(mpctx);
// Backward playback -> start from end by default.
if (play_start_pts == MP_NOPTS_VALUE && opts->play_dir < 0)
play_start_pts = MPMAX(mpctx->demuxer->duration, 0);
if (play_start_pts != MP_NOPTS_VALUE) {
/*
* get_play_start_pts returns rebased values, but

View File

@ -280,6 +280,7 @@ struct MPContext *mp_create(void)
.playback_abort = mp_cancel_new(mpctx),
.thread_pool = mp_thread_pool_create(mpctx, 0, 1, 30),
.stop_play = PT_STOP,
.play_dir = 1,
};
pthread_mutex_init(&mpctx->abort_lock, NULL);

View File

@ -42,6 +42,7 @@
#include "audio/out/ao.h"
#include "demux/demux.h"
#include "stream/stream.h"
#include "sub/dec_sub.h"
#include "sub/osd.h"
#include "video/out/vo.h"
@ -223,6 +224,14 @@ void reset_playback_state(struct MPContext *mpctx)
reset_audio_state(mpctx);
reset_subtitle_state(mpctx);
for (int n = 0; n < mpctx->num_tracks; n++) {
struct track *t = mpctx->tracks[n];
if (t->dec)
t->dec->play_dir = mpctx->play_dir;
if (t->d_sub)
sub_set_play_dir(t->d_sub, mpctx->play_dir);
}
mpctx->hrseek_active = false;
mpctx->hrseek_lastframe = false;
mpctx->hrseek_backstep = false;
@ -317,6 +326,10 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
if (!mpctx->demuxer->seekable)
demux_flags |= SEEK_CACHED;
int play_dir = opts->play_dir;
if (play_dir < 0)
demux_flags |= SEEK_SATAN;
if (!demux_seek(mpctx->demuxer, demux_pts, demux_flags)) {
if (!mpctx->demuxer->seekable) {
MP_ERR(mpctx, "Cannot seek in this stream.\n");
@ -325,6 +338,8 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
return;
}
mpctx->play_dir = play_dir;
// Seek external, extra files too:
bool has_video = false;
struct track *external_audio = NULL;
@ -336,7 +351,7 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
main_new_pos += get_track_seek_offset(mpctx, track);
if (demux_flags & SEEK_FACTOR)
main_new_pos = seek_pts;
demux_seek(track->demuxer, main_new_pos, 0);
demux_seek(track->demuxer, main_new_pos, demux_flags & SEEK_SATAN);
if (track->type == STREAM_AUDIO && !external_audio)
external_audio = track;
}
@ -357,7 +372,9 @@ static void mp_seek(MPContext *mpctx, struct seek_params seek)
// granularity is coarser than audio). The result would be playing video with
// silence until the audio seek target is reached. Work around by blocking
// the demuxer (decoders can't read) and seeking to video position later.
if (has_video && external_audio && !hr_seek && !(demux_flags & SEEK_FORWARD)) {
if (has_video && external_audio && !hr_seek && mpctx->play_dir > 0 &&
!(demux_flags & SEEK_FORWARD))
{
MP_VERBOSE(mpctx, "delayed seek for aid=%d\n", external_audio->user_tid);
demux_block_reading(external_audio->demuxer, true);
mpctx->seek_slave = external_audio;
@ -370,7 +387,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_pts = seek_pts;
mpctx->hrseek_pts = seek_pts * mpctx->play_dir;
// allow decoder to drop frames before hrseek_pts
bool hrseek_framedrop = !hr_seek_very_exact && opts->hr_seek_framedrop;
@ -472,7 +489,7 @@ double get_current_time(struct MPContext *mpctx)
struct demuxer *demuxer = mpctx->demuxer;
if (demuxer) {
if (mpctx->playback_pts != MP_NOPTS_VALUE)
return mpctx->playback_pts;
return mpctx->playback_pts * mpctx->play_dir;
if (mpctx->last_seek_pts != MP_NOPTS_VALUE)
return mpctx->last_seek_pts;
}
@ -630,7 +647,7 @@ static void handle_update_cache(struct MPContext *mpctx)
int cache_buffer = 100;
bool use_pause_on_low_cache = demux_is_network_cached(mpctx->demuxer) &&
opts->cache_pause;
opts->cache_pause && mpctx->play_dir > 0;
if (!mpctx->restart_complete) {
// Audio or video is restarting, and initial buffering is enabled. Make

View File

@ -58,6 +58,7 @@ struct dec_sub {
struct attachment_list *attachments;
struct sh_stream *sh;
int play_dir;
double last_pkt_pts;
bool preload_attempted;
double video_fps;
@ -97,7 +98,7 @@ static double pts_to_subtitle(struct dec_sub *sub, double pts)
struct mp_subtitle_opts *opts = sub->opts;
if (pts != MP_NOPTS_VALUE)
pts = (pts - opts->sub_delay) / sub->sub_speed;
pts = (pts * sub->play_dir - opts->sub_delay) / sub->sub_speed;
return pts;
}
@ -107,7 +108,7 @@ static double pts_from_subtitle(struct dec_sub *sub, double pts)
struct mp_subtitle_opts *opts = sub->opts;
if (pts != MP_NOPTS_VALUE)
pts = pts * sub->sub_speed + opts->sub_delay;
pts = (pts * sub->sub_speed + opts->sub_delay) * sub->play_dir;
return pts;
}
@ -186,6 +187,7 @@ struct dec_sub *sub_create(struct mpv_global *global, struct sh_stream *sh,
.sh = sh,
.codec = sh->codec,
.attachments = talloc_steal(sub, attachments),
.play_dir = 1,
.last_pkt_pts = MP_NOPTS_VALUE,
.last_vo_pts = MP_NOPTS_VALUE,
.start = MP_NOPTS_VALUE,
@ -433,3 +435,9 @@ void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink)
pthread_mutex_unlock(&sub->lock);
}
void sub_set_play_dir(struct dec_sub *sub, int dir)
{
pthread_mutex_lock(&sub->lock);
sub->play_dir = dir;
pthread_mutex_unlock(&sub->lock);
}

View File

@ -42,6 +42,7 @@ void sub_reset(struct dec_sub *sub);
void sub_select(struct dec_sub *sub, bool selected);
void sub_update_opts(struct dec_sub *sub);
void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink);
void sub_set_play_dir(struct dec_sub *sub, int dir);
int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg);

View File

@ -265,6 +265,19 @@ struct mp_image *mp_image_alloc(int imgfmt, int w, int h)
return mpi;
}
int mp_image_approx_byte_size(struct mp_image *img)
{
int total = sizeof(*img);
for (int n = 0; n < MP_MAX_PLANES; n++) {
struct AVBufferRef *buf = img->bufs[n];
if (buf)
total += buf->size;
}
return total;
}
struct mp_image *mp_image_new_copy(struct mp_image *img)
{
struct mp_image *new = mp_image_alloc(img->imgfmt, img->w, img->h);

View File

@ -163,6 +163,8 @@ void mp_image_setfmt(mp_image_t* mpi, int out_fmt);
void mp_image_steal_data(struct mp_image *dst, struct mp_image *src);
void mp_image_unref_data(struct mp_image *img);
int mp_image_approx_byte_size(struct mp_image *img);
struct mp_image *mp_image_new_dummy_ref(struct mp_image *img);
struct mp_image *mp_image_new_custom_ref(struct mp_image *img, void *arg,
void (*free)(void *arg));