diff --git a/reader.go b/reader.go index 8755c393..67d1c941 100644 --- a/reader.go +++ b/reader.go @@ -581,6 +581,8 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l case line == "#EXT-X-ENDLIST": state.listType = MEDIA p.Closed = true + case line == "#EXT-X-INDEPENDENT-SEGMENTS": + p.SetIndependentSegments(true) case strings.HasPrefix(line, "#EXT-X-VERSION:"): state.listType = MEDIA if _, err = fmt.Sscanf(line, "#EXT-X-VERSION:%d", &p.ver); strict && err != nil { diff --git a/reader_test.go b/reader_test.go index b2b0d9c2..874de302 100644 --- a/reader_test.go +++ b/reader_test.go @@ -1132,3 +1132,75 @@ func BenchmarkDecodeMediaPlaylist(b *testing.B) { } } } + +func TestDecodeMediaPlaylistWithIndependentSegments(t *testing.T) { + f, err := os.Open("sample-playlists/media-playlist-with-independent-segments.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.") + } + err = pp.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + if !pp.IndependentSegments() { + t.Error("Expected independent segments to be true") + } +} + +func TestDecodeMediaPlaylistWithoutIndependentSegments(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.") + } + err = pp.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + if pp.IndependentSegments() { + t.Error("Expected independentsegments to be false") + } +} + +func TestWriteMediaPlaylistWithoutIndependentSegments(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.") + } + err = pp.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + if pp.IndependentSegments() { + t.Error("Expected independentsegments to be false") + } +} diff --git a/sample-playlists/media-playlist-with-independent-segments.m3u8 b/sample-playlists/media-playlist-with-independent-segments.m3u8 new file mode 100644 index 00000000..b2bd38c1 --- /dev/null +++ b/sample-playlists/media-playlist-with-independent-segments.m3u8 @@ -0,0 +1,26 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-INDEPENDENT-SEGMENTS + +#EXTINF:10.000, +0.ts +#EXTINF:10.000, +1.ts +#EXTINF:10.000, +2.ts +#EXTINF:10.000, +3.ts +#EXTINF:10.000, +4.ts +#EXTINF:10.000, +5.ts +#EXTINF:10.000, +6.ts +#EXTINF:10.000, +7.ts +#EXTINF:10.000, +8.ts +#EXTINF:10.000, +9.ts diff --git a/structure.go b/structure.go index 1de9b788..784d6368 100644 --- a/structure.go +++ b/structure.go @@ -106,30 +106,31 @@ const ( // #EXTINF:7.975, // https://priv.example.com/fileSequence2682.ts type MediaPlaylist struct { - TargetDuration float64 - SeqNo uint64 // EXT-X-MEDIA-SEQUENCE - Segments []*MediaSegment - Args string // optional arguments placed after URIs (URI?Args) - Iframe bool // EXT-X-I-FRAMES-ONLY - Closed bool // is this VOD (closed) or Live (sliding) playlist? - MediaType MediaType - DiscontinuitySeq uint64 // EXT-X-DISCONTINUITY-SEQUENCE - StartTime float64 - StartTimePrecise bool - durationAsInt bool // output durations as integers of floats? - keyformat int - winsize uint // max number of segments displayed in an encoded playlist; need set to zero for VOD playlists - capacity uint // total capacity of slice used for the playlist - head uint // head of FIFO, we add segments to head - tail uint // tail of FIFO, we remove segments from tail - count uint // number of segments added to the playlist - buf bytes.Buffer - ver uint8 - Key *Key // EXT-X-KEY is optional encryption key displayed before any segments (default key for the playlist) - Map *Map // EXT-X-MAP is optional tag specifies how to obtain the Media Initialization Section (default map for the playlist) - WV *WV // Widevine related tags outside of M3U8 specs - Custom map[string]CustomTag - customDecoders []CustomDecoder + TargetDuration float64 + SeqNo uint64 // EXT-X-MEDIA-SEQUENCE + Segments []*MediaSegment + Args string // optional arguments placed after URIs (URI?Args) + Iframe bool // EXT-X-I-FRAMES-ONLY + Closed bool // is this VOD (closed) or Live (sliding) playlist? + MediaType MediaType + DiscontinuitySeq uint64 // EXT-X-DISCONTINUITY-SEQUENCE + StartTime float64 + StartTimePrecise bool + durationAsInt bool // output durations as integers of floats? + keyformat int + winsize uint // max number of segments displayed in an encoded playlist; need set to zero for VOD playlists + capacity uint // total capacity of slice used for the playlist + head uint // head of FIFO, we add segments to head + tail uint // tail of FIFO, we remove segments from tail + count uint // number of segments added to the playlist + buf bytes.Buffer + ver uint8 + independentSegments bool + Key *Key // EXT-X-KEY is optional encryption key displayed before any segments (default key for the playlist) + Map *Map // EXT-X-MAP is optional tag specifies how to obtain the Media Initialization Section (default map for the playlist) + WV *WV // Widevine related tags outside of M3U8 specs + Custom map[string]CustomTag + customDecoders []CustomDecoder } // MasterPlaylist structure represents a master playlist which diff --git a/writer.go b/writer.go index 9abab9e6..8e65445c 100644 --- a/writer.go +++ b/writer.go @@ -405,6 +405,18 @@ func (p *MediaPlaylist) ResetCache() { p.buf.Reset() } +// IndependentSegments returns true if all media samples in a segment can be +// decoded without information from other segments. +func (p *MediaPlaylist) IndependentSegments() bool { + return p.independentSegments +} + +// SetIndependentSegments sets whether all media samples in a segment can be +// decoded without information from other segments. +func (p *MediaPlaylist) SetIndependentSegments(b bool) { + p.independentSegments = b +} + // Encode generates output in M3U8 format. Marshal `winsize` elements // from bottom of the `segments` queue. func (p *MediaPlaylist) Encode() *bytes.Buffer { @@ -416,6 +428,10 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(strver(p.ver)) p.buf.WriteRune('\n') + if p.IndependentSegments() { + p.buf.WriteString("#EXT-X-INDEPENDENT-SEGMENTS\n") + } + // Write any custom master tags if p.Custom != nil { for _, v := range p.Custom { diff --git a/writer_test.go b/writer_test.go index 755b936b..6427900a 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1223,3 +1223,62 @@ func BenchmarkEncodeMediaPlaylist(b *testing.B) { _ = p.Encode() // disregard output } } + +func TestMediaPlaylistIndependentSegments(t *testing.T) { + p, _ := NewMediaPlaylist(3, 5) + for i := 0; i < 5; i++ { + p.Append(fmt.Sprintf("test%d.ts", i), 5.0, "") + } + if p.IndependentSegments() { + t.Errorf("Expected independent segments to be false by default") + } + p.SetIndependentSegments(true) + if !p.IndependentSegments() { + t.Errorf("Expected independent segments to be true") + } + if !strings.Contains(p.Encode().String(), "#EXT-X-INDEPENDENT-SEGMENTS") { + t.Error("Expected playlist to contain EXT-X-INDEPENDENT-SEGMENTS tag") + } + fmt.Print(p) + // Output: + // #EXTM3U + // #EXT-X-VERSION:3 + // #EXT-X-INDEPENDENT-SEGMENTS + // #EXT-X-MEDIA-SEQUENCE:0 + // #EXT-X-TARGETDURATION:5 + // #EXTINF:5.000, + // test0.ts + // #EXTINF:5.000, + // test1.ts + // #EXTINF:5.000, + // test2.ts +} + +func TestMediaPlaylistWithoutIndependentSegments(t *testing.T) { + p, _ := NewMediaPlaylist(3, 5) + for i := 0; i < 5; i++ { + p.Append(fmt.Sprintf("test%d.ts", i), 5.0, "") + } + if p.IndependentSegments() != false { + t.Errorf("Expected independent segments to be false by default") + } + p.SetIndependentSegments(false) + if p.IndependentSegments() { + t.Errorf("Expected independent segments to be false") + } + if strings.Contains(p.Encode().String(), "#EXT-X-INDEPENDENT-SEGMENTS") { + t.Error("Expected playlist shouldn't contain EXT-X-INDEPENDENT-SEGMENTS tag") + } + fmt.Print(p) + // Output: + // #EXTM3U + // #EXT-X-VERSION:3 + // #EXT-X-MEDIA-SEQUENCE:0 + // #EXT-X-TARGETDURATION:5 + // #EXTINF:5.000, + // test0.ts + // #EXTINF:5.000, + // test1.ts + // #EXTINF:5.000, + // test2.ts +}