86 lines
3.7 KiB
Python
86 lines
3.7 KiB
Python
"""Восстановление целых видеопакетов и срок жизни фрагментов 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)
|