26 changed files with 2474 additions and 1743 deletions
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
package client |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
"strings" |
||||
) |
||||
|
||||
func ipEqualOrInRange(ip net.IP, ips []interface{}) bool { |
||||
for _, item := range ips { |
||||
switch titem := item.(type) { |
||||
case net.IP: |
||||
if titem.Equal(ip) { |
||||
return true |
||||
} |
||||
|
||||
case *net.IPNet: |
||||
if titem.Contains(ip) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func removeQueryFromPath(path string) string { |
||||
i := strings.Index(path, "?") |
||||
if i >= 0 { |
||||
return path[:i] |
||||
} |
||||
return path |
||||
} |
||||
|
||||
func splitPathIntoBaseAndControl(path string) (string, string, error) { |
||||
pos := func() int { |
||||
for i := len(path) - 1; i >= 0; i-- { |
||||
if path[i] == '/' { |
||||
return i |
||||
} |
||||
} |
||||
return -1 |
||||
}() |
||||
|
||||
if pos < 0 { |
||||
return "", "", fmt.Errorf("the path must contain a base path and a control path (%s)", path) |
||||
} |
||||
|
||||
basePath := path[:pos] |
||||
controlPath := path[pos+1:] |
||||
|
||||
if len(basePath) == 0 { |
||||
return "", "", fmt.Errorf("empty base path (%s)", basePath) |
||||
} |
||||
|
||||
if len(controlPath) == 0 { |
||||
return "", "", fmt.Errorf("empty control path (%s)", controlPath) |
||||
} |
||||
|
||||
return basePath, controlPath, nil |
||||
} |
||||
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
package clientman |
||||
|
||||
import ( |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
"github.com/aler9/gortsplib/headers" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/client" |
||||
"github.com/aler9/rtsp-simple-server/pathman" |
||||
"github.com/aler9/rtsp-simple-server/servertcp" |
||||
"github.com/aler9/rtsp-simple-server/serverudp" |
||||
"github.com/aler9/rtsp-simple-server/stats" |
||||
) |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
} |
||||
|
||||
type ClientManager struct { |
||||
stats *stats.Stats |
||||
serverUdpRtp *serverudp.Server |
||||
serverUdpRtcp *serverudp.Server |
||||
readTimeout time.Duration |
||||
writeTimeout time.Duration |
||||
runOnConnect string |
||||
protocols map[headers.StreamProtocol]struct{} |
||||
pathMan *pathman.PathManager |
||||
serverTcp *servertcp.Server |
||||
parent Parent |
||||
|
||||
clients map[*client.Client]struct{} |
||||
wg sync.WaitGroup |
||||
|
||||
// in
|
||||
clientClose chan *client.Client |
||||
terminate chan struct{} |
||||
|
||||
// out
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func New(stats *stats.Stats, |
||||
serverUdpRtp *serverudp.Server, |
||||
serverUdpRtcp *serverudp.Server, |
||||
readTimeout time.Duration, |
||||
writeTimeout time.Duration, |
||||
runOnConnect string, |
||||
protocols map[headers.StreamProtocol]struct{}, |
||||
pathMan *pathman.PathManager, |
||||
serverTcp *servertcp.Server, |
||||
parent Parent) *ClientManager { |
||||
|
||||
cm := &ClientManager{ |
||||
stats: stats, |
||||
serverUdpRtp: serverUdpRtp, |
||||
serverUdpRtcp: serverUdpRtcp, |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
runOnConnect: runOnConnect, |
||||
protocols: protocols, |
||||
pathMan: pathMan, |
||||
serverTcp: serverTcp, |
||||
parent: parent, |
||||
clients: make(map[*client.Client]struct{}), |
||||
clientClose: make(chan *client.Client), |
||||
terminate: make(chan struct{}), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
go cm.run() |
||||
return cm |
||||
} |
||||
|
||||
func (cm *ClientManager) Close() { |
||||
close(cm.terminate) |
||||
<-cm.done |
||||
} |
||||
|
||||
func (cm *ClientManager) Log(format string, args ...interface{}) { |
||||
cm.parent.Log(format, args...) |
||||
} |
||||
|
||||
func (cm *ClientManager) run() { |
||||
defer close(cm.done) |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case conn := <-cm.serverTcp.Accept(): |
||||
c := client.New(&cm.wg, |
||||
cm.stats, |
||||
cm.serverUdpRtp, |
||||
cm.serverUdpRtcp, |
||||
cm.readTimeout, |
||||
cm.writeTimeout, |
||||
cm.runOnConnect, |
||||
cm.protocols, |
||||
conn, |
||||
cm) |
||||
cm.clients[c] = struct{}{} |
||||
|
||||
case c := <-cm.pathMan.ClientClose(): |
||||
if _, ok := cm.clients[c]; !ok { |
||||
continue |
||||
} |
||||
delete(cm.clients, c) |
||||
c.Close() |
||||
|
||||
case c := <-cm.clientClose: |
||||
if _, ok := cm.clients[c]; !ok { |
||||
continue |
||||
} |
||||
delete(cm.clients, c) |
||||
c.Close() |
||||
|
||||
case <-cm.terminate: |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
select { |
||||
case <-cm.clientClose: |
||||
} |
||||
} |
||||
}() |
||||
|
||||
for c := range cm.clients { |
||||
c.Close() |
||||
} |
||||
cm.wg.Wait() |
||||
|
||||
close(cm.clientClose) |
||||
} |
||||
|
||||
func (cm *ClientManager) OnClientClose(c *client.Client) { |
||||
cm.clientClose <- c |
||||
} |
||||
|
||||
func (cm *ClientManager) OnClientDescribe(c *client.Client, pathName string, req *base.Request) (client.Path, error) { |
||||
return cm.pathMan.OnClientDescribe(c, pathName, req) |
||||
} |
||||
|
||||
func (cm *ClientManager) OnClientAnnounce(c *client.Client, pathName string, tracks gortsplib.Tracks, req *base.Request) (client.Path, error) { |
||||
return cm.pathMan.OnClientAnnounce(c, pathName, tracks, req) |
||||
} |
||||
|
||||
func (cm *ClientManager) OnClientSetupPlay(c *client.Client, pathName string, trackId int, req *base.Request) (client.Path, error) { |
||||
return cm.pathMan.OnClientSetupPlay(c, pathName, trackId, req) |
||||
} |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
package conf |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
"regexp" |
||||
) |
||||
|
||||
var rePathName = regexp.MustCompile("^[0-9a-zA-Z_\\-/]+$") |
||||
|
||||
func CheckPathName(name string) error { |
||||
if name == "" { |
||||
return fmt.Errorf("cannot be empty") |
||||
} |
||||
|
||||
if name[0] == '/' { |
||||
return fmt.Errorf("can't begin with a slash") |
||||
} |
||||
|
||||
if name[len(name)-1] == '/' { |
||||
return fmt.Errorf("can't end with a slash") |
||||
} |
||||
|
||||
if !rePathName.MatchString(name) { |
||||
return fmt.Errorf("can contain only alfanumeric characters, underscore, minus or slash") |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func parseIpCidrList(in []string) ([]interface{}, error) { |
||||
if len(in) == 0 { |
||||
return nil, nil |
||||
} |
||||
|
||||
var ret []interface{} |
||||
for _, t := range in { |
||||
_, ipnet, err := net.ParseCIDR(t) |
||||
if err == nil { |
||||
ret = append(ret, ipnet) |
||||
continue |
||||
} |
||||
|
||||
ip := net.ParseIP(t) |
||||
if ip != nil { |
||||
ret = append(ret, ip) |
||||
continue |
||||
} |
||||
|
||||
return nil, fmt.Errorf("unable to parse ip/network '%s'", t) |
||||
} |
||||
return ret, nil |
||||
} |
||||
@ -1,293 +0,0 @@
@@ -1,293 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/conf" |
||||
"github.com/aler9/rtsp-simple-server/externalcmd" |
||||
) |
||||
|
||||
const ( |
||||
describeTimeout = 5 * time.Second |
||||
sourceStopAfterDescribePeriod = 10 * time.Second |
||||
onDemandCmdStopAfterDescribePeriod = 10 * time.Second |
||||
) |
||||
|
||||
// a source can be a client, a sourceRtsp or a sourceRtmp
|
||||
type source interface { |
||||
isSource() |
||||
} |
||||
|
||||
type path struct { |
||||
p *program |
||||
name string |
||||
conf *conf.PathConf |
||||
source source |
||||
sourceReady bool |
||||
sourceTrackCount int |
||||
sourceSdp []byte |
||||
lastDescribeReq time.Time |
||||
lastDescribeActivation time.Time |
||||
onInitCmd *externalcmd.ExternalCmd |
||||
onDemandCmd *externalcmd.ExternalCmd |
||||
} |
||||
|
||||
func newPath(p *program, name string, conf *conf.PathConf) *path { |
||||
pa := &path{ |
||||
p: p, |
||||
name: name, |
||||
conf: conf, |
||||
} |
||||
|
||||
if strings.HasPrefix(conf.Source, "rtsp://") { |
||||
s := newSourceRtsp(p, pa) |
||||
pa.source = s |
||||
|
||||
} else if strings.HasPrefix(conf.Source, "rtmp://") { |
||||
s := newSourceRtmp(p, pa) |
||||
pa.source = s |
||||
} |
||||
|
||||
return pa |
||||
} |
||||
|
||||
func (pa *path) log(format string, args ...interface{}) { |
||||
pa.p.log("[path "+pa.name+"] "+format, args...) |
||||
} |
||||
|
||||
func (pa *path) onInit() { |
||||
if source, ok := pa.source.(*sourceRtsp); ok { |
||||
go source.run(source.state) |
||||
|
||||
} else if source, ok := pa.source.(*sourceRtmp); ok { |
||||
go source.run(source.state) |
||||
} |
||||
|
||||
if pa.conf.RunOnInit != "" { |
||||
pa.log("starting on init command") |
||||
|
||||
var err error |
||||
pa.onInitCmd, err = externalcmd.New(pa.conf.RunOnInit, pa.name) |
||||
if err != nil { |
||||
pa.log("ERR: %s", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *path) onClose() { |
||||
if source, ok := pa.source.(*sourceRtsp); ok { |
||||
close(source.terminate) |
||||
<-source.done |
||||
|
||||
} else if source, ok := pa.source.(*sourceRtmp); ok { |
||||
close(source.terminate) |
||||
<-source.done |
||||
} |
||||
|
||||
if pa.onInitCmd != nil { |
||||
pa.log("stopping on init command (closing)") |
||||
pa.onInitCmd.Close() |
||||
} |
||||
|
||||
if pa.onDemandCmd != nil { |
||||
pa.log("stopping on demand command (closing)") |
||||
pa.onDemandCmd.Close() |
||||
} |
||||
|
||||
for c := range pa.p.clients { |
||||
if c.path == pa { |
||||
if c.state == clientStateWaitDescription { |
||||
c.path = nil |
||||
c.state = clientStateInitial |
||||
c.describe <- describeRes{nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)} |
||||
} else { |
||||
c.close() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *path) hasClients() bool { |
||||
for c := range pa.p.clients { |
||||
if c.path == pa { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pa *path) hasClientsWaitingDescribe() bool { |
||||
for c := range pa.p.clients { |
||||
if c.state == clientStateWaitDescription && c.path == pa { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pa *path) hasClientReaders() bool { |
||||
for c := range pa.p.clients { |
||||
if c.path == pa && c != pa.source { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pa *path) onCheck() { |
||||
// reply to DESCRIBE requests if they are in timeout
|
||||
if pa.hasClientsWaitingDescribe() && |
||||
time.Since(pa.lastDescribeActivation) >= describeTimeout { |
||||
for c := range pa.p.clients { |
||||
if c.state == clientStateWaitDescription && |
||||
c.path == pa { |
||||
c.path = nil |
||||
c.state = clientStateInitial |
||||
c.describe <- describeRes{nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// stop on demand rtsp source if needed
|
||||
if source, ok := pa.source.(*sourceRtsp); ok { |
||||
if pa.conf.SourceOnDemand && |
||||
source.state == sourceRtspStateRunning && |
||||
!pa.hasClients() && |
||||
time.Since(pa.lastDescribeReq) >= sourceStopAfterDescribePeriod { |
||||
pa.log("stopping on demand rtsp source (not requested anymore)") |
||||
atomic.AddInt64(pa.p.countSourcesRtspRunning, -1) |
||||
source.state = sourceRtspStateStopped |
||||
source.setState <- source.state |
||||
} |
||||
|
||||
// stop on demand rtmp source if needed
|
||||
} else if source, ok := pa.source.(*sourceRtmp); ok { |
||||
if pa.conf.SourceOnDemand && |
||||
source.state == sourceRtmpStateRunning && |
||||
!pa.hasClients() && |
||||
time.Since(pa.lastDescribeReq) >= sourceStopAfterDescribePeriod { |
||||
pa.log("stopping on demand rtmp source (not requested anymore)") |
||||
atomic.AddInt64(pa.p.countSourcesRtmpRunning, -1) |
||||
source.state = sourceRtmpStateStopped |
||||
source.setState <- source.state |
||||
} |
||||
} |
||||
|
||||
// stop on demand command if needed
|
||||
if pa.onDemandCmd != nil && |
||||
!pa.hasClientReaders() && |
||||
time.Since(pa.lastDescribeReq) >= onDemandCmdStopAfterDescribePeriod { |
||||
pa.log("stopping on demand command (not requested anymore)") |
||||
pa.onDemandCmd.Close() |
||||
pa.onDemandCmd = nil |
||||
} |
||||
|
||||
// remove regular expression paths
|
||||
if pa.conf.Regexp != nil && |
||||
pa.source == nil && |
||||
!pa.hasClients() { |
||||
pa.onClose() |
||||
delete(pa.p.paths, pa.name) |
||||
} |
||||
} |
||||
|
||||
func (pa *path) onSourceRemove() { |
||||
pa.source = nil |
||||
|
||||
// close all clients that are reading or waiting for reading
|
||||
for c := range pa.p.clients { |
||||
if c.path == pa && |
||||
c.state != clientStateWaitDescription && |
||||
c != pa.source { |
||||
c.close() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *path) onSourceSetReady() { |
||||
pa.sourceReady = true |
||||
|
||||
// reply to all clients that are waiting for a description
|
||||
for c := range pa.p.clients { |
||||
if c.state == clientStateWaitDescription && |
||||
c.path == pa { |
||||
c.path = nil |
||||
c.state = clientStateInitial |
||||
c.describe <- describeRes{pa.sourceSdp, nil} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *path) onSourceSetNotReady() { |
||||
pa.sourceReady = false |
||||
|
||||
// close all clients that are reading or waiting for reading
|
||||
for c := range pa.p.clients { |
||||
if c.path == pa && |
||||
c.state != clientStateWaitDescription && |
||||
c != pa.source { |
||||
c.close() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *path) onDescribe(client *client) { |
||||
pa.lastDescribeReq = time.Now() |
||||
|
||||
// publisher not found
|
||||
if pa.source == nil { |
||||
// on demand command is available: put the client on hold
|
||||
if pa.conf.RunOnDemand != "" { |
||||
if pa.onDemandCmd == nil { // start if needed
|
||||
pa.log("starting on demand command") |
||||
pa.lastDescribeActivation = time.Now() |
||||
|
||||
var err error |
||||
pa.onDemandCmd, err = externalcmd.New(pa.conf.RunOnDemand, pa.name) |
||||
if err != nil { |
||||
pa.log("ERR: %s", err) |
||||
} |
||||
} |
||||
|
||||
client.path = pa |
||||
client.state = clientStateWaitDescription |
||||
|
||||
// no on-demand: reply with 404
|
||||
} else { |
||||
client.describe <- describeRes{nil, fmt.Errorf("no one is publishing on path '%s'", pa.name)} |
||||
} |
||||
|
||||
// publisher was found but is not ready: put the client on hold
|
||||
} else if !pa.sourceReady { |
||||
// start rtsp source if needed
|
||||
if source, ok := pa.source.(*sourceRtsp); ok { |
||||
if source.state == sourceRtspStateStopped { |
||||
pa.log("starting on demand rtsp source") |
||||
pa.lastDescribeActivation = time.Now() |
||||
atomic.AddInt64(pa.p.countSourcesRtspRunning, +1) |
||||
source.state = sourceRtspStateRunning |
||||
source.setState <- source.state |
||||
} |
||||
|
||||
// start rtmp source if needed
|
||||
} else if source, ok := pa.source.(*sourceRtmp); ok { |
||||
if source.state == sourceRtmpStateStopped { |
||||
pa.log("starting on demand rtmp source") |
||||
pa.lastDescribeActivation = time.Now() |
||||
atomic.AddInt64(pa.p.countSourcesRtmpRunning, +1) |
||||
source.state = sourceRtmpStateRunning |
||||
source.setState <- source.state |
||||
} |
||||
} |
||||
|
||||
client.path = pa |
||||
client.state = clientStateWaitDescription |
||||
|
||||
// publisher was found and is ready
|
||||
} else { |
||||
client.describe <- describeRes{pa.sourceSdp, nil} |
||||
} |
||||
} |
||||
@ -0,0 +1,660 @@
@@ -0,0 +1,660 @@
|
||||
package path |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/client" |
||||
"github.com/aler9/rtsp-simple-server/conf" |
||||
"github.com/aler9/rtsp-simple-server/externalcmd" |
||||
"github.com/aler9/rtsp-simple-server/serverudp" |
||||
"github.com/aler9/rtsp-simple-server/sourcertmp" |
||||
"github.com/aler9/rtsp-simple-server/sourcertsp" |
||||
"github.com/aler9/rtsp-simple-server/stats" |
||||
) |
||||
|
||||
const ( |
||||
pathCheckPeriod = 5 * time.Second |
||||
describeTimeout = 5 * time.Second |
||||
sourceStopAfterDescribePeriod = 10 * time.Second |
||||
onDemandCmdStopAfterDescribePeriod = 10 * time.Second |
||||
) |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
OnPathClose(*Path) |
||||
OnPathClientClose(*client.Client) |
||||
} |
||||
|
||||
// a source can be a client, a sourcertsp.Source or a sourcertmp.Source
|
||||
type source interface { |
||||
IsSource() |
||||
} |
||||
|
||||
type ClientDescribeRes struct { |
||||
Path client.Path |
||||
Err error |
||||
} |
||||
|
||||
type ClientDescribeReq struct { |
||||
Res chan ClientDescribeRes |
||||
Client *client.Client |
||||
PathName string |
||||
Req *base.Request |
||||
} |
||||
|
||||
type ClientAnnounceRes struct { |
||||
Path client.Path |
||||
Err error |
||||
} |
||||
|
||||
type ClientAnnounceReq struct { |
||||
Res chan ClientAnnounceRes |
||||
Client *client.Client |
||||
PathName string |
||||
Tracks gortsplib.Tracks |
||||
Req *base.Request |
||||
} |
||||
|
||||
type ClientSetupPlayRes struct { |
||||
Path client.Path |
||||
Err error |
||||
} |
||||
|
||||
type ClientSetupPlayReq struct { |
||||
Res chan ClientSetupPlayRes |
||||
Client *client.Client |
||||
PathName string |
||||
TrackId int |
||||
Req *base.Request |
||||
} |
||||
|
||||
type clientRemoveReq struct { |
||||
res chan struct{} |
||||
client *client.Client |
||||
} |
||||
|
||||
type clientPlayReq struct { |
||||
res chan struct{} |
||||
client *client.Client |
||||
} |
||||
|
||||
type clientRecordReq struct { |
||||
res chan struct{} |
||||
client *client.Client |
||||
} |
||||
|
||||
type clientState int |
||||
|
||||
const ( |
||||
clientStateWaitingDescribe clientState = iota |
||||
clientStatePrePlay |
||||
clientStatePlay |
||||
clientStatePreRecord |
||||
clientStateRecord |
||||
) |
||||
|
||||
type Path struct { |
||||
wg *sync.WaitGroup |
||||
stats *stats.Stats |
||||
serverUdpRtp *serverudp.Server |
||||
serverUdpRtcp *serverudp.Server |
||||
readTimeout time.Duration |
||||
writeTimeout time.Duration |
||||
name string |
||||
conf *conf.PathConf |
||||
parent Parent |
||||
|
||||
clients map[*client.Client]clientState |
||||
source source |
||||
sourceReady bool |
||||
sourceTrackCount int |
||||
sourceSdp []byte |
||||
lastDescribeReq time.Time |
||||
lastDescribeActivation time.Time |
||||
readers *readersMap |
||||
onInitCmd *externalcmd.ExternalCmd |
||||
onDemandCmd *externalcmd.ExternalCmd |
||||
|
||||
// in
|
||||
sourceSetReady chan struct{} // from source
|
||||
sourceSetNotReady chan struct{} // from source
|
||||
clientDescribe chan ClientDescribeReq // from program
|
||||
clientAnnounce chan ClientAnnounceReq // from program
|
||||
clientSetupPlay chan ClientSetupPlayReq // from program
|
||||
clientPlay chan clientPlayReq // from client
|
||||
clientRecord chan clientRecordReq // from client
|
||||
clientRemove chan clientRemoveReq // from client
|
||||
terminate chan struct{} |
||||
} |
||||
|
||||
func New( |
||||
wg *sync.WaitGroup, |
||||
stats *stats.Stats, |
||||
serverUdpRtp *serverudp.Server, |
||||
serverUdpRtcp *serverudp.Server, |
||||
readTimeout time.Duration, |
||||
writeTimeout time.Duration, |
||||
name string, |
||||
conf *conf.PathConf, |
||||
parent Parent) *Path { |
||||
|
||||
pa := &Path{ |
||||
wg: wg, |
||||
stats: stats, |
||||
serverUdpRtp: serverUdpRtp, |
||||
serverUdpRtcp: serverUdpRtcp, |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
name: name, |
||||
conf: conf, |
||||
parent: parent, |
||||
clients: make(map[*client.Client]clientState), |
||||
readers: newReadersMap(), |
||||
sourceSetReady: make(chan struct{}), |
||||
sourceSetNotReady: make(chan struct{}), |
||||
clientDescribe: make(chan ClientDescribeReq), |
||||
clientAnnounce: make(chan ClientAnnounceReq), |
||||
clientSetupPlay: make(chan ClientSetupPlayReq), |
||||
clientPlay: make(chan clientPlayReq), |
||||
clientRecord: make(chan clientRecordReq), |
||||
clientRemove: make(chan clientRemoveReq), |
||||
terminate: make(chan struct{}), |
||||
} |
||||
|
||||
pa.wg.Add(1) |
||||
go pa.run() |
||||
return pa |
||||
} |
||||
|
||||
func (pa *Path) Close() { |
||||
close(pa.terminate) |
||||
} |
||||
|
||||
func (pa *Path) Log(format string, args ...interface{}) { |
||||
pa.parent.Log("[path "+pa.name+"] "+format, args...) |
||||
} |
||||
|
||||
func (pa *Path) run() { |
||||
defer pa.wg.Done() |
||||
|
||||
if strings.HasPrefix(pa.conf.Source, "rtsp://") { |
||||
state := sourcertsp.StateStopped |
||||
if !pa.conf.SourceOnDemand { |
||||
state = sourcertsp.StateRunning |
||||
} |
||||
|
||||
s := sourcertsp.New( |
||||
pa.conf.Source, |
||||
pa.conf.SourceProtocolParsed, |
||||
pa.readTimeout, |
||||
pa.writeTimeout, |
||||
state, |
||||
pa) |
||||
pa.source = s |
||||
|
||||
atomic.AddInt64(pa.stats.CountSourcesRtsp, +1) |
||||
if !pa.conf.SourceOnDemand { |
||||
atomic.AddInt64(pa.stats.CountSourcesRtspRunning, +1) |
||||
} |
||||
|
||||
} else if strings.HasPrefix(pa.conf.Source, "rtmp://") { |
||||
state := sourcertmp.StateStopped |
||||
if !pa.conf.SourceOnDemand { |
||||
state = sourcertmp.StateRunning |
||||
} |
||||
|
||||
s := sourcertmp.New( |
||||
pa.conf.Source, |
||||
state, |
||||
pa) |
||||
pa.source = s |
||||
|
||||
atomic.AddInt64(pa.stats.CountSourcesRtmp, +1) |
||||
if !pa.conf.SourceOnDemand { |
||||
atomic.AddInt64(pa.stats.CountSourcesRtmpRunning, +1) |
||||
} |
||||
} |
||||
|
||||
if pa.conf.RunOnInit != "" { |
||||
pa.Log("starting on init command") |
||||
|
||||
var err error |
||||
pa.onInitCmd, err = externalcmd.New(pa.conf.RunOnInit, pa.name) |
||||
if err != nil { |
||||
pa.Log("ERR: %s", err) |
||||
} |
||||
} |
||||
|
||||
tickerCheck := time.NewTicker(pathCheckPeriod) |
||||
defer tickerCheck.Stop() |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-tickerCheck.C: |
||||
ok := pa.onCheck() |
||||
if !ok { |
||||
pa.parent.OnPathClose(pa) |
||||
<-pa.terminate |
||||
break outer |
||||
} |
||||
|
||||
case <-pa.sourceSetReady: |
||||
pa.onSourceSetReady() |
||||
|
||||
case <-pa.sourceSetNotReady: |
||||
pa.onSourceSetNotReady() |
||||
|
||||
case req := <-pa.clientDescribe: |
||||
// reply immediately
|
||||
req.Res <- ClientDescribeRes{pa, nil} |
||||
pa.onClientDescribe(req.Client) |
||||
|
||||
case req := <-pa.clientSetupPlay: |
||||
err := pa.onClientSetupPlay(req.Client, req.TrackId) |
||||
if err != nil { |
||||
req.Res <- ClientSetupPlayRes{nil, err} |
||||
continue |
||||
} |
||||
req.Res <- ClientSetupPlayRes{pa, nil} |
||||
|
||||
case req := <-pa.clientPlay: |
||||
if _, ok := pa.clients[req.client]; ok { |
||||
pa.onClientPlay(req.client) |
||||
} |
||||
close(req.res) |
||||
|
||||
case req := <-pa.clientAnnounce: |
||||
err := pa.onClientAnnounce(req.Client, req.Tracks) |
||||
if err != nil { |
||||
req.Res <- ClientAnnounceRes{nil, err} |
||||
continue |
||||
} |
||||
req.Res <- ClientAnnounceRes{pa, nil} |
||||
|
||||
case req := <-pa.clientRecord: |
||||
if _, ok := pa.clients[req.client]; ok { |
||||
pa.onClientRecord(req.client) |
||||
} |
||||
close(req.res) |
||||
|
||||
case req := <-pa.clientRemove: |
||||
if _, ok := pa.clients[req.client]; ok { |
||||
pa.onClientRemove(req.client) |
||||
} |
||||
close(req.res) |
||||
|
||||
case <-pa.terminate: |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
select { |
||||
case _, ok := <-pa.sourceSetReady: |
||||
if !ok { |
||||
return |
||||
} |
||||
|
||||
case _, ok := <-pa.sourceSetNotReady: |
||||
if !ok { |
||||
return |
||||
} |
||||
|
||||
case req, ok := <-pa.clientDescribe: |
||||
if !ok { |
||||
return |
||||
} |
||||
req.Res <- ClientDescribeRes{nil, fmt.Errorf("terminated")} |
||||
|
||||
case req, ok := <-pa.clientAnnounce: |
||||
if !ok { |
||||
return |
||||
} |
||||
req.Res <- ClientAnnounceRes{nil, fmt.Errorf("terminated")} |
||||
|
||||
case req, ok := <-pa.clientSetupPlay: |
||||
if !ok { |
||||
return |
||||
} |
||||
req.Res <- ClientSetupPlayRes{nil, fmt.Errorf("terminated")} |
||||
|
||||
case req, ok := <-pa.clientPlay: |
||||
if !ok { |
||||
return |
||||
} |
||||
close(req.res) |
||||
|
||||
case req, ok := <-pa.clientRecord: |
||||
if !ok { |
||||
return |
||||
} |
||||
close(req.res) |
||||
|
||||
case req, ok := <-pa.clientRemove: |
||||
if !ok { |
||||
return |
||||
} |
||||
close(req.res) |
||||
} |
||||
} |
||||
}() |
||||
|
||||
if pa.onInitCmd != nil { |
||||
pa.Log("stopping on init command (closing)") |
||||
pa.onInitCmd.Close() |
||||
} |
||||
|
||||
if source, ok := pa.source.(*sourcertsp.Source); ok { |
||||
source.Close() |
||||
|
||||
} else if source, ok := pa.source.(*sourcertmp.Source); ok { |
||||
source.Close() |
||||
} |
||||
|
||||
if pa.onDemandCmd != nil { |
||||
pa.Log("stopping on demand command (closing)") |
||||
pa.onDemandCmd.Close() |
||||
} |
||||
|
||||
for c, state := range pa.clients { |
||||
if state == clientStateWaitingDescribe { |
||||
delete(pa.clients, c) |
||||
c.OnPathDescribeData(nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)) |
||||
} else { |
||||
pa.onClientRemove(c) |
||||
pa.parent.OnPathClientClose(c) |
||||
} |
||||
} |
||||
|
||||
close(pa.sourceSetReady) |
||||
close(pa.sourceSetNotReady) |
||||
close(pa.clientDescribe) |
||||
close(pa.clientAnnounce) |
||||
close(pa.clientSetupPlay) |
||||
close(pa.clientPlay) |
||||
close(pa.clientRecord) |
||||
close(pa.clientRemove) |
||||
} |
||||
|
||||
func (pa *Path) hasClients() bool { |
||||
return len(pa.clients) > 0 |
||||
} |
||||
|
||||
func (pa *Path) hasClientsWaitingDescribe() bool { |
||||
for _, state := range pa.clients { |
||||
if state == clientStateWaitingDescribe { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pa *Path) hasClientReadersOrWaitingDescribe() bool { |
||||
for c := range pa.clients { |
||||
if c != pa.source { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func (pa *Path) onCheck() bool { |
||||
// reply to DESCRIBE requests if they are in timeout
|
||||
if pa.hasClientsWaitingDescribe() && |
||||
time.Since(pa.lastDescribeActivation) >= describeTimeout { |
||||
for c, state := range pa.clients { |
||||
if state == clientStateWaitingDescribe { |
||||
delete(pa.clients, c) |
||||
c.OnPathDescribeData(nil, fmt.Errorf("publisher of path '%s' has timed out", pa.name)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// stop on demand rtsp source if needed
|
||||
if source, ok := pa.source.(*sourcertsp.Source); ok { |
||||
if pa.conf.SourceOnDemand && |
||||
source.State() == sourcertsp.StateRunning && |
||||
!pa.hasClients() && |
||||
time.Since(pa.lastDescribeReq) >= sourceStopAfterDescribePeriod { |
||||
pa.Log("stopping on demand rtsp source (not requested anymore)") |
||||
atomic.AddInt64(pa.stats.CountSourcesRtspRunning, -1) |
||||
source.SetState(sourcertsp.StateStopped) |
||||
} |
||||
|
||||
// stop on demand rtmp source if needed
|
||||
} else if source, ok := pa.source.(*sourcertmp.Source); ok { |
||||
if pa.conf.SourceOnDemand && |
||||
source.State() == sourcertmp.StateRunning && |
||||
!pa.hasClients() && |
||||
time.Since(pa.lastDescribeReq) >= sourceStopAfterDescribePeriod { |
||||
pa.Log("stopping on demand rtmp source (not requested anymore)") |
||||
atomic.AddInt64(pa.stats.CountSourcesRtmpRunning, -1) |
||||
source.SetState(sourcertmp.StateStopped) |
||||
} |
||||
} |
||||
|
||||
// stop on demand command if needed
|
||||
if pa.onDemandCmd != nil && |
||||
!pa.hasClientReadersOrWaitingDescribe() && |
||||
time.Since(pa.lastDescribeReq) >= onDemandCmdStopAfterDescribePeriod { |
||||
pa.Log("stopping on demand command (not requested anymore)") |
||||
pa.onDemandCmd.Close() |
||||
pa.onDemandCmd = nil |
||||
} |
||||
|
||||
// remove path if is regexp and has no clients
|
||||
if pa.conf.Regexp != nil && |
||||
pa.source == nil && |
||||
!pa.hasClients() { |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
func (pa *Path) onSourceSetReady() { |
||||
pa.sourceReady = true |
||||
|
||||
// reply to all clients that are waiting for a description
|
||||
for c, state := range pa.clients { |
||||
if state == clientStateWaitingDescribe { |
||||
delete(pa.clients, c) |
||||
c.OnPathDescribeData(pa.sourceSdp, nil) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *Path) onSourceSetNotReady() { |
||||
pa.sourceReady = false |
||||
|
||||
// close all clients that are reading or waiting to read
|
||||
for c, state := range pa.clients { |
||||
if state != clientStateWaitingDescribe && c != pa.source { |
||||
pa.onClientRemove(c) |
||||
pa.parent.OnPathClientClose(c) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *Path) onClientDescribe(c *client.Client) { |
||||
pa.lastDescribeReq = time.Now() |
||||
|
||||
// publisher not found
|
||||
if pa.source == nil { |
||||
// on demand command is available: put the client on hold
|
||||
if pa.conf.RunOnDemand != "" { |
||||
if pa.onDemandCmd == nil { // start if needed
|
||||
pa.Log("starting on demand command") |
||||
pa.lastDescribeActivation = time.Now() |
||||
|
||||
var err error |
||||
pa.onDemandCmd, err = externalcmd.New(pa.conf.RunOnDemand, pa.name) |
||||
if err != nil { |
||||
pa.Log("ERR: %s", err) |
||||
} |
||||
} |
||||
|
||||
pa.clients[c] = clientStateWaitingDescribe |
||||
|
||||
// no on-demand: reply with 404
|
||||
} else { |
||||
c.OnPathDescribeData(nil, fmt.Errorf("no one is publishing on path '%s'", pa.name)) |
||||
} |
||||
|
||||
// publisher was found but is not ready: put the client on hold
|
||||
} else if !pa.sourceReady { |
||||
// start rtsp source if needed
|
||||
if source, ok := pa.source.(*sourcertsp.Source); ok { |
||||
if source.State() == sourcertsp.StateStopped { |
||||
pa.Log("starting on demand rtsp source") |
||||
pa.lastDescribeActivation = time.Now() |
||||
atomic.AddInt64(pa.stats.CountSourcesRtspRunning, +1) |
||||
source.SetState(sourcertsp.StateRunning) |
||||
} |
||||
|
||||
// start rtmp source if needed
|
||||
} else if source, ok := pa.source.(*sourcertmp.Source); ok { |
||||
if source.State() == sourcertmp.StateStopped { |
||||
pa.Log("starting on demand rtmp source") |
||||
pa.lastDescribeActivation = time.Now() |
||||
atomic.AddInt64(pa.stats.CountSourcesRtmpRunning, +1) |
||||
source.SetState(sourcertmp.StateRunning) |
||||
} |
||||
} |
||||
|
||||
pa.clients[c] = clientStateWaitingDescribe |
||||
|
||||
// publisher was found and is ready
|
||||
} else { |
||||
c.OnPathDescribeData(pa.sourceSdp, nil) |
||||
} |
||||
} |
||||
|
||||
func (pa *Path) onClientSetupPlay(c *client.Client, trackId int) error { |
||||
if !pa.sourceReady { |
||||
return fmt.Errorf("no one is publishing on path '%s'", pa.name) |
||||
} |
||||
|
||||
if trackId >= pa.sourceTrackCount { |
||||
return fmt.Errorf("track %d does not exist", trackId) |
||||
} |
||||
|
||||
pa.clients[c] = clientStatePrePlay |
||||
return nil |
||||
} |
||||
|
||||
func (pa *Path) onClientPlay(c *client.Client) { |
||||
atomic.AddInt64(pa.stats.CountReaders, 1) |
||||
pa.clients[c] = clientStatePlay |
||||
pa.readers.add(c) |
||||
} |
||||
|
||||
func (pa *Path) onClientAnnounce(c *client.Client, tracks gortsplib.Tracks) error { |
||||
if pa.source != nil { |
||||
return fmt.Errorf("someone is already publishing on path '%s'", pa.name) |
||||
} |
||||
|
||||
pa.clients[c] = clientStatePreRecord |
||||
pa.source = c |
||||
pa.sourceTrackCount = len(tracks) |
||||
pa.sourceSdp = tracks.Write() |
||||
return nil |
||||
} |
||||
|
||||
func (pa *Path) onClientRecord(c *client.Client) { |
||||
atomic.AddInt64(pa.stats.CountPublishers, 1) |
||||
pa.clients[c] = clientStateRecord |
||||
pa.onSourceSetReady() |
||||
} |
||||
|
||||
func (pa *Path) onClientRemove(c *client.Client) { |
||||
state := pa.clients[c] |
||||
delete(pa.clients, c) |
||||
|
||||
switch state { |
||||
case clientStatePlay: |
||||
atomic.AddInt64(pa.stats.CountReaders, -1) |
||||
pa.readers.remove(c) |
||||
|
||||
case clientStateRecord: |
||||
atomic.AddInt64(pa.stats.CountPublishers, -1) |
||||
pa.onSourceSetNotReady() |
||||
} |
||||
|
||||
if pa.source == c { |
||||
pa.source = nil |
||||
|
||||
// close all clients that are reading or waiting to read
|
||||
for oc, state := range pa.clients { |
||||
if state != clientStateWaitingDescribe && oc != pa.source { |
||||
pa.onClientRemove(oc) |
||||
pa.parent.OnPathClientClose(oc) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (pa *Path) OnSourceReady(tracks gortsplib.Tracks) { |
||||
pa.sourceSdp = tracks.Write() |
||||
pa.sourceTrackCount = len(tracks) |
||||
pa.sourceSetReady <- struct{}{} |
||||
} |
||||
|
||||
func (pa *Path) OnSourceNotReady() { |
||||
pa.sourceSetNotReady <- struct{}{} |
||||
} |
||||
|
||||
func (pa *Path) Name() string { |
||||
return pa.name |
||||
} |
||||
|
||||
func (pa *Path) SourceTrackCount() int { |
||||
return pa.sourceTrackCount |
||||
} |
||||
|
||||
func (pa *Path) Conf() *conf.PathConf { |
||||
return pa.conf |
||||
} |
||||
|
||||
func (pa *Path) OnPathManDescribe(req ClientDescribeReq) { |
||||
pa.clientDescribe <- req |
||||
} |
||||
|
||||
func (pa *Path) OnPathManSetupPlay(req ClientSetupPlayReq) { |
||||
pa.clientSetupPlay <- req |
||||
} |
||||
|
||||
func (pa *Path) OnPathManAnnounce(req ClientAnnounceReq) { |
||||
pa.clientAnnounce <- req |
||||
} |
||||
|
||||
func (pa *Path) OnClientRemove(c *client.Client) { |
||||
res := make(chan struct{}) |
||||
pa.clientRemove <- clientRemoveReq{res, c} |
||||
<-res |
||||
} |
||||
|
||||
func (pa *Path) OnClientPlay(c *client.Client) { |
||||
res := make(chan struct{}) |
||||
pa.clientPlay <- clientPlayReq{res, c} |
||||
<-res |
||||
} |
||||
|
||||
func (pa *Path) OnClientRecord(c *client.Client) { |
||||
res := make(chan struct{}) |
||||
pa.clientRecord <- clientRecordReq{res, c} |
||||
<-res |
||||
} |
||||
|
||||
func (pa *Path) OnFrame(trackId int, streamType gortsplib.StreamType, buf []byte) { |
||||
pa.readers.forwardFrame(trackId, streamType, buf) |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
package path |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
) |
||||
|
||||
type Reader interface { |
||||
OnReaderFrame(int, base.StreamType, []byte) |
||||
} |
||||
|
||||
type readersMap struct { |
||||
mutex sync.RWMutex |
||||
ma map[Reader]struct{} |
||||
} |
||||
|
||||
func newReadersMap() *readersMap { |
||||
return &readersMap{ |
||||
ma: make(map[Reader]struct{}), |
||||
} |
||||
} |
||||
|
||||
func (m *readersMap) add(reader Reader) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
m.ma[reader] = struct{}{} |
||||
} |
||||
|
||||
func (m *readersMap) remove(reader Reader) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
delete(m.ma, reader) |
||||
} |
||||
|
||||
func (m *readersMap) forwardFrame(trackId int, streamType gortsplib.StreamType, buf []byte) { |
||||
m.mutex.RLock() |
||||
defer m.mutex.RUnlock() |
||||
|
||||
for c := range m.ma { |
||||
c.OnReaderFrame(trackId, streamType, buf) |
||||
} |
||||
} |
||||
@ -0,0 +1,267 @@
@@ -0,0 +1,267 @@
|
||||
package pathman |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
"github.com/aler9/gortsplib/headers" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/client" |
||||
"github.com/aler9/rtsp-simple-server/conf" |
||||
"github.com/aler9/rtsp-simple-server/path" |
||||
"github.com/aler9/rtsp-simple-server/serverudp" |
||||
"github.com/aler9/rtsp-simple-server/stats" |
||||
) |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
} |
||||
|
||||
type PathManager struct { |
||||
stats *stats.Stats |
||||
serverUdpRtp *serverudp.Server |
||||
serverUdpRtcp *serverudp.Server |
||||
readTimeout time.Duration |
||||
writeTimeout time.Duration |
||||
authMethods []headers.AuthMethod |
||||
confPaths map[string]*conf.PathConf |
||||
parent Parent |
||||
|
||||
paths map[string]*path.Path |
||||
wg sync.WaitGroup |
||||
|
||||
// in
|
||||
pathClose chan *path.Path |
||||
clientDescribe chan path.ClientDescribeReq |
||||
clientAnnounce chan path.ClientAnnounceReq |
||||
clientSetupPlay chan path.ClientSetupPlayReq |
||||
terminate chan struct{} |
||||
|
||||
// out
|
||||
clientClose chan *client.Client |
||||
done chan struct{} |
||||
} |
||||
|
||||
func New(stats *stats.Stats, |
||||
serverUdpRtp *serverudp.Server, |
||||
serverUdpRtcp *serverudp.Server, |
||||
readTimeout time.Duration, |
||||
writeTimeout time.Duration, |
||||
authMethods []headers.AuthMethod, |
||||
confPaths map[string]*conf.PathConf, |
||||
parent Parent) *PathManager { |
||||
|
||||
pm := &PathManager{ |
||||
stats: stats, |
||||
serverUdpRtp: serverUdpRtp, |
||||
serverUdpRtcp: serverUdpRtcp, |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
authMethods: authMethods, |
||||
confPaths: confPaths, |
||||
parent: parent, |
||||
paths: make(map[string]*path.Path), |
||||
pathClose: make(chan *path.Path), |
||||
clientDescribe: make(chan path.ClientDescribeReq), |
||||
clientAnnounce: make(chan path.ClientAnnounceReq), |
||||
clientSetupPlay: make(chan path.ClientSetupPlayReq), |
||||
terminate: make(chan struct{}), |
||||
clientClose: make(chan *client.Client), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
for name, pathConf := range confPaths { |
||||
if pathConf.Regexp == nil { |
||||
pa := path.New(&pm.wg, pm.stats, pm.serverUdpRtp, pm.serverUdpRtcp, |
||||
pm.readTimeout, pm.writeTimeout, name, pathConf, pm) |
||||
pm.paths[name] = pa |
||||
} |
||||
} |
||||
|
||||
go pm.run() |
||||
return pm |
||||
} |
||||
|
||||
func (pm *PathManager) Close() { |
||||
go func() { |
||||
for range pm.clientClose { |
||||
} |
||||
}() |
||||
close(pm.terminate) |
||||
<-pm.done |
||||
} |
||||
|
||||
func (pm *PathManager) Log(format string, args ...interface{}) { |
||||
pm.parent.Log(format, args...) |
||||
} |
||||
|
||||
func (pm *PathManager) run() { |
||||
defer close(pm.done) |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case pa := <-pm.pathClose: |
||||
delete(pm.paths, pa.Name()) |
||||
pa.Close() |
||||
|
||||
case req := <-pm.clientDescribe: |
||||
pathConf, err := pm.findPathConf(req.PathName) |
||||
if err != nil { |
||||
req.Res <- path.ClientDescribeRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
err = req.Client.Authenticate(pm.authMethods, pathConf.ReadIpsParsed, |
||||
pathConf.ReadUser, pathConf.ReadPass, req.Req) |
||||
if err != nil { |
||||
req.Res <- path.ClientDescribeRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok { |
||||
pa := path.New(&pm.wg, pm.stats, pm.serverUdpRtp, pm.serverUdpRtcp, |
||||
pm.readTimeout, pm.writeTimeout, req.PathName, pathConf, pm) |
||||
pm.paths[req.PathName] = pa |
||||
} |
||||
|
||||
pm.paths[req.PathName].OnPathManDescribe(req) |
||||
|
||||
case req := <-pm.clientAnnounce: |
||||
pathConf, err := pm.findPathConf(req.PathName) |
||||
if err != nil { |
||||
req.Res <- path.ClientAnnounceRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
err = req.Client.Authenticate(pm.authMethods, |
||||
pathConf.PublishIpsParsed, pathConf.PublishUser, pathConf.PublishPass, req.Req) |
||||
if err != nil { |
||||
req.Res <- path.ClientAnnounceRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
// create path if it doesn't exist
|
||||
if _, ok := pm.paths[req.PathName]; !ok { |
||||
pa := path.New(&pm.wg, pm.stats, pm.serverUdpRtp, pm.serverUdpRtcp, |
||||
pm.readTimeout, pm.writeTimeout, req.PathName, pathConf, pm) |
||||
pm.paths[req.PathName] = pa |
||||
} |
||||
|
||||
pm.paths[req.PathName].OnPathManAnnounce(req) |
||||
|
||||
case req := <-pm.clientSetupPlay: |
||||
if _, ok := pm.paths[req.PathName]; !ok { |
||||
req.Res <- path.ClientSetupPlayRes{nil, fmt.Errorf("no one is publishing on path '%s'", req.PathName)} |
||||
continue |
||||
} |
||||
|
||||
pathConf, err := pm.findPathConf(req.PathName) |
||||
if err != nil { |
||||
req.Res <- path.ClientSetupPlayRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
err = req.Client.Authenticate(pm.authMethods, |
||||
pathConf.ReadIpsParsed, pathConf.ReadUser, pathConf.ReadPass, req.Req) |
||||
if err != nil { |
||||
req.Res <- path.ClientSetupPlayRes{nil, err} |
||||
continue |
||||
} |
||||
|
||||
pm.paths[req.PathName].OnPathManSetupPlay(req) |
||||
|
||||
case <-pm.terminate: |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
select { |
||||
case _, ok := <-pm.pathClose: |
||||
if !ok { |
||||
return |
||||
} |
||||
|
||||
case req := <-pm.clientDescribe: |
||||
req.Res <- path.ClientDescribeRes{nil, fmt.Errorf("terminated")} |
||||
|
||||
case req := <-pm.clientAnnounce: |
||||
req.Res <- path.ClientAnnounceRes{nil, fmt.Errorf("terminated")} |
||||
|
||||
case req := <-pm.clientSetupPlay: |
||||
req.Res <- path.ClientSetupPlayRes{nil, fmt.Errorf("terminated")} |
||||
} |
||||
} |
||||
}() |
||||
|
||||
for _, pa := range pm.paths { |
||||
pa.Close() |
||||
} |
||||
pm.wg.Wait() |
||||
|
||||
close(pm.clientClose) |
||||
close(pm.pathClose) |
||||
close(pm.clientDescribe) |
||||
close(pm.clientAnnounce) |
||||
close(pm.clientSetupPlay) |
||||
} |
||||
|
||||
func (pm *PathManager) findPathConf(name string) (*conf.PathConf, error) { |
||||
err := conf.CheckPathName(name) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("invalid path name: %s (%s)", err, name) |
||||
} |
||||
|
||||
// normal path
|
||||
if pathConf, ok := pm.confPaths[name]; ok { |
||||
return pathConf, nil |
||||
} |
||||
|
||||
// regular expression path
|
||||
for _, pathConf := range pm.confPaths { |
||||
if pathConf.Regexp != nil && pathConf.Regexp.MatchString(name) { |
||||
return pathConf, nil |
||||
} |
||||
} |
||||
|
||||
return nil, fmt.Errorf("unable to find a valid configuration for path '%s'", name) |
||||
} |
||||
|
||||
func (pm *PathManager) OnPathClose(pa *path.Path) { |
||||
pm.pathClose <- pa |
||||
} |
||||
|
||||
func (pm *PathManager) OnPathClientClose(c *client.Client) { |
||||
pm.clientClose <- c |
||||
} |
||||
|
||||
func (pm *PathManager) OnClientDescribe(c *client.Client, pathName string, req *base.Request) (client.Path, error) { |
||||
res := make(chan path.ClientDescribeRes) |
||||
pm.clientDescribe <- path.ClientDescribeReq{res, c, pathName, req} |
||||
re := <-res |
||||
return re.Path, re.Err |
||||
} |
||||
|
||||
func (pm *PathManager) OnClientAnnounce(c *client.Client, pathName string, tracks gortsplib.Tracks, req *base.Request) (client.Path, error) { |
||||
res := make(chan path.ClientAnnounceRes) |
||||
pm.clientAnnounce <- path.ClientAnnounceReq{res, c, pathName, tracks, req} |
||||
re := <-res |
||||
return re.Path, re.Err |
||||
} |
||||
|
||||
func (pm *PathManager) OnClientSetupPlay(c *client.Client, pathName string, trackId int, req *base.Request) (client.Path, error) { |
||||
res := make(chan path.ClientSetupPlayRes) |
||||
pm.clientSetupPlay <- path.ClientSetupPlayReq{res, c, pathName, trackId, req} |
||||
re := <-res |
||||
return re.Path, re.Err |
||||
} |
||||
|
||||
func (pm *PathManager) ClientClose() chan *client.Client { |
||||
return pm.clientClose |
||||
} |
||||
@ -1,52 +0,0 @@
@@ -1,52 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"net" |
||||
) |
||||
|
||||
type serverTCP struct { |
||||
p *program |
||||
listener *net.TCPListener |
||||
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func newServerTCP(p *program) (*serverTCP, error) { |
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{ |
||||
Port: p.conf.RtspPort, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
l := &serverTCP{ |
||||
p: p, |
||||
listener: listener, |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
l.log("opened on :%d", p.conf.RtspPort) |
||||
return l, nil |
||||
} |
||||
|
||||
func (l *serverTCP) log(format string, args ...interface{}) { |
||||
l.p.log("[TCP server] "+format, args...) |
||||
} |
||||
|
||||
func (l *serverTCP) run() { |
||||
defer close(l.done) |
||||
|
||||
for { |
||||
conn, err := l.listener.AcceptTCP() |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
l.p.clientNew <- conn |
||||
} |
||||
} |
||||
|
||||
func (l *serverTCP) close() { |
||||
l.listener.Close() |
||||
<-l.done |
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
package servertcp |
||||
|
||||
import ( |
||||
"net" |
||||
) |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
} |
||||
|
||||
type Server struct { |
||||
parent Parent |
||||
|
||||
listener *net.TCPListener |
||||
|
||||
// out
|
||||
accept chan net.Conn |
||||
done chan struct{} |
||||
} |
||||
|
||||
func New(port int, parent Parent) (*Server, error) { |
||||
listener, err := net.ListenTCP("tcp", &net.TCPAddr{ |
||||
Port: port, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
s := &Server{ |
||||
parent: parent, |
||||
listener: listener, |
||||
accept: make(chan net.Conn), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
parent.Log("[TCP server] opened on :%d", port) |
||||
|
||||
go s.run() |
||||
return s, nil |
||||
} |
||||
|
||||
func (s *Server) Close() { |
||||
go func() { |
||||
for co := range s.accept { |
||||
co.Close() |
||||
} |
||||
}() |
||||
s.listener.Close() |
||||
<-s.done |
||||
} |
||||
|
||||
func (s *Server) run() { |
||||
defer close(s.done) |
||||
|
||||
for { |
||||
conn, err := s.listener.AcceptTCP() |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
s.accept <- conn |
||||
} |
||||
|
||||
close(s.accept) |
||||
} |
||||
|
||||
func (s *Server) Accept() <-chan net.Conn { |
||||
return s.accept |
||||
} |
||||
@ -1,113 +0,0 @@
@@ -1,113 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"net" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/multibuffer" |
||||
) |
||||
|
||||
const ( |
||||
udpReadBufferSize = 2048 |
||||
) |
||||
|
||||
type udpBufAddrPair struct { |
||||
buf []byte |
||||
addr *net.UDPAddr |
||||
} |
||||
|
||||
type serverUDP struct { |
||||
p *program |
||||
pc *net.UDPConn |
||||
streamType gortsplib.StreamType |
||||
readBuf *multibuffer.MultiBuffer |
||||
|
||||
writec chan udpBufAddrPair |
||||
done chan struct{} |
||||
} |
||||
|
||||
func newServerUDP(p *program, port int, streamType gortsplib.StreamType) (*serverUDP, error) { |
||||
pc, err := net.ListenUDP("udp", &net.UDPAddr{ |
||||
Port: port, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
l := &serverUDP{ |
||||
p: p, |
||||
pc: pc, |
||||
streamType: streamType, |
||||
readBuf: multibuffer.New(2, udpReadBufferSize), |
||||
writec: make(chan udpBufAddrPair), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
l.log("opened on :%d", port) |
||||
return l, nil |
||||
} |
||||
|
||||
func (l *serverUDP) log(format string, args ...interface{}) { |
||||
var label string |
||||
if l.streamType == gortsplib.StreamTypeRtp { |
||||
label = "RTP" |
||||
} else { |
||||
label = "RTCP" |
||||
} |
||||
l.p.log("[UDP/"+label+" server] "+format, args...) |
||||
} |
||||
|
||||
func (l *serverUDP) run() { |
||||
defer close(l.done) |
||||
|
||||
writeDone := make(chan struct{}) |
||||
go func() { |
||||
defer close(writeDone) |
||||
for w := range l.writec { |
||||
l.pc.SetWriteDeadline(time.Now().Add(l.p.conf.WriteTimeout)) |
||||
l.pc.WriteTo(w.buf, w.addr) |
||||
} |
||||
}() |
||||
|
||||
for { |
||||
buf := l.readBuf.Next() |
||||
n, addr, err := l.pc.ReadFromUDP(buf) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
pub := l.p.udpPublishersMap.get(makeUDPPublisherAddr(addr.IP, addr.Port)) |
||||
if pub == nil { |
||||
continue |
||||
} |
||||
|
||||
// client sent RTP on RTCP port or vice-versa
|
||||
if pub.streamType != l.streamType { |
||||
continue |
||||
} |
||||
|
||||
atomic.StoreInt64(pub.client.udpLastFrameTimes[pub.trackId], time.Now().Unix()) |
||||
|
||||
pub.client.rtcpReceivers[pub.trackId].OnFrame(l.streamType, buf[:n]) |
||||
|
||||
l.p.readersMap.forwardFrame(pub.client.path, |
||||
pub.trackId, |
||||
l.streamType, |
||||
buf[:n]) |
||||
|
||||
} |
||||
|
||||
close(l.writec) |
||||
<-writeDone |
||||
} |
||||
|
||||
func (l *serverUDP) close() { |
||||
l.pc.Close() |
||||
<-l.done |
||||
} |
||||
|
||||
func (l *serverUDP) write(data []byte, addr *net.UDPAddr) { |
||||
l.writec <- udpBufAddrPair{data, addr} |
||||
} |
||||
@ -0,0 +1,180 @@
@@ -0,0 +1,180 @@
|
||||
package serverudp |
||||
|
||||
import ( |
||||
"net" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
"github.com/aler9/gortsplib/multibuffer" |
||||
) |
||||
|
||||
const ( |
||||
readBufferSize = 2048 |
||||
) |
||||
|
||||
type Publisher interface { |
||||
OnUdpPublisherFrame(int, base.StreamType, []byte) |
||||
} |
||||
|
||||
type publisherData struct { |
||||
publisher Publisher |
||||
trackId int |
||||
} |
||||
|
||||
type bufAddrPair struct { |
||||
buf []byte |
||||
addr *net.UDPAddr |
||||
} |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
} |
||||
|
||||
type publisherAddr struct { |
||||
ip [net.IPv6len]byte // use a fixed-size array to enable the equality operator
|
||||
port int |
||||
} |
||||
|
||||
func (p *publisherAddr) fill(ip net.IP, port int) { |
||||
p.port = port |
||||
|
||||
if len(ip) == net.IPv4len { |
||||
copy(p.ip[0:], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}) // v4InV6Prefix
|
||||
copy(p.ip[12:], ip) |
||||
} else { |
||||
copy(p.ip[:], ip) |
||||
} |
||||
} |
||||
|
||||
type Server struct { |
||||
writeTimeout time.Duration |
||||
streamType gortsplib.StreamType |
||||
|
||||
pc *net.UDPConn |
||||
readBuf *multibuffer.MultiBuffer |
||||
publishersMutex sync.RWMutex |
||||
publishers map[publisherAddr]*publisherData |
||||
|
||||
// in
|
||||
write chan bufAddrPair |
||||
|
||||
// out
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func New(writeTimeout time.Duration, |
||||
port int, |
||||
streamType gortsplib.StreamType, |
||||
parent Parent) (*Server, error) { |
||||
|
||||
pc, err := net.ListenUDP("udp", &net.UDPAddr{ |
||||
Port: port, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
s := &Server{ |
||||
writeTimeout: writeTimeout, |
||||
streamType: streamType, |
||||
pc: pc, |
||||
readBuf: multibuffer.New(2, readBufferSize), |
||||
publishers: make(map[publisherAddr]*publisherData), |
||||
write: make(chan bufAddrPair), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
var label string |
||||
if s.streamType == gortsplib.StreamTypeRtp { |
||||
label = "RTP" |
||||
} else { |
||||
label = "RTCP" |
||||
} |
||||
parent.Log("[UDP/"+label+" server] opened on :%d", port) |
||||
|
||||
go s.run() |
||||
return s, nil |
||||
} |
||||
|
||||
func (s *Server) Close() { |
||||
s.pc.Close() |
||||
<-s.done |
||||
} |
||||
|
||||
func (s *Server) run() { |
||||
defer close(s.done) |
||||
|
||||
writeDone := make(chan struct{}) |
||||
go func() { |
||||
defer close(writeDone) |
||||
for w := range s.write { |
||||
s.pc.SetWriteDeadline(time.Now().Add(s.writeTimeout)) |
||||
s.pc.WriteTo(w.buf, w.addr) |
||||
} |
||||
}() |
||||
|
||||
for { |
||||
buf := s.readBuf.Next() |
||||
n, addr, err := s.pc.ReadFromUDP(buf) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
pub := s.getPublisher(addr.IP, addr.Port) |
||||
if pub == nil { |
||||
continue |
||||
} |
||||
|
||||
pub.publisher.OnUdpPublisherFrame(pub.trackId, s.streamType, buf[:n]) |
||||
} |
||||
|
||||
close(s.write) |
||||
<-writeDone |
||||
} |
||||
|
||||
func (s *Server) Port() int { |
||||
return s.pc.LocalAddr().(*net.UDPAddr).Port |
||||
} |
||||
|
||||
func (s *Server) Write(data []byte, addr *net.UDPAddr) { |
||||
s.write <- bufAddrPair{data, addr} |
||||
} |
||||
|
||||
func (s *Server) AddPublisher(ip net.IP, port int, publisher Publisher, trackId int) { |
||||
s.publishersMutex.Lock() |
||||
defer s.publishersMutex.Unlock() |
||||
|
||||
var addr publisherAddr |
||||
addr.fill(ip, port) |
||||
|
||||
s.publishers[addr] = &publisherData{ |
||||
publisher: publisher, |
||||
trackId: trackId, |
||||
} |
||||
} |
||||
|
||||
func (s *Server) RemovePublisher(ip net.IP, port int, publisher Publisher) { |
||||
s.publishersMutex.Lock() |
||||
defer s.publishersMutex.Unlock() |
||||
|
||||
var addr publisherAddr |
||||
addr.fill(ip, port) |
||||
|
||||
delete(s.publishers, addr) |
||||
} |
||||
|
||||
func (s *Server) getPublisher(ip net.IP, port int) *publisherData { |
||||
s.publishersMutex.RLock() |
||||
defer s.publishersMutex.RUnlock() |
||||
|
||||
var addr publisherAddr |
||||
addr.fill(ip, port) |
||||
|
||||
el, ok := s.publishers[addr] |
||||
if !ok { |
||||
return nil |
||||
} |
||||
return el |
||||
} |
||||
@ -1,322 +0,0 @@
@@ -1,322 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"sync" |
||||
"sync/atomic" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
const ( |
||||
sourceRtspRetryInterval = 5 * time.Second |
||||
) |
||||
|
||||
type sourceRtspState int |
||||
|
||||
const ( |
||||
sourceRtspStateStopped sourceRtspState = iota |
||||
sourceRtspStateRunning |
||||
) |
||||
|
||||
type sourceRtsp struct { |
||||
p *program |
||||
path *path |
||||
state sourceRtspState |
||||
tracks []*gortsplib.Track |
||||
innerRunning bool |
||||
|
||||
innerTerminate chan struct{} |
||||
innerDone chan struct{} |
||||
setState chan sourceRtspState |
||||
terminate chan struct{} |
||||
done chan struct{} |
||||
} |
||||
|
||||
func newSourceRtsp(p *program, path *path) *sourceRtsp { |
||||
s := &sourceRtsp{ |
||||
p: p, |
||||
path: path, |
||||
setState: make(chan sourceRtspState), |
||||
terminate: make(chan struct{}), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
atomic.AddInt64(p.countSourcesRtsp, +1) |
||||
|
||||
if path.conf.SourceOnDemand { |
||||
s.state = sourceRtspStateStopped |
||||
} else { |
||||
s.state = sourceRtspStateRunning |
||||
atomic.AddInt64(p.countSourcesRtspRunning, +1) |
||||
} |
||||
|
||||
return s |
||||
} |
||||
|
||||
func (s *sourceRtsp) isSource() {} |
||||
|
||||
func (s *sourceRtsp) run(initialState sourceRtspState) { |
||||
defer close(s.done) |
||||
|
||||
s.applyState(initialState) |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case state := <-s.setState: |
||||
s.applyState(state) |
||||
|
||||
case <-s.terminate: |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
if s.innerRunning { |
||||
close(s.innerTerminate) |
||||
<-s.innerDone |
||||
} |
||||
|
||||
close(s.setState) |
||||
} |
||||
|
||||
func (s *sourceRtsp) applyState(state sourceRtspState) { |
||||
if state == sourceRtspStateRunning { |
||||
if !s.innerRunning { |
||||
s.path.log("rtsp source started") |
||||
s.innerRunning = true |
||||
s.innerTerminate = make(chan struct{}) |
||||
s.innerDone = make(chan struct{}) |
||||
go s.runInner() |
||||
} |
||||
} else { |
||||
if s.innerRunning { |
||||
close(s.innerTerminate) |
||||
<-s.innerDone |
||||
s.innerRunning = false |
||||
s.path.log("rtsp source stopped") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *sourceRtsp) runInner() { |
||||
defer close(s.innerDone) |
||||
|
||||
outer: |
||||
for { |
||||
ok := s.runInnerInner() |
||||
if !ok { |
||||
break outer |
||||
} |
||||
|
||||
t := time.NewTimer(sourceRtspRetryInterval) |
||||
defer t.Stop() |
||||
|
||||
select { |
||||
case <-s.innerTerminate: |
||||
break outer |
||||
case <-t.C: |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *sourceRtsp) runInnerInner() bool { |
||||
s.path.log("connecting to rtsp source") |
||||
|
||||
var conn *gortsplib.ConnClient |
||||
var err error |
||||
dialDone := make(chan struct{}, 1) |
||||
go func() { |
||||
defer close(dialDone) |
||||
conn, err = gortsplib.NewConnClient(gortsplib.ConnClientConf{ |
||||
Host: s.path.conf.SourceUrl.Host, |
||||
ReadTimeout: s.p.conf.ReadTimeout, |
||||
WriteTimeout: s.p.conf.WriteTimeout, |
||||
ReadBufferCount: 2, |
||||
}) |
||||
}() |
||||
|
||||
select { |
||||
case <-s.innerTerminate: |
||||
return false |
||||
case <-dialDone: |
||||
} |
||||
|
||||
if err != nil { |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
_, err = conn.Options(s.path.conf.SourceUrl) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
tracks, _, err := conn.Describe(s.path.conf.SourceUrl) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
// create a filtered SDP that is used by the server (not by the client)
|
||||
s.path.sourceSdp = tracks.Write() |
||||
s.path.sourceTrackCount = len(tracks) |
||||
s.tracks = tracks |
||||
|
||||
if s.path.conf.SourceProtocolParsed == gortsplib.StreamProtocolUDP { |
||||
return s.runUDP(conn) |
||||
} else { |
||||
return s.runTCP(conn) |
||||
} |
||||
} |
||||
|
||||
func (s *sourceRtsp) runUDP(conn *gortsplib.ConnClient) bool { |
||||
for _, track := range s.tracks { |
||||
_, err := conn.SetupUDP(s.path.conf.SourceUrl, gortsplib.TransportModePlay, track, 0, 0) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
_, err := conn.Play(s.path.conf.SourceUrl) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
s.p.sourceRtspReady <- s |
||||
s.path.log("rtsp source ready") |
||||
|
||||
var wg sync.WaitGroup |
||||
|
||||
// receive RTP packets
|
||||
for trackId := range s.tracks { |
||||
wg.Add(1) |
||||
go func(trackId int) { |
||||
defer wg.Done() |
||||
|
||||
for { |
||||
buf, err := conn.ReadFrameUDP(trackId, gortsplib.StreamTypeRtp) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
s.p.readersMap.forwardFrame(s.path, trackId, |
||||
gortsplib.StreamTypeRtp, buf) |
||||
} |
||||
}(trackId) |
||||
} |
||||
|
||||
// receive RTCP packets
|
||||
for trackId := range s.tracks { |
||||
wg.Add(1) |
||||
go func(trackId int) { |
||||
defer wg.Done() |
||||
|
||||
for { |
||||
buf, err := conn.ReadFrameUDP(trackId, gortsplib.StreamTypeRtcp) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
s.p.readersMap.forwardFrame(s.path, trackId, |
||||
gortsplib.StreamTypeRtcp, buf) |
||||
} |
||||
}(trackId) |
||||
} |
||||
|
||||
tcpConnDone := make(chan error) |
||||
go func() { |
||||
tcpConnDone <- conn.LoopUDP() |
||||
}() |
||||
|
||||
var ret bool |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-s.innerTerminate: |
||||
conn.Close() |
||||
<-tcpConnDone |
||||
ret = false |
||||
break outer |
||||
|
||||
case err := <-tcpConnDone: |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
ret = true |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
wg.Wait() |
||||
|
||||
s.p.sourceRtspNotReady <- s |
||||
s.path.log("rtsp source not ready") |
||||
|
||||
return ret |
||||
} |
||||
|
||||
func (s *sourceRtsp) runTCP(conn *gortsplib.ConnClient) bool { |
||||
for _, track := range s.tracks { |
||||
_, err := conn.SetupTCP(s.path.conf.SourceUrl, gortsplib.TransportModePlay, track) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
_, err := conn.Play(s.path.conf.SourceUrl) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
s.p.sourceRtspReady <- s |
||||
s.path.log("rtsp source ready") |
||||
|
||||
tcpConnDone := make(chan error) |
||||
go func() { |
||||
for { |
||||
trackId, streamType, content, err := conn.ReadFrameTCP() |
||||
if err != nil { |
||||
tcpConnDone <- err |
||||
return |
||||
} |
||||
|
||||
s.p.readersMap.forwardFrame(s.path, trackId, streamType, content) |
||||
} |
||||
}() |
||||
|
||||
var ret bool |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-s.innerTerminate: |
||||
conn.Close() |
||||
<-tcpConnDone |
||||
ret = false |
||||
break outer |
||||
|
||||
case err := <-tcpConnDone: |
||||
conn.Close() |
||||
s.path.log("rtsp source ERR: %s", err) |
||||
ret = true |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
s.p.sourceRtspNotReady <- s |
||||
s.path.log("rtsp source not ready") |
||||
|
||||
return ret |
||||
} |
||||
@ -0,0 +1,345 @@
@@ -0,0 +1,345 @@
|
||||
package sourcertsp |
||||
|
||||
import ( |
||||
"net/url" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
const ( |
||||
retryInterval = 5 * time.Second |
||||
) |
||||
|
||||
type Parent interface { |
||||
Log(string, ...interface{}) |
||||
OnSourceReady(gortsplib.Tracks) |
||||
OnSourceNotReady() |
||||
OnFrame(int, gortsplib.StreamType, []byte) |
||||
} |
||||
|
||||
type State int |
||||
|
||||
const ( |
||||
StateStopped State = iota |
||||
StateRunning |
||||
) |
||||
|
||||
type Source struct { |
||||
ur string |
||||
proto gortsplib.StreamProtocol |
||||
readTimeout time.Duration |
||||
writeTimeout time.Duration |
||||
state State |
||||
parent Parent |
||||
|
||||
innerRunning bool |
||||
|
||||
// in
|
||||
innerTerminate chan struct{} |
||||
innerDone chan struct{} |
||||
stateChange chan State |
||||
terminate chan struct{} |
||||
|
||||
// out
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func New(ur string, |
||||
proto gortsplib.StreamProtocol, |
||||
readTimeout time.Duration, |
||||
writeTimeout time.Duration, |
||||
state State, |
||||
parent Parent) *Source { |
||||
s := &Source{ |
||||
ur: ur, |
||||
proto: proto, |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
state: state, |
||||
parent: parent, |
||||
stateChange: make(chan State), |
||||
terminate: make(chan struct{}), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
go s.run(s.state) |
||||
return s |
||||
} |
||||
|
||||
func (s *Source) Close() { |
||||
close(s.terminate) |
||||
<-s.done |
||||
} |
||||
|
||||
func (s *Source) IsSource() {} |
||||
|
||||
func (s *Source) State() State { |
||||
return s.state |
||||
} |
||||
|
||||
func (s *Source) SetState(state State) { |
||||
s.state = state |
||||
s.stateChange <- s.state |
||||
} |
||||
|
||||
func (s *Source) run(initialState State) { |
||||
defer close(s.done) |
||||
|
||||
s.applyState(initialState) |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case state := <-s.stateChange: |
||||
s.applyState(state) |
||||
|
||||
case <-s.terminate: |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
if s.innerRunning { |
||||
close(s.innerTerminate) |
||||
<-s.innerDone |
||||
} |
||||
|
||||
close(s.stateChange) |
||||
} |
||||
|
||||
func (s *Source) applyState(state State) { |
||||
if state == StateRunning { |
||||
if !s.innerRunning { |
||||
s.parent.Log("rtsp source started") |
||||
s.innerRunning = true |
||||
s.innerTerminate = make(chan struct{}) |
||||
s.innerDone = make(chan struct{}) |
||||
go s.runInner() |
||||
} |
||||
} else { |
||||
if s.innerRunning { |
||||
close(s.innerTerminate) |
||||
<-s.innerDone |
||||
s.innerRunning = false |
||||
s.parent.Log("rtsp source stopped") |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Source) runInner() { |
||||
defer close(s.innerDone) |
||||
|
||||
outer: |
||||
for { |
||||
ok := s.runInnerInner() |
||||
if !ok { |
||||
break outer |
||||
} |
||||
|
||||
t := time.NewTimer(retryInterval) |
||||
defer t.Stop() |
||||
|
||||
select { |
||||
case <-s.innerTerminate: |
||||
break outer |
||||
case <-t.C: |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Source) runInnerInner() bool { |
||||
s.parent.Log("connecting to rtsp source") |
||||
|
||||
u, _ := url.Parse(s.ur) |
||||
|
||||
var conn *gortsplib.ConnClient |
||||
var err error |
||||
dialDone := make(chan struct{}, 1) |
||||
go func() { |
||||
defer close(dialDone) |
||||
conn, err = gortsplib.NewConnClient(gortsplib.ConnClientConf{ |
||||
Host: u.Host, |
||||
ReadTimeout: s.readTimeout, |
||||
WriteTimeout: s.writeTimeout, |
||||
ReadBufferCount: 2, |
||||
}) |
||||
}() |
||||
|
||||
select { |
||||
case <-s.innerTerminate: |
||||
return false |
||||
case <-dialDone: |
||||
} |
||||
|
||||
if err != nil { |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
_, err = conn.Options(u) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
tracks, _, err := conn.Describe(u) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
if s.proto == gortsplib.StreamProtocolUDP { |
||||
return s.runUDP(u, conn, tracks) |
||||
} else { |
||||
return s.runTCP(u, conn, tracks) |
||||
} |
||||
} |
||||
|
||||
func (s *Source) runUDP(u *url.URL, conn *gortsplib.ConnClient, tracks gortsplib.Tracks) bool { |
||||
for _, track := range tracks { |
||||
_, err := conn.SetupUDP(u, gortsplib.TransportModePlay, track, 0, 0) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
_, err := conn.Play(u) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
s.parent.OnSourceReady(tracks) |
||||
s.parent.Log("rtsp source ready") |
||||
|
||||
var wg sync.WaitGroup |
||||
|
||||
// receive RTP packets
|
||||
for trackId := range tracks { |
||||
wg.Add(1) |
||||
go func(trackId int) { |
||||
defer wg.Done() |
||||
|
||||
for { |
||||
buf, err := conn.ReadFrameUDP(trackId, gortsplib.StreamTypeRtp) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
s.parent.OnFrame(trackId, gortsplib.StreamTypeRtp, buf) |
||||
} |
||||
}(trackId) |
||||
} |
||||
|
||||
// receive RTCP packets
|
||||
for trackId := range tracks { |
||||
wg.Add(1) |
||||
go func(trackId int) { |
||||
defer wg.Done() |
||||
|
||||
for { |
||||
buf, err := conn.ReadFrameUDP(trackId, gortsplib.StreamTypeRtcp) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
s.parent.OnFrame(trackId, gortsplib.StreamTypeRtcp, buf) |
||||
} |
||||
}(trackId) |
||||
} |
||||
|
||||
tcpConnDone := make(chan error) |
||||
go func() { |
||||
tcpConnDone <- conn.LoopUDP() |
||||
}() |
||||
|
||||
var ret bool |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-s.innerTerminate: |
||||
conn.Close() |
||||
<-tcpConnDone |
||||
ret = false |
||||
break outer |
||||
|
||||
case err := <-tcpConnDone: |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
ret = true |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
wg.Wait() |
||||
|
||||
s.parent.OnSourceNotReady() |
||||
s.parent.Log("rtsp source not ready") |
||||
|
||||
return ret |
||||
} |
||||
|
||||
func (s *Source) runTCP(u *url.URL, conn *gortsplib.ConnClient, tracks gortsplib.Tracks) bool { |
||||
for _, track := range tracks { |
||||
_, err := conn.SetupTCP(u, gortsplib.TransportModePlay, track) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
_, err := conn.Play(u) |
||||
if err != nil { |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
s.parent.OnSourceReady(tracks) |
||||
s.parent.Log("rtsp source ready") |
||||
|
||||
tcpConnDone := make(chan error) |
||||
go func() { |
||||
for { |
||||
trackId, streamType, content, err := conn.ReadFrameTCP() |
||||
if err != nil { |
||||
tcpConnDone <- err |
||||
return |
||||
} |
||||
|
||||
s.parent.OnFrame(trackId, streamType, content) |
||||
} |
||||
}() |
||||
|
||||
var ret bool |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case <-s.innerTerminate: |
||||
conn.Close() |
||||
<-tcpConnDone |
||||
ret = false |
||||
break outer |
||||
|
||||
case err := <-tcpConnDone: |
||||
conn.Close() |
||||
s.parent.Log("rtsp source ERR: %s", err) |
||||
ret = true |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
s.parent.OnSourceNotReady() |
||||
s.parent.Log("rtsp source not ready") |
||||
|
||||
return ret |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
package stats |
||||
|
||||
func ptrInt64() *int64 { |
||||
v := int64(0) |
||||
return &v |
||||
} |
||||
|
||||
type Stats struct { |
||||
// use pointers to avoid a crash on 32bit platforms
|
||||
// https://github.com/golang/go/issues/9959
|
||||
CountClients *int64 |
||||
CountPublishers *int64 |
||||
CountReaders *int64 |
||||
CountSourcesRtsp *int64 |
||||
CountSourcesRtspRunning *int64 |
||||
CountSourcesRtmp *int64 |
||||
CountSourcesRtmpRunning *int64 |
||||
} |
||||
|
||||
func New() *Stats { |
||||
return &Stats{ |
||||
CountClients: ptrInt64(), |
||||
CountPublishers: ptrInt64(), |
||||
CountReaders: ptrInt64(), |
||||
CountSourcesRtsp: ptrInt64(), |
||||
CountSourcesRtspRunning: ptrInt64(), |
||||
CountSourcesRtmp: ptrInt64(), |
||||
CountSourcesRtmpRunning: ptrInt64(), |
||||
} |
||||
} |
||||
@ -1,205 +0,0 @@
@@ -1,205 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
"strings" |
||||
"sync" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/base" |
||||
) |
||||
|
||||
func ipEqualOrInRange(ip net.IP, ips []interface{}) bool { |
||||
for _, item := range ips { |
||||
switch titem := item.(type) { |
||||
case net.IP: |
||||
if titem.Equal(ip) { |
||||
return true |
||||
} |
||||
|
||||
case *net.IPNet: |
||||
if titem.Contains(ip) { |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func splitPath(path string) (string, string, error) { |
||||
pos := func() int { |
||||
for i := len(path) - 1; i >= 0; i-- { |
||||
if path[i] == '/' { |
||||
return i |
||||
} |
||||
} |
||||
return -1 |
||||
}() |
||||
|
||||
if pos < 0 { |
||||
return "", "", fmt.Errorf("the path must contain a base path and a control path (%s)", path) |
||||
} |
||||
|
||||
basePath := path[:pos] |
||||
controlPath := path[pos+1:] |
||||
|
||||
if len(basePath) == 0 { |
||||
return "", "", fmt.Errorf("empty base path (%s)", basePath) |
||||
} |
||||
|
||||
if len(controlPath) == 0 { |
||||
return "", "", fmt.Errorf("empty control path (%s)", controlPath) |
||||
} |
||||
|
||||
return basePath, controlPath, nil |
||||
} |
||||
|
||||
func removeQueryFromPath(path string) string { |
||||
i := strings.Index(path, "?") |
||||
if i >= 0 { |
||||
return path[:i] |
||||
} |
||||
return path |
||||
} |
||||
|
||||
type udpPublisherAddr struct { |
||||
ip [net.IPv6len]byte // use a fixed-size array to enable the equality operator
|
||||
port int |
||||
} |
||||
|
||||
func makeUDPPublisherAddr(ip net.IP, port int) udpPublisherAddr { |
||||
ret := udpPublisherAddr{ |
||||
port: port, |
||||
} |
||||
|
||||
if len(ip) == net.IPv4len { |
||||
copy(ret.ip[0:], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}) // v4InV6Prefix
|
||||
copy(ret.ip[12:], ip) |
||||
} else { |
||||
copy(ret.ip[:], ip) |
||||
} |
||||
|
||||
return ret |
||||
} |
||||
|
||||
type udpPublisher struct { |
||||
client *client |
||||
trackId int |
||||
streamType gortsplib.StreamType |
||||
} |
||||
|
||||
type udpPublishersMap struct { |
||||
mutex sync.RWMutex |
||||
ma map[udpPublisherAddr]*udpPublisher |
||||
} |
||||
|
||||
func newUdpPublisherMap() *udpPublishersMap { |
||||
return &udpPublishersMap{ |
||||
ma: make(map[udpPublisherAddr]*udpPublisher), |
||||
} |
||||
} |
||||
|
||||
func (m *udpPublishersMap) clear() { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
m.ma = make(map[udpPublisherAddr]*udpPublisher) |
||||
} |
||||
|
||||
func (m *udpPublishersMap) add(addr udpPublisherAddr, pub *udpPublisher) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
m.ma[addr] = pub |
||||
} |
||||
|
||||
func (m *udpPublishersMap) remove(addr udpPublisherAddr) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
delete(m.ma, addr) |
||||
} |
||||
|
||||
func (m *udpPublishersMap) get(addr udpPublisherAddr) *udpPublisher { |
||||
m.mutex.RLock() |
||||
defer m.mutex.RUnlock() |
||||
|
||||
el, ok := m.ma[addr] |
||||
if !ok { |
||||
return nil |
||||
} |
||||
return el |
||||
} |
||||
|
||||
type readersMap struct { |
||||
mutex sync.RWMutex |
||||
ma map[*client]struct{} |
||||
} |
||||
|
||||
func newReadersMap() *readersMap { |
||||
return &readersMap{ |
||||
ma: make(map[*client]struct{}), |
||||
} |
||||
} |
||||
|
||||
func (m *readersMap) clear() { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
m.ma = make(map[*client]struct{}) |
||||
} |
||||
|
||||
func (m *readersMap) add(reader *client) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
m.ma[reader] = struct{}{} |
||||
} |
||||
|
||||
func (m *readersMap) remove(reader *client) { |
||||
m.mutex.Lock() |
||||
defer m.mutex.Unlock() |
||||
|
||||
delete(m.ma, reader) |
||||
} |
||||
|
||||
func (m *readersMap) forwardFrame(path *path, trackId int, streamType gortsplib.StreamType, frame []byte) { |
||||
m.mutex.RLock() |
||||
defer m.mutex.RUnlock() |
||||
|
||||
for c := range m.ma { |
||||
if c.path != path { |
||||
continue |
||||
} |
||||
|
||||
track, ok := c.streamTracks[trackId] |
||||
if !ok { |
||||
continue |
||||
} |
||||
|
||||
if c.streamProtocol == gortsplib.StreamProtocolUDP { |
||||
if streamType == gortsplib.StreamTypeRtp { |
||||
c.p.serverUdpRtp.write(frame, &net.UDPAddr{ |
||||
IP: c.ip(), |
||||
Zone: c.zone(), |
||||
Port: track.rtpPort, |
||||
}) |
||||
|
||||
} else { |
||||
c.p.serverUdpRtcp.write(frame, &net.UDPAddr{ |
||||
IP: c.ip(), |
||||
Zone: c.zone(), |
||||
Port: track.rtcpPort, |
||||
}) |
||||
} |
||||
|
||||
} else { |
||||
c.tcpFrame <- &base.InterleavedFrame{ |
||||
TrackId: trackId, |
||||
StreamType: streamType, |
||||
Content: frame, |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue