Complete VIDEO-001 through VIDEO-004 software video studies
This commit is contained in:
130
tests/test_video002_transport.py
Normal file
130
tests/test_video002_transport.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Проверки целостности и ограничения возраста видеокадра."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from protocol.video_stream_transport import (
|
||||
BoundedFrameQueue, FrameReassembler, FrameType, VideoFrame,
|
||||
VideoTransportError, decode_fragment, encode_fragment, is_newer,
|
||||
split_frame,
|
||||
)
|
||||
from tools.video002_radio_budget import estimate, margin_fits
|
||||
|
||||
|
||||
def frame(number: int = 1, capture_ms: int = 0, size: int = 600) -> VideoFrame:
|
||||
return VideoFrame(7, number, FrameType.KEYFRAME, capture_ms, bytes(range(256)) *
|
||||
(size // 256) + bytes(range(size % 256)))
|
||||
|
||||
|
||||
def test_header_roundtrip_and_corruption() -> None:
|
||||
part = split_frame(frame(), 256)[0]
|
||||
wire = encode_fragment(part, 256)
|
||||
assert len(wire) == 256
|
||||
assert decode_fragment(wire, 256) == part
|
||||
with pytest.raises(VideoTransportError):
|
||||
decode_fragment(wire[:-1], 256)
|
||||
with pytest.raises(VideoTransportError):
|
||||
decode_fragment(bytes([0x21]) + wire[1:], 256)
|
||||
with pytest.raises(VideoTransportError):
|
||||
encode_fragment(replace(part, fragment_index=part.fragment_count), 256)
|
||||
|
||||
|
||||
def test_out_of_order_reassembly_no_partial_or_replay() -> None:
|
||||
source = frame()
|
||||
parts = [encode_fragment(item, 256) for item in split_frame(source, 256)]
|
||||
receiver = FrameReassembler(7, 256)
|
||||
assert receiver.offer(parts[2], 20) is None
|
||||
assert receiver.offer(parts[0], 21) is None
|
||||
assert receiver.offer(parts[0], 22) is None
|
||||
complete = receiver.offer(parts[1], 23)
|
||||
assert complete is not None and complete.data == source.data
|
||||
assert receiver.offer(parts[1], 24) is None
|
||||
assert receiver.rejected_fragments == 1
|
||||
|
||||
|
||||
def test_incomplete_frame_expires_and_new_frame_advances() -> None:
|
||||
receiver = FrameReassembler(7, 256, max_age_ms=100)
|
||||
first = encode_fragment(split_frame(frame(1), 256)[0], 256)
|
||||
assert receiver.offer(first, 0) is None
|
||||
assert receiver.expire(100) == (1,)
|
||||
assert receiver.offer(first, 101) is None
|
||||
next_frame = frame(2, 101, 25)
|
||||
complete = receiver.offer(encode_fragment(split_frame(next_frame, 256)[0], 256), 102)
|
||||
assert complete is not None and complete.data == next_frame.data
|
||||
|
||||
|
||||
def test_sender_bounded_queue_discards_old_frame() -> None:
|
||||
sender = BoundedFrameQueue(256, max_frames=2, max_age_ms=100)
|
||||
sender.enqueue(frame(1, 0), 0)
|
||||
sender.enqueue(frame(2, 20), 20)
|
||||
sender.enqueue(frame(3, 40), 40)
|
||||
assert sender.depth == 2 and sender.capacity_frame_drops == 1
|
||||
part = sender.next_fragment(40, 50)
|
||||
assert part is not None and part.frame_id == 2
|
||||
sender.expire(120)
|
||||
assert sender.stale_frame_drops == 1
|
||||
assert sender.next_fragment(120, 130).frame_id == 3
|
||||
sender.expire(140)
|
||||
assert sender.depth == 0 and sender.stale_frame_drops == 2
|
||||
|
||||
|
||||
def test_fractional_packet_end_drops_before_receiver_age_rounding() -> None:
|
||||
sender = BoundedFrameQueue(256, max_age_ms=300)
|
||||
sender.enqueue(frame(1, 0, 25), 0)
|
||||
assert sender.next_fragment(299, 299.1) is None
|
||||
assert sender.stale_frame_drops == 1
|
||||
|
||||
|
||||
def test_conflicting_fragment_closes_frame() -> None:
|
||||
receiver = FrameReassembler(7, 256)
|
||||
original = split_frame(frame(), 256)[0]
|
||||
receiver.offer(encode_fragment(original, 256), 0)
|
||||
bad = replace(original, data=b"X" + original.data[1:])
|
||||
with pytest.raises(VideoTransportError):
|
||||
receiver.offer(encode_fragment(bad, 256), 1)
|
||||
assert receiver.offer(encode_fragment(original, 256), 2) is None
|
||||
|
||||
|
||||
def test_wrong_stream_and_wraparound() -> None:
|
||||
receiver = FrameReassembler(7, 256)
|
||||
other = encode_fragment(split_frame(replace(frame(size=1), stream_id=8), 256)[0], 256)
|
||||
assert receiver.offer(other, 0) is None
|
||||
assert is_newer(0, 0xFFFFFFFF)
|
||||
assert not is_newer(0xFFFFFFFE, 1)
|
||||
|
||||
|
||||
def test_expired_frame_cannot_replay_after_closed_cache_rollover() -> None:
|
||||
receiver = FrameReassembler(7, 256, max_age_ms=10)
|
||||
for frame_id in range(1025):
|
||||
part = split_frame(frame(frame_id, frame_id * 20, 300), 256)[0]
|
||||
assert receiver.offer(encode_fragment(part, 256), frame_id * 20) is None
|
||||
receiver.expire(frame_id * 20 + 10)
|
||||
assert 0 not in receiver._closed
|
||||
old_parts = [encode_fragment(part, 256) for part in split_frame(frame(0, 0, 300), 256)]
|
||||
assert receiver.offer(old_parts[0], 20_501) is None
|
||||
assert receiver.offer(old_parts[1], 20_502) is None
|
||||
assert receiver.rejected_fragments >= 2
|
||||
|
||||
|
||||
def test_two_new_frames_can_start_out_of_order() -> None:
|
||||
receiver = FrameReassembler(7, 256)
|
||||
newer_parts = [encode_fragment(item, 256) for item in split_frame(frame(11, 10, 300), 256)]
|
||||
older = encode_fragment(split_frame(frame(10, 0, 25), 256)[0], 256)
|
||||
assert receiver.offer(newer_parts[0], 20) is None
|
||||
assert receiver.offer(older, 21).frame_id == 10
|
||||
assert receiver.offer(newer_parts[1], 22).frame_id == 11
|
||||
|
||||
|
||||
def test_radio_formula_and_margin() -> None:
|
||||
current = estimate("BPSK", 500_000, 256, 5)
|
||||
assert current.coded_body_bits == 4300
|
||||
assert current.airtime_ms == pytest.approx(11.532)
|
||||
assert current.video_bytes_per_packet == 239
|
||||
assert current.useful_video_bps == pytest.approx(115_654.488)
|
||||
future = estimate("QPSK", 500_000, 1024, 0)
|
||||
assert not future.within_current_packet_limit
|
||||
assert not future.implemented_in_current_phy
|
||||
assert not margin_fits(future.useful_video_bps, 376_000, 0.2)
|
||||
Reference in New Issue
Block a user