playback: add "url" field to recording timespans in /list (#3619)
This commit is contained in:
@@ -1186,7 +1186,7 @@ The JWT is expected to contain the `mediamtx_permissions` scope, with a list of
|
||||
}
|
||||
```
|
||||
|
||||
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of any other protocol), for instance (RTSP):
|
||||
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of all other protocols), for instance:
|
||||
|
||||
```
|
||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT
|
||||
@@ -1350,16 +1350,18 @@ Where [mypath] is the name of a path. The server will return a list of timespans
|
||||
[
|
||||
{
|
||||
"start": "2006-01-02T15:04:05Z07:00",
|
||||
"duration": "60.0"
|
||||
"duration": "60.0",
|
||||
"url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A04%3A05Z07%3A00&duration=60.0"
|
||||
},
|
||||
{
|
||||
"start": "2006-01-02T15:07:05Z07:00",
|
||||
"duration": "32.33"
|
||||
"duration": "32.33",
|
||||
"url": "http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A07%3A05Z07%3A00&duration=32.33"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The server provides an endpoint for downloading recordings:
|
||||
The server provides an endpoint to download recordings:
|
||||
|
||||
```
|
||||
http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=[format]
|
||||
@@ -1375,7 +1377,7 @@ Where:
|
||||
All parameters must be [url-encoded](https://www.urlencoder.org/). For instance:
|
||||
|
||||
```
|
||||
http://localhost:9996/get?path=stream2&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5
|
||||
http://localhost:9996/get?path=mypath&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5
|
||||
```
|
||||
|
||||
The resulting stream uses the fMP4 format, that is natively compatible with any browser, therefore its URL can be directly inserted into a \<video> tag:
|
||||
|
||||
+12
-12
@@ -111,22 +111,22 @@ func seekAndMux(
|
||||
return fmt.Errorf("MPEG-TS format is not supported yet")
|
||||
}
|
||||
|
||||
func (p *Server) onGet(ctx *gin.Context) {
|
||||
func (s *Server) onGet(ctx *gin.Context) {
|
||||
pathName := ctx.Query("path")
|
||||
|
||||
if !p.doAuth(ctx, pathName) {
|
||||
if !s.doAuth(ctx, pathName) {
|
||||
return
|
||||
}
|
||||
|
||||
start, err := time.Parse(time.RFC3339, ctx.Query("start"))
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
|
||||
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := parseDuration(ctx.Query("duration"))
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid duration: %w", err))
|
||||
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid duration: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,22 +142,22 @@ func (p *Server) onGet(ctx *gin.Context) {
|
||||
m = &muxerMP4{w: ww}
|
||||
|
||||
default:
|
||||
p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid format: %s", format))
|
||||
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid format: %s", format))
|
||||
return
|
||||
}
|
||||
|
||||
pathConf, err := p.safeFindPathConf(pathName)
|
||||
pathConf, err := s.safeFindPathConf(pathName)
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
s.writeError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
segments, err := findSegmentsInTimespan(pathConf, pathName, start, duration)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoSegmentsFound) {
|
||||
p.writeError(ctx, http.StatusNotFound, err)
|
||||
s.writeError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
s.writeError(ctx, http.StatusBadRequest, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -173,15 +173,15 @@ func (p *Server) onGet(ctx *gin.Context) {
|
||||
// nothing has been written yet; send back JSON
|
||||
if !ww.written {
|
||||
if errors.Is(err, errNoSegmentsFound) {
|
||||
p.writeError(ctx, http.StatusNotFound, err)
|
||||
s.writeError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
s.writeError(ctx, http.StatusBadRequest, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// something has already been written: abort and write logs only
|
||||
p.Log(logger.Error, err.Error())
|
||||
s.Log(logger.Error, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
|
||||
@@ -23,6 +25,7 @@ func (d listEntryDuration) MarshalJSON() ([]byte, error) {
|
||||
type listEntry struct {
|
||||
Start time.Time `json:"start"`
|
||||
Duration listEntryDuration `json:"duration"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func computeDurationAndConcatenate(recordFormat conf.RecordFormat, segments []*Segment) ([]listEntry, error) {
|
||||
@@ -83,34 +86,55 @@ func computeDurationAndConcatenate(recordFormat conf.RecordFormat, segments []*S
|
||||
return nil, fmt.Errorf("MPEG-TS format is not supported yet")
|
||||
}
|
||||
|
||||
func (p *Server) onList(ctx *gin.Context) {
|
||||
func (s *Server) onList(ctx *gin.Context) {
|
||||
pathName := ctx.Query("path")
|
||||
|
||||
if !p.doAuth(ctx, pathName) {
|
||||
if !s.doAuth(ctx, pathName) {
|
||||
return
|
||||
}
|
||||
|
||||
pathConf, err := p.safeFindPathConf(pathName)
|
||||
pathConf, err := s.safeFindPathConf(pathName)
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
s.writeError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
segments, err := FindSegments(pathConf, pathName)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoSegmentsFound) {
|
||||
p.writeError(ctx, http.StatusNotFound, err)
|
||||
s.writeError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
p.writeError(ctx, http.StatusBadRequest, err)
|
||||
s.writeError(ctx, http.StatusBadRequest, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
out, err := computeDurationAndConcatenate(pathConf.RecordFormat, segments)
|
||||
entries, err := computeDurationAndConcatenate(pathConf.RecordFormat, segments)
|
||||
if err != nil {
|
||||
p.writeError(ctx, http.StatusInternalServerError, err)
|
||||
s.writeError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, out)
|
||||
var scheme string
|
||||
if s.Encryption {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
for i := range entries {
|
||||
v := url.Values{}
|
||||
v.Add("path", pathName)
|
||||
v.Add("start", entries[i].Start.Format(time.RFC3339Nano))
|
||||
v.Add("duration", strconv.FormatFloat(time.Duration(entries[i].Duration).Seconds(), 'f', -1, 64))
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: ctx.Request.Host,
|
||||
Path: "/get",
|
||||
RawQuery: v.Encode(),
|
||||
}
|
||||
entries[i].URL = u.String()
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, entries)
|
||||
}
|
||||
|
||||
@@ -78,10 +78,14 @@ func TestOnList(t *testing.T) {
|
||||
map[string]interface{}{
|
||||
"duration": float64(65),
|
||||
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=65&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
map[string]interface{}{
|
||||
"duration": float64(3),
|
||||
"start": time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=3&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2009, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
}, out)
|
||||
}
|
||||
@@ -136,10 +140,14 @@ func TestOnListDifferentInit(t *testing.T) {
|
||||
map[string]interface{}{
|
||||
"duration": float64(62),
|
||||
"start": time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=62&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
map[string]interface{}{
|
||||
"duration": float64(1),
|
||||
"start": time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),
|
||||
"url": "http://localhost:9996/get?duration=1&path=mypath&start=" +
|
||||
url.QueryEscape(time.Date(2008, 11, 0o7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),
|
||||
},
|
||||
}, out)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user