mirror of
https://github.com/bluenviron/mediamtx
synced 2025-02-20 05:26:53 +00:00
This commit is contained in:
parent
f21604399c
commit
d38b7e95fc
14
README.md
14
README.md
@ -1578,10 +1578,16 @@ playbackAddress: :9996
|
||||
The server provides an endpoint to list recorded timespans:
|
||||
|
||||
```
|
||||
http://localhost:9996/list?path=[mypath]
|
||||
http://localhost:9996/list?path=[mypath]&start=[start]&end=[end]
|
||||
```
|
||||
|
||||
Where [mypath] is the name of a path. The server will return a list of timespans in JSON format:
|
||||
Where:
|
||||
|
||||
* [mypath] is the name of a path
|
||||
* [start] (optional) is the start date in [RFC3339 format](https://www.utctime.net/)
|
||||
* [end] (optional) is the end date in [RFC3339 format](https://www.utctime.net/)
|
||||
|
||||
The server will return a list of timespans in JSON format:
|
||||
|
||||
```json
|
||||
[
|
||||
@ -1601,13 +1607,13 @@ Where [mypath] is the name of a path. The server will return a list of timespans
|
||||
The server provides an endpoint to download recordings:
|
||||
|
||||
```
|
||||
http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=[format]
|
||||
http://localhost:9996/get?path=[mypath]&start=[start]&duration=[duration]&format=[format]
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* [mypath] is the path name
|
||||
* [start_date] is the start date in [RFC3339 format](https://www.utctime.net/)
|
||||
* [start] is the start date in [RFC3339 format](https://www.utctime.net/)
|
||||
* [duration] is the maximum duration of the recording in seconds
|
||||
* [format] (optional) is the output format of the stream. Available values are "fmp4" (default) and "mp4"
|
||||
|
||||
|
@ -64,7 +64,7 @@ func recordingsOfPath(
|
||||
Name: pathName,
|
||||
}
|
||||
|
||||
segments, _ := recordstore.FindSegments(pathConf, pathName)
|
||||
segments, _ := recordstore.FindSegments(pathConf, pathName, nil, nil)
|
||||
|
||||
ret.Segments = make([]*defs.APIRecordingSegment, len(segments))
|
||||
|
||||
|
@ -153,7 +153,8 @@ func (s *Server) onGet(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
segments, err := recordstore.FindSegmentsInTimespan(pathConf, pathName, start, duration)
|
||||
end := start.Add(duration)
|
||||
segments, err := recordstore.FindSegments(pathConf, pathName, &start, &end)
|
||||
if err != nil {
|
||||
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
|
||||
s.writeError(ctx, http.StatusNotFound, err)
|
||||
|
@ -55,7 +55,7 @@ func computeDurationAndConcatenate(
|
||||
return err
|
||||
}
|
||||
|
||||
maxDuration, err := segmentFMP4ReadMaxDuration(f, init)
|
||||
maxDuration, err := segmentFMP4ReadDuration(f, init)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -103,7 +103,31 @@ func (s *Server) onList(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
segments, err := recordstore.FindSegments(pathConf, pathName)
|
||||
var start *time.Time
|
||||
rawStart := ctx.Query("start")
|
||||
if rawStart != "" {
|
||||
var tmp time.Time
|
||||
tmp, err = time.Parse(time.RFC3339, rawStart)
|
||||
if err != nil {
|
||||
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
|
||||
return
|
||||
}
|
||||
start = &tmp
|
||||
}
|
||||
|
||||
var end *time.Time
|
||||
rawEnd := ctx.Query("end")
|
||||
if rawEnd != "" {
|
||||
var tmp time.Time
|
||||
tmp, err = time.Parse(time.RFC3339, rawEnd)
|
||||
if err != nil {
|
||||
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid end: %w", err))
|
||||
return
|
||||
}
|
||||
end = &tmp
|
||||
}
|
||||
|
||||
segments, err := recordstore.FindSegments(pathConf, pathName, start, end)
|
||||
if err != nil {
|
||||
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
|
||||
s.writeError(ctx, http.StatusNotFound, err)
|
||||
@ -119,6 +143,21 @@ func (s *Server) onList(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if start != nil {
|
||||
firstEntry := entries[0]
|
||||
if firstEntry.Start.Before(*start) {
|
||||
entries[0].Duration -= listEntryDuration(start.Sub(firstEntry.Start))
|
||||
entries[0].Start = *start
|
||||
}
|
||||
}
|
||||
|
||||
if end != nil {
|
||||
lastEntry := entries[len(entries)-1]
|
||||
if lastEntry.Start.Add(time.Duration(lastEntry.Duration)).After(*end) {
|
||||
entries[len(entries)-1].Duration = listEntryDuration(end.Sub(lastEntry.Start))
|
||||
}
|
||||
}
|
||||
|
||||
var scheme string
|
||||
if s.Encryption {
|
||||
scheme = "https"
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOnList(t *testing.T) {
|
||||
func TestOnListUnfiltered(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
@ -78,6 +78,72 @@ func TestOnList(t *testing.T) {
|
||||
}, out)
|
||||
}
|
||||
|
||||
func TestOnListFiltered(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
err = os.Mkdir(filepath.Join(dir, "mypath"), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
writeSegment1(t, filepath.Join(dir, "mypath", "2008-11-07_11-22-00-500000.mp4"))
|
||||
writeSegment2(t, filepath.Join(dir, "mypath", "2008-11-07_11-23-02-500000.mp4"))
|
||||
writeSegment2(t, filepath.Join(dir, "mypath", "2009-11-07_11-23-02-500000.mp4"))
|
||||
|
||||
s := &Server{
|
||||
Address: "127.0.0.1:9996",
|
||||
ReadTimeout: conf.StringDuration(10 * time.Second),
|
||||
PathConfs: map[string]*conf.Path{
|
||||
"mypath": {
|
||||
Name: "mypath",
|
||||
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
|
||||
},
|
||||
},
|
||||
AuthManager: test.NilAuthManager,
|
||||
Parent: test.NilLogger,
|
||||
}
|
||||
err = s.Initialize()
|
||||
require.NoError(t, err)
|
||||
defer s.Close()
|
||||
|
||||
u, err := url.Parse("http://myuser:mypass@localhost:9996/list?start=")
|
||||
require.NoError(t, err)
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("path", "mypath")
|
||||
v.Set("start", time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano))
|
||||
v.Set("end", time.Date(2009, 11, 0o7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
var out interface{}
|
||||
err = json.NewDecoder(res.Body).Decode(&out)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, []interface{}{
|
||||
map[string]interface{}{
|
||||
"duration": float64(64),
|
||||
"start": time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=64&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
map[string]interface{}{
|
||||
"duration": float64(2),
|
||||
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=2&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
}, out)
|
||||
}
|
||||
|
||||
func TestOnListDifferentInit(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "mediamtx-playback")
|
||||
require.NoError(t, err)
|
||||
|
@ -114,7 +114,7 @@ func segmentFMP4ReadInit(r io.ReadSeeker) (*fmp4.Init, error) {
|
||||
return &init, nil
|
||||
}
|
||||
|
||||
func segmentFMP4ReadMaxDuration(
|
||||
func segmentFMP4ReadDuration(
|
||||
r io.ReadSeeker,
|
||||
init *fmp4.Init,
|
||||
) (time.Duration, error) {
|
||||
|
@ -118,16 +118,15 @@ func (c *Cleaner) processPath(now time.Time, pathName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
segments, err := recordstore.FindSegments(pathConf, pathName)
|
||||
end := now.Add(-time.Duration(pathConf.RecordDeleteAfter))
|
||||
segments, err := recordstore.FindSegments(pathConf, pathName, nil, &end)
|
||||
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)
|
||||
}
|
||||
c.Log(logger.Debug, "removing %s", seg.Fpath)
|
||||
os.Remove(seg.Fpath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -89,7 +89,7 @@ func regexpPathFindPathsWithSegments(pathConf *conf.Path) map[string]struct{} {
|
||||
return ret
|
||||
}
|
||||
|
||||
// FindAllPathsWithSegments returns all paths that do have segments.
|
||||
// FindAllPathsWithSegments returns all paths that have at least one segment.
|
||||
func FindAllPathsWithSegments(pathConfs map[string]*conf.Path) []string {
|
||||
pathNames := make(map[string]struct{})
|
||||
|
||||
@ -117,9 +117,12 @@ func FindAllPathsWithSegments(pathConfs map[string]*conf.Path) []string {
|
||||
}
|
||||
|
||||
// FindSegments returns all segments of a path.
|
||||
// Segments can be filtered by start date and end date.
|
||||
func FindSegments(
|
||||
pathConf *conf.Path,
|
||||
pathName string,
|
||||
start *time.Time,
|
||||
end *time.Time,
|
||||
) ([]*Segment, error) {
|
||||
recordPath := PathAddExtension(
|
||||
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
|
||||
@ -133,59 +136,6 @@ func FindSegments(
|
||||
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
|
||||
@ -196,7 +146,7 @@ func FindSegmentsInTimespan(
|
||||
ok := pa.Decode(recordPath, fpath)
|
||||
|
||||
// gather all segments that starts before the end of the playback
|
||||
if ok && !end.Before(pa.Start) {
|
||||
if ok && (end == nil || !end.Before(pa.Start)) {
|
||||
segments = append(segments, &Segment{
|
||||
Fpath: fpath,
|
||||
Start: pa.Start,
|
||||
@ -218,21 +168,23 @@ func FindSegmentsInTimespan(
|
||||
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
|
||||
if start != nil {
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,79 +45,86 @@ func TestFindAllPathsWithSegments(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindSegments(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "mediamtx-recordstore")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
t.Run("no filtering", func(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, "path1"), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.Mkdir(filepath.Join(dir, "path2"), 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", "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)
|
||||
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)
|
||||
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",
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
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)
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("filtering", func(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)
|
||||
|
||||
start := time.Date(2015, 5, 19, 22, 18, 25, 427000, time.Local)
|
||||
end := start.Add(60 * time.Minute)
|
||||
|
||||
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",
|
||||
&start,
|
||||
&end,
|
||||
)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user