playback: add "url" field to recording timespans in /list (#3619)

This commit is contained in:
Alessandro Ros
2024-08-25 23:04:36 +02:00
committed by GitHub
parent dc3b5f4e57
commit 6da35c8041
4 changed files with 60 additions and 26 deletions
+7 -5
View File
@@ -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
View File
@@ -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
}
}
+33 -9
View File
@@ -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)
}
+8
View File
@@ -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)
}