diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 814d7a6825..9049bf8c9d 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -1462,6 +1462,10 @@ Property list (may not account correctly for various overhead), and stops at the demuxer position (it ignores seek ranges after it). + ``file-cache-bytes`` is the number of bytes stored in the file cache. This + includes all overhead, and possibly unused data (like pruned data). This + member is missing if the file cache is not active. + When querying the property with the client API using ``MPV_FORMAT_NODE``, or with Lua ``mp.get_property_native``, this will return a mpv_node with the following contents: @@ -1476,6 +1480,7 @@ Property list "bof-cached" MPV_FORMAT_FLAG "eof-cached" MPV_FORMAT_FLAG "fw-bytes" MPV_FORMAT_INT64 + "file-cache-bytes" MPV_FORMAT_INT64 Other fields (might be changed or removed in the future): diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index ba9bac7e63..c70b26aa2c 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -3958,6 +3958,33 @@ Cache very high, so the actually achieved readahead will usually be limited by the value of the ``--demuxer-max-bytes`` option. +``--cache-on-disk=`` + Write packet data to a temporary file, instead of keeping them in memory. + This makes sense only with ``--cache``. If the normal cache is disabled, + this option is ignored. + + You need to set ``--cache-dir`` to use this. + + The cache file is append-only. Even if the player appears to prune data, the + file space freed by it is not reused. The cache file is deleted when + playback is closed. + + Note that packet metadata is still kept in memory. ``--demuxer-max-bytes`` + and related options are applied to metadata *only*. The size of this + metadata varies, but 50 MB per hour of media is typical. The cache + statistics will report this metadats size, instead of the size of the cache + file. If the metadata hits the size limits, the metadata is pruned (but not + the cache file). + + When the media is closed, the cache file is deleted. A cache file is + generally worthless after the media is closed, and it's hard to retrieve + any media data from it (it's not supported by design). + +``--cache-dir=`` + Directory where to create temporary files (default: none). + + Currently, this is used for ``--cache-on-disk`` only. + ``--cache-pause=`` Whether the player should automatically pause when the cache runs out of data and stalls decoding/playback (default: yes). If enabled, it will @@ -3986,6 +4013,25 @@ Cache This option also triggers when playback is restarted after seeking. +``--cache-unlink-files=`` + Whether or when to unlink cache files (default: immediate). This affects + cache files which are inherently temporary, and which make no sense to + remain on disk after the player terminates. This is a debugging option. + + ``immediate`` + Unlink cache file after they were created. The cache files won't be + visible anymore, even though they're in use. This ensures they are + guaranteed to be removed from disk when the player terminates, even if + it crashes. + + ``whendone`` + Delete cache files after they are closed. + + ``no`` + Don't delete cache files. They will consume disk space without having a + use. + + Currently, this is used for ``--cache-on-disk`` only. Network ------- diff --git a/demux/cache.c b/demux/cache.c new file mode 100644 index 0000000000..4404c870de --- /dev/null +++ b/demux/cache.c @@ -0,0 +1,323 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "cache.h" +#include "common/msg.h" +#include "common/av_common.h" +#include "demux.h" +#include "options/path.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "osdep/io.h" + +struct demux_cache_opts { + char *cache_dir; + int unlink_files; +}; + +#define OPT_BASE_STRUCT struct demux_cache_opts + +const struct m_sub_options demux_cache_conf = { + .opts = (const struct m_option[]){ + OPT_STRING("cache-dir", cache_dir, M_OPT_FILE), + OPT_CHOICE("cache-unlink-files", unlink_files, 0, + ({"immediate", 2}, {"whendone", 1}, {"no", 0})), + {0} + }, + .size = sizeof(struct demux_cache_opts), + .defaults = &(const struct demux_cache_opts){ + .unlink_files = 2, + }, +}; + +struct demux_cache { + struct mp_log *log; + struct demux_cache_opts *opts; + + char *filename; + bool need_unlink; + int fd; + int64_t file_pos; + uint64_t file_size; +}; + +struct pkt_header { + uint32_t data_len; + uint32_t av_flags; + uint32_t num_sd; +}; + +struct sd_header { + uint32_t av_type; + uint32_t len; +}; + +static void cache_destroy(void *p) +{ + struct demux_cache *cache = p; + + if (cache->fd >= 0) + close(cache->fd); + + if (cache->need_unlink && cache->opts->unlink_files >= 1) { + if (unlink(cache->filename)) + MP_ERR(cache, "Failed to delete cache temporary file.\n"); + } +} + +// Create a cache. This also initializes the cache file from the options. The +// log parameter must stay valid until demux_cache is destroyed. +// Free with talloc_free(). +struct demux_cache *demux_cache_create(struct mpv_global *global, + struct mp_log *log) +{ + struct demux_cache *cache = talloc_zero(NULL, struct demux_cache); + talloc_set_destructor(cache, cache_destroy); + cache->opts = mp_get_config_group(cache, global, &demux_cache_conf); + cache->log = log; + cache->fd = -1; + + char *cache_dir = cache->opts->cache_dir; + if (!(cache_dir && cache_dir[0])) { + MP_ERR(cache, "No cache data directory supplied.\n"); + goto fail; + } + + cache->filename = mp_path_join(cache, cache_dir, "mpv-cache-XXXXXX.dat"); + cache->fd = mp_mkostemps(cache->filename, 4, O_CLOEXEC); + if (cache->fd < 0) { + MP_ERR(cache, "Failed to create cache temporary file.\n"); + goto fail; + } + cache->need_unlink = true; + if (cache->opts->unlink_files >= 2) { + if (unlink(cache->filename)) { + MP_ERR(cache, "Failed to unlink cache temporary file after creation.\n"); + } else { + cache->need_unlink = false; + } + } + + return cache; +fail: + talloc_free(cache); + return NULL; +} + +uint64_t demux_cache_get_size(struct demux_cache *cache) +{ + return cache->file_size; +} + +static bool do_seek(struct demux_cache *cache, uint64_t pos) +{ + if (cache->file_pos == pos) + return true; + + off_t res = lseek(cache->fd, pos, SEEK_SET); + + if (res == (off_t)-1) { + MP_ERR(cache, "Failed to seek in cache file.\n"); + cache->file_pos = -1; + } else { + cache->file_pos = res; + } + + return cache->file_pos >= 0; +} + +static bool write_raw(struct demux_cache *cache, void *ptr, size_t len) +{ + ssize_t res = write(cache->fd, ptr, len); + + if (res < 0) { + MP_ERR(cache, "Failed to write to cache file: %s\n", mp_strerror(errno)); + return false; + } + + cache->file_pos += res; + cache->file_size = MPMAX(cache->file_size, cache->file_pos); + + // Should never happen, unless the disk is full, or someone succeeded to + // trick us to write into a pipe or a socket. + if (res != len) { + MP_ERR(cache, "Could not write all data.\n"); + return false; + } + + return true; +} + +static bool read_raw(struct demux_cache *cache, void *ptr, size_t len) +{ + ssize_t res = read(cache->fd, ptr, len); + + if (res < 0) { + MP_ERR(cache, "Failed to read cache file: %s\n", mp_strerror(errno)); + return false; + } + + cache->file_pos += res; + + // Should never happen, unless the file was cut short, or someone succeeded + // to rick us to write into a pipe or a socket. + if (res != len) { + MP_ERR(cache, "Could not read all data.\n"); + return false; + } + + return true; +} + +// Serialize a packet to the cache file. Returns the packet position, which can +// be passed to demux_cache_read() to read the packet again. +// Returns a negative value on errors, i.e. writing the file failed. +int64_t demux_cache_write(struct demux_cache *cache, struct demux_packet *dp) +{ + assert(dp->avpacket); + + // AV_PKT_FLAG_TRUSTED usually means there are embedded pointers and such + // in the packet data. The pointer will become invalid if the packet is + // unreferenced. + if (dp->avpacket->flags & AV_PKT_FLAG_TRUSTED) { + MP_ERR(cache, "Cannot serialize this packet to cache file.\n"); + return -1; + } + + assert(!dp->is_cached); + assert(dp->len >= 0 && dp->len <= INT32_MAX); + assert(dp->avpacket->flags >= 0 && dp->avpacket->flags <= INT32_MAX); + assert(dp->avpacket->side_data_elems >= 0 && + dp->avpacket->side_data_elems <= INT32_MAX); + + if (!do_seek(cache, cache->file_size)) + return -1; + + uint64_t pos = cache->file_pos; + + struct pkt_header hd = { + .data_len = dp->len, + .av_flags = dp->avpacket->flags, + .num_sd = dp->avpacket->side_data_elems, + }; + + if (!write_raw(cache, &hd, sizeof(hd))) + goto fail; + + if (!write_raw(cache, dp->buffer, dp->len)) + goto fail; + + // The handling of FFmpeg side data requires an extra long comment to + // explain why this code is fragile and insane. + // FFmpeg packet side data is per-packet out of band data, that contains + // further information for the decoder (extra metadata and such), which is + // not part of the codec itself and thus isn't contained in the packet + // payload. All types use a flat byte array. The format of this byte array + // is non-standard and FFmpeg-specific, and depends on the side data type + // field. The side data type is of course a FFmpeg ABI artifact. + // In some cases, the format is described as fixed byte layout. In others, + // it contains a struct, i.e. is bound to FFmpeg ABI. Some newer types make + // the format explicitly internal (and _not_ part of the ABI), and you need + // to use separate accessors to turn it into complex data structures. + // As of now, FFmpeg fortunately adheres to the idea that side data can not + // contain embedded pointers (due to API rules, but also because they forgot + // adding a refcount field, and can't change this until they break ABI). + // We rely on this. We hope that FFmpeg won't silently change their + // semantics, and add refcounting and embedded pointers. This way we can + // for example dump the data in a disk cache, even though we can't use the + // data from another process or if this process is restarted (unless we're + // absolutely sure the FFmpeg internals didn't change). The data has to be + // treated as a memory dump. + for (int n = 0; n < dp->avpacket->side_data_elems; n++) { + AVPacketSideData *sd = &dp->avpacket->side_data[n]; + + assert(sd->size >= 0 && sd->size <= INT32_MAX); + assert(sd->type >= 0 && sd->type <= INT32_MAX); + + struct sd_header sd_hd = { + .av_type = sd->type, + .len = sd->size, + }; + + if (!write_raw(cache, &sd_hd, sizeof(sd_hd))) + goto fail; + if (!write_raw(cache, sd->data, sd->size)) + goto fail; + } + + return pos; + +fail: + // Reset file_size (try not to append crap forever). + do_seek(cache, pos); + cache->file_size = cache->file_pos; + return -1; +} + +struct demux_packet *demux_cache_read(struct demux_cache *cache, uint64_t pos) +{ + if (!do_seek(cache, pos)) + return NULL; + + struct pkt_header hd; + + if (!read_raw(cache, &hd, sizeof(hd))) + return NULL; + + if (hd.data_len >= (size_t)-1) + return NULL; + + struct demux_packet *dp = new_demux_packet(hd.data_len); + if (!dp) + goto fail; + + if (!read_raw(cache, dp->buffer, dp->len)) + goto fail; + + dp->avpacket->flags = hd.av_flags; + + for (uint32_t n = 0; n < hd.num_sd; n++) { + struct sd_header sd_hd; + + if (!read_raw(cache, &sd_hd, sizeof(sd_hd))) + goto fail; + + if (sd_hd.len > INT_MAX) + goto fail; + + uint8_t *sd = av_packet_new_side_data(dp->avpacket, sd_hd.av_type, + sd_hd.len); + if (!sd) + goto fail; + + if (!read_raw(cache, sd, sd_hd.len)) + goto fail; + } + + return dp; + +fail: + talloc_free(dp); + return NULL; +} diff --git a/demux/cache.h b/demux/cache.h new file mode 100644 index 0000000000..95ea9649c0 --- /dev/null +++ b/demux/cache.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct demux_packet; +struct mp_log; +struct mpv_global; + +struct demux_cache; + +struct demux_cache *demux_cache_create(struct mpv_global *global, + struct mp_log *log); + +int64_t demux_cache_write(struct demux_cache *cache, struct demux_packet *pkt); +struct demux_packet *demux_cache_read(struct demux_cache *cache, uint64_t pos); +uint64_t demux_cache_get_size(struct demux_cache *cache); diff --git a/demux/demux.c b/demux/demux.c index 5fc81ad41e..b9101a3054 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -29,6 +29,7 @@ #include #include +#include "cache.h" #include "config.h" #include "options/m_config.h" #include "options/m_option.h" @@ -80,6 +81,7 @@ static const demuxer_desc_t *const demuxer_list[] = { struct demux_opts { int enable_cache; + int disk_cache; int64_t max_bytes; int64_t max_bytes_bw; double min_secs; @@ -103,6 +105,7 @@ const struct m_sub_options demux_conf = { .opts = (const struct m_option[]){ OPT_CHOICE("cache", enable_cache, 0, ({"no", 0}, {"auto", -1}, {"yes", 1})), + OPT_FLAG("cache-on-disk", disk_cache, 0), OPT_DOUBLE("demuxer-readahead-secs", min_secs, M_OPT_MIN, .min = 0), // (The MAX_BYTES sizes may not be accurate because the max field is // of double type.) @@ -179,6 +182,8 @@ struct demux_internal { int events; + struct demux_cache *cache; + bool warned_queue_overflow; bool last_eof; // last actual global EOF status bool eof; // whether we're in EOF state (reset for retry) @@ -1039,6 +1044,10 @@ static void demux_shutdown(struct demux_internal *in) in->current_range = NULL; free_empty_cached_ranges(in); + + talloc_free(in->cache); + in->cache = NULL; + if (in->owns_stream) free_stream(demuxer->stream); demuxer->stream = NULL; @@ -1612,9 +1621,10 @@ static void attempt_range_joining(struct demux_internal *in) // in case pos/dts are not "correct" across the ranges (we // never actually check that). if (dp->dts != end->dts || dp->pos != end->pos || - dp->pts != end->pts || dp->len != end->len) + dp->pts != end->pts) { - MP_WARN(in, "stream %d: weird demuxer behavior\n", n); + MP_WARN(in, + "stream %d: non-repeatable demuxer behavior\n", n); goto failed; } @@ -1822,6 +1832,36 @@ static void adjust_seek_range_on_packet(struct demux_stream *ds, } } +static void record_packet(struct demux_internal *in, struct demux_packet *dp) +{ + // (should preferably be outside of the lock) + if (in->enable_recording && !in->recorder && + in->opts->record_file && in->opts->record_file[0]) + { + // Later failures shouldn't make it retry and overwrite the previously + // recorded file. + in->enable_recording = false; + + in->recorder = + mp_recorder_create(in->d_thread->global, in->opts->record_file, + in->streams, in->num_streams); + if (!in->recorder) + MP_ERR(in, "Disabling recording.\n"); + } + + if (in->recorder) { + struct mp_recorder_sink *sink = + mp_recorder_get_sink(in->recorder, dp->stream); + if (sink) { + mp_recorder_feed_packet(sink, dp); + } else { + MP_ERR(in, "New stream appeared; stopping recording.\n"); + mp_recorder_destroy(in->recorder); + in->recorder = NULL; + } + } +} + static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp) { struct demux_stream *ds = stream ? stream->ds : NULL; @@ -1864,6 +1904,17 @@ static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp) return; } + record_packet(in, dp); + + if (in->cache) { + int64_t pos = demux_cache_write(in->cache, dp); + if (pos >= 0) { + demux_packet_unref_contents(dp); + dp->is_cached = true; + dp->cached_data.pos = pos; + } + } + queue->correct_pos &= dp->pos >= 0 && dp->pos > queue->last_pos; queue->correct_dts &= dp->dts != MP_NOPTS_VALUE && dp->dts > queue->last_dts; queue->last_pos = dp->pos; @@ -1940,33 +1991,6 @@ static void add_packet_locked(struct sh_stream *stream, demux_packet_t *dp) if (!ds->reader_head) return; - // (should preferably be outside of the lock) - if (in->enable_recording && !in->recorder && - in->opts->record_file && in->opts->record_file[0]) - { - // Later failures shouldn't make it retry and overwrite the previously - // recorded file. - in->enable_recording = false; - - in->recorder = - mp_recorder_create(in->d_thread->global, in->opts->record_file, - in->streams, in->num_streams); - if (!in->recorder) - MP_ERR(in, "Disabling recording.\n"); - } - - if (in->recorder) { - struct mp_recorder_sink *sink = - mp_recorder_get_sink(in->recorder, dp->stream); - if (sink) { - mp_recorder_feed_packet(sink, dp); - } else { - MP_ERR(in, "New stream appeared; stopping recording.\n"); - mp_recorder_destroy(in->recorder); - in->recorder = NULL; - } - } - back_demux_see_packets(ds); wakeup_ds(ds); @@ -2396,11 +2420,21 @@ static int dequeue_packet(struct demux_stream *ds, struct demux_packet **res) struct demux_packet *pkt = advance_reader_head(ds); assert(pkt); - // The returned packet is mutated etc. and will be owned by the user. - pkt = demux_copy_packet(pkt); + if (pkt->is_cached) { + assert(in->cache); + struct demux_packet *meta = pkt; + pkt = demux_cache_read(in->cache, pkt->cached_data.pos); + if (pkt) { + demux_packet_copy_attribs(pkt, meta); + } else { + MP_ERR(in, "Failed to retrieve packet from cache.\n"); + } + } else { + // The returned packet is mutated etc. and will be owned by the user. + pkt = demux_copy_packet(pkt); + } if (!pkt) - abort(); - pkt->next = NULL; + return 0; if (in->back_demuxing) { if (pkt->keyframe) { @@ -3007,11 +3041,19 @@ static struct demuxer *open_given_type(struct mpv_global *global, timeline_destroy(tl); } } + if (!(params && params->is_top_level) || sub) { in->seekable_cache = false; in->min_secs = 0; in->max_bytes = 1; } + + if (in->seekable_cache && opts->disk_cache) { + in->cache = demux_cache_create(global, log); + if (!in->cache) + MP_ERR(in, "Failed to create file cache.\n"); + } + switch_to_fresh_cache_range(in); demux_update(demuxer, MP_NOPTS_VALUE); @@ -3803,6 +3845,7 @@ void demux_get_reader_state(struct demuxer *demuxer, struct demux_reader_state * .low_level_seeks = in->low_level_seeks, .ts_last = in->demux_ts, .bytes_per_second = in->bytes_per_second, + .file_cache_bytes = in->cache ? demux_cache_get_size(in->cache) : -1, }; bool any_packets = false; for (int n = 0; n < in->num_streams; n++) { diff --git a/demux/demux.h b/demux/demux.h index 99acc1f015..14d145704a 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -44,6 +44,7 @@ struct demux_reader_state { double ts_end; // approx. timestamp of end of buffered range int64_t total_bytes; int64_t fw_bytes; + int64_t file_cache_bytes; double seeking; // current low level seek target, or NOPTS int low_level_seeks; // number of started low level seeks double ts_last; // approx. timestamp of demuxer position diff --git a/demux/packet.c b/demux/packet.c index 60c3e6aba0..32d799f9ce 100644 --- a/demux/packet.c +++ b/demux/packet.c @@ -31,10 +31,25 @@ #include "packet.h" +// Free any refcounted data dp holds (but don't free dp itself). This does not +// care about pointers that are _not_ refcounted (like demux_packet.codec). +// Normally, a user should use talloc_free(dp). This function is only for +// annoyingly specific obscure use cases. +void demux_packet_unref_contents(struct demux_packet *dp) +{ + if (dp->avpacket) { + av_packet_unref(dp->avpacket); + assert(!dp->is_cached); + dp->avpacket = NULL; + dp->buffer = NULL; + dp->len = 0; + } +} + static void packet_destroy(void *ptr) { struct demux_packet *dp = ptr; - av_packet_unref(dp->avpacket); + demux_packet_unref_contents(dp); } // This actually preserves only data and side data, not PTS/DTS/pos/etc. @@ -161,8 +176,9 @@ struct demux_packet *demux_copy_packet(struct demux_packet *dp) size_t demux_packet_estimate_total_size(struct demux_packet *dp) { size_t size = ROUND_ALLOC(sizeof(struct demux_packet)); - size += ROUND_ALLOC(dp->len); if (dp->avpacket) { + assert(!dp->is_cached); + size += ROUND_ALLOC(dp->len); size += ROUND_ALLOC(sizeof(AVPacket)); size += ROUND_ALLOC(sizeof(AVBufferRef)); size += 64; // upper bound estimate on sizeof(AVBuffer) diff --git a/demux/packet.h b/demux/packet.h index f4570004e8..cd1183d417 100644 --- a/demux/packet.h +++ b/demux/packet.h @@ -29,16 +29,29 @@ typedef struct demux_packet { double duration; int64_t pos; // position in source file byte stream - unsigned char *buffer; - size_t len; + union { + // Normally valid for packets. + struct { + unsigned char *buffer; + size_t len; + }; + + // Used if is_cached==true, special uses only. + struct { + uint64_t pos; + } cached_data; + }; int stream; // source stream index (typically sh_stream.index) bool keyframe; // backward playback - bool back_restart; // restart point (reverse and return previous frames) - bool back_preroll; // initial discarded frame for smooth decoder reinit + bool back_restart : 1; // restart point (reverse and return previous frames) + bool back_preroll : 1; // initial discarded frame for smooth decoder reinit + + // If true, cached_data is valid, while buffer/len are not. + bool is_cached : 1; // segmentation (ordered chapters, EDL) bool segmented; @@ -68,4 +81,6 @@ int demux_packet_set_padding(struct demux_packet *dp, int start, int end); int demux_packet_add_blockadditional(struct demux_packet *dp, uint64_t id, void *data, size_t size); +void demux_packet_unref_contents(struct demux_packet *dp); + #endif /* MPLAYER_DEMUX_PACKET_H */ diff --git a/options/options.c b/options/options.c index 2e373f7469..a4d04c68f7 100644 --- a/options/options.c +++ b/options/options.c @@ -73,6 +73,7 @@ extern const struct m_sub_options gl_video_conf; extern const struct m_sub_options ao_alsa_conf; extern const struct m_sub_options demux_conf; +extern const struct m_sub_options demux_cache_conf; extern const struct m_obj_list vf_obj_list; extern const struct m_obj_list af_obj_list; @@ -700,6 +701,7 @@ const m_option_t mp_opts[] = { OPT_SUBSTRUCT("", vo, vo_sub_opts, 0), OPT_SUBSTRUCT("", demux_opts, demux_conf, 0), + OPT_SUBSTRUCT("", demux_cache_opts, demux_cache_conf, 0), OPT_SUBSTRUCT("", gl_video_opts, gl_video_conf, 0), OPT_SUBSTRUCT("", spirv_opts, spirv_conf, 0), diff --git a/options/options.h b/options/options.h index 8489c79aa9..182b901cd4 100644 --- a/options/options.h +++ b/options/options.h @@ -303,6 +303,7 @@ typedef struct MPOpts { struct demux_mkv_opts *demux_mkv; struct demux_opts *demux_opts; + struct demux_cache_opts *demux_cache_opts; struct vd_lavc_params *vd_lavc_params; struct ad_lavc_params *ad_lavc_params; diff --git a/player/command.c b/player/command.c index 333e4fe784..c4d492cdf9 100644 --- a/player/command.c +++ b/player/command.c @@ -1532,6 +1532,8 @@ static int mp_property_demuxer_cache_state(void *ctx, struct m_property *prop, node_map_add_flag(r, "idle", s.idle); node_map_add_int64(r, "total-bytes", s.total_bytes); node_map_add_int64(r, "fw-bytes", s.fw_bytes); + if (s.file_cache_bytes >= 0) + node_map_add_int64(r, "file-cache-bytes", s.file_cache_bytes); if (s.seeking != MP_NOPTS_VALUE) node_map_add_double(r, "debug-seeking", s.seeking); node_map_add_int64(r, "debug-low-level-seeks", s.low_level_seeks); diff --git a/wscript_build.py b/wscript_build.py index 127fe820fe..51bb01fac4 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -273,6 +273,7 @@ def build(ctx): ## Demuxers ( "demux/codec_tags.c" ), ( "demux/cue.c" ), + ( "demux/cache.c" ), ( "demux/demux.c" ), ( "demux/demux_cue.c" ), ( "demux/demux_edl.c" ),