from __future__ import annotations import hashlib import json import sys import time from io import BytesIO from pathlib import Path import pytest import k1link.web.xgrids_k1_camera as camera_module from k1link.web.xgrids_k1_camera import ( CAMERA_MEDIA_TYPE, XgridsK1CameraGateway, _build_ffmpeg_argv, _read_mp4_box, ) from k1link.web.xgrids_k1_facade import ( XGRIDS_K1_COMPATIBILITY_PROFILE_ID, XGRIDS_K1_PLUGIN_ID, CameraPreviewSelectRequest, CameraPreviewStopRequest, XgridsK1CompatibilityService, ) def _fake_ffmpeg(tmp_path: Path) -> Path: executable = tmp_path / "fake-ffmpeg" executable.write_text( f"#!{sys.executable}\n" "import sys, time\n" "def box(kind, payload=b''):\n" " return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n" "payload = (box(b'ftyp', b'isom') + box(b'moov') + " "box(b'moof') + box(b'mdat', b'frame'))\n" "sys.stdout.buffer.write(payload)\n" "sys.stdout.buffer.flush()\n" "time.sleep(10)\n", encoding="utf-8", ) executable.chmod(0o700) return executable def _burst_ffmpeg(tmp_path: Path) -> Path: executable = tmp_path / "burst-ffmpeg" executable.write_text( f"#!{sys.executable}\n" "import sys, time\n" "def box(kind, payload=b''):\n" " return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n" "sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n" "sys.stdout.buffer.flush()\n" "time.sleep(0.2)\n" "for index in range(10):\n" " sys.stdout.buffer.write(box(b'moof') + box(b'mdat', bytes([index])))\n" " sys.stdout.buffer.flush()\n" " time.sleep(0.01)\n" "time.sleep(10)\n", encoding="utf-8", ) executable.chmod(0o700) return executable def _buffered_tail_ffmpeg(tmp_path: Path, sentinel: Path, *, fragments: int) -> Path: executable = tmp_path / "buffered-tail-ffmpeg" executable.write_text( f"#!{sys.executable}\n" "import pathlib, sys, time\n" "def box(kind, payload=b''):\n" " return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n" "sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov'))\n" f"for index in range({fragments}):\n" " sys.stdout.buffer.write(box(b'moof') + box(b'mdat', index.to_bytes(2, 'big')))\n" "sys.stdout.buffer.flush()\n" f"pathlib.Path({str(sentinel)!r}).write_text('ready')\n" "time.sleep(10)\n", encoding="utf-8", ) executable.chmod(0o700) return executable def _incomplete_tail_ffmpeg(tmp_path: Path, sentinel: Path) -> Path: executable = tmp_path / "incomplete-tail-ffmpeg" executable.write_text( f"#!{sys.executable}\n" "import pathlib, sys, time\n" "def box(kind, payload=b''):\n" " return (8 + len(payload)).to_bytes(4, 'big') + kind + payload\n" "sys.stdout.buffer.write(box(b'ftyp', b'isom') + box(b'moov') + box(b'moof'))\n" "sys.stdout.buffer.flush()\n" f"pathlib.Path({str(sentinel)!r}).write_text('ready')\n" "time.sleep(10)\n", encoding="utf-8", ) executable.chmod(0o700) return executable def _gateway( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> XgridsK1CameraGateway: monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path))) return XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID) def _wait_until(predicate: object, *, timeout: float = 3.0) -> None: assert callable(predicate) deadline = time.monotonic() + timeout while time.monotonic() < deadline: if predicate(): return time.sleep(0.01) raise AssertionError("condition was not reached before timeout") def test_camera_selection_is_exclusive_and_hides_device_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = _gateway(tmp_path, monkeypatch) try: left = gateway.select("sensor.camera.left", "192.168.8.52") assert left["active_source_id"] == "sensor.camera.left" assert left["generation"] == 1 assert left["activation"]["max_active"] == 1 assert left["delivery"]["kind"] == "mse-fmp4-websocket" assert left["delivery"]["media_type"] == CAMERA_MEDIA_TYPE serialized = json.dumps(left) assert "192.168.8.52" not in serialized assert "rtsp://" not in serialized assert "chn_left_main" not in serialized right = gateway.select("sensor.camera.right", "192.168.8.52") assert right["active_source_id"] == "sensor.camera.right" assert right["generation"] == 2 assert right["delivery"]["url"].endswith("/camera-preview/2") with pytest.raises(ValueError, match="generation"): gateway.stop(1) assert gateway.snapshot()["active_source_id"] == "sensor.camera.right" stopped = gateway.stop(2) assert stopped["phase"] == "idle" assert stopped["delivery"] is None finally: gateway.close() def test_camera_ffmpeg_command_is_allowlisted_copy_remux() -> None: argv = _build_ffmpeg_argv( Path("/trusted/ffmpeg"), "10.0.0.24", "sensor.camera.left", ) assert argv[0] == "/trusted/ffmpeg" assert argv[argv.index("-i") + 1] == ("rtsp://10.0.0.24:8554/live/chn_left_main") assert argv[argv.index("-c:v") + 1] == "copy" assert argv[argv.index("-allowed_media_types") + 1] == "video" assert argv[argv.index("-flush_packets") + 1] == "1" assert "-c:v" in argv assert ";" not in " ".join(argv) with pytest.raises(ValueError, match="private IPv4"): _build_ffmpeg_argv( Path("/trusted/ffmpeg"), "example.com", "sensor.camera.left", ) def test_iso_bmff_reader_preserves_complete_boxes() -> None: def box(kind: bytes, payload: bytes = b"") -> bytes: return (8 + len(payload)).to_bytes(4, "big") + kind + payload stream = BytesIO(box(b"ftyp", b"isom") + box(b"moov") + box(b"moof")) assert _read_mp4_box(stream) == (b"ftyp", box(b"ftyp", b"isom")) assert _read_mp4_box(stream) == (b"moov", box(b"moov")) assert _read_mp4_box(stream) == (b"moof", box(b"moof")) oversized = (9 * 1024 * 1024).to_bytes(4, "big") + b"mdat" with pytest.raises(ValueError, match="unbounded"): _read_mp4_box(BytesIO(oversized)) def test_gateway_emits_init_and_complete_media_segments_without_transcoding( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = _gateway(tmp_path, monkeypatch) try: state = gateway.select("sensor.camera.right", "192.168.1.20") lease = gateway.open_delivery(state["generation"]) init_segment = lease.segments.get(timeout=3) media_segment = lease.segments.get(timeout=3) assert init_segment is not None and init_segment[0] == "init" assert b"ftyp" in init_segment[1] and b"moov" in init_segment[1] assert media_segment is not None and media_segment[0] == "media" assert b"moof" in media_segment[1] and b"mdat" in media_segment[1] gateway.mark_streaming(lease) assert gateway.snapshot()["phase"] == "streaming" gateway.release_delivery(lease, client_closed=True) assert gateway.snapshot()["phase"] == "selected" finally: gateway.close() def test_acquisition_records_without_browser_and_source_switch_seals_epochs( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = _gateway(tmp_path, monkeypatch) session = tmp_path / "sessions" / "camera-acquisition" try: left = gateway.select("sensor.camera.left", "192.168.1.20") with pytest.raises(ValueError, match="does not exist"): gateway.start_recording(session) assert session.exists() is False session.mkdir(parents=True) gateway.start_recording(session) left_epoch = session / "media" / "sensor.camera.left" / f"epoch-{left['generation']}" # No open_delivery/WebSocket exists: acquisition ownership alone starts # FFmpeg and commits the init segment before any preview consumer. _wait_until( lambda: ( (left_epoch / "init.mp4").is_file() and len(list((left_epoch / "segments").glob("*.m4s"))) == 1 ) ) _wait_until(lambda: gateway.snapshot()["phase"] == "streaming") right = gateway.select("sensor.camera.right", "192.168.1.20") right_epoch = session / "media" / "sensor.camera.right" / f"epoch-{right['generation']}" _wait_until(lambda: gateway.snapshot()["phase"] == "streaming") _wait_until(lambda: len(list((right_epoch / "segments").glob("*.m4s"))) == 1) lease = gateway.open_delivery(right["generation"]) init_segment = lease.segments.get(timeout=1) assert init_segment is not None and init_segment[0] == "init" gateway.release_delivery(lease, client_closed=True) # Browser disposal is not producer disposal while recording is active. assert lease.process.poll() is None assert gateway.snapshot()["recording"]["active"] is True assert gateway.snapshot()["phase"] == "streaming" stopped = gateway.stop_recording(status="complete") assert stopped["recording"]["active"] is False assert stopped["recording"]["completed_epochs"] == 2 summaries = [] for epoch in (left_epoch, right_epoch): init = (epoch / "init.mp4").read_bytes() entries = [ json.loads(line) for line in (epoch / "index.jsonl").read_text(encoding="utf-8").splitlines() ] summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8")) summaries.append(summary) assert [entry["kind"] for entry in entries] == ["media"] assert all( entry["length"] == len((epoch / entry["path"]).read_bytes()) and hashlib.sha256((epoch / entry["path"]).read_bytes()).hexdigest() == entry["sha256"] for entry in entries ) assert summary["status"] == "complete" assert summary["segment_count"] == 1 assert summary["entry_count"] == 1 assert summary["valid_bytes"] == len(init) + sum( entry["length"] for entry in entries ) assert summaries[0]["failure_code"] == "source-switch" assert summaries[1]["failure_code"] is None serialized = json.dumps(stopped) assert "192.168.1.20" not in serialized assert "rtsp://" not in serialized finally: gateway.close() def test_camera_storage_open_failure_is_loud_and_never_starts_ffmpeg( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: gateway = _gateway(tmp_path, monkeypatch) session = tmp_path / "session" session.mkdir() popen_called = False class FailingArchive: def __init__(self, *_: object, **__: object) -> None: raise camera_module.CameraArchiveError("synthetic storage failure") def forbidden_popen(*_: object, **__: object) -> object: nonlocal popen_called popen_called = True raise AssertionError("FFmpeg must not start without durable storage") monkeypatch.setattr(camera_module, "CameraArchiveWriter", FailingArchive) monkeypatch.setattr(camera_module.subprocess, "Popen", forbidden_popen) try: gateway.select("sensor.camera.left", "192.168.1.20") with pytest.raises(RuntimeError, match="хранилище"): gateway.start_recording(session) state = gateway.snapshot() assert popen_called is False assert state["phase"] == "error" assert state["error"]["code"] == "camera-storage-failed" assert "192.168.1.20" not in json.dumps(state) finally: gateway.close() def test_camera_storage_append_failure_fails_producer_and_epoch_loudly( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: original_append = camera_module.CameraArchiveWriter.append def failing_append( writer: object, kind: object, payload: bytes, **timestamps: object, ) -> dict[str, object]: if kind == "media": raise camera_module.CameraArchiveError("synthetic media commit failure") return original_append(writer, kind, payload, **timestamps) # type: ignore[arg-type] monkeypatch.setattr(camera_module.CameraArchiveWriter, "append", failing_append) gateway = _gateway(tmp_path, monkeypatch) session = tmp_path / "session" session.mkdir() try: selected = gateway.select("sensor.camera.left", "192.168.1.20") gateway.start_recording(session) _wait_until(lambda: gateway.snapshot()["phase"] == "error") state = gateway.snapshot() assert state["error"]["code"] == "camera-storage-failed" summary_path = ( session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}" / "summary.json" ) _wait_until( lambda: ( summary_path.is_file() and json.loads(summary_path.read_text(encoding="utf-8"))["status"] == "failed" ) ) summary = json.loads(summary_path.read_text(encoding="utf-8")) assert summary["status"] == "failed" assert summary["failure_code"] == "camera-storage-failed" assert "192.168.1.20" not in json.dumps(state) finally: gateway.close() def test_slow_browser_is_dropped_without_stopping_archive_producer( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_burst_ffmpeg(tmp_path))) gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID) session = tmp_path / "session" session.mkdir() try: selected = gateway.select("sensor.camera.left", "192.168.1.20") gateway.start_recording(session) lease = gateway.open_delivery(selected["generation"]) # Deliberately never drain the bounded queue. _wait_until(lambda: lease.failure_code == "consumer-too-slow") _wait_until(lambda: gateway.snapshot()["phase"] == "streaming") assert lease.process.poll() is None assert gateway.snapshot()["recording"]["active"] is True gateway.release_delivery(lease, client_closed=False) epoch = ( session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}" ) _wait_until( lambda: len(list((epoch / "segments").glob("*.m4s"))) == 10, ) gateway.stop_recording(status="complete") summary_path = epoch / "summary.json" summary = json.loads(summary_path.read_text(encoding="utf-8")) assert summary["status"] == "complete" assert summary["segment_count"] == 10 assert summary["media_segment_count"] == 10 finally: gateway.close() def test_clean_stop_drains_ffmpeg_stdout_before_sealing_archive( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: fragment_count = 100 sentinel = tmp_path / "ffmpeg-wrote-buffered-tail" monkeypatch.setenv( "MISSIONCORE_FFMPEG_BINARY", str(_buffered_tail_ffmpeg(tmp_path, sentinel, fragments=fragment_count)), ) gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID) session = tmp_path / "session" session.mkdir() try: selected = gateway.select("sensor.camera.left", "192.168.1.20") gateway.start_recording(session) _wait_until(sentinel.is_file) gateway.stop_recording(status="complete") epoch = ( session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}" ) summary = json.loads((epoch / "summary.json").read_text(encoding="utf-8")) assert summary["status"] == "complete" assert summary["segment_count"] == fragment_count assert len(list((epoch / "segments").glob("*.m4s"))) == fragment_count assert len((epoch / "index.jsonl").read_text(encoding="utf-8").splitlines()) == ( fragment_count ) finally: gateway.close() def test_clean_stop_marks_incomplete_fragment_tail_interrupted( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: sentinel = tmp_path / "ffmpeg-wrote-incomplete-tail" monkeypatch.setenv( "MISSIONCORE_FFMPEG_BINARY", str(_incomplete_tail_ffmpeg(tmp_path, sentinel)), ) gateway = XgridsK1CameraGateway(tmp_path, XGRIDS_K1_PLUGIN_ID) session = tmp_path / "session" session.mkdir() try: selected = gateway.select("sensor.camera.left", "192.168.1.20") gateway.start_recording(session) _wait_until(sentinel.is_file) gateway.stop_recording(status="complete") summary_path = ( session / "media" / "sensor.camera.left" / f"epoch-{selected['generation']}" / "summary.json" ) summary = json.loads(summary_path.read_text(encoding="utf-8")) assert summary["status"] == "interrupted" assert summary["failure_code"] == "incomplete-fmp4-fragment" assert summary["segment_count"] == 0 state = gateway.snapshot() assert state["phase"] == "error" assert state["error"]["code"] == "incomplete-fmp4-fragment" finally: gateway.close() def test_service_publishes_two_dynamic_camera_rows_and_stale_stop_is_safe( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("MISSIONCORE_FFMPEG_BINARY", str(_fake_ffmpeg(tmp_path))) service = XgridsK1CompatibilityService(tmp_path) try: with service._lock: service._k1_ip = "192.168.1.20" service._device_id = "device-k1-test" service._device_session_id = "device-session-test" service._device_session_opened_at = "2026-07-16T20:00:00Z" service._compatibility_attestation = { "firmware_version": "3.0.2", "topology": "direct-lan", "basis": "operator-attested", "observed_at": "2026-07-16T20:00:00Z", } state = service.select_camera_preview( CameraPreviewSelectRequest( source_id="sensor.camera.left", device_session_id="device-session-test", ) ) cameras = [ stream for stream in state["sensor_catalog"]["streams"] if stream.get("semantic_channel_id") == "camera.preview.live" ] assert len(cameras) == 2 assert sum(bool(stream["activation"]["selected"]) for stream in cameras) == 1 assert sum(stream["delivery"] is not None for stream in cameras) == 1 assert state["compatibility"]["profile_id"] == XGRIDS_K1_COMPATIBILITY_PROFILE_ID camera_contract = json.dumps( {"camera_preview": state["camera_preview"], "streams": cameras} ) assert "rtsp://" not in camera_contract assert "192.168.1.20" not in camera_contract generation = state["camera_preview"]["generation"] stopped = service.stop_camera_preview( CameraPreviewStopRequest( device_session_id="device-session-test", generation=generation, ) ) assert stopped["camera_preview"]["phase"] == "idle" with pytest.raises(ValueError, match="device-сессия"): service.select_camera_preview( CameraPreviewSelectRequest( source_id="sensor.camera.right", device_session_id="stale-session", ) ) finally: service.close()