mirror of
				https://github.com/aler9/rtsp-simple-server
				synced 2025-10-31 11:06:28 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			189 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			189 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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
 | |
| 	}
 | |
| }
 | 
