Browse Source
In this way, EXT-X-TARGETDURATION and EXTINF are always filled correctly. If no segments have been generated yet, the playlist is not returned until a segment is inserted or the muxer is closed. This causes timeout issues on iOS Safari, that are solved by waiting for a fetch() before starting the video.pull/543/head
8 changed files with 291 additions and 266 deletions
@ -1,68 +0,0 @@
@@ -1,68 +0,0 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"sync" |
||||
) |
||||
|
||||
type multiAccessBufferReader struct { |
||||
m *multiAccessBuffer |
||||
readPos int |
||||
} |
||||
|
||||
func (r *multiAccessBufferReader) Read(p []byte) (int, error) { |
||||
r.m.mutex.Lock() |
||||
defer r.m.mutex.Unlock() |
||||
|
||||
if r.m.closed && r.m.writePos == r.readPos { |
||||
return 0, io.EOF |
||||
} |
||||
|
||||
for !r.m.closed && r.m.writePos == r.readPos { |
||||
r.m.cond.Wait() |
||||
} |
||||
|
||||
buf := r.m.buf.Bytes() |
||||
n := copy(p, buf[r.readPos:]) |
||||
r.readPos += n |
||||
|
||||
return n, nil |
||||
} |
||||
|
||||
type multiAccessBuffer struct { |
||||
buf bytes.Buffer |
||||
closed bool |
||||
writePos int |
||||
mutex sync.Mutex |
||||
cond *sync.Cond |
||||
} |
||||
|
||||
func newMultiAccessBuffer() *multiAccessBuffer { |
||||
m := &multiAccessBuffer{} |
||||
m.cond = sync.NewCond(&m.mutex) |
||||
return m |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) Close() error { |
||||
m.mutex.Lock() |
||||
m.closed = true |
||||
m.mutex.Unlock() |
||||
m.cond.Broadcast() |
||||
return nil |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) Write(p []byte) (int, error) { |
||||
m.mutex.Lock() |
||||
n, _ := m.buf.Write(p) |
||||
m.writePos += n |
||||
m.mutex.Unlock() |
||||
m.cond.Broadcast() |
||||
return n, nil |
||||
} |
||||
|
||||
func (m *multiAccessBuffer) NewReader() io.Reader { |
||||
return &multiAccessBufferReader{ |
||||
m: m, |
||||
} |
||||
} |
||||
@ -1,39 +0,0 @@
@@ -1,39 +0,0 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"io" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestMultiAccessBuffer(t *testing.T) { |
||||
m := newMultiAccessBuffer() |
||||
|
||||
m.Write([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}) |
||||
|
||||
r := m.NewReader() |
||||
|
||||
buf := make([]byte, 4) |
||||
n, err := r.Read(buf) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []byte{0x01, 0x02, 0x03, 0x04}, buf[:n]) |
||||
|
||||
buf = make([]byte, 10) |
||||
n, err = r.Read(buf) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []byte{0x05, 0x06, 0x07, 0x08}, buf[:n]) |
||||
|
||||
m.Write([]byte{0x09, 0x0a, 0x0b, 0x0c}) |
||||
|
||||
m.Close() |
||||
|
||||
buf = make([]byte, 10) |
||||
n, err = r.Read(buf) |
||||
require.NoError(t, err) |
||||
require.Equal(t, []byte{0x09, 0x0a, 0x0b, 0x0c}, buf[:n]) |
||||
|
||||
buf = make([]byte, 10) |
||||
_, err = r.Read(buf) |
||||
require.Equal(t, io.EOF, err) |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/hex" |
||||
"io" |
||||
"strings" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
) |
||||
|
||||
type primaryPlaylist struct { |
||||
videoTrack *gortsplib.Track |
||||
audioTrack *gortsplib.Track |
||||
h264SPS []byte |
||||
h264PPS []byte |
||||
|
||||
breader *bytes.Reader |
||||
} |
||||
|
||||
func newPrimaryPlaylist( |
||||
videoTrack *gortsplib.Track, |
||||
audioTrack *gortsplib.Track, |
||||
h264SPS []byte, |
||||
h264PPS []byte, |
||||
) *primaryPlaylist { |
||||
p := &primaryPlaylist{ |
||||
videoTrack: videoTrack, |
||||
audioTrack: audioTrack, |
||||
h264SPS: h264SPS, |
||||
h264PPS: h264PPS, |
||||
} |
||||
|
||||
var codecs []string |
||||
|
||||
if p.videoTrack != nil { |
||||
codecs = append(codecs, "avc1."+hex.EncodeToString(p.h264SPS[1:4])) |
||||
} |
||||
|
||||
if p.audioTrack != nil { |
||||
codecs = append(codecs, "mp4a.40.2") |
||||
} |
||||
|
||||
cnt := "#EXTM3U\n" |
||||
cnt += "#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"" + strings.Join(codecs, ",") + "\"\n" |
||||
cnt += "stream.m3u8\n" |
||||
|
||||
p.breader = bytes.NewReader([]byte(cnt)) |
||||
|
||||
return p |
||||
} |
||||
|
||||
func (p *primaryPlaylist) reader() io.Reader { |
||||
return p.breader |
||||
} |
||||
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
package hls |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io" |
||||
"math" |
||||
"strconv" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
type readerFunc struct { |
||||
wrapped func() []byte |
||||
reader *bytes.Reader |
||||
} |
||||
|
||||
func (r *readerFunc) Read(buf []byte) (int, error) { |
||||
if r.reader == nil { |
||||
cnt := r.wrapped() |
||||
r.reader = bytes.NewReader(cnt) |
||||
} |
||||
return r.reader.Read(buf) |
||||
} |
||||
|
||||
type streamPlaylist struct { |
||||
hlsSegmentCount int |
||||
|
||||
mutex sync.Mutex |
||||
cond *sync.Cond |
||||
closed bool |
||||
segments []*segment |
||||
segmentByName map[string]*segment |
||||
segmentDeleteCount int |
||||
} |
||||
|
||||
func newStreamPlaylist(hlsSegmentCount int) *streamPlaylist { |
||||
p := &streamPlaylist{ |
||||
hlsSegmentCount: hlsSegmentCount, |
||||
segmentByName: make(map[string]*segment), |
||||
} |
||||
p.cond = sync.NewCond(&p.mutex) |
||||
return p |
||||
} |
||||
|
||||
func (p *streamPlaylist) close() { |
||||
func() { |
||||
p.mutex.Lock() |
||||
defer p.mutex.Unlock() |
||||
p.closed = true |
||||
}() |
||||
|
||||
p.cond.Broadcast() |
||||
} |
||||
|
||||
func (p *streamPlaylist) reader() io.Reader { |
||||
return &readerFunc{wrapped: 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 _, f := range p.segments { |
||||
v2 := uint(math.Round(f.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" |
||||
|
||||
for _, f := range p.segments { |
||||
cnt += "#EXTINF:" + strconv.FormatFloat(f.duration().Seconds(), 'f', -1, 64) + ",\n" |
||||
cnt += f.name + ".ts\n" |
||||
} |
||||
|
||||
return []byte(cnt) |
||||
}} |
||||
} |
||||
|
||||
func (p *streamPlaylist) 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 *streamPlaylist) pushSegment(t *segment) { |
||||
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() |
||||
} |
||||
Loading…
Reference in new issue