Files
mediamtx/internal/core/path.go
T
Alessandro RosandGitHub f87d9e659e hls: track sessions (#962) (#5683)
sessions are now tracked through cookies or query parameters.

This provides the ability to inspect sessions through logs, metrics and
API, allows more precise tracking of outbound bytes, decreases load on
external HTTP authentication URLs since they are now called once per
session and not once per request.
2026-04-25 21:10:34 +02:00

1104 lines
27 KiB
Go

package core
import (
"context"
"fmt"
"net"
"sort"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/formatlabel"
"github.com/bluenviron/mediamtx/internal/hooks"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/recorder"
"github.com/bluenviron/mediamtx/internal/staticsources"
"github.com/bluenviron/mediamtx/internal/stream"
)
func emptyTimer() *time.Timer {
t := time.NewTimer(0)
<-t.C
return t
}
type pathParent interface {
logger.Writer
setPathReady(*path)
setPathNotReady(*path)
closePathIfIdle(*path)
removePath(*path)
AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)
}
type pathOnDemandState int
const (
pathOnDemandStateInitial pathOnDemandState = iota
pathOnDemandStateWaitingReady
pathOnDemandStateReady
pathOnDemandStateClosing
)
type pathAPIPathsListRes struct {
data *defs.APIPathList
paths map[string]*path
}
type pathAPIPathsListReq struct {
res chan pathAPIPathsListRes
}
type pathAPIPathsGetRes struct {
path *path
data *defs.APIPath
err error
}
type pathAPIPathsGetReq struct {
name string
res chan pathAPIPathsGetRes
}
type path struct {
parentCtx context.Context
logLevel conf.LogLevel
dumpPackets bool
rtspAddress string
readTimeout conf.Duration
writeTimeout conf.Duration
writeQueueSize int
udpReadBufferSize uint
rtpMaxPayloadSize int
conf *conf.Path
name string
matches []string
wg *sync.WaitGroup
externalCmdPool *externalcmd.Pool
parent pathParent
// accessed by pathManager only
ready bool
confName string
ctx context.Context
ctxCancel func()
pendingRequests atomic.Int64
confMutex sync.RWMutex
source defs.Source
stream *stream.Stream
recorder *recorder.Recorder
availableTime time.Time
onlineTime time.Time
onUnDemandHook func(string)
onNotReadyHook func()
readers map[defs.Reader]struct{}
describeRequestsOnHold []defs.PathDescribeReq
readerAddRequestsOnHold []defs.PathAddReaderReq
onDemandStaticSourceState pathOnDemandState
onDemandStaticSourceReadyTimer *time.Timer
onDemandStaticSourceCloseTimer *time.Timer
onDemandPublisherState pathOnDemandState
onDemandPublisherReadyTimer *time.Timer
onDemandPublisherCloseTimer *time.Timer
// in
chReloadConf chan *conf.Path
chStaticSourceSetReady chan defs.PathSourceStaticSetReadyReq
chStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq
chDescribe chan defs.PathDescribeReq
chAddPublisher chan defs.PathAddPublisherReq
chRemovePublisher chan defs.PathRemovePublisherReq
chAddReader chan defs.PathAddReaderReq
chRemoveReader chan defs.PathRemoveReaderReq
chAPIPathsGet chan pathAPIPathsGetReq
// out
done chan struct{}
}
func (pa *path) initialize() {
ctx, ctxCancel := context.WithCancel(pa.parentCtx)
pa.confName = pa.conf.Name
pa.ctx = ctx
pa.ctxCancel = ctxCancel
pa.readers = make(map[defs.Reader]struct{})
pa.onDemandStaticSourceReadyTimer = emptyTimer()
pa.onDemandStaticSourceCloseTimer = emptyTimer()
pa.onDemandPublisherReadyTimer = emptyTimer()
pa.onDemandPublisherCloseTimer = emptyTimer()
pa.chReloadConf = make(chan *conf.Path)
pa.chStaticSourceSetReady = make(chan defs.PathSourceStaticSetReadyReq)
pa.chStaticSourceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)
pa.chDescribe = make(chan defs.PathDescribeReq)
pa.chAddPublisher = make(chan defs.PathAddPublisherReq)
pa.chRemovePublisher = make(chan defs.PathRemovePublisherReq)
pa.chAddReader = make(chan defs.PathAddReaderReq)
pa.chRemoveReader = make(chan defs.PathRemoveReaderReq)
pa.chAPIPathsGet = make(chan pathAPIPathsGetReq)
pa.done = make(chan struct{})
pa.Log(logger.Debug, "created")
pa.wg.Add(1)
go pa.run()
}
func (pa *path) close() {
pa.ctxCancel()
}
func (pa *path) wait() {
<-pa.done
}
// Log implements logger.Writer.
func (pa *path) Log(level logger.Level, format string, args ...any) {
pa.parent.Log(level, "[path "+pa.name+"] "+format, args...)
}
func (pa *path) Name() string {
return pa.name
}
func (pa *path) isAvailable() bool {
return pa.stream != nil
}
func (pa *path) isOnline() bool {
return pa.source != nil
}
func (pa *path) run() {
defer close(pa.done)
defer pa.wg.Done()
if pa.conf.AlwaysAvailable {
err := pa.setAvailable(nil, "", nil, true)
if err != nil {
panic(err)
}
}
if pa.conf.Source == "redirect" {
pa.source = &sourceRedirect{}
} else if pa.conf.HasStaticSource() {
pa.source = &staticsources.Handler{
Conf: pa.conf,
LogLevel: pa.logLevel,
DumpPackets: pa.dumpPackets,
ReadTimeout: pa.readTimeout,
WriteTimeout: pa.writeTimeout,
WriteQueueSize: pa.writeQueueSize,
UDPReadBufferSize: pa.udpReadBufferSize,
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
Matches: pa.matches,
PathManager: pa.parent,
Parent: pa,
}
pa.source.(*staticsources.Handler).Initialize()
if !pa.conf.SourceOnDemand {
pa.source.(*staticsources.Handler).Start(false, "")
}
}
onUnInitHook := hooks.OnInit(hooks.OnInitParams{
Logger: pa,
ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf,
ExternalCmdEnv: pa.ExternalCmdEnv(),
})
err := pa.runInner()
// call before destroying context
pa.parent.removePath(pa)
pa.ctxCancel()
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceCloseTimer.Stop()
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherCloseTimer.Stop()
onUnInitHook()
for _, req := range pa.describeRequestsOnHold {
req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
}
for _, req := range pa.readerAddRequestsOnHold {
req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("terminated")}
}
if pa.stream != nil {
pa.setNotAvailable()
}
if pa.source != nil {
if source, ok := pa.source.(*staticsources.Handler); ok {
if !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial {
source.Close("path is closing")
}
} else if source, ok2 := pa.source.(defs.Publisher); ok2 {
source.Close()
}
}
if pa.onUnDemandHook != nil {
pa.onUnDemandHook("path destroyed")
}
pa.Log(logger.Debug, "destroyed: %v", err)
}
func (pa *path) runInner() error {
for {
select {
case <-pa.onDemandStaticSourceReadyTimer.C:
pa.doOnDemandStaticSourceReadyTimer()
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case <-pa.onDemandStaticSourceCloseTimer.C:
pa.doOnDemandStaticSourceCloseTimer()
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case <-pa.onDemandPublisherReadyTimer.C:
pa.doOnDemandPublisherReadyTimer()
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case <-pa.onDemandPublisherCloseTimer.C:
pa.doOnDemandPublisherCloseTimer()
case newConf := <-pa.chReloadConf:
pa.doReloadConf(newConf)
case req := <-pa.chStaticSourceSetReady:
pa.doSourceStaticSetReady(req)
case req := <-pa.chStaticSourceSetNotReady:
pa.doSourceStaticSetNotReady(req)
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case req := <-pa.chDescribe:
pa.doDescribe(req)
pa.pendingRequests.Add(-1)
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case req := <-pa.chAddPublisher:
pa.doAddPublisher(req)
pa.pendingRequests.Add(-1)
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case req := <-pa.chRemovePublisher:
pa.doRemovePublisher(req)
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case req := <-pa.chAddReader:
pa.doAddReader(req)
pa.pendingRequests.Add(-1)
if pa.shouldClose() {
pa.parent.closePathIfIdle(pa)
}
case req := <-pa.chRemoveReader:
pa.doRemoveReader(req)
case req := <-pa.chAPIPathsGet:
pa.doAPIPathsGet(req)
case <-pa.ctx.Done():
return fmt.Errorf("terminated")
}
}
}
func (pa *path) doOnDemandStaticSourceReadyTimer() {
for _, req := range pa.describeRequestsOnHold {
req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold {
req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.readerAddRequestsOnHold = nil
pa.onDemandStaticSourceStop("timed out")
}
func (pa *path) doOnDemandStaticSourceCloseTimer() {
if pa.conf.AlwaysAvailable {
panic("should not happen")
}
pa.setNotAvailable()
pa.onDemandStaticSourceStop("not needed by anyone")
}
func (pa *path) doOnDemandPublisherReadyTimer() {
for _, req := range pa.describeRequestsOnHold {
req.Res <- defs.PathDescribeRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold {
req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("source of path '%s' has timed out", pa.name)}
}
pa.readerAddRequestsOnHold = nil
pa.onDemandPublisherStop("timed out")
}
func (pa *path) doOnDemandPublisherCloseTimer() {
pa.onDemandPublisherStop("not needed by anyone")
}
func (pa *path) doReloadConf(newConf *conf.Path) {
pa.confMutex.Lock()
oldConf := pa.conf
pa.conf = newConf
pa.confMutex.Unlock()
if pa.conf.HasStaticSource() {
pa.source.(*staticsources.Handler).ReloadConf(newConf)
}
if pa.recorder != nil &&
(newConf.Record != oldConf.Record ||
newConf.RecordPath != oldConf.RecordPath ||
newConf.RecordFormat != oldConf.RecordFormat ||
newConf.RecordPartDuration != oldConf.RecordPartDuration ||
newConf.RecordMaxPartSize != oldConf.RecordMaxPartSize ||
newConf.RecordSegmentDuration != oldConf.RecordSegmentDuration ||
newConf.RecordDeleteAfter != oldConf.RecordDeleteAfter) {
pa.recorder.Close()
pa.recorder = nil
}
if newConf.Record && pa.stream != nil && pa.recorder == nil {
pa.startRecording()
}
}
func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
if !pa.conf.AlwaysAvailable {
err := pa.setAvailable(pa.source, "", req.Desc, req.ReplaceNTP)
if err != nil {
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return
}
}
subStream := &stream.SubStream{
Stream: pa.stream,
UseRTPPackets: req.UseRTPPackets,
}
if pa.conf.AlwaysAvailable {
subStream.CurDesc = req.Desc
}
err := subStream.Initialize()
if err != nil {
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return
}
if pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.HasOnDemandStaticSource() {
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = emptyTimer()
pa.onDemandStaticSourceScheduleClose()
}
pa.consumeOnHoldRequests()
req.Res <- defs.PathSourceStaticSetReadyRes{SubStream: subStream}
}
func (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
if !pa.conf.AlwaysAvailable {
pa.setNotAvailable()
} else {
err := pa.stream.StartOfflineSubStream()
if err != nil {
panic("should not happen")
}
}
// send response before calling onDemandStaticSourceStop()
// in order to avoid a deadlock due to staticsources.Handler.stop()
close(req.Res)
if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial {
pa.onDemandStaticSourceStop("an error occurred")
}
}
func (pa *path) doDescribe(req defs.PathDescribeReq) {
if _, ok := pa.source.(*sourceRedirect); ok {
req.Res <- defs.PathDescribeRes{
Redirect: pa.conf.SourceRedirect,
}
return
}
if pa.stream != nil {
req.Res <- defs.PathDescribeRes{
Stream: pa.stream,
}
return
}
if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
pa.onDemandStaticSourceStart(req.AccessRequest.Query)
}
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
return
}
if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial {
pa.onDemandPublisherStart(req.AccessRequest.Query)
}
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)
return
}
if pa.conf.Fallback != nil {
req.Res <- defs.PathDescribeRes{Redirect: *pa.conf.Fallback}
return
}
req.Res <- defs.PathDescribeRes{Err: &defs.PathNoStreamAvailableError{PathName: pa.name}}
}
func (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {
if pa.source == req.Author {
pa.executeRemovePublisher()
}
close(req.Res)
}
func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {
if pa.conf.Source != "publisher" {
req.Res <- defs.PathAddPublisherRes{
Err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name),
}
return
}
if pa.source != nil {
if !pa.conf.OverridePublisher {
req.Res <- defs.PathAddPublisherRes{Err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)}
return
}
pa.Log(logger.Info, "closing existing publisher")
pa.source.(defs.Publisher).Close()
pa.executeRemovePublisher()
}
if !pa.conf.AlwaysAvailable {
err := pa.setAvailable(req.Author, req.AccessRequest.Query, req.Desc, req.ReplaceNTP)
if err != nil {
req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
}
subStream := &stream.SubStream{
Stream: pa.stream,
UseRTPPackets: req.UseRTPPackets,
}
if pa.conf.AlwaysAvailable {
subStream.CurDesc = req.Desc
}
err := subStream.Initialize()
if err != nil {
req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
pa.source = req.Author
req.Author.Log(logger.Info, "is publishing to path '%s'",
pa.name)
if pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial {
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherReadyTimer = emptyTimer()
pa.onDemandPublisherScheduleClose()
}
pa.consumeOnHoldRequests()
req.Res <- defs.PathAddPublisherRes{SubStream: subStream}
}
func (pa *path) doAddReader(req defs.PathAddReaderReq) {
if pa.stream != nil {
pa.addReaderPost(req)
return
}
if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateInitial {
pa.onDemandStaticSourceStart(req.AccessRequest.Query)
}
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
return
}
if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateInitial {
pa.onDemandPublisherStart(req.AccessRequest.Query)
}
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)
return
}
req.Res <- defs.PathAddReaderRes{Err: &defs.PathNoStreamAvailableError{PathName: pa.name}}
}
func (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {
if _, ok := pa.readers[req.Author]; ok {
pa.executeRemoveReader(req.Author)
}
close(req.Res)
if len(pa.readers) == 0 {
if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateReady {
pa.onDemandStaticSourceScheduleClose()
}
} else if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateReady {
pa.onDemandPublisherScheduleClose()
}
}
}
}
func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
req.res <- pathAPIPathsGetRes{
data: &defs.APIPath{
Name: pa.name,
ConfName: pa.conf.Name,
Ready: pa.isAvailable(),
ReadyTime: func() *time.Time {
if !pa.isAvailable() {
return nil
}
v := pa.availableTime
return &v
}(),
Available: pa.isAvailable(),
AvailableTime: func() *time.Time {
if !pa.isAvailable() {
return nil
}
v := pa.availableTime
return &v
}(),
Online: pa.isOnline(),
OnlineTime: func() *time.Time {
if !pa.isOnline() {
return nil
}
v := pa.onlineTime
return &v
}(),
Source: func() *defs.APIPathSource {
if pa.source == nil {
return nil
}
v := pa.source.APISourceDescribe()
return v
}(),
Tracks: func() []defs.APIPathTrackCodec {
if !pa.isAvailable() {
return []defs.APIPathTrackCodec{}
}
return formatlabel.MediasToLabels(pa.stream.Desc.Medias)
}(),
Tracks2: func() []defs.APIPathTrack {
if !pa.isAvailable() {
return []defs.APIPathTrack{}
}
return defs.MediasToTracks(pa.stream.Desc.Medias)
}(),
InboundBytes: func() uint64 {
if !pa.isAvailable() {
return 0
}
return pa.stream.InboundBytes()
}(),
OutboundBytes: func() uint64 {
if !pa.isAvailable() {
return 0
}
return pa.stream.OutboundBytes()
}(),
InboundFramesInError: func() uint64 {
if !pa.isAvailable() {
return 0
}
return pa.stream.InboundFramesInError()
}(),
BytesReceived: func() uint64 {
if !pa.isAvailable() {
return 0
}
return pa.stream.InboundBytes()
}(),
BytesSent: func() uint64 {
if !pa.isAvailable() {
return 0
}
return pa.stream.OutboundBytes()
}(),
Readers: func() []defs.APIPathReader {
ret := make([]defs.APIPathReader, 0, len(pa.readers))
for r := range pa.readers {
desc := *r.APIReaderDescribe()
if desc.Type != defs.APIPathReaderTypeHidden {
ret = append(ret, desc)
}
}
sort.Slice(ret, func(i, j int) bool {
if ret[i].Type != ret[j].Type {
return ret[i].Type < ret[j].Type
}
return ret[i].ID < ret[j].ID
})
return ret
}(),
},
}
}
func (pa *path) SafeConf() *conf.Path {
pa.confMutex.RLock()
defer pa.confMutex.RUnlock()
return pa.conf
}
func (pa *path) ExternalCmdEnv() externalcmd.Environment {
_, port, _ := net.SplitHostPort(pa.rtspAddress)
env := externalcmd.Environment{
"MTX_PATH": pa.name,
"RTSP_PATH": pa.name, // deprecated
"RTSP_PORT": port,
}
if len(pa.matches) > 1 {
for i, ma := range pa.matches[1:] {
env["G"+strconv.FormatInt(int64(i+1), 10)] = ma
}
}
return env
}
func (pa *path) shouldClose() bool {
return pa.conf.Regexp != nil &&
pa.source == nil &&
len(pa.readers) == 0 &&
len(pa.describeRequestsOnHold) == 0 &&
len(pa.readerAddRequestsOnHold) == 0
}
func (pa *path) onDemandStaticSourceStart(query string) {
pa.source.(*staticsources.Handler).Start(true, query)
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
pa.onDemandStaticSourceState = pathOnDemandStateWaitingReady
}
func (pa *path) onDemandStaticSourceScheduleClose() {
pa.onDemandStaticSourceCloseTimer.Stop()
pa.onDemandStaticSourceCloseTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandCloseAfter))
pa.onDemandStaticSourceState = pathOnDemandStateClosing
}
func (pa *path) onDemandStaticSourceStop(reason string) {
if pa.onDemandStaticSourceState == pathOnDemandStateClosing {
pa.onDemandStaticSourceCloseTimer.Stop()
pa.onDemandStaticSourceCloseTimer = emptyTimer()
}
pa.onDemandStaticSourceState = pathOnDemandStateInitial
pa.source.(*staticsources.Handler).Stop(reason)
}
func (pa *path) onDemandPublisherStart(query string) {
pa.onUnDemandHook = hooks.OnDemand(hooks.OnDemandParams{
Logger: pa,
ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf,
ExternalCmdEnv: pa.ExternalCmdEnv(),
Query: query,
})
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout))
pa.onDemandPublisherState = pathOnDemandStateWaitingReady
}
func (pa *path) onDemandPublisherScheduleClose() {
pa.onDemandPublisherCloseTimer.Stop()
pa.onDemandPublisherCloseTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandCloseAfter))
pa.onDemandPublisherState = pathOnDemandStateClosing
}
func (pa *path) onDemandPublisherStop(reason string) {
if pa.onDemandPublisherState == pathOnDemandStateClosing {
pa.onDemandPublisherCloseTimer.Stop()
pa.onDemandPublisherCloseTimer = emptyTimer()
}
pa.onUnDemandHook(reason)
pa.onUnDemandHook = nil
pa.onDemandPublisherState = pathOnDemandStateInitial
}
func (pa *path) setAvailable(
source defs.Source,
publisherQuery string,
desc *description.Session,
replaceNTP bool,
) error {
pa.stream = &stream.Stream{
Desc: desc,
AlwaysAvailable: pa.conf.AlwaysAvailable,
AlwaysAvailableTracks: pa.conf.AlwaysAvailableTracks,
AlwaysAvailableFile: pa.conf.AlwaysAvailableFile,
WriteQueueSize: pa.writeQueueSize,
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
ReplaceNTP: replaceNTP,
Parent: pa,
}
err := pa.stream.Initialize()
if err != nil {
return err
}
pa.availableTime = time.Now()
if !pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.Record {
pa.startRecording()
}
var sourceDesc *defs.APIPathSource
if source != nil {
sourceDesc = source.APISourceDescribe()
}
pa.onNotReadyHook = hooks.OnReady(hooks.OnReadyParams{
Logger: pa,
ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf,
ExternalCmdEnv: pa.ExternalCmdEnv(),
Desc: sourceDesc,
Query: publisherQuery,
})
if pa.conf.AlwaysAvailable {
pa.Log(logger.Info, "stream is available, %s", defs.MediasInfo(pa.stream.Desc.Medias))
} else {
pa.Log(logger.Info, "stream is available and online, %s", defs.MediasInfo(pa.stream.Desc.Medias))
}
pa.parent.setPathReady(pa)
return nil
}
func (pa *path) consumeOnHoldRequests() {
for _, req := range pa.describeRequestsOnHold {
req.Res <- defs.PathDescribeRes{
Stream: pa.stream,
}
}
pa.describeRequestsOnHold = nil
for _, req := range pa.readerAddRequestsOnHold {
pa.addReaderPost(req)
}
pa.readerAddRequestsOnHold = nil
}
func (pa *path) setNotAvailable() {
pa.parent.setPathNotReady(pa)
for r := range pa.readers {
pa.executeRemoveReader(r)
r.Close()
}
pa.onNotReadyHook()
if pa.recorder != nil {
pa.recorder.Close()
pa.recorder = nil
}
if pa.stream != nil {
pa.stream.Close()
pa.stream = nil
}
}
func (pa *path) startRecording() {
pa.recorder = &recorder.Recorder{
PathFormat: pa.conf.RecordPath,
Format: pa.conf.RecordFormat,
PartDuration: time.Duration(pa.conf.RecordPartDuration),
MaxPartSize: pa.conf.RecordMaxPartSize,
SegmentDuration: time.Duration(pa.conf.RecordSegmentDuration),
PathName: pa.name,
Stream: pa.stream,
OnSegmentCreate: func(segmentPath string) {
if pa.conf.RunOnRecordSegmentCreate != "" {
env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = segmentPath
pa.Log(logger.Info, "runOnRecordSegmentCreate command launched")
externalcmd.NewCmd(
pa.externalCmdPool,
pa.conf.RunOnRecordSegmentCreate,
false,
env,
nil)
}
},
OnSegmentComplete: func(segmentPath string, segmentDuration time.Duration) {
if pa.conf.RunOnRecordSegmentComplete != "" {
env := pa.ExternalCmdEnv()
env["MTX_SEGMENT_PATH"] = segmentPath
env["MTX_SEGMENT_DURATION"] = strconv.FormatFloat(segmentDuration.Seconds(), 'f', -1, 64)
pa.Log(logger.Info, "runOnRecordSegmentComplete command launched")
externalcmd.NewCmd(
pa.externalCmdPool,
pa.conf.RunOnRecordSegmentComplete,
false,
env,
nil)
}
},
Parent: pa,
}
pa.recorder.Initialize()
}
func (pa *path) executeRemoveReader(r defs.Reader) {
delete(pa.readers, r)
}
func (pa *path) executeRemovePublisher() {
if !pa.conf.AlwaysAvailable {
pa.setNotAvailable()
} else {
err := pa.stream.StartOfflineSubStream()
if err != nil {
panic("should not happen")
}
}
pa.source = nil
}
func (pa *path) addReaderPost(req defs.PathAddReaderReq) {
if _, ok := pa.readers[req.Author]; ok {
req.Res <- defs.PathAddReaderRes{Stream: pa.stream}
return
}
if pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders {
req.Res <- defs.PathAddReaderRes{Err: fmt.Errorf("maximum reader count reached")}
return
}
pa.readers[req.Author] = struct{}{}
if pa.conf.HasOnDemandStaticSource() {
if pa.onDemandStaticSourceState == pathOnDemandStateClosing {
pa.onDemandStaticSourceState = pathOnDemandStateReady
pa.onDemandStaticSourceCloseTimer.Stop()
pa.onDemandStaticSourceCloseTimer = emptyTimer()
}
} else if pa.conf.HasOnDemandPublisher() {
if pa.onDemandPublisherState == pathOnDemandStateClosing {
pa.onDemandPublisherState = pathOnDemandStateReady
pa.onDemandPublisherCloseTimer.Stop()
pa.onDemandPublisherCloseTimer = emptyTimer()
}
}
req.Res <- defs.PathAddReaderRes{Stream: pa.stream}
}
// reloadConf is called by pathManager.
func (pa *path) reloadConf(newConf *conf.Path) {
select {
case pa.chReloadConf <- newConf:
case <-pa.ctx.Done():
}
}
// StaticSourceHandlerSetReady is called by staticsources.Handler.
func (pa *path) StaticSourceHandlerSetReady(
ctx context.Context, req defs.PathSourceStaticSetReadyReq,
) {
select {
case pa.chStaticSourceSetReady <- req:
case <-pa.ctx.Done():
req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
// this avoids:
// - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop()
case <-ctx.Done():
req.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
}
}
// StaticSourceHandlerSetNotReady is called by staticsources.Handler.
func (pa *path) StaticSourceHandlerSetNotReady(
ctx context.Context, req defs.PathSourceStaticSetNotReadyReq,
) {
select {
case pa.chStaticSourceSetNotReady <- req:
case <-pa.ctx.Done():
close(req.Res)
// this avoids:
// - invalid requests sent after the source has been terminated
// - deadlocks caused by <-done inside stop()
case <-ctx.Done():
close(req.Res)
}
}
// describe is called by a reader or publisher through pathManager.
func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes {
select {
case pa.chDescribe <- req:
return <-req.Res
case <-pa.ctx.Done():
return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
}
}
// addPublisher is called by a publisher through pathManager.
func (pa *path) addPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {
select {
case pa.chAddPublisher <- req:
res := <-req.Res
return &res, res.Err
case <-pa.ctx.Done():
return nil, fmt.Errorf("terminated")
}
}
// RemovePublisher is called by a publisher.
func (pa *path) RemovePublisher(req defs.PathRemovePublisherReq) {
req.Res = make(chan struct{})
select {
case pa.chRemovePublisher <- req:
<-req.Res
case <-pa.ctx.Done():
}
}
// addReader is called by a reader through pathManager.
func (pa *path) addReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
select {
case pa.chAddReader <- req:
res := <-req.Res
return &res, res.Err
case <-pa.ctx.Done():
return nil, fmt.Errorf("terminated")
}
}
// RemoveReader is called by a reader.
func (pa *path) RemoveReader(req defs.PathRemoveReaderReq) {
req.Res = make(chan struct{})
select {
case pa.chRemoveReader <- req:
<-req.Res
case <-pa.ctx.Done():
}
}
// APIPathsGet implements defs.APIPathManager.
func (pa *path) APIPathsGet(req pathAPIPathsGetReq) (*defs.APIPath, error) {
req.res = make(chan pathAPIPathsGetRes)
select {
case pa.chAPIPathsGet <- req:
res := <-req.res
return res.data, res.err
case <-pa.ctx.Done():
return nil, fmt.Errorf("terminated")
}
}