streaming

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

commit 8b9dabb3f7e20b95c9d353d7cdc8deed50330b9f
parent df15482535093988ec1b4514e5fc9e0f12c4de49
Author: Oliver Lowe <o@olowe.co>
Date:   Fri, 17 May 2024 17:28:49 +1000

m3u8: implement EXT-X-DATETIME tag encoding

For testing out our scte35 package.

Diffstat:
Mm3u8/segment.go | 1+
Mm3u8/write.go | 43+++++++++++++++++++++++++++++++++++++++++++
Mm3u8/write_test.go | 21+++++++++++++++++++++
3 files changed, 65 insertions(+), 0 deletions(-)

diff --git a/m3u8/segment.go b/m3u8/segment.go @@ -19,6 +19,7 @@ const ( tagGap = "#EXT-X-GAP" tagBitrate = "#EXT-X-BITRATE" tagPart = "#EXT-X-PART" + tagDateRange = "#EXT-X-DATERANGE" ) // parseSegment returns the next segment from items and the leading diff --git a/m3u8/write.go b/m3u8/write.go @@ -1,8 +1,10 @@ package m3u8 import ( + "encoding/hex" "fmt" "io" + "strings" "time" ) @@ -12,6 +14,14 @@ func Encode(w io.Writer, p *Playlist) error { fmt.Fprintf(w, "%s:%s\n", tagPlaylistType, p.Type) fmt.Fprintf(w, "%s:%d\n", tagTargetDuration, p.TargetDuration/time.Second) 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) + } + } 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) @@ -22,3 +32,36 @@ func Encode(w io.Writer, p *Playlist) error { } return nil } + +func writeDateRange(w io.Writer, dr *DateRange) error { + fmt.Fprint(w, tagDateRange+":") + var attrs []string + if dr.ID != "" { + attrs = append(attrs, fmt.Sprintf("ID=%q", dr.ID)) + } + if dr.Class != "" { + attrs = append(attrs, fmt.Sprintf("CLASS=%q", dr.Class)) + } + if !dr.Start.IsZero() { + attrs = append(attrs, fmt.Sprintf("START-DATE=%q", dr.Start.Format(time.RFC3339))) + } + if !dr.End.IsZero() { + attrs = append(attrs, fmt.Sprintf("END-DATE=%q", dr.End.Format(time.RFC3339))) + } + // TODO(otl): dr.Duration, dr.Planned. Differentiate zero value and user-set zero. + // TODO(otl): dr.Custom. + // TODO(otl): dr.CueCommand, when to write this versuse cuein, cueout. + if dr.CueIn != nil { + attrs = append(attrs, fmt.Sprintf("SCTE35-IN=0x%s", hex.EncodeToString(dr.CueIn))) + } + if dr.CueOut != nil { + attrs = append(attrs, fmt.Sprintf("SCTE35-OUT=0x%s", hex.EncodeToString(dr.CueOut))) + } + if dr.EndOnNext { + attrs = append(attrs, "END-ON-NEXT:YES") + } + if _, err := fmt.Fprintln(w, strings.Join(attrs, ",")); err != nil { + return err + } + return nil +} diff --git a/m3u8/write_test.go b/m3u8/write_test.go @@ -3,6 +3,7 @@ package m3u8 import ( "bufio" "bytes" + "encoding/base64" "testing" "time" ) @@ -32,3 +33,23 @@ func TestEncodeSegDuration(t *testing.T) { t.Errorf("no matching segment duration %s", want) } } + +func TestDateRange(t *testing.T) { + // splice insert from SCTE 35 section 14.2 + out, err := base64.StdEncoding.DecodeString("/DAvAAAAAAAA///wFAVIAACPf+/+c2nALv4AUsz1AAAAAAAKAAhDVUVJAAABNWLbowo=") + if err != nil { + t.Fatal(err) + } + dr := DateRange{ + ID: "break", + CueOut: out, + } + buf := &bytes.Buffer{} + if err := writeDateRange(buf, &dr); err != nil { + t.Fatalf("write date range: %v", err) + } + want := `#EXT-X-DATERANGE:ID="break",SCTE35-OUT=0xfc302f000000000000fffff014054800008f7feffe7369c02efe0052ccf500000000000a0008435545490000013562dba30a` + "\n" + if buf.String() != want { + t.Errorf("encode %v: got %s, want %s", dr, buf.String(), want) + } +}