This commit is contained in:
+30
-9
@@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
||||||
"github.com/bluenviron/mediamtx/internal/record"
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
|
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/rtmp"
|
"github.com/bluenviron/mediamtx/internal/servers/rtmp"
|
||||||
@@ -56,6 +56,27 @@ func paramName(ctx *gin.Context) (string, bool) {
|
|||||||
return name[1:], true
|
return name[1:], true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordingsOfPath(
|
||||||
|
pathConf *conf.Path,
|
||||||
|
pathName string,
|
||||||
|
) *defs.APIRecording {
|
||||||
|
ret := &defs.APIRecording{
|
||||||
|
Name: pathName,
|
||||||
|
}
|
||||||
|
|
||||||
|
segments, _ := recordstore.FindSegments(pathConf, pathName)
|
||||||
|
|
||||||
|
ret.Segments = make([]*defs.APIRecordingSegment, len(segments))
|
||||||
|
|
||||||
|
for i, seg := range segments {
|
||||||
|
ret.Segments[i] = &defs.APIRecordingSegment{
|
||||||
|
Start: seg.Start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// PathManager contains methods used by the API and Metrics server.
|
// PathManager contains methods used by the API and Metrics server.
|
||||||
type PathManager interface {
|
type PathManager interface {
|
||||||
APIPathsList() (*defs.APIPathList, error)
|
APIPathsList() (*defs.APIPathList, error)
|
||||||
@@ -1062,7 +1083,7 @@ func (a *API) onRecordingsList(ctx *gin.Context) {
|
|||||||
c := a.Conf
|
c := a.Conf
|
||||||
a.mutex.RUnlock()
|
a.mutex.RUnlock()
|
||||||
|
|
||||||
pathNames := getAllPathsWithRecordings(c.Paths)
|
pathNames := recordstore.FindAllPathsWithSegments(c.Paths)
|
||||||
|
|
||||||
data := defs.APIRecordingList{}
|
data := defs.APIRecordingList{}
|
||||||
|
|
||||||
@@ -1077,8 +1098,8 @@ func (a *API) onRecordingsList(ctx *gin.Context) {
|
|||||||
data.Items = make([]*defs.APIRecording, len(pathNames))
|
data.Items = make([]*defs.APIRecording, len(pathNames))
|
||||||
|
|
||||||
for i, pathName := range pathNames {
|
for i, pathName := range pathNames {
|
||||||
_, pathConf, _, _ := conf.FindPathConf(c.Paths, pathName)
|
pathConf, _, _ := conf.FindPathConf(c.Paths, pathName)
|
||||||
data.Items[i] = recordingEntry(pathConf, pathName)
|
data.Items[i] = recordingsOfPath(pathConf, pathName)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, data)
|
ctx.JSON(http.StatusOK, data)
|
||||||
@@ -1095,13 +1116,13 @@ func (a *API) onRecordingsGet(ctx *gin.Context) {
|
|||||||
c := a.Conf
|
c := a.Conf
|
||||||
a.mutex.RUnlock()
|
a.mutex.RUnlock()
|
||||||
|
|
||||||
_, pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
|
pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.writeError(ctx, http.StatusBadRequest, err)
|
a.writeError(ctx, http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, recordingEntry(pathConf, pathName))
|
ctx.JSON(http.StatusOK, recordingsOfPath(pathConf, pathName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) onRecordingDeleteSegment(ctx *gin.Context) {
|
func (a *API) onRecordingDeleteSegment(ctx *gin.Context) {
|
||||||
@@ -1117,18 +1138,18 @@ func (a *API) onRecordingDeleteSegment(ctx *gin.Context) {
|
|||||||
c := a.Conf
|
c := a.Conf
|
||||||
a.mutex.RUnlock()
|
a.mutex.RUnlock()
|
||||||
|
|
||||||
_, pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
|
pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.writeError(ctx, http.StatusBadRequest, err)
|
a.writeError(ctx, http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pathFormat := record.PathAddExtension(
|
pathFormat := recordstore.PathAddExtension(
|
||||||
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
||||||
pathConf.RecordFormat,
|
pathConf.RecordFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
segmentPath := record.Path{
|
segmentPath := recordstore.Path{
|
||||||
Start: start,
|
Start: start,
|
||||||
}.Encode(pathFormat)
|
}.Encode(pathFormat)
|
||||||
|
|
||||||
|
|||||||
@@ -595,6 +595,7 @@ func TestRecordingsList(t *testing.T) {
|
|||||||
cnf := tempConf(t, "pathDefaults:\n"+
|
cnf := tempConf(t, "pathDefaults:\n"+
|
||||||
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
|
" recordPath: "+filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")+"\n"+
|
||||||
"paths:\n"+
|
"paths:\n"+
|
||||||
|
" mypath1:\n"+
|
||||||
" all_others:\n")
|
" all_others:\n")
|
||||||
|
|
||||||
api := API{
|
api := API{
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/playback"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/record"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errFound = errors.New("found")
|
|
||||||
|
|
||||||
func fixedPathHasRecordings(pathConf *conf.Path) bool {
|
|
||||||
recordPath := record.PathAddExtension(
|
|
||||||
strings.ReplaceAll(pathConf.RecordPath, "%path", pathConf.Name),
|
|
||||||
pathConf.RecordFormat,
|
|
||||||
)
|
|
||||||
|
|
||||||
// we have to convert to absolute paths
|
|
||||||
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
|
||||||
recordPath, _ = filepath.Abs(recordPath)
|
|
||||||
|
|
||||||
commonPath := record.CommonPath(recordPath)
|
|
||||||
|
|
||||||
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
var pa record.Path
|
|
||||||
ok := pa.Decode(recordPath, fpath)
|
|
||||||
if ok {
|
|
||||||
return errFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil && !errors.Is(err, errFound) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Is(err, errFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func regexpPathGetRecordings(pathConf *conf.Path) []string {
|
|
||||||
recordPath := record.PathAddExtension(
|
|
||||||
pathConf.RecordPath,
|
|
||||||
pathConf.RecordFormat,
|
|
||||||
)
|
|
||||||
|
|
||||||
// we have to convert to absolute paths
|
|
||||||
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
|
||||||
recordPath, _ = filepath.Abs(recordPath)
|
|
||||||
|
|
||||||
commonPath := record.CommonPath(recordPath)
|
|
||||||
|
|
||||||
var ret []string
|
|
||||||
|
|
||||||
filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
var pa record.Path
|
|
||||||
ok := pa.Decode(recordPath, fpath)
|
|
||||||
if ok && pathConf.Regexp.FindStringSubmatch(pa.Path) != nil {
|
|
||||||
ret = append(ret, pa.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeDuplicatesAndSort(in []string) []string {
|
|
||||||
ma := make(map[string]struct{}, len(in))
|
|
||||||
for _, i := range in {
|
|
||||||
ma[i] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
out := []string{}
|
|
||||||
|
|
||||||
for k := range ma {
|
|
||||||
out = append(out, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(out)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllPathsWithRecordings(paths map[string]*conf.Path) []string {
|
|
||||||
pathNames := []string{}
|
|
||||||
|
|
||||||
for _, pathConf := range paths {
|
|
||||||
if pathConf.Regexp == nil {
|
|
||||||
if fixedPathHasRecordings(pathConf) {
|
|
||||||
pathNames = append(pathNames, pathConf.Name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pathNames = append(pathNames, regexpPathGetRecordings(pathConf)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeDuplicatesAndSort(pathNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
func recordingEntry(
|
|
||||||
pathConf *conf.Path,
|
|
||||||
pathName string,
|
|
||||||
) *defs.APIRecording {
|
|
||||||
ret := &defs.APIRecording{
|
|
||||||
Name: pathName,
|
|
||||||
}
|
|
||||||
|
|
||||||
segments, _ := playback.FindSegments(pathConf, pathName)
|
|
||||||
|
|
||||||
ret.Segments = make([]*defs.APIRecordingSegment, len(segments))
|
|
||||||
|
|
||||||
for i, seg := range segments {
|
|
||||||
ret.Segments[i] = &defs.APIRecordingSegment{
|
|
||||||
Start: seg.Start,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
@@ -46,15 +46,15 @@ func srtCheckPassphrase(passphrase string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FindPathConf returns the configuration corresponding to the given path name.
|
// FindPathConf returns the configuration corresponding to the given path name.
|
||||||
func FindPathConf(pathConfs map[string]*Path, name string) (string, *Path, []string, error) {
|
func FindPathConf(pathConfs map[string]*Path, name string) (*Path, []string, error) {
|
||||||
err := isValidPathName(name)
|
err := isValidPathName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, fmt.Errorf("invalid path name: %w (%s)", err, name)
|
return nil, nil, fmt.Errorf("invalid path name: %w (%s)", err, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// normal path
|
// normal path
|
||||||
if pathConf, ok := pathConfs[name]; ok {
|
if pathConf, ok := pathConfs[name]; ok {
|
||||||
return name, pathConf, nil, nil
|
return pathConf, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// regular expression-based path
|
// regular expression-based path
|
||||||
@@ -62,22 +62,22 @@ func FindPathConf(pathConfs map[string]*Path, name string) (string, *Path, []str
|
|||||||
if pathConf.Regexp != nil && pathConfName != "all" && pathConfName != "all_others" {
|
if pathConf.Regexp != nil && pathConfName != "all" && pathConfName != "all_others" {
|
||||||
m := pathConf.Regexp.FindStringSubmatch(name)
|
m := pathConf.Regexp.FindStringSubmatch(name)
|
||||||
if m != nil {
|
if m != nil {
|
||||||
return pathConfName, pathConf, m, nil
|
return pathConf, m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// all_others
|
// process all_others after every other entry
|
||||||
for pathConfName, pathConf := range pathConfs {
|
for pathConfName, pathConf := range pathConfs {
|
||||||
if pathConfName == "all" || pathConfName == "all_others" {
|
if pathConfName == "all" || pathConfName == "all_others" {
|
||||||
m := pathConf.Regexp.FindStringSubmatch(name)
|
m := pathConf.Regexp.FindStringSubmatch(name)
|
||||||
if m != nil {
|
if m != nil {
|
||||||
return pathConfName, pathConf, m, nil
|
return pathConf, m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil, nil, fmt.Errorf("path '%s' is not configured", name)
|
return nil, nil, fmt.Errorf("path '%s' is not configured", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path is a path configuration.
|
// Path is a path configuration.
|
||||||
|
|||||||
+9
-42
@@ -8,7 +8,6 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/metrics"
|
"github.com/bluenviron/mediamtx/internal/metrics"
|
||||||
"github.com/bluenviron/mediamtx/internal/playback"
|
"github.com/bluenviron/mediamtx/internal/playback"
|
||||||
"github.com/bluenviron/mediamtx/internal/pprof"
|
"github.com/bluenviron/mediamtx/internal/pprof"
|
||||||
"github.com/bluenviron/mediamtx/internal/record"
|
"github.com/bluenviron/mediamtx/internal/recordcleaner"
|
||||||
"github.com/bluenviron/mediamtx/internal/rlimit"
|
"github.com/bluenviron/mediamtx/internal/rlimit"
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
"github.com/bluenviron/mediamtx/internal/servers/hls"
|
||||||
"github.com/bluenviron/mediamtx/internal/servers/rtmp"
|
"github.com/bluenviron/mediamtx/internal/servers/rtmp"
|
||||||
@@ -44,38 +43,6 @@ 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 && pa.RecordDeleteAfter != 0 {
|
|
||||||
entry := record.CleanerEntry{
|
|
||||||
Path: pa.RecordPath,
|
|
||||||
Format: pa.RecordFormat,
|
|
||||||
DeleteAfter: 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].Path != out2[j].Path {
|
|
||||||
return out2[i].Path < out2[j].Path
|
|
||||||
}
|
|
||||||
return out2[i].DeleteAfter < out2[j].DeleteAfter
|
|
||||||
})
|
|
||||||
|
|
||||||
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:""`
|
||||||
@@ -92,7 +59,7 @@ type Core struct {
|
|||||||
authManager *auth.Manager
|
authManager *auth.Manager
|
||||||
metrics *metrics.Metrics
|
metrics *metrics.Metrics
|
||||||
pprof *pprof.PPROF
|
pprof *pprof.PPROF
|
||||||
recordCleaner *record.Cleaner
|
recordCleaner *recordcleaner.Cleaner
|
||||||
playbackServer *playback.Server
|
playbackServer *playback.Server
|
||||||
pathManager *pathManager
|
pathManager *pathManager
|
||||||
rtspServer *rtsp.Server
|
rtspServer *rtsp.Server
|
||||||
@@ -333,12 +300,10 @@ func (p *Core) createResources(initial bool) error {
|
|||||||
p.pprof = i
|
p.pprof = i
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanerEntries := gatherCleanerEntries(p.conf.Paths)
|
if p.recordCleaner == nil {
|
||||||
if len(cleanerEntries) != 0 &&
|
p.recordCleaner = &recordcleaner.Cleaner{
|
||||||
p.recordCleaner == nil {
|
PathConfs: p.conf.Paths,
|
||||||
p.recordCleaner = &record.Cleaner{
|
Parent: p,
|
||||||
Entries: cleanerEntries,
|
|
||||||
Parent: p,
|
|
||||||
}
|
}
|
||||||
p.recordCleaner.Initialize()
|
p.recordCleaner.Initialize()
|
||||||
}
|
}
|
||||||
@@ -707,8 +672,10 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
|||||||
closeLogger
|
closeLogger
|
||||||
|
|
||||||
closeRecorderCleaner := newConf == nil ||
|
closeRecorderCleaner := newConf == nil ||
|
||||||
!reflect.DeepEqual(gatherCleanerEntries(newConf.Paths), gatherCleanerEntries(p.conf.Paths)) ||
|
|
||||||
closeLogger
|
closeLogger
|
||||||
|
if !closeRecorderCleaner && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {
|
||||||
|
p.recordCleaner.ReloadPathConfs(newConf.Paths)
|
||||||
|
}
|
||||||
|
|
||||||
closePlaybackServer := newConf == nil ||
|
closePlaybackServer := newConf == nil ||
|
||||||
newConf.Playback != p.conf.Playback ||
|
newConf.Playback != p.conf.Playback ||
|
||||||
|
|||||||
+12
-13
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
"github.com/bluenviron/mediamtx/internal/externalcmd"
|
||||||
"github.com/bluenviron/mediamtx/internal/hooks"
|
"github.com/bluenviron/mediamtx/internal/hooks"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/record"
|
"github.com/bluenviron/mediamtx/internal/recorder"
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,7 +71,6 @@ type path struct {
|
|||||||
writeTimeout conf.StringDuration
|
writeTimeout conf.StringDuration
|
||||||
writeQueueSize int
|
writeQueueSize int
|
||||||
udpMaxPayloadSize int
|
udpMaxPayloadSize int
|
||||||
confName string
|
|
||||||
conf *conf.Path
|
conf *conf.Path
|
||||||
name string
|
name string
|
||||||
matches []string
|
matches []string
|
||||||
@@ -85,7 +84,7 @@ type path struct {
|
|||||||
source defs.Source
|
source defs.Source
|
||||||
publisherQuery string
|
publisherQuery string
|
||||||
stream *stream.Stream
|
stream *stream.Stream
|
||||||
recordAgent *record.Agent
|
recorder *recorder.Recorder
|
||||||
readyTime time.Time
|
readyTime time.Time
|
||||||
onUnDemandHook func(string)
|
onUnDemandHook func(string)
|
||||||
onNotReadyHook func()
|
onNotReadyHook func()
|
||||||
@@ -368,12 +367,12 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pa.conf.Record {
|
if pa.conf.Record {
|
||||||
if pa.stream != nil && pa.recordAgent == nil {
|
if pa.stream != nil && pa.recorder == nil {
|
||||||
pa.startRecording()
|
pa.startRecording()
|
||||||
}
|
}
|
||||||
} else if pa.recordAgent != nil {
|
} else if pa.recorder != nil {
|
||||||
pa.recordAgent.Close()
|
pa.recorder.Close()
|
||||||
pa.recordAgent = nil
|
pa.recorder = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +571,7 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
|
|||||||
req.res <- pathAPIPathsGetRes{
|
req.res <- pathAPIPathsGetRes{
|
||||||
data: &defs.APIPath{
|
data: &defs.APIPath{
|
||||||
Name: pa.name,
|
Name: pa.name,
|
||||||
ConfName: pa.confName,
|
ConfName: pa.conf.Name,
|
||||||
Source: func() *defs.APIPathSourceOrReader {
|
Source: func() *defs.APIPathSourceOrReader {
|
||||||
if pa.source == nil {
|
if pa.source == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -765,9 +764,9 @@ func (pa *path) setNotReady() {
|
|||||||
|
|
||||||
pa.onNotReadyHook()
|
pa.onNotReadyHook()
|
||||||
|
|
||||||
if pa.recordAgent != nil {
|
if pa.recorder != nil {
|
||||||
pa.recordAgent.Close()
|
pa.recorder.Close()
|
||||||
pa.recordAgent = nil
|
pa.recorder = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if pa.stream != nil {
|
if pa.stream != nil {
|
||||||
@@ -777,7 +776,7 @@ func (pa *path) setNotReady() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) startRecording() {
|
func (pa *path) startRecording() {
|
||||||
pa.recordAgent = &record.Agent{
|
pa.recorder = &recorder.Recorder{
|
||||||
WriteQueueSize: pa.writeQueueSize,
|
WriteQueueSize: pa.writeQueueSize,
|
||||||
PathFormat: pa.conf.RecordPath,
|
PathFormat: pa.conf.RecordPath,
|
||||||
Format: pa.conf.RecordFormat,
|
Format: pa.conf.RecordFormat,
|
||||||
@@ -816,7 +815,7 @@ func (pa *path) startRecording() {
|
|||||||
},
|
},
|
||||||
Parent: pa,
|
Parent: pa,
|
||||||
}
|
}
|
||||||
pa.recordAgent.Initialize()
|
pa.recorder.Initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) executeRemoveReader(r defs.Reader) {
|
func (pa *path) executeRemoveReader(r defs.Reader) {
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ func (pm *pathManager) initialize() {
|
|||||||
pm.chAPIPathsList = make(chan pathAPIPathsListReq)
|
pm.chAPIPathsList = make(chan pathAPIPathsListReq)
|
||||||
pm.chAPIPathsGet = make(chan pathAPIPathsGetReq)
|
pm.chAPIPathsGet = make(chan pathAPIPathsGetReq)
|
||||||
|
|
||||||
for pathConfName, pathConf := range pm.pathConfs {
|
for _, pathConf := range pm.pathConfs {
|
||||||
if pathConf.Regexp == nil {
|
if pathConf.Regexp == nil {
|
||||||
pm.createPath(pathConfName, pathConf, pathConfName, nil)
|
pm.createPath(pathConf, pathConf.Name, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ func (pm *pathManager) doReloadConf(newPaths map[string]*conf.Path) {
|
|||||||
// add new paths
|
// add new paths
|
||||||
for pathConfName, pathConf := range pm.pathConfs {
|
for pathConfName, pathConf := range pm.pathConfs {
|
||||||
if _, ok := pm.paths[pathConfName]; !ok && pathConf.Regexp == nil {
|
if _, ok := pm.paths[pathConfName]; !ok && pathConf.Regexp == nil {
|
||||||
pm.createPath(pathConfName, pathConf, pathConfName, nil)
|
pm.createPath(pathConf, pathConfName, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +231,7 @@ func (pm *pathManager) doPathNotReady(pa *path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {
|
func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {
|
||||||
_, pathConf, _, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
pathConf, _, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Res <- defs.PathFindPathConfRes{Err: err}
|
req.Res <- defs.PathFindPathConfRes{Err: err}
|
||||||
return
|
return
|
||||||
@@ -247,7 +247,7 @@ func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
|
func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
|
||||||
pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Res <- defs.PathDescribeRes{Err: err}
|
req.Res <- defs.PathDescribeRes{Err: err}
|
||||||
return
|
return
|
||||||
@@ -261,14 +261,14 @@ func (pm *pathManager) doDescribe(req defs.PathDescribeReq) {
|
|||||||
|
|
||||||
// create path if it doesn't exist
|
// create path if it doesn't exist
|
||||||
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
||||||
pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
|
pm.createPath(pathConf, req.AccessRequest.Name, pathMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Res <- defs.PathDescribeRes{Path: pm.paths[req.AccessRequest.Name]}
|
req.Res <- defs.PathDescribeRes{Path: pm.paths[req.AccessRequest.Name]}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
|
func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
|
||||||
pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Res <- defs.PathAddReaderRes{Err: err}
|
req.Res <- defs.PathAddReaderRes{Err: err}
|
||||||
return
|
return
|
||||||
@@ -284,14 +284,14 @@ func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {
|
|||||||
|
|
||||||
// create path if it doesn't exist
|
// create path if it doesn't exist
|
||||||
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
||||||
pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
|
pm.createPath(pathConf, req.AccessRequest.Name, pathMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Res <- defs.PathAddReaderRes{Path: pm.paths[req.AccessRequest.Name]}
|
req.Res <- defs.PathAddReaderRes{Path: pm.paths[req.AccessRequest.Name]}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
|
func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
|
||||||
pathConfName, pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
pathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Res <- defs.PathAddPublisherRes{Err: err}
|
req.Res <- defs.PathAddPublisherRes{Err: err}
|
||||||
return
|
return
|
||||||
@@ -307,7 +307,7 @@ func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {
|
|||||||
|
|
||||||
// create path if it doesn't exist
|
// create path if it doesn't exist
|
||||||
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
if _, ok := pm.paths[req.AccessRequest.Name]; !ok {
|
||||||
pm.createPath(pathConfName, pathConf, req.AccessRequest.Name, pathMatches)
|
pm.createPath(pathConf, req.AccessRequest.Name, pathMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Res <- defs.PathAddPublisherRes{Path: pm.paths[req.AccessRequest.Name]}
|
req.Res <- defs.PathAddPublisherRes{Path: pm.paths[req.AccessRequest.Name]}
|
||||||
@@ -334,7 +334,6 @@ func (pm *pathManager) doAPIPathsGet(req pathAPIPathsGetReq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) createPath(
|
func (pm *pathManager) createPath(
|
||||||
pathConfName string,
|
|
||||||
pathConf *conf.Path,
|
pathConf *conf.Path,
|
||||||
name string,
|
name string,
|
||||||
matches []string,
|
matches []string,
|
||||||
@@ -347,7 +346,6 @@ func (pm *pathManager) createPath(
|
|||||||
writeTimeout: pm.writeTimeout,
|
writeTimeout: pm.writeTimeout,
|
||||||
writeQueueSize: pm.writeQueueSize,
|
writeQueueSize: pm.writeQueueSize,
|
||||||
udpMaxPayloadSize: pm.udpMaxPayloadSize,
|
udpMaxPayloadSize: pm.udpMaxPayloadSize,
|
||||||
confName: pathConfName,
|
|
||||||
conf: pathConf,
|
conf: pathConf,
|
||||||
name: name,
|
name: name,
|
||||||
matches: matches,
|
matches: matches,
|
||||||
@@ -359,16 +357,16 @@ func (pm *pathManager) createPath(
|
|||||||
|
|
||||||
pm.paths[name] = pa
|
pm.paths[name] = pa
|
||||||
|
|
||||||
if _, ok := pm.pathsByConf[pathConfName]; !ok {
|
if _, ok := pm.pathsByConf[pathConf.Name]; !ok {
|
||||||
pm.pathsByConf[pathConfName] = make(map[*path]struct{})
|
pm.pathsByConf[pathConf.Name] = make(map[*path]struct{})
|
||||||
}
|
}
|
||||||
pm.pathsByConf[pathConfName][pa] = struct{}{}
|
pm.pathsByConf[pathConf.Name][pa] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *pathManager) removePath(pa *path) {
|
func (pm *pathManager) removePath(pa *path) {
|
||||||
delete(pm.pathsByConf[pa.confName], pa)
|
delete(pm.pathsByConf[pa.conf.Name], pa)
|
||||||
if len(pm.pathsByConf[pa.confName]) == 0 {
|
if len(pm.pathsByConf[pa.conf.Name]) == 0 {
|
||||||
delete(pm.pathsByConf, pa.confName)
|
delete(pm.pathsByConf, pa.conf.Name)
|
||||||
}
|
}
|
||||||
delete(pm.paths, pa.name)
|
delete(pm.paths, pa.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ func parseDuration(raw string) (time.Duration, error) {
|
|||||||
|
|
||||||
func seekAndMux(
|
func seekAndMux(
|
||||||
recordFormat conf.RecordFormat,
|
recordFormat conf.RecordFormat,
|
||||||
segments []*Segment,
|
segments []*recordstore.Segment,
|
||||||
start time.Time,
|
start time.Time,
|
||||||
duration time.Duration,
|
duration time.Duration,
|
||||||
m muxer,
|
m muxer,
|
||||||
@@ -152,9 +153,9 @@ func (s *Server) onGet(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
segments, err := findSegmentsInTimespan(pathConf, pathName, start, duration)
|
segments, err := recordstore.FindSegmentsInTimespan(pathConf, pathName, start, duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errNoSegmentsFound) {
|
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
|
||||||
s.writeError(ctx, http.StatusNotFound, err)
|
s.writeError(ctx, http.StatusNotFound, err)
|
||||||
} else {
|
} else {
|
||||||
s.writeError(ctx, http.StatusBadRequest, err)
|
s.writeError(ctx, http.StatusBadRequest, err)
|
||||||
@@ -172,7 +173,7 @@ func (s *Server) onGet(ctx *gin.Context) {
|
|||||||
|
|
||||||
// nothing has been written yet; send back JSON
|
// nothing has been written yet; send back JSON
|
||||||
if !ww.written {
|
if !ww.written {
|
||||||
if errors.Is(err, errNoSegmentsFound) {
|
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
|
||||||
s.writeError(ctx, http.StatusNotFound, err)
|
s.writeError(ctx, http.StatusNotFound, err)
|
||||||
} else {
|
} else {
|
||||||
s.writeError(ctx, http.StatusBadRequest, err)
|
s.writeError(ctx, http.StatusBadRequest, err)
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ func TestOnGet(t *testing.T) {
|
|||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
PathConfs: map[string]*conf.Path{
|
PathConfs: map[string]*conf.Path{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
|
Name: "mypath",
|
||||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -520,6 +521,7 @@ func TestOnGetDifferentInit(t *testing.T) {
|
|||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
PathConfs: map[string]*conf.Path{
|
PathConfs: map[string]*conf.Path{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
|
Name: "mypath",
|
||||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -595,6 +597,7 @@ func TestOnGetNTPCompensation(t *testing.T) {
|
|||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
PathConfs: map[string]*conf.Path{
|
PathConfs: map[string]*conf.Path{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
|
Name: "mypath",
|
||||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +29,10 @@ type listEntry struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeDurationAndConcatenate(recordFormat conf.RecordFormat, segments []*Segment) ([]listEntry, error) {
|
func computeDurationAndConcatenate(
|
||||||
|
recordFormat conf.RecordFormat,
|
||||||
|
segments []*recordstore.Segment,
|
||||||
|
) ([]listEntry, error) {
|
||||||
if recordFormat == conf.RecordFormatFMP4 {
|
if recordFormat == conf.RecordFormatFMP4 {
|
||||||
out := []listEntry{}
|
out := []listEntry{}
|
||||||
var prevInit *fmp4.Init
|
var prevInit *fmp4.Init
|
||||||
@@ -99,9 +103,9 @@ func (s *Server) onList(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
segments, err := FindSegments(pathConf, pathName)
|
segments, err := recordstore.FindSegments(pathConf, pathName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errNoSegmentsFound) {
|
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
|
||||||
s.writeError(ctx, http.StatusNotFound, err)
|
s.writeError(ctx, http.StatusNotFound, err)
|
||||||
} else {
|
} else {
|
||||||
s.writeError(ctx, http.StatusBadRequest, err)
|
s.writeError(ctx, http.StatusBadRequest, err)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func TestOnList(t *testing.T) {
|
|||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
PathConfs: map[string]*conf.Path{
|
PathConfs: map[string]*conf.Path{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
|
Name: "mypath",
|
||||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,6 +107,7 @@ func TestOnListDifferentInit(t *testing.T) {
|
|||||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||||
PathConfs: map[string]*conf.Path{
|
PathConfs: map[string]*conf.Path{
|
||||||
"mypath": {
|
"mypath": {
|
||||||
|
Name: "mypath",
|
||||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
package playback
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/record"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Segment is a recording segment.
|
|
||||||
type Segment struct {
|
|
||||||
Fpath string
|
|
||||||
Start time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func findSegmentsInTimespan(
|
|
||||||
pathConf *conf.Path,
|
|
||||||
pathName string,
|
|
||||||
start time.Time,
|
|
||||||
duration time.Duration,
|
|
||||||
) ([]*Segment, error) {
|
|
||||||
recordPath := record.PathAddExtension(
|
|
||||||
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
|
||||||
pathConf.RecordFormat,
|
|
||||||
)
|
|
||||||
|
|
||||||
// we have to convert to absolute paths
|
|
||||||
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
|
||||||
recordPath, _ = filepath.Abs(recordPath)
|
|
||||||
|
|
||||||
commonPath := record.CommonPath(recordPath)
|
|
||||||
end := start.Add(duration)
|
|
||||||
var segments []*Segment
|
|
||||||
|
|
||||||
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
var pa record.Path
|
|
||||||
ok := pa.Decode(recordPath, fpath)
|
|
||||||
|
|
||||||
// gather all segments that starts before the end of the playback
|
|
||||||
if ok && !end.Before(pa.Start) {
|
|
||||||
segments = append(segments, &Segment{
|
|
||||||
Fpath: fpath,
|
|
||||||
Start: pa.Start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if segments == nil {
|
|
||||||
return nil, errNoSegmentsFound
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(segments, func(i, j int) bool {
|
|
||||||
return segments[i].Start.Before(segments[j].Start)
|
|
||||||
})
|
|
||||||
|
|
||||||
// find the segment that may contain the start of the playback and remove all previous ones
|
|
||||||
found := false
|
|
||||||
for i := 0; i < len(segments)-1; i++ {
|
|
||||||
if !start.Before(segments[i].Start) && start.Before(segments[i+1].Start) {
|
|
||||||
segments = segments[i:]
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, keep the last segment only and check if it may contain the start of the playback
|
|
||||||
if !found {
|
|
||||||
segments = segments[len(segments)-1:]
|
|
||||||
if segments[len(segments)-1].Start.After(start) {
|
|
||||||
return nil, errNoSegmentsFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindSegments returns all segments of a path.
|
|
||||||
func FindSegments(
|
|
||||||
pathConf *conf.Path,
|
|
||||||
pathName string,
|
|
||||||
) ([]*Segment, error) {
|
|
||||||
recordPath := record.PathAddExtension(
|
|
||||||
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
|
||||||
pathConf.RecordFormat,
|
|
||||||
)
|
|
||||||
|
|
||||||
// we have to convert to absolute paths
|
|
||||||
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
|
||||||
recordPath, _ = filepath.Abs(recordPath)
|
|
||||||
|
|
||||||
commonPath := record.CommonPath(recordPath)
|
|
||||||
var segments []*Segment
|
|
||||||
|
|
||||||
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
var pa record.Path
|
|
||||||
ok := pa.Decode(recordPath, fpath)
|
|
||||||
if ok {
|
|
||||||
segments = append(segments, &Segment{
|
|
||||||
Fpath: fpath,
|
|
||||||
Start: pa.Start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if segments == nil {
|
|
||||||
return nil, errNoSegmentsFound
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(segments, func(i, j int) bool {
|
|
||||||
return segments[i].Start.Before(segments[j].Start)
|
|
||||||
})
|
|
||||||
|
|
||||||
return segments, nil
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/abema/go-mp4"
|
"github.com/abema/go-mp4"
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -466,7 +467,7 @@ func segmentFMP4SeekAndMuxParts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !atLeastOnePartWritten {
|
if !atLeastOnePartWritten {
|
||||||
return 0, errNoSegmentsFound
|
return 0, recordstore.ErrNoSegmentsFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return maxMuxerDTS, nil
|
return maxMuxerDTS, nil
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNoSegmentsFound = errors.New("no recording segments found")
|
|
||||||
|
|
||||||
type serverAuthManager interface {
|
type serverAuthManager interface {
|
||||||
Authenticate(req *auth.Request) error
|
Authenticate(req *auth.Request) error
|
||||||
}
|
}
|
||||||
@@ -102,7 +100,7 @@ func (s *Server) safeFindPathConf(name string) (*conf.Path, error) {
|
|||||||
s.mutex.RLock()
|
s.mutex.RLock()
|
||||||
defer s.mutex.RUnlock()
|
defer s.mutex.RUnlock()
|
||||||
|
|
||||||
_, pathConf, _, err := conf.FindPathConf(s.PathConfs, name)
|
pathConf, _, err := conf.FindPathConf(s.PathConfs, name)
|
||||||
return pathConf, err
|
return pathConf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package record
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var timeNow = time.Now
|
|
||||||
|
|
||||||
// CleanerEntry is a cleaner entry.
|
|
||||||
type CleanerEntry struct {
|
|
||||||
Path string
|
|
||||||
Format conf.RecordFormat
|
|
||||||
DeleteAfter time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleaner removes expired recording segments from disk.
|
|
||||||
type Cleaner struct {
|
|
||||||
Entries []CleanerEntry
|
|
||||||
Parent logger.Writer
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
ctxCancel func()
|
|
||||||
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes a Cleaner.
|
|
||||||
func (c *Cleaner) Initialize() {
|
|
||||||
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
|
|
||||||
c.done = make(chan struct{})
|
|
||||||
|
|
||||||
go c.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the Cleaner.
|
|
||||||
func (c *Cleaner) Close() {
|
|
||||||
c.ctxCancel()
|
|
||||||
<-c.done
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
|
||||||
func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) {
|
|
||||||
c.Parent.Log(level, "[record cleaner]"+format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cleaner) run() {
|
|
||||||
defer close(c.done)
|
|
||||||
|
|
||||||
interval := 30 * 60 * time.Second
|
|
||||||
for _, e := range c.Entries {
|
|
||||||
if interval > (e.DeleteAfter / 2) {
|
|
||||||
interval = e.DeleteAfter / 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.doRun() //nolint:errcheck
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(interval):
|
|
||||||
c.doRun()
|
|
||||||
|
|
||||||
case <-c.ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cleaner) doRun() {
|
|
||||||
for _, e := range c.Entries {
|
|
||||||
c.doRunEntry(&e) //nolint:errcheck
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cleaner) doRunEntry(e *CleanerEntry) error {
|
|
||||||
entryPath := PathAddExtension(e.Path, e.Format)
|
|
||||||
|
|
||||||
// we have to convert to absolute paths
|
|
||||||
// otherwise, entryPath and fpath inside Walk() won't have common elements
|
|
||||||
entryPath, _ = filepath.Abs(entryPath)
|
|
||||||
|
|
||||||
commonPath := CommonPath(entryPath)
|
|
||||||
now := timeNow()
|
|
||||||
|
|
||||||
filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
var pa Path
|
|
||||||
ok := pa.Decode(entryPath, fpath)
|
|
||||||
if ok {
|
|
||||||
if now.Sub(pa.Start) > e.DeleteAfter {
|
|
||||||
c.Log(logger.Debug, "removing %s", fpath)
|
|
||||||
os.Remove(fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
os.Remove(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package record
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
|
||||||
"github.com/bluenviron/mediamtx/internal/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCleaner(t *testing.T) {
|
|
||||||
timeNow = func() time.Time {
|
|
||||||
return time.Date(2009, 0o5, 20, 22, 15, 25, 427000, time.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, err := os.MkdirTemp("", "mediamtx-cleaner")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
const specialChars = "_-+*?^$()[]{}|"
|
|
||||||
|
|
||||||
err = os.Mkdir(filepath.Join(dir, specialChars+"_mypath"), 0o755)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"), []byte{1}, 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
c := &Cleaner{
|
|
||||||
Entries: []CleanerEntry{{
|
|
||||||
Path: filepath.Join(dir, specialChars+"_%path/%Y-%m-%d_%H-%M-%S-%f"),
|
|
||||||
Format: conf.RecordFormatFMP4,
|
|
||||||
DeleteAfter: 10 * time.Second,
|
|
||||||
}},
|
|
||||||
Parent: test.NilLogger,
|
|
||||||
}
|
|
||||||
c.Initialize()
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
_, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"))
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
_, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package record contains the recording system.
|
|
||||||
package record
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// Package recordcleaner contains the recording cleaner.
|
||||||
|
package recordcleaner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timeNow = time.Now
|
||||||
|
|
||||||
|
// Cleaner removes expired recording segments from disk.
|
||||||
|
type Cleaner struct {
|
||||||
|
PathConfs map[string]*conf.Path
|
||||||
|
Parent logger.Writer
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel func()
|
||||||
|
|
||||||
|
chReloadConf chan map[string]*conf.Path
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initializes a Cleaner.
|
||||||
|
func (c *Cleaner) Initialize() {
|
||||||
|
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
c.chReloadConf = make(chan map[string]*conf.Path)
|
||||||
|
c.done = make(chan struct{})
|
||||||
|
|
||||||
|
go c.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Cleaner.
|
||||||
|
func (c *Cleaner) Close() {
|
||||||
|
c.ctxCancel()
|
||||||
|
<-c.done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log implements logger.Writer.
|
||||||
|
func (c *Cleaner) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
|
c.Parent.Log(level, "[record cleaner]"+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadPathConfs is called by core.Core.
|
||||||
|
func (c *Cleaner) ReloadPathConfs(pathConfs map[string]*conf.Path) {
|
||||||
|
select {
|
||||||
|
case c.chReloadConf <- pathConfs:
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cleaner) run() {
|
||||||
|
defer close(c.done)
|
||||||
|
|
||||||
|
c.doRun() //nolint:errcheck
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(c.cleanInterval()):
|
||||||
|
c.doRun()
|
||||||
|
|
||||||
|
case cnf := <-c.chReloadConf:
|
||||||
|
c.PathConfs = cnf
|
||||||
|
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cleaner) atLeastOneRecordDeleteAfter() bool {
|
||||||
|
for _, e := range c.PathConfs {
|
||||||
|
if e.RecordDeleteAfter != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cleaner) cleanInterval() time.Duration {
|
||||||
|
if !c.atLeastOneRecordDeleteAfter() {
|
||||||
|
return 365 * 24 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := 30 * 60 * time.Second
|
||||||
|
|
||||||
|
for _, e := range c.PathConfs {
|
||||||
|
if e.RecordDeleteAfter != 0 &&
|
||||||
|
interval > (time.Duration(e.RecordDeleteAfter)/2) {
|
||||||
|
interval = time.Duration(e.RecordDeleteAfter) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cleaner) doRun() {
|
||||||
|
now := timeNow()
|
||||||
|
|
||||||
|
pathNames := recordstore.FindAllPathsWithSegments(c.PathConfs)
|
||||||
|
|
||||||
|
for _, pathName := range pathNames {
|
||||||
|
c.processPath(now, pathName) //nolint:errcheck
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cleaner) processPath(now time.Time, pathName string) error {
|
||||||
|
pathConf, _, err := conf.FindPathConf(c.PathConfs, pathName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathConf.RecordDeleteAfter == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
segments, err := recordstore.FindSegments(pathConf, pathName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seg := range segments {
|
||||||
|
if now.Sub(seg.Start) > time.Duration(pathConf.RecordDeleteAfter) {
|
||||||
|
c.Log(logger.Debug, "removing %s", seg.Fpath)
|
||||||
|
os.Remove(seg.Fpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package recordcleaner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/test"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCleaner(t *testing.T) {
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2009, 5, 20, 22, 15, 25, 427000, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, err := os.MkdirTemp("", "mediamtx-cleaner")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
const specialChars = "_-+*?^$()[]{}|"
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, specialChars+"_mypath"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Cleaner{
|
||||||
|
PathConfs: map[string]*conf.Path{
|
||||||
|
"~^.*$": {
|
||||||
|
Name: "~^.*$",
|
||||||
|
Regexp: regexp.MustCompile("^.*$"),
|
||||||
|
RecordPath: filepath.Join(dir, specialChars+"_%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
RecordDeleteAfter: conf.StringDuration(10 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parent: test.NilLogger,
|
||||||
|
}
|
||||||
|
c.Initialize()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2008-05-20_22-15-25-000125.mp4"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, specialChars+"_mypath", "2009-05-20_22-15-25-000427.mp4"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanerMultipleEntriesSamePath(t *testing.T) {
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return time.Date(2009, 5, 20, 22, 15, 25, 427000, time.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, err := os.MkdirTemp("", "mediamtx-cleaner")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path1"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path2"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2009-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path2", "2009-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
c := &Cleaner{
|
||||||
|
PathConfs: map[string]*conf.Path{
|
||||||
|
"path1": {
|
||||||
|
Name: "path1",
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
RecordDeleteAfter: conf.StringDuration(10 * time.Second),
|
||||||
|
},
|
||||||
|
"path2": {
|
||||||
|
Name: "path2",
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
RecordDeleteAfter: conf.StringDuration(10 * 24 * time.Hour),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parent: test.NilLogger,
|
||||||
|
}
|
||||||
|
c.Initialize()
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "path1", "2009-05-19_22-15-25-000427.mp4"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Join(dir, "path2", "2009-05-19_22-15-25-000427.mp4"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
type format interface {
|
type format interface {
|
||||||
initialize()
|
initialize()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writePart(
|
func writePart(
|
||||||
@@ -54,7 +55,7 @@ func (p *formatFMP4Part) initialize() {
|
|||||||
|
|
||||||
func (p *formatFMP4Part) close() error {
|
func (p *formatFMP4Part) close() error {
|
||||||
if p.s.fi == nil {
|
if p.s.fi == nil {
|
||||||
p.s.path = Path{Start: p.s.startNTP}.Encode(p.s.f.ai.pathFormat)
|
p.s.path = recordstore.Path{Start: p.s.startNTP}.Encode(p.s.f.ai.pathFormat)
|
||||||
p.s.f.ai.Log(logger.Debug, "creating segment %s", p.s.path)
|
p.s.f.ai.Log(logger.Debug, "creating segment %s", p.s.path)
|
||||||
|
|
||||||
err := os.MkdirAll(filepath.Dir(p.s.path), 0o755)
|
err := os.MkdirAll(filepath.Dir(p.s.path), 0o755)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type formatMPEGTSSegment struct {
|
type formatMPEGTSSegment struct {
|
||||||
@@ -46,7 +47,7 @@ func (s *formatMPEGTSSegment) close() error {
|
|||||||
|
|
||||||
func (s *formatMPEGTSSegment) Write(p []byte) (int, error) {
|
func (s *formatMPEGTSSegment) Write(p []byte) (int, error) {
|
||||||
if s.fi == nil {
|
if s.fi == nil {
|
||||||
s.path = Path{Start: s.startNTP}.Encode(s.f.ai.pathFormat)
|
s.path = recordstore.Path{Start: s.startNTP}.Encode(s.f.ai.pathFormat)
|
||||||
s.f.ai.Log(logger.Debug, "creating segment %s", s.path)
|
s.f.ai.Log(logger.Debug, "creating segment %s", s.path)
|
||||||
|
|
||||||
err := os.MkdirAll(filepath.Dir(s.path), 0o755)
|
err := os.MkdirAll(filepath.Dir(s.path), 0o755)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
"github.com/bluenviron/mediamtx/internal/asyncwriter"
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sample struct {
|
type sample struct {
|
||||||
@@ -18,7 +19,7 @@ type sample struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type agentInstance struct {
|
type agentInstance struct {
|
||||||
agent *Agent
|
agent *Recorder
|
||||||
|
|
||||||
pathFormat string
|
pathFormat string
|
||||||
writer *asyncwriter.Writer
|
writer *asyncwriter.Writer
|
||||||
@@ -36,7 +37,7 @@ func (ai *agentInstance) Log(level logger.Level, format string, args ...interfac
|
|||||||
func (ai *agentInstance) initialize() {
|
func (ai *agentInstance) initialize() {
|
||||||
ai.pathFormat = ai.agent.PathFormat
|
ai.pathFormat = ai.agent.PathFormat
|
||||||
|
|
||||||
ai.pathFormat = PathAddExtension(
|
ai.pathFormat = recordstore.PathAddExtension(
|
||||||
strings.ReplaceAll(ai.pathFormat, "%path", ai.agent.PathName),
|
strings.ReplaceAll(ai.pathFormat, "%path", ai.agent.PathName),
|
||||||
ai.agent.Format,
|
ai.agent.Format,
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
package record
|
// Package recorder contains the recorder.
|
||||||
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -14,8 +15,8 @@ type OnSegmentCreateFunc = func(path string)
|
|||||||
// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete
|
// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete
|
||||||
type OnSegmentCompleteFunc = func(path string, duration time.Duration)
|
type OnSegmentCompleteFunc = func(path string, duration time.Duration)
|
||||||
|
|
||||||
// Agent writes recordings to disk.
|
// Recorder writes recordings to disk.
|
||||||
type Agent struct {
|
type Recorder struct {
|
||||||
WriteQueueSize int
|
WriteQueueSize int
|
||||||
PathFormat string
|
PathFormat string
|
||||||
Format conf.RecordFormat
|
Format conf.RecordFormat
|
||||||
@@ -35,8 +36,8 @@ type Agent struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize initializes Agent.
|
// Initialize initializes Recorder.
|
||||||
func (w *Agent) Initialize() {
|
func (w *Recorder) Initialize() {
|
||||||
if w.OnSegmentCreate == nil {
|
if w.OnSegmentCreate == nil {
|
||||||
w.OnSegmentCreate = func(string) {
|
w.OnSegmentCreate = func(string) {
|
||||||
}
|
}
|
||||||
@@ -61,18 +62,18 @@ func (w *Agent) Initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
func (w *Agent) Log(level logger.Level, format string, args ...interface{}) {
|
func (w *Recorder) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
w.Parent.Log(level, "[record] "+format, args...)
|
w.Parent.Log(level, "[recorder] "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the agent.
|
// Close closes the agent.
|
||||||
func (w *Agent) Close() {
|
func (w *Recorder) Close() {
|
||||||
w.Log(logger.Info, "recording stopped")
|
w.Log(logger.Info, "recording stopped")
|
||||||
close(w.terminate)
|
close(w.terminate)
|
||||||
<-w.done
|
<-w.done
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Agent) run() {
|
func (w *Recorder) run() {
|
||||||
defer close(w.done)
|
defer close(w.done)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recorder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"github.com/bluenviron/mediamtx/internal/unit"
|
"github.com/bluenviron/mediamtx/internal/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAgent(t *testing.T) {
|
func TestRecorder(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{
|
desc := &description.Session{Medias: []*description.Media{
|
||||||
{
|
{
|
||||||
Type: description.MediaTypeVideo,
|
Type: description.MediaTypeVideo,
|
||||||
@@ -156,7 +156,7 @@ func TestAgent(t *testing.T) {
|
|||||||
|
|
||||||
n := 0
|
n := 0
|
||||||
|
|
||||||
w := &Agent{
|
w := &Recorder{
|
||||||
WriteQueueSize: 1024,
|
WriteQueueSize: 1024,
|
||||||
PathFormat: recordPath,
|
PathFormat: recordPath,
|
||||||
Format: f,
|
Format: f,
|
||||||
@@ -197,11 +197,11 @@ func TestAgent(t *testing.T) {
|
|||||||
|
|
||||||
writeToStream(stream,
|
writeToStream(stream,
|
||||||
50*time.Second,
|
50*time.Second,
|
||||||
time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC))
|
||||||
|
|
||||||
writeToStream(stream,
|
writeToStream(stream,
|
||||||
52*time.Second,
|
52*time.Second,
|
||||||
time.Date(2008, 0o5, 20, 22, 16, 25, 0, time.UTC))
|
time.Date(2008, 5, 20, 22, 16, 25, 0, time.UTC))
|
||||||
|
|
||||||
// simulate a write error
|
// simulate a write error
|
||||||
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
||||||
@@ -295,7 +295,7 @@ func TestAgent(t *testing.T) {
|
|||||||
|
|
||||||
writeToStream(stream,
|
writeToStream(stream,
|
||||||
300*time.Second,
|
300*time.Second,
|
||||||
time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC))
|
time.Date(2010, 5, 20, 22, 15, 25, 0, time.UTC))
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ func TestAgent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAgentFMP4NegativeDTS(t *testing.T) {
|
func TestRecorderFMP4NegativeDTS(t *testing.T) {
|
||||||
desc := &description.Session{Medias: []*description.Media{
|
desc := &description.Session{Medias: []*description.Media{
|
||||||
{
|
{
|
||||||
Type: description.MediaTypeVideo,
|
Type: description.MediaTypeVideo,
|
||||||
@@ -350,7 +350,7 @@ func TestAgentFMP4NegativeDTS(t *testing.T) {
|
|||||||
|
|
||||||
recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")
|
recordPath := filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f")
|
||||||
|
|
||||||
w := &Agent{
|
w := &Recorder{
|
||||||
WriteQueueSize: 1024,
|
WriteQueueSize: 1024,
|
||||||
PathFormat: recordPath,
|
PathFormat: recordPath,
|
||||||
Format: conf.RecordFormatFMP4,
|
Format: conf.RecordFormatFMP4,
|
||||||
@@ -366,7 +366,7 @@ func TestAgentFMP4NegativeDTS(t *testing.T) {
|
|||||||
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{
|
||||||
Base: unit.Base{
|
Base: unit.Base{
|
||||||
PTS: -50*time.Millisecond + (time.Duration(i) * 200 * time.Millisecond),
|
PTS: -50*time.Millisecond + (time.Duration(i) * 200 * time.Millisecond),
|
||||||
NTP: time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC),
|
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
|
||||||
},
|
},
|
||||||
AU: [][]byte{
|
AU: [][]byte{
|
||||||
test.FormatH264.SPS,
|
test.FormatH264.SPS,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recordstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -23,7 +23,7 @@ func leadingZeros(v int, size int) string {
|
|||||||
return out2 + out
|
return out2 + out
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathAddExtension adds the file extension to path.
|
// PathAddExtension adds the file extension to the path.
|
||||||
func PathAddExtension(path string, format conf.RecordFormat) string {
|
func PathAddExtension(path string, format conf.RecordFormat) string {
|
||||||
switch format {
|
switch format {
|
||||||
case conf.RecordFormatMPEGTS:
|
case conf.RecordFormatMPEGTS:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package record
|
package recordstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Package recordstore contains utilities to store/retrieve recordings to/from disk.
|
||||||
|
package recordstore
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package recordstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoSegmentsFound is returned when no recording segments have been found.
|
||||||
|
var ErrNoSegmentsFound = errors.New("no recording segments found")
|
||||||
|
|
||||||
|
var errFound = errors.New("found")
|
||||||
|
|
||||||
|
// Segment is a recording segment.
|
||||||
|
type Segment struct {
|
||||||
|
Fpath string
|
||||||
|
Start time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixedPathHasSegments(pathConf *conf.Path) bool {
|
||||||
|
recordPath := PathAddExtension(
|
||||||
|
strings.ReplaceAll(pathConf.RecordPath, "%path", pathConf.Name),
|
||||||
|
pathConf.RecordFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
// we have to convert to absolute paths
|
||||||
|
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
||||||
|
recordPath, _ = filepath.Abs(recordPath)
|
||||||
|
|
||||||
|
commonPath := CommonPath(recordPath)
|
||||||
|
|
||||||
|
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
var pa Path
|
||||||
|
ok := pa.Decode(recordPath, fpath)
|
||||||
|
if ok {
|
||||||
|
return errFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, errFound) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Is(err, errFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func regexpPathFindPathsWithSegments(pathConf *conf.Path) map[string]struct{} {
|
||||||
|
recordPath := PathAddExtension(
|
||||||
|
pathConf.RecordPath,
|
||||||
|
pathConf.RecordFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
// we have to convert to absolute paths
|
||||||
|
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
||||||
|
recordPath, _ = filepath.Abs(recordPath)
|
||||||
|
|
||||||
|
commonPath := CommonPath(recordPath)
|
||||||
|
|
||||||
|
ret := make(map[string]struct{})
|
||||||
|
|
||||||
|
filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error { //nolint:errcheck
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
var pa Path
|
||||||
|
ok := pa.Decode(recordPath, fpath)
|
||||||
|
if ok && pathConf.Regexp.FindStringSubmatch(pa.Path) != nil {
|
||||||
|
ret[pa.Path] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAllPathsWithSegments returns all paths that do have segments.
|
||||||
|
func FindAllPathsWithSegments(pathConfs map[string]*conf.Path) []string {
|
||||||
|
pathNames := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, pathConf := range pathConfs {
|
||||||
|
if pathConf.Regexp == nil {
|
||||||
|
if fixedPathHasSegments(pathConf) {
|
||||||
|
pathNames[pathConf.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for name := range regexpPathFindPathsWithSegments(pathConf) {
|
||||||
|
pathNames[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, len(pathNames))
|
||||||
|
n := 0
|
||||||
|
for k := range pathNames {
|
||||||
|
out[n] = k
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSegments returns all segments of a path.
|
||||||
|
func FindSegments(
|
||||||
|
pathConf *conf.Path,
|
||||||
|
pathName string,
|
||||||
|
) ([]*Segment, error) {
|
||||||
|
recordPath := PathAddExtension(
|
||||||
|
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
||||||
|
pathConf.RecordFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
// we have to convert to absolute paths
|
||||||
|
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
||||||
|
recordPath, _ = filepath.Abs(recordPath)
|
||||||
|
|
||||||
|
commonPath := CommonPath(recordPath)
|
||||||
|
var segments []*Segment
|
||||||
|
|
||||||
|
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
var pa Path
|
||||||
|
ok := pa.Decode(recordPath, fpath)
|
||||||
|
if ok {
|
||||||
|
segments = append(segments, &Segment{
|
||||||
|
Fpath: fpath,
|
||||||
|
Start: pa.Start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if segments == nil {
|
||||||
|
return nil, ErrNoSegmentsFound
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(segments, func(i, j int) bool {
|
||||||
|
return segments[i].Start.Before(segments[j].Start)
|
||||||
|
})
|
||||||
|
|
||||||
|
return segments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSegmentsInTimespan returns all segments in a certain timestamp.
|
||||||
|
func FindSegmentsInTimespan(
|
||||||
|
pathConf *conf.Path,
|
||||||
|
pathName string,
|
||||||
|
start time.Time,
|
||||||
|
duration time.Duration,
|
||||||
|
) ([]*Segment, error) {
|
||||||
|
recordPath := PathAddExtension(
|
||||||
|
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
||||||
|
pathConf.RecordFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
// we have to convert to absolute paths
|
||||||
|
// otherwise, recordPath and fpath inside Walk() won't have common elements
|
||||||
|
recordPath, _ = filepath.Abs(recordPath)
|
||||||
|
|
||||||
|
commonPath := CommonPath(recordPath)
|
||||||
|
end := start.Add(duration)
|
||||||
|
var segments []*Segment
|
||||||
|
|
||||||
|
err := filepath.Walk(commonPath, func(fpath string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
var pa Path
|
||||||
|
ok := pa.Decode(recordPath, fpath)
|
||||||
|
|
||||||
|
// gather all segments that starts before the end of the playback
|
||||||
|
if ok && !end.Before(pa.Start) {
|
||||||
|
segments = append(segments, &Segment{
|
||||||
|
Fpath: fpath,
|
||||||
|
Start: pa.Start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if segments == nil {
|
||||||
|
return nil, ErrNoSegmentsFound
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(segments, func(i, j int) bool {
|
||||||
|
return segments[i].Start.Before(segments[j].Start)
|
||||||
|
})
|
||||||
|
|
||||||
|
// find the segment that may contain the start of the playback and remove all previous ones
|
||||||
|
found := false
|
||||||
|
for i := 0; i < len(segments)-1; i++ {
|
||||||
|
if !start.Before(segments[i].Start) && start.Before(segments[i+1].Start) {
|
||||||
|
segments = segments[i:]
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, keep the last segment only and check if it may contain the start of the playback
|
||||||
|
if !found {
|
||||||
|
segments = segments[len(segments)-1:]
|
||||||
|
if segments[len(segments)-1].Start.After(start) {
|
||||||
|
return nil, ErrNoSegmentsFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package recordstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindAllPathsWithSegments(t *testing.T) {
|
||||||
|
dir, err := os.MkdirTemp("", "mediamtx-recordstore")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path1"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path2"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2015-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path2", "2015-07-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
paths := FindAllPathsWithSegments(map[string]*conf.Path{
|
||||||
|
"~^.*$": {
|
||||||
|
Name: "~^.*$",
|
||||||
|
Regexp: regexp.MustCompile("^.*$"),
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
},
|
||||||
|
"path2": {
|
||||||
|
Name: "path2",
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Equal(t, []string{"path1", "path2"}, paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSegments(t *testing.T) {
|
||||||
|
dir, err := os.MkdirTemp("", "mediamtx-recordstore")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path1"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path2"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2015-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2016-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
segments, err := FindSegments(
|
||||||
|
&conf.Path{
|
||||||
|
Name: "~^.*$",
|
||||||
|
Regexp: regexp.MustCompile("^.*$"),
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
},
|
||||||
|
"path1",
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, []*Segment{
|
||||||
|
{
|
||||||
|
Fpath: filepath.Join(dir, "path1", "2015-05-19_22-15-25-000427.mp4"),
|
||||||
|
Start: time.Date(2015, 5, 19, 22, 15, 25, 427000, time.Local),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Fpath: filepath.Join(dir, "path1", "2016-05-19_22-15-25-000427.mp4"),
|
||||||
|
Start: time.Date(2016, 5, 19, 22, 15, 25, 427000, time.Local),
|
||||||
|
},
|
||||||
|
}, segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSegmentsInTimespan(t *testing.T) {
|
||||||
|
dir, err := os.MkdirTemp("", "mediamtx-recordstore")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path1"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.Mkdir(filepath.Join(dir, "path2"), 0o755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2015-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dir, "path1", "2016-05-19_22-15-25-000427.mp4"), []byte{1}, 0o644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
segments, err := FindSegmentsInTimespan(
|
||||||
|
&conf.Path{
|
||||||
|
Name: "~^.*$",
|
||||||
|
Regexp: regexp.MustCompile("^.*$"),
|
||||||
|
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||||
|
RecordFormat: conf.RecordFormatFMP4,
|
||||||
|
},
|
||||||
|
"path1",
|
||||||
|
time.Date(2015, 5, 19, 22, 18, 25, 427000, time.Local),
|
||||||
|
60*time.Minute,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, []*Segment{
|
||||||
|
{
|
||||||
|
Fpath: filepath.Join(dir, "path1", "2015-05-19_22-15-25-000427.mp4"),
|
||||||
|
Start: time.Date(2015, 5, 19, 22, 15, 25, 427000, time.Local),
|
||||||
|
},
|
||||||
|
}, segments)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user