feat: add fallbackSource path option for seamless standby-to-live switching - #5934
feat: add fallbackSource path option for seamless standby-to-live switching#5934poeggi wants to merge 15 commits into
Conversation
|
ready for review |
|
Identified some issues during testing - working on it. |
When a path has fallbackSource configured, mediamtx connects to a
standby pull-source (RTSP, RTMP, SRT, or any supported URL) and
switches to it seamlessly when the live publisher drops -- without
disconnecting readers.
Two modes:
preconnect (default): fallback stays connected at all times;
switch on primary-drop is instant (SubStream pointer swap only).
ondemand: fallback is started only when primary drops, stopped
when primary reconnects; saves upstream bandwidth at the cost
of a connect-time gap.
Implementation:
- conf.Path: FallbackSource + FallbackSourceMode fields + validation
- stream.Stream: HasFallbackSource flag; ActivateSubStream() for
direct pointer swaps without re-running Initialize()
- stream.SubStream: FallbackSwap flag allows Initialize() to swap
an already-active SubStream (mediasAreCompatible check applies)
- core.path: fallbackHandlerParent adapter routes handler callbacks
to dedicated channels; doFallbackSourceSetReady/NotReady manage
the three-state machine (OFFLINE / FALLBACK / PRIMARY)
Known limitations / TODOs:
- No IDR-gated switch: swap happens on first available packet, not
necessarily at a keyframe boundary (clients may show a brief glitch)
- preconnect parking uses a double-swap workaround when fallback
connects while primary is active; a proper park-without-activating
SubStream API would eliminate this
- fallbackSource is not yet exposed in the API response schema
…, docs IDR-gated switching (stream/sub_stream.go): - Split Initialize() into SetupFormats() + activate() + ScheduleActivation() - SetupFormats sets up per-format codec state without activating the SubStream - ScheduleActivation defers the pointer swap to the next keyframe via pendingActivation - isKeyframeUnit() detects IDR for H.264 (NAL type 5) and H.265 (types 19/20); all other codecs (audio, AV1, VP8/VP9) are treated as keyframes so switching is never unnecessarily delayed - WriteUnit checks pendingActivation before the RLock; CompareAndSwap ensures exactly one goroutine triggers the swap even under concurrent access Clean preconnect parking (core/path.go): - doFallbackSourceSetReady: calls SetupFormats then ScheduleActivation (not active yet) when primary is absent; when primary is active, SubStream is left parked — no double-swap hack needed - doAddPublisher (FallbackSource + stream exists): SetupFormats + ScheduleActivation so primary activates on its first keyframe without displacing fallback mid-GOP - executeRemovePublisher: calls ScheduleActivation on the already-parked fallback SubStream; it activates on the next keyframe it delivers API schema (api/openapi.yaml): - Re-generated; fallbackSource and fallbackSourceMode now appear under PathConf Config documentation (mediamtx.yml): - New section documents fallbackSource and fallbackSourceMode with examples of both preconnect and ondemand modes
On every source swap (alwaysAvailable file<->live, fallbackSource primary<->fallback, either direction), reset the video track's RTP encoder with a new SSRC and timestamp offset so downstream decoders receive the standard RTP-layer reset signal. This handles PTS discontinuities, codec parameter changes, and timing gaps without requiring the client to detect the specific cause of the switch. Reset is scoped to H264/H265 video tracks only. The SSRC write happens in the same goroutine that owns the subsequent video write path, eliminating any concurrent reader race. Audio SSRC is left unchanged — audio decoders do not need a reset on source swap and benefit from timestamp continuity. Enabled by default (sourceSwapSSRCReset: true). Set to false to disable.
…ce; fix non-ASCII comment char
- Activate() now clears pendingActivation before firing, preventing a scheduled keyframe from double-activating the same SubStream - add CancelActivation() to SubStream; call it when primary connects so a parked fallback SubStream cannot override the live primary - set FallbackSwap=true before Activate() on revert so SPS/PPS and SSRC reset are injected when switching back to the fallback - guard ondemand fallbackHandler.Start() with !fallbackHandlerRunning to prevent double-start on repeated primary drop/reconnect cycles - call setNotAvailable() when preconnect fallback has not yet connected, so readers are not silently stuck on a dead stream - remove dead primarySubStream field (set in three places, read nowhere)
…et on source swap
edf196d to
379ef62
Compare
|
Tested the full failover cycle end-to-end with a local H.264 RTSP publisher (cam) and an AlwaysAvailable H.264 filler path as the FallbackSource. Test config: Cycle observed (log evidence):
All four transitions logged correctly and cleanly. No stuck state, no double-activation, no stream gap beyond one GOP on reconnect. RTP timestamps: confirmed pass-through for UseRTPPackets=true sources (primary publisher). gortsplib only overwrites SSRC on the server send path -- Timestamp field is untouched. Clients signal a source swap via timestamp discontinuity from the RTP encoder reset (committed in �df196d7). Branch also rebased onto upstream main as of today (2026-07-19), incorporating the offline_sub_stream_track.go PTS fix (#5960) and the UDP port range change (#5958). |
Summary
Adds
fallbackSource-- a path-level option that designates a secondary static source (RTSP, RTMP, SRT, etc.) to take over seamlessly when the primary publisher is absent, then steps back down when the primary reconnects.Includes fixes and improvements that also help the
alwaysAvailable/alwaysAvailableFilelive-recovery path, discovered during real-world testing:Fixes/improvements developed during debugging
Gate live reactivation on first keyframe
When a live source reconnects on an
alwaysAvailablepath, the SubStream now waits for the first IDR before becoming active. Prevents clients from receiving non-IDR video at stream start.Require video IDR specifically, not audio frames
isKeyframeUnit()returnstruefor all audio payload types (default case). Without this fix, an audio frame arriving before the video IDR would trigger activation, unblocking non-IDR video. Fixed by checkinginMedia.Type == MediaTypeVideowhen the stream has video tracks.Pass live source PTS through unchanged
On live reconnect,
ptsOffsetis set to 0 so the encoder's absolute PTS is forwarded as-is. Previously,ptsOffsetwas incorrectly set tolastPTS + elapsedfor live sources, inflating timestamps on every reconnect. Offline fallback PTS continues to uselastPTS + elapsedfor smooth continuation.RTP encoder reset on source swap
On any source swap (either direction, either mode), the video track's RTP encoder is recreated with a new timestamp base and packetizer state. This produces a timestamp and sequence-number discontinuity that downstream decoders can use to detect the source change and resync. Scoped to H264/H265 video only (audio encoder stays continuous). Suppressed on the initial offline activation at startup -- only fires when a prior live source actually existed. Note: for RTSP readers the SSRC field is rewritten per-session by gortsplib's server layer and does not change; the observable client signal is the timestamp discontinuity.
Docs: alternative source format requirements
Documents that any alternative source -- whether an offline media file or a fallback stream -- ideally matches the primary stream's codec type and audio parameters; video resolution may differ (SPS/PPS are swapped automatically on source switch).
Not in this PR
#EXT-X-DISCONTINUITYand MPEG-TS discontinuity indicator on source swap (possible follow-up feature)