Files
AV-Live/data_only_viz/tests/test_iphone_usb_source.py
T
L'électron rare 57e3a200bc
CI build oscope-of / build-check (push) Has been cancelled
fix(viz): harden iphone-usb source
Fix 8 defects from whole-branch review (a79bc14..8c4d9f4):

- write_hands param: MediaPipe owns persons_hands in iphone-usb mode;
  IphoneUSBSource gets write_hands=False in multi.py
- Guard cap.start() failure: return early if USB source unavailable
- Stale frame on disconnect: read() returns (False,None) when
  _opened=False after thread exits
- ARKit validity: update only valid joints, matching OSC listener
- Decode exception: except Exception replaces removal-prone av.AVError
- release() unblock: socket.shutdown() before join wakes recv-blocked
  reader immediately
- Log stutter: remove redundant "multi —" prefix from USB log line
- Unit tests: 10 pure no-device tests for _to_annexb + _decode_hands;
  add iphone-usb optional extra (av>=12.0) to pyproject.toml
2026-06-27 15:27:06 +02:00

125 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pure (no-device) unit tests for iphone_usb_source helpers.
Tests _to_annexb and _decode_hands without any hardware or network connection.
"""
import struct
import pytest
from data_only_viz.iphone_usb_source import _to_annexb, _decode_hands, _HAND_BYTES
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def _avcc(nals: list[bytes]) -> bytes:
"""Build an AVCC-style buffer with 4-byte big-endian length prefixes."""
out = bytearray()
for nal in nals:
out += struct.pack(">I", len(nal)) + nal
return bytes(out)
def _make_hands_payload(hands: list[list[tuple[float, float, float]]]) -> bytes:
"""Build a synthetic HandsPayload matching the wire layout.
count:u8, then per hand: chirality:u8 (1=right) + 21 × (x,y,z) big-endian f32.
"""
out = bytearray()
out += bytes([len(hands)])
for hand_kps in hands:
out += bytes([1]) # chirality = right
for x, y, z in hand_kps:
out += struct.pack(">fff", x, y, z)
return bytes(out)
# ---------------------------------------------------------------------------
# _to_annexb
# ---------------------------------------------------------------------------
def test_to_annexb_two_nals():
nal1 = b"\x67\x01\x02\x03" # SPS-like
nal2 = b"\x41\xAB\xCD"
avcc = _avcc([nal1, nal2])
result = _to_annexb(avcc)
# First unit: Annex-B start code + nal1
assert result[:4] == b"\x00\x00\x00\x01"
assert result[4:4 + len(nal1)] == nal1
# Second unit: Annex-B start code + nal2
rest = result[4 + len(nal1):]
assert rest[:4] == b"\x00\x00\x00\x01"
assert rest[4:4 + len(nal2)] == nal2
assert len(result) == 4 + len(nal1) + 4 + len(nal2)
def test_to_annexb_payload_bytes_preserved():
nal = bytes(range(32))
result = _to_annexb(_avcc([nal]))
assert result[4:] == nal
def test_to_annexb_empty():
assert _to_annexb(b"") == b""
def test_to_annexb_truncated_nal_is_dropped():
# Length prefix claims 10 bytes but only 3 are present
buf = struct.pack(">I", 10) + b"\x00" * 3
result = _to_annexb(buf)
assert result == b""
def test_to_annexb_single_nal():
nal = b"\x65\xB8"
result = _to_annexb(_avcc([nal]))
assert result == b"\x00\x00\x00\x01" + nal
# ---------------------------------------------------------------------------
# _decode_hands
# ---------------------------------------------------------------------------
def test_decode_hands_empty_payload():
assert _decode_hands(b"") is None
def test_decode_hands_count_zero():
assert _decode_hands(bytes([0])) == []
def test_decode_hands_two_hands_length_and_coords():
hand_a = [(float(i) * 0.1, float(i) * 0.2, float(i) * 0.05) for i in range(21)]
hand_b = [(1.0 - float(i) * 0.03, 0.5, float(i) * 0.01) for i in range(21)]
payload = _make_hands_payload([hand_a, hand_b])
result = _decode_hands(payload)
assert isinstance(result, list)
assert len(result) == 2
for hand in result:
assert len(hand) == 21
# x/y spot-checks for hand A
assert abs(result[0][0].x - hand_a[0][0]) < 1e-5
assert abs(result[0][5].y - hand_a[5][1]) < 1e-5
# x/y spot-checks for hand B
assert abs(result[1][3].x - hand_b[3][0]) < 1e-5
assert abs(result[1][20].y - hand_b[20][1]) < 1e-5
def test_decode_hands_one_hand():
kps = [(0.1 * i, 0.2 * i, 0.0) for i in range(21)]
result = _decode_hands(_make_hands_payload([kps]))
assert isinstance(result, list)
assert len(result) == 1
assert len(result[0]) == 21
def test_decode_hands_truncated_does_not_raise():
hand_kps = [(0.1, 0.2, 0.3)] * 21
full = _make_hands_payload([hand_kps, hand_kps])
# Truncate to half — second hand will be incomplete
truncated = full[:len(full) // 2]
# Must not raise
result = _decode_hands(truncated)
assert isinstance(result, (list, type(None)))