commit 65ddb6704d4b8a8a8c1dc40ff84556f3c56d1dd9
parent 61ffe36e6efb13fc404698f0769f345d2807a2a6
Author: Oliver Lowe <o@olowe.co>
Date: Wed, 5 Jun 2024 14:23:03 +1000
m3u8: split out Segment text marshalling
Makes the tests easier to understand, and uses something from the
stdlib to work with other stdlib packages. While here, rename
ParsePlaylist to Decode for consistency with Encode.
Diffstat:
4 files changed, 65 insertions(+), 45 deletions(-)
diff --git a/m3u8/parse.go b/m3u8/parse.go
@@ -22,7 +22,7 @@ const (
tagSessionData = "#EXT-X-SESSION-DATA" // RFC 8216, 4.3.4.4
)
-func ParsePlaylist(rd io.Reader) (*Playlist, error) {
+func Decode(rd io.Reader) (*Playlist, error) {
lex := newLexer(rd)
go lex.run()
it := <-lex.items
diff --git a/m3u8/parse_test.go b/m3u8/parse_test.go
@@ -8,7 +8,7 @@ import (
"time"
)
-func TestParse(t *testing.T) {
+func TestDecode(t *testing.T) {
names, err := filepath.Glob("testdata/*.m3u8")
if err != nil {
t.Fatal(err)
@@ -20,7 +20,7 @@ func TestParse(t *testing.T) {
t.Fatal(err)
}
defer f.Close()
- _, err = ParsePlaylist(f)
+ _, err = Decode(f)
if err != nil {
t.Fatal(err)
}
@@ -73,7 +73,7 @@ func TestFrameRate(t *testing.T) {
t.Fatal(err)
}
defer f.Close()
- plist, err := ParsePlaylist(f)
+ plist, err := Decode(f)
if err != nil {
t.Fatal(err)
}
diff --git a/m3u8/segment.go b/m3u8/segment.go
@@ -1,6 +1,7 @@
package m3u8
import (
+ "bytes"
"errors"
"fmt"
"io"
@@ -120,37 +121,56 @@ func parseSegmentDuration(it item) (time.Duration, error) {
}
func writeSegments(w io.Writer, segments []Segment) (n int, err error) {
- for _, seg := range segments {
- if seg.URI == "" {
- return 0, fmt.Errorf("empty URI")
- }
- if seg.Duration == 0 {
- return 0, fmt.Errorf("zero duration")
- }
- if seg.Discontinuity {
- fmt.Fprintln(w, tagDiscontinuity)
- }
- if seg.DateRange != nil {
- if err := writeDateRange(w, seg.DateRange); err != nil {
- return 0, fmt.Errorf("write date range: %w", err)
- }
- }
- if seg.Range != [2]int{0, 0} {
- fmt.Fprintf(w, "%s:%s\n", tagByteRange, seg.Range)
+ for i, seg := range segments {
+ b, err := seg.MarshalText()
+ if err != nil {
+ return n, fmt.Errorf("segment %d: %w", i, err)
}
- if seg.Key != nil {
- fmt.Fprintln(w, seg.Key)
+ nn, err := fmt.Fprintln(w, string(b))
+ n += nn
+ if err != nil {
+ return n, err
}
- if seg.Map != nil {
- fmt.Fprintln(w, seg.Map)
+ }
+ return n, nil
+}
+
+func (seg *Segment) MarshalText() ([]byte, error) {
+ if seg.URI == "" {
+ return nil, fmt.Errorf("empty URI")
+ }
+ if seg.Duration == 0 {
+ return nil, fmt.Errorf("zero duration")
+ }
+ var tags []string
+ if seg.Discontinuity {
+ tags = append(tags, tagDiscontinuity)
+ }
+ if seg.DateRange != nil {
+ buf := &bytes.Buffer{}
+ if err := writeDateRange(buf, seg.DateRange); err != nil {
+ return nil, fmt.Errorf("write date range: %w", err)
}
- if !seg.DateTime.IsZero() {
- fmt.Fprintf(w, "%s:%s\n", tagDateTime, seg.DateTime.Format(RFC3339Milli))
+ tags = append(tags, buf.String())
+ }
+ if seg.Range != [2]int{0, 0} {
+ if seg.Range[0] >= seg.Range[1] {
+ return nil, fmt.Errorf("impossible range: offset (%d) must be smaller than next %d", seg.Range[0], seg.Range[1])
}
- us := seg.Duration / time.Microsecond
- // we do .03f for the same precision as test-streams.mux.dev.
- fmt.Fprintf(w, "%s:%.03f\n", tagSegmentDuration, float32(us)/1e6)
- fmt.Fprintln(w, seg.URI)
+ tags = append(tags, fmt.Sprintf("%s:%s", tagByteRange, seg.Range))
+ }
+ if seg.Key != nil {
+ tags = append(tags, seg.Key.String())
+ }
+ if seg.Map != nil {
+ tags = append(tags, seg.Map.String())
+ }
+ if !seg.DateTime.IsZero() {
+ tags = append(tags, fmt.Sprintf("%s:%s", tagDateTime, seg.DateTime.Format(RFC3339Milli)))
}
- return 0, nil
+ us := seg.Duration / time.Microsecond
+ // we do .03f for the same precision as test-streams.mux.dev.
+ tags = append(tags, fmt.Sprintf("%s:%.03f", tagSegmentDuration, float32(us)/1e6))
+ tags = append(tags, seg.URI)
+ return []byte(strings.Join(tags, "\n")), nil
}
diff --git a/m3u8/segment_test.go b/m3u8/segment_test.go
@@ -1,14 +1,12 @@
package m3u8
import (
- "bytes"
"encoding/binary"
- "io"
"testing"
"time"
)
-func TestWriteSegments(t *testing.T) {
+func TestMarshalSegments(t *testing.T) {
var cases = []struct {
name string
seg Segment
@@ -17,12 +15,12 @@ func TestWriteSegments(t *testing.T) {
{
"duration",
Segment{Duration: 10 * time.Second, URI: "bunny.ts"},
- "#EXTINF:10.000\nbunny.ts\n",
+ "#EXTINF:10.000\nbunny.ts",
},
{
"duration milliseconds",
Segment{URI: "something.ts", Duration: 9967 * time.Millisecond},
- "#EXTINF:9.967\nsomething.ts\n",
+ "#EXTINF:9.967\nsomething.ts",
},
{
"discontinuity with URI",
@@ -31,7 +29,7 @@ func TestWriteSegments(t *testing.T) {
Discontinuity: true,
URI: "adbreak.ts",
},
- "#EXT-X-DISCONTINUITY\n#EXTINF:30.000\nadbreak.ts\n",
+ "#EXT-X-DISCONTINUITY\n#EXTINF:30.000\nadbreak.ts",
},
{
"byte range",
@@ -40,35 +38,37 @@ func TestWriteSegments(t *testing.T) {
URI: "vid.ts",
Range: ByteRange{69, 420},
},
- "#EXT-X-BYTERANGE:69@420\n#EXTINF:2.000\nvid.ts\n",
+ "#EXT-X-BYTERANGE:69@420\n#EXTINF:2.000\nvid.ts",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
- buf := &bytes.Buffer{}
- if _, err := writeSegments(buf, []Segment{tt.seg}); err != nil {
+ b, err := tt.seg.MarshalText()
+ if err != nil {
t.Fatal(err)
}
- if buf.String() != tt.out {
+ got := string(b)
+ if got != tt.out {
t.Errorf("segment text does not match expected")
- t.Log("got:", buf.String())
+ t.Log("got:", got)
t.Log("want:", tt.out)
}
})
}
}
-func TestWriteBadSegments(t *testing.T) {
+func TestMarshalBadSegments(t *testing.T) {
var cases = []struct {
name string
seg Segment
}{
{"empty", Segment{}},
{"no duration", Segment{URI: "video.ts"}},
+ {"impossible range", Segment{URI: "bbb.ts", Duration: 6 * time.Second, Range: ByteRange{999, 10}}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
- if _, err := writeSegments(io.Discard, []Segment{tt.seg}); err == nil {
+ if _, err := tt.seg.MarshalText(); err == nil {
t.Fatalf("nil error encoding invalid segment")
}
})