From d01ef2d5d0ab5048ad8e2c662bdbf1e7d65479bd Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:23:21 +0200 Subject: [PATCH 1/8] Let OWE own video backgrounds while it runs The background plugin now watches for the OWE daemon socket. While OWE is running, the desktop yields video playback to it and the shell keeps stills. The lock screen keeps its own playback. This lets Omarchy cooperate with OWE without OWE editing shell.json, so the engine can ship as a package. Add a package-list note that owe-wallpaper-engine must be added once it is packaged. --- install/omarchy-base.packages | 2 ++ manual/39-backgrounds.md | 2 +- shell/Ui/BackgroundMedia.qml | 6 +++++- shell/plugins/background/Background.qml | 25 ++++++++++++++++++++++++- test/shell.d/video-background-test.sh | 9 +++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 76e74110..b38c68b8 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -107,6 +107,8 @@ python-poetry-core ttfx qemu-user-static-binfmt qrencode +# TODO: add owe-wallpaper-engine here once it is packaged. The shell yields +# video backgrounds to it while its daemon runs. qt6-imageformats qt6-multimedia qt6-multimedia-ffmpeg diff --git a/manual/39-backgrounds.md b/manual/39-backgrounds.md index f2cd51eb..f0291653 100644 --- a/manual/39-backgrounds.md +++ b/manual/39-backgrounds.md @@ -4,6 +4,6 @@ Every theme ships with its own set of backgrounds, and you can add extras of you You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`. -Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images, playing on a loop. Only your first monitor's wallpaper plays a video's sound track, through the default audio output at the system volume, and the lock screen stays silent. Playback stops on its own whenever nothing can see it — while a fullscreen window covers that monitor, while the screensaver is up, and once a locked screen has gone dark — but a video wallpaper still costs far more power than a still one, and each monitor decodes its own copy. +Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images, playing on a loop. Only your first monitor's wallpaper plays a video's sound track, through the default audio output at the system volume, and the lock screen stays silent. Playback stops on its own whenever nothing can see it — while a fullscreen window covers that monitor, while the screensaver is up, and once a locked screen has gone dark — but a video wallpaper still costs far more power than a still one, and each monitor decodes its own copy. When the OWE wallpaper engine is running, it takes over video backgrounds and the shell keeps only stills. You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls. diff --git a/shell/Ui/BackgroundMedia.qml b/shell/Ui/BackgroundMedia.qml index b281ae22..f3ab7743 100644 --- a/shell/Ui/BackgroundMedia.qml +++ b/shell/Ui/BackgroundMedia.qml @@ -8,6 +8,10 @@ Item { property int version: 0 property bool playbackEnabled: true property bool audioEnabled: false + // The desktop yields video playback to OWE while it runs, so two engines + // never decode the same file. The lock screen leaves this off and keeps its + // own playback. + property bool deferVideo: false // Bumped when the file behind an unchanged path may have been replaced. // Images cache-bust through version; a video is rebuilt, since FFmpeg // would read a query as part of the filename. @@ -37,7 +41,7 @@ Item { Loader { id: videoLoader anchors.fill: parent - active: root.path !== "" && root.video && !root.reloading + active: root.path !== "" && root.video && !root.reloading && !root.deferVideo source: "BackgroundVideo.qml" } diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index dea93a4c..f8929fa9 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -32,6 +32,25 @@ Item { // services so playback can stop whenever nothing can see the wallpaper. property var shell: null + // When the OWE wallpaper engine is running, it owns video backgrounds. This + // plugin keeps stills, which OWE hands back to it, and the lock screen keeps + // its own playback. + property bool oweActive: false + + Process { + id: oweStatusProc + command: ["bash", "-c", "test -S \"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/owe/owed.sock\""] + onExited: root.oweActive = (exitCode === 0) + } + + Timer { + id: oweStatusTimer + interval: 5000 + repeat: true + running: true + onTriggered: if (!oweStatusProc.running) oweStatusProc.running = true + } + // Stop a video wallpaper's decoding whenever it is covered. Qt's FFmpeg // engine drives its own clock, so an unseen player keeps decoding until it // is told not to — a locked laptop would otherwise decode until it died. @@ -204,7 +223,10 @@ Item { } } - Component.onCompleted: refreshBackground() + Component.onCompleted: { + oweStatusProc.running = true + refreshBackground() + } Variants { model: Quickshell.screens @@ -264,6 +286,7 @@ Item { anchors.fill: parent path: root.displayedBackground reloads: root.displayedReloads + deferVideo: root.oweActive playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere audioEnabled: panel.firstScreen onReadyChanged: { diff --git a/test/shell.d/video-background-test.sh b/test/shell.d/video-background-test.sh index 7ebad86b..d3591b4f 100755 --- a/test/shell.d/video-background-test.sh +++ b/test/shell.d/video-background-test.sh @@ -87,6 +87,15 @@ assert( 'video switches bypass the image-only reveal stack and use the durable background path' ) assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering') +assert( + backgroundQml.includes('property bool oweActive: false') && + backgroundQml.includes('/owe/owed.sock') && + /deferVideo: root\.oweActive/.test(backgroundQml) && + /property bool deferVideo: false/.test(mediaQml) && + /active: root\.path !== "" && root\.video && !root\.reloading && !root\.deferVideo/.test(mediaQml) && + !lockQml.includes('deferVideo'), + 'the desktop yields video backgrounds to OWE while it is running, and the lock keeps its own playback' +) assert( lockQml.includes('source: wallpaper.video ? null : wallpaper') && lockQml.includes('visible: !wallpaper.video') && From 02bb92bc74acd2d0b5ff01708f4043e4eec87c92 Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:36:07 +0200 Subject: [PATCH 2/8] Mark the desktop video path as the no-OWE fallback --- shell/plugins/background/Background.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index f8929fa9..21616940 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -34,7 +34,8 @@ Item { // When the OWE wallpaper engine is running, it owns video backgrounds. This // plugin keeps stills, which OWE hands back to it, and the lock screen keeps - // its own playback. + // its own playback. The desktop video path and its pause policy stay as the + // fallback for systems without OWE. property bool oweActive: false Process { From dbebc458db93bcffd4f453a2fa6af1bc12df4d4d Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:46:17 +0200 Subject: [PATCH 3/8] Replace the desktop video path with OWE The desktop background no longer plays videos. OWE owns video backgrounds, and the shell layer stays empty behind one. The shell keeps stills, which OWE hands back to it. Remove the desktop video pause plumbing that only existed to stop an unseen player: the lock, idle, and battery service lookups, the per-output fullscreen check, the first-screen audio opt-in, and the audio output in BackgroundVideo. The lock screen keeps its own silent playback. Update the background tests, the manual, and the package note. --- install/omarchy-base.packages | 4 +- manual/39-backgrounds.md | 2 +- shell/Ui/BackgroundMedia.qml | 19 +++------ shell/Ui/BackgroundVideo.qml | 23 +++------- shell/plugins/background/Background.qml | 57 ++----------------------- test/shell.d/video-background-test.sh | 51 +++++++++------------- 6 files changed, 37 insertions(+), 119 deletions(-) diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index b38c68b8..634b72aa 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -107,8 +107,8 @@ python-poetry-core ttfx qemu-user-static-binfmt qrencode -# TODO: add owe-wallpaper-engine here once it is packaged. The shell yields -# video backgrounds to it while its daemon runs. +# TODO: add owe-wallpaper-engine here once it is packaged. The shell keeps +# stills and the lock screen, and OWE owns desktop video backgrounds. qt6-imageformats qt6-multimedia qt6-multimedia-ffmpeg diff --git a/manual/39-backgrounds.md b/manual/39-backgrounds.md index f0291653..7670ea61 100644 --- a/manual/39-backgrounds.md +++ b/manual/39-backgrounds.md @@ -4,6 +4,6 @@ Every theme ships with its own set of backgrounds, and you can add extras of you You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`. -Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images, playing on a loop. Only your first monitor's wallpaper plays a video's sound track, through the default audio output at the system volume, and the lock screen stays silent. Playback stops on its own whenever nothing can see it — while a fullscreen window covers that monitor, while the screensaver is up, and once a locked screen has gone dark — but a video wallpaper still costs far more power than a still one, and each monitor decodes its own copy. When the OWE wallpaper engine is running, it takes over video backgrounds and the shell keeps only stills. +Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes once for every monitor and keeps playback silent, and it stops playback whenever nothing can see it. The lock screen keeps its own silent playback. A video wallpaper still costs far more power than a still one. You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls. diff --git a/shell/Ui/BackgroundMedia.qml b/shell/Ui/BackgroundMedia.qml index f3ab7743..fa6b3345 100644 --- a/shell/Ui/BackgroundMedia.qml +++ b/shell/Ui/BackgroundMedia.qml @@ -7,11 +7,9 @@ Item { property string path: "" property int version: 0 property bool playbackEnabled: true - property bool audioEnabled: false - // The desktop yields video playback to OWE while it runs, so two engines - // never decode the same file. The lock screen leaves this off and keeps its - // own playback. - property bool deferVideo: false + // The desktop turns this off because OWE owns video backgrounds. The lock + // screen leaves it on and keeps its own playback. + property bool videoEnabled: true // Bumped when the file behind an unchanged path may have been replaced. // Images cache-bust through version; a video is rebuilt, since FFmpeg // would read a query as part of the filename. @@ -27,7 +25,7 @@ Item { // Both test the path directly: going through `video` lets a URL evaluate // against the stale flag and leak the wrong file for one pass. readonly property url imageUrl: path && !Util.isVideoPath(path) ? Util.fileUrl(path) + (version ? "?v=" + version : "") : "" - readonly property url videoUrl: path && Util.isVideoPath(path) ? Util.fileUrl(path) : "" + readonly property url videoUrl: path && Util.isVideoPath(path) && videoEnabled ? Util.fileUrl(path) : "" Loader { id: imageLoader @@ -41,7 +39,7 @@ Item { Loader { id: videoLoader anchors.fill: parent - active: root.path !== "" && root.video && !root.reloading && !root.deferVideo + active: root.path !== "" && root.video && !root.reloading && root.videoEnabled source: "BackgroundVideo.qml" } @@ -68,13 +66,6 @@ Item { when: videoLoader.item !== null } - Binding { - target: videoLoader.item - property: "audioEnabled" - value: root.audioEnabled - when: videoLoader.item !== null - } - Component { id: imageComponent diff --git a/shell/Ui/BackgroundVideo.qml b/shell/Ui/BackgroundVideo.qml index 519e8bd9..0d0d34e1 100644 --- a/shell/Ui/BackgroundVideo.qml +++ b/shell/Ui/BackgroundVideo.qml @@ -1,15 +1,15 @@ import QtQuick import QtMultimedia -// Deliberately a bare MediaPlayer and VideoOutput rather than the Video -// convenience type: Video always builds an AudioOutput, and a muted sink still -// decodes the audio stream and opens an audio client on every output. +// The lock screen is the only user. Playback is always silent, so this is a +// bare MediaPlayer and VideoOutput rather than the Video convenience type: +// Video always builds an AudioOutput, and a muted sink still decodes the audio +// stream and opens an audio client. Item { id: root property url mediaSource: "" property bool playbackEnabled: true - property bool audioEnabled: false property int mediaGeneration: 0 property bool priming: false property int primingGeneration: -1 @@ -74,23 +74,12 @@ Item { fillMode: VideoOutput.PreserveAspectCrop } - // Sound is opted into per output: with a player per monitor, every output - // playing the track would layer copies of it. The sink is only built once - // the media reports a sound track, so a silent file never opens an audio - // client or its threads. Priming a paused player must not be heard. - Loader { - id: audioLoader - active: root.audioEnabled && player.hasAudio - sourceComponent: AudioOutput { - muted: root.priming || !root.playbackEnabled - } - } - + // No audio output is built, so the lock never decodes an audio stream or + // opens an audio client. MediaPlayer { id: player source: root.mediaSource videoOutput: output - audioOutput: audioLoader.item loops: MediaPlayer.Infinite autoPlay: root.playbackEnabled onMediaStatusChanged: { diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index 21616940..c51181ed 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -1,5 +1,4 @@ import Quickshell -import Quickshell.Hyprland import Quickshell.Io import Quickshell.Wayland import QtQuick @@ -28,43 +27,6 @@ Item { property string pendingShellRaw: "" property real revealProgress: 1 - // Injected by the first-party service loader; used to reach the lock and idle - // services so playback can stop whenever nothing can see the wallpaper. - property var shell: null - - // When the OWE wallpaper engine is running, it owns video backgrounds. This - // plugin keeps stills, which OWE hands back to it, and the lock screen keeps - // its own playback. The desktop video path and its pause policy stay as the - // fallback for systems without OWE. - property bool oweActive: false - - Process { - id: oweStatusProc - command: ["bash", "-c", "test -S \"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/owe/owed.sock\""] - onExited: root.oweActive = (exitCode === 0) - } - - Timer { - id: oweStatusTimer - interval: 5000 - repeat: true - running: true - onTriggered: if (!oweStatusProc.running) oweStatusProc.running = true - } - - // Stop a video wallpaper's decoding whenever it is covered. Qt's FFmpeg - // engine drives its own clock, so an unseen player keeps decoding until it - // is told not to — a locked laptop would otherwise decode until it died. - readonly property var lockService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.lock") : null - readonly property var idleService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.idle") : null - readonly property var batteryService: shell && shell.services ? shell.firstPartyServiceFor("omarchy.battery") : null - readonly property bool lockActive: lockService ? lockService.locked : false - readonly property bool screensaverActive: idleService ? idleService.screensaverWindowCount > 0 : false - readonly property bool powerSaverActive: batteryService ? batteryService.powerSaverOnBattery : false - // A lock or a screensaver covers every output, so it is decided once here. - // Fullscreen is decided per output below, because it only covers its own. - readonly property bool sessionObscured: lockActive || screensaverActive - function isVideo(path) { return Util.isVideoPath(path) } @@ -252,19 +214,6 @@ Item { // by pausing playback rather than by parking the layer. updatesEnabled: true - // Pausing every wallpaper for one fullscreen window would freeze the one - // still on show next to it, which costs a viewer more than it saves. The - // workspace on show here knows whether a fullscreen window covers it, - // wherever focus happens to be. - readonly property var hyprlandMonitor: Hyprland.monitorFor(modelData) - readonly property var visibleWorkspace: hyprlandMonitor ? hyprlandMonitor.activeWorkspace : null - readonly property bool fullscreenHere: visibleWorkspace ? visibleWorkspace.hasFullscreen : false - - // A sound track plays from one output only, or every monitor would - // layer its own copy of it. - readonly property bool firstScreen: Quickshell.screens.length > 0 - && String(Quickshell.screens[0].name || "") === String(modelData.name || "") - property bool maskReady: false function maybeStartReveal() { @@ -282,14 +231,14 @@ Item { WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore + // OWE owns video backgrounds. This layer draws stills, and stays empty + // behind a video so OWE's own layer shows through. BackgroundMedia { id: base anchors.fill: parent path: root.displayedBackground reloads: root.displayedReloads - deferVideo: root.oweActive - playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere - audioEnabled: panel.firstScreen + videoEnabled: false onReadyChanged: { if (ready && root.finishingTransition) { root.incomingBackground = "" diff --git a/test/shell.d/video-background-test.sh b/test/shell.d/video-background-test.sh index d3591b4f..f9eab0db 100755 --- a/test/shell.d/video-background-test.sh +++ b/test/shell.d/video-background-test.sh @@ -33,7 +33,7 @@ assert( videoQml.includes('autoPlay: root.playbackEnabled') && videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') && /imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) && - /videoUrl: path && Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) : ""/.test(mediaQml), + /videoUrl: path && Util\.isVideoPath\(path\) && videoEnabled \? Util\.fileUrl\(path\) : ""/.test(mediaQml), 'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file' ) assert( @@ -62,16 +62,12 @@ assert( ) assert( !/^\s*Video\s*\{/m.test(videoQml) && - /property bool audioEnabled: false/.test(videoQml) && - /property bool audioEnabled: false/.test(mediaQml) && - /active: root\.audioEnabled && player\.hasAudio/.test(videoQml) && - /audioOutput: audioLoader\.item/.test(videoQml) && - /muted: root\.priming \|\| !root\.playbackEnabled/.test(videoQml) && - /property: "audioEnabled"\s*\n\s*value: root\.audioEnabled/.test(mediaQml) && - /firstScreen: Quickshell\.screens\.length > 0\s*\n\s*&& String\(Quickshell\.screens\[0\]\.name/.test(backgroundQml) && - backgroundQml.includes('audioEnabled: panel.firstScreen') && + !videoQml.includes('AudioOutput {') && + !videoQml.includes('audioOutput:') && + !mediaQml.includes('audioEnabled') && + !backgroundQml.includes('audioEnabled') && !lockQml.includes('audioEnabled'), - 'a sound track plays from the first monitor only, a silent file builds no audio output, and the lock stays quiet' + 'playback is silent everywhere, so no audio output or audio client is built' ) assert( /property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) && @@ -88,13 +84,20 @@ assert( ) assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering') assert( - backgroundQml.includes('property bool oweActive: false') && - backgroundQml.includes('/owe/owed.sock') && - /deferVideo: root\.oweActive/.test(backgroundQml) && - /property bool deferVideo: false/.test(mediaQml) && - /active: root\.path !== "" && root\.video && !root\.reloading && !root\.deferVideo/.test(mediaQml) && - !lockQml.includes('deferVideo'), - 'the desktop yields video backgrounds to OWE while it is running, and the lock keeps its own playback' + backgroundQml.includes('videoEnabled: false') && + /property bool videoEnabled: true/.test(mediaQml) && + /active: root\.path !== "" && root\.video && !root\.reloading && root\.videoEnabled/.test(mediaQml) && + !lockQml.includes('videoEnabled'), + 'the desktop hands video backgrounds to OWE, and the lock keeps its own playback' +) +assert( + !backgroundQml.includes('playbackEnabled') && + !backgroundQml.includes('sessionObscured') && + !backgroundQml.includes('fullscreenHere') && + !backgroundQml.includes('Hyprland.monitorFor') && + !backgroundQml.includes('omarchy.lock') && + !backgroundQml.includes('omarchy.battery'), + 'the desktop no longer carries the shell video pause policy' ) assert( lockQml.includes('source: wallpaper.video ? null : wallpaper') && @@ -102,20 +105,6 @@ assert( lockQml.includes('visible: wallpaper.video'), 'lock screen bypasses its image effect for video output' ) -assert( - /sessionObscured:\s*lockActive \|\| screensaverActive/.test(backgroundQml) && - backgroundQml.includes('playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere') && - backgroundQml.includes('omarchy.lock') && - backgroundQml.includes('omarchy.idle') && - backgroundQml.includes('omarchy.battery'), - 'desktop playback stops while covered or on battery power-saver' -) -assert( - backgroundQml.includes('Hyprland.monitorFor(modelData)') && - /fullscreenHere: visibleWorkspace \? visibleWorkspace\.hasFullscreen : false/.test(backgroundQml) && - !backgroundQml.includes('ToplevelManager.activeToplevel'), - 'a fullscreen window pauses only the output it covers, wherever focus is' -) assert( /if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) && backgroundQml.includes('reloads: root.displayedReloads') && From 831626daea4acaaf745d336937da30d411cde635 Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:02:01 +0200 Subject: [PATCH 4/8] Note that OWE plays the desktop video audio --- manual/39-backgrounds.md | 2 +- test/shell.d/video-background-test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manual/39-backgrounds.md b/manual/39-backgrounds.md index 7670ea61..6629ef27 100644 --- a/manual/39-backgrounds.md +++ b/manual/39-backgrounds.md @@ -4,6 +4,6 @@ Every theme ships with its own set of backgrounds, and you can add extras of you You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`. -Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes once for every monitor and keeps playback silent, and it stops playback whenever nothing can see it. The lock screen keeps its own silent playback. A video wallpaper still costs far more power than a still one. +Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes once for every monitor and plays the video's sound through the default audio output, and it stops playback whenever nothing can see it. The lock screen keeps its own silent playback. A video wallpaper still costs far more power than a still one. You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls. diff --git a/test/shell.d/video-background-test.sh b/test/shell.d/video-background-test.sh index f9eab0db..12653721 100755 --- a/test/shell.d/video-background-test.sh +++ b/test/shell.d/video-background-test.sh @@ -67,7 +67,7 @@ assert( !mediaQml.includes('audioEnabled') && !backgroundQml.includes('audioEnabled') && !lockQml.includes('audioEnabled'), - 'playback is silent everywhere, so no audio output or audio client is built' + 'the shell builds no audio output: OWE plays the desktop video audio and the lock stays silent' ) assert( /property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) && From 2e3142a105cfce2f4c06036c84c6bf96a056d92f Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:28:04 +0200 Subject: [PATCH 5/8] Point the owe package note at the omarchy-pkgs pull request --- install/omarchy-base.packages | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 634b72aa..0ec925d0 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -107,8 +107,9 @@ python-poetry-core ttfx qemu-user-static-binfmt qrencode -# TODO: add owe-wallpaper-engine here once it is packaged. The shell keeps -# stills and the lock screen, and OWE owns desktop video backgrounds. +# TODO: add owe here once omarchy-pkgs#519 lands. The shell keeps stills and +# the lock screen, and OWE owns desktop video backgrounds. Version 0.1.1 or +# newer plays the video's audio. qt6-imageformats qt6-multimedia qt6-multimedia-ffmpeg From eff410f91e4c54b932db03670883706ca9e5c498 Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:49:47 +0200 Subject: [PATCH 6/8] Draw the lock screen video from the OWE lock feed The shell drops its own player: BackgroundMedia is image-only, and the lock loads Owe.LockFeedSurface through a Loader, so a system without the module shows no lock video instead of losing the whole lock screen. The feed pauses per output when the panel blanks or power saver turns on. The lock view keeps its still effect path and darkens the feed for legibility. QtMultimedia and the shell video pause policy are gone, and the base package list requires owe and owe-lockfeed instead. --- install/omarchy-base.packages | 8 +- shell/Ui/BackgroundMedia.qml | 56 ++---------- shell/Ui/BackgroundVideo.qml | 111 ------------------------ shell/Ui/qmldir | 1 - shell/plugins/background/Background.qml | 7 -- shell/plugins/lock/LockFeedSurface.qml | 11 +++ shell/plugins/lock/LockView.qml | 35 ++++++-- test/shell.d/video-background-test.sh | 111 ++++++++++-------------- 8 files changed, 92 insertions(+), 248 deletions(-) delete mode 100644 shell/Ui/BackgroundVideo.qml create mode 100644 shell/plugins/lock/LockFeedSurface.qml diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 0ec925d0..c292828f 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -107,12 +107,10 @@ python-poetry-core ttfx qemu-user-static-binfmt qrencode -# TODO: add owe here once omarchy-pkgs#519 lands. The shell keeps stills and -# the lock screen, and OWE owns desktop video backgrounds. Version 0.1.1 or -# newer plays the video's audio. +# OWE owns desktop video backgrounds and feeds the lock screen its frames. +owe +owe-lockfeed qt6-imageformats -qt6-multimedia -qt6-multimedia-ffmpeg quickshell ripgrep ruby diff --git a/shell/Ui/BackgroundMedia.qml b/shell/Ui/BackgroundMedia.qml index fa6b3345..aabde8d1 100644 --- a/shell/Ui/BackgroundMedia.qml +++ b/shell/Ui/BackgroundMedia.qml @@ -1,31 +1,19 @@ import QtQuick import qs.Commons +// The shell draws stills only. OWE owns video backgrounds on the desktop, and +// it feeds the lock screen through its own socket. Item { id: root property string path: "" property int version: 0 - property bool playbackEnabled: true - // The desktop turns this off because OWE owns video backgrounds. The lock - // screen leaves it on and keeps its own playback. - property bool videoEnabled: true - // Bumped when the file behind an unchanged path may have been replaced. - // Images cache-bust through version; a video is rebuilt, since FFmpeg - // would read a query as part of the filename. - property int reloads: 0 - property bool reloading: false - readonly property var current: video ? videoLoader.item : imageLoader.item + readonly property var current: imageLoader.item readonly property bool ready: current ? current.ready : false readonly property bool video: Util.isVideoPath(path) - // Cache-bust images selected in a running lock session. FFmpeg treats the - // query as part of a local filename, so videos must keep their plain URL. - // Each URL is empty for the other kind, so a switch never hands the still - // loader a video, or the player a still, in the moment before it unloads. - // Both test the path directly: going through `video` lets a URL evaluate - // against the stale flag and leak the wrong file for one pass. - readonly property url imageUrl: path && !Util.isVideoPath(path) ? Util.fileUrl(path) + (version ? "?v=" + version : "") : "" - readonly property url videoUrl: path && Util.isVideoPath(path) && videoEnabled ? Util.fileUrl(path) : "" + // Cache-bust images selected in a running lock session, so a theme switch + // that replaces the file behind an unchanged path shows the new pixels. + readonly property url imageUrl: path && !video ? Util.fileUrl(path) + (version ? "?v=" + version : "") : "" Loader { id: imageLoader @@ -34,38 +22,6 @@ Item { sourceComponent: imageComponent } - // Loaded by URL rather than from a Component here, so QtMultimedia and its - // audio dependency closure never map into a session that only shows images. - Loader { - id: videoLoader - anchors.fill: parent - active: root.path !== "" && root.video && !root.reloading && root.videoEnabled - source: "BackgroundVideo.qml" - } - - onReloadsChanged: { - if (!video) return - reloading = true - Qt.callLater(function() { root.reloading = false }) - } - - // A player on its way out keeps its source: pushing an empty one starts a - // load of nothing that its destructor then cancels, which FFmpeg logs. - Binding { - target: videoLoader.item - property: "mediaSource" - value: root.videoUrl - when: videoLoader.item !== null && Util.isVideoPath(root.path) - restoreMode: Binding.RestoreNone - } - - Binding { - target: videoLoader.item - property: "playbackEnabled" - value: root.playbackEnabled - when: videoLoader.item !== null - } - Component { id: imageComponent diff --git a/shell/Ui/BackgroundVideo.qml b/shell/Ui/BackgroundVideo.qml deleted file mode 100644 index 0d0d34e1..00000000 --- a/shell/Ui/BackgroundVideo.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick -import QtMultimedia - -// The lock screen is the only user. Playback is always silent, so this is a -// bare MediaPlayer and VideoOutput rather than the Video convenience type: -// Video always builds an AudioOutput, and a muted sink still decodes the audio -// stream and opens an audio client. -Item { - id: root - - property url mediaSource: "" - property bool playbackEnabled: true - property int mediaGeneration: 0 - property bool priming: false - property int primingGeneration: -1 - property bool frameReceived: false - readonly property bool ready: player.hasVideo - - onMediaSourceChanged: { - mediaGeneration += 1 - priming = false - primingGeneration = -1 - frameReceived = false - primePauseTimer.stop() - framePauseTimer.stop() - output.clearOutput() - } - - onPlaybackEnabledChanged: { - priming = false - frameReceived = false - primePauseTimer.stop() - framePauseTimer.stop() - if (playbackEnabled) player.play() - else player.pause() - } - - function pauseAfterPrimedFrame() { - if (!priming - || root.playbackEnabled - || primingGeneration !== root.mediaGeneration) return - - priming = false - primePauseTimer.stop() - framePauseTimer.stop() - player.pause() - } - - // A paused MediaPlayer can load a source without presenting its first frame. - // Prime it until VideoOutput receives a frame, with a timeout so a stalled - // decoder cannot keep running indefinitely on battery. - Timer { - id: primePauseTimer - interval: 1000 - repeat: false - - onTriggered: root.pauseAfterPrimedFrame() - } - - // Receiving a frame means the decoder has produced it, but the scene graph - // may not have committed it yet. Give VideoOutput a render cycle before - // pausing so the first frame is not lost on a source switch. - Timer { - id: framePauseTimer - interval: 50 - repeat: false - - onTriggered: root.pauseAfterPrimedFrame() - } - - VideoOutput { - id: output - anchors.fill: parent - fillMode: VideoOutput.PreserveAspectCrop - } - - // No audio output is built, so the lock never decodes an audio stream or - // opens an audio client. - MediaPlayer { - id: player - source: root.mediaSource - videoOutput: output - loops: MediaPlayer.Infinite - autoPlay: root.playbackEnabled - onMediaStatusChanged: { - if (mediaStatus !== MediaPlayer.LoadedMedia) return - - if (!root.playbackEnabled) { - root.priming = true - root.primingGeneration = root.mediaGeneration - root.frameReceived = false - primePauseTimer.restart() - } - player.play() - } - } - - Connections { - target: output.videoSink - function onVideoFrameChanged() { - if (player.mediaStatus !== MediaPlayer.BufferedMedia) return - if (!root.priming - || root.playbackEnabled - || root.primingGeneration !== root.mediaGeneration - || root.frameReceived) return - - root.frameReceived = true - framePauseTimer.restart() - } - } -} diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index 092caf6c..464e25b7 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -4,7 +4,6 @@ BarIndicator 1.0 BarIndicator.qml BarIconButton 1.0 BarIconButton.qml BarWidget 1.0 BarWidget.qml BackgroundMedia 1.0 BackgroundMedia.qml -BackgroundVideo 1.0 BackgroundVideo.qml BorderOverlay 1.0 BorderOverlay.qml BorderSurface 1.0 BorderSurface.qml Button 1.0 Button.qml diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index c51181ed..8c113e4b 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -16,7 +16,6 @@ Item { property string currentBackground: "" property string displayedBackground: "" - property int displayedReloads: 0 property string incomingBackground: "" property string oldBackground: "" property bool finishingTransition: false @@ -60,9 +59,6 @@ Item { if (instant || !displayedBackground || isVideo(path) || isVideo(displayedBackground)) { oldBackground = "" incomingBackground = "" - // A theme switch can replace the file behind an unchanged path, which - // an unchanged property would never pick up. - if (displayedBackground === finalPath) displayedReloads += 1 displayedBackground = finalPath revealProgress = 1 return @@ -187,7 +183,6 @@ Item { } Component.onCompleted: { - oweStatusProc.running = true refreshBackground() } @@ -237,8 +232,6 @@ Item { id: base anchors.fill: parent path: root.displayedBackground - reloads: root.displayedReloads - videoEnabled: false onReadyChanged: { if (ready && root.finishingTransition) { root.incomingBackground = "" diff --git a/shell/plugins/lock/LockFeedSurface.qml b/shell/plugins/lock/LockFeedSurface.qml new file mode 100644 index 00000000..38bd4612 --- /dev/null +++ b/shell/plugins/lock/LockFeedSurface.qml @@ -0,0 +1,11 @@ +import QtQuick +import Owe.LockFeed + +// Loaded through a Loader from LockView, so a system without the module shows +// no lock video instead of losing the whole lock screen. +LockFeed { + id: root + + property bool feedEnabled: true + active: root.feedEnabled +} diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index e7a430ac..9366b289 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -43,6 +43,9 @@ Item { ? Border.surfaceSpec("lock", "border-error", Color.lock.borderError, root.outlineThickness, "border-alpha") : Border.surfaceSpec("lock", "border-active", Color.lock.borderActive, root.outlineThickness, "border-alpha") + readonly property bool video: Util.isVideoPath(root.backgroundPath) + readonly property bool feedActive: root.video && root.loadBackground && !root.displaysBlank && !root.powerSaverActive + signal submitPassword(string password) signal passwordTextEdited(string password) signal clearFailureRequested() @@ -89,15 +92,33 @@ Item { BackgroundMedia { id: wallpaper anchors.fill: parent - path: root.loadBackground ? root.backgroundPath : "" + path: root.video ? "" : root.backgroundPath version: root.backgroundVersion - playbackEnabled: root.loadBackground && !root.displaysBlank && !root.powerSaverActive + visible: !root.video + } + + // OWE decodes the video once and feeds these frames to every lock surface. + // The feed pauses when this output blanks or on battery power saver. + Loader { + id: feedLoader + anchors.fill: parent + active: root.video + source: "LockFeedSurface.qml" + visible: status === Loader.Ready + + Binding { + target: feedLoader.item + property: "feedEnabled" + value: root.feedActive + when: feedLoader.item !== null + restoreMode: Binding.RestoreNone + } } MultiEffect { anchors.fill: wallpaper - source: wallpaper.video ? null : wallpaper - visible: !wallpaper.video + source: root.video ? null : wallpaper + visible: !root.video autoPaddingEnabled: false blurEnabled: root.loadBackground && wallpaper.ready blur: 1.0 @@ -106,11 +127,11 @@ Item { contrast: -0.08 } - // Qt's video output cannot be sampled by MultiEffect on every renderer. + // The feed item cannot be sampled by MultiEffect on every renderer. // Keep video wallpapers visible and darken them slightly for legibility. Rectangle { - anchors.fill: wallpaper - visible: wallpaper.video + anchors.fill: feedLoader + visible: root.video color: "#22000000" } diff --git a/test/shell.d/video-background-test.sh b/test/shell.d/video-background-test.sh index 12653721..31ac754d 100755 --- a/test/shell.d/video-background-test.sh +++ b/test/shell.d/video-background-test.sh @@ -9,15 +9,13 @@ const fs = require('fs') const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8') const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8') -const videoQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundVideo.qml'), 'utf8') const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8') const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8') +const lockFeedQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockFeedSurface.qml'), 'utf8') const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8') const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8') -const multimediaMigration = fs.readFileSync(path.join(root, 'migrations/1786609204.sh'), 'utf8') const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8') const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8') -const lockView = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8') const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8') const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8') const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8') @@ -29,50 +27,45 @@ assert( 'shared media helper identifies video paths without truncating valid local names' ) assert( - videoQml.includes('loops: MediaPlayer.Infinite') && - videoQml.includes('autoPlay: root.playbackEnabled') && - videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') && - /imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) && - /videoUrl: path && Util\.isVideoPath\(path\) && videoEnabled \? Util\.fileUrl\(path\) : ""/.test(mediaQml), - 'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file' + !fs.existsSync(path.join(root, 'shell/Ui/BackgroundVideo.qml')), + 'the shell keeps no video player of its own' ) assert( - videoQml.includes('MediaPlayer.LoadedMedia') && - videoQml.includes('primePauseTimer') && - videoQml.includes('mediaGeneration') && - videoQml.includes('videoSink') && - videoQml.includes('onVideoFrameChanged') && - videoQml.includes('MediaPlayer.BufferedMedia') && - videoQml.includes('mediaStatus !== MediaPlayer.BufferedMedia') && - videoQml.includes('interval: 1000') && - videoQml.includes('interval: 50') && - videoQml.includes('frameReceived') && - videoQml.includes('output.clearOutput()') && - !videoQml.includes('KeepLastFrame') && - /onPlaybackEnabledChanged:[\s\S]*?if \(playbackEnabled\) player\.play\(\)[\s\S]*?else player\.pause\(\)/.test(videoQml) && - videoQml.includes('primingGeneration') && - videoQml.includes('player.play()') && - videoQml.includes('player.pause()'), - 'paused video sources are primed to display their first frame' + /imageUrl: path && !video \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) && + /active: root\.path !== "" && !root\.video/.test(mediaQml) && + !mediaQml.includes('videoUrl') && + !mediaQml.includes('videoLoader') && + !mediaQml.includes('videoEnabled'), + 'background media is image-only: it loads stills and leaves videos to OWE' ) assert( !/^\s*import QtMultimedia/m.test(mediaQml) && - mediaQml.includes('source: "BackgroundVideo.qml"'), - 'the still-image path never imports QtMultimedia, so image-only sessions do not map it' + !/^\s*import QtMultimedia/m.test(backgroundQml) && + !/^\s*import QtMultimedia/m.test(lockQml) && + !backgroundQml.includes('MediaPlayer') && + !lockQml.includes('MediaPlayer'), + 'no shell surface builds a Qt Multimedia pipeline' ) assert( - !/^\s*Video\s*\{/m.test(videoQml) && - !videoQml.includes('AudioOutput {') && - !videoQml.includes('audioOutput:') && - !mediaQml.includes('audioEnabled') && - !backgroundQml.includes('audioEnabled') && - !lockQml.includes('audioEnabled'), - 'the shell builds no audio output: OWE plays the desktop video audio and the lock stays silent' + /^import Owe\.LockFeed$/m.test(lockFeedQml) && + lockFeedQml.includes('LockFeed {') && + lockFeedQml.includes('active: root.feedEnabled') && + lockQml.includes('source: "LockFeedSurface.qml"') && + lockQml.includes('active: root.video') && + /feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) && + /property: "feedEnabled"[\s\S]*?value: root\.feedActive/.test(lockQml), + 'the lock screen shows video through the OWE lock feed, loaded so a missing module costs only the video' ) assert( - /property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) && - !videoQml.includes('Component.onDestruction'), - 'a player on its way out keeps its source, so nothing is left loading for its destructor to cancel' + !/^import Owe\./m.test(lockQml), + 'the lock view itself carries no foreign import, so the lock still loads without the feed module' +) +assert( + lockQml.includes('path: root.video ? "" : root.backgroundPath') && + lockQml.includes('visible: !root.video') && + lockQml.includes('visible: root.video') && + !lockQml.includes('wallpaper.video'), + 'the lock screen keeps its image effect for stills and shows the feed for videos' ) assert( !mediaQml.includes('mipmap'), @@ -82,13 +75,12 @@ assert( /instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml), 'video switches bypass the image-only reveal stack and use the durable background path' ) -assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering') +assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share still rendering') assert( - backgroundQml.includes('videoEnabled: false') && - /property bool videoEnabled: true/.test(mediaQml) && - /active: root\.path !== "" && root\.video && !root\.reloading && root\.videoEnabled/.test(mediaQml) && - !lockQml.includes('videoEnabled'), - 'the desktop hands video backgrounds to OWE, and the lock keeps its own playback' + !backgroundQml.includes('videoEnabled') && + !backgroundQml.includes('displayedReloads') && + !mediaQml.includes('displayedReloads'), + 'the desktop hands video backgrounds to OWE with no player switch to carry' ) assert( !backgroundQml.includes('playbackEnabled') && @@ -99,19 +91,6 @@ assert( !backgroundQml.includes('omarchy.battery'), 'the desktop no longer carries the shell video pause policy' ) -assert( - lockQml.includes('source: wallpaper.video ? null : wallpaper') && - lockQml.includes('visible: !wallpaper.video') && - lockQml.includes('visible: wallpaper.video'), - 'lock screen bypasses its image effect for video output' -) -assert( - /if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) && - backgroundQml.includes('reloads: root.displayedReloads') && - /active: root\.path !== "" && root\.video && !root\.reloading/.test(mediaQml) && - /onReloadsChanged: \{[\s\S]*?reloading = true/.test(mediaQml), - 'a theme switch that keeps the video path still reopens the replaced file' -) assert( barTextColor.includes('magick "$background_path[0]"'), 'bar colour sampling reads one frame instead of decoding a whole video' @@ -161,13 +140,12 @@ assert( 'a locked video wallpaper follows what each panel actually did, not only what the lock asked for' ) assert( - lockView.includes('playbackEnabled: root.loadBackground && !root.displaysBlank') && - lockView.includes('&& !root.powerSaverActive') && + /feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) && /displaysBlank: root\.screenBlank\(/.test(lockService) && /powerSaverActive: root\.powerSaverActive/.test(lockService) && /function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) && /function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService), - 'the lock screen stops playback once displays go dark or power-saver is active' + 'the lock feed stops once displays go dark or power-saver is active' ) assert( batteryService.includes('property string activePowerProfile') && @@ -184,10 +162,6 @@ assert( 'theme switcher previews video-only themes, named preview files included' ) assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background') -assert( - multimediaMigration.includes('omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg'), - 'existing Quattro installations receive video playback dependencies' -) JS test_tmp=$(mktemp -d) @@ -301,8 +275,11 @@ timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \ timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit) [[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed" -grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia runtime is a base package" -grep -qx 'qt6-multimedia-ffmpeg' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia FFmpeg backend is a base package" +grep -qx 'owe' "$ROOT/install/omarchy-base.packages" || fail "OWE is a base package" +grep -qx 'owe-lockfeed' "$ROOT/install/omarchy-base.packages" || fail "the OWE lock feed module is a base package" +if grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages"; then + fail "Qt Multimedia is no longer needed by the shell" +fi pass "menu image generator creates thumbnails consumed by the picker" pass "direct picker generates and reuses still thumbnails" @@ -310,7 +287,7 @@ pass "direct picker omits videos whose thumbnails cannot be generated" pass "a rejected video is remembered so it costs nothing on the next open" pass "a timed out video is left to retry" pass "a locked video wallpaper follows the panels' real DPMS state" -pass "Qt Multimedia playback dependencies are declared" +pass "OWE and its lock feed module are declared" source <(awk ' /^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 } From b0bf4876b10619743e5b93b577e19a37cbbe45cc Mon Sep 17 00:00:00 2001 From: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:55:27 +0200 Subject: [PATCH 7/8] Describe the lock feed in the background manual --- manual/39-backgrounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/39-backgrounds.md b/manual/39-backgrounds.md index 6629ef27..f57d23d2 100644 --- a/manual/39-backgrounds.md +++ b/manual/39-backgrounds.md @@ -4,6 +4,6 @@ Every theme ships with its own set of backgrounds, and you can add extras of you You can do this most easily by going to _Install > Style > Background_ in the Omarchy Menu. That'll bring up the folder where the backgrounds for that theme is stored. Hit `Super + Shift + F` to start another file manager, find your background, copy it over. Now it'll be included in the choices of backgrounds you can select between using `Super + Ctrl + Space`. -Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes once for every monitor and plays the video's sound through the default audio output, and it stops playback whenever nothing can see it. The lock screen keeps its own silent playback. A video wallpaper still costs far more power than a still one. +Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes the video once for all monitors and plays its sound through the default audio output, and it stops playback whenever nothing can see it. The lock screen draws the same decode, muted, through OWE. A video wallpaper still costs far more power than a still one. You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls. From b423f4993d68b0db70ae778f8cd4b3f516e3c500 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 20 Sep 2026 20:36:19 -0500 Subject: [PATCH 8/8] Complete OWE service setup and lock feed fallback --- install/omarchy-base.packages | 6 +- install/user/first-run/enable-user-units.sh | 3 + manual/39-backgrounds.md | 2 + migrations/1786609204.sh | 4 +- migrations/1789764927.sh | 17 ++++ shell/plugins/background/Background.qml | 7 +- shell/plugins/lock/LockFeedSurface.qml | 4 - shell/plugins/lock/LockView.qml | 37 ++++----- shell/plugins/lock/Service.qml | 29 +++++++ shell/plugins/lock/poster.sh | 24 ++++++ test/shell.d/fixtures/owe-lock/shell.qml | 69 ++++++++++++++++ test/shell.d/owe-integration-test.sh | 90 +++++++++++++++++++++ test/shell.d/owe-lock-test.sh | 30 +++++++ test/shell.d/video-background-test.sh | 10 +-- 14 files changed, 289 insertions(+), 43 deletions(-) create mode 100644 migrations/1789764927.sh create mode 100755 shell/plugins/lock/poster.sh create mode 100644 test/shell.d/fixtures/owe-lock/shell.qml create mode 100644 test/shell.d/owe-integration-test.sh create mode 100644 test/shell.d/owe-lock-test.sh diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index c292828f..e4dd69a2 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -95,6 +95,9 @@ omacalc omacut omawrite omarchy-nvim +# OWE owns desktop video backgrounds and feeds the lock screen its frames. +owe +owe-lockfeed pacman-contrib pamixer pinta @@ -107,9 +110,6 @@ python-poetry-core ttfx qemu-user-static-binfmt qrencode -# OWE owns desktop video backgrounds and feeds the lock screen its frames. -owe -owe-lockfeed qt6-imageformats quickshell ripgrep diff --git a/install/user/first-run/enable-user-units.sh b/install/user/first-run/enable-user-units.sh index 9865fc20..e08885c9 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -14,8 +14,11 @@ set -euo pipefail systemctl --user daemon-reload systemctl --user enable --now \ bt-agent.service \ + owed.service \ omarchy-recover-internal-monitor.service \ omarchy-sleep-lock.service \ omarchy-migrate-notify.service \ omarchy-fcitx5.service \ omarchy-crash-watch.service + +omarchy-hook-install theme-set /usr/share/owe/10-owe-sync diff --git a/manual/39-backgrounds.md b/manual/39-backgrounds.md index f57d23d2..3bc1f77b 100644 --- a/manual/39-backgrounds.md +++ b/manual/39-backgrounds.md @@ -7,3 +7,5 @@ You can do this most easily by going to _Install > Style > Background_ in the Om Backgrounds can be videos as well as stills. Drop an `mp4`, `m4v`, `mov`, `webm`, `mkv`, or `avi` file in the same folder and it appears alongside the images. Videos are played by the OWE wallpaper engine. It decodes the video once for all monitors and plays its sound through the default audio output, and it stops playback whenever nothing can see it. The lock screen draws the same decode, muted, through OWE. A video wallpaper still costs far more power than a still one. You can find a huge collection of cool curated backgrounds on https://github.com/dharmx/walls. + +Video backgrounds keep a cached still on the lock screen while playback is paused or unavailable. Animated GIFs play on the desktop and show a still frame on the lock screen. diff --git a/migrations/1786609204.sh b/migrations/1786609204.sh index b7833092..ce1442ca 100644 --- a/migrations/1786609204.sh +++ b/migrations/1786609204.sh @@ -1,3 +1 @@ -echo "Install native video wallpaper playback dependencies" - -omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg +echo "Native video dependencies are superseded by the OWE migration" diff --git a/migrations/1789764927.sh b/migrations/1789764927.sh new file mode 100644 index 00000000..e2d1541e --- /dev/null +++ b/migrations/1789764927.sh @@ -0,0 +1,17 @@ +echo "Enable OWE desktop video backgrounds and lock feed" + +omarchy-pkg-add owe owe-lockfeed +omarchy-hook-install theme-set /usr/share/owe/10-owe-sync + +systemctl --user daemon-reload >/dev/null 2>&1 || true +if ! systemctl --user enable owed.service; then + wants_dir="$HOME/.config/systemd/user/graphical-session.target.wants" + mkdir -p "$wants_dir" + ln -sfn /usr/lib/systemd/user/owed.service "$wants_dir/owed.service" +fi + +# A TTY update enables the next graphical login without starting a renderer +# against a missing Wayland session. A failed live start leaves this pending. +if [[ ${OMARCHY_UPGRADE_TO_QUATTRO_LIVE:-0} != 1 ]] && systemctl --user is-active --quiet graphical-session.target; then + systemctl --user start owed.service +fi diff --git a/shell/plugins/background/Background.qml b/shell/plugins/background/Background.qml index 8c113e4b..b3dd7dab 100644 --- a/shell/plugins/background/Background.qml +++ b/shell/plugins/background/Background.qml @@ -182,9 +182,7 @@ Item { } } - Component.onCompleted: { - refreshBackground() - } + Component.onCompleted: refreshBackground() Variants { model: Quickshell.screens @@ -205,8 +203,7 @@ Item { // Keep render updates enabled. The background layer has been observed to // lose its committed buffer while parked with updatesEnabled=false, // leaving a black desktop until omarchy-shell is restarted. A still - // wallpaper costs nothing to keep enabled, and a video one is throttled - // by pausing playback rather than by parking the layer. + // wallpaper costs nothing to keep enabled. OWE manages video layers. updatesEnabled: true property bool maskReady: false diff --git a/shell/plugins/lock/LockFeedSurface.qml b/shell/plugins/lock/LockFeedSurface.qml index 38bd4612..ecd1f7d4 100644 --- a/shell/plugins/lock/LockFeedSurface.qml +++ b/shell/plugins/lock/LockFeedSurface.qml @@ -4,8 +4,4 @@ import Owe.LockFeed // Loaded through a Loader from LockView, so a system without the module shows // no lock video instead of losing the whole lock screen. LockFeed { - id: root - - property bool feedEnabled: true - active: root.feedEnabled } diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml index 9366b289..7084fb0f 100644 --- a/shell/plugins/lock/LockView.qml +++ b/shell/plugins/lock/LockView.qml @@ -7,6 +7,7 @@ Item { id: root property string backgroundPath: "" + property string videoPosterPath: "" property int backgroundVersion: 0 property bool fingerprintConfigured: false property bool authenticatingPassword: false @@ -91,34 +92,15 @@ Item { BackgroundMedia { id: wallpaper + objectName: "lockWallpaper" anchors.fill: parent - path: root.video ? "" : root.backgroundPath + path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : "" version: root.backgroundVersion - visible: !root.video - } - - // OWE decodes the video once and feeds these frames to every lock surface. - // The feed pauses when this output blanks or on battery power saver. - Loader { - id: feedLoader - anchors.fill: parent - active: root.video - source: "LockFeedSurface.qml" - visible: status === Loader.Ready - - Binding { - target: feedLoader.item - property: "feedEnabled" - value: root.feedActive - when: feedLoader.item !== null - restoreMode: Binding.RestoreNone - } } MultiEffect { anchors.fill: wallpaper - source: root.video ? null : wallpaper - visible: !root.video + source: wallpaper autoPaddingEnabled: false blurEnabled: root.loadBackground && wallpaper.ready blur: 1.0 @@ -127,6 +109,17 @@ Item { contrast: -0.08 } + // The cached poster stays behind the feed when policy pauses playback, + // the module is unavailable, or a new connection has not received a frame. + Loader { + id: feedLoader + objectName: "lockFeedLoader" + anchors.fill: parent + active: root.feedActive + source: "LockFeedSurface.qml" + visible: status === Loader.Ready + } + // The feed item cannot be sampled by MultiEffect on every renderer. // Keep video wallpapers visible and darken them slightly for legibility. Rectangle { diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml index 94d43b68..6e1f703e 100644 --- a/shell/plugins/lock/Service.qml +++ b/shell/plugins/lock/Service.qml @@ -28,6 +28,7 @@ Item { property string failureMessage: "" property int failedAttempts: 0 property string backgroundPath: "" + property string videoPosterPath: "" property int backgroundVersion: 0 property string lastEvent: "init" property string lastEventAt: "" @@ -113,6 +114,16 @@ Item { if (!readlinkProc.running) readlinkProc.running = true } + function refreshPoster() { + if (!root.videoBackground) { + root.videoPosterPath = "" + return + } + if (posterProc.running) return + posterProc.sourcePath = root.backgroundPath + posterProc.running = true + } + function refreshFingerprintStatus() { if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true } @@ -307,6 +318,7 @@ Item { id: lockView anchors.fill: parent backgroundPath: root.backgroundPath + videoPosterPath: root.videoPosterPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured authenticatingPassword: root.authenticatingPassword @@ -339,6 +351,7 @@ Item { LockView { anchors.fill: parent backgroundPath: root.backgroundPath + videoPosterPath: root.videoPosterPath backgroundVersion: root.backgroundVersion fingerprintConfigured: root.fingerprintConfigured authenticatingPassword: false @@ -409,9 +422,25 @@ Item { onStreamFinished: { var next = String(text || "").trim() if (next !== root.backgroundPath) { + root.videoPosterPath = "" root.backgroundPath = next root.backgroundVersion += 1 } + root.refreshPoster() + } + } + } + + Process { + id: posterProc + property string sourcePath: "" + command: ["bash", Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/poster.sh", sourcePath] + stdout: StdioCollector { id: posterOutput; waitForEnd: true } + onExited: function(exitCode) { + if (sourcePath !== root.backgroundPath) { + root.refreshPoster() + } else { + root.videoPosterPath = exitCode === 0 ? String(posterOutput.text || "").trim() : "" } } } diff --git a/shell/plugins/lock/poster.sh b/shell/plugins/lock/poster.sh new file mode 100755 index 00000000..836d01f0 --- /dev/null +++ b/shell/plugins/lock/poster.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# One cached frame shared by all lock outputs, including when OWE is paused. +set -euo pipefail + +source_path=$1 +cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy/lock-poster" +signature=$(stat -Lc '%s:%y:%z' "$source_path") +key=$(printf '%s\n%s' "$source_path" "$signature" | sha256sum | cut -d ' ' -f 1) +poster="$cache_dir/poster-$key.jpg" +mkdir -p "$cache_dir" +exec {lock_fd}>"$cache_dir/.lock" +flock -w 10 "$lock_fd" + +if [[ ! -s $poster ]]; then + temporary=$(mktemp "$cache_dir/.poster-XXXXXX.jpg") + trap 'rm -f "$temporary"' EXIT + timeout -k 1 5 ffmpegthumbnailer -i "$source_path" -o "$temporary" -s 1920 -q 8 {lock_fd}>&- + [[ -s $temporary ]] + mv -f "$temporary" "$poster" + find "$cache_dir" -maxdepth 1 -type f -name 'poster-*.jpg' ! -name "poster-$key.jpg" -delete +fi + +printf '%s\n' "$poster" diff --git a/test/shell.d/fixtures/owe-lock/shell.qml b/test/shell.d/fixtures/owe-lock/shell.qml new file mode 100644 index 00000000..dc9f88fa --- /dev/null +++ b/test/shell.d/fixtures/owe-lock/shell.qml @@ -0,0 +1,69 @@ +import QtQuick +import Quickshell + +ShellRoot { + id: root + property var view + property var wallpaper + property var feed + property int step: 0 + + Item { id: host; width: 1000; height: 700 } + + function findItem(item, name) { + if (item.objectName === name) return item + for (var i = 0; i < item.children.length; i++) { + var found = findItem(item.children[i], name) + if (found) return found + } + return null + } + + function check(condition, message) { + if (!condition) throw new Error(message) + } + + Timer { + interval: 150 + repeat: true + running: true + onTriggered: { + try { + if (root.step === 0) { + var component = Qt.createComponent("file://" + Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/LockView.qml") + check(component.status === Component.Ready, component.errorString()) + root.view = component.createObject(host, {width: 1000, height: 700, backgroundPath: "/still.png", loadBackground: false}) + check(root.view !== null, component.errorString()) + root.wallpaper = findItem(root.view, "lockWallpaper") + root.feed = findItem(root.view, "lockFeedLoader") + check(root.wallpaper.path === "", "hidden still must not decode") + root.view.backgroundPath = "/video.mp4" + root.view.videoPosterPath = "/poster.jpg" + } else if (root.step === 1) { + check(!root.feed.active && root.feed.item === null, "hidden video must not load a feed client") + root.view.loadBackground = true + root.view.powerSaverActive = true + } else if (root.step === 2) { + check(root.wallpaper.path === "/poster.jpg", "power saver keeps the video poster") + check(!root.feed.active, "power saver must not connect to the feed") + root.view.powerSaverActive = false + } else if (root.step === 3) { + check(root.feed.active, "visible video enables the isolated feed loader") + check(root.wallpaper.path === "/poster.jpg", "poster stays beneath the feed before its first frame") + root.view.displaysBlank = true + } else if (root.step === 4) { + check(!root.feed.active && root.feed.item === null, "blanking releases the feed client") + root.view.loadBackground = false + } else { + check(root.wallpaper.path === "", "unlock releases the poster image") + console.log("OWE_LOCK_TEST_PASS") + Qt.quit() + } + root.step++ + } catch (error) { + console.error("OWE_LOCK_TEST_FAIL: " + error) + Qt.quit() + } + } + } +} diff --git a/test/shell.d/owe-integration-test.sh b/test/shell.d/owe-integration-test.sh new file mode 100644 index 00000000..cde79db2 --- /dev/null +++ b/test/shell.d/owe-integration-test.sh @@ -0,0 +1,90 @@ +#!/bin/bash +set -euo pipefail +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/bin" "$work/home" +export TEST_CALLS="$work/calls" +for command in omarchy-pkg-add omarchy-hook-install systemctl; do + cat >"$work/bin/$command" <<'SH' +#!/bin/bash +printf '%s %s\n' "${0##*/}" "$*" >>"$TEST_CALLS" +case "$*" in + '--user enable owed.service') [[ ${TEST_TTY:-0} == 0 ]] ;; + '--user is-active --quiet graphical-session.target') [[ ${TEST_TTY:-0} == 0 ]] ;; + '--user start owed.service') [[ ${TEST_START_FAIL:-0} == 0 ]] ;; + *) exit 0 ;; +esac +SH + chmod +x "$work/bin/$command" +done + +migration="$ROOT/migrations/1789764927.sh" +run_migration() { + HOME="$work/home" PATH="$work/bin:$PATH" bash -euo pipefail "$migration" +} +run_migration +run_migration +grep -Fx 'omarchy-pkg-add owe owe-lockfeed' "$TEST_CALLS" >/dev/null +grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null +grep -Fx 'systemctl --user start owed.service' "$TEST_CALLS" >/dev/null +pass "migration installs OWE, its theme refresh hook, and starts the graphical session service" + +: >"$TEST_CALLS" +TEST_TTY=1 run_migration +TEST_TTY=1 run_migration +[[ $(readlink "$work/home/.config/systemd/user/graphical-session.target.wants/owed.service") == /usr/lib/systemd/user/owed.service ]] +if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then + fail "TTY migration must not start the renderer" +fi +pass "TTY migrations enable the next login without starting a renderer" + +: >"$TEST_CALLS" +OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 run_migration +if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then + fail "Quattro upgrade must defer OWE until the new graphical session" +fi +pass "Quattro upgrade enables OWE without starting it in the old shell" + +if TEST_START_FAIL=1 run_migration; then + fail "a failed live service start leaves the migration pending" +fi +pass "a failed live service start fails the migration" + +: >"$TEST_CALLS" +HOME="$work/home" PATH="$work/bin:$PATH" bash "$ROOT/install/user/first-run/enable-user-units.sh" +grep -E '^systemctl --user enable --now .*owed.service' "$TEST_CALLS" >/dev/null +grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null +pass "fresh installs enable OWE and install the theme refresh hook" + +cat >"$work/bin/ffmpegthumbnailer" <<'SH' +#!/bin/bash +printf 'thumbnail\n' >>"$TEST_CALLS" +while (( $# )); do + case "$1" in + -o) output=$2; shift 2 ;; + *) shift ;; + esac +done +[[ ${TEST_POSTER_FAIL:-0} == 0 ]] || exit 1 +printf 'poster\n' >"$output" +SH +chmod +x "$work/bin/ffmpegthumbnailer" +source_path="$work/video with 'quotes'.mp4" +printf 'source\n' >"$source_path" +poster() { + PATH="$work/bin:$PATH" XDG_CACHE_HOME="$work/cache" bash "$ROOT/shell/plugins/lock/poster.sh" "$source_path" +} +: >"$TEST_CALLS" +first=$(poster) +[[ -s $first && $(poster) == "$first" ]] +[[ $(wc -l <"$TEST_CALLS") == 1 ]] +printf 'changed source\n' >>"$source_path" +second=$(poster) +[[ -s $second && $second != "$first" && ! -e $first ]] +pass "lock posters are cached, refreshed after source changes, and old frames are evicted" +printf 'broken source\n' >>"$source_path" +if TEST_POSTER_FAIL=1 poster; then fail "failed poster generation must fail"; fi +[[ -z $(find "$work/cache" -name '.poster-*.jpg' -print -quit) ]] +pass "failed poster generation never publishes a partial image" diff --git a/test/shell.d/owe-lock-test.sh b/test/shell.d/owe-lock-test.sh new file mode 100644 index 00000000..4f3831c7 --- /dev/null +++ b/test/shell.d/owe-lock-test.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +if ! command -v quickshell >/dev/null 2>&1; then + pass "quickshell unavailable; skipping lock feed QML lifecycle" + exit 0 +fi +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/config" "$work/runtime" "$work/home" +chmod 700 "$work/runtime" +cp "$SHELL_TEST_DIR/fixtures/owe-lock/shell.qml" "$work/config/shell.qml" +ln -s "$ROOT/shell/Ui" "$work/config/Ui" +ln -s "$ROOT/shell/Commons" "$work/config/Commons" + +# Offscreen rendering exercises the real QML bindings without taking a lock +# or connecting to the user's compositor. +HOME="$work/home" XDG_RUNTIME_DIR="$work/runtime" \ + OMARCHY_PATH="$ROOT" QT_QPA_PLATFORM=offscreen QT_QPA_PLATFORMTHEME= \ + QT_STYLE_OVERRIDE= QT_QUICK_BACKEND=software \ + timeout 15 quickshell -p "$work/config" --no-color >"$work/log" 2>&1 || { + cat "$work/log" >&2 + fail "lock feed QML lifecycle runs" + } +if ! grep -q 'OWE_LOCK_TEST_PASS' "$work/log" || grep -q 'OWE_LOCK_TEST_FAIL' "$work/log"; then + cat "$work/log" >&2 + fail "lock feed respects visibility and keeps its poster fallback" +fi +pass "lock feed respects visibility, power saver and blanking; hidden views release images" diff --git a/test/shell.d/video-background-test.sh b/test/shell.d/video-background-test.sh index 31ac754d..761c97a9 100755 --- a/test/shell.d/video-background-test.sh +++ b/test/shell.d/video-background-test.sh @@ -49,11 +49,9 @@ assert( assert( /^import Owe\.LockFeed$/m.test(lockFeedQml) && lockFeedQml.includes('LockFeed {') && - lockFeedQml.includes('active: root.feedEnabled') && lockQml.includes('source: "LockFeedSurface.qml"') && - lockQml.includes('active: root.video') && - /feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) && - /property: "feedEnabled"[\s\S]*?value: root\.feedActive/.test(lockQml), + lockQml.includes('active: root.feedActive') && + /feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml), 'the lock screen shows video through the OWE lock feed, loaded so a missing module costs only the video' ) assert( @@ -61,8 +59,8 @@ assert( 'the lock view itself carries no foreign import, so the lock still loads without the feed module' ) assert( - lockQml.includes('path: root.video ? "" : root.backgroundPath') && - lockQml.includes('visible: !root.video') && + lockQml.includes('path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""') && + lockQml.indexOf('id: feedLoader') > lockQml.indexOf('MultiEffect {') && lockQml.includes('visible: root.video') && !lockQml.includes('wallpaper.video'), 'the lock screen keeps its image effect for stills and shows the feed for videos'