From 2eda287a80ef7209b9ec427030cfc7e2d274709e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 13 Sep 2025 22:58:57 +1000 Subject: record: wl-screenrec -> gpu-screen-recorder Supports NVIDIA, so no need for having a fallback Also supports pausing --- README.md | 6 +- default.nix | 4 +- src/caelestia/parser.py | 1 + src/caelestia/subcommands/record.py | 111 ++++++++++++++---------------------- 4 files changed, 47 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index edfa443..67948d8 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,8 @@ The main control script for the Caelestia dotfiles. - [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps - [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard - [`slurp`](https://github.com/emersion/slurp) - selecting an area -- [`wl-screenrec`](https://github.com/russelltg/wl-screenrec) - screen recording (default) -- [`wf-recorder`](https://github.com/ammen99/wf-recorder) - screen recording (for NVIDIA GPUs) +- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording - `glib2` - closing notifications -- `libpulse` - getting audio device - [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history - [`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) ```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` diff --git a/default.nix b/default.nix index 87e03e8..492983a 100644 --- a/default.nix +++ b/default.nix @@ -12,7 +12,7 @@ dart-sass, grim, fuzzel, - wl-screenrec, + gpu-screen-recorder, dconf, killall, caelestia-shell, @@ -50,7 +50,7 @@ python3.pkgs.buildPythonApplication { dart-sass grim fuzzel - wl-screenrec + gpu-screen-recorder dconf killall ] diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py index 311b782..840ead5 100644 --- a/src/caelestia/parser.py +++ b/src/caelestia/parser.py @@ -70,6 +70,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): record_parser.set_defaults(cls=record.Command) 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("-p", "--pause", action="store_true", help="pause/resume the recording") # Create parser for clipboard opts clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history") diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py index 3cfde82..2b946c6 100644 --- a/src/caelestia/subcommands/record.py +++ b/src/caelestia/subcommands/record.py @@ -1,4 +1,5 @@ import json +import re import shutil import subprocess import time @@ -8,108 +9,80 @@ from datetime import datetime from caelestia.utils.notify import close_notification, notify from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir +RECORDER = "gpu-screen-recorder" + class Command: args: Namespace - recorder: str def __init__(self, args: Namespace) -> None: 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: - if self.proc_running(): + if self.args.pause: + subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL) + elif self.proc_running(): self.stop() else: self.start() 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: - args = [] + args = ["-w"] + monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) if self.args.region: 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: - region = self.args.region - args += ["-g", region.strip()] + region = self.args.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: - monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"])) focused_monitor = next(monitor for monitor in monitors if monitor["focused"]) if focused_monitor: - args += ["-o", focused_monitor["name"]] + args += [focused_monitor["name"], "-f", str(round(focused_monitor["refreshRate"]))] if self.args.sound: - sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines() - 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] + args += ["-a", "default_output"] recording_path.parent.mkdir(parents=True, exist_ok=True) - proc = subprocess.Popen( - [self.recorder, *args, "-f", recording_path], - stderr=subprocess.PIPE, - text=True, - start_new_session=True, - ) + proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], start_new_session=True) notif = notify("-p", "Recording started", "Recording...") recording_notif_path.write_text(notif) - for _ in range(5): - if proc.poll() is not None: - if proc.returncode != 0: - close_notification(notif) - notify("Recording failed", f"Recording error: {proc.communicate()[1]}") - return - time.sleep(0.2) + try: + if proc.wait(1) != 0: + close_notification(notif) + notify( + "Recording failed", + "An error occurred attempting to start recorder. " + f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}", + ) + except subprocess.TimeoutExpired: + pass def stop(self) -> None: # 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 while self.proc_running(): -- cgit v1.2.3-freya