diff --git a/README.md b/README.md index 7a146d17..2cc2af86 100644 --- a/README.md +++ b/README.md @@ -1321,71 +1321,80 @@ paths: `runOnReady` allows to run a command when a stream is ready to be read: ```yml -paths: - mypath: - # This is terminated with SIGINT when the stream is not ready anymore. - # The following environment variables are available: - # * MTX_PATH: path name - # * MTX_SOURCE_TYPE: source type - # * MTX_SOURCE_ID: source ID - # * RTSP_PORT: RTSP server port - # * G1, G2, ...: regular expression groups, if path name is - # a regular expression. - runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID - # Restart the command if it exits. - runOnReadyRestart: no +pathDefaults: + # This is terminated with SIGINT when the stream is not ready anymore. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_SOURCE_TYPE: source type + # * MTX_SOURCE_ID: source ID + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID + # Restart the command if it exits. + runOnReadyRestart: no ``` `runOnNotReady` allows to run a command when a stream is not available anymore: ```yml -paths: - mypath: - # Environment variables are the same of runOnReady. - runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID +pathDefaults: + # Environment variables are the same of runOnReady. + runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID ``` `runOnRead` allows to run a command when a client starts reading: ```yml -paths: - mypath: - # This is terminated with SIGINT when a client stops reading. - # The following environment variables are available: - # * MTX_PATH: path name - # * MTX_READER_TYPE: reader type - # * MTX_READER_ID: reader ID - # * RTSP_PORT: RTSP server port - # * G1, G2, ...: regular expression groups, if path name is - # a regular expression. - runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID - # Restart the command if it exits. - runOnReadRestart: no +pathDefaults: + # This is terminated with SIGINT when a client stops reading. + # The following environment variables are available: + # * MTX_PATH: path name + # * MTX_READER_TYPE: reader type + # * MTX_READER_ID: reader ID + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID + # Restart the command if it exits. + runOnReadRestart: no ``` `runOnUnread` allows to run a command when a client stops reading: ```yml -paths: - mypath: - # Command to run when a client stops reading. - # Environment variables are the same of runOnRead. - runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID +pathDefaults: + # Command to run when a client stops reading. + # Environment variables are the same of runOnRead. + runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID +``` + +`runOnRecordSegmentCreate` allows to run a command when a recording segment is created: + +```yml +pathDefaults: + # Command to run when a recording segment is created. + # The following environment variables are available: + # * MTX_PATH: path name + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + # * MTX_SEGMENT_PATH: segment file path + runOnRecordSegmentCreate: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH ``` `runOnRecordSegmentComplete` allows to run a command when a recording segment is complete: ```yml -paths: - mypath: - # Command to run when a recording segment is complete. - # The following environment variables are available: - # * MTX_PATH: path name - # * RTSP_PORT: RTSP server port - # * G1, G2, ...: regular expression groups, if path name is - # a regular expression. - # * MTX_SEGMENT_PATH: segment file path - runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH +pathDefaults: + # Command to run when a recording segment is complete. + # The following environment variables are available: + # * MTX_PATH: path name + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + # * MTX_SEGMENT_PATH: segment file path + runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH ``` ### API diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 8ff566b0..ce7bbd82 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -347,6 +347,8 @@ components: type: boolean runOnUnread: type: string + runOnRecordSegmentCreate: + type: string runOnRecordSegmentComplete: type: string diff --git a/internal/conf/path.go b/internal/conf/path.go index ef8d3684..b84b25ec 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -138,6 +138,7 @@ type Path struct { RunOnRead string `json:"runOnRead"` RunOnReadRestart bool `json:"runOnReadRestart"` RunOnUnread string `json:"runOnUnread"` + RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"` RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"` } diff --git a/internal/core/path.go b/internal/core/path.go index 3d3d8ef6..4005bdc4 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -957,6 +957,20 @@ func (pa *path) startRecording() { time.Duration(pa.conf.RecordSegmentDuration), pa.name, pa.stream, + func(segmentPath string) { + if pa.conf.RunOnRecordSegmentCreate != "" { + env := pa.externalCmdEnv() + env["MTX_SEGMENT_PATH"] = segmentPath + + pa.Log(logger.Info, "runOnRecordSegmentCreate command launched") + externalcmd.NewCmd( + pa.externalCmdPool, + pa.conf.RunOnRecordSegmentCreate, + false, + env, + nil) + } + }, func(segmentPath string) { if pa.conf.RunOnRecordSegmentComplete != "" { env := pa.externalCmdEnv() diff --git a/internal/record/agent.go b/internal/record/agent.go index 5ce1c2e3..36bf83f2 100644 --- a/internal/record/agent.go +++ b/internal/record/agent.go @@ -26,6 +26,9 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) +// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete +type OnSegmentFunc = func(string) + func durationGoToMp4(v time.Duration, timeScale uint32) uint64 { timeScale64 := uint64(timeScale) secs := v / time.Second @@ -111,7 +114,8 @@ type Agent struct { partDuration time.Duration segmentDuration time.Duration stream *stream.Stream - onSegmentComplete func(string) + onSegmentCreate OnSegmentFunc + onSegmentComplete OnSegmentFunc parent logger.Writer ctx context.Context @@ -125,7 +129,7 @@ type Agent struct { done chan struct{} } -// NewAgent allocates a nAgent. +// NewAgent allocates an Agent. func NewAgent( writeQueueSize int, recordPath string, @@ -133,16 +137,13 @@ func NewAgent( segmentDuration time.Duration, pathName string, stream *stream.Stream, - onSegmentComplete func(string), + onSegmentCreate OnSegmentFunc, + onSegmentComplete OnSegmentFunc, parent logger.Writer, ) *Agent { recordPath = strings.ReplaceAll(recordPath, "%path", pathName) recordPath += ".mp4" - if onSegmentComplete == nil { - onSegmentComplete = func(_ string) {} - } - ctx, ctxCancel := context.WithCancel(context.Background()) r := &Agent{ @@ -150,6 +151,7 @@ func NewAgent( partDuration: partDuration, segmentDuration: segmentDuration, stream: stream, + onSegmentCreate: onSegmentCreate, onSegmentComplete: onSegmentComplete, parent: parent, ctx: ctx, diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go index 8ae98800..92e29aec 100644 --- a/internal/record/agent_test.go +++ b/internal/record/agent_test.go @@ -77,6 +77,7 @@ func TestAgent(t *testing.T) { recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f") + segCreated := make(chan struct{}, 2) segDone := make(chan struct{}, 2) a := NewAgent( @@ -86,6 +87,9 @@ func TestAgent(t *testing.T) { 1*time.Second, "mypath", stream, + func(fpath string) { + segCreated <- struct{}{} + }, func(fpath string) { segDone <- struct{}{} }, @@ -145,8 +149,11 @@ func TestAgent(t *testing.T) { }) } - <-segDone - <-segDone + for i := 0; i < 2; i++ { + <-segCreated + <-segDone + } + a.Close() _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000125.mp4")) diff --git a/internal/record/part.go b/internal/record/part.go index 32b7e9fd..88131af1 100644 --- a/internal/record/part.go +++ b/internal/record/part.go @@ -60,7 +60,7 @@ func newPart( func (p *part) close() error { if p.s.f == nil { p.s.fpath = encodeRecordPath(&recordPathParams{time: timeNow()}, p.s.r.path) - p.s.r.Log(logger.Debug, "opening segment %s", p.s.fpath) + p.s.r.Log(logger.Debug, "creating segment %s", p.s.fpath) err := os.MkdirAll(filepath.Dir(p.s.fpath), 0o755) if err != nil { @@ -72,6 +72,8 @@ func (p *part) close() error { return err } + p.s.r.onSegmentCreate(p.s.fpath) + err = writeInit(f, p.s.r.tracks) if err != nil { f.Close() diff --git a/mediamtx.yml b/mediamtx.yml index a7b55271..55f043d2 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -518,6 +518,15 @@ pathDefaults: # Environment variables are the same of runOnRead. runOnUnread: + # Command to run when a recording segment is created. + # The following environment variables are available: + # * MTX_PATH: path name + # * RTSP_PORT: RTSP server port + # * G1, G2, ...: regular expression groups, if path name is + # a regular expression. + # * MTX_SEGMENT_PATH: segment file path + runOnRecordSegmentCreate: + # Command to run when a recording segment is complete. # The following environment variables are available: # * MTX_PATH: path name @@ -531,7 +540,8 @@ pathDefaults: # Path settings # Settings in "paths" are applied to specific paths, and the map key -# is the name of the path. Any setting in "pathDefaults" can be overridden. +# is the name of the path. +# Any setting in "pathDefaults" can be overridden here. # 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 "~^prefix" will match all paths that start with "prefix".