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.
229 lines
7.6 KiB
Python
229 lines
7.6 KiB
Python
"""5x4 camera mosaic helpers."""
|
|
import numpy as np
|
|
|
|
from display_grid import (
|
|
CameraGrid,
|
|
bgra_to_i420,
|
|
camera_ids,
|
|
describe_livekit_error,
|
|
display_name,
|
|
format_livekit_status,
|
|
grid_shape,
|
|
i420_nbytes,
|
|
is_camera_identity,
|
|
letterbox_bgra,
|
|
letterbox_i420,
|
|
placeholder,
|
|
status_screen,
|
|
)
|
|
|
|
|
|
def test_camera_ids():
|
|
ids = camera_ids(20)
|
|
assert ids[0] == "cam-01" and ids[-1] == "cam-20"
|
|
assert len(ids) == 20
|
|
|
|
|
|
def test_is_camera_identity():
|
|
assert is_camera_identity("cam-07")
|
|
assert is_camera_identity("cam-07", "cam")
|
|
assert not is_camera_identity("speaker")
|
|
assert not is_camera_identity("display-wall")
|
|
|
|
|
|
def test_display_name():
|
|
assert display_name("cam-07") == "CAM-07"
|
|
assert display_name("cam-01") == "CAM-01"
|
|
|
|
|
|
def test_letterbox_preserves_canvas_size():
|
|
src = np.zeros((720, 1280, 4), dtype=np.uint8)
|
|
src[:] = (0, 0, 255, 255)
|
|
out = letterbox_bgra(src, 384, 270)
|
|
assert out.shape == (270, 384, 4)
|
|
assert out.dtype == np.uint8
|
|
|
|
|
|
def test_grid_render_size_and_slots():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
|
assert g.tile_w == (1920 - g.gap * 6) // 5
|
|
assert g.tile_h == (1080 - g.gap * 5) // 4
|
|
assert g.inner_w < g.tile_w
|
|
assert g.inner_h < g.tile_h
|
|
assert len(g.identities) == 20
|
|
buf = g.render()
|
|
assert len(buf) == 1920 * 1080 * 4
|
|
frame = np.zeros((720, 1280, 4), dtype=np.uint8)
|
|
frame[:] = (255, 0, 0, 255)
|
|
g.set_frame("cam-07", frame)
|
|
buf2 = g.render()
|
|
assert buf2 != buf
|
|
g.clear("cam-07")
|
|
assert len(g.render()) == len(buf)
|
|
|
|
|
|
def test_empty_tiles_have_name_placeholder():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
|
# Name is drawn in the inner region — not a flat fill.
|
|
tile = g._tiles["cam-01"]
|
|
assert tile.shape == (g.tile_h, g.tile_w, 4)
|
|
assert tile.sum() > 0
|
|
inner = tile[g.border : g.border + g.inner_h, g.border : g.border + g.inner_w]
|
|
# Center of an empty tile is not uniform (text glyphs).
|
|
assert inner.std() > 1.0
|
|
|
|
|
|
def test_gap_separates_tiles():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4, gap=8)
|
|
canvas = np.frombuffer(g.render(), dtype=np.uint8).reshape(1080, 1920, 4)
|
|
# Pixel in the gutter between col 0 and col 1 is canvas background, not a tile.
|
|
x = g.gap + g.tile_w + g.gap // 2
|
|
y = g.gap + 4
|
|
assert tuple(canvas[y, x]) == (10, 10, 10, 255)
|
|
|
|
|
|
def test_inner_rect_cam01_is_top_left_inside_border():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4, gap=8, border=3)
|
|
x, y, w, h = g.inner_rect(0)
|
|
assert (x, y) == (g.gap + g.border, g.gap + g.border)
|
|
assert (w, h) == (g.inner_w, g.inner_h)
|
|
x2, y2, _, _ = g.inner_rect(1)
|
|
assert x2 == g.gap + (g.tile_w + g.gap) + g.border
|
|
assert y2 == y
|
|
|
|
|
|
def test_placeholder_draws_text():
|
|
img = placeholder(640, 360, ["Screen share", "stub"])
|
|
assert img.shape == (360, 640, 4)
|
|
assert img.sum() > 0
|
|
|
|
|
|
def test_letterbox_i420_size_and_even_uv():
|
|
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
|
src[:] = (0, 255, 0, 255)
|
|
blob = bgra_to_i420(src)
|
|
out = letterbox_i420(blob, 640, 360, 368, 226)
|
|
assert len(out) == i420_nbytes(368, 226)
|
|
y = np.frombuffer(out, dtype=np.uint8, count=368 * 226).reshape(226, 368)
|
|
assert 0 not in y[0]
|
|
assert 0 not in y[-1]
|
|
|
|
|
|
def test_grid_i420_render_and_set_frame():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
|
buf = g.render_i420()
|
|
assert len(buf) == i420_nbytes(1920, 1080)
|
|
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
|
src[:] = (255, 0, 0, 255)
|
|
g.set_frame_i420("cam-07", bgra_to_i420(src), 640, 360)
|
|
buf2 = g.render_i420()
|
|
assert buf2 != buf
|
|
g.clear("cam-07")
|
|
assert g.render_i420() == buf
|
|
|
|
|
|
def test_ensure_slot_binds_remote_to_empty_tile():
|
|
g = CameraGrid(width=640, height=360, cols=2, rows=1, identities=["cam-01", "cam-02"])
|
|
assert g.ensure_slot("alice") is True
|
|
assert "alice" in g._tiles
|
|
assert g.identities[0] == "alice"
|
|
frame = np.zeros((90, 160, 4), dtype=np.uint8)
|
|
g.set_frame("alice", frame)
|
|
assert "alice" in g._live
|
|
assert g.ensure_slot("alice") is True
|
|
|
|
|
|
def test_grid_shape_shrinks_with_live_count():
|
|
assert grid_shape(0) == (1, 1)
|
|
assert grid_shape(1) == (1, 1)
|
|
assert grid_shape(2) == (2, 1)
|
|
assert grid_shape(4) == (2, 2)
|
|
assert grid_shape(5) == (3, 2)
|
|
assert grid_shape(6) == (3, 2)
|
|
assert grid_shape(9) == (3, 3)
|
|
assert grid_shape(12) == (4, 3)
|
|
assert grid_shape(20) == (5, 4)
|
|
cols, rows = grid_shape(5)
|
|
assert cols * rows >= 5
|
|
assert cols <= 5 and rows <= 4
|
|
|
|
|
|
def test_set_identities_rebuilds_compact_layout():
|
|
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
|
g.set_identities(["cam-08", "cam-09", "cam-10", "cam-11", "cam-13"])
|
|
assert g.identities == ["cam-08", "cam-09", "cam-10", "cam-11", "cam-13"]
|
|
assert (g.cols, g.rows) == (3, 2)
|
|
assert g.tile_w > CameraGrid(width=1920, height=1080, cols=5, rows=4).tile_w
|
|
g.set_identities(["cam-08", "cam-09"])
|
|
assert (g.cols, g.rows) == (2, 1)
|
|
assert g.identities == ["cam-08", "cam-09"]
|
|
g.set_identities([])
|
|
assert g.identities == []
|
|
assert (g.cols, g.rows) == (1, 1)
|
|
|
|
|
|
def test_status_overlay_replaces_mosaic_until_cleared():
|
|
g = CameraGrid(width=320, height=180, cols=2, rows=1, identities=["cam-01", "cam-02"])
|
|
mosaic = g.render()
|
|
g.set_status(["LIVEKIT DISCONNECTED", "ws://10.200.0.21:7880", "host unreachable"])
|
|
overlay = g.render()
|
|
assert overlay != mosaic
|
|
assert len(overlay) == 320 * 180 * 4
|
|
i420 = g.render_i420()
|
|
assert len(i420) == i420_nbytes(320, 180)
|
|
g.set_status(None)
|
|
assert g.render() == mosaic
|
|
|
|
|
|
def test_format_livekit_status_and_errors():
|
|
assert format_livekit_status(url="ws://10.200.0.21:7880", state="connected") == []
|
|
connecting = format_livekit_status(url="ws://10.200.0.21:7880", state="connecting")
|
|
assert connecting[0] == "CONNECTING TO LIVEKIT"
|
|
assert "10.200.0.21:7880" in connecting[1]
|
|
lines = format_livekit_status(
|
|
url="ws://10.200.0.21:7880",
|
|
state="disconnected",
|
|
detail="host unreachable",
|
|
retry_s=5,
|
|
)
|
|
assert lines[0] == "LIVEKIT DISCONNECTED"
|
|
assert "host unreachable" in lines
|
|
assert any("retry" in ln.lower() for ln in lines)
|
|
assert "unreachable" in describe_livekit_error(
|
|
OSError("No route to host")).lower() or "host" in describe_livekit_error(
|
|
OSError("[Errno 113] No route to host")).lower()
|
|
assert "timed out" in describe_livekit_error(TimeoutError("timed out")).lower()
|
|
assert "refused" in describe_livekit_error(ConnectionRefusedError("Connection refused")).lower()
|
|
|
|
|
|
def test_status_screen_disconnected_uses_composed_layout():
|
|
img = status_screen(1920, 1080, [
|
|
"LIVEKIT DISCONNECTED",
|
|
"ws://10.200.0.21:7880",
|
|
"host unreachable",
|
|
"retrying in 5s",
|
|
])
|
|
assert img.shape == (1080, 1920, 4)
|
|
assert img.dtype == np.uint8
|
|
# Composed: background, accent rail, card — not a flat fill.
|
|
assert len(np.unique(img.reshape(-1, 4), axis=0)) > 12
|
|
bg = img[24, 80, :3]
|
|
card = img[540, 960, :3]
|
|
assert tuple(int(x) for x in bg) != tuple(int(x) for x in card)
|
|
# Disconnected accent rail is warm amber (BGRA: R > G > B).
|
|
stripe = img[540, :8, :3].mean(axis=0)
|
|
b, g, r = (float(x) for x in stripe)
|
|
assert r > g > b
|
|
assert r > 180
|
|
|
|
|
|
def test_status_screen_connecting_uses_cool_accent():
|
|
img = status_screen(1920, 1080, [
|
|
"CONNECTING TO LIVEKIT",
|
|
"ws://10.200.0.21:7880",
|
|
])
|
|
stripe = img[540, :8, :3].mean(axis=0)
|
|
b, g, r = (float(x) for x in stripe)
|
|
assert b > r
|
|
assert b > 160
|