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.
1104 lines
27 KiB
Go
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")
|
|
}
|
|
}
|