""" Lab028. Packetization of synchronous BASE + ROI JPEG video objects. The laboratory forms the selected Lab027E profile directly from the source video, keeps every JPEG and packet in memory, verifies the transport layer, and writes only aggregate CSV/report/plot artifacts. """ from __future__ import annotations import csv from dataclasses import dataclass from pathlib import Path import random from typing import Callable import cv2 import matplotlib import numpy as np matplotlib.use("Agg") import matplotlib.pyplot as plt from protocol.video_packet import ( CompositeFrame, CompositeReassembler, HEADER_FORMAT, HEADER_SIZE, ObjectCRCError, ObjectType, PacketCRCError, VideoPacket, decode_packet, encode_packet, packetize_jpeg, ) SOURCE_VIDEO_PATH = Path("data/raw/lab026_rover_source.mp4") OUTPUT_DIRECTORY = Path("data/processed/lab028") CSV_PATH = OUTPUT_DIRECTORY / "lab028_packet_payload_results.csv" REPORT_PATH = OUTPUT_DIRECTORY / "lab028_report.txt" OVERHEAD_PLOT_PATH = ( OUTPUT_DIRECTORY / "lab028_overhead_efficiency.png" ) TRAFFIC_PLOT_PATH = ( OUTPUT_DIRECTORY / "lab028_packets_wire_bitrate.png" ) COMPOSITE_FPS = 3.0 BASE_WIDTH = 240 BASE_HEIGHT = 135 BASE_QUALITY = 23 ROI_WIDTH = 320 ROI_HEIGHT = 180 ROI_QUALITY = 33 ROI_X_MIN = 0.20 ROI_X_MAX = 0.80 ROI_Y_MIN = 0.42 ROI_Y_MAX = 1.00 PAYLOAD_LENGTHS = (64, 128, 256, 512, 1024) FRAME_TIME_EPSILON_SECONDS = 1e-9 CSV_FIELDS = [ "max_payload_bytes", "composite_frames", "mean_base_packets_per_frame", "mean_roi_packets_per_frame", "mean_total_packets_per_frame", "max_total_packets_per_frame", "packets_per_second", "jpeg_payload_bytes", "jpeg_payload_bitrate_kbps", "header_bytes_per_second", "header_bitrate_kbps", "wire_bytes", "wire_bitrate_kbps", "service_data_percent", "efficiency_percent", "mean_wire_packet_bytes", "p95_wire_packet_bytes", "max_wire_packet_bytes", ] @dataclass(frozen=True) class VideoMetadata: width: int height: int fps: float frame_count: int duration_seconds: float @dataclass(frozen=True) class EncodedComposite: composite_frame_id: int source_frame_index: int base_jpeg: bytes roi_jpeg: bytes @dataclass(frozen=True) class PayloadMetrics: max_payload_bytes: int composite_frames: int mean_base_packets_per_frame: float mean_roi_packets_per_frame: float mean_total_packets_per_frame: float max_total_packets_per_frame: int packets_per_second: float jpeg_payload_bytes: int jpeg_payload_bitrate_kbps: float header_bytes_per_second: float header_bitrate_kbps: float wire_bytes: int wire_bitrate_kbps: float service_data_percent: float efficiency_percent: float mean_wire_packet_bytes: float p95_wire_packet_bytes: float max_wire_packet_bytes: int @dataclass(frozen=True) class TestResult: name: str passed: bool detail: str def normalized_roi_to_pixels( width: int, height: int, ) -> tuple[int, int, int, int]: """Use the normalized ROI coordinates from Lab027 through Lab027E.""" coordinates = ( int(round(width * ROI_X_MIN)), int(round(height * ROI_Y_MIN)), int(round(width * ROI_X_MAX)), int(round(height * ROI_Y_MAX)), ) x_min, y_min, x_max, y_max = coordinates if not ( 0 <= x_min < x_max <= width and 0 <= y_min < y_max <= height ): raise RuntimeError("calculated ROI is outside the source frame") return coordinates def encode_grayscale_jpeg(image: np.ndarray, quality: int) -> bytes: """Encode one grayscale image to an in-memory JPEG.""" encoded, buffer = cv2.imencode( ".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), int(quality)], ) if not encoded: raise RuntimeError("OpenCV could not encode JPEG") jpeg = buffer.tobytes() if not jpeg: raise RuntimeError("OpenCV produced an empty JPEG") return jpeg def encode_composite( source_frame: np.ndarray, source_roi: tuple[int, int, int, int], composite_frame_id: int, source_frame_index: int, ) -> EncodedComposite: """Form synchronous BASE and ROI JPEGs from exactly one source frame.""" grayscale = cv2.cvtColor(source_frame, cv2.COLOR_BGR2GRAY) base = cv2.resize( grayscale, (BASE_WIDTH, BASE_HEIGHT), interpolation=cv2.INTER_AREA, ) x_min, y_min, x_max, y_max = source_roi roi = grayscale[y_min:y_max, x_min:x_max] if roi.size == 0: raise RuntimeError("source ROI is empty") roi = cv2.resize( roi, (ROI_WIDTH, ROI_HEIGHT), interpolation=cv2.INTER_AREA, ) return EncodedComposite( composite_frame_id=composite_frame_id, source_frame_index=source_frame_index, base_jpeg=encode_grayscale_jpeg(base, BASE_QUALITY), roi_jpeg=encode_grayscale_jpeg(roi, ROI_QUALITY), ) def load_video_profile( source_path: Path, ) -> tuple[VideoMetadata, list[EncodedComposite]]: """Read the video sequentially and select synchronous updates at 3 fps.""" if not source_path.exists(): raise FileNotFoundError(f"source video is missing: {source_path}") capture = cv2.VideoCapture(str(source_path)) if not capture.isOpened(): raise RuntimeError(f"OpenCV could not open {source_path}") width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = float(capture.get(cv2.CAP_PROP_FPS)) declared_frame_count = int( capture.get(cv2.CAP_PROP_FRAME_COUNT) ) if width <= 0 or height <= 0 or fps <= 0.0: capture.release() raise RuntimeError("invalid source video metadata") source_roi = normalized_roi_to_pixels(width, height) selected: list[EncodedComposite] = [] source_frame_index = 0 next_composite_time = 0.0 try: while True: frame_read, source_frame = capture.read() if not frame_read or source_frame is None: break source_time = source_frame_index / fps if ( source_time + FRAME_TIME_EPSILON_SECONDS >= next_composite_time ): selected.append( encode_composite( source_frame, source_roi, len(selected), source_frame_index, ) ) next_composite_time += 1.0 / COMPOSITE_FPS source_frame_index += 1 finally: capture.release() if source_frame_index <= 0 or not selected: raise RuntimeError("source video did not yield frames") if ( declared_frame_count > 0 and source_frame_index != declared_frame_count ): raise RuntimeError( "decoded frame count differs from video metadata: " f"{source_frame_index} != {declared_frame_count}" ) metadata = VideoMetadata( width=width, height=height, fps=fps, frame_count=source_frame_index, duration_seconds=source_frame_index / fps, ) return metadata, selected def packets_for_composite( composite: EncodedComposite, max_payload_bytes: int, ) -> tuple[list[bytes], list[bytes]]: """Packetize BASE and ROI separately with one composite frame ID.""" base_packets = packetize_jpeg( composite.base_jpeg, composite.composite_frame_id, ObjectType.BASE, max_payload_bytes, ) roi_packets = packetize_jpeg( composite.roi_jpeg, composite.composite_frame_id, ObjectType.ROI, max_payload_bytes, ) return base_packets, roi_packets def calculate_payload_metrics( composites: list[EncodedComposite], duration_seconds: float, max_payload_bytes: int, ) -> PayloadMetrics: """Calculate actual packet and bitrate statistics for one payload limit.""" base_counts: list[int] = [] roi_counts: list[int] = [] total_counts: list[int] = [] wire_packet_sizes: list[int] = [] for composite in composites: base_packets, roi_packets = packets_for_composite( composite, max_payload_bytes ) base_counts.append(len(base_packets)) roi_counts.append(len(roi_packets)) total_counts.append(len(base_packets) + len(roi_packets)) wire_packet_sizes.extend( len(packet) for packet in base_packets + roi_packets ) jpeg_payload_bytes = sum( len(composite.base_jpeg) + len(composite.roi_jpeg) for composite in composites ) packet_count = len(wire_packet_sizes) header_bytes = packet_count * HEADER_SIZE wire_bytes = jpeg_payload_bytes + header_bytes return PayloadMetrics( max_payload_bytes=max_payload_bytes, composite_frames=len(composites), mean_base_packets_per_frame=float(np.mean(base_counts)), mean_roi_packets_per_frame=float(np.mean(roi_counts)), mean_total_packets_per_frame=float(np.mean(total_counts)), max_total_packets_per_frame=max(total_counts), packets_per_second=packet_count / duration_seconds, jpeg_payload_bytes=jpeg_payload_bytes, jpeg_payload_bitrate_kbps=( jpeg_payload_bytes * 8.0 / duration_seconds / 1000.0 ), header_bytes_per_second=header_bytes / duration_seconds, header_bitrate_kbps=( header_bytes * 8.0 / duration_seconds / 1000.0 ), wire_bytes=wire_bytes, wire_bitrate_kbps=( wire_bytes * 8.0 / duration_seconds / 1000.0 ), service_data_percent=header_bytes / wire_bytes * 100.0, efficiency_percent=jpeg_payload_bytes / wire_bytes * 100.0, mean_wire_packet_bytes=float(np.mean(wire_packet_sizes)), p95_wire_packet_bytes=float( np.percentile(wire_packet_sizes, 95) ), max_wire_packet_bytes=max(wire_packet_sizes), ) def feed_packets( packets: list[bytes], reassembler: CompositeReassembler | None = None, ) -> tuple[list[CompositeFrame], CompositeReassembler]: """Feed packets and collect every atomically published frame.""" receiver = reassembler or CompositeReassembler() completed = [] for packet in packets: frame = receiver.ingest(packet) if frame is not None: completed.append(frame) return completed, receiver def assert_frame_matches( frame: CompositeFrame, expected: EncodedComposite, ) -> None: if frame.composite_frame_id != expected.composite_frame_id: raise AssertionError("composite frame ID differs") if frame.base_jpeg != expected.base_jpeg: raise AssertionError("BASE JPEG differs byte-for-byte") if frame.roi_jpeg != expected.roi_jpeg: raise AssertionError("ROI JPEG differs byte-for-byte") def run_functional_tests( composites: list[EncodedComposite], ) -> list[TestResult]: """Run header and all mandatory Lab028 transport checks.""" if len(composites) < 2: raise RuntimeError("functional checks need two video frames") first = composites[0] second = composites[1] base_packets, roi_packets = packets_for_composite(first, 256) all_packets = base_packets + roi_packets tests: list[tuple[str, Callable[[], str]]] = [] def header_round_trip() -> str: parsed = decode_packet(all_packets[0]) rebuilt = encode_packet( VideoPacket( composite_frame_id=parsed.composite_frame_id, object_type=parsed.object_type, fragment_index=parsed.fragment_index, fragment_count=parsed.fragment_count, jpeg_size=parsed.jpeg_size, object_crc32=parsed.object_crc32, payload=parsed.payload, ) ) if rebuilt != all_packets[0]: raise AssertionError("serialized bytes changed after round trip") return f"fixed {HEADER_SIZE}-byte header round trip is exact" def ordered_lossless() -> str: frames, _ = feed_packets(all_packets) if len(frames) != 1: raise AssertionError("ordered transfer did not emit one frame") assert_frame_matches(frames[0], first) return "BASE and ROI match original JPEG bytes" def shuffled_packets() -> str: shuffled = list(all_packets) random.Random(28001).shuffle(shuffled) frames, _ = feed_packets(shuffled) if len(frames) != 1: raise AssertionError("shuffled transfer did not emit one frame") assert_frame_matches(frames[0], first) return "arbitrary packet order reconstructed correctly" def duplicate_packets() -> str: duplicated = list(all_packets) duplicated.extend( [all_packets[0], all_packets[len(base_packets)]] ) random.Random(28002).shuffle(duplicated) frames, receiver = feed_packets(duplicated) if len(frames) != 1: raise AssertionError("duplicates changed publication count") assert_frame_matches(frames[0], first) if receiver.duplicate_packets < 2: raise AssertionError("exact duplicates were not counted") return f"{receiver.duplicate_packets} exact duplicates ignored" def packet_crc_corruption() -> str: corrupted = bytearray(all_packets[0]) corrupted[-1] ^= 0x01 receiver = CompositeReassembler() try: receiver.ingest(bytes(corrupted)) except PacketCRCError: pass else: raise AssertionError("corrupted packet passed packet CRC") frames, _ = feed_packets(all_packets[1:], receiver) if frames: raise AssertionError("frame emitted despite rejected packet") return "payload bit flip rejected; composite not emitted" def missing_fragment() -> str: missing_packet = roi_packets[len(roi_packets) // 2] missing_index = decode_packet(missing_packet).fragment_index remaining = [ packet for packet in all_packets if packet is not missing_packet ] frames, receiver = feed_packets(remaining) if frames: raise AssertionError("frame emitted with a missing fragment") missing = receiver.missing_fragments( first.composite_frame_id, ObjectType.ROI ) if missing is None or missing_index not in missing: raise AssertionError("missing fragment was not reported") return f"ROI fragment {missing_index} reported missing" def object_crc_corruption() -> str: target_position = len(base_packets) parsed = decode_packet(all_packets[target_position]) changed_payload = bytearray(parsed.payload) changed_payload[0] ^= 0x01 altered = encode_packet( VideoPacket( composite_frame_id=parsed.composite_frame_id, object_type=parsed.object_type, fragment_index=parsed.fragment_index, fragment_count=parsed.fragment_count, jpeg_size=parsed.jpeg_size, object_crc32=parsed.object_crc32, payload=bytes(changed_payload), ) ) formally_valid = list(all_packets) formally_valid[target_position] = altered receiver = CompositeReassembler() emitted = [] object_error_seen = False for packet in formally_valid: try: frame = receiver.ingest(packet) except ObjectCRCError: object_error_seen = True continue if frame is not None: emitted.append(frame) if not object_error_seen: raise AssertionError("object CRC did not detect changed JPEG") if emitted: raise AssertionError("frame emitted after object CRC failure") return "valid packet CRCs still failed full-object CRC" def adjacent_frames_do_not_mix() -> str: first_packets = sum(packets_for_composite(first, 256), []) second_packets = sum(packets_for_composite(second, 256), []) interleaved = first_packets + second_packets random.Random(28003).shuffle(interleaved) frames, _ = feed_packets(interleaved) by_id = {frame.composite_frame_id: frame for frame in frames} if set(by_id) != { first.composite_frame_id, second.composite_frame_id, }: raise AssertionError("adjacent frames were lost or mixed") assert_frame_matches(by_id[first.composite_frame_id], first) assert_frame_matches(by_id[second.composite_frame_id], second) return "two interleaved frame IDs remained independent" def base_only_is_not_atomic() -> str: frames, receiver = feed_packets(base_packets) if frames: raise AssertionError("BASE-only input emitted a composite") if not receiver.object_is_complete( first.composite_frame_id, ObjectType.BASE ): raise AssertionError("complete BASE object was not retained") return "complete BASE retained while composite stayed unpublished" tests.extend( [ ("header_serialization_round_trip", header_round_trip), ("ordered_lossless_transfer", ordered_lossless), ("random_packet_order", shuffled_packets), ("exact_duplicate_packets", duplicate_packets), ("packet_crc_corruption", packet_crc_corruption), ("missing_fragment", missing_fragment), ("object_crc_corruption", object_crc_corruption), ("adjacent_frame_isolation", adjacent_frames_do_not_mix), ("base_without_roi_atomicity", base_only_is_not_atomic), ] ) results = [] for name, test in tests: try: detail = test() except Exception as error: results.append(TestResult(name, False, str(error))) else: results.append(TestResult(name, True, detail)) failed = [result for result in results if not result.passed] if failed: details = "; ".join( f"{result.name}: {result.detail}" for result in failed ) raise RuntimeError(f"functional transport tests failed: {details}") return results def save_csv(metrics: list[PayloadMetrics]) -> None: OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) with CSV_PATH.open("w", encoding="utf-8", newline="") as csv_file: writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS) writer.writeheader() for item in metrics: row = {} for field_name in CSV_FIELDS: value = getattr(item, field_name) row[field_name] = ( f"{value:.6f}" if isinstance(value, float) else value ) writer.writerow(row) def save_plots(metrics: list[PayloadMetrics]) -> None: payloads = [item.max_payload_bytes for item in metrics] figure, axis = plt.subplots(figsize=(9, 5.5)) axis.plot( payloads, [item.service_data_percent for item in metrics], marker="o", linewidth=2, label="Service data (header / wire)", ) axis.plot( payloads, [item.efficiency_percent for item in metrics], marker="s", linewidth=2, label="Efficiency (JPEG / wire)", ) axis.set_xscale("log", base=2) axis.set_xticks(payloads) axis.set_xticklabels([str(value) for value in payloads]) axis.set_xlabel("Maximum packet payload, bytes") axis.set_ylabel("Share, %") axis.set_title("Lab028 packet overhead and efficiency") axis.grid(True, alpha=0.3) axis.legend() figure.tight_layout() figure.savefig(OVERHEAD_PLOT_PATH, dpi=160) plt.close(figure) figure, packet_axis = plt.subplots(figsize=(9, 5.5)) bitrate_axis = packet_axis.twinx() packet_line = packet_axis.plot( payloads, [item.packets_per_second for item in metrics], color="tab:blue", marker="o", linewidth=2, label="Packets/s", ) bitrate_lines = bitrate_axis.plot( payloads, [item.wire_bitrate_kbps for item in metrics], color="tab:red", marker="s", linewidth=2, label="Wire bitrate", ) bitrate_axis.plot( payloads, [item.jpeg_payload_bitrate_kbps for item in metrics], color="tab:green", linestyle="--", linewidth=2, label="JPEG payload bitrate", ) packet_axis.set_xscale("log", base=2) packet_axis.set_xticks(payloads) packet_axis.set_xticklabels([str(value) for value in payloads]) packet_axis.set_xlabel("Maximum packet payload, bytes") packet_axis.set_ylabel("Packets per second", color="tab:blue") bitrate_axis.set_ylabel("Bitrate, kbit/s", color="tab:red") packet_axis.set_title("Lab028 packet rate and wire bitrate") packet_axis.grid(True, alpha=0.3) lines = packet_line + bitrate_lines + bitrate_axis.lines[1:] packet_axis.legend( lines, [line.get_label() for line in lines], loc="best", ) figure.tight_layout() figure.savefig(TRAFFIC_PLOT_PATH, dpi=160) plt.close(figure) def metrics_table(metrics: list[PayloadMetrics]) -> list[str]: lines = [ ( "payload | BASE pkt/frame | ROI pkt/frame | all pkt/frame | " "max pkt/frame | pkt/s | JPEG kbit/s | headers B/s | " "wire kbit/s | service % | efficiency % | packet mean/p95/max" ), ( "-------:|---------------:|--------------:|--------------:|" "--------------:|------:|------------:|------------:|" "------------:|----------:|-------------:|--------------------:" ), ] for item in metrics: lines.append( f"{item.max_payload_bytes} | " f"{item.mean_base_packets_per_frame:.3f} | " f"{item.mean_roi_packets_per_frame:.3f} | " f"{item.mean_total_packets_per_frame:.3f} | " f"{item.max_total_packets_per_frame} | " f"{item.packets_per_second:.3f} | " f"{item.jpeg_payload_bitrate_kbps:.3f} | " f"{item.header_bytes_per_second:.3f} | " f"{item.wire_bitrate_kbps:.3f} | " f"{item.service_data_percent:.3f} | " f"{item.efficiency_percent:.3f} | " f"{item.mean_wire_packet_bytes:.1f}/" f"{item.p95_wire_packet_bytes:.1f}/" f"{item.max_wire_packet_bytes}" ) return lines def write_report( metadata: VideoMetadata, composites: list[EncodedComposite], metrics: list[PayloadMetrics], test_results: list[TestResult], ) -> None: source_roi = normalized_roi_to_pixels( metadata.width, metadata.height ) base_sizes = [len(item.base_jpeg) for item in composites] roi_sizes = [len(item.roi_jpeg) for item in composites] lines = [ "Lab028. Пакетирование синхронного BASE + ROI", "", "Профиль и исходные данные", f"- Видео: {SOURCE_VIDEO_PATH}", ( f"- Источник: {metadata.width}x{metadata.height}, " f"{metadata.fps:.6f} fps, {metadata.frame_count} кадров, " f"{metadata.duration_seconds:.6f} с." ), ( f"- ROI: x={ROI_X_MIN:.2f}...{ROI_X_MAX:.2f}, " f"y={ROI_Y_MIN:.2f}...{ROI_Y_MAX:.2f}; " f"пиксели x={source_roi[0]}...{source_roi[2]}, " f"y={source_roi[1]}...{source_roi[3]}." ), ( f"- Синхронные составные кадры: {len(composites)} " f"при {COMPOSITE_FPS:.3f} fps." ), ( f"- BASE: {BASE_WIDTH}x{BASE_HEIGHT}, grayscale JPEG Q" f"{BASE_QUALITY}; средний размер {np.mean(base_sizes):.3f} B, " f"min/max {min(base_sizes)}/{max(base_sizes)} B." ), ( f"- ROI: {ROI_WIDTH}x{ROI_HEIGHT}, grayscale JPEG Q" f"{ROI_QUALITY}; средний размер {np.mean(roi_sizes):.3f} B, " f"min/max {min(roi_sizes)}/{max(roi_sizes)} B." ), ( "- BASE и ROI формируются из одного source frame и имеют " "общий composite_frame_id." ), "", "Бинарный заголовок", f"- struct format: {HEADER_FORMAT}", f"- Размер: {HEADER_SIZE} байта; network byte order; padding нет.", ( "- Layout: magic[4] @0, version:u8 @4, object_type:u8 @5, " "header_size:u16 @6, composite_frame_id:u32 @8, " "fragment_index:u16 @12, fragment_count:u16 @14, " "payload_length:u16 @16, flags:u16 @18, jpeg_size:u32 @20, " "object_crc32:u32 @24, packet_crc32:u32 @28." ), "- object_type: 1=BASE, 2=ROI; flags зарезервирован и равен 0.", "", "CRC32", ( "- object_crc32 = zlib.crc32(полный JPEG) & 0xFFFFFFFF; " "значение помещается во все фрагменты объекта и проверяется " "после полной сборки." ), ( "- packet_crc32: сначала поле packet_crc32 заголовка " "обнуляется, затем CRC считается как " "zlib.crc32(header_with_zero_crc + payload) & 0xFFFFFFFF." ), "- Таким образом packet CRC защищает все поля заголовка и payload.", "", "Функциональные проверки", ] lines.extend( f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in test_results ) lines.extend( [ "", "Результаты размеров packet payload", *metrics_table(metrics), "", ( "JPEG payload bitrate одинаков для всех строк, потому что " "исходные JPEG не меняются при выборе размера фрагмента." ), ( "Wire bitrate включает только JPEG payload и 32-байтные " "заголовки каждого пакета." ), ( "Wire bitrate пока НЕ включает FEC, преамбулу, " "синхронизацию, модуляцию, интервалы, повторные передачи, " "команды управления и телеметрию." ), ( "Лучший размер packet payload автоматически не выбирается: " "таблица показывает только транспортный компромисс." ), "", "Артефакты", f"- CSV: {CSV_PATH}", f"- Overhead/efficiency: {OVERHEAD_PLOT_PATH}", f"- Packet count/wire bitrate: {TRAFFIC_PLOT_PATH}", ( "- Промежуточные JPEG и пакеты сохранялись только в памяти; " "бинарные дампы не создавались." ), "", ] ) REPORT_PATH.write_text("\n".join(lines), encoding="utf-8") def validate_outputs(metrics: list[PayloadMetrics]) -> None: if len(metrics) != len(PAYLOAD_LENGTHS): raise RuntimeError("not all payload sizes were measured") if [item.max_payload_bytes for item in metrics] != list( PAYLOAD_LENGTHS ): raise RuntimeError("payload result order differs") if any( abs( item.service_data_percent + item.efficiency_percent - 100.0 ) > 1e-9 for item in metrics ): raise RuntimeError("overhead and efficiency do not sum to 100%") for path in ( CSV_PATH, REPORT_PATH, OVERHEAD_PLOT_PATH, TRAFFIC_PLOT_PATH, ): if not path.exists() or path.stat().st_size <= 0: raise RuntimeError(f"missing or empty output: {path}") def main() -> None: print("Lab028: loading and JPEG-encoding the Lab027E profile...") metadata, composites = load_video_profile(SOURCE_VIDEO_PATH) print( f" source={metadata.width}x{metadata.height}, " f"{metadata.fps:.3f} fps, frames={metadata.frame_count}" ) print(f" synchronous composite frames={len(composites)}") print("Running functional transport checks...") test_results = run_functional_tests(composites) for result in test_results: print(f" PASS {result.name}: {result.detail}") print("Measuring packet payload sizes...") metrics = [ calculate_payload_metrics( composites, metadata.duration_seconds, payload_length, ) for payload_length in PAYLOAD_LENGTHS ] for item in metrics: print( f" payload={item.max_payload_bytes:4d} B: " f"{item.packets_per_second:.3f} packet/s, " f"wire={item.wire_bitrate_kbps:.3f} kbit/s, " f"service={item.service_data_percent:.3f}%" ) OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True) save_csv(metrics) save_plots(metrics) write_report(metadata, composites, metrics, test_results) validate_outputs(metrics) print(f"CSV: {CSV_PATH}") print(f"Report: {REPORT_PATH}") print(f"Plots: {OVERHEAD_PLOT_PATH}, {TRAFFIC_PLOT_PATH}") print("Lab028 completed successfully.") if __name__ == "__main__": main()