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 authored Oct 7, 2023
1 parent 9a01ab7 commit 8a633d2
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 177 deletions.
79 changes: 35 additions & 44 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,17 +1141,18 @@ 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).
Be aware that not all tracks can be saved. A compatibility matrix is available at the beginning of the README.
Be aware that not all codecs can be saved with all formats, as described in the compatibility matrix at the beginning of the README.
To upload recordings to a remote location, you can use _MediaMTX_ together with [rclone](https://github.com/rclone/rclone), a command line tool that provides file synchronization capabilities with a huge variety of services (including S3, FTP, SMB, Google Drive):
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
}
if conf.RecordPath != nil {
conf.PathDefaults.RecordPath = *conf.RecordPath
}
if conf.RecordFormat != nil {
conf.PathDefaults.RecordFormat = *conf.RecordFormat
}
if conf.RecordPartDuration != nil {
conf.PathDefaults.RecordPartDuration = *conf.RecordPartDuration
}
if conf.RecordSegmentDuration != nil {
conf.PathDefaults.RecordSegmentDuration = *conf.RecordSegmentDuration
}
if conf.RecordDeleteAfter != nil {
conf.PathDefaults.RecordDeleteAfter = *conf.RecordDeleteAfter
}

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)
}

// 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
})

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 8a633d2

Please sign in to comment.