Skip to content

Commit

Permalink
allow setting different recording parameters for each path (#2410)
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Oct 7, 2023
1 parent 9a01ab7 commit 61d24eb
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 176 deletions.
77 changes: 34 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1029,13 +1029,12 @@ There are 3 ways to change the configuration:
### Authentication
Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
Edit `mediamtx.yml` and set `publishUser` and `publishPass`:
```yml
paths:
all:
publishUser: myuser
publishPass: mypass
pathDefaults:
publishUser: myuser
publishPass: mypass
```
Only publishers that provide both username and password will be able to proceed:
Expand All @@ -1047,13 +1046,9 @@ ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://myuser:mypass@local
It's possible to setup authentication for readers too:
```yml
paths:
all:
publishUser: myuser
publishPass: mypass
readUser: user
readPass: userpass
pathDefaults:
readUser: user
readPass: userpass
```
If storing plain credentials in the configuration file is a security problem, username and passwords can be stored as sha256-hashed strings; a string must be hashed with sha256 and encoded with base64:
Expand All @@ -1065,10 +1060,9 @@ echo -n "userpass" | openssl dgst -binary -sha256 | openssl base64
Then stored with the `sha256:` prefix:
```yml
paths:
all:
readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
pathDefaults:
readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
```
**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials in transit.
Expand Down Expand Up @@ -1133,7 +1127,7 @@ To change the format, codec or compression of a stream, use _FFmpeg_ or _GStream
```yml
paths:
all:
compressed:
original:
runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
Expand All @@ -1147,12 +1141,13 @@ paths:
To save available streams to disk, set the `record` and the `recordPath` parameter in the configuration file:
```yml
# Record streams to disk.
record: yes
# Path of recording segments.
# Extension is added automatically.
# Available variables are %path (path name), %Y %m %d %H %M %S %f (time in strftime format)
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
pathDefaults:
# Record streams to disk.
record: yes
# Path of recording segments.
# Extension is added automatically.
# Available variables are %path (path name), %Y %m %d %H %M %S %f (time in strftime format)
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
```
All available recording parameters are listed in the [sample configuration file](/mediamtx.yml).
Expand All @@ -1172,17 +1167,14 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
3. Place `rclone` into the `runOnInit` and `runOnRecordSegmentComplete` hooks:
```yml
record: yes
paths:
mypath:
# this is needed to sync segments after a crash.
# replace myconfig with the name of the rclone config.
runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings
# this is called when a segment has been finalized.
# replace myconfig with the name of the rclone config.
runOnRecordSegmentComplete: rclone sync -v --min-age=1ms ./recordings myconfig:/my-path/recordings
pathDefaults:
# this is needed to sync segments after a crash.
# replace myconfig with the name of the rclone config.
runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings
# this is called when a segment has been finalized.
# replace myconfig with the name of the rclone config.
runOnRecordSegmentComplete: rclone sync -v --min-age=1ms ./recordings myconfig:/my-path/recordings
```
If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.
Expand All @@ -1192,13 +1184,12 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
```yml
paths:
all:
runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
-c copy
-f rtsp rtsp://another-server/another-path
runOnReadyRestart: yes
pathDefaults:
runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
-c copy
-f rtsp rtsp://another-server/another-path
runOnReadyRestart: yes
```
### On-demand publishing
Expand Down Expand Up @@ -1382,12 +1373,12 @@ paths:
runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID
```
`runOnRecordSegmentComplete` allows to run a command when a record segment is complete:
`runOnRecordSegmentComplete` allows to run a command when a recording segment is complete:
```yml
paths:
mypath:
# Command to run when a record segment is complete.
# Command to run when a recording segment is complete.
# The following environment variables are available:
# * MTX_PATH: path name
# * RTSP_PORT: RTSP server port
Expand Down
39 changes: 23 additions & 16 deletions internal/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ type Conf struct {
SRTAddress string `json:"srtAddress"`

// Record
Record bool `json:"record"`
RecordPath string `json:"recordPath"`
RecordFormat string `json:"recordFormat"`
RecordPartDuration StringDuration `json:"recordPartDuration"`
RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`
Record *bool `json:"record,omitempty"` // deprecated
RecordPath *string `json:"recordPath,omitempty"` // deprecated
RecordFormat *string `json:"recordFormat,omitempty"` // deprecated
RecordPartDuration *StringDuration `json:"recordPartDuration,omitempty"` // deprecated
RecordSegmentDuration *StringDuration `json:"recordSegmentDuration,omitempty"` // deprecated
RecordDeleteAfter *StringDuration `json:"recordDeleteAfter,omitempty"` // deprecated

// Path defaults
PathDefaults Path `json:"pathDefaults"`
Expand Down Expand Up @@ -242,13 +242,6 @@ func (conf *Conf) setDefaults() {
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()
}

Expand Down Expand Up @@ -414,9 +407,23 @@ func (conf *Conf) Check() error {
}

// Record

if conf.RecordFormat != "fmp4" {
return fmt.Errorf("unsupported record format '%s'", conf.RecordFormat)
if conf.Record != nil {
conf.PathDefaults.Record = *conf.Record
}

Check warning on line 412 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L411-L412

Added lines #L411 - L412 were not covered by tests
if conf.RecordPath != nil {
conf.PathDefaults.RecordPath = *conf.RecordPath
}

Check warning on line 415 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L414-L415

Added lines #L414 - L415 were not covered by tests
if conf.RecordFormat != nil {
conf.PathDefaults.RecordFormat = *conf.RecordFormat
}

Check warning on line 418 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L417-L418

Added lines #L417 - L418 were not covered by tests
if conf.RecordPartDuration != nil {
conf.PathDefaults.RecordPartDuration = *conf.RecordPartDuration
}

Check warning on line 421 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L420-L421

Added lines #L420 - L421 were not covered by tests
if conf.RecordSegmentDuration != nil {
conf.PathDefaults.RecordSegmentDuration = *conf.RecordSegmentDuration
}

Check warning on line 424 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L423-L424

Added lines #L423 - L424 were not covered by tests
if conf.RecordDeleteAfter != nil {
conf.PathDefaults.RecordDeleteAfter = *conf.RecordDeleteAfter

Check warning on line 426 in internal/conf/conf.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/conf.go#L426

Added line #L426 was not covered by tests
}

conf.Paths = make(map[string]*Path)
Expand Down
6 changes: 5 additions & 1 deletion internal/conf/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ func TestConfFromFile(t *testing.T) {
Source: "publisher",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 10 * StringDuration(time.Second),
Record: true,
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
RecordFormat: "fmp4",
RecordPartDuration: 100000000,
RecordSegmentDuration: 3600000000000,
RecordDeleteAfter: 86400000000000,
OverridePublisher: true,
RPICameraWidth: 1920,
RPICameraHeight: 1080,
Expand Down
23 changes: 21 additions & 2 deletions internal/conf/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ type Path struct {
SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"`
MaxReaders int `json:"maxReaders"`
SRTReadPassphrase string `json:"srtReadPassphrase"`
Record bool `json:"record"`

// Record
Record bool `json:"record"`
RecordPath string `json:"recordPath"`
RecordFormat string `json:"recordFormat"`
RecordPartDuration StringDuration `json:"recordPartDuration"`
RecordSegmentDuration StringDuration `json:"recordSegmentDuration"`
RecordDeleteAfter StringDuration `json:"recordDeleteAfter"`

// Authentication
PublishUser Credential `json:"publishUser"`
Expand Down Expand Up @@ -139,7 +146,13 @@ func (pconf *Path) setDefaults() {
pconf.Source = "publisher"
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
pconf.SourceOnDemandCloseAfter = 10 * StringDuration(time.Second)
pconf.Record = true

// Record
pconf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
pconf.RecordFormat = "fmp4"
pconf.RecordPartDuration = 100 * StringDuration(time.Millisecond)
pconf.RecordSegmentDuration = 3600 * StringDuration(time.Second)
pconf.RecordDeleteAfter = 24 * 3600 * StringDuration(time.Second)

// Publisher
pconf.OverridePublisher = true
Expand Down Expand Up @@ -386,6 +399,12 @@ func (pconf *Path) check(conf *Conf, name string) error {
}
}

// Record

if pconf.RecordFormat != "fmp4" {
return fmt.Errorf("unsupported record format '%s'", pconf.RecordFormat)
}

Check warning on line 406 in internal/conf/path.go

View check run for this annotation

Codecov / codecov/patch

internal/conf/path.go#L405-L406

Added lines #L405 - L406 were not covered by tests

// Publisher

if pconf.DisablePublisherOverride != nil {
Expand Down
52 changes: 37 additions & 15 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/signal"
"path/filepath"
"reflect"
"sort"
"strings"
"time"

Expand All @@ -33,6 +34,37 @@ var defaultConfPaths = []string{
"/etc/mediamtx/mediamtx.yml",
}

func gatherCleanerEntries(paths map[string]*conf.Path) []record.CleanerEntry {
out := make(map[record.CleanerEntry]struct{})

for _, pa := range paths {
if pa.Record {
entry := record.CleanerEntry{
RecordPath: pa.RecordPath,
RecordDeleteAfter: time.Duration(pa.RecordDeleteAfter),
}
out[entry] = struct{}{}
}
}

out2 := make([]record.CleanerEntry, len(out))
i := 0

for v := range out {
out2[i] = v
i++
}

sort.Slice(out2, func(i, j int) bool {
if out2[i].RecordPath != out2[j].RecordPath {
return out2[i].RecordPath < out2[j].RecordPath
}
return out2[i].RecordDeleteAfter < out2[j].RecordDeleteAfter

Check warning on line 62 in internal/core/core.go

View check run for this annotation

Codecov / codecov/patch

internal/core/core.go#L59-L62

Added lines #L59 - L62 were not covered by tests
})

return out2
}

var cli struct {
Version bool `help:"print version"`
Confpath string `arg:"" default:""`
Expand Down Expand Up @@ -259,12 +291,11 @@ func (p *Core) createResources(initial bool) error {
}
}

if p.conf.Record &&
p.conf.RecordDeleteAfter != 0 &&
cleanerEntries := gatherCleanerEntries(p.conf.Paths)
if len(cleanerEntries) != 0 &&
p.recordCleaner == nil {
p.recordCleaner = record.NewCleaner(
p.conf.RecordPath,
time.Duration(p.conf.RecordDeleteAfter),
cleanerEntries,
p,
)
}
Expand All @@ -278,10 +309,6 @@ func (p *Core) createResources(initial bool) error {
p.conf.WriteTimeout,
p.conf.WriteQueueSize,
p.conf.UDPMaxPayloadSize,
p.conf.Record,
p.conf.RecordPath,
p.conf.RecordPartDuration,
p.conf.RecordSegmentDuration,
p.conf.Paths,
p.externalCmdPool,
p.metrics,
Expand Down Expand Up @@ -539,9 +566,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeLogger

closeRecorderCleaner := newConf == nil ||
newConf.Record != p.conf.Record ||
newConf.RecordPath != p.conf.RecordPath ||
newConf.RecordDeleteAfter != p.conf.RecordDeleteAfter
!reflect.DeepEqual(gatherCleanerEntries(newConf.Paths), gatherCleanerEntries(p.conf.Paths)) ||
closeLogger

closePathManager := newConf == nil ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
Expand All @@ -551,10 +577,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.WriteQueueSize != p.conf.WriteQueueSize ||
newConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize ||
newConf.Record != p.conf.Record ||
newConf.RecordPath != p.conf.RecordPath ||
newConf.RecordPartDuration != p.conf.RecordPartDuration ||
newConf.RecordSegmentDuration != p.conf.RecordSegmentDuration ||
closeMetrics ||
closeLogger
if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
Expand Down
Loading

0 comments on commit 61d24eb

Please sign in to comment.