Self review #1
|
@ -0,0 +1,72 @@
|
||||||
|
package playlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"segmento/internal/segment"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultPlaylistDuration = 20 * time.Second
|
||||||
|
|
||||||
|
type Playlist interface {
|
||||||
|
// These could be moved to an interface?
|
||||||
|
Duration() time.Duration
|
||||||
|
TargetDuration() time.Duration
|
||||||
|
AddSegment(s *segment.Segment) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaPlaylist struct {
|
||||||
|
Segments []*segment.Segment
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaPlaylist() *MediaPlaylist {
|
||||||
|
return &MediaPlaylist{
|
||||||
|
Segments: make([]*segment.Segment, 0, 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MediaPlaylist) Duration() time.Duration {
|
||||||
|
return p.durationOf(p.Segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MediaPlaylist) TargetDuration() time.Duration {
|
||||||
|
return DefaultPlaylistDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MediaPlaylist) AddSegment(s *segment.Segment) error {
|
||||||
|
p.Segments = append(p.Segments, s)
|
||||||
|
|
||||||
|
if len(p.Segments) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if p.durationOf(p.Segments[1:]) > p.TargetDuration() {
|
||||||
|
p.Segments = p.Segments[1:]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MediaPlaylist) durationOf(ss []*segment.Segment) time.Duration {
|
||||||
|
var t time.Duration
|
||||||
|
for _, s := range ss {
|
||||||
|
t += s.Duration()
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MediaPlaylist) Run() error {
|
||||||
|
for {
|
||||||
|
// TODO block here and listen to the channel of incoming segments.
|
||||||
|
// As the reader is Read and segments are produced, update the Playlist
|
||||||
|
// struct and possibly notify consumers.
|
||||||
|
// What would actually be a useful API and/or Go best practices?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlaylistListener interface {
|
||||||
|
SegmentAdded(s *segment.Segment) error
|
||||||
|
SegmentRemoved(s *segment.Segment) error
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package playlist_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"segmento/internal/playlist"
|
||||||
|
"segmento/internal/segment"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMediaPlaylistImplements(t *testing.T) {
|
||||||
|
require.Implements(t, (*playlist.Playlist)(nil), playlist.NewMediaPlaylist())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMediaPlaylist(t *testing.T) {
|
||||||
|
playlist := playlist.NewMediaPlaylist()
|
||||||
|
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
s := segment.NewSegment(10*time.Second, 0)
|
||||||
|
s.IncrementDuration(3 * time.Second)
|
||||||
|
playlist.AddSegment(s)
|
||||||
|
}
|
||||||
|
require.Equal(t, 21*time.Second, playlist.Duration())
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package segment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tcolgate/mp3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultTargetDuration = 3 * time.Second
|
||||||
|
|
||||||
|
type Segment struct {
|
||||||
|
targetDuration time.Duration
|
||||||
|
duration time.Duration
|
||||||
|
data *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a new Segment struct. The capacity is the initial maximum capacity of the internal
|
||||||
|
// buffer, in bytes. It should be initialized with a value greater than the expected maximum buffer size,
|
||||||
|
// depending on the implementation.
|
||||||
|
func NewSegment(targetDuration time.Duration, capacity int) *Segment {
|
||||||
|
return &Segment{
|
||||||
|
data: bytes.NewBuffer(make([]byte, 0, capacity)),
|
||||||
|
duration: 0,
|
||||||
|
targetDuration: targetDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Segment) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
return s.data.ReadFrom(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Segment) IncrementDuration(d time.Duration) time.Duration {
|
||||||
|
s.duration += d
|
||||||
|
return s.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Segment) CanWrite(d time.Duration) bool {
|
||||||
|
return s.targetDuration-s.duration >= d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Segment) Duration() time.Duration {
|
||||||
|
return s.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Segment) Len() int {
|
||||||
|
return s.data.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MP3HTTPSegmenter struct {
|
||||||
|
decoder *mp3.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MP3HTTPSegmenter) Segment(r io.Reader) (chan *Segment, error) {
|
||||||
|
c := make(chan *Segment)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
d := mp3.NewDecoder(r)
|
||||||
|
|
||||||
|
var (
|
||||||
|
v mp3.Frame
|
||||||
|
skipped int
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
s *Segment
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := d.Decode(&v, &skipped); err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != nil && !s.CanWrite(v.Duration()) {
|
||||||
|
// publish the current segment, and initialize a new one.
|
||||||
|
c <- s
|
||||||
|
s = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
// TODO what is a good initial capacity?
|
||||||
|
s = NewSegment(DefaultTargetDuration, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := s.ReadFrom(v.Reader())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err) // TODO: some proper error handling
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != int64(v.Size()) {
|
||||||
|
// TODO would this ever happen? What should IncrementDuration be called with?
|
||||||
|
log.Fatal("unexpeced frame size, want = ", v.Size(), "got = ", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.IncrementDuration(v.Duration())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMP3HTTPSegmenter() *MP3HTTPSegmenter {
|
||||||
|
return &MP3HTTPSegmenter{}
|
||||||
|
}
|
|
@ -1,14 +1,16 @@
|
||||||
package main
|
package segment_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"segmento/internal/segment"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSegment(t *testing.T) {
|
func TestSegment(t *testing.T) {
|
||||||
segment := newSegment(10*time.Second, 0)
|
segment := segment.NewSegment(10*time.Second, 0)
|
||||||
|
|
||||||
require.Equal(t, time.Duration(0), segment.Duration())
|
require.Equal(t, time.Duration(0), segment.Duration())
|
||||||
require.True(t, segment.CanWrite(9*time.Second))
|
require.True(t, segment.CanWrite(9*time.Second))
|
||||||
|
@ -20,18 +22,3 @@ func TestSegment(t *testing.T) {
|
||||||
require.Equal(t, 10*time.Second, segment.Duration())
|
require.Equal(t, 10*time.Second, segment.Duration())
|
||||||
require.False(t, segment.CanWrite(1*time.Millisecond))
|
require.False(t, segment.CanWrite(1*time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaPlaylistImplements(t *testing.T) {
|
|
||||||
require.Implements(t, (*Playlist)(nil), newMediaPlaylist())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaPlaylist(t *testing.T) {
|
|
||||||
playlist := newMediaPlaylist()
|
|
||||||
|
|
||||||
for i := 0; i < 8; i++ {
|
|
||||||
s := newSegment(10*time.Second, 0)
|
|
||||||
s.IncrementDuration(3 * time.Second)
|
|
||||||
playlist.AddSegment(s)
|
|
||||||
}
|
|
||||||
require.Equal(t, 21*time.Second, playlist.Duration())
|
|
||||||
}
|
|
170
main.go
170
main.go
|
@ -1,181 +1,15 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tcolgate/mp3"
|
"segmento/internal/segment"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultTargetDuration = 3 * time.Second
|
|
||||||
const DefaultPlaylistDuration = 20 * time.Second
|
|
||||||
|
|
||||||
type Segment struct {
|
|
||||||
targetDuration time.Duration
|
|
||||||
duration time.Duration
|
|
||||||
data *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a new Segment struct. The capacity is the initial maximum capacity of the internal
|
|
||||||
// buffer, in bytes. It should be initialized with a value greater than the expected maximum buffer size,
|
|
||||||
// depending on the implementation.
|
|
||||||
func newSegment(targetDuration time.Duration, capacity int) *Segment {
|
|
||||||
return &Segment{
|
|
||||||
data: bytes.NewBuffer(make([]byte, 0, capacity)),
|
|
||||||
duration: 0,
|
|
||||||
targetDuration: targetDuration,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Segment) ReadFrom(r io.Reader) (n int64, err error) {
|
|
||||||
return s.data.ReadFrom(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Segment) IncrementDuration(d time.Duration) time.Duration {
|
|
||||||
s.duration += d
|
|
||||||
return s.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Segment) CanWrite(d time.Duration) bool {
|
|
||||||
return s.targetDuration-s.duration >= d
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Segment) Duration() time.Duration {
|
|
||||||
return s.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Segment) Len() int {
|
|
||||||
return s.data.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
type MP3HTTPSegmenter struct {
|
|
||||||
decoder *mp3.Decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MP3HTTPSegmenter) Segment(r io.Reader) (chan *Segment, error) {
|
|
||||||
c := make(chan *Segment)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
d := mp3.NewDecoder(r)
|
|
||||||
|
|
||||||
var (
|
|
||||||
v mp3.Frame
|
|
||||||
skipped int
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
s *Segment
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
if err := d.Decode(&v, &skipped); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s != nil && !s.CanWrite(v.Duration()) {
|
|
||||||
// publish the current segment, and initialize a new one.
|
|
||||||
c <- s
|
|
||||||
s = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if s == nil {
|
|
||||||
// TODO what is a good initial capacity?
|
|
||||||
s = newSegment(DefaultTargetDuration, 1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := s.ReadFrom(v.Reader())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err) // TODO: some proper error handling
|
|
||||||
}
|
|
||||||
|
|
||||||
if n != int64(v.Size()) {
|
|
||||||
// TODO would this ever happen? What should IncrementDuration be called with?
|
|
||||||
log.Fatal("unexpeced frame size, want = ", v.Size(), "got = ", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.IncrementDuration(v.Duration())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMP3HTTPSegmenter() *MP3HTTPSegmenter {
|
|
||||||
return &MP3HTTPSegmenter{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Playlist interface {
|
|
||||||
// These could be moved to an interface?
|
|
||||||
Duration() time.Duration
|
|
||||||
TargetDuration() time.Duration
|
|
||||||
AddSegment(s *Segment) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type MediaPlaylist struct {
|
|
||||||
Segments []*Segment
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMediaPlaylist() *MediaPlaylist {
|
|
||||||
return &MediaPlaylist{
|
|
||||||
Segments: make([]*Segment, 0, 10),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MediaPlaylist) Duration() time.Duration {
|
|
||||||
return p.durationOf(p.Segments)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MediaPlaylist) TargetDuration() time.Duration {
|
|
||||||
return DefaultPlaylistDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MediaPlaylist) AddSegment(s *Segment) error {
|
|
||||||
p.Segments = append(p.Segments, s)
|
|
||||||
|
|
||||||
if len(p.Segments) == 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
if p.durationOf(p.Segments[1:]) > p.TargetDuration() {
|
|
||||||
p.Segments = p.Segments[1:]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MediaPlaylist) durationOf(ss []*Segment) time.Duration {
|
|
||||||
var t time.Duration
|
|
||||||
for _, s := range ss {
|
|
||||||
t += s.Duration()
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *MediaPlaylist) Run() error {
|
|
||||||
for {
|
|
||||||
// TODO block here and listen to the channel of incoming segments.
|
|
||||||
// As the reader is Read and segments are produced, update the Playlist
|
|
||||||
// struct and possibly notify consumers.
|
|
||||||
// What would actually be a useful API and/or Go best practices?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PlaylistListener interface {
|
|
||||||
SegmentAdded(s *Segment) error
|
|
||||||
SegmentRemoved(s *Segment) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// TODO accept some flags with:
|
// TODO accept some flags with:
|
||||||
// URL - source of stream
|
// URL - source of stream
|
||||||
|
@ -204,7 +38,7 @@ func main() {
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
segmenter := newMP3HTTPSegmenter()
|
segmenter := segment.NewMP3HTTPSegmenter()
|
||||||
segments, err := segmenter.Segment(resp.Body)
|
segments, err := segmenter.Segment(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
Loading…
Reference in New Issue