Skip to content

Feature/video background - #983

Open
mcgi5sr2 wants to merge 10 commits into
hyprwm:mainfrom
mcgi5sr2:feature/video-background
Open

mcgi5sr2 wants to merge 10 commits into
hyprwm:mainfrom
mcgi5sr2:feature/video-background

Conversation

@mcgi5sr2

@mcgi5sr2 mcgi5sr2 commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

given this another stab, hopefully this will make it easier to merge in future

@mcgi5sr2

mcgi5sr2 commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

build failure is just lack of ffmpeg dependency, I have two commits that will resolve it

  1. just add ffmpeg dependency to nix
  2. make it all optional at compile time

@mcgi5sr2

mcgi5sr2 commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

apologies for commit spam, the change in log::log arrived mid work on this.

hyprlock-demo-trimmed.mp4

video of what this allows, as eye candy

@PointerDilemma

Copy link
Copy Markdown
Collaborator

I have nothing against it in particular. But I have been telling people to use this method with Hyprland, which allows for this feature without support in hyprlock:

  1. hyprctl keyword misc:session_lock_xray 1
  2. make hyprlock background transparent
index 2e28cb5a2e..5807e4ea04 100644
--- a/assets/example.conf
+++ b/assets/example.conf
@@ -35,8 +35,9 @@
 
 background {
     monitor =
-    path = screenshot
-    blur_passes = 3
+    path =
+    blur_passes = 0
+    color = rgba(00000000)
 }
 
 input-field {
  1. launch smth like mpvpaper to run the video. mpvpaper -o "loop" -l overlay DP-3 <video>

The PR has some minor style issues, e.g. Single line if's, too much code in the Background widget. I am also a bit worried about memory safety in the decode thread. Otherwise it looks ok from a first glance.

@mcgi5sr2

Copy link
Copy Markdown
Contributor Author

yea I appreciate that works, but it didn't seem to me to be the way a lock screen should work with a transparent background and then requiring another programs overlay. I can keep this maintained for personal use, and if you want it upstream I am happy to push

@Keyaruga-Reese

Keyaruga-Reese commented Apr 16, 2026 •

Copy link
Copy Markdown

I did build this version and is working great. I have been using this for over a week and haven't run into any issue.
I do hope, and pray that this gets merged. Or at least, maintained as a fork.

Edit 1:
I did try making the hyprlock background transparent but I am not a fan of it. Moreover, mpvpaper (mpv in general) causes huge memory leaks on nvidia (i run hyprland on nvidia).
Hence, in my case (may apply to many as there are people who use nvidia gpu's and run hyprland), I think this feature is pretty good.

@mcgi5sr2

Copy link
Copy Markdown
Contributor Author

I'll maintain it for the foreseeable, but as we all know that's not forever as I get into other work streams

@mcgi5sr2
mcgi5sr2 force-pushed the feature/video-background branch from a48820a to cc1836d Compare June 26, 2026 11:06
@mcgi5sr2
mcgi5sr2 force-pushed the feature/video-background branch from cc1836d to 6040d02 Compare August 12, 2026 11:13
Add libavcodec, libavformat, libavutil, and libswscale to the
pkg_check_modules dependency list to support native video decoding
in the background widget.
Decoding runs on a background thread owned by CVideoBackend; the
Background widget uploads frames to a GL texture and composites them
like any other background asset. Supports mp4, mkv, webm and other
common containers, with seamless looping.
Add VIDEO_BACKEND cmake option (default ON) that controls whether FFmpeg
is required and video playback is compiled in. When OFF, VideoBackend.cpp
is excluded from the build and all video code paths in Background are
gated by HYPRLOCK_HAS_VIDEO, allowing a no-FFmpeg build for packagers
who want to ship without the dependency.
…otation

- add AVIO interrupt callback + condition-variable pacing so stop() no
  longer stalls on av_read_frame or a sleeping PTS wait
- set swscale colorspace coefficients from the frame (BT.601/709/2020,
  HD/SD heuristic for unspecified) instead of the BT.601 defaults
- read display-matrix side data and expose rotation for the renderer
- own the GL texture + upload path so the widget only composites
- split stop control from thread-alive status so isRunning() is honest
- break instead of spinning when a source cannot be seeked for looping
…coder exit

- expand ~ and relative paths before opening a video
- crossfade from the lock screenshot during fade-in, matching the image path
- stop requesting frames when the decoder has died or playback cannot loop,
  holding the last frame instead of rendering at refresh rate forever
- apply display-matrix rotation (direction verified against VLC), including
  through the blur path by generalizing renderToFB to take a transform
- move the GL upload into CVideoBackend and slim draw() to a drawVideo() call
@mcgi5sr2
mcgi5sr2 force-pushed the feature/video-background branch from 6040d02 to 4008cda Compare August 13, 2026 11:25
@mcgi5sr2

Copy link
Copy Markdown
Contributor Author

@PointerDilemma I've reworked the decode thread to tighten this up (0040ce5, 4008cda).

The decode thread owns all FFmpeg state after open() returns, the render thread owns the GL texture. The only shared state is a pre-sized buffer and a flag behind a mutex, published by std::swap, so no pointers into AVFrame data ever cross threads and nothing allocates after open(). The sws_scale destination is a fixed 4WH buffer with matching stride, so a mid-stream resolution change scales into the same bounds instead of overrunning them.

The thread also can't outlive the object: stop() sets an atomic flag, wakes the pacing wait, and joins before freeing anything. An interrupt callback on the format context aborts a blocked av_read_frame, so the join is bounded.

Style points are addressed too: video code is out of the Background widget, single-line ifs are gone, and the branch is clang-formatted. Also fixed since your review: BT.709 colourspace, display-matrix rotation, ~ path expansion, and cross-fade from the screenshot on fade-in.

I plan to look at the N monitor issue next, and even if we could move a lot of this into hypr-graphics and then have it as a call here, and perhaps hypr-paper as well.

- open the file on the decode thread: a FIFO or dead network mount can no
  longer freeze the lock screen's first frame; failures degrade to the
  background color
- drive redraws from a frame-publish callback instead of returning true
  every draw, so a 24fps video no longer re-renders at 144Hz; also removes
  the final-frame drop race at decoder exit
- pace frames relative to stream start_time, fixing fast-forward or frozen
  playback on MPEG-TS captures with PCR offsets
- drain the decoder's reorder tail at EOF before seeking, so B-frame clips
  loop without dropping frames and clips shorter than the decoder delay
  still display
- pace PTS-less frames and loop seeks at the container frame rate, fixing
  100% CPU spins on static gifs and timestamp-less streams
- hold the stop mutex while setting the stop flag, fixing a lost wakeup
  that could stall unlock by up to 5s
- premultiply alpha frames and render the background color underneath,
  fixing transparent gif/video compositing
- keep reload_cmd working with video paths and allow reloads to switch
  between images and videos in both directions
- fail configure on FFmpeg older than 6.1 instead of failing the build
- reject streams with no valid dimensions; name the decode thread
- move the HYPRLOCK_HAS_VIDEO seam into VideoBackend.hpp via a no-video
  stub, removing all ifdef blocks from the Background widget; the video
  branches statically compile out when FFmpeg is absent
- extract renderFallback() shared by the image and video draw paths
- scope frame-publish redraws to the video's own output instead of
  re-rendering every monitor
- cap the swscale destination at the output's aspect-fill size, cutting
  per-frame convert/premultiply/upload/blur bandwidth for video larger
  than the screen (never upscales)
- drop dead isRunning()/m_threadAlive, the optional<eTransform> wrapper,
  the redundant reload reset dance and the m_alphaChecked latch; derive
  the odd-rotation size swap from transform parity; store the frame
  interval as steady_clock::duration
- nix: require ffmpeg unconditionally like other inputs
@mcgi5sr2

Copy link
Copy Markdown
Contributor Author

used a Claude agent to review the PR found a couple of issues I agreed with so applied the later couple of commits.

@mcgi5sr2

Copy link
Copy Markdown
Contributor Author

Feature

  • Video file backgrounds via FFmpeg (mp4, mkv, webm, mov, gif and others) with seamless looping
  • Optional at compile time (VIDEO_BACKEND cmake flag, default on). Without FFmpeg the widgets compile against a stub and video paths are removed by the compiler
  • Requires FFmpeg 6.1 or newer, checked at configure time

Correctness

  • Files are opened on the decode thread, so a dead mount or FIFO cannot freeze the lock screen
  • Colours converted with the stream's real colorspace (BT.601/709/2020) instead of swscale defaults
  • Display matrix rotation honoured, including through the blur path, verified against VLC
  • Frame timing uses stream start_time, fixing MPEG-TS captures that played fast or froze
  • Decoder drained at end of file before looping, so B-frame clips loop cleanly and very short clips display at all
  • Transparent video and gifs premultiplied and composited over the background colour
  • Paths with ~ or relative components resolved before opening
  • reload_cmd works with videos and can switch between image and video on reload
  • Screenshot crossfade on lock fade-in now works for video backgrounds

Performance

  • Redraws are driven by decoded frames, not display refresh: a 24fps video renders 24 times a second on any monitor
  • Only the output showing the video is redrawn on a new frame
  • Frames convert at no more than the output size, cutting bandwidth roughly 4x for 4K sources on smaller screens
  • Static gifs and streams without timestamps pace at the container frame rate instead of spinning a core
  • Unlock is immediate: the decode thread stops via interrupt callback and condition variable, no multi-second stall

Known limits

  • Software decode only for now (36% of one core for 1080p24 on this machine, hardware decode blocked on Asahi AVD driver and FFmpeg support)
  • One decoder per monitor showing the same file, shared decoding is planned as follow-up work
  • .gif files now route to FFmpeg rather than the image loader

Next

  • hardware decode
  • N monitors shared decoder

addTimer runs on non-main threads (PAM, resource workers, and now the
video decode thread at frame rate), so the unlocked vector copy in
getTimers could race a reallocation. Copy under the same lock.
- registry of weak backends keyed by (path, revision): M monitors showing
  the same file now share a single decode pipeline instead of running M;
  the revision key keeps reload_cmd overwrites working while other outputs
  still hold the old decoder
- publish/texture serial pair replaces the new-frame flag; outputs compare
  serials so each re-blurs exactly once per frame while only the first
  caller uploads
- frame listeners are token-based; a global and a monitor-specific
  background on one output register independently
- hardware decode: try each of the codec's hw configs, first device that
  creates wins, frames download via av_hwframe_transfer_data; falls back
  to software at device creation, codec open, format negotiation and
  frame download (on Asahi the Vulkan device creates but hwaccel setup
  fails, exercising the fallback end to end)
- backpressure: the decoder parks when published frames go unconsumed
  (display off / DPMS) instead of decoding all night, and rebases pacing
  on resume
- drop late frames before conversion so a decoder that falls behind stops
  burning a core on frames that would never be shown
- hold single-frame files (static gifs) instead of re-decoding an
  identical frame for the whole session
- keep the current video playing when a reload is refused, ignore stale
  image assets resolving while a video plays, and clear pendingResource
  on failed asset updates so reloads cannot wedge permanently
- log av_read_frame errors instead of dying silently; check
  avcodec_parameters_to_context; grow the conversion target when a larger
  output attaches; recognize ogv/mpg/mpeg extensions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants