mediamtx/internal/hls/client_processor_fmp4.go
Alessandro Ros e5ab731d14
Improve HLS client (#1179)
* hls source: support fMP4s video streams

* hls source: start reading live streams from (end of playlist - starting point)

* hls client: wait processing of current fMP4 segment before downloading another one

* hls client: support fmp4 trun boxes with default sample duration, flags and size

* hls client: merge fmp4 init file reader and writer

* hls client: merge fmp4 part reader and writer

* hls client: improve precision of go <-> mp4 time conversion

* hls client: fix esds generation in go-mp4

* hls client: support audio in separate playlist

* hls client: support an arbitrary number of tracks in fmp4 init files

* hls client: support EXT-X-BYTERANGE

* hls client: support fmp4 segments with multiple parts at once

* hls client: support an arbitrary number of mpeg-ts tracks

* hls client: synchronize tracks around a primary track

* update go-mp4

* hls: synchronize track reproduction around a leading one

* hls client: reset stream if playback is too late

* hls client: add limit on DTS-RTC difference

* hls client: support again streams that don't provide codecs in master playlist
2022-10-23 14:04:33 +02:00

218 lines
4.8 KiB
Go

package hls
import (
"context"
"fmt"
"time"
"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/h264"
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
)
func fmp4PickLeadingTrack(init *fmp4.Init) int {
// pick first video track
for _, track := range init.Tracks {
if _, ok := track.Track.(*gortsplib.TrackH264); ok {
return track.ID
}
}
// otherwise, pick first track
return init.Tracks[0].ID
}
type clientProcessorFMP4 struct {
isLeading bool
segmentQueue *clientSegmentQueue
logger ClientLogger
rp *clientRoutinePool
onSetLeadingTimeSync func(clientTimeSync)
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool)
onVideoData func(time.Duration, [][]byte)
onAudioData func(time.Duration, []byte)
init fmp4.Init
leadingTrackID int
trackProcs map[int]*clientProcessorFMP4Track
// in
subpartProcessed chan struct{}
}
func newClientProcessorFMP4(
ctx context.Context,
isLeading bool,
initFile []byte,
segmentQueue *clientSegmentQueue,
logger ClientLogger,
rp *clientRoutinePool,
onStreamTracks func(context.Context, []gortsplib.Track) bool,
onSetLeadingTimeSync func(clientTimeSync),
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool),
onVideoData func(time.Duration, [][]byte),
onAudioData func(time.Duration, []byte),
) (*clientProcessorFMP4, error) {
p := &clientProcessorFMP4{
isLeading: isLeading,
segmentQueue: segmentQueue,
logger: logger,
rp: rp,
onSetLeadingTimeSync: onSetLeadingTimeSync,
onGetLeadingTimeSync: onGetLeadingTimeSync,
onVideoData: onVideoData,
onAudioData: onAudioData,
subpartProcessed: make(chan struct{}, clientFMP4MaxPartTracksPerSegment),
}
err := p.init.Unmarshal(initFile)
if err != nil {
return nil, err
}
p.leadingTrackID = fmp4PickLeadingTrack(&p.init)
tracks := make([]gortsplib.Track, len(p.init.Tracks))
for i, track := range p.init.Tracks {
tracks[i] = track.Track
}
ok := onStreamTracks(ctx, tracks)
if !ok {
return nil, fmt.Errorf("terminated")
}
return p, nil
}
func (p *clientProcessorFMP4) run(ctx context.Context) error {
for {
seg, ok := p.segmentQueue.pull(ctx)
if !ok {
return fmt.Errorf("terminated")
}
err := p.processSegment(ctx, seg)
if err != nil {
return err
}
}
}
func (p *clientProcessorFMP4) processSegment(ctx context.Context, byts []byte) error {
var parts fmp4.Parts
err := parts.Unmarshal(byts)
if err != nil {
return err
}
processingCount := 0
for _, part := range parts {
for _, track := range part.Tracks {
if p.trackProcs == nil {
var ts *clientTimeSyncFMP4
if p.isLeading {
if track.ID != p.leadingTrackID {
continue
}
timeScale := func() uint32 {
for _, track := range p.init.Tracks {
if track.ID == p.leadingTrackID {
return track.TimeScale
}
}
return 0
}()
ts = newClientTimeSyncFMP4(timeScale, track.BaseTime)
p.onSetLeadingTimeSync(ts)
} else {
rawTS, ok := p.onGetLeadingTimeSync(ctx)
if !ok {
return fmt.Errorf("terminated")
}
ts, ok = rawTS.(*clientTimeSyncFMP4)
if !ok {
return fmt.Errorf("stream playlists are mixed MPEGTS/FMP4")
}
}
p.initializeTrackProcs(ts)
}
proc, ok := p.trackProcs[track.ID]
if !ok {
return fmt.Errorf("track ID %d not present in init file", track.ID)
}
if processingCount >= (clientFMP4MaxPartTracksPerSegment - 1) {
return fmt.Errorf("too many part tracks at once")
}
select {
case proc.queue <- track:
case <-ctx.Done():
return fmt.Errorf("terminated")
}
processingCount++
}
}
for i := 0; i < processingCount; i++ {
select {
case <-p.subpartProcessed:
case <-ctx.Done():
return fmt.Errorf("terminated")
}
}
return nil
}
func (p *clientProcessorFMP4) onPartTrackProcessed(ctx context.Context) {
select {
case p.subpartProcessed <- struct{}{}:
case <-ctx.Done():
}
}
func (p *clientProcessorFMP4) initializeTrackProcs(ts *clientTimeSyncFMP4) {
p.trackProcs = make(map[int]*clientProcessorFMP4Track)
for _, track := range p.init.Tracks {
var cb func(time.Duration, []byte) error
switch track.Track.(type) {
case *gortsplib.TrackH264:
cb = func(pts time.Duration, payload []byte) error {
nalus, err := h264.AVCCUnmarshal(payload)
if err != nil {
return err
}
p.onVideoData(pts, nalus)
return nil
}
case *gortsplib.TrackMPEG4Audio:
cb = func(pts time.Duration, payload []byte) error {
p.onAudioData(pts, payload)
return nil
}
}
proc := newClientProcessorFMP4Track(
track.TimeScale,
ts,
p.onPartTrackProcessed,
cb,
)
p.rp.add(proc)
p.trackProcs[track.ID] = proc
}
}