Skip to content
This repository has been archived by the owner on Dec 8, 2024. It is now read-only.

Commit

Permalink
HLS V7 Tags reader & writer; with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
soldiermoth committed Apr 19, 2019
1 parent a61a310 commit bc5ec7d
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 43 deletions.
15 changes: 15 additions & 0 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.HCDPLevel = v
}
}
case state.tagStreamInf && !strings.HasPrefix(line, "#"):
Expand Down Expand Up @@ -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.HCDPLevel = v
}
}
case strings.HasPrefix(line, "#"): // unknown tags treated as comments
Expand Down
58 changes: 56 additions & 2 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", HCDPLevel: "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", HCDPLevel: "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", HCDPLevel: "TYPE-1", FrameRate: 23.976},
"dolby_720/prog_index.m3u8": {Bandwidth: 5327059, AverageBandwidth: 3385450, Codecs: "dvh1.05.01", Resolution: "1280x720", Captions: "NONE", VideoRange: "PQ", HCDPLevel: "NONE", FrameRate: 23.976},
"dolby_1080/prog_index.m3u8": {Bandwidth: 12876596, AverageBandwidth: 7999361, Codecs: "dvh1.05.03", Resolution: "1920x1080", Captions: "NONE", VideoRange: "PQ", HCDPLevel: "TYPE-0", FrameRate: 23.976},
"dolby_2160/prog_index.m3u8": {Bandwidth: 30041698, AverageBandwidth: 24975091, Codecs: "dvh1.05.06", Resolution: "3840x2160", Captions: "NONE", VideoRange: "PQ", HCDPLevel: "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", HCDPLevel: "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", HCDPLevel: "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", HCDPLevel: "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", HCDPLevel: "NONE"},
"sdr_1080/iframe_index.m3u8": {Bandwidth: 956552, AverageBandwidth: 399790, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "SDR", HCDPLevel: "TYPE-0"},
"sdr_2160/iframe_index.m3u8": {Bandwidth: 1941397, AverageBandwidth: 826971, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "SDR", HCDPLevel: "TYPE-1"},
"dolby_720/iframe_index.m3u8": {Bandwidth: 573073, AverageBandwidth: 232253, Codecs: "dvh1.05.01", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HCDPLevel: "NONE"},
"dolby_1080/iframe_index.m3u8": {Bandwidth: 905037, AverageBandwidth: 365337, Codecs: "dvh1.05.03", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HCDPLevel: "TYPE-0"},
"dolby_2160/iframe_index.m3u8": {Bandwidth: 1893236, AverageBandwidth: 739114, Codecs: "dvh1.05.06", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HCDPLevel: "TYPE-1"},
"hdr10_720/iframe_index.m3u8": {Bandwidth: 572673, AverageBandwidth: 232511, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HCDPLevel: "NONE"},
"hdr10_1080/iframe_index.m3u8": {Bandwidth: 905053, AverageBandwidth: 364552, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HCDPLevel: "TYPE-0"},
"hdr10_2160/iframe_index.m3u8": {Bandwidth: 1895477, AverageBandwidth: 739757, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HCDPLevel: "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 *
****************************/
Expand Down Expand Up @@ -435,7 +489,7 @@ func TestDecodeMediaPlaylistAutoDetectExtend(t *testing.T) {
// Test for FullTimeParse of EXT-X-PROGRAM-DATE-TIME
// We testing ISO/IEC 8601:2004 where we can get time in UTC, UTC with Nanoseconds
// timeZone in formats '±00:00', '±0000', '±00'
// m3u8.FullTimeParse()
// FullTimeParse()
func TestFullTimeParse(t *testing.T) {
var timestamps = []struct {
name string
Expand Down Expand Up @@ -463,7 +517,7 @@ func TestFullTimeParse(t *testing.T) {
// Test for StrictTimeParse of EXT-X-PROGRAM-DATE-TIME
// We testing Strict format of RFC3339 where we can get time in UTC, UTC with Nanoseconds
// timeZone in formats '±00:00', '±0000', '±00'
// m3u8.StrictTimeParse()
// StrictTimeParse()
func TestStrictTimeParse(t *testing.T) {
var timestamps = []struct {
name string
Expand Down
32 changes: 32 additions & 0 deletions sample-playlists/master-with-hlsv7.m3u8
Original file line number Diff line number Diff line change
@@ -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"
20 changes: 15 additions & 5 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.HCDPLevel != "" {
p.buf.WriteString(",HDCP-LEVEL=")
p.buf.WriteString(pl.HCDPLevel)
}
if pl.URI != "" {
p.buf.WriteString(",URI=\"")
p.buf.WriteString(pl.URI)
Expand All @@ -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=\"")
Expand Down Expand Up @@ -226,14 +238,12 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer {
p.buf.WriteString(strconv.FormatFloat(pl.FrameRate, 'f', 3, 64))
}
if pl.VideoRange != "" {
p.buf.WriteString(",VIDEO-RANGE=\"")
p.buf.WriteString(",VIDEO-RANGE=")
p.buf.WriteString(pl.VideoRange)
p.buf.WriteRune('"')
}
if pl.HCDPLevel != "" {
p.buf.WriteString(",HDCP-LEVEL=\"")
p.buf.WriteString(",HDCP-LEVEL=")
p.buf.WriteString(pl.HCDPLevel)
p.buf.WriteRune('"')
}

p.buf.WriteRune('\n')
Expand Down
53 changes: 17 additions & 36 deletions writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,42 +857,6 @@ func TestMasterSetVersion(t *testing.T) {
}
}

// Create new master playlist supporting CLOSED-CAPTIONS=NONE
func TestNewMasterPlaylistWithV7Params(t *testing.T) {
m := NewMasterPlaylist()

vp := &VariantParams{
ProgramId: 0,
Bandwidth: 8000,
Codecs: "avc1",
Resolution: "1280x720",
Audio: "audio0",
Captions: "NONE",
VideoRange: "SDR",
FrameRate: 29.97,
HCDPLevel: "NONE",
}

p, err := NewMediaPlaylist(1, 1)
if err != nil {
t.Fatalf("Create media playlist failed: %s", err)
}
m.Append(fmt.Sprintf("eng_rendition_rendition.m3u8"), p, *vp)

expected := "CLOSED-CAPTIONS=NONE"
if !strings.Contains(m.String(), expected) {
t.Fatalf("Master playlist did not contain: %s\nMaster Playlist:\n%v", expected, m.String())
}
// quotes need to be include if not eq NONE
vp.Captions = "CC1"
m2 := NewMasterPlaylist()
m2.Append(fmt.Sprintf("eng_rendition_rendition.m3u8"), p, *vp)
expected = `CLOSED-CAPTIONS="CC1"`
if !strings.Contains(m2.String(), expected) {
t.Fatalf("Master playlist did not contain: %s\nMaster Playlist:\n%v", expected, m2.String())
}
}

/******************************
* Code generation examples *
******************************/
Expand Down Expand Up @@ -975,6 +939,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", HCDPLevel: "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", HCDPLevel: "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)
Expand Down

0 comments on commit bc5ec7d

Please sign in to comment.