mediamtx/internal/playback/on_get.go

189 lines
3.9 KiB
Go

package playback
import (
"errors"
"fmt"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/recordstore"
"github.com/gin-gonic/gin"
)
type writerWrapper struct {
ctx *gin.Context
written bool
}
func (w *writerWrapper) Write(p []byte) (int, error) {
if !w.written {
w.written = true
w.ctx.Header("Accept-Ranges", "none")
w.ctx.Header("Content-Type", "video/mp4")
}
return w.ctx.Writer.Write(p)
}
func parseDuration(raw string) (time.Duration, error) {
// seconds
if secs, err := strconv.ParseFloat(raw, 64); err == nil {
return time.Duration(secs * float64(time.Second)), nil
}
// deprecated, golang format
return time.ParseDuration(raw)
}
func seekAndMux(
recordFormat conf.RecordFormat,
segments []*recordstore.Segment,
start time.Time,
duration time.Duration,
m muxer,
) error {
if recordFormat == conf.RecordFormatFMP4 {
var firstInit *fmp4.Init
var segmentEnd time.Time
f, err := os.Open(segments[0].Fpath)
if err != nil {
return err
}
defer f.Close()
firstInit, err = segmentFMP4ReadInit(f)
if err != nil {
return err
}
m.writeInit(firstInit)
segmentStartOffset := start.Sub(segments[0].Start)
segmentMaxElapsed, err := segmentFMP4SeekAndMuxParts(f, segmentStartOffset, duration, firstInit, m)
if err != nil {
return err
}
segmentEnd = start.Add(segmentMaxElapsed)
for _, seg := range segments[1:] {
f, err = os.Open(seg.Fpath)
if err != nil {
return err
}
defer f.Close()
var init *fmp4.Init
init, err = segmentFMP4ReadInit(f)
if err != nil {
return err
}
if !segmentFMP4CanBeConcatenated(firstInit, segmentEnd, init, seg.Start) {
break
}
segmentStartOffset := seg.Start.Sub(start)
var segmentMaxElapsed time.Duration
segmentMaxElapsed, err = segmentFMP4MuxParts(f, segmentStartOffset, duration, firstInit, m)
if err != nil {
return err
}
segmentEnd = start.Add(segmentMaxElapsed)
}
err = m.flush()
if err != nil {
return err
}
return nil
}
return fmt.Errorf("MPEG-TS format is not supported yet")
}
func (s *Server) onGet(ctx *gin.Context) {
pathName := ctx.Query("path")
if !s.doAuth(ctx, pathName) {
return
}
start, err := time.Parse(time.RFC3339, ctx.Query("start"))
if err != nil {
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid start: %w", err))
return
}
duration, err := parseDuration(ctx.Query("duration"))
if err != nil {
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid duration: %w", err))
return
}
ww := &writerWrapper{ctx: ctx}
var m muxer
format := ctx.Query("format")
switch format {
case "", "fmp4":
m = &muxerFMP4{w: ww}
case "mp4":
m = &muxerMP4{w: ww}
default:
s.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid format: %s", format))
return
}
pathConf, err := s.safeFindPathConf(pathName)
if err != nil {
s.writeError(ctx, http.StatusBadRequest, err)
return
}
segments, err := recordstore.FindSegmentsInTimespan(pathConf, pathName, start, duration)
if err != nil {
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
s.writeError(ctx, http.StatusNotFound, err)
} else {
s.writeError(ctx, http.StatusBadRequest, err)
}
return
}
err = seekAndMux(pathConf.RecordFormat, segments, start, duration, m)
if err != nil {
// user aborted the download
var neterr *net.OpError
if errors.As(err, &neterr) {
return
}
// nothing has been written yet; send back JSON
if !ww.written {
if errors.Is(err, recordstore.ErrNoSegmentsFound) {
s.writeError(ctx, http.StatusNotFound, err)
} else {
s.writeError(ctx, http.StatusBadRequest, err)
}
return
}
// something has already been written: abort and write logs only
s.Log(logger.Error, err.Error())
return
}
}