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 }