diff --git a/AUTHORS b/AUTHORS index d3bbdd8a..f4cdd037 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,6 +12,7 @@ to the project. They listed below in an alphabetical order: - Kz26 - Makombo - Scott Kidder +- Vishal Kumar Tuniki - Zac Shenker If you want to be added to this list (or removed for any reason) diff --git a/reader.go b/reader.go index 2afcb3fe..2b3b79e4 100644 --- a/reader.go +++ b/reader.go @@ -443,6 +443,20 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l return fmt.Errorf("Byterange sub-range offset value parsing error: %s", err) } } + case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-SCTE35:"): + state.tagSCTE35 = true + state.listType = MEDIA + state.scte = new(SCTE) + for attribute, value := range decodeParamsLine(line[12:]) { + switch attribute { + case "CUE": + state.scte.Cue = value + case "ID": + state.scte.ID = value + case "TIME": + state.scte.Time, _ = strconv.ParseFloat(value, 64) + } + } case !state.tagInf && strings.HasPrefix(line, "#EXTINF:"): state.tagInf = true state.listType = MEDIA @@ -471,6 +485,13 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l } state.tagRange = false } + if state.tagSCTE35 { + state.tagSCTE35 = false + scte := *state.scte + if err = p.SetSCTE(scte.Cue, scte.ID, scte.Time); strict && err != nil { + return err + } + } if state.tagDiscontinuity { state.tagDiscontinuity = false if err = p.SetDiscontinuity(); strict && err != nil { diff --git a/reader_test.go b/reader_test.go index 2e2c83dc..2812c38b 100644 --- a/reader_test.go +++ b/reader_test.go @@ -243,3 +243,51 @@ func ExampleMediaPlaylist_DurationAsInt() { // #EXTINF:10, // movieB.ts } + +func TestMediaPlaylistWithSCTE35Tag(t *testing.T) { + test_cases := []struct { + playlistLocation string + expectedSCTEIndex int + expectedSCTECue string + expectedSCTEID string + expectedSCTETime float64 + }{ + { + "sample-playlists/media-playlist-with-scte35.m3u8", + 2, + "/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==", + "123", + 123.12, + }, + { + "sample-playlists/media-playlist-with-scte35-1.m3u8", + 1, + "/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAA", + "", + 0, + }, + } + for _, c := range test_cases { + f, _ := os.Open(c.playlistLocation) + playlist, _, _ := DecodeFrom(bufio.NewReader(f), true) + mediaPlaylist := playlist.(*MediaPlaylist) + for index, item := range mediaPlaylist.Segments { + if item == nil { + break + } + if index != c.expectedSCTEIndex && item.SCTE != nil { + t.Error("Not expecting SCTE information on this segment") + } else if index == c.expectedSCTEIndex && item.SCTE == nil { + t.Error("Expecting SCTE information on this segment") + } else if index == c.expectedSCTEIndex && item.SCTE != nil { + if (*item.SCTE).Cue != c.expectedSCTECue { + t.Error("Expected ", c.expectedSCTECue, " got ", (*item.SCTE).Cue) + } else if (*item.SCTE).ID != c.expectedSCTEID { + t.Error("Expected ", c.expectedSCTEID, " got ", (*item.SCTE).ID) + } else if (*item.SCTE).Time != c.expectedSCTETime { + t.Error("Expected ", c.expectedSCTETime, " got ", (*item.SCTE).Time) + } + } + } + } +} diff --git a/sample-playlists/media-playlist-with-scte35-1.m3u8 b/sample-playlists/media-playlist-with-scte35-1.m3u8 new file mode 100644 index 00000000..fea864c5 --- /dev/null +++ b/sample-playlists/media-playlist-with-scte35-1.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.000, +media0.ts +#EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAA" +#EXTINF:10.000, +media1.ts +#EXTINF:10.000, +media2.ts diff --git a/sample-playlists/media-playlist-with-scte35.m3u8 b/sample-playlists/media-playlist-with-scte35.m3u8 new file mode 100644 index 00000000..f47c907d --- /dev/null +++ b/sample-playlists/media-playlist-with-scte35.m3u8 @@ -0,0 +1,11 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.000, +media0.ts +#EXTINF:10.000, +media1.ts +#EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==", ID="123", TIME=123.12 +#EXTINF:10.000, +media2.ts diff --git a/structure.go b/structure.go index afa3aa9c..fbdebde2 100644 --- a/structure.go +++ b/structure.go @@ -188,9 +188,16 @@ type MediaSegment struct { Key *Key // EXT-X-KEY displayed before the segment and means changing of encryption key (in theory each segment may have own key) Map *Map // EXT-X-MAP displayed before the segment Discontinuity bool // EXT-X-DISCONTINUITY indicates an encoding discontinuity between the media segment that follows it and the one that preceded it (i.e. file format, number and type of tracks, encoding parameters, encoding sequence, timestamp sequence) + SCTE *SCTE // EXT-SCTE35 used for Ad signaling in HLS ProgramDateTime time.Time // EXT-X-PROGRAM-DATE-TIME tag associates the first sample of a media segment with an absolute date and/or time } +type SCTE struct { + Cue string + ID string + Time float64 +} + // This structure represents information about stream encryption. // // Realizes EXT-X-KEY tag. @@ -252,6 +259,7 @@ type decodingState struct { tagStreamInf bool tagIframeStreamInf bool tagInf bool + tagSCTE35 bool tagRange bool tagDiscontinuity bool tagProgramDateTime bool @@ -264,4 +272,5 @@ type decodingState struct { variant *Variant xkey *Key xmap *Map + scte *SCTE } diff --git a/writer.go b/writer.go index 9f557cc8..c9038143 100644 --- a/writer.go +++ b/writer.go @@ -609,6 +609,14 @@ func (p *MediaPlaylist) SetRange(limit, offset int64) error { return nil } +func (p *MediaPlaylist) SetSCTE(cue string, id string, time float64) error { + if p.count == 0 { + return errors.New("playlist is empty") + } + p.Segments[(p.tail-1)%p.capacity].SCTE = &SCTE{cue, id, time} + return nil +} + // Set discontinuity flag for the current media segment. // EXT-X-DISCONTINUITY indicates an encoding discontinuity between the media segment // that follows it and the one that preceded it (i.e. file format, number and type of tracks,