Files
livekit-cameras/gst_sink.py
T
root 736eccb932 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.
2026-08-28 07:14:21 +00:00

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