package hls import ( "bytes" "io" "math" "net/http" "strconv" "strings" "sync" "time" "github.com/aler9/gortsplib/v2/pkg/format" ) type muxerVariantFMP4SegmentOrGap interface { getRenderedDuration() time.Duration } type muxerVariantFMP4Gap struct { renderedDuration time.Duration } func (g muxerVariantFMP4Gap) getRenderedDuration() time.Duration { return g.renderedDuration } func targetDuration(segments []muxerVariantFMP4SegmentOrGap) uint { ret := uint(0) // EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION for _, sog := range segments { v := uint(math.Round(sog.getRenderedDuration().Seconds())) if v > ret { ret = v } } return ret } func partTargetDuration( segments []muxerVariantFMP4SegmentOrGap, nextSegmentParts []*muxerVariantFMP4Part, ) time.Duration { var ret time.Duration for _, sog := range segments { seg, ok := sog.(*muxerVariantFMP4Segment) if !ok { continue } for _, part := range seg.parts { if part.renderedDuration > ret { ret = part.renderedDuration } } } for _, part := range nextSegmentParts { if part.renderedDuration > ret { ret = part.renderedDuration } } return ret } type muxerVariantFMP4Playlist struct { lowLatency bool segmentCount int videoTrack format.Format audioTrack *format.MPEG4Audio mutex sync.Mutex cond *sync.Cond closed bool segments []muxerVariantFMP4SegmentOrGap segmentsByName map[string]*muxerVariantFMP4Segment segmentDeleteCount int parts []*muxerVariantFMP4Part partsByName map[string]*muxerVariantFMP4Part nextSegmentID uint64 nextSegmentParts []*muxerVariantFMP4Part nextPartID uint64 } func newMuxerVariantFMP4Playlist( lowLatency bool, segmentCount int, videoTrack format.Format, audioTrack *format.MPEG4Audio, ) *muxerVariantFMP4Playlist { p := &muxerVariantFMP4Playlist{ lowLatency: lowLatency, segmentCount: segmentCount, videoTrack: videoTrack, audioTrack: audioTrack, segmentsByName: make(map[string]*muxerVariantFMP4Segment), partsByName: make(map[string]*muxerVariantFMP4Part), } p.cond = sync.NewCond(&p.mutex) return p } func (p *muxerVariantFMP4Playlist) close() { func() { p.mutex.Lock() defer p.mutex.Unlock() p.closed = true }() p.cond.Broadcast() } func (p *muxerVariantFMP4Playlist) hasContent() bool { if p.lowLatency { return len(p.segments) >= 1 } return len(p.segments) >= 2 } func (p *muxerVariantFMP4Playlist) hasPart(segmentID uint64, partID uint64) bool { if !p.hasContent() { return false } for _, sop := range p.segments { seg, ok := sop.(*muxerVariantFMP4Segment) if !ok { continue } if segmentID != seg.id { continue } // If the Client requests a Part Index greater than that of the final // Partial Segment of the Parent Segment, the Server MUST treat the // request as one for Part Index 0 of the following Parent Segment. if partID >= uint64(len(seg.parts)) { segmentID++ partID = 0 continue } return true } if segmentID != p.nextSegmentID { return false } if partID >= uint64(len(p.nextSegmentParts)) { return false } return true } func (p *muxerVariantFMP4Playlist) file(name string, msn string, part string, skip string) *MuxerFileResponse { switch { case name == "stream.m3u8": return p.playlistReader(msn, part, skip) case strings.HasSuffix(name, ".mp4"): return p.segmentReader(name) default: return &MuxerFileResponse{Status: http.StatusNotFound} } } func (p *muxerVariantFMP4Playlist) playlistReader(msn string, part string, skip string) *MuxerFileResponse { isDeltaUpdate := false if p.lowLatency { isDeltaUpdate = skip == "YES" || skip == "v2" var msnint uint64 if msn != "" { var err error msnint, err = strconv.ParseUint(msn, 10, 64) if err != nil { return &MuxerFileResponse{Status: http.StatusBadRequest} } } var partint uint64 if part != "" { var err error partint, err = strconv.ParseUint(part, 10, 64) if err != nil { return &MuxerFileResponse{Status: http.StatusBadRequest} } } if msn != "" { p.mutex.Lock() defer p.mutex.Unlock() // If the _HLS_msn is greater than the Media Sequence Number of the last // Media Segment in the current Playlist plus two, or if the _HLS_part // exceeds the last Partial Segment in the current Playlist by the // Advance Part Limit, then the server SHOULD immediately return Bad // Request, such as HTTP 400. if msnint > (p.nextSegmentID + 1) { return &MuxerFileResponse{Status: http.StatusBadRequest} } for !p.closed && !p.hasPart(msnint, partint) { p.cond.Wait() } if p.closed { return &MuxerFileResponse{Status: http.StatusInternalServerError} } return &MuxerFileResponse{ Status: http.StatusOK, Header: map[string]string{ "Content-Type": `application/x-mpegURL`, }, Body: p.fullPlaylist(isDeltaUpdate), } } // part without msn is not supported. if part != "" { return &MuxerFileResponse{Status: http.StatusBadRequest} } } p.mutex.Lock() defer p.mutex.Unlock() for !p.closed && !p.hasContent() { p.cond.Wait() } if p.closed { return &MuxerFileResponse{Status: http.StatusInternalServerError} } return &MuxerFileResponse{ Status: http.StatusOK, Header: map[string]string{ "Content-Type": `application/x-mpegURL`, }, Body: p.fullPlaylist(isDeltaUpdate), } } func (p *muxerVariantFMP4Playlist) fullPlaylist(isDeltaUpdate bool) io.Reader { cnt := "#EXTM3U\n" cnt += "#EXT-X-VERSION:9\n" targetDuration := targetDuration(p.segments) cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n" skipBoundary := float64(targetDuration * 6) if p.lowLatency { partTargetDuration := partTargetDuration(p.segments, p.nextSegmentParts) // The value is an enumerated-string whose value is YES if the server // supports Blocking Playlist Reload cnt += "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES" // The value is a decimal-floating-point number of seconds that // indicates the server-recommended minimum distance from the end of // the Playlist at which clients should begin to play or to which // they should seek when playing in Low-Latency Mode. Its value MUST // be at least twice the Part Target Duration. Its value SHOULD be // at least three times the Part Target Duration. cnt += ",PART-HOLD-BACK=" + strconv.FormatFloat((partTargetDuration).Seconds()*2.5, 'f', 5, 64) // Indicates that the Server can produce Playlist Delta Updates in // response to the _HLS_skip Delivery Directive. Its value is the // Skip Boundary, a decimal-floating-point number of seconds. The // Skip Boundary MUST be at least six times the Target Duration. cnt += ",CAN-SKIP-UNTIL=" + strconv.FormatFloat(skipBoundary, 'f', -1, 64) cnt += "\n" cnt += "#EXT-X-PART-INF:PART-TARGET=" + strconv.FormatFloat(partTargetDuration.Seconds(), 'f', -1, 64) + "\n" } cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n" skipped := 0 if !isDeltaUpdate { cnt += "#EXT-X-MAP:URI=\"init.mp4\"\n" } else { var curDuration time.Duration shown := 0 for _, segment := range p.segments { curDuration += segment.getRenderedDuration() if curDuration.Seconds() >= skipBoundary { break } shown++ } skipped = len(p.segments) - shown cnt += "#EXT-X-SKIP:SKIPPED-SEGMENTS=" + strconv.FormatInt(int64(skipped), 10) + "\n" } for i, sog := range p.segments { if i < skipped { continue } switch seg := sog.(type) { case *muxerVariantFMP4Segment: if (len(p.segments) - i) <= 2 { cnt += "#EXT-X-PROGRAM-DATE-TIME:" + seg.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" } if p.lowLatency && (len(p.segments)-i) <= 2 { for _, part := range seg.parts { cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) + ",URI=\"" + part.name() + ".mp4\"" if part.isIndependent { cnt += ",INDEPENDENT=YES" } cnt += "\n" } } cnt += "#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" + seg.name + ".mp4\n" case *muxerVariantFMP4Gap: cnt += "#EXT-X-GAP\n" + "#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" + "gap.mp4\n" } } if p.lowLatency { for _, part := range p.nextSegmentParts { cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) + ",URI=\"" + part.name() + ".mp4\"" if part.isIndependent { cnt += ",INDEPENDENT=YES" } cnt += "\n" } // preload hint must always be present // otherwise hls.js goes into a loop cnt += "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"" + fmp4PartName(p.nextPartID) + ".mp4\"\n" } return bytes.NewReader([]byte(cnt)) } func (p *muxerVariantFMP4Playlist) segmentReader(fname string) *MuxerFileResponse { switch { case strings.HasPrefix(fname, "seg"): base := strings.TrimSuffix(fname, ".mp4") p.mutex.Lock() segment, ok := p.segmentsByName[base] p.mutex.Unlock() if !ok { return &MuxerFileResponse{Status: http.StatusNotFound} } return &MuxerFileResponse{ Status: http.StatusOK, Header: map[string]string{ "Content-Type": "video/mp4", }, Body: segment.reader(), } case strings.HasPrefix(fname, "part"): base := strings.TrimSuffix(fname, ".mp4") p.mutex.Lock() part, ok := p.partsByName[base] nextPartID := p.nextPartID p.mutex.Unlock() if ok { return &MuxerFileResponse{ Status: http.StatusOK, Header: map[string]string{ "Content-Type": "video/mp4", }, Body: part.reader(), } } // EXT-X-PRELOAD-HINT support nextPartName := fmp4PartName(p.nextPartID) if base == nextPartName { p.mutex.Lock() defer p.mutex.Unlock() for { if p.closed { break } if p.nextPartID > nextPartID { break } p.cond.Wait() } if p.closed { return &MuxerFileResponse{Status: http.StatusInternalServerError} } return &MuxerFileResponse{ Status: http.StatusOK, Header: map[string]string{ "Content-Type": "video/mp4", }, Body: p.partsByName[nextPartName].reader(), } } return &MuxerFileResponse{Status: http.StatusNotFound} default: return &MuxerFileResponse{Status: http.StatusNotFound} } } func (p *muxerVariantFMP4Playlist) onSegmentFinalized(segment *muxerVariantFMP4Segment) { func() { p.mutex.Lock() defer p.mutex.Unlock() // add initial gaps, required by iOS LL-HLS if p.lowLatency && len(p.segments) == 0 { for i := 0; i < 7; i++ { p.segments = append(p.segments, &muxerVariantFMP4Gap{ renderedDuration: segment.renderedDuration, }) } } p.segmentsByName[segment.name] = segment p.segments = append(p.segments, segment) p.nextSegmentID = segment.id + 1 p.nextSegmentParts = p.nextSegmentParts[:0] if len(p.segments) > p.segmentCount { toDelete := p.segments[0] if toDeleteSeg, ok := toDelete.(*muxerVariantFMP4Segment); ok { for _, part := range toDeleteSeg.parts { delete(p.partsByName, part.name()) } p.parts = p.parts[len(toDeleteSeg.parts):] delete(p.segmentsByName, toDeleteSeg.name) } p.segments = p.segments[1:] p.segmentDeleteCount++ } }() p.cond.Broadcast() } func (p *muxerVariantFMP4Playlist) onPartFinalized(part *muxerVariantFMP4Part) { func() { p.mutex.Lock() defer p.mutex.Unlock() p.partsByName[part.name()] = part p.parts = append(p.parts, part) p.nextSegmentParts = append(p.nextSegmentParts, part) p.nextPartID = part.id + 1 }() p.cond.Broadcast() }