Browse Source

allow setting different recording parameters for each path (#2410) (#2457)

pull/2479/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
8a633d2b79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 43
      README.md
  2. 39
      internal/conf/conf.go
  3. 6
      internal/conf/conf_test.go
  4. 21
      internal/conf/path.go
  5. 52
      internal/core/core.go
  6. 26
      internal/core/path.go
  7. 16
      internal/core/path_manager.go
  8. 42
      internal/record/cleaner.go
  9. 6
      internal/record/cleaner_test.go
  10. 48
      mediamtx.yml

43
README.md

@ -1029,11 +1029,10 @@ There are 3 ways to change the configuration:
### Authentication ### Authentication
Edit `mediamtx.yml` and replace everything inside section `paths` with the following content: Edit `mediamtx.yml` and set `publishUser` and `publishPass`:
```yml ```yml
paths: pathDefaults:
all:
publishUser: myuser publishUser: myuser
publishPass: mypass publishPass: mypass
``` ```
@ -1047,11 +1046,7 @@ 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: It's possible to setup authentication for readers too:
```yml ```yml
paths: pathDefaults:
all:
publishUser: myuser
publishPass: mypass
readUser: user readUser: user
readPass: userpass readPass: userpass
``` ```
@ -1065,8 +1060,7 @@ echo -n "userpass" | openssl dgst -binary -sha256 | openssl base64
Then stored with the `sha256:` prefix: Then stored with the `sha256:` prefix:
```yml ```yml
paths: pathDefaults:
all:
readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo= readUser: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=
readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ= readPass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=
``` ```
@ -1133,7 +1127,7 @@ To change the format, codec or compression of a stream, use _FFmpeg_ or _GStream
```yml ```yml
paths: paths:
all: compressed:
original: original:
runOnReady: > runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
@ -1147,17 +1141,18 @@ paths:
To save available streams to disk, set the `record` and the `recordPath` parameter in the configuration file: To save available streams to disk, set the `record` and the `recordPath` parameter in the configuration file:
```yml ```yml
# Record streams to disk. pathDefaults:
record: yes # Record streams to disk.
# Path of recording segments. record: yes
# Extension is added automatically. # Path of recording segments.
# Available variables are %path (path name), %Y %m %d %H %M %S %f (time in strftime format) # Extension is added automatically.
recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f # 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). 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): 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):
@ -1172,10 +1167,7 @@ To upload recordings to a remote location, you can use _MediaMTX_ together with
3. Place `rclone` into the `runOnInit` and `runOnRecordSegmentComplete` hooks: 3. Place `rclone` into the `runOnInit` and `runOnRecordSegmentComplete` hooks:
```yml ```yml
record: yes pathDefaults:
paths:
mypath:
# this is needed to sync segments after a crash. # this is needed to sync segments after a crash.
# replace myconfig with the name of the rclone config. # replace myconfig with the name of the rclone config.
runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings
@ -1192,8 +1184,7 @@ 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: To forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:
```yml ```yml
paths: pathDefaults:
all:
runOnReady: > runOnReady: >
ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH
-c copy -c copy
@ -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 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 ```yml
paths: paths:
mypath: 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: # The following environment variables are available:
# * MTX_PATH: path name # * MTX_PATH: path name
# * RTSP_PORT: RTSP server port # * RTSP_PORT: RTSP server port

39
internal/conf/conf.go

@ -163,12 +163,12 @@ type Conf struct {
SRTAddress string `json:"srtAddress"` SRTAddress string `json:"srtAddress"`
// Record // Record
Record bool `json:"record"` Record *bool `json:"record,omitempty"` // deprecated
RecordPath string `json:"recordPath"` RecordPath *string `json:"recordPath,omitempty"` // deprecated
RecordFormat string `json:"recordFormat"` RecordFormat *string `json:"recordFormat,omitempty"` // deprecated
RecordPartDuration StringDuration `json:"recordPartDuration"` RecordPartDuration *StringDuration `json:"recordPartDuration,omitempty"` // deprecated
RecordSegmentDuration StringDuration `json:"recordSegmentDuration"` RecordSegmentDuration *StringDuration `json:"recordSegmentDuration,omitempty"` // deprecated
RecordDeleteAfter StringDuration `json:"recordDeleteAfter"` RecordDeleteAfter *StringDuration `json:"recordDeleteAfter,omitempty"` // deprecated
// Path defaults // Path defaults
PathDefaults Path `json:"pathDefaults"` PathDefaults Path `json:"pathDefaults"`
@ -242,13 +242,6 @@ func (conf *Conf) setDefaults() {
conf.SRT = true conf.SRT = true
conf.SRTAddress = ":8890" 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() conf.PathDefaults.setDefaults()
} }
@ -414,9 +407,23 @@ func (conf *Conf) Check() error {
} }
// Record // Record
if conf.Record != nil {
if conf.RecordFormat != "fmp4" { conf.PathDefaults.Record = *conf.Record
return fmt.Errorf("unsupported record format '%s'", conf.RecordFormat) }
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) conf.Paths = make(map[string]*Path)

6
internal/conf/conf_test.go

@ -51,7 +51,11 @@ func TestConfFromFile(t *testing.T) {
Source: "publisher", Source: "publisher",
SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandStartTimeout: 10 * StringDuration(time.Second),
SourceOnDemandCloseAfter: 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, OverridePublisher: true,
RPICameraWidth: 1920, RPICameraWidth: 1920,
RPICameraHeight: 1080, RPICameraHeight: 1080,

21
internal/conf/path.go

@ -59,7 +59,14 @@ type Path struct {
SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"` SourceOnDemandCloseAfter StringDuration `json:"sourceOnDemandCloseAfter"`
MaxReaders int `json:"maxReaders"` MaxReaders int `json:"maxReaders"`
SRTReadPassphrase string `json:"srtReadPassphrase"` SRTReadPassphrase string `json:"srtReadPassphrase"`
// Record
Record bool `json:"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 // Authentication
PublishUser Credential `json:"publishUser"` PublishUser Credential `json:"publishUser"`
@ -139,7 +146,13 @@ func (pconf *Path) setDefaults() {
pconf.Source = "publisher" pconf.Source = "publisher"
pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second) pconf.SourceOnDemandStartTimeout = 10 * StringDuration(time.Second)
pconf.SourceOnDemandCloseAfter = 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 // Publisher
pconf.OverridePublisher = true pconf.OverridePublisher = true
@ -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 // Publisher
if pconf.DisablePublisherOverride != nil { if pconf.DisablePublisherOverride != nil {

52
internal/core/core.go

@ -8,6 +8,7 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort"
"strings" "strings"
"time" "time"
@ -33,6 +34,37 @@ var defaultConfPaths = []string{
"/etc/mediamtx/mediamtx.yml", "/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 { var cli struct {
Version bool `help:"print version"` Version bool `help:"print version"`
Confpath string `arg:"" default:""` Confpath string `arg:"" default:""`
@ -259,12 +291,11 @@ func (p *Core) createResources(initial bool) error {
} }
} }
if p.conf.Record && cleanerEntries := gatherCleanerEntries(p.conf.Paths)
p.conf.RecordDeleteAfter != 0 && if len(cleanerEntries) != 0 &&
p.recordCleaner == nil { p.recordCleaner == nil {
p.recordCleaner = record.NewCleaner( p.recordCleaner = record.NewCleaner(
p.conf.RecordPath, cleanerEntries,
time.Duration(p.conf.RecordDeleteAfter),
p, p,
) )
} }
@ -278,10 +309,6 @@ func (p *Core) createResources(initial bool) error {
p.conf.WriteTimeout, p.conf.WriteTimeout,
p.conf.WriteQueueSize, p.conf.WriteQueueSize,
p.conf.UDPMaxPayloadSize, p.conf.UDPMaxPayloadSize,
p.conf.Record,
p.conf.RecordPath,
p.conf.RecordPartDuration,
p.conf.RecordSegmentDuration,
p.conf.Paths, p.conf.Paths,
p.externalCmdPool, p.externalCmdPool,
p.metrics, p.metrics,
@ -539,9 +566,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
closeLogger closeLogger
closeRecorderCleaner := newConf == nil || closeRecorderCleaner := newConf == nil ||
newConf.Record != p.conf.Record || !reflect.DeepEqual(gatherCleanerEntries(newConf.Paths), gatherCleanerEntries(p.conf.Paths)) ||
newConf.RecordPath != p.conf.RecordPath || closeLogger
newConf.RecordDeleteAfter != p.conf.RecordDeleteAfter
closePathManager := newConf == nil || closePathManager := newConf == nil ||
newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL ||
@ -551,10 +577,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteTimeout != p.conf.WriteTimeout ||
newConf.WriteQueueSize != p.conf.WriteQueueSize || newConf.WriteQueueSize != p.conf.WriteQueueSize ||
newConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize || 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 || closeMetrics ||
closeLogger closeLogger
if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) { if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {

26
internal/core/path.go

@ -171,10 +171,6 @@ type path struct {
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
writeQueueSize int writeQueueSize int
udpMaxPayloadSize int udpMaxPayloadSize int
record bool
recordPath string
recordPartDuration conf.StringDuration
recordSegmentDuration conf.StringDuration
confName string confName string
conf *conf.Path conf *conf.Path
name string name string
@ -226,10 +222,6 @@ func newPath(
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
writeQueueSize int, writeQueueSize int,
udpMaxPayloadSize int, udpMaxPayloadSize int,
record bool,
recordPath string,
recordPartDuration conf.StringDuration,
recordSegmentDuration conf.StringDuration,
confName string, confName string,
cnf *conf.Path, cnf *conf.Path,
name string, name string,
@ -246,10 +238,6 @@ func newPath(
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
writeQueueSize: writeQueueSize, writeQueueSize: writeQueueSize,
udpMaxPayloadSize: udpMaxPayloadSize, udpMaxPayloadSize: udpMaxPayloadSize,
record: record,
recordPath: recordPath,
recordPartDuration: recordPartDuration,
recordSegmentDuration: recordSegmentDuration,
confName: confName, confName: confName,
conf: cnf, conf: cnf,
name: name, name: name,
@ -514,7 +502,7 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
go pa.source.(*sourceStatic).reloadConf(newConf) go pa.source.(*sourceStatic).reloadConf(newConf)
} }
if pa.recordingEnabled() { if pa.conf.Record {
if pa.stream != nil && pa.recordAgent == nil { if pa.stream != nil && pa.recordAgent == nil {
pa.startRecording() pa.startRecording()
} }
@ -793,10 +781,6 @@ func (pa *path) shouldClose() bool {
len(pa.readerAddRequestsOnHold) == 0 len(pa.readerAddRequestsOnHold) == 0
} }
func (pa *path) recordingEnabled() bool {
return pa.record && pa.conf.Record
}
func (pa *path) externalCmdEnv() externalcmd.Environment { func (pa *path) externalCmdEnv() externalcmd.Environment {
_, port, _ := net.SplitHostPort(pa.rtspAddress) _, port, _ := net.SplitHostPort(pa.rtspAddress)
env := externalcmd.Environment{ env := externalcmd.Environment{
@ -897,7 +881,7 @@ func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error
return err return err
} }
if pa.recordingEnabled() { if pa.conf.Record {
pa.startRecording() pa.startRecording()
} }
@ -968,9 +952,9 @@ func (pa *path) setNotReady() {
func (pa *path) startRecording() { func (pa *path) startRecording() {
pa.recordAgent = record.NewAgent( pa.recordAgent = record.NewAgent(
pa.writeQueueSize, pa.writeQueueSize,
pa.recordPath, pa.conf.RecordPath,
time.Duration(pa.recordPartDuration), time.Duration(pa.conf.RecordPartDuration),
time.Duration(pa.recordSegmentDuration), time.Duration(pa.conf.RecordSegmentDuration),
pa.name, pa.name,
pa.stream, pa.stream,
func(segmentPath string) { func(segmentPath string) {

16
internal/core/path_manager.go

@ -73,10 +73,6 @@ type pathManager struct {
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
writeQueueSize int writeQueueSize int
udpMaxPayloadSize int udpMaxPayloadSize int
record bool
recordPath string
recordPartDuration conf.StringDuration
recordSegmentDuration conf.StringDuration
pathConfs map[string]*conf.Path pathConfs map[string]*conf.Path
externalCmdPool *externalcmd.Pool externalCmdPool *externalcmd.Pool
metrics *metrics metrics *metrics
@ -111,10 +107,6 @@ func newPathManager(
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
writeQueueSize int, writeQueueSize int,
udpMaxPayloadSize int, udpMaxPayloadSize int,
record bool,
recordPath string,
recordPartDuration conf.StringDuration,
recordSegmentDuration conf.StringDuration,
pathConfs map[string]*conf.Path, pathConfs map[string]*conf.Path,
externalCmdPool *externalcmd.Pool, externalCmdPool *externalcmd.Pool,
metrics *metrics, metrics *metrics,
@ -130,10 +122,6 @@ func newPathManager(
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
writeQueueSize: writeQueueSize, writeQueueSize: writeQueueSize,
udpMaxPayloadSize: udpMaxPayloadSize, udpMaxPayloadSize: udpMaxPayloadSize,
record: record,
recordPath: recordPath,
recordPartDuration: recordPartDuration,
recordSegmentDuration: recordSegmentDuration,
pathConfs: pathConfs, pathConfs: pathConfs,
externalCmdPool: externalCmdPool, externalCmdPool: externalCmdPool,
metrics: metrics, metrics: metrics,
@ -412,10 +400,6 @@ func (pm *pathManager) createPath(
pm.writeTimeout, pm.writeTimeout,
pm.writeQueueSize, pm.writeQueueSize,
pm.udpMaxPayloadSize, pm.udpMaxPayloadSize,
pm.record,
pm.recordPath,
pm.recordPartDuration,
pm.recordSegmentDuration,
pathConfName, pathConfName,
pathConf, pathConf,
name, name,

42
internal/record/cleaner.go

@ -38,12 +38,17 @@ func commonPath(v string) string {
return common return common
} }
// Cleaner removes expired recordings from disk. // CleanerEntry is a cleaner entry.
type CleanerEntry struct {
RecordPath string
RecordDeleteAfter time.Duration
}
// Cleaner removes expired recording segments from disk.
type Cleaner struct { type Cleaner struct {
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
path string entries []CleanerEntry
deleteAfter time.Duration
parent logger.Writer parent logger.Writer
done chan struct{} done chan struct{}
@ -51,19 +56,15 @@ type Cleaner struct {
// NewCleaner allocates a Cleaner. // NewCleaner allocates a Cleaner.
func NewCleaner( func NewCleaner(
recordPath string, entries []CleanerEntry,
deleteAfter time.Duration,
parent logger.Writer, parent logger.Writer,
) *Cleaner { ) *Cleaner {
recordPath += ".mp4"
ctx, ctxCancel := context.WithCancel(context.Background()) ctx, ctxCancel := context.WithCancel(context.Background())
c := &Cleaner{ c := &Cleaner{
ctx: ctx, ctx: ctx,
ctxCancel: ctxCancel, ctxCancel: ctxCancel,
path: recordPath, entries: entries,
deleteAfter: deleteAfter,
parent: parent, parent: parent,
done: make(chan struct{}), done: make(chan struct{}),
} }
@ -88,8 +89,10 @@ func (c *Cleaner) run() {
defer close(c.done) defer close(c.done)
interval := 30 * 60 * time.Second interval := 30 * 60 * time.Second
if interval > (c.deleteAfter / 2) { for _, e := range c.entries {
interval = c.deleteAfter / 2 if interval > (e.RecordDeleteAfter / 2) {
interval = e.RecordDeleteAfter / 2
}
} }
c.doRun() //nolint:errcheck c.doRun() //nolint:errcheck
@ -97,7 +100,7 @@ func (c *Cleaner) run() {
for { for {
select { select {
case <-time.After(interval): case <-time.After(interval):
c.doRun() //nolint:errcheck c.doRun()
case <-c.ctx.Done(): case <-c.ctx.Done():
return return
@ -105,8 +108,15 @@ func (c *Cleaner) run() {
} }
} }
func (c *Cleaner) doRun() error { func (c *Cleaner) doRun() {
commonPath := commonPath(c.path) for _, e := range c.entries {
c.doRunEntry(&e) //nolint:errcheck
}
}
func (c *Cleaner) doRunEntry(e *CleanerEntry) error {
recordPath := e.RecordPath + ".mp4"
commonPath := commonPath(recordPath)
now := timeNow() now := timeNow()
filepath.Walk(commonPath, func(path string, info fs.FileInfo, err error) error { //nolint:errcheck filepath.Walk(commonPath, func(path string, info fs.FileInfo, err error) error { //nolint:errcheck
@ -115,9 +125,9 @@ func (c *Cleaner) doRun() error {
} }
if !info.IsDir() { if !info.IsDir() {
params := decodeRecordPath(c.path, path) params := decodeRecordPath(recordPath, path)
if params != nil { if params != nil {
if now.Sub(params.time) > c.deleteAfter { if now.Sub(params.time) > e.RecordDeleteAfter {
c.Log(logger.Debug, "removing %s", path) c.Log(logger.Debug, "removing %s", path)
os.Remove(path) os.Remove(path)
} }

6
internal/record/cleaner_test.go

@ -30,8 +30,10 @@ func TestCleaner(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
c := NewCleaner( c := NewCleaner(
recordPath, []CleanerEntry{{
10*time.Second, RecordPath: recordPath,
RecordDeleteAfter: 10 * time.Second,
}},
nilLogger{}, nilLogger{},
) )
defer c.Close() defer c.Close()

48
mediamtx.yml

@ -245,28 +245,6 @@ srt: yes
# Address of the SRT listener. # Address of the SRT listener.
srtAddress: :8890 srtAddress: :8890
###############################################
# Global settings -> Recording
# Record streams to disk.
record: no
# 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
# Format of recorded segments.
# Currently the only available format is fmp4 (fragmented MP4).
recordFormat: fmp4
# fMP4 segments are concatenation of small MP4 files (parts), each with this duration.
# When a system failure occurs, the last part gets lost.
# Therefore, the part duration is equal to the RPO (recovery point objective).
recordPartDuration: 100ms
# Minimum duration of each segment.
recordSegmentDuration: 1h
# Delete segments after this timespan.
# Set to 0s to disable automatic deletion.
recordDeleteAfter: 24h
############################################### ###############################################
# Default path settings # Default path settings
@ -311,8 +289,28 @@ pathDefaults:
maxReaders: 0 maxReaders: 0
# SRT encryption passphrase require to read from this path # SRT encryption passphrase require to read from this path
srtReadPassphrase: srtReadPassphrase:
# Record streams to disk (if global recording is enabled).
record: yes ###############################################
# Default path settings -> Recording
# Record streams to disk.
record: no
# 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
# Format of recorded segments.
# Currently the only available format is fmp4 (fragmented MP4).
recordFormat: fmp4
# fMP4 segments are concatenation of small MP4 files (parts), each with this duration.
# When a system failure occurs, the last part gets lost.
# Therefore, the part duration is equal to the RPO (recovery point objective).
recordPartDuration: 100ms
# Minimum duration of each segment.
recordSegmentDuration: 1h
# Delete segments after this timespan.
# Set to 0s to disable automatic deletion.
recordDeleteAfter: 24h
############################################### ###############################################
# Default path settings -> Authentication # Default path settings -> Authentication
@ -520,7 +518,7 @@ pathDefaults:
# Environment variables are the same of runOnRead. # Environment variables are the same of runOnRead.
runOnUnread: runOnUnread:
# Command to run when a record segment is complete. # Command to run when a recording segment is complete.
# The following environment variables are available: # The following environment variables are available:
# * MTX_PATH: path name # * MTX_PATH: path name
# * RTSP_PORT: RTSP server port # * RTSP_PORT: RTSP server port

Loading…
Cancel
Save