Add D455 sensor host and shared Node/Core preparation surface
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user