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.
333 lines
8.0 KiB
Go
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)
|
|
}
|