segmento/pkg/playlist/playlist.go

129 lines
3.2 KiB
Go

package playlist
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
}
// TODO does it make sense to do 50% dependency injection and 50% consumers here?
// Why not just pass everything through the consumer?
// Or how about define these methods on the interface and force implementations to provide them?
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
}