diff --git a/reader.go b/reader.go index 97cc81dd..8755c393 100644 --- a/reader.go +++ b/reader.go @@ -532,6 +532,12 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l return err } } + if state.tagGap { + state.tagGap = false + if err = p.SetGap(); strict && err != nil { + return err + } + } if state.tagProgramDateTime && p.Count() > 0 { state.tagProgramDateTime = false if err = p.SetProgramDateTime(state.programDateTime); strict && err != nil { @@ -768,6 +774,9 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l case !state.tagDiscontinuity && strings.HasPrefix(line, "#EXT-X-DISCONTINUITY"): state.tagDiscontinuity = true state.listType = MEDIA + case !state.tagGap && strings.HasPrefix(line, "#EXT-X-GAP"): + state.tagGap = true + state.listType = MEDIA case strings.HasPrefix(line, "#EXT-X-I-FRAMES-ONLY"): state.listType = MEDIA p.Iframe = true diff --git a/reader_test.go b/reader_test.go index 6952d525..b2b0d9c2 100644 --- a/reader_test.go +++ b/reader_test.go @@ -897,6 +897,30 @@ func TestDecodeMediaPlaylistWithCustomTags(t *testing.T) { } } +func TestDecodeMediaPlaylistWithGap(t *testing.T) { + f, err := os.Open("sample-playlists/media-playlist-with-gap.m3u8") + if err != nil { + t.Fatal(err) + } + p, listType, err := DecodeFrom(bufio.NewReader(f), true) + if err != nil { + t.Fatal(err) + } + pp := p.(*MediaPlaylist) + CheckType(t, pp) + if listType != MEDIA { + t.Error("Sample not recognized as media playlist.") + } + var seqId, idx uint + for seqId, idx = 0, 0; idx < pp.Count(); seqId, idx = seqId+1, idx+1 { + if pp.Segments[idx].Duration == 0 { + if !pp.Segments[idx].Gap { + t.Error("Expected Gap set to true on segment with duration 0.") + } + } + } +} + /*************************** * Code parsing examples * ***************************/ diff --git a/sample-playlists/media-playlist-with-gap.m3u8 b/sample-playlists/media-playlist-with-gap.m3u8 new file mode 100644 index 00000000..2f6ad1ad --- /dev/null +++ b/sample-playlists/media-playlist-with-gap.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.0, +1.ts +#EXTINF:8.0, +2.ts +#EXT-X-GAP +#EXTINF:0.0, no desc +3.ts +#EXT-X-DISCONTINUITY +#EXTINF:10.0, +4.ts +#EXTINF:10.0, +5.ts +#EXTINF:10.0, +6.ts diff --git a/structure.go b/structure.go index 9fe41a26..1de9b788 100644 --- a/structure.go +++ b/structure.go @@ -214,6 +214,7 @@ 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) + Gap bool // EXT-X-GAP indicates that the segment URI to which it applies does not contain media data and SHOULD NOT be loaded by clients DateRange []*DateRange // EXT-X-DATERANGE tags SCTE *SCTE // SCTE-35 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 @@ -335,6 +336,7 @@ type decodingState struct { tagSCTE35 bool tagRange bool tagDiscontinuity bool + tagGap bool tagProgramDateTime bool tagKey bool tagMap bool diff --git a/writer.go b/writer.go index bc7b0614..9abab9e6 100644 --- a/writer.go +++ b/writer.go @@ -710,6 +710,9 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { if seg.Discontinuity { p.buf.WriteString("#EXT-X-DISCONTINUITY\n") } + if seg.Gap { + p.buf.WriteString("#EXT-X-GAP\n") + } // ignore segment Map if default playlist Map is present if p.Map == nil && seg.Map != nil { p.buf.WriteString("#EXT-X-MAP:") @@ -932,6 +935,17 @@ func (p *MediaPlaylist) SetDiscontinuity() error { return nil } +// SetGap sets gap flag for the current media +// segment. EXT-X-GAP indicates that the segment URI to which it applies +// does not contain media data and SHOULD NOT be loaded by clients/ +func (p *MediaPlaylist) SetGap() error { + if p.count == 0 { + return errors.New("playlist is empty") + } + p.Segments[p.last()].Gap = true + return nil +} + // SetProgramDateTime sets program date and time for the current media // segment. EXT-X-PROGRAM-DATE-TIME tag associates the first sample of // a media segment with an absolute date and/or time. It applies only diff --git a/writer_test.go b/writer_test.go index 7b74c383..755b936b 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1013,6 +1013,40 @@ func TestEncodeMediaPlaylistDateRangeTags(t *testing.T) { } } +// Create new media playlist +// Add three segments to media playlist +// Set gap tag for the 2nd segment. +func TestGapForMediaPlaylist(t *testing.T) { + var e error + p, e := NewMediaPlaylist(3, 4) + if e != nil { + t.Fatalf("Create media playlist failed: %s", e) + } + p.Close() + if e = p.Append("test01.ts", 5.0, ""); e != nil { + t.Errorf("Add 1st segment to a media playlist failed: %s", e) + } + if e = p.Append("test02.ts", 0.0, ""); e != nil { + t.Errorf("Add 2nd segment to a media playlist failed: %s", e) + } + if e = p.SetGap(); e != nil { + t.Error("Can't set gap tag") + } + if e = p.Append("test03.ts", 6.0, ""); e != nil { + t.Errorf("Add 3nd segment to a media playlist failed: %s", e) + } + if e = p.SetDiscontinuity(); e != nil { + t.Error("Can't set discontinuity tag") + } + encoded := p.Encode().String() + expectedStrings := []string{"#EXT-X-GAP"} + for _, expected := range expectedStrings { + if !strings.Contains(encoded, expected) { + t.Fatalf("Media playlist does not contain tag: %s\nMedia Playlist:\n%v", expected, encoded) + } + } +} + /****************************** * Code generation examples * ******************************/