diff --git a/reader.go b/reader.go index 0b449fc0..e8240698 100644 --- a/reader.go +++ b/reader.go @@ -312,6 +312,10 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st if state.variant.FrameRate, err = strconv.ParseFloat(v, 64); strict && err != nil { return err } + case "VIDEO-RANGE": + state.variant.VideoRange = v + case "HDCP-LEVEL": + state.variant.HDCPLevel = v } } case state.tagStreamInf && !strings.HasPrefix(line, "#"): @@ -352,6 +356,17 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st state.variant.Audio = v case "VIDEO": state.variant.Video = v + case "AVERAGE-BANDWIDTH": + var val int + val, err = strconv.Atoi(v) + if strict && err != nil { + return err + } + state.variant.AverageBandwidth = uint32(val) + case "VIDEO-RANGE": + state.variant.VideoRange = v + case "HDCP-LEVEL": + state.variant.HDCPLevel = v } } case strings.HasPrefix(line, "#"): // unknown tags treated as comments diff --git a/reader_test.go b/reader_test.go index 230badbf..8fd73e93 100644 --- a/reader_test.go +++ b/reader_test.go @@ -233,6 +233,60 @@ func TestDecodeMasterPlaylistWithIndependentSegments(t *testing.T) { } } +func TestDecodeMasterWithHLSV7(t *testing.T) { + f, err := os.Open("sample-playlists/master-with-hlsv7.m3u8") + if err != nil { + t.Fatal(err) + } + p := NewMasterPlaylist() + err = p.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + var unexpected []*Variant + expected := map[string]VariantParams{ + "sdr_720/prog_index.m3u8": {Bandwidth: 3971374, AverageBandwidth: 2778321, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "NONE", FrameRate: 23.976}, + "sdr_1080/prog_index.m3u8": {Bandwidth: 10022043, AverageBandwidth: 6759875, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "TYPE-0", FrameRate: 23.976}, + "sdr_2160/prog_index.m3u8": {Bandwidth: 28058971, AverageBandwidth: 20985770, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "TYPE-1", FrameRate: 23.976}, + "dolby_720/prog_index.m3u8": {Bandwidth: 5327059, AverageBandwidth: 3385450, Codecs: "dvh1.05.01", Resolution: "1280x720", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "NONE", FrameRate: 23.976}, + "dolby_1080/prog_index.m3u8": {Bandwidth: 12876596, AverageBandwidth: 7999361, Codecs: "dvh1.05.03", Resolution: "1920x1080", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-0", FrameRate: 23.976}, + "dolby_2160/prog_index.m3u8": {Bandwidth: 30041698, AverageBandwidth: 24975091, Codecs: "dvh1.05.06", Resolution: "3840x2160", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-1", FrameRate: 23.976}, + "hdr10_720/prog_index.m3u8": {Bandwidth: 5280654, AverageBandwidth: 3320040, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "NONE", FrameRate: 23.976}, + "hdr10_1080/prog_index.m3u8": {Bandwidth: 12886714, AverageBandwidth: 7964551, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-0", FrameRate: 23.976}, + "hdr10_2160/prog_index.m3u8": {Bandwidth: 29983769, AverageBandwidth: 24833402, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-1", FrameRate: 23.976}, + "sdr_720/iframe_index.m3u8": {Bandwidth: 593626, AverageBandwidth: 248586, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Iframe: true, VideoRange: "SDR", HDCPLevel: "NONE"}, + "sdr_1080/iframe_index.m3u8": {Bandwidth: 956552, AverageBandwidth: 399790, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "SDR", HDCPLevel: "TYPE-0"}, + "sdr_2160/iframe_index.m3u8": {Bandwidth: 1941397, AverageBandwidth: 826971, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "SDR", HDCPLevel: "TYPE-1"}, + "dolby_720/iframe_index.m3u8": {Bandwidth: 573073, AverageBandwidth: 232253, Codecs: "dvh1.05.01", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HDCPLevel: "NONE"}, + "dolby_1080/iframe_index.m3u8": {Bandwidth: 905037, AverageBandwidth: 365337, Codecs: "dvh1.05.03", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-0"}, + "dolby_2160/iframe_index.m3u8": {Bandwidth: 1893236, AverageBandwidth: 739114, Codecs: "dvh1.05.06", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-1"}, + "hdr10_720/iframe_index.m3u8": {Bandwidth: 572673, AverageBandwidth: 232511, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HDCPLevel: "NONE"}, + "hdr10_1080/iframe_index.m3u8": {Bandwidth: 905053, AverageBandwidth: 364552, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-0"}, + "hdr10_2160/iframe_index.m3u8": {Bandwidth: 1895477, AverageBandwidth: 739757, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-1"}, + } + for _, variant := range p.Variants { + var found bool + for uri, vp := range expected { + if variant == nil || variant.URI != uri { + continue + } + if reflect.DeepEqual(variant.VariantParams, vp) { + delete(expected, uri) + found = true + } + } + if !found { + unexpected = append(unexpected, variant) + } + } + for uri, expect := range expected { + t.Errorf("not found: uri=%q %+v", uri, expect) + } + for _, unexpect := range unexpected { + t.Errorf("found but not expecting:%+v", unexpect) + } +} + /**************************** * Begin Test MediaPlaylist * ****************************/ diff --git a/sample-playlists/master-with-hlsv7.m3u8 b/sample-playlists/master-with-hlsv7.m3u8 new file mode 100644 index 00000000..75ff36cd --- /dev/null +++ b/sample-playlists/master-with-hlsv7.m3u8 @@ -0,0 +1,32 @@ +# https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices/hls_authoring_specification_for_apple_devices_appendices +# +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2778321,BANDWIDTH=3971374,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE +sdr_720/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6759875,BANDWIDTH=10022043,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 +sdr_1080/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=20985770,BANDWIDTH=28058971,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 +sdr_2160/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3385450,BANDWIDTH=5327059,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE +dolby_720/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7999361,BANDWIDTH=12876596,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 +dolby_1080/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24975091,BANDWIDTH=30041698,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 +dolby_2160/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3320040,BANDWIDTH=5280654,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE +hdr10_720/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7964551,BANDWIDTH=12886714,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 +hdr10_1080/prog_index.m3u8 +#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24833402,BANDWIDTH=29983769,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 +hdr10_2160/prog_index.m3u8 +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=248586,BANDWIDTH=593626,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="sdr_720/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=399790,BANDWIDTH=956552,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="sdr_1080/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=826971,BANDWIDTH=1941397,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="sdr_2160/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232253,BANDWIDTH=573073,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="dolby_720/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=365337,BANDWIDTH=905037,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="dolby_1080/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739114,BANDWIDTH=1893236,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="dolby_2160/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232511,BANDWIDTH=572673,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="hdr10_720/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=364552,BANDWIDTH=905053,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="hdr10_1080/iframe_index.m3u8" +#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739757,BANDWIDTH=1895477,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="hdr10_2160/iframe_index.m3u8" \ No newline at end of file diff --git a/structure.go b/structure.go index 7bd0ac2f..1cb1bb85 100644 --- a/structure.go +++ b/structure.go @@ -168,11 +168,13 @@ type VariantParams struct { Resolution string Audio string // EXT-X-STREAM-INF only Video string - Subtitles string // EXT-X-STREAM-INF only - Captions string // EXT-X-STREAM-INF only - Name string // EXT-X-STREAM-INF only (non standard Wowza/JWPlayer extension to name the variant/quality in UA) + Subtitles string // EXT-X-STREAM-INF only + Captions string // EXT-X-STREAM-INF only + Name string // EXT-X-STREAM-INF only (non standard Wowza/JWPlayer extension to name the variant/quality in UA) + Iframe bool // EXT-X-I-FRAME-STREAM-INF + VideoRange string + HDCPLevel string FrameRate float64 // EXT-X-STREAM-INF - Iframe bool // EXT-X-I-FRAME-STREAM-INF Alternatives []*Alternative // EXT-X-MEDIA } diff --git a/writer.go b/writer.go index 8692fd66..ffea44a3 100644 --- a/writer.go +++ b/writer.go @@ -153,6 +153,10 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10)) p.buf.WriteString(",BANDWIDTH=") p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) + if pl.AverageBandwidth != 0 { + p.buf.WriteString(",AVERAGE-BANDWIDTH=") + p.buf.WriteString(strconv.FormatUint(uint64(pl.AverageBandwidth), 10)) + } if pl.Codecs != "" { p.buf.WriteString(",CODECS=\"") p.buf.WriteString(pl.Codecs) @@ -167,6 +171,14 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(pl.Video) p.buf.WriteRune('"') } + if pl.VideoRange != "" { + p.buf.WriteString(",VIDEO-RANGE=") + p.buf.WriteString(pl.VideoRange) + } + if pl.HDCPLevel != "" { + p.buf.WriteString(",HDCP-LEVEL=") + p.buf.WriteString(pl.HDCPLevel) + } if pl.URI != "" { p.buf.WriteString(",URI=\"") p.buf.WriteString(pl.URI) @@ -180,7 +192,7 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) if pl.AverageBandwidth != 0 { p.buf.WriteString(",AVERAGE-BANDWIDTH=") - p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) + p.buf.WriteString(strconv.FormatUint(uint64(pl.AverageBandwidth), 10)) } if pl.Codecs != "" { p.buf.WriteString(",CODECS=\"") @@ -225,6 +237,15 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(",FRAME-RATE=") p.buf.WriteString(strconv.FormatFloat(pl.FrameRate, 'f', 3, 64)) } + if pl.VideoRange != "" { + p.buf.WriteString(",VIDEO-RANGE=") + p.buf.WriteString(pl.VideoRange) + } + if pl.HDCPLevel != "" { + p.buf.WriteString(",HDCP-LEVEL=") + p.buf.WriteString(pl.HDCPLevel) + } + p.buf.WriteRune('\n') p.buf.WriteString(pl.URI) if p.Args != "" { diff --git a/writer_test.go b/writer_test.go index 9784b290..76db9899 100644 --- a/writer_test.go +++ b/writer_test.go @@ -954,6 +954,23 @@ func ExampleMasterPlaylist_String() { // chunklist2.m3u8 } +func ExampleMasterPlaylist_String_with_hlsv7() { + m := NewMasterPlaylist() + m.SetVersion(7) + m.SetIndependentSegments(true) + p, _ := NewMediaPlaylist(3, 5) + m.Append("hdr10_1080/prog_index.m3u8", p, VariantParams{AverageBandwidth: 7964551, Bandwidth: 12886714, VideoRange: "PQ", Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", FrameRate: 23.976, Captions: "NONE", HDCPLevel: "TYPE-0"}) + m.Append("hdr10_1080/iframe_index.m3u8", p, VariantParams{Iframe: true, AverageBandwidth: 364552, Bandwidth: 905053, VideoRange: "PQ", Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", HDCPLevel: "TYPE-0"}) + fmt.Printf("%s", m) + // Output: + // #EXTM3U + // #EXT-X-VERSION:7 + // #EXT-X-INDEPENDENT-SEGMENTS + // #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=12886714,AVERAGE-BANDWIDTH=7964551,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,CLOSED-CAPTIONS=NONE,FRAME-RATE=23.976,VIDEO-RANGE=PQ,HDCP-LEVEL=TYPE-0 + // hdr10_1080/prog_index.m3u8 + // #EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=905053,AVERAGE-BANDWIDTH=364552,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,VIDEO-RANGE=PQ,HDCP-LEVEL=TYPE-0,URI="hdr10_1080/iframe_index.m3u8" +} + func ExampleMediaPlaylist_Segments_scte35_oatcls() { f, _ := os.Open("sample-playlists/media-playlist-with-oatcls-scte35.m3u8") p, _, _ := DecodeFrom(bufio.NewReader(f), true)