"""VIDEO-003: реальные H.264 кадры в модели очереди VIDEO-002 и нового PHY.""" from __future__ import annotations import argparse import json from pathlib import Path from protocol.video_stream_transport import FrameType, VideoFrame from tools.video002_simulation import load_real_stream, simulate from tools.video003_codec import BASELINE, SOURCE, encode_case, source_frames from tools.video003_design import ROOT, estimate DEFAULT_OUTPUT = ROOT / "data/processed/video003/transport_results.json" CODEC_RESULTS = ROOT / "data/processed/video003/codec_results.json" RESULT_160 = ROOT / "data/processed/video001/comparison_traffic_160k.json" SCENARIOS = tuple(("random", loss) for loss in (0, 1, 2, 5, 10)) BURST_SCENARIOS = tuple(("burst", length) for length in (2, 5, 10)) LINKS = ( ("CURRENT_BPSK_256_5MS", "BPSK", 500_000, "1/2", 256, 5.0), ("BPSK_500_R12_1012_0MS", "BPSK", 500_000, "1/2", 1012, 0.0), ("QPSK_250_R34_1012_0MS", "QPSK", 250_000, "3/4", 1012, 0.0), ("QPSK_500_R12_1012_0MS", "QPSK", 500_000, "1/2", 1012, 0.0), ("QPSK_500_R23_1012_0MS", "QPSK", 500_000, "2/3", 1012, 0.0), ("QPSK_500_R34_1012_0MS", "QPSK", 500_000, "3/4", 1012, 0.0), ("QPSK_500_R34_768_0P5MS", "QPSK", 500_000, "3/4", 768, 0.5), ) def _frames(mode: str, width: int, height: int, fps: int, window: dict, codec_results: dict, baseline_160: dict) -> tuple[list[VideoFrame], str]: if mode == "ROBUST": cases = [case for case in baseline_160["cases"] if case["window"] == window["name"] and case["resolution"] == [width, height] and case["fps"] == fps and case["codec"] == "H264_LIBX264"] if len(cases) != 1: raise ValueError("нет единственной исходной серии ROBUST") return load_real_stream(SOURCE, window, width, height, fps, 160_000, cases[0]["frame_sizes_bytes"]), "CURRENT_VIDEO001" pixels = source_frames(SOURCE, width, height, fps, window["start_seconds"], window["duration_seconds"]) frames: list[VideoFrame] = [] for profile, target in (("CURRENT", None), ("SHORT_GOP", 400_000)): case = [item for item in codec_results["cases"] if item["mode"] == mode and item["window"] == window["name"] and item["profile"] == profile and item["target_bps"] == target] if len(case) != 1: raise ValueError("неоднозначный кодековый прогон") result, packets = encode_case(pixels, fps, profile, target) if (result["frame_sizes_bytes"] != case[0]["frame_sizes_bytes"] or result["keyframe_positions"] != case[0]["keyframe_positions"]): raise ValueError("повтор кодирования отличается от сохранённого результата") keyframes = set(result["keyframe_positions"]) for index, packet in enumerate(packets): frames.append(VideoFrame( 0x56303033 if profile == "CURRENT" else 0x56303334, index, FrameType.KEYFRAME if index in keyframes else FrameType.DELTA_FRAME, round(index * 1000 / fps), packet)) return frames, "CURRENT_AND_SHORT_GOP_400K" def run() -> dict: codec = json.loads(CODEC_RESULTS.read_text(encoding="utf-8")) baseline_320 = json.loads(BASELINE.read_text(encoding="utf-8")) baseline_160 = json.loads(RESULT_160.read_text(encoding="utf-8")) if not (codec["source_sha256"] == baseline_320["source_sha256"] == baseline_160["source_sha256"]): raise ValueError("исходные ролики не совпадают") modes = (("ROBUST", 320, 240, 5), ("NORMAL", 640, 360, 10), ("HIGH_QUALITY", 854, 480, 10), ("720P5", 1280, 720, 5)) results = [] for mode, width, height, fps in modes: for window in baseline_320["windows"]: combined, label = _frames(mode, width, height, fps, window, codec, baseline_160) groups = [(label, combined)] if mode == "ROBUST" else [ ("CURRENT", [frame for frame in combined if frame.stream_id == 0x56303033]), ("SHORT_GOP_400K", [frame for frame in combined if frame.stream_id == 0x56303334]), ] for codec_profile, frames in groups: for link_name, modulation, symbol_rate, fec_rate, payload, gap_ms in LINKS: budget = estimate(modulation, symbol_rate, fec_rate, payload, gap_ms) link = (link_name, modulation, symbol_rate, payload, gap_ms) for scenario in SCENARIOS + BURST_SCENARIOS: outcome = simulate(frames, fps, link, scenario, airtime_override_ms=budget.packet_airtime_ms) outcome.update({ "mode": mode, "codec_profile": codec_profile, "window": window["name"], "resolution": [width, height], "fps": fps, "modulation": modulation, "symbol_rate": symbol_rate, "fec_rate": fec_rate, "phy_payload_bytes": payload, "gap_ms": gap_ms, "calculated_useful_rate_bps": budget.useful_video_rate_bps, }) results.append(outcome) print(f"{mode}/{window['name']}: {len(results)} записей", flush=True) return { "source_sha256": codec["source_sha256"], "random_loss_model": "independent Bernoulli per transmitted packet", "burst_loss_model": "separate deterministic runs of 2/5/10 packet erasures", "radio_model": "exact packet airtime from VIDEO-003 design table; full-packet conservative airtime", "receiver": "VIDEO-002 bounded queue, max age 300 ms, actual H.264 decoder", "cases": results, "rf_tx": 0, } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() result = run() args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(f"VIDEO-003: {len(result['cases'])} прогонов транспорта; {args.output}") if __name__ == "__main__": main()