2020-07-08 17:46:14 +00:00
|
|
|
package playlist
|
2020-07-08 17:31:57 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"segmento/pkg/media"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const DefaultMaxSegments = 10
|
|
|
|
|
|
|
|
type PlaylistSegment struct {
|
|
|
|
media.Segment
|
|
|
|
url string
|
|
|
|
seqId int
|
|
|
|
}
|
|
|
|
|
|
|
|
type Playlist interface {
|
|
|
|
Len() int
|
|
|
|
AddConsumer(c Consumer)
|
|
|
|
RemoveConsumer(c Consumer) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type Consumer interface {
|
|
|
|
PlaylistUpdated(p Playlist)
|
|
|
|
PlaylistSegmentAdded(p Playlist, s *PlaylistSegment)
|
|
|
|
}
|
|
|
|
|
|
|
|
// so we know that we can publish a segment (i.e. make available a URL to access it from elsewhere)
|
|
|
|
// but what does it mean to publish a Playlist?
|
|
|
|
// A playlist contains lists of Segments but it wouldn't necessarily be published in the same location
|
|
|
|
// for example, Segments may be published to S3 but a playlist may be published periodically to a static
|
|
|
|
// hosting location elsewhere.
|
|
|
|
|
|
|
|
type MediaPlaylist struct {
|
|
|
|
nextSeqId int
|
|
|
|
src io.Reader // read a stream of bytes e.g. MP3
|
|
|
|
segmenter media.Segmenter // segment the incoming bytes. For now, up to the caller to provider a matching src and segmenter.
|
|
|
|
publisher media.SegmentPublisher // publish the segments somewhere (i.e. make available a URL with them)
|
|
|
|
segments []*PlaylistSegment // a slice of the last n segments
|
|
|
|
consumers map[Consumer]bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMediaPlaylist(src io.Reader, segmenter media.Segmenter, publisher media.SegmentPublisher) *MediaPlaylist {
|
|
|
|
p := MediaPlaylist{
|
|
|
|
src: src,
|
|
|
|
segmenter: segmenter,
|
|
|
|
publisher: publisher,
|
|
|
|
segments: make([]*PlaylistSegment, 0, 10),
|
|
|
|
consumers: make(map[Consumer]bool),
|
|
|
|
}
|
|
|
|
return &p
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) Len() int {
|
|
|
|
return len(p.segments)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) AddConsumer(c Consumer) {
|
|
|
|
p.consumers[c] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) RemoveConsumer(c Consumer) error {
|
|
|
|
delete(p.consumers, c)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) Run() error {
|
|
|
|
segments, err := p.segmenter.Segment(p.src)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for s := range segments {
|
|
|
|
if err = p.handleSegment(s); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) handleSegment(s *media.Segment) error {
|
|
|
|
// first, publish the segment:
|
|
|
|
url, err := p.publisher.Publish(s)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// initialize a new playlist segment:
|
|
|
|
nextSeqId := p.nextSeqId
|
|
|
|
p.nextSeqId++
|
|
|
|
ps := PlaylistSegment{
|
|
|
|
Segment: *s, // TODO make the Segmenter publish values, not pointers
|
|
|
|
seqId: nextSeqId,
|
|
|
|
url: url,
|
|
|
|
}
|
|
|
|
|
|
|
|
// append the playlist segment to our slice of segments:
|
|
|
|
p.segments = append(p.segments, &ps)
|
|
|
|
|
|
|
|
// trim the start of the playlist if needed:
|
|
|
|
if len(p.segments) > DefaultMaxSegments {
|
|
|
|
p.segments = p.segments[len(p.segments)-DefaultMaxSegments:]
|
|
|
|
}
|
|
|
|
|
|
|
|
for c, _ := range p.consumers {
|
|
|
|
c.PlaylistSegmentAdded(p, &ps)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *MediaPlaylist) Render() string {
|
|
|
|
var r string
|
|
|
|
r += "#EXTM3U\n"
|
|
|
|
r += "#EXT-X-VERSION:3\n"
|
|
|
|
r += "#EXT-X-TARGETDURATION:3\n" // TODO
|
|
|
|
for _, s := range p.segments {
|
|
|
|
r += fmt.Sprintf("#EXTINF:%.05f\n", float32(s.Duration())/float32(time.Second))
|
|
|
|
r += "http://www.example.com/x.mp3\n"
|
|
|
|
}
|
|
|
|
r += "#EXT-X-ENDLIST"
|
|
|
|
return r
|
|
|
|
}
|