Add FrameTag.skeleton2D (raw 6) and Skeleton2DPayload for
2D-projected ARKit joints: 91×SIMD2<Float> + 91 valid bytes,
big-endian wire format mirroring SkeletonPayload style.
Note: raw value is 6 (not 5 as drafted) because face=5 is taken.
HandsPayload.encoded() wrote min(count,255) in the header but looped
over all hands; use hands.prefix(255) so header and body agree.
Hoist the float reader func f() to the outer hand loop, matching
FacePayload style; declared once per hand instead of per point.
Add FrameTag.hands (=4) and FrameTag.face (=5) without renumbering
existing tags. Implement HandsPayload (up to 255 hands, 21 SIMD3
points each, big-endian codec) and FacePayload (confidence + N
SIMD2 landmarks). Both follow the SkeletonPayload/VideoPayload
codec pattern. Five new XCTest round-trip and rejection tests pass.
Feeds 20 framed skeleton payloads through StreamDemuxer in
7-byte chunks (worst-case TCP fragmentation). Fixed a split
range operator from the plan that did not parse.
A corrupt header with a huge UInt32 length made feed buffer
forever waiting for bytes that never arrive. Add an 8 MB max
payload cap; a header exceeding it is treated as corrupt, its
magic is skipped, and the demuxer resyncs on the next frame.
Add StreamDemuxer that accepts arbitrary byte chunks from a
non-frame-aligned stream and emits complete (FrameHeader, Data)
frames, resyncing on the magic prefix after corruption.
Add FrameHeader, a fixed-size binary record so the demuxer can
frame and resync the iPhone USB stream. Layout is big-endian:
4-byte magic AVL1, tag u8, pid i16, timestamp f64, length u32.
The magic prefix lets a reader detect and skip corrupt bytes.
Decoding rejects short buffers and bad magic by returning nil.
Big-endian append/parse helpers are added as Data/UInt extensions
to keep the codec self-contained.