streaming

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

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:
Mm3u8/parse.go | 19+++++++++++--------
Mm3u8/write.go | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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)