- Add cache *keyFrameCache field to Source (nil by default).
- Add EnableKeyFrameCache() — call before Start() on video sources to
activate IDR burst caching.
- readLoop: call cache.push(pkt) after each successful unmarshal, before
the subscriber fanout. No lock held at push time — push acquires its
own mutex internally.
- Subscribe: snapshot the cache outside s.mu to avoid any cross-lock
complexity, then pre-fill the new channel with the burst before
registering it in the subscriber set. Uses a labeled break to stop
pre-filling if the channel is full (bufDepth too small for the burst;
the subscriber will wait for the next live keyframe instead).
Introduces keyFrameCache — a bounded ring buffer that retains all RTP
packets from the most recent H.264 IDR NAL unit until the packet just
before the next one. New WHEP subscribers receive this burst immediately
on Subscribe(), cutting first-frame latency from up to one IDR interval
(typically ~2 s at GOP=60/30fps) to nearly zero.
Design notes:
- Detection covers single-NAL (type 5) and FU-A start (type 28, start
bit set, inner type 5). STAP-A IDR leading is not handled — FFmpeg
never uses STAP-A for IDR slices in practice.
- Bounded at 512 packets / 2 MiB per source to cap memory per stream.
- push() is called only from the single-goroutine readLoop; the lock
it holds is tiny and brief.
- snapshot() returns a shallow copy; *rtp.Packet values are immutable
after being placed in the cache so sharing is safe.
IngestPeer is the symmetric inverse of the WHEP Peer:
- Creates a recvonly PeerConnection (Pion receives tracks from the publisher)
- OnTrack -> reads RTP packets from the remote track and writes them to
loopback UDP ports (videoPort, audioPort) that FFmpeg is listening on
- Full lifecycle: Done(), Connected(), Close(), AddICECandidate()
PeerFactory.CreateIngestPeer() follows the same ctx/offer/ICE-gather
pattern as CreatePeerFromSources() so the app/webrtc handler layer can
use a uniform error matrix.
Two small additions to support the M3 handler:
- Peer.Done() — read-only view of the existing 'done' channel,
closed on Close(). Lets external indexes (Handler, admin API)
await peer teardown without polling.
- Peer.AddICECandidate — passthrough so the WHEP PATCH handler
can forward trickle-ICE candidates without reaching into the
PeerConnection directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces the subsystem layer that sits alongside api.API and wires
the M1 core/webrtc primitives into the per-process restream lifecycle.
app/webrtc/subsystem.go:
- Subsystem struct holding the global WebRTC config, core PeerFactory,
per-process stream map, and logger
- New(config.DataWebRTC, logger) constructor
- Enabled(), Hooks(), Close(), lookup() methods
app/webrtc/lifecycle.go:
- onProcessStart: allocates an adjacent UDP port pair, binds two
Pion Sources (video on V, audio on V+1), registers them under the
process id, and returns the two RTP output legs to append to the
FFmpeg command.
- onProcessStop: tears down the pair.
- allocAdjacentPair: retries up to 10 times to find a free (V, V+1)
pair since the kernel's ephemeral picker can hand us an odd port.
- splitRTPLegs: converts BuildArgs' flat []string into two ConfigIO
entries by splitting on the second -map token.
core/webrtc/peer.go + forward.go:
- Adds PeerFactory.CreatePeerFromSources for the M2 two-source
forwarding mode (video and audio on separate UDP ports, no
payload-type sniffing). Leaves CreatePeer intact for the M1 PoC.
- Adds forwardRTPSplit companion goroutine.
config/data.go:
- Promote anonymous WebRTC struct to named type DataWebRTC so
app/webrtc can accept it by value.
- core/webrtc: NewSourceOn(streamID, host, port) allows binding the
RTP UDP socket on something other than 127.0.0.1, required when the
PoC runs in a container and must accept RTP from LAN publishers.
NewSource(streamID, port) stays as a convenience wrapper on
127.0.0.1 for existing tests and tight local tests.
- cmd/webrtc-poc: new -rtp-host flag (default 127.0.0.1 for safety).
- deploy/docker/Dockerfile: two-stage build, scratch runtime, ~14 MB.
- deploy/truenas/docker-compose.yml: host-networked stack template
driven by a .env file. Host networking is required for WebRTC ICE
to work without NAT rewriting per-candidate.
- deploy/truenas/README.md: operator runbook with port picking,
bring-up, verification curls, and security notes.