"""GStreamer kmssink pipelines for one DRM connector.""" from __future__ import annotations import logging import os import shutil import subprocess from typing import Optional from drm_outputs import DrmOutput log = logging.getLogger("cameras.display") GST_LAUNCH = shutil.which("gst-launch-1.0") or "gst-launch-1.0" def _kms_props(out: DrmOutput) -> list[str]: props = [ "kmssink", f"connector-id={out.connector_id}", "force-modesetting=true", "can-scale=true", "sync=false", "restore-crtc=true", ] if out.driver: props.append(f"driver-name={out.driver}") return props def test_pattern_cmd(out: DrmOutput, pattern: str = "smpte", width: int = 1280, height: int = 720) -> list[str]: return [ GST_LAUNCH, "-e", "videotestsrc", "is-live=true", f"pattern={pattern}", "!", f"video/x-raw,width={width},height={height},framerate=30/1", "!", "videoconvert", "!", "videoscale", "!", *_kms_props(out), ] def appsrc_cmd(out: DrmOutput, width: int, height: int, fps: int = 30) -> list[str]: """Raw BGRA frames on stdin -> kmssink.""" block = width * height * 4 return [ GST_LAUNCH, "-e", "fdsrc", "fd=0", f"blocksize={block}", "!", "videoparse", f"width={width}", f"height={height}", "format=bgra", f"framerate={fps}/1", "!", "queue", "max-size-buffers=2", "leaky=downstream", "!", "videoconvert", "!", "videoscale", "method=0", "!", *_kms_props(out), ] class KmsPipeline: """One gst-launch kmssink process, fed raw BGRA on stdin (or self-running testsrc).""" def __init__(self, out: DrmOutput, identity: str): self.out = out self.identity = identity self.proc: Optional[subprocess.Popen] = None self.width = 0 self.height = 0 self.mode = "idle" @property def alive(self) -> bool: return self.proc is not None and self.proc.poll() is None def stop(self) -> None: if self.proc is None: return if self.proc.stdin: try: self.proc.stdin.close() except OSError: pass if self.proc.poll() is None: self.proc.terminate() try: self.proc.wait(timeout=3) except subprocess.TimeoutExpired: self.proc.kill() self.proc.wait(timeout=2) self.proc = None self.mode = "idle" def start_test_pattern(self, pattern: str = "smpte") -> None: self.stop() if not self.out.connected: log.warning("%s: %s not connected, skip test pattern", self.identity, self.out.name) return cmd = test_pattern_cmd(self.out, pattern=pattern) log.info("%s: gst test pattern %s on %s id=%s", self.identity, pattern, self.out.name, self.out.connector_id) self.proc = subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, env=_gst_env(), ) self.mode = "pattern" def start_appsrc(self, width: int, height: int, fps: int = 30) -> None: if self.alive and self.mode == "appsrc" and self.width == width and self.height == height: return self.stop() if not self.out.connected: log.warning("%s: %s not connected, cannot start kmssink", self.identity, self.out.name) return cmd = appsrc_cmd(self.out, width, height, fps) log.info( "%s: gst appsrc %dx%d -> %s (connector %s %s)", self.identity, width, height, self.out.name, self.out.connector_id, self.out.driver, ) self.proc = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, env=_gst_env(), bufsize=0, ) self.width = width self.height = height self.mode = "appsrc" def push(self, frame: bytes) -> bool: if not self.alive or self.proc is None or self.proc.stdin is None: return False try: self.proc.stdin.write(frame) return True except BrokenPipeError: log.warning("%s: gst stdin closed (rc=%s)", self.identity, self.proc.poll()) self.proc = None return False def gst_errors(self) -> str: if self.proc is None or self.proc.stderr is None: return "" try: os.set_blocking(self.proc.stderr.fileno(), False) except OSError: pass try: chunk = self.proc.stderr.read() or b"" except OSError: return "" return chunk.decode("utf-8", "replace") def _gst_env() -> dict: env = os.environ.copy() # Never try X11/Wayland; kmssink is the point. env.pop("DISPLAY", None) env.pop("WAYLAND_DISPLAY", None) env.setdefault("GST_GL_PLATFORM", "egl") return env