Complete VIDEO-001 through VIDEO-004 software video studies

This commit is contained in:
LittleSam129
2026-09-16 18:27:35 +03:00
parent af7c05f2da
commit 95f664076a
403 changed files with 392009 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
"""Потеря реального H.264 кадра и восстановление декодера на следующем IDR."""
from __future__ import annotations
import av
import numpy as np
from protocol.video_stream_transport import FrameType, VideoFrame
from tools.video002_simulation import simulate
from tools.video_compare import new_h264_encoder
def test_h264_recovers_after_lost_keyframe_and_delta() -> None:
fps = 5
encoder, _ = new_h264_encoder(64, 48, fps, 160_000)
frames = []
for index in range(15):
pixels = np.zeros((48, 64, 3), dtype=np.uint8)
pixels[:, :, 1] = (index * 13) % 255
pixels[10:22, index * 3:index * 3 + 12, 0] = 230
raw = av.VideoFrame.from_ndarray(pixels, format="rgb24").reformat(format="yuv420p")
raw.pts = index
packets = encoder.encode(raw)
assert len(packets) == 1
packet = packets[0]
frames.append(VideoFrame(17, index,
FrameType.KEYFRAME if packet.is_keyframe
else FrameType.DELTA_FRAME,
index * 200, bytes(packet)))
assert not encoder.encode(None)
assert [item.frame_id for item in frames if item.frame_type == FrameType.KEYFRAME] == [0, 5, 10]
link = ("SOFTWARE_2MBPS", None, 2_000_000, 1024, 0)
clean = simulate(frames, fps, link, ("random", 0))
key_lost = simulate(frames, fps, link, ("keyframe", 1))
delta_lost = simulate(frames, fps, link, ("delta", 1))
assert clean["displayed_frame_ids"] == list(range(15))
assert key_lost["displayed_frame_ids"] == list(range(5)) + list(range(10, 15))
assert delta_lost["displayed_frame_ids"] == list(range(6)) + list(range(10, 15))
assert key_lost["keyframe_sha256_by_id"]["10"] == clean["keyframe_sha256_by_id"]["10"]
assert key_lost["unique_decoded_images"] == key_lost["displayed_frames"]
assert key_lost["no_old_frame_replay"] and delta_lost["no_old_frame_replay"]

View 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)

View File

@@ -0,0 +1,63 @@
"""Границы PHY, расчёт полезной скорости и отдельный компактный заголовок."""
from __future__ import annotations
import pytest
from protocol.video003_compact_header import HEADER_SIZE as COMPACT_SIZE
from protocol.video003_compact_header import decode, encode
from protocol.video_stream_transport import (
HEADER_SIZE as CURRENT_SIZE, FrameType, VideoFragment, VideoTransportError,
)
from tools.video003_design import estimate, run
def test_current_packet_limit_and_exact_max_video_fragment() -> None:
result = run()
assert result["max_full_phy_packet_bytes"] == 1024
assert result["current_fixed_lab_generator_phy_payload_bytes"] == 256
assert result["packet_header_bytes"] == 8
assert result["packet_crc_bytes"] == 4
assert result["video_header_bytes"] == 17
assert result["max_format_phy_payload_bytes"] == 1012
assert result["max_format_video_fragment_bytes"] == 995
assert len(result["rows"]) == 7 * 4 * 5
assert sorted(set(row["gap_ms"] for row in result["rows"])) == [0, 0.5, 1, 2, 5]
with pytest.raises(ValueError, match="1024"):
estimate("QPSK", 500_000, "3/4", 1013, 0)
def test_maximum_does_not_reach_600k_even_without_video_header() -> None:
result = run()
best = result["best_calculated"]
assert best["modulation"] == "QPSK"
assert best["symbol_rate"] == 500_000
assert best["fec_rate"] == "3/4"
assert best["full_packet_bytes"] == 1024
assert best["gap_ms"] == 0
assert best["useful_video_rate_bps"] == pytest.approx(574_148.8747836122)
assert best["useful_video_rate_bps"] < 600_000
no_video_header = estimate("QPSK", 500_000, "3/4", 1012, 0, video_header_bytes=0)
assert no_video_header.useful_video_rate_bps < 600_000
def test_compact_header_roundtrip_preserves_identity_age_and_reassembly_fields() -> None:
assert CURRENT_SIZE == 17 and COMPACT_SIZE == 13
fragment = VideoFragment(0x56303032, 0xFFFFFFFE, 255, 256,
FrameType.KEYFRAME, 299, b"video")
wire = encode(fragment, 1012)
assert len(wire) == COMPACT_SIZE + len(fragment.data)
assert decode(wire, 1012) == fragment
assert (CURRENT_SIZE - COMPACT_SIZE) / CURRENT_SIZE == pytest.approx(4 / 17)
with pytest.raises(VideoTransportError):
decode(bytes([0x12]) + wire[1:], 1012)
with pytest.raises(VideoTransportError):
decode(wire[:9], 1012)
def test_compact_header_does_not_make_600k_reachable() -> None:
row = estimate("QPSK", 500_000, "3/4", 1012, 0,
video_header_bytes=COMPACT_SIZE)
assert row.video_fragment_bytes == 999
assert row.useful_video_rate_bps == pytest.approx(576_457.0109636468)
assert row.useful_video_rate_bps < 600_000

View File

@@ -0,0 +1,105 @@
"""Эталонные векторы и строгий приём программного QPSK VIDEO-003."""
from __future__ import annotations
import math
import numpy as np
import pytest
from protocol import punctured_fec
from protocol.video003_modem import (
PREAMBLE_SYMBOLS,
build_frame,
known_preamble_bits,
qpsk_soft,
qpsk_symbols,
receive_frame,
rrc_roundtrip,
samples_per_symbol,
)
def test_gray_mapping_known_vector_and_unit_energy() -> None:
bits = np.array([0, 0, 0, 1, 1, 1, 1, 0], dtype=np.uint8)
expected = np.array([-1-1j, -1+1j, 1+1j, 1-1j]) / math.sqrt(2)
assert np.allclose(qpsk_symbols(bits), expected)
assert np.allclose(np.abs(expected) ** 2, 1.0)
assert np.array_equal((qpsk_soft(expected) >= 0).astype(np.uint8), bits)
pairs = bits.reshape(4, 2)
assert all(np.count_nonzero(pairs[index] != pairs[(index + 1) % 4]) == 1
for index in range(4))
def test_preamble_matches_lab043_reference() -> None:
from experiments.lab043_pluto_to_rtlsdr import pilot_prbs7
assert np.array_equal(known_preamble_bits(), pilot_prbs7(PREAMBLE_SYMBOLS, 0x7F))
@pytest.mark.parametrize("rate", ["1/2", "2/3", "3/4"])
def test_puncturing_preserves_existing_code_and_decodes_without_noise(rate: str) -> None:
random_generator = np.random.default_rng(3303)
for size in (1, 13, 256, 1012):
bits = random_generator.integers(0, 2, size * 8, dtype=np.uint8)
coded = punctured_fec.encode(bits, rate)
assert len(coded) == punctured_fec.coded_bit_count(len(bits), rate)
restored = punctured_fec.decode_soft(2.0 * coded - 1.0, len(bits), rate)
assert np.array_equal(restored, bits)
assert punctured_fec.effective_rate(len(bits), rate) < float(rate[0]) / float(rate[2])
def test_puncturing_patterns_and_erasure_locations() -> None:
assert punctured_fec.PUNCTURE_PATTERNS == {
"1/2": (1, 1), "2/3": (1, 1, 1, 0), "3/4": (1, 1, 1, 0, 0, 1)
}
values = np.arange(punctured_fec.coded_bit_count(8, "2/3"), dtype=float) + 1
restored = punctured_fec.depuncture_soft(values, 8, "2/3")
assert np.array_equal(restored[3::4], np.zeros(len(restored[3::4])))
with pytest.raises(ValueError):
punctured_fec.depuncture_soft(values[:-1], 8, "2/3")
@pytest.mark.parametrize("modulation,symbol_rate", [
("BPSK", 500_000), ("QPSK", 250_000), ("QPSK", 500_000)
])
@pytest.mark.parametrize("rate", ["1/2", "2/3", "3/4"])
def test_independent_receive_phase_quadrant_and_cfo(
modulation: str, symbol_rate: int, rate: str
) -> None:
payload = bytes(range(48))
frame = build_frame(payload, modulation, rate)
indexes = np.arange(len(frame.symbols), dtype=float)
phase = 0.23 + (math.pi / 2 if modulation == "QPSK" else 0.0)
shifted = frame.symbols * np.exp(1j * (phase + 2 * math.pi * 73 * indexes / symbol_rate))
prefix = np.full(9, 0.1 + 0.2j)
outcome = receive_frame(np.concatenate((prefix, shifted)), modulation, rate, symbol_rate)
assert outcome.preamble_found and outcome.phy_header_pass and outcome.crc_pass
assert outcome.start_symbol == 9
assert outcome.payload == payload
assert outcome.frequency_offset_hz == pytest.approx(73, abs=0.1)
@pytest.mark.parametrize("symbol_rate", [250_000, 500_000])
def test_rrc_known_timing_preserves_qpsk_packet(symbol_rate: int) -> None:
frame = build_frame(b"RRC check", "QPSK", "2/3")
filtered = rrc_roundtrip(frame.symbols, symbol_rate)
result = receive_frame(filtered, "QPSK", "2/3", symbol_rate)
assert samples_per_symbol(symbol_rate) == 8_000_000 // symbol_rate
assert result.crc_pass and result.payload == b"RRC check"
def test_corruption_and_truncation_cannot_become_crc_pass() -> None:
frame = build_frame(bytes(range(32)), "QPSK", "3/4")
truncated = receive_frame(frame.symbols[:-20], "QPSK", "3/4", 500_000)
assert truncated.preamble_found and truncated.phy_header_pass and not truncated.crc_pass
broken = frame.symbols.copy()
broken[PREAMBLE_SYMBOLS:PREAMBLE_SYMBOLS + 144] *= -1
rejected = receive_frame(broken, "QPSK", "3/4", 500_000)
assert rejected.preamble_found and not rejected.phy_header_pass and not rejected.crc_pass
def test_full_packet_limit_is_enforced_before_modulation() -> None:
assert len(build_frame(bytes(1012), "QPSK", "3/4").packet) == 1024
with pytest.raises(ValueError, match="1024"):
build_frame(bytes(1013), "QPSK", "3/4")

View File

@@ -0,0 +1,20 @@
"""Непрерывный программный поток не получает границы из данных передатчика."""
from __future__ import annotations
import pytest
from tools.video003_stream import run
def test_two_full_packets_per_rate_decode_from_received_header() -> None:
result = run(packet_count=2, seed=20260916)
assert result["rf_tx"] == 0
for item in result["results"]:
assert item["attempted_packets"] == 2
assert item["crc_pass_count"] == item["exact_packet_count"] == 2
assert item["verified_video_fragment_count"] == 2
assert item["offline_verified_video_bps"] == pytest.approx(
item["calculated_zero_loss_video_bps"])
assert item["records"][0]["start_symbol_relative"] == 11
assert item["records"][1]["start_symbol_relative"] == 0

View File

@@ -0,0 +1,62 @@
"""Проверка временной модели VIDEO-003 на настоящих малых H.264 кадрах."""
from __future__ import annotations
import math
import av
import numpy as np
import pytest
from protocol.video_stream_transport import FrameType, VideoFrame
from tools.video002_simulation import simulate
from tools.video_compare import new_h264_encoder
from tools.video003_design import estimate
def _frames() -> list[VideoFrame]:
encoder, _ = new_h264_encoder(64, 48, 5, 160_000)
output = []
for index in range(10):
pixels = np.zeros((48, 64, 3), dtype=np.uint8)
pixels[:, :, 1] = index * 17
pixels[8:20, 3 * index:3 * index + 12, 0] = 240
raw = av.VideoFrame.from_ndarray(pixels, format="rgb24").reformat(format="yuv420p")
raw.pts = index
packets = encoder.encode(raw)
assert len(packets) == 1
packet = packets[0]
output.append(VideoFrame(33, index,
FrameType.KEYFRAME if packet.is_keyframe
else FrameType.DELTA_FRAME,
200 * index, bytes(packet)))
assert not encoder.encode(None)
return output
def test_explicit_fec_airtime_controls_video_age_and_p95() -> None:
frames = _frames()
row = estimate("QPSK", 500_000, "2/3", 1012, 0)
link = ("QPSK_500_R23", "QPSK", 500_000, 1012, 0)
calculated = simulate(frames, 5, link, ("random", 0),
airtime_override_ms=row.packet_airtime_ms)
slower = simulate(frames, 5, link, ("random", 0),
airtime_override_ms=row.packet_airtime_ms * 2)
assert calculated["displayed_frames"] == 10
assert slower["displayed_frames"] == 10
assert calculated["p95_end_to_end_age_ms"] <= slower["p95_end_to_end_age_ms"]
assert calculated["p95_end_to_end_age_ms"] <= calculated["max_end_to_end_age_ms"]
assert calculated["no_old_frame_replay"] and slower["no_old_frame_replay"]
with pytest.raises(ValueError):
simulate(frames, 5, link, ("random", 0), airtime_override_ms=math.nan)
def test_design_headroom_uses_actual_video001_peaks() -> None:
from tools.video003_design import measured_video_profiles
profiles = measured_video_profiles()
assert profiles["NORMAL"]["measured_peak_1s_bps"] == pytest.approx(375_776, abs=200)
assert profiles["HIGH_QUALITY"]["measured_peak_1s_bps"] == pytest.approx(455_600, abs=200)
for profile in profiles.values():
assert profile["required_20pct_bps"] == pytest.approx(
profile["measured_peak_1s_bps"] * 1.2)

View File

@@ -0,0 +1,85 @@
"""Восстановление целых видеопакетов и срок жизни фрагментов 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)

View File

@@ -0,0 +1,25 @@
"""Усиление опорного кадра проверяется двумя потерями в одном блоке."""
from __future__ import annotations
import json
from tools.video004_transport import (
BASELINE_160, BASELINE_320, CODEC_RESULTS, _frames, simulate,
)
def test_double_keyframe_loss_needs_two_parity_symbols() -> None:
codec = json.loads(CODEC_RESULTS.read_text(encoding="utf-8"))
baseline = json.loads(BASELINE_320.read_text(encoding="utf-8"))
baseline_160 = json.loads(BASELINE_160.read_text(encoding="utf-8"))
combined, _ = _frames("NORMAL", 640, 360, 10, baseline["windows"][0],
codec, baseline_160)
frames = [frame for frame in combined if frame.stream_id == 0x56303334]
base = simulate(frames, 10, "2/3", 8, 1, ("keyframe_double", 2))
boosted = simulate(frames, 10, "2/3", 8, 1, ("keyframe_double", 2),
keyframe_boost=True)
assert base["displayed_frames"] == 55
assert base["unrecoverable_blocks"] == 1
assert boosted["displayed_frames"] == 60
assert boosted["parity_recoveries"] == 2

View File

@@ -0,0 +1,24 @@
"""Полный доступный путь символы → CRC → стирания → видеокадр."""
from __future__ import annotations
import hashlib
import json
import numpy as np
import pytest
pytest.importorskip("numba")
from tools.video004_pipeline_benchmark import MANIFEST, SIGNAL, decode
def test_four_frames_exact_after_four_packet_erasures() -> None:
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
received = np.load(SIGNAL, allow_pickle=False)
assert hashlib.sha256(received.tobytes()).hexdigest() == manifest["signal_sha256"]
result = decode(received, "numba")
assert result["packet_sha256"] == manifest["expected_packet_sha256"]
assert result["frame_sha256"] == manifest["expected_frame_sha256"]
assert result["parity_recoveries"] == 4
assert result["unrecoverable_blocks"] == 0

View File

@@ -0,0 +1,34 @@
"""Детерминированность модели и запрет позднего видеофрагмента."""
from __future__ import annotations
import json
from protocol.video_stream_transport import FrameReassembler, FrameType, VideoFragment
from tools.video004_transport import (
BASELINE_160, BASELINE_320, CODEC_RESULTS, _frames, simulate,
)
def test_v2_fragment_can_enter_reassembler_without_reserialization() -> None:
receiver = FrameReassembler(19, 1012, max_age_ms=300)
fragment = VideoFragment(19, 0, 0, 1, FrameType.KEYFRAME, 10, b"h264")
completed = receiver.offer_fragment(fragment, 10)
assert completed is not None and completed.data == b"h264"
assert receiver.offer_fragment(fragment, 11) is None
def test_loss_model_repeats_and_zero_loss_shows_all_normal_frames() -> None:
codec = json.loads(CODEC_RESULTS.read_text(encoding="utf-8"))
baseline = json.loads(BASELINE_320.read_text(encoding="utf-8"))
baseline_160 = json.loads(BASELINE_160.read_text(encoding="utf-8"))
combined, _ = _frames("NORMAL", 640, 360, 10, baseline["windows"][0],
codec, baseline_160)
frames = [frame for frame in combined if frame.stream_id == 0x56303334]
clean = simulate(frames, 10, "2/3", 8, 1, ("random", 0))
first = simulate(frames, 10, "2/3", 8, 2, ("random", 5))
second = simulate(frames, 10, "2/3", 8, 2, ("random", 5))
assert clean["displayed_frames"] == clean["input_frames"] == 60
assert first == second
assert first["parity_recoveries"] > 0
assert first["no_old_frame_replay"]

View File

@@ -0,0 +1,34 @@
"""Побитовая эквивалентность JIT и исходного Витерби."""
from __future__ import annotations
import numpy as np
import pytest
pytest.importorskip("numba")
from protocol import punctured_fec
from protocol.video004_viterbi import decode_soft
from tools.video004_benchmark import load_fixture, decode_stream
@pytest.mark.parametrize("rate", ["1/2", "2/3", "3/4"])
@pytest.mark.parametrize("byte_count", [13, 256, 1012])
def test_jit_matches_legacy_under_noise(rate: str, byte_count: int) -> None:
generator = np.random.default_rng(4004 + byte_count)
bits = generator.integers(0, 2, byte_count * 8, dtype=np.uint8)
coded = punctured_fec.encode(bits, rate)
soft = 2.0 * coded.astype(np.float64) - 1.0
soft += generator.normal(0, 0.8, len(soft))
expected = punctured_fec.decode_soft(soft, len(bits), rate)
actual = decode_soft(soft, len(bits), rate)
assert np.array_equal(actual, expected)
def test_canonical_qpsk_decisions_fec_crc_and_packets_are_equal() -> None:
received, manifest = load_fixture()
before = decode_stream(received, "legacy")
after = decode_stream(received, "numba")
assert before == after
assert len(after) == 32
assert [row["packet_sha256"] for row in after] == manifest["expected_packet_sha256"]

View File

@@ -0,0 +1,48 @@
"""Проверки измерителя VIDEO-001 без камеры, SDR и исходного ролика."""
from __future__ import annotations
import numpy as np
from tools.video_compare import fit_frame, h264_case, jpeg_case, peak_second, radio_screen
def test_four_by_three_output_preserves_source_geometry() -> None:
source = np.full((72, 128, 3), 128, dtype=np.uint8)
result = fit_frame(source, 640, 480)
assert result.shape == (480, 640, 3)
assert np.all(result[:60] == 0)
assert np.all(result[60:420] == 128)
assert np.all(result[420:] == 0)
def test_h264_restores_exact_decoding_at_next_reference_frame(tmp_path) -> None:
frames = []
for index in range(10):
frame = np.zeros((96, 128, 3), dtype=np.uint8)
frame[:, :, 1] = 30 + index * 12
frame[30:60, index * 5:index * 5 + 30, 0] = 250
frames.append(frame)
result = h264_case(frames, 5, 160_000, tmp_path / "example")
assert result["decoded_frame_count"] == 10
assert result["unique_decoded_frames"] == 10
assert len(result["keyframe_sizes_bytes"]) == 2
assert result["loss_recovery"]["lost_reference"]["first_exact_frame"] == 5
assert result["loss_recovery"]["lost_delta"]["first_exact_frame"] == 5
def test_jpeg_restores_on_the_next_independent_frame(tmp_path) -> None:
frames = [np.full((32, 32, 3), index * 25, dtype=np.uint8) for index in range(5)]
result = jpeg_case(frames, 5, tmp_path / "jpeg")
assert result["decoded_frame_count"] == 5
assert result["loss_recovery"]["first_exact_frame"] == 1
def test_packetization_and_one_second_peak_do_not_use_mean_bitrate() -> None:
assert peak_second([100, 100, 1000, 100, 100, 100], 2) == 8_800
small = radio_screen([244] * 5, 5)
assert small["peak_packet_slots_per_second"] == 5
assert small["fits_current_radio_budget"] == "NOT_ESTABLISHED_NO_SUSTAINED_GOODPUT_TEST"
large = radio_screen([10_000] * 5, 5)
assert large["peak_packet_slots_per_second"] == 205
assert large["fits_current_radio_budget"] == "NO_ON_OBSERVED_BURST_SCREEN"