"""Восстановление целых видеопакетов и срок жизни фрагментов VIDEO-004.""" from __future__ import annotations import random import pytest from protocol.packet_erasure_fec import InsufficientSymbolsError from protocol.video_stream_transport import FrameType, VideoFragment from protocol.video004_erasure import ( MAX_VIDEO_BYTES, VideoErasureCollector, VideoErasureError, encode_block, parse_symbol, recover_block, ) def _fragments(count: int, size: int = MAX_VIDEO_BYTES) -> tuple[tuple[VideoFragment, int], ...]: return tuple((VideoFragment(19, index // 4, index % 4, 4, FrameType.KEYFRAME if index < 4 else FrameType.DELTA_FRAME, 0, bytes([index]) * (size if index != count - 1 else 7)), (index // 4) * 100) for index in range(count)) @pytest.mark.parametrize("source_count,parity_count", [ (8, 1), (8, 2), (12, 2), (12, 3), (16, 2), (16, 4), ]) def test_recover_exact_source_from_one_and_up_to_r_losses( source_count: int, parity_count: int, ) -> None: fragments = _fragments(source_count) wires = encode_block(fragments, 27, parity_count) assert len(wires) == source_count + parity_count assert all(len(wire) == 1012 for wire in wires) assert all(parse_symbol(wire).block_id == 27 for wire in wires) expected = recover_block(wires[:source_count]) assert [item.fragment for item in expected] == [item[0] for item in fragments] assert [item.capture_ms for item in expected] == [item[1] for item in fragments] for erased_count in (1, parity_count): erased = set(random.Random(source_count * 100 + erased_count).sample( range(source_count), erased_count)) available = tuple(wire for index, wire in enumerate(wires) if index not in erased) recovered = recover_block(available) assert [item.raw_video_wire for item in recovered] == [ item.raw_video_wire for item in expected] assert {item.index for item in recovered if item.recovered} == erased with pytest.raises(InsufficientSymbolsError): recover_block(wires[parity_count + 1:]) def test_collector_releases_systematic_immediately_and_rejects_late_recovery() -> None: fragments = _fragments(8) wires = encode_block(fragments, 5, 2) collector = VideoErasureCollector(19) first = collector.offer(wires[0], 15) assert len(first) == 1 and first[0].index == 0 and not first[0].recovered for index, wire in enumerate(wires[1:8], start=1): collector.offer(wire, 20 if index < 4 else 120) assert collector.parity_recoveries == 0 missing = VideoErasureCollector(19) for index, wire in enumerate(wires[1:8], start=1): missing.offer(wire, 20 if index < 4 else 120) recovered = missing.offer(wires[8], 301) assert not recovered assert missing.stale_rejections >= 1 timely = VideoErasureCollector(19) for index, wire in enumerate(wires[1:8], start=1): timely.offer(wire, 20 if index < 4 else 120) recovered = timely.offer(wires[8], 250) assert [item.index for item in recovered] == [0] assert timely.parity_recoveries == 1 def test_invalid_metadata_and_mixed_stream_are_rejected() -> None: fragments = _fragments(8) wire = encode_block(fragments, 2, 1)[0] broken = bytearray(wire) broken[0] ^= 0xFF with pytest.raises(VideoErasureError): parse_symbol(bytes(broken)) other = VideoErasureCollector(20) with pytest.raises(VideoErasureError): other.offer(wire, 0) collector = VideoErasureCollector(19) with pytest.raises(VideoErasureError, match="будущим"): collector.offer(encode_block(_fragments(8), 3, 1)[4], 10)