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.
This commit is contained in:
8 files changed
+92
-248
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user