feat: known-good CPU audio + 1080p VAAPI + KMS display wall
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.
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user