26 changed files with 2474 additions and 1743 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
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