242 lines
12 KiB
Python
242 lines
12 KiB
Python
"""Систематическая защита видеофрагментов от потери целых PHY-пакетов.
|
|
|
|
Вызывающий код передаёт сюда только payload после строгого PHY/FEC/CRC.
|
|
Арифметика GF(256) повторно используется из packet_erasure_fec; упаковка
|
|
Lab028 не используется. Каждый внешний символ занимает полный PHY payload.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from collections import deque
|
|
import struct
|
|
from typing import Sequence
|
|
|
|
from protocol.packet_erasure_fec import (
|
|
InsufficientSymbolsError, _linear_combine, encode_parity_symbols,
|
|
matrix_inverse, systematic_generator_matrix,
|
|
)
|
|
from protocol.video003_compact_header import (
|
|
HEADER_SIZE as VIDEO_HEADER_SIZE, decode as decode_video, encode as encode_video,
|
|
)
|
|
from protocol.video_stream_transport import VideoFragment
|
|
|
|
|
|
MAGIC = 0xFEC4
|
|
VERSION = 1
|
|
HEADER_FORMAT = "!HBBIIBBBB"
|
|
HEADER_SIZE = struct.calcsize(HEADER_FORMAT) # 16
|
|
SOURCE_PREFIX_FORMAT = "!HI"
|
|
SOURCE_PREFIX_SIZE = struct.calcsize(SOURCE_PREFIX_FORMAT) # length + capture_ms = 6
|
|
MAX_PHY_PAYLOAD = 1012
|
|
SYMBOL_SIZE = MAX_PHY_PAYLOAD - HEADER_SIZE # 996
|
|
MAX_VIDEO_BYTES = SYMBOL_SIZE - SOURCE_PREFIX_SIZE - VIDEO_HEADER_SIZE # 977
|
|
MAX_AGE_MS = 300
|
|
|
|
|
|
class VideoErasureError(ValueError):
|
|
"""Неверный внешний символ или противоречивый блок."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Symbol:
|
|
stream_id: int
|
|
block_id: int
|
|
index: int
|
|
source_count: int
|
|
parity_count: int
|
|
is_parity: bool
|
|
data: bytes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Delivery:
|
|
index: int
|
|
fragment: VideoFragment
|
|
capture_ms: int
|
|
recovered: bool
|
|
raw_video_wire: bytes
|
|
|
|
|
|
def _header(stream_id: int, block_id: int, index: int,
|
|
source_count: int, parity_count: int) -> bytes:
|
|
if not (0 <= stream_id <= 0xFFFFFFFF and 0 <= block_id <= 0xFFFFFFFF
|
|
and 1 <= source_count <= 255 and 1 <= parity_count <= 255 - source_count
|
|
and 0 <= index < source_count + parity_count):
|
|
raise VideoErasureError("поля внешнего FEC-заголовка вне диапазона")
|
|
return struct.pack(HEADER_FORMAT, MAGIC, VERSION,
|
|
int(index >= source_count), stream_id, block_id,
|
|
index, source_count, parity_count, 0)
|
|
|
|
|
|
def parse_symbol(wire: bytes) -> Symbol:
|
|
"""Разобрать payload только после CRC-проверки внешнего радиопакета."""
|
|
if not isinstance(wire, bytes) or len(wire) != MAX_PHY_PAYLOAD:
|
|
raise VideoErasureError("длина внешнего символа должна быть 1012 байт")
|
|
magic, version, flags, stream_id, block_id, index, source_count, parity_count, reserved = (
|
|
struct.unpack(HEADER_FORMAT, wire[:HEADER_SIZE]))
|
|
if (magic != MAGIC or version != VERSION or flags not in (0, 1) or reserved != 0
|
|
or not 1 <= source_count <= 255
|
|
or not 1 <= parity_count <= 255 - source_count
|
|
or index >= source_count + parity_count
|
|
or bool(flags) != (index >= source_count)):
|
|
raise VideoErasureError("неверные метаданные внешнего символа")
|
|
return Symbol(stream_id, block_id, index, source_count, parity_count,
|
|
bool(flags), wire[HEADER_SIZE:])
|
|
|
|
|
|
def _source_data(fragment: VideoFragment, capture_ms: int) -> bytes:
|
|
if not 0 <= capture_ms <= 0xFFFFFFFF or len(fragment.data) > MAX_VIDEO_BYTES:
|
|
raise VideoErasureError("время или размер исходного видеофрагмента вне диапазона")
|
|
video_wire = encode_video(fragment, SYMBOL_SIZE - SOURCE_PREFIX_SIZE)
|
|
source = struct.pack(SOURCE_PREFIX_FORMAT, len(video_wire), capture_ms) + video_wire
|
|
return source + bytes(SYMBOL_SIZE - len(source))
|
|
|
|
|
|
def encode_block(fragments: Sequence[tuple[VideoFragment, int]],
|
|
block_id: int, parity_count: int) -> tuple[bytes, ...]:
|
|
"""Отправить K источников первыми, затем R проверочных символов."""
|
|
if not fragments:
|
|
raise VideoErasureError("пустой блок")
|
|
stream_id = fragments[0][0].stream_id
|
|
if any(fragment.stream_id != stream_id for fragment, _ in fragments):
|
|
raise VideoErasureError("в одном блоке разные потоки")
|
|
source_count = len(fragments)
|
|
sources = tuple(_source_data(fragment, capture_ms)
|
|
for fragment, capture_ms in fragments)
|
|
parity = encode_parity_symbols(sources, parity_count)
|
|
all_data = sources + parity
|
|
return tuple(_header(stream_id, block_id, index, source_count, parity_count) + data
|
|
for index, data in enumerate(all_data))
|
|
|
|
|
|
def decode_source(data: bytes, stream_id: int, index: int,
|
|
recovered: bool) -> Delivery:
|
|
if len(data) != SYMBOL_SIZE:
|
|
raise VideoErasureError("неверная длина исходного символа")
|
|
length, capture_ms = struct.unpack(SOURCE_PREFIX_FORMAT, data[:SOURCE_PREFIX_SIZE])
|
|
if not VIDEO_HEADER_SIZE < length <= SYMBOL_SIZE - SOURCE_PREFIX_SIZE:
|
|
raise VideoErasureError("длина восстановленного видеофрагмента вне диапазона")
|
|
end = SOURCE_PREFIX_SIZE + length
|
|
if any(data[end:]):
|
|
raise VideoErasureError("ненулевое заполнение исходного символа")
|
|
video_wire = data[SOURCE_PREFIX_SIZE:end]
|
|
fragment = decode_video(video_wire, SYMBOL_SIZE - SOURCE_PREFIX_SIZE)
|
|
if fragment.stream_id != stream_id:
|
|
raise VideoErasureError("поток в FEC и видео заголовках не совпал")
|
|
return Delivery(index, fragment, capture_ms, recovered, video_wire)
|
|
|
|
|
|
def recover_block(wires: Sequence[bytes]) -> tuple[Delivery, ...]:
|
|
"""Восстановить все K исходных символов при наличии любых K из K+R."""
|
|
if not wires:
|
|
raise InsufficientSymbolsError("нет символов блока")
|
|
symbols: dict[int, Symbol] = {}
|
|
first = parse_symbol(wires[0])
|
|
metadata = (first.stream_id, first.block_id, first.source_count, first.parity_count)
|
|
for wire in wires:
|
|
symbol = parse_symbol(wire)
|
|
if (symbol.stream_id, symbol.block_id, symbol.source_count,
|
|
symbol.parity_count) != metadata:
|
|
raise VideoErasureError("в одном блоке разные метаданные")
|
|
old = symbols.get(symbol.index)
|
|
if old is not None and old != symbol:
|
|
raise VideoErasureError("противоречивый повтор внешнего символа")
|
|
symbols[symbol.index] = symbol
|
|
k = first.source_count
|
|
if len(symbols) < k:
|
|
raise InsufficientSymbolsError(f"получено {len(symbols)} из необходимых {k}")
|
|
missing = [index for index in range(k) if index not in symbols]
|
|
if not missing:
|
|
return tuple(decode_source(symbols[index].data, first.stream_id, index, False)
|
|
for index in range(k))
|
|
selected = tuple(sorted(symbols)[:k])
|
|
generator = systematic_generator_matrix(k, first.parity_count)
|
|
inverse = matrix_inverse(tuple(generator[index] for index in selected))
|
|
received = tuple(symbols[index].data for index in selected)
|
|
return tuple(decode_source(symbols[index].data if index in symbols else
|
|
_linear_combine(inverse[index], received, SYMBOL_SIZE),
|
|
first.stream_id, index, index in missing)
|
|
for index in range(k))
|
|
|
|
|
|
@dataclass
|
|
class _PendingBlock:
|
|
source_count: int
|
|
parity_count: int
|
|
wires: dict[int, bytes] = field(default_factory=dict)
|
|
delivered: set[int] = field(default_factory=set)
|
|
|
|
|
|
class VideoErasureCollector:
|
|
"""Сразу выдаёт принятые источники; поздно восстановленные отбрасывает."""
|
|
|
|
def __init__(self, stream_id: int, max_age_ms: int = MAX_AGE_MS,
|
|
max_pending_blocks: int = 8) -> None:
|
|
if not 0 <= stream_id <= 0xFFFFFFFF or max_age_ms <= 0 or max_pending_blocks < 1:
|
|
raise ValueError("неверный поток или срок")
|
|
self.stream_id = stream_id
|
|
self.max_age_ms = max_age_ms
|
|
self.max_pending_blocks = max_pending_blocks
|
|
self.blocks: dict[int, _PendingBlock] = {}
|
|
self.closed: deque[int] = deque(maxlen=1024)
|
|
self.parity_recoveries = 0
|
|
self.stale_rejections = 0
|
|
self.unrecoverable_blocks = 0
|
|
|
|
def offer(self, wire: bytes, now_ms: int) -> tuple[Delivery, ...]:
|
|
if not isinstance(now_ms, int) or now_ms < 0:
|
|
raise VideoErasureError("неверное время приёма")
|
|
symbol = parse_symbol(wire)
|
|
if symbol.stream_id != self.stream_id:
|
|
raise VideoErasureError("чужой поток")
|
|
direct_item = None
|
|
if not symbol.is_parity:
|
|
direct_item = decode_source(symbol.data, symbol.stream_id, symbol.index, False)
|
|
if now_ms < direct_item.capture_ms:
|
|
raise VideoErasureError("фрагмент датирован будущим временем")
|
|
if symbol.block_id in self.closed:
|
|
return ()
|
|
if symbol.block_id not in self.blocks and len(self.blocks) >= self.max_pending_blocks:
|
|
oldest_id = next(iter(self.blocks))
|
|
oldest = self.blocks.pop(oldest_id)
|
|
self.unrecoverable_blocks += len(oldest.wires) < oldest.source_count
|
|
self.closed.append(oldest_id)
|
|
block = self.blocks.setdefault(symbol.block_id,
|
|
_PendingBlock(symbol.source_count, symbol.parity_count))
|
|
if (block.source_count, block.parity_count) != (symbol.source_count, symbol.parity_count):
|
|
raise VideoErasureError("противоречивые параметры блока")
|
|
old = block.wires.get(symbol.index)
|
|
if old is not None and old != wire:
|
|
raise VideoErasureError("противоречивый повтор символа")
|
|
block.wires[symbol.index] = wire
|
|
output = []
|
|
if not symbol.is_parity and symbol.index not in block.delivered:
|
|
item = direct_item
|
|
block.delivered.add(symbol.index)
|
|
if now_ms - item.capture_ms < self.max_age_ms:
|
|
output.append(item)
|
|
else:
|
|
self.stale_rejections += 1
|
|
if len(block.wires) >= block.source_count and len(block.delivered) < block.source_count:
|
|
for item in recover_block(tuple(block.wires.values())):
|
|
if item.index in block.delivered:
|
|
continue
|
|
if now_ms < item.capture_ms:
|
|
raise VideoErasureError("восстановленный фрагмент датирован будущим временем")
|
|
block.delivered.add(item.index)
|
|
if now_ms - item.capture_ms >= self.max_age_ms:
|
|
self.stale_rejections += 1
|
|
continue
|
|
self.parity_recoveries += 1
|
|
output.append(item)
|
|
if len(block.delivered) == block.source_count:
|
|
del self.blocks[symbol.block_id]
|
|
self.closed.append(symbol.block_id)
|
|
return tuple(output)
|
|
|
|
def finish(self) -> None:
|
|
self.unrecoverable_blocks += sum(
|
|
len(block.wires) < block.source_count for block in self.blocks.values())
|
|
self.blocks.clear()
|