From 9a01ab7fd57a766ea87898e1a9fdf51449334ddb Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 7 Oct 2023 23:32:15 +0200 Subject: [PATCH] allow changing default path settings; bump API in order to allow so (#2455) --- README.md | 2 +- apidocs/openapi.yaml | 284 ++++++++++++---- internal/conf/auth_method.go | 6 +- internal/conf/conf.go | 406 +++++++++++++--------- internal/conf/conf_test.go | 82 ++--- internal/conf/credential.go | 6 +- internal/conf/encryption.go | 6 +- internal/conf/env/env.go | 88 +++-- internal/conf/env/env_test.go | 38 +-- internal/conf/global.go | 41 +++ internal/conf/hls_variant.go | 6 +- internal/conf/ips_or_cidrs.go | 6 +- internal/conf/log_destination.go | 6 +- internal/conf/log_level.go | 6 +- internal/conf/optional_global.go | 60 ++++ internal/conf/optional_path.go | 70 ++++ internal/conf/path.go | 148 ++++---- internal/conf/protocol.go | 6 +- internal/conf/rtsp_range_type.go | 6 +- internal/conf/source_protocol.go | 6 +- internal/conf/string_duration.go | 6 +- internal/conf/string_size.go | 4 +- internal/core/api.go | 335 ++++++++++-------- internal/core/api_defs.go | 20 +- internal/core/api_test.go | 269 +++++++++++---- internal/core/authentication.go | 2 +- internal/core/hls_manager.go | 6 +- internal/core/hls_source.go | 2 +- internal/core/metrics.go | 2 +- internal/core/path.go | 22 +- internal/core/path_manager.go | 36 +- internal/core/path_test.go | 4 +- internal/core/rpicamera_source.go | 4 +- internal/core/rtmp_server.go | 6 +- internal/core/rtmp_source.go | 2 +- internal/core/rtsp_server.go | 4 +- internal/core/rtsp_source.go | 4 +- internal/core/source_static.go | 14 +- internal/core/srt_server.go | 6 +- internal/core/srt_source.go | 2 +- internal/core/udp_source.go | 2 +- internal/core/webrtc_manager.go | 6 +- internal/core/webrtc_source.go | 2 +- mediamtx.yml | 547 ++++++++++++++++-------------- 44 files changed, 1591 insertions(+), 995 deletions(-) create mode 100644 internal/conf/global.go create mode 100644 internal/conf/optional_global.go create mode 100644 internal/conf/optional_path.go diff --git a/README.md b/README.md index 7785c2dc..f7140d52 100644 --- a/README.md +++ b/README.md @@ -642,7 +642,7 @@ paths: The resulting stream will be available in path `/proxied`. -The server supports any number of source streams (count is just limited by hardware capability) it's enough to add additional entries to the paths section: +The server supports any number of source streams (count is just limited by available hardware resources) it's enough to add additional entries to the paths section: ```yml paths: diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 0ebfb148..8ff566b0 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -15,7 +15,7 @@ security: [] components: schemas: - Conf: + GlobalConf: type: object properties: # General @@ -195,12 +195,6 @@ components: recordDeleteAfter: type: string - # Paths - paths: - type: object - additionalProperties: - $ref: '#/components/schemas/PathConf' - PathConf: type: object properties: @@ -356,6 +350,16 @@ components: runOnRecordSegmentComplete: type: string + PathConfList: + type: object + properties: + pageCount: + type: integer + items: + type: array + items: + $ref: '#/components/schemas/PathConf' + Path: type: object properties: @@ -363,10 +367,8 @@ components: type: string confName: type: string - conf: - $ref: '#/components/schemas/PathConf' source: - $ref: '#/components/schemas/PathSourceOrReader' + $ref: '#/components/schemas/PathSource' nullable: true ready: type: boolean @@ -383,9 +385,9 @@ components: readers: type: array items: - $ref: '#/components/schemas/PathSourceOrReader' + $ref: '#/components/schemas/PathReader' - PathsList: + PathList: type: object properties: pageCount: @@ -395,13 +397,12 @@ components: items: $ref: '#/components/schemas/Path' - PathSourceOrReader: + PathSource: type: object properties: type: type: string enum: - - hlsMuxer - hlsSource - redirect - rpiCameraSource @@ -418,6 +419,21 @@ components: id: type: string + PathReader: + type: object + properties: + type: + type: string + enum: + - hlsMuxer + - rtmpConn + - rtspSession + - rtspsSession + - srtConn + - webRTCSession + id: + type: string + HLSMuxer: type: object properties: @@ -431,7 +447,7 @@ components: type: integer format: int64 - HLSMuxersList: + HLSMuxerList: type: object properties: pageCount: @@ -462,7 +478,7 @@ components: type: integer format: int64 - RTMPConnsList: + RTMPConnList: type: object properties: pageCount: @@ -488,7 +504,7 @@ components: type: integer format: int64 - RTSPConnsList: + RTSPConnList: type: object properties: pageCount: @@ -522,7 +538,7 @@ components: type: integer format: int64 - RTSPSessionsList: + RTSPSessionList: type: object properties: pageCount: @@ -553,7 +569,7 @@ components: type: integer format: int64 - SRTConnsList: + SRTConnList: type: object properties: pageCount: @@ -590,7 +606,7 @@ components: type: integer format: int64 - WebRTCSessionsList: + WebRTCSessionList: type: object properties: pageCount: @@ -601,10 +617,10 @@ components: $ref: '#/components/schemas/WebRTCSession' paths: - /v2/config/get: + /v3/config/global/get: get: - operationId: configGet - summary: returns the configuration. + operationId: configGlobalGet + summary: returns the global configuration. description: '' responses: '200': @@ -612,23 +628,59 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Conf' + $ref: '#/components/schemas/GlobalConf' '400': description: invalid request. '500': description: internal server error. - /v2/config/set: - post: - operationId: configSet - summary: changes the configuration. - description: all fields are optional. paths can't be edited with this request, use /v2/config/paths/{operation}/{name} to edit them. + /v3/config/global/patch: + patch: + operationId: configGlobalSet + summary: patches the global configuration. + description: all fields are optional. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GlobalConf' + responses: + '200': + description: the request was successful. + '400': + description: invalid request. + '500': + description: internal server error. + + /v3/config/pathdefaults/get: + get: + operationId: configPathDefaultsGet + summary: returns the default path configuration. + description: '' + responses: + '200': + description: the request was successful. + content: + application/json: + schema: + $ref: '#/components/schemas/PathConf' + '400': + description: invalid request. + '500': + description: internal server error. + + /v3/config/pathdefaults/patch: + patch: + operationId: configPathDefaultsPatch + summary: patches the default path configuration. + description: all fields are optional. requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/Conf' + $ref: '#/components/schemas/PathConf' responses: '200': description: the request was successful. @@ -637,10 +689,64 @@ paths: '500': description: internal server error. - /v2/config/paths/add/{name}: + /v3/config/paths/list: + get: + operationId: configPathsList + summary: returns all path configurations. + description: '' + parameters: + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 + responses: + '200': + description: the request was successful. + content: + application/json: + schema: + $ref: '#/components/schemas/PathConfList' + '400': + description: invalid request. + '500': + description: internal server error. + + /v3/config/paths/get/{name}: + get: + operationId: configPathsGet + summary: returns a path configuration. + description: '' + parameters: + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string + responses: + '200': + description: the request was successful. + content: + application/json: + schema: + $ref: '#/components/schemas/PathConf' + '400': + description: invalid request. + '500': + description: internal server error. + + /v3/config/paths/add/{name}: post: operationId: configPathsAdd - summary: adds the configuration of a path. + summary: adds a path configuration. description: all fields are optional. parameters: - name: name @@ -663,10 +769,10 @@ paths: '500': description: internal server error. - /v2/config/paths/edit/{name}: - post: - operationId: configPathsEdit - summary: changes the configuration of a path. + /v3/config/paths/patch/{name}: + patch: + operationId: configPathsPatch + summary: patches a path configuration. description: all fields are optional. parameters: - name: name @@ -691,10 +797,38 @@ paths: '500': description: internal server error. - /v2/config/paths/remove/{name}: + /v3/config/paths/replace/{name}: post: - operationId: configPathsRemove - summary: removes the configuration of a path. + operationId: configPathsReplace + summary: replaces all values of a path configuration. + description: all fields are optional. + parameters: + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PathConf' + responses: + '200': + description: the request was successful. + '400': + description: invalid request. + '404': + description: configuration not found. + '500': + description: internal server error. + + /v3/config/paths/delete/{name}: + delete: + operationId: configPathsDelete + summary: removes a path configuration. description: '' parameters: - name: name @@ -713,7 +847,7 @@ paths: '500': description: internal server error. - /v2/hlsmuxers/list: + /v3/hlsmuxers/list: get: operationId: hlsMuxersList summary: returns all HLS muxers. @@ -737,13 +871,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/HLSMuxersList' + $ref: '#/components/schemas/HLSMuxerList' '400': description: invalid request. '500': description: internal server error. - /v2/hlsmuxers/get/{name}: + /v3/hlsmuxers/get/{name}: get: operationId: hlsMuxersGet summary: returns a HLS muxer. @@ -769,7 +903,7 @@ paths: '500': description: internal server error. - /v2/paths/list: + /v3/paths/list: get: operationId: pathsList summary: returns all paths. @@ -793,13 +927,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PathsList' + $ref: '#/components/schemas/PathList' '400': description: invalid request. '500': description: internal server error. - /v2/paths/get/{name}: + /v3/paths/get/{name}: get: operationId: pathsGet summary: returns a path. @@ -825,7 +959,7 @@ paths: '500': description: internal server error. - /v2/rtspconns/list: + /v3/rtspconns/list: get: operationId: rtspConnsList summary: returns all RTSP connections. @@ -849,13 +983,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTSPConnsList' + $ref: '#/components/schemas/RTSPConnList' '400': description: invalid request. '500': description: internal server error. - /v2/rtspconns/get/{id}: + /v3/rtspconns/get/{id}: get: operationId: rtspConnsGet summary: returns a RTSP connection. @@ -881,7 +1015,7 @@ paths: '500': description: internal server error. - /v2/rtspsessions/list: + /v3/rtspsessions/list: get: operationId: rtspSessionsList summary: returns all RTSP sessions. @@ -905,13 +1039,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTSPSessionsList' + $ref: '#/components/schemas/RTSPSessionList' '400': description: invalid request. '500': description: internal server error. - /v2/rtspsessions/get/{id}: + /v3/rtspsessions/get/{id}: get: operationId: rtspSessionsGet summary: returns a RTSP session. @@ -937,7 +1071,7 @@ paths: '500': description: internal server error. - /v2/rtspsessions/kick/{id}: + /v3/rtspsessions/kick/{id}: post: operationId: rtspSessionsKick summary: kicks out a RTSP session from the server. @@ -959,7 +1093,7 @@ paths: '500': description: internal server error. - /v2/rtspsconns/list: + /v3/rtspsconns/list: get: operationId: rtspsConnsList summary: returns all RTSPS connections. @@ -983,13 +1117,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTSPConnsList' + $ref: '#/components/schemas/RTSPConnList' '400': description: invalid request. '500': description: internal server error. - /v2/rtspsconns/get/{id}: + /v3/rtspsconns/get/{id}: get: operationId: rtspsConnsGet summary: returns a RTSPS connection. @@ -1015,7 +1149,7 @@ paths: '500': description: internal server error. - /v2/rtspssessions/list: + /v3/rtspssessions/list: get: operationId: rtspsSessionsList summary: returns all RTSPS sessions. @@ -1039,7 +1173,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTSPSessionsList' + $ref: '#/components/schemas/RTSPSessionList' '400': description: invalid request. '404': @@ -1047,7 +1181,7 @@ paths: '500': description: internal server error. - /v2/rtspssessions/get/{id}: + /v3/rtspssessions/get/{id}: get: operationId: rtspsSessionsGet summary: returns a RTSPS session. @@ -1073,7 +1207,7 @@ paths: '500': description: internal server error. - /v2/rtspssessions/kick/{id}: + /v3/rtspssessions/kick/{id}: post: operationId: rtspsSessionsKick summary: kicks out a RTSPS session from the server. @@ -1095,7 +1229,7 @@ paths: '500': description: internal server error. - /v2/rtmpconns/list: + /v3/rtmpconns/list: get: operationId: rtmpConnsList summary: returns all RTMP connections. @@ -1119,13 +1253,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTMPConnsList' + $ref: '#/components/schemas/RTMPConnList' '400': description: invalid request. '500': description: internal server error. - /v2/rtmpconns/get/{id}: + /v3/rtmpconns/get/{id}: get: operationId: rtmpConnectionsGet summary: returns a RTMP connection. @@ -1151,7 +1285,7 @@ paths: '500': description: internal server error. - /v2/rtmpconns/kick/{id}: + /v3/rtmpconns/kick/{id}: post: operationId: rtmpConnsKick summary: kicks out a RTMP connection from the server. @@ -1173,7 +1307,7 @@ paths: '500': description: internal server error. - /v2/rtmpsconns/list: + /v3/rtmpsconns/list: get: operationId: rtmpsConnsList summary: returns all RTMPS connections. @@ -1197,13 +1331,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/RTMPConnsList' + $ref: '#/components/schemas/RTMPConnList' '400': description: invalid request. '500': description: internal server error. - /v2/rtmpsconns/get/{id}: + /v3/rtmpsconns/get/{id}: get: operationId: rtmpsConnectionsGet summary: returns a RTMPS connection. @@ -1229,7 +1363,7 @@ paths: '500': description: internal server error. - /v2/rtmpsconns/kick/{id}: + /v3/rtmpsconns/kick/{id}: post: operationId: rtmpsConnsKick summary: kicks out a RTMPS connection from the server. @@ -1251,7 +1385,7 @@ paths: '500': description: internal server error. - /v2/srtconns/list: + /v3/srtconns/list: get: operationId: srtConnsList summary: returns all SRT connections. @@ -1275,13 +1409,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/SRTConnsList' + $ref: '#/components/schemas/SRTConnList' '400': description: invalid request. '500': description: internal server error. - /v2/srtconns/get/{id}: + /v3/srtconns/get/{id}: get: operationId: srtConnsGet summary: returns a SRT connection. @@ -1307,7 +1441,7 @@ paths: '500': description: internal server error. - /v2/srtconns/kick/{id}: + /v3/srtconns/kick/{id}: post: operationId: srtConnsKick summary: kicks out a SRT connection from the server. @@ -1329,7 +1463,7 @@ paths: '500': description: internal server error. - /v2/webrtcsessions/list: + /v3/webrtcsessions/list: get: operationId: webrtcSessionsList summary: returns all WebRTC sessions. @@ -1353,13 +1487,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/WebRTCSessionsList' + $ref: '#/components/schemas/WebRTCSessionList' '400': description: invalid request. '500': description: internal server error. - /v2/webrtcsessions/get/{id}: + /v3/webrtcsessions/get/{id}: get: operationId: webrtcSessionsGet summary: returns a WebRTC session. @@ -1385,7 +1519,7 @@ paths: '500': description: internal server error. - /v2/webrtcsessions/kick/{id}: + /v3/webrtcsessions/kick/{id}: post: operationId: webrtcSessionsKick summary: kicks out a WebRTC session from the server. diff --git a/internal/conf/auth_method.go b/internal/conf/auth_method.go index 353764d0..e759fc4d 100644 --- a/internal/conf/auth_method.go +++ b/internal/conf/auth_method.go @@ -59,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *AuthMethods) UnmarshalEnv(s string) error { - byts, _ := json.Marshal(strings.Split(s, ",")) +// UnmarshalEnv implements env.Unmarshaler. +func (d *AuthMethods) UnmarshalEnv(_ string, v string) error { + byts, _ := json.Marshal(strings.Split(v, ",")) return d.UnmarshalJSON(byts) } diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 33c58667..2ba6b7ff 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "sort" "strings" "time" @@ -20,7 +21,7 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" ) -func getSortedKeys(paths map[string]*PathConf) []string { +func sortedKeys(paths map[string]*OptionalPath) []string { ret := make([]string, len(paths)) i := 0 for name := range paths { @@ -41,45 +42,6 @@ func firstThatExists(paths []string) string { return "" } -func loadFromFile(fpath string, defaultConfPaths []string, conf *Conf) (string, error) { - if fpath == "" { - fpath = firstThatExists(defaultConfPaths) - - // when the configuration file is not explicitly set, - // it is optional. Load defaults. - if fpath == "" { - conf.UnmarshalJSON(nil) //nolint:errcheck - return "", nil - } - } - - byts, err := os.ReadFile(fpath) - if err != nil { - return "", err - } - - if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format - byts, err = decrypt.Decrypt(key, byts) - if err != nil { - return "", err - } - } - - if key, ok := os.LookupEnv("MTX_CONFKEY"); ok { - byts, err = decrypt.Decrypt(key, byts) - if err != nil { - return "", err - } - } - - err = yaml.Load(byts, conf) - if err != nil { - return "", err - } - - return fpath, nil -} - func contains(list []headers.AuthMethod, item headers.AuthMethod) bool { for _, i := range list { if i == item { @@ -89,6 +51,33 @@ func contains(list []headers.AuthMethod, item headers.AuthMethod) bool { return false } +func copyStructFields(dest interface{}, source interface{}) { + rvsource := reflect.ValueOf(source).Elem() + rvdest := reflect.ValueOf(dest) + nf := rvsource.NumField() + var zero reflect.Value + + for i := 0; i < nf; i++ { + fnew := rvsource.Field(i) + f := rvdest.Elem().FieldByName(rvsource.Type().Field(i).Name) + if f == zero { + continue + } + + if fnew.Kind() == reflect.Pointer { + if !fnew.IsNil() { + if f.Kind() == reflect.Ptr { + f.Set(fnew) + } else { + f.Set(fnew.Elem()) + } + } + } else { + f.Set(fnew) + } + } +} + // Conf is a configuration. type Conf struct { // General @@ -97,7 +86,7 @@ type Conf struct { LogFile string `json:"logFile"` ReadTimeout StringDuration `json:"readTimeout"` WriteTimeout StringDuration `json:"writeTimeout"` - ReadBufferCount int `json:"readBufferCount"` // deprecated + ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated WriteQueueSize int `json:"writeQueueSize"` UDPMaxPayloadSize int `json:"udpMaxPayloadSize"` ExternalAuthenticationURL string `json:"externalAuthenticationURL"` @@ -113,7 +102,7 @@ type Conf struct { // RTSP RTSP bool `json:"rtsp"` - RTSPDisable bool `json:"rtspDisable"` // deprecated + RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated Protocols Protocols `json:"protocols"` Encryption Encryption `json:"encryption"` RTSPAddress string `json:"rtspAddress"` @@ -129,7 +118,7 @@ type Conf struct { // RTMP RTMP bool `json:"rtmp"` - RTMPDisable bool `json:"rtmpDisable"` // deprecated + RTMPDisable *bool `json:"rtmpDisable,omitempty"` // deprecated RTMPAddress string `json:"rtmpAddress"` RTMPEncryption Encryption `json:"rtmpEncryption"` RTMPSAddress string `json:"rtmpsAddress"` @@ -138,7 +127,7 @@ type Conf struct { // HLS HLS bool `json:"hls"` - HLSDisable bool `json:"hlsDisable"` // depreacted + HLSDisable *bool `json:"hlsDisable,omitempty"` // depreacted HLSAddress string `json:"hlsAddress"` HLSEncryption bool `json:"hlsEncryption"` HLSServerKey string `json:"hlsServerKey"` @@ -155,14 +144,14 @@ type Conf struct { // WebRTC WebRTC bool `json:"webrtc"` - WebRTCDisable bool `json:"webrtcDisable"` // deprecated + WebRTCDisable *bool `json:"webrtcDisable,omitempty"` // deprecated WebRTCAddress string `json:"webrtcAddress"` WebRTCEncryption bool `json:"webrtcEncryption"` WebRTCServerKey string `json:"webrtcServerKey"` WebRTCServerCert string `json:"webrtcServerCert"` WebRTCAllowOrigin string `json:"webrtcAllowOrigin"` WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"` - WebRTCICEServers []string `json:"webrtcICEServers"` // deprecated + WebRTCICEServers *[]string `json:"webrtcICEServers,omitempty"` // deprecated WebRTCICEServers2 []WebRTCICEServer `json:"webrtcICEServers2"` WebRTCICEInterfaces []string `json:"webrtcICEInterfaces"` WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"` @@ -181,15 +170,93 @@ type Conf struct { RecordSegmentDuration StringDuration `json:"recordSegmentDuration"` RecordDeleteAfter StringDuration `json:"recordDeleteAfter"` + // Path defaults + PathDefaults Path `json:"pathDefaults"` + // Paths - Paths map[string]*PathConf `json:"paths"` + OptionalPaths map[string]*OptionalPath `json:"paths"` + Paths map[string]*Path `json:"-"` +} + +func (conf *Conf) setDefaults() { + // General + conf.LogLevel = LogLevel(logger.Info) + conf.LogDestinations = LogDestinations{logger.DestinationStdout} + conf.LogFile = "mediamtx.log" + conf.ReadTimeout = 10 * StringDuration(time.Second) + conf.WriteTimeout = 10 * StringDuration(time.Second) + conf.WriteQueueSize = 512 + conf.UDPMaxPayloadSize = 1472 + conf.APIAddress = "127.0.0.1:9997" + conf.MetricsAddress = "127.0.0.1:9998" + conf.PPROFAddress = "127.0.0.1:9999" + + // RTSP + conf.RTSP = true + conf.Protocols = Protocols{ + Protocol(gortsplib.TransportUDP): {}, + Protocol(gortsplib.TransportUDPMulticast): {}, + Protocol(gortsplib.TransportTCP): {}, + } + conf.RTSPAddress = ":8554" + conf.RTSPSAddress = ":8322" + conf.RTPAddress = ":8000" + conf.RTCPAddress = ":8001" + conf.MulticastIPRange = "224.1.0.0/16" + conf.MulticastRTPPort = 8002 + conf.MulticastRTCPPort = 8003 + conf.ServerKey = "server.key" + conf.ServerCert = "server.crt" + conf.AuthMethods = AuthMethods{headers.AuthBasic} + + // RTMP + conf.RTMP = true + conf.RTMPAddress = ":1935" + conf.RTMPSAddress = ":1936" + conf.RTMPServerKey = "server.key" + conf.RTMPServerCert = "server.crt" + + // HLS + conf.HLS = true + conf.HLSAddress = ":8888" + conf.HLSServerKey = "server.key" + conf.HLSServerCert = "server.crt" + conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency) + conf.HLSSegmentCount = 7 + conf.HLSSegmentDuration = 1 * StringDuration(time.Second) + conf.HLSPartDuration = 200 * StringDuration(time.Millisecond) + conf.HLSSegmentMaxSize = 50 * 1024 * 1024 + conf.HLSAllowOrigin = "*" + + // WebRTC + conf.WebRTC = true + conf.WebRTCAddress = ":8889" + conf.WebRTCServerKey = "server.key" + conf.WebRTCServerCert = "server.crt" + conf.WebRTCAllowOrigin = "*" + conf.WebRTCICEServers2 = []WebRTCICEServer{{URL: "stun:stun.l.google.com:19302"}} + conf.WebRTCICEInterfaces = []string{} + conf.WebRTCICEHostNAT1To1IPs = []string{} + + // SRT + conf.SRT = true + conf.SRTAddress = ":8890" + + // Record + conf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f" + conf.RecordFormat = "fmp4" + conf.RecordPartDuration = 100 * StringDuration(time.Millisecond) + conf.RecordSegmentDuration = 3600 * StringDuration(time.Second) + conf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second) + + conf.PathDefaults.setDefaults() } // Load loads a Conf. func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) { conf := &Conf{} - fpath, err := loadFromFile(fpath, defaultConfPaths, conf) + fpath, err := conf.loadFromFile(fpath, defaultConfPaths) if err != nil { return nil, "", err } @@ -212,6 +279,45 @@ func Load(fpath string, defaultConfPaths []string) (*Conf, string, error) { return conf, fpath, nil } +func (conf *Conf) loadFromFile(fpath string, defaultConfPaths []string) (string, error) { + if fpath == "" { + fpath = firstThatExists(defaultConfPaths) + + // when the configuration file is not explicitly set, + // it is optional. + if fpath == "" { + conf.setDefaults() + return "", nil + } + } + + byts, err := os.ReadFile(fpath) + if err != nil { + return "", err + } + + if key, ok := os.LookupEnv("RTSP_CONFKEY"); ok { // legacy format + byts, err = decrypt.Decrypt(key, byts) + if err != nil { + return "", err + } + } + + if key, ok := os.LookupEnv("MTX_CONFKEY"); ok { + byts, err = decrypt.Decrypt(key, byts) + if err != nil { + return "", err + } + } + + err = yaml.Load(byts, conf) + if err != nil { + return "", err + } + + return fpath, nil +} + // Clone clones the configuration. func (conf Conf) Clone() *Conf { enc, err := json.Marshal(conf) @@ -232,8 +338,8 @@ func (conf Conf) Clone() *Conf { func (conf *Conf) Check() error { // General - if conf.ReadBufferCount != 0 { - conf.WriteQueueSize = conf.ReadBufferCount + if conf.ReadBufferCount != nil { + conf.WriteQueueSize = *conf.ReadBufferCount } if (conf.WriteQueueSize & (conf.WriteQueueSize - 1)) != 0 { return fmt.Errorf("'writeQueueSize' must be a power of two") @@ -254,8 +360,8 @@ func (conf *Conf) Check() error { // RTSP - if conf.RTSPDisable { - conf.RTSP = false + if conf.RTSPDisable != nil { + conf.RTSP = !*conf.RTSPDisable } if conf.Encryption == EncryptionStrict { if _, ok := conf.Protocols[Protocol(gortsplib.TransportUDP)]; ok { @@ -268,36 +374,37 @@ func (conf *Conf) Check() error { // RTMP - if conf.RTMPDisable { - conf.RTMP = false + if conf.RTMPDisable != nil { + conf.RTMP = !*conf.RTMPDisable } // HLS - if conf.HLSDisable { - conf.HLS = false + if conf.HLSDisable != nil { + conf.HLS = !*conf.HLSDisable } // WebRTC - if conf.WebRTCDisable { - conf.WebRTC = false - } - for _, server := range conf.WebRTCICEServers { - parts := strings.Split(server, ":") - if len(parts) == 5 { - conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{ - URL: parts[0] + ":" + parts[3] + ":" + parts[4], - Username: parts[1], - Password: parts[2], - }) - } else { - conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{ - URL: server, - }) + if conf.WebRTCDisable != nil { + conf.WebRTC = !*conf.WebRTCDisable + } + if conf.WebRTCICEServers != nil { + for _, server := range *conf.WebRTCICEServers { + parts := strings.Split(server, ":") + if len(parts) == 5 { + conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{ + URL: parts[0] + ":" + parts[3] + ":" + parts[4], + Username: parts[1], + Password: parts[2], + }) + } else { + conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{ + URL: server, + }) + } } } - conf.WebRTCICEServers = nil for _, server := range conf.WebRTCICEServers2 { if !strings.HasPrefix(server.URL, "stun:") && !strings.HasPrefix(server.URL, "turn:") && @@ -312,21 +419,19 @@ func (conf *Conf) Check() error { return fmt.Errorf("unsupported record format '%s'", conf.RecordFormat) } - // do not add automatically "all", since user may want to - // initialize all paths through API or hot reloading. - if conf.Paths == nil { - conf.Paths = make(map[string]*PathConf) - } + conf.Paths = make(map[string]*Path) - for _, name := range getSortedKeys(conf.Paths) { - pconf := conf.Paths[name] - if pconf == nil { - pconf = &PathConf{} - // load defaults - pconf.UnmarshalJSON(nil) //nolint:errcheck - conf.Paths[name] = pconf + for _, name := range sortedKeys(conf.OptionalPaths) { + optional := conf.OptionalPaths[name] + if optional == nil { + optional = &OptionalPath{ + Values: newOptionalPathValues(), + } } + pconf := newPath(&conf.PathDefaults, optional) + conf.Paths[name] = pconf + err := pconf.check(conf, name) if err != nil { return err @@ -336,82 +441,77 @@ func (conf *Conf) Check() error { return nil } -// UnmarshalJSON implements json.Unmarshaler. It is used to: -// - force DisallowUnknownFields -// - set default values +// UnmarshalJSON implements json.Unmarshaler. func (conf *Conf) UnmarshalJSON(b []byte) error { - // general - conf.LogLevel = LogLevel(logger.Info) - conf.LogDestinations = LogDestinations{logger.DestinationStdout} - conf.LogFile = "mediamtx.log" - conf.ReadTimeout = 10 * StringDuration(time.Second) - conf.WriteTimeout = 10 * StringDuration(time.Second) - conf.WriteQueueSize = 512 - conf.UDPMaxPayloadSize = 1472 - conf.APIAddress = "127.0.0.1:9997" - conf.MetricsAddress = "127.0.0.1:9998" - conf.PPROFAddress = "127.0.0.1:9999" + conf.setDefaults() - // RTSP - conf.RTSP = true - conf.Protocols = Protocols{ - Protocol(gortsplib.TransportUDP): {}, - Protocol(gortsplib.TransportUDPMulticast): {}, - Protocol(gortsplib.TransportTCP): {}, + type alias Conf + d := json.NewDecoder(bytes.NewReader(b)) + d.DisallowUnknownFields() + return d.Decode((*alias)(conf)) +} + +// Global returns the global part of Conf. +func (conf *Conf) Global() *Global { + g := &Global{ + Values: newGlobalValues(), } - conf.RTSPAddress = ":8554" - conf.RTSPSAddress = ":8322" - conf.RTPAddress = ":8000" - conf.RTCPAddress = ":8001" - conf.MulticastIPRange = "224.1.0.0/16" - conf.MulticastRTPPort = 8002 - conf.MulticastRTCPPort = 8003 - conf.ServerKey = "server.key" - conf.ServerCert = "server.crt" - conf.AuthMethods = AuthMethods{headers.AuthBasic} + copyStructFields(g.Values, conf) + return g +} - // RTMP - conf.RTMP = true - conf.RTMPAddress = ":1935" - conf.RTMPSAddress = ":1936" - conf.RTMPServerKey = "server.key" - conf.RTMPServerCert = "server.crt" +// PatchGlobal patches the global configuration. +func (conf *Conf) PatchGlobal(optional *OptionalGlobal) { + copyStructFields(conf, optional.Values) +} - // HLS - conf.HLS = true - conf.HLSAddress = ":8888" - conf.HLSServerKey = "server.key" - conf.HLSServerCert = "server.crt" - conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency) - conf.HLSSegmentCount = 7 - conf.HLSSegmentDuration = 1 * StringDuration(time.Second) - conf.HLSPartDuration = 200 * StringDuration(time.Millisecond) - conf.HLSSegmentMaxSize = 50 * 1024 * 1024 - conf.HLSAllowOrigin = "*" +// PatchPathDefaults patches path default settings. +func (conf *Conf) PatchPathDefaults(optional *OptionalPath) { + copyStructFields(&conf.PathDefaults, optional.Values) +} - // WebRTC - conf.WebRTC = true - conf.WebRTCAddress = ":8889" - conf.WebRTCServerKey = "server.key" - conf.WebRTCServerCert = "server.crt" - conf.WebRTCAllowOrigin = "*" - conf.WebRTCICEServers2 = []WebRTCICEServer{{URL: "stun:stun.l.google.com:19302"}} - conf.WebRTCICEInterfaces = []string{} - conf.WebRTCICEHostNAT1To1IPs = []string{} +// AddPath adds a path. +func (conf *Conf) AddPath(name string, p *OptionalPath) error { + if _, ok := conf.OptionalPaths[name]; ok { + return fmt.Errorf("path already exists") + } - // SRT - conf.SRT = true - conf.SRTAddress = ":8890" + if conf.OptionalPaths == nil { + conf.OptionalPaths = make(map[string]*OptionalPath) + } - // Record - conf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f" - conf.RecordFormat = "fmp4" - conf.RecordPartDuration = 100 * StringDuration(time.Millisecond) - conf.RecordSegmentDuration = 3600 * StringDuration(time.Second) - conf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second) + conf.OptionalPaths[name] = p + return nil +} - type alias Conf - d := json.NewDecoder(bytes.NewReader(b)) - d.DisallowUnknownFields() - return d.Decode((*alias)(conf)) +// PatchPath patches a path. +func (conf *Conf) PatchPath(name string, optional2 *OptionalPath) error { + optional, ok := conf.OptionalPaths[name] + if !ok { + return fmt.Errorf("path not found") + } + + copyStructFields(optional.Values, optional2.Values) + return nil +} + +// ReplacePath replaces a path. +func (conf *Conf) ReplacePath(name string, optional2 *OptionalPath) error { + _, ok := conf.OptionalPaths[name] + if !ok { + return fmt.Errorf("path not found") + } + + conf.OptionalPaths[name] = optional2 + return nil +} + +// RemovePath removes a path. +func (conf *Conf) RemovePath(name string) error { + if _, ok := conf.OptionalPaths[name]; !ok { + return fmt.Errorf("path not found") + } + + delete(conf.OptionalPaths, name) + return nil } diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index b6b6057d..374e3362 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -47,7 +47,7 @@ func TestConfFromFile(t *testing.T) { pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) - require.Equal(t, &PathConf{ + require.Equal(t, &Path{ Source: "publisher", SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), @@ -107,11 +107,21 @@ func TestConfFromFile(t *testing.T) { } func TestConfFromFileAndEnv(t *testing.T) { + // global parameter + os.Setenv("RTSP_PROTOCOLS", "tcp") + defer os.Unsetenv("RTSP_PROTOCOLS") + + // path parameter os.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing") defer os.Unsetenv("MTX_PATHS_CAM1_SOURCE") - os.Setenv("RTSP_PROTOCOLS", "tcp") - defer os.Unsetenv("RTSP_PROTOCOLS") + // deprecated global parameter + os.Setenv("MTX_RTMPDISABLE", "yes") + defer os.Unsetenv("MTX_RTMPDISABLE") + + // deprecated path parameter + os.Setenv("MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE", "yes") + defer os.Unsetenv("MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE") tmpf, err := writeTempFile([]byte("{}")) require.NoError(t, err) @@ -122,36 +132,15 @@ func TestConfFromFileAndEnv(t *testing.T) { require.Equal(t, tmpf, confPath) require.Equal(t, Protocols{Protocol(gortsplib.TransportTCP): {}}, conf.Protocols) + require.Equal(t, false, conf.RTMP) pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) - require.Equal(t, &PathConf{ - Source: "rtsp://testing", - SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), - SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), - Record: true, - OverridePublisher: true, - RPICameraWidth: 1920, - RPICameraHeight: 1080, - RPICameraContrast: 1, - RPICameraSaturation: 1, - RPICameraSharpness: 1, - RPICameraExposure: "normal", - RPICameraAWB: "auto", - RPICameraDenoise: "off", - RPICameraMetering: "centre", - RPICameraFPS: 30, - RPICameraIDRPeriod: 60, - RPICameraBitrate: 1000000, - RPICameraProfile: "main", - RPICameraLevel: "4.1", - RPICameraAfMode: "auto", - RPICameraAfRange: "normal", - RPICameraAfSpeed: "normal", - RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", - RunOnDemandStartTimeout: 10 * StringDuration(time.Second), - RunOnDemandCloseAfter: 10 * StringDuration(time.Second), - }, pa) + require.Equal(t, "rtsp://testing", pa.Source) + + pa, ok = conf.Paths["cam2"] + require.Equal(t, true, ok) + require.Equal(t, false, pa.OverridePublisher) } func TestConfFromEnvOnly(t *testing.T) { @@ -164,33 +153,7 @@ func TestConfFromEnvOnly(t *testing.T) { pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) - require.Equal(t, &PathConf{ - Source: "rtsp://testing", - SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), - SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), - Record: true, - OverridePublisher: true, - RPICameraWidth: 1920, - RPICameraHeight: 1080, - RPICameraContrast: 1, - RPICameraSaturation: 1, - RPICameraSharpness: 1, - RPICameraExposure: "normal", - RPICameraAWB: "auto", - RPICameraDenoise: "off", - RPICameraMetering: "centre", - RPICameraFPS: 30, - RPICameraIDRPeriod: 60, - RPICameraBitrate: 1000000, - RPICameraProfile: "main", - RPICameraLevel: "4.1", - RPICameraAfMode: "auto", - RPICameraAfRange: "normal", - RPICameraAfSpeed: "normal", - RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", - RunOnDemandStartTimeout: 10 * StringDuration(time.Second), - RunOnDemandCloseAfter: 10 * StringDuration(time.Second), - }, pa) + require.Equal(t, "rtsp://testing", pa.Source) } func TestConfEncryption(t *testing.T) { @@ -299,7 +262,7 @@ func TestConfErrors(t *testing.T) { " source: rpiCamera\n" + " cam2:\n" + " source: rpiCamera\n", - "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam1' and 'cam2'", + "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam2' and 'cam1'", }, { "invalid srt publish passphrase", @@ -332,7 +295,8 @@ func TestSampleConfFile(t *testing.T) { conf1, confPath1, err := Load("../../mediamtx.yml", nil) require.NoError(t, err) require.Equal(t, "../../mediamtx.yml", confPath1) - delete(conf1.Paths, "all") + conf1.Paths = make(map[string]*Path) + conf1.OptionalPaths = nil conf2, confPath2, err := Load("", nil) require.NoError(t, err) diff --git a/internal/conf/credential.go b/internal/conf/credential.go index 2c5abe97..8c5106f7 100644 --- a/internal/conf/credential.go +++ b/internal/conf/credential.go @@ -36,7 +36,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *Credential) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *Credential) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/encryption.go b/internal/conf/encryption.go index c9e0feab..3f0e4028 100644 --- a/internal/conf/encryption.go +++ b/internal/conf/encryption.go @@ -60,7 +60,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *Encryption) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *Encryption) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/env/env.go b/internal/conf/env/env.go index bd8b2a29..0eb20304 100644 --- a/internal/conf/env/env.go +++ b/internal/conf/env/env.go @@ -2,7 +2,6 @@ package env import ( - "encoding/json" "fmt" "os" "reflect" @@ -10,8 +9,9 @@ import ( "strings" ) -type envUnmarshaler interface { - UnmarshalEnv(string) error +// Unmarshaler can be implemented to override the unmarshaling process. +type Unmarshaler interface { + UnmarshalEnv(prefix string, v string) error } func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool { @@ -23,12 +23,19 @@ func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool { return false } -func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error { - rt := rv.Type() +func loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) error { + if prv.Kind() != reflect.Pointer { + return loadEnvInternal(env, prefix, prv.Addr()) + } - if i, ok := rv.Addr().Interface().(envUnmarshaler); ok { + if i, ok := prv.Interface().(Unmarshaler); ok { if ev, ok := env[prefix]; ok { - err := i.UnmarshalEnv(ev) + err := i.UnmarshalEnv(prefix, ev) + if err != nil { + return fmt.Errorf("%s: %s", prefix, err) + } + } else if envHasAtLeastAKeyWithPrefix(env, prefix) { + err := i.UnmarshalEnv(prefix, "") if err != nil { return fmt.Errorf("%s: %s", prefix, err) } @@ -36,51 +43,68 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err return nil } + rt := prv.Type().Elem() + switch rt { case reflect.TypeOf(""): if ev, ok := env[prefix]; ok { - rv.SetString(ev) + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } + prv.Elem().SetString(ev) } return nil case reflect.TypeOf(int(0)): if ev, ok := env[prefix]; ok { + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } iv, err := strconv.ParseInt(ev, 10, 32) if err != nil { return fmt.Errorf("%s: %s", prefix, err) } - rv.SetInt(iv) + prv.Elem().SetInt(iv) } return nil case reflect.TypeOf(uint64(0)): if ev, ok := env[prefix]; ok { + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } iv, err := strconv.ParseUint(ev, 10, 32) if err != nil { return fmt.Errorf("%s: %s", prefix, err) } - rv.SetUint(iv) + prv.Elem().SetUint(iv) } return nil case reflect.TypeOf(float64(0)): if ev, ok := env[prefix]; ok { + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } iv, err := strconv.ParseFloat(ev, 64) if err != nil { return fmt.Errorf("%s: %s", prefix, err) } - rv.SetFloat(iv) + prv.Elem().SetFloat(iv) } return nil case reflect.TypeOf(bool(false)): if ev, ok := env[prefix]; ok { + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } switch strings.ToLower(ev) { case "yes", "true": - rv.SetBool(true) + prv.Elem().SetBool(true) case "no", "false": - rv.SetBool(false) + prv.Elem().SetBool(false) default: return fmt.Errorf("%s: invalid value '%s'", prefix, ev) @@ -107,20 +131,16 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err } // initialize only if there's at least one key - if rv.IsNil() { - rv.Set(reflect.MakeMap(rt)) + if prv.Elem().IsNil() { + prv.Elem().Set(reflect.MakeMap(rt)) } mapKeyLower := strings.ToLower(mapKey) - nv := rv.MapIndex(reflect.ValueOf(mapKeyLower)) + nv := prv.Elem().MapIndex(reflect.ValueOf(mapKeyLower)) zero := reflect.Value{} if nv == zero { nv = reflect.New(rt.Elem().Elem()) - if unm, ok := nv.Interface().(json.Unmarshaler); ok { - // load defaults - unm.UnmarshalJSON(nil) //nolint:errcheck - } - rv.SetMapIndex(reflect.ValueOf(mapKeyLower), nv) + prv.Elem().SetMapIndex(reflect.ValueOf(mapKeyLower), nv) } err := loadEnvInternal(env, prefix+"_"+mapKey, nv.Elem()) @@ -134,13 +154,15 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err flen := rt.NumField() for i := 0; i < flen; i++ { f := rt.Field(i) + jsonTag := f.Tag.Get("json") // load only public fields - if f.Tag.Get("json") == "-" { + if jsonTag == "-" { continue } - err := loadEnvInternal(env, prefix+"_"+strings.ToUpper(f.Name), rv.Field(i)) + err := loadEnvInternal(env, prefix+"_"+ + strings.ToUpper(strings.TrimSuffix(jsonTag, ",omitempty")), prv.Elem().Field(i)) if err != nil { return err } @@ -148,20 +170,26 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err return nil case reflect.Slice: - if rt.Elem() == reflect.TypeOf("") { + switch { + case rt.Elem() == reflect.TypeOf(""): if ev, ok := env[prefix]; ok { + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } if ev == "" { - rv.Set(reflect.MakeSlice(rv.Type(), 0, 0)) + prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0)) } else { - rv.Set(reflect.ValueOf(strings.Split(ev, ","))) + prv.Elem().Set(reflect.ValueOf(strings.Split(ev, ","))) } } return nil - } - if rt.Elem().Kind() == reflect.Struct { + case rt.Elem().Kind() == reflect.Struct: if ev, ok := env[prefix]; ok && ev == "" { // special case: empty list - rv.Set(reflect.MakeSlice(rv.Type(), 0, 0)) + if prv.IsNil() { + prv.Set(reflect.New(rt)) + } + prv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0)) } else { for i := 0; ; i++ { itemPrefix := prefix + "_" + strconv.FormatInt(int64(i), 10) @@ -175,7 +203,7 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err return err } - rv.Set(reflect.Append(rv, elem.Elem())) + prv.Elem().Set(reflect.Append(prv.Elem(), elem.Elem())) } } return nil diff --git a/internal/conf/env/env_test.go b/internal/conf/env/env_test.go index b1f64fc6..092cdd17 100644 --- a/internal/conf/env/env_test.go +++ b/internal/conf/env/env_test.go @@ -10,12 +10,12 @@ import ( ) type subStruct struct { - MyParam int + MyParam int `json:"myParam"` } type mapEntry struct { - MyValue string - MyStruct subStruct + MyValue string `json:"myValue"` + MyStruct subStruct `json:"myStruct"` } type myDuration time.Duration @@ -35,28 +35,28 @@ func (d *myDuration) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *myDuration) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *myDuration) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } type mySubStruct struct { - URL string - Username string - Password string + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` } type testStruct struct { - MyString string - MyInt int - MyFloat float64 - MyBool bool - MyDuration myDuration - MyMap map[string]*mapEntry - MySlice []string - MySliceEmpty []string - MySliceSubStruct []mySubStruct - MySliceSubStructEmpty []mySubStruct + MyString string `json:"myString"` + MyInt int `json:"myInt"` + MyFloat float64 `json:"myFloat"` + MyBool bool `json:"myBool"` + MyDuration myDuration `json:"myDuration"` + MyMap map[string]*mapEntry `json:"myMap"` + MySlice []string `json:"mySlice"` + MySliceEmpty []string `json:"mySliceEmpty"` + MySliceSubStruct []mySubStruct `json:"mySliceSubStruct"` + MySliceSubStructEmpty []mySubStruct `json:"mySliceSubStructEmpty"` } func TestLoad(t *testing.T) { diff --git a/internal/conf/global.go b/internal/conf/global.go new file mode 100644 index 00000000..51ee02a4 --- /dev/null +++ b/internal/conf/global.go @@ -0,0 +1,41 @@ +package conf + +import ( + "encoding/json" + "reflect" +) + +var globalValuesType = func() reflect.Type { + var fields []reflect.StructField + rt := reflect.TypeOf(Conf{}) + nf := rt.NumField() + + for i := 0; i < nf; i++ { + f := rt.Field(i) + j := f.Tag.Get("json") + + if j != "-" && j != "pathDefaults" && j != "paths" { + fields = append(fields, reflect.StructField{ + Name: f.Name, + Type: f.Type, + Tag: f.Tag, + }) + } + } + + return reflect.StructOf(fields) +}() + +func newGlobalValues() interface{} { + return reflect.New(globalValuesType).Interface() +} + +// Global is the global part of Conf. +type Global struct { + Values interface{} +} + +// MarshalJSON implements json.Marshaler. +func (p *Global) MarshalJSON() ([]byte, error) { + return json.Marshal(p.Values) +} diff --git a/internal/conf/hls_variant.go b/internal/conf/hls_variant.go index 18182a06..9d0dfc05 100644 --- a/internal/conf/hls_variant.go +++ b/internal/conf/hls_variant.go @@ -55,7 +55,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *HLSVariant) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *HLSVariant) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/ips_or_cidrs.go b/internal/conf/ips_or_cidrs.go index bc637630..59d56d32 100644 --- a/internal/conf/ips_or_cidrs.go +++ b/internal/conf/ips_or_cidrs.go @@ -50,9 +50,9 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *IPsOrCIDRs) UnmarshalEnv(s string) error { - byts, _ := json.Marshal(strings.Split(s, ",")) +// UnmarshalEnv implements env.Unmarshaler. +func (d *IPsOrCIDRs) UnmarshalEnv(_ string, v string) error { + byts, _ := json.Marshal(strings.Split(v, ",")) return d.UnmarshalJSON(byts) } diff --git a/internal/conf/log_destination.go b/internal/conf/log_destination.go index c9493d7b..cb369f00 100644 --- a/internal/conf/log_destination.go +++ b/internal/conf/log_destination.go @@ -84,8 +84,8 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *LogDestinations) UnmarshalEnv(s string) error { - byts, _ := json.Marshal(strings.Split(s, ",")) +// UnmarshalEnv implements env.Unmarshaler. +func (d *LogDestinations) UnmarshalEnv(_ string, v string) error { + byts, _ := json.Marshal(strings.Split(v, ",")) return d.UnmarshalJSON(byts) } diff --git a/internal/conf/log_level.go b/internal/conf/log_level.go index 4c2f5b8d..2231067c 100644 --- a/internal/conf/log_level.go +++ b/internal/conf/log_level.go @@ -61,7 +61,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *LogLevel) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *LogLevel) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/optional_global.go b/internal/conf/optional_global.go new file mode 100644 index 00000000..afaeb158 --- /dev/null +++ b/internal/conf/optional_global.go @@ -0,0 +1,60 @@ +package conf + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" +) + +var optionalGlobalValuesType = func() reflect.Type { + var fields []reflect.StructField + rt := reflect.TypeOf(Conf{}) + nf := rt.NumField() + + for i := 0; i < nf; i++ { + f := rt.Field(i) + j := f.Tag.Get("json") + + if j != "-" && j != "pathDefaults" && j != "paths" { + if !strings.Contains(j, ",omitempty") { + j += ",omitempty" + } + + typ := f.Type + if typ.Kind() != reflect.Pointer { + typ = reflect.PtrTo(typ) + } + + fields = append(fields, reflect.StructField{ + Name: f.Name, + Type: typ, + Tag: reflect.StructTag(`json:"` + j + `"`), + }) + } + } + + return reflect.StructOf(fields) +}() + +func newOptionalGlobalValues() interface{} { + return reflect.New(optionalGlobalValuesType).Interface() +} + +// OptionalGlobal is a Conf whose values can all be optional. +type OptionalGlobal struct { + Values interface{} +} + +// UnmarshalJSON implements json.Unmarshaler. +func (p *OptionalGlobal) UnmarshalJSON(b []byte) error { + p.Values = newOptionalGlobalValues() + d := json.NewDecoder(bytes.NewReader(b)) + d.DisallowUnknownFields() + return d.Decode(p.Values) +} + +// MarshalJSON implements json.Marshaler. +func (p *OptionalGlobal) MarshalJSON() ([]byte, error) { + return json.Marshal(p.Values) +} diff --git a/internal/conf/optional_path.go b/internal/conf/optional_path.go new file mode 100644 index 00000000..47eff598 --- /dev/null +++ b/internal/conf/optional_path.go @@ -0,0 +1,70 @@ +package conf + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" + + "github.com/bluenviron/mediamtx/internal/conf/env" +) + +var optionalPathValuesType = func() reflect.Type { + var fields []reflect.StructField + rt := reflect.TypeOf(Path{}) + nf := rt.NumField() + + for i := 0; i < nf; i++ { + f := rt.Field(i) + j := f.Tag.Get("json") + + if j != "-" { + if !strings.Contains(j, ",omitempty") { + j += ",omitempty" + } + + typ := f.Type + if typ.Kind() != reflect.Pointer { + typ = reflect.PtrTo(typ) + } + + fields = append(fields, reflect.StructField{ + Name: f.Name, + Type: typ, + Tag: reflect.StructTag(`json:"` + j + `"`), + }) + } + } + + return reflect.StructOf(fields) +}() + +func newOptionalPathValues() interface{} { + return reflect.New(optionalPathValuesType).Interface() +} + +// OptionalPath is a Path whose values can all be optional. +type OptionalPath struct { + Values interface{} +} + +// UnmarshalJSON implements json.Unmarshaler. +func (p *OptionalPath) UnmarshalJSON(b []byte) error { + p.Values = newOptionalPathValues() + d := json.NewDecoder(bytes.NewReader(b)) + d.DisallowUnknownFields() + return d.Decode(p.Values) +} + +// UnmarshalEnv implements env.Unmarshaler. +func (p *OptionalPath) UnmarshalEnv(prefix string, _ string) error { + if p.Values == nil { + p.Values = newOptionalPathValues() + } + return env.Load(prefix, p.Values) +} + +// MarshalJSON implements json.Marshaler. +func (p *OptionalPath) MarshalJSON() ([]byte, error) { + return json.Marshal(p.Values) +} diff --git a/internal/conf/path.go b/internal/conf/path.go index aecfb319..4380be60 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -1,7 +1,6 @@ package conf import ( - "bytes" "encoding/json" "fmt" "net" @@ -48,8 +47,8 @@ func srtCheckPassphrase(passphrase string) error { } } -// PathConf is a path configuration. -type PathConf struct { +// Path is a path configuration. +type Path struct { Regexp *regexp.Regexp `json:"-"` // General @@ -72,7 +71,7 @@ type PathConf struct { // Publisher OverridePublisher bool `json:"overridePublisher"` - DisablePublisherOverride bool `json:"disablePublisherOverride"` // deprecated + DisablePublisherOverride *bool `json:"disablePublisherOverride,omitempty"` // deprecated Fallback string `json:"fallback"` SRTPublishPassphrase string `json:"srtPublishPassphrase"` @@ -135,7 +134,67 @@ type PathConf struct { RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"` } -func (pconf *PathConf) check(conf *Conf, name string) error { +func (pconf *Path) setDefaults() { + // General + pconf.Source = "publisher" + pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second) + pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second) + pconf.Record = true + + // Publisher + pconf.OverridePublisher = true + + // Raspberry Pi Camera + pconf.RPICameraWidth = 1920 + pconf.RPICameraHeight = 1080 + pconf.RPICameraContrast = 1 + pconf.RPICameraSaturation = 1 + pconf.RPICameraSharpness = 1 + pconf.RPICameraExposure = "normal" + pconf.RPICameraAWB = "auto" + pconf.RPICameraDenoise = "off" + pconf.RPICameraMetering = "centre" + pconf.RPICameraFPS = 30 + pconf.RPICameraIDRPeriod = 60 + pconf.RPICameraBitrate = 1000000 + pconf.RPICameraProfile = "main" + pconf.RPICameraLevel = "4.1" + pconf.RPICameraAfMode = "auto" + pconf.RPICameraAfRange = "normal" + pconf.RPICameraAfSpeed = "normal" + pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" + + // Hooks + pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) + pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second) +} + +func newPath(defaults *Path, partial *OptionalPath) *Path { + pconf := &Path{} + copyStructFields(pconf, defaults) + copyStructFields(pconf, partial.Values) + return pconf +} + +// Clone clones the configuration. +func (pconf Path) Clone() *Path { + enc, err := json.Marshal(pconf) + if err != nil { + panic(err) + } + + var dest Path + err = json.Unmarshal(enc, &dest) + if err != nil { + panic(err) + } + + dest.Regexp = pconf.Regexp + + return &dest +} + +func (pconf *Path) check(conf *Conf, name string) error { switch { case name == "all": pconf.Regexp = regexp.MustCompile("^.*$") @@ -147,11 +206,11 @@ func (pconf *PathConf) check(conf *Conf, name string) error { } default: // regular expression-based path - pathRegexp, err := regexp.Compile(name[1:]) + regexp, err := regexp.Compile(name[1:]) if err != nil { return fmt.Errorf("invalid regular expression: %s", name[1:]) } - pconf.Regexp = pathRegexp + pconf.Regexp = regexp } // General @@ -329,8 +388,8 @@ func (pconf *PathConf) check(conf *Conf, name string) error { // Publisher - if pconf.DisablePublisherOverride { - pconf.OverridePublisher = true + if pconf.DisablePublisherOverride != nil { + pconf.OverridePublisher = !*pconf.DisablePublisherOverride } if pconf.Fallback != "" { if pconf.Source != "publisher" { @@ -408,31 +467,13 @@ func (pconf *PathConf) check(conf *Conf, name string) error { return nil } -// Equal checks whether two PathConfs are equal. -func (pconf *PathConf) Equal(other *PathConf) bool { +// Equal checks whether two Paths are equal. +func (pconf *Path) Equal(other *Path) bool { return reflect.DeepEqual(pconf, other) } -// Clone clones the configuration. -func (pconf PathConf) Clone() *PathConf { - enc, err := json.Marshal(pconf) - if err != nil { - panic(err) - } - - var dest PathConf - err = json.Unmarshal(enc, &dest) - if err != nil { - panic(err) - } - - dest.Regexp = pconf.Regexp - - return &dest -} - // HasStaticSource checks whether the path has a static source. -func (pconf PathConf) HasStaticSource() bool { +func (pconf Path) HasStaticSource() bool { return strings.HasPrefix(pconf.Source, "rtsp://") || strings.HasPrefix(pconf.Source, "rtsps://") || strings.HasPrefix(pconf.Source, "rtmp://") || @@ -447,54 +488,11 @@ func (pconf PathConf) HasStaticSource() bool { } // HasOnDemandStaticSource checks whether the path has a on demand static source. -func (pconf PathConf) HasOnDemandStaticSource() bool { +func (pconf Path) HasOnDemandStaticSource() bool { return pconf.HasStaticSource() && pconf.SourceOnDemand } // HasOnDemandPublisher checks whether the path has a on-demand publisher. -func (pconf PathConf) HasOnDemandPublisher() bool { +func (pconf Path) HasOnDemandPublisher() bool { return pconf.RunOnDemand != "" } - -// UnmarshalJSON implements json.Unmarshaler. It is used to: -// - force DisallowUnknownFields -// - set default values -func (pconf *PathConf) UnmarshalJSON(b []byte) error { - // General - pconf.Source = "publisher" - pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second) - pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second) - pconf.Record = true - - // Publisher - pconf.OverridePublisher = true - - // Raspberry Pi Camera - pconf.RPICameraWidth = 1920 - pconf.RPICameraHeight = 1080 - pconf.RPICameraContrast = 1 - pconf.RPICameraSaturation = 1 - pconf.RPICameraSharpness = 1 - pconf.RPICameraExposure = "normal" - pconf.RPICameraAWB = "auto" - pconf.RPICameraDenoise = "off" - pconf.RPICameraMetering = "centre" - pconf.RPICameraFPS = 30 - pconf.RPICameraIDRPeriod = 60 - pconf.RPICameraBitrate = 1000000 - pconf.RPICameraProfile = "main" - pconf.RPICameraLevel = "4.1" - pconf.RPICameraAfMode = "auto" - pconf.RPICameraAfRange = "normal" - pconf.RPICameraAfSpeed = "normal" - pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" - - // Hooks - pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second) - pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second) - - type alias PathConf - d := json.NewDecoder(bytes.NewReader(b)) - d.DisallowUnknownFields() - return d.Decode((*alias)(pconf)) -} diff --git a/internal/conf/protocol.go b/internal/conf/protocol.go index 5c1713d5..e2e46c94 100644 --- a/internal/conf/protocol.go +++ b/internal/conf/protocol.go @@ -74,8 +74,8 @@ func (d *Protocols) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *Protocols) UnmarshalEnv(s string) error { - byts, _ := json.Marshal(strings.Split(s, ",")) +// UnmarshalEnv implements env.Unmarshaler. +func (d *Protocols) UnmarshalEnv(_ string, v string) error { + byts, _ := json.Marshal(strings.Split(v, ",")) return d.UnmarshalJSON(byts) } diff --git a/internal/conf/rtsp_range_type.go b/internal/conf/rtsp_range_type.go index 6acecd24..168f306d 100644 --- a/internal/conf/rtsp_range_type.go +++ b/internal/conf/rtsp_range_type.go @@ -67,7 +67,7 @@ func (d *RTSPRangeType) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *RTSPRangeType) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *RTSPRangeType) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/source_protocol.go b/internal/conf/source_protocol.go index c681f552..4204c5ca 100644 --- a/internal/conf/source_protocol.go +++ b/internal/conf/source_protocol.go @@ -67,7 +67,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *SourceProtocol) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *SourceProtocol) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/string_duration.go b/internal/conf/string_duration.go index a1184f2c..57e6dbaa 100644 --- a/internal/conf/string_duration.go +++ b/internal/conf/string_duration.go @@ -30,7 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (d *StringDuration) UnmarshalEnv(s string) error { - return d.UnmarshalJSON([]byte(`"` + s + `"`)) +// UnmarshalEnv implements env.Unmarshaler. +func (d *StringDuration) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/string_size.go b/internal/conf/string_size.go index 4dba8e1c..e3a1dcc4 100644 --- a/internal/conf/string_size.go +++ b/internal/conf/string_size.go @@ -30,7 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error { return nil } -// UnmarshalEnv implements envUnmarshaler. -func (s *StringSize) UnmarshalEnv(v string) error { +// UnmarshalEnv implements env.Unmarshaler. +func (s *StringSize) UnmarshalEnv(_ string, v string) error { return s.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/core/api.go b/internal/core/api.go index f66f4adc..783206b6 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "reflect" + "sort" "strconv" "sync" "time" @@ -24,68 +25,6 @@ func interfaceIsEmpty(i interface{}) bool { return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil() } -func fillStruct(dest interface{}, source interface{}) { - rvsource := reflect.ValueOf(source).Elem() - rvdest := reflect.ValueOf(dest) - nf := rvsource.NumField() - for i := 0; i < nf; i++ { - fnew := rvsource.Field(i) - if !fnew.IsNil() { - f := rvdest.Elem().FieldByName(rvsource.Type().Field(i).Name) - if f.Kind() == reflect.Ptr { - f.Set(fnew) - } else { - f.Set(fnew.Elem()) - } - } - } -} - -func generateStructWithOptionalFields(model interface{}) interface{} { - var fields []reflect.StructField - - rt := reflect.TypeOf(model) - nf := rt.NumField() - for i := 0; i < nf; i++ { - f := rt.Field(i) - j := f.Tag.Get("json") - - if j != "-" && j != "paths" { - fields = append(fields, reflect.StructField{ - Name: f.Name, - Type: reflect.PtrTo(f.Type), - Tag: f.Tag, - }) - } - } - - return reflect.New(reflect.StructOf(fields)).Interface() -} - -func loadConfData(ctx *gin.Context) (interface{}, error) { - in := generateStructWithOptionalFields(conf.Conf{}) - d := json.NewDecoder(ctx.Request.Body) - d.DisallowUnknownFields() - err := d.Decode(in) - if err != nil { - return nil, err - } - - return in, err -} - -func loadConfPathData(ctx *gin.Context) (interface{}, error) { - in := generateStructWithOptionalFields(conf.PathConf{}) - d := json.NewDecoder(ctx.Request.Body) - d.DisallowUnknownFields() - err := d.Decode(in) - if err != nil { - return nil, err - } - - return in, err -} - func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int { ritems := reflect.ValueOf(itemsPtr).Elem() @@ -138,6 +77,17 @@ func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int return paginate2(itemsPtr, itemsPerPage, page), nil } +func sortedKeys(paths map[string]*conf.OptionalPath) []string { + ret := make([]string, len(paths)) + i := 0 + for name := range paths { + ret[i] = name + i++ + } + sort.Strings(ret) + return ret +} + func paramName(ctx *gin.Context) (string, bool) { name := ctx.Param("name") @@ -149,37 +99,37 @@ func paramName(ctx *gin.Context) (string, bool) { } type apiPathManager interface { - apiPathsList() (*apiPathsList, error) + apiPathsList() (*apiPathList, error) apiPathsGet(string) (*apiPath, error) } type apiHLSManager interface { - apiMuxersList() (*apiHLSMuxersList, error) + apiMuxersList() (*apiHLSMuxerList, error) apiMuxersGet(string) (*apiHLSMuxer, error) } type apiRTSPServer interface { apiConnsList() (*apiRTSPConnsList, error) apiConnsGet(uuid.UUID) (*apiRTSPConn, error) - apiSessionsList() (*apiRTSPSessionsList, error) + apiSessionsList() (*apiRTSPSessionList, error) apiSessionsGet(uuid.UUID) (*apiRTSPSession, error) apiSessionsKick(uuid.UUID) error } type apiRTMPServer interface { - apiConnsList() (*apiRTMPConnsList, error) + apiConnsList() (*apiRTMPConnList, error) apiConnsGet(uuid.UUID) (*apiRTMPConn, error) apiConnsKick(uuid.UUID) error } type apiWebRTCManager interface { - apiSessionsList() (*apiWebRTCSessionsList, error) + apiSessionsList() (*apiWebRTCSessionList, error) apiSessionsGet(uuid.UUID) (*apiWebRTCSession, error) apiSessionsKick(uuid.UUID) error } type apiSRTServer interface { - apiConnsList() (*apiSRTConnsList, error) + apiConnsList() (*apiSRTConnList, error) apiConnsGet(uuid.UUID) (*apiSRTConn, error) apiConnsKick(uuid.UUID) error } @@ -237,58 +187,65 @@ func newAPI( group := router.Group("/") - group.GET("/v2/config/get", a.onConfigGet) - group.POST("/v2/config/set", a.onConfigSet) - group.POST("/v2/config/paths/add/*name", a.onConfigPathsAdd) - group.POST("/v2/config/paths/edit/*name", a.onConfigPathsEdit) - group.POST("/v2/config/paths/remove/*name", a.onConfigPathsDelete) + group.GET("/v3/config/global/get", a.onConfigGlobalGet) + group.PATCH("/v3/config/global/patch", a.onConfigGlobalPatch) + + group.GET("/v3/config/pathdefaults/get", a.onConfigPathDefaultsGet) + group.PATCH("/v3/config/pathdefaults/patch", a.onConfigPathDefaultsPatch) + + group.GET("/v3/config/paths/list", a.onConfigPathsList) + group.GET("/v3/config/paths/get/*name", a.onConfigPathsGet) + group.POST("/v3/config/paths/add/*name", a.onConfigPathsAdd) + group.PATCH("/v3/config/paths/patch/*name", a.onConfigPathsPatch) + group.POST("/v3/config/paths/replace/*name", a.onConfigPathsReplace) + group.DELETE("/v3/config/paths/delete/*name", a.onConfigPathsDelete) + + group.GET("/v3/paths/list", a.onPathsList) + group.GET("/v3/paths/get/*name", a.onPathsGet) if !interfaceIsEmpty(a.hlsManager) { - group.GET("/v2/hlsmuxers/list", a.onHLSMuxersList) - group.GET("/v2/hlsmuxers/get/*name", a.onHLSMuxersGet) + group.GET("/v3/hlsmuxers/list", a.onHLSMuxersList) + group.GET("/v3/hlsmuxers/get/*name", a.onHLSMuxersGet) } - group.GET("/v2/paths/list", a.onPathsList) - group.GET("/v2/paths/get/*name", a.onPathsGet) - if !interfaceIsEmpty(a.rtspServer) { - group.GET("/v2/rtspconns/list", a.onRTSPConnsList) - group.GET("/v2/rtspconns/get/:id", a.onRTSPConnsGet) - group.GET("/v2/rtspsessions/list", a.onRTSPSessionsList) - group.GET("/v2/rtspsessions/get/:id", a.onRTSPSessionsGet) - group.POST("/v2/rtspsessions/kick/:id", a.onRTSPSessionsKick) + group.GET("/v3/rtspconns/list", a.onRTSPConnsList) + group.GET("/v3/rtspconns/get/:id", a.onRTSPConnsGet) + group.GET("/v3/rtspsessions/list", a.onRTSPSessionsList) + group.GET("/v3/rtspsessions/get/:id", a.onRTSPSessionsGet) + group.POST("/v3/rtspsessions/kick/:id", a.onRTSPSessionsKick) } if !interfaceIsEmpty(a.rtspsServer) { - group.GET("/v2/rtspsconns/list", a.onRTSPSConnsList) - group.GET("/v2/rtspsconns/get/:id", a.onRTSPSConnsGet) - group.GET("/v2/rtspssessions/list", a.onRTSPSSessionsList) - group.GET("/v2/rtspssessions/get/:id", a.onRTSPSSessionsGet) - group.POST("/v2/rtspssessions/kick/:id", a.onRTSPSSessionsKick) + group.GET("/v3/rtspsconns/list", a.onRTSPSConnsList) + group.GET("/v3/rtspsconns/get/:id", a.onRTSPSConnsGet) + group.GET("/v3/rtspssessions/list", a.onRTSPSSessionsList) + group.GET("/v3/rtspssessions/get/:id", a.onRTSPSSessionsGet) + group.POST("/v3/rtspssessions/kick/:id", a.onRTSPSSessionsKick) } if !interfaceIsEmpty(a.rtmpServer) { - group.GET("/v2/rtmpconns/list", a.onRTMPConnsList) - group.GET("/v2/rtmpconns/get/:id", a.onRTMPConnsGet) - group.POST("/v2/rtmpconns/kick/:id", a.onRTMPConnsKick) + group.GET("/v3/rtmpconns/list", a.onRTMPConnsList) + group.GET("/v3/rtmpconns/get/:id", a.onRTMPConnsGet) + group.POST("/v3/rtmpconns/kick/:id", a.onRTMPConnsKick) } if !interfaceIsEmpty(a.rtmpsServer) { - group.GET("/v2/rtmpsconns/list", a.onRTMPSConnsList) - group.GET("/v2/rtmpsconns/get/:id", a.onRTMPSConnsGet) - group.POST("/v2/rtmpsconns/kick/:id", a.onRTMPSConnsKick) + group.GET("/v3/rtmpsconns/list", a.onRTMPSConnsList) + group.GET("/v3/rtmpsconns/get/:id", a.onRTMPSConnsGet) + group.POST("/v3/rtmpsconns/kick/:id", a.onRTMPSConnsKick) } if !interfaceIsEmpty(a.webRTCManager) { - group.GET("/v2/webrtcsessions/list", a.onWebRTCSessionsList) - group.GET("/v2/webrtcsessions/get/:id", a.onWebRTCSessionsGet) - group.POST("/v2/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) + group.GET("/v3/webrtcsessions/list", a.onWebRTCSessionsList) + group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet) + group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) } if !interfaceIsEmpty(a.srtServer) { - group.GET("/v2/srtconns/list", a.onSRTConnsList) - group.GET("/v2/srtconns/get/:id", a.onSRTConnsGet) - group.POST("/v2/srtconns/kick/:id", a.onSRTConnsKick) + group.GET("/v3/srtconns/list", a.onSRTConnsList) + group.GET("/v3/srtconns/get/:id", a.onSRTConnsGet) + group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick) } network, address := restrictNetwork("tcp", address) @@ -345,16 +302,17 @@ func (a *api) writeServerErrorOrNotFound(ctx *gin.Context, err error) { } } -func (a *api) onConfigGet(ctx *gin.Context) { +func (a *api) onConfigGlobalGet(ctx *gin.Context) { a.mutex.Lock() c := a.conf a.mutex.Unlock() - ctx.JSON(http.StatusOK, c) + ctx.JSON(http.StatusOK, c.Global()) } -func (a *api) onConfigSet(ctx *gin.Context) { - in, err := loadConfData(ctx) +func (a *api) onConfigGlobalPatch(ctx *gin.Context) { + var c conf.OptionalGlobal + err := json.NewDecoder(ctx.Request.Body).Decode(&c) if err != nil { a.writeUserError(ctx, err) return @@ -365,7 +323,7 @@ func (a *api) onConfigSet(ctx *gin.Context) { newConf := a.conf.Clone() - fillStruct(newConf, in) + newConf.PatchGlobal(&c) err = newConf.Check() if err != nil { @@ -382,14 +340,94 @@ func (a *api) onConfigSet(ctx *gin.Context) { ctx.Status(http.StatusOK) } -func (a *api) onConfigPathsAdd(ctx *gin.Context) { +func (a *api) onConfigPathDefaultsGet(ctx *gin.Context) { + a.mutex.Lock() + c := a.conf + a.mutex.Unlock() + + ctx.JSON(http.StatusOK, c.PathDefaults) +} + +func (a *api) onConfigPathDefaultsPatch(ctx *gin.Context) { + var p conf.OptionalPath + err := json.NewDecoder(ctx.Request.Body).Decode(&p) + if err != nil { + a.writeUserError(ctx, err) + return + } + + a.mutex.Lock() + defer a.mutex.Unlock() + + newConf := a.conf.Clone() + + newConf.PatchPathDefaults(&p) + + err = newConf.Check() + if err != nil { + a.writeUserError(ctx, err) + return + } + + a.conf = newConf + a.parent.apiConfigSet(newConf) + + ctx.Status(http.StatusOK) +} + +func (a *api) onConfigPathsList(ctx *gin.Context) { + a.mutex.Lock() + c := a.conf + a.mutex.Unlock() + + data := &apiPathConfList{ + Items: make([]*conf.OptionalPath, len(c.OptionalPaths)), + } + + for i, key := range sortedKeys(c.OptionalPaths) { + data.Items[i] = c.OptionalPaths[key] + } + + data.ItemCount = len(data.Items) + pageCount, err := paginate(&data.Items, ctx.Query("itemsPerPage"), ctx.Query("page")) + if err != nil { + a.writeUserError(ctx, err) + return + } + data.PageCount = pageCount + + ctx.JSON(http.StatusOK, data) +} + +func (a *api) onConfigPathsGet(ctx *gin.Context) { + name, ok := paramName(ctx) + if !ok { + a.writeUserError(ctx, fmt.Errorf("invalid name")) + return + } + + a.mutex.Lock() + c := a.conf + a.mutex.Unlock() + + p, ok := c.OptionalPaths[name] + if !ok { + a.writeNotFound(ctx) + return + } + + ctx.JSON(http.StatusOK, p) +} + +func (a *api) onConfigPathsAdd(ctx *gin.Context) { //nolint:dupl name, ok := paramName(ctx) if !ok { a.writeUserError(ctx, fmt.Errorf("invalid name")) return } - in, err := loadConfPathData(ctx) + var p conf.OptionalPath + err := json.NewDecoder(ctx.Request.Body).Decode(&p) if err != nil { a.writeUserError(ctx, err) return @@ -400,19 +438,48 @@ func (a *api) onConfigPathsAdd(ctx *gin.Context) { newConf := a.conf.Clone() - if _, ok := newConf.Paths[name]; ok { - a.writeUserError(ctx, fmt.Errorf("path already exists")) + err = newConf.AddPath(name, &p) + if err != nil { + a.writeUserError(ctx, err) + return + } + + err = newConf.Check() + if err != nil { + a.writeUserError(ctx, err) + return + } + + a.conf = newConf + a.parent.apiConfigSet(newConf) + + ctx.Status(http.StatusOK) +} + +func (a *api) onConfigPathsPatch(ctx *gin.Context) { //nolint:dupl + name, ok := paramName(ctx) + if !ok { + a.writeUserError(ctx, fmt.Errorf("invalid name")) return } - newConfPath := &conf.PathConf{} + var p conf.OptionalPath + err := json.NewDecoder(ctx.Request.Body).Decode(&p) + if err != nil { + a.writeUserError(ctx, err) + return + } - // load default values - newConfPath.UnmarshalJSON([]byte("{}")) //nolint:errcheck + a.mutex.Lock() + defer a.mutex.Unlock() - fillStruct(newConfPath, in) + newConf := a.conf.Clone() - newConf.Paths[name] = newConfPath + err = newConf.PatchPath(name, &p) + if err != nil { + a.writeUserError(ctx, err) + return + } err = newConf.Check() if err != nil { @@ -421,22 +488,20 @@ func (a *api) onConfigPathsAdd(ctx *gin.Context) { } a.conf = newConf - - // since reloading the configuration can cause the shutdown of the API, - // call it in a goroutine - go a.parent.apiConfigSet(newConf) + a.parent.apiConfigSet(newConf) ctx.Status(http.StatusOK) } -func (a *api) onConfigPathsEdit(ctx *gin.Context) { +func (a *api) onConfigPathsReplace(ctx *gin.Context) { //nolint:dupl name, ok := paramName(ctx) if !ok { a.writeUserError(ctx, fmt.Errorf("invalid name")) return } - in, err := loadConfPathData(ctx) + var p conf.OptionalPath + err := json.NewDecoder(ctx.Request.Body).Decode(&p) if err != nil { a.writeUserError(ctx, err) return @@ -447,14 +512,12 @@ func (a *api) onConfigPathsEdit(ctx *gin.Context) { newConf := a.conf.Clone() - newConfPath, ok := newConf.Paths[name] - if !ok { - a.writeNotFound(ctx) + err = newConf.ReplacePath(name, &p) + if err != nil { + a.writeUserError(ctx, err) return } - fillStruct(newConfPath, in) - err = newConf.Check() if err != nil { a.writeUserError(ctx, err) @@ -462,10 +525,7 @@ func (a *api) onConfigPathsEdit(ctx *gin.Context) { } a.conf = newConf - - // since reloading the configuration can cause the shutdown of the API, - // call it in a goroutine - go a.parent.apiConfigSet(newConf) + a.parent.apiConfigSet(newConf) ctx.Status(http.StatusOK) } @@ -480,25 +540,22 @@ func (a *api) onConfigPathsDelete(ctx *gin.Context) { a.mutex.Lock() defer a.mutex.Unlock() - if _, ok := a.conf.Paths[name]; !ok { - a.writeNotFound(ctx) + newConf := a.conf.Clone() + + err := newConf.RemovePath(name) + if err != nil { + a.writeUserError(ctx, err) return } - newConf := a.conf.Clone() - delete(newConf.Paths, name) - - err := newConf.Check() + err = newConf.Check() if err != nil { a.writeUserError(ctx, err) return } a.conf = newConf - - // since reloading the configuration can cause the shutdown of the API, - // call it in a goroutine - go a.parent.apiConfigSet(newConf) + a.parent.apiConfigSet(newConf) ctx.Status(http.StatusOK) } diff --git a/internal/core/api_defs.go b/internal/core/api_defs.go index 6be6978d..c791507b 100644 --- a/internal/core/api_defs.go +++ b/internal/core/api_defs.go @@ -8,6 +8,12 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" ) +type apiPathConfList struct { + ItemCount int `json:"itemCount"` + PageCount int `json:"pageCount"` + Items []*conf.OptionalPath `json:"items"` +} + type apiPathSourceOrReader struct { Type string `json:"type"` ID string `json:"id"` @@ -16,9 +22,7 @@ type apiPathSourceOrReader struct { type apiPath struct { Name string `json:"name"` ConfName string `json:"confName"` - Conf *conf.PathConf `json:"conf"` Source *apiPathSourceOrReader `json:"source"` - SourceReady bool `json:"sourceReady"` // Deprecated: renamed to Ready Ready bool `json:"ready"` ReadyTime *time.Time `json:"readyTime"` Tracks []string `json:"tracks"` @@ -26,7 +30,7 @@ type apiPath struct { Readers []apiPathSourceOrReader `json:"readers"` } -type apiPathsList struct { +type apiPathList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiPath `json:"items"` @@ -39,7 +43,7 @@ type apiHLSMuxer struct { BytesSent uint64 `json:"bytesSent"` } -type apiHLSMuxersList struct { +type apiHLSMuxerList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiHLSMuxer `json:"items"` @@ -77,7 +81,7 @@ type apiRTMPConn struct { BytesSent uint64 `json:"bytesSent"` } -type apiRTMPConnsList struct { +type apiRTMPConnList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiRTMPConn `json:"items"` @@ -102,7 +106,7 @@ type apiRTSPSession struct { BytesSent uint64 `json:"bytesSent"` } -type apiRTSPSessionsList struct { +type apiRTSPSessionList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiRTSPSession `json:"items"` @@ -126,7 +130,7 @@ type apiSRTConn struct { BytesSent uint64 `json:"bytesSent"` } -type apiSRTConnsList struct { +type apiSRTConnList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiSRTConn `json:"items"` @@ -152,7 +156,7 @@ type apiWebRTCSession struct { BytesSent uint64 `json:"bytesSent"` } -type apiWebRTCSessionsList struct { +type apiWebRTCSessionList struct { ItemCount int `json:"itemCount"` PageCount int `json:"pageCount"` Items []*apiWebRTCSession `json:"items"` diff --git a/internal/core/api_test.go b/internal/core/api_test.go index d297c849..60b3e92a 100644 --- a/internal/core/api_test.go +++ b/internal/core/api_test.go @@ -121,7 +121,7 @@ func TestPagination(t *testing.T) { require.Equal(t, []int{5}, items) } -func TestAPIConfigGet(t *testing.T) { +func TestAPIConfigGlobalGet(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() @@ -129,33 +129,35 @@ func TestAPIConfigGet(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} var out map[string]interface{} - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out) require.Equal(t, true, out["api"]) } -func TestAPIConfigSet(t *testing.T) { +func TestAPIConfigGlobalPatch(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() hc := &http.Client{Transport: &http.Transport{}} - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/set", map[string]interface{}{ - "rtmp": false, - "readTimeout": "7s", - "protocols": []string{"tcp"}, + httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/global/patch", map[string]interface{}{ + "rtmp": false, + "readTimeout": "7s", + "protocols": []string{"tcp"}, + "readBufferCount": 4096, // test setting a deprecated parameter }, nil) time.Sleep(500 * time.Millisecond) var out map[string]interface{} - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/global/get", nil, &out) require.Equal(t, false, out["rtmp"]) require.Equal(t, "7s", out["readTimeout"]) require.Equal(t, []interface{}{"tcp"}, out["protocols"]) + require.Equal(t, float64(4096), out["readBufferCount"]) } -func TestAPIConfigSetUnknownField(t *testing.T) { +func TestAPIConfigGlobalPatchUnknownField(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() @@ -169,37 +171,125 @@ func TestAPIConfigSetUnknownField(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} - req, err := http.NewRequest("POST", "http://localhost:9997/v2/config/set", bytes.NewReader(byts)) - require.NoError(t, err) + func() { + req, err := http.NewRequest(http.MethodPatch, "http://localhost:9997/v3/config/global/patch", bytes.NewReader(byts)) + require.NoError(t, err) - res, err := hc.Do(req) - require.NoError(t, err) - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }() } -func TestAPIConfigPathsAdd(t *testing.T) { +func TestAPIConfigPathDefaultsGet(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() hc := &http.Client{Transport: &http.Transport{}} - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{ - "source": "rtsp://127.0.0.1:9999/mypath", - "sourceOnDemand": true, + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out) + require.Equal(t, "publisher", out["source"]) +} + +func TestAPIConfigPathDefaultsPatch(t *testing.T) { + p, ok := newInstance("api: yes\n") + require.Equal(t, true, ok) + defer p.Close() + + hc := &http.Client{Transport: &http.Transport{}} + + httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch", map[string]interface{}{ + "readUser": "myuser", + "readPass": "mypass", }, nil) - var out struct { - Paths map[string]struct { - Source string `json:"source"` - SourceOnDemandStartTimeout string `json:"sourceOnDemandStartTimeout"` - } `json:"paths"` + time.Sleep(500 * time.Millisecond) + + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out) + require.Equal(t, "myuser", out["readUser"]) + require.Equal(t, "mypass", out["readPass"]) +} + +func TestAPIConfigPathsList(t *testing.T) { + p, ok := newInstance("api: yes\n" + + "paths:\n" + + " path1:\n" + + " readUser: myuser\n" + + " readPass: mypass\n" + + " path2:\n" + + " readUser: myuser\n" + + " readPass: mypass\n") + require.Equal(t, true, ok) + defer p.Close() + + type pathConfig map[string]interface{} + + type listRes struct { + ItemCount int `json:"itemCount"` + PageCount int `json:"pageCount"` + Items []pathConfig `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out) - require.Equal(t, "rtsp://127.0.0.1:9999/mypath", out.Paths["my/path"].Source) - require.Equal(t, "10s", out.Paths["my/path"].SourceOnDemandStartTimeout) + hc := &http.Client{Transport: &http.Transport{}} + + var out listRes + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/list", nil, &out) + require.Equal(t, listRes{ + ItemCount: 2, + PageCount: 1, + Items: []pathConfig{ + { + "readUser": "myuser", + "readPass": "mypass", + }, + { + "readUser": "myuser", + "readPass": "mypass", + }, + }, + }, out) +} + +func TestAPIConfigPathsGet(t *testing.T) { + p, ok := newInstance("api: yes\n" + + "paths:\n" + + " my/path:\n" + + " readUser: myuser\n" + + " readPass: mypass\n") + require.Equal(t, true, ok) + defer p.Close() + + hc := &http.Client{Transport: &http.Transport{}} + + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out) + require.Equal(t, "myuser", out["readUser"]) +} + +func TestAPIConfigPathsAdd(t *testing.T) { + p, ok := newInstance("api: yes\n") + require.Equal(t, true, ok) + defer p.Close() + + hc := &http.Client{Transport: &http.Transport{}} + + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{ + "source": "rtsp://127.0.0.1:9999/mypath", + "sourceOnDemand": true, + "disablePublisherOverride": true, // test setting a deprecated parameter + }, nil) + + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out) + require.Equal(t, map[string]interface{}{ + "source": "rtsp://127.0.0.1:9999/mypath", + "sourceOnDemand": true, + "disablePublisherOverride": true, + }, out) } func TestAPIConfigPathsAddUnknownField(t *testing.T) { @@ -216,61 +306,94 @@ func TestAPIConfigPathsAddUnknownField(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} - req, err := http.NewRequest("POST", "http://localhost:9997/v2/config/paths/add/my/path", bytes.NewReader(byts)) - require.NoError(t, err) + func() { + req, err := http.NewRequest(http.MethodPost, + "http://localhost:9997/v3/config/paths/add/my/path", bytes.NewReader(byts)) + require.NoError(t, err) - res, err := hc.Do(req) - require.NoError(t, err) - defer res.Body.Close() - require.Equal(t, http.StatusBadRequest, res.StatusCode) + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }() } -func TestAPIConfigPathsEdit(t *testing.T) { +func TestAPIConfigPathsPatch(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() hc := &http.Client{Transport: &http.Transport{}} - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{ - "source": "rtsp://127.0.0.1:9999/mypath", + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{ + "source": "rtsp://127.0.0.1:9999/mypath", + "sourceOnDemand": true, + "disablePublisherOverride": true, // test setting a deprecated parameter + }, nil) + + httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/my/path", map[string]interface{}{ + "source": "rtsp://127.0.0.1:9998/mypath", "sourceOnDemand": true, }, nil) - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/my/path", map[string]interface{}{ + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out) + require.Equal(t, map[string]interface{}{ + "source": "rtsp://127.0.0.1:9998/mypath", + "sourceOnDemand": true, + "disablePublisherOverride": true, + }, out) +} + +func TestAPIConfigPathsReplace(t *testing.T) { + p, ok := newInstance("api: yes\n") + require.Equal(t, true, ok) + defer p.Close() + + hc := &http.Client{Transport: &http.Transport{}} + + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{ + "source": "rtsp://127.0.0.1:9999/mypath", + "sourceOnDemand": true, + "disablePublisherOverride": true, // test setting a deprecated parameter + }, nil) + + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/replace/my/path", map[string]interface{}{ "source": "rtsp://127.0.0.1:9998/mypath", "sourceOnDemand": true, }, nil) - var out struct { - Paths map[string]struct { - Source string `json:"source"` - } `json:"paths"` - } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out) - require.Equal(t, "rtsp://127.0.0.1:9998/mypath", out.Paths["my/path"].Source) + var out map[string]interface{} + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil, &out) + require.Equal(t, map[string]interface{}{ + "source": "rtsp://127.0.0.1:9998/mypath", + "sourceOnDemand": true, + }, out) } -func TestAPIConfigPathsRemove(t *testing.T) { +func TestAPIConfigPathsDelete(t *testing.T) { p, ok := newInstance("api: yes\n") require.Equal(t, true, ok) defer p.Close() hc := &http.Client{Transport: &http.Transport{}} - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/add/my/path", map[string]interface{}{ + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/config/paths/add/my/path", map[string]interface{}{ "source": "rtsp://127.0.0.1:9999/mypath", "sourceOnDemand": true, }, nil) - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/remove/my/path", nil, nil) + httpRequest(t, hc, http.MethodDelete, "http://localhost:9997/v3/config/paths/delete/my/path", nil, nil) - var out struct { - Paths map[string]interface{} `json:"paths"` - } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/config/get", nil, &out) - _, ok = out.Paths["my/path"] - require.Equal(t, false, ok) + func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/config/paths/get/my/path", nil) + require.NoError(t, err) + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusNotFound, res.StatusCode) + }() } func TestAPIPathsList(t *testing.T) { @@ -323,7 +446,7 @@ func TestAPIPathsList(t *testing.T) { require.NoError(t, err) var out pathList - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out) require.Equal(t, pathList{ ItemCount: 1, PageCount: 1, @@ -369,7 +492,7 @@ func TestAPIPathsList(t *testing.T) { defer source.Close() var out pathList - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out) require.Equal(t, pathList{ ItemCount: 1, PageCount: 1, @@ -396,7 +519,7 @@ func TestAPIPathsList(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} var out pathList - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out) require.Equal(t, pathList{ ItemCount: 1, PageCount: 1, @@ -423,7 +546,7 @@ func TestAPIPathsList(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} var out pathList - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out) require.Equal(t, pathList{ ItemCount: 1, PageCount: 1, @@ -450,7 +573,7 @@ func TestAPIPathsList(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} var out pathList - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/list", nil, &out) require.Equal(t, pathList{ ItemCount: 1, PageCount: 1, @@ -508,7 +631,7 @@ func TestAPIPathsGet(t *testing.T) { defer source.Close() var out path - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/paths/get/"+pathName, nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/paths/get/"+pathName, nil, &out) require.Equal(t, path{ Name: pathName, Source: pathSource{ @@ -518,7 +641,7 @@ func TestAPIPathsGet(t *testing.T) { Tracks: []string{"H264"}, }, out) } else { - res, err := hc.Get("http://localhost:9997/v2/paths/get/" + pathName) + res, err := hc.Get("http://localhost:9997/v3/paths/get/" + pathName) require.NoError(t, err) defer res.Body.Close() require.Equal(t, 404, res.StatusCode) @@ -765,7 +888,7 @@ func TestAPIProtocolList(t *testing.T) { ItemCount int `json:"itemCount"` Items []item `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out) if ca != "rtsp conns" && ca != "rtsps conns" { require.Equal(t, item{ @@ -784,7 +907,7 @@ func TestAPIProtocolList(t *testing.T) { ItemCount int `json:"itemCount"` Items []item `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/hlsmuxers/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/hlsmuxers/list", nil, &out) s := fmt.Sprintf("^%d-", time.Now().Year()) require.Regexp(t, s, out.Items[0].Created) @@ -801,7 +924,7 @@ func TestAPIProtocolList(t *testing.T) { ItemCount int `json:"itemCount"` Items []item `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/list", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/list", nil, &out) require.Equal(t, item{ PeerConnectionEstablished: true, @@ -1050,14 +1173,14 @@ func TestAPIProtocolGet(t *testing.T) { var out1 struct { Items []item `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out1) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1) if ca != "rtsp conns" && ca != "rtsps conns" { require.Equal(t, "publish", out1.Items[0].State) } var out2 item - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/get/"+out1.Items[0].ID, nil, &out2) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+out1.Items[0].ID, nil, &out2) if ca != "rtsp conns" && ca != "rtsps conns" { require.Equal(t, "publish", out2.State) @@ -1070,7 +1193,7 @@ func TestAPIProtocolGet(t *testing.T) { } var out item - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/hlsmuxers/get/mypath", nil, &out) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/hlsmuxers/get/mypath", nil, &out) s := fmt.Sprintf("^%d-", time.Now().Year()) require.Regexp(t, s, out.Created) @@ -1091,10 +1214,10 @@ func TestAPIProtocolGet(t *testing.T) { var out1 struct { Items []item `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/list", nil, &out1) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/list", nil, &out1) var out2 item - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/webrtcsessions/get/"+out1.Items[0].ID, nil, &out2) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/webrtcsessions/get/"+out1.Items[0].ID, nil, &out2) require.Equal(t, true, out2.PeerConnectionEstablished) } @@ -1178,7 +1301,7 @@ func TestAPIProtocolGetNotFound(t *testing.T) { } func() { - req, err := http.NewRequest("GET", "http://localhost:9997/v2/"+pa+"/get/"+uuid.New().String(), nil) + req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/"+pa+"/get/"+uuid.New().String(), nil) require.NoError(t, err) res, err := hc.Do(req) @@ -1310,16 +1433,16 @@ func TestAPIProtocolKick(t *testing.T) { ID string `json:"id"` } `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out1) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out1) - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/"+pa+"/kick/"+out1.Items[0].ID, nil, nil) + httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v3/"+pa+"/kick/"+out1.Items[0].ID, nil, nil) var out2 struct { Items []struct { ID string `json:"id"` } `json:"items"` } - httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v2/"+pa+"/list", nil, &out2) + httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out2) require.Equal(t, 0, len(out2.Items)) }) } @@ -1379,7 +1502,7 @@ func TestAPIProtocolKickNotFound(t *testing.T) { } func() { - req, err := http.NewRequest("GET", "http://localhost:9997/v2/"+pa+"/kick/"+uuid.New().String(), nil) + req, err := http.NewRequest(http.MethodGet, "http://localhost:9997/v3/"+pa+"/kick/"+uuid.New().String(), nil) require.NoError(t, err) res, err := hc.Do(req) diff --git a/internal/core/authentication.go b/internal/core/authentication.go index 61aaf25b..6bf8631d 100644 --- a/internal/core/authentication.go +++ b/internal/core/authentication.go @@ -115,7 +115,7 @@ func doAuthentication( externalAuthenticationURL string, rtspAuthMethods conf.AuthMethods, pathName string, - pathConf *conf.PathConf, + pathConf *conf.Path, publish bool, credentials authCredentials, ) error { diff --git a/internal/core/hls_manager.go b/internal/core/hls_manager.go index 2f43ccae..5e321a64 100644 --- a/internal/core/hls_manager.go +++ b/internal/core/hls_manager.go @@ -11,7 +11,7 @@ import ( ) type hlsManagerAPIMuxersListRes struct { - data *apiHLSMuxersList + data *apiHLSMuxerList err error } @@ -189,7 +189,7 @@ outer: delete(m.muxers, c.PathName()) case req := <-m.chAPIMuxerList: - data := &apiHLSMuxersList{ + data := &apiHLSMuxerList{ Items: []*apiHLSMuxer{}, } @@ -275,7 +275,7 @@ func (m *hlsManager) pathNotReady(pa *path) { } // apiMuxersList is called by api. -func (m *hlsManager) apiMuxersList() (*apiHLSMuxersList, error) { +func (m *hlsManager) apiMuxersList() (*apiHLSMuxerList, error) { req := hlsManagerAPIMuxersListReq{ res: make(chan hlsManagerAPIMuxersListRes), } diff --git a/internal/core/hls_source.go b/internal/core/hls_source.go index f876710a..fa2d5b14 100644 --- a/internal/core/hls_source.go +++ b/internal/core/hls_source.go @@ -39,7 +39,7 @@ func (s *hlsSource) Log(level logger.Level, format string, args ...interface{}) } // run implements sourceStaticImpl. -func (s *hlsSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { +func (s *hlsSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { var stream *stream.Stream defer func() { diff --git a/internal/core/metrics.go b/internal/core/metrics.go index a05fe218..8763c345 100644 --- a/internal/core/metrics.go +++ b/internal/core/metrics.go @@ -86,7 +86,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { if err == nil && len(data.Items) != 0 { for _, i := range data.Items { var state string - if i.SourceReady { + if i.Ready { state = "ready" } else { state = "notReady" diff --git a/internal/core/path.go b/internal/core/path.go index 951b2cf2..a9776e4a 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -76,7 +76,7 @@ type pathRemovePublisherReq struct { } type pathGetConfForPathRes struct { - conf *conf.PathConf + conf *conf.Path err error } @@ -146,7 +146,7 @@ type pathStopPublisherReq struct { } type pathAPIPathsListRes struct { - data *apiPathsList + data *apiPathList paths map[string]*path } @@ -176,7 +176,7 @@ type path struct { recordPartDuration conf.StringDuration recordSegmentDuration conf.StringDuration confName string - conf *conf.PathConf + conf *conf.Path name string matches []string wg *sync.WaitGroup @@ -203,7 +203,7 @@ type path struct { onDemandPublisherCloseTimer *time.Timer // in - chReloadConf chan *conf.PathConf + chReloadConf chan *conf.Path chSourceStaticSetReady chan pathSourceStaticSetReadyReq chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq chDescribe chan pathDescribeReq @@ -231,7 +231,7 @@ func newPath( recordPartDuration conf.StringDuration, recordSegmentDuration conf.StringDuration, confName string, - cnf *conf.PathConf, + cnf *conf.Path, name string, matches []string, wg *sync.WaitGroup, @@ -264,7 +264,7 @@ func newPath( onDemandStaticSourceCloseTimer: newEmptyTimer(), onDemandPublisherReadyTimer: newEmptyTimer(), onDemandPublisherCloseTimer: newEmptyTimer(), - chReloadConf: make(chan *conf.PathConf), + chReloadConf: make(chan *conf.Path), chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq), chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq), chDescribe: make(chan pathDescribeReq), @@ -505,7 +505,7 @@ func (pa *path) doOnDemandPublisherCloseTimer() { pa.onDemandPublisherStop("not needed by anyone") } -func (pa *path) doReloadConf(newConf *conf.PathConf) { +func (pa *path) doReloadConf(newConf *conf.Path) { pa.confMutex.Lock() pa.conf = newConf pa.confMutex.Unlock() @@ -741,7 +741,6 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { data: &apiPath{ Name: pa.name, ConfName: pa.confName, - Conf: pa.conf, Source: func() *apiPathSourceOrReader { if pa.source == nil { return nil @@ -749,8 +748,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { v := pa.source.apiSourceDescribe() return &v }(), - SourceReady: pa.stream != nil, - Ready: pa.stream != nil, + Ready: pa.stream != nil, ReadyTime: func() *time.Time { if pa.stream == nil { return nil @@ -781,7 +779,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) { } } -func (pa *path) safeConf() *conf.PathConf { +func (pa *path) safeConf() *conf.Path { pa.confMutex.RLock() defer pa.confMutex.RUnlock() return pa.conf @@ -1044,7 +1042,7 @@ func (pa *path) addReaderPost(req pathAddReaderReq) { } // reloadConf is called by pathManager. -func (pa *path) reloadConf(newConf *conf.PathConf) { +func (pa *path) reloadConf(newConf *conf.Path) { select { case pa.chReloadConf <- newConf: case <-pa.ctx.Done(): diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index 5ae81adb..271cb14e 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -11,7 +11,7 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" ) -func pathConfCanBeUpdated(oldPathConf *conf.PathConf, newPathConf *conf.PathConf) bool { +func pathConfCanBeUpdated(oldPathConf *conf.Path, newPathConf *conf.Path) bool { clone := oldPathConf.Clone() clone.Record = newPathConf.Record @@ -32,7 +32,7 @@ func pathConfCanBeUpdated(oldPathConf *conf.PathConf, newPathConf *conf.PathConf return newPathConf.Equal(clone) } -func getConfForPath(pathConfs map[string]*conf.PathConf, name string) (string, *conf.PathConf, []string, error) { +func getConfForPath(pathConfs map[string]*conf.Path, name string) (string, *conf.Path, []string, error) { err := conf.IsValidPathName(name) if err != nil { return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name) @@ -77,7 +77,7 @@ type pathManager struct { recordPath string recordPartDuration conf.StringDuration recordSegmentDuration conf.StringDuration - pathConfs map[string]*conf.PathConf + pathConfs map[string]*conf.Path externalCmdPool *externalcmd.Pool metrics *metrics parent pathManagerParent @@ -90,7 +90,7 @@ type pathManager struct { pathsByConf map[string]map[*path]struct{} // in - chReloadConf chan map[string]*conf.PathConf + chReloadConf chan map[string]*conf.Path chSetHLSManager chan pathManagerHLSManager chClosePath chan *path chPathReady chan *path @@ -115,7 +115,7 @@ func newPathManager( recordPath string, recordPartDuration conf.StringDuration, recordSegmentDuration conf.StringDuration, - pathConfs map[string]*conf.PathConf, + pathConfs map[string]*conf.Path, externalCmdPool *externalcmd.Pool, metrics *metrics, parent pathManagerParent, @@ -142,7 +142,7 @@ func newPathManager( ctxCancel: ctxCancel, paths: make(map[string]*path), pathsByConf: make(map[string]map[*path]struct{}), - chReloadConf: make(chan map[string]*conf.PathConf), + chReloadConf: make(chan map[string]*conf.Path), chSetHLSManager: make(chan pathManagerHLSManager), chClosePath: make(chan *path), chPathReady: make(chan *path), @@ -190,8 +190,8 @@ func (pm *pathManager) run() { outer: for { select { - case newPathConfs := <-pm.chReloadConf: - pm.doReloadConf(newPathConfs) + case newPaths := <-pm.chReloadConf: + pm.doReloadConf(newPaths) case m := <-pm.chSetHLSManager: pm.doSetHLSManager(m) @@ -235,14 +235,14 @@ outer: } } -func (pm *pathManager) doReloadConf(newPathConfs map[string]*conf.PathConf) { +func (pm *pathManager) doReloadConf(newPaths map[string]*conf.Path) { for confName, pathConf := range pm.pathConfs { - if newPathConf, ok := newPathConfs[confName]; ok { + if newPath, ok := newPaths[confName]; ok { // configuration has changed - if !newPathConf.Equal(pathConf) { - if pathConfCanBeUpdated(pathConf, newPathConf) { // paths associated with the configuration can be updated + if !newPath.Equal(pathConf) { + if pathConfCanBeUpdated(pathConf, newPath) { // paths associated with the configuration can be updated for pa := range pm.pathsByConf[confName] { - go pa.reloadConf(newPathConf) + go pa.reloadConf(newPath) } } else { // paths associated with the configuration must be recreated for pa := range pm.pathsByConf[confName] { @@ -262,7 +262,7 @@ func (pm *pathManager) doReloadConf(newPathConfs map[string]*conf.PathConf) { } } - pm.pathConfs = newPathConfs + pm.pathConfs = newPaths // add new paths for pathConfName, pathConf := range pm.pathConfs { @@ -401,7 +401,7 @@ func (pm *pathManager) doAPIPathsGet(req pathAPIPathsGetReq) { func (pm *pathManager) createPath( pathConfName string, - pathConf *conf.PathConf, + pathConf *conf.Path, name string, matches []string, ) { @@ -441,7 +441,7 @@ func (pm *pathManager) removePath(pa *path) { } // confReload is called by core. -func (pm *pathManager) confReload(pathConfs map[string]*conf.PathConf) { +func (pm *pathManager) confReload(pathConfs map[string]*conf.Path) { select { case pm.chReloadConf <- pathConfs: case <-pm.ctx.Done(): @@ -553,7 +553,7 @@ func (pm *pathManager) setHLSManager(s pathManagerHLSManager) { } // apiPathsList is called by api. -func (pm *pathManager) apiPathsList() (*apiPathsList, error) { +func (pm *pathManager) apiPathsList() (*apiPathList, error) { req := pathAPIPathsListReq{ res: make(chan pathAPIPathsListRes), } @@ -562,7 +562,7 @@ func (pm *pathManager) apiPathsList() (*apiPathsList, error) { case pm.chAPIPathsList <- req: res := <-req.res - res.data = &apiPathsList{ + res.data = &apiPathList{ Items: []*apiPath{}, } diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 60e5fb53..50cbd261 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -455,13 +455,13 @@ func TestPathRecord(t *testing.T) { hc := &http.Client{Transport: &http.Transport{}} - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/all", map[string]interface{}{ + httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all", map[string]interface{}{ "record": false, }, nil) time.Sleep(500 * time.Millisecond) - httpRequest(t, hc, http.MethodPost, "http://localhost:9997/v2/config/paths/edit/all", map[string]interface{}{ + httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/paths/patch/all", map[string]interface{}{ "record": true, }, nil) diff --git a/internal/core/rpicamera_source.go b/internal/core/rpicamera_source.go index 26282b48..e1b7ced4 100644 --- a/internal/core/rpicamera_source.go +++ b/internal/core/rpicamera_source.go @@ -14,7 +14,7 @@ import ( "github.com/bluenviron/mediamtx/internal/unit" ) -func paramsFromConf(cnf *conf.PathConf) rpicamera.Params { +func paramsFromConf(cnf *conf.Path) rpicamera.Params { return rpicamera.Params{ CameraID: cnf.RPICameraCamID, Width: cnf.RPICameraWidth, @@ -74,7 +74,7 @@ func (s *rpiCameraSource) Log(level logger.Level, format string, args ...interfa } // run implements sourceStaticImpl. -func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { +func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { medi := &description.Media{ Type: description.MediaTypeVideo, Formats: []format.Format{&format.H264{ diff --git a/internal/core/rtmp_server.go b/internal/core/rtmp_server.go index 3553195e..d8a6e043 100644 --- a/internal/core/rtmp_server.go +++ b/internal/core/rtmp_server.go @@ -16,7 +16,7 @@ import ( ) type rtmpServerAPIConnsListRes struct { - data *apiRTMPConnsList + data *apiRTMPConnList err error } @@ -203,7 +203,7 @@ outer: delete(s.conns, c) case req := <-s.chAPIConnsList: - data := &apiRTMPConnsList{ + data := &apiRTMPConnList{ Items: []*apiRTMPConn{}, } @@ -286,7 +286,7 @@ func (s *rtmpServer) closeConn(c *rtmpConn) { } // apiConnsList is called by api. -func (s *rtmpServer) apiConnsList() (*apiRTMPConnsList, error) { +func (s *rtmpServer) apiConnsList() (*apiRTMPConnList, error) { req := rtmpServerAPIConnsListReq{ res: make(chan rtmpServerAPIConnsListRes), } diff --git a/internal/core/rtmp_source.go b/internal/core/rtmp_source.go index 8ffc17aa..1c244a64 100644 --- a/internal/core/rtmp_source.go +++ b/internal/core/rtmp_source.go @@ -47,7 +47,7 @@ func (s *rtmpSource) Log(level logger.Level, format string, args ...interface{}) } // run implements sourceStaticImpl. -func (s *rtmpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { +func (s *rtmpSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { s.Log(logger.Debug, "connecting") u, err := url.Parse(cnf.Source) diff --git a/internal/core/rtsp_server.go b/internal/core/rtsp_server.go index bcab3d11..1ba013d9 100644 --- a/internal/core/rtsp_server.go +++ b/internal/core/rtsp_server.go @@ -405,7 +405,7 @@ func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*apiRTSPConn, error) { } // apiSessionsList is called by api and metrics. -func (s *rtspServer) apiSessionsList() (*apiRTSPSessionsList, error) { +func (s *rtspServer) apiSessionsList() (*apiRTSPSessionList, error) { select { case <-s.ctx.Done(): return nil, fmt.Errorf("terminated") @@ -415,7 +415,7 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionsList, error) { s.mutex.RLock() defer s.mutex.RUnlock() - data := &apiRTSPSessionsList{ + data := &apiRTSPSessionList{ Items: []*apiRTSPSession{}, } diff --git a/internal/core/rtsp_source.go b/internal/core/rtsp_source.go index 8470b327..c5b98c5d 100644 --- a/internal/core/rtsp_source.go +++ b/internal/core/rtsp_source.go @@ -14,7 +14,7 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" ) -func createRangeHeader(cnf *conf.PathConf) (*headers.Range, error) { +func createRangeHeader(cnf *conf.Path) (*headers.Range, error) { switch cnf.RTSPRangeType { case conf.RTSPRangeTypeClock: start, err := time.Parse("20060102T150405Z", cnf.RTSPRangeStart) @@ -91,7 +91,7 @@ func (s *rtspSource) Log(level logger.Level, format string, args ...interface{}) } // run implements sourceStaticImpl. -func (s *rtspSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { +func (s *rtspSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { s.Log(logger.Debug, "connecting") decodeErrLogger := logger.NewLimitedLogger(s) diff --git a/internal/core/source_static.go b/internal/core/source_static.go index 246fc6be..3fba226e 100644 --- a/internal/core/source_static.go +++ b/internal/core/source_static.go @@ -16,7 +16,7 @@ const ( type sourceStaticImpl interface { logger.Writer - run(context.Context, *conf.PathConf, chan *conf.PathConf) error + run(context.Context, *conf.Path, chan *conf.Path) error apiSourceDescribe() apiPathSourceOrReader } @@ -28,7 +28,7 @@ type sourceStaticParent interface { // sourceStatic is a static source. type sourceStatic struct { - conf *conf.PathConf + conf *conf.Path parent sourceStaticParent ctx context.Context @@ -37,7 +37,7 @@ type sourceStatic struct { running bool // in - chReloadConf chan *conf.PathConf + chReloadConf chan *conf.Path chSourceStaticImplSetReady chan pathSourceStaticSetReadyReq chSourceStaticImplSetNotReady chan pathSourceStaticSetNotReadyReq @@ -46,7 +46,7 @@ type sourceStatic struct { } func newSourceStatic( - cnf *conf.PathConf, + cnf *conf.Path, readTimeout conf.StringDuration, writeTimeout conf.StringDuration, writeQueueSize int, @@ -55,7 +55,7 @@ func newSourceStatic( s := &sourceStatic{ conf: cnf, parent: parent, - chReloadConf: make(chan *conf.PathConf), + chReloadConf: make(chan *conf.Path), chSourceStaticImplSetReady: make(chan pathSourceStaticSetReadyReq), chSourceStaticImplSetNotReady: make(chan pathSourceStaticSetNotReadyReq), } @@ -153,7 +153,7 @@ func (s *sourceStatic) run() { var innerCtx context.Context var innerCtxCancel func() implErr := make(chan error) - innerReloadConf := make(chan *conf.PathConf) + innerReloadConf := make(chan *conf.Path) recreate := func() { innerCtx, innerCtxCancel = context.WithCancel(context.Background()) @@ -208,7 +208,7 @@ func (s *sourceStatic) run() { } } -func (s *sourceStatic) reloadConf(newConf *conf.PathConf) { +func (s *sourceStatic) reloadConf(newConf *conf.Path) { select { case s.chReloadConf <- newConf: case <-s.ctx.Done(): diff --git a/internal/core/srt_server.go b/internal/core/srt_server.go index 0edebae8..0b2d5871 100644 --- a/internal/core/srt_server.go +++ b/internal/core/srt_server.go @@ -25,7 +25,7 @@ type srtNewConnReq struct { } type srtServerAPIConnsListRes struct { - data *apiSRTConnsList + data *apiSRTConnList err error } @@ -191,7 +191,7 @@ outer: delete(s.conns, c) case req := <-s.chAPIConnsList: - data := &apiSRTConnsList{ + data := &apiSRTConnList{ Items: []*apiSRTConn{}, } @@ -279,7 +279,7 @@ func (s *srtServer) closeConn(c *srtConn) { } // apiConnsList is called by api. -func (s *srtServer) apiConnsList() (*apiSRTConnsList, error) { +func (s *srtServer) apiConnsList() (*apiSRTConnList, error) { req := srtServerAPIConnsListReq{ res: make(chan srtServerAPIConnsListRes), } diff --git a/internal/core/srt_source.go b/internal/core/srt_source.go index bbe90936..cd882a06 100644 --- a/internal/core/srt_source.go +++ b/internal/core/srt_source.go @@ -41,7 +41,7 @@ func (s *srtSource) Log(level logger.Level, format string, args ...interface{}) } // run implements sourceStaticImpl. -func (s *srtSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { +func (s *srtSource) run(ctx context.Context, cnf *conf.Path, reloadConf chan *conf.Path) error { s.Log(logger.Debug, "connecting") conf := srt.DefaultConfig() diff --git a/internal/core/udp_source.go b/internal/core/udp_source.go index b0dca1ff..cd21477b 100644 --- a/internal/core/udp_source.go +++ b/internal/core/udp_source.go @@ -66,7 +66,7 @@ func (s *udpSource) Log(level logger.Level, format string, args ...interface{}) } // run implements sourceStaticImpl. -func (s *udpSource) run(ctx context.Context, cnf *conf.PathConf, _ chan *conf.PathConf) error { +func (s *udpSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) error { s.Log(logger.Debug, "connecting") hostPort := cnf.Source[len("udp://"):] diff --git a/internal/core/webrtc_manager.go b/internal/core/webrtc_manager.go index fc0606d8..a3a95407 100644 --- a/internal/core/webrtc_manager.go +++ b/internal/core/webrtc_manager.go @@ -232,7 +232,7 @@ func webrtcNewAPI( } type webRTCManagerAPISessionsListRes struct { - data *apiWebRTCSessionsList + data *apiWebRTCSessionList err error } @@ -483,7 +483,7 @@ outer: req.res <- webRTCAddSessionCandidatesRes{sx: sx} case req := <-m.chAPISessionsList: - data := &apiWebRTCSessionsList{ + data := &apiWebRTCSessionList{ Items: []*apiWebRTCSession{}, } @@ -620,7 +620,7 @@ func (m *webRTCManager) addSessionCandidates( } // apiSessionsList is called by api. -func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionsList, error) { +func (m *webRTCManager) apiSessionsList() (*apiWebRTCSessionList, error) { req := webRTCManagerAPISessionsListReq{ res: make(chan webRTCManagerAPISessionsListRes), } diff --git a/internal/core/webrtc_source.go b/internal/core/webrtc_source.go index 5f81900b..99db75d9 100644 --- a/internal/core/webrtc_source.go +++ b/internal/core/webrtc_source.go @@ -48,7 +48,7 @@ func (s *webRTCSource) Log(level logger.Level, format string, args ...interface{ } // run implements sourceStaticImpl. -func (s *webRTCSource) run(ctx context.Context, cnf *conf.PathConf, _ chan *conf.PathConf) error { +func (s *webRTCSource) run(ctx context.Context, cnf *conf.Path, _ chan *conf.Path) error { s.Log(logger.Debug, "connecting") u, err := url.Parse(cnf.Source) diff --git a/mediamtx.yml b/mediamtx.yml index e74ecfa3..248c2272 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -1,7 +1,12 @@ ############################################### -# General settings +# Global settings -# Sets the verbosity of the program; available values are "error", "warn", "info", "debug". +# Settings in this section are applied anywhere. + +############################################### +# Global settings -> General + +# Verbosity of the program; available values are "error", "warn", "info", "debug". logLevel: info # Destinations of log messages; available values are "stdout", "file" and "syslog". logDestinations: [stdout] @@ -65,7 +70,7 @@ runOnConnectRestart: no runOnDisconnect: ############################################### -# RTSP settings +# Global settings -> RTSP # Allow publishing and reading streams with the RTSP protocol. rtsp: yes @@ -105,7 +110,7 @@ serverCert: server.crt authMethods: [basic] ############################################### -# RTMP settings +# Global settings -> RTMP # Allow publishing and reading streams with the RTMP protocol. rtmp: yes @@ -125,7 +130,7 @@ rtmpServerKey: server.key rtmpServerCert: server.crt ############################################### -# HLS settings +# Global settings -> HLS # Allow reading streams with the HLS protocol. hls: yes @@ -181,7 +186,7 @@ hlsTrustedProxies: [] hlsDirectory: '' ############################################### -# WebRTC settings +# Global settings -> WebRTC # Allow publishing and reading streams with the WebRTC protocol. webrtc: yes @@ -233,7 +238,7 @@ webrtcICEUDPMuxAddress: webrtcICETCPMuxAddress: ############################################### -# SRT settings +# Global settings -> SRT # Allow publishing and reading streams with the SRT protocol. srt: yes @@ -241,7 +246,7 @@ srt: yes srtAddress: :8890 ############################################### -# Recording settings +# Global settings -> Recording # Record streams to disk. record: no @@ -262,268 +267,282 @@ recordSegmentDuration: 1h # Set to 0s to disable automatic deletion. recordDeleteAfter: 24h +############################################### +# Default path settings + +# Settings in "pathDefaults" are applied anywhere, +# unless they are overridden in "paths". +pathDefaults: + + ############################################### + # Default path settings -> General + + # Source of the stream. This can be: + # * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client + # * rtsp://existing-url -> the stream is pulled from another RTSP server / camera + # * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS + # * rtmp://existing-url -> the stream is pulled from another RTMP server / camera + # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS + # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera + # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera with HTTPS + # * udp://ip:port -> the stream is pulled with UDP, by listening on the specified IP and port + # * srt://existing-url -> the stream is pulled from another SRT server / camera + # * whep://existing-url -> the stream is pulled from another WebRTC server / camera + # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS + # * redirect -> the stream is provided by another path or server + # * rpiCamera -> the stream is provided by a Raspberry Pi Camera + source: publisher + # If the source is a URL, and the source certificate is self-signed + # or invalid, you can provide the fingerprint of the certificate in order to + # validate it anyway. It can be obtained by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: + # If the source is a URL, it will be pulled only when at least + # one reader is connected, saving bandwidth. + sourceOnDemand: no + # If sourceOnDemand is "yes", readers will be put on hold until the source is + # ready or until this amount of time has passed. + sourceOnDemandStartTimeout: 10s + # If sourceOnDemand is "yes", the source will be closed when there are no + # readers connected and this amount of time has passed. + sourceOnDemandCloseAfter: 10s + # Maximum number of readers. Zero means no limit. + maxReaders: 0 + # SRT encryption passphrase require to read from this path + srtReadPassphrase: + # Record streams to disk (if global recording is enabled). + record: yes + + ############################################### + # Default path settings -> Authentication + + # Username required to publish. + # SHA256-hashed values can be inserted with the "sha256:" prefix. + publishUser: + # Password required to publish. + # SHA256-hashed values can be inserted with the "sha256:" prefix. + publishPass: + # IPs or networks (x.x.x.x/24) allowed to publish. + publishIPs: [] + + # Username required to read. + # SHA256-hashed values can be inserted with the "sha256:" prefix. + readUser: + # password required to read. + # SHA256-hashed values can be inserted with the "sha256:" prefix. + readPass: + # IPs or networks (x.x.x.x/24) allowed to read. + readIPs: [] + + ############################################### + # Default path settings -> Publisher source (when source is "publisher") + + # 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. + # 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 + srtPublishPassphrase: + + ############################################### + # 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 + # 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. + # available values: + # * clock: Absolute time + # * npt: Normal Play Time + # * smpte: SMPTE timestamps relative to the start of the recording + rtspRangeType: + # 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" + rtspRangeStart: + + ############################################### + # Default path settings -> Redirect source (when source is "redirect") + + # RTSP URL which clients will be redirected to. + sourceRedirect: + + ############################################### + # Default path settings -> Raspberry Pi Camera source (when source is "rpiCamera") + + # ID of the camera + rpiCameraCamID: 0 + # width of frames + rpiCameraWidth: 1920 + # height of frames + rpiCameraHeight: 1080 + # flip horizontally + rpiCameraHFlip: false + # flip vertically + rpiCameraVFlip: false + # brightness [-1, 1] + rpiCameraBrightness: 0 + # contrast [0, 16] + rpiCameraContrast: 1 + # saturation [0, 16] + rpiCameraSaturation: 1 + # sharpness [0, 16] + rpiCameraSharpness: 1 + # exposure mode. + # values: normal, short, long, custom + rpiCameraExposure: normal + # auto-white-balance mode. + # values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom + rpiCameraAWB: auto + # denoise operating mode. + # values: off, cdn_off, cdn_fast, cdn_hq + rpiCameraDenoise: "off" + # fixed shutter speed, in microseconds. + rpiCameraShutter: 0 + # metering mode of the AEC/AGC algorithm. + # values: centre, spot, matrix, custom + rpiCameraMetering: centre + # fixed gain + rpiCameraGain: 0 + # EV compensation of the image [-10, 10] + rpiCameraEV: 0 + # Region of interest, in format x,y,width,height + rpiCameraROI: + # whether to enable HDR on Raspberry Camera 3. + rpiCameraHDR: false + # tuning file + rpiCameraTuningFile: + # sensor mode, in format [width]:[height]:[bit-depth]:[packing] + # bit-depth and packing are optional. + rpiCameraMode: + # frames per second + rpiCameraFPS: 30 + # period between IDR frames + rpiCameraIDRPeriod: 60 + # bitrate + rpiCameraBitrate: 1000000 + # H264 profile + rpiCameraProfile: main + # H264 level + rpiCameraLevel: '4.1' + # Autofocus mode + # values: auto, manual, continuous + rpiCameraAfMode: auto + # Autofocus range + # values: normal, macro, full + rpiCameraAfRange: normal + # Autofocus speed + # values: normal, fast + rpiCameraAfSpeed: normal + # Lens position (for manual autofocus only), will be set to focus to a specific distance + # calculated by the following formula: d = 1 / value + # Examples: 0 moves the lens to infinity. + # 0.5 moves the lens to focus on objects 2m away. + # 2 moves the lens to focus on objects 50cm away. + rpiCameraLensPosition: 0.0 + # Specifies the autofocus window, in the form x,y,width,height where the coordinates + # are given as a proportion of the entire image. + rpiCameraAfWindow: + # enables printing text on each frame. + rpiCameraTextOverlayEnable: false + # text that is printed on each frame. + # format is the one of the strftime() function. + rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX' + + ############################################### + # Default path settings -> Hooks + + # Command to run when this path is initialized. + # This can be used to publish a stream when the server is launched. + # This is terminated with SIGINT when the program closes. + # 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. + runOnInit: + # Restart the command if it exits. + runOnInitRestart: no + + # Command to run when this path is requested by a reader. + # This can be used to publish a stream on demand. + # This is terminated with SIGINT when the path is not requested anymore. + # 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. + runOnDemand: + # Restart the command if it exits. + runOnDemandRestart: no + # Readers will be put on hold until the runOnDemand command starts publishing + # or until this amount of time has passed. + runOnDemandStartTimeout: 10s + # The command will be closed when there are no + # readers connected and this amount of time has passed. + runOnDemandCloseAfter: 10s + + # Command to run when the stream is ready to be read, whenever it is + # published by a client or pulled from a server / camera. + # This is terminated with SIGINT when the stream is not ready anymore. + # 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_SOURCE_TYPE: source type + # * MTX_SOURCE_ID: source ID + runOnReady: + # Restart the command if it exits. + runOnReadyRestart: no + # Command to run when the stream is not available anymore. + # Environment variables are the same of runOnReady. + runOnNotReady: + + # Command to run when a client starts reading. + # This is terminated with SIGINT when a client stops reading. + # 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_READER_TYPE: reader type + # * MTX_READER_ID: reader ID + runOnRead: + # Restart the command if it exits. + runOnReadRestart: no + # Command to run when a client stops reading. + # Environment variables are the same of runOnRead. + runOnUnread: + + # Command to run when a record 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: + ############################################### # Path settings -# These settings are path-dependent, and the map key is the name of the path. +# Settings in "paths" are applied to specific paths, and the map key +# is the name of the path. # 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". -# Settings under the path "all" are applied to all paths that do not match +# Settings under path "all" are applied to all paths that do not match # another entry. paths: all: - ############################################### - # General path settings - - # Source of the stream. This can be: - # * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client - # * rtsp://existing-url -> the stream is pulled from another RTSP server / camera - # * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS - # * rtmp://existing-url -> the stream is pulled from another RTMP server / camera - # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS - # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera - # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera with HTTPS - # * udp://ip:port -> the stream is pulled with UDP, by listening on the specified IP and port - # * srt://existing-url -> the stream is pulled from another SRT server / camera - # * whep://existing-url -> the stream is pulled from another WebRTC server / camera - # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS - # * redirect -> the stream is provided by another path or server - # * rpiCamera -> the stream is provided by a Raspberry Pi Camera - source: publisher - # If the source is a URL, and the source certificate is self-signed - # or invalid, you can provide the fingerprint of the certificate in order to - # validate it anyway. It can be obtained by running: - # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt - # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' - sourceFingerprint: - # If the source is a URL, it will be pulled only when at least - # one reader is connected, saving bandwidth. - sourceOnDemand: no - # If sourceOnDemand is "yes", readers will be put on hold until the source is - # ready or until this amount of time has passed. - sourceOnDemandStartTimeout: 10s - # If sourceOnDemand is "yes", the source will be closed when there are no - # readers connected and this amount of time has passed. - sourceOnDemandCloseAfter: 10s - # Maximum number of readers. Zero means no limit. - maxReaders: 0 - # SRT encryption passphrase require to read from this path - srtReadPassphrase: - # Record streams to disk (if global recording is enabled). - record: yes - - ############################################### - # Authentication path settings - - # Username required to publish. - # SHA256-hashed values can be inserted with the "sha256:" prefix. - publishUser: - # Password required to publish. - # SHA256-hashed values can be inserted with the "sha256:" prefix. - publishPass: - # IPs or networks (x.x.x.x/24) allowed to publish. - publishIPs: [] - - # Username required to read. - # SHA256-hashed values can be inserted with the "sha256:" prefix. - readUser: - # password required to read. - # SHA256-hashed values can be inserted with the "sha256:" prefix. - readPass: - # IPs or networks (x.x.x.x/24) allowed to read. - readIPs: [] - - ############################################### - # Publisher path settings (when source is "publisher") - - # 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. - # 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 - srtPublishPassphrase: - - ############################################### - # RTSP path settings (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 - # 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. - # available values: - # * clock: Absolute time - # * npt: Normal Play Time - # * smpte: SMPTE timestamps relative to the start of the recording - rtspRangeType: - # 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" - rtspRangeStart: - - ############################################### - # Redirect path settings (when source is "redirect") - - # RTSP URL which clients will be redirected to. - sourceRedirect: - - ############################################### - # Raspberry Pi Camera path settings (when source is "rpiCamera") - - # ID of the camera - rpiCameraCamID: 0 - # width of frames - rpiCameraWidth: 1920 - # height of frames - rpiCameraHeight: 1080 - # flip horizontally - rpiCameraHFlip: false - # flip vertically - rpiCameraVFlip: false - # brightness [-1, 1] - rpiCameraBrightness: 0 - # contrast [0, 16] - rpiCameraContrast: 1 - # saturation [0, 16] - rpiCameraSaturation: 1 - # sharpness [0, 16] - rpiCameraSharpness: 1 - # exposure mode. - # values: normal, short, long, custom - rpiCameraExposure: normal - # auto-white-balance mode. - # values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom - rpiCameraAWB: auto - # denoise operating mode. - # values: off, cdn_off, cdn_fast, cdn_hq - rpiCameraDenoise: "off" - # fixed shutter speed, in microseconds. - rpiCameraShutter: 0 - # metering mode of the AEC/AGC algorithm. - # values: centre, spot, matrix, custom - rpiCameraMetering: centre - # fixed gain - rpiCameraGain: 0 - # EV compensation of the image [-10, 10] - rpiCameraEV: 0 - # Region of interest, in format x,y,width,height - rpiCameraROI: - # whether to enable HDR on Raspberry Camera 3. - rpiCameraHDR: false - # tuning file - rpiCameraTuningFile: - # sensor mode, in format [width]:[height]:[bit-depth]:[packing] - # bit-depth and packing are optional. - rpiCameraMode: - # frames per second - rpiCameraFPS: 30 - # period between IDR frames - rpiCameraIDRPeriod: 60 - # bitrate - rpiCameraBitrate: 1000000 - # H264 profile - rpiCameraProfile: main - # H264 level - rpiCameraLevel: '4.1' - # Autofocus mode - # values: auto, manual, continuous - rpiCameraAfMode: auto - # Autofocus range - # values: normal, macro, full - rpiCameraAfRange: normal - # Autofocus speed - # values: normal, fast - rpiCameraAfSpeed: normal - # Lens position (for manual autofocus only), will be set to focus to a specific distance - # calculated by the following formula: d = 1 / value - # Examples: 0 moves the lens to infinity. - # 0.5 moves the lens to focus on objects 2m away. - # 2 moves the lens to focus on objects 50cm away. - rpiCameraLensPosition: 0.0 - # Specifies the autofocus window, in the form x,y,width,height where the coordinates - # are given as a proportion of the entire image. - rpiCameraAfWindow: - # enables printing text on each frame. - rpiCameraTextOverlayEnable: false - # text that is printed on each frame. - # format is the one of the strftime() function. - rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX' - - ############################################### - # Hooks path settings - - # Command to run when this path is initialized. - # This can be used to publish a stream when the server is launched. - # This is terminated with SIGINT when the program closes. - # 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. - runOnInit: - # Restart the command if it exits. - runOnInitRestart: no - - # Command to run when this path is requested by a reader. - # This can be used to publish a stream on demand. - # This is terminated with SIGINT when the path is not requested anymore. - # 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. - runOnDemand: - # Restart the command if it exits. - runOnDemandRestart: no - # Readers will be put on hold until the runOnDemand command starts publishing - # or until this amount of time has passed. - runOnDemandStartTimeout: 10s - # The command will be closed when there are no - # readers connected and this amount of time has passed. - runOnDemandCloseAfter: 10s - - # Command to run when the stream is ready to be read, whenever it is - # published by a client or pulled from a server / camera. - # This is terminated with SIGINT when the stream is not ready anymore. - # 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_SOURCE_TYPE: source type - # * MTX_SOURCE_ID: source ID - runOnReady: - # Restart the command if it exits. - runOnReadyRestart: no - # Command to run when the stream is not available anymore. - # Environment variables are the same of runOnReady. - runOnNotReady: - - # Command to run when a client starts reading. - # This is terminated with SIGINT when a client stops reading. - # 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_READER_TYPE: reader type - # * MTX_READER_ID: reader ID - runOnRead: - # Restart the command if it exits. - runOnReadRestart: no - # Command to run when a client stops reading. - # Environment variables are the same of runOnRead. - runOnUnread: - - # Command to run when a record 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: + + # any setting in "pathDefaults" can be overridden here. + # for instance: + # my_camera: + # source: rtsp://my_camera