golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1048 lines
26 KiB
1048 lines
26 KiB
package core |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"net" |
|
"strconv" |
|
"strings" |
|
"sync" |
|
"sync/atomic" |
|
"time" |
|
|
|
"github.com/bluenviron/gortsplib/v3/pkg/media" |
|
"github.com/bluenviron/gortsplib/v3/pkg/url" |
|
|
|
"github.com/bluenviron/mediamtx/internal/conf" |
|
"github.com/bluenviron/mediamtx/internal/externalcmd" |
|
"github.com/bluenviron/mediamtx/internal/logger" |
|
) |
|
|
|
func newEmptyTimer() *time.Timer { |
|
t := time.NewTimer(0) |
|
<-t.C |
|
return t |
|
} |
|
|
|
type pathErrAuth struct { |
|
wrapped error |
|
} |
|
|
|
// Error implements the error interface. |
|
func (e pathErrAuth) Error() string { |
|
return "authentication error" |
|
} |
|
|
|
type pathErrNoOnePublishing struct { |
|
pathName string |
|
} |
|
|
|
// Error implements the error interface. |
|
func (e pathErrNoOnePublishing) Error() string { |
|
return fmt.Sprintf("no one is publishing to path '%s'", e.pathName) |
|
} |
|
|
|
type pathParent interface { |
|
logger.Writer |
|
pathSourceReady(*path) |
|
pathSourceNotReady(*path) |
|
onPathClose(*path) |
|
} |
|
|
|
type pathOnDemandState int |
|
|
|
const ( |
|
pathOnDemandStateInitial pathOnDemandState = iota |
|
pathOnDemandStateWaitingReady |
|
pathOnDemandStateReady |
|
pathOnDemandStateClosing |
|
) |
|
|
|
type pathSourceStaticSetReadyRes struct { |
|
stream *stream |
|
err error |
|
} |
|
|
|
type pathSourceStaticSetReadyReq struct { |
|
medias media.Medias |
|
generateRTPPackets bool |
|
res chan pathSourceStaticSetReadyRes |
|
} |
|
|
|
type pathSourceStaticSetNotReadyReq struct { |
|
res chan struct{} |
|
} |
|
|
|
type pathReaderRemoveReq struct { |
|
author reader |
|
res chan struct{} |
|
} |
|
|
|
type pathPublisherRemoveReq struct { |
|
author publisher |
|
res chan struct{} |
|
} |
|
|
|
type pathGetPathConfRes struct { |
|
conf *conf.PathConf |
|
err error |
|
} |
|
|
|
type pathGetPathConfReq struct { |
|
name string |
|
publish bool |
|
credentials authCredentials |
|
res chan pathGetPathConfRes |
|
} |
|
|
|
type pathDescribeRes struct { |
|
path *path |
|
stream *stream |
|
redirect string |
|
err error |
|
} |
|
|
|
type pathDescribeReq struct { |
|
pathName string |
|
url *url.URL |
|
credentials authCredentials |
|
res chan pathDescribeRes |
|
} |
|
|
|
type pathReaderSetupPlayRes struct { |
|
path *path |
|
stream *stream |
|
err error |
|
} |
|
|
|
type pathReaderAddReq struct { |
|
author reader |
|
pathName string |
|
skipAuth bool |
|
credentials authCredentials |
|
res chan pathReaderSetupPlayRes |
|
} |
|
|
|
type pathPublisherAnnounceRes struct { |
|
path *path |
|
err error |
|
} |
|
|
|
type pathPublisherAddReq struct { |
|
author publisher |
|
pathName string |
|
skipAuth bool |
|
credentials authCredentials |
|
res chan pathPublisherAnnounceRes |
|
} |
|
|
|
type pathPublisherRecordRes struct { |
|
stream *stream |
|
err error |
|
} |
|
|
|
type pathPublisherStartReq struct { |
|
author publisher |
|
medias media.Medias |
|
generateRTPPackets bool |
|
res chan pathPublisherRecordRes |
|
} |
|
|
|
type pathPublisherStopReq struct { |
|
author publisher |
|
res chan struct{} |
|
} |
|
|
|
type pathAPISourceOrReader struct { |
|
Type string `json:"type"` |
|
ID string `json:"id"` |
|
} |
|
|
|
type pathAPIPathsListItem struct { |
|
ConfName string `json:"confName"` |
|
Conf *conf.PathConf `json:"conf"` |
|
Source interface{} `json:"source"` |
|
SourceReady bool `json:"sourceReady"` |
|
Tracks []string `json:"tracks"` |
|
BytesReceived uint64 `json:"bytesReceived"` |
|
Readers []interface{} `json:"readers"` |
|
} |
|
|
|
type pathAPIPathsListData struct { |
|
Items map[string]pathAPIPathsListItem `json:"items"` |
|
} |
|
|
|
type pathAPIPathsListRes struct { |
|
data *pathAPIPathsListData |
|
paths map[string]*path |
|
err error |
|
} |
|
|
|
type pathAPIPathsListReq struct { |
|
res chan pathAPIPathsListRes |
|
} |
|
|
|
type pathAPIPathsListSubReq struct { |
|
data *pathAPIPathsListData |
|
res chan struct{} |
|
} |
|
|
|
type path struct { |
|
rtspAddress string |
|
readTimeout conf.StringDuration |
|
writeTimeout conf.StringDuration |
|
readBufferCount int |
|
udpMaxPayloadSize int |
|
confName string |
|
conf *conf.PathConf |
|
name string |
|
matches []string |
|
wg *sync.WaitGroup |
|
externalCmdPool *externalcmd.Pool |
|
parent pathParent |
|
|
|
ctx context.Context |
|
ctxCancel func() |
|
confMutex sync.RWMutex |
|
source source |
|
bytesReceived *uint64 |
|
stream *stream |
|
readers map[reader]struct{} |
|
describeRequestsOnHold []pathDescribeReq |
|
readerAddRequestsOnHold []pathReaderAddReq |
|
onDemandCmd *externalcmd.Cmd |
|
onReadyCmd *externalcmd.Cmd |
|
onDemandStaticSourceState pathOnDemandState |
|
onDemandStaticSourceReadyTimer *time.Timer |
|
onDemandStaticSourceCloseTimer *time.Timer |
|
onDemandPublisherState pathOnDemandState |
|
onDemandPublisherReadyTimer *time.Timer |
|
onDemandPublisherCloseTimer *time.Timer |
|
|
|
// in |
|
chReloadConf chan *conf.PathConf |
|
chSourceStaticSetReady chan pathSourceStaticSetReadyReq |
|
chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq |
|
chDescribe chan pathDescribeReq |
|
chPublisherRemove chan pathPublisherRemoveReq |
|
chPublisherAdd chan pathPublisherAddReq |
|
chPublisherStart chan pathPublisherStartReq |
|
chPublisherStop chan pathPublisherStopReq |
|
chReaderAdd chan pathReaderAddReq |
|
chReaderRemove chan pathReaderRemoveReq |
|
chAPIPathsList chan pathAPIPathsListSubReq |
|
|
|
// out |
|
done chan struct{} |
|
} |
|
|
|
func newPath( |
|
parentCtx context.Context, |
|
rtspAddress string, |
|
readTimeout conf.StringDuration, |
|
writeTimeout conf.StringDuration, |
|
readBufferCount int, |
|
udpMaxPayloadSize int, |
|
confName string, |
|
cnf *conf.PathConf, |
|
name string, |
|
matches []string, |
|
wg *sync.WaitGroup, |
|
externalCmdPool *externalcmd.Pool, |
|
parent pathParent, |
|
) *path { |
|
ctx, ctxCancel := context.WithCancel(parentCtx) |
|
|
|
pa := &path{ |
|
rtspAddress: rtspAddress, |
|
readTimeout: readTimeout, |
|
writeTimeout: writeTimeout, |
|
readBufferCount: readBufferCount, |
|
udpMaxPayloadSize: udpMaxPayloadSize, |
|
confName: confName, |
|
conf: cnf, |
|
name: name, |
|
matches: matches, |
|
wg: wg, |
|
externalCmdPool: externalCmdPool, |
|
parent: parent, |
|
ctx: ctx, |
|
ctxCancel: ctxCancel, |
|
bytesReceived: new(uint64), |
|
readers: make(map[reader]struct{}), |
|
onDemandStaticSourceReadyTimer: newEmptyTimer(), |
|
onDemandStaticSourceCloseTimer: newEmptyTimer(), |
|
onDemandPublisherReadyTimer: newEmptyTimer(), |
|
onDemandPublisherCloseTimer: newEmptyTimer(), |
|
chReloadConf: make(chan *conf.PathConf), |
|
chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq), |
|
chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq), |
|
chDescribe: make(chan pathDescribeReq), |
|
chPublisherRemove: make(chan pathPublisherRemoveReq), |
|
chPublisherAdd: make(chan pathPublisherAddReq), |
|
chPublisherStart: make(chan pathPublisherStartReq), |
|
chPublisherStop: make(chan pathPublisherStopReq), |
|
chReaderAdd: make(chan pathReaderAddReq), |
|
chReaderRemove: make(chan pathReaderRemoveReq), |
|
chAPIPathsList: make(chan pathAPIPathsListSubReq), |
|
done: make(chan struct{}), |
|
} |
|
|
|
pa.Log(logger.Debug, "created") |
|
|
|
pa.wg.Add(1) |
|
go pa.run() |
|
|
|
return pa |
|
} |
|
|
|
func (pa *path) close() { |
|
pa.ctxCancel() |
|
} |
|
|
|
func (pa *path) wait() { |
|
<-pa.done |
|
} |
|
|
|
// Log is the main logging function. |
|
func (pa *path) Log(level logger.Level, format string, args ...interface{}) { |
|
pa.parent.Log(level, "[path "+pa.name+"] "+format, args...) |
|
} |
|
|
|
func (pa *path) safeConf() *conf.PathConf { |
|
pa.confMutex.RLock() |
|
defer pa.confMutex.RUnlock() |
|
return pa.conf |
|
} |
|
|
|
func (pa *path) run() { |
|
defer close(pa.done) |
|
defer pa.wg.Done() |
|
|
|
if pa.conf.Source == "redirect" { |
|
pa.source = &sourceRedirect{} |
|
} else if pa.conf.HasStaticSource() { |
|
pa.source = newSourceStatic( |
|
pa.conf, |
|
pa.readTimeout, |
|
pa.writeTimeout, |
|
pa.readBufferCount, |
|
pa) |
|
|
|
if !pa.conf.SourceOnDemand { |
|
pa.source.(*sourceStatic).start() |
|
} |
|
} |
|
|
|
var onInitCmd *externalcmd.Cmd |
|
if pa.conf.RunOnInit != "" { |
|
pa.Log(logger.Info, "runOnInit command started") |
|
onInitCmd = externalcmd.NewCmd( |
|
pa.externalCmdPool, |
|
pa.conf.RunOnInit, |
|
pa.conf.RunOnInitRestart, |
|
pa.externalCmdEnv(), |
|
func(co int) { |
|
pa.Log(logger.Info, "runOnInit command exited with code %d", co) |
|
}) |
|
} |
|
|
|
err := func() error { |
|
for { |
|
select { |
|
case <-pa.onDemandStaticSourceReadyTimer.C: |
|
for _, req := range pa.describeRequestsOnHold { |
|
req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} |
|
} |
|
pa.describeRequestsOnHold = nil |
|
|
|
for _, req := range pa.readerAddRequestsOnHold { |
|
req.res <- pathReaderSetupPlayRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} |
|
} |
|
pa.readerAddRequestsOnHold = nil |
|
|
|
pa.onDemandStaticSourceStop() |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case <-pa.onDemandStaticSourceCloseTimer.C: |
|
pa.sourceSetNotReady() |
|
pa.onDemandStaticSourceStop() |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case <-pa.onDemandPublisherReadyTimer.C: |
|
for _, req := range pa.describeRequestsOnHold { |
|
req.res <- pathDescribeRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} |
|
} |
|
pa.describeRequestsOnHold = nil |
|
|
|
for _, req := range pa.readerAddRequestsOnHold { |
|
req.res <- pathReaderSetupPlayRes{err: fmt.Errorf("source of path '%s' has timed out", pa.name)} |
|
} |
|
pa.readerAddRequestsOnHold = nil |
|
|
|
pa.onDemandPublisherStop() |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case <-pa.onDemandPublisherCloseTimer.C: |
|
pa.onDemandPublisherStop() |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case newConf := <-pa.chReloadConf: |
|
if pa.conf.HasStaticSource() { |
|
go pa.source.(*sourceStatic).reloadConf(newConf) |
|
} |
|
|
|
pa.confMutex.Lock() |
|
pa.conf = newConf |
|
pa.confMutex.Unlock() |
|
|
|
case req := <-pa.chSourceStaticSetReady: |
|
err := pa.sourceSetReady(req.medias, req.generateRTPPackets) |
|
if err != nil { |
|
req.res <- pathSourceStaticSetReadyRes{err: err} |
|
} else { |
|
if pa.conf.HasOnDemandStaticSource() { |
|
pa.onDemandStaticSourceReadyTimer.Stop() |
|
pa.onDemandStaticSourceReadyTimer = newEmptyTimer() |
|
|
|
pa.onDemandStaticSourceScheduleClose() |
|
|
|
for _, req := range pa.describeRequestsOnHold { |
|
req.res <- pathDescribeRes{ |
|
stream: pa.stream, |
|
} |
|
} |
|
pa.describeRequestsOnHold = nil |
|
|
|
for _, req := range pa.readerAddRequestsOnHold { |
|
pa.handleReaderAddPost(req) |
|
} |
|
pa.readerAddRequestsOnHold = nil |
|
} |
|
|
|
req.res <- pathSourceStaticSetReadyRes{stream: pa.stream} |
|
} |
|
|
|
case req := <-pa.chSourceStaticSetNotReady: |
|
pa.sourceSetNotReady() |
|
|
|
// send response before calling onDemandStaticSourceStop() |
|
// in order to avoid a deadlock due to sourceStatic.stop() |
|
close(req.res) |
|
|
|
if pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial { |
|
pa.onDemandStaticSourceStop() |
|
} |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case req := <-pa.chDescribe: |
|
pa.handleDescribe(req) |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case req := <-pa.chPublisherRemove: |
|
pa.handlePublisherRemove(req) |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case req := <-pa.chPublisherAdd: |
|
pa.handlePublisherAdd(req) |
|
|
|
case req := <-pa.chPublisherStart: |
|
pa.handlePublisherStart(req) |
|
|
|
case req := <-pa.chPublisherStop: |
|
pa.handlePublisherStop(req) |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case req := <-pa.chReaderAdd: |
|
pa.handleReaderAdd(req) |
|
|
|
if pa.shouldClose() { |
|
return fmt.Errorf("not in use") |
|
} |
|
|
|
case req := <-pa.chReaderRemove: |
|
pa.handleReaderRemove(req) |
|
|
|
case req := <-pa.chAPIPathsList: |
|
pa.handleAPIPathsList(req) |
|
|
|
case <-pa.ctx.Done(): |
|
return fmt.Errorf("terminated") |
|
} |
|
} |
|
}() |
|
|
|
// call before destroying context |
|
pa.parent.onPathClose(pa) |
|
|
|
pa.ctxCancel() |
|
|
|
pa.onDemandStaticSourceReadyTimer.Stop() |
|
pa.onDemandStaticSourceCloseTimer.Stop() |
|
pa.onDemandPublisherReadyTimer.Stop() |
|
pa.onDemandPublisherCloseTimer.Stop() |
|
|
|
if onInitCmd != nil { |
|
onInitCmd.Close() |
|
pa.Log(logger.Info, "runOnInit command stopped") |
|
} |
|
|
|
for _, req := range pa.describeRequestsOnHold { |
|
req.res <- pathDescribeRes{err: fmt.Errorf("terminated")} |
|
} |
|
|
|
for _, req := range pa.readerAddRequestsOnHold { |
|
req.res <- pathReaderSetupPlayRes{err: fmt.Errorf("terminated")} |
|
} |
|
|
|
if pa.stream != nil { |
|
pa.sourceSetNotReady() |
|
} |
|
|
|
if pa.source != nil { |
|
if source, ok := pa.source.(*sourceStatic); ok { |
|
source.close() |
|
} else if source, ok := pa.source.(publisher); ok { |
|
source.close() |
|
} |
|
} |
|
|
|
if pa.onDemandCmd != nil { |
|
pa.onDemandCmd.Close() |
|
pa.Log(logger.Info, "runOnDemand command stopped") |
|
} |
|
|
|
pa.Log(logger.Debug, "destroyed (%v)", err) |
|
} |
|
|
|
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) externalCmdEnv() externalcmd.Environment { |
|
_, port, _ := net.SplitHostPort(pa.rtspAddress) |
|
env := externalcmd.Environment{ |
|
"RTSP_PATH": pa.name, |
|
"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) onDemandStaticSourceStart() { |
|
pa.source.(*sourceStatic).start() |
|
|
|
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() { |
|
if pa.onDemandStaticSourceState == pathOnDemandStateClosing { |
|
pa.onDemandStaticSourceCloseTimer.Stop() |
|
pa.onDemandStaticSourceCloseTimer = newEmptyTimer() |
|
} |
|
|
|
pa.onDemandStaticSourceState = pathOnDemandStateInitial |
|
|
|
pa.source.(*sourceStatic).stop() |
|
} |
|
|
|
func (pa *path) onDemandPublisherStart() { |
|
pa.Log(logger.Info, "runOnDemand command started") |
|
pa.onDemandCmd = externalcmd.NewCmd( |
|
pa.externalCmdPool, |
|
pa.conf.RunOnDemand, |
|
pa.conf.RunOnDemandRestart, |
|
pa.externalCmdEnv(), |
|
func(co int) { |
|
pa.Log(logger.Info, "runOnDemand command exited with code %d", co) |
|
}) |
|
|
|
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() { |
|
if pa.onDemandPublisherState == pathOnDemandStateClosing { |
|
pa.onDemandPublisherCloseTimer.Stop() |
|
pa.onDemandPublisherCloseTimer = newEmptyTimer() |
|
} |
|
|
|
// set state before doPublisherRemove() |
|
pa.onDemandPublisherState = pathOnDemandStateInitial |
|
|
|
if pa.source != nil { |
|
pa.source.(publisher).close() |
|
pa.doPublisherRemove() |
|
} |
|
|
|
if pa.onDemandCmd != nil { |
|
pa.onDemandCmd.Close() |
|
pa.onDemandCmd = nil |
|
pa.Log(logger.Info, "runOnDemand command stopped") |
|
} |
|
} |
|
|
|
func (pa *path) sourceSetReady(medias media.Medias, allocateEncoder bool) error { |
|
stream, err := newStream( |
|
pa.udpMaxPayloadSize, |
|
medias, |
|
allocateEncoder, |
|
pa.bytesReceived, |
|
pa.source, |
|
) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
pa.stream = stream |
|
|
|
if pa.conf.RunOnReady != "" { |
|
pa.Log(logger.Info, "runOnReady command started") |
|
pa.onReadyCmd = externalcmd.NewCmd( |
|
pa.externalCmdPool, |
|
pa.conf.RunOnReady, |
|
pa.conf.RunOnReadyRestart, |
|
pa.externalCmdEnv(), |
|
func(co int) { |
|
pa.Log(logger.Info, "runOnReady command exited with code %d", co) |
|
}) |
|
} |
|
|
|
pa.parent.pathSourceReady(pa) |
|
|
|
return nil |
|
} |
|
|
|
func (pa *path) sourceSetNotReady() { |
|
pa.parent.pathSourceNotReady(pa) |
|
|
|
for r := range pa.readers { |
|
pa.doReaderRemove(r) |
|
r.close() |
|
} |
|
|
|
if pa.onReadyCmd != nil { |
|
pa.onReadyCmd.Close() |
|
pa.onReadyCmd = nil |
|
pa.Log(logger.Info, "runOnReady command stopped") |
|
} |
|
|
|
if pa.stream != nil { |
|
pa.stream.close() |
|
pa.stream = nil |
|
} |
|
} |
|
|
|
func (pa *path) doReaderRemove(r reader) { |
|
delete(pa.readers, r) |
|
} |
|
|
|
func (pa *path) doPublisherRemove() { |
|
if pa.stream != nil { |
|
if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial { |
|
pa.onDemandPublisherStop() |
|
} else { |
|
pa.sourceSetNotReady() |
|
} |
|
} |
|
|
|
pa.source = nil |
|
} |
|
|
|
func (pa *path) handleDescribe(req pathDescribeReq) { |
|
if _, ok := pa.source.(*sourceRedirect); ok { |
|
req.res <- pathDescribeRes{ |
|
redirect: pa.conf.SourceRedirect, |
|
} |
|
return |
|
} |
|
|
|
if pa.stream != nil { |
|
req.res <- pathDescribeRes{ |
|
stream: pa.stream, |
|
} |
|
return |
|
} |
|
|
|
if pa.conf.HasOnDemandStaticSource() { |
|
if pa.onDemandStaticSourceState == pathOnDemandStateInitial { |
|
pa.onDemandStaticSourceStart() |
|
} |
|
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) |
|
return |
|
} |
|
|
|
if pa.conf.HasOnDemandPublisher() { |
|
if pa.onDemandPublisherState == pathOnDemandStateInitial { |
|
pa.onDemandPublisherStart() |
|
} |
|
pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) |
|
return |
|
} |
|
|
|
if pa.conf.Fallback != "" { |
|
fallbackURL := func() string { |
|
if strings.HasPrefix(pa.conf.Fallback, "/") { |
|
ur := url.URL{ |
|
Scheme: req.url.Scheme, |
|
User: req.url.User, |
|
Host: req.url.Host, |
|
Path: pa.conf.Fallback, |
|
} |
|
return ur.String() |
|
} |
|
return pa.conf.Fallback |
|
}() |
|
req.res <- pathDescribeRes{redirect: fallbackURL} |
|
return |
|
} |
|
|
|
req.res <- pathDescribeRes{err: pathErrNoOnePublishing{pathName: pa.name}} |
|
} |
|
|
|
func (pa *path) handlePublisherRemove(req pathPublisherRemoveReq) { |
|
if pa.source == req.author { |
|
pa.doPublisherRemove() |
|
} |
|
close(req.res) |
|
} |
|
|
|
func (pa *path) handlePublisherAdd(req pathPublisherAddReq) { |
|
if pa.conf.Source != "publisher" { |
|
req.res <- pathPublisherAnnounceRes{ |
|
err: fmt.Errorf("can't publish to path '%s' since 'source' is not 'publisher'", pa.name), |
|
} |
|
return |
|
} |
|
|
|
if pa.source != nil { |
|
if pa.conf.DisablePublisherOverride { |
|
req.res <- pathPublisherAnnounceRes{err: fmt.Errorf("someone is already publishing to path '%s'", pa.name)} |
|
return |
|
} |
|
|
|
pa.Log(logger.Info, "closing existing publisher") |
|
pa.source.(publisher).close() |
|
pa.doPublisherRemove() |
|
} |
|
|
|
pa.source = req.author |
|
|
|
req.res <- pathPublisherAnnounceRes{path: pa} |
|
} |
|
|
|
func (pa *path) handlePublisherStart(req pathPublisherStartReq) { |
|
if pa.source != req.author { |
|
req.res <- pathPublisherRecordRes{err: fmt.Errorf("publisher is not assigned to this path anymore")} |
|
return |
|
} |
|
|
|
err := pa.sourceSetReady(req.medias, req.generateRTPPackets) |
|
if err != nil { |
|
req.res <- pathPublisherRecordRes{err: err} |
|
return |
|
} |
|
|
|
if pa.conf.HasOnDemandPublisher() { |
|
pa.onDemandPublisherReadyTimer.Stop() |
|
pa.onDemandPublisherReadyTimer = newEmptyTimer() |
|
|
|
pa.onDemandPublisherScheduleClose() |
|
|
|
for _, req := range pa.describeRequestsOnHold { |
|
req.res <- pathDescribeRes{ |
|
stream: pa.stream, |
|
} |
|
} |
|
pa.describeRequestsOnHold = nil |
|
|
|
for _, req := range pa.readerAddRequestsOnHold { |
|
pa.handleReaderAddPost(req) |
|
} |
|
pa.readerAddRequestsOnHold = nil |
|
} |
|
|
|
req.res <- pathPublisherRecordRes{stream: pa.stream} |
|
} |
|
|
|
func (pa *path) handlePublisherStop(req pathPublisherStopReq) { |
|
if req.author == pa.source && pa.stream != nil { |
|
if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial { |
|
pa.onDemandPublisherStop() |
|
} else { |
|
pa.sourceSetNotReady() |
|
} |
|
} |
|
close(req.res) |
|
} |
|
|
|
func (pa *path) handleReaderRemove(req pathReaderRemoveReq) { |
|
if _, ok := pa.readers[req.author]; ok { |
|
pa.doReaderRemove(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) handleReaderAdd(req pathReaderAddReq) { |
|
if pa.stream != nil { |
|
pa.handleReaderAddPost(req) |
|
return |
|
} |
|
|
|
if pa.conf.HasOnDemandStaticSource() { |
|
if pa.onDemandStaticSourceState == pathOnDemandStateInitial { |
|
pa.onDemandStaticSourceStart() |
|
} |
|
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) |
|
return |
|
} |
|
|
|
if pa.conf.HasOnDemandPublisher() { |
|
if pa.onDemandPublisherState == pathOnDemandStateInitial { |
|
pa.onDemandPublisherStart() |
|
} |
|
pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) |
|
return |
|
} |
|
|
|
req.res <- pathReaderSetupPlayRes{err: pathErrNoOnePublishing{pathName: pa.name}} |
|
} |
|
|
|
func (pa *path) handleReaderAddPost(req pathReaderAddReq) { |
|
pa.readers[req.author] = struct{}{} |
|
|
|
if pa.conf.HasOnDemandStaticSource() { |
|
if pa.onDemandStaticSourceState == pathOnDemandStateClosing { |
|
pa.onDemandStaticSourceState = pathOnDemandStateReady |
|
pa.onDemandStaticSourceCloseTimer.Stop() |
|
pa.onDemandStaticSourceCloseTimer = newEmptyTimer() |
|
} |
|
} else if pa.conf.HasOnDemandPublisher() { |
|
if pa.onDemandPublisherState == pathOnDemandStateClosing { |
|
pa.onDemandPublisherState = pathOnDemandStateReady |
|
pa.onDemandPublisherCloseTimer.Stop() |
|
pa.onDemandPublisherCloseTimer = newEmptyTimer() |
|
} |
|
} |
|
|
|
req.res <- pathReaderSetupPlayRes{ |
|
path: pa, |
|
stream: pa.stream, |
|
} |
|
} |
|
|
|
func (pa *path) handleAPIPathsList(req pathAPIPathsListSubReq) { |
|
req.data.Items[pa.name] = pathAPIPathsListItem{ |
|
ConfName: pa.confName, |
|
Conf: pa.conf, |
|
Source: func() interface{} { |
|
if pa.source == nil { |
|
return nil |
|
} |
|
return pa.source.apiSourceDescribe() |
|
}(), |
|
SourceReady: pa.stream != nil, |
|
Tracks: func() []string { |
|
if pa.stream == nil { |
|
return []string{} |
|
} |
|
return mediasDescription(pa.stream.medias()) |
|
}(), |
|
BytesReceived: atomic.LoadUint64(pa.bytesReceived), |
|
Readers: func() []interface{} { |
|
ret := []interface{}{} |
|
for r := range pa.readers { |
|
ret = append(ret, r.apiReaderDescribe()) |
|
} |
|
return ret |
|
}(), |
|
} |
|
close(req.res) |
|
} |
|
|
|
// reloadConf is called by pathManager. |
|
func (pa *path) reloadConf(newConf *conf.PathConf) { |
|
select { |
|
case pa.chReloadConf <- newConf: |
|
case <-pa.ctx.Done(): |
|
} |
|
} |
|
|
|
// sourceStaticSetReady is called by sourceStatic. |
|
func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) { |
|
select { |
|
case pa.chSourceStaticSetReady <- req: |
|
|
|
case <-pa.ctx.Done(): |
|
req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} |
|
|
|
// this avoids: |
|
// - invalid requests sent after the source has been terminated |
|
// - deadlocks caused by <-done inside stop() |
|
case <-sourceStaticCtx.Done(): |
|
req.res <- pathSourceStaticSetReadyRes{err: fmt.Errorf("terminated")} |
|
} |
|
} |
|
|
|
// sourceStaticSetNotReady is called by sourceStatic. |
|
func (pa *path) sourceStaticSetNotReady(sourceStaticCtx context.Context, req pathSourceStaticSetNotReadyReq) { |
|
select { |
|
case pa.chSourceStaticSetNotReady <- 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 <-sourceStaticCtx.Done(): |
|
close(req.res) |
|
} |
|
} |
|
|
|
// describe is called by a reader or publisher through pathManager. |
|
func (pa *path) describe(req pathDescribeReq) pathDescribeRes { |
|
select { |
|
case pa.chDescribe <- req: |
|
return <-req.res |
|
case <-pa.ctx.Done(): |
|
return pathDescribeRes{err: fmt.Errorf("terminated")} |
|
} |
|
} |
|
|
|
// publisherRemove is called by a publisher. |
|
func (pa *path) publisherRemove(req pathPublisherRemoveReq) { |
|
req.res = make(chan struct{}) |
|
select { |
|
case pa.chPublisherRemove <- req: |
|
<-req.res |
|
case <-pa.ctx.Done(): |
|
} |
|
} |
|
|
|
// publisherAdd is called by a publisher through pathManager. |
|
func (pa *path) publisherAdd(req pathPublisherAddReq) pathPublisherAnnounceRes { |
|
select { |
|
case pa.chPublisherAdd <- req: |
|
return <-req.res |
|
case <-pa.ctx.Done(): |
|
return pathPublisherAnnounceRes{err: fmt.Errorf("terminated")} |
|
} |
|
} |
|
|
|
// publisherStart is called by a publisher. |
|
func (pa *path) publisherStart(req pathPublisherStartReq) pathPublisherRecordRes { |
|
req.res = make(chan pathPublisherRecordRes) |
|
select { |
|
case pa.chPublisherStart <- req: |
|
return <-req.res |
|
case <-pa.ctx.Done(): |
|
return pathPublisherRecordRes{err: fmt.Errorf("terminated")} |
|
} |
|
} |
|
|
|
// publisherStop is called by a publisher. |
|
func (pa *path) publisherStop(req pathPublisherStopReq) { |
|
req.res = make(chan struct{}) |
|
select { |
|
case pa.chPublisherStop <- req: |
|
<-req.res |
|
case <-pa.ctx.Done(): |
|
} |
|
} |
|
|
|
// readerAdd is called by a reader through pathManager. |
|
func (pa *path) readerAdd(req pathReaderAddReq) pathReaderSetupPlayRes { |
|
select { |
|
case pa.chReaderAdd <- req: |
|
return <-req.res |
|
case <-pa.ctx.Done(): |
|
return pathReaderSetupPlayRes{err: fmt.Errorf("terminated")} |
|
} |
|
} |
|
|
|
// readerRemove is called by a reader. |
|
func (pa *path) readerRemove(req pathReaderRemoveReq) { |
|
req.res = make(chan struct{}) |
|
select { |
|
case pa.chReaderRemove <- req: |
|
<-req.res |
|
case <-pa.ctx.Done(): |
|
} |
|
} |
|
|
|
// apiPathsList is called by api. |
|
func (pa *path) apiPathsList(req pathAPIPathsListSubReq) { |
|
req.res = make(chan struct{}) |
|
select { |
|
case pa.chAPIPathsList <- req: |
|
<-req.res |
|
|
|
case <-pa.ctx.Done(): |
|
} |
|
}
|
|
|