Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
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.
 
 
 
 
 
 

487 lines
12 KiB

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.H264
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.H264,
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(),
}
}
if fname == fmp4PartName(p.nextPartID) {
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[fmp4PartName(nextPartID)].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()
}