package playback import ( "errors" "fmt" "net" "net/http" "os" "strconv" "time" "github.com/bluenviron/mediacommon/v2/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 = segmentFMP4ReadHeader(f) if err != nil { return err } m.writeInit(firstInit) segmentStartOffset := segments[0].Start.Sub(start) // this is negative segmentDuration, err := segmentFMP4MuxParts(f, segmentStartOffset, duration, firstInit.Tracks, m) if err != nil { return err } segmentEnd = start.Add(segmentDuration) 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 = segmentFMP4ReadHeader(f) if err != nil { return err } if !segmentFMP4CanBeConcatenated(firstInit, segmentEnd, init, seg.Start) { break } segmentStartOffset = seg.Start.Sub(start) // this is positive segmentDuration, err = segmentFMP4MuxParts(f, segmentStartOffset, duration, firstInit.Tracks, m) if err != nil { return err } segmentEnd = start.Add(segmentDuration) } 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 } 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) } 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 } }