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/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 []*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 (p *Server) onGet(ctx *gin.Context) { pathName := ctx.Query("path") if !p.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)) return } duration, err := parseDuration(ctx.Query("duration")) if err != nil { p.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: p.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid format: %s", format)) return } pathConf, err := p.safeFindPathConf(pathName) if err != nil { p.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) } else { p.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, errNoSegmentsFound) { p.writeError(ctx, http.StatusNotFound, err) } else { p.writeError(ctx, http.StatusBadRequest, err) } return } // something has already been written: abort and write logs only p.Log(logger.Error, err.Error()) return } }