mediamtx/internal/recorder/format_fmp4_segment.go
Alessandro Ros ac0ddc9e8a
playback: improve /list response time (#3637) (#4096)
Response times of the /list endpoint were slow because the duration of
each segment was computed from scratch by summing the duration of each
of its parts.

This is improved by storing the duration of the overall segment in the
header and using that, if available.
2025-01-03 13:08:46 +01:00

175 lines
3.3 KiB
Go

package recorder
import (
"bytes"
"fmt"
"io"
"os"
"time"
"github.com/abema/go-mp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
"github.com/bluenviron/mediamtx/internal/logger"
)
func writeInit(f io.Writer, tracks []*formatFMP4Track) error {
fmp4Tracks := make([]*fmp4.InitTrack, len(tracks))
for i, track := range tracks {
fmp4Tracks[i] = track.initTrack
}
init := fmp4.Init{
Tracks: fmp4Tracks,
}
var buf seekablebuffer.Buffer
err := init.Marshal(&buf)
if err != nil {
return err
}
_, err = f.Write(buf.Bytes())
return err
}
func writeDuration(f io.ReadWriteSeeker, d time.Duration) error {
_, err := f.Seek(0, io.SeekStart)
if err != nil {
return err
}
// check and skip ftyp header and content
buf := make([]byte, 8)
_, err = io.ReadFull(f, buf)
if err != nil {
return err
}
if !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {
return fmt.Errorf("ftyp box not found")
}
ftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
_, err = f.Seek(int64(ftypSize), io.SeekStart)
if err != nil {
return err
}
// check and skip moov header
_, err = io.ReadFull(f, buf)
if err != nil {
return err
}
if !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {
return fmt.Errorf("moov box not found")
}
moovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])
moovPos, err := f.Seek(8, io.SeekCurrent)
if err != nil {
return err
}
var mvhd mp4.Mvhd
_, err = mp4.Unmarshal(f, uint64(moovSize-8), &mvhd, mp4.Context{})
if err != nil {
return err
}
mvhd.DurationV0 = uint32(d / time.Millisecond)
_, err = f.Seek(moovPos, io.SeekStart)
if err != nil {
return err
}
_, err = mp4.Marshal(f, &mvhd, mp4.Context{})
if err != nil {
return err
}
return nil
}
type formatFMP4Segment struct {
f *formatFMP4
startDTS time.Duration
startNTP time.Time
path string
fi *os.File
curPart *formatFMP4Part
lastDTS time.Duration
}
func (s *formatFMP4Segment) initialize() {
s.lastDTS = s.startDTS
}
func (s *formatFMP4Segment) close() error {
var err error
if s.curPart != nil {
err = s.curPart.close()
}
if s.fi != nil {
s.f.ri.Log(logger.Debug, "closing segment %s", s.path)
// write overall duration in the header in order to speed up the playback server
duration := s.lastDTS - s.startDTS
err2 := writeDuration(s.fi, duration)
if err == nil {
err = err2
}
err2 = s.fi.Close()
if err == nil {
err = err2
}
if err2 == nil {
s.f.ri.rec.OnSegmentComplete(s.path, duration)
}
}
return err
}
func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample, dtsDuration time.Duration) error {
s.lastDTS = dtsDuration
if s.curPart == nil {
s.curPart = &formatFMP4Part{
s: s,
sequenceNumber: s.f.nextSequenceNumber,
startDTS: dtsDuration,
}
s.curPart.initialize()
s.f.nextSequenceNumber++
} else if s.curPart.duration() >= s.f.ri.rec.PartDuration {
err := s.curPart.close()
s.curPart = nil
if err != nil {
return err
}
s.curPart = &formatFMP4Part{
s: s,
sequenceNumber: s.f.nextSequenceNumber,
startDTS: dtsDuration,
}
s.curPart.initialize()
s.f.nextSequenceNumber++
}
return s.curPart.write(track, sample, dtsDuration)
}