mirror of
https://github.com/bluenviron/mediamtx
synced 2025-02-26 16:40:30 +00:00
188 lines
3.8 KiB
Go
188 lines
3.8 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/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
|
|
}
|
|
}
|