summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwm4 <wm4@nowhere>2019-05-18 02:10:51 +0200
committerwm4 <wm4@nowhere>2019-09-19 20:37:04 +0200
commitb9d351f02a3266b76256a90fc9c51f9d3cbf185d (patch)
tree4025ca41166baf946ebf8ad2c09066f2fb796054
parentfc4e59f25d68aeb2a33333b01f12a440d5b737e6 (diff)
downloadmpv-b9d351f02a3266b76256a90fc9c51f9d3cbf185d.tar.bz2
mpv-b9d351f02a3266b76256a90fc9c51f9d3cbf185d.tar.xz
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.
-rw-r--r--DOCS/man/options.rst191
-rw-r--r--audio/aframe.c50
-rw-r--r--audio/aframe.h4
-rw-r--r--demux/demux.c425
-rw-r--r--demux/demux.h1
-rw-r--r--demux/packet.c2
-rw-r--r--demux/packet.h4
-rw-r--r--filters/f_decoder_wrapper.c128
-rw-r--r--filters/f_decoder_wrapper.h1
-rw-r--r--filters/frame.c20
-rw-r--r--filters/frame.h3
-rw-r--r--options/options.c8
-rw-r--r--options/options.h3
-rw-r--r--player/command.c15
-rw-r--r--player/core.h1
-rw-r--r--player/loadfile.c5
-rw-r--r--player/main.c1
-rw-r--r--player/playloop.c27
-rw-r--r--sub/dec_sub.c12
-rw-r--r--sub/dec_sub.h1
-rw-r--r--video/mp_image.c13
-rw-r--r--video/mp_image.h2
22 files changed, 874 insertions, 43 deletions
diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst
index 8da721c834..f49b5f4f46 100644
--- a/DOCS/man/options.rst
+++ b/DOCS/man/options.rst
@@ -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
----------------
diff --git a/audio/aframe.c b/audio/aframe.c
index cb5d412f98..bc43bc98d2 100644
--- a/audio/aframe.c
+++ b/audio/aframe.c
@@ -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;
diff --git a/audio/aframe.h b/audio/aframe.h
index ed92c223f6..21d4494f5f 100644
--- a/audio/aframe.h
+++ b/audio/aframe.h
@@ -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))
diff --git a/demux/demux.c b/demux/demux.c
index ae6d3a96f6..92121c9b7b 100644
--- a/demux/demux.c
+++ b/demux/demux.c
@@ -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);
+
+