From ece1c034d51bb05c558bf2d208bba8c74a14109a Mon Sep 17 00:00:00 2001 From: Andrew Rabert <6550543+andrewrabert@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:38:45 -0500 Subject: [PATCH] Expose mpv buffered ranges to the web player (#1166) Observe demuxer-cache-state from mpv, extract seekable-ranges, and emit them as a QVariantList signal. JavaScript connects to the signal and stores the ranges for getBufferedRanges(). --- native/mpvAudioPlayer.js | 11 ++++++++++- native/mpvVideoPlayer.js | 17 ++++++++++++++++- src/player/PlayerComponent.cpp | 18 ++++++++++++++++++ src/player/PlayerComponent.h | 4 ++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/native/mpvAudioPlayer.js b/native/mpvAudioPlayer.js index a7d5619..e8a2b13 100644 --- a/native/mpvAudioPlayer.js +++ b/native/mpvAudioPlayer.js @@ -50,12 +50,14 @@ class mpvAudioPlayer { self._volume = self.getSavedVolume() * 100; self._playRate = 1; self._hasConnection = false; + self._bufferedRanges = []; self.play = (options) => { self._started = false; self._timeUpdated = false; self._currentTime = null; self._duration = undefined; + self._bufferedRanges = []; const player = window.api.player; if (!self._hasConnection) { @@ -66,6 +68,7 @@ class mpvAudioPlayer { player.updateDuration.connect(onDuration); player.error.connect(onError); player.paused.connect(onPause); + player.bufferedRangesUpdated.connect(onBufferedRangesUpdated); } return setCurrentSrc(options); @@ -139,6 +142,8 @@ class mpvAudioPlayer { player.updateDuration.disconnect(onDuration); player.error.disconnect(onError); player.paused.disconnect(onPause); + player.bufferedRangesUpdated.disconnect(onBufferedRangesUpdated); + self._bufferedRanges = []; }; function onDuration(duration) { @@ -180,6 +185,10 @@ class mpvAudioPlayer { self.events.trigger(self, 'pause'); } + function onBufferedRangesUpdated(ranges) { + self._bufferedRanges = ranges; + } + function onError(error) { console.error(`media error: ${error}`); toast(`media error: ${error}`); @@ -240,7 +249,7 @@ class mpvAudioPlayer { } getBufferedRanges() { - return []; + return this._bufferedRanges; } pause() { diff --git a/native/mpvVideoPlayer.js b/native/mpvVideoPlayer.js index eb78914..43f9e1d 100644 --- a/native/mpvVideoPlayer.js +++ b/native/mpvVideoPlayer.js @@ -114,6 +114,17 @@ * @type {boolean} */ this._hasConnection = false; + /** + * @type {Array<{start: number, end: number}>} + */ + this._bufferedRanges = []; + + /** + * @private + */ + this.onBufferedRangesUpdated = (ranges) => { + this._bufferedRanges = ranges; + }; /** * @private @@ -234,6 +245,7 @@ this._started = false; this._timeUpdated = false; this._currentTime = null; + this._bufferedRanges = []; this.resetSubtitleOffset(); if (options.fullscreen) { @@ -481,6 +493,7 @@ const player = window.api.player; this._hasConnection = false; + this._bufferedRanges = []; player.playing.disconnect(this.onPlaying); player.positionUpdate.disconnect(this.onTimeUpdate); player.finished.disconnect(this.onEnded); @@ -488,6 +501,7 @@ player.updateDuration.disconnect(this.onDuration); player.error.disconnect(this.onError); player.paused.disconnect(this.onPause); + player.bufferedRangesUpdated.disconnect(this.onBufferedRangesUpdated); } /** @@ -535,6 +549,7 @@ player.updateDuration.connect(this.onDuration); player.error.connect(this.onError); player.paused.connect(this.onPause); + player.bufferedRangesUpdated.connect(this.onBufferedRangesUpdated); // Log all other signals player.buffering.connect((percent) => { @@ -804,7 +819,7 @@ } getBufferedRanges() { - return []; + return this._bufferedRanges; } getStats() { diff --git a/src/player/PlayerComponent.cpp b/src/player/PlayerComponent.cpp index 32b6e88..297c77b 100644 --- a/src/player/PlayerComponent.cpp +++ b/src/player/PlayerComponent.cpp @@ -186,6 +186,7 @@ void PlayerComponent::initializeMpv() mpv_observe_property(m_mpv->mpv(), 0, "duration", MPV_FORMAT_DOUBLE); mpv_observe_property(m_mpv->mpv(), 0, "audio-device-list", MPV_FORMAT_NODE); mpv_observe_property(m_mpv->mpv(), 0, "video-dec-params", MPV_FORMAT_NODE); + mpv_observe_property(m_mpv->mpv(), 0, "demuxer-cache-state", MPV_FORMAT_NODE); // Setup a hook with the ID 1, which is run during the file is loaded. // Used to delay playback start for display framerate switching. @@ -569,6 +570,23 @@ void PlayerComponent::handleMpvEvent(mpv_event *event) { updateAudioDeviceList(); } + else if (strcmp(prop->name, "demuxer-cache-state") == 0 && prop->format == MPV_FORMAT_NODE) + { + constexpr double ticksPerSecond = 10000000.0; // 100ns ticks (Jellyfin's internal unit) + auto *node = static_cast(prop->data); + QVariantMap cacheState = mpv::qt::node_to_variant(node).toMap(); + QVariantList seekableRanges = cacheState[QStringLiteral("seekable-ranges")].toList(); + QVariantList ranges; + for (const QVariant &entry : seekableRanges) + { + QVariantMap rangeMap = entry.toMap(); + QVariantMap r; + r[QStringLiteral("start")] = static_cast(rangeMap[QStringLiteral("start")].toDouble() * ticksPerSecond); + r[QStringLiteral("end")] = static_cast(rangeMap[QStringLiteral("end")].toDouble() * ticksPerSecond); + ranges.append(r); + } + emit bufferedRangesUpdated(ranges); + } else if (strcmp(prop->name, "video-dec-params") == 0) { // Aspect might be known now (or it changed during playback), so update settings diff --git a/src/player/PlayerComponent.h b/src/player/PlayerComponent.h index fdb03dd..99eec57 100644 --- a/src/player/PlayerComponent.h +++ b/src/player/PlayerComponent.h @@ -219,6 +219,10 @@ Q_SIGNALS: void seekPerformed(qint64 positionMs); void metadataChanged(const QVariantMap& metadata); void volumeChanged(double volume); + + // Buffered ranges from demuxer-cache-state, as list of {start, end} in ticks + void bufferedRangesUpdated(const QVariantList& ranges); + private: // this is the function actually implemented in the backends. the variantmap contains // a few known keys: