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().
This commit is contained in:
Andrew Rabert authored and GitHub committed 2026-02-26 21:38:45 -05:00
1 parent 75d19da3a0
commit ece1c034d5
4 files changed
+48 -2

No files matched your search

+10 -1
View File
@@ -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() {
+16 -1
View File
@@ -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() {
+18
View File
@@ -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<mpv_node*>(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<qint64>(rangeMap[QStringLiteral("start")].toDouble() * ticksPerSecond);
r[QStringLiteral("end")] = static_cast<qint64>(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
+4
View File
@@ -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: