feat(k1): stabilize LAB bridge and isolate onboard device integration
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user