import asyncio import queue import pytest from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription from k1link.viewer.node_media import MEDIA_PROTOCOL, NodeMediaPeers, admit_sdp SDP_HEADER = "v=0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n" @pytest.mark.parametrize( "address,kind", [("8.8.8.8", "host"), ("192.168.1.2", "relay"), ("::1", "host")] ) def test_media_rejects_non_private_or_relay_candidates(address, kind): with pytest.raises(ValueError): admit_sdp(SDP_HEADER + f"a=candidate:1 1 udp 1 {address} 12345 typ {kind}\r\n") def test_media_accepts_paired_lan_and_tailnet_candidates(): for address in ("192.168.1.2", "100.80.6.113", "peer.local"): admit_sdp(SDP_HEADER + f"a=candidate:1 1 udp 1 {address} 12345 typ host\r\n") def test_failed_media_offer_retires_peer_without_camera_channel(monkeypatch): async def reject(*_): raise ValueError("Invalid remote description") monkeypatch.setattr(RTCPeerConnection, "setRemoteDescription", reject) async def run(): class Camera: def snapshot(self): return {} peers = NodeMediaPeers(None, Camera()) try: with pytest.raises(ValueError): await peers.offer({"sdp": SDP_HEADER}) assert peers.items == {} finally: await peers.close_all() asyncio.run(run()) def test_native_webrtc_roundtrip_and_missing_camera_preserve_rrd(monkeypatch): """One bounded loopback peer, no STUN/TURN, device or external network.""" import aioice.ice class Subscription: def __init__(self): self.output = queue.Queue() self.output.put(payload) def read(self): try: return self.output.get(timeout=0.1) except queue.Empty: return b"" def close(self): pass class Hub: def subscribe(self): return Subscription() class Camera: def snapshot(self): return {"generation": None, "phase": "error"} # The old test's sub-16KB fake payload missed the actual fragmentation bug. import numpy as np import rerun as rr recording = rr.RecordingStream("synthetic-node-media") binary = recording.binary_stream() recording.log("points", rr.Points3D(np.random.default_rng(42).random((5000, 3)))) payload = binary.read() assert payload.startswith(b"RRF2") and len(payload) > 16384 async def run(): peers = NodeMediaPeers(Hub(), Camera()) monkeypatch.setattr(aioice.ice, "get_host_addresses", lambda **_: ["127.0.0.1"]) client = RTCPeerConnection(RTCConfiguration(iceServers=[])) channel = client.createDataChannel("rrd", ordered=True) client.createDataChannel("camera", ordered=True) received = asyncio.Event() payloads = [] @channel.on("message") def message(data): payloads.append(data) if len(payloads) > 1 and sum(map(len, payloads[1:])) == len(payload): received.set() try: await client.setLocalDescription(await client.createOffer()) answer = await peers.offer({"sdp": client.localDescription.sdp}) await client.setRemoteDescription( RTCSessionDescription(sdp=answer["sdp"], type="answer") ) await asyncio.wait_for(received.wait(), timeout=8) assert answer["media_protocol"] == MEDIA_PROTOCOL assert payloads[0] == b"MCF1" + len(payload).to_bytes(4, "big") assert b"".join(payloads[1:]) == payload assert all(len(part) <= 16384 for part in payloads[1:]) assert answer["peer_id"] in peers.items assert channel.readyState == "open" finally: await client.close() await peers.close_all() assert not peers.items asyncio.run(run()) def test_camera_waits_for_post_calibration_producer_and_delivers_metadata(): class Camera: calls = 0 opened = [] def snapshot(self): self.calls += 1 return {"generation": None} if self.calls < 2 else { "generation": 3, "delivery": {"media_type": "video/mp4"}} def open_delivery(self, generation): self.opened.append(generation) return "lease" class Channel: messages = [] def send(self, data): self.messages.append(data) async def run(): camera, channel = Camera(), Channel() peers = NodeMediaPeers(None, camera) peers.items["synthetic"] = {} assert await peers.camera_delivery("synthetic", channel) == "lease" assert camera.opened == [3] import json assert json.loads(channel.messages[0]) == {"type": "camera-ready", "mime": "video/mp4"} asyncio.run(run()) def test_installed_camera_uses_declared_os_ffmpeg(): from pathlib import Path root = Path(__file__).resolve().parents[1] unit = (root / "plugins/xgrids-k1/packaging/mission-core-k1.service").read_text() assert "Environment=MISSIONCORE_FFMPEG_BINARY=/usr/bin/ffmpeg" in unit assert "iproute2, ffmpeg" in (root / "plugins/xgrids-k1/packaging/build_deb.py").read_text()