""" Transport packets for synchronous BASE + ROI JPEG composite frames. The wire header has a fixed 32-byte, padding-free network-byte-order layout:: !4sBBHIHHHHIII Offset Size Field 0 4 magic (b"SRV1") 4 1 version 5 1 object_type (1=BASE, 2=ROI) 6 2 header_size 8 4 composite_frame_id 12 2 fragment_index 14 2 fragment_count 16 2 payload_length 18 2 flags (reserved, must be zero) 20 4 jpeg_size 24 4 object_crc32 28 4 packet_crc32 packet_crc32 is calculated over the complete header with packet_crc32 set to zero, followed by the packet payload. object_crc32 is calculated over the complete JPEG before fragmentation and checked after reassembly. """ from __future__ import annotations from dataclasses import dataclass, field from enum import IntEnum import struct import zlib MAGIC = b"SRV1" VERSION = 1 HEADER_FORMAT = "!4sBBHIHHHHIII" HEADER_SIZE = struct.calcsize(HEADER_FORMAT) MAX_PAYLOAD_LENGTH = 0xFFFF MAX_FRAGMENT_COUNT = 0xFFFF MAX_JPEG_SIZE = 0xFFFFFFFF class ObjectType(IntEnum): """JPEG object carried by a packet.""" BASE = 1 ROI = 2 class VideoPacketError(ValueError): """Base class for video transport validation errors.""" class PacketCRCError(VideoPacketError): """The packet header or payload failed its CRC32 check.""" class ObjectCRCError(VideoPacketError): """A completely reassembled JPEG failed its CRC32 check.""" class ObjectConsistencyError(VideoPacketError): """Fragments for one object contain inconsistent metadata or data.""" @dataclass(frozen=True) class VideoPacket: """Decoded and CRC-checked transport packet.""" composite_frame_id: int object_type: ObjectType fragment_index: int fragment_count: int jpeg_size: int object_crc32: int payload: bytes packet_crc32: int = 0 @dataclass(frozen=True) class CompositeFrame: """Atomically reassembled BASE and ROI JPEGs for one source update.""" composite_frame_id: int base_jpeg: bytes roi_jpeg: bytes def crc32(data: bytes) -> int: """Return an unsigned IEEE CRC32.""" return zlib.crc32(data) & 0xFFFFFFFF def _validate_packet_fields(packet: VideoPacket) -> None: if not 0 <= packet.composite_frame_id <= 0xFFFFFFFF: raise VideoPacketError("composite_frame_id is outside uint32") if packet.object_type not in (ObjectType.BASE, ObjectType.ROI): raise VideoPacketError("unsupported object_type") if not 1 <= packet.fragment_count <= MAX_FRAGMENT_COUNT: raise VideoPacketError("fragment_count is outside uint16") if not 0 <= packet.fragment_index < packet.fragment_count: raise VideoPacketError("fragment_index is outside fragment_count") if not 1 <= len(packet.payload) <= MAX_PAYLOAD_LENGTH: raise VideoPacketError("payload length is outside uint16") if not 1 <= packet.jpeg_size <= MAX_JPEG_SIZE: raise VideoPacketError("jpeg_size is outside uint32") if len(packet.payload) > packet.jpeg_size: raise VideoPacketError("payload is larger than the JPEG object") if not 0 <= packet.object_crc32 <= 0xFFFFFFFF: raise VideoPacketError("object_crc32 is outside uint32") def _pack_header(packet: VideoPacket, packet_crc32: int) -> bytes: return struct.pack( HEADER_FORMAT, MAGIC, VERSION, int(packet.object_type), HEADER_SIZE, packet.composite_frame_id, packet.fragment_index, packet.fragment_count, len(packet.payload), 0, packet.jpeg_size, packet.object_crc32, packet_crc32, ) def encode_packet(packet: VideoPacket) -> bytes: """Serialize a packet and calculate its packet CRC32.""" if not isinstance(packet, VideoPacket): raise TypeError("packet must be VideoPacket") _validate_packet_fields(packet) header_with_zero_crc = _pack_header(packet, 0) packet_crc32 = crc32(header_with_zero_crc + packet.payload) return _pack_header(packet, packet_crc32) + packet.payload def decode_packet(wire_packet: bytes) -> VideoPacket: """Deserialize a packet and validate its fixed header and packet CRC32.""" if not isinstance(wire_packet, (bytes, bytearray)): raise TypeError("wire_packet must be bytes or bytearray") wire_packet = bytes(wire_packet) if len(wire_packet) < HEADER_SIZE + 1: raise VideoPacketError("packet is shorter than header plus payload") ( magic, version, object_type_value, header_size, composite_frame_id, fragment_index, fragment_count, payload_length, flags, jpeg_size, object_crc32, received_packet_crc32, ) = struct.unpack(HEADER_FORMAT, wire_packet[:HEADER_SIZE]) if magic != MAGIC: raise VideoPacketError("invalid packet magic") if version != VERSION: raise VideoPacketError("unsupported packet version") if header_size != HEADER_SIZE: raise VideoPacketError("invalid fixed header size") if flags != 0: raise VideoPacketError("reserved flags must be zero") try: object_type = ObjectType(object_type_value) except ValueError as error: raise VideoPacketError("unsupported object_type") from error if len(wire_packet) != HEADER_SIZE + payload_length: raise VideoPacketError("packet length does not match payload_length") packet = VideoPacket( composite_frame_id=composite_frame_id, object_type=object_type, fragment_index=fragment_index, fragment_count=fragment_count, jpeg_size=jpeg_size, object_crc32=object_crc32, payload=wire_packet[HEADER_SIZE:], packet_crc32=received_packet_crc32, ) _validate_packet_fields(packet) calculated_packet_crc32 = crc32( _pack_header(packet, 0) + packet.payload ) if calculated_packet_crc32 != received_packet_crc32: raise PacketCRCError( "packet CRC mismatch: " f"received 0x{received_packet_crc32:08X}, " f"calculated 0x{calculated_packet_crc32:08X}" ) return packet def packetize_jpeg( jpeg: bytes, composite_frame_id: int, object_type: ObjectType, max_payload_length: int, ) -> list[bytes]: """Split one non-empty JPEG object into serialized packets.""" if not isinstance(jpeg, (bytes, bytearray)): raise TypeError("jpeg must be bytes or bytearray") jpeg = bytes(jpeg) if not jpeg: raise ValueError("JPEG object must not be empty") if len(jpeg) > MAX_JPEG_SIZE: raise ValueError("JPEG object is larger than uint32") if not 1 <= max_payload_length <= MAX_PAYLOAD_LENGTH: raise ValueError("max_payload_length is outside uint16") if not 0 <= composite_frame_id <= 0xFFFFFFFF: raise ValueError("composite_frame_id is outside uint32") try: object_type = ObjectType(object_type) except ValueError as error: raise ValueError("unsupported object_type") from error fragment_count = ( len(jpeg) + max_payload_length - 1 ) // max_payload_length if fragment_count > MAX_FRAGMENT_COUNT: raise ValueError("JPEG needs more than 65535 fragments") object_crc32 = crc32(jpeg) packets = [] for fragment_index in range(fragment_count): start = fragment_index * max_payload_length payload = jpeg[start:start + max_payload_length] packets.append( encode_packet( VideoPacket( composite_frame_id=composite_frame_id, object_type=object_type, fragment_index=fragment_index, fragment_count=fragment_count, jpeg_size=len(jpeg), object_crc32=object_crc32, payload=payload, ) ) ) return packets @dataclass class _ObjectAssembly: composite_frame_id: int object_type: ObjectType fragment_count: int jpeg_size: int object_crc32: int fragments: dict[int, bytes] = field(default_factory=dict) @classmethod def from_packet(cls, packet: VideoPacket) -> "_ObjectAssembly": return cls( composite_frame_id=packet.composite_frame_id, object_type=packet.object_type, fragment_count=packet.fragment_count, jpeg_size=packet.jpeg_size, object_crc32=packet.object_crc32, ) def add(self, packet: VideoPacket) -> bool: metadata = ( packet.composite_frame_id, packet.object_type, packet.fragment_count, packet.jpeg_size, packet.object_crc32, ) expected = ( self.composite_frame_id, self.object_type, self.fragment_count, self.jpeg_size, self.object_crc32, ) if metadata != expected: raise ObjectConsistencyError( "fragment metadata conflicts with object assembly" ) existing = self.fragments.get(packet.fragment_index) if existing is not None: if existing != packet.payload: raise ObjectConsistencyError( "different payload for an existing fragment index" ) return False self.fragments[packet.fragment_index] = packet.payload return True def missing_fragments(self) -> tuple[int, ...]: return tuple( index for index in range(self.fragment_count) if index not in self.fragments ) def assemble_if_complete(self) -> bytes | None: if self.missing_fragments(): return None jpeg = b"".join( self.fragments[index] for index in range(self.fragment_count) ) if len(jpeg) != self.jpeg_size: raise ObjectConsistencyError( "reassembled JPEG length does not match jpeg_size" ) calculated_object_crc32 = crc32(jpeg) if calculated_object_crc32 != self.object_crc32: raise ObjectCRCError( "object CRC mismatch: " f"received 0x{self.object_crc32:08X}, " f"calculated 0x{calculated_object_crc32:08X}" ) return jpeg class CompositeReassembler: """Reassemble interleaved BASE/ROI packets and publish atomic frames.""" def __init__(self) -> None: self._objects: dict[ tuple[int, ObjectType], _ObjectAssembly ] = {} self._completed: dict[ int, dict[ObjectType, bytes] ] = {} self._finalized_frame_ids: set[int] = set() self.duplicate_packets = 0 def ingest(self, wire_packet: bytes) -> CompositeFrame | None: """Accept one packet and return a frame only when BASE and ROI exist.""" packet = decode_packet(wire_packet) if packet.composite_frame_id in self._finalized_frame_ids: self.duplicate_packets += 1 return None key = (packet.composite_frame_id, packet.object_type) assembly = self._objects.get(key) if assembly is None: assembly = _ObjectAssembly.from_packet(packet) self._objects[key] = assembly if not assembly.add(packet): self.duplicate_packets += 1 return None jpeg = assembly.assemble_if_complete() if jpeg is None: return None frame_parts = self._completed.setdefault( packet.composite_frame_id, {} ) frame_parts[packet.object_type] = jpeg if not all( object_type in frame_parts for object_type in (ObjectType.BASE, ObjectType.ROI) ): return None frame = CompositeFrame( composite_frame_id=packet.composite_frame_id, base_jpeg=frame_parts[ObjectType.BASE], roi_jpeg=frame_parts[ObjectType.ROI], ) self._finalized_frame_ids.add(packet.composite_frame_id) self._completed.pop(packet.composite_frame_id, None) for object_type in (ObjectType.BASE, ObjectType.ROI): self._objects.pop( (packet.composite_frame_id, object_type), None ) return frame def missing_fragments( self, composite_frame_id: int, object_type: ObjectType, ) -> tuple[int, ...] | None: """Return missing indexes, or None if this object has not started.""" assembly = self._objects.get( (composite_frame_id, ObjectType(object_type)) ) if assembly is None: return None return assembly.missing_fragments() def object_is_complete( self, composite_frame_id: int, object_type: ObjectType, ) -> bool: """Report whether one CRC-validated object awaits its counterpart.""" return ObjectType(object_type) in self._completed.get( composite_frame_id, {} )