commit 6c65438854fef924686286ebc2b20dbfd647c52b
parent 314f50a588e4e749071e0aa8d51a1b4aa16fad7b
Author: Oliver Lowe <o@olowe.co>
Date: Thu, 23 May 2024 19:39:41 +1000
m3u8: handle writing master playlist renditions
Enough for Tears of Steel from test-streams.mux.dev.
Diffstat:
2 files changed, 70 insertions(+), 10 deletions(-)
diff --git a/m3u8/parse.go b/m3u8/parse.go
@@ -10,13 +10,14 @@ import (
)
const (
- tagHead = tagStart + "M3U"
- tagVersion = "#EXT-X-VERSION"
- tagVariant = "#EXT-X-STREAM-INF"
- tagRendition = "#EXT-X-MEDIA"
- tagPlaylistType = "#EXT-X-PLAYLIST-TYPE" // RFC 8216, 4.4.3.5
- tagTargetDuration = "#EXT-X-TARGETDURATION" // RFC 8216, 4.4.3.1
- tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4
+ tagHead = tagStart + "M3U"
+ tagVersion = "#EXT-X-VERSION"
+ tagVariant = "#EXT-X-STREAM-INF"
+ tagRendition = "#EXT-X-MEDIA"
+ tagPlaylistType = "#EXT-X-PLAYLIST-TYPE" // RFC 8216, 4.4.3.5
+ tagTargetDuration = "#EXT-X-TARGETDURATION" // RFC 8216, 4.4.3.1
+ tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4
+ tagIndependentSegments = "#EXT-X-INDEPENDENT-SEGMENTS" // RFC 8216, 4.3.5.1
)
func ParsePlaylist(rd io.Reader) (*Playlist, error) {
@@ -46,6 +47,8 @@ func ParsePlaylist(rd io.Reader) (*Playlist, error) {
if err != nil {
return p, fmt.Errorf("parse playlist version: %w", err)
}
+ case tagIndependentSegments:
+ p.IndependentSegments = true
case tagVariant:
variant, err := parseVariant(lex.items)
if err != nil {
@@ -260,7 +263,7 @@ func parseRendition(items chan item) (*Rendition, error) {
case "CHARACTERISTICS":
rend.Characteristics = strings.Split(it.val, ",")
case "CHANNELS":
- rend.Channels = strings.Split(it.val, "/")
+ rend.Channels = strings.Split(strings.Trim(it.val, `"`), "/")
default:
return nil, fmt.Errorf("unknown rendition attribute %s", attr.val)
}
diff --git a/m3u8/write.go b/m3u8/write.go
@@ -18,6 +18,9 @@ func Encode(w io.Writer, p *Playlist) error {
if p.Type != PlaylistNone {
fmt.Fprintf(w, "%s:%s\n", tagPlaylistType, p.Type)
}
+ if p.IndependentSegments {
+ fmt.Fprintln(w, tagIndependentSegments)
+ }
if p.TargetDuration > 0 {
fmt.Fprintf(w, "%s:%d\n", tagTargetDuration, p.TargetDuration/time.Second)
}
@@ -36,19 +39,73 @@ func Encode(w io.Writer, p *Playlist) error {
fmt.Fprintln(w, seg.URI)
}
+ for _, r := range p.Media {
+ if r.Name == "" {
+ return fmt.Errorf("empty name")
+ }
+ fmt.Fprintf(w, tagRendition+":")
+ // TODO(otl): use string slice, then strings.Join(s, ",")
+ // then we don't worry about errors from w.Write
+ fmt.Fprintf(w, "NAME=%q,", r.Name)
+ rname := fmt.Sprintf("rendition %s", r.Name)
+ if r.Type > MediaClosedCaptions {
+ return fmt.Errorf("%s: unknown type %s", rname, r.Type)
+ }
+ fmt.Fprintf(w, "TYPE=%s,", r.Type)
+ if r.URI != "" {
+ fmt.Fprintf(w, "URI=%q,", r.URI)
+ }
+ if r.Group == "" {
+ return fmt.Errorf("%s: empty group", rname)
+ }
+ fmt.Fprintf(w, "GROUP-ID=%q,", r.Group)
+ if r.Language != "" {
+ fmt.Fprintf(w, "LANGUAGE=%q,", r.Language)
+ }
+ if r.AssocLanguage != "" {
+ fmt.Fprintf(w, "ASSOC-LANGUAGE=%q,", r.AssocLanguage)
+ }
+ if r.Default {
+ fmt.Fprint(w, "DEFAULT=YES,")
+ }
+ if r.AutoSelect {
+ fmt.Fprint(w, "AUTOSELECT=YES,")
+ }
+ if r.Forced {
+ fmt.Fprint(w, "FORCED=YES,")
+ }
+
+ if r.Type != MediaClosedCaptions && r.InstreamID != nil {
+ return fmt.Errorf("%s: instream-id set but type is %s", rname, r.Type)
+ }
+ if r.Type == MediaClosedCaptions {
+ if r.InstreamID == nil {
+ return fmt.Errorf("%s: nil instream-id", rname)
+ }
+ fmt.Fprintf(w, "INSTREAM-ID=%q,", r.InstreamID)
+ }
+ if len(r.Characteristics) > 0 {
+ fmt.Fprintf(w, "CHARACTERISTICS=%q,", strings.Join(r.Characteristics, ","))
+ }
+ if len(r.Channels) > 0 {
+ fmt.Fprintf(w, "CHANNELS=%q,", strings.Join(r.Channels, "/"))
+ }
+ fmt.Fprintln(w)
+ }
+
for _, v := range p.Variants {
fmt.Fprint(w, tagVariant+":")
if v.Bandwidth > 0 {
fmt.Fprintf(w, "BANDWIDTH=%d,", v.Bandwidth)
}
if v.AverageBandwidth > 0 {
- fmt.Fprintf(w, "AVERAGE-BANDWIDTH=%d", v.AverageBandwidth)
+ 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])
+ fmt.Fprintf(w, "RESOLUTION=%dx%d,", v.Resolution[0], v.Resolution[1])
}
fmt.Fprintln(w)
fmt.Fprintln(w, v.URI)