zm::UnixSocket copies the path with a truncating strncpy, so a
PATH_SOCKS long enough to overflow sun_path made the server bind one
file while chmod, chown and unlink acted on another, and the client
connect to a truncated, different path. Both now refuse such a path with
an error. Start() also closes the listener on its own failure paths.
SendMedia indexed the two-entry sequence array with the stream id; a
caller passing StreamId::Monitor would have written past it. Ignore
anything that is not video or audio.
Tests: server and client refuse a 200 character path; a Monitor stream
id is ignored and does not disturb the video sequence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The cached snapshot was framed when the status last changed, so after a
generation bump or further events a new consumer received it stamped
with a stale generation and an old event-sequence baseline. Keep only
the body and frame it in AcceptClient, so the header carries the
generation and sequence in effect at the moment of connection.
Tests: a snapshot cached at generation 0 with no events is delivered to
a later consumer with the current generation and sequence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two problems with announcing streams one at a time. SetAudioParams only
bumped the generation when audio had been announced before, so audio
joining a video-only stream was appended to the current generation after
its video HELLO, which the protocol says is the last HELLO of a
generation. And a re-prime that changed both streams bumped twice:
consumers saw, and a connecting consumer was handed, an intermediate
generation pairing the new audio with the old video, which
zm_rtsp_server built a session for and tore down again.
Add StreamSocket::SetStreams(video, audio), which applies the whole set
under one lock: unchanged is a no-op, the first announcement stays in
generation 0, and any change once a video HELLO has gone out - new
parameters, a stream appearing, a stream disappearing - is exactly one
bump with every remaining stream re-announced, audio first. The single
stream setters and the new ClearVideoParams are wrappers over the same
logic, and a null or codec-less parameter set means "no such stream".
PrimeCapture passes both streams in one call.
Tests: audio joining an announced video stream bumps the generation;
SetStreams keeps the initial announcement in generation 0, changes both
streams in one generation with consistent pairing, and drops a video
stream the source no longer has.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Correct three problems in the stream socket generation tracking added by
the previous review fix-up:
- ClearAudioParams bumped the generation without restarting the video
sequence or dropping the cached keyframe, so a late joiner received a
HELLO at generation N+1 followed by a KEYFRAME still stamped N, and
sequences did not restart as documented. It now does the same full bump
as SetVideoParams/SetAudioParams.
- zm_rtsp_server rebuilt the xop session whenever the generation changed,
even with identical codec parameters. Generations restart at 0 when zmc
restarts, so every zmc restart dropped the RTSP clients; the original
code kept the session in that case. Unchanged parameters now just adopt
the new generation without a teardown.
- Deciding whether audio belongs to the current generation by comparing
per-stream generation numbers raced the two HELLOs of a generation
(double rebuild when Update() ran between them) and cannot tell a
producer restart apart. The producer now sends the audio HELLO before
the video HELLO within a generation (on connect and on every bump, and
PrimeCapture announces audio before video), so the video HELLO always
completes a generation's parameter set. The consumer forgets the
previous generation's HELLOs when a new generation starts and on
disconnect, and builds only once the video HELLO of the latest
generation is in. It also records whether audio was announced at build
time rather than whether a packer was created, so an unsupported audio
codec no longer triggers a rebuild on every pass.
The ordering guarantee is documented in the protocol header and the
stream socket docs. Tests pin the audio-first order on connect and on a
video reconfigure, and the keyframe drop and sequence restart on audio
removal.
refs #5143
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4UcdJLt1bxwdpcigGxZRD
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
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>
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>
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>
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>