Files
cf5cd38cf9 webrtc: fix deprecated webrtcICEServers parser for IPv6 hosts; (#5932)
strings.Split produces wrong part count when the host contains colons
(IPv6). SplitN(s, ";", 4) keeps the hostport token intact.

---------

Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com>
2026-08-25 06:55:05 +00:00

1008 lines
28 KiB
Go

package conf
import (
"crypto/rand"
"encoding/base64"
"io"
"os"
"testing"
"time"
"github.com/bluenviron/gortsplib/v5"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/nacl/secretbox"
"github.com/bluenviron/mediamtx/internal/logger"
)
func createTempFile(t *testing.T, byts []byte) string {
tmpf, err := os.CreateTemp(t.TempDir(), "rtsp-")
require.NoError(t, err)
defer tmpf.Close()
_, err = tmpf.Write(byts)
require.NoError(t, err)
return tmpf.Name()
}
func TestConfFromFile(t *testing.T) {
t.Run("explicit path config", func(t *testing.T) {
tmpf := createTempFile(t, []byte("logLevel: debug\n"+
"paths:\n"+
" cam1:\n"+
" runOnDemandStartTimeout: 5s\n"))
conf, confPath, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, tmpf, confPath)
require.Equal(t, LogLevel(logger.Debug), conf.LogLevel)
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
require.Equal(t, &Path{
Name: "cam1",
Source: "publisher",
SourceOnDemandStartTimeout: 10 * Duration(time.Second),
SourceOnDemandCloseAfter: 10 * Duration(time.Second),
OverridePublisher: true,
Forward: Forward{},
AlwaysAvailableTracks: []AlwaysAvailableTrack{},
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
RecordFormat: RecordFormatFMP4,
RecordPartDuration: Duration(1 * time.Second),
RecordMaxPartSize: 50 * 1024 * 1024,
RecordSegmentDuration: 3600000000000,
RecordDeleteAfter: 86400000000000,
RTSPUDPSourcePortRange: []uint{32768, 60999},
MoQTransport: MoQTransportQUIC,
WHEPSTUNGatherTimeout: 5 * Duration(time.Second),
WHEPHandshakeTimeout: 10 * Duration(time.Second),
WHEPTrackGatherTimeout: 2 * Duration(time.Second),
RPICameraWidth: 1920,
RPICameraHeight: 1080,
RPICameraContrast: 1,
RPICameraSaturation: 1,
RPICameraSharpness: 1,
RPICameraExposure: "normal",
RPICameraAWB: "auto",
RPICameraAWBGains: []float64{0, 0},
RPICameraDenoise: "off",
RPICameraMetering: "centre",
RPICameraFPS: 30,
RPICameraAfMode: "continuous",
RPICameraAfRange: "normal",
RPICameraAfSpeed: "normal",
RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX",
RPICameraCodec: "auto",
RPICameraIDRPeriod: 60,
RPICameraBitrate: 5000000,
RPICameraH264Profile: "auto",
RPICameraH264Level: "4.1",
RPICameraMJPEGQuality: 60,
RunOnDemandStartTimeout: 5 * Duration(time.Second),
RunOnDemandCloseAfter: 10 * Duration(time.Second),
}, pa)
})
t.Run("empty file", func(t *testing.T) {
tmpf := createTempFile(t, []byte(``))
_, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
})
t.Run("paths key without entries", func(t *testing.T) {
tmpf := createTempFile(t, []byte(`paths:`))
_, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
})
t.Run("single empty path entry", func(t *testing.T) {
tmpf := createTempFile(t, []byte(
"paths:\n"+
" mypath:\n"))
_, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
})
for _, ca := range []struct {
name string
source string
}{
{
name: "rtmp with placeholders",
source: "rtmp://$G1:$G2/live?token=$G3",
},
{
name: "https with placeholders",
source: "https://$G1/$G2/index.m3u8",
},
{
name: "srt with placeholders",
source: "srt://$G1:$G2/$G3",
},
{
name: "whep with placeholders",
source: "whep://$G1:$G2/$G3",
},
{
name: "wheps with placeholders",
source: "wheps://$G1:$G2/$G3",
},
{
name: "moqt with placeholders",
source: "moqt://$G1:$G2/$G3",
},
{
name: "udp with placeholders",
source: "udp://$G1:$G2",
},
{
name: "udp mpegts with placeholders",
source: "udp+mpegts://$G1:$G2",
},
{
name: "udp rtp with placeholders",
source: "udp+rtp://$G1:$G2",
},
} {
t.Run(ca.name, func(t *testing.T) {
conf := "paths:\n" +
" test:\n" +
" rtpSDP: abc\n" +
" source: " + ca.source + "\n"
tmpf := createTempFile(t, []byte(conf))
_, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
})
}
}
func TestConfFromFileAndEnv(t *testing.T) {
// global parameter
t.Setenv("RTSP_PROTOCOLS", "tcp")
// path parameter
t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
// deprecated global parameter
t.Setenv("MTX_RTMPDISABLE", "yes")
// deprecated path parameter
t.Setenv("MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE", "yes")
tmpf := createTempFile(t, []byte("{}"))
conf, confPath, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, tmpf, confPath)
require.Equal(t, RTSPTransports{gortsplib.ProtocolTCP: {}}, conf.RTSPTransports)
require.Equal(t, false, conf.RTMP)
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
require.Equal(t, "rtsp://testing", pa.Source)
pa, ok = conf.Paths["cam2"]
require.Equal(t, true, ok)
require.Equal(t, false, pa.OverridePublisher)
}
func TestConfFromEnv(t *testing.T) {
t.Run("path source", func(t *testing.T) {
t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing")
conf, confPath, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath)
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
require.Equal(t, "rtsp://testing", pa.Source)
})
t.Run("empty auth internal user ips", func(t *testing.T) {
t.Setenv("MTX_AUTHINTERNALUSERS_0_IPS", "")
conf, confPath, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath)
require.Len(t, conf.AuthInternalUsers, len(defaultAuthInternalUsers))
require.Empty(t, conf.AuthInternalUsers[0].IPs)
require.Equal(t, defaultAuthInternalUsers[0].Permissions, conf.AuthInternalUsers[0].Permissions)
})
t.Run("empty log destinations", func(t *testing.T) {
t.Setenv("MTX_LOGDESTINATIONS", "")
conf, confPath, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath)
require.Empty(t, conf.LogDestinations)
})
t.Run("empty rtsp transports", func(t *testing.T) {
t.Setenv("MTX_RTSPTRANSPORTS", "")
conf, confPath, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath)
require.Empty(t, conf.RTSPTransports)
})
t.Run("empty rtsp auth methods", func(t *testing.T) {
t.Setenv("MTX_RTSP", "no")
t.Setenv("MTX_RTSPAUTHMETHODS", "")
conf, confPath, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath)
require.False(t, conf.RTSP)
require.Empty(t, conf.RTSPAuthMethods)
})
}
func TestConfEncryption(t *testing.T) {
key := "testing123testin"
plaintext := "paths:\n" +
" path1:\n" +
" path2:\n"
encryptedConf := func() string {
var secretKey [32]byte
copy(secretKey[:], key)
var nonce [24]byte
_, err := io.ReadFull(rand.Reader, nonce[:])
require.NoError(t, err)
encrypted := secretbox.Seal(nonce[:], []byte(plaintext), &nonce, &secretKey)
return base64.StdEncoding.EncodeToString(encrypted)
}()
t.Setenv("RTSP_CONFKEY", key)
tmpf := createTempFile(t, []byte(encryptedConf))
conf, confPath, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, tmpf, confPath)
_, ok := conf.Paths["path1"]
require.Equal(t, true, ok)
_, ok = conf.Paths["path2"]
require.Equal(t, true, ok)
}
func TestConfDeprecatedAuth(t *testing.T) {
tmpf := createTempFile(t, []byte(
"paths:\n"+
" cam:\n"+
" readUser: myuser\n"+
" readPass: mypass\n"))
conf, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, []AuthInternalUser{
{
User: "any",
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPlayback,
},
},
},
{
User: "any",
IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionAPI,
},
{
Action: AuthActionMetrics,
},
{
Action: AuthActionPprof,
},
},
},
{
User: "any",
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionPublish,
Path: "cam",
},
},
},
{
User: "myuser",
Pass: "mypass",
IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")},
Permissions: []AuthInternalUserPermission{
{
Action: AuthActionRead,
Path: "cam",
},
},
},
}, conf.AuthInternalUsers)
}
func TestConfDeprecatedWebRTCICEServersIPv6(t *testing.T) {
tmpf := createTempFile(t, []byte(
"webrtcICEServers:\n"+
"- \"turn:myuser:mypass:[2001:db8::1]:3478?transport=tcp\"\n"))
conf, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, []WebRTCICEServer{{
URL: "turn:[2001:db8::1]:3478?transport=tcp",
Username: "myuser",
Password: "mypass",
}}, conf.WebRTCICEServers2)
}
func TestConfErrors(t *testing.T) {
for _, ca := range []struct {
name string
conf string
err string
}{
{
"duplicate parameter",
"paths:\n" +
"paths:\n",
"[2:1] mapping key \"paths\" already defined at [1:1]\n 1 | null\n> 2 | paths:\n ^\n",
},
{
"non existent parameter",
`invalid: param`,
"json: unknown field \"invalid\"",
},
{
"invalid readTimeout",
"readTimeout: 0s\n",
"'readTimeout' must be greater than zero",
},
{
"invalid writeTimeout",
"writeTimeout: 0s\n",
"'writeTimeout' must be greater than zero",
},
{
"invalid writeQueueSize 1",
"writeQueueSize: 0\n",
"'writeQueueSize' must be greater than zero",
},
{
"invalid writeQueueSize 2",
"writeQueueSize: 1001\n",
"'writeQueueSize' must be a power of two",
},
{
"invalid udpMaxPayloadSize",
"udpMaxPayloadSize: 5000\n",
"'udpMaxPayloadSize' must be less than 1472",
},
{
"invalid ICE server",
"webrtcICEServers: [testing]\n",
"invalid ICE server: 'testing'",
},
{
"non existent parameter in path",
"paths:\n" +
" mypath:\n" +
" invalid: parameter\n",
"json: unknown field \"invalid\"",
},
{
"non existent parameter in auth",
"authInternalUsers:\n" +
"- users: test\n",
"json: unknown field \"authInternalUsers[0].users\"",
},
{
"invalid path name",
"paths:\n" +
" '':\n" +
" source: publisher\n",
"invalid path name '': cannot be empty",
},
{
"double raspberry pi camera",
"paths:\n" +
" cam1:\n" +
" source: rpiCamera\n" +
" cam2:\n" +
" source: rpiCamera\n",
"'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam1' and 'cam2'",
},
{
"invalid rpi camera mjpeg width",
"paths:\n" +
" cam:\n" +
" source: rpiCamera\n" +
" rpiCameraCodec: mjpeg\n" +
" rpiCameraWidth: 1921\n",
"'rpiCameraWidth' must be a multiple of 8 and less than 2048 when using MJPEG",
},
{
"invalid rpi camera mjpeg height",
"paths:\n" +
" cam:\n" +
" source: rpiCamera\n" +
" rpiCameraCodec: mjpeg\n" +
" rpiCameraHeight: 2048\n",
"'rpiCameraHeight' must be a multiple of 8 and less than 2048 when using MJPEG",
},
{
"invalid srt publish passphrase",
"paths:\n" +
" mypath:\n" +
" srtPublishPassphrase: a\n",
`invalid 'srtPublishPassphrase': must be between 10 and 79 characters`,
},
{
"invalid srt read passphrase",
"paths:\n" +
" mypath:\n" +
" srtReadPassphrase: a\n",
`invalid 'readRTPassphrase': must be between 10 and 79 characters`,
},
{
"all_others aliases",
"paths:\n" +
" all:\n" +
" all_others:\n",
`all_others, all and '~^.*$' are aliases`,
},
{
"all_others aliases",
"paths:\n" +
" all_others:\n" +
" ~^.*$:\n",
`all_others, all and '~^.*$' are aliases`,
},
{
"jwt jwks empty",
"authMethod: jwt\n" +
"authJWTJWKS: \"\"\n" +
"authJWTClaimKey: test",
"'authJWTJWKS' is empty",
},
{
"invalid jwt jwks url",
"authMethod: jwt\n" +
"authJWTJWKS: ftp://invalid\n" +
"authJWTClaimKey: test",
"'authJWTJWKS' must be a HTTP URL",
},
{
"jwt claim key empty",
"authMethod: jwt\n" +
"authJWTJWKS: https://not-real.com\n" +
"authJWTClaimKey: \"\"",
"'authJWTClaimKey' is empty",
},
{
"http auth address empty",
"authMethod: http\n" +
"authHTTPAddress: \"\"",
"'authHTTPAddress' is empty",
},
{
"invalid http auth address",
"authMethod: http\n" +
"authHTTPAddress: ftp://invalid",
"'externalAuthenticationURL' must be a HTTP URL",
},
{
"invalid rtsp auth methods",
"rtspAuthMethods: []",
"at least one 'rtspAuthMethods' must be provided",
},
{
"rtsp digest with non-internal auth",
"authMethod: http\n" +
"authHTTPAddress: http://localhost:9000\n" +
"rtspAuthMethods: [digest]\n",
"when RTSP digest is enabled, the only supported auth method is 'internal'",
},
{
"rtsp digest with hashed credentials",
"rtspAuthMethods: [digest]\n" +
"authInternalUsers:\n" +
"- user: sha256:test\n" +
" pass: test\n" +
" permissions:\n" +
" - action: publish\n",
"when RTSP digest is enabled, hashed credentials cannot be used",
},
{
"invalid fallback",
"paths:\n" +
" my_path:\n" +
" fallback: invalid://invalid",
`'invalid://invalid' is not a valid RTSP URL`,
},
{
"invalid source redirect",
"paths:\n" +
" my_path:\n" +
" source: redirect\n" +
" sourceRedirect: invalid://invalid",
`'invalid://invalid' is not a valid RTSP URL`,
},
{
"useless source redirect",
"paths:\n" +
" my_path:\n" +
" sourceRedirect: invalid://invalid",
`'sourceRedirect' is useless when source is not 'redirect'`,
},
{
"invalid user",
"authInternalUsers:\n" +
"- user:\n" +
" pass: test\n" +
" permissions:\n" +
" - action: publish\n",
"empty usernames are not supported",
},
{
"invalid pass",
"authInternalUsers:\n" +
"- user: any\n" +
" pass: test\n" +
" permissions:\n" +
" - action: publish\n",
`using a password with 'any' user is not supported`,
},
{
"invalid record path 1",
"paths:\n" +
" my_path:\n" +
" recordPath: invalid\n",
`'recordPath' must contain %path`,
},
{
"invalid record path 2",
"paths:\n" +
" my_path:\n" +
" recordPath: '%path/invalid'\n",
`'recordPath' must contain either %s or %Y %m %d %H %M %S`,
},
{
"invalid record path 3",
"playback: true\n" +
"paths:\n" +
" my_path:\n" +
" recordPath: '%path/%s'\n",
`'recordPath' must contain %f`,
},
{
"invalid record delete after",
"paths:\n" +
" my_path:\n" +
" recordSegmentDuration: 30m\n" +
" recordDeleteAfter: 20m\n",
`'recordDeleteAfter' cannot be lower than 'recordSegmentDuration'`,
},
{
"missing rtpAddress with UDP and no encryption",
"rtspEncryption: \"no\"\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ''\n",
"'rtpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing rtcpAddress with UDP and no encryption",
"rtspEncryption: \"no\"\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ''\n",
"'rtcpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing rtpAddress with UDP and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ''\n",
"'rtpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing rtcpAddress with UDP and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ''\n",
"'rtcpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastIPRange with UDP multicast and no encryption",
"rtspEncryption: \"no\"\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: ''\n",
"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastRTPPort with UDP multicast and no encryption",
"rtspEncryption: \"no\"\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 0\n",
"'multicastRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastRTCPPort with UDP multicast and no encryption",
"rtspEncryption: \"no\"\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 8002\n" +
"multicastRTCPPort: 0\n",
"'multicastRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastIPRange with UDP multicast and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: ''\n",
"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastRTPPort with UDP multicast and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 0\n",
"'multicastRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastRTCPPort with UDP multicast and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 8002\n" +
"multicastRTCPPort: 0\n",
"'multicastRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing srtpAddress with UDP and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ':8001'\n" +
"srtpAddress: ''\n",
"'srtpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing srtcpAddress with UDP and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [udp]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ':8001'\n" +
"srtpAddress: ':8004'\n" +
"srtcpAddress: ''\n",
"'srtcpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing srtpAddress with UDP and strict encryption",
"rtspEncryption: strict\n" +
"rtspTransports: [udp]\n" +
"srtpAddress: ''\n",
"'srtpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing srtcpAddress with UDP and strict encryption",
"rtspEncryption: strict\n" +
"rtspTransports: [udp]\n" +
"srtpAddress: ':8004'\n" +
"srtcpAddress: ''\n",
"'srtcpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing multicastIPRange with UDP multicast and optional encryption second check",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ':8001'\n" +
"multicastIPRange: ''\n",
"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing multicastSRTPPort with UDP multicast and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ':8001'\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 8002\n" +
"multicastRTCPPort: 8003\n" +
"srtpAddress: ':8004'\n" +
"srtcpAddress: ':8005'\n" +
"multicastSRTPPort: 0\n",
"'multicastSRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing multicastSRTCPPort with UDP multicast and optional encryption",
"rtspEncryption: optional\n" +
"rtspTransports: [multicast]\n" +
"rtpAddress: ':8000'\n" +
"rtcpAddress: ':8001'\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastRTPPort: 8002\n" +
"multicastRTCPPort: 8003\n" +
"srtpAddress: ':8004'\n" +
"srtcpAddress: ':8005'\n" +
"multicastSRTPPort: 8006\n" +
"multicastSRTCPPort: 0\n",
"'multicastSRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing multicastIPRange with UDP multicast and strict encryption",
"rtspEncryption: strict\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: ''\n",
"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing multicastSRTPPort with UDP multicast and strict encryption",
"rtspEncryption: strict\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastSRTPPort: 0\n",
"'multicastSRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing multicastSRTCPPort with UDP multicast and strict encryption",
"rtspEncryption: strict\n" +
"rtspTransports: [multicast]\n" +
"multicastIPRange: '224.1.0.0/16'\n" +
"multicastSRTPPort: 8006\n" +
"multicastSRTCPPort: 0\n",
"'multicastSRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing rtspAddress with RTSP enabled and no encryption",
"rtsp: yes\n" +
"rtspEncryption: \"no\"\n" +
"rtspAddress: ''\n",
"'rtspAddress' must be set when RTSP is enabled and RTSP encryption is 'no' or 'optional'",
},
{
"missing rtspsAddress with RTSP enabled and strict encryption",
"rtsp: yes\n" +
"rtspEncryption: strict\n" +
"rtspsAddress: ''\n",
"'rtspsAddress' must be set when RTSP is enabled and RTSP encryption is 'optional' or 'strict'",
},
{
"missing rtmpAddress with RTMP enabled",
"rtmp: yes\n" +
"rtmpAddress: ''\n",
"'rtmpAddress' must be set when RTMP is enabled",
},
{
"missing hlsAddress with HLS enabled",
"hls: yes\n" +
"hlsAddress: ''\n",
"'hlsAddress' must be set when HLS is enabled",
},
{
"missing webrtcAddress with WebRTC enabled",
"webrtc: yes\n" +
"webrtcAddress: ''\n",
"'webrtcAddress' must be set when WebRTC is enabled",
},
{
"webrtc missing local addresses and ice servers",
"webrtc: yes\n" +
"webrtcLocalUDPAddress: ''\n" +
"webrtcLocalTCPAddress: ''\n" +
"webrtcICEServers2: []\n",
"at least one between 'webrtcLocalUDPAddress', 'webrtcLocalTCPAddress' or 'webrtcICEServers2' must be filled",
},
{
"webrtc missing ips config",
"webrtc: yes\n" +
"webrtcLocalUDPAddress: ':8189'\n" +
"webrtcIPsFromInterfaces: false\n" +
"webrtcAdditionalHosts: []\n",
"at least one between 'webrtcIPsFromInterfaces' or 'webrtcAdditionalHosts' must be filled",
},
{
"missing apiAddress with API enabled",
"api: yes\n" +
"apiAddress: ''\n",
"'apiAddress' must be set when API is enabled",
},
{
"missing metricsAddress with metrics enabled",
"metrics: yes\n" +
"metricsAddress: ''\n",
"'metricsAddress' must be set when metrics are enabled",
},
{
"missing pprofAddress with pprof enabled",
"pprof: yes\n" +
"pprofAddress: ''\n",
"'pprofAddress' must be set when pprof is enabled",
},
{
"missing playbackAddress with playback enabled",
"playback: yes\n" +
"playbackAddress: ''\n",
"'playbackAddress' must be set when playback is enabled",
},
{
"alwaysAvailableTracks and alwaysAvailableFile together",
"paths:\n" +
" mypath:\n" +
" alwaysAvailable: yes\n" +
" alwaysAvailableTracks:\n" +
" - codec: H264\n" +
" alwaysAvailableFile: /path/to/file.mp4\n",
"'alwaysAvailableFile' and 'alwaysAvailableTracks' cannot be used together",
},
{
"alwaysAvailableTracks g711 sampleRate too low",
"paths:\n" +
" mypath:\n" +
" alwaysAvailable: yes\n" +
" alwaysAvailableTracks:\n" +
" - codec: G711\n" +
" sampleRate: 7999\n" +
" channelCount: 1\n",
"sampleRate must be greater than or equal to 8000 for codec 'G711'",
},
{
"alwaysAvailableTracks mpeg4audio sampleRate too low",
"paths:\n" +
" mypath:\n" +
" alwaysAvailable: yes\n" +
" alwaysAvailableTracks:\n" +
" - codec: MPEG4Audio\n" +
" sampleRate: 22049\n" +
" channelCount: 1\n",
"sampleRate must be greater than or equal to 22050 for codec 'MPEG4Audio'",
},
{
"missing udp port",
"paths:\n" +
" mypath:\n" +
" source: udp://$G1\n",
"'udp://$G1' is missing the port",
},
{
"missing rtmp authority",
"paths:\n" +
" mypath:\n" +
" source: rtmp://$G1:$G2@\n",
"'rtmp://$G1:$G2@' is not a valid URL",
},
{
"rtsp source username without password",
"paths:\n" +
" mypath:\n" +
" source: rtsp://user@localhost/stream\n",
"username and password must be both provided",
},
{
"valid whip forward destination",
"paths:\n" +
" mypath:\n" +
" forward:\n" +
" - dest: whip://localhost/stream/whip\n" +
" destFingerprint: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" +
" whipBearerToken: mytoken\n",
"",
},
{
"invalid forward destination",
"paths:\n" +
" mypath:\n" +
" forward:\n" +
" - dest: http://localhost/stream\n",
"invalid 'forward': entry 0: unsupported scheme 'http', supported ones are " +
"rtmp, rtmps, rtsp, rtsps, srt, whip and whips",
},
} {
t.Run(ca.name, func(t *testing.T) {
tmpf := createTempFile(t, []byte(ca.conf))
_, _, err := Load(tmpf, nil, nil)
if ca.err == "" {
require.NoError(t, err)
} else {
require.EqualError(t, err, ca.err)
}
})
}
}
func TestDeprecatedAvailabilityHooks(t *testing.T) {
tmpf := createTempFile(t, []byte("paths:\n"+
" mypath:\n"+
" runOnReady: command1\n"+
" runOnReadyRestart: yes\n"+
" runOnNotReady: command2\n"))
conf, _, err := Load(tmpf, nil, nil)
require.NoError(t, err)
pa := conf.Paths["mypath"]
require.Equal(t, "command1", pa.RunOnAvailable)
require.Equal(t, true, pa.RunOnAvailableRestart)
require.Equal(t, "command2", pa.RunOnUnavailable)
require.Equal(t, "command1", *pa.RunOnReady)
require.Equal(t, true, *pa.RunOnReadyRestart)
require.Equal(t, "command2", *pa.RunOnNotReady)
}
func TestAlwaysAvailableFileErrorMagicBytes(t *testing.T) {
tmpf := createTempFile(t, []byte("ABCDEFGHI"))
tmpConf := createTempFile(t, []byte("paths:\n"+
" mypath:\n"+
" alwaysAvailable: yes\n"+
" alwaysAvailableFile: "+tmpf+"\n"))
_, _, err := Load(tmpConf, nil, nil)
require.EqualError(t, err, "invalid 'alwaysAvailableFile': file is not MP4, magic bytes are [69 70 71 72]")
}
func TestDefaultConfFile(t *testing.T) {
func() {
conf1, confPath1, err := Load("../../mediamtx.yml", nil, nil)
require.NoError(t, err)
require.Equal(t, "../../mediamtx.yml", confPath1)
conf1.Paths = make(map[string]*Path)
conf1.OptionalPaths = nil
conf2, confPath2, err := Load("", nil, nil)
require.NoError(t, err)
require.Equal(t, "", confPath2)
require.Equal(t, conf1, conf2)
}()
func() {
conf1, confPath1, err := Load("../../mediamtx.yml", nil, nil)
require.NoError(t, err)
require.Equal(t, "../../mediamtx.yml", confPath1)
tmpf := createTempFile(t, []byte("paths:\n all_others:"))
conf2, confPath2, err := Load(tmpf, nil, nil)
require.NoError(t, err)
require.Equal(t, tmpf, confPath2)
require.Equal(t, conf1.Paths, conf2.Paths)
}()
}
func TestClone(t *testing.T) {
conf1, _, err := Load("", nil, nil)
require.NoError(t, err)
conf2 := conf1.Clone()
require.Equal(t, conf1, conf2)
}