Commit Graph
5 Commits
Author SHA1 Message Date
Claude 281a7d967e fix: harden stream socket transport and protocol from PR review
Address several stream socket review findings on the transport, its
consumer client and the wire protocol:

- ParseAllowedUids rejects negative, out-of-range and non-round-tripping
  uids instead of wrapping or truncating them (e.g. 2^32 no longer
  becomes uid 0).
- StreamSocketClient backs off after a connection the producer closes
  before any message, so a rejected consumer (uid allow-list, client
  limit) no longer busy-loops; a rejection is not reported as a
  disconnect.
- SendMedia drops packets for a stream that has no announced HELLO, and
  ClearAudioParams forgets a previously announced audio stream (bumping
  the generation and re-issuing the surviving video HELLO), so a stale
  audio HELLO is never replayed and media never precedes its HELLO.
- Header pts_us is encoded as signed (two's-complement) microseconds so
  negative and AV_NOPTS_VALUE timestamps survive the wire; the dump tool
  decodes it as signed and tracks sequence gaps per generation so a
  generation reset is not mistaken for packet loss.

Tests cover the uid rejections, the audio HELLO clearing and media
guard, the connection-rejection backoff, and signed pts round-trips.

refs #5143

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4UcdJLt1bxwdpcigGxZRD
2026-09-19 23:00:03 +00:00
Steve GilvarryandClaude Fable 5.1 c11e484eca test: use a per-process socket path in the stream socket tests
Both suites bound a fixed path under /tmp, so two test binaries running
at once (parallel ctest, a developer and CI on one box) would unlink
each other's listener. Include the pid in the path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 05:12:43 +10:00
Steve GilvarryandClaude Fable 5.1 84b1c06430 fix: drop the cached stream socket keyframe when the capture source closes
The keyframe cached for late joiners survived Monitor::Close(). A
consumer connecting while the camera reconnected was primed with a
keyframe from the previous capture session, whose pts can be ahead of
what the new session produces, and with identical stream parameters
there is no generation bump to warn it. Add StreamSocket::InvalidateKeyframe()
and call it from Close(); the next keyframe from the new session fills
the cache again.

Tests: after InvalidateKeyframe() a new consumer gets HELLO and then the
next live packet, with no KEYFRAME replay in between.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 05:12:42 +10:00
SteveGilvarryandClaude Fable 5.1 f69700eb6a feat: serve monitor lifecycle events on the stream socket
Add SendMonitorEvent() to broadcast an EVENT frame to every connected
consumer, framed with a per-monitor event sequence (independent of the
media sequences, so it is not reset by a media generation bump) and the
current media generation for correlation. Events are control messages and
are never dropped from a client queue; the sequence still advances when no
consumer is connected, so a late joiner sees the loss as a gap.

Add SetSnapshotEvent() to cache the current-status snapshot replayed to
each new consumer on connect, the events analogue of the cached keyframe.
AcceptClient now enqueues HELLO(s), the snapshot, then the keyframe.

Tests: a broadcast EVENT round-trips with the right type/stream/sequence;
the event sequence advances across a clientless gap; the snapshot is
replayed after HELLO on connect. Ran ./tests/tests '[stream_socket]':
130 assertions in 11 cases pass.

refs #2875

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 05:02:24 +10:00
SteveGilvarryandClaude Fable 5 b46be59622 feat: add StreamSocket unix-socket media server class
Per-monitor media stream server for the wire protocol added previously:
a poll()-driven listener thread serving multiple consumers at once over
PATH_SOCKS/stream_{monitor_id}.sock.

Memory and blocking behaviour:
- each message is serialized once and shared across all client queues;
  media payloads are reference-counted via av_packet_clone, no copies
- header and payload are written with writev, never concatenated
- the producer (capture thread) never blocks: per-client queues are
  bounded by bytes and message count; on overflow the oldest non-control
  messages are dropped for that client only, observable as sequence gaps
  and in STATS; clients making no progress are disconnected
- the latest keyframe access unit is cached (refcount only) and replayed
  to late joiners after HELLO for immediate first-frame rendering

Parameter changes (SetVideoParams/SetAudioParams) bump the generation,
reset sequences and rebroadcast HELLO. Peers are checked via SO_PEERCRED
against ZM_STREAM_SOCKET_ALLOWED_UIDS; sockets are chmod 0660 with group
ZM_STREAM_SOCKET_GROUP. Stop() sends BYE so consumers can distinguish
shutdown from failure.

Tests: 8 new Catch2 test cases (lifecycle/permissions, HELLO-first
ordering, late-joiner keyframe replay, queue overflow with sequence-gap
and STATS accounting, stalled-vs-live client isolation, generation bump,
BYE on stop, allowed-uids parsing). Full suite: 98/98 pass via ctest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-16 05:00:44 +10:00