diff --git a/reader.go b/reader.go index b19324eb..0fcf2bdf 100644 --- a/reader.go +++ b/reader.go @@ -65,6 +65,7 @@ func (p *MasterPlaylist) decode(buf *bytes.Buffer, strict bool) error { var eof bool state := new(decodingState) + state.alternatives = make(map[string][]*Alternative) for !eof { line, err := buf.ReadString('\n') @@ -81,9 +82,56 @@ func (p *MasterPlaylist) decode(buf *bytes.Buffer, strict bool) error { if strict && !state.m3u { return errors.New("#EXTM3U absent") } + + p.setAlternatives(state) + return nil } +// Set alternatives for variants in master playlist. Internal function. +func (p *MasterPlaylist) setAlternatives(state *decodingState) { + for _, variant := range p.Variants { + alts := []*Alternative{} + + if variant.Audio != "" { + toSearch := state.alternatives["AUDIO"] + for _, alt := range toSearch { + if variant.Audio == alt.GroupId { + alts = append(alts, alt) + } + } + } + if variant.Video != "" { + toSearch := state.alternatives["VIDEO"] + for _, alt := range toSearch { + if variant.Video == alt.GroupId { + alts = append(alts, alt) + } + } + } + if variant.Subtitles != "" { + toSearch := state.alternatives["SUBTITLES"] + for _, alt := range toSearch { + if variant.Subtitles == alt.GroupId { + alts = append(alts, alt) + } + } + } + if variant.Captions != "" { + toSearch := state.alternatives["CLOSED-CAPTIONS"] + for _, alt := range toSearch { + if variant.Captions == alt.GroupId { + alts = append(alts, alt) + } + } + } + + if len(alts) != 0 { + variant.Alternatives = alts + } + } +} + // Decode parses a media playlist passed from the buffer. If `strict` // parameter is true then return first syntax error. func (p *MediaPlaylist) Decode(data bytes.Buffer, strict bool) error { @@ -190,6 +238,7 @@ func decode(buf *bytes.Buffer, strict bool, customDecoders []CustomDecoder) (Pla var err error state := new(decodingState) + state.alternatives = make(map[string][]*Alternative) // create the map for alternatives wv := new(WV) master = NewMasterPlaylist() @@ -240,6 +289,7 @@ func decode(buf *bytes.Buffer, strict bool, customDecoders []CustomDecoder) (Pla switch state.listType { case MASTER: + master.setAlternatives(state) return master, MASTER, nil case MEDIA: if media.Closed || media.MediaType == EVENT { @@ -331,15 +381,11 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st alt.URI = v } } - state.alternatives = append(state.alternatives, &alt) + state.alternatives[alt.Type] = append(state.alternatives[alt.Type], &alt) case !state.tagStreamInf && strings.HasPrefix(line, "#EXT-X-STREAM-INF:"): state.tagStreamInf = true state.listType = MASTER state.variant = new(Variant) - if len(state.alternatives) > 0 { - state.variant.Alternatives = state.alternatives - state.alternatives = nil - } p.Variants = append(p.Variants, state.variant) for k, v := range decodeParamsLine(line[18:]) { switch k { @@ -395,10 +441,6 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st state.listType = MASTER state.variant = new(Variant) state.variant.Iframe = true - if len(state.alternatives) > 0 { - state.variant.Alternatives = state.alternatives - state.alternatives = nil - } p.Variants = append(p.Variants, state.variant) for k, v := range decodeParamsLine(line[26:]) { switch k { diff --git a/reader_test.go b/reader_test.go index 8d60b16c..15094320 100644 --- a/reader_test.go +++ b/reader_test.go @@ -102,6 +102,50 @@ func TestDecodeMasterPlaylistWithAlternatives(t *testing.T) { // fmt.Println(p.Encode().String()) } +func TestDecodeMasterPlaylistWithAlternativesDifferentOrder(t *testing.T) { + f, err := os.Open("sample-playlists/master-with-alternatives-diff-order.m3u8") + if err != nil { + t.Fatal(err) + } + p := NewMasterPlaylist() + err = p.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + // check parsed values + if p.ver != 3 { + t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) + } + if len(p.Variants) != 7 { + t.Fatal("not all variants in master playlist parsed") + } + + for i, v := range p.Variants { + if i == 0 && len(v.Alternatives) != 3 { + t.Errorf("Expect 3 alternatives at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 1 && len(v.Alternatives) != 4 { + t.Errorf("Expect 4 alternatives at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 2 && len(v.Alternatives) != 4 { + t.Errorf("Expect 4 alternatives at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 3 && len(v.Alternatives) != 1 { + t.Errorf("Expect 1 alternative at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 4 && len(v.Alternatives) != 0 { + t.Errorf("Expect 0 alternatives at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 5 && len(v.Alternatives) != 1 { + t.Errorf("Expect 1 alternative at %d but got %d\n", i, len(v.Alternatives)) + } + if i == 6 && len(v.Alternatives) != 1 { + t.Errorf("Expect 1 alternative at %d but got %d\n", i, len(v.Alternatives)) + } + } + +} + func TestDecodeMasterPlaylistWithClosedCaptionEqNone(t *testing.T) { f, err := os.Open("sample-playlists/master-with-closed-captions-eq-none.m3u8") if err != nil { diff --git a/sample-playlists/master-with-alternatives-diff-order.m3u8 b/sample-playlists/master-with-alternatives-diff-order.m3u8 new file mode 100644 index 00000000..6b9341db --- /dev/null +++ b/sample-playlists/master-with-alternatives-diff-order.m3u8 @@ -0,0 +1,54 @@ +#EXTM3U + +# Videos +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" + +# Audios +#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="a1",NAME="Audio1",DEFAULT=YES,URI="audio/a1.m3u8" + +# Captions +#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES + +# Subtitles +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,FORCED=NO,URI="s1/en/prog_index.m3u8" + +# External Media with no usage +#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub2",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,FORCED=NO,URI="s1/en/prog_index.m3u8" + +# Video +#EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="low" +low/main/audio-video.m3u8 + +# Video and CC +#EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="mid",CLOSED-CAPTIONS="cc" +mid/main/audio-video.m3u8 + +# Video and Subtitles +#EXT-X-STREAM-INF:BANDWIDTH=7680000,VIDEO="hi",SUBTITLES="sub1" +hi/main/audio-video.m3u8 + +# Audio +#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="a1" +main/audio-only.m3u8 + +# Non-existent group-id +#EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="missing" +missing-id/main/audio-video.m3u8 + +# One group-id present and one non-existent group id +#EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="missing",AUDIO="a1" +missing-and-valid-id/main/audio-video.m3u8 + +# Stream coming before external media +#EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="v1" +pre-media/main/audio-video.m3u8 + +#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="v1",NAME="AfterStream",DEFAULT=NO,URI="after-stream/audio-video.m3u8" \ No newline at end of file diff --git a/structure.go b/structure.go index eb5d01a2..b05ef2e4 100644 --- a/structure.go +++ b/structure.go @@ -327,7 +327,7 @@ type decodingState struct { duration float64 title string variant *Variant - alternatives []*Alternative + alternatives map[string][]*Alternative xkey *Key xmap *Map scte *SCTE