streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

commit 12b4cf5015a6017ee960c93072ddffdc51bc746c
parent c0e0995563ad5bbb07ebdf0be7dbd67aed858ee0
Author: Oliver Lowe <o@olowe.co>
Date:   Sat,  1 Jun 2024 18:44:46 +1000

m3u8: Return valid HLS tags with String()

Without the tag, the string is essentially invalid. Since we're not
exporting the HLS tag names (e.g. "#EXT-STREAM-INF") the String()
method was not useful. Now doing a fmt.Println(variant) returns a
valid HLS tag.

Fixes: https://github.com/untangledco/streaming/issues/14

Diffstat:
Mm3u8/m3u8.go | 13++++++++++---
Mm3u8/master.go | 35++++++++++++++++++++++++++++++++++-
Mm3u8/segment.go | 4++--
Mm3u8/segment_test.go | 2+-
Mm3u8/write.go | 33++-------------------------------
5 files changed, 49 insertions(+), 38 deletions(-)

diff --git a/m3u8/m3u8.go b/m3u8/m3u8.go @@ -72,8 +72,6 @@ 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)) @@ -89,9 +87,11 @@ func (k Key) String() string { } attrs = append(attrs, fmt.Sprintf("KEYFORMATVERSIONS=%q", strings.Join(ss, "/"))) } - return strings.Join(attrs, ",") + return tagKey + ":" + strings.Join(attrs, ",") } +const defaultKeyFormat string = "identity" + type EncryptMethod uint8 const ( @@ -117,6 +117,13 @@ type Map struct { ByteRange ByteRange } +func (m Map) String() string { + if m.ByteRange != [2]int{0, 0} { + return fmt.Sprintf("%s:URI=%q,BYTERANGE=%s", tagMap, m.URI, m.ByteRange) + } + return fmt.Sprintf("%s:URI=%q", tagMap, m.URI) +} + // ByteRange represents... // The first entry is an offset, the second...? type ByteRange [2]int diff --git a/m3u8/master.go b/m3u8/master.go @@ -81,6 +81,39 @@ type Variant struct { ClosedCaptions string } +func (v Variant) String() string { + var attrs []string + 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)) + } + return fmt.Sprintf("%s:%s\n%s", tagVariant, strings.Join(attrs, ","), v.URI) +} + // NoClosedCaptions may be the value for Variant.ClosedCaptions to // explicitly indicate that no closed captions are available for the // Variant. @@ -134,5 +167,5 @@ func (sd SessionData) String() string { if sd.Language != "" { attrs = append(attrs, fmt.Sprintf("LANGUAGE=%q", sd.Language)) } - return strings.Join(attrs, ",") + return tagSessionData + ":" + strings.Join(attrs, ",") } diff --git a/m3u8/segment.go b/m3u8/segment.go @@ -139,10 +139,10 @@ func writeSegments(w io.Writer, segments []Segment) (n int, err error) { fmt.Fprintf(w, "%s:%s\n", tagByteRange, seg.Range) } if seg.Key != nil { - fmt.Fprintf(w, "%s:%s\n", tagKey, seg.Key) + fmt.Fprintln(w, seg.Key) } if seg.Map != nil { - writeMap(w, *seg.Map) + fmt.Fprintln(w, seg.Map) } if !seg.DateTime.IsZero() { fmt.Fprintf(w, "%s:%s\n", tagDateTime, seg.DateTime.Format(RFC3339Milli)) diff --git a/m3u8/segment_test.go b/m3u8/segment_test.go @@ -86,7 +86,7 @@ func TestWriteKey(t *testing.T) { Format: defaultKeyFormat, FormatVersions: []uint32{1, 2, 5}, } - want := `METHOD=AES-128,URI="magic.key",IV=0x1027000000000000780ae30500000000,KEYFORMAT="identity",KEYFORMATVERSIONS="1/2/5"` + want := `#EXT-X-KEY: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()) diff --git a/m3u8/write.go b/m3u8/write.go @@ -103,42 +103,13 @@ func Encode(w io.Writer, p *Playlist) error { } 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) + return fmt.Fprintln(w, v) } func writeDateRange(w io.Writer, dr *DateRange) error { @@ -196,5 +167,5 @@ func writeSessionData(w io.Writer, sd SessionData) (n int, err error) { if sd.URI != "" && sd.Value != "" { return 0, fmt.Errorf("only one of Value or URI may be set") } - return fmt.Fprintf(w, "%s:%s\n", tagSessionData, sd) + return fmt.Fprintln(w, sd) }