Skip to content

feat: add fallbackSource path option for seamless standby-to-live switching - #5934

Open
poeggi wants to merge 15 commits into
bluenviron:mainfrom
poeggi:feat/fallback-source
Open

feat: add fallbackSource path option for seamless standby-to-live switching#5934
poeggi wants to merge 15 commits into
bluenviron:mainfrom
poeggi:feat/fallback-source

Conversation

@poeggi

@poeggi poeggi commented Jul 8, 2026

Copy link
Copy Markdown

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 / alwaysAvailableFile live-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 alwaysAvailable path, 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() returns true for 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 checking inMedia.Type == MediaTypeVideo when the stream has video tracks.

Pass live source PTS through unchanged
On live reconnect, ptsOffset is set to 0 so the encoder's absolute PTS is forwarded as-is. Previously, ptsOffset was incorrectly set to lastPTS + elapsed for live sources, inflating timestamps on every reconnect. Offline fallback PTS continues to use lastPTS + elapsed for 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

  • HLS #EXT-X-DISCONTINUITY and MPEG-TS discontinuity indicator on source swap (possible follow-up feature)

@poeggi

poeggi commented Jul 8, 2026

Copy link
Copy Markdown
Author

ready for review

@poeggi
poeggi marked this pull request as draft July 8, 2026 08:07
@poeggi

poeggi commented Jul 8, 2026

Copy link
Copy Markdown
Author

Identified some issues during testing - working on it.

poeggi added 14 commits July 19, 2026 07:56
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.
- 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)
@poeggi
poeggi force-pushed the feat/fallback-source branch from edf196d to 379ef62 Compare July 19, 2026 06:00
@poeggi

poeggi commented Jul 19, 2026

Copy link
Copy Markdown
Author

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:
yaml paths: filler: alwaysAvailable: yes alwaysAvailableTracks: - codec: H264 cam: fallbackSource: rtsp://127.0.0.1:8554/filler fallbackSourceMode: preconnect

Cycle observed (log evidence):

  1. Startup: filler available, fallback source connects and parks, cam comes online with fallback stream.
  2. Primary publisher connects: activates on first keyframe, fallback steps aside.
  3. Primary disconnects: fallback activates immediately (no IDR gate on primary-drop path).
  4. Primary reconnects: activates on next keyframe, fallback steps back down.

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).

@poeggi
poeggi marked this pull request as ready for review July 19, 2026 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant