From 3db86e6e73745c1d5ffccb5b31116998662905a1 Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Thu, 30 Jun 2022 14:58:28 -0700 Subject: [PATCH 1/2] Allows discard of overlapping iFrame only chunks Consider two I-Frame only playlists, to allow adaption based on frame rate the two playlist have different I-Frame duration. The I-Frame only segment has a single sample (IDR), of sub-second duration (e.g. 0.0333s for 30fps source) but a duration equal to the time distance between the I-Frames. Playlist P1 -- 6 seconds between IDR samples, and Playlist P2 -- 4 seconds between IDR samples: ```` +-----+-----+-----+ | 1 | 2 | 3 | +-----+-----+-----+ +---+---+---+---+---+ | 1 | 2 | 3 | 4 | 5 | +---+---+---+---+---+ ```` If the `AdaptiveTrackSelection` changes from P2 after loading segments from P1 there appears to be a sample overlap that would trigger `shouldSpliceIn` to be set on the iFrame `MediaChunk` when no sample splicing is required. ```` +-----+--- | 1 | 2 --- loaded in P1 +-----+--- +---+---+---+ | 3 | 4 | 5 | -- switched to P2 +---+---+---+ ```` Because the `startTimeUs` of P2 segment 3 is less then the `endTimeUs` of P1 segment 3 it appears samples would overlap, as there is only one sample in each segment this is not possible. Setting this `shouldSpliceIn` flag incorrectly prevents pruning the buffered chunks. --- .../android/exoplayer2/source/hls/HlsChunkSource.java | 2 +- .../android/exoplayer2/source/hls/HlsMediaChunk.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 5d5d398370a..21d1db9e64f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -484,7 +484,7 @@ public void getNextChunk( boolean shouldSpliceIn = HlsMediaChunk.shouldSpliceIn( - previous, selectedPlaylistUrl, playlist, segmentBaseHolder, startOfPlaylistInPeriodUs); + previous, selectedPlaylistUrl, playlist, playlistFormats[selectedTrackIndex], segmentBaseHolder, startOfPlaylistInPeriodUs); if (shouldSpliceIn && segmentBaseHolder.isPreload) { // We don't support discarding spliced-in segments [internal: b/159904763], but preload // parts may need to be discarded if they are removed before becoming permanently published. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 9bdc2b90796..ad035c87a18 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -197,6 +197,7 @@ public static HlsMediaChunk createInstance( * in the queue. * @param playlistUrl The URL of the playlist from which the new chunk will be obtained. * @param mediaPlaylist The {@link HlsMediaPlaylist} containing the new chunk. + * @param playlistFormat The {@link Format} of the playlist for the new chunk * @param segmentBaseHolder The {@link HlsChunkSource.SegmentBaseHolder} with information about * the new chunk. * @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds. @@ -206,6 +207,7 @@ public static boolean shouldSpliceIn( @Nullable HlsMediaChunk previousChunk, Uri playlistUrl, HlsMediaPlaylist mediaPlaylist, + Format playlistFormat, HlsChunkSource.SegmentBaseHolder segmentBaseHolder, long startOfPlaylistInPeriodUs) { if (previousChunk == null) { @@ -221,8 +223,11 @@ public static boolean shouldSpliceIn( // non-overlapping segments to avoid the splice. long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segmentBaseHolder.segmentBase.relativeStartTimeUs; + boolean areBothChunksTrickplay = (previousChunk.trackFormat.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0 + && (playlistFormat.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0; + return !isIndependent(segmentBaseHolder, mediaPlaylist) - || segmentStartTimeInPeriodUs < previousChunk.endTimeUs; + || (segmentStartTimeInPeriodUs < previousChunk.endTimeUs && !areBothChunksTrickplay); } public static final String PRIV_TIMESTAMP_FRAME_OWNER = From d79a4504bb431ef24dfbe25246e2d84be17d17ed Mon Sep 17 00:00:00 2001 From: Steve Mayhew Date: Fri, 15 Jul 2022 14:22:42 -0700 Subject: [PATCH 2/2] Add test cases for `shouldSpliceIn(...)` method Add test cases to cover the exiting logic for `shouldSpliceIn(...)` including two cases that cover: 1. Current pull request proposed changes (Intra-iframe only track switch 2. Proposed logic for to/from iFrame only track --- .../exoplayer2/source/hls/HlsMediaChunk.java | 6 + .../source/hls/HlsMediaChunkTest.java | 260 ++++++++++++++++++ .../exoplayer2/source/hls/HlsTestUtils.java | 48 ++++ ..._low_latency_segment_with_independent_part | 10 + .../media_playlist_independent_2second_iframe | 15 + .../media_playlist_independent_4second_iframe | 12 + 6 files changed, 351 insertions(+) create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkTest.java create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsTestUtils.java create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_segment_with_independent_part create mode 100644 testdata/src/test/assets/media/m3u8/media_playlist_independent_2second_iframe create mode 100644 testdata/src/test/assets/media/m3u8/media_playlist_independent_4second_iframe diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index ad035c87a18..e17f5469a44 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -19,6 +19,7 @@ import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.analytics.PlayerId; @@ -402,6 +403,11 @@ public void load() throws IOException { } } + @VisibleForTesting + void setLoadCompleted() { + loadCompleted = true; + } + /** * Whether the chunk is a published chunk as opposed to a preload hint that may change when the * playlist updates. diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkTest.java new file mode 100644 index 00000000000..1f0a120b88d --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaChunkTest.java @@ -0,0 +1,260 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.analytics.PlayerId; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.io.InputStream; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** Unit tests for {@link HlsMediaChunk}. */ +@RunWith(AndroidJUnit4.class) +public class HlsMediaChunkTest { + + private static final String PLAYLIST_INDEPENDENT_SEGMENTS = + "media/m3u8/media_playlist_independent_segments"; + private static final String PLAYLIST_INDEPENDENT_PART = + "media/m3u8/live_low_latency_segment_with_independent_part"; + + private static final String PLAYLIST_IFRAME_2s = + "media/m3u8/media_playlist_independent_2second_iframe"; + private static final String PLAYLIST_IFRAME_4s = + "media/m3u8/media_playlist_independent_4second_iframe"; + + private static final Uri PLAYLIST_URI = Uri.parse("http://example.com/"); + + private static final String PLAYLIST_NON_INDEPENDENT_SEGMENTS = + "media/m3u8/media_playlist"; + + @Mock private HlsExtractorFactory mockExtractorFactory; + @Mock private DataSource mockDataSource; + private HlsMediaPlaylist playlist; + + + private static final Format BASE_VIDEO_FORMAT = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setAverageBitrate(30_000) + .setWidth(1280) + .setHeight(720) + .build(); + + private static final Format IFRAME_FORMAT = + BASE_VIDEO_FORMAT.buildUpon() + .setRoleFlags(C.ROLE_FLAG_TRICK_PLAY) + .build(); + @Before + public void setUp() throws Exception { + + InputStream inputStream = + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), PLAYLIST_INDEPENDENT_SEGMENTS); + playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(Uri.EMPTY, inputStream); + + mockDataSource = new FakeDataSource(); + mockExtractorFactory = new DefaultHlsExtractorFactory(); + } + + @Test + public void test_shouldSpliceIn_isFalse_NoPrevious() { + boolean result = + HlsMediaChunk.shouldSpliceIn(null, Uri.EMPTY, playlist, BASE_VIDEO_FORMAT, null, 0); + assertThat(result).isFalse(); + } + + @Test + public void test_shouldSpliceIn_PreviousLoaded_SamePlaylist() { + HlsChunkSource.SegmentBaseHolder segmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(BASE_VIDEO_FORMAT, segmentBaseHolder, PLAYLIST_URI, true); + previousChunk.setLoadCompleted(); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, PLAYLIST_URI, playlist, BASE_VIDEO_FORMAT, segmentBaseHolder, 0); + + assertThat(result).isFalse(); + } + + @Test + public void test_shouldSpliceIn_NotIndependent_DifferentPlaylist() { + HlsChunkSource.SegmentBaseHolder previousSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + + Uri variant1 = Uri.parse("http://example.com/variant1.m3u8"); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(BASE_VIDEO_FORMAT, previousSegmentBaseHolder, variant1, true); + + Uri variant2 = Uri.parse("http://example.com/variant2.m3u8"); + HlsMediaPlaylist nonIndependentPlaylist = HlsTestUtils.getHlsMediaPlaylist(PLAYLIST_NON_INDEPENDENT_SEGMENTS, variant2); + HlsChunkSource.SegmentBaseHolder nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(nonIndependentPlaylist.segments.get(0), 0, 0); + + // Switch to non-independent segment playlist requires splice-in, regardless if the prev and + // next segments overlap or not + + assertThat(previousSegmentBaseHolder.segmentBase.relativeStartTimeUs).isEqualTo(0); // inputs assertions + assertThat(nextSegmentBaseHolder.segmentBase.relativeStartTimeUs + nextSegmentBaseHolder.segmentBase.durationUs).isGreaterThan(0); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, nonIndependentPlaylist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + assertThat(result).isTrue(); + + nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(nonIndependentPlaylist.segments.get(1), 1, 0); + result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, nonIndependentPlaylist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + assertThat(result).isTrue(); + } + + @Test + public void test_shouldSpliceIn_Independent_DifferentPlaylist() { + HlsChunkSource.SegmentBaseHolder previousSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + + Uri variant1 = Uri.parse("http://example.com/variant1.m3u8"); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(BASE_VIDEO_FORMAT, previousSegmentBaseHolder, variant1, true); + + // NOTE, playlist change is checked by Uri match, so can use same playlist, mock change with URI + Uri variant2 = Uri.parse("http://example.com/variant2.m3u8"); + HlsChunkSource.SegmentBaseHolder nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + + // Switch to inependent segment playlist requires splice-in, only if the start of the next segment is + // less than the end of the previous + + assertThat(previousSegmentBaseHolder.segmentBase.relativeStartTimeUs).isEqualTo(0); + assertThat(nextSegmentBaseHolder.segmentBase.relativeStartTimeUs + nextSegmentBaseHolder.segmentBase.durationUs).isGreaterThan(0); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, playlist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + assertThat(result).isTrue(); + + nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(1), 1, 0); + result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, playlist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + assertThat(result).isFalse(); + } + + @Test + public void test_shouldSpliceIn_SegmentParts() { + HlsChunkSource.SegmentBaseHolder previousSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + + Uri variant1 = Uri.parse("http://example.com/variant1.m3u8"); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(BASE_VIDEO_FORMAT, previousSegmentBaseHolder, variant1, true); + previousChunk.setLoadCompleted(); + + Uri variant2 = Uri.parse("http://example.com/variant2.m3u8"); + HlsMediaPlaylist hlsMediaPlaylist = HlsTestUtils.getHlsMediaPlaylist(PLAYLIST_INDEPENDENT_PART, variant2); + + // First Part checks segment level independent + HlsChunkSource.SegmentBaseHolder nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(hlsMediaPlaylist.trailingParts.get(0), 0, 0); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, hlsMediaPlaylist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + + assertThat(result).isFalse(); + + // Additional Parts must be themselves independent, regardless of the independence of the playlist + nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(hlsMediaPlaylist.trailingParts.get(1), 1, 1); + result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, hlsMediaPlaylist, BASE_VIDEO_FORMAT, nextSegmentBaseHolder, 0); + assertThat(result).isFalse(); + + } + + @Test + public void test_shouldSpliceIn_IntraTrickPlay() { + + // Trick play to trick play track should never need a splice, even overlapping + + Uri variant4 = Uri.parse("http://example.com/iframe4.m3u8"); + HlsMediaPlaylist hlsMediaPlaylist4s = HlsTestUtils.getHlsMediaPlaylist(PLAYLIST_IFRAME_4s, variant4); + HlsChunkSource.SegmentBaseHolder previousSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(hlsMediaPlaylist4s.segments.get(0), 0, 0); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(IFRAME_FORMAT, previousSegmentBaseHolder, variant4, true); + + Uri variant2 = Uri.parse("http://example.com/iframe2.m3u8"); + HlsMediaPlaylist hlsMediaPlaylist2s = HlsTestUtils.getHlsMediaPlaylist(PLAYLIST_IFRAME_2s, variant2); + HlsChunkSource.SegmentBaseHolder nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(hlsMediaPlaylist2s.segments.get(1), 0, 0); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, hlsMediaPlaylist2s, IFRAME_FORMAT, nextSegmentBaseHolder, 0); + + assertThat(result).isFalse(); + } + + @Test + @Ignore + public void test_shouldSpliceIn_NonTrickPlay_To_TrickPlay() { + + // Switch to a trick-play track never requires splice in, even if overlapping + HlsChunkSource.SegmentBaseHolder previousSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(playlist.segments.get(0), 0, 0); + HlsMediaChunk previousChunk = createTestHlsMediaChunk(BASE_VIDEO_FORMAT, previousSegmentBaseHolder, PLAYLIST_URI, true); + + Uri variant2 = Uri.parse("http://example.com/iframe2.m3u8"); + HlsMediaPlaylist hlsMediaPlaylist2s = HlsTestUtils.getHlsMediaPlaylist(PLAYLIST_IFRAME_2s, variant2); + HlsChunkSource.SegmentBaseHolder nextSegmentBaseHolder = + new HlsChunkSource.SegmentBaseHolder(hlsMediaPlaylist2s.segments.get(1), 0, 0); + boolean result = + HlsMediaChunk.shouldSpliceIn(previousChunk, variant2, hlsMediaPlaylist2s, IFRAME_FORMAT, nextSegmentBaseHolder, 0); + + assertThat(result).isFalse(); + } + + private HlsMediaChunk createTestHlsMediaChunk( + Format selectedTrackFormat, + HlsChunkSource.SegmentBaseHolder segmentBaseHolder, + Uri selectedPlaylistUrl, + boolean shouldSpliceIn) { + return HlsMediaChunk.createInstance( + mockExtractorFactory, + mockDataSource, + selectedTrackFormat, + 0, + playlist, + segmentBaseHolder, + selectedPlaylistUrl, + null, + C.SELECTION_REASON_INITIAL, + null, + true, + new TimestampAdjusterProvider(), + null, + null, + null, + shouldSpliceIn, + new PlayerId()); + } +} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsTestUtils.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsTestUtils.java new file mode 100644 index 00000000000..9be40d64e1d --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsTestUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import static org.junit.Assert.fail; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; + +public class HlsTestUtils { + + /** + * Load a mock HLS playlist from a test asset file. + * + * @param file - source of the text of the test playlist + * @param playlistUri - Uri to set as base for the playlist + * @return test {@link HlsMediaPlaylist} + */ + public static HlsMediaPlaylist getHlsMediaPlaylist(String file, Uri playlistUri) { + try { + return (HlsMediaPlaylist) + new HlsPlaylistParser() + .parse( + playlistUri, + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), file)); + } catch (IOException e) { + fail(e.getMessage()); + } + return null; + } +} diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_segment_with_independent_part b/testdata/src/test/assets/media/m3u8/live_low_latency_segment_with_independent_part new file mode 100644 index 00000000000..29570f096c5 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_segment_with_independent_part @@ -0,0 +1,10 @@ +#EXTM3U +#EXT-X-TARGETDURATION:4 +#EXT-X-PART-INF:PART-TARGET=1.000400 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:10 +#EXTINF:4.00000, +fileSequence15.ts +#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.0.ts" +#EXT-X-PART:DURATION=1.00000,INDEPENDENT=YES,URI="fileSequence16.1.ts" diff --git a/testdata/src/test/assets/media/m3u8/media_playlist_independent_2second_iframe b/testdata/src/test/assets/media/m3u8/media_playlist_independent_2second_iframe new file mode 100644 index 00000000000..2b0e923a835 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/media_playlist_independent_2second_iframe @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-MEDIA-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-I-FRAMES-ONLY +#EXT-X-MAP:URI="init.mp4" +#EXTINF:2, +#EXT-X-BYTERANGE:3008@376 +2.mp4 +#EXTINF:2, +#EXT-X-BYTERANGE:10716@3384 +2.mp4 +#EXTINF:2, +#EXT-X-BYTERANGE:58844@14100 +2.mp4 +#EXT-X-ENDLIST diff --git a/testdata/src/test/assets/media/m3u8/media_playlist_independent_4second_iframe b/testdata/src/test/assets/media/m3u8/media_playlist_independent_4second_iframe new file mode 100644 index 00000000000..5d83d57dd27 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/media_playlist_independent_4second_iframe @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-MEDIA-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-I-FRAMES-ONLY +#EXT-X-MAP:URI="init.mp4" +#EXTINF:4, +#EXT-X-BYTERANGE:3008@376 +2.mp4 +#EXTINF:4, +#EXT-X-BYTERANGE:58844@14100 +2.mp4 +#EXT-X-ENDLIST