record: wl-screenrec -> gpu-screen-recorder

Supports NVIDIA, so no need for having a fallback
Also supports pausing
This commit is contained in:
2 * r + 2 * t
2025-09-13 22:58:57 +10:00
parent 4263e5f809
commit 2eda287a80
4 changed files with 47 additions and 75 deletions
+2 -4
View File
@@ -11,10 +11,8 @@ The main control script for the Caelestia dotfiles.
- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps - [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard - [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
- [`slurp`](https://github.com/emersion/slurp) - selecting an area - [`slurp`](https://github.com/emersion/slurp) - selecting an area
- [`wl-screenrec`](https://github.com/russelltg/wl-screenrec) - screen recording (default) - [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording
- [`wf-recorder`](https://github.com/ammen99/wf-recorder) - screen recording (for NVIDIA GPUs)
- `glib2` - closing notifications - `glib2` - closing notifications
- `libpulse` - getting audio device
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history - [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker - [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
@@ -45,7 +43,7 @@ Install all [dependencies](#dependencies), then install
e.g. via an AUR helper (yay) e.g. via an AUR helper (yay)
```sh ```sh
yay -S libnotify swappy grim dart-sass app2unit wl-clipboard slurp wl-screenrec wf-recorder glib2 libpulse cliphist fuzzel python-build python-installer python-hatch python-hatch-vcs yay -S libnotify swappy grim dart-sass app2unit wl-clipboard slurp gpu-screen-recorder glib2 cliphist fuzzel python-build python-installer python-hatch python-hatch-vcs
``` ```
Now, clone the repo, `cd` into it, build the wheel via `python -m build --wheel` Now, clone the repo, `cd` into it, build the wheel via `python -m build --wheel`
+2 -2
View File
@@ -12,7 +12,7 @@
dart-sass, dart-sass,
grim, grim,
fuzzel, fuzzel,
wl-screenrec, gpu-screen-recorder,
dconf, dconf,
killall, killall,
caelestia-shell, caelestia-shell,
@@ -50,7 +50,7 @@ python3.pkgs.buildPythonApplication {
dart-sass dart-sass
grim grim
fuzzel fuzzel
wl-screenrec gpu-screen-recorder
dconf dconf
killall killall
] ]
+1
View File
@@ -70,6 +70,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
record_parser.set_defaults(cls=record.Command) record_parser.set_defaults(cls=record.Command)
record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region") record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region")
record_parser.add_argument("-s", "--sound", action="store_true", help="record audio") record_parser.add_argument("-s", "--sound", action="store_true", help="record audio")
record_parser.add_argument("-p", "--pause", action="store_true", help="pause/resume the recording")
# Create parser for clipboard opts # Create parser for clipboard opts
clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history") clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history")
+42 -69
View File
@@ -1,4 +1,5 @@
import json import json
import re
import shutil import shutil
import subprocess import subprocess
import time import time
@@ -8,108 +9,80 @@ from datetime import datetime
from caelestia.utils.notify import close_notification, notify from caelestia.utils.notify import close_notification, notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir
RECORDER = "gpu-screen-recorder"
class Command: class Command:
args: Namespace args: Namespace
recorder: str
def __init__(self, args: Namespace) -> None: def __init__(self, args: Namespace) -> None:
self.args = args self.args = args
self.recorder = self._detect_recorder()
def _detect_recorder(self) -> str:
"""Detect which screen recorder to use based on GPU."""
try:
# Check for NVIDIA GPU
lspci_output = subprocess.check_output(["lspci"], text=True)
if "nvidia" in lspci_output.lower():
# Check if wf-recorder is available
if shutil.which("wf-recorder"):
return "wf-recorder"
# Default to wl-screenrec if available
if shutil.which("wl-screenrec"):
return "wl-screenrec"
# Fallback to wf-recorder if wl-screenrec is not available
if shutil.which("wf-recorder"):
return "wf-recorder"
raise RuntimeError("No compatible screen recorder found")
except subprocess.CalledProcessError:
# If lspci fails, default to wl-screenrec
return "wl-screenrec" if shutil.which("wl-screenrec") else "wf-recorder"
def run(self) -> None: def run(self) -> None:
if self.proc_running(): if self.args.pause:
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL)
elif self.proc_running():
self.stop() self.stop()
else: else:
self.start() self.start()
def proc_running(self) -> bool: def proc_running(self) -> bool:
return subprocess.run(["pidof", self.recorder], stdout=subprocess.DEVNULL).returncode == 0 return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL).returncode == 0
def intersects(self, a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> bool:
return a[0] < b[0] + b[2] and a[0] + a[2] > b[0] and a[1] < b[1] + b[3] and a[1] + a[3] > b[1]
def start(self) -> None: def start(self) -> None:
args = [] args = ["-w"]
monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"]))
if self.args.region: if self.args.region:
if self.args.region == "slurp": if self.args.region == "slurp":
region = subprocess.check_output(["slurp"], text=True) region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True)
else: else:
region = self.args.region region = self.args.region.strip()
args += ["-g", region.strip()] args += ["region", "-region", region]
m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region)
if not m:
raise ValueError(f"Invalid region: {region}")
w, h, x, y = map(int, m.groups())
r = x, y, w, h
max_rr = 0
for monitor in monitors:
if self.intersects((monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r):
rr = round(monitor["refreshRate"])
max_rr = max(max_rr, rr)
args += ["-f", str(max_rr)]
else: else:
monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"]))
focused_monitor = next(monitor for monitor in monitors if monitor["focused"]) focused_monitor = next(monitor for monitor in monitors if monitor["focused"])
if focused_monitor: if focused_monitor:
args += ["-o", focused_monitor["name"]] args += [focused_monitor["name"], "-f", str(round(focused_monitor["refreshRate"]))]
if self.args.sound: if self.args.sound:
sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines() args += ["-a", "default_output"]
audio_source = None
for source in sources:
if "RUNNING" in source:
audio_source = source.split()[1]
break
# Fallback to IDLE source if no RUNNING source
if not audio_source:
for source in sources:
if "IDLE" in source:
audio_source = source.split()[1]
break
if not audio_source:
raise ValueError("No audio source found")
if self.recorder == "wf-recorder":
args += [f"--audio={audio_source}"]
else:
args += ["--audio", "--audio-device", audio_source]
recording_path.parent.mkdir(parents=True, exist_ok=True) recording_path.parent.mkdir(parents=True, exist_ok=True)
proc = subprocess.Popen( proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], start_new_session=True)
[self.recorder, *args, "-f", recording_path],
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
notif = notify("-p", "Recording started", "Recording...") notif = notify("-p", "Recording started", "Recording...")
recording_notif_path.write_text(notif) recording_notif_path.write_text(notif)
for _ in range(5): try:
if proc.poll() is not None: if proc.wait(1) != 0:
if proc.returncode != 0: close_notification(notif)
close_notification(notif) notify(
notify("Recording failed", f"Recording error: {proc.communicate()[1]}") "Recording failed",
return "An error occurred attempting to start recorder. "
time.sleep(0.2) f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}",
)
except subprocess.TimeoutExpired:
pass
def stop(self) -> None: def stop(self) -> None:
# Start killing recording process # Start killing recording process
subprocess.run(["pkill", self.recorder]) subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL)
# Wait for recording to finish to avoid corrupted video file # Wait for recording to finish to avoid corrupted video file
while self.proc_running(): while self.proc_running():