Files
mediamtx/internal/staticsources/handler.go
T
Alessandro RosandGitHub a56408db19 add always available streams (#5335)
When the publisher or source of a stream is offline, the server can be
configured to fill gaps in the stream with a video that is played on
repeat until a publisher comes back online. This allows readers to stay
connected regardless of the state of the stream. The offline video and
any future online stream are concatenated without decoding or
re-encoding packets, using the original codec.
2026-01-31 14:44:58 +01:00

333 lines
8.0 KiB
Go

// Package staticsources contains static source implementations.
package staticsources
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger"
sshls "github.com/bluenviron/mediamtx/internal/staticsources/hls"
ssmpegts "github.com/bluenviron/mediamtx/internal/staticsources/mpegts"
ssrpicamera "github.com/bluenviron/mediamtx/internal/staticsources/rpicamera"
ssrtmp "github.com/bluenviron/mediamtx/internal/staticsources/rtmp"
ssrtp "github.com/bluenviron/mediamtx/internal/staticsources/rtp"
ssrtsp "github.com/bluenviron/mediamtx/internal/staticsources/rtsp"
sssrt "github.com/bluenviron/mediamtx/internal/staticsources/srt"
sswebrtc "github.com/bluenviron/mediamtx/internal/staticsources/webrtc"
"github.com/bluenviron/mediamtx/internal/stream"
)
const (
retryPause = 5 * time.Second
)
func emptyTimer() *time.Timer {
t := time.NewTimer(0)
<-t.C
return t
}
func resolveSource(s string, matches []string, query string) string {
if len(matches) > 1 {
for i, ma := range matches[1:] {
s = strings.ReplaceAll(s, "$G"+strconv.FormatInt(int64(i+1), 10), ma)
}
}
s = strings.ReplaceAll(s, "$MTX_QUERY", query)
return s
}
type staticSource interface {
logger.Writer
Run(defs.StaticSourceRunParams) error
APISourceDescribe() *defs.APIPathSource
}
type handlerPathManager interface {
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
type handlerParent interface {
logger.Writer
StaticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
StaticSourceHandlerSetNotReady(context.Context, defs.PathSourceStaticSetNotReadyReq)
}
// Handler is a static source handler.
type Handler struct {
Conf *conf.Path
LogLevel conf.LogLevel
ReadTimeout conf.Duration
WriteTimeout conf.Duration
WriteQueueSize int
UDPReadBufferSize uint
RTPMaxPayloadSize int
Matches []string
PathManager handlerPathManager
Parent handlerParent
ctx context.Context
ctxCancel func()
instance staticSource
running bool
query string
// in
chReloadConf chan *conf.Path
chInstanceSetReady chan defs.PathSourceStaticSetReadyReq
chInstanceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
// out
done chan struct{}
}
// Initialize initializes Handler.
func (s *Handler) Initialize() {
s.chReloadConf = make(chan *conf.Path)
s.chInstanceSetReady = make(chan defs.PathSourceStaticSetReadyReq)
s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
switch {
case strings.HasPrefix(s.Conf.Source, "rtsp://") ||
strings.HasPrefix(s.Conf.Source, "rtsps://") ||
strings.HasPrefix(s.Conf.Source, "rtsp+http://") ||
strings.HasPrefix(s.Conf.Source, "rtsps+http://") ||
strings.HasPrefix(s.Conf.Source, "rtsp+ws://") ||
strings.HasPrefix(s.Conf.Source, "rtsps+ws://"):
s.instance = &ssrtsp.Source{
ReadTimeout: s.ReadTimeout,
WriteTimeout: s.WriteTimeout,
WriteQueueSize: s.WriteQueueSize,
UDPReadBufferSize: s.UDPReadBufferSize,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "rtmp://") ||
strings.HasPrefix(s.Conf.Source, "rtmps://"):
s.instance = &ssrtmp.Source{
ReadTimeout: s.ReadTimeout,
WriteTimeout: s.WriteTimeout,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "http://") ||
strings.HasPrefix(s.Conf.Source, "https://"):
s.instance = &sshls.Source{
ReadTimeout: s.ReadTimeout,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "udp://") ||
strings.HasPrefix(s.Conf.Source, "udp+mpegts://") ||
strings.HasPrefix(s.Conf.Source, "unix+mpegts://"):
s.instance = &ssmpegts.Source{
ReadTimeout: s.ReadTimeout,
UDPReadBufferSize: s.UDPReadBufferSize,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "srt://"):
s.instance = &sssrt.Source{
ReadTimeout: s.ReadTimeout,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "whep://") ||
strings.HasPrefix(s.Conf.Source, "wheps://"):
s.instance = &sswebrtc.Source{
ReadTimeout: s.ReadTimeout,
UDPReadBufferSize: s.UDPReadBufferSize,
Parent: s,
}
case strings.HasPrefix(s.Conf.Source, "udp+rtp://") ||
strings.HasPrefix(s.Conf.Source, "unix+rtp://"):
s.instance = &ssrtp.Source{
ReadTimeout: s.ReadTimeout,
UDPReadBufferSize: s.UDPReadBufferSize,
Parent: s,
}
case s.Conf.Source == "rpiCamera":
s.instance = &ssrpicamera.Source{
RTPMaxPayloadSize: s.RTPMaxPayloadSize,
LogLevel: s.LogLevel,
Parent: s,
}
default:
panic("should not happen")
}
}
// Close closes Handler.
func (s *Handler) Close(reason string) {
s.Stop(reason)
}
// Start starts Handler.
func (s *Handler) Start(onDemand bool, query string) {
if s.running {
panic("should not happen")
}
s.running = true
s.query = query
s.ctx, s.ctxCancel = context.WithCancel(context.Background())
s.done = make(chan struct{})
s.instance.Log(logger.Info, "started%s",
func() string {
if onDemand {
return " on demand"
}
return ""
}())
go s.run()
}
// Stop stops Handler.
func (s *Handler) Stop(reason string) {
if !s.running {
panic("should not happen")
}
s.running = false
s.instance.Log(logger.Info, "stopped: %s", reason)
s.ctxCancel()
// we must wait since s.ctx is not thread safe
<-s.done
}
// Log implements logger.Writer.
func (s *Handler) Log(level logger.Level, format string, args ...any) {
s.Parent.Log(level, format, args...)
}
func (s *Handler) run() {
defer close(s.done)
var runCtx context.Context
var runCtxCancel func()
runErr := make(chan error)
runReloadConf := make(chan *conf.Path)
recreate := func() {
resolvedSource := resolveSource(s.Conf.Source, s.Matches, s.query)
runCtx, runCtxCancel = context.WithCancel(context.Background())
go func() {
runErr <- s.instance.Run(defs.StaticSourceRunParams{
Context: runCtx,
ResolvedSource: resolvedSource,
Conf: s.Conf,
ReloadConf: runReloadConf,
})
}()
}
recreate()
recreating := false
recreateTimer := emptyTimer()
for {
select {
case err := <-runErr:
runCtxCancel()
s.instance.Log(logger.Error, err.Error())
recreating = true
recreateTimer = time.NewTimer(retryPause)
case req := <-s.chInstanceSetReady:
s.Parent.StaticSourceHandlerSetReady(s.ctx, req)
case req := <-s.chInstanceSetNotReady:
s.Parent.StaticSourceHandlerSetNotReady(s.ctx, req)
case newConf := <-s.chReloadConf:
s.Conf = newConf
if !recreating {
cReloadConf := runReloadConf
cInnerCtx := runCtx
go func() {
select {
case cReloadConf <- newConf:
case <-cInnerCtx.Done():
}
}()
}
case <-recreateTimer.C:
recreate()
recreating = false
case <-s.ctx.Done():
if !recreating {
runCtxCancel()
<-runErr
}
return
}
}
}
// ReloadConf is called by path.
func (s *Handler) ReloadConf(newConf *conf.Path) {
ctx := s.ctx
if !s.running {
return
}
go func() {
select {
case s.chReloadConf <- newConf:
case <-ctx.Done():
}
}()
}
// APISourceDescribe instanceements source.
func (s *Handler) APISourceDescribe() *defs.APIPathSource {
return s.instance.APISourceDescribe()
}
// SetReady is called by a staticSource.
func (s *Handler) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes {
req.Res = make(chan defs.PathSourceStaticSetReadyRes)
select {
case s.chInstanceSetReady <- req:
return <-req.Res
case <-s.ctx.Done():
return defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
}
}
// SetNotReady is called by a staticSource.
func (s *Handler) SetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
req.Res = make(chan struct{})
select {
case s.chInstanceSetNotReady <- req:
<-req.Res
case <-s.ctx.Done():
}
}
// AddReader is called by a staticSource.
func (s *Handler) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return s.PathManager.AddReader(req)
}