feat: point the fleet at wss LiveKit room uwh-telhai
Publish and subscribe over wss://livekit.uni-wh.de:7800 and refuse cleartext ws://. Conference room is uwh-telhai. Includes the uncommitted encoded H.264 publish path, Rally hairpin, and KMS wall overlay.
This commit is contained in:
+175
@@ -0,0 +1,175 @@
|
||||
"""GStreamer capture: C920 MJPEG -> jpegdec -> I420.
|
||||
|
||||
Shared capture daemon hosts a small pool of gst-launch processes (not
|
||||
one per worker). No sidecar H.264: LiveKit VAAPI is the only encoder.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
|
||||
GST_LAUNCH = shutil.which("gst-launch-1.0") or "gst-launch-1.0"
|
||||
|
||||
# 5 cameras per gst-launch. group=1 does not raise fps (UVC still 15);
|
||||
# a small pool still cuts 20 worker pipelines.
|
||||
GST_GROUP_SIZE = 5
|
||||
|
||||
_QUEUE = (
|
||||
"queue",
|
||||
"max-size-buffers=1",
|
||||
"max-size-bytes=0",
|
||||
"max-size-time=0",
|
||||
"leaky=downstream",
|
||||
)
|
||||
|
||||
|
||||
def _caps(width: int, height: int, fps: int) -> str:
|
||||
fps = max(1, int(fps))
|
||||
return f"image/jpeg,width={int(width)},height={int(height)},framerate={fps}/1"
|
||||
|
||||
|
||||
JPEG_MCU_FLICKER_CAMS = frozenset()
|
||||
|
||||
|
||||
def jpeg_mcu_bottom_crop(height: int) -> int:
|
||||
"""Rows in the last 16-tall 4:2:0 JPEG MCU (0 if height is aligned).
|
||||
|
||||
C920 MJPEG 640x360 leaves 8 leftover rows. Some sensors fill that
|
||||
pad with garbage that flickers on the wall (cam-07/14/15).
|
||||
"""
|
||||
h = int(height)
|
||||
if h < 32:
|
||||
return 0
|
||||
rem = h % 16
|
||||
if rem == 0:
|
||||
return 0
|
||||
return rem + 16
|
||||
|
||||
|
||||
def needs_jpeg_mcu_fix(ident: str) -> bool:
|
||||
return ident in JPEG_MCU_FLICKER_CAMS
|
||||
|
||||
|
||||
def crop_scale_i420_drop_bottom(
|
||||
buf: bytearray | bytes, width: int, height: int, crop: int
|
||||
) -> bytearray:
|
||||
"""Drop the bottom `crop` I420 rows; fill with studio black (Y=16).
|
||||
|
||||
Unused on the live path: Y=16 is a raised bar once encode is 16-aligned.
|
||||
Clone is the live fix on JPEG_MCU_FLICKER_CAMS. Kept for tests.
|
||||
"""
|
||||
crop = int(crop)
|
||||
width, height = int(width), int(height)
|
||||
need = width * height * 3 // 2
|
||||
out = bytearray(buf[:need] if len(buf) >= need else buf)
|
||||
if crop <= 0 or height <= crop or width < 2 or height % 2 or crop % 2:
|
||||
return out
|
||||
y_stride = width
|
||||
y_size = width * height
|
||||
uv_w = width // 2
|
||||
uv_h = height // 2
|
||||
for r in range(height - crop, height):
|
||||
dst = r * y_stride
|
||||
out[dst:dst + y_stride] = bytes([16]) * y_stride
|
||||
uv_crop = crop // 2
|
||||
u_base = y_size
|
||||
v_base = y_size + uv_w * uv_h
|
||||
z = bytes([128]) * uv_w
|
||||
for r in range(uv_h - uv_crop, uv_h):
|
||||
out[u_base + r * uv_w:u_base + (r + 1) * uv_w] = z
|
||||
out[v_base + r * uv_w:v_base + (r + 1) * uv_w] = z
|
||||
return out
|
||||
|
||||
|
||||
def repeat_i420_bottom_from_last_good(
|
||||
buf: bytearray, width: int, height: int, crop: int
|
||||
) -> None:
|
||||
"""Overwrite the bottom `crop` I420 rows by cloning the last good line.
|
||||
|
||||
Used after videocrop+videobox (studio-black pad). Cloning beats a
|
||||
visible 8px bar and beats videoscale (which re-edged cam-07).
|
||||
"""
|
||||
crop = int(crop)
|
||||
if crop <= 0:
|
||||
return
|
||||
width, height = int(width), int(height)
|
||||
if width <= 0 or height <= crop or width % 2 or height % 2 or crop % 2:
|
||||
return
|
||||
y_stride = width
|
||||
y_size = width * height
|
||||
uv_w = width // 2
|
||||
uv_h = height // 2
|
||||
uv_crop = crop // 2
|
||||
last_y = height - crop - 1
|
||||
src_y = last_y * y_stride
|
||||
src_row = buf[src_y:src_y + y_stride]
|
||||
for r in range(height - crop, height):
|
||||
dst = r * y_stride
|
||||
buf[dst:dst + y_stride] = src_row
|
||||
last_uv = uv_h - uv_crop - 1
|
||||
u_base = y_size
|
||||
v_base = y_size + uv_w * uv_h
|
||||
src_u = buf[u_base + last_uv * uv_w:u_base + (last_uv + 1) * uv_w]
|
||||
src_v = buf[v_base + last_uv * uv_w:v_base + (last_uv + 1) * uv_w]
|
||||
for r in range(uv_h - uv_crop, uv_h):
|
||||
buf[u_base + r * uv_w:u_base + (r + 1) * uv_w] = src_u
|
||||
buf[v_base + r * uv_w:v_base + (r + 1) * uv_w] = src_v
|
||||
|
||||
|
||||
def _one_cam(device: str, width: int, height: int, fps: int, fd: int) -> list[str]:
|
||||
width, height = int(width), int(height)
|
||||
tail: list[str] = [
|
||||
"videoconvert", "qos=false", "n-threads=2",
|
||||
"!", "video/x-raw,format=I420",
|
||||
"!", *_QUEUE,
|
||||
"!", "fdsink", f"fd={int(fd)}", "sync=false",
|
||||
]
|
||||
return [
|
||||
"v4l2src", f"device={device}", "do-timestamp=false",
|
||||
"!", _caps(width, height, fps),
|
||||
"!", "jpegparse",
|
||||
"!", "jpegdec", "qos=false",
|
||||
"!", *tail,
|
||||
]
|
||||
|
||||
|
||||
def video_capture_cmd(
|
||||
device: str,
|
||||
width: int,
|
||||
height: int,
|
||||
fps: int,
|
||||
h264_fifo: str | None = None, # ignored: no sidecar encode
|
||||
bitrate_kbps: int = 800,
|
||||
fd: int = 1,
|
||||
) -> list[str]:
|
||||
"""Single-camera pipeline: jpegdec -> I420, latest-frame queue, fdsink."""
|
||||
del h264_fifo, bitrate_kbps
|
||||
return [GST_LAUNCH, "-q", *_one_cam(device, width, height, fps, fd)]
|
||||
|
||||
|
||||
def shared_capture_cmd(
|
||||
cameras: list[tuple[str, str]],
|
||||
width: int,
|
||||
height: int,
|
||||
fps: int,
|
||||
fds: list[int],
|
||||
) -> list[str]:
|
||||
"""One gst-launch with N jpegdec->I420->fdsink graphs (no h264enc)."""
|
||||
if len(cameras) != len(fds):
|
||||
raise ValueError("cameras and fds length mismatch")
|
||||
cmd: list[str] = [GST_LAUNCH, "-q"]
|
||||
for (_ident, device), fd in zip(cameras, fds):
|
||||
cmd += _one_cam(device, width, height, fps, fd)
|
||||
return cmd
|
||||
|
||||
|
||||
def grouped_cameras(
|
||||
cameras: list[tuple[str, str]],
|
||||
fds: list[int],
|
||||
group_size: int = GST_GROUP_SIZE,
|
||||
) -> list[tuple[list[tuple[str, str]], list[int]]]:
|
||||
if group_size < 1:
|
||||
raise ValueError("group_size must be >= 1")
|
||||
out: list[tuple[list[tuple[str, str]], list[int]]] = []
|
||||
for i in range(0, len(cameras), group_size):
|
||||
out.append((cameras[i:i + group_size], fds[i:i + group_size]))
|
||||
return out
|
||||
Reference in New Issue
Block a user