Add packaged Insta360 X4 integration and recover paired Node channels
Discover independent camera instances and prepare their versioned runtime from Node or remote Core. Add isolated SDK workers, camera controls, raw dual-fisheye WebRTC preview, and shared action/region loading states. Recover existing Node bindings over known Tailscale addresses after a Core LAN address change. Preserve identities and trust, pin both peers, migrate endpoints with revision checks, and require real heartbeats for online status. Fix the Python client certificate profile for Go X509 verification. Pin Design Guideline 8c53f73 and retain installer/build/acceptance history. Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending. Validation: qualified DG/Node builds and Go race tests; 31 fleet tests; Python-to-Go certificate interoperability and live tailnet recovery with five fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance. Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Bounded image verification; packets alone never count as a working camera."""
|
||||
|
||||
import time
|
||||
|
||||
from .errors import failure
|
||||
from .operations import UNKNOWN
|
||||
|
||||
|
||||
class Control:
|
||||
def __init__(self, camera, feed=None):
|
||||
self.camera = camera
|
||||
self.feed = feed
|
||||
self.verified = False
|
||||
|
||||
def call(self, action, values):
|
||||
if action == "verify":
|
||||
return self.verify()
|
||||
if self.feed and action in ("preview.start", "preview.stop"):
|
||||
# The native adapter treats an already reached state as a no-op.
|
||||
# Preserve the same feed generation: clearing it would discard the
|
||||
# only initial keyframe even though the camera sends no new START.
|
||||
current = self.camera.call("details", {})
|
||||
target = 1 if action == "preview.start" else 0
|
||||
if (
|
||||
current.get("state") == "complete"
|
||||
and current.get("result", {}).get("connected") is True
|
||||
and current["result"].get("preview") == target
|
||||
):
|
||||
return self.camera.call(action, values)
|
||||
return self.feed.change(lambda: self.camera.call(action, values))
|
||||
return self.camera.call(action, values)
|
||||
|
||||
def verify(self, timeout=8):
|
||||
import av
|
||||
|
||||
self.verified = False
|
||||
result = self.camera.call("details", {})
|
||||
status = result.get("result", {})
|
||||
if (
|
||||
result.get("state") != "complete"
|
||||
or status.get("connected") is not True
|
||||
or status.get("recording") != 0
|
||||
or status.get("preview") not in (0, 1)
|
||||
):
|
||||
return failure("verification_recording_active")
|
||||
owned = status["preview"] == 0
|
||||
decoders, observed = {}, {}
|
||||
outcome = failure("preview_no_decodable_image")
|
||||
start = time.monotonic()
|
||||
read_video = self.feed.reader() if self.feed else self.camera.video
|
||||
try:
|
||||
if owned and self.call("preview.start", {}).get("state") != "complete":
|
||||
return dict(UNKNOWN)
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
value = read_video()
|
||||
if value is None:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
header, data = value
|
||||
index, codec, generation = (
|
||||
header["stream_index"],
|
||||
header["codec"],
|
||||
header["generation"],
|
||||
)
|
||||
name = "h264" if codec == 0 else "hevc" if codec == 1 else None
|
||||
if index not in (0, 1) or name is None:
|
||||
return dict(UNKNOWN)
|
||||
key = (index, codec, generation)
|
||||
if header.get("gap") or index not in decoders or decoders[index][0] != key:
|
||||
decoder = av.CodecContext.create(name, "r")
|
||||
decoder.thread_count = 1
|
||||
decoders[index] = (key, decoder)
|
||||
observed.pop(index, None)
|
||||
decoder = decoders[index][1]
|
||||
try:
|
||||
for packet in decoder.parse(data):
|
||||
for frame in decoder.decode(packet):
|
||||
if not (0 < frame.width <= 4096 and 0 < frame.height <= 2160):
|
||||
return dict(UNKNOWN)
|
||||
current = observed.setdefault(
|
||||
index,
|
||||
{
|
||||
"stream_index": index,
|
||||
"codec": name,
|
||||
"width": frame.width,
|
||||
"height": frame.height,
|
||||
"frames": 0,
|
||||
},
|
||||
)
|
||||
current["frames"] += 1
|
||||
except av.error.FFmpegError:
|
||||
decoders.pop(index, None)
|
||||
observed.pop(index, None)
|
||||
continue
|
||||
if any(item["frames"] >= 2 for item in observed.values()):
|
||||
outcome = {
|
||||
"state": "complete",
|
||||
"result": {
|
||||
"verified": True,
|
||||
"streams": list(observed.values()),
|
||||
"duration_ms": round((time.monotonic() - start) * 1000),
|
||||
},
|
||||
}
|
||||
break
|
||||
finally:
|
||||
if owned:
|
||||
try:
|
||||
if self.call("preview.stop", {}).get("state") != "complete":
|
||||
outcome = dict(UNKNOWN)
|
||||
except (RuntimeError, ValueError, OSError):
|
||||
outcome = dict(UNKNOWN)
|
||||
self.verified = outcome["state"] == "complete"
|
||||
return outcome
|
||||
Reference in New Issue
Block a user