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);
+
+ 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,
diff --git a/demux/demux.h b/demux/demux.h
index 085fa26cff..da315adebb 100644
--- a/demux/demux.h
+++ b/demux/demux.h
@@ -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.
diff --git a/demux/packet.c b/demux/packet.c
index 98ca24d93d..f2291c21bb 100644
--- a/demux/packet.c
+++ b/demux/packet.c
@@ -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);
diff --git a/demux/packet.h b/demux/packet.h
index a5df6ce121..9e28ed52d0 100644
--- a/demux/packet.h
+++ b/demux/packet.h
@@ -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
diff --git a/filters/f_decoder_wrapper.c b/filters/f_decoder_wrapper.c
index 0edca5c83d..0b2a4f52ec 100644
--- a/filters/f_decoder_wrapper.c
+++ b/filters/f_decoder_wrapper.c
@@ -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");
diff --git a/filters/f_decoder_wrapper.h b/filters/f_decoder_wrapper.h
index 119e0f9eb6..b69c0c7680 100644
--- a/filters/f_decoder_wrapper.h
+++ b/filters/f_decoder_wrapper.h
@@ -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
diff --git a/filters/frame.c b/filters/frame.c
index f1d4c98eab..200e900fa2 100644
--- a/filters/frame.c
+++ b/filters/frame.c
@@ -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)
diff --git a/filters/frame.h b/filters/frame.h
index 606ca38846..4c6c4ef127 100644
--- a/filters/frame.h
+++ b/filters/frame.h
@@ -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);
diff --git a/options/options.c b/options/options.c
index 2c2fcc941d..65c6d7234e 100644
--- a/options/options.c
+++ b/options/options.c
@@ -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,
diff --git a/options/options.h b/options/options.h
index 5f7c560e71..26693221a3 100644
--- a/options/options.h
+++ b/options/options.h
@@ -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;
diff --git a/player/command.c b/player/command.c
index 6682398b8b..68ce861c6f 100644
--- a/player/command.c
+++ b/player/command.c
@@ -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"),
diff --git a/player/core.h b/player/core.h
index f0a66ffdff..d2183a7537 100644
--- a/player/core.h
+++ b/player/core.h
@@ -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;
diff --git a/player/loadfile.c b/player/loadfile.c
index f4fad8a914..c16b8e8152 100644
--- a/player/loadfile.c
+++ b/player/loadfile.c
@@ -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
diff --git a/player/main.c b/player/main.c
index 0a11bcf7d2..e1f3285984 100644
--- a/player/main.c
+++ b/player/main.c
@@ -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);
diff --git a/player/playloop.c b/player/playloop.c
index 61ade84755..85890c2ef1 100644
--- a/player/playloop.c
+++ b/player/playloop.c
@@ -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
diff --git a/sub/dec_sub.c b/sub/dec_sub.c
index ae6a064be2..6a0a753076 100644
--- a/sub/dec_sub.c
+++ b/sub/dec_sub.c
@@ -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);
+}
diff --git a/sub/dec_sub.h b/sub/dec_sub.h
index 3303cc9a5c..06d4a6127e 100644
--- a/sub/dec_sub.h
+++ b/sub/dec_sub.h
@@ -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);
diff --git a/video/mp_image.c b/video/mp_image.c
index b5780b08a0..f846b0d3d3 100644
--- a/video/mp_image.c
+++ b/video/mp_image.c
@@ -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);
diff --git a/video/mp_image.h b/video/mp_image.h
index d321a27285..4727ed6f8d 100644
--- a/video/mp_image.h
+++ b/video/mp_image.h
@@ -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));