"""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