Browse Source

allow changing default path settings; bump API in order to allow so (#2455)

pull/2457/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
9a01ab7fd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 284
      apidocs/openapi.yaml
  3. 6
      internal/conf/auth_method.go
  4. 406
      internal/conf/conf.go
  5. 82
      internal/conf/conf_test.go
  6. 6
      internal/conf/credential.go
  7. 6
      internal/conf/encryption.go
  8. 88
      internal/conf/env/env.go
  9. 38
      internal/conf/env/env_test.go
  10. 41
      internal/conf/global.go
  11. 6
      internal/conf/hls_variant.go
  12. 6
      internal/conf/ips_or_cidrs.go
  13. 6
      internal/conf/log_destination.go
  14. 6
      internal/conf/log_level.go
  15. 60
      internal/conf/optional_global.go
  16. 70
      internal/conf/optional_path.go
  17. 148
      internal/conf/path.go
  18. 6
      internal/conf/protocol.go
  19. 6
      internal/conf/rtsp_range_type.go
  20. 6
      internal/conf/source_protocol.go
  21. 6
      internal/conf/string_duration.go
  22. 4
      internal/conf/string_size.go
  23. 335
      internal/core/api.go
  24. 20
      internal/core/api_defs.go
  25. 269
      internal/core/api_test.go
  26. 2
      internal/core/authentication.go
  27. 6
      internal/core/hls_manager.go
  28. 2
      internal/core/hls_source.go
  29. 2
      internal/core/metrics.go
  30. 22
      internal/core/path.go
  31. 36
      internal/core/path_manager.go
  32. 4
      internal/core/path_test.go
  33. 4
      internal/core/rpicamera_source.go
  34. 6
      internal/core/rtmp_server.go
  35. 2
      internal/core/rtmp_source.go
  36. 4
      internal/core/rtsp_server.go
  37. 4
      internal/core/rtsp_source.go
  38. 14
      internal/core/source_static.go
  39. 6
      internal/core/srt_server.go
  40. 2
      internal/core/srt_source.go
  41. 2
      internal/core/udp_source.go
  42. 6
      internal/core/webrtc_manager.go
  43. 2
      internal/core/webrtc_source.go
  44. 547
      mediamtx.yml

2
README.md

@ -642,7 +642,7 @@ paths: @@ -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:

284
apidocs/openapi.yaml

@ -15,7 +15,7 @@ security: [] @@ -15,7 +15,7 @@ security: []
components:
schemas:
Conf:
GlobalConf:
type: object
properties:
# General
@ -195,12 +195,6 @@ components: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -431,7 +447,7 @@ components:
type: integer
format: int64
HLSMuxersList:
HLSMuxerList:
type: object
properties:
pageCount:
@ -462,7 +478,7 @@ components: @@ -462,7 +478,7 @@ components:
type: integer
format: int64
RTMPConnsList:
RTMPConnList:
type: object
properties:
pageCount:
@ -488,7 +504,7 @@ components: @@ -488,7 +504,7 @@ components:
type: integer
format: int64
RTSPConnsList:
RTSPConnList:
type: object
properties:
pageCount:
@ -522,7 +538,7 @@ components: @@ -522,7 +538,7 @@ components:
type: integer
format: int64
RTSPSessionsList:
RTSPSessionList:
type: object
properties:
pageCount:
@ -553,7 +569,7 @@ components: @@ -553,7 +569,7 @@ components:
type: integer
format: int64
SRTConnsList:
SRTConnList:
type: object
properties:
pageCount:
@ -590,7 +606,7 @@ components: @@ -590,7 +606,7 @@ components:
type: integer
format: int64
WebRTCSessionsList:
WebRTCSessionList:
type: object
properties:
pageCount:
@ -601,10 +617,10 @@ components: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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.

6
internal/conf/auth_method.go

@ -59,8 +59,8 @@ func (d *AuthMethods) UnmarshalJSON(b []byte) error { @@ -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)
}

406
internal/conf/conf.go

@ -6,6 +6,7 @@ import ( @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"
"time"
@ -20,7 +21,7 @@ import ( @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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) { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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
}

82
internal/conf/conf_test.go

@ -47,7 +47,7 @@ func TestConfFromFile(t *testing.T) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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)

6
internal/conf/credential.go

@ -36,7 +36,7 @@ func (d *Credential) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

6
internal/conf/encryption.go

@ -60,7 +60,7 @@ func (d *Encryption) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

88
internal/conf/env/env.go vendored

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
package env
import (
"encoding/json"
"fmt"
"os"
"reflect"
@ -10,8 +9,9 @@ import ( @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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

38
internal/conf/env/env_test.go vendored

@ -10,12 +10,12 @@ import ( @@ -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 { @@ -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) {

41
internal/conf/global.go

@ -0,0 +1,41 @@ @@ -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)
}

6
internal/conf/hls_variant.go

@ -55,7 +55,7 @@ func (d *HLSVariant) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

6
internal/conf/ips_or_cidrs.go

@ -50,9 +50,9 @@ func (d *IPsOrCIDRs) UnmarshalJSON(b []byte) error { @@ -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)
}

6
internal/conf/log_destination.go

@ -84,8 +84,8 @@ func (d *LogDestinations) UnmarshalJSON(b []byte) error { @@ -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)
}

6
internal/conf/log_level.go

@ -61,7 +61,7 @@ func (d *LogLevel) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

60
internal/conf/optional_global.go

@ -0,0 +1,60 @@ @@ -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)
}

70
internal/conf/optional_path.go

@ -0,0 +1,70 @@ @@ -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)
}

148
internal/conf/path.go

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
package conf
import (
"bytes"
"encoding/json"
"fmt"
"net"
@ -48,8 +47,8 @@ func srtCheckPassphrase(passphrase string) error { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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))
}

6
internal/conf/protocol.go

@ -74,8 +74,8 @@ func (d *Protocols) UnmarshalJSON(b []byte) error { @@ -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)
}

6
internal/conf/rtsp_range_type.go

@ -67,7 +67,7 @@ func (d *RTSPRangeType) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

6
internal/conf/source_protocol.go

@ -67,7 +67,7 @@ func (d *SourceProtocol) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

6
internal/conf/string_duration.go

@ -30,7 +30,7 @@ func (d *StringDuration) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

4
internal/conf/string_size.go

@ -30,7 +30,7 @@ func (s *StringSize) UnmarshalJSON(b []byte) error { @@ -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 + `"`))
}

335
internal/core/api.go

@ -6,6 +6,7 @@ import ( @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"reflect"
"sort"
"strconv"
"sync"
"time"
@ -24,68 +25,6 @@ func interfaceIsEmpty(i interface{}) bool { @@ -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 @@ -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) { @@ -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( @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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)
}

20
internal/core/api_defs.go

@ -8,6 +8,12 @@ import ( @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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"`

269
internal/core/api_test.go

@ -121,7 +121,7 @@ func TestPagination(t *testing.T) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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)

2
internal/core/authentication.go

@ -115,7 +115,7 @@ func doAuthentication( @@ -115,7 +115,7 @@ func doAuthentication(
externalAuthenticationURL string,
rtspAuthMethods conf.AuthMethods,
pathName string,
pathConf *conf.PathConf,
pathConf *conf.Path,
publish bool,
credentials authCredentials,
) error {

6
internal/core/hls_manager.go

@ -11,7 +11,7 @@ import ( @@ -11,7 +11,7 @@ import (
)
type hlsManagerAPIMuxersListRes struct {
data *apiHLSMuxersList
data *apiHLSMuxerList
err error
}
@ -189,7 +189,7 @@ outer: @@ -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) { @@ -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),
}

2
internal/core/hls_source.go

@ -39,7 +39,7 @@ func (s *hlsSource) Log(level logger.Level, format string, args ...interface{}) @@ -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() {

2
internal/core/metrics.go

@ -86,7 +86,7 @@ func (m *metrics) onMetrics(ctx *gin.Context) { @@ -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"

22
internal/core/path.go

@ -76,7 +76,7 @@ type pathRemovePublisherReq struct { @@ -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 { @@ -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 { @@ -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 { @@ -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( @@ -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( @@ -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() { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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():

36
internal/core/path_manager.go

@ -11,7 +11,7 @@ import ( @@ -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 @@ -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 { @@ -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 { @@ -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( @@ -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( @@ -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() { @@ -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: @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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{},
}

4
internal/core/path_test.go

@ -455,13 +455,13 @@ func TestPathRecord(t *testing.T) { @@ -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)

4
internal/core/rpicamera_source.go

@ -14,7 +14,7 @@ import ( @@ -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 @@ -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{

6
internal/core/rtmp_server.go

@ -16,7 +16,7 @@ import ( @@ -16,7 +16,7 @@ import (
)
type rtmpServerAPIConnsListRes struct {
data *apiRTMPConnsList
data *apiRTMPConnList
err error
}
@ -203,7 +203,7 @@ outer: @@ -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) { @@ -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),
}

2
internal/core/rtmp_source.go

@ -47,7 +47,7 @@ func (s *rtmpSource) Log(level logger.Level, format string, args ...interface{}) @@ -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)

4
internal/core/rtsp_server.go

@ -405,7 +405,7 @@ func (s *rtspServer) apiConnsGet(uuid uuid.UUID) (*apiRTSPConn, error) { @@ -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) { @@ -415,7 +415,7 @@ func (s *rtspServer) apiSessionsList() (*apiRTSPSessionsList, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
data := &apiRTSPSessionsList{
data := &apiRTSPSessionList{
Items: []*apiRTSPSession{},
}

4
internal/core/rtsp_source.go

@ -14,7 +14,7 @@ import ( @@ -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{}) @@ -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)

14
internal/core/source_static.go

@ -16,7 +16,7 @@ const ( @@ -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 { @@ -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 { @@ -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 { @@ -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( @@ -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() { @@ -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() { @@ -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():

6
internal/core/srt_server.go

@ -25,7 +25,7 @@ type srtNewConnReq struct { @@ -25,7 +25,7 @@ type srtNewConnReq struct {
}
type srtServerAPIConnsListRes struct {
data *apiSRTConnsList
data *apiSRTConnList
err error
}
@ -191,7 +191,7 @@ outer: @@ -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) { @@ -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),
}

2
internal/core/srt_source.go

@ -41,7 +41,7 @@ func (s *srtSource) Log(level logger.Level, format string, args ...interface{}) @@ -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()

2
internal/core/udp_source.go

@ -66,7 +66,7 @@ func (s *udpSource) Log(level logger.Level, format string, args ...interface{}) @@ -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://"):]

6
internal/core/webrtc_manager.go

@ -232,7 +232,7 @@ func webrtcNewAPI( @@ -232,7 +232,7 @@ func webrtcNewAPI(
}
type webRTCManagerAPISessionsListRes struct {
data *apiWebRTCSessionsList
data *apiWebRTCSessionList
err error
}
@ -483,7 +483,7 @@ outer: @@ -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( @@ -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),
}

2
internal/core/webrtc_source.go

@ -48,7 +48,7 @@ func (s *webRTCSource) Log(level logger.Level, format string, args ...interface{ @@ -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)

547
mediamtx.yml

@ -1,7 +1,12 @@ @@ -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 @@ -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 @@ -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 @@ -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: [] @@ -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: @@ -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 @@ -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 @@ -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 2>/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 2>/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

Loading…
Cancel
Save