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:
BIN
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
"""Annex-B access-unit split."""
|
||||
from annexb import AnnexBSplitter, is_keyframe, nal_unit_type
|
||||
|
||||
|
||||
def test_splitter_emits_complete_nal():
|
||||
s = AnnexBSplitter()
|
||||
assert s.push(b"") == []
|
||||
first = b"\x00\x00\x00\x01\x67SPS"
|
||||
second = b"\x00\x00\x00\x01\x68PPS"
|
||||
assert s.push(first + second) == [first]
|
||||
assert s.push(b"\x00\x00\x00\x01\x65IDR") == [second]
|
||||
|
||||
|
||||
def test_is_keyframe_idr():
|
||||
au = b"\x00\x00\x00\x01\x67\x00\x00\x00\x01\x65\x88"
|
||||
assert is_keyframe(au) is True
|
||||
assert nal_unit_type(au) == 7
|
||||
|
||||
|
||||
def test_is_keyframe_delta():
|
||||
au = b"\x00\x00\x00\x01\x41\x00"
|
||||
assert is_keyframe(au) is False
|
||||
assert nal_unit_type(au) == 1
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Hop energy gate so neighboring C920s do not publish bleed."""
|
||||
import numpy as np
|
||||
|
||||
from audio_gate import HopGate, gate_enabled_for, rms_dbfs
|
||||
|
||||
|
||||
def _sine(n=1600, amp=8000):
|
||||
return (np.sin(2 * np.pi * 440 * np.arange(n) / 16000) * amp).astype(np.int16)
|
||||
|
||||
|
||||
def test_rms_dbfs_silence_is_floor():
|
||||
assert rms_dbfs(np.zeros(1600, dtype=np.int16)) <= -120.0
|
||||
|
||||
|
||||
def test_rms_dbfs_sine_is_near_minus_15():
|
||||
db = rms_dbfs(_sine())
|
||||
assert -16.5 < db < -14.0
|
||||
|
||||
|
||||
def test_gate_silences_quiet_hop():
|
||||
g = HopGate(open_db=-32.0, close_db=-40.0, hold_hops=3)
|
||||
quiet = (np.random.randn(1600) * 80).astype(np.int16)
|
||||
out = g.apply(quiet)
|
||||
assert int(np.max(np.abs(out))) == 0
|
||||
assert g.open is False
|
||||
|
||||
|
||||
def test_gate_passes_loud_hop():
|
||||
g = HopGate(open_db=-32.0, close_db=-40.0, hold_hops=3)
|
||||
loud = _sine()
|
||||
out = g.apply(loud)
|
||||
assert int(np.max(np.abs(out))) > 1000
|
||||
assert g.open is True
|
||||
|
||||
|
||||
def test_gate_hold_then_closes():
|
||||
g = HopGate(open_db=-32.0, close_db=-40.0, hold_hops=2)
|
||||
g.apply(_sine())
|
||||
quiet = np.zeros(1600, dtype=np.int16)
|
||||
assert int(np.max(np.abs(g.apply(quiet)))) == 0 or g.open is True
|
||||
g.apply(quiet)
|
||||
g.apply(quiet)
|
||||
assert g.open is False
|
||||
assert int(np.max(np.abs(g.apply(quiet)))) == 0
|
||||
|
||||
|
||||
def test_gate_skips_rally():
|
||||
assert gate_enabled_for("rally", "rally", True) is False
|
||||
assert gate_enabled_for("cam-07", "rally", True) is True
|
||||
assert gate_enabled_for("cam-07", "rally", False) is False
|
||||
|
||||
|
||||
def test_cleaner_gates_quiet_after_dsp():
|
||||
from audio_cleanup import AudioCleaner
|
||||
c = AudioCleaner(mode="off", beamform="off", gate=HopGate(open_db=-32.0, close_db=-40.0, hold_hops=1))
|
||||
quiet = (np.random.randn(1600) * 80).astype(np.int16)
|
||||
out = c.process(quiet)
|
||||
assert out.shape == (1600,)
|
||||
assert int(np.max(np.abs(out))) == 0
|
||||
loud = _sine(1600, 8000)
|
||||
out = c.process(loud)
|
||||
assert int(np.max(np.abs(out))) > 1000
|
||||
|
||||
|
||||
def test_cleaner_gate_uses_raw_energy_not_dsp_makeup():
|
||||
"""Room floor ~-34 dBFS must stay closed even though DSP makeup is ~+3 dB."""
|
||||
from audio_cleanup import AudioCleaner
|
||||
c = AudioCleaner(mode="off", beamform="off", gate=HopGate(open_db=-32.0, close_db=-40.0, hold_hops=1))
|
||||
# sine RMS -34 dBFS => amplitude ~924
|
||||
floor = _sine(1600, 924)
|
||||
assert -35.5 < rms_dbfs(floor) < -32.5
|
||||
out = c.process(floor)
|
||||
assert int(np.max(np.abs(out))) == 0
|
||||
@@ -0,0 +1,19 @@
|
||||
from audio_gst import grouped_audio, shared_audio_cmd
|
||||
|
||||
|
||||
def test_shared_audio_cmd_no_video_elements():
|
||||
cams = [("cam-01", 4), ("cam-02", 5)]
|
||||
cmd = shared_audio_cmd(cams, 16000, [8, 9])
|
||||
joined = " ".join(cmd)
|
||||
assert joined.count("alsasrc") == 2
|
||||
assert "device=hw:4,0" in joined
|
||||
assert "device=hw:5,0" in joined
|
||||
assert "h264enc" not in joined
|
||||
assert "v4l2src" not in joined
|
||||
|
||||
|
||||
def test_grouped_audio_splits():
|
||||
cams = [(f"cam-{i:02d}", i + 1) for i in range(1, 21)]
|
||||
fds = list(range(20))
|
||||
groups = grouped_audio(cams, fds, 5)
|
||||
assert len(groups) == 4
|
||||
@@ -0,0 +1,50 @@
|
||||
from capture_va import grouped_cameras, shared_capture_cmd, video_capture_cmd
|
||||
|
||||
|
||||
def test_video_capture_cmd_latest_frame_no_sidecar_h264():
|
||||
cmd = video_capture_cmd("/dev/cam01", 640, 360, 30)
|
||||
joined = " ".join(cmd)
|
||||
assert "v4l2src" in joined
|
||||
assert "device=/dev/cam01" in joined
|
||||
assert "jpegdec" in joined
|
||||
assert "videoconvert" in joined
|
||||
assert "I420" in joined
|
||||
assert "leaky=downstream" in joined
|
||||
assert "max-size-buffers=1" in joined
|
||||
assert "max-size-time=0" in joined
|
||||
assert "fdsink" in joined
|
||||
assert "h264enc" not in joined
|
||||
assert "tee" not in joined
|
||||
|
||||
|
||||
def test_capture_does_not_gst_crop_or_box():
|
||||
"""MCU crop is Python-side on 07/14/15 only; gst stays full 360."""
|
||||
for dev in ("/dev/cam01", "/dev/cam07"):
|
||||
joined = " ".join(video_capture_cmd(dev, 640, 360, 30))
|
||||
assert "videocrop" not in joined
|
||||
assert "videobox" not in joined
|
||||
assert "videoscale" not in joined
|
||||
|
||||
|
||||
def test_shared_capture_cmd_one_process_no_sidecar_h264():
|
||||
cams = [("cam-01", "/dev/cam01"), ("cam-02", "/dev/cam02")]
|
||||
cmd = shared_capture_cmd(cams, 640, 360, 30, [8, 9])
|
||||
joined = " ".join(cmd)
|
||||
assert cmd[0].endswith("gst-launch-1.0") or "gst-launch-1.0" in cmd[0]
|
||||
assert joined.count("v4l2src") == 2
|
||||
assert joined.count("jpegdec") == 2
|
||||
assert joined.count("videoconvert") == 2
|
||||
assert joined.count("fdsink") == 2
|
||||
assert "fd=8" in joined and "fd=9" in joined
|
||||
assert "h264enc" not in joined
|
||||
assert "tee" not in joined
|
||||
|
||||
|
||||
def test_grouped_cameras_splits_into_small_pool():
|
||||
cams = [(f"cam-{i:02d}", f"/dev/cam{i:02d}") for i in range(1, 21)]
|
||||
fds = list(range(20, 40))
|
||||
groups = grouped_cameras(cams, fds, 5)
|
||||
assert len(groups) == 4
|
||||
assert all(len(c) == 5 and len(f) == 5 for c, f in groups)
|
||||
assert groups[0][0][0][0] == "cam-01"
|
||||
assert groups[-1][0][-1][0] == "cam-20"
|
||||
+124
-1
@@ -1,4 +1,4 @@
|
||||
from config import Config, _parse_cameras
|
||||
from config import Config, _parse_cameras, load_config
|
||||
|
||||
|
||||
def test_parse_cameras_list():
|
||||
@@ -17,3 +17,126 @@ def test_torch_num_threads_default_one():
|
||||
|
||||
def test_enhance_infer_default_auto():
|
||||
assert Config().enhance_infer == "auto"
|
||||
|
||||
|
||||
def test_latency_flag_defaults():
|
||||
c = Config()
|
||||
assert c.audio_queue_ms is None
|
||||
assert c.video_hold_s is None
|
||||
assert c.worker_split_executor is False
|
||||
assert c.enhance_speaker_only is False
|
||||
assert c.audio_gate is False
|
||||
assert c.audio_gate_open_db == -28.0
|
||||
assert c.audio_gate_close_db == -34.0
|
||||
assert c.audio_gate_hold_s == 0.3
|
||||
assert c.display_video_capacity == 0
|
||||
assert c.display_video_format == ""
|
||||
assert c.display_gst_queue_buffers == 2
|
||||
assert c.display_speaker_push == "timer"
|
||||
assert c.participant_tags == ["uwh"]
|
||||
assert c.display_hide_tags == []
|
||||
assert c.display_hand_raise is True
|
||||
assert c.display_hand_raise_hz == 4.0
|
||||
assert c.display_hand_raise_hold_s == 1.0
|
||||
assert c.display_hand_raise_model == "thunder"
|
||||
assert c.display_speak_min_db == -40.0
|
||||
assert c.display_speak_margin_db == 6.0
|
||||
assert c.display_speak_hold_s == 0.6
|
||||
assert c.display_speak_rise_db == 3.0
|
||||
assert c.display_speak_corr == 0.4
|
||||
assert c.display_speak_confirm == 2
|
||||
assert c.portrait_blur is True
|
||||
assert c.portrait_shm_dir == "/run/livekit-cameras/portrait"
|
||||
assert c.portrait_hz == 8.0
|
||||
assert c.portrait_blur_px == 7
|
||||
assert c.portrait_hold_s == 0.8
|
||||
|
||||
|
||||
def test_enhance_daemon_defaults():
|
||||
c = Config()
|
||||
assert c.enhance_daemon is True
|
||||
assert c.enhance_socket == "/tmp/livekit-enhance.sock"
|
||||
assert c.capture_daemon is True
|
||||
assert c.capture_shm_dir == "/run/livekit-cameras/raw"
|
||||
assert c.audio_daemon is True
|
||||
assert c.audio_shm_dir == "/run/livekit-cameras/pcm"
|
||||
assert c.single_publisher is True
|
||||
|
||||
|
||||
def test_load_config_enhance_daemon_env(monkeypatch, tmp_path):
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("")
|
||||
monkeypatch.setenv("ENHANCE_DAEMON", "0")
|
||||
monkeypatch.setenv("ENHANCE_SOCKET", "/run/foo.sock")
|
||||
cfg = load_config(env)
|
||||
assert cfg.enhance_daemon is False
|
||||
assert cfg.enhance_socket == "/run/foo.sock"
|
||||
|
||||
|
||||
def test_load_config_latency_env(monkeypatch, tmp_path):
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("")
|
||||
monkeypatch.setenv("AUDIO_QUEUE_MS", "60")
|
||||
monkeypatch.setenv("VIDEO_HOLD_S", "0")
|
||||
monkeypatch.setenv("WORKER_SPLIT_EXECUTOR", "1")
|
||||
monkeypatch.setenv("ENHANCE_SPEAKER_ONLY", "true")
|
||||
monkeypatch.setenv("AUDIO_GATE", "0")
|
||||
monkeypatch.setenv("AUDIO_GATE_OPEN_DB", "-28")
|
||||
monkeypatch.setenv("AUDIO_GATE_HOLD_S", "0.5")
|
||||
monkeypatch.setenv("DISPLAY_VIDEO_CAPACITY", "1")
|
||||
monkeypatch.setenv("DISPLAY_VIDEO_FORMAT", "BGRA")
|
||||
monkeypatch.setenv("DISPLAY_GST_QUEUE_BUFFERS", "1")
|
||||
monkeypatch.setenv("DISPLAY_SPEAKER_PUSH", "arrival")
|
||||
cfg = load_config(env)
|
||||
assert cfg.audio_queue_ms == 60
|
||||
assert cfg.video_hold_s == 0.0
|
||||
assert cfg.worker_split_executor is True
|
||||
assert cfg.enhance_speaker_only is True
|
||||
assert cfg.audio_gate is False
|
||||
assert cfg.audio_gate_open_db == -28.0
|
||||
assert cfg.audio_gate_hold_s == 0.5
|
||||
assert cfg.display_video_capacity == 1
|
||||
assert cfg.display_video_format == "bgra"
|
||||
assert cfg.display_gst_queue_buffers == 1
|
||||
assert cfg.display_speaker_push == "arrival"
|
||||
|
||||
|
||||
def test_load_config_site_tags(monkeypatch, tmp_path):
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("")
|
||||
monkeypatch.setenv("PARTICIPANT_TAGS", "uwh")
|
||||
monkeypatch.setenv("DISPLAY_HIDE_TAGS", "")
|
||||
cfg = load_config(env)
|
||||
assert cfg.participant_tags == ["uwh"]
|
||||
assert cfg.display_hide_tags == []
|
||||
monkeypatch.setenv("DISPLAY_HIDE_TAGS", "uwh")
|
||||
cfg = load_config(env)
|
||||
assert cfg.display_hide_tags == ["uwh"]
|
||||
|
||||
|
||||
def test_load_config_hand_raise_env(monkeypatch, tmp_path):
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("")
|
||||
monkeypatch.setenv("DISPLAY_HAND_RAISE", "0")
|
||||
monkeypatch.setenv("DISPLAY_HAND_RAISE_HZ", "3")
|
||||
monkeypatch.setenv("DISPLAY_HAND_RAISE_HOLD_S", "1.2")
|
||||
cfg = load_config(env)
|
||||
assert cfg.display_hand_raise is False
|
||||
assert cfg.display_hand_raise_hz == 3.0
|
||||
assert cfg.display_hand_raise_hold_s == 1.2
|
||||
|
||||
|
||||
def test_load_config_portrait_env(monkeypatch, tmp_path):
|
||||
env = tmp_path / ".env"
|
||||
env.write_text("")
|
||||
monkeypatch.setenv("PORTRAIT_BLUR", "0")
|
||||
monkeypatch.setenv("PORTRAIT_SHM_DIR", "/tmp/portrait")
|
||||
monkeypatch.setenv("PORTRAIT_HZ", "5")
|
||||
monkeypatch.setenv("PORTRAIT_BLUR_PX", "11")
|
||||
monkeypatch.setenv("PORTRAIT_HOLD_S", "0.4")
|
||||
cfg = load_config(env)
|
||||
assert cfg.portrait_blur is False
|
||||
assert cfg.portrait_shm_dir == "/tmp/portrait"
|
||||
assert cfg.portrait_hz == 5.0
|
||||
assert cfg.portrait_blur_px == 11
|
||||
assert cfg.portrait_hold_s == 0.4
|
||||
|
||||
@@ -3,11 +3,18 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@@ -75,7 +82,147 @@ def test_gap_separates_tiles():
|
||||
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
|
||||
|
||||
@@ -19,3 +19,29 @@ def test_select_three_prefers_xe_hdmi():
|
||||
assert three[0].name == "HDMI-A-7" or three[0].connected
|
||||
ids = [o.connector_id for o in three]
|
||||
assert len(set(ids)) == 3
|
||||
|
||||
|
||||
def test_named_disconnected_falls_back_to_connected_xe():
|
||||
from drm_outputs import DrmOutput, pick_outputs
|
||||
|
||||
wall = DrmOutput(
|
||||
name="HDMI-A-7", sysfs="card0-HDMI-A-7", node="/dev/dri/card0",
|
||||
driver="xe", connector_id=538, status="connected", width=3840, height=2160,
|
||||
)
|
||||
stale_grid = DrmOutput(
|
||||
name="HDMI-A-3", sysfs="card1-HDMI-A-3", node="/dev/dri/card1",
|
||||
driver="i915", connector_id=530, status="disconnected",
|
||||
)
|
||||
stale_spk = DrmOutput(
|
||||
name="HDMI-A-5", sysfs="card0-HDMI-A-5", node="/dev/dri/card0",
|
||||
driver="xe", connector_id=520, status="disconnected",
|
||||
)
|
||||
picked = pick_outputs(
|
||||
[stale_grid, stale_spk, wall],
|
||||
n=2,
|
||||
names=["HDMI-A-3", "HDMI-A-5"],
|
||||
)
|
||||
assert len(picked) == 1
|
||||
assert picked[0].name == "HDMI-A-7"
|
||||
assert picked[0].connected
|
||||
assert picked[0].driver == "xe"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from encode_daemon import _one_enc
|
||||
|
||||
|
||||
def test_h264_uses_igpu_va_not_x264_or_arc_av1():
|
||||
cmd = _one_enc(640, 368, 30, 2000, 3, 4, "h264", render="/dev/dri/renderD129")
|
||||
s = " ".join(cmd)
|
||||
assert "varenderD129h264enc" in cmd
|
||||
assert "x264enc" not in cmd
|
||||
assert "vaav1enc" not in cmd
|
||||
assert "format=nv12" in s
|
||||
assert "key-int-max=1" in cmd
|
||||
assert "b-frames=0" in cmd
|
||||
|
||||
|
||||
def test_h264_factory_follows_render_node():
|
||||
cmd = _one_enc(640, 368, 30, 2000, 3, 4, "h264", render="/dev/dri/renderD128")
|
||||
assert "varenderD128h264enc" in cmd
|
||||
|
||||
|
||||
def test_c920_stays_fast_gop1():
|
||||
cmd = _one_enc(640, 368, 30, 2000, 3, 4, "h264", render="/dev/dri/renderD129")
|
||||
assert "bitrate=2000" in cmd
|
||||
assert "key-int-max=1" in cmd
|
||||
assert "target-usage=7" in cmd
|
||||
|
||||
|
||||
def test_rally_1080_uses_higher_bitrate_and_no_motion_trails():
|
||||
cmd = _one_enc(1920, 1088, 30, 2000, 3, 4, "h264", render="/dev/dri/renderD129")
|
||||
assert "bitrate=20000" in cmd
|
||||
assert "key-int-max=1" in cmd
|
||||
assert "target-usage=2" in cmd
|
||||
assert "b-frames=0" in cmd
|
||||
|
||||
|
||||
def test_rally_bitrate_rollbacks_via_hi_kbps():
|
||||
cmd = _one_enc(1920, 1088, 30, 2000, 3, 4, "h264",
|
||||
render="/dev/dri/renderD129", hi_kbps=12000)
|
||||
assert "bitrate=12000" in cmd
|
||||
|
||||
|
||||
def test_av1_stays_on_arc_vaav1enc():
|
||||
cmd = _one_enc(640, 368, 30, 2000, 3, 4, "av1")
|
||||
assert "vaav1enc" in cmd
|
||||
assert "varenderD129h264enc" not in cmd
|
||||
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
||||
def test_frame_roundtrip_stereo_hop():
|
||||
from enhance_ipc import dump_frame, load_frame
|
||||
|
||||
hop = 1600
|
||||
t = np.arange(hop, dtype=np.int16)
|
||||
pcm = np.stack([t, t + 1], axis=1)
|
||||
blob = dump_frame("cam-07", pcm)
|
||||
ident, out = load_frame(blob)
|
||||
assert ident == "cam-07"
|
||||
assert out.dtype == np.int16
|
||||
assert out.shape == (hop, 2)
|
||||
np.testing.assert_array_equal(out, pcm)
|
||||
|
||||
|
||||
def test_unix_client_gets_handler_output(tmp_path):
|
||||
from enhance_daemon import serve_unix
|
||||
from enhance_ipc import EnhanceClient
|
||||
|
||||
sock = str(tmp_path / "enhance.sock")
|
||||
stop = threading.Event()
|
||||
|
||||
def handler(ident, pcm):
|
||||
assert ident == "cam-03"
|
||||
# fake "enhance": average channels, invert
|
||||
if pcm.ndim == 2:
|
||||
mono = pcm.mean(axis=1)
|
||||
else:
|
||||
mono = pcm
|
||||
return (-mono).astype(np.int16)
|
||||
|
||||
t = threading.Thread(target=serve_unix, args=(sock, handler, stop), daemon=True)
|
||||
t.start()
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline and not (tmp_path / "enhance.sock").exists():
|
||||
time.sleep(0.01)
|
||||
hop = 800
|
||||
pcm = np.stack([np.full(hop, 1000, np.int16), np.full(hop, 3000, np.int16)], axis=1)
|
||||
client = EnhanceClient(sock)
|
||||
try:
|
||||
out = client.process("cam-03", pcm)
|
||||
finally:
|
||||
client.close()
|
||||
stop.set()
|
||||
assert out.dtype == np.int16
|
||||
assert out.shape == (hop,)
|
||||
assert int(out[0]) == -2000
|
||||
|
||||
|
||||
def _start_echo_daemon(sock, stop):
|
||||
from enhance_daemon import serve_unix
|
||||
|
||||
def handler(ident, pcm):
|
||||
if pcm.ndim == 2:
|
||||
mono = pcm.mean(axis=1)
|
||||
else:
|
||||
mono = pcm
|
||||
return np.asarray(mono, dtype=np.int16)
|
||||
|
||||
t = threading.Thread(target=serve_unix, args=(sock, handler, stop), daemon=True)
|
||||
t.start()
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline and not os.path.exists(sock):
|
||||
time.sleep(0.01)
|
||||
return t
|
||||
|
||||
|
||||
def test_audio_cleaner_remote_does_not_load_model(tmp_path):
|
||||
from audio_cleanup import AudioCleaner
|
||||
|
||||
sock = str(tmp_path / "enhance.sock")
|
||||
stop = threading.Event()
|
||||
_start_echo_daemon(sock, stop)
|
||||
try:
|
||||
c = AudioCleaner(
|
||||
mode="force", beamform="force", enhance_socket=sock,
|
||||
identity="cam-09")
|
||||
assert c._enhancer is None
|
||||
assert c._beamformer is None
|
||||
assert c.backend == "daemon"
|
||||
hop = 1600
|
||||
stereo = np.stack([
|
||||
np.full(hop, 400, np.int16),
|
||||
np.full(hop, 800, np.int16),
|
||||
], axis=1)
|
||||
out = c.process(stereo)
|
||||
assert out.shape == (hop,)
|
||||
assert out.dtype == np.int16
|
||||
assert int(out[0]) == 600
|
||||
finally:
|
||||
stop.set()
|
||||
|
||||
|
||||
def test_shared_engine_constructs_one_cleaner_for_two_cams():
|
||||
from audio_cleanup import AudioCleaner
|
||||
from enhance_daemon import SharedEnhanceEngine
|
||||
|
||||
n = {"c": 0}
|
||||
|
||||
def make():
|
||||
n["c"] += 1
|
||||
return AudioCleaner(mode="off", beamform="off")
|
||||
|
||||
eng = SharedEnhanceEngine(make_cleaner=make)
|
||||
hop = np.zeros(800, dtype=np.int16)
|
||||
out_a = eng.process("cam-01", hop)
|
||||
out_b = eng.process("cam-02", hop)
|
||||
out_a2 = eng.process("cam-01", hop)
|
||||
assert n["c"] == 1
|
||||
assert set(eng.identities()) == {"cam-01", "cam-02"}
|
||||
assert out_a.shape == (800,)
|
||||
assert out_b.shape == (800,)
|
||||
assert out_a2.shape == (800,)
|
||||
|
||||
|
||||
def test_wait_for_socket_false_then_true(tmp_path):
|
||||
from enhance_daemon import wait_for_socket, serve_unix
|
||||
|
||||
sock = str(tmp_path / "missing.sock")
|
||||
assert wait_for_socket(sock, timeout=0.15) is False
|
||||
stop = threading.Event()
|
||||
t = threading.Thread(
|
||||
target=serve_unix, args=(sock, lambda i, p: p, stop), daemon=True)
|
||||
t.start()
|
||||
try:
|
||||
assert wait_for_socket(sock, timeout=2.0) is True
|
||||
finally:
|
||||
stop.set()
|
||||
@@ -0,0 +1,23 @@
|
||||
from frame_shm import LatestFrameReader, LatestFrameWriter, frame_bytes
|
||||
|
||||
|
||||
def test_latest_frame_roundtrip(tmp_path):
|
||||
path = str(tmp_path / "cam-01.i420")
|
||||
w, h = 16, 16
|
||||
n = frame_bytes(w, h)
|
||||
writer = LatestFrameWriter(path, w, h)
|
||||
payload = bytes(range(256)) * (n // 256 + 1)
|
||||
payload = payload[:n]
|
||||
seq = writer.write(payload)
|
||||
assert seq == 1
|
||||
reader = LatestFrameReader(path)
|
||||
got = reader.latest()
|
||||
assert got is not None
|
||||
assert got[0] == 1
|
||||
assert got[1] == payload
|
||||
seq2 = writer.write(bytes([3]) * n)
|
||||
got2 = reader.latest()
|
||||
assert got2 is not None
|
||||
assert got2[0] == seq2
|
||||
assert got2[1][0] == 3
|
||||
writer.close()
|
||||
@@ -0,0 +1,66 @@
|
||||
from display_grid import CameraGrid
|
||||
from drm_outputs import DrmOutput
|
||||
from gst_sink import appsrc_cmd, va_grid_cmd
|
||||
|
||||
|
||||
def _out():
|
||||
return DrmOutput(
|
||||
name="HDMI-A-7",
|
||||
sysfs="card0-HDMI-A-7",
|
||||
node="/dev/dri/card0",
|
||||
driver="xe",
|
||||
connector_id=538,
|
||||
status="connected",
|
||||
)
|
||||
|
||||
|
||||
def test_appsrc_queue_buffers_default_two():
|
||||
cmd = appsrc_cmd(_out(), 1920, 1080, 30)
|
||||
assert "max-size-buffers=2" in cmd
|
||||
|
||||
|
||||
def test_appsrc_queue_buffers_one():
|
||||
cmd = appsrc_cmd(_out(), 1920, 1080, 30, queue_buffers=1)
|
||||
assert "max-size-buffers=1" in cmd
|
||||
assert "max-size-buffers=2" not in cmd
|
||||
|
||||
|
||||
def test_appsrc_i420_blocksize():
|
||||
cmd = appsrc_cmd(_out(), 1920, 1080, 30, pixel_format="i420")
|
||||
joined = " ".join(cmd)
|
||||
assert "format=i420" in joined
|
||||
assert "blocksize=3110400" in joined # 1920*1080*3//2
|
||||
assert "format=bgra" not in joined
|
||||
|
||||
|
||||
def test_va_grid_cmd_pins_arc_compositor_and_twenty_tiles():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
tile_fds = list(range(4, 24))
|
||||
cmd = va_grid_cmd(
|
||||
_out(), g, ingest_w=640, ingest_h=360, fps=15,
|
||||
tile_fds=tile_fds, chrome_fd=3, queue_buffers=1)
|
||||
joined = " ".join(cmd)
|
||||
assert "varenderD129compositor" in joined
|
||||
assert "varenderD129postproc" in joined
|
||||
assert "kmssink" in joined
|
||||
assert "connector-id=538" in joined
|
||||
assert "driver-name=xe" in joined
|
||||
assert joined.count("fdsrc") == 21 # chrome + 20 tiles
|
||||
x, y, w, h = g.inner_rect(0)
|
||||
assert f"xpos={x}" in joined
|
||||
assert f"ypos={y}" in joined
|
||||
assert "format=i420" in joined
|
||||
assert "blocksize=345600" in joined # 640*360*1.5
|
||||
|
||||
|
||||
def test_va_grid_cmd_h264_uses_arc_decoder():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
paths = [f"/tmp/cam-{i:02d}.h264" for i in range(1, 21)]
|
||||
cmd = va_grid_cmd(
|
||||
_out(), g, ingest_w=640, ingest_h=360, fps=15,
|
||||
tile_h264_paths=paths, chrome_fd=3)
|
||||
joined = " ".join(cmd)
|
||||
assert "varenderD129h264dec" in joined
|
||||
assert joined.count("h264parse") == 20
|
||||
assert "fdsrc" in joined # chrome only
|
||||
assert joined.count("filesrc") == 20
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
import stat
|
||||
|
||||
from h264_fifo import hold_fifo
|
||||
|
||||
|
||||
def test_hold_fifo_creates_rdwr_pipe(tmp_path):
|
||||
path = str(tmp_path / "cam-01.h264")
|
||||
fd = hold_fifo(path)
|
||||
try:
|
||||
st = os.stat(path)
|
||||
assert stat.S_ISFIFO(st.st_mode)
|
||||
os.write(fd, b"\x00\x01")
|
||||
assert os.read(fd, 2) == b"\x00\x01"
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def test_hold_fifo_does_not_replace_existing_fifo(tmp_path):
|
||||
path = str(tmp_path / "cam-02.h264")
|
||||
a = hold_fifo(path)
|
||||
ino = os.stat(path).st_ino
|
||||
b = hold_fifo(path)
|
||||
try:
|
||||
assert os.stat(path).st_ino == ino
|
||||
os.write(a, b"ab")
|
||||
assert os.read(b, 2) == b"ab"
|
||||
finally:
|
||||
os.close(a)
|
||||
os.close(b)
|
||||
@@ -0,0 +1,171 @@
|
||||
"""Hand-raise rule, latch, and yellow I420 chrome."""
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from display_grid import FRAME_RAISE, CameraGrid, bgra_to_i420, i420_nbytes
|
||||
from hand_raise import RaiseLatch, letterbox_rgb, wrist_is_raised
|
||||
|
||||
|
||||
def _kpts() -> np.ndarray:
|
||||
k = np.zeros((17, 3), dtype=np.float32)
|
||||
k[:, 2] = 0.8
|
||||
k[0] = [0.35, 0.50, 0.9] # nose
|
||||
k[5] = [0.50, 0.35, 0.9] # L shoulder
|
||||
k[6] = [0.50, 0.65, 0.9] # R shoulder
|
||||
k[7] = [0.62, 0.30, 0.9] # L elbow
|
||||
k[8] = [0.62, 0.70, 0.9]
|
||||
k[9] = [0.70, 0.28, 0.9] # L wrist down
|
||||
k[10] = [0.70, 0.72, 0.9]
|
||||
return k
|
||||
|
||||
|
||||
def test_wrist_down_is_not_raised():
|
||||
assert wrist_is_raised(_kpts()) is False
|
||||
|
||||
|
||||
def test_left_wrist_above_shoulder_is_raised():
|
||||
k = _kpts()
|
||||
k[7] = [0.32, 0.30, 0.9] # elbow up
|
||||
k[9] = [0.18, 0.32, 0.9] # wrist above shoulder (y 0.50)
|
||||
assert wrist_is_raised(k) is True
|
||||
|
||||
|
||||
def test_modest_wrist_lift_is_raised():
|
||||
"""Webcam framing: hand just above the shoulder still counts."""
|
||||
k = _kpts()
|
||||
k[7] = [0.46, 0.30, 0.9]
|
||||
k[9] = [0.43, 0.32, 0.9] # 0.07 above shoulder y=0.50
|
||||
assert wrist_is_raised(k) is True
|
||||
|
||||
|
||||
def test_wrist_on_face_rejected():
|
||||
k = _kpts()
|
||||
k[7] = [0.38, 0.48, 0.9]
|
||||
k[9] = [0.28, 0.50, 0.9] # up, but next to the nose
|
||||
assert wrist_is_raised(k) is False
|
||||
|
||||
|
||||
def test_wrist_above_head_without_shoulders():
|
||||
"""Close-up webcam: shoulders off-frame, hand still clearly up."""
|
||||
k = np.zeros((17, 3), dtype=np.float32)
|
||||
k[0] = [0.55, 0.50, 0.85] # nose
|
||||
k[9] = [0.18, 0.62, 0.7] # wrist high, not on the face
|
||||
assert wrist_is_raised(k) is True
|
||||
|
||||
|
||||
def test_low_score_not_raised():
|
||||
k = _kpts()
|
||||
k[:, 2] = 0.05
|
||||
assert wrist_is_raised(k) is False
|
||||
assert wrist_is_raised(np.zeros((17, 3), dtype=np.float32)) is False
|
||||
|
||||
|
||||
def test_raise_latch_needs_hits_then_holds():
|
||||
latch = RaiseLatch(hits=2, hold_s=0.4, window=2)
|
||||
t = 10.0
|
||||
assert latch.update(True, t) is False
|
||||
assert latch.update(True, t + 0.1) is True
|
||||
assert latch.update(False, t + 0.2) is True
|
||||
assert latch.update(False, t + 0.55) is False
|
||||
|
||||
|
||||
def test_raise_latch_survives_one_miss():
|
||||
latch = RaiseLatch(hits=3, hold_s=1.0, window=5)
|
||||
t = 10.0
|
||||
assert latch.update(True, t) is False
|
||||
assert latch.update(True, t + 0.1) is False
|
||||
assert latch.update(False, t + 0.2) is False
|
||||
assert latch.update(True, t + 0.3) is True
|
||||
assert latch.update(True, t + 0.4) is True
|
||||
assert latch.update(False, t + 0.5) is True
|
||||
|
||||
|
||||
def test_letterbox_rgb_pads_16x9():
|
||||
rgb = np.full((360, 640, 3), 200, dtype=np.uint8)
|
||||
out = letterbox_rgb(rgb, 192)
|
||||
assert out.shape == (192, 192, 3)
|
||||
assert int(out[0, 96, 0]) == 0
|
||||
assert int(out[96, 96, 0]) == 200
|
||||
thunder = letterbox_rgb(rgb, 256)
|
||||
assert thunder.shape == (256, 256, 3)
|
||||
|
||||
|
||||
def test_resolve_movenet_thunder_is_256():
|
||||
from pathlib import Path
|
||||
from hand_raise import resolve_movenet
|
||||
path, size, name = resolve_movenet("thunder")
|
||||
assert name == "thunder"
|
||||
assert size == 256
|
||||
assert path.name == "movenet_singlepose_thunder.onnx"
|
||||
path, size, name = resolve_movenet("lightning")
|
||||
assert name == "lightning" and size == 192
|
||||
assert Path(path).name == "movenet_singlepose_lightning.onnx"
|
||||
|
||||
|
||||
def test_set_highlight_paints_yellow_i420_border():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
||||
src[:] = (40, 40, 40, 255)
|
||||
g.set_frame_i420("cam-07", bgra_to_i420(src), 640, 360)
|
||||
before = np.frombuffer(g.render_i420(), dtype=np.uint8).copy()
|
||||
g.set_highlight("cam-07", True)
|
||||
assert g.highlighted("cam-07") is True
|
||||
after = np.frombuffer(g.render_i420(), dtype=np.uint8)
|
||||
assert not np.array_equal(before, after)
|
||||
idx = g.identities.index("cam-07")
|
||||
ox, oy = g.tile_origin(idx)
|
||||
yplane = after[: g.width * g.height].reshape(g.height, g.width)
|
||||
yv, _, _ = g._yuv_raise
|
||||
# Top edge of the tile chrome should be yellow-luma, not gray live.
|
||||
assert int(yplane[oy, ox + g.tile_w // 2]) == yv
|
||||
assert int(yplane[oy - 2, ox + g.tile_w // 2]) == yv
|
||||
ix, iy, iw, _ih = g.inner_rect(idx)
|
||||
assert int(yplane[iy, ix + iw // 2]) != yv
|
||||
g.set_highlight("cam-07", False)
|
||||
assert g.highlighted("cam-07") is False
|
||||
restored = np.frombuffer(g.render_i420(), dtype=np.uint8)
|
||||
assert int(restored[: g.width * g.height].reshape(g.height, g.width)[oy, ox + g.tile_w // 2]) != yv
|
||||
|
||||
|
||||
def test_clear_drops_yellow():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
||||
g.set_frame_i420("cam-01", bgra_to_i420(src), 640, 360)
|
||||
g.set_highlight("cam-01", True)
|
||||
g.clear("cam-01")
|
||||
assert g.highlighted("cam-01") is False
|
||||
assert len(g.render_i420()) == i420_nbytes(1920, 1080)
|
||||
|
||||
|
||||
def test_fp32_movenet_sees_person_not_raised():
|
||||
"""INT8 Xenova weights scored ~0.05 on a real person; FP32 must lock on."""
|
||||
from pathlib import Path
|
||||
from hand_raise import HandRaiseMonitor, MODEL_PATH
|
||||
|
||||
fixture = Path(__file__).parent / "fixtures" / "movenet_warrior_192.npy"
|
||||
assert MODEL_PATH.name == "movenet_singlepose_lightning.onnx"
|
||||
rgb = np.load(fixture)
|
||||
mon = HandRaiseMonitor(enabled=True, model="lightning")
|
||||
assert mon.enabled
|
||||
k = mon.infer_rgb(rgb)
|
||||
assert float(k[:, 2].max()) > 0.4
|
||||
assert float(k[0, 2]) > 0.3 # nose
|
||||
assert wrist_is_raised(k) is False # warrior: arms out, not up
|
||||
|
||||
|
||||
def test_fp32_thunder_sees_person_not_raised():
|
||||
from pathlib import Path
|
||||
from hand_raise import HandRaiseMonitor, resolve_movenet
|
||||
|
||||
path, size, name = resolve_movenet("thunder")
|
||||
assert path.is_file()
|
||||
assert size == 256 and name == "thunder"
|
||||
rgb = np.load(Path(__file__).parent / "fixtures" / "movenet_warrior_192.npy")
|
||||
mon = HandRaiseMonitor(enabled=True, model="thunder")
|
||||
assert mon.enabled
|
||||
assert mon._input_size == 256
|
||||
k = mon.infer_rgb(rgb)
|
||||
assert float(k[:, 2].max()) > 0.4
|
||||
assert float(k[0, 2]) > 0.3
|
||||
assert wrist_is_raised(k) is False
|
||||
@@ -0,0 +1,47 @@
|
||||
from hop_shm import HopReader, HopWriter, hop_bytes
|
||||
|
||||
|
||||
def test_hop_ring_preserves_order(tmp_path):
|
||||
path = str(tmp_path / "cam-01.pcm")
|
||||
n = hop_bytes(16000, 2, 0.1)
|
||||
w = HopWriter(path, n, nslots=4)
|
||||
r = HopReader(path)
|
||||
for i in range(3):
|
||||
payload = bytes([i]) * n
|
||||
w.write(payload)
|
||||
got = [r.read() for _ in range(3)]
|
||||
assert got[0] is not None and got[0][0] == 0
|
||||
assert got[1] is not None and got[1][0] == 1
|
||||
assert got[2] is not None and got[2][0] == 2
|
||||
assert r.read() is None
|
||||
w.close()
|
||||
|
||||
|
||||
def test_hop_ring_skips_when_reader_lags(tmp_path):
|
||||
path = str(tmp_path / "cam-01.pcm")
|
||||
n = 32
|
||||
w = HopWriter(path, n, nslots=4)
|
||||
r = HopReader(path)
|
||||
for i in range(10):
|
||||
w.write(bytes([i]) * n)
|
||||
last = r.read()
|
||||
assert last is not None
|
||||
w.close()
|
||||
|
||||
|
||||
def test_reader_follows_writer_restart(tmp_path):
|
||||
"""Cameras-only restart zeros seq; the wall must not stay stuck on _last."""
|
||||
path = str(tmp_path / "cam-01.pcm")
|
||||
n = 32
|
||||
w = HopWriter(path, n, nslots=4)
|
||||
r = HopReader(path)
|
||||
for i in range(8):
|
||||
w.write(bytes([i]) * n)
|
||||
assert r.read() is not None
|
||||
w.close()
|
||||
w2 = HopWriter(path, n, nslots=4)
|
||||
w2.write(bytes([99]) * n)
|
||||
got = r.read()
|
||||
assert got is not None and got[0] == 99
|
||||
assert r.read() is None
|
||||
w2.close()
|
||||
@@ -0,0 +1,56 @@
|
||||
from i420_nv12 import (
|
||||
coded_dim,
|
||||
drop_va_pad_i420,
|
||||
i420_to_nv12_into,
|
||||
nv12_bytes,
|
||||
va_visible_height,
|
||||
)
|
||||
|
||||
def test_coded_dim_pads_up():
|
||||
assert coded_dim(360) == 368
|
||||
assert coded_dim(1080) == 1088
|
||||
assert coded_dim(640) == 640
|
||||
assert coded_dim(352) == 352
|
||||
|
||||
|
||||
def test_i420_to_nv12_pads_last_row():
|
||||
w, h = 640, 360
|
||||
ysz = w * h
|
||||
uv = (w // 2) * (h // 2)
|
||||
src = bytearray(ysz + 2 * uv)
|
||||
src[:ysz] = b"\x80" * ysz
|
||||
src[(h - 1) * w:ysz] = b"\x90" * w
|
||||
src[ysz:ysz + uv] = b"\x40" * uv
|
||||
src[ysz + uv:] = b"\xc0" * uv
|
||||
cw, ch = 640, 368
|
||||
out = bytearray(nv12_bytes(cw, ch))
|
||||
n = i420_to_nv12_into(src, w, h, out)
|
||||
assert n == nv12_bytes(cw, ch)
|
||||
assert out[0] == 0x80
|
||||
# cloned pad rows 360-367
|
||||
assert out[360 * cw] == 0x90
|
||||
assert out[367 * cw] == 0x90
|
||||
assert out[cw * ch] == 0x40
|
||||
assert out[cw * ch + 1] == 0xc0
|
||||
|
||||
|
||||
def test_drop_va_pad_restores_360():
|
||||
w, h = 640, 368
|
||||
vis = 360
|
||||
ysz = w * h
|
||||
uv = (w // 2) * (h // 2)
|
||||
src = bytearray(ysz + 2 * uv)
|
||||
src[: w * vis] = b"\x77" * (w * vis)
|
||||
src[w * vis:ysz] = b"\x11" * (w * 8)
|
||||
out, ow, oh = drop_va_pad_i420(src, w, h)
|
||||
assert (ow, oh) == (w, vis)
|
||||
assert len(out) == w * vis * 3 // 2
|
||||
assert out[0] == 0x77
|
||||
assert 0x11 not in out[: w * vis]
|
||||
|
||||
|
||||
def test_va_visible_height():
|
||||
assert va_visible_height(640, 368) == 360
|
||||
assert va_visible_height(1920, 1088) == 1080
|
||||
assert va_visible_height(640, 360) == 360
|
||||
assert va_visible_height(640, 352) == 352
|
||||
@@ -0,0 +1,99 @@
|
||||
from capture_va import (
|
||||
crop_scale_i420_drop_bottom,
|
||||
jpeg_mcu_bottom_crop,
|
||||
needs_jpeg_mcu_fix,
|
||||
repeat_i420_bottom_from_last_good,
|
||||
)
|
||||
|
||||
|
||||
def _i420(width, height, y=16, u=128, v=128):
|
||||
y_size = width * height
|
||||
uv = (width // 2) * (height // 2)
|
||||
buf = bytearray(y_size + 2 * uv)
|
||||
buf[:y_size] = bytes([y]) * y_size
|
||||
buf[y_size:y_size + uv] = bytes([u]) * uv
|
||||
buf[y_size + uv:] = bytes([v]) * uv
|
||||
return buf
|
||||
|
||||
|
||||
def test_repeat_clones_last_good_y_and_chroma_over_black_pad():
|
||||
w, h, crop = 640, 360, 8
|
||||
buf = _i420(w, h, y=16, u=128, v=128)
|
||||
last_good = h - crop - 1
|
||||
y_stride = w
|
||||
# last good Y row = 90; last good UV row = 40 (chroma 4:2:0)
|
||||
src = last_good * y_stride
|
||||
buf[src:src + y_stride] = bytes([90]) * y_stride
|
||||
uv_w = w // 2
|
||||
uv_h = h // 2
|
||||
uv_crop = crop // 2
|
||||
last_uv = uv_h - uv_crop - 1
|
||||
y_size = w * h
|
||||
u0 = y_size + last_uv * uv_w
|
||||
v0 = y_size + uv_w * uv_h + last_uv * uv_w
|
||||
buf[u0:u0 + uv_w] = bytes([40]) * uv_w
|
||||
buf[v0:v0 + uv_w] = bytes([200]) * uv_w
|
||||
|
||||
repeat_i420_bottom_from_last_good(buf, w, h, crop)
|
||||
|
||||
for r in range(h - crop, h):
|
||||
row = buf[r * y_stride:(r + 1) * y_stride]
|
||||
assert set(row) == {90}, r
|
||||
for r in range(uv_h - uv_crop, uv_h):
|
||||
u = buf[y_size + r * uv_w:y_size + (r + 1) * uv_w]
|
||||
v = buf[y_size + uv_w * uv_h + r * uv_w:
|
||||
y_size + uv_w * uv_h + (r + 1) * uv_w]
|
||||
assert set(u) == {40}, r
|
||||
assert set(v) == {200}, r
|
||||
# rows above the pad stay put
|
||||
mid = (h // 2) * y_stride
|
||||
assert set(buf[mid:mid + y_stride]) == {16}
|
||||
|
||||
|
||||
def test_360p_crops_partial_mcu_and_last_full_mcu():
|
||||
"""360 % 16 = 8 leftover; last full 16-row MCU is also garbage on 07/14/15."""
|
||||
assert jpeg_mcu_bottom_crop(360) == 24
|
||||
assert jpeg_mcu_bottom_crop(480) == 0
|
||||
assert jpeg_mcu_bottom_crop(352) == 0
|
||||
assert jpeg_mcu_bottom_crop(8) == 0
|
||||
buf = _i420(640, 480, y=77)
|
||||
before = bytes(buf)
|
||||
repeat_i420_bottom_from_last_good(buf, 640, 480, jpeg_mcu_bottom_crop(480))
|
||||
assert bytes(buf) == before
|
||||
|
||||
|
||||
def test_mcu_fix_cams_off():
|
||||
assert not needs_jpeg_mcu_fix("cam-07")
|
||||
assert not needs_jpeg_mcu_fix("cam-12")
|
||||
assert not needs_jpeg_mcu_fix("cam-14")
|
||||
assert not needs_jpeg_mcu_fix("cam-15")
|
||||
assert not needs_jpeg_mcu_fix("cam-01")
|
||||
assert not needs_jpeg_mcu_fix("cam-20")
|
||||
|
||||
|
||||
def test_crop_scale_drops_garbage_without_cloned_stripe():
|
||||
w, h = 640, 360
|
||||
crop = 24
|
||||
buf = _i420(w, h, y=40, u=100, v=140)
|
||||
y_size = w * h
|
||||
# garbage last 24 Y rows
|
||||
buf[(h - crop) * w:y_size] = bytes([250]) * (crop * w)
|
||||
out = crop_scale_i420_drop_bottom(buf, w, h, crop)
|
||||
assert len(out) == len(buf)
|
||||
y = memoryview(out)[:y_size]
|
||||
last = bytes(y[(h - 8) * w:h * w])
|
||||
assert last.count(250) == 0
|
||||
assert set(last) == {16}
|
||||
|
||||
|
||||
def test_live_path_clones_not_studio_black():
|
||||
w, h, crop = 640, 360, 24
|
||||
buf = _i420(w, h, y=40, u=100, v=140)
|
||||
last_good = h - crop - 1
|
||||
buf[last_good * w:(last_good + 1) * w] = bytes([90]) * w
|
||||
buf[(h - crop) * w:h * w] = bytes([250]) * (crop * w)
|
||||
repeat_i420_bottom_from_last_good(buf, w, h, crop)
|
||||
visible = bytes(buf[(h - crop) * w:h * w])
|
||||
assert set(visible) == {90}
|
||||
assert 16 not in visible
|
||||
assert 250 not in visible
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Env-flagged low-latency knobs (hop/queue, hold, enhance scope)."""
|
||||
from latency import (
|
||||
audio_queue_size_ms,
|
||||
effective_enhance_mode,
|
||||
video_hold_seconds,
|
||||
video_stream_kwargs,
|
||||
)
|
||||
|
||||
|
||||
def test_audio_queue_defaults_to_legacy_floor():
|
||||
assert audio_queue_size_ms(0.1, audio_queue_ms=None) == 150
|
||||
assert audio_queue_size_ms(0.2, audio_queue_ms=None) == 250
|
||||
|
||||
|
||||
def test_audio_queue_env_overrides_floor():
|
||||
assert audio_queue_size_ms(0.1, audio_queue_ms=60) == 60
|
||||
assert audio_queue_size_ms(0.05, audio_queue_ms=60) == 60
|
||||
|
||||
|
||||
def test_video_hold_matches_hop_when_audio_present():
|
||||
assert video_hold_seconds(0.1, has_audio=True, video_hold_s=None) == 0.1
|
||||
assert video_hold_seconds(0.05, has_audio=True, video_hold_s=None) == 0.05
|
||||
|
||||
|
||||
def test_video_hold_zero_without_audio():
|
||||
assert video_hold_seconds(0.1, has_audio=False, video_hold_s=None) == 0.0
|
||||
assert video_hold_seconds(0.1, has_audio=False, video_hold_s=0.2) == 0.0
|
||||
|
||||
|
||||
def test_video_hold_env_overrides_including_zero():
|
||||
assert video_hold_seconds(0.1, has_audio=True, video_hold_s=0.0) == 0.0
|
||||
assert video_hold_seconds(0.1, has_audio=True, video_hold_s=0.05) == 0.05
|
||||
|
||||
|
||||
def test_enhance_speaker_only_rally_disables_c920s():
|
||||
assert effective_enhance_mode(
|
||||
"auto", "cam-01", speaker_only=True, speaker_camera="rally",
|
||||
) == "off"
|
||||
assert effective_enhance_mode(
|
||||
"auto", "rally", speaker_only=True, speaker_camera="rally",
|
||||
) == "auto"
|
||||
|
||||
|
||||
def test_enhance_speaker_only_inactive_leaves_mode():
|
||||
assert effective_enhance_mode(
|
||||
"auto", "cam-07", speaker_only=False, speaker_camera="cam-01",
|
||||
) == "auto"
|
||||
|
||||
|
||||
def test_enhance_speaker_only_ignored_when_speaker_is_active():
|
||||
assert effective_enhance_mode(
|
||||
"auto", "cam-07", speaker_only=True, speaker_camera="active",
|
||||
) == "auto"
|
||||
|
||||
|
||||
def test_video_stream_legacy_unbounded_native():
|
||||
assert video_stream_kwargs(capacity=0, format_name="") == {}
|
||||
|
||||
|
||||
def test_video_stream_latest_bgra():
|
||||
assert video_stream_kwargs(capacity=1, format_name="bgra") == {
|
||||
"capacity": 1,
|
||||
"format": "bgra",
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Latest-AU H.264 mmap: overwrite unread payloads; seq is monotonic."""
|
||||
from nal_shm import AU_MAX, NalReader, NalWriter
|
||||
|
||||
|
||||
def test_writer_seq_starts_at_one_and_increments(tmp_path):
|
||||
path = str(tmp_path / "cam-01.h264")
|
||||
w = NalWriter(path, max_payload=64)
|
||||
assert w.write(b"\x00\x00\x00\x01\x65" + b"x" * 10) == 1
|
||||
assert w.write(b"\x00\x00\x00\x01\x41" + b"y" * 10) == 2
|
||||
w.close()
|
||||
|
||||
|
||||
def test_reader_returns_latest_payload(tmp_path):
|
||||
path = str(tmp_path / "cam-01.h264")
|
||||
w = NalWriter(path, max_payload=64)
|
||||
w.write(b"first-access-unit-xxxx")
|
||||
w.write(b"second-access-unit-yyy")
|
||||
r = NalReader(path)
|
||||
seq, payload = r.read()
|
||||
assert seq == 2
|
||||
assert payload.startswith(b"second-access-unit")
|
||||
w.close()
|
||||
r.close()
|
||||
|
||||
|
||||
def test_reader_none_when_no_new_au(tmp_path):
|
||||
path = str(tmp_path / "cam-01.h264")
|
||||
w = NalWriter(path, max_payload=64)
|
||||
w.write(b"only-once")
|
||||
r = NalReader(path)
|
||||
assert r.read()[0] == 1
|
||||
assert r.read() is None
|
||||
w.close()
|
||||
r.close()
|
||||
|
||||
|
||||
def test_write_drops_oversize_payload(tmp_path):
|
||||
path = str(tmp_path / "cam-01.h264")
|
||||
w = NalWriter(path, max_payload=8)
|
||||
assert w.write(b"123456789") == 0
|
||||
w.close()
|
||||
|
||||
|
||||
def test_au_max_is_bounded():
|
||||
assert AU_MAX >= 64 * 1024
|
||||
assert AU_MAX <= 2 * 1024 * 1024
|
||||
@@ -0,0 +1,88 @@
|
||||
from participant_tags import (
|
||||
should_hide_participant,
|
||||
tags_from_fields,
|
||||
token_attributes,
|
||||
want_display_video,
|
||||
)
|
||||
|
||||
|
||||
def test_tags_from_attribute_tag():
|
||||
assert tags_from_fields({"tag": "uwh"}, "") == {"uwh"}
|
||||
|
||||
|
||||
def test_tags_from_comma_separated_tags_attr():
|
||||
assert tags_from_fields({"tags": "uwh,lab"}, None) == {"uwh", "lab"}
|
||||
|
||||
|
||||
def test_tags_from_json_metadata():
|
||||
assert tags_from_fields({}, '{"tag": "uwh"}') == {"uwh"}
|
||||
|
||||
|
||||
def test_hide_disabled_when_hide_tags_empty():
|
||||
assert should_hide_participant({"tag": "uwh"}, "", []) is False
|
||||
|
||||
|
||||
def test_hide_uwh_when_filter_enabled():
|
||||
assert should_hide_participant({"tag": "uwh"}, "", ["uwh"]) is True
|
||||
assert should_hide_participant({"tag": "nyc"}, "", ["uwh"]) is False
|
||||
assert should_hide_participant({}, "", ["uwh"]) is False
|
||||
|
||||
|
||||
def test_token_attributes_sets_tag_and_tags():
|
||||
assert token_attributes(["uwh"]) == {"tag": "uwh", "tags": "uwh"}
|
||||
assert token_attributes([]) == {}
|
||||
|
||||
|
||||
def test_want_display_keeps_cameras_when_filter_off():
|
||||
assert want_display_video(
|
||||
identity="cam-01",
|
||||
attributes={"tag": "uwh"},
|
||||
metadata="",
|
||||
hide_tags=[],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
) is True
|
||||
assert want_display_video(
|
||||
identity="alice",
|
||||
attributes={},
|
||||
metadata="",
|
||||
hide_tags=[],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
) is False
|
||||
assert want_display_video(
|
||||
identity="rally",
|
||||
attributes={},
|
||||
metadata="",
|
||||
hide_tags=[],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
speaker_identity="rally",
|
||||
) is True
|
||||
assert want_display_video(
|
||||
identity="rally",
|
||||
attributes={},
|
||||
metadata="",
|
||||
hide_tags=[],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
) is False
|
||||
|
||||
|
||||
def test_want_display_hides_uwh_and_shows_remote_when_filter_on():
|
||||
assert want_display_video(
|
||||
identity="cam-01",
|
||||
attributes={"tag": "uwh"},
|
||||
metadata="",
|
||||
hide_tags=["uwh"],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
) is False
|
||||
assert want_display_video(
|
||||
identity="alice",
|
||||
attributes={"tag": "nyc"},
|
||||
metadata="",
|
||||
hide_tags=["uwh"],
|
||||
participant_prefix="cam",
|
||||
display_identity="display-wall",
|
||||
) is True
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Person-aware background blur: composite, hold, I420, encode path."""
|
||||
import numpy as np
|
||||
|
||||
from portrait import (
|
||||
MaskHold,
|
||||
apply_i420,
|
||||
apply_portrait,
|
||||
bgr_to_i420,
|
||||
composite_bgr,
|
||||
i420_nbytes,
|
||||
i420_source_path,
|
||||
i420_to_bgr,
|
||||
person_present,
|
||||
soft_blur_bgr,
|
||||
)
|
||||
|
||||
|
||||
def _checker(h: int, w: int) -> np.ndarray:
|
||||
img = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
img[::4, :] = 255
|
||||
img[:, ::4] = 255
|
||||
return img
|
||||
|
||||
|
||||
def test_composite_keeps_person_pixels():
|
||||
h, w = 80, 128
|
||||
bg = _checker(h, w)
|
||||
person = bg.copy()
|
||||
person[16:64, 32:96] = (40, 180, 40)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[16:64, 32:96] = 1.0
|
||||
out = composite_bgr(person, soft_blur_bgr(person, 9), mask)
|
||||
roi = out[24:56, 40:88]
|
||||
assert abs(int(roi[:, :, 1].mean()) - 180) < 8
|
||||
assert abs(int(roi[:, :, 0].mean()) - 40) < 8
|
||||
|
||||
|
||||
def test_composite_blurs_background_checker():
|
||||
h, w = 80, 128
|
||||
bg = _checker(h, w)
|
||||
person = bg.copy()
|
||||
person[16:64, 32:96] = (40, 180, 40)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[16:64, 32:96] = 1.0
|
||||
sharp_var = float(bg[0:12, :].var())
|
||||
out = composite_bgr(person, soft_blur_bgr(person, 9), mask)
|
||||
blur_var = float(out[0:12, :].var())
|
||||
assert sharp_var > 1000
|
||||
assert blur_var < sharp_var * 0.5
|
||||
|
||||
|
||||
def test_empty_mask_is_fully_blurred():
|
||||
img = _checker(40, 64)
|
||||
mask = np.zeros((40, 64), dtype=np.float32)
|
||||
out = apply_portrait(img, mask)
|
||||
assert person_present(mask) is False
|
||||
assert float(out.var()) < float(img.var()) * 0.5
|
||||
assert not np.array_equal(out, img)
|
||||
|
||||
|
||||
def test_tiny_blob_is_not_a_person():
|
||||
mask = np.zeros((40, 64), dtype=np.float32)
|
||||
mask[10:12, 10:12] = 0.9
|
||||
assert person_present(mask) is False
|
||||
|
||||
|
||||
def test_mask_hold_keeps_last_on_miss():
|
||||
hold = MaskHold(hold_s=0.5, ema=1.0)
|
||||
mask = np.ones((8, 8), dtype=np.float32)
|
||||
got = hold.update(mask, True, 1.0)
|
||||
assert got is not None
|
||||
missed = hold.update(None, False, 1.2)
|
||||
assert missed is not None
|
||||
assert float(missed.mean()) == 1.0
|
||||
assert hold.update(None, False, 1.6) is None
|
||||
|
||||
|
||||
def test_ema_smooths_mask_flicker():
|
||||
hold = MaskHold(hold_s=1.0, ema=0.5)
|
||||
a = np.zeros((4, 4), dtype=np.float32)
|
||||
b = np.ones((4, 4), dtype=np.float32)
|
||||
hold.update(a, True, 0.0)
|
||||
out = hold.update(b, True, 0.1)
|
||||
assert 0.4 < float(out.mean()) < 0.6
|
||||
|
||||
|
||||
def test_i420_roundtrip_keeps_640x360():
|
||||
w, h = 640, 360
|
||||
bgr = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
bgr[20:200, 80:400] = (30, 90, 200)
|
||||
payload = bgr_to_i420(bgr)
|
||||
assert len(payload) == i420_nbytes(w, h)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[20:200, 80:400] = 1.0
|
||||
out = apply_i420(payload, w, h, mask, radius=9)
|
||||
assert len(out) == i420_nbytes(w, h)
|
||||
back = i420_to_bgr(out, w, h)
|
||||
assert back.shape == (h, w, 3)
|
||||
|
||||
|
||||
def test_missing_mask_blurs_whole_frame():
|
||||
w, h = 48, 32
|
||||
bgr = _checker(h, w)
|
||||
payload = bgr_to_i420(bgr)
|
||||
out = apply_i420(payload, w, h, None, radius=7)
|
||||
y0 = np.frombuffer(payload, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
y1 = np.frombuffer(out, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
assert float(y1.var()) < float(y0.var()) * 0.6
|
||||
|
||||
|
||||
def test_apply_i420_blurs_y_background():
|
||||
w, h = 64, 48
|
||||
bgr = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
bgr[::3, :] = 255
|
||||
bgr[:, ::3] = 255
|
||||
bgr[8:40, 16:48] = (20, 200, 20)
|
||||
payload = bgr_to_i420(bgr)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[8:40, 16:48] = 1.0
|
||||
out = apply_i420(payload, w, h, mask, radius=9)
|
||||
y0 = np.frombuffer(payload, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
y1 = np.frombuffer(out, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
assert float(y1[0:6, :].var()) < float(y0[0:6, :].var()) * 0.6
|
||||
assert abs(int(y1[12:36, 20:44].mean()) - int(y0[12:36, 20:44].mean())) < 12
|
||||
|
||||
|
||||
def test_c920_uses_portrait_dir_rally_stays_raw():
|
||||
assert i420_source_path("cam-01", "/run/raw", "/run/por") == "/run/por/cam-01.i420"
|
||||
assert i420_source_path("cam-15", "/run/raw", "/run/por") == "/run/por/cam-15.i420"
|
||||
assert i420_source_path("rally", "/run/raw", "/run/por") == "/run/raw/rally.i420"
|
||||
assert i420_source_path("cam-01", "/run/raw", "") == "/run/raw/cam-01.i420"
|
||||
|
||||
|
||||
def test_letterbox_mask_maps_back_to_16x9():
|
||||
from portrait import letterbox_bgr, unletterbox_mask
|
||||
|
||||
bgr = np.zeros((360, 640, 3), dtype=np.uint8)
|
||||
canvas, box = letterbox_bgr(bgr, 256)
|
||||
assert canvas.shape == (256, 256, 3)
|
||||
x0, y0, nw, nh = box
|
||||
assert nw > nh
|
||||
m = np.zeros((256, 256), dtype=np.float32)
|
||||
cy, cx = y0 + nh // 2, x0 + nw // 2
|
||||
m[cy - 2:cy + 3, cx - 2:cx + 3] = 1.0
|
||||
full = unletterbox_mask(m, box, 360, 640)
|
||||
assert full.shape == (360, 640)
|
||||
assert float(full[180, 320]) > 0.5
|
||||
assert float(full[2, 2]) < 0.1
|
||||
|
||||
|
||||
def test_selfie_cpu_sees_warrior_not_empty():
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
|
||||
from portrait import PersonSeg
|
||||
|
||||
rgb = np.load(Path(__file__).parent / "fixtures" / "movenet_warrior_192.npy")
|
||||
bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
|
||||
seg = PersonSeg()
|
||||
assert seg.device == "CPU"
|
||||
mask = seg.infer_bgr(bgr)
|
||||
assert person_present(mask)
|
||||
empty = np.full((360, 640, 3), 40, dtype=np.uint8)
|
||||
assert person_present(seg.infer_bgr(empty)) is False
|
||||
|
||||
|
||||
def test_pose_protect_keeps_wrists_and_face():
|
||||
from portrait import pose_protect_mask
|
||||
|
||||
k = np.zeros((17, 3), dtype=np.float32)
|
||||
k[0] = [0.30, 0.50, 0.9] # nose
|
||||
k[9] = [0.12, 0.18, 0.9] # L wrist
|
||||
k[10] = [0.12, 0.82, 0.9] # R wrist
|
||||
m = pose_protect_mask(100, 200, k)
|
||||
assert float(m[12, 36]) > 0.7
|
||||
assert float(m[12, 164]) > 0.7
|
||||
assert float(m[30, 100]) > 0.7
|
||||
assert float(m[90, 10]) < 0.2
|
||||
|
||||
|
||||
def test_union_masks_keeps_face_and_body():
|
||||
from portrait import union_masks
|
||||
|
||||
body = np.zeros((40, 40), dtype=np.float32)
|
||||
body[20:36, 10:30] = 1.0
|
||||
face = np.zeros((40, 40), dtype=np.float32)
|
||||
face[4:14, 14:26] = 1.0
|
||||
out = union_masks(body, face)
|
||||
assert float(out[8, 20]) == 1.0
|
||||
assert float(out[28, 20]) == 1.0
|
||||
assert float(out[2, 2]) == 0.0
|
||||
|
||||
|
||||
def test_lighter_blur_keeps_more_background_detail():
|
||||
w, h = 64, 48
|
||||
bgr = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
bgr[::3, :] = 255
|
||||
bgr[:, ::3] = 255
|
||||
payload = bgr_to_i420(bgr)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[8:40, 16:48] = 1.0
|
||||
heavy = apply_i420(payload, w, h, mask, radius=15)
|
||||
light = apply_i420(payload, w, h, mask, radius=7)
|
||||
y0 = np.frombuffer(payload, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
yh = np.frombuffer(heavy, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
yl = np.frombuffer(light, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
bg0 = float(y0[0:6, :].var())
|
||||
assert float(yl[0:6, :].var()) > float(yh[0:6, :].var())
|
||||
assert float(yl[0:6, :].var()) < bg0
|
||||
@@ -0,0 +1,87 @@
|
||||
"""SHM round-trip for portrait daemon with a fake mask."""
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from frame_shm import LatestFrameReader, LatestFrameWriter, wait_for_ready
|
||||
from portrait import bgr_to_i420, i420_nbytes, i420_to_bgr, person_present
|
||||
from portrait_daemon import CamSlot, tick_once
|
||||
|
||||
|
||||
def test_tick_blurs_bg_and_keeps_person(tmp_path):
|
||||
w, h = 64, 48
|
||||
bgr = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
bgr[::3, :] = 255
|
||||
bgr[:, ::3] = 255
|
||||
bgr[8:40, 16:48] = (20, 200, 20)
|
||||
payload = bgr_to_i420(bgr)
|
||||
src = tmp_path / "raw" / "cam-01.i420"
|
||||
dst = tmp_path / "por" / "cam-01.i420"
|
||||
writer = LatestFrameWriter(str(src), w, h)
|
||||
writer.write(payload)
|
||||
mask = np.zeros((h, w), dtype=np.float32)
|
||||
mask[8:40, 16:48] = 1.0
|
||||
|
||||
def infer(_bgr):
|
||||
return mask
|
||||
|
||||
slot = CamSlot(
|
||||
identity="cam-01",
|
||||
width=w,
|
||||
height=h,
|
||||
reader=LatestFrameReader(str(src)),
|
||||
writer=LatestFrameWriter(str(dst), w, h),
|
||||
infer_hz=30.0,
|
||||
hold_s=0.8,
|
||||
blur_px=9,
|
||||
)
|
||||
n = tick_once([slot], infer, time.monotonic(), infer_budget_s=0.05)
|
||||
assert n == 1
|
||||
out_r = LatestFrameReader(str(dst))
|
||||
buf = bytearray(i420_nbytes(w, h))
|
||||
seq = out_r.copy_into(buf)
|
||||
assert seq == 1
|
||||
out = i420_to_bgr(buf, w, h)
|
||||
roi = out[12:36, 20:44]
|
||||
assert abs(int(roi[:, :, 1].mean()) - 200) < 25
|
||||
bg = out[0:6, :]
|
||||
assert float(bg.var()) < float(bgr[0:6, :].var()) * 0.6
|
||||
|
||||
|
||||
def test_tick_blurs_when_empty(tmp_path):
|
||||
w, h = 32, 32
|
||||
bgr = np.zeros((h, w, 3), dtype=np.uint8)
|
||||
bgr[::3, :] = 255
|
||||
bgr[:, ::3] = 255
|
||||
payload = bgr_to_i420(bgr)
|
||||
src = tmp_path / "raw" / "cam-02.i420"
|
||||
dst = tmp_path / "por" / "cam-02.i420"
|
||||
LatestFrameWriter(str(src), w, h).write(payload)
|
||||
|
||||
def infer(_bgr):
|
||||
return np.zeros((h, w), dtype=np.float32)
|
||||
|
||||
slot = CamSlot(
|
||||
identity="cam-02",
|
||||
width=w,
|
||||
height=h,
|
||||
reader=LatestFrameReader(str(src)),
|
||||
writer=LatestFrameWriter(str(dst), w, h),
|
||||
infer_hz=30.0,
|
||||
hold_s=0.2,
|
||||
blur_px=9,
|
||||
)
|
||||
tick_once([slot], infer, time.monotonic(), infer_budget_s=0.05)
|
||||
buf = bytearray(i420_nbytes(w, h))
|
||||
assert LatestFrameReader(str(dst)).copy_into(buf) == 1
|
||||
y0 = np.frombuffer(payload, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
y1 = np.frombuffer(buf, dtype=np.uint8, count=w * h).reshape(h, w)
|
||||
assert float(y1.var()) < float(y0.var()) * 0.6
|
||||
assert person_present(np.zeros((h, w), dtype=np.float32)) is False
|
||||
|
||||
|
||||
def test_wait_ready_helper(tmp_path):
|
||||
p = tmp_path / "ready"
|
||||
assert wait_for_ready(str(p), timeout=0.05) is False
|
||||
p.write_text("ok\n")
|
||||
assert wait_for_ready(str(p), timeout=0.2) is True
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from discovery import Camera
|
||||
from rally import dante_capture_card, discover_rally, rally_video_node
|
||||
from run import fleet_cameras
|
||||
from usb_ids import RALLY_IDENTITY
|
||||
|
||||
_HAVE_RALLY = rally_video_node() is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAVE_RALLY, reason="Rally V-R0010 not on USB")
|
||||
def test_rally_node_is_pinned_or_uvc():
|
||||
node = rally_video_node()
|
||||
assert node is not None
|
||||
assert Path(node).exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAVE_RALLY, reason="Rally V-R0010 not on USB")
|
||||
def test_discover_rally_not_a_cam_slot():
|
||||
cam = discover_rally()
|
||||
assert cam is not None
|
||||
assert cam.identity == RALLY_IDENTITY
|
||||
assert cam.width == 1920
|
||||
assert cam.height == 1080
|
||||
assert cam.identity != "cam-01"
|
||||
card = dante_capture_card()
|
||||
assert cam.audio_card == card
|
||||
|
||||
|
||||
def test_fleet_cameras_drops_rally():
|
||||
cams = [
|
||||
Camera(index=0, identity="cam-01", video_device="/dev/cam01", audio_card=1),
|
||||
Camera(index=20, identity="rally", video_device="/dev/rally", audio_card=0),
|
||||
]
|
||||
assert [c.identity for c in fleet_cameras(cams)] == ["cam-01"]
|
||||
@@ -0,0 +1,172 @@
|
||||
"""YuNet face gates for Rally follow — reject ceiling/stand/noise."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from rally_follow import FaceGate, accept_yunet_face, select_yunet_face
|
||||
|
||||
|
||||
def _face(
|
||||
*,
|
||||
x=400.0,
|
||||
y=200.0,
|
||||
w=80.0,
|
||||
h=100.0,
|
||||
score=0.93,
|
||||
re=None,
|
||||
le=None,
|
||||
nose=None,
|
||||
rm=None,
|
||||
lm=None,
|
||||
) -> np.ndarray:
|
||||
"""YuNet 15-vector: box + 5 landmarks + score, in 960x540 detect space."""
|
||||
if re is None:
|
||||
re = (x + 0.30 * w, y + 0.38 * h)
|
||||
if le is None:
|
||||
le = (x + 0.70 * w, y + 0.38 * h)
|
||||
if nose is None:
|
||||
nose = (x + 0.50 * w, y + 0.55 * h)
|
||||
if rm is None:
|
||||
rm = (x + 0.35 * w, y + 0.78 * h)
|
||||
if lm is None:
|
||||
lm = (x + 0.65 * w, y + 0.78 * h)
|
||||
row = np.zeros(15, dtype=np.float32)
|
||||
row[0:4] = (x, y, w, h)
|
||||
row[4:6] = re
|
||||
row[6:8] = le
|
||||
row[8:10] = nose
|
||||
row[10:12] = rm
|
||||
row[12:14] = lm
|
||||
row[14] = score
|
||||
return row
|
||||
|
||||
|
||||
FW, FH = 960, 540
|
||||
|
||||
|
||||
def test_good_face_accepted():
|
||||
assert accept_yunet_face(_face(), FW, FH) is True
|
||||
|
||||
|
||||
def test_tiny_blob_rejected():
|
||||
# Live FP: 25x34 @ 0.805, nose not between the eyes.
|
||||
row = _face(x=512, y=250, w=25, h=34, score=0.805,
|
||||
re=(528.5, 264), le=(532.1, 263), nose=(536.1, 271),
|
||||
rm=(529.7, 277), lm=(532.1, 278))
|
||||
assert accept_yunet_face(row, FW, FH) is False
|
||||
|
||||
|
||||
def test_low_score_rejected():
|
||||
assert accept_yunet_face(_face(score=0.61), FW, FH) is False
|
||||
|
||||
|
||||
def test_ceiling_band_rejected():
|
||||
row = _face(x=400, y=2, w=80, h=100)
|
||||
assert accept_yunet_face(row, FW, FH) is False
|
||||
|
||||
|
||||
def test_stand_band_rejected():
|
||||
row = _face(x=400, y=470, w=80, h=100)
|
||||
assert accept_yunet_face(row, FW, FH) is False
|
||||
|
||||
|
||||
def test_nose_not_between_eyes_rejected():
|
||||
row = _face(re=(430, 238), le=(450, 238), nose=(470, 255))
|
||||
assert accept_yunet_face(row, FW, FH) is False
|
||||
|
||||
|
||||
def test_upside_down_landmarks_rejected():
|
||||
row = _face(
|
||||
re=(424, 270), le=(456, 270), nose=(440, 240),
|
||||
rm=(428, 220), lm=(452, 220),
|
||||
)
|
||||
assert accept_yunet_face(row, FW, FH) is False
|
||||
|
||||
|
||||
def test_select_skips_large_invalid_for_smaller_valid():
|
||||
junk = _face(x=10, y=2, w=80, h=100, score=0.99) # ceiling
|
||||
good = _face(x=400, y=200, w=80, h=100, score=0.88)
|
||||
picked = select_yunet_face(np.stack([junk, good]), FW, FH)
|
||||
assert picked is not None
|
||||
assert float(picked[0]) == 400.0
|
||||
|
||||
|
||||
def test_select_empty():
|
||||
assert select_yunet_face(None, FW, FH) is None
|
||||
assert select_yunet_face(np.zeros((0, 15)), FW, FH) is None
|
||||
|
||||
|
||||
def test_face_gate_needs_confirm():
|
||||
gate = FaceGate(need=3, max_jump=0.18)
|
||||
box = (400, 200, 80, 100)
|
||||
assert gate.update(box, 960, 540) is None
|
||||
assert gate.update(box, 960, 540) is None
|
||||
assert gate.update(box, 960, 540) == box
|
||||
|
||||
|
||||
def test_face_gate_jump_resets():
|
||||
gate = FaceGate(need=3, max_jump=0.18)
|
||||
a = (400, 200, 80, 100)
|
||||
b = (800, 400, 80, 100)
|
||||
assert gate.update(a, 960, 540) is None
|
||||
assert gate.update(a, 960, 540) is None
|
||||
assert gate.update(b, 960, 540) is None # new target
|
||||
assert gate.update(b, 960, 540) is None
|
||||
assert gate.update(b, 960, 540) == b
|
||||
|
||||
|
||||
def test_face_gate_loss_clears():
|
||||
gate = FaceGate(need=2, max_jump=0.18)
|
||||
box = (400, 200, 80, 100)
|
||||
gate.update(box, 960, 540)
|
||||
assert gate.update(box, 960, 540) == box
|
||||
assert gate.update(None, 960, 540) is None
|
||||
assert gate.update(box, 960, 540) is None
|
||||
|
||||
|
||||
def test_profile_face_accepted():
|
||||
"""Head turned: eyes cluster, nose not between them — still a face."""
|
||||
row = _face(
|
||||
x=400, y=200, w=80, h=100, score=0.90,
|
||||
re=(418, 238), le=(426, 237),
|
||||
nose=(410, 255),
|
||||
rm=(416, 278), lm=(428, 277),
|
||||
)
|
||||
assert accept_yunet_face(row, FW, FH) is True
|
||||
|
||||
|
||||
def test_head_hold_keeps_box_on_roi_motion():
|
||||
from rally_follow import HeadHold
|
||||
hold = HeadHold(hold_s=1.0)
|
||||
box = (400, 200, 80, 100)
|
||||
t = 10.0
|
||||
assert hold.update(box, moving=False, now=t) == box
|
||||
assert hold.update(None, moving=True, now=t + 0.3) == box
|
||||
assert hold.update(None, moving=False, now=t + 0.4) == box # coast
|
||||
assert hold.update(None, moving=False, now=t + 2.0) is None
|
||||
|
||||
|
||||
def test_roi_motion_ignores_outside_last_box():
|
||||
from rally_follow import roi_motion
|
||||
prev = np.zeros((540, 960), dtype=np.uint8)
|
||||
now = prev.copy()
|
||||
now[10:40, 10:40] = 200 # motion far from the face
|
||||
assert roi_motion(prev, now, (400, 200, 80, 100)) is False
|
||||
now2 = prev.copy()
|
||||
now2[210:260, 420:460] = 200
|
||||
assert roi_motion(prev, now2, (400, 200, 80, 100)) is True
|
||||
|
||||
|
||||
def test_stale_hold_freezes_does_not_keep_panning():
|
||||
"""Last image-space box is not a pan target — that derails as the camera moves."""
|
||||
from rally_follow import lock_action, steer_speeds
|
||||
left = (80, 400, 160, 200)
|
||||
pan, tilt = steer_speeds(left, 1920, 1080, deadband=0.14)
|
||||
assert pan == -1
|
||||
assert lock_action(confirmed=left, held=left) == "steer"
|
||||
assert lock_action(confirmed=None, held=left) == "freeze"
|
||||
assert lock_action(confirmed=None, held=None) == "lost"
|
||||
cx = 1920 / 2 - 40
|
||||
mid = (int(cx - 80), 440, 160, 200)
|
||||
pan, tilt = steer_speeds(mid, 1920, 1080, deadband=0.14)
|
||||
assert pan == 0 and tilt == 0
|
||||
@@ -0,0 +1,11 @@
|
||||
from rally_follow import video_cmd
|
||||
|
||||
|
||||
def test_video_cmd_without_preview_has_no_kmssink():
|
||||
cmd = video_cmd("/dev/rally", 1920, 1080, 30, 8, preview=None)
|
||||
s = " ".join(cmd)
|
||||
assert "kmssink" not in s
|
||||
assert "tee" not in cmd
|
||||
assert "fdsink" in cmd
|
||||
assert "fd=8" in cmd
|
||||
assert "device=/dev/rally" in s
|
||||
@@ -0,0 +1,103 @@
|
||||
from config import Config
|
||||
from discovery import Camera
|
||||
from run import worker_args_for
|
||||
import json
|
||||
import jwt
|
||||
|
||||
|
||||
def _cfg(**kw) -> Config:
|
||||
return Config(url="ws://127.0.0.1:7880", api_key="k", api_secret="s", **kw)
|
||||
|
||||
|
||||
def _cam(identity: str = "cam-07") -> Camera:
|
||||
return Camera(index=0, identity=identity, video_device="/dev/cam07", audio_card=2)
|
||||
|
||||
|
||||
def test_worker_args_legacy_omits_optional_latency_flags():
|
||||
args = worker_args_for(_cfg(), _cam())
|
||||
assert "--audio-queue-ms" not in args
|
||||
assert "--video-hold-s" not in args
|
||||
assert "--split-executor" not in args
|
||||
assert args[args.index("--enhance-mode") + 1] == "auto"
|
||||
|
||||
|
||||
def test_worker_args_latency_flags_and_non_speaker_enhance_off():
|
||||
cfg = _cfg(
|
||||
audio_queue_ms=60,
|
||||
video_hold_s=0.0,
|
||||
worker_split_executor=True,
|
||||
enhance_speaker_only=True,
|
||||
display_speaker_camera="cam-01",
|
||||
enhance_mode="auto",
|
||||
)
|
||||
args = worker_args_for(cfg, _cam("cam-07"))
|
||||
assert args[args.index("--audio-queue-ms") + 1] == "60"
|
||||
assert args[args.index("--video-hold-s") + 1] == "0.0"
|
||||
assert "--split-executor" in args
|
||||
assert args[args.index("--enhance-mode") + 1] == "off"
|
||||
assert args[args.index("--beamform-mode") + 1] == "off"
|
||||
|
||||
|
||||
def test_worker_args_speaker_keeps_enhance_mode():
|
||||
cfg = _cfg(
|
||||
enhance_mode="force",
|
||||
enhance_speaker_only=True,
|
||||
display_speaker_camera="cam-07",
|
||||
)
|
||||
args = worker_args_for(cfg, _cam("cam-07"))
|
||||
assert args[args.index("--enhance-mode") + 1] == "force"
|
||||
assert args[args.index("--beamform-mode") + 1] == "auto"
|
||||
|
||||
|
||||
def test_worker_token_carries_uwh_tag():
|
||||
args = worker_args_for(_cfg(), _cam("cam-07"))
|
||||
token = args[args.index("--token") + 1]
|
||||
claims = jwt.decode(token, "s", algorithms=["HS256"])
|
||||
assert claims["attributes"]["tag"] == "uwh"
|
||||
assert claims["attributes"]["tags"] == "uwh"
|
||||
|
||||
|
||||
def test_worker_args_passes_enhance_socket():
|
||||
args = worker_args_for(_cfg(enhance_socket="/tmp/e.sock"), _cam())
|
||||
assert args[args.index("--enhance-socket") + 1] == "/tmp/e.sock"
|
||||
|
||||
|
||||
def test_worker_args_omits_enhance_socket_when_empty():
|
||||
args = worker_args_for(_cfg(enhance_socket=""), _cam())
|
||||
assert "--enhance-socket" not in args
|
||||
|
||||
|
||||
def test_worker_args_passes_video_shm():
|
||||
args = worker_args_for(_cfg(), _cam("cam-07"))
|
||||
assert args[args.index("--video-shm") + 1] == "/run/livekit-cameras/raw/cam-07.i420"
|
||||
|
||||
|
||||
def test_worker_args_omits_video_shm_when_capture_daemon_off():
|
||||
args = worker_args_for(_cfg(capture_daemon=False), _cam())
|
||||
assert "--video-shm" not in args
|
||||
|
||||
|
||||
def test_publisher_args_single_process_and_speaker_only():
|
||||
from run import publisher_args_for
|
||||
cfg = _cfg(enhance_speaker_only=True, display_speaker_camera="cam-01")
|
||||
args = publisher_args_for(cfg, [_cam("cam-07"), _cam("cam-01")])
|
||||
assert "--enhance-speaker-only" in args
|
||||
assert "--audio-gate" not in args
|
||||
payload = json.loads(args[args.index("--cameras-json") + 1])
|
||||
assert {p["identity"] for p in payload} == {"cam-07", "cam-01"}
|
||||
assert all("token" in p for p in payload)
|
||||
assert args[args.index("--video-shm-dir") + 1] == "/run/livekit-cameras/raw"
|
||||
assert args[args.index("--audio-shm-dir") + 1] == "/run/livekit-cameras/pcm"
|
||||
|
||||
|
||||
def test_publisher_args_rally_keeps_1080p_dims():
|
||||
from run import publisher_args_for
|
||||
rally = Camera(index=20, identity="rally", video_device="/dev/rally",
|
||||
audio_card=0, width=1920, height=1080)
|
||||
args = publisher_args_for(_cfg(), [_cam("cam-01"), rally])
|
||||
payload = json.loads(args[args.index("--cameras-json") + 1])
|
||||
by_id = {p["identity"]: p for p in payload}
|
||||
assert by_id["rally"]["width"] == 1920
|
||||
assert by_id["rally"]["height"] == 1080
|
||||
assert by_id["cam-01"]["width"] is None
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Green speaking chrome on the KMS grid (LiveKit active-speaker)."""
|
||||
import numpy as np
|
||||
|
||||
from display_grid import FRAME_RAISE, FRAME_SPEAK, CameraGrid, bgra_to_i420, bgra_to_yuv_pixel
|
||||
|
||||
|
||||
def test_set_speakers_paints_green_i420_border():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
||||
src[:] = (40, 40, 40, 255)
|
||||
g.set_frame_i420("cam-07", bgra_to_i420(src), 640, 360)
|
||||
before = np.frombuffer(g.render_i420(), dtype=np.uint8).copy()
|
||||
g.set_speakers(["cam-07"])
|
||||
assert g.speaking("cam-07") is True
|
||||
after = np.frombuffer(g.render_i420(), dtype=np.uint8)
|
||||
assert not np.array_equal(before, after)
|
||||
idx = g.identities.index("cam-07")
|
||||
ox, oy = g.tile_origin(idx)
|
||||
yplane = after[: g.width * g.height].reshape(g.height, g.width)
|
||||
yv, _, _ = bgra_to_yuv_pixel(FRAME_SPEAK)
|
||||
assert int(yplane[oy, ox + g.tile_w // 2]) == yv
|
||||
# Extra thickness lives in the gutter, not on the feed.
|
||||
assert int(yplane[oy - 2, ox + g.tile_w // 2]) == yv
|
||||
ix, iy, iw, _ih = g.inner_rect(idx)
|
||||
assert int(yplane[iy, ix + iw // 2]) != yv
|
||||
|
||||
|
||||
def test_speaking_overrides_yellow_then_yellow_returns():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
||||
g.set_frame_i420("cam-07", bgra_to_i420(src), 640, 360)
|
||||
g.set_highlight("cam-07", True)
|
||||
g.set_speakers(["cam-07"])
|
||||
idx = g.identities.index("cam-07")
|
||||
ox, oy = g.tile_origin(idx)
|
||||
yplane = np.frombuffer(g.render_i420(), dtype=np.uint8)[: g.width * g.height].reshape(
|
||||
g.height, g.width)
|
||||
y_speak, _, _ = bgra_to_yuv_pixel(FRAME_SPEAK)
|
||||
y_raise, _, _ = bgra_to_yuv_pixel(FRAME_RAISE)
|
||||
assert int(yplane[oy, ox + g.tile_w // 2]) == y_speak
|
||||
g.set_speakers([])
|
||||
yplane = np.frombuffer(g.render_i420(), dtype=np.uint8)[: g.width * g.height].reshape(
|
||||
g.height, g.width)
|
||||
assert g.speaking("cam-07") is False
|
||||
assert g.highlighted("cam-07") is True
|
||||
assert int(yplane[oy, ox + g.tile_w // 2]) == y_raise
|
||||
|
||||
|
||||
def test_clear_drops_speaking():
|
||||
g = CameraGrid(width=1920, height=1080, cols=5, rows=4)
|
||||
src = np.zeros((360, 640, 4), dtype=np.uint8)
|
||||
g.set_frame_i420("cam-01", bgra_to_i420(src), 640, 360)
|
||||
g.set_speakers(["cam-01"])
|
||||
g.clear("cam-01")
|
||||
assert g.speaking("cam-01") is False
|
||||
|
||||
|
||||
class _P:
|
||||
def __init__(self, identity, keep=True):
|
||||
self.identity = identity
|
||||
self.keep = keep
|
||||
|
||||
|
||||
def test_active_speaker_ids_keeps_order_and_filter():
|
||||
from display_grid import active_speaker_ids, primary_grid_speaker
|
||||
speakers = [_P("cam-07"), _P("rally", False), _P("cam-03"), _P("")]
|
||||
assert active_speaker_ids(speakers, want=lambda p: p.keep) == ["cam-07", "cam-03"]
|
||||
ids = ["rally", "cam-03", "cam-07"]
|
||||
assert primary_grid_speaker(ids, ["cam-01", "cam-03", "cam-07"]) == ["cam-03"]
|
||||
assert primary_grid_speaker(["rally"], ["cam-01"]) == []
|
||||
|
||||
|
||||
def test_select_grid_speakers_drops_floor_noise():
|
||||
from display_grid import select_grid_speakers
|
||||
assert select_grid_speakers(
|
||||
[("cam-01", -34.0), ("cam-02", -36.0)], min_db=-24.0, margin_db=6.0,
|
||||
) == []
|
||||
|
||||
|
||||
def test_select_grid_speakers_bleed_keeps_louder_only():
|
||||
from display_grid import select_grid_speakers
|
||||
assert select_grid_speakers(
|
||||
[("cam-07", -12.0), ("cam-08", -22.0)], min_db=-24.0, margin_db=6.0,
|
||||
) == ["cam-07"]
|
||||
|
||||
|
||||
def test_select_grid_speakers_two_talkers_both_kept():
|
||||
from display_grid import select_grid_speakers
|
||||
assert select_grid_speakers(
|
||||
[("cam-07", -12.0), ("cam-08", -14.0)], min_db=-24.0, margin_db=6.0,
|
||||
) == ["cam-07", "cam-08"]
|
||||
|
||||
|
||||
def test_select_grid_speakers_normal_speech_above_mic_floor():
|
||||
"""Conversational C920 speech is often ~-24 dBFS; absolute -20 missed it."""
|
||||
from display_grid import select_grid_speakers
|
||||
floors = {"cam-07": -34.0, "cam-08": -34.0}
|
||||
assert select_grid_speakers(
|
||||
[("cam-07", -24.0), ("cam-08", -33.0)],
|
||||
min_db=-40.0, margin_db=6.0, floors=floors, rise_db=6.0,
|
||||
) == ["cam-07"]
|
||||
|
||||
|
||||
def test_select_grid_speakers_two_talkers_each_above_own_floor():
|
||||
from display_grid import select_grid_speakers
|
||||
floors = {"cam-07": -34.0, "cam-08": -36.0}
|
||||
assert select_grid_speakers(
|
||||
[("cam-07", -24.0), ("cam-08", -26.0)],
|
||||
min_db=-40.0, margin_db=6.0, floors=floors, rise_db=6.0,
|
||||
) == ["cam-07", "cam-08"]
|
||||
|
||||
|
||||
def test_select_quiet_mic_speech_not_cut_by_abs_min():
|
||||
"""Quiet C920 idle is ~-45; +3 dB speech is still below -40 dBFS."""
|
||||
from display_grid import select_grid_speakers
|
||||
floors = {"cam-10": -45.0, "cam-01": -37.0}
|
||||
assert select_grid_speakers(
|
||||
[("cam-10", -41.5), ("cam-01", -36.5)],
|
||||
min_db=-40.0, margin_db=6.0, floors=floors, rise_db=3.0,
|
||||
) == ["cam-10"]
|
||||
|
||||
|
||||
def test_update_noise_floor_tracks_idle_not_speech():
|
||||
from display_grid import update_noise_floor
|
||||
fl = update_noise_floor(-34.0, -33.5, speaking=False, rise_db=4.0)
|
||||
assert -34.0 < fl < -33.5
|
||||
stuck = update_noise_floor(-34.0, -20.0, speaking=True, rise_db=4.0)
|
||||
assert stuck == -34.0
|
||||
|
||||
|
||||
def test_confirm_speakers_needs_two_hops_new_talker():
|
||||
from display_grid import confirm_speakers
|
||||
first, counts = confirm_speakers({}, ["cam-07"], need=2)
|
||||
assert first == []
|
||||
second, counts = confirm_speakers(counts, ["cam-07"], need=2)
|
||||
assert second == ["cam-07"]
|
||||
# already on: one hop of continued speech keeps chrome
|
||||
stay, _ = confirm_speakers(counts, ["cam-07"], need=2, already=["cam-07"])
|
||||
assert stay == ["cam-07"]
|
||||
# a one-hop idle spike on another seat does not light
|
||||
spike, _ = confirm_speakers({}, ["cam-01"], need=2, already=["cam-07"])
|
||||
assert spike == []
|
||||
|
||||
|
||||
def test_waveform_similarity_high_for_delayed_copy():
|
||||
from display_grid import waveform_similarity
|
||||
t = np.linspace(0, 1, 1600, endpoint=False)
|
||||
a = np.sin(2 * np.pi * 220 * t).astype(np.float32)
|
||||
b = np.roll(a * 0.4, 80)
|
||||
assert waveform_similarity(a, b) > 0.85
|
||||
|
||||
|
||||
def test_waveform_similarity_low_for_independent_signals():
|
||||
from display_grid import waveform_similarity
|
||||
rng = np.random.default_rng(0)
|
||||
a = rng.normal(size=1600).astype(np.float32)
|
||||
b = rng.normal(size=1600).astype(np.float32)
|
||||
assert waveform_similarity(a, b) < 0.35
|
||||
|
||||
|
||||
def test_collapse_bleed_drops_similar_quieter_mic():
|
||||
from display_grid import collapse_bleed
|
||||
t = np.linspace(0, 1, 1600, endpoint=False)
|
||||
src = np.sin(2 * np.pi * 180 * t).astype(np.float32)
|
||||
waves = {"cam-07": src, "cam-08": np.roll(src * 0.3, 40)}
|
||||
levels = {"cam-07": -12.0, "cam-08": -18.0}
|
||||
assert collapse_bleed(["cam-07", "cam-08"], waves, levels, corr_min=0.4) == ["cam-07"]
|
||||
|
||||
|
||||
def test_collapse_bleed_keeps_two_unlike_talkers():
|
||||
from display_grid import collapse_bleed
|
||||
t = np.linspace(0, 1, 1600, endpoint=False)
|
||||
a = np.sin(2 * np.pi * 180 * t).astype(np.float32)
|
||||
b = np.sin(2 * np.pi * 440 * t + 1.3).astype(np.float32)
|
||||
waves = {"cam-07": a, "cam-08": b}
|
||||
levels = {"cam-07": -12.0, "cam-08": -13.0}
|
||||
assert collapse_bleed(["cam-07", "cam-08"], waves, levels, corr_min=0.6) == [
|
||||
"cam-07", "cam-08",
|
||||
]
|
||||
|
||||
|
||||
def test_hold_speakers_adds_second_talker_immediately():
|
||||
from display_grid import hold_speakers
|
||||
cur, until = hold_speakers(
|
||||
["cam-07"], ["cam-07", "cam-08"], prev_peak=-12.0, new_peak=-12.0,
|
||||
now=10.0, hold_until=10.5, hold_s=0.6, switch_db=3.0)
|
||||
assert cur == ["cam-07", "cam-08"]
|
||||
|
||||
|
||||
def test_hold_speakers_ignores_similar_bleed_flicker():
|
||||
from display_grid import hold_speakers
|
||||
cur, until = hold_speakers(
|
||||
["cam-07"], ["cam-08"], prev_peak=-12.0, new_peak=-12.5,
|
||||
now=10.2, hold_until=10.8, hold_s=0.6, switch_db=3.0)
|
||||
assert cur == ["cam-07"]
|
||||
cur, _ = hold_speakers(
|
||||
["cam-07"], ["cam-08"], prev_peak=-12.0, new_peak=-8.0,
|
||||
now=10.2, hold_until=10.8, hold_s=0.6, switch_db=3.0)
|
||||
assert cur == ["cam-08"]
|
||||
@@ -0,0 +1,24 @@
|
||||
import jwt
|
||||
|
||||
from tokens import make_token
|
||||
|
||||
|
||||
def test_make_token_includes_uwh_attributes():
|
||||
token = make_token(
|
||||
"ws://127.0.0.1:7880", "devkey", "secret", "cameras", "cam-01",
|
||||
attributes={"tag": "uwh", "tags": "uwh"},
|
||||
)
|
||||
claims = jwt.decode(token, "secret", algorithms=["HS256"])
|
||||
assert claims["sub"] == "cam-01"
|
||||
video = claims.get("video") or {}
|
||||
assert video.get("canPublish") is True
|
||||
assert video.get("canSubscribe") is False
|
||||
assert claims["attributes"]["tag"] == "uwh"
|
||||
assert claims["attributes"]["tags"] == "uwh"
|
||||
|
||||
|
||||
def test_make_token_omits_attributes_when_empty():
|
||||
token = make_token(
|
||||
"ws://127.0.0.1:7880", "devkey", "secret", "cameras", "cam-01")
|
||||
claims = jwt.decode(token, "secret", algorithms=["HS256"])
|
||||
assert "attributes" not in claims or not claims.get("attributes")
|
||||
@@ -0,0 +1,15 @@
|
||||
from usb_ids import (
|
||||
HUDDLY_VID,
|
||||
LOGITECH_VID,
|
||||
is_c920_pid,
|
||||
is_fleet_video,
|
||||
is_rally_pid,
|
||||
)
|
||||
|
||||
|
||||
def test_c920_is_fleet_huddly_and_rally_are_not():
|
||||
assert is_c920_pid(0x08E5)
|
||||
assert is_fleet_video(LOGITECH_VID, 0x08E5)
|
||||
assert not is_fleet_video(HUDDLY_VID, 0x0021)
|
||||
assert not is_fleet_video(LOGITECH_VID, 0x0881)
|
||||
assert is_rally_pid(0x0881)
|
||||
@@ -0,0 +1,16 @@
|
||||
from uvc_exposure import pin_cmd
|
||||
|
||||
|
||||
def test_pin_cmd_order_manual_then_time_then_dynfps():
|
||||
cmd = pin_cmd("/dev/cam07")
|
||||
assert cmd[:3] == ["v4l2-ctl", "-d", "/dev/cam07"]
|
||||
assert cmd[3] == "--set-ctrl=auto_exposure=1"
|
||||
assert cmd[4] == "--set-ctrl=exposure_time_absolute=333"
|
||||
assert cmd[5] == "--set-ctrl=exposure_dynamic_framerate=0"
|
||||
|
||||
|
||||
def test_pin_cmd_auto_skips_inactive_time():
|
||||
cmd = pin_cmd("/dev/cam07", auto_exposure=3, dynamic_framerate=1)
|
||||
assert "--set-ctrl=auto_exposure=3" in cmd
|
||||
assert "--set-ctrl=exposure_dynamic_framerate=1" in cmd
|
||||
assert not any(c.startswith("--set-ctrl=exposure_time_absolute=") for c in cmd)
|
||||
@@ -0,0 +1,38 @@
|
||||
from vaapi_pin import (
|
||||
classify_render_nodes,
|
||||
igpu_render,
|
||||
pin_encode_env,
|
||||
should_pin_vaapi,
|
||||
)
|
||||
|
||||
|
||||
def test_classify_by_pci_even_if_card_numbers_swap():
|
||||
def pci_id_of(node: str) -> str:
|
||||
return {
|
||||
"/dev/dri/renderD128": "0xe20b",
|
||||
"/dev/dri/renderD129": "0xa780",
|
||||
}[node]
|
||||
|
||||
c = classify_render_nodes(
|
||||
["/dev/dri/renderD128", "/dev/dri/renderD129"],
|
||||
pci_id_of=pci_id_of,
|
||||
)
|
||||
assert c["arc"] == "/dev/dri/renderD128"
|
||||
assert c["igpu"] == "/dev/dri/renderD129"
|
||||
|
||||
|
||||
def test_pin_encode_env_points_at_igpu_not_arc():
|
||||
env = pin_encode_env({}, igpu="/dev/dri/renderD129")
|
||||
assert env["LIBVA_DRIVER_NAME"] == "iHD"
|
||||
assert env["LIVEKIT_ENCODE_RENDER"] == "/dev/dri/renderD129"
|
||||
assert env.get("LIBVA_DRM_DEVICE") != "/dev/dri/renderD128"
|
||||
|
||||
|
||||
def test_should_not_pin_vaapi_when_preencoded():
|
||||
assert should_pin_vaapi(preencoded=True) is False
|
||||
assert should_pin_vaapi(preencoded=False) is True
|
||||
|
||||
|
||||
def test_igpu_render_matches_live_a780():
|
||||
node = igpu_render()
|
||||
assert node.startswith("/dev/dri/renderD")
|
||||
@@ -0,0 +1,69 @@
|
||||
"""VideoSource in-flight cap + VAAPI stale-frame drop (encoder behind)."""
|
||||
from latency import (
|
||||
should_drop_encoder_queue_depth,
|
||||
should_drop_stale_vaapi_frame,
|
||||
video_capture_gate_acquire,
|
||||
video_capture_gate_release,
|
||||
)
|
||||
|
||||
|
||||
def test_vaapi_keeps_fresh_delta_frame():
|
||||
# 30 fps, 33 ms old — still inside the 3-frame budget.
|
||||
assert should_drop_stale_vaapi_frame(
|
||||
now_us=1_000_000, capture_us=967_000, fps=30, max_frames_behind=3,
|
||||
is_keyframe=False,
|
||||
) is False
|
||||
|
||||
|
||||
def test_vaapi_drops_delta_older_than_three_frame_periods():
|
||||
# 30 fps × 3 = 100 ms. 150 ms on EncoderQueue → drop before ToI420.
|
||||
assert should_drop_stale_vaapi_frame(
|
||||
now_us=1_000_000, capture_us=850_000, fps=30, max_frames_behind=3,
|
||||
is_keyframe=False,
|
||||
) is True
|
||||
|
||||
|
||||
def test_vaapi_never_drops_keyframe():
|
||||
assert should_drop_stale_vaapi_frame(
|
||||
now_us=1_000_000, capture_us=1, fps=30, max_frames_behind=3,
|
||||
is_keyframe=True,
|
||||
) is False
|
||||
|
||||
|
||||
def test_vaapi_missing_timestamp_is_not_dropped():
|
||||
assert should_drop_stale_vaapi_frame(
|
||||
now_us=1_000_000, capture_us=0, fps=30, max_frames_behind=3,
|
||||
is_keyframe=False,
|
||||
) is False
|
||||
|
||||
|
||||
def test_gate_admits_until_max_in_flight():
|
||||
gate = {"in_flight": 0, "dropped": 0, "max_in_flight": 3}
|
||||
assert video_capture_gate_acquire(gate) is True
|
||||
assert video_capture_gate_acquire(gate) is True
|
||||
assert video_capture_gate_acquire(gate) is True
|
||||
assert gate["in_flight"] == 3
|
||||
assert video_capture_gate_acquire(gate) is False
|
||||
assert gate["dropped"] == 1
|
||||
assert gate["in_flight"] == 3
|
||||
|
||||
|
||||
def test_gate_release_allows_another_capture():
|
||||
gate = {"in_flight": 3, "dropped": 0, "max_in_flight": 3}
|
||||
video_capture_gate_release(gate)
|
||||
assert gate["in_flight"] == 2
|
||||
assert video_capture_gate_acquire(gate) is True
|
||||
assert gate["in_flight"] == 3
|
||||
|
||||
|
||||
def test_queue_depth_keeps_slot_when_empty():
|
||||
assert should_drop_encoder_queue_depth(queued=0, max_queued=1) is False
|
||||
|
||||
|
||||
def test_queue_depth_drops_when_at_max():
|
||||
# Latest-frame cap: one in-flight OnFrame/Encode, overwrite don't push.
|
||||
assert should_drop_encoder_queue_depth(queued=1, max_queued=1) is True
|
||||
|
||||
|
||||
def test_queue_depth_drops_when_over_max():
|
||||
assert should_drop_encoder_queue_depth(queued=3, max_queued=1) is True
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Publisher must not catch up frames and must recycle native encoders on RSS."""
|
||||
from latency import (
|
||||
next_video_capture,
|
||||
should_recycle_encoders,
|
||||
)
|
||||
|
||||
|
||||
def test_next_video_capture_sleeps_until_due():
|
||||
capture, due = next_video_capture(now=10.0, due=10.05, period=1 / 30)
|
||||
assert capture is False
|
||||
assert due == 10.05
|
||||
|
||||
|
||||
def test_next_video_capture_fires_on_due_and_advances_one_period():
|
||||
capture, due = next_video_capture(now=10.0, due=10.0, period=0.05)
|
||||
assert capture is True
|
||||
assert due == 10.05
|
||||
|
||||
|
||||
def test_next_video_capture_skips_catchup_when_behind():
|
||||
# 80 ms late at 30 fps would be two extra ticks if we kept adding period.
|
||||
capture, due = next_video_capture(now=10.08, due=10.0, period=1 / 30)
|
||||
assert capture is True
|
||||
assert due == 10.08 + (1 / 30)
|
||||
|
||||
|
||||
def test_should_recycle_when_anon_rss_exceeds_limit_and_interval_elapsed():
|
||||
assert should_recycle_encoders(
|
||||
rss_anon_kb=4_000_000, limit_kb=3_000_000,
|
||||
now=100.0, last_recycle_at=0.0, min_interval_s=60.0,
|
||||
) is True
|
||||
|
||||
|
||||
def test_should_not_recycle_below_limit():
|
||||
assert should_recycle_encoders(
|
||||
rss_anon_kb=1_000_000, limit_kb=3_000_000,
|
||||
now=100.0, last_recycle_at=0.0, min_interval_s=60.0,
|
||||
) is False
|
||||
|
||||
|
||||
def test_should_not_recycle_inside_min_interval():
|
||||
assert should_recycle_encoders(
|
||||
rss_anon_kb=5_000_000, limit_kb=3_000_000,
|
||||
now=100.0, last_recycle_at=80.0, min_interval_s=60.0,
|
||||
) is False
|
||||
|
||||
|
||||
def test_should_not_recycle_when_limit_disabled():
|
||||
assert should_recycle_encoders(
|
||||
rss_anon_kb=9_000_000, limit_kb=0,
|
||||
now=100.0, last_recycle_at=0.0, min_interval_s=60.0,
|
||||
) is False
|
||||
@@ -0,0 +1,25 @@
|
||||
from visca_xu_bridge import parse_visca, visca_frames
|
||||
|
||||
|
||||
def test_visca_split_frames():
|
||||
frames, rest = visca_frames(bytes([0x81, 0x01, 0x06, 0x04, 0xFF, 0x81]))
|
||||
assert frames == [bytes([0x81, 0x01, 0x06, 0x04, 0xFF])]
|
||||
assert rest == bytes([0x81])
|
||||
|
||||
|
||||
def test_visca_stop_and_move():
|
||||
stop = parse_visca(bytes([0x81, 0x01, 0x06, 0x01, 0x15, 0x15, 0x03, 0x03, 0xFF]))
|
||||
assert stop["action"] == "stop"
|
||||
left = parse_visca(bytes([0x81, 0x01, 0x06, 0x01, 0x08, 0x08, 0x01, 0x03, 0xFF]))
|
||||
assert left == {"action": "move", "pan": -1.0, "tilt": 0.0, "zoom": 0.0}
|
||||
up = parse_visca(bytes([0x81, 0x01, 0x06, 0x01, 0x08, 0x08, 0x03, 0x01, 0xFF]))
|
||||
assert up["tilt"] == 1.0
|
||||
|
||||
|
||||
def test_visca_zoom_home_preset():
|
||||
assert parse_visca(bytes([0x81, 0x01, 0x04, 0x07, 0x00, 0xFF]))["action"] == "zoom_stop"
|
||||
tele = parse_visca(bytes([0x81, 0x01, 0x04, 0x07, 0x27, 0xFF]))
|
||||
assert tele == {"action": "zoom", "zoom": 1.0}
|
||||
assert parse_visca(bytes([0x81, 0x01, 0x06, 0x04, 0xFF]))["action"] == "home"
|
||||
rec = parse_visca(bytes([0x81, 0x01, 0x04, 0x3F, 0x02, 0x01, 0xFF]))
|
||||
assert rec == {"action": "preset_recall", "index": 1}
|
||||
Reference in New Issue
Block a user