118 lines
6.3 KiB
Python
118 lines
6.3 KiB
Python
"""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()
|