add runOnUnDemand hook (#2645)

This commit is contained in:
Alessandro Ros 2023-11-04 13:07:51 +01:00 committed by GitHub
parent 1d1d64cb89
commit 813611057d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 261 additions and 223 deletions

View File

@ -1311,20 +1311,28 @@ paths:
`runOnDemand` allows to run a command when a path is requested by a reader. This can be used to publish a stream on demand:
```yml
paths:
mypath:
# Command to run when this path is requested by a reader
# and no one is publishing to this path yet.
# This is terminated with SIGINT when the program closes.
# The following environment variables are available:
# * MTX_PATH: path name
# * MTX_QUERY: query parameters (passed by first reader)
# * RTSP_PORT: RTSP server port
# * G1, G2, ...: regular expression groups, if path name is
# a regular expression.
runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
# Restart the command if it exits.
runOnDemandRestart: no
pathDefaults:
# Command to run when this path is requested by a reader
# and no one is publishing to this path yet.
# This is terminated with SIGINT when there are no readers anymore.
# The following environment variables are available:
# * MTX_PATH: path name
# * MTX_QUERY: query parameters (passed by first reader)
# * RTSP_PORT: RTSP server port
# * G1, G2, ...: regular expression groups, if path name is
# a regular expression.
runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath
# Restart the command if it exits.
runOnDemandRestart: no
```
`runOnUnDemand` allows to run a command when there are no readers anymore:
```yml
pathDefaults:
# Command to run when there are no readers anymore.
# Environment variables are the same of runOnDemand.
runOnUnDemand:
```
`runOnReady` allows to run a command when a stream is ready to be read:

View File

@ -344,6 +344,8 @@ components:
type: string
runOnDemandCloseAfter:
type: string
runOnUnDemand:
type: string
runOnReady:
type: string
runOnReadyRestart:

View File

@ -135,6 +135,7 @@ type Path struct {
RunOnDemandRestart bool `json:"runOnDemandRestart"`
RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"`
RunOnDemandCloseAfter StringDuration `json:"runOnDemandCloseAfter"`
RunOnUnDemand string `json:"runOnUnDemand"`
RunOnReady string `json:"runOnReady"`
RunOnReadyRestart bool `json:"runOnReadyRestart"`
RunOnNotReady string `json:"runOnNotReady"`
@ -489,8 +490,8 @@ func (pconf *Path) check(conf *Conf, name string) error {
return fmt.Errorf("a path with a regular expression (or path 'all')" +
" does not support option 'runOnInit'; use another path")
}
if pconf.RunOnDemand != "" && pconf.Source != "publisher" {
return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'")
if (pconf.RunOnDemand != "" || pconf.RunOnUnDemand != "") && pconf.Source != "publisher" {
return fmt.Errorf("'runOnDemand' and 'runOnUnDemand' can be used only when source is 'publisher'")
}
return nil

View File

@ -1,8 +1,6 @@
package core
import (
"net"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
@ -16,7 +14,7 @@ type conn struct {
externalCmdPool *externalcmd.Pool
logger logger.Writer
onConnectCmd *externalcmd.Cmd
onDisconnectHook func()
}
func newConn(
@ -38,48 +36,9 @@ func newConn(
}
func (c *conn) open(desc defs.APIPathSourceOrReader) {
if c.runOnConnect != "" {
c.logger.Log(logger.Info, "runOnConnect command started")
_, port, _ := net.SplitHostPort(c.rtspAddress)
env := externalcmd.Environment{
"RTSP_PORT": port,
"MTX_CONN_TYPE": desc.Type,
"MTX_CONN_ID": desc.ID,
}
c.onConnectCmd = externalcmd.NewCmd(
c.externalCmdPool,
c.runOnConnect,
c.runOnConnectRestart,
env,
func(err error) {
c.logger.Log(logger.Info, "runOnConnect command exited: %v", err)
})
}
c.onDisconnectHook = onConnectHook(c, desc)
}
func (c *conn) close(desc defs.APIPathSourceOrReader) {
if c.onConnectCmd != nil {
c.onConnectCmd.Close()
c.logger.Log(logger.Info, "runOnConnect command stopped")
}
if c.runOnDisconnect != "" {
c.logger.Log(logger.Info, "runOnDisconnect command launched")
_, port, _ := net.SplitHostPort(c.rtspAddress)
env := externalcmd.Environment{
"RTSP_PORT": port,
"MTX_CONN_TYPE": desc.Type,
"MTX_CONN_ID": desc.ID,
}
externalcmd.NewCmd(
c.externalCmdPool,
c.runOnDisconnect,
false,
env,
nil)
}
func (c *conn) close() {
c.onDisconnectHook()
}

208
internal/core/hooks.go Normal file
View File

@ -0,0 +1,208 @@
package core
import (
"net"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
)
func onInitHook(path *path) func() {
var onInitCmd *externalcmd.Cmd
if path.conf.RunOnInit != "" {
path.Log(logger.Info, "runOnInit command started")
onInitCmd = externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnInit,
path.conf.RunOnInitRestart,
path.externalCmdEnv(),
func(err error) {
path.Log(logger.Info, "runOnInit command exited: %v", err)
})
}
return func() {
if onInitCmd != nil {
onInitCmd.Close()
path.Log(logger.Info, "runOnInit command stopped")
}
}
}
func onConnectHook(c *conn, desc defs.APIPathSourceOrReader) func() {
var env externalcmd.Environment
var onConnectCmd *externalcmd.Cmd
if c.runOnConnect != "" || c.runOnDisconnect != "" {
_, port, _ := net.SplitHostPort(c.rtspAddress)
env = externalcmd.Environment{
"RTSP_PORT": port,
"MTX_CONN_TYPE": desc.Type,
"MTX_CONN_ID": desc.ID,
}
}
if c.runOnConnect != "" {
c.logger.Log(logger.Info, "runOnConnect command started")
onConnectCmd = externalcmd.NewCmd(
c.externalCmdPool,
c.runOnConnect,
c.runOnConnectRestart,
env,
func(err error) {
c.logger.Log(logger.Info, "runOnConnect command exited: %v", err)
})
}
return func() {
if onConnectCmd != nil {
onConnectCmd.Close()
c.logger.Log(logger.Info, "runOnConnect command stopped")
}
if c.runOnDisconnect != "" {
c.logger.Log(logger.Info, "runOnDisconnect command launched")
externalcmd.NewCmd(
c.externalCmdPool,
c.runOnDisconnect,
false,
env,
nil)
}
}
}
func onDemandHook(path *path, query string) func(string) {
var env externalcmd.Environment
var onDemandCmd *externalcmd.Cmd
if path.conf.RunOnDemand != "" || path.conf.RunOnUnDemand != "" {
env = path.externalCmdEnv()
env["MTX_QUERY"] = query
}
if path.conf.RunOnDemand != "" {
path.Log(logger.Info, "runOnDemand command started")
onDemandCmd = externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnDemand,
path.conf.RunOnDemandRestart,
env,
func(err error) {
path.Log(logger.Info, "runOnDemand command exited: %v", err)
})
}
return func(reason string) {
if onDemandCmd != nil {
onDemandCmd.Close()
path.Log(logger.Info, "runOnDemand command stopped: %v", reason)
}
if path.conf.RunOnUnDemand != "" {
path.Log(logger.Info, "runOnUnDemand command launched")
externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnUnDemand,
false,
env,
nil)
}
}
}
func onReadyHook(path *path) func() {
var env externalcmd.Environment
var onReadyCmd *externalcmd.Cmd
if path.conf.RunOnReady != "" || path.conf.RunOnNotReady != "" {
env = path.externalCmdEnv()
desc := path.source.APISourceDescribe()
env["MTX_QUERY"] = path.publisherQuery
env["MTX_SOURCE_TYPE"] = desc.Type
env["MTX_SOURCE_ID"] = desc.ID
}
if path.conf.RunOnReady != "" {
path.Log(logger.Info, "runOnReady command started")
onReadyCmd = externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnReady,
path.conf.RunOnReadyRestart,
env,
func(err error) {
path.Log(logger.Info, "runOnReady command exited: %v", err)
})
}
return func() {
if onReadyCmd != nil {
onReadyCmd.Close()
path.Log(logger.Info, "runOnReady command stopped")
}
if path.conf.RunOnNotReady != "" {
path.Log(logger.Info, "runOnNotReady command launched")
externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnNotReady,
false,
env,
nil)
}
}
}
func onReadHook(
externalCmdPool *externalcmd.Pool,
pathConf *conf.Path,
path *path,
reader defs.APIPathSourceOrReader,
query string,
l logger.Writer,
) func() {
var env externalcmd.Environment
var onReadCmd *externalcmd.Cmd
if pathConf.RunOnRead != "" || pathConf.RunOnUnread != "" {
env = path.externalCmdEnv()
desc := reader
env["MTX_QUERY"] = query
env["MTX_READER_TYPE"] = desc.Type
env["MTX_READER_ID"] = desc.ID
}
if pathConf.RunOnRead != "" {
l.Log(logger.Info, "runOnRead command started")
onReadCmd = externalcmd.NewCmd(
externalCmdPool,
pathConf.RunOnRead,
pathConf.RunOnReadRestart,
env,
func(err error) {
l.Log(logger.Info, "runOnRead command exited: %v", err)
})
}
return func() {
if onReadCmd != nil {
onReadCmd.Close()
l.Log(logger.Info, "runOnRead command stopped")
}
if pathConf.RunOnUnread != "" {
l.Log(logger.Info, "runOnUnread command launched")
externalcmd.NewCmd(
externalCmdPool,
pathConf.RunOnUnread,
false,
env,
nil)
}
}
}

View File

@ -304,18 +304,7 @@ func (pa *path) run() {
}
}
var onInitCmd *externalcmd.Cmd
if pa.conf.RunOnInit != "" {
pa.Log(logger.Info, "runOnInit command started")
onInitCmd = externalcmd.NewCmd(
pa.externalCmdPool,
pa.conf.RunOnInit,
pa.conf.RunOnInitRestart,
pa.externalCmdEnv(),
func(err error) {
pa.Log(logger.Info, "runOnInit command exited: %v", err)
})
}
onUnInitHook := onInitHook(pa)
err := pa.runInner()
@ -329,10 +318,7 @@ func (pa *path) run() {
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherCloseTimer.Stop()
if onInitCmd != nil {
onInitCmd.Close()
pa.Log(logger.Info, "runOnInit command stopped")
}
onUnInitHook()
for _, req := range pa.describeRequestsOnHold {
req.res <- pathDescribeRes{err: fmt.Errorf("terminated")}
@ -802,7 +788,7 @@ func (pa *path) onDemandStaticSourceStop(reason string) {
}
func (pa *path) onDemandPublisherStart(query string) {
pa.onUnDemandHook = publisherOnDemandHook(pa, query)
pa.onUnDemandHook = onDemandHook(pa, query)
pa.onDemandPublisherReadyTimer.Stop()
pa.onDemandPublisherReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout))
@ -847,7 +833,7 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error
pa.readyTime = time.Now()
pa.onNotReadyHook = sourceOnReadyHook(pa)
pa.onNotReadyHook = onReadyHook(pa)
pa.parent.pathReady(pa)

View File

@ -84,6 +84,7 @@ func main() {
func TestPathRunOnDemand(t *testing.T) {
onDemandFile := filepath.Join(os.TempDir(), "ondemand")
onUnDemandFile := filepath.Join(os.TempDir(), "ondisconnect")
srcFile := filepath.Join(os.TempDir(), "ondemand.go")
err := os.WriteFile(srcFile,
@ -103,6 +104,7 @@ func TestPathRunOnDemand(t *testing.T) {
for _, ca := range []string{"describe", "setup", "describe and setup"} {
t.Run(ca, func(t *testing.T) {
defer os.Remove(onDemandFile)
defer os.Remove(onUnDemandFile)
p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+
"hls: no\n"+
@ -110,7 +112,8 @@ func TestPathRunOnDemand(t *testing.T) {
"paths:\n"+
" '~^(on)demand$':\n"+
" runOnDemand: %s\n"+
" runOnDemandCloseAfter: 1s\n", execFile))
" runOnDemandCloseAfter: 1s\n"+
" runOnUnDemand: touch %s\n", execFile, onUnDemandFile))
require.Equal(t, true, ok)
defer p1.Close()
@ -185,6 +188,9 @@ func TestPathRunOnDemand(t *testing.T) {
}
time.Sleep(100 * time.Millisecond)
}
_, err := os.Stat(onUnDemandFile)
require.NoError(t, err)
})
}
}

View File

@ -1,39 +1,7 @@
package core
import (
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
)
// publisher is an entity that can publish a stream.
type publisher interface {
source
close()
}
func publisherOnDemandHook(path *path, query string) func(string) {
var onDemandCmd *externalcmd.Cmd
if path.conf.RunOnDemand != "" {
env := path.externalCmdEnv()
env["MTX_QUERY"] = query
path.Log(logger.Info, "runOnDemand command started")
onDemandCmd = externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnDemand,
path.conf.RunOnDemandRestart,
env,
func(err error) {
path.Log(logger.Info, "runOnDemand command exited: %v", err)
})
}
return func(reason string) {
if onDemandCmd != nil {
onDemandCmd.Close()
path.Log(logger.Info, "runOnDemand command stopped: %v", reason)
}
}
}

View File

@ -2,10 +2,7 @@ package core
import (
"github.com/bluenviron/mediamtx/internal/asyncwriter"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
)
@ -18,52 +15,3 @@ type reader interface {
func readerMediaInfo(r *asyncwriter.Writer, stream *stream.Stream) string {
return mediaInfo(stream.MediasForReader(r))
}
func readerOnReadHook(
externalCmdPool *externalcmd.Pool,
pathConf *conf.Path,
path *path,
reader defs.APIPathSourceOrReader,
query string,
l logger.Writer,
) func() {
var env externalcmd.Environment
var onReadCmd *externalcmd.Cmd
if pathConf.RunOnRead != "" || pathConf.RunOnUnread != "" {
env = path.externalCmdEnv()
desc := reader
env["MTX_QUERY"] = query
env["MTX_READER_TYPE"] = desc.Type
env["MTX_READER_ID"] = desc.ID
}
if pathConf.RunOnRead != "" {
l.Log(logger.Info, "runOnRead command started")
onReadCmd = externalcmd.NewCmd(
externalCmdPool,
pathConf.RunOnRead,
pathConf.RunOnReadRestart,
env,
func(err error) {
l.Log(logger.Info, "runOnRead command exited: %v", err)
})
}
return func() {
if onReadCmd != nil {
onReadCmd.Close()
l.Log(logger.Info, "runOnRead command stopped")
}
if pathConf.RunOnUnread != "" {
l.Log(logger.Info, "runOnUnread command launched")
externalcmd.NewCmd(
externalCmdPool,
pathConf.RunOnUnread,
false,
env,
nil)
}
}
}

View File

@ -151,7 +151,7 @@ func (c *rtmpConn) run() { //nolint:dupl
desc := c.apiReaderDescribe()
c.conn.open(desc)
defer c.conn.close(desc)
defer c.conn.close()
err := c.runInner()
@ -256,7 +256,7 @@ func (c *rtmpConn) runRead(conn *rtmp.Conn, u *url.URL) error {
pathConf := res.path.safeConf()
onUnreadHook := readerOnReadHook(
onUnreadHook := onReadHook(
c.externalCmdPool,
pathConf,
res.path,

View File

@ -114,15 +114,7 @@ func (c *rtspConn) ip() net.IP {
func (c *rtspConn) onClose(err error) {
c.Log(logger.Info, "closed: %v", err)
c.conn.close(defs.APIPathSourceOrReader{
Type: func() string {
if c.isTLS {
return "rtspsConn"
}
return "rtspConn"
}(),
ID: c.uuid.String(),
})
c.conn.close()
}
// onRequest is called by rtspServer.

View File

@ -292,7 +292,7 @@ func (s *rtspSession) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Respons
pathConf := s.path.safeConf()
s.onUnreadHook = readerOnReadHook(
s.onUnreadHook = onReadHook(
s.externalCmdPool,
pathConf,
s.path,

View File

@ -7,7 +7,6 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/description"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
)
@ -48,45 +47,3 @@ func mediaInfo(medias []*description.Media) string {
}(),
strings.Join(mediasDescription(medias), ", "))
}
func sourceOnReadyHook(path *path) func() {
var env externalcmd.Environment
var onReadyCmd *externalcmd.Cmd
if path.conf.RunOnReady != "" {
env = path.externalCmdEnv()
desc := path.source.APISourceDescribe()
env["MTX_QUERY"] = path.publisherQuery
env["MTX_SOURCE_TYPE"] = desc.Type
env["MTX_SOURCE_ID"] = desc.ID
}
if path.conf.RunOnReady != "" {
path.Log(logger.Info, "runOnReady command started")
onReadyCmd = externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnReady,
path.conf.RunOnReadyRestart,
env,
func(err error) {
path.Log(logger.Info, "runOnReady command exited: %v", err)
})
}
return func() {
if onReadyCmd != nil {
onReadyCmd.Close()
path.Log(logger.Info, "runOnReady command stopped")
}
if path.conf.RunOnNotReady != "" {
path.Log(logger.Info, "runOnNotReady command launched")
externalcmd.NewCmd(
path.externalCmdPool,
path.conf.RunOnNotReady,
false,
env,
nil)
}
}
}

View File

@ -156,7 +156,7 @@ func (c *srtConn) run() { //nolint:dupl
desc := c.apiReaderDescribe()
c.conn.open(desc)
defer c.conn.close(desc)
defer c.conn.close()
err := c.runInner()
@ -363,7 +363,7 @@ func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass
pathConf := res.path.safeConf()
onUnreadHook := readerOnReadHook(
onUnreadHook := onReadHook(
c.externalCmdPool,
pathConf,
res.path,

View File

@ -612,7 +612,7 @@ func (s *webRTCSession) runRead() (int, error) {
pathConf := res.path.safeConf()
onUnreadHook := readerOnReadHook(
onUnreadHook := onReadHook(
s.externalCmdPool,
pathConf,
res.path,

View File

@ -471,7 +471,7 @@ pathDefaults:
# Command to run when this path is requested by a reader
# and no one is publishing to this path yet.
# 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 there are no readers anymore.
# The following environment variables are available:
# * MTX_PATH: path name
# * MTX_QUERY: query parameters (passed by first reader)
@ -487,6 +487,9 @@ pathDefaults:
# The command will be closed when there are no
# readers connected and this amount of time has passed.
runOnDemandCloseAfter: 10s
# Command to run when there are no readers anymore.
# Environment variables are the same of runOnDemand.
runOnUnDemand:
# Command to run when the stream is ready to be read, whenever it is
# published by a client or pulled from a server / camera.