"""Private WebRTC preview. No capture ownership, relay, STUN or public candidates.""" import asyncio import ipaddress import json import time import uuid import aioice.ice from aiortc import RTCConfiguration, RTCPeerConnection, RTCSessionDescription, VideoStreamTrack from av import VideoFrame def private(address): try: value = ipaddress.ip_address(address) return value.version == 4 and ( value.is_loopback or any( value in ipaddress.ip_network(n) for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10") ) ) except ValueError: return False host_addresses = aioice.ice.get_host_addresses aioice.ice.get_host_addresses = lambda use_ipv4, use_ipv6: [ v for v in host_addresses(use_ipv4=True, use_ipv6=False) if private(v) ] class CameraTrack(VideoStreamTrack): def __init__(self, device, layer): super().__init__() self.device, self.layer = device, layer async def recv(self): pts, base = await self.next_timestamp() # Bound preview to 15 Hz; hardware profiles and raw recording are independent. await asyncio.sleep(1 / 30) while self.layer not in self.device.images: await asyncio.sleep(0.1) frame = VideoFrame.from_ndarray(self.device.images[self.layer], format="rgb24") frame.pts, frame.time_base = pts, base return frame class Peers: def __init__(self): self.items = {} async def offer(self, device, params): layer = params.get("layer", "color") if layer not in ("color", "depth", "infrared1", "infrared2", "points", "motion"): raise ValueError("Неизвестный слой камеры.") if len(self.items) >= 4: raise ValueError("Закройте лишние окна просмотра камеры.") sdp = params.get("sdp", "") if not isinstance(sdp, str) or len(sdp) > 32768: raise ValueError("Некорректное приглашение просмотра.") for line in sdp.splitlines(): if line.startswith("a=candidate:"): fields = line.split() if len(fields) < 8 or (not private(fields[4]) and not fields[4].endswith(".local")): raise ValueError("Просмотр доступен только в частной сети.") pc = RTCPeerConnection(RTCConfiguration(iceServers=[])) ident = "peer_" + uuid.uuid4().hex self.items[ident] = {"pc": pc, "seen": time.monotonic()} async def telemetry(channel): try: while pc.connectionState not in ("failed", "closed"): if time.monotonic() - self.items.get(ident, {}).get("seen", 0) > 30: break if channel.readyState == "open" and channel.bufferedAmount < 65536: payload = { "layer": layer, "motion": device.motion, "frame": device.last_frame, "acquisition": device.acquisition, } if layer == "points": payload["points"] = await asyncio.to_thread(device.points) channel.send(json.dumps(payload, allow_nan=False)) await asyncio.sleep(0.25) finally: await self.close(ident) @pc.on("datachannel") def datachannel(channel): @channel.on("message") def message(value): if value == "keepalive" and ident in self.items: self.items[ident]["seen"] = time.monotonic() asyncio.create_task(telemetry(channel)) @pc.on("connectionstatechange") async def changed(): if pc.connectionState in ("failed", "closed"): self.items.pop(ident, None) try: await pc.setRemoteDescription(RTCSessionDescription(sdp=sdp, type="offer")) if layer not in ("points", "motion"): pc.addTrack(CameraTrack(device, layer)) await pc.setLocalDescription(await pc.createAnswer()) async def expiry(): await asyncio.sleep(30) if ident in self.items and pc.connectionState != "connected": await self.close(ident) asyncio.create_task(expiry()) return {"peer_id": ident, "sdp": pc.localDescription.sdp, "type": "answer"} except Exception: await self.close(ident) raise async def close(self, ident): entry = self.items.pop(ident, None) if entry: await entry["pc"].close() return {"ok": True}