Browse Source

add option runOnDemand (#36)

pull/52/head
aler9 6 years ago
parent
commit
e409f673d4
  1. 7
      Makefile
  2. 119
      client.go
  3. 115
      conf.go
  4. 116
      main.go
  5. 170
      path.go
  6. 5
      rtsp-simple-server.yml
  7. 49
      source.go
  8. 1
      utils.go

7
Makefile

@ -75,9 +75,10 @@ paths:
# readPass: tast # readPass: tast
proxied: proxied:
source: rtsp://192.168.2.198:8554/stream # source: rtsp://192.168.2.198:8554/stream
sourceProtocol: tcp # sourceProtocol: tcp
sourceOnDemand: yes # sourceOnDemand: yes
runOnDemand: ffmpeg -i rtsp://192.168.2.198:8554/stream -c copy -f rtsp rtsp://localhost:8554/proxied2
# original: # original:
# runOnPublish: ffmpeg -i rtsp://localhost:8554/original -b:a 64k -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:8554/compressed # runOnPublish: ffmpeg -i rtsp://localhost:8554/original -b:a 64k -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:8554/compressed

119
client.go

@ -23,6 +23,11 @@ const (
clientUdpWriteBufferSize = 128 * 1024 clientUdpWriteBufferSize = 128 * 1024
) )
type describeRes struct {
sdp []byte
err error
}
type clientTrack struct { type clientTrack struct {
rtpPort int rtpPort int
rtcpPort int rtcpPort int
@ -77,7 +82,7 @@ type client struct {
p *program p *program
conn *gortsplib.ConnServer conn *gortsplib.ConnServer
state clientState state clientState
path string pathId string
authUser string authUser string
authPass string authPass string
authHelper *gortsplib.AuthServer authHelper *gortsplib.AuthServer
@ -88,12 +93,12 @@ type client struct {
readBuf *doubleBuffer readBuf *doubleBuffer
writeBuf *doubleBuffer writeBuf *doubleBuffer
describeRes chan []byte describeRes chan describeRes
events chan clientEvent // only if state = Play and gortsplib.StreamProtocol = TCP events chan clientEvent // only if state = Play and gortsplib.StreamProtocol = TCP
done chan struct{} done chan struct{}
} }
func newServerClient(p *program, nconn net.Conn) *client { func newClient(p *program, nconn net.Conn) *client {
c := &client{ c := &client{
p: p, p: p,
conn: gortsplib.NewConnServer(gortsplib.ConnServerConf{ conn: gortsplib.NewConnServer(gortsplib.ConnServerConf{
@ -125,12 +130,12 @@ func (c *client) zone() string {
} }
func (c *client) run() { func (c *client) run() {
var runOnConnectCmd *exec.Cmd var onConnectCmd *exec.Cmd
if c.p.conf.RunOnConnect != "" { if c.p.conf.RunOnConnect != "" {
runOnConnectCmd = exec.Command("/bin/sh", "-c", c.p.conf.RunOnConnect) onConnectCmd = exec.Command("/bin/sh", "-c", c.p.conf.RunOnConnect)
runOnConnectCmd.Stdout = os.Stdout onConnectCmd.Stdout = os.Stdout
runOnConnectCmd.Stderr = os.Stderr onConnectCmd.Stderr = os.Stderr
err := runOnConnectCmd.Start() err := onConnectCmd.Start()
if err != nil { if err != nil {
c.log("ERR: %s", err) c.log("ERR: %s", err)
} }
@ -158,9 +163,9 @@ outer:
c.conn.NetConn().Close() // close socket in case it has not been closed yet c.conn.NetConn().Close() // close socket in case it has not been closed yet
if runOnConnectCmd != nil { if onConnectCmd != nil {
runOnConnectCmd.Process.Signal(os.Interrupt) onConnectCmd.Process.Signal(os.Interrupt)
runOnConnectCmd.Wait() onConnectCmd.Wait()
} }
close(c.done) // close() never blocks close(c.done) // close() never blocks
@ -311,14 +316,14 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
pconf := c.p.findConfForPath(path) confp := c.p.findConfForPath(path)
if pconf == nil { if confp == nil {
c.writeResError(req, gortsplib.StatusBadRequest, c.writeResError(req, gortsplib.StatusBadRequest,
fmt.Errorf("unable to find a valid configuration for path '%s'", path)) fmt.Errorf("unable to find a valid configuration for path '%s'", path))
return false return false
} }
err := c.authenticate(pconf.readIpsParsed, pconf.ReadUser, pconf.ReadPass, req) err := c.authenticate(confp.readIpsParsed, confp.ReadUser, confp.ReadPass, req)
if err != nil { if err != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false
@ -326,11 +331,11 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return true return true
} }
c.describeRes = make(chan []byte) c.describeRes = make(chan describeRes)
c.p.events <- programEventClientDescribe{c, path} c.p.events <- programEventClientDescribe{c, path}
sdp := <-c.describeRes describeRes := <-c.describeRes
if sdp == nil { if describeRes.err != nil {
c.writeResError(req, gortsplib.StatusNotFound, fmt.Errorf("no one is publishing on path '%s'", path)) c.writeResError(req, gortsplib.StatusNotFound, describeRes.err)
return false return false
} }
@ -341,7 +346,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
"Content-Base": gortsplib.HeaderValue{req.Url.String() + "/"}, "Content-Base": gortsplib.HeaderValue{req.Url.String() + "/"},
"Content-Type": gortsplib.HeaderValue{"application/sdp"}, "Content-Type": gortsplib.HeaderValue{"application/sdp"},
}, },
Content: sdp, Content: describeRes.sdp,
}) })
return true return true
@ -357,14 +362,14 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
pconf := c.p.findConfForPath(path) confp := c.p.findConfForPath(path)
if pconf == nil { if confp == nil {
c.writeResError(req, gortsplib.StatusBadRequest, c.writeResError(req, gortsplib.StatusBadRequest,
fmt.Errorf("unable to find a valid configuration for path '%s'", path)) fmt.Errorf("unable to find a valid configuration for path '%s'", path))
return false return false
} }
err := c.authenticate(pconf.publishIpsParsed, pconf.PublishUser, pconf.PublishPass, req) err := c.authenticate(confp.publishIpsParsed, confp.PublishUser, confp.PublishPass, req)
if err != nil { if err != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false
@ -435,14 +440,14 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
switch c.state { switch c.state {
// play // play
case clientStateInitial, clientStatePrePlay: case clientStateInitial, clientStatePrePlay:
pconf := c.p.findConfForPath(path) confp := c.p.findConfForPath(path)
if pconf == nil { if confp == nil {
c.writeResError(req, gortsplib.StatusBadRequest, c.writeResError(req, gortsplib.StatusBadRequest,
fmt.Errorf("unable to find a valid configuration for path '%s'", path)) fmt.Errorf("unable to find a valid configuration for path '%s'", path))
return false return false
} }
err := c.authenticate(pconf.readIpsParsed, pconf.ReadUser, pconf.ReadPass, req) err := c.authenticate(confp.readIpsParsed, confp.ReadUser, confp.ReadPass, req)
if err != nil { if err != nil {
if err == errAuthCritical { if err == errAuthCritical {
return false return false
@ -473,7 +478,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if c.path != "" && path != c.path { if c.pathId != "" && path != c.pathId {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
return false return false
} }
@ -513,7 +518,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if c.path != "" && path != c.path { if c.pathId != "" && path != c.pathId {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
return false return false
} }
@ -559,8 +564,8 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
// after ANNOUNCE, c.path is already set // after ANNOUNCE, c.pathId is already set
if path != c.path { if path != c.pathId {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
return false return false
} }
@ -593,7 +598,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if len(c.streamTracks) >= len(c.p.paths[c.path].publisherSdpParsed.MediaDescriptions) { if len(c.streamTracks) >= len(c.p.paths[c.pathId].publisherSdpParsed.MediaDescriptions) {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("all the tracks have already been setup")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("all the tracks have already been setup"))
return false return false
} }
@ -645,7 +650,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if len(c.streamTracks) >= len(c.p.paths[c.path].publisherSdpParsed.MediaDescriptions) { if len(c.streamTracks) >= len(c.p.paths[c.pathId].publisherSdpParsed.MediaDescriptions) {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("all the tracks have already been setup")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("all the tracks have already been setup"))
return false return false
} }
@ -689,7 +694,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if path != c.path { if path != c.pathId {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
return false return false
} }
@ -724,12 +729,12 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
return false return false
} }
if path != c.path { if path != c.pathId {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("path has changed"))
return false return false
} }
if len(c.streamTracks) != len(c.p.paths[c.path].publisherSdpParsed.MediaDescriptions) { if len(c.streamTracks) != len(c.p.paths[c.pathId].publisherSdpParsed.MediaDescriptions) {
c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("not all tracks have been setup")) c.writeResError(req, gortsplib.StatusBadRequest, fmt.Errorf("not all tracks have been setup"))
return false return false
} }
@ -756,7 +761,7 @@ func (c *client) handleRequest(req *gortsplib.Request) bool {
} }
func (c *client) runPlay(path string) { func (c *client) runPlay(path string) {
pconf := c.p.findConfForPath(path) confp := c.p.findConfForPath(path)
if c.streamProtocol == gortsplib.StreamProtocolTcp { if c.streamProtocol == gortsplib.StreamProtocolTcp {
c.writeBuf = newDoubleBuffer(clientTcpWriteBufferSize) c.writeBuf = newDoubleBuffer(clientTcpWriteBufferSize)
@ -767,19 +772,19 @@ func (c *client) runPlay(path string) {
c.p.events <- programEventClientPlay2{done, c} c.p.events <- programEventClientPlay2{done, c}
<-done <-done
c.log("is receiving on path '%s', %d %s via %s", c.path, len(c.streamTracks), func() string { c.log("is receiving on path '%s', %d %s via %s", c.pathId, len(c.streamTracks), func() string {
if len(c.streamTracks) == 1 { if len(c.streamTracks) == 1 {
return "track" return "track"
} }
return "tracks" return "tracks"
}(), c.streamProtocol) }(), c.streamProtocol)
var runOnReadCmd *exec.Cmd var onReadCmd *exec.Cmd
if pconf.RunOnRead != "" { if confp.RunOnRead != "" {
runOnReadCmd = exec.Command("/bin/sh", "-c", pconf.RunOnRead) onReadCmd = exec.Command("/bin/sh", "-c", confp.RunOnRead)
runOnReadCmd.Stdout = os.Stdout onReadCmd.Stdout = os.Stdout
runOnReadCmd.Stderr = os.Stderr onReadCmd.Stderr = os.Stderr
err := runOnReadCmd.Start() err := onReadCmd.Start()
if err != nil { if err != nil {
c.log("ERR: %s", err) c.log("ERR: %s", err)
} }
@ -848,14 +853,14 @@ func (c *client) runPlay(path string) {
close(c.events) close(c.events)
} }
if runOnReadCmd != nil { if onReadCmd != nil {
runOnReadCmd.Process.Signal(os.Interrupt) onReadCmd.Process.Signal(os.Interrupt)
runOnReadCmd.Wait() onReadCmd.Wait()
} }
} }
func (c *client) runRecord(path string) { func (c *client) runRecord(path string) {
pconf := c.p.findConfForPath(path) confp := c.p.findConfForPath(path)
c.rtcpReceivers = make([]*gortsplib.RtcpReceiver, len(c.streamTracks)) c.rtcpReceivers = make([]*gortsplib.RtcpReceiver, len(c.streamTracks))
for trackId := range c.streamTracks { for trackId := range c.streamTracks {
@ -866,19 +871,19 @@ func (c *client) runRecord(path string) {
c.p.events <- programEventClientRecord{done, c} c.p.events <- programEventClientRecord{done, c}
<-done <-done
c.log("is publishing on path '%s', %d %s via %s", c.path, len(c.streamTracks), func() string { c.log("is publishing on path '%s', %d %s via %s", c.pathId, len(c.streamTracks), func() string {
if len(c.streamTracks) == 1 { if len(c.streamTracks) == 1 {
return "track" return "track"
} }
return "tracks" return "tracks"
}(), c.streamProtocol) }(), c.streamProtocol)
var runOnPublishCmd *exec.Cmd var onPublishCmd *exec.Cmd
if pconf.RunOnPublish != "" { if confp.RunOnPublish != "" {
runOnPublishCmd = exec.Command("/bin/sh", "-c", pconf.RunOnPublish) onPublishCmd = exec.Command("/bin/sh", "-c", confp.RunOnPublish)
runOnPublishCmd.Stdout = os.Stdout onPublishCmd.Stdout = os.Stdout
runOnPublishCmd.Stderr = os.Stderr onPublishCmd.Stderr = os.Stderr
err := runOnPublishCmd.Start() err := onPublishCmd.Start()
if err != nil { if err != nil {
c.log("ERR: %s", err) c.log("ERR: %s", err)
} }
@ -967,7 +972,7 @@ func (c *client) runRecord(path string) {
c.rtcpReceivers[frame.TrackId].OnFrame(frame.StreamType, frame.Content) c.rtcpReceivers[frame.TrackId].OnFrame(frame.StreamType, frame.Content)
c.p.events <- programEventClientFrameTcp{ c.p.events <- programEventClientFrameTcp{
c.path, c.pathId,
frame.TrackId, frame.TrackId,
frame.StreamType, frame.StreamType,
frame.Content, frame.Content,
@ -1017,8 +1022,8 @@ func (c *client) runRecord(path string) {
c.rtcpReceivers[trackId].Close() c.rtcpReceivers[trackId].Close()
} }
if runOnPublishCmd != nil { if onPublishCmd != nil {
runOnPublishCmd.Process.Signal(os.Interrupt) onPublishCmd.Process.Signal(os.Interrupt)
runOnPublishCmd.Wait() onPublishCmd.Wait()
} }
} }

115
conf.go

@ -26,6 +26,7 @@ type confPath struct {
ReadPass string `yaml:"readPass"` ReadPass string `yaml:"readPass"`
ReadIps []string `yaml:"readIps"` ReadIps []string `yaml:"readIps"`
readIpsParsed []interface{} readIpsParsed []interface{}
RunOnDemand string `yaml:"runOnDemand"`
RunOnPublish string `yaml:"runOnPublish"` RunOnPublish string `yaml:"runOnPublish"`
RunOnRead string `yaml:"runOnRead"` RunOnRead string `yaml:"runOnRead"`
} }
@ -148,90 +149,94 @@ func loadConf(fpath string, stdin io.Reader) (*conf, error) {
} }
} }
for path, pconf := range conf.Paths { for path, confp := range conf.Paths {
if pconf == nil { if confp == nil {
conf.Paths[path] = &confPath{} conf.Paths[path] = &confPath{}
pconf = conf.Paths[path] confp = conf.Paths[path]
} }
if pconf.Source == "" { if confp.Source == "" {
pconf.Source = "record" confp.Source = "record"
} }
if pconf.PublishUser != "" { if confp.Source != "record" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishUser) { if path == "all" {
return nil, fmt.Errorf("publish username must be alphanumeric") return nil, fmt.Errorf("path 'all' cannot have a RTSP source")
} }
if confp.SourceProtocol == "" {
confp.SourceProtocol = "udp"
} }
if pconf.PublishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.PublishPass) { confp.sourceUrl, err = url.Parse(confp.Source)
return nil, fmt.Errorf("publish password must be alphanumeric") if err != nil {
return nil, fmt.Errorf("'%s' is not a valid RTSP url", confp.Source)
} }
if confp.sourceUrl.Scheme != "rtsp" {
return nil, fmt.Errorf("'%s' is not a valid RTSP url", confp.Source)
}
if confp.sourceUrl.Port() == "" {
confp.sourceUrl.Host += ":554"
}
if confp.sourceUrl.User != nil {
pass, _ := confp.sourceUrl.User.Password()
user := confp.sourceUrl.User.Username()
if user != "" && pass == "" ||
user == "" && pass != "" {
fmt.Errorf("username and password must be both provided")
} }
pconf.publishIpsParsed, err = parseIpCidrList(pconf.PublishIps)
if err != nil {
return nil, err
} }
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" { switch confp.SourceProtocol {
return nil, fmt.Errorf("read username and password must be both filled") case "udp":
confp.sourceProtocolParsed = gortsplib.StreamProtocolUdp
case "tcp":
confp.sourceProtocolParsed = gortsplib.StreamProtocolTcp
default:
return nil, fmt.Errorf("unsupported protocol '%s'", confp.SourceProtocol)
} }
if pconf.ReadUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadUser) {
return nil, fmt.Errorf("read username must be alphanumeric")
} }
if confp.PublishUser != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(confp.PublishUser) {
return nil, fmt.Errorf("publish username must be alphanumeric")
} }
if pconf.ReadPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(pconf.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric")
} }
if confp.PublishPass != "" {
if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(confp.PublishPass) {
return nil, fmt.Errorf("publish password must be alphanumeric")
} }
if pconf.ReadUser != "" && pconf.ReadPass == "" || pconf.ReadUser == "" && pconf.ReadPass != "" {
return nil, fmt.Errorf("read username and password must be both filled")
} }
pconf.readIpsParsed, err = parseIpCidrList(pconf.ReadIps) confp.publishIpsParsed, err = parseIpCidrList(confp.PublishIps)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if pconf.Source != "record" { if confp.ReadUser != "" && confp.ReadPass == "" || confp.ReadUser == "" && confp.ReadPass != "" {
if path == "all" { return nil, fmt.Errorf("read username and password must be both filled")
return nil, fmt.Errorf("path 'all' cannot have a RTSP source")
} }
if confp.ReadUser != "" {
if pconf.SourceProtocol == "" { if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(confp.ReadUser) {
pconf.SourceProtocol = "udp" return nil, fmt.Errorf("read username must be alphanumeric")
} }
pconf.sourceUrl, err = url.Parse(pconf.Source)
if err != nil {
return nil, fmt.Errorf("'%s' is not a valid RTSP url", pconf.Source)
} }
if pconf.sourceUrl.Scheme != "rtsp" { if confp.ReadPass != "" {
return nil, fmt.Errorf("'%s' is not a valid RTSP url", pconf.Source) if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(confp.ReadPass) {
return nil, fmt.Errorf("read password must be alphanumeric")
} }
if pconf.sourceUrl.Port() == "" {
pconf.sourceUrl.Host += ":554"
} }
if pconf.sourceUrl.User != nil { if confp.ReadUser != "" && confp.ReadPass == "" || confp.ReadUser == "" && confp.ReadPass != "" {
pass, _ := pconf.sourceUrl.User.Password() return nil, fmt.Errorf("read username and password must be both filled")
user := pconf.sourceUrl.User.Username()
if user != "" && pass == "" ||
user == "" && pass != "" {
fmt.Errorf("username and password must be both provided")
} }
confp.readIpsParsed, err = parseIpCidrList(confp.ReadIps)
if err != nil {
return nil, err
} }
switch pconf.SourceProtocol { if confp.RunOnDemand != "" && path == "all" {
case "udp": return nil, fmt.Errorf("option 'runOnDemand' cannot be used in path 'all'")
pconf.sourceProtocolParsed = gortsplib.StreamProtocolUdp
case "tcp":
pconf.sourceProtocolParsed = gortsplib.StreamProtocolTcp
default:
return nil, fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol)
}
} }
} }

116
main.go

@ -145,12 +145,6 @@ type programEventSourceFrame struct {
func (programEventSourceFrame) isProgramEvent() {} func (programEventSourceFrame) isProgramEvent() {}
type programEventSourceReset struct {
source *source
}
func (programEventSourceReset) isProgramEvent() {}
type programEventTerminate struct{} type programEventTerminate struct{}
func (programEventTerminate) isProgramEvent() {} func (programEventTerminate) isProgramEvent() {}
@ -197,11 +191,17 @@ func newProgram(args []string, stdin io.Reader) (*program, error) {
done: make(chan struct{}), done: make(chan struct{}),
} }
for path, pconf := range conf.Paths { for path, confp := range conf.Paths {
if pconf.Source != "record" { if path == "all" {
s := newSource(p, path, pconf) continue
}
newPath(p, path, confp, true)
if confp.Source != "record" {
s := newSource(p, path, confp)
p.sources = append(p.sources, s) p.sources = append(p.sources, s)
p.paths[path] = newPath(p, path, s) p.paths[path].publisher = s
} }
} }
@ -265,21 +265,21 @@ outer:
case rawEvt := <-p.events: case rawEvt := <-p.events:
switch evt := rawEvt.(type) { switch evt := rawEvt.(type) {
case programEventClientNew: case programEventClientNew:
c := newServerClient(p, evt.nconn) c := newClient(p, evt.nconn)
p.clients[c] = struct{}{} p.clients[c] = struct{}{}
c.log("connected") c.log("connected")
case programEventClientClose: case programEventClientClose:
delete(p.clients, evt.client) delete(p.clients, evt.client)
if evt.client.path != "" { if evt.client.pathId != "" {
if path, ok := p.paths[evt.client.path]; ok { if path, ok := p.paths[evt.client.pathId]; ok {
// if this is a publisher
if path.publisher == evt.client { if path.publisher == evt.client {
path.publisherReset() path.publisherRemove()
// delete the path if !path.permanent {
delete(p.paths, evt.client.path) delete(p.paths, evt.client.pathId)
}
} }
} }
} }
@ -289,35 +289,30 @@ outer:
case programEventClientDescribe: case programEventClientDescribe:
path, ok := p.paths[evt.path] path, ok := p.paths[evt.path]
// no path: return 404
if !ok { if !ok {
evt.client.describeRes <- nil evt.client.describeRes <- describeRes{nil, fmt.Errorf("no one is publishing on path '%s'", evt.path)}
continue
}
sdpText, wait := path.describe()
if wait {
evt.client.path = evt.path
evt.client.state = clientStateWaitingDescription
continue continue
} }
evt.client.describeRes <- sdpText path.describe(evt.client)
case programEventClientAnnounce: case programEventClientAnnounce:
_, ok := p.paths[evt.path] if path, ok := p.paths[evt.path]; ok {
if ok { if path.publisher != nil {
evt.res <- fmt.Errorf("someone is already publishing on path '%s'", evt.path) evt.res <- fmt.Errorf("someone is already publishing on path '%s'", evt.path)
continue continue
} }
evt.client.path = evt.path } else {
evt.client.state = clientStateAnnounce newPath(p, evt.path, p.findConfForPath(evt.path), false)
p.paths[evt.path] = newPath(p, evt.path, evt.client) }
p.paths[evt.path].publisher = evt.client
p.paths[evt.path].publisherSdpText = evt.sdpText p.paths[evt.path].publisherSdpText = evt.sdpText
p.paths[evt.path].publisherSdpParsed = evt.sdpParsed p.paths[evt.path].publisherSdpParsed = evt.sdpParsed
evt.client.pathId = evt.path
evt.client.state = clientStateAnnounce
evt.res <- nil evt.res <- nil
case programEventClientSetupPlay: case programEventClientSetupPlay:
@ -332,7 +327,7 @@ outer:
continue continue
} }
evt.client.path = evt.path evt.client.pathId = evt.path
evt.client.streamProtocol = evt.protocol evt.client.streamProtocol = evt.protocol
evt.client.streamTracks = append(evt.client.streamTracks, &clientTrack{ evt.client.streamTracks = append(evt.client.streamTracks, &clientTrack{
rtpPort: evt.rtpPort, rtpPort: evt.rtpPort,
@ -351,9 +346,9 @@ outer:
evt.res <- nil evt.res <- nil
case programEventClientPlay1: case programEventClientPlay1:
path, ok := p.paths[evt.client.path] path, ok := p.paths[evt.client.pathId]
if !ok || !path.publisherReady { if !ok || !path.publisherReady {
evt.res <- fmt.Errorf("no one is publishing on path '%s'", evt.client.path) evt.res <- fmt.Errorf("no one is publishing on path '%s'", evt.client.pathId)
continue continue
} }
@ -377,13 +372,13 @@ outer:
case programEventClientRecord: case programEventClientRecord:
p.publisherCount += 1 p.publisherCount += 1
evt.client.state = clientStateRecord evt.client.state = clientStateRecord
p.paths[evt.client.path].publisherSetReady() p.paths[evt.client.pathId].publisherSetReady()
close(evt.done) close(evt.done)
case programEventClientRecordStop: case programEventClientRecordStop:
p.publisherCount -= 1 p.publisherCount -= 1
evt.client.state = clientStatePreRecord evt.client.state = clientStatePreRecord
p.paths[evt.client.path].publisherSetNotReady() p.paths[evt.client.pathId].publisherSetNotReady()
close(evt.done) close(evt.done)
case programEventClientFrameUdp: case programEventClientFrameUdp:
@ -393,24 +388,21 @@ outer:
} }
client.rtcpReceivers[trackId].OnFrame(evt.streamType, evt.buf) client.rtcpReceivers[trackId].OnFrame(evt.streamType, evt.buf)
p.forwardFrame(client.path, trackId, evt.streamType, evt.buf) p.forwardFrame(client.pathId, trackId, evt.streamType, evt.buf)
case programEventClientFrameTcp: case programEventClientFrameTcp:
p.forwardFrame(evt.path, evt.trackId, evt.streamType, evt.buf) p.forwardFrame(evt.path, evt.trackId, evt.streamType, evt.buf)
case programEventSourceReady: case programEventSourceReady:
evt.source.log("ready") evt.source.log("ready")
p.paths[evt.source.path].publisherSetReady() p.paths[evt.source.pathId].publisherSetReady()
case programEventSourceNotReady: case programEventSourceNotReady:
evt.source.log("not ready") evt.source.log("not ready")
p.paths[evt.source.path].publisherSetNotReady() p.paths[evt.source.pathId].publisherSetNotReady()
case programEventSourceFrame: case programEventSourceFrame:
p.forwardFrame(evt.source.path, evt.trackId, evt.streamType, evt.buf) p.forwardFrame(evt.source.pathId, evt.trackId, evt.streamType, evt.buf)
case programEventSourceReset:
p.paths[evt.source.path].publisherReset()
case programEventTerminate: case programEventTerminate:
break outer break outer
@ -425,7 +417,7 @@ outer:
close(evt.done) close(evt.done)
case programEventClientDescribe: case programEventClientDescribe:
evt.client.describeRes <- nil evt.client.describeRes <- describeRes{nil, fmt.Errorf("terminated")}
case programEventClientAnnounce: case programEventClientAnnounce:
evt.res <- fmt.Errorf("terminated") evt.res <- fmt.Errorf("terminated")
@ -478,12 +470,12 @@ func (p *program) close() {
} }
func (p *program) findConfForPath(path string) *confPath { func (p *program) findConfForPath(path string) *confPath {
if pconf, ok := p.conf.Paths[path]; ok { if confp, ok := p.conf.Paths[path]; ok {
return pconf return confp
} }
if pconf, ok := p.conf.Paths["all"]; ok { if confp, ok := p.conf.Paths["all"]; ok {
return pconf return confp
} }
return nil return nil
@ -518,35 +510,35 @@ func (p *program) findClientPublisher(addr *net.UDPAddr, streamType gortsplib.St
} }
func (p *program) forwardFrame(path string, trackId int, streamType gortsplib.StreamType, frame []byte) { func (p *program) forwardFrame(path string, trackId int, streamType gortsplib.StreamType, frame []byte) {
for client := range p.clients { for c := range p.clients {
if client.path == path && client.state == clientStatePlay { if c.pathId == path && c.state == clientStatePlay {
if client.streamProtocol == gortsplib.StreamProtocolUdp { if c.streamProtocol == gortsplib.StreamProtocolUdp {
if streamType == gortsplib.StreamTypeRtp { if streamType == gortsplib.StreamTypeRtp {
p.rtpl.write(&udpAddrBufPair{ p.rtpl.write(&udpAddrBufPair{
addr: &net.UDPAddr{ addr: &net.UDPAddr{
IP: client.ip(), IP: c.ip(),
Zone: client.zone(), Zone: c.zone(),
Port: client.streamTracks[trackId].rtpPort, Port: c.streamTracks[trackId].rtpPort,
}, },
buf: frame, buf: frame,
}) })
} else { } else {
p.rtcpl.write(&udpAddrBufPair{ p.rtcpl.write(&udpAddrBufPair{
addr: &net.UDPAddr{ addr: &net.UDPAddr{
IP: client.ip(), IP: c.ip(),
Zone: client.zone(), Zone: c.zone(),
Port: client.streamTracks[trackId].rtcpPort, Port: c.streamTracks[trackId].rtcpPort,
}, },
buf: frame, buf: frame,
}) })
} }
} else { } else {
buf := client.writeBuf.swap() buf := c.writeBuf.swap()
buf = buf[:len(frame)] buf = buf[:len(frame)]
copy(buf, frame) copy(buf, frame)
client.events <- clientEventFrameTcp{ c.events <- clientEventFrameTcp{
frame: &gortsplib.InterleavedFrame{ frame: &gortsplib.InterleavedFrame{
TrackId: trackId, TrackId: trackId,
StreamType: streamType, StreamType: streamType,

170
path.go

@ -1,6 +1,9 @@
package main package main
import ( import (
"fmt"
"os"
"os/exec"
"time" "time"
"github.com/aler9/sdp/v3" "github.com/aler9/sdp/v3"
@ -14,97 +17,182 @@ type publisher interface {
type path struct { type path struct {
p *program p *program
id string id string
confp *confPath
permanent bool
publisher publisher publisher publisher
publisherReady bool publisherReady bool
publisherSdpText []byte publisherSdpText []byte
publisherSdpParsed *sdp.SessionDescription publisherSdpParsed *sdp.SessionDescription
lastRequested time.Time lastRequested time.Time
lastActivation time.Time
onDemandCmd *exec.Cmd
} }
func newPath(p *program, id string, publisher publisher) *path { func newPath(p *program, id string, confp *confPath, permanent bool) {
return &path{ pa := &path{
p: p, p: p,
id: id, id: id,
publisher: publisher, confp: confp,
permanent: permanent,
} }
p.paths[id] = pa
} }
func (p *path) check() { func (pa *path) check() {
hasClientsWaitingDescribe := func() bool {
for c := range pa.p.clients {
if c.state == clientStateWaitingDescription && c.pathId == pa.id {
return true
}
}
return false
}()
// reply to DESCRIBE requests if they are in timeout
if hasClientsWaitingDescribe &&
time.Since(pa.lastActivation) >= 5*time.Second {
for c := range pa.p.clients {
if c.state == clientStateWaitingDescription &&
c.pathId == pa.id {
c.pathId = ""
c.state = clientStateInitial
c.describeRes <- describeRes{nil, fmt.Errorf("publisher of path '%s' has timed out", pa.id)}
}
}
// perform actions below in next run
return
}
if source, ok := pa.publisher.(*source); ok {
if source.state == sourceStateRunning {
hasClients := func() bool { hasClients := func() bool {
for c := range p.p.clients { for c := range pa.p.clients {
if c.path == p.id { if c.pathId == pa.id {
return true return true
} }
} }
return false return false
}() }()
source, publisherIsSource := p.publisher.(*source)
// stop source if needed // stop source if needed
if !hasClients && if !hasClients &&
publisherIsSource && time.Since(pa.lastRequested) >= 10*time.Second {
source.state == sourceStateRunning && source.log("stopping since we're not requested anymore")
time.Since(p.lastRequested) >= 10*time.Second {
source.log("stopping due to inactivity")
source.state = sourceStateStopped source.state = sourceStateStopped
source.events <- sourceEventApplyState{source.state} source.events <- sourceEventApplyState{source.state}
} }
}
} else {
if pa.onDemandCmd != nil {
hasClientReaders := func() bool {
for c := range pa.p.clients {
if c.pathId == pa.id && c != pa.publisher {
return true
}
}
return false
}()
// stop on demand command if needed
if !hasClientReaders &&
time.Since(pa.lastRequested) >= 10*time.Second {
pa.p.log("stopping on demand command since it is not requested anymore")
pa.onDemandCmd.Process.Signal(os.Interrupt)
pa.onDemandCmd.Wait()
pa.onDemandCmd = nil
}
}
}
} }
func (p *path) describe() ([]byte, bool) { func (pa *path) describe(client *client) {
p.lastRequested = time.Now() pa.lastRequested = time.Now()
// publisher not found
if pa.publisher == nil {
if pa.confp.RunOnDemand != "" {
if pa.onDemandCmd == nil {
pa.p.log("starting on demand command")
pa.lastActivation = time.Now()
pa.onDemandCmd = exec.Command("/bin/sh", "-c", pa.confp.RunOnDemand)
pa.onDemandCmd.Stdout = os.Stdout
pa.onDemandCmd.Stderr = os.Stderr
err := pa.onDemandCmd.Start()
if err != nil {
pa.p.log("ERR: %s", err)
}
}
// publisher was found but is not ready: wait client.pathId = pa.id
if !p.publisherReady { client.state = clientStateWaitingDescription
return
} else {
client.describeRes <- describeRes{nil, fmt.Errorf("no one is publishing on path '%s'", pa.id)}
return
}
}
// publisher was found but is not ready: put the client on hold
if !pa.publisherReady {
// start source if needed // start source if needed
if source, ok := p.publisher.(*source); ok && source.state == sourceStateStopped { if source, ok := pa.publisher.(*source); ok && source.state == sourceStateStopped {
source.log("starting on demand") source.log("starting on demand")
pa.lastActivation = time.Now()
source.state = sourceStateRunning source.state = sourceStateRunning
source.events <- sourceEventApplyState{source.state} source.events <- sourceEventApplyState{source.state}
} }
return nil, true client.pathId = pa.id
client.state = clientStateWaitingDescription
return
} }
// publisher was found and is ready // publisher was found and is ready
return p.publisherSdpText, false client.describeRes <- describeRes{pa.publisherSdpText, nil}
} }
func (p *path) publisherSetReady() { func (pa *path) publisherRemove() {
p.publisherReady = true for c := range pa.p.clients {
if c.state == clientStateWaitingDescription &&
c.pathId == pa.id {
c.pathId = ""
c.state = clientStateInitial
c.describeRes <- describeRes{nil, fmt.Errorf("publisher of path '%s' is not available anymore", pa.id)}
}
}
pa.publisher = nil
}
func (pa *path) publisherSetReady() {
pa.publisherReady = true
// reply to all clients that are waiting for a description // reply to all clients that are waiting for a description
for c := range p.p.clients { for c := range pa.p.clients {
if c.state == clientStateWaitingDescription && if c.state == clientStateWaitingDescription &&
c.path == p.id { c.pathId == pa.id {
c.path = "" c.pathId = ""
c.state = clientStateInitial c.state = clientStateInitial
c.describeRes <- p.publisherSdpText c.describeRes <- describeRes{pa.publisherSdpText, nil}
} }
} }
} }
func (p *path) publisherSetNotReady() { func (pa *path) publisherSetNotReady() {
p.publisherReady = false pa.publisherReady = false
// close all clients that are reading // close all clients that are reading
for c := range p.p.clients { for c := range pa.p.clients {
if c.state != clientStateWaitingDescription && if c.state != clientStateWaitingDescription &&
c != p.publisher && c != pa.publisher &&
c.path == p.id { c.pathId == pa.id {
c.conn.NetConn().Close() c.conn.NetConn().Close()
} }
} }
} }
func (p *path) publisherReset() {
// reply to all clients that were waiting for a description
for oc := range p.p.clients {
if oc.state == clientStateWaitingDescription &&
oc.path == p.id {
oc.path = ""
oc.state = clientStateInitial
oc.describeRes <- nil
}
}
}

5
rtsp-simple-server.yml

@ -48,6 +48,11 @@ paths:
# IPs or networks (x.x.x.x/24) allowed to read # IPs or networks (x.x.x.x/24) allowed to read
readIps: [] readIps: []
# command to run when this path is requested.
# This can be used, for example, to publish a stream on demand.
# This is terminated with SIGINT when a client stops publishing.
runOnDemand:
# command to run when a client starts publishing. # command to run when a client starts publishing.
# This is terminated with SIGINT when a client stops publishing. # This is terminated with SIGINT when a client stops publishing.
runOnPublish: runOnPublish:

49
source.go

@ -39,8 +39,8 @@ func (sourceEventTerminate) isSourceEvent() {}
type source struct { type source struct {
p *program p *program
path string pathId string
pconf *confPath confp *confPath
state sourceState state sourceState
tracks []*gortsplib.Track tracks []*gortsplib.Track
@ -48,16 +48,16 @@ type source struct {
done chan struct{} done chan struct{}
} }
func newSource(p *program, path string, pconf *confPath) *source { func newSource(p *program, pathId string, confp *confPath) *source {
s := &source{ s := &source{
p: p, p: p,
path: path, pathId: pathId,
pconf: pconf, confp: confp,
events: make(chan sourceEvent), events: make(chan sourceEvent),
done: make(chan struct{}), done: make(chan struct{}),
} }
if pconf.SourceOnDemand { if confp.SourceOnDemand {
s.state = sourceStateStopped s.state = sourceStateStopped
} else { } else {
s.state = sourceStateRunning s.state = sourceStateRunning
@ -67,7 +67,7 @@ func newSource(p *program, path string, pconf *confPath) *source {
} }
func (s *source) log(format string, args ...interface{}) { func (s *source) log(format string, args ...interface{}) {
s.p.log("[source "+s.path+"] "+format, args...) s.p.log("[source "+s.pathId+"] "+format, args...)
} }
func (s *source) isPublisher() {} func (s *source) isPublisher() {}
@ -121,23 +121,24 @@ func (s *source) do(terminate chan struct{}, done chan struct{}) {
defer close(done) defer close(done)
for { for {
ok := func() bool {
ok := s.doInner(terminate) ok := s.doInner(terminate)
if !ok { if !ok {
break return false
} }
s.p.events <- programEventSourceReset{s}
if !func() bool {
t := time.NewTimer(sourceRetryInterval) t := time.NewTimer(sourceRetryInterval)
defer t.Stop() defer t.Stop()
select { select {
case <-terminate: case <-terminate:
return false return false
case <-t.C: case <-t.C:
return true
} }
}() {
return true
}()
if !ok {
break break
} }
} }
@ -151,7 +152,7 @@ func (s *source) doInner(terminate chan struct{}) bool {
dialDone := make(chan struct{}) dialDone := make(chan struct{})
go func() { go func() {
conn, err = gortsplib.NewConnClient(gortsplib.ConnClientConf{ conn, err = gortsplib.NewConnClient(gortsplib.ConnClientConf{
Host: s.pconf.sourceUrl.Host, Host: s.confp.sourceUrl.Host,
ReadTimeout: s.p.conf.ReadTimeout, ReadTimeout: s.p.conf.ReadTimeout,
WriteTimeout: s.p.conf.WriteTimeout, WriteTimeout: s.p.conf.WriteTimeout,
}) })
@ -171,13 +172,13 @@ func (s *source) doInner(terminate chan struct{}) bool {
defer conn.Close() defer conn.Close()
_, err = conn.Options(s.pconf.sourceUrl) _, err = conn.Options(s.confp.sourceUrl)
if err != nil { if err != nil {
s.log("ERR: %s", err) s.log("ERR: %s", err)
return true return true
} }
tracks, _, err := conn.Describe(s.pconf.sourceUrl) tracks, _, err := conn.Describe(s.confp.sourceUrl)
if err != nil { if err != nil {
s.log("ERR: %s", err) s.log("ERR: %s", err)
return true return true
@ -187,10 +188,10 @@ func (s *source) doInner(terminate chan struct{}) bool {
serverSdpParsed, serverSdpText := sdpForServer(tracks) serverSdpParsed, serverSdpText := sdpForServer(tracks)
s.tracks = tracks s.tracks = tracks
s.p.paths[s.path].publisherSdpText = serverSdpText s.p.paths[s.pathId].publisherSdpText = serverSdpText
s.p.paths[s.path].publisherSdpParsed = serverSdpParsed s.p.paths[s.pathId].publisherSdpParsed = serverSdpParsed
if s.pconf.sourceProtocolParsed == gortsplib.StreamProtocolUdp { if s.confp.sourceProtocolParsed == gortsplib.StreamProtocolUdp {
return s.runUdp(terminate, conn) return s.runUdp(terminate, conn)
} else { } else {
return s.runTcp(terminate, conn) return s.runTcp(terminate, conn)
@ -215,7 +216,7 @@ func (s *source) runUdp(terminate chan struct{}, conn *gortsplib.ConnClient) boo
rtpPort := (rand.Intn((65535-10000)/2) * 2) + 10000 rtpPort := (rand.Intn((65535-10000)/2) * 2) + 10000
rtcpPort := rtpPort + 1 rtcpPort := rtpPort + 1
rtpl, rtcpl, _, err = conn.SetupUdp(s.pconf.sourceUrl, track, rtpPort, rtcpPort) rtpl, rtcpl, _, err = conn.SetupUdp(s.confp.sourceUrl, track, rtpPort, rtcpPort)
if err != nil { if err != nil {
// retry if it's a bind error // retry if it's a bind error
if nerr, ok := err.(*net.OpError); ok { if nerr, ok := err.(*net.OpError); ok {
@ -239,7 +240,7 @@ func (s *source) runUdp(terminate chan struct{}, conn *gortsplib.ConnClient) boo
}) })
} }
_, err := conn.Play(s.pconf.sourceUrl) _, err := conn.Play(s.confp.sourceUrl)
if err != nil { if err != nil {
s.log("ERR: %s", err) s.log("ERR: %s", err)
return true return true
@ -289,7 +290,7 @@ func (s *source) runUdp(terminate chan struct{}, conn *gortsplib.ConnClient) boo
tcpConnDone := make(chan error) tcpConnDone := make(chan error)
go func() { go func() {
tcpConnDone <- conn.LoopUDP(s.pconf.sourceUrl) tcpConnDone <- conn.LoopUDP(s.confp.sourceUrl)
}() }()
var ret bool var ret bool
@ -323,14 +324,14 @@ outer:
func (s *source) runTcp(terminate chan struct{}, conn *gortsplib.ConnClient) bool { func (s *source) runTcp(terminate chan struct{}, conn *gortsplib.ConnClient) bool {
for _, track := range s.tracks { for _, track := range s.tracks {
_, err := conn.SetupTcp(s.pconf.sourceUrl, track) _, err := conn.SetupTcp(s.confp.sourceUrl, track)
if err != nil { if err != nil {
s.log("ERR: %s", err) s.log("ERR: %s", err)
return true return true
} }
} }
_, err := conn.Play(s.pconf.sourceUrl) _, err := conn.Play(s.confp.sourceUrl)
if err != nil { if err != nil {
s.log("ERR: %s", err) s.log("ERR: %s", err)
return true return true

1
utils.go

@ -74,6 +74,7 @@ func (db *doubleBuffer) swap() []byte {
return ret return ret
} }
// generate a sdp from scratch
func sdpForServer(tracks []*gortsplib.Track) (*sdp.SessionDescription, []byte) { func sdpForServer(tracks []*gortsplib.Track) (*sdp.SessionDescription, []byte) {
sout := &sdp.SessionDescription{ sout := &sdp.SessionDescription{
SessionName: func() *sdp.SessionName { SessionName: func() *sdp.SessionName {

Loading…
Cancel
Save