mirror of
https://github.com/bluenviron/mediamtx.git
synced 2026-01-26 05:19:15 -08:00
accept durations expressed as days (i.e. '1d') (#4094)
This commit is contained in:
parent
8cbbbc05c3
commit
b49acb1e00
53 changed files with 378 additions and 257 deletions
|
|
@ -161,8 +161,8 @@ type Conf struct {
|
|||
LogLevel LogLevel `json:"logLevel"`
|
||||
LogDestinations LogDestinations `json:"logDestinations"`
|
||||
LogFile string `json:"logFile"`
|
||||
ReadTimeout StringDuration `json:"readTimeout"`
|
||||
WriteTimeout StringDuration `json:"writeTimeout"`
|
||||
ReadTimeout Duration `json:"readTimeout"`
|
||||
WriteTimeout Duration `json:"writeTimeout"`
|
||||
ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated
|
||||
WriteQueueSize int `json:"writeQueueSize"`
|
||||
UDPMaxPayloadSize int `json:"udpMaxPayloadSize"`
|
||||
|
|
@ -246,22 +246,22 @@ type Conf struct {
|
|||
RTMPServerCert string `json:"rtmpServerCert"`
|
||||
|
||||
// HLS server
|
||||
HLS bool `json:"hls"`
|
||||
HLSDisable *bool `json:"hlsDisable,omitempty"` // deprecated
|
||||
HLSAddress string `json:"hlsAddress"`
|
||||
HLSEncryption bool `json:"hlsEncryption"`
|
||||
HLSServerKey string `json:"hlsServerKey"`
|
||||
HLSServerCert string `json:"hlsServerCert"`
|
||||
HLSAllowOrigin string `json:"hlsAllowOrigin"`
|
||||
HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"`
|
||||
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
|
||||
HLSVariant HLSVariant `json:"hlsVariant"`
|
||||
HLSSegmentCount int `json:"hlsSegmentCount"`
|
||||
HLSSegmentDuration StringDuration `json:"hlsSegmentDuration"`
|
||||
HLSPartDuration StringDuration `json:"hlsPartDuration"`
|
||||
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
|
||||
HLSDirectory string `json:"hlsDirectory"`
|
||||
HLSMuxerCloseAfter StringDuration `json:"hlsMuxerCloseAfter"`
|
||||
HLS bool `json:"hls"`
|
||||
HLSDisable *bool `json:"hlsDisable,omitempty"` // deprecated
|
||||
HLSAddress string `json:"hlsAddress"`
|
||||
HLSEncryption bool `json:"hlsEncryption"`
|
||||
HLSServerKey string `json:"hlsServerKey"`
|
||||
HLSServerCert string `json:"hlsServerCert"`
|
||||
HLSAllowOrigin string `json:"hlsAllowOrigin"`
|
||||
HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"`
|
||||
HLSAlwaysRemux bool `json:"hlsAlwaysRemux"`
|
||||
HLSVariant HLSVariant `json:"hlsVariant"`
|
||||
HLSSegmentCount int `json:"hlsSegmentCount"`
|
||||
HLSSegmentDuration Duration `json:"hlsSegmentDuration"`
|
||||
HLSPartDuration Duration `json:"hlsPartDuration"`
|
||||
HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"`
|
||||
HLSDirectory string `json:"hlsDirectory"`
|
||||
HLSMuxerCloseAfter Duration `json:"hlsMuxerCloseAfter"`
|
||||
|
||||
// WebRTC server
|
||||
WebRTC bool `json:"webrtc"`
|
||||
|
|
@ -278,8 +278,8 @@ type Conf struct {
|
|||
WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"`
|
||||
WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"`
|
||||
WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"`
|
||||
WebRTCHandshakeTimeout StringDuration `json:"webrtcHandshakeTimeout"`
|
||||
WebRTCTrackGatherTimeout StringDuration `json:"webrtcTrackGatherTimeout"`
|
||||
WebRTCHandshakeTimeout Duration `json:"webrtcHandshakeTimeout"`
|
||||
WebRTCTrackGatherTimeout Duration `json:"webrtcTrackGatherTimeout"`
|
||||
WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated
|
||||
WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated
|
||||
WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated
|
||||
|
|
@ -290,12 +290,12 @@ type Conf struct {
|
|||
SRTAddress string `json:"srtAddress"`
|
||||
|
||||
// Record (deprecated)
|
||||
Record *bool `json:"record,omitempty"` // deprecated
|
||||
RecordPath *string `json:"recordPath,omitempty"` // deprecated
|
||||
RecordFormat *RecordFormat `json:"recordFormat,omitempty"` // deprecated
|
||||
RecordPartDuration *StringDuration `json:"recordPartDuration,omitempty"` // deprecated
|
||||
RecordSegmentDuration *StringDuration `json:"recordSegmentDuration,omitempty"` // deprecated
|
||||
RecordDeleteAfter *StringDuration `json:"recordDeleteAfter,omitempty"` // deprecated
|
||||
Record *bool `json:"record,omitempty"` // deprecated
|
||||
RecordPath *string `json:"recordPath,omitempty"` // deprecated
|
||||
RecordFormat *RecordFormat `json:"recordFormat,omitempty"` // deprecated
|
||||
RecordPartDuration *Duration `json:"recordPartDuration,omitempty"` // deprecated
|
||||
RecordSegmentDuration *Duration `json:"recordSegmentDuration,omitempty"` // deprecated
|
||||
RecordDeleteAfter *Duration `json:"recordDeleteAfter,omitempty"` // deprecated
|
||||
|
||||
// Path defaults
|
||||
PathDefaults Path `json:"pathDefaults"`
|
||||
|
|
@ -310,8 +310,8 @@ func (conf *Conf) setDefaults() {
|
|||
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.ReadTimeout = 10 * Duration(time.Second)
|
||||
conf.WriteTimeout = 10 * Duration(time.Second)
|
||||
conf.WriteQueueSize = 512
|
||||
conf.UDPMaxPayloadSize = 1472
|
||||
|
||||
|
|
@ -387,10 +387,10 @@ func (conf *Conf) setDefaults() {
|
|||
conf.HLSAllowOrigin = "*"
|
||||
conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency)
|
||||
conf.HLSSegmentCount = 7
|
||||
conf.HLSSegmentDuration = 1 * StringDuration(time.Second)
|
||||
conf.HLSPartDuration = 200 * StringDuration(time.Millisecond)
|
||||
conf.HLSSegmentDuration = 1 * Duration(time.Second)
|
||||
conf.HLSPartDuration = 200 * Duration(time.Millisecond)
|
||||
conf.HLSSegmentMaxSize = 50 * 1024 * 1024
|
||||
conf.HLSMuxerCloseAfter = 60 * StringDuration(time.Second)
|
||||
conf.HLSMuxerCloseAfter = 60 * Duration(time.Second)
|
||||
|
||||
// WebRTC server
|
||||
conf.WebRTC = true
|
||||
|
|
@ -403,8 +403,8 @@ func (conf *Conf) setDefaults() {
|
|||
conf.WebRTCIPsFromInterfacesList = []string{}
|
||||
conf.WebRTCAdditionalHosts = []string{}
|
||||
conf.WebRTCICEServers2 = []WebRTCICEServer{}
|
||||
conf.WebRTCHandshakeTimeout = 10 * StringDuration(time.Second)
|
||||
conf.WebRTCTrackGatherTimeout = 2 * StringDuration(time.Second)
|
||||
conf.WebRTCHandshakeTimeout = 10 * Duration(time.Second)
|
||||
conf.WebRTCTrackGatherTimeout = 2 * Duration(time.Second)
|
||||
|
||||
// SRT server
|
||||
conf.SRT = true
|
||||
|
|
|
|||
|
|
@ -50,11 +50,11 @@ func TestConfFromFile(t *testing.T) {
|
|||
require.Equal(t, &Path{
|
||||
Name: "cam1",
|
||||
Source: "publisher",
|
||||
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
|
||||
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
|
||||
SourceOnDemandStartTimeout: 10 * Duration(time.Second),
|
||||
SourceOnDemandCloseAfter: 10 * Duration(time.Second),
|
||||
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
|
||||
RecordFormat: RecordFormatFMP4,
|
||||
RecordPartDuration: StringDuration(1 * time.Second),
|
||||
RecordPartDuration: Duration(1 * time.Second),
|
||||
RecordSegmentDuration: 3600000000000,
|
||||
RecordDeleteAfter: 86400000000000,
|
||||
OverridePublisher: true,
|
||||
|
|
@ -78,8 +78,8 @@ func TestConfFromFile(t *testing.T) {
|
|||
RPICameraBitrate: 5000000,
|
||||
RPICameraProfile: "main",
|
||||
RPICameraLevel: "4.1",
|
||||
RunOnDemandStartTimeout: 5 * StringDuration(time.Second),
|
||||
RunOnDemandCloseAfter: 10 * StringDuration(time.Second),
|
||||
RunOnDemandStartTimeout: 5 * Duration(time.Second),
|
||||
RunOnDemandCloseAfter: 10 * Duration(time.Second),
|
||||
}, pa)
|
||||
}()
|
||||
|
||||
|
|
|
|||
101
internal/conf/duration.go
Normal file
101
internal/conf/duration.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var reDays = regexp.MustCompile("^(-?[0-9]+)d")
|
||||
|
||||
// Duration is a duration. It differs from the standard duration in these ways:
|
||||
// - it is unmarshaled/marshaled from/to a string (instead of a number)
|
||||
// - it supports days
|
||||
type Duration time.Duration
|
||||
|
||||
func (d Duration) marshalInternal() string {
|
||||
negative := false
|
||||
if d < 0 {
|
||||
negative = true
|
||||
d = -d
|
||||
}
|
||||
|
||||
day := Duration(86400 * time.Second)
|
||||
days := d / day
|
||||
nonDays := d % day
|
||||
|
||||
ret := ""
|
||||
if negative {
|
||||
ret += "-"
|
||||
}
|
||||
|
||||
if days > 0 {
|
||||
ret += strconv.FormatInt(int64(days), 10) + "d"
|
||||
}
|
||||
|
||||
if nonDays != 0 {
|
||||
ret += time.Duration(nonDays).String()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(d.marshalInternal())
|
||||
}
|
||||
|
||||
func (d *Duration) unmarshalInternal(in string) error {
|
||||
negative := false
|
||||
days := int64(0)
|
||||
|
||||
m := reDays.FindStringSubmatch(in)
|
||||
if m != nil {
|
||||
days, _ = strconv.ParseInt(m[1], 10, 64)
|
||||
if days < 0 {
|
||||
negative = true
|
||||
days = -days
|
||||
}
|
||||
|
||||
in = in[len(m[0]):]
|
||||
}
|
||||
|
||||
var nonDays time.Duration
|
||||
|
||||
if len(in) != 0 {
|
||||
var err error
|
||||
nonDays, err = time.ParseDuration(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nonDays += time.Duration(days) * 24 * time.Hour
|
||||
if negative {
|
||||
nonDays = -nonDays
|
||||
}
|
||||
|
||||
*d = Duration(nonDays)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var in string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := d.unmarshalInternal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *Duration) UnmarshalEnv(_ string, v string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + v + `"`))
|
||||
}
|
||||
56
internal/conf/duration_test.go
Normal file
56
internal/conf/duration_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var casesDuration = []struct {
|
||||
name string
|
||||
dec Duration
|
||||
enc string
|
||||
}{
|
||||
{
|
||||
"standard",
|
||||
Duration(13456 * time.Second),
|
||||
`"3h44m16s"`,
|
||||
},
|
||||
{
|
||||
"days",
|
||||
Duration(50 * 13456 * time.Second),
|
||||
`"7d18h53m20s"`,
|
||||
},
|
||||
{
|
||||
"days negative",
|
||||
Duration(-50 * 13456 * time.Second),
|
||||
`"-7d18h53m20s"`,
|
||||
},
|
||||
{
|
||||
"days even",
|
||||
Duration(7 * 24 * time.Hour),
|
||||
`"7d"`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDurationUnmarshal(t *testing.T) {
|
||||
for _, ca := range casesDuration {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
var dec Duration
|
||||
err := dec.UnmarshalJSON([]byte(ca.enc))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.dec, dec)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationMarshal(t *testing.T) {
|
||||
for _, ca := range casesDuration {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
enc, err := ca.dec.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ca.enc, string(enc))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -88,23 +88,23 @@ type Path struct {
|
|||
Name string `json:"name"` // filled by Check()
|
||||
|
||||
// General
|
||||
Source string `json:"source"`
|
||||
SourceFingerprint string `json:"sourceFingerprint"`
|
||||
SourceOnDemand bool `json:"sourceOnDemand"`
|
||||
SourceOnDemandStartTimeout StringDuration `json:"sourceOnDemandStartTimeout"`
|
||||
SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"`
|
||||
MaxReaders int `json:"maxReaders"`
|
||||
SRTReadPassphrase string `json:"srtReadPassphrase"`
|
||||
Fallback string `json:"fallback"`
|
||||
Source string `json:"source"`
|
||||
SourceFingerprint string `json:"sourceFingerprint"`
|
||||
SourceOnDemand bool `json:"sourceOnDemand"`
|
||||
SourceOnDemandStartTimeout Duration `json:"sourceOnDemandStartTimeout"`
|
||||
SourceOnDemandCloseAfter Duration `json:"sourceOnDemandCloseAfter"`
|
||||
MaxReaders int `json:"maxReaders"`
|
||||
SRTReadPassphrase string `json:"srtReadPassphrase"`
|
||||
Fallback string `json:"fallback"`
|
||||
|
||||
// Record
|
||||
Record bool `json:"record"`
|
||||
Playback *bool `json:"playback,omitempty"` // deprecated
|
||||
RecordPath string `json:"recordPath"`
|
||||
RecordFormat RecordFormat `json:"recordFormat"`
|
||||
RecordPartDuration StringDuration `json:"recordPartDuration"`
|
||||
RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
|
||||
RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`
|
||||
Record bool `json:"record"`
|
||||
Playback *bool `json:"playback,omitempty"` // deprecated
|
||||
RecordPath string `json:"recordPath"`
|
||||
RecordFormat RecordFormat `json:"recordFormat"`
|
||||
RecordPartDuration Duration `json:"recordPartDuration"`
|
||||
RecordSegmentDuration Duration `json:"recordSegmentDuration"`
|
||||
RecordDeleteAfter Duration `json:"recordDeleteAfter"`
|
||||
|
||||
// Authentication (deprecated)
|
||||
PublishUser *Credential `json:"publishUser,omitempty"` // deprecated
|
||||
|
|
@ -168,35 +168,35 @@ type Path struct {
|
|||
RPICameraLevel string `json:"rpiCameraLevel"`
|
||||
|
||||
// Hooks
|
||||
RunOnInit string `json:"runOnInit"`
|
||||
RunOnInitRestart bool `json:"runOnInitRestart"`
|
||||
RunOnDemand string `json:"runOnDemand"`
|
||||
RunOnDemandRestart bool `json:"runOnDemandRestart"`
|
||||
RunOnDemandStartTimeout StringDuration `json:"runOnDemandStartTimeout"`
|
||||
RunOnDemandCloseAfter StringDuration `json:"runOnDemandCloseAfter"`
|
||||
RunOnUnDemand string `json:"runOnUnDemand"`
|
||||
RunOnReady string `json:"runOnReady"`
|
||||
RunOnReadyRestart bool `json:"runOnReadyRestart"`
|
||||
RunOnNotReady string `json:"runOnNotReady"`
|
||||
RunOnRead string `json:"runOnRead"`
|
||||
RunOnReadRestart bool `json:"runOnReadRestart"`
|
||||
RunOnUnread string `json:"runOnUnread"`
|
||||
RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
|
||||
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
|
||||
RunOnInit string `json:"runOnInit"`
|
||||
RunOnInitRestart bool `json:"runOnInitRestart"`
|
||||
RunOnDemand string `json:"runOnDemand"`
|
||||
RunOnDemandRestart bool `json:"runOnDemandRestart"`
|
||||
RunOnDemandStartTimeout Duration `json:"runOnDemandStartTimeout"`
|
||||
RunOnDemandCloseAfter Duration `json:"runOnDemandCloseAfter"`
|
||||
RunOnUnDemand string `json:"runOnUnDemand"`
|
||||
RunOnReady string `json:"runOnReady"`
|
||||
RunOnReadyRestart bool `json:"runOnReadyRestart"`
|
||||
RunOnNotReady string `json:"runOnNotReady"`
|
||||
RunOnRead string `json:"runOnRead"`
|
||||
RunOnReadRestart bool `json:"runOnReadRestart"`
|
||||
RunOnUnread string `json:"runOnUnread"`
|
||||
RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"`
|
||||
RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"`
|
||||
}
|
||||
|
||||
func (pconf *Path) setDefaults() {
|
||||
// General
|
||||
pconf.Source = "publisher"
|
||||
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
|
||||
pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
|
||||
pconf.SourceOnDemandStartTimeout = 10 * Duration(time.Second)
|
||||
pconf.SourceOnDemandCloseAfter = 10 * Duration(time.Second)
|
||||
|
||||
// Record
|
||||
pconf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
|
||||
pconf.RecordFormat = RecordFormatFMP4
|
||||
pconf.RecordPartDuration = StringDuration(1 * time.Second)
|
||||
pconf.RecordSegmentDuration = 3600 * StringDuration(time.Second)
|
||||
pconf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second)
|
||||
pconf.RecordPartDuration = Duration(1 * time.Second)
|
||||
pconf.RecordSegmentDuration = 3600 * Duration(time.Second)
|
||||
pconf.RecordDeleteAfter = 24 * 3600 * Duration(time.Second)
|
||||
|
||||
// Publisher source
|
||||
pconf.OverridePublisher = true
|
||||
|
|
@ -224,8 +224,8 @@ func (pconf *Path) setDefaults() {
|
|||
pconf.RPICameraLevel = "4.1"
|
||||
|
||||
// Hooks
|
||||
pconf.RunOnDemandStartTimeout = 10 * StringDuration(time.Second)
|
||||
pconf.RunOnDemandCloseAfter = 10 * StringDuration(time.Second)
|
||||
pconf.RunOnDemandStartTimeout = 10 * Duration(time.Second)
|
||||
pconf.RunOnDemandCloseAfter = 10 * Duration(time.Second)
|
||||
}
|
||||
|
||||
func newPath(defaults *Path, partial *OptionalPath) *Path {
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StringDuration is a duration that is unmarshaled from a string.
|
||||
// Durations are normally unmarshaled from numbers.
|
||||
type StringDuration time.Duration
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (d StringDuration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *StringDuration) UnmarshalJSON(b []byte) error {
|
||||
var in string
|
||||
if err := json.Unmarshal(b, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
du, err := time.ParseDuration(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = StringDuration(du)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalEnv implements env.Unmarshaler.
|
||||
func (d *StringDuration) UnmarshalEnv(_ string, v string) error {
|
||||
return d.UnmarshalJSON([]byte(`"` + v + `"`))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue