From 6a58adfb2993d6f9f55d993c417aa1dbcfffd697 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 10 Jul 2026 13:35:51 -0400 Subject: [PATCH] osd/media playback: delay showing until album art is ready and avoid re-showing when duplicate metadata comes from mpris related #2787 port 1.5 --- quickshell/Modules/OSD/MediaPlaybackOSD.qml | 92 ++++++++++++++++----- quickshell/Modules/OSD/MicVolumeOSD.qml | 3 +- quickshell/Services/TrackArtService.qml | 5 ++ 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/quickshell/Modules/OSD/MediaPlaybackOSD.qml b/quickshell/Modules/OSD/MediaPlaybackOSD.qml index 91c602ddf..10d969dbd 100644 --- a/quickshell/Modules/OSD/MediaPlaybackOSD.qml +++ b/quickshell/Modules/OSD/MediaPlaybackOSD.qml @@ -23,7 +23,7 @@ DankOSD { if (!player) { _displayIcon = "music_note"; iconDebounce.stop(); - return; + return false; } let icon = "music_note"; switch (player.playbackState) { @@ -35,10 +35,13 @@ DankOSD { icon = "play_arrow"; break; } - if (icon === _displayIcon) - return; + if (icon === _displayIcon) { + iconDebounce.stop(); + return false; + } iconDebounce.pendingIcon = icon; iconDebounce.restart(); + return true; } function togglePlaying() { @@ -52,6 +55,29 @@ DankOSD { property string _displayArtist: "" property string _displayAlbum: "" + function _showPending() { + _pendingShow = false; + pendingShowFallback.stop(); + show(); + } + + function _evaluateShow() { + // art url can land in a later metadata update than the title + if (TrackArtService.getArtworkUrl(player) === "") { + _pendingShow = true; + pendingShowFallback.interval = 600; + pendingShowFallback.restart(); + return; + } + if (TrackArtService.artReadyFor(player) && artPreloader.status === Image.Ready) { + _showPending(); + return; + } + _pendingShow = true; + pendingShowFallback.interval = 1500; + pendingShowFallback.restart(); + } + Timer { id: iconDebounce interval: 150 @@ -59,6 +85,17 @@ DankOSD { onTriggered: root._displayIcon = pendingIcon } + Timer { + id: pendingShowFallback + interval: 1500 + onTriggered: { + if (!root._pendingShow) + return; + root._pendingShow = false; + root.show(); + } + } + Image { id: artPreloader source: TrackArtService.resolvedArtUrl @@ -70,6 +107,7 @@ DankOSD { onPlayerChanged: { if (!player) { _pendingShow = false; + pendingShowFallback.stop(); hide(); } } @@ -77,12 +115,19 @@ DankOSD { Connections { target: TrackArtService function onLoadingChanged() { - if (TrackArtService.loading || !root._pendingShow) + if (!root._pendingShow) + return; + if (TrackArtService.loading) { + pendingShowFallback.interval = 1500; + pendingShowFallback.restart(); return; - if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) { - root._pendingShow = false; - root.show(); } + if (!TrackArtService.resolvedArtUrl) { + root._showPending(); + return; + } + if (TrackArtService.artReadyFor(root.player) && artPreloader.status === Image.Ready) + root._showPending(); } } @@ -94,8 +139,7 @@ DankOSD { switch (artPreloader.status) { case Image.Ready: case Image.Error: - root._pendingShow = false; - root.show(); + root._showPending(); break; } } @@ -112,26 +156,32 @@ DankOSD { if (MprisController.isFirefoxYoutubeHoverPreview(player)) return; - root._displayTitle = player.trackTitle || ""; - root._displayArtist = player.trackArtist || ""; - root._displayAlbum = player.trackAlbum || ""; + const newTitle = player.trackTitle || ""; + const newArtist = player.trackArtist || ""; + const newAlbum = player.trackAlbum || ""; + const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum; - root.updatePlaybackIcon(); - const resolvedArtUrl = TrackArtService.resolvedArtUrl; + root._displayTitle = newTitle; + root._displayArtist = newArtist; + root._displayAlbum = newAlbum; - if (!resolvedArtUrl || resolvedArtUrl === "") { + const iconChanged = root.updatePlaybackIcon(); + + // live streams re-emit metadata as mpris:length grows - ignore churn + if (!trackChanged && !iconChanged) + return; + + // vertical layout has no art background + if (root.useVertical) { root.show(); return; } - if (TrackArtService.loading) { - root._pendingShow = true; + if (trackChanged) { + root._evaluateShow(); return; } - if (!TrackArtService.resolvedArtUrl || artPreloader.status === Image.Ready) { + if (!root._pendingShow) root.show(); - return; - } - root._pendingShow = true; } function onTrackArtUrlChanged() { diff --git a/quickshell/Modules/OSD/MicVolumeOSD.qml b/quickshell/Modules/OSD/MicVolumeOSD.qml index 32a6d8567..8939826a2 100644 --- a/quickshell/Modules/OSD/MicVolumeOSD.qml +++ b/quickshell/Modules/OSD/MicVolumeOSD.qml @@ -23,8 +23,7 @@ DankOSD { Connections { target: AudioService.source?.audio ?? null - // Sync only - apps adjusting mic gain (WebRTC AGC etc.) fire this - // constantly, so external volume changes must not surface the OSD + // sync only - app AGC rewrites mic gain constantly, must not surface the OSD function onVolumeChanged() { root._syncVolume(); } diff --git a/quickshell/Services/TrackArtService.qml b/quickshell/Services/TrackArtService.qml index 6962a01a5..ed6c152d0 100644 --- a/quickshell/Services/TrackArtService.qml +++ b/quickshell/Services/TrackArtService.qml @@ -191,6 +191,11 @@ Singleton { return (p.trackTitle || "") + "" + (p.trackArtist || "") + "" + (p.trackAlbum || ""); } + function artReadyFor(player) { + const url = getArtworkUrl(player); + return url !== "" && url === _lastArtUrl && !loading && resolvedArtUrl !== ""; + } + function _updateArtUrl() { const key = _trackKey(); // Skip once real art is committed for this track (dedup Chrome's multi-size