mirror of
				https://github.com/aler9/rtsp-simple-server
				synced 2025-10-31 11:06:28 +08:00 
			
		
		
		
	Add new parameter 'runOnReady' (#752)
This is called when a stream is ready, whether it is published or proxied. It replaces 'runOnPublsh'.
This commit is contained in:
		
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @@ -46,7 +46,7 @@ Features: | |||||||
|   * [Encrypt the configuration](#encrypt-the-configuration) |   * [Encrypt the configuration](#encrypt-the-configuration) | ||||||
|   * [Proxy mode](#proxy-mode) |   * [Proxy mode](#proxy-mode) | ||||||
|   * [Remuxing, re-encoding, compression](#remuxing-re-encoding-compression) |   * [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) |   * [On-demand publishing](#on-demand-publishing) | ||||||
|   * [Start on boot with systemd](#start-on-boot-with-systemd) |   * [Start on boot with systemd](#start-on-boot-with-systemd) | ||||||
|   * [HTTP API](#http-api) |   * [HTTP API](#http-api) | ||||||
| @@ -304,20 +304,20 @@ To change the format, codec or compression of a stream, use _FFmpeg_ or _Gstream | |||||||
| paths: | paths: | ||||||
|   all: |   all: | ||||||
|   original: |   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 |     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 | ||||||
|     runOnPublishRestart: yes |     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 | ```yml | ||||||
| paths: | paths: | ||||||
|   all: |   all: | ||||||
|   original: |   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 |     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 | ||||||
|     runOnPublishRestart: yes |     runOnReadyRestart: yes | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### On-demand publishing | ### On-demand publishing | ||||||
|   | |||||||
| @@ -165,9 +165,9 @@ components: | |||||||
|           type: string |           type: string | ||||||
|         runOnDemandCloseAfter: |         runOnDemandCloseAfter: | ||||||
|           type: string |           type: string | ||||||
|         runOnPublish: |         runOnReady: | ||||||
|           type: string |           type: string | ||||||
|         runOnPublishRestart: |         runOnReadyRestart: | ||||||
|           type: boolean |           type: boolean | ||||||
|         runOnRead: |         runOnRead: | ||||||
|           type: string |           type: string | ||||||
|   | |||||||
| @@ -65,8 +65,10 @@ type PathConf struct { | |||||||
| 	RunOnDemandRestart      bool           `json:"runOnDemandRestart"` | 	RunOnDemandRestart      bool           `json:"runOnDemandRestart"` | ||||||
| 	RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"` | 	RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"` | ||||||
| 	RunOnDemandCloseAfter   StringDuration `json:"runOnDemandCloseAfter"` | 	RunOnDemandCloseAfter   StringDuration `json:"runOnDemandCloseAfter"` | ||||||
| 	RunOnPublish            string         `json:"runOnPublish"` | 	RunOnReady              string         `json:"runOnReady"` | ||||||
| 	RunOnPublishRestart     bool           `json:"runOnPublishRestart"` | 	RunOnReadyRestart       bool           `json:"runOnReadyRestart"` | ||||||
|  | 	RunOnPublish            string         `json:"runOnPublish"`        // deprecated, replaced by runOnReady | ||||||
|  | 	RunOnPublishRestart     bool           `json:"runOnPublishRestart"` // deprecated, replaced by runOnReadyRestart | ||||||
| 	RunOnRead               string         `json:"runOnRead"` | 	RunOnRead               string         `json:"runOnRead"` | ||||||
| 	RunOnReadRestart        bool           `json:"runOnReadRestart"` | 	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") | 		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" { | 	if pconf.RunOnDemand != "" && pconf.Source != "publisher" { | ||||||
| 		return fmt.Errorf("'runOnDemand' can be used only when source is '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 { | 	if pconf.RunOnDemandStartTimeout == 0 { | ||||||
| 		pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) | 		pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -125,8 +125,10 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) { | |||||||
| 		RunOnDemandRestart      *bool                `json:"runOnDemandRestart"` | 		RunOnDemandRestart      *bool                `json:"runOnDemandRestart"` | ||||||
| 		RunOnDemandStartTimeout *conf.StringDuration `json:"runOnDemandStartTimeout"` | 		RunOnDemandStartTimeout *conf.StringDuration `json:"runOnDemandStartTimeout"` | ||||||
| 		RunOnDemandCloseAfter   *conf.StringDuration `json:"runOnDemandCloseAfter"` | 		RunOnDemandCloseAfter   *conf.StringDuration `json:"runOnDemandCloseAfter"` | ||||||
| 		RunOnPublish            *string              `json:"runOnPublish"` | 		RunOnReady              *string              `json:"runOnReady"` | ||||||
| 		RunOnPublishRestart     *bool                `json:"runOnPublishRestart"` | 		RunOnReadyRestart       *bool                `json:"runOnReadyRestart"` | ||||||
|  | 		RunOnPublish            *string              `json:"runOnPublish"`        // deprecated, replaced by runOnReady | ||||||
|  | 		RunOnPublishRestart     *bool                `json:"runOnPublishRestart"` // deprecated, replaced by runOnReadyRestart | ||||||
| 		RunOnRead               *string              `json:"runOnRead"` | 		RunOnRead               *string              `json:"runOnRead"` | ||||||
| 		RunOnReadRestart        *bool                `json:"runOnReadRestart"` | 		RunOnReadRestart        *bool                `json:"runOnReadRestart"` | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -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) { | func TestCoreHotReloading(t *testing.T) { | ||||||
| 	confPath := filepath.Join(os.TempDir(), "rtsp-conf") | 	confPath := filepath.Join(os.TempDir(), "rtsp-conf") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -235,7 +235,7 @@ type path struct { | |||||||
| 	setupPlayRequests  []pathReaderSetupPlayReq | 	setupPlayRequests  []pathReaderSetupPlayReq | ||||||
| 	stream             *stream | 	stream             *stream | ||||||
| 	onDemandCmd        *externalcmd.Cmd | 	onDemandCmd        *externalcmd.Cmd | ||||||
| 	onPublishCmd       *externalcmd.Cmd | 	onReadyCmd         *externalcmd.Cmd | ||||||
| 	onDemandReadyTimer *time.Timer | 	onDemandReadyTimer *time.Timer | ||||||
| 	onDemandCloseTimer *time.Timer | 	onDemandCloseTimer *time.Timer | ||||||
| 	onDemandState      pathOnDemandState | 	onDemandState      pathOnDemandState | ||||||
| @@ -622,6 +622,18 @@ func (pa *path) sourceSetReady(tracks gortsplib.Tracks) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pa.parent.onPathSourceReady(pa) | 	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() { | func (pa *path) sourceSetNotReady() { | ||||||
| @@ -630,9 +642,9 @@ func (pa *path) sourceSetNotReady() { | |||||||
| 		r.close() | 		r.close() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if pa.onPublishCmd != nil { | 	if pa.onReadyCmd != nil { | ||||||
| 		pa.onPublishCmd.Close() | 		pa.onReadyCmd.Close() | ||||||
| 		pa.onPublishCmd = nil | 		pa.onReadyCmd = nil | ||||||
| 		pa.log(logger.Info, "runOnReady command stopped") | 		pa.log(logger.Info, "runOnReady command stopped") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -696,11 +708,6 @@ func (pa *path) doPublisherRemove() { | |||||||
| 		} else { | 		} else { | ||||||
| 			pa.sourceSetNotReady() | 			pa.sourceSetNotReady() | ||||||
| 		} | 		} | ||||||
| 	} else { |  | ||||||
| 		for r := range pa.readers { |  | ||||||
| 			pa.doReaderRemove(r) |  | ||||||
| 			r.close() |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	pa.source = nil | 	pa.source = nil | ||||||
| @@ -788,18 +795,6 @@ func (pa *path) handlePublisherRecord(req pathPublisherRecordReq) { | |||||||
|  |  | ||||||
| 	pa.sourceSetReady(req.tracks) | 	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} | 	req.res <- pathPublisherRecordRes{stream: pa.stream} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -234,16 +234,17 @@ paths: | |||||||
|     # readers connected and this amount of time has passed. |     # readers connected and this amount of time has passed. | ||||||
|     runOnDemandCloseAfter: 10s |     runOnDemandCloseAfter: 10s | ||||||
|  |  | ||||||
|     # Command to run when a client starts publishing. |     # Command to run when the stream is ready to be read, whether it is | ||||||
|     # This is terminated with SIGINT when a client stops publishing. |     # 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: |     # The following environment variables are available: | ||||||
|     # * RTSP_PATH: path name |     # * RTSP_PATH: path name | ||||||
|     # * RTSP_PORT: server port |     # * RTSP_PORT: server port | ||||||
|     # * 1, 2, ...: regular expression groups, if path name is |     # * 1, 2, ...: regular expression groups, if path name is | ||||||
|     #   a regular expression. |     #   a regular expression. | ||||||
|     runOnPublish: |     runOnReady: | ||||||
|     # Restart the command if it exits suddenly. |     # Restart the command if it exits suddenly. | ||||||
|     runOnPublishRestart: no |     runOnReadyRestart: 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. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 aler9
					aler9