Browse Source
* add hlsVariant parameter * hls: split muxer into variants * hls: implement fmp4 segments * hls muxer: implement low latency mode * hls muxer: support audio with fmp4 mode * hls muxer: rewrite file router * hls muxer: implement preload hint * hls muxer: add various error codes * hls muxer: use explicit flags * hls muxer: fix error in aac pts * hls muxer: fix sudden freezes with video+audio * hls muxer: skip empty parts * hls muxer: fix video FPS * hls muxer: add parameter hlsPartDuration * hls muxer: refactor fmp4 muxer * hls muxer: fix CAN-SKIP-UNTIL * hls muxer: refactor code * hls muxer: show only parts of last 2 segments * hls muxer: implementa playlist delta updates * hls muxer: change playlist content type * hls muxer: improve video dts precision * hls muxer: fix video sample flags * hls muxer: improve iphone audio support * hls muxer: improve mp4 timestamp precision * hls muxer: add offset between pts and dts * hls muxer: close muxer in case of error * hls muxer: stop logging requests with the info level * hls muxer: rename entry into sample * hls muxer: compensate video dts error over time * hls muxer: change default segment count * hls muxer: add starting gap * hls muxer: set default part duration to 200ms * hls muxer: fix audio-only streams on ios * hls muxer: add playsinline attribute to video tag of default web page * hls muxer: keep mpegts as the default hls variant * hls muxer: implement encryption * hls muxer: rewrite dts estimation * hls muxer: improve DTS precision * hls muxer: use right SPS/PPS for each sample * hls muxer: adjust part duration dynamically * add comments * update readme * hls muxer: fix memory leak * hls muxer: decrease ram consumptionpull/1003/head
30 changed files with 3340 additions and 557 deletions
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
package conf |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"github.com/aler9/rtsp-simple-server/internal/hls" |
||||
) |
||||
|
||||
// HLSVariant is the hlsVariant parameter.
|
||||
type HLSVariant hls.MuxerVariant |
||||
|
||||
// supported HLS variants.
|
||||
const ( |
||||
HLSVariantMPEGTS HLSVariant = HLSVariant(hls.MuxerVariantMPEGTS) |
||||
HLSVariantFMP4 HLSVariant = HLSVariant(hls.MuxerVariantFMP4) |
||||
HLSVariantLowLatency HLSVariant = HLSVariant(hls.MuxerVariantLowLatency) |
||||
) |
||||
|
||||
// MarshalJSON marshals a HLSVariant into JSON.
|
||||
func (d HLSVariant) MarshalJSON() ([]byte, error) { |
||||
var out string |
||||
|
||||
switch d { |
||||
case HLSVariantMPEGTS: |
||||
out = "mpegts" |
||||
|
||||
case HLSVariantFMP4: |
||||
out = "fmp4" |
||||
|
||||
default: |
||||
out = "lowLatency" |
||||
} |
||||
|
||||
return json.Marshal(out) |
||||
} |
||||
|
||||
// UnmarshalJSON unmarshals a HLSVariant from JSON.
|
||||
func (d *HLSVariant) UnmarshalJSON(b []byte) error { |
||||
var in string |
||||
if err := json.Unmarshal(b, &in); err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch in { |
||||
case "mpegts": |
||||
*d = HLSVariantMPEGTS |
||||
|
||||
case "fmp4": |
||||
*d = HLSVariantFMP4 |
||||
|
||||
case "lowLatency": |
||||
*d = HLSVariantLowLatency |
||||
|
||||
default: |
||||
return fmt.Errorf("invalid hlsVariant value: '%s'", in) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (d *HLSVariant) unmarshalEnv(s string) error { |
||||
return d.UnmarshalJSON([]byte(`"` + s + `"`)) |
||||
} |
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"io" |
||||
|
||||
"github.com/abema/go-mp4" |
||||
"github.com/orcaman/writerseeker" |
||||
) |
||||
|
||||
type mp4Writer struct { |
||||
buf *writerseeker.WriterSeeker |
||||
w *mp4.Writer |
||||
} |
||||
|
||||
func newMP4Writer() *mp4Writer { |
||||
w := &mp4Writer{ |
||||
buf: &writerseeker.WriterSeeker{}, |
||||
} |
||||
|
||||
w.w = mp4.NewWriter(w.buf) |
||||
|
||||
return w |
||||
} |
||||
|
||||
func (w *mp4Writer) writeBoxStart(box mp4.IImmutableBox) (int, error) { |
||||
bi := &mp4.BoxInfo{ |
||||
Type: box.GetType(), |
||||
} |
||||
var err error |
||||
bi, err = w.w.StartBox(bi) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
_, err = mp4.Marshal(w.w, box, mp4.Context{}) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
return int(bi.Offset), nil |
||||
} |
||||
|
||||
func (w *mp4Writer) writeBoxEnd() error { |
||||
_, err := w.w.EndBox() |
||||
return err |
||||
} |
||||
|
||||
func (w *mp4Writer) writeBox(box mp4.IImmutableBox) (int, error) { |
||||
off, err := w.writeBoxStart(box) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
return off, nil |
||||
} |
||||
|
||||
func (w *mp4Writer) rewriteBox(off int, box mp4.IImmutableBox) error { |
||||
prevOff, err := w.w.Seek(0, io.SeekCurrent) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.w.Seek(int64(off), io.SeekStart) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(box) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.w.Seek(prevOff, io.SeekStart) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (w *mp4Writer) bytes() []byte { |
||||
byts, _ := io.ReadAll(w.buf.Reader()) |
||||
return byts |
||||
} |
@ -1,130 +0,0 @@
@@ -1,130 +0,0 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"math" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
type asyncReader struct { |
||||
generator func() []byte |
||||
reader *bytes.Reader |
||||
} |
||||
|
||||
func (r *asyncReader) Read(buf []byte) (int, error) { |
||||
if r.reader == nil { |
||||
r.reader = bytes.NewReader(r.generator()) |
||||
} |
||||
return r.reader.Read(buf) |
||||
} |
||||
|
||||
type muxerStreamPlaylist struct { |
||||
hlsSegmentCount int |
||||
|
||||
mutex sync.Mutex |
||||
cond *sync.Cond |
||||
closed bool |
||||
segments []*muxerTSSegment |
||||
segmentByName map[string]*muxerTSSegment |
||||
segmentDeleteCount int |
||||
} |
||||
|
||||
func newMuxerStreamPlaylist(hlsSegmentCount int) *muxerStreamPlaylist { |
||||
p := &muxerStreamPlaylist{ |
||||
hlsSegmentCount: hlsSegmentCount, |
||||
segmentByName: make(map[string]*muxerTSSegment), |
||||
} |
||||
p.cond = sync.NewCond(&p.mutex) |
||||
return p |
||||
} |
||||
|
||||
func (p *muxerStreamPlaylist) close() { |
||||
func() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.closed = true |
||||
}() |
||||
|
||||
p.cond.Broadcast() |
||||
} |
||||
|
||||
func (p *muxerStreamPlaylist) reader() io.Reader { |
||||
return &asyncReader{generator: func() []byte { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
|
||||
if !p.closed && len(p.segments) == 0 { |
||||
p.cond.Wait() |
||||
} |
||||
|
||||
if p.closed { |
||||
return nil |
||||
} |
||||
|
||||
cnt := "#EXTM3U\n" |
||||
cnt += "#EXT-X-VERSION:3\n" |
||||
cnt += "#EXT-X-ALLOW-CACHE:NO\n" |
||||
|
||||
targetDuration := func() uint { |
||||
ret := uint(0) |
||||
|
||||
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
|
||||
for _, s := range p.segments { |
||||
v2 := uint(math.Round(s.duration().Seconds())) |
||||
if v2 > ret { |
||||
ret = v2 |
||||
} |
||||
} |
||||
|
||||
return ret |
||||
}() |
||||
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n" |
||||
|
||||
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n" |
||||
cnt += "#EXT-X-INDEPENDENT-SEGMENTS\n" |
||||
cnt += "\n" |
||||
|
||||
for _, s := range p.segments { |
||||
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + s.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" + |
||||
"#EXTINF:" + strconv.FormatFloat(s.duration().Seconds(), 'f', -1, 64) + ",\n" + |
||||
s.name + ".ts\n" |
||||
} |
||||
|
||||
return []byte(cnt) |
||||
}} |
||||
} |
||||
|
||||
func (p *muxerStreamPlaylist) segment(fname string) io.Reader { |
||||
base := strings.TrimSuffix(fname, ".ts") |
||||
|
||||
p.mutex.Lock() |
||||
f, ok := p.segmentByName[base] |
||||
p.mutex.Unlock() |
||||
|
||||
if !ok { |
||||
return nil |
||||
} |
||||
|
||||
return f.reader() |
||||
} |
||||
|
||||
func (p *muxerStreamPlaylist) pushSegment(t *muxerTSSegment) { |
||||
func() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
|
||||
p.segmentByName[t.name] = t |
||||
p.segments = append(p.segments, t) |
||||
|
||||
if len(p.segments) > p.hlsSegmentCount { |
||||
delete(p.segmentByName, p.segments[0].name) |
||||
p.segments = p.segments[1:] |
||||
p.segmentDeleteCount++ |
||||
} |
||||
}() |
||||
|
||||
p.cond.Broadcast() |
||||
} |
@ -1,201 +0,0 @@
@@ -1,201 +0,0 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/aac" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
"github.com/asticode/go-astits" |
||||
) |
||||
|
||||
const ( |
||||
segmentMinAUCount = 100 |
||||
) |
||||
|
||||
type writerFunc func(p []byte) (int, error) |
||||
|
||||
func (f writerFunc) Write(p []byte) (int, error) { |
||||
return f(p) |
||||
} |
||||
|
||||
type muxerTSGenerator struct { |
||||
hlsSegmentCount int |
||||
hlsSegmentDuration time.Duration |
||||
hlsSegmentMaxSize uint64 |
||||
videoTrack *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
streamPlaylist *muxerStreamPlaylist |
||||
|
||||
writer *astits.Muxer |
||||
currentSegment *muxerTSSegment |
||||
videoDTSEst *h264.DTSEstimator |
||||
startPCR time.Time |
||||
startPTS time.Duration |
||||
} |
||||
|
||||
func newMuxerTSGenerator( |
||||
hlsSegmentCount int, |
||||
hlsSegmentDuration time.Duration, |
||||
hlsSegmentMaxSize uint64, |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
streamPlaylist *muxerStreamPlaylist, |
||||
) *muxerTSGenerator { |
||||
m := &muxerTSGenerator{ |
||||
hlsSegmentCount: hlsSegmentCount, |
||||
hlsSegmentDuration: hlsSegmentDuration, |
||||
hlsSegmentMaxSize: hlsSegmentMaxSize, |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
streamPlaylist: streamPlaylist, |
||||
} |
||||
|
||||
m.writer = astits.NewMuxer( |
||||
context.Background(), |
||||
writerFunc(func(p []byte) (int, error) { |
||||
return m.currentSegment.write(p) |
||||
})) |
||||
|
||||
if videoTrack != nil { |
||||
m.writer.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 256, |
||||
StreamType: astits.StreamTypeH264Video, |
||||
}) |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
m.writer.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 257, |
||||
StreamType: astits.StreamTypeAACAudio, |
||||
}) |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
m.writer.SetPCRPID(256) |
||||
} else { |
||||
m.writer.SetPCRPID(257) |
||||
} |
||||
|
||||
return m |
||||
} |
||||
|
||||
func (m *muxerTSGenerator) writeH264(pts time.Duration, nalus [][]byte) error { |
||||
now := time.Now() |
||||
idrPresent := h264.IDRPresent(nalus) |
||||
|
||||
if m.currentSegment == nil { |
||||
// skip groups silently until we find one with a IDR
|
||||
if !idrPresent { |
||||
return nil |
||||
} |
||||
|
||||
// create first segment
|
||||
m.startPCR = now |
||||
m.currentSegment = newMuxerTSSegment(now, m.hlsSegmentMaxSize, |
||||
m.videoTrack, m.writer.WriteData) |
||||
m.videoDTSEst = h264.NewDTSEstimator() |
||||
m.startPTS = pts |
||||
pts = 0 |
||||
} else { |
||||
pts -= m.startPTS |
||||
|
||||
// switch segment
|
||||
if idrPresent && |
||||
m.currentSegment.startPTS != nil && |
||||
(pts-*m.currentSegment.startPTS) >= m.hlsSegmentDuration { |
||||
m.currentSegment.endPTS = pts |
||||
m.streamPlaylist.pushSegment(m.currentSegment) |
||||
m.currentSegment = newMuxerTSSegment(now, m.hlsSegmentMaxSize, |
||||
m.videoTrack, m.writer.WriteData) |
||||
} |
||||
} |
||||
|
||||
dts := m.videoDTSEst.Feed(pts) |
||||
|
||||
// prepend an AUD. This is required by video.js and iOS
|
||||
nalus = append([][]byte{{byte(h264.NALUTypeAccessUnitDelimiter), 240}}, nalus...) |
||||
|
||||
enc, err := h264.AnnexBEncode(nalus) |
||||
if err != nil { |
||||
if m.currentSegment.buf.Len() > 0 { |
||||
m.streamPlaylist.pushSegment(m.currentSegment) |
||||
} |
||||
m.currentSegment = nil |
||||
return err |
||||
} |
||||
|
||||
err = m.currentSegment.writeH264(now.Sub(m.startPCR), dts, |
||||
pts, idrPresent, enc) |
||||
if err != nil { |
||||
if m.currentSegment.buf.Len() > 0 { |
||||
m.streamPlaylist.pushSegment(m.currentSegment) |
||||
} |
||||
m.currentSegment = nil |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (m *muxerTSGenerator) writeAAC(pts time.Duration, aus [][]byte) error { |
||||
now := time.Now() |
||||
|
||||
if m.videoTrack == nil { |
||||
if m.currentSegment == nil { |
||||
// create first segment
|
||||
m.startPCR = now |
||||
m.currentSegment = newMuxerTSSegment(now, m.hlsSegmentMaxSize, |
||||
m.videoTrack, m.writer.WriteData) |
||||
m.startPTS = pts |
||||
pts = 0 |
||||
} else { |
||||
pts -= m.startPTS |
||||
|
||||
// switch segment
|
||||
if m.currentSegment.audioAUCount >= segmentMinAUCount && |
||||
m.currentSegment.startPTS != nil && |
||||
(pts-*m.currentSegment.startPTS) >= m.hlsSegmentDuration { |
||||
m.currentSegment.endPTS = pts |
||||
m.streamPlaylist.pushSegment(m.currentSegment) |
||||
m.currentSegment = newMuxerTSSegment(now, m.hlsSegmentMaxSize, |
||||
m.videoTrack, m.writer.WriteData) |
||||
} |
||||
} |
||||
} else { |
||||
// wait for the video track
|
||||
if m.currentSegment == nil { |
||||
return nil |
||||
} |
||||
|
||||
pts -= m.startPTS |
||||
} |
||||
|
||||
pkts := make([]*aac.ADTSPacket, len(aus)) |
||||
|
||||
for i, au := range aus { |
||||
pkts[i] = &aac.ADTSPacket{ |
||||
Type: m.audioTrack.Type(), |
||||
SampleRate: m.audioTrack.ClockRate(), |
||||
ChannelCount: m.audioTrack.ChannelCount(), |
||||
AU: au, |
||||
} |
||||
} |
||||
|
||||
enc, err := aac.EncodeADTS(pkts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = m.currentSegment.writeAAC(now.Sub(m.startPCR), pts, enc, len(aus)) |
||||
if err != nil { |
||||
if m.currentSegment.buf.Len() > 0 { |
||||
m.streamPlaylist.pushSegment(m.currentSegment) |
||||
} |
||||
m.currentSegment = nil |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"time" |
||||
) |
||||
|
||||
// MuxerVariant is a muxer variant.
|
||||
type MuxerVariant int |
||||
|
||||
// supported variants.
|
||||
const ( |
||||
MuxerVariantMPEGTS MuxerVariant = iota |
||||
MuxerVariantFMP4 |
||||
MuxerVariantLowLatency |
||||
) |
||||
|
||||
type muxerVariant interface { |
||||
close() |
||||
writeH264(pts time.Duration, nalus [][]byte) error |
||||
writeAAC(pts time.Duration, aus [][]byte) error |
||||
file(name string, msn string, part string, skip string) *MuxerFileResponse |
||||
} |
@ -0,0 +1,247 @@
@@ -0,0 +1,247 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"math" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
"github.com/icza/bitio" |
||||
) |
||||
|
||||
const ( |
||||
fmp4VideoTimescale = 90000 |
||||
) |
||||
|
||||
func readGolombUnsigned(br *bitio.Reader) (uint32, error) { |
||||
leadingZeroBits := uint32(0) |
||||
|
||||
for { |
||||
b, err := br.ReadBits(1) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
if b != 0 { |
||||
break |
||||
} |
||||
|
||||
leadingZeroBits++ |
||||
} |
||||
|
||||
codeNum := uint32(0) |
||||
|
||||
for n := leadingZeroBits; n > 0; n-- { |
||||
b, err := br.ReadBits(1) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
codeNum |= uint32(b) << (n - 1) |
||||
} |
||||
|
||||
codeNum = (1 << leadingZeroBits) - 1 + codeNum |
||||
|
||||
return codeNum, nil |
||||
} |
||||
|
||||
func getPOC(buf []byte, sps *h264.SPS) (uint32, error) { |
||||
buf = h264.AntiCompetitionRemove(buf[:10]) |
||||
|
||||
isIDR := h264.NALUType(buf[0]&0x1F) == h264.NALUTypeIDR |
||||
|
||||
r := bytes.NewReader(buf[1:]) |
||||
br := bitio.NewReader(r) |
||||
|
||||
// first_mb_in_slice
|
||||
_, err := readGolombUnsigned(br) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
// slice_type
|
||||
_, err = readGolombUnsigned(br) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
// pic_parameter_set_id
|
||||
_, err = readGolombUnsigned(br) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
// frame_num
|
||||
_, err = br.ReadBits(uint8(sps.Log2MaxFrameNumMinus4 + 4)) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
if !sps.FrameMbsOnlyFlag { |
||||
return 0, fmt.Errorf("unsupported") |
||||
} |
||||
|
||||
if isIDR { |
||||
// idr_pic_id
|
||||
_, err := readGolombUnsigned(br) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
} |
||||
|
||||
var picOrderCntLsb uint64 |
||||
switch { |
||||
case sps.PicOrderCntType == 0: |
||||
picOrderCntLsb, err = br.ReadBits(uint8(sps.Log2MaxPicOrderCntLsbMinus4 + 4)) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
|
||||
default: |
||||
return 0, fmt.Errorf("pic_order_cnt_type = 1 is unsupported") |
||||
} |
||||
|
||||
return uint32(picOrderCntLsb), nil |
||||
} |
||||
|
||||
func getNALUSPOC(nalus [][]byte, sps *h264.SPS) (uint32, error) { |
||||
for _, nalu := range nalus { |
||||
typ := h264.NALUType(nalu[0] & 0x1F) |
||||
if typ == h264.NALUTypeIDR || typ == h264.NALUTypeNonIDR { |
||||
poc, err := getPOC(nalu, sps) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
return poc, nil |
||||
} |
||||
} |
||||
return 0, fmt.Errorf("POC not found") |
||||
} |
||||
|
||||
func getPOCDiff(poc uint32, expectedPOC uint32, sps *h264.SPS) int32 { |
||||
diff := int32(poc) - int32(expectedPOC) |
||||
switch { |
||||
case diff < -((1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 3)) - 1): |
||||
diff += (1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 4)) |
||||
|
||||
case diff > ((1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 3)) - 1): |
||||
diff -= (1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 4)) |
||||
} |
||||
return diff |
||||
} |
||||
|
||||
type fmp4VideoSample struct { |
||||
pts time.Duration |
||||
dts time.Duration |
||||
nalus [][]byte |
||||
avcc []byte |
||||
idrPresent bool |
||||
next *fmp4VideoSample |
||||
pocDiff int32 |
||||
} |
||||
|
||||
func (s *fmp4VideoSample) fillDTS( |
||||
prev *fmp4VideoSample, |
||||
sps *h264.SPS, |
||||
expectedPOC *uint32, |
||||
) error { |
||||
if s.idrPresent || sps.PicOrderCntType == 2 { |
||||
s.dts = s.pts |
||||
*expectedPOC = 0 |
||||
} else { |
||||
*expectedPOC += 2 |
||||
*expectedPOC &= ((1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 4)) - 1) |
||||
|
||||
poc, err := getNALUSPOC(s.nalus, sps) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.pocDiff = getPOCDiff(poc, *expectedPOC, sps) |
||||
|
||||
if s.pocDiff == 0 { |
||||
s.dts = s.pts |
||||
} else { |
||||
if prev.pocDiff == 0 { |
||||
if s.pocDiff == -2 { |
||||
return fmt.Errorf("invalid frame POC") |
||||
} |
||||
s.dts = prev.pts + time.Duration(math.Round(float64(s.pts-prev.pts)/float64(s.pocDiff/2+1))) |
||||
} else { |
||||
s.dts = s.pts + time.Duration(math.Round(float64(prev.dts-prev.pts)*float64(s.pocDiff)/float64(prev.pocDiff))) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s fmp4VideoSample) duration() time.Duration { |
||||
return s.next.dts - s.dts |
||||
} |
||||
|
||||
type fmp4AudioSample struct { |
||||
pts time.Duration |
||||
au []byte |
||||
next *fmp4AudioSample |
||||
} |
||||
|
||||
func (s fmp4AudioSample) duration() time.Duration { |
||||
return s.next.pts - s.pts |
||||
} |
||||
|
||||
type muxerVariantFMP4 struct { |
||||
playlist *muxerVariantFMP4Playlist |
||||
segmenter *muxerVariantFMP4Segmenter |
||||
} |
||||
|
||||
func newMuxerVariantFMP4( |
||||
lowLatency bool, |
||||
segmentCount int, |
||||
segmentDuration time.Duration, |
||||
partDuration time.Duration, |
||||
segmentMaxSize uint64, |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
) *muxerVariantFMP4 { |
||||
v := &muxerVariantFMP4{} |
||||
|
||||
v.playlist = newMuxerVariantFMP4Playlist( |
||||
lowLatency, |
||||
segmentCount, |
||||
videoTrack, |
||||
audioTrack, |
||||
) |
||||
|
||||
v.segmenter = newMuxerVariantFMP4Segmenter( |
||||
lowLatency, |
||||
segmentCount, |
||||
segmentDuration, |
||||
partDuration, |
||||
segmentMaxSize, |
||||
videoTrack, |
||||
audioTrack, |
||||
v.playlist.onSegmentFinalized, |
||||
v.playlist.onPartFinalized, |
||||
) |
||||
|
||||
return v |
||||
} |
||||
|
||||
func (v *muxerVariantFMP4) close() { |
||||
v.playlist.close() |
||||
} |
||||
|
||||
func (v *muxerVariantFMP4) writeH264(pts time.Duration, nalus [][]byte) error { |
||||
return v.segmenter.writeH264(pts, nalus) |
||||
} |
||||
|
||||
func (v *muxerVariantFMP4) writeAAC(pts time.Duration, aus [][]byte) error { |
||||
return v.segmenter.writeAAC(pts, aus) |
||||
} |
||||
|
||||
func (v *muxerVariantFMP4) file(name string, msn string, part string, skip string) *MuxerFileResponse { |
||||
return v.playlist.file(name, msn, part, skip) |
||||
} |
@ -0,0 +1,621 @@
@@ -0,0 +1,621 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/aac" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
type myEsds struct { |
||||
mp4.FullBox `mp4:"0,extend"` |
||||
Data []byte `mp4:"1,size=8"` |
||||
} |
||||
|
||||
func (*myEsds) GetType() mp4.BoxType { |
||||
return mp4.StrToBoxType("esds") |
||||
} |
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
mp4.AddBoxDef(&myEsds{}, 0) |
||||
} |
||||
|
||||
func mp4InitGenerateVideoTrack(w *mp4Writer, trackID int, videoTrack *gortsplib.TrackH264) error { |
||||
/* |
||||
trak |
||||
- tkhd |
||||
- mdia |
||||
- mdhd |
||||
- hdlr |
||||
- minf |
||||
- vmhd |
||||
- dinf |
||||
- dref |
||||
- url |
||||
- stbl |
||||
- stsd |
||||
- avc1 |
||||
- avcC |
||||
- pasp |
||||
- btrt |
||||
- stts |
||||
- stsc |
||||
- stsz |
||||
- stco |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&mp4.Trak{}) // <trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sps := videoTrack.SPS() |
||||
pps := videoTrack.PPS() |
||||
|
||||
var spsp h264.SPS |
||||
err = spsp.Unmarshal(sps) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
width := spsp.Width() |
||||
height := spsp.Height() |
||||
|
||||
_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
Width: uint32(width * 65536), |
||||
Height: uint32(height * 65536), |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Mdia{}) // <mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Mdhd{ // <mdhd/>
|
||||
Timescale: fmp4VideoTimescale, // the number of time units that pass per second
|
||||
Language: [3]byte{'u', 'n', 'd'}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'v', 'i', 'd', 'e'}, |
||||
Name: "VideoHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Minf{}) // <minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Vmhd{ // <vmhd/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Dinf{}) // <dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Dref{ // <dref>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Url{ // <url/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dref>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Stbl{}) // <stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Stsd{ // <stsd>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // <avc1>
|
||||
SampleEntry: mp4.SampleEntry{ |
||||
AnyTypeBox: mp4.AnyTypeBox{ |
||||
Type: mp4.BoxTypeAvc1(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
Width: uint16(width), |
||||
Height: uint16(height), |
||||
Horizresolution: 4718592, |
||||
Vertresolution: 4718592, |
||||
FrameCount: 1, |
||||
Depth: 24, |
||||
PreDefined3: -1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.AVCDecoderConfiguration{ // <avcc/>
|
||||
AnyTypeBox: mp4.AnyTypeBox{ |
||||
Type: mp4.BoxTypeAvcC(), |
||||
}, |
||||
ConfigurationVersion: 1, |
||||
Profile: spsp.ProfileIdc, |
||||
ProfileCompatibility: sps[2], |
||||
Level: spsp.LevelIdc, |
||||
LengthSizeMinusOne: 3, |
||||
NumOfSequenceParameterSets: 1, |
||||
SequenceParameterSets: []mp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(sps)), |
||||
NALUnit: sps, |
||||
}, |
||||
}, |
||||
NumOfPictureParameterSets: 1, |
||||
PictureParameterSets: []mp4.AVCParameterSet{ |
||||
{ |
||||
Length: uint16(len(pps)), |
||||
NALUnit: pps, |
||||
}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 1000000, |
||||
AvgBitrate: 1000000, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </avc1>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stsd>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stts{ // <stts>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stsc{ // <stsc>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stsz{ // <stsz>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stco{ // <stco>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func mp4InitGenerateAudioTrack(w *mp4Writer, trackID int, audioTrack *gortsplib.TrackAAC) error { |
||||
/* |
||||
trak |
||||
- tkhd |
||||
- mdia |
||||
- mdhd |
||||
- hdlr |
||||
- minf |
||||
- smhd |
||||
- dinf |
||||
- dref |
||||
- url |
||||
- stbl |
||||
- stsd |
||||
- mp4a |
||||
- esds |
||||
- btrt |
||||
- stts |
||||
- stsc |
||||
- stsz |
||||
- stco |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&mp4.Trak{}) // <trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 3}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
AlternateGroup: 1, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Mdia{}) // <mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Mdhd{ // <mdhd/>
|
||||
Timescale: uint32(audioTrack.ClockRate()), |
||||
Language: [3]byte{'u', 'n', 'd'}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>
|
||||
HandlerType: [4]byte{'s', 'o', 'u', 'n'}, |
||||
Name: "SoundHandler", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Minf{}) // <minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Smhd{ // <smhd/>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Dinf{}) // <dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Dref{ // <dref>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Url{ // <url/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{0, 0, 1}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dref>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </dinf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Stbl{}) // <stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Stsd{ // <stsd>
|
||||
EntryCount: 1, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // <mp4a>
|
||||
SampleEntry: mp4.SampleEntry{ |
||||
AnyTypeBox: mp4.AnyTypeBox{ |
||||
Type: mp4.BoxTypeMp4a(), |
||||
}, |
||||
DataReferenceIndex: 1, |
||||
}, |
||||
ChannelCount: uint16(audioTrack.ChannelCount()), |
||||
SampleSize: 16, |
||||
SampleRate: uint32(audioTrack.ClockRate() * 65536), |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
c := aac.MPEG4AudioConfig{ |
||||
Type: aac.MPEG4AudioType(audioTrack.Type()), |
||||
SampleRate: audioTrack.ClockRate(), |
||||
ChannelCount: audioTrack.ChannelCount(), |
||||
AOTSpecificConfig: audioTrack.AOTSpecificConfig(), |
||||
} |
||||
conf, _ := c.Encode() |
||||
|
||||
decSpecificInfoTagSize := uint8(len(conf)) |
||||
decSpecificInfoTag := append( |
||||
[]byte{ |
||||
mp4.DecSpecificInfoTag, |
||||
0x80, 0x80, 0x80, decSpecificInfoTagSize, // size
|
||||
}, |
||||
conf..., |
||||
) |
||||
|
||||
esDescrTag := []byte{ |
||||
mp4.ESDescrTag, |
||||
0x80, 0x80, 0x80, 32 + decSpecificInfoTagSize, // size
|
||||
0x00, |
||||
byte(trackID), // ES_ID
|
||||
0x00, |
||||
} |
||||
|
||||
decoderConfigDescrTag := []byte{ |
||||
mp4.DecoderConfigDescrTag, |
||||
0x80, 0x80, 0x80, 18 + decSpecificInfoTagSize, // size
|
||||
0x40, // object type indicator (MPEG-4 Audio)
|
||||
0x15, 0x00, |
||||
0x00, 0x00, 0x00, 0x01, |
||||
0xf7, 0x39, 0x00, 0x01, |
||||
0xf7, 0x39, |
||||
} |
||||
|
||||
slConfigDescrTag := []byte{ |
||||
mp4.SLConfigDescrTag, |
||||
0x80, 0x80, 0x80, 0x01, // size (1)
|
||||
0x02, |
||||
} |
||||
|
||||
data := make([]byte, len(esDescrTag)+len(decoderConfigDescrTag)+len(decSpecificInfoTag)+len(slConfigDescrTag)) |
||||
pos := 0 |
||||
|
||||
pos += copy(data[pos:], esDescrTag) |
||||
pos += copy(data[pos:], decoderConfigDescrTag) |
||||
pos += copy(data[pos:], decSpecificInfoTag) |
||||
copy(data[pos:], slConfigDescrTag) |
||||
|
||||
_, err = w.writeBox(&myEsds{ // <esds/>
|
||||
Data: data, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Btrt{ // <btrt/>
|
||||
MaxBitrate: 128825, |
||||
AvgBitrate: 128825, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mp4a>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stsd>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stts{ // <stts>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stsc{ // <stsc>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stsz{ // <stsz>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Stco{ // <stco>
|
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </stbl>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </minf>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mdia>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </trak>
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func mp4InitGenerate(videoTrack *gortsplib.TrackH264, audioTrack *gortsplib.TrackAAC) ([]byte, error) { |
||||
/* |
||||
- ftyp |
||||
- moov |
||||
- mvhd |
||||
- trak (video) |
||||
- trak (audio) |
||||
- mvex |
||||
- trex (video) |
||||
- trex (audio) |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
_, err := w.writeBox(&mp4.Ftyp{ // <ftyp/>
|
||||
MajorBrand: [4]byte{'m', 'p', '4', '2'}, |
||||
MinorVersion: 1, |
||||
CompatibleBrands: []mp4.CompatibleBrandElem{ |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}}, |
||||
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, |
||||
{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}}, |
||||
{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Moov{}) // <moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Mvhd{ // <mvhd/>
|
||||
Timescale: 1000, |
||||
Rate: 65536, |
||||
Volume: 256, |
||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000}, |
||||
NextTrackID: 2, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID := 1 |
||||
|
||||
if videoTrack != nil { |
||||
err := mp4InitGenerateVideoTrack(w, trackID, videoTrack) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
err := mp4InitGenerateAudioTrack(w, trackID, audioTrack) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
_, err = w.writeBoxStart(&mp4.Mvex{}) // <mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID = 1 |
||||
|
||||
if videoTrack != nil { |
||||
_, err = w.writeBox(&mp4.Trex{ // <trex/>
|
||||
TrackID: uint32(trackID), |
||||
DefaultSampleDescriptionIndex: 1, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
_, err = w.writeBox(&mp4.Trex{ // <trex/>
|
||||
TrackID: uint32(trackID), |
||||
DefaultSampleDescriptionIndex: 1, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </mvex>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moov>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
@ -0,0 +1,390 @@
@@ -0,0 +1,390 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"math" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/abema/go-mp4" |
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/aac" |
||||
) |
||||
|
||||
func durationGoToMp4(v time.Duration, timescale time.Duration) int64 { |
||||
return int64(math.Round(float64(v*timescale) / float64(time.Second))) |
||||
} |
||||
|
||||
func mp4PartGenerateVideoTraf( |
||||
w *mp4Writer, |
||||
trackID int, |
||||
videoSamples []*fmp4VideoSample, |
||||
startDTS time.Duration, |
||||
) (*mp4.Trun, int, error) { |
||||
/* |
||||
traf |
||||
- tfhd |
||||
- tfdt |
||||
- trun |
||||
*/ |
||||
|
||||
_, err := w.writeBoxStart(&mp4.Traf{}) // <traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags := 0 |
||||
|
||||
_, err = w.writeBox(&mp4.Tfhd{ // <tfhd/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Tfdt{ // <tfdt/>
|
||||
FullBox: mp4.FullBox{ |
||||
Version: 1, |
||||
}, |
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(startDTS, fmp4VideoTimescale)), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags = 0 |
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
flags |= 0x400 // sample flags present
|
||||
flags |= 0x800 // sample composition time offset present or v1
|
||||
|
||||
trun := &mp4.Trun{ // <trun/>
|
||||
FullBox: mp4.FullBox{ |
||||
Version: 1, |
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
SampleCount: uint32(len(videoSamples)), |
||||
} |
||||
|
||||
for _, e := range videoSamples { |
||||
off := e.pts - e.dts |
||||
|
||||
flags := uint32(0) |
||||
if !e.idrPresent { |
||||
flags |= 1 << 16 // sample_is_non_sync_sample
|
||||
} |
||||
|
||||
trun.Entries = append(trun.Entries, mp4.TrunEntry{ |
||||
SampleDuration: uint32(durationGoToMp4(e.duration(), fmp4VideoTimescale)), |
||||
SampleSize: uint32(len(e.avcc)), |
||||
SampleFlags: flags, |
||||
SampleCompositionTimeOffsetV1: int32(durationGoToMp4(off, fmp4VideoTimescale)), |
||||
}) |
||||
} |
||||
|
||||
trunOffset, err := w.writeBox(trun) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return trun, trunOffset, nil |
||||
} |
||||
|
||||
func mp4PartGenerateAudioTraf( |
||||
w *mp4Writer, |
||||
trackID int, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
audioSamples []*fmp4AudioSample, |
||||
) (*mp4.Trun, int, error) { |
||||
/* |
||||
traf |
||||
- tfhd |
||||
- tfdt |
||||
- trun |
||||
*/ |
||||
|
||||
if len(audioSamples) == 0 { |
||||
return nil, 0, nil |
||||
} |
||||
|
||||
_, err := w.writeBoxStart(&mp4.Traf{}) // <traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags := 0 |
||||
|
||||
_, err = w.writeBox(&mp4.Tfhd{ // <tfhd/>
|
||||
FullBox: mp4.FullBox{ |
||||
Flags: [3]byte{2, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
TrackID: uint32(trackID), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Tfdt{ // <tfdt/>
|
||||
FullBox: mp4.FullBox{ |
||||
Version: 1, |
||||
}, |
||||
// sum of decode durations of all earlier samples
|
||||
BaseMediaDecodeTimeV1: uint64(durationGoToMp4(audioSamples[0].pts, time.Duration(audioTrack.ClockRate()))), |
||||
}) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
flags = 0 |
||||
flags |= 0x01 // data offset present
|
||||
flags |= 0x100 // sample duration present
|
||||
flags |= 0x200 // sample size present
|
||||
|
||||
trun := &mp4.Trun{ // <trun/>
|
||||
FullBox: mp4.FullBox{ |
||||
Version: 0, |
||||
Flags: [3]byte{0, byte(flags >> 8), byte(flags)}, |
||||
}, |
||||
SampleCount: uint32(len(audioSamples)), |
||||
} |
||||
|
||||
for _, e := range audioSamples { |
||||
trun.Entries = append(trun.Entries, mp4.TrunEntry{ |
||||
SampleDuration: uint32(durationGoToMp4(e.duration(), time.Duration(audioTrack.ClockRate()))), |
||||
SampleSize: uint32(len(e.au)), |
||||
}) |
||||
} |
||||
|
||||
trunOffset, err := w.writeBox(trun) |
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </traf>
|
||||
if err != nil { |
||||
return nil, 0, err |
||||
} |
||||
|
||||
return trun, trunOffset, nil |
||||
} |
||||
|
||||
func mp4PartGenerate( |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
videoSamples []*fmp4VideoSample, |
||||
audioSamples []*fmp4AudioSample, |
||||
startDTS time.Duration, |
||||
) ([]byte, error) { |
||||
/* |
||||
moof |
||||
- mfhd |
||||
- traf (video) |
||||
- traf (audio) |
||||
mdat |
||||
*/ |
||||
|
||||
w := newMP4Writer() |
||||
|
||||
moofOffset, err := w.writeBoxStart(&mp4.Moof{}) // <moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
_, err = w.writeBox(&mp4.Mfhd{ // <mfhd/>
|
||||
SequenceNumber: 0, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID := 1 |
||||
|
||||
var videoTrun *mp4.Trun |
||||
var videoTrunOffset int |
||||
if videoTrack != nil { |
||||
var err error |
||||
videoTrun, videoTrunOffset, err = mp4PartGenerateVideoTraf( |
||||
w, trackID, videoSamples, startDTS) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
trackID++ |
||||
} |
||||
|
||||
var audioTrun *mp4.Trun |
||||
var audioTrunOffset int |
||||
if audioTrack != nil { |
||||
var err error |
||||
audioTrun, audioTrunOffset, err = mp4PartGenerateAudioTraf(w, trackID, audioTrack, audioSamples) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
err = w.writeBoxEnd() // </moof>
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
mdat := &mp4.Mdat{} // <mdat/>
|
||||
|
||||
dataSize := 0 |
||||
videoDataSize := 0 |
||||
|
||||
if videoTrack != nil { |
||||
for _, e := range videoSamples { |
||||
dataSize += len(e.avcc) |
||||
} |
||||
videoDataSize = dataSize |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
for _, e := range audioSamples { |
||||
dataSize += len(e.au) |
||||
} |
||||
} |
||||
|
||||
mdat.Data = make([]byte, dataSize) |
||||
pos := 0 |
||||
|
||||
if videoTrack != nil { |
||||
for _, e := range videoSamples { |
||||
pos += copy(mdat.Data[pos:], e.avcc) |
||||
} |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
for _, e := range audioSamples { |
||||
pos += copy(mdat.Data[pos:], e.au) |
||||
} |
||||
} |
||||
|
||||
mdatOffset, err := w.writeBox(mdat) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
videoTrun.DataOffset = int32(mdatOffset - moofOffset + 8) |
||||
err = w.rewriteBox(videoTrunOffset, videoTrun) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if audioTrack != nil && audioTrun != nil { |
||||
audioTrun.DataOffset = int32(videoDataSize + mdatOffset - moofOffset + 8) |
||||
err = w.rewriteBox(audioTrunOffset, audioTrun) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return w.bytes(), nil |
||||
} |
||||
|
||||
func fmp4PartName(id uint64) string { |
||||
return "part" + strconv.FormatUint(id, 10) |
||||
} |
||||
|
||||
type muxerVariantFMP4Part struct { |
||||
videoTrack *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
id uint64 |
||||
startDTS time.Duration |
||||
|
||||
isIndependent bool |
||||
videoSamples []*fmp4VideoSample |
||||
audioSamples []*fmp4AudioSample |
||||
renderedContent []byte |
||||
renderedDuration time.Duration |
||||
} |
||||
|
||||
func newMuxerVariantFMP4Part( |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
id uint64, |
||||
startDTS time.Duration, |
||||
) *muxerVariantFMP4Part { |
||||
p := &muxerVariantFMP4Part{ |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
id: id, |
||||
startDTS: startDTS, |
||||
} |
||||
|
||||
if videoTrack == nil { |
||||
p.isIndependent = true |
||||
} |
||||
|
||||
return p |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) name() string { |
||||
return fmp4PartName(p.id) |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) reader() io.Reader { |
||||
return bytes.NewReader(p.renderedContent) |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) duration() time.Duration { |
||||
if p.videoTrack != nil { |
||||
ret := time.Duration(0) |
||||
for _, e := range p.videoSamples { |
||||
ret += e.duration() |
||||
} |
||||
return ret |
||||
} |
||||
|
||||
// use the sum of the default duration of all samples,
|
||||
// not the real duration,
|
||||
// otherwise on iPhone iOS the stream freezes.
|
||||
return time.Duration(len(p.audioSamples)) * time.Second * |
||||
time.Duration(aac.SamplesPerAccessUnit) / time.Duration(p.audioTrack.ClockRate()) |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) finalize() error { |
||||
if len(p.videoSamples) > 0 || len(p.audioSamples) > 0 { |
||||
var err error |
||||
p.renderedContent, err = mp4PartGenerate( |
||||
p.videoTrack, |
||||
p.audioTrack, |
||||
p.videoSamples, |
||||
p.audioSamples, |
||||
p.startDTS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
p.renderedDuration = p.duration() |
||||
} |
||||
|
||||
p.videoSamples = nil |
||||
p.audioSamples = nil |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) writeH264(sample *fmp4VideoSample) { |
||||
if sample.idrPresent { |
||||
p.isIndependent = true |
||||
} |
||||
p.videoSamples = append(p.videoSamples, sample) |
||||
} |
||||
|
||||
func (p *muxerVariantFMP4Part) writeAAC(sample *fmp4AudioSample) { |
||||
p.audioSamples = append(p.audioSamples, sample) |
||||
} |
@ -0,0 +1,503 @@
@@ -0,0 +1,503 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"math" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
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 *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
|
||||
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 *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
) *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 { |
||||
return len(p.segments) > 0 |
||||
} |
||||
|
||||
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": `audio/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": `audio/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" |
||||
} |
||||
|
||||
cnt += "\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 fname == "init.mp4": |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
|
||||
byts, err := mp4InitGenerate(p.videoTrack, p.audioTrack) |
||||
if err != nil { |
||||
return &MuxerFileResponse{Status: http.StatusInternalServerError} |
||||
} |
||||
|
||||
return &MuxerFileResponse{ |
||||
Status: http.StatusOK, |
||||
Header: map[string]string{ |
||||
"Content-Type": "video/mp4", |
||||
}, |
||||
Body: bytes.NewReader(byts), |
||||
} |
||||
|
||||
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() |
||||
|
||||
// create initial gap
|
||||
if len(p.segments) == 0 { |
||||
for i := 0; i < p.segmentCount; 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() |
||||
} |
@ -0,0 +1,196 @@
@@ -0,0 +1,196 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"strconv" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
type partsReader struct { |
||||
parts []*muxerVariantFMP4Part |
||||
curPart int |
||||
curPos int |
||||
} |
||||
|
||||
func (mbr *partsReader) Read(p []byte) (int, error) { |
||||
n := 0 |
||||
lenp := len(p) |
||||
|
||||
for { |
||||
if mbr.curPart >= len(mbr.parts) { |
||||
return n, io.EOF |
||||
} |
||||
|
||||
copied := copy(p[n:], mbr.parts[mbr.curPart].renderedContent[mbr.curPos:]) |
||||
mbr.curPos += copied |
||||
n += copied |
||||
|
||||
if mbr.curPos == len(mbr.parts[mbr.curPart].renderedContent) { |
||||
mbr.curPart++ |
||||
mbr.curPos = 0 |
||||
} |
||||
|
||||
if n == lenp { |
||||
return n, nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
type muxerVariantFMP4Segment struct { |
||||
lowLatency bool |
||||
id uint64 |
||||
startTime time.Time |
||||
startDTS time.Duration |
||||
segmentMaxSize uint64 |
||||
videoTrack *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
genPartID func() uint64 |
||||
onPartFinalized func(*muxerVariantFMP4Part) |
||||
|
||||
size uint64 |
||||
parts []*muxerVariantFMP4Part |
||||
currentPart *muxerVariantFMP4Part |
||||
renderedDuration time.Duration |
||||
} |
||||
|
||||
func newMuxerVariantFMP4Segment( |
||||
lowLatency bool, |
||||
id uint64, |
||||
startTime time.Time, |
||||
startDTS time.Duration, |
||||
segmentMaxSize uint64, |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
genPartID func() uint64, |
||||
onPartFinalized func(*muxerVariantFMP4Part), |
||||
) *muxerVariantFMP4Segment { |
||||
s := &muxerVariantFMP4Segment{ |
||||
lowLatency: lowLatency, |
||||
id: id, |
||||
startTime: startTime, |
||||
startDTS: startDTS, |
||||
segmentMaxSize: segmentMaxSize, |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
genPartID: genPartID, |
||||
onPartFinalized: onPartFinalized, |
||||
} |
||||
|
||||
s.currentPart = newMuxerVariantFMP4Part( |
||||
s.videoTrack, |
||||
s.audioTrack, |
||||
s.genPartID(), |
||||
s.startDTS, |
||||
) |
||||
|
||||
return s |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) name() string { |
||||
return "seg" + strconv.FormatUint(s.id, 10) |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) reader() io.Reader { |
||||
return &partsReader{parts: s.parts} |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) getRenderedDuration() time.Duration { |
||||
return s.renderedDuration |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) finalize( |
||||
nextVideoSample *fmp4VideoSample, |
||||
nextAudioSample *fmp4AudioSample, |
||||
) error { |
||||
err := s.currentPart.finalize() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if s.currentPart.renderedContent != nil { |
||||
s.onPartFinalized(s.currentPart) |
||||
s.parts = append(s.parts, s.currentPart) |
||||
} |
||||
|
||||
s.currentPart = nil |
||||
|
||||
if s.videoTrack != nil { |
||||
s.renderedDuration = nextVideoSample.pts - s.startDTS |
||||
} else { |
||||
s.renderedDuration = 0 |
||||
for _, pa := range s.parts { |
||||
s.renderedDuration += pa.renderedDuration |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) writeH264(sample *fmp4VideoSample, adjustedPartDuration time.Duration) error { |
||||
size := uint64(len(sample.avcc)) |
||||
|
||||
if (s.size + size) > s.segmentMaxSize { |
||||
return fmt.Errorf("reached maximum segment size") |
||||
} |
||||
|
||||
s.currentPart.writeH264(sample) |
||||
|
||||
s.size += size |
||||
|
||||
// switch part
|
||||
if s.lowLatency && |
||||
s.currentPart.duration() >= adjustedPartDuration { |
||||
err := s.currentPart.finalize() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.parts = append(s.parts, s.currentPart) |
||||
s.onPartFinalized(s.currentPart) |
||||
|
||||
s.currentPart = newMuxerVariantFMP4Part( |
||||
s.videoTrack, |
||||
s.audioTrack, |
||||
s.genPartID(), |
||||
sample.next.dts, |
||||
) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s *muxerVariantFMP4Segment) writeAAC(sample *fmp4AudioSample, adjustedPartDuration time.Duration) error { |
||||
size := uint64(len(sample.au)) |
||||
|
||||
if (s.size + size) > s.segmentMaxSize { |
||||
return fmt.Errorf("reached maximum segment size") |
||||
} |
||||
|
||||
s.currentPart.writeAAC(sample) |
||||
|
||||
s.size += size |
||||
|
||||
// switch part
|
||||
if s.lowLatency && s.videoTrack == nil && |
||||
s.currentPart.duration() >= adjustedPartDuration { |
||||
err := s.currentPart.finalize() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.parts = append(s.parts, s.currentPart) |
||||
s.onPartFinalized(s.currentPart) |
||||
|
||||
s.currentPart = newMuxerVariantFMP4Part( |
||||
s.videoTrack, |
||||
s.audioTrack, |
||||
s.genPartID(), |
||||
sample.next.pts, |
||||
) |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,344 @@
@@ -0,0 +1,344 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/aac" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
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 findCompatiblePartDuration( |
||||
minPartDuration time.Duration, |
||||
sampleDurations map[time.Duration]struct{}, |
||||
) time.Duration { |
||||
i := minPartDuration |
||||
for ; i < 5*time.Second; i += 5 * time.Millisecond { |
||||
isCompatible := func() bool { |
||||
for sd := range sampleDurations { |
||||
if !partDurationIsCompatible(i, sd) { |
||||
return false |
||||
} |
||||
} |
||||
return true |
||||
}() |
||||
if isCompatible { |
||||
break |
||||
} |
||||
} |
||||
return i |
||||
} |
||||
|
||||
type muxerVariantFMP4Segmenter struct { |
||||
lowLatency bool |
||||
segmentDuration time.Duration |
||||
partDuration time.Duration |
||||
segmentMaxSize uint64 |
||||
videoTrack *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
onSegmentFinalized func(*muxerVariantFMP4Segment) |
||||
onPartFinalized func(*muxerVariantFMP4Part) |
||||
|
||||
currentSegment *muxerVariantFMP4Segment |
||||
startPTS time.Duration |
||||
videoSPSP *h264.SPS |
||||
videoSPS []byte |
||||
videoPPS []byte |
||||
videoNextSPSP *h264.SPS |
||||
videoNextSPS []byte |
||||
videoNextPPS []byte |
||||
nextSegmentID uint64 |
||||
nextPartID uint64 |
||||
nextVideoSample *fmp4VideoSample |
||||
nextAudioSample *fmp4AudioSample |
||||
videoExpectedPOC uint32 |
||||
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 *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
onSegmentFinalized func(*muxerVariantFMP4Segment), |
||||
onPartFinalized func(*muxerVariantFMP4Part), |
||||
) *muxerVariantFMP4Segmenter { |
||||
return &muxerVariantFMP4Segmenter{ |
||||
lowLatency: lowLatency, |
||||
segmentDuration: segmentDuration, |
||||
partDuration: partDuration, |
||||
segmentMaxSize: segmentMaxSize, |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
onSegmentFinalized: onSegmentFinalized, |
||||
onPartFinalized: onPartFinalized, |
||||
nextSegmentID: uint64(segmentCount), |
||||
sampleDurations: make(map[time.Duration]struct{}), |
||||
} |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) genSegmentID() uint64 { |
||||
id := m.nextSegmentID |
||||
m.nextSegmentID++ |
||||
return id |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) genPartID() uint64 { |
||||
id := m.nextPartID |
||||
m.nextPartID++ |
||||
return id |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) adjustPartDuration(du time.Duration) { |
||||
if !m.lowLatency { |
||||
return |
||||
} |
||||
if m.firstSegmentFinalized { |
||||
return |
||||
} |
||||
|
||||
// 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
|
||||
if _, ok := m.sampleDurations[du]; !ok { |
||||
m.sampleDurations[du] = struct{}{} |
||||
m.adjustedPartDuration = findCompatiblePartDuration( |
||||
m.partDuration, |
||||
m.sampleDurations, |
||||
) |
||||
} |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeH264(pts time.Duration, nalus [][]byte) error { |
||||
avcc, err := h264.AVCCEncode(nalus) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
idrPresent := h264.IDRPresent(nalus) |
||||
|
||||
return m.writeH264Entry(&fmp4VideoSample{ |
||||
pts: pts, |
||||
nalus: nalus, |
||||
avcc: avcc, |
||||
idrPresent: idrPresent, |
||||
}) |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeH264Entry(sample *fmp4VideoSample) error { |
||||
// put SPS/PPS into a queue in order to sync them with the sample queue
|
||||
m.videoSPSP = m.videoNextSPSP |
||||
m.videoSPS = m.videoNextSPS |
||||
m.videoPPS = m.videoNextPPS |
||||
spsChanged := false |
||||
if sample.idrPresent { |
||||
videoNextSPS := m.videoTrack.SPS() |
||||
videoNextPPS := m.videoTrack.PPS() |
||||
|
||||
if m.videoSPS == nil || |
||||
!bytes.Equal(m.videoNextSPS, videoNextSPS) || |
||||
!bytes.Equal(m.videoNextPPS, videoNextPPS) { |
||||
spsChanged = true |
||||
|
||||
var videoSPSP h264.SPS |
||||
err := videoSPSP.Unmarshal(videoNextSPS) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
m.videoNextSPSP = &videoSPSP |
||||
m.videoNextSPS = videoNextSPS |
||||
m.videoNextPPS = videoNextPPS |
||||
} |
||||
} |
||||
|
||||
sample.pts -= m.startPTS |
||||
|
||||
// put samples into a queue in order to
|
||||
// - allow to compute sample dts
|
||||
// - allow to compute sample duration
|
||||
// - check if next sample is IDR
|
||||
sample, m.nextVideoSample = m.nextVideoSample, sample |
||||
if sample == nil { |
||||
return nil |
||||
} |
||||
sample.next = m.nextVideoSample |
||||
|
||||
now := time.Now() |
||||
|
||||
if m.currentSegment == nil { |
||||
// skip groups silently until we find one with a IDR
|
||||
if !sample.idrPresent { |
||||
return nil |
||||
} |
||||
|
||||
// create first segment
|
||||
m.currentSegment = newMuxerVariantFMP4Segment( |
||||
m.lowLatency, |
||||
m.genSegmentID(), |
||||
now, |
||||
0, |
||||
m.segmentMaxSize, |
||||
m.videoTrack, |
||||
m.audioTrack, |
||||
m.genPartID, |
||||
m.onPartFinalized, |
||||
) |
||||
|
||||
m.startPTS = sample.pts |
||||
sample.pts = 0 |
||||
sample.next.pts -= m.startPTS |
||||
} |
||||
|
||||
err := sample.next.fillDTS(sample, m.videoNextSPSP, &m.videoExpectedPOC) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
sample.next.nalus = nil |
||||
|
||||
m.adjustPartDuration(sample.duration()) |
||||
|
||||
err = m.currentSegment.writeH264(sample, m.adjustedPartDuration) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// switch segment
|
||||
if sample.next.idrPresent { |
||||
if (sample.next.pts-m.currentSegment.startDTS) >= m.segmentDuration || |
||||
spsChanged { |
||||
err := m.currentSegment.finalize(sample.next, nil) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
m.onSegmentFinalized(m.currentSegment) |
||||
|
||||
m.firstSegmentFinalized = true |
||||
|
||||
m.currentSegment = newMuxerVariantFMP4Segment( |
||||
m.lowLatency, |
||||
m.genSegmentID(), |
||||
now, |
||||
sample.next.pts, |
||||
m.segmentMaxSize, |
||||
m.videoTrack, |
||||
m.audioTrack, |
||||
m.genPartID, |
||||
m.onPartFinalized, |
||||
) |
||||
|
||||
// if SPS changed, reset adjusted part duration
|
||||
if spsChanged { |
||||
m.firstSegmentFinalized = false |
||||
m.sampleDurations = make(map[time.Duration]struct{}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeAAC(pts time.Duration, aus [][]byte) error { |
||||
for i, au := range aus { |
||||
err := m.writeAACEntry(&fmp4AudioSample{ |
||||
pts: pts + time.Duration(i)*aac.SamplesPerAccessUnit*time.Second/time.Duration(m.audioTrack.ClockRate()), |
||||
au: au, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (m *muxerVariantFMP4Segmenter) writeAACEntry(sample *fmp4AudioSample) error { |
||||
sample.pts -= m.startPTS |
||||
|
||||
// put samples into a queue in order to
|
||||
// allow to compute the sample duration
|
||||
sample, m.nextAudioSample = m.nextAudioSample, sample |
||||
if sample == nil { |
||||
return nil |
||||
} |
||||
sample.next = m.nextAudioSample |
||||
|
||||
now := time.Now() |
||||
|
||||
if m.videoTrack == nil { |
||||
if m.currentSegment == nil { |
||||
// create first segment
|
||||
m.currentSegment = newMuxerVariantFMP4Segment( |
||||
m.lowLatency, |
||||
m.genSegmentID(), |
||||
now, |
||||
0, |
||||
m.segmentMaxSize, |
||||
m.videoTrack, |
||||
m.audioTrack, |
||||
m.genPartID, |
||||
m.onPartFinalized, |
||||
) |
||||
|
||||
m.startPTS = sample.pts |
||||
sample.pts = 0 |
||||
sample.next.pts -= m.startPTS |
||||
} |
||||
} else { |
||||
// wait for the video track
|
||||
if m.currentSegment == nil { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
m.adjustPartDuration(sample.duration()) |
||||
|
||||
err := m.currentSegment.writeAAC(sample, m.adjustedPartDuration) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// switch segment
|
||||
if m.videoTrack == nil && |
||||
(sample.next.pts-m.currentSegment.startDTS) >= m.segmentDuration { |
||||
err := m.currentSegment.finalize(nil, sample.next) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
m.onSegmentFinalized(m.currentSegment) |
||||
|
||||
m.firstSegmentFinalized = true |
||||
|
||||
m.currentSegment = newMuxerVariantFMP4Segment( |
||||
m.lowLatency, |
||||
m.genSegmentID(), |
||||
now, |
||||
sample.next.pts, |
||||
m.segmentMaxSize, |
||||
m.videoTrack, |
||||
m.audioTrack, |
||||
m.genPartID, |
||||
m.onPartFinalized, |
||||
) |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
type muxerVariantMPEGTS struct { |
||||
playlist *muxerVariantMPEGTSPlaylist |
||||
segmenter *muxerVariantMPEGTSSegmenter |
||||
} |
||||
|
||||
func newMuxerVariantMPEGTS( |
||||
segmentCount int, |
||||
segmentDuration time.Duration, |
||||
segmentMaxSize uint64, |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
) *muxerVariantMPEGTS { |
||||
v := &muxerVariantMPEGTS{} |
||||
|
||||
v.playlist = newMuxerVariantMPEGTSPlaylist(segmentCount) |
||||
|
||||
v.segmenter = newMuxerVariantMPEGTSSegmenter( |
||||
segmentDuration, |
||||
segmentMaxSize, |
||||
videoTrack, |
||||
audioTrack, |
||||
func(seg *muxerVariantMPEGTSSegment) { |
||||
v.playlist.pushSegment(seg) |
||||
}, |
||||
) |
||||
|
||||
return v |
||||
} |
||||
|
||||
func (v *muxerVariantMPEGTS) close() { |
||||
v.playlist.close() |
||||
} |
||||
|
||||
func (v *muxerVariantMPEGTS) writeH264(pts time.Duration, nalus [][]byte) error { |
||||
return v.segmenter.writeH264(pts, nalus) |
||||
} |
||||
|
||||
func (v *muxerVariantMPEGTS) writeAAC(pts time.Duration, aus [][]byte) error { |
||||
return v.segmenter.writeAAC(pts, aus) |
||||
} |
||||
|
||||
func (v *muxerVariantMPEGTS) file(name string, msn string, part string, skip string) *MuxerFileResponse { |
||||
return v.playlist.file(name) |
||||
} |
@ -0,0 +1,146 @@
@@ -0,0 +1,146 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"math" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
type muxerVariantMPEGTSPlaylist struct { |
||||
segmentCount int |
||||
|
||||
mutex sync.Mutex |
||||
cond *sync.Cond |
||||
closed bool |
||||
segments []*muxerVariantMPEGTSSegment |
||||
segmentByName map[string]*muxerVariantMPEGTSSegment |
||||
segmentDeleteCount int |
||||
} |
||||
|
||||
func newMuxerVariantMPEGTSPlaylist(segmentCount int) *muxerVariantMPEGTSPlaylist { |
||||
p := &muxerVariantMPEGTSPlaylist{ |
||||
segmentCount: segmentCount, |
||||
segmentByName: make(map[string]*muxerVariantMPEGTSSegment), |
||||
} |
||||
p.cond = sync.NewCond(&p.mutex) |
||||
|
||||
return p |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) close() { |
||||
func() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.closed = true |
||||
}() |
||||
|
||||
p.cond.Broadcast() |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) file(name string) *MuxerFileResponse { |
||||
switch { |
||||
case name == "stream.m3u8": |
||||
return p.playlistReader() |
||||
|
||||
case strings.HasSuffix(name, ".ts"): |
||||
return p.segmentReader(name) |
||||
|
||||
default: |
||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
||||
} |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) playlist() io.Reader { |
||||
cnt := "#EXTM3U\n" |
||||
cnt += "#EXT-X-VERSION:3\n" |
||||
cnt += "#EXT-X-ALLOW-CACHE:NO\n" |
||||
|
||||
targetDuration := func() uint { |
||||
ret := uint(0) |
||||
|
||||
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
|
||||
for _, s := range p.segments { |
||||
v2 := uint(math.Round(s.duration().Seconds())) |
||||
if v2 > ret { |
||||
ret = v2 |
||||
} |
||||
} |
||||
|
||||
return ret |
||||
}() |
||||
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n" |
||||
|
||||
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n" |
||||
cnt += "\n" |
||||
|
||||
for _, s := range p.segments { |
||||
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + s.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" + |
||||
"#EXTINF:" + strconv.FormatFloat(s.duration().Seconds(), 'f', -1, 64) + ",\n" + |
||||
s.name + ".ts\n" |
||||
} |
||||
|
||||
return bytes.NewReader([]byte(cnt)) |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) playlistReader() *MuxerFileResponse { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
|
||||
if !p.closed && len(p.segments) == 0 { |
||||
p.cond.Wait() |
||||
} |
||||
|
||||
if p.closed { |
||||
return &MuxerFileResponse{Status: http.StatusInternalServerError} |
||||
} |
||||
|
||||
return &MuxerFileResponse{ |
||||
Status: http.StatusOK, |
||||
Header: map[string]string{ |
||||
"Content-Type": `audio/mpegURL`, |
||||
}, |
||||
Body: p.playlist(), |
||||
} |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) segmentReader(fname string) *MuxerFileResponse { |
||||
base := strings.TrimSuffix(fname, ".ts") |
||||
|
||||
p.mutex.Lock() |
||||
f, ok := p.segmentByName[base] |
||||
p.mutex.Unlock() |
||||
|
||||
if !ok { |
||||
return &MuxerFileResponse{Status: http.StatusNotFound} |
||||
} |
||||
|
||||
return &MuxerFileResponse{ |
||||
Status: http.StatusOK, |
||||
Header: map[string]string{ |
||||
"Content-Type": "video/MP2T", |
||||
}, |
||||
Body: f.reader(), |
||||
} |
||||
} |
||||
|
||||
func (p *muxerVariantMPEGTSPlaylist) pushSegment(t *muxerVariantMPEGTSSegment) { |
||||
func() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
|
||||
p.segmentByName[t.name] = t |
||||
p.segments = append(p.segments, t) |
||||
|
||||
if len(p.segments) > p.segmentCount { |
||||
delete(p.segmentByName, p.segments[0].name) |
||||
p.segments = p.segments[1:] |
||||
p.segmentDeleteCount++ |
||||
} |
||||
}() |
||||
|
||||
p.cond.Broadcast() |
||||
} |
@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
"github.com/asticode/go-astits" |
||||
) |
||||
|
||||
const ( |
||||
mpegtsSegmentMinAUCount = 100 |
||||
) |
||||
|
||||
type writerFunc func(p []byte) (int, error) |
||||
|
||||
func (f writerFunc) Write(p []byte) (int, error) { |
||||
return f(p) |
||||
} |
||||
|
||||
type muxerVariantMPEGTSSegmenter struct { |
||||
segmentDuration time.Duration |
||||
segmentMaxSize uint64 |
||||
videoTrack *gortsplib.TrackH264 |
||||
audioTrack *gortsplib.TrackAAC |
||||
onSegmentReady func(*muxerVariantMPEGTSSegment) |
||||
|
||||
writer *astits.Muxer |
||||
currentSegment *muxerVariantMPEGTSSegment |
||||
videoDTSEst *h264.DTSEstimator |
||||
startPCR time.Time |
||||
startPTS time.Duration |
||||
} |
||||
|
||||
func newMuxerVariantMPEGTSSegmenter( |
||||
segmentDuration time.Duration, |
||||
segmentMaxSize uint64, |
||||
videoTrack *gortsplib.TrackH264, |
||||
audioTrack *gortsplib.TrackAAC, |
||||
onSegmentReady func(*muxerVariantMPEGTSSegment), |
||||
) *muxerVariantMPEGTSSegmenter { |
||||
m := &muxerVariantMPEGTSSegmenter{ |
||||
segmentDuration: segmentDuration, |
||||
segmentMaxSize: segmentMaxSize, |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
onSegmentReady: onSegmentReady, |
||||
} |
||||
|
||||
m.writer = astits.NewMuxer( |
||||
context.Background(), |
||||
writerFunc(func(p []byte) (int, error) { |
||||
return m.currentSegment.write(p) |
||||
})) |
||||
|
||||
if videoTrack != nil { |
||||
m.writer.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 256, |
||||
StreamType: astits.StreamTypeH264Video, |
||||
}) |
||||
} |
||||
|
||||
if audioTrack != nil { |
||||
m.writer.AddElementaryStream(astits.PMTElementaryStream{ |
||||
ElementaryPID: 257, |
||||
StreamType: astits.StreamTypeAACAudio, |
||||
}) |
||||
} |
||||
|
||||
if videoTrack != nil { |
||||
m.writer.SetPCRPID(256) |
||||
} else { |
||||
m.writer.SetPCRPID(257) |
||||
} |
||||
|
||||
return m |
||||
} |
||||
|
||||
func (m *muxerVariantMPEGTSSegmenter) writeH264(pts time.Duration, nalus [][]byte) error { |
||||
now := time.Now() |
||||
idrPresent := h264.IDRPresent(nalus) |
||||
|
||||
if m.currentSegment == nil { |
||||
// skip groups silently until we find one with a IDR
|
||||
if !idrPresent { |
||||
return nil |
||||
} |
||||
|
||||
// create first segment
|
||||
m.currentSegment = newMuxerVariantMPEGTSSegment(now, m.segmentMaxSize, |
||||
m.videoTrack, m.audioTrack, m.writer.WriteData) |
||||
m.startPCR = now |
||||
m.videoDTSEst = h264.NewDTSEstimator() |
||||
m.startPTS = pts |
||||
pts = 0 |
||||
} else { |
||||
pts -= m.startPTS |
||||
|
||||
// switch segment
|
||||
if idrPresent && |
||||
m.currentSegment.startPTS != nil && |
||||
(pts-*m.currentSegment.startPTS) >= m.segmentDuration { |
||||
m.currentSegment.endPTS = pts |
||||
m.onSegmentReady(m.currentSegment) |
||||
m.currentSegment = newMuxerVariantMPEGTSSegment(now, m.segmentMaxSize, |
||||
m.videoTrack, m.audioTrack, m.writer.WriteData) |
||||
} |
||||
} |
||||
|
||||
dts := m.videoDTSEst.Feed(pts) |
||||
|
||||
err := m.currentSegment.writeH264(now.Sub(m.startPCR), dts, |
||||
pts, idrPresent, nalus) |
||||
if err != nil { |
||||
if m.currentSegment.buf.Len() > 0 { |
||||
m.onSegmentReady(m.currentSegment) |
||||
} |
||||
m.currentSegment = nil |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (m *muxerVariantMPEGTSSegmenter) writeAAC(pts time.Duration, aus [][]byte) error { |
||||
now := time.Now() |
||||
|
||||
if m.videoTrack == nil { |
||||
if m.currentSegment == nil { |
||||
// create first segment
|
||||
m.currentSegment = newMuxerVariantMPEGTSSegment(now, m.segmentMaxSize, |
||||
m.videoTrack, m.audioTrack, m.writer.WriteData) |
||||
m.startPCR = now |
||||
m.startPTS = pts |
||||
pts = 0 |
||||
} else { |
||||
pts -= m.startPTS |
||||
|
||||
// switch segment
|
||||
if m.currentSegment.audioAUCount >= mpegtsSegmentMinAUCount && |
||||
m.currentSegment.startPTS != nil && |
||||
(pts-*m.currentSegment.startPTS) >= m.segmentDuration { |
||||
m.currentSegment.endPTS = pts |
||||
m.onSegmentReady(m.currentSegment) |
||||
m.currentSegment = newMuxerVariantMPEGTSSegment(now, m.segmentMaxSize, |
||||
m.videoTrack, m.audioTrack, m.writer.WriteData) |
||||
} |
||||
} |
||||
} else { |
||||
// wait for the video track
|
||||
if m.currentSegment == nil { |
||||
return nil |
||||
} |
||||
|
||||
pts -= m.startPTS |
||||
} |
||||
|
||||
err := m.currentSegment.writeAAC(now.Sub(m.startPCR), pts, aus) |
||||
if err != nil { |
||||
if m.currentSegment.buf.Len() > 0 { |
||||
m.onSegmentReady(m.currentSegment) |
||||
} |
||||
m.currentSegment = nil |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
Loading…
Reference in new issue