diff --git a/README.md b/README.md index 8b17419b..9bbd794d 100644 --- a/README.md +++ b/README.md @@ -1546,7 +1546,7 @@ In some scenarios, when publishing or reading from the server with RTSP, frames paths: test: source: rtsp://.. - sourceProtocol: tcp + rtspTransport: tcp ``` * The stream throughput is too big to be handled by the network between server and readers. Upgrade the network or decrease the stream bitrate by re-encoding it. diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index c03442ce..eb8e0aa4 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -252,9 +252,9 @@ components: type: string # RTSP source - sourceProtocol: + rtspTransport: type: string - sourceAnyPortEnable: + rtspAnyPort: type: boolean rtspRangeType: type: string diff --git a/bench/proxy/start.sh b/bench/proxy/start.sh index 4d2f0fb5..2afe40ee 100644 --- a/bench/proxy/start.sh +++ b/bench/proxy/start.sh @@ -40,7 +40,7 @@ CONF="${CONF}paths:\n" for i in $(seq 1 $PROXY_COUNT); do CONF="${CONF} proxy$i:\n" CONF="${CONF} source: rtsp://localhost:8555/source\n" - CONF="${CONF} sourceProtocol: $PROXY_PROTOCOL\n" + CONF="${CONF} rtspTransport: $PROXY_PROTOCOL\n" done echo -e "$CONF" > /proxy.conf diff --git a/internal/conf/path.go b/internal/conf/path.go index e3f16d8b..715bfa41 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -84,8 +84,10 @@ type Path struct { SRTPublishPassphrase string `json:"srtPublishPassphrase"` // RTSP source - SourceProtocol SourceProtocol `json:"sourceProtocol"` - SourceAnyPortEnable bool `json:"sourceAnyPortEnable"` + RTSPTransport RTSPTransport `json:"rtspTransport"` + RTSPAnyPort bool `json:"rtspAnyPort"` + SourceProtocol *RTSPTransport `json:"sourceProtocol,omitempty"` // deprecated + SourceAnyPortEnable *bool `json:"sourceAnyPortEnable,omitempty"` // deprecated RTSPRangeType RTSPRangeType `json:"rtspRangeType"` RTSPRangeStart string `json:"rtspRangeStart"` @@ -322,14 +324,6 @@ func (pconf *Path) check(conf *Conf, name string) error { } case pconf.Source == "redirect": - if pconf.SourceRedirect == "" { - return fmt.Errorf("source redirect must be filled") - } - - _, err := url.Parse(pconf.SourceRedirect) - if err != nil { - return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect) - } case pconf.Source == "rpiCamera": if pconf.Regexp != nil { @@ -337,60 +331,9 @@ func (pconf *Path) check(conf *Conf, name string) error { "a path with a regular expression (or path 'all') cannot have 'rpiCamera' as source. use another path") } - for otherName, otherPath := range conf.Paths { - if otherPath != pconf && otherPath != nil && - otherPath.Source == "rpiCamera" && otherPath.RPICameraCamID == pconf.RPICameraCamID { - return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'", - pconf.RPICameraCamID, name, otherName) - } - } - - switch pconf.RPICameraExposure { - case "normal", "short", "long", "custom": - default: - return fmt.Errorf("invalid 'rpiCameraExposure' value") - } - - switch pconf.RPICameraAWB { - case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom": - default: - return fmt.Errorf("invalid 'rpiCameraAWB' value") - } - - switch pconf.RPICameraDenoise { - case "off", "cdn_off", "cdn_fast", "cdn_hq": - default: - return fmt.Errorf("invalid 'rpiCameraDenoise' value") - } - - switch pconf.RPICameraMetering { - case "centre", "spot", "matrix", "custom": - default: - return fmt.Errorf("invalid 'rpiCameraMetering' value") - } - - switch pconf.RPICameraAfMode { - case "auto", "manual", "continuous": - default: - return fmt.Errorf("invalid 'rpiCameraAfMode' value") - } - - switch pconf.RPICameraAfRange { - case "normal", "macro", "full": - default: - return fmt.Errorf("invalid 'rpiCameraAfRange' value") - } - - switch pconf.RPICameraAfSpeed { - case "normal", "fast": - default: - return fmt.Errorf("invalid 'rpiCameraAfSpeed' value") - } - default: return fmt.Errorf("invalid source: '%s'", pconf.Source) } - if pconf.SourceOnDemand { if pconf.Source == "publisher" { return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'") @@ -403,39 +346,6 @@ func (pconf *Path) check(conf *Conf, name string) error { } } - // Publisher - - if pconf.DisablePublisherOverride != nil { - pconf.OverridePublisher = !*pconf.DisablePublisherOverride - } - if pconf.Fallback != "" { - if pconf.Source != "publisher" { - return fmt.Errorf("'fallback' can only be used when source is 'publisher'") - } - - if strings.HasPrefix(pconf.Fallback, "/") { - err := IsValidPathName(pconf.Fallback[1:]) - if err != nil { - return fmt.Errorf("'%s': %s", pconf.Fallback, err) - } - } else { - _, err := url.Parse(pconf.Fallback) - if err != nil { - return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Fallback) - } - } - } - if pconf.SRTPublishPassphrase != "" { - if pconf.Source != "publisher" { - return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'") - } - - err := srtCheckPassphrase(pconf.SRTPublishPassphrase) - if err != nil { - return fmt.Errorf("invalid 'srtPublishPassphrase': %v", err) - } - } - // Authentication if (pconf.PublishUser != "" && pconf.PublishPass == "") || @@ -471,6 +381,108 @@ func (pconf *Path) check(conf *Conf, name string) error { } } + // Publisher source + + if pconf.DisablePublisherOverride != nil { + pconf.OverridePublisher = !*pconf.DisablePublisherOverride + } + if pconf.Fallback != "" { + if pconf.Source != "publisher" { + return fmt.Errorf("'fallback' can only be used when source is 'publisher'") + } + + if strings.HasPrefix(pconf.Fallback, "/") { + err := IsValidPathName(pconf.Fallback[1:]) + if err != nil { + return fmt.Errorf("'%s': %s", pconf.Fallback, err) + } + } else { + _, err := url.Parse(pconf.Fallback) + if err != nil { + return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.Fallback) + } + } + } + if pconf.SRTPublishPassphrase != "" { + if pconf.Source != "publisher" { + return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'") + } + + err := srtCheckPassphrase(pconf.SRTPublishPassphrase) + if err != nil { + return fmt.Errorf("invalid 'srtPublishPassphrase': %v", err) + } + } + + // RTSP source + + if pconf.SourceProtocol != nil { + pconf.RTSPTransport = *pconf.SourceProtocol + } + if pconf.SourceAnyPortEnable != nil { + pconf.RTSPAnyPort = *pconf.SourceAnyPortEnable + } + + // Redirect source + + if pconf.Source == "redirect" { + if pconf.SourceRedirect == "" { + return fmt.Errorf("source redirect must be filled") + } + + _, err := url.Parse(pconf.SourceRedirect) + if err != nil { + return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect) + } + } + + // Raspberry Pi Camera source + + if pconf.Source == "rpiCamera" { + for otherName, otherPath := range conf.Paths { + if otherPath != pconf && otherPath != nil && + otherPath.Source == "rpiCamera" && otherPath.RPICameraCamID == pconf.RPICameraCamID { + return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'", + pconf.RPICameraCamID, name, otherName) + } + } + } + switch pconf.RPICameraExposure { + case "normal", "short", "long", "custom": + default: + return fmt.Errorf("invalid 'rpiCameraExposure' value") + } + switch pconf.RPICameraAWB { + case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom": + default: + return fmt.Errorf("invalid 'rpiCameraAWB' value") + } + switch pconf.RPICameraDenoise { + case "off", "cdn_off", "cdn_fast", "cdn_hq": + default: + return fmt.Errorf("invalid 'rpiCameraDenoise' value") + } + switch pconf.RPICameraMetering { + case "centre", "spot", "matrix", "custom": + default: + return fmt.Errorf("invalid 'rpiCameraMetering' value") + } + switch pconf.RPICameraAfMode { + case "auto", "manual", "continuous": + default: + return fmt.Errorf("invalid 'rpiCameraAfMode' value") + } + switch pconf.RPICameraAfRange { + case "normal", "macro", "full": + default: + return fmt.Errorf("invalid 'rpiCameraAfRange' value") + } + switch pconf.RPICameraAfSpeed { + case "normal", "fast": + default: + return fmt.Errorf("invalid 'rpiCameraAfSpeed' value") + } + // Hooks if pconf.RunOnInit != "" && pconf.Regexp != nil { diff --git a/internal/conf/source_protocol.go b/internal/conf/rtsp_transport.go similarity index 80% rename from internal/conf/source_protocol.go rename to internal/conf/rtsp_transport.go index 4204c5ca..cb49a11c 100644 --- a/internal/conf/source_protocol.go +++ b/internal/conf/rtsp_transport.go @@ -7,13 +7,13 @@ import ( "github.com/bluenviron/gortsplib/v4" ) -// SourceProtocol is the sourceProtocol parameter. -type SourceProtocol struct { +// RTSPTransport is the rtspTransport parameter. +type RTSPTransport struct { *gortsplib.Transport } // MarshalJSON implements json.Marshaler. -func (d SourceProtocol) MarshalJSON() ([]byte, error) { +func (d RTSPTransport) MarshalJSON() ([]byte, error) { var out string if d.Transport == nil { @@ -38,7 +38,7 @@ func (d SourceProtocol) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements json.Unmarshaler. -func (d *SourceProtocol) UnmarshalJSON(b []byte) error { +func (d *RTSPTransport) UnmarshalJSON(b []byte) error { var in string if err := json.Unmarshal(b, &in); err != nil { return err @@ -68,6 +68,6 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error { } // UnmarshalEnv implements env.Unmarshaler. -func (d *SourceProtocol) UnmarshalEnv(_ string, v string) error { +func (d *RTSPTransport) UnmarshalEnv(_ string, v string) error { return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index dc661e65..4c7debd8 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -81,12 +81,12 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { decodeErrLogger := logger.NewLimitedLogger(s) c := &gortsplib.Client{ - Transport: params.Conf.SourceProtocol.Transport, + Transport: params.Conf.RTSPTransport.Transport, TLSConfig: tls.ConfigForFingerprint(params.Conf.SourceFingerprint), ReadTimeout: time.Duration(s.ReadTimeout), WriteTimeout: time.Duration(s.WriteTimeout), WriteQueueSize: s.WriteQueueSize, - AnyPortEnable: params.Conf.SourceAnyPortEnable, + AnyPortEnable: params.Conf.RTSPAnyPort, OnRequest: func(req *base.Request) { s.Log(logger.Debug, "[c->s] %v", req) }, diff --git a/internal/staticsources/rtsp/source_test.go b/internal/staticsources/rtsp/source_test.go index d94f88dc..31acc2ea 100644 --- a/internal/staticsources/rtsp/source_test.go +++ b/internal/staticsources/rtsp/source_test.go @@ -210,7 +210,7 @@ func TestRTSPSource(t *testing.T) { var te *tester.Tester if source != "tls" { - var sp conf.SourceProtocol + var sp conf.RTSPTransport sp.UnmarshalJSON([]byte(`"` + source + `"`)) //nolint:errcheck te = tester.New( @@ -223,8 +223,8 @@ func TestRTSPSource(t *testing.T) { } }, &conf.Path{ - Source: "rtsp://testuser:testpass@localhost:8555/teststream", - SourceProtocol: sp, + Source: "rtsp://testuser:testpass@localhost:8555/teststream", + RTSPTransport: sp, }, ) } else { @@ -312,7 +312,7 @@ func TestRTSPSourceNoPassword(t *testing.T) { stream = gortsplib.NewServerStream(&s, &description.Session{Medias: []*description.Media{testMediaH264}}) defer stream.Close() - var sp conf.SourceProtocol + var sp conf.RTSPTransport sp.UnmarshalJSON([]byte(`"tcp"`)) //nolint:errcheck te := tester.New( @@ -325,8 +325,8 @@ func TestRTSPSourceNoPassword(t *testing.T) { } }, &conf.Path{ - Source: "rtsp://testuser:@127.0.0.1:8555/teststream", - SourceProtocol: sp, + Source: "rtsp://testuser:@127.0.0.1:8555/teststream", + RTSPTransport: sp, }, ) defer te.Close() diff --git a/mediamtx.yml b/mediamtx.yml index e5beee19..1e9f5fbf 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -337,9 +337,9 @@ pathDefaults: ############################################### # Default path settings -> Publisher source (when source is "publisher") - # allow another client to disconnect the current publisher and publish in its place. + # Allow another client to disconnect the current publisher and publish in its place. overridePublisher: yes - # if no one is publishing, redirect readers to this path. + # If no one is publishing, redirect readers to this path. # It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL. fallback: # SRT encryption passphrase required to publish to this path @@ -348,18 +348,18 @@ pathDefaults: ############################################### # Default path settings -> RTSP source (when source is a RTSP or a RTSPS URL) - # protocol used to pull the stream. available values are "automatic", "udp", "multicast", "tcp". - sourceProtocol: automatic - # support sources that don't provide server ports or use random server ports. This is a security issue + # Transport protocol used to pull the stream. available values are "automatic", "udp", "multicast", "tcp". + rtspTransport: automatic + # Support sources that don't provide server ports or use random server ports. This is a security issue # and must be used only when interacting with sources that require it. - sourceAnyPortEnable: no - # range header to send to the source, in order to start streaming from the specified offset. + rtspAnyPort: no + # Range header to send to the source, in order to start streaming from the specified offset. # available values: # * clock: Absolute time # * npt: Normal Play Time # * smpte: SMPTE timestamps relative to the start of the recording rtspRangeType: - # available values: + # Available values: # * clock: UTC ISO 8601 combined date and time string, e.g. 20230812T120000Z # * npt: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" # * smpte: duration such as "300ms", "1.5m" or "2h45m", valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" diff --git a/scripts/run.mk b/scripts/run.mk index 87609114..3f13d7f8 100644 --- a/scripts/run.mk +++ b/scripts/run.mk @@ -28,7 +28,7 @@ paths: # proxied: # source: rtsp://192.168.2.198:554/stream -# sourceProtocol: tcp +# rtspTransport: tcp # sourceOnDemand: yes # runOnDemand: ffmpeg -i rtsp://192.168.2.198:554/stream -c copy -f rtsp rtsp://localhost:$$RTSP_PORT/proxied2