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:
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)
+ }
+}