"""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