Files
shaders/render_wallpaper.py
emil28092005 92e8b287a3 Render the ice as long videos and play them as wallpapers
render_wallpaper.py drives headless Firefox frame by frame (four at a time,
resuming where it stopped), then ffmpeg. Thirty seconds at 30 fps is exactly
one plasma cycle, so frame 900 meets frame 0 and the loop has no seam.

plasma-plugin/dev.emil.video is a minimal Plasma 6 video wallpaper: one file
per screen, filling the screen, looping. H.264 is encoded Baseline because
Qt here decodes through openh264, which rejects High profile and leaves a
frozen frame; a new source also has to be played explicitly.

The #shot hash learned info=0 to hide the on-screen hint for renders.
2026-10-31 01:22:16 +03:00

70 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Render a seamless video of one of the pages: frames first, then ffmpeg.
render_wallpaper.py <page.html> <out.mp4> <width> <height> [seconds] [fps]
Frames land in <out.mp4>.frames/ as PNG and are kept so a re-run resumes.
The page's loop is exactly 30 s, so keep the duration a multiple of that to
make the video loop without a seam."""
import os
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
page, out = Path(sys.argv[1]).resolve(), Path(sys.argv[2]).resolve()
width, height = int(sys.argv[3]), int(sys.argv[4])
seconds = float(sys.argv[5]) if len(sys.argv) > 5 else 30.0
fps = int(sys.argv[6]) if len(sys.argv) > 6 else 30
workers = min(4, os.cpu_count() or 2) # 8 headless Firefox at once segfaults the GPU driver
frames = out.with_suffix('.frames')
frames.mkdir(parents=True, exist_ok=True)
profiles = [Path(f'/tmp/ff_wall_{i}') for i in range(workers)]
for profile in profiles:
profile.mkdir(exist_ok=True)
total = int(round(seconds * fps))
def render(index):
target = frames / f'{index:04d}.png'
if target.exists() and target.stat().st_size > 20000:
return 0.0
url = f'{page.as_uri()}#shot&t={index / fps:.6f}&info=0'
profile = profiles[index % workers]
for attempt in range(4):
started = time.time()
result = subprocess.run(['firefox', '--headless', '--new-instance',
'--profile', str(profile),
f'--window-size={width},{height}',
f'--screenshot={target}', url],
timeout=300,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if result.returncode == 0 and target.exists() and target.stat().st_size > 20000:
return time.time() - started
target.unlink(missing_ok=True)
time.sleep(1 + attempt)
raise RuntimeError(f'frame {index} failed')
began = time.time()
with ThreadPoolExecutor(workers) as pool:
for done, _ in enumerate(pool.map(render, range(total)), 1):
if done % 30 == 0 or done == total:
elapsed = time.time() - began
print(f'{done}/{total} frames, {elapsed:.0f}s elapsed, '
f'~{elapsed / done * (total - done):.0f}s left', flush=True)
out.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(['ffmpeg', '-y', '-hide_banner', '-loglevel', 'warning',
'-framerate', str(fps), '-i', str(frames / '%04d.png'),
# Baseline on purpose: Fedora's Qt Multimedia decodes H.264 through
# openh264, which rejects High profile ("DecodeFrame failed" in the
# journal) and then Plasma shows a frozen frame.
'-c:v', 'h264_nvenc', '-profile:v', 'baseline', '-preset', 'p7',
'-rc', 'constqp', '-qp', '18', '-pix_fmt', 'yuv420p',
'-movflags', '+faststart', str(out)], check=True)
print(f'{out} written ({out.stat().st_size / 1e6:.1f} MB)')