commit 95f3dab6a9cc691761cc0b2ce737533ea17f02fb
parent baaea663292b3165b7b67c663676c2f8798c7ce5
Author: Oliver Lowe <o@olowe.co>
Date: Tue, 28 May 2024 16:39:44 +1000
m3u8: factor, test segment encoding separately
To make it easier to add new test cases. The Encode() function was
getting pretty big, too.
Diffstat:
5 files changed, 137 insertions(+), 59 deletions(-)
diff --git a/m3u8/m3u8.go b/m3u8/m3u8.go
@@ -74,6 +74,8 @@ type Key struct {
IV [16]byte
}
+const defaultKeyFormat string = "identity"
+
func (k Key) String() string {
var attrs []string
attrs = append(attrs, fmt.Sprintf("METHOD=%s", k.Method))
diff --git a/m3u8/segment.go b/m3u8/segment.go
@@ -3,6 +3,7 @@ package m3u8
import (
"errors"
"fmt"
+ "io"
"strconv"
"strings"
"time"
@@ -117,3 +118,39 @@ func parseSegmentDuration(it item) (time.Duration, error) {
microseconds := seconds * 1e6
return time.Duration(microseconds) * time.Microsecond, nil
}
+
+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)
+ }
+ if seg.Key != nil {
+ fmt.Fprintf(w, "%s:%s\n", tagKey, seg.Key)
+ }
+ if seg.Map != nil {
+ writeMap(w, *seg.Map)
+ }
+ if !seg.DateTime.IsZero() {
+ fmt.Fprintf(w, "%s:%s\n", tagDateTime, seg.DateTime.Format(RFC3339Milli))
+ }
+ 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)
+ }
+ return 0, nil
+}
diff --git a/m3u8/segment_test.go b/m3u8/segment_test.go
@@ -0,0 +1,95 @@
+package m3u8
+
+import (
+ "bytes"
+ "encoding/binary"
+ "io"
+ "testing"
+ "time"
+)
+
+func TestWriteSegments(t *testing.T) {
+ var cases = []struct {
+ name string
+ seg Segment
+ out string
+ }{
+ {
+ "duration",
+ Segment{Duration: 10 * time.Second, URI: "bunny.ts"},
+ "#EXTINF:10.000\nbunny.ts\n",
+ },
+ {
+ "duration milliseconds",
+ Segment{URI: "something.ts", Duration: 9967 * time.Millisecond},
+ "#EXTINF:9.967\nsomething.ts\n",
+ },
+ {
+ "discontinuity with URI",
+ Segment{
+ Duration: 30 * time.Second,
+ Discontinuity: true,
+ URI: "adbreak.ts",
+ },
+ "#EXT-X-DISCONTINUITY\n#EXTINF:30.000\nadbreak.ts\n",
+ },
+ {
+ "byte range",
+ Segment{
+ Duration: 2 * time.Second,
+ URI: "vid.ts",
+ Range: ByteRange{69, 420},
+ },
+ "#EXT-X-BYTERANGE:69@420\n#EXTINF:2.000\nvid.ts\n",
+ },
+ }
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := &bytes.Buffer{}
+ if _, err := writeSegments(buf, []Segment{tt.seg}); err != nil {
+ t.Fatal(err)
+ }
+ if buf.String() != tt.out {
+ t.Errorf("segment text does not match expected")
+ t.Log("got:", buf.String())
+ t.Log("want:", tt.out)
+ }
+ })
+ }
+}
+
+func TestWriteBadSegments(t *testing.T) {
+ var cases = []struct {
+ name string
+ seg Segment
+ }{
+ {"empty", Segment{}},
+ {"no duration", Segment{URI: "video.ts"}},
+ }
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ if _, err := writeSegments(io.Discard, []Segment{tt.seg}); err == nil {
+ t.Fatalf("nil error encoding invalid segment")
+ }
+ })
+ }
+}
+
+func TestWriteKey(t *testing.T) {
+ var iv [16]byte
+ binary.LittleEndian.PutUint64(iv[:8], 10000)
+ binary.LittleEndian.PutUint64(iv[8:], 98765432)
+ k := Key{
+ Method: EncryptMethodAES128,
+ URI: "magic.key",
+ IV: iv,
+ Format: defaultKeyFormat,
+ FormatVersions: []uint32{1, 2, 5},
+ }
+ want := `METHOD=AES-128,URI="magic.key",IV=0x1027000000000000780ae30500000000,KEYFORMAT="identity",KEYFORMATVERSIONS="1/2/5"`
+ if k.String() != want {
+ t.Errorf("unexpected segment key text")
+ t.Log("got:", k.String())
+ t.Log("want:", want)
+ }
+}
diff --git a/m3u8/write.go b/m3u8/write.go
@@ -25,31 +25,9 @@ func Encode(w io.Writer, p *Playlist) error {
fmt.Fprintf(w, "%s:%d\n", tagTargetDuration, p.TargetDuration/time.Second)
}
fmt.Fprintf(w, "%s:%d\n", tagMediaSequence, p.Sequence)
- for _, seg := range p.Segments {
- if seg.Discontinuity {
- fmt.Fprintln(w, tagDiscontinuity)
- }
- if seg.DateRange != nil {
- if err := writeDateRange(w, seg.DateRange); err != nil {
- return fmt.Errorf("write date range: %w", err)
- }
- }
- if seg.Range != [2]int{0, 0} {
- fmt.Fprintf(w, "%s:%s\n", tagByteRange, seg.Range)
- }
- if seg.Key != nil {
- fmt.Fprintf(w, "%s:%s\n", tagKey, seg.Key)
- }
- if seg.Map != nil {
- writeMap(w, *seg.Map)
- }
- if !seg.DateTime.IsZero() {
- fmt.Fprintf(w, "%s:%s\n", tagDateTime, seg.DateTime.Format(RFC3339Milli))
- }
- 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)
+
+ if _, err := writeSegments(w, p.Segments); err != nil {
+ return fmt.Errorf("write segments: %w", err)
}
for _, r := range p.Media {
diff --git a/m3u8/write_test.go b/m3u8/write_test.go
@@ -1,34 +0,0 @@
-package m3u8
-
-import (
- "bufio"
- "bytes"
- "testing"
- "time"
-)
-
-func TestEncodeSegDuration(t *testing.T) {
- plist := &Playlist{
- Version: 7,
- Segments: []Segment{{Duration: 9967 * time.Millisecond}},
- }
- buf := &bytes.Buffer{}
- if err := Encode(buf, plist); err != nil {
- t.Fatal(err)
- }
- sc := bufio.NewScanner(buf)
- var linenum = 1
- var found bool
- want := "#EXTINF:9.967"
- for sc.Scan() {
- t.Log(sc.Text())
- if sc.Text() == want {
- found = true
- return
- }
- linenum++
- }
- if !found {
- t.Errorf("no matching segment duration %s", want)
- }
-}