new structure
This commit is contained in:
parent
d9df32ec85
commit
80f46921c9
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
||||
}
|
95
conf/conf.go
95
conf/conf.go
|
@ -2,7 +2,6 @@ package conf
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
|
@ -17,56 +16,9 @@ import (
|
|||
"github.com/aler9/rtsp-simple-server/loghandler"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type PathConf struct {
|
||||
Regexp *regexp.Regexp `yaml:"-"`
|
||||
Source string `yaml:"source"`
|
||||
SourceUrl *url.URL `yaml:"-"`
|
||||
SourceProtocol string `yaml:"sourceProtocol"`
|
||||
SourceProtocolParsed gortsplib.StreamProtocol `yaml:"-"`
|
||||
SourceOnDemand bool `yaml:"sourceOnDemand"`
|
||||
|
@ -133,7 +85,7 @@ func Load(fpath string) (*Conf, error) {
|
|||
}
|
||||
|
||||
// read from environment
|
||||
err = confenv.Process("RTSP", conf)
|
||||
err = confenv.Load("RTSP", conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -244,7 +196,7 @@ func Load(fpath string) (*Conf, error) {
|
|||
|
||||
// normal path
|
||||
if name[0] != '~' {
|
||||
err := checkPathName(name)
|
||||
err := CheckPathName(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path name: %s (%s)", err, name)
|
||||
}
|
||||
|
@ -267,16 +219,13 @@ func Load(fpath string) (*Conf, error) {
|
|||
return nil, fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTSP source; use another path")
|
||||
}
|
||||
|
||||
pconf.SourceUrl, err = url.Parse(pconf.Source)
|
||||
u, err := url.Parse(pconf.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s' is not a valid url", pconf.Source)
|
||||
}
|
||||
if pconf.SourceUrl.Port() == "" {
|
||||
pconf.SourceUrl.Host += ":554"
|
||||
}
|
||||
if pconf.SourceUrl.User != nil {
|
||||
pass, _ := pconf.SourceUrl.User.Password()
|
||||
user := pconf.SourceUrl.User.Username()
|
||||
if u.User != nil {
|
||||
pass, _ := u.User.Password()
|
||||
user := u.User.Username()
|
||||
if user != "" && pass == "" ||
|
||||
user == "" && pass != "" {
|
||||
fmt.Errorf("username and password must be both provided")
|
||||
|
@ -302,12 +251,17 @@ func Load(fpath string) (*Conf, error) {
|
|||
return nil, fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path")
|
||||
}
|
||||
|
||||
pconf.SourceUrl, err = url.Parse(pconf.Source)
|
||||
u, err := url.Parse(pconf.Source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("'%s' is not a valid url", pconf.Source)
|
||||
}
|
||||
if pconf.SourceUrl.Port() == "" {
|
||||
pconf.SourceUrl.Host += ":1935"
|
||||
if u.User != nil {
|
||||
pass, _ := u.User.Password()
|
||||
user := u.User.Username()
|
||||
if user != "" && pass == "" ||
|
||||
user == "" && pass != "" {
|
||||
fmt.Errorf("username and password must be both provided")
|
||||
}
|
||||
}
|
||||
|
||||
} else if pconf.Source == "record" {
|
||||
|
@ -371,24 +325,3 @@ func Load(fpath string) (*Conf, error) {
|
|||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (conf *Conf) CheckPathNameAndFindConf(name string) (*PathConf, error) {
|
||||
err := checkPathName(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path name: %s (%s)", err, name)
|
||||
}
|
||||
|
||||
// normal path
|
||||
if pconf, ok := conf.Paths[name]; ok {
|
||||
return pconf, nil
|
||||
}
|
||||
|
||||
// regular expression path
|
||||
for _, pconf := range conf.Paths {
|
||||
if pconf.Regexp != nil && pconf.Regexp.MatchString(name) {
|
||||
return pconf, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to find a valid configuration for path '%s'", name)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -9,7 +9,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func process(env map[string]string, envKey string, rv reflect.Value) error {
|
||||
func load(env map[string]string, envKey string, rv reflect.Value) error {
|
||||
rt := rv.Type()
|
||||
|
||||
switch rt {
|
||||
|
@ -93,7 +93,7 @@ func process(env map[string]string, envKey string, rv reflect.Value) error {
|
|||
rv.SetMapIndex(reflect.ValueOf(mapKey), nv)
|
||||
}
|
||||
|
||||
err := process(env, envKey+"_"+strings.ToUpper(mapKey), nv.Elem())
|
||||
err := load(env, envKey+"_"+strings.ToUpper(mapKey), nv.Elem())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -105,13 +105,13 @@ func process(env map[string]string, envKey string, rv reflect.Value) error {
|
|||
for i := 0; i < flen; i++ {
|
||||
f := rt.Field(i)
|
||||
|
||||
// process only public fields
|
||||
// load only public fields
|
||||
if f.Tag.Get("yaml") == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldEnvKey := envKey + "_" + strings.ToUpper(f.Name)
|
||||
err := process(env, fieldEnvKey, rv.Field(i))
|
||||
err := load(env, fieldEnvKey, rv.Field(i))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -122,12 +122,12 @@ func process(env map[string]string, envKey string, rv reflect.Value) error {
|
|||
return fmt.Errorf("unsupported type: %v", rt)
|
||||
}
|
||||
|
||||
func Process(envKey string, v interface{}) error {
|
||||
func Load(envKey string, v interface{}) error {
|
||||
env := make(map[string]string)
|
||||
for _, kv := range os.Environ() {
|
||||
tmp := strings.Split(kv, "=")
|
||||
env[tmp[0]] = tmp[1]
|
||||
}
|
||||
|
||||
return process(env, envKey, reflect.ValueOf(v).Elem())
|
||||
return load(env, envKey, reflect.ValueOf(v).Elem())
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.14
|
|||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
|
||||
github.com/aler9/gortsplib v0.0.0-20201016210035-b647e5ee314a
|
||||
github.com/aler9/gortsplib v0.0.0-20201017143703-0b7201de6890
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/notedit/rtmp v0.0.2
|
||||
github.com/pion/rtp v1.6.1 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -2,8 +2,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
|||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aler9/gortsplib v0.0.0-20201016210035-b647e5ee314a h1:oSyGggUDkjjKrNw8M0GH58zbV9XJRtOCITMExgGG6aA=
|
||||
github.com/aler9/gortsplib v0.0.0-20201016210035-b647e5ee314a/go.mod h1:8mpBfMEJIZn2C5fMM6vRYHgGH49WX0EH8gP1SDxv0Uw=
|
||||
github.com/aler9/gortsplib v0.0.0-20201017143703-0b7201de6890 h1:p5tvUXK9CrNrkVdRhVetDuOsT64y+qEos5ZWnbxdUMo=
|
||||
github.com/aler9/gortsplib v0.0.0-20201017143703-0b7201de6890/go.mod h1:8mpBfMEJIZn2C5fMM6vRYHgGH49WX0EH8gP1SDxv0Uw=
|
||||
github.com/aler9/sdp-dirty/v3 v3.0.0-20200919115950-f1abc664f625 h1:A3upkpYzceQTuBPvVleu1zd6R8jInhg5ifimSO7ku/o=
|
||||
github.com/aler9/sdp-dirty/v3 v3.0.0-20200919115950-f1abc664f625/go.mod h1:5bO/aUQr9m3OasDatNNcVqKAgs7r5hgGXmszWHaC6mI=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
|
|
|
@ -16,10 +16,17 @@ const (
|
|||
DestinationSyslog
|
||||
)
|
||||
|
||||
type writeFunc func(p []byte) (int, error)
|
||||
|
||||
func (f writeFunc) Write(p []byte) (int, error) {
|
||||
return f(p)
|
||||
}
|
||||
|
||||
type LogHandler struct {
|
||||
destinations map[Destination]struct{}
|
||||
file *os.File
|
||||
syslog io.WriteCloser
|
||||
|
||||
file *os.File
|
||||
syslog io.WriteCloser
|
||||
}
|
||||
|
||||
func New(destinations map[Destination]struct{}, filePath string) (*LogHandler, error) {
|
||||
|
@ -45,7 +52,7 @@ func New(destinations map[Destination]struct{}, filePath string) (*LogHandler, e
|
|||
}
|
||||
}
|
||||
|
||||
log.SetOutput(lh)
|
||||
log.SetOutput(writeFunc(lh.write))
|
||||
|
||||
return lh, nil
|
||||
}
|
||||
|
@ -60,7 +67,7 @@ func (lh *LogHandler) Close() {
|
|||
}
|
||||
}
|
||||
|
||||
func (lh *LogHandler) Write(p []byte) (int, error) {
|
||||
func (lh *LogHandler) write(p []byte) (int, error) {
|
||||
if _, ok := lh.destinations[DestinationStdout]; ok {
|
||||
print(string(p))
|
||||
}
|
||||
|
|
359
main.go
359
main.go
|
@ -3,61 +3,39 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/aler9/gortsplib"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/clientman"
|
||||
"github.com/aler9/rtsp-simple-server/conf"
|
||||
"github.com/aler9/rtsp-simple-server/loghandler"
|
||||
"github.com/aler9/rtsp-simple-server/metrics"
|
||||
"github.com/aler9/rtsp-simple-server/pathman"
|
||||
"github.com/aler9/rtsp-simple-server/pprof"
|
||||
"github.com/aler9/rtsp-simple-server/servertcp"
|
||||
"github.com/aler9/rtsp-simple-server/serverudp"
|
||||
"github.com/aler9/rtsp-simple-server/stats"
|
||||
)
|
||||
|
||||
var Version = "v0.0.0"
|
||||
|
||||
const (
|
||||
checkPathPeriod = 5 * time.Second
|
||||
)
|
||||
|
||||
type program struct {
|
||||
conf *conf.Conf
|
||||
logHandler *loghandler.LogHandler
|
||||
metrics *metrics
|
||||
pprof *pprof
|
||||
paths map[string]*path
|
||||
serverUdpRtp *serverUDP
|
||||
serverUdpRtcp *serverUDP
|
||||
serverTcp *serverTCP
|
||||
clients map[*client]struct{}
|
||||
clientsWg sync.WaitGroup
|
||||
udpPublishersMap *udpPublishersMap
|
||||
readersMap *readersMap
|
||||
// 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
|
||||
conf *conf.Conf
|
||||
stats *stats.Stats
|
||||
logHandler *loghandler.LogHandler
|
||||
metrics *metrics.Metrics
|
||||
pprof *pprof.Pprof
|
||||
serverUdpRtp *serverudp.Server
|
||||
serverUdpRtcp *serverudp.Server
|
||||
serverTcp *servertcp.Server
|
||||
pathMan *pathman.PathManager
|
||||
clientMan *clientman.ClientManager
|
||||
|
||||
clientNew chan net.Conn
|
||||
clientClose chan *client
|
||||
clientDescribe chan clientDescribeReq
|
||||
clientAnnounce chan clientAnnounceReq
|
||||
clientSetupPlay chan clientSetupPlayReq
|
||||
clientPlay chan *client
|
||||
clientRecord chan *client
|
||||
sourceRtspReady chan *sourceRtsp
|
||||
sourceRtspNotReady chan *sourceRtsp
|
||||
sourceRtmpReady chan *sourceRtmp
|
||||
sourceRtmpNotReady chan *sourceRtmp
|
||||
terminate chan struct{}
|
||||
done chan struct{}
|
||||
terminate chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newProgram(args []string) (*program, error) {
|
||||
|
@ -79,109 +57,76 @@ func newProgram(args []string) (*program, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
logHandler, err := loghandler.New(conf.LogDestinationsParsed, conf.LogFile)
|
||||
p := &program{
|
||||
conf: conf,
|
||||
terminate: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.stats = stats.New()
|
||||
|
||||
p.logHandler, err = loghandler.New(conf.LogDestinationsParsed, conf.LogFile)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &program{
|
||||
conf: conf,
|
||||
logHandler: logHandler,
|
||||
paths: make(map[string]*path),
|
||||
clients: make(map[*client]struct{}),
|
||||
udpPublishersMap: newUdpPublisherMap(),
|
||||
readersMap: newReadersMap(),
|
||||
countClients: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countPublishers: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countReaders: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countSourcesRtsp: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countSourcesRtspRunning: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countSourcesRtmp: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
countSourcesRtmpRunning: func() *int64 {
|
||||
v := int64(0)
|
||||
return &v
|
||||
}(),
|
||||
clientNew: make(chan net.Conn),
|
||||
clientClose: make(chan *client),
|
||||
clientDescribe: make(chan clientDescribeReq),
|
||||
clientAnnounce: make(chan clientAnnounceReq),
|
||||
clientSetupPlay: make(chan clientSetupPlayReq),
|
||||
clientPlay: make(chan *client),
|
||||
clientRecord: make(chan *client),
|
||||
sourceRtspReady: make(chan *sourceRtsp),
|
||||
sourceRtspNotReady: make(chan *sourceRtsp),
|
||||
sourceRtmpReady: make(chan *sourceRtmp),
|
||||
sourceRtmpNotReady: make(chan *sourceRtmp),
|
||||
terminate: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.log("rtsp-simple-server %s", Version)
|
||||
p.Log("rtsp-simple-server %s", Version)
|
||||
|
||||
if conf.Metrics {
|
||||
p.metrics, err = newMetrics(p)
|
||||
p.metrics, err = metrics.New(p.stats, p)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Pprof {
|
||||
p.pprof, err = newPprof(p)
|
||||
p.pprof, err = pprof.New(p)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for name, pathConf := range conf.Paths {
|
||||
if pathConf.Regexp == nil {
|
||||
p.paths[name] = newPath(p, name, pathConf)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := conf.ProtocolsParsed[gortsplib.StreamProtocolUDP]; ok {
|
||||
p.serverUdpRtp, err = newServerUDP(p, conf.RtpPort, gortsplib.StreamTypeRtp)
|
||||
p.serverUdpRtp, err = serverudp.New(p.conf.WriteTimeout,
|
||||
conf.RtpPort, gortsplib.StreamTypeRtp, p)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.serverUdpRtcp, err = newServerUDP(p, conf.RtcpPort, gortsplib.StreamTypeRtcp)
|
||||
p.serverUdpRtcp, err = serverudp.New(p.conf.WriteTimeout,
|
||||
conf.RtcpPort, gortsplib.StreamTypeRtcp, p)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
p.serverTcp, err = newServerTCP(p)
|
||||
p.serverTcp, err = servertcp.New(conf.RtspPort, p)
|
||||
if err != nil {
|
||||
p.closeResources()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go p.run()
|
||||
p.pathMan = pathman.New(p.stats, p.serverUdpRtp, p.serverUdpRtcp,
|
||||
p.conf.ReadTimeout, p.conf.WriteTimeout, p.conf.AuthMethodsParsed,
|
||||
conf.Paths, p)
|
||||
|
||||
p.clientMan = clientman.New(p.stats, p.serverUdpRtp, p.serverUdpRtcp,
|
||||
p.conf.ReadTimeout, p.conf.WriteTimeout, p.conf.RunOnConnect,
|
||||
p.conf.ProtocolsParsed, p.pathMan, p.serverTcp, p)
|
||||
|
||||
go p.run()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *program) log(format string, args ...interface{}) {
|
||||
countClients := atomic.LoadInt64(p.countClients)
|
||||
countPublishers := atomic.LoadInt64(p.countPublishers)
|
||||
countReaders := atomic.LoadInt64(p.countReaders)
|
||||
func (p *program) Log(format string, args ...interface{}) {
|
||||
countClients := atomic.LoadInt64(p.stats.CountClients)
|
||||
countPublishers := atomic.LoadInt64(p.stats.CountPublishers)
|
||||
countReaders := atomic.LoadInt64(p.stats.CountReaders)
|
||||
|
||||
log.Printf(fmt.Sprintf("[%d/%d/%d] "+format, append([]interface{}{countClients,
|
||||
countPublishers, countReaders}, args...)...))
|
||||
|
@ -190,207 +135,49 @@ func (p *program) log(format string, args ...interface{}) {
|
|||
func (p *program) run() {
|
||||
defer close(p.done)
|
||||
|
||||
if p.metrics != nil {
|
||||
go p.metrics.run()
|
||||
}
|
||||
|
||||
if p.pprof != nil {
|
||||
go p.pprof.run()
|
||||
}
|
||||
|
||||
if p.serverUdpRtp != nil {
|
||||
go p.serverUdpRtp.run()
|
||||
}
|
||||
|
||||
if p.serverUdpRtcp != nil {
|
||||
go p.serverUdpRtcp.run()
|
||||
}
|
||||
|
||||
go p.serverTcp.run()
|
||||
|
||||
for _, p := range p.paths {
|
||||
p.onInit()
|
||||
}
|
||||
|
||||
checkPathsTicker := time.NewTicker(checkPathPeriod)
|
||||
defer checkPathsTicker.Stop()
|
||||
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case <-checkPathsTicker.C:
|
||||
for _, path := range p.paths {
|
||||
path.onCheck()
|
||||
}
|
||||
|
||||
case conn := <-p.clientNew:
|
||||
newClient(p, conn)
|
||||
|
||||
case client := <-p.clientClose:
|
||||
if _, ok := p.clients[client]; !ok {
|
||||
continue
|
||||
}
|
||||
client.close()
|
||||
|
||||
case req := <-p.clientDescribe:
|
||||
// create path if it doesn't exist
|
||||
if _, ok := p.paths[req.pathName]; !ok {
|
||||
p.paths[req.pathName] = newPath(p, req.pathName, req.pathConf)
|
||||
}
|
||||
|
||||
p.paths[req.pathName].onDescribe(req.client)
|
||||
|
||||
case req := <-p.clientAnnounce:
|
||||
// create path if it doesn't exist
|
||||
if path, ok := p.paths[req.pathName]; !ok {
|
||||
p.paths[req.pathName] = newPath(p, req.pathName, req.pathConf)
|
||||
|
||||
} else {
|
||||
if path.source != nil {
|
||||
req.res <- fmt.Errorf("someone is already publishing on path '%s'", req.pathName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
p.paths[req.pathName].source = req.client
|
||||
p.paths[req.pathName].sourceTrackCount = req.trackCount
|
||||
p.paths[req.pathName].sourceSdp = req.sdp
|
||||
|
||||
req.client.path = p.paths[req.pathName]
|
||||
req.client.state = clientStatePreRecord
|
||||
req.res <- nil
|
||||
|
||||
case req := <-p.clientSetupPlay:
|
||||
path, ok := p.paths[req.pathName]
|
||||
if !ok || !path.sourceReady {
|
||||
req.res <- fmt.Errorf("no one is publishing on path '%s'", req.pathName)
|
||||
continue
|
||||
}
|
||||
|
||||
if req.trackId >= path.sourceTrackCount {
|
||||
req.res <- fmt.Errorf("track %d does not exist", req.trackId)
|
||||
continue
|
||||
}
|
||||
|
||||
req.client.path = path
|
||||
req.client.state = clientStatePrePlay
|
||||
req.res <- nil
|
||||
|
||||
case client := <-p.clientPlay:
|
||||
atomic.AddInt64(p.countReaders, 1)
|
||||
client.state = clientStatePlay
|
||||
p.readersMap.add(client)
|
||||
|
||||
case client := <-p.clientRecord:
|
||||
atomic.AddInt64(p.countPublishers, 1)
|
||||
client.state = clientStateRecord
|
||||
|
||||
if client.streamProtocol == gortsplib.StreamProtocolUDP {
|
||||
for trackId, track := range client.streamTracks {
|
||||
addr := makeUDPPublisherAddr(client.ip(), track.rtpPort)
|
||||
p.udpPublishersMap.add(addr, &udpPublisher{
|
||||
client: client,
|
||||
trackId: trackId,
|
||||
streamType: gortsplib.StreamTypeRtp,
|
||||
})
|
||||
|
||||
addr = makeUDPPublisherAddr(client.ip(), track.rtcpPort)
|
||||
p.udpPublishersMap.add(addr, &udpPublisher{
|
||||
client: client,
|
||||
trackId: trackId,
|
||||
streamType: gortsplib.StreamTypeRtcp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
client.path.onSourceSetReady()
|
||||
|
||||
case s := <-p.sourceRtspReady:
|
||||
s.path.onSourceSetReady()
|
||||
|
||||
case s := <-p.sourceRtspNotReady:
|
||||
s.path.onSourceSetNotReady()
|
||||
|
||||
case s := <-p.sourceRtmpReady:
|
||||
s.path.onSourceSetReady()
|
||||
|
||||
case s := <-p.sourceRtmpNotReady:
|
||||
s.path.onSourceSetNotReady()
|
||||
|
||||
case <-p.terminate:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-p.clientNew:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.closeResources()
|
||||
}
|
||||
|
||||
case <-p.clientClose:
|
||||
case <-p.clientDescribe:
|
||||
|
||||
case req := <-p.clientAnnounce:
|
||||
req.res <- fmt.Errorf("terminated")
|
||||
|
||||
case req := <-p.clientSetupPlay:
|
||||
req.res <- fmt.Errorf("terminated")
|
||||
|
||||
case <-p.clientPlay:
|
||||
case <-p.clientRecord:
|
||||
case <-p.sourceRtspReady:
|
||||
case <-p.sourceRtspNotReady:
|
||||
case <-p.sourceRtmpReady:
|
||||
case <-p.sourceRtmpNotReady:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
p.udpPublishersMap.clear()
|
||||
p.readersMap.clear()
|
||||
|
||||
for _, p := range p.paths {
|
||||
p.onClose()
|
||||
func (p *program) closeResources() {
|
||||
if p.clientMan != nil {
|
||||
p.clientMan.Close()
|
||||
}
|
||||
|
||||
p.serverTcp.close()
|
||||
if p.pathMan != nil {
|
||||
p.pathMan.Close()
|
||||
}
|
||||
|
||||
if p.serverTcp != nil {
|
||||
p.serverTcp.Close()
|
||||
}
|
||||
|
||||
if p.serverUdpRtcp != nil {
|
||||
p.serverUdpRtcp.close()
|
||||
p.serverUdpRtcp.Close()
|
||||
}
|
||||
|
||||
if p.serverUdpRtp != nil {
|
||||
p.serverUdpRtp.close()
|
||||
p.serverUdpRtp.Close()
|
||||
}
|
||||
|
||||
for c := range p.clients {
|
||||
c.close()
|
||||
}
|
||||
|
||||
p.clientsWg.Wait()
|
||||
|
||||
if p.metrics != nil {
|
||||
p.metrics.close()
|
||||
p.metrics.Close()
|
||||
}
|
||||
|
||||
if p.pprof != nil {
|
||||
p.pprof.close()
|
||||
p.pprof.Close()
|
||||
}
|
||||
|
||||
p.logHandler.Close()
|
||||
|
||||
close(p.clientNew)
|
||||
close(p.clientClose)
|
||||
close(p.clientDescribe)
|
||||
close(p.clientAnnounce)
|
||||
close(p.clientSetupPlay)
|
||||
close(p.clientPlay)
|
||||
close(p.clientRecord)
|
||||
close(p.sourceRtspReady)
|
||||
close(p.sourceRtspNotReady)
|
||||
if p.logHandler != nil {
|
||||
p.logHandler.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *program) close() {
|
||||
|
|
13
main_test.go
13
main_test.go
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
@ -191,11 +190,7 @@ func TestEnvironment(t *testing.T) {
|
|||
pa, ok = p.conf.Paths["cam1"]
|
||||
require.Equal(t, true, ok)
|
||||
require.Equal(t, &conf.PathConf{
|
||||
Source: "rtsp://testing",
|
||||
SourceUrl: func() *url.URL {
|
||||
u, _ := url.Parse("rtsp://testing:554")
|
||||
return u
|
||||
}(),
|
||||
Source: "rtsp://testing",
|
||||
SourceProtocol: "tcp",
|
||||
SourceProtocolParsed: gortsplib.StreamProtocolTCP,
|
||||
SourceOnDemand: true,
|
||||
|
@ -213,11 +208,7 @@ func TestEnvironmentNoFile(t *testing.T) {
|
|||
pa, ok := p.conf.Paths["cam1"]
|
||||
require.Equal(t, true, ok)
|
||||
require.Equal(t, &conf.PathConf{
|
||||
Source: "rtsp://testing",
|
||||
SourceUrl: func() *url.URL {
|
||||
u, _ := url.Parse("rtsp://testing:554")
|
||||
return u
|
||||
}(),
|
||||
Source: "rtsp://testing",
|
||||
SourceProtocol: "udp",
|
||||
}, pa)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -8,27 +8,34 @@ import (
|
|||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/aler9/rtsp-simple-server/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsAddress = ":9998"
|
||||
address = ":9998"
|
||||
)
|
||||
|
||||
type metrics struct {
|
||||
p *program
|
||||
type Parent interface {
|
||||
Log(string, ...interface{})
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
stats *stats.Stats
|
||||
|
||||
listener net.Listener
|
||||
mux *http.ServeMux
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func newMetrics(p *program) (*metrics, error) {
|
||||
listener, err := net.Listen("tcp", metricsAddress)
|
||||
func New(stats *stats.Stats, parent Parent) (*Metrics, error) {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &metrics{
|
||||
p: p,
|
||||
m := &Metrics{
|
||||
stats: stats,
|
||||
listener: listener,
|
||||
}
|
||||
|
||||
|
@ -39,31 +46,33 @@ func newMetrics(p *program) (*metrics, error) {
|
|||
Handler: m.mux,
|
||||
}
|
||||
|
||||
m.p.log("[metrics] opened on " + metricsAddress)
|
||||
parent.Log("[metrics] opened on " + address)
|
||||
|
||||
go m.run()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *metrics) run() {
|
||||
func (m *Metrics) Close() {
|
||||
m.server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (m *Metrics) run() {
|
||||
err := m.server.Serve(m.listener)
|
||||
if err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *metrics) close() {
|
||||
m.server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (m *metrics) onMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
func (m *Metrics) onMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
now := time.Now().UnixNano() / 1000000
|
||||
|
||||
countClients := atomic.LoadInt64(m.p.countClients)
|
||||
countPublishers := atomic.LoadInt64(m.p.countPublishers)
|
||||
countReaders := atomic.LoadInt64(m.p.countReaders)
|
||||
countSourcesRtsp := atomic.LoadInt64(m.p.countSourcesRtsp)
|
||||
countSourcesRtspRunning := atomic.LoadInt64(m.p.countSourcesRtspRunning)
|
||||
countSourcesRtmp := atomic.LoadInt64(m.p.countSourcesRtmp)
|
||||
countSourcesRtmpRunning := atomic.LoadInt64(m.p.countSourcesRtmpRunning)
|
||||
countClients := atomic.LoadInt64(m.stats.CountClients)
|
||||
countPublishers := atomic.LoadInt64(m.stats.CountPublishers)
|
||||
countReaders := atomic.LoadInt64(m.stats.CountReaders)
|
||||
countSourcesRtsp := atomic.LoadInt64(m.stats.CountSourcesRtsp)
|
||||
countSourcesRtspRunning := atomic.LoadInt64(m.stats.CountSourcesRtspRunning)
|
||||
countSourcesRtmp := atomic.LoadInt64(m.stats.CountSourcesRtmp)
|
||||
countSourcesRtmpRunning := atomic.LoadInt64(m.stats.CountSourcesRtmpRunning)
|
||||
|
||||
out := ""
|
||||
out += fmt.Sprintf("rtsp_clients{state=\"idle\"} %d %v\n",
|
293
path.go
293
path.go
|
@ -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,4 +1,4 @@
|
|||
package main
|
||||
package pprof
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -8,21 +8,25 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
pprofAddress = ":9999"
|
||||
address = ":9999"
|
||||
)
|
||||
|
||||
type pprof struct {
|
||||
type Parent interface {
|
||||
Log(string, ...interface{})
|
||||
}
|
||||
|
||||
type Pprof struct {
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func newPprof(p *program) (*pprof, error) {
|
||||
listener, err := net.Listen("tcp", pprofAddress)
|
||||
func New(parent Parent) (*Pprof, error) {
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pp := &pprof{
|
||||
pp := &Pprof{
|
||||
listener: listener,
|
||||
}
|
||||
|
||||
|
@ -30,17 +34,19 @@ func newPprof(p *program) (*pprof, error) {
|
|||
Handler: http.DefaultServeMux,
|
||||
}
|
||||
|
||||
p.log("[pprof] opened on " + pprofAddress)
|
||||
parent.Log("[pprof] opened on " + address)
|
||||
|
||||
go pp.run()
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
func (pp *pprof) run() {
|
||||
func (pp *Pprof) Close() {
|
||||
pp.server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func (pp *Pprof) run() {
|
||||
err := pp.server.Serve(pp.listener)
|
||||
if err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *pprof) close() {
|
||||
pp.server.Shutdown(context.Background())
|
||||
}
|
52
servertcp.go
52
servertcp.go
|
@ -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
|
||||
}
|
113
serverudp.go
113
serverudp.go
|
@ -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,4 +1,4 @@
|
|||
package main
|
||||
package sourcertmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -15,53 +15,73 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sourceRtmpRetryInterval = 5 * time.Second
|
||||
retryInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
type sourceRtmpState int
|
||||
|
||||
const (
|
||||
sourceRtmpStateStopped sourceRtmpState = iota
|
||||
sourceRtmpStateRunning
|
||||
)
|
||||
|
||||
type sourceRtmp struct {
|
||||
p *program
|
||||
path *path
|
||||
state sourceRtmpState
|
||||
innerRunning bool
|
||||
|
||||
innerTerminate chan struct{}
|
||||
innerDone chan struct{}
|
||||
setState chan sourceRtmpState
|
||||
terminate chan struct{}
|
||||
done chan struct{}
|
||||
type Parent interface {
|
||||
Log(string, ...interface{})
|
||||
OnSourceReady(gortsplib.Tracks)
|
||||
OnSourceNotReady()
|
||||
OnFrame(int, gortsplib.StreamType, []byte)
|
||||
}
|
||||
|
||||
func newSourceRtmp(p *program, path *path) *sourceRtmp {
|
||||
s := &sourceRtmp{
|
||||
p: p,
|
||||
path: path,
|
||||
setState: make(chan sourceRtmpState),
|
||||
terminate: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
atomic.AddInt64(p.countSourcesRtmp, +1)
|
||||
|
||||
if path.conf.SourceOnDemand {
|
||||
s.state = sourceRtmpStateStopped
|
||||
} else {
|
||||
s.state = sourceRtmpStateRunning
|
||||
atomic.AddInt64(p.countSourcesRtmpRunning, +1)
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateStopped State = iota
|
||||
StateRunning
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
ur string
|
||||
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,
|
||||
state State,
|
||||
parent Parent) *Source {
|
||||
s := &Source{
|
||||
ur: ur,
|
||||
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 *sourceRtmp) isSource() {}
|
||||
func (s *Source) Close() {
|
||||
close(s.terminate)
|
||||
<-s.done
|
||||
}
|
||||
|
||||
func (s *sourceRtmp) run(initialState sourceRtmpState) {
|
||||
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)
|
||||
|
@ -69,7 +89,7 @@ func (s *sourceRtmp) run(initialState sourceRtmpState) {
|
|||
outer:
|
||||
for {
|
||||
select {
|
||||
case state := <-s.setState:
|
||||
case state := <-s.stateChange:
|
||||
s.applyState(state)
|
||||
|
||||
case <-s.terminate:
|
||||
|
@ -82,13 +102,13 @@ outer:
|
|||
<-s.innerDone
|
||||
}
|
||||
|
||||
close(s.setState)
|
||||
close(s.stateChange)
|
||||
}
|
||||
|
||||
func (s *sourceRtmp) applyState(state sourceRtmpState) {
|
||||
if state == sourceRtmpStateRunning {
|
||||
func (s *Source) applyState(state State) {
|
||||
if state == StateRunning {
|
||||
if !s.innerRunning {
|
||||
s.path.log("rtmp source started")
|
||||
s.parent.Log("rtmp source started")
|
||||
s.innerRunning = true
|
||||
s.innerTerminate = make(chan struct{})
|
||||
s.innerDone = make(chan struct{})
|
||||
|
@ -99,12 +119,12 @@ func (s *sourceRtmp) applyState(state sourceRtmpState) {
|
|||
close(s.innerTerminate)
|
||||
<-s.innerDone
|
||||
s.innerRunning = false
|
||||
s.path.log("rtmp source stopped")
|
||||
s.parent.Log("rtmp source stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sourceRtmp) runInner() {
|
||||
func (s *Source) runInner() {
|
||||
defer close(s.innerDone)
|
||||
|
||||
outer:
|
||||
|
@ -114,7 +134,7 @@ outer:
|
|||
break outer
|
||||
}
|
||||
|
||||
t := time.NewTimer(sourceRtmpRetryInterval)
|
||||
t := time.NewTimer(retryInterval)
|
||||
defer t.Stop()
|
||||
|
||||
select {
|
||||
|
@ -125,8 +145,8 @@ outer:
|
|||
}
|
||||
}
|
||||
|
||||
func (s *sourceRtmp) runInnerInner() bool {
|
||||
s.path.log("connecting to rtmp source")
|
||||
func (s *Source) runInnerInner() bool {
|
||||
s.parent.Log("connecting to rtmp source")
|
||||
|
||||
var conn *rtmp.Conn
|
||||
var nconn net.Conn
|
||||
|
@ -134,7 +154,7 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
dialDone := make(chan struct{}, 1)
|
||||
go func() {
|
||||
defer close(dialDone)
|
||||
conn, nconn, err = rtmp.NewClient().Dial(s.path.conf.Source, rtmp.PrepareReading)
|
||||
conn, nconn, err = rtmp.NewClient().Dial(s.ur, rtmp.PrepareReading)
|
||||
}()
|
||||
|
||||
select {
|
||||
|
@ -144,7 +164,7 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -202,7 +222,7 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -215,13 +235,13 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
if h264Sps != nil {
|
||||
videoTrack, err = gortsplib.NewTrackH264(len(tracks), h264Sps, h264Pps)
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
h264Encoder, err = rtph264.NewEncoder(uint8(len(tracks)))
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -231,13 +251,13 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
if aacConfig != nil {
|
||||
audioTrack, err = gortsplib.NewTrackAac(len(tracks), aacConfig)
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
aacEncoder, err = rtpaac.NewEncoder(uint8(len(tracks)), aacConfig)
|
||||
if err != nil {
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -245,15 +265,12 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
}
|
||||
|
||||
if len(tracks) == 0 {
|
||||
s.path.log("rtmp source ERR: no tracks found")
|
||||
s.parent.Log("rtmp source ERR: no tracks found")
|
||||
return true
|
||||
}
|
||||
|
||||
s.path.sourceSdp = tracks.Write()
|
||||
s.path.sourceTrackCount = len(tracks)
|
||||
|
||||
s.p.sourceRtmpReady <- s
|
||||
s.path.log("rtmp source ready")
|
||||
s.parent.OnSourceReady(tracks)
|
||||
s.parent.Log("rtmp source ready")
|
||||
|
||||
readDone := make(chan error)
|
||||
go func() {
|
||||
|
@ -286,7 +303,7 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
}
|
||||
|
||||
for _, f := range frames {
|
||||
s.p.readersMap.forwardFrame(s.path, videoTrack.Id, gortsplib.StreamTypeRtp, f)
|
||||
s.parent.OnFrame(videoTrack.Id, gortsplib.StreamTypeRtp, f)
|
||||
}
|
||||
|
||||
case av.AAC:
|
||||
|
@ -302,7 +319,7 @@ func (s *sourceRtmp) runInnerInner() bool {
|
|||
}
|
||||
|
||||
for _, f := range frames {
|
||||
s.p.readersMap.forwardFrame(s.path, audioTrack.Id, gortsplib.StreamTypeRtp, f)
|
||||
s.parent.OnFrame(audioTrack.Id, gortsplib.StreamTypeRtp, f)
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -325,14 +342,14 @@ outer:
|
|||
|
||||
case err := <-readDone:
|
||||
nconn.Close()
|
||||
s.path.log("rtmp source ERR: %s", err)
|
||||
s.parent.Log("rtmp source ERR: %s", err)
|
||||
ret = true
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
s.p.sourceRtmpNotReady <- s
|
||||
s.path.log("rtmp source not ready")
|
||||
s.parent.OnSourceNotReady()
|
||||
s.parent.Log("rtmp source not ready")
|
||||
|
||||
return ret
|
||||
}
|
322
sourcertsp.go
322
sourcertsp.go
|
@ -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(),
|
||||
}
|
||||
}
|
205
utils.go
205
utils.go
|
@ -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