commit 1c6017bc874091eb87e3509f767454b5f5c5b168
parent 2d91eff9749102b83cc82919f67c016e44096676
Author: Oliver Lowe <o@olowe.co>
Date: Fri, 31 May 2024 17:41:35 +1000
m3u8: extract, add more validation for writing Variants
Encode() was getting pretty big. We also use append() rather than
continually calling fmt.Fprint to avoid a bunch of error handling.
Validation involves checking that required fields of Variant are set,
and ensuring we round frame rate to the precision specified in RFC
8216.
Resolves https://github.com/untangledco/streaming/issues/9
Diffstat:
| M | m3u8/write.go | | | 75 | ++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
| A | m3u8/write_test.go | | | 64 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 106 insertions(+), 33 deletions(-)
diff --git a/m3u8/write.go b/m3u8/write.go
@@ -84,40 +84,10 @@ func Encode(w io.Writer, p *Playlist) error {
fmt.Fprintln(w)
}
- for _, v := range p.Variants {
- fmt.Fprint(w, tagVariant+":")
- if v.Bandwidth > 0 {
- fmt.Fprintf(w, "BANDWIDTH=%d,", v.Bandwidth)
+ for i, v := range p.Variants {
+ if _, err := writeVariant(w, &v); err != nil {
+ return fmt.Errorf("write variant %d: %w", i, err)
}
- if v.AverageBandwidth > 0 {
- fmt.Fprintf(w, "AVERAGE-BANDWIDTH=%d,", v.AverageBandwidth)
- }
- if len(v.Codecs) > 0 {
- fmt.Fprintf(w, "CODECS=%q,", strings.Join(v.Codecs, ","))
- }
- if v.Resolution != [2]int{0, 0} {
- fmt.Fprintf(w, "RESOLUTION=%dx%d,", v.Resolution[0], v.Resolution[1])
- }
- if v.FrameRate > 0 {
- fmt.Fprintf(w, "FRAME-RATE=%f,", v.FrameRate)
- }
- if v.HDCP != HDCPNone {
- fmt.Fprintf(w, "HDCP-LEVEL=%s,", v.HDCP)
- }
- if v.Audio != "" {
- fmt.Fprintf(w, "AUDIO=%q,", v.Audio)
- }
- if v.Video != "" {
- fmt.Fprintf(w, "VIDEO=%q,", v.Video)
- }
- if v.Subtitles != "" {
- fmt.Fprintf(w, "SUBTITLES=%q,", v.Subtitles)
- }
- if v.ClosedCaptions != "" && v.ClosedCaptions != NoClosedCaptions {
- fmt.Fprintf(w, "CLOSED-CAPTIONS=%q,", v.ClosedCaptions)
- }
- fmt.Fprintln(w)
- fmt.Fprintln(w, v.URI)
}
for i, sd := range p.SessionData {
@@ -132,6 +102,45 @@ func Encode(w io.Writer, p *Playlist) error {
return nil
}
+func writeVariant(w io.Writer, v *Variant) (n int, err error) {
+ var attrs []string
+ if v.Bandwidth <= 0 {
+ return 0, fmt.Errorf("invalid bandwidth %d: must be larger than zero", v.Bandwidth)
+ }
+ attrs = append(attrs, fmt.Sprintf("BANDWIDTH=%d", v.Bandwidth))
+ if v.AverageBandwidth > 0 {
+ attrs = append(attrs, fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", v.AverageBandwidth))
+ }
+ if len(v.Codecs) > 0 {
+ attrs = append(attrs, fmt.Sprintf("CODECS=%q", strings.Join(v.Codecs, ",")))
+ }
+ if v.Resolution != [2]int{0, 0} {
+ attrs = append(attrs, fmt.Sprintf("RESOLUTION=%dx%d", v.Resolution[0], v.Resolution[1]))
+ }
+ if v.FrameRate > 0 {
+ attrs = append(attrs, fmt.Sprintf("FRAME-RATE=%.03f", v.FrameRate))
+ }
+ if v.HDCP != HDCPNone {
+ attrs = append(attrs, fmt.Sprintf("HDCP-LEVEL=%s", v.HDCP))
+ }
+ if v.Audio != "" {
+ attrs = append(attrs, fmt.Sprintf("AUDIO=%q", v.Audio))
+ }
+ if v.Video != "" {
+ attrs = append(attrs, fmt.Sprintf("VIDEO=%q", v.Video))
+ }
+ if v.Subtitles != "" {
+ attrs = append(attrs, fmt.Sprintf("SUBTITLES=%q", v.Subtitles))
+ }
+ if v.ClosedCaptions != "" && v.ClosedCaptions != NoClosedCaptions {
+ attrs = append(attrs, fmt.Sprintf("CLOSED-CAPTIONS=%q", v.ClosedCaptions))
+ }
+ if v.URI == "" {
+ return 0, fmt.Errorf("empty URI")
+ }
+ return fmt.Fprintf(w, "%s:%s\n%s\n", tagVariant, strings.Join(attrs, ","), v.URI)
+}
+
func writeDateRange(w io.Writer, dr *DateRange) error {
fmt.Fprint(w, tagDateRange+":")
var attrs []string
diff --git a/m3u8/write_test.go b/m3u8/write_test.go
@@ -0,0 +1,64 @@
+package m3u8
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func TestWriteVariant(t *testing.T) {
+ var cases = []struct {
+ name string
+ v Variant
+ want string
+ valid bool
+ }{
+ {
+ "simple",
+ Variant{
+ URI: "url_0/193039199_mp4_h264_aac_hd_7.m3u8",
+ Bandwidth: 2149280,
+ Codecs: []string{"mp4a.40.2", "avc1.64001f"},
+ Resolution: [2]int{1280, 720},
+ },
+ `#EXT-X-STREAM-INF:BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720
+url_0/193039199_mp4_h264_aac_hd_7.m3u8`,
+ true,
+ },
+ {
+ "rounded frame rate",
+ Variant{
+ URI: "small.m3u8",
+ Bandwidth: 10000,
+ FrameRate: 60 / 1.001, // busted NTSC
+ },
+ `#EXT-X-STREAM-INF:BANDWIDTH=10000,FRAME-RATE=59.940
+small.m3u8`,
+ true,
+ },
+ {
+ "no bandwidth",
+ Variant{URI: "url_0/193039199_mp4_h264_aac_hd_7.m3u8"},
+ "",
+ false,
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := &bytes.Buffer{}
+ _, err := writeVariant(buf, &tt.v)
+ if err != nil && tt.valid {
+ t.Fatalf("non-nil error for valid variant %v: %v", tt.v, err)
+ } else if !tt.valid && err == nil {
+ t.Fatalf("nil error for invalid variant %v: %v", tt.v, err)
+ }
+ got := strings.TrimSpace(buf.String()) // trim newline
+ if got != tt.want {
+ t.Errorf("unexpected variant text")
+ t.Logf("got: %s", got)
+ t.Logf("want: %s", tt.want)
+ }
+ })
+ }
+}