fix(recording): track backend subprocess for active state
active shelled out to pidof on every read, blocking the main loop and missing our own recorder handle. Track the spawned Gio.Subprocess for an exact answer and cache the external pidof probe for 2s. Resets the cache at ownership boundaries (start/stop).
This commit is contained in:
2 files changed
+43
-3
No files matched your search
@@ -24,6 +24,12 @@ from utils.functions import is_app_running, resolve_monitor
|
|||||||
|
|
||||||
_RECORDER_PROCESSES = ("wf-recorder", "gpu-screen-recorder")
|
_RECORDER_PROCESSES = ("wf-recorder", "gpu-screen-recorder")
|
||||||
|
|
||||||
|
# How long a cached external-recorder probe stays valid. Detecting a recorder
|
||||||
|
# spawned outside this service requires a ``pidof`` call; caching the result
|
||||||
|
# means reading the ``active`` property never spawns a subprocess on the main
|
||||||
|
# loop more than once per this window.
|
||||||
|
_ACTIVE_PROBE_TTL = 2.0
|
||||||
|
|
||||||
|
|
||||||
class ScreenRecorder(Service):
|
class ScreenRecorder(Service):
|
||||||
"""Record the screen through a pluggable :class:`CaptureBackend`."""
|
"""Record the screen through a pluggable :class:`CaptureBackend`."""
|
||||||
@@ -69,6 +75,8 @@ class ScreenRecorder(Service):
|
|||||||
self._output_file: str | None = None
|
self._output_file: str | None = None
|
||||||
self._start_time: float | None = None
|
self._start_time: float | None = None
|
||||||
self._tick_source_id: int | None = None
|
self._tick_source_id: int | None = None
|
||||||
|
self._external_recorder_active = False
|
||||||
|
self._external_probe_time = 0.0
|
||||||
|
|
||||||
self.recordings_dir = Path.home() / "Videos" / "Recordings"
|
self.recordings_dir = Path.home() / "Videos" / "Recordings"
|
||||||
self.recordings_dir.mkdir(parents=True, exist_ok=True)
|
self.recordings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -121,10 +129,21 @@ class ScreenRecorder(Service):
|
|||||||
def active(self) -> bool:
|
def active(self) -> bool:
|
||||||
"""Whether a recorder process is actually running.
|
"""Whether a recorder process is actually running.
|
||||||
|
|
||||||
Unlike :attr:`recording`, this reflects the real process state and
|
A recording started through this service is tracked exactly via its
|
||||||
also detects recordings started outside this service.
|
subprocess handle. Recordings started outside modus are detected with
|
||||||
|
a short-lived cached ``pidof`` probe, so reading this property never
|
||||||
|
blocks the main loop more than once per ``_ACTIVE_PROBE_TTL`` seconds.
|
||||||
"""
|
"""
|
||||||
return any(is_app_running(proc) for proc in _RECORDER_PROCESSES)
|
ours = self._backend.is_recorder_running()
|
||||||
|
if ours is not None:
|
||||||
|
return ours
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - self._external_probe_time >= _ACTIVE_PROBE_TTL:
|
||||||
|
self._external_recorder_active = any(
|
||||||
|
is_app_running(proc) for proc in _RECORDER_PROCESSES
|
||||||
|
)
|
||||||
|
self._external_probe_time = now
|
||||||
|
return self._external_recorder_active
|
||||||
|
|
||||||
# -- public API --------------------------------------------------------
|
# -- public API --------------------------------------------------------
|
||||||
|
|
||||||
@@ -220,6 +239,10 @@ class ScreenRecorder(Service):
|
|||||||
self._recording = True
|
self._recording = True
|
||||||
self._paused = False
|
self._paused = False
|
||||||
self._start_time = time.monotonic()
|
self._start_time = time.monotonic()
|
||||||
|
# This service now owns the recorder: drop the external cache so the
|
||||||
|
# next ``active`` read resolves exactly from the tracked subprocess.
|
||||||
|
self._external_recorder_active = False
|
||||||
|
self._external_probe_time = 0.0
|
||||||
self._start_tick()
|
self._start_tick()
|
||||||
self.notify("output-file")
|
self.notify("output-file")
|
||||||
self.notify("recording")
|
self.notify("recording")
|
||||||
@@ -230,6 +253,8 @@ class ScreenRecorder(Service):
|
|||||||
self._recording = False
|
self._recording = False
|
||||||
self._paused = False
|
self._paused = False
|
||||||
self._start_time = None
|
self._start_time = None
|
||||||
|
# Force a fresh external probe on the next ``active`` read.
|
||||||
|
self._external_probe_time = 0.0
|
||||||
self.notify("recording")
|
self.notify("recording")
|
||||||
self.notify("busy")
|
self.notify("busy")
|
||||||
self.notify("elapsed-seconds")
|
self.notify("elapsed-seconds")
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ class HyprlandCaptureBackend(CaptureBackend):
|
|||||||
tool: find_binary(tool) is not None
|
tool: find_binary(tool) is not None
|
||||||
for tool in ("hyprshot", "grim", "slurp", self._RECORDER)
|
for tool in ("hyprshot", "grim", "slurp", self._RECORDER)
|
||||||
}
|
}
|
||||||
|
self._recorder_proc: Gio.Subprocess | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capabilities(self) -> BackendCapabilities:
|
def capabilities(self) -> BackendCapabilities:
|
||||||
@@ -163,6 +164,18 @@ class HyprlandCaptureBackend(CaptureBackend):
|
|||||||
"""Return whether a wrapped tool is available on this system."""
|
"""Return whether a wrapped tool is available on this system."""
|
||||||
return self._tools.get(tool, False)
|
return self._tools.get(tool, False)
|
||||||
|
|
||||||
|
def is_recorder_running(self) -> bool | None:
|
||||||
|
"""Return whether the recorder spawned by this backend is still alive.
|
||||||
|
|
||||||
|
``None`` means no recorder was started through this backend, so the
|
||||||
|
caller must fall back to external detection. Cheap: reads the already
|
||||||
|
tracked subprocess instead of shelling out.
|
||||||
|
"""
|
||||||
|
proc = self._recorder_proc
|
||||||
|
if proc is None:
|
||||||
|
return None
|
||||||
|
return not proc.get_if_exited()
|
||||||
|
|
||||||
# screenshots
|
# screenshots
|
||||||
|
|
||||||
def screenshot(self, request: ScreenshotRequest, on_result: ResultCallback) -> bool:
|
def screenshot(self, request: ScreenshotRequest, on_result: ResultCallback) -> bool:
|
||||||
@@ -322,10 +335,12 @@ class HyprlandCaptureBackend(CaptureBackend):
|
|||||||
if proc is None:
|
if proc is None:
|
||||||
on_result(CaptureResult.failure(f"failed to launch {self._RECORDER}"))
|
on_result(CaptureResult.failure(f"failed to launch {self._RECORDER}"))
|
||||||
return False
|
return False
|
||||||
|
self._recorder_proc = proc
|
||||||
on_result(CaptureResult.ok(str(request.output_file)))
|
on_result(CaptureResult.ok(str(request.output_file)))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def stop_recording(self) -> bool:
|
def stop_recording(self) -> bool:
|
||||||
|
self._recorder_proc = None
|
||||||
kill_process(self._RECORDER)
|
kill_process(self._RECORDER)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user