summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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);
+
+ 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;
+ }
+
+