diff --git a/README.md b/README.md index a3318957..35bbdcab 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Features: * [Encrypt the configuration](#encrypt-the-configuration) * [Proxy mode](#proxy-mode) * [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression) - * [Save published videos to disk](#save-published-videos-to-disk) + * [Save streams to disk](#save-streams-to-disk) * [On-demand publishing](#on-demand-publishing) * [Start on boot with systemd](#start-on-boot-with-systemd) * [HTTP API](#http-api) @@ -304,20 +304,20 @@ To change the format, codec or compression of a stream, use _FFmpeg_ or _Gstream paths: all: original: - runOnPublish: ffmpeg -i rtsp://localhost:$RTSP_PORT/$RTSP_PATH -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:$RTSP_PORT/compressed - runOnPublishRestart: yes + runOnReady: ffmpeg -i rtsp://localhost:$RTSP_PORT/$RTSP_PATH -c:v libx264 -preset ultrafast -b:v 500k -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:$RTSP_PORT/compressed + runOnReadyRestart: yes ``` -### Save published videos to disk +### Save streams to disk -To Save published videos to disk, put an _FFmpeg_ command inside `runOnPublish`: +To save available streams to disk, you can use the `runOnReady` parameter and _FFmpeg_: ```yml paths: all: original: - runOnPublish: ffmpeg -i rtsp://localhost:$RTSP_PORT/$RTSP_PATH -c copy -f segment -strftime 1 -segment_time 60 -segment_format mp4 saved_%Y-%m-%d_%H-%M-%S.mp4 - runOnPublishRestart: yes + runOnReady: ffmpeg -i rtsp://localhost:$RTSP_PORT/$RTSP_PATH -c copy -f segment -strftime 1 -segment_time 60 -segment_format mp4 saved_%Y-%m-%d_%H-%M-%S.mp4 + runOnReadyRestart: yes ``` ### On-demand publishing diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 463f2b0e..e5b3adbd 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -165,9 +165,9 @@ components: type: string runOnDemandCloseAfter: type: string - runOnPublish: + runOnReady: type: string - runOnPublishRestart: + runOnReadyRestart: type: boolean runOnRead: type: string diff --git a/internal/conf/path.go b/internal/conf/path.go index 60e1bca6..cf5c002c 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -65,8 +65,10 @@ type PathConf struct { RunOnDemandRestart bool `json:"runOnDemandRestart"` RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"` RunOnDemandCloseAfter StringDuration `json:"runOnDemandCloseAfter"` - RunOnPublish string `json:"runOnPublish"` - RunOnPublishRestart bool `json:"runOnPublishRestart"` + RunOnReady string `json:"runOnReady"` + RunOnReadyRestart bool `json:"runOnReadyRestart"` + RunOnPublish string `json:"runOnPublish"` // deprecated, replaced by runOnReady + RunOnPublishRestart bool `json:"runOnPublishRestart"` // deprecated, replaced by runOnReadyRestart RunOnRead string `json:"runOnRead"` RunOnReadRestart bool `json:"runOnReadRestart"` } @@ -237,15 +239,18 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { return fmt.Errorf("a path with a regular expression does not support option 'runOnInit'; use another path") } - if pconf.RunOnPublish != "" && pconf.Source != "publisher" { - return fmt.Errorf("'runOnPublish' is useless when source is not 'publisher', since " + - "the stream is not provided by a publisher, but by a fixed source") - } - if pconf.RunOnDemand != "" && pconf.Source != "publisher" { return fmt.Errorf("'runOnDemand' can be used only when source is 'publisher'") } + if pconf.RunOnPublish != "" { + pconf.RunOnReady = pconf.RunOnPublish + } + + if pconf.RunOnPublishRestart { + pconf.RunOnReadyRestart = true + } + if pconf.RunOnDemandStartTimeout == 0 { pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) } diff --git a/internal/core/api.go b/internal/core/api.go index e1c4ce2b..a0c8ccda 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -125,8 +125,10 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) { RunOnDemandRestart *bool `json:"runOnDemandRestart"` RunOnDemandStartTimeout *conf.StringDuration `json:"runOnDemandStartTimeout"` RunOnDemandCloseAfter *conf.StringDuration `json:"runOnDemandCloseAfter"` - RunOnPublish *string `json:"runOnPublish"` - RunOnPublishRestart *bool `json:"runOnPublishRestart"` + RunOnReady *string `json:"runOnReady"` + RunOnReadyRestart *bool `json:"runOnReadyRestart"` + RunOnPublish *string `json:"runOnPublish"` // deprecated, replaced by runOnReady + RunOnPublishRestart *bool `json:"runOnPublishRestart"` // deprecated, replaced by runOnReadyRestart RunOnRead *string `json:"runOnRead"` RunOnReadRestart *bool `json:"runOnReadRestart"` } diff --git a/internal/core/core_test.go b/internal/core/core_test.go index fac07a21..8f3b6d83 100644 --- a/internal/core/core_test.go +++ b/internal/core/core_test.go @@ -313,6 +313,36 @@ func main() { } } +func TestCorePathRunOnReady(t *testing.T) { + doneFile := filepath.Join(os.TempDir(), "onready_done") + defer os.Remove(doneFile) + + p, ok := newInstance(fmt.Sprintf("rtmpDisable: yes\n"+ + "hlsDisable: yes\n"+ + "paths:\n"+ + " test:\n"+ + " runOnReady: touch %s\n", + doneFile)) + require.Equal(t, true, ok) + defer p.close() + track, err := gortsplib.NewTrackH264(96, + &gortsplib.TrackConfigH264{SPS: []byte{0x01, 0x02, 0x03, 0x04}, PPS: []byte{0x01, 0x02, 0x03, 0x04}}) + require.NoError(t, err) + + c := gortsplib.Client{} + + err = c.StartPublishing( + "rtsp://localhost:8554/test", + gortsplib.Tracks{track}) + require.NoError(t, err) + defer c.Close() + + time.Sleep(1 * time.Second) + + _, err = os.Stat(doneFile) + require.NoError(t, err) +} + func TestCoreHotReloading(t *testing.T) { confPath := filepath.Join(os.TempDir(), "rtsp-conf") diff --git a/internal/core/path.go b/internal/core/path.go index 3715a62d..66b133bf 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -235,7 +235,7 @@ type path struct { setupPlayRequests []pathReaderSetupPlayReq stream *stream onDemandCmd *externalcmd.Cmd - onPublishCmd *externalcmd.Cmd + onReadyCmd *externalcmd.Cmd onDemandReadyTimer *time.Timer onDemandCloseTimer *time.Timer onDemandState pathOnDemandState @@ -622,6 +622,18 @@ func (pa *path) sourceSetReady(tracks gortsplib.Tracks) { } pa.parent.onPathSourceReady(pa) + + if pa.conf.RunOnReady != "" { + pa.log(logger.Info, "runOnReady command started") + pa.onReadyCmd = externalcmd.NewCmd( + pa.externalCmdPool, + pa.conf.RunOnReady, + pa.conf.RunOnReadyRestart, + pa.externalCmdEnv(), + func(co int) { + pa.log(logger.Info, "runOnReady command exited with code %d", co) + }) + } } func (pa *path) sourceSetNotReady() { @@ -630,9 +642,9 @@ func (pa *path) sourceSetNotReady() { r.close() } - if pa.onPublishCmd != nil { - pa.onPublishCmd.Close() - pa.onPublishCmd = nil + if pa.onReadyCmd != nil { + pa.onReadyCmd.Close() + pa.onReadyCmd = nil pa.log(logger.Info, "runOnReady command stopped") } @@ -696,11 +708,6 @@ func (pa *path) doPublisherRemove() { } else { pa.sourceSetNotReady() } - } else { - for r := range pa.readers { - pa.doReaderRemove(r) - r.close() - } } pa.source = nil @@ -788,18 +795,6 @@ func (pa *path) handlePublisherRecord(req pathPublisherRecordReq) { pa.sourceSetReady(req.tracks) - if pa.conf.RunOnPublish != "" { - pa.log(logger.Info, "runOnPublish command started") - pa.onPublishCmd = externalcmd.NewCmd( - pa.externalCmdPool, - pa.conf.RunOnPublish, - pa.conf.RunOnPublishRestart, - pa.externalCmdEnv(), - func(co int) { - pa.log(logger.Info, "runOnPublish command exited with code %d", co) - }) - } - req.res <- pathPublisherRecordRes{stream: pa.stream} } diff --git a/rtsp-simple-server.yml b/rtsp-simple-server.yml index 0a432051..71e309a1 100644 --- a/rtsp-simple-server.yml +++ b/rtsp-simple-server.yml @@ -234,16 +234,17 @@ paths: # readers connected and this amount of time has passed. runOnDemandCloseAfter: 10s - # Command to run when a client starts publishing. - # This is terminated with SIGINT when a client stops publishing. + # Command to run when the stream is ready to be read, whether it is + # published by a client or read by a server / camera. + # This is terminated with SIGINT when the stream is not ready anymore. # The following environment variables are available: # * RTSP_PATH: path name # * RTSP_PORT: server port # * 1, 2, ...: regular expression groups, if path name is # a regular expression. - runOnPublish: + runOnReady: # Restart the command if it exits suddenly. - runOnPublishRestart: no + runOnReadyRestart: no # Command to run when a clients starts reading. # This is terminated with SIGINT when a client stops reading.