Working snapshot before the OpenVINO MetricGAN GPU spike: thread-capped SpeechBrain DelaySum+MetricGAN on CPU, hop 200 ms, video hold for A/V sync, Arc VAAPI H.264, headless kmssink wall. 32 tests passing. Do not treat this as a GPU audio path.
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
"""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
|