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.
This commit is contained in:
Bjarne Oeverli committed 2026-09-18 18:46:17 +02:00
1 parent 02bb92bc74
commit dbebc458db
6 files changed
+37 -119

No files matched your search

+2 -2
View File
@@ -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
+1 -1
View File
@@ -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.
+5 -14
View File
@@ -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
+6 -17
View File
@@ -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: {
+3 -54
View File
@@ -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 = ""
+20 -31
View File
@@ -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') &&