Browse Source

make regexp groups available to custom commands (#642)

pull/745/head
aler9 5 years ago
parent
commit
ebc201bda2
  1. 6
      internal/core/core_test.go
  2. 45
      internal/core/path.go
  3. 38
      internal/core/path_manager.go
  4. 18
      internal/core/rtmp_conn.go
  5. 9
      internal/core/rtsp_conn.go
  6. 1
      internal/core/rtsp_server.go
  7. 12
      internal/core/rtsp_session.go
  8. 5
      internal/externalcmd/cmd.go
  9. 8
      internal/externalcmd/cmd_unix.go
  10. 24
      internal/externalcmd/cmd_win.go
  11. 33
      rtsp-simple-server.yml

6
internal/core/core_test.go

@ -214,6 +214,10 @@ import (
) )
func main() { func main() {
if os.Getenv("1") != "on" {
panic("environment not set")
}
track, err := gortsplib.NewTrackH264(96, track, err := gortsplib.NewTrackH264(96,
&gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}}) &gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}})
if err != nil { if err != nil {
@ -259,7 +263,7 @@ func main() {
p1, ok := newInstance(fmt.Sprintf("rtmpDisable: yes\n"+ p1, ok := newInstance(fmt.Sprintf("rtmpDisable: yes\n"+
"hlsDisable: yes\n"+ "hlsDisable: yes\n"+
"paths:\n"+ "paths:\n"+
" all:\n"+ " '~^(on)demand$':\n"+
" runOnDemand: %s\n"+ " runOnDemand: %s\n"+
" runOnDemandCloseAfter: 1s\n", execFile)) " runOnDemandCloseAfter: 1s\n", execFile))
require.Equal(t, true, ok) require.Equal(t, true, ok)

45
internal/core/path.go

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -215,6 +216,7 @@ type path struct {
confName string confName string
conf *conf.PathConf conf *conf.PathConf
name string name string
matches []string
wg *sync.WaitGroup wg *sync.WaitGroup
parent pathParent parent pathParent
@ -258,6 +260,7 @@ func newPath(
confName string, confName string,
conf *conf.PathConf, conf *conf.PathConf,
name string, name string,
matches []string,
wg *sync.WaitGroup, wg *sync.WaitGroup,
parent pathParent) *path { parent pathParent) *path {
ctx, ctxCancel := context.WithCancel(parentCtx) ctx, ctxCancel := context.WithCancel(parentCtx)
@ -271,6 +274,7 @@ func newPath(
confName: confName, confName: confName,
conf: conf, conf: conf,
name: name, name: name,
matches: matches,
wg: wg, wg: wg,
parent: parent, parent: parent,
ctx: ctx, ctx: ctx,
@ -336,11 +340,10 @@ func (pa *path) run() {
var onInitCmd *externalcmd.Cmd var onInitCmd *externalcmd.Cmd
if pa.conf.RunOnInit != "" { if pa.conf.RunOnInit != "" {
pa.log(logger.Info, "runOnInit command started") pa.log(logger.Info, "runOnInit command started")
_, port, _ := net.SplitHostPort(pa.rtspAddress) onInitCmd = externalcmd.New(
onInitCmd = externalcmd.New(pa.conf.RunOnInit, pa.conf.RunOnInitRestart, externalcmd.Environment{ pa.conf.RunOnInit,
Path: pa.name, pa.conf.RunOnInitRestart,
Port: port, pa.externalCmdEnv())
})
} }
err := func() error { err := func() error {
@ -514,6 +517,20 @@ func (pa *path) isOnDemand() bool {
return (pa.hasStaticSource() && pa.conf.SourceOnDemand) || pa.conf.RunOnDemand != "" return (pa.hasStaticSource() && pa.conf.SourceOnDemand) || pa.conf.RunOnDemand != ""
} }
func (pa *path) externalCmdEnv() externalcmd.Environment {
_, port, _ := net.SplitHostPort(pa.rtspAddress)
env := externalcmd.Environment{
"RTSP_PATH": pa.name,
"RTSP_PORT": port,
}
for i, ma := range pa.matches[1:] {
env[strconv.FormatInt(int64(i+1), 10)] = ma
}
return env
}
func (pa *path) onDemandStartSource() { func (pa *path) onDemandStartSource() {
pa.onDemandReadyTimer.Stop() pa.onDemandReadyTimer.Stop()
if pa.hasStaticSource() { if pa.hasStaticSource() {
@ -521,11 +538,10 @@ func (pa *path) onDemandStartSource() {
pa.onDemandReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout)) pa.onDemandReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))
} else { } else {
pa.log(logger.Info, "runOnDemand command started") pa.log(logger.Info, "runOnDemand command started")
_, port, _ := net.SplitHostPort(pa.rtspAddress) pa.onDemandCmd = externalcmd.New(
pa.onDemandCmd = externalcmd.New(pa.conf.RunOnDemand, pa.conf.RunOnDemandRestart, externalcmd.Environment{ pa.conf.RunOnDemand,
Path: pa.name, pa.conf.RunOnDemandRestart,
Port: port, pa.externalCmdEnv())
})
pa.onDemandReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout)) pa.onDemandReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout))
} }
@ -775,11 +791,10 @@ func (pa *path) handlePublisherRecord(req pathPublisherRecordReq) {
if pa.conf.RunOnPublish != "" { if pa.conf.RunOnPublish != "" {
pa.log(logger.Info, "runOnPublish command started") pa.log(logger.Info, "runOnPublish command started")
_, port, _ := net.SplitHostPort(pa.rtspAddress) pa.onPublishCmd = externalcmd.New(
pa.onPublishCmd = externalcmd.New(pa.conf.RunOnPublish, pa.conf.RunOnPublishRestart, externalcmd.Environment{ pa.conf.RunOnPublish,
Path: pa.name, pa.conf.RunOnPublishRestart,
Port: port, pa.externalCmdEnv())
})
} }
req.Res <- pathPublisherRecordRes{Stream: pa.stream} req.Res <- pathPublisherRecordRes{Stream: pa.stream}

38
internal/core/path_manager.go

@ -83,7 +83,7 @@ func newPathManager(
for pathName, pathConf := range pm.pathConfs { for pathName, pathConf := range pm.pathConfs {
if pathConf.Regexp == nil { if pathConf.Regexp == nil {
pm.createPath(pathName, pathConf, pathName) pm.createPath(pathName, pathConf, pathName, nil)
} }
} }
@ -147,7 +147,7 @@ outer:
// add new paths // add new paths
for pathName, pathConf := range pm.pathConfs { for pathName, pathConf := range pm.pathConfs {
if _, ok := pm.paths[pathName]; !ok && pathConf.Regexp == nil { if _, ok := pm.paths[pathName]; !ok && pathConf.Regexp == nil {
pm.createPath(pathName, pathConf, pathName) pm.createPath(pathName, pathConf, pathName, nil)
} }
} }
@ -164,7 +164,7 @@ outer:
} }
case req := <-pm.describe: case req := <-pm.describe:
pathName, pathConf, err := pm.findPathConf(req.PathName) pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil { if err != nil {
req.Res <- pathDescribeRes{Err: err} req.Res <- pathDescribeRes{Err: err}
continue continue
@ -185,13 +185,13 @@ outer:
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok { if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName) pm.createPath(pathName, pathConf, req.PathName, pathMatches)
} }
req.Res <- pathDescribeRes{Path: pm.paths[req.PathName]} req.Res <- pathDescribeRes{Path: pm.paths[req.PathName]}
case req := <-pm.readerSetupPlay: case req := <-pm.readerSetupPlay:
pathName, pathConf, err := pm.findPathConf(req.PathName) pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil { if err != nil {
req.Res <- pathReaderSetupPlayRes{Err: err} req.Res <- pathReaderSetupPlayRes{Err: err}
continue continue
@ -212,13 +212,13 @@ outer:
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok { if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName) pm.createPath(pathName, pathConf, req.PathName, pathMatches)
} }
req.Res <- pathReaderSetupPlayRes{Path: pm.paths[req.PathName]} req.Res <- pathReaderSetupPlayRes{Path: pm.paths[req.PathName]}
case req := <-pm.publisherAnnounce: case req := <-pm.publisherAnnounce:
pathName, pathConf, err := pm.findPathConf(req.PathName) pathName, pathConf, pathMatches, err := pm.findPathConf(req.PathName)
if err != nil { if err != nil {
req.Res <- pathPublisherAnnounceRes{Err: err} req.Res <- pathPublisherAnnounceRes{Err: err}
continue continue
@ -239,7 +239,7 @@ outer:
// create path if it doesn't exist // create path if it doesn't exist
if _, ok := pm.paths[req.PathName]; !ok { if _, ok := pm.paths[req.PathName]; !ok {
pm.createPath(pathName, pathConf, req.PathName) pm.createPath(pathName, pathConf, req.PathName, pathMatches)
} }
req.Res <- pathPublisherAnnounceRes{Path: pm.paths[req.PathName]} req.Res <- pathPublisherAnnounceRes{Path: pm.paths[req.PathName]}
@ -270,7 +270,11 @@ outer:
} }
} }
func (pm *pathManager) createPath(confName string, conf *conf.PathConf, name string) { func (pm *pathManager) createPath(
confName string,
conf *conf.PathConf,
name string,
matches []string) {
pm.paths[name] = newPath( pm.paths[name] = newPath(
pm.ctx, pm.ctx,
pm.rtspAddress, pm.rtspAddress,
@ -281,29 +285,33 @@ func (pm *pathManager) createPath(confName string, conf *conf.PathConf, name str
confName, confName,
conf, conf,
name, name,
matches,
&pm.wg, &pm.wg,
pm) pm)
} }
func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, error) { func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []string, error) {
err := conf.IsValidPathName(name) err := conf.IsValidPathName(name)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("invalid path name: %s (%s)", err, name) return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name)
} }
// normal path // normal path
if pathConf, ok := pm.pathConfs[name]; ok { if pathConf, ok := pm.pathConfs[name]; ok {
return name, pathConf, nil return name, pathConf, nil, nil
} }
// regular expression path // regular expression path
for pathName, pathConf := range pm.pathConfs { for pathName, pathConf := range pm.pathConfs {
if pathConf.Regexp != nil && pathConf.Regexp.MatchString(name) { if pathConf.Regexp != nil {
return pathName, pathConf, nil m := pathConf.Regexp.FindStringSubmatch(name)
if m != nil {
return pathName, pathConf, m, nil
}
} }
} }
return "", nil, fmt.Errorf("path '%s' is not configured", name) return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
} }
func (pm *pathManager) authenticate( func (pm *pathManager) authenticate(

18
internal/core/rtmp_conn.go

@ -148,9 +148,12 @@ func (c *rtmpConn) run() {
if c.runOnConnect != "" { if c.runOnConnect != "" {
c.log(logger.Info, "runOnConnect command started") c.log(logger.Info, "runOnConnect command started")
_, port, _ := net.SplitHostPort(c.rtspAddress) _, port, _ := net.SplitHostPort(c.rtspAddress)
onConnectCmd := externalcmd.New(c.runOnConnect, c.runOnConnectRestart, externalcmd.Environment{ onConnectCmd := externalcmd.New(
Path: "", c.runOnConnect,
Port: port, c.runOnConnectRestart,
externalcmd.Environment{
"RTSP_PATH": "",
"RTSP_PORT": port,
}) })
defer func() { defer func() {
@ -283,11 +286,10 @@ func (c *rtmpConn) runRead(ctx context.Context) error {
if c.path.Conf().RunOnRead != "" { if c.path.Conf().RunOnRead != "" {
c.log(logger.Info, "runOnRead command started") c.log(logger.Info, "runOnRead command started")
_, port, _ := net.SplitHostPort(c.rtspAddress) onReadCmd := externalcmd.New(
onReadCmd := externalcmd.New(c.path.Conf().RunOnRead, c.path.Conf().RunOnReadRestart, externalcmd.Environment{ c.path.Conf().RunOnRead,
Path: c.path.Name(), c.path.Conf().RunOnReadRestart,
Port: port, c.path.externalCmdEnv())
})
defer func() { defer func() {
onReadCmd.Close() onReadCmd.Close()
c.log(logger.Info, "runOnRead command stopped") c.log(logger.Info, "runOnRead command stopped")

9
internal/core/rtsp_conn.go

@ -65,9 +65,12 @@ func newRTSPConn(
if c.runOnConnect != "" { if c.runOnConnect != "" {
c.log(logger.Info, "runOnConnect command started") c.log(logger.Info, "runOnConnect command started")
_, port, _ := net.SplitHostPort(c.rtspAddress) _, port, _ := net.SplitHostPort(c.rtspAddress)
c.onConnectCmd = externalcmd.New(c.runOnConnect, c.runOnConnectRestart, externalcmd.Environment{ c.onConnectCmd = externalcmd.New(
Path: "", c.runOnConnect,
Port: port, c.runOnConnectRestart,
externalcmd.Environment{
"RTSP_PATH": "",
"RTSP_PORT": port,
}) })
} }

1
internal/core/rtsp_server.go

@ -301,7 +301,6 @@ func (s *rtspServer) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx)
se := newRTSPSession( se := newRTSPSession(
s.isTLS, s.isTLS,
s.rtspAddress,
s.protocols, s.protocols,
id, id,
ctx.Session, ctx.Session,

12
internal/core/rtsp_session.go

@ -30,7 +30,6 @@ type rtspSessionParent interface {
type rtspSession struct { type rtspSession struct {
isTLS bool isTLS bool
rtspAddress string
protocols map[conf.Protocol]struct{} protocols map[conf.Protocol]struct{}
id string id string
ss *gortsplib.ServerSession ss *gortsplib.ServerSession
@ -49,7 +48,6 @@ type rtspSession struct {
func newRTSPSession( func newRTSPSession(
isTLS bool, isTLS bool,
rtspAddress string,
protocols map[conf.Protocol]struct{}, protocols map[conf.Protocol]struct{},
id string, id string,
ss *gortsplib.ServerSession, ss *gortsplib.ServerSession,
@ -58,7 +56,6 @@ func newRTSPSession(
parent rtspSessionParent) *rtspSession { parent rtspSessionParent) *rtspSession {
s := &rtspSession{ s := &rtspSession{
isTLS: isTLS, isTLS: isTLS,
rtspAddress: rtspAddress,
protocols: protocols, protocols: protocols,
id: id, id: id,
ss: ss, ss: ss,
@ -279,11 +276,10 @@ func (s *rtspSession) onPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo
if s.path.Conf().RunOnRead != "" { if s.path.Conf().RunOnRead != "" {
s.log(logger.Info, "runOnRead command started") s.log(logger.Info, "runOnRead command started")
_, port, _ := net.SplitHostPort(s.rtspAddress) s.onReadCmd = externalcmd.New(
s.onReadCmd = externalcmd.New(s.path.Conf().RunOnRead, s.path.Conf().RunOnReadRestart, externalcmd.Environment{ s.path.Conf().RunOnRead,
Path: s.path.Name(), s.path.Conf().RunOnReadRestart,
Port: port, s.path.externalCmdEnv())
})
} }
s.stateMutex.Lock() s.stateMutex.Lock()

5
internal/externalcmd/cmd.go

@ -9,10 +9,7 @@ const (
) )
// Environment is a Cmd environment. // Environment is a Cmd environment.
type Environment struct { type Environment map[string]string
Path string
Port string
}
// Cmd is an external command. // Cmd is an external command.
type Cmd struct { type Cmd struct {

8
internal/externalcmd/cmd_unix.go

@ -12,10 +12,10 @@ import (
func (e *Cmd) runInner() bool { func (e *Cmd) runInner() bool {
cmd := exec.Command("/bin/sh", "-c", "exec "+e.cmdstr) cmd := exec.Command("/bin/sh", "-c", "exec "+e.cmdstr)
cmd.Env = append(os.Environ(), cmd.Env = append([]string(nil), os.Environ()...)
"RTSP_PATH="+e.env.Path, for key, val := range e.env {
"RTSP_PORT="+e.env.Port, cmd.Env = append(cmd.Env, key+"="+val)
) }
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr

24
internal/externalcmd/cmd_win.go

@ -12,11 +12,13 @@ import (
) )
func (e *Cmd) runInner() bool { func (e *Cmd) runInner() bool {
// on Windows the shell is not used and command is started directly // On Windows, the shell is not used and command is started directly.
// variables are replaced manually in order to guarantee compatibility // Variables are replaced manually in order to guarantee compatibility
// with Linux commands // with Linux commands.
tmp := strings.ReplaceAll(e.cmdstr, "$RTSP_PATH", e.env.Path) tmp := e.cmdstr
tmp = strings.ReplaceAll(tmp, "$RTSP_PORT", e.env.Port) for key, val := range e.env {
tmp = strings.ReplaceAll(tmp, "$"+key, val)
}
parts, err := shellquote.Split(tmp) parts, err := shellquote.Split(tmp)
if err != nil { if err != nil {
return true return true
@ -24,10 +26,10 @@ func (e *Cmd) runInner() bool {
cmd := exec.Command(parts[0], parts[1:]...) cmd := exec.Command(parts[0], parts[1:]...)
cmd.Env = append(os.Environ(), cmd.Env = append([]string(nil), os.Environ()...)
"RTSP_PATH="+e.env.Path, for key, val := range e.env {
"RTSP_PORT="+e.env.Port, cmd.Env = append(cmd.Env, key+"="+val)
) }
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -45,8 +47,8 @@ func (e *Cmd) runInner() bool {
select { select {
case <-e.terminate: case <-e.terminate:
// on Windows it's not possible to send os.Interrupt to a process // on Windows, it's not possible to send os.Interrupt to a process.
// Kill() is the only supported way // Kill() is the only supported way.
cmd.Process.Kill() cmd.Process.Kill()
<-cmdDone <-cmdDone
return false return false

33
rtsp-simple-server.yml

@ -35,7 +35,8 @@ pprofAddress: 127.0.0.1:9999
# command to run when a client connects to the server. # command to run when a client connects to the server.
# this is terminated with SIGINT when a client disconnects from the server. # this is terminated with SIGINT when a client disconnects from the server.
# the server port is available in the RTSP_PORT variable. # the following environment variables are available:
# * RTSP_PORT: server port
runOnConnect: runOnConnect:
# the restart parameter allows to restart the command if it exits suddenly. # the restart parameter allows to restart the command if it exits suddenly.
runOnConnectRestart: no runOnConnectRestart: no
@ -116,7 +117,7 @@ hlsAllowOrigin: '*'
############################################### ###############################################
# Path parameters # Path parameters
# these settings are path-dependent. # these settings are path-dependent, and the map key is the name of the path.
# it's possible to use regular expressions by using a tilde as prefix. # it's possible to use regular expressions by using a tilde as prefix.
# for example, "~^(test1|test2)$" will match both "test1" and "test2". # for example, "~^(test1|test2)$" will match both "test1" and "test2".
# for example, "~^prefix" will match all paths that start with "prefix". # for example, "~^prefix" will match all paths that start with "prefix".
@ -195,8 +196,11 @@ paths:
# command to run when this path is initialized. # command to run when this path is initialized.
# this can be used to publish a stream and keep it always opened. # this can be used to publish a stream and keep it always opened.
# this is terminated with SIGINT when the program closes. # this is terminated with SIGINT when the program closes.
# the path name is available in the RTSP_PATH variable. # the following environment variables are available:
# the server port is available in the RTSP_PORT variable. # * RTSP_PATH: path name
# * RTSP_PORT: server port
# * 1, 2, ...: regular expression groups, if path name is
# a regular expression.
runOnInit: runOnInit:
# the restart parameter allows to restart the command if it exits suddenly. # the restart parameter allows to restart the command if it exits suddenly.
runOnInitRestart: no runOnInitRestart: no
@ -204,8 +208,11 @@ paths:
# command to run when this path is requested. # command to run when this path is requested.
# this can be used to publish a stream on demand. # this can be used to publish a stream on demand.
# this is terminated with SIGINT when the path is not requested anymore. # this is terminated with SIGINT when the path is not requested anymore.
# the path name is available in the RTSP_PATH variable. # the following environment variables are available:
# the server port is available in the RTSP_PORT variable. # * RTSP_PATH: path name
# * RTSP_PORT: server port
# * 1, 2, ...: regular expression groups, if path name is
# a regular expression.
runOnDemand: runOnDemand:
# the restart parameter allows to restart the command if it exits suddenly. # the restart parameter allows to restart the command if it exits suddenly.
runOnDemandRestart: no runOnDemandRestart: no
@ -218,16 +225,22 @@ paths:
# 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.
# the path name is available in the RTSP_PATH variable. # the following environment variables are available:
# the server port is available in the RTSP_PORT variable. # * RTSP_PATH: path name
# * RTSP_PORT: server port
# * 1, 2, ...: regular expression groups, if path name is
# a regular expression.
runOnPublish: runOnPublish:
# the restart parameter allows to restart the command if it exits suddenly. # the restart parameter allows to restart the command if it exits suddenly.
runOnPublishRestart: no runOnPublishRestart: no
# command to run when a clients starts reading. # command to run when a clients starts reading.
# this is terminated with SIGINT when a client stops reading. # this is terminated with SIGINT when a client stops reading.
# the path name is available in the RTSP_PATH variable. # the following environment variables are available:
# the server port is available in the RTSP_PORT variable. # * RTSP_PATH: path name
# * RTSP_PORT: server port
# * 1, 2, ...: regular expression groups, if path name is
# a regular expression.
runOnRead: runOnRead:
# the restart parameter allows to restart the command if it exits suddenly. # the restart parameter allows to restart the command if it exits suddenly.
runOnReadRestart: no runOnReadRestart: no

Loading…
Cancel
Save