golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
		
		
		
		
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							406 lines
						
					
					
						
							9.1 KiB
						
					
					
				
			
		
		
	
	
							406 lines
						
					
					
						
							9.1 KiB
						
					
					
				package hls | 
						|
 | 
						|
import ( | 
						|
	"fmt" | 
						|
	"time" | 
						|
 | 
						|
	"github.com/aler9/gortsplib/v2/pkg/codecs/h264" | 
						|
	"github.com/aler9/gortsplib/v2/pkg/codecs/h265" | 
						|
	"github.com/aler9/gortsplib/v2/pkg/format" | 
						|
 | 
						|
	"github.com/aler9/rtsp-simple-server/internal/hls/fmp4" | 
						|
) | 
						|
 | 
						|
func partDurationIsCompatible(partDuration time.Duration, sampleDuration time.Duration) bool { | 
						|
	if sampleDuration > partDuration { | 
						|
		return false | 
						|
	} | 
						|
 | 
						|
	f := (partDuration / sampleDuration) | 
						|
	if (partDuration % sampleDuration) != 0 { | 
						|
		f++ | 
						|
	} | 
						|
	f *= sampleDuration | 
						|
 | 
						|
	return partDuration > ((f * 85) / 100) | 
						|
} | 
						|
 | 
						|
func partDurationIsCompatibleWithAll(partDuration time.Duration, sampleDurations map[time.Duration]struct{}) bool { | 
						|
	for sd := range sampleDurations { | 
						|
		if !partDurationIsCompatible(partDuration, sd) { | 
						|
			return false | 
						|
		} | 
						|
	} | 
						|
	return true | 
						|
} | 
						|
 | 
						|
func findCompatiblePartDuration( | 
						|
	minPartDuration time.Duration, | 
						|
	sampleDurations map[time.Duration]struct{}, | 
						|
) time.Duration { | 
						|
	i := minPartDuration | 
						|
	for ; i < 5*time.Second; i += 5 * time.Millisecond { | 
						|
		if partDurationIsCompatibleWithAll(i, sampleDurations) { | 
						|
			break | 
						|
		} | 
						|
	} | 
						|
	return i | 
						|
} | 
						|
 | 
						|
type dtsExtractor interface { | 
						|
	Extract([][]byte, time.Duration) (time.Duration, error) | 
						|
} | 
						|
 | 
						|
func allocateDTSExtractor(track format.Format) dtsExtractor { | 
						|
	switch track.(type) { | 
						|
	case *format.H264: | 
						|
		return h264.NewDTSExtractor() | 
						|
 | 
						|
	case *format.H265: | 
						|
		return h265.NewDTSExtractor() | 
						|
	} | 
						|
	return nil | 
						|
} | 
						|
 | 
						|
type augmentedVideoSample struct { | 
						|
	fmp4.PartSample | 
						|
	dts time.Duration | 
						|
	ntp time.Time | 
						|
} | 
						|
 | 
						|
type augmentedAudioSample struct { | 
						|
	fmp4.PartSample | 
						|
	dts time.Duration | 
						|
	ntp time.Time | 
						|
} | 
						|
 | 
						|
type muxerVariantFMP4Segmenter struct { | 
						|
	lowLatency         bool | 
						|
	segmentDuration    time.Duration | 
						|
	partDuration       time.Duration | 
						|
	segmentMaxSize     uint64 | 
						|
	videoTrack         format.Format | 
						|
	audioTrack         format.Format | 
						|
	onSegmentFinalized func(*muxerVariantFMP4Segment) | 
						|
	onPartFinalized    func(*muxerVariantFMP4Part) | 
						|
 | 
						|
	startDTS                       time.Duration | 
						|
	videoFirstRandomAccessReceived bool | 
						|
	videoDTSExtractor              dtsExtractor | 
						|
	lastVideoParams                [][]byte | 
						|
	currentSegment                 *muxerVariantFMP4Segment | 
						|
	nextSegmentID                  uint64 | 
						|
	nextPartID                     uint64 | 
						|
	nextVideoSample                *augmentedVideoSample | 
						|
	nextAudioSample                *augmentedAudioSample | 
						|
	firstSegmentFinalized          bool | 
						|
	sampleDurations                map[time.Duration]struct{} | 
						|
	adjustedPartDuration           time.Duration | 
						|
} | 
						|
 | 
						|
func newMuxerVariantFMP4Segmenter( | 
						|
	lowLatency bool, | 
						|
	segmentCount int, | 
						|
	segmentDuration time.Duration, | 
						|
	partDuration time.Duration, | 
						|
	segmentMaxSize uint64, | 
						|
	videoTrack format.Format, | 
						|
	audioTrack format.Format, | 
						|
	onSegmentFinalized func(*muxerVariantFMP4Segment), | 
						|
	onPartFinalized func(*muxerVariantFMP4Part), | 
						|
) *muxerVariantFMP4Segmenter { | 
						|
	m := &muxerVariantFMP4Segmenter{ | 
						|
		lowLatency:         lowLatency, | 
						|
		segmentDuration:    segmentDuration, | 
						|
		partDuration:       partDuration, | 
						|
		segmentMaxSize:     segmentMaxSize, | 
						|
		videoTrack:         videoTrack, | 
						|
		audioTrack:         audioTrack, | 
						|
		onSegmentFinalized: onSegmentFinalized, | 
						|
		onPartFinalized:    onPartFinalized, | 
						|
		sampleDurations:    make(map[time.Duration]struct{}), | 
						|
	} | 
						|
 | 
						|
	// add initial gaps, required by iOS LL-HLS | 
						|
	if m.lowLatency { | 
						|
		m.nextSegmentID = 7 | 
						|
	} | 
						|
 | 
						|
	return m | 
						|
} | 
						|
 | 
						|
func (m *muxerVariantFMP4Segmenter) genSegmentID() uint64 { | 
						|
	id := m.nextSegmentID | 
						|
	m.nextSegmentID++ | 
						|
	return id | 
						|
} | 
						|
 | 
						|
func (m *muxerVariantFMP4Segmenter) genPartID() uint64 { | 
						|
	id := m.nextPartID | 
						|
	m.nextPartID++ | 
						|
	return id | 
						|
} | 
						|
 | 
						|
// iPhone iOS fails if part durations are less than 85% of maximum part duration. | 
						|
// find a part duration that is compatible with all received sample durations | 
						|
func (m *muxerVariantFMP4Segmenter) adjustPartDuration(du time.Duration) { | 
						|
	if !m.lowLatency || m.firstSegmentFinalized { | 
						|
		return | 
						|
	} | 
						|
 | 
						|
	// avoid a crash by skipping invalid durations | 
						|
	if du == 0 { | 
						|
		return | 
						|
	} | 
						|
 | 
						|
	if _, ok := m.sampleDurations[du]; !ok { | 
						|
		m.sampleDurations[du] = struct{}{} | 
						|
		m.adjustedPartDuration = findCompatiblePartDuration( | 
						|
			m.partDuration, | 
						|
			m.sampleDurations, | 
						|
		) | 
						|
	} | 
						|
} | 
						|
 | 
						|
func (m *muxerVariantFMP4Segmenter) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error { | 
						|
	randomAccessPresent := false | 
						|
 | 
						|
	switch m.videoTrack.(type) { | 
						|
	case *format.H264: | 
						|
		nonIDRPresent := false | 
						|
 | 
						|
		for _, nalu := range au { | 
						|
			typ := h264.NALUType(nalu[0] & 0x1F) | 
						|
 | 
						|
			switch typ { | 
						|
			case h264.NALUTypeIDR: | 
						|
				randomAccessPresent = true | 
						|
 | 
						|
			case h264.NALUTypeNonIDR: | 
						|
				nonIDRPresent = true | 
						|
			} | 
						|
		} | 
						|
 | 
						|
		if !randomAccessPresent && !nonIDRPresent { | 
						|
			return nil | 
						|
		} | 
						|
 | 
						|
	case *format.H265: | 
						|
		for _, nalu := range au { | 
						|
			typ := h265.NALUType((nalu[0] >> 1) & 0b111111) | 
						|
 | 
						|
			switch typ { | 
						|
			case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT: | 
						|
				randomAccessPresent = true | 
						|
			} | 
						|
		} | 
						|
	} | 
						|
 | 
						|
	return m.writeH26xEntry(ntp, pts, au, randomAccessPresent) | 
						|
} | 
						|
 | 
						|
func (m *muxerVariantFMP4Segmenter) writeH26xEntry( | 
						|
	ntp time.Time, | 
						|
	pts time.Duration, | 
						|
	au [][]byte, | 
						|
	randomAccessPresent bool, | 
						|
) error { | 
						|
	var dts time.Duration | 
						|
 | 
						|
	if !m.videoFirstRandomAccessReceived { | 
						|
		// skip sample silently until we find one with an IDR | 
						|
		if !randomAccessPresent { | 
						|
			return nil | 
						|
		} | 
						|
 | 
						|
		m.videoFirstRandomAccessReceived = true | 
						|
		m.videoDTSExtractor = allocateDTSExtractor(m.videoTrack) | 
						|
		m.lastVideoParams = extractVideoParams(m.videoTrack) | 
						|
 | 
						|
		var err error | 
						|
		dts, err = m.videoDTSExtractor.Extract(au, pts) | 
						|
		if err != nil { | 
						|
			return fmt.Errorf("unable to extract DTS: %v", err) | 
						|
		} | 
						|
 | 
						|
		m.startDTS = dts | 
						|
		dts = 0 | 
						|
		pts -= m.startDTS | 
						|
	} else { | 
						|
		var err error | 
						|
		dts, err = m.videoDTSExtractor.Extract(au, pts) | 
						|
		if err != nil { | 
						|
			return fmt.Errorf("unable to extract DTS: %v", err) | 
						|
		} | 
						|
 | 
						|
		dts -= m.startDTS | 
						|
		pts -= m.startDTS | 
						|
	} | 
						|
 | 
						|
	avcc, err := h264.AVCCMarshal(au) | 
						|
	if err != nil { | 
						|
		return err | 
						|
	} | 
						|
 | 
						|
	sample := &augmentedVideoSample{ | 
						|
		PartSample: fmp4.PartSample{ | 
						|
			PTSOffset:       int32(durationGoToMp4(pts-dts, 90000)), | 
						|
			IsNonSyncSample: !randomAccessPresent, | 
						|
			Payload:         avcc, | 
						|
		}, | 
						|
		dts: dts, | 
						|
		ntp: ntp, | 
						|
	} | 
						|
 | 
						|
	// put samples into a queue in order to | 
						|
	// - compute sample duration | 
						|
	// - check if next sample is IDR | 
						|
	sample, m.nextVideoSample = m.nextVideoSample, sample | 
						|
	if sample == nil { | 
						|
		return nil | 
						|
	} | 
						|
	sample.Duration = uint32(durationGoToMp4(m.nextVideoSample.dts-sample.dts, 90000)) | 
						|
 | 
						|
	if m.currentSegment == nil { | 
						|
		// create first segment | 
						|
		m.currentSegment = newMuxerVariantFMP4Segment( | 
						|
			m.lowLatency, | 
						|
			m.genSegmentID(), | 
						|
			sample.ntp, | 
						|
			sample.dts, | 
						|
			m.segmentMaxSize, | 
						|
			m.videoTrack, | 
						|
			m.audioTrack, | 
						|
			m.genPartID, | 
						|
			m.onPartFinalized, | 
						|
		) | 
						|
	} | 
						|
 | 
						|
	m.adjustPartDuration(durationMp4ToGo(uint64(sample.Duration), 90000)) | 
						|
 | 
						|
	err = m.currentSegment.writeH264(sample, m.adjustedPartDuration) | 
						|
	if err != nil { | 
						|
		return err | 
						|
	} | 
						|
 | 
						|
	// switch segment | 
						|
	if randomAccessPresent { | 
						|
		videoParams := extractVideoParams(m.videoTrack) | 
						|
		paramsChanged := !videoParamsEqual(m.lastVideoParams, videoParams) | 
						|
 | 
						|
		if (m.nextVideoSample.dts-m.currentSegment.startDTS) >= m.segmentDuration || | 
						|
			paramsChanged { | 
						|
			err := m.currentSegment.finalize(m.nextVideoSample.dts) | 
						|
			if err != nil { | 
						|
				return err | 
						|
			} | 
						|
			m.onSegmentFinalized(m.currentSegment) | 
						|
 | 
						|
			m.firstSegmentFinalized = true | 
						|
 | 
						|
			m.currentSegment = newMuxerVariantFMP4Segment( | 
						|
				m.lowLatency, | 
						|
				m.genSegmentID(), | 
						|
				m.nextVideoSample.ntp, | 
						|
				m.nextVideoSample.dts, | 
						|
				m.segmentMaxSize, | 
						|
				m.videoTrack, | 
						|
				m.audioTrack, | 
						|
				m.genPartID, | 
						|
				m.onPartFinalized, | 
						|
			) | 
						|
 | 
						|
			if paramsChanged { | 
						|
				m.lastVideoParams = videoParams | 
						|
				m.firstSegmentFinalized = false | 
						|
 | 
						|
				// reset adjusted part duration | 
						|
				m.sampleDurations = make(map[time.Duration]struct{}) | 
						|
			} | 
						|
		} | 
						|
	} | 
						|
 | 
						|
	return nil | 
						|
} | 
						|
 | 
						|
func (m *muxerVariantFMP4Segmenter) writeAudio(ntp time.Time, dts time.Duration, au []byte) error { | 
						|
	if m.videoTrack != nil { | 
						|
		// wait for the video track | 
						|
		if !m.videoFirstRandomAccessReceived { | 
						|
			return nil | 
						|
		} | 
						|
 | 
						|
		dts -= m.startDTS | 
						|
		if dts < 0 { | 
						|
			return nil | 
						|
		} | 
						|
	} | 
						|
 | 
						|
	sample := &augmentedAudioSample{ | 
						|
		PartSample: fmp4.PartSample{ | 
						|
			Payload: au, | 
						|
		}, | 
						|
		dts: dts, | 
						|
		ntp: ntp, | 
						|
	} | 
						|
 | 
						|
	// put samples into a queue in order to compute the sample duration | 
						|
	sample, m.nextAudioSample = m.nextAudioSample, sample | 
						|
	if sample == nil { | 
						|
		return nil | 
						|
	} | 
						|
	sample.Duration = uint32(durationGoToMp4(m.nextAudioSample.dts-sample.dts, uint32(m.audioTrack.ClockRate()))) | 
						|
 | 
						|
	if m.videoTrack == nil { | 
						|
		if m.currentSegment == nil { | 
						|
			// create first segment | 
						|
			m.currentSegment = newMuxerVariantFMP4Segment( | 
						|
				m.lowLatency, | 
						|
				m.genSegmentID(), | 
						|
				sample.ntp, | 
						|
				sample.dts, | 
						|
				m.segmentMaxSize, | 
						|
				m.videoTrack, | 
						|
				m.audioTrack, | 
						|
				m.genPartID, | 
						|
				m.onPartFinalized, | 
						|
			) | 
						|
		} | 
						|
	} else { | 
						|
		// wait for the video track | 
						|
		if m.currentSegment == nil { | 
						|
			return nil | 
						|
		} | 
						|
	} | 
						|
 | 
						|
	err := m.currentSegment.writeAudio(sample, m.partDuration) | 
						|
	if err != nil { | 
						|
		return err | 
						|
	} | 
						|
 | 
						|
	// switch segment | 
						|
	if m.videoTrack == nil && | 
						|
		(m.nextAudioSample.dts-m.currentSegment.startDTS) >= m.segmentDuration { | 
						|
		err := m.currentSegment.finalize(0) | 
						|
		if err != nil { | 
						|
			return err | 
						|
		} | 
						|
		m.onSegmentFinalized(m.currentSegment) | 
						|
 | 
						|
		m.firstSegmentFinalized = true | 
						|
 | 
						|
		m.currentSegment = newMuxerVariantFMP4Segment( | 
						|
			m.lowLatency, | 
						|
			m.genSegmentID(), | 
						|
			m.nextAudioSample.ntp, | 
						|
			m.nextAudioSample.dts, | 
						|
			m.segmentMaxSize, | 
						|
			m.videoTrack, | 
						|
			m.audioTrack, | 
						|
			m.genPartID, | 
						|
			m.onPartFinalized, | 
						|
		) | 
						|
	} | 
						|
 | 
						|
	return nil | 
						|
}
 | 
						|
 |