"""Paired signalling, private ICE candidates and bounded live data channels.""" import asyncio import ipaddress import queue import time from contextlib import suppress from uuid import uuid4 import aioice.ice from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription PRIVATE_NETWORKS = tuple( ipaddress.ip_network(v) for v in ("127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10") ) def private(address): try: value = ipaddress.ip_address(address) return value.version == 4 and any(value in network for network in PRIVATE_NETWORKS) except ValueError: return False def admit_sdp(sdp): if not isinstance(sdp, str) or not 1 <= len(sdp) <= 32768: raise ValueError("Invalid media invitation") media = [line for line in sdp.splitlines() if line.startswith("m=")] if not sdp.startswith("v=0") or len(media) != 1 or not media[0].startswith("m=application "): raise ValueError("A single data-channel media section is required") for line in sdp.splitlines(): if line.startswith("a=candidate:"): parts = line.split() if ( len(parts) < 8 or parts[7] != "host" or not (private(parts[4]) or parts[4].endswith(".local")) ): raise ValueError("Only private host ICE candidates are admitted") class NodeMediaPeers: def __init__(self, hub, camera): self.hub, self.camera, self.items = hub, camera, {} # Process-local adapter in this dedicated worker; no public/STUN/TURN ICE. original = aioice.ice.get_host_addresses aioice.ice.get_host_addresses = lambda use_ipv4, use_ipv6: [ value for value in original(use_ipv4=True, use_ipv6=False) if private(value) ] async def offer(self, parameters): admit_sdp(parameters.get("sdp")) if len(self.items) >= 2: raise ValueError("Close another live viewer") pc = RTCPeerConnection(RTCConfiguration(iceServers=[])) identifier = "peer_" + uuid4().hex entry = {"pc": pc, "seen": time.monotonic(), "tasks": [], "labels": set()} self.items[identifier] = entry @pc.on("datachannel") def datachannel(channel): if channel.label not in {"rrd", "camera"} or channel.label in entry["labels"]: channel.close() return entry["labels"].add(channel.label) @channel.on("message") def message(value): if value == "keepalive": entry["seen"] = time.monotonic() entry["tasks"].append(asyncio.create_task(self.deliver(identifier, channel))) @pc.on("connectionstatechange") async def changed(): if pc.connectionState in {"closed", "failed"}: await self.close(identifier) try: await pc.setRemoteDescription( RTCSessionDescription(sdp=parameters["sdp"], type="offer") ) await pc.setLocalDescription(await pc.createAnswer()) async def expiry(): await asyncio.sleep(25) if pc.connectionState != "connected": await self.close(identifier) entry["tasks"].append(asyncio.create_task(expiry())) snapshot = self.camera.snapshot() return { "peer_id": identifier, "sdp": pc.localDescription.sdp, "type": "answer", "camera_mime": (snapshot.get("delivery") or {}).get("media_type"), "profile": "live-acquisition", "transport": "webrtc-rrd-fmp4", } except BaseException: await self.close(identifier) raise async def send(self, channel, payload): if len(payload) > 8 * 1024 * 1024: raise RuntimeError("Preview fragment exceeds bound") for offset in range(0, len(payload), 16384): deadline = time.monotonic() + 2 while channel.readyState != "open" or channel.bufferedAmount > 1024 * 1024: if channel.readyState in {"closed", "closing"} or time.monotonic() > deadline: raise RuntimeError("Preview consumer unavailable") await asyncio.sleep(0.01) channel.send(payload[offset : offset + 16384]) await asyncio.sleep(0) async def deliver(self, identifier, channel): subscriber = lease = None try: entry = self.items[identifier] if channel.label == "rrd": subscriber = await asyncio.to_thread(self.hub.subscribe) else: generation = self.camera.snapshot().get("generation") if generation is None: channel.close() return lease = await asyncio.to_thread(self.camera.open_delivery, generation) while identifier in self.items and time.monotonic() - entry["seen"] < 30: if subscriber: payload = await asyncio.to_thread(subscriber.read) else: try: segment = await asyncio.to_thread(lease.segments.get, 0.5) except queue.Empty: continue if segment is None: break kind, payload = segment if kind == "media": self.camera.mark_streaming(lease) if payload is None: break if payload: await self.send(channel, payload) except (Exception, asyncio.CancelledError): pass finally: if subscriber: subscriber.close() if lease: self.camera.release_delivery(lease, client_closed=True) if channel.label == "camera": channel.close() else: await self.close(identifier) async def close(self, identifier): entry = self.items.pop(identifier, None) if entry: for task in entry["tasks"]: if task is not asyncio.current_task(): task.cancel() with suppress(Exception): await entry["pc"].close() async def close_all(self): for identifier in list(self.items): await self.close(identifier)