Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,12 @@ components:
type: string
nullable: true
deprecated: true
fallbackSource:
type: string
description: Fallback source
fallbackSourceMode:
type: string
description: "\"preconnect\" (default) or \"ondemand\""
maxReaders:
type: integer
format: int64
Expand Down
8 changes: 8 additions & 0 deletions docs/2-features/08-always-available.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ paths:
alwaysAvailable: true
alwaysAvailableFile: "./h264.mp4"
```

Any alternative source -- whether an offline media file or a fallback stream -- must be format-compatible with the primary live stream:

* The number of tracks and their codec types must match exactly (e.g. one H264 video track and one MPEG4Audio audio track).
* Audio parameters (sample rate, channel count, AAC profile) must match exactly. A mismatch causes the source to be rejected at connection time.
* Video resolution and bitrate may differ; SPS/PPS are swapped automatically on source switch.

The safest approach is to configure all sources with the same encoder settings.
1 change: 1 addition & 0 deletions internal/conf/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestConfFromFile(t *testing.T) {
SourceOnDemandCloseAfter: 10 * Duration(time.Second),
OverridePublisher: true,
AlwaysAvailableTracks: []AlwaysAvailableTrack{},
FallbackSourceMode: "preconnect",
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
RecordFormat: RecordFormatFMP4,
RecordPartDuration: Duration(1 * time.Second),
Expand Down
35 changes: 35 additions & 0 deletions internal/conf/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ type Path struct {
AlwaysAvailableTracks []AlwaysAvailableTrack `json:"alwaysAvailableTracks"`
AlwaysAvailableFile string `json:"alwaysAvailableFile"`

// Fallback source
FallbackSource string `json:"fallbackSource"`
FallbackSourceMode string `json:"fallbackSourceMode"` // "preconnect" (default) or "ondemand"

// Record
Record bool `json:"record"`
Playback *bool `json:"playback,omitempty" deprecated:"true"`
Expand Down Expand Up @@ -371,6 +375,8 @@ func (pconf *Path) setDefaults() {
pconf.RecordSegmentDuration = 3600 * Duration(time.Second)
pconf.RecordDeleteAfter = 24 * 3600 * Duration(time.Second)

// Fallback source
pconf.FallbackSourceMode = "preconnect"
// Publisher source
pconf.OverridePublisher = true

Expand Down Expand Up @@ -826,6 +832,30 @@ func (pconf *Path) validate(
}
}

// Fallback source

if pconf.FallbackSource != "" {
if pconf.Source != "publisher" {
return fmt.Errorf("'fallbackSource' can only be used when 'source' is 'publisher'")
}
if pconf.Regexp != nil {
return fmt.Errorf("'fallbackSource' cannot be used in a path with a regular expression")
}
if pconf.AlwaysAvailable {
return fmt.Errorf("'fallbackSource' and 'alwaysAvailable' cannot be used together")
}
if pconf.SourceOnDemand {
return fmt.Errorf("'fallbackSource' is not compatible with 'sourceOnDemand'")
}
_, err := validateURL(pconf.FallbackSource)
if err != nil {
return fmt.Errorf("invalid 'fallbackSource': %w", err)
}
if pconf.FallbackSourceMode != "preconnect" && pconf.FallbackSourceMode != "ondemand" {
return fmt.Errorf("'fallbackSourceMode' must be 'preconnect' or 'ondemand'")
}
}

// Record

if pconf.Playback != nil {
Expand Down Expand Up @@ -974,3 +1004,8 @@ func (pconf Path) HasOnDemandStaticSource() bool {
func (pconf Path) HasOnDemandPublisher() bool {
return pconf.RunOnDemand != ""
}

// HasFallbackSource checks whether the path has a fallback source configured.
func (pconf Path) HasFallbackSource() bool {
return pconf.FallbackSource != ""
}
Loading