Use blocking HLS media playlist reload for segments
Issue: #5011 PiperOrigin-RevId: 340477795
This commit is contained in:
parent
5fd1601f91
commit
c04dd8b328
@ -32,6 +32,7 @@ dependencies {
|
||||
testImplementation project(modulePrefix + 'robolectricutils')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
testImplementation project(modulePrefix + 'testdata')
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import android.net.Uri;
|
||||
@ -31,6 +32,7 @@ import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
@ -216,7 +218,7 @@ public final class DefaultHlsPlaylistTracker
|
||||
|
||||
@Override
|
||||
public void refreshPlaylist(Uri url) {
|
||||
playlistBundles.get(url).loadPlaylist();
|
||||
playlistBundles.get(url).loadPlaylist(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -241,7 +243,6 @@ public final class DefaultHlsPlaylistTracker
|
||||
mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist);
|
||||
primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url;
|
||||
createBundles(masterPlaylist.mediaPlaylistUrls);
|
||||
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
|
||||
LoadEventInfo loadEventInfo =
|
||||
new LoadEventInfo(
|
||||
loadable.loadTaskId,
|
||||
@ -251,11 +252,12 @@ public final class DefaultHlsPlaylistTracker
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.bytesLoaded());
|
||||
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
|
||||
if (isMediaPlaylist) {
|
||||
// We don't need to load the playlist again. We can use the same result.
|
||||
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);
|
||||
} else {
|
||||
primaryBundle.loadPlaylist();
|
||||
primaryBundle.loadPlaylist(primaryMediaPlaylistUrl);
|
||||
}
|
||||
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
|
||||
eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST);
|
||||
@ -320,7 +322,7 @@ public final class DefaultHlsPlaylistTracker
|
||||
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url);
|
||||
if (currentTimeMs > bundle.excludeUntilMs) {
|
||||
primaryMediaPlaylistUrl = bundle.playlistUrl;
|
||||
bundle.loadPlaylist();
|
||||
bundle.loadPlaylist(primaryMediaPlaylistUrl);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -336,7 +338,7 @@ public final class DefaultHlsPlaylistTracker
|
||||
return;
|
||||
}
|
||||
primaryMediaPlaylistUrl = url;
|
||||
playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist();
|
||||
playlistBundles.get(primaryMediaPlaylistUrl).loadPlaylist(url);
|
||||
}
|
||||
|
||||
/** Returns whether any of the variants in the master playlist have the specified playlist URL. */
|
||||
@ -460,8 +462,10 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
|
||||
/** Holds all information related to a specific Media Playlist. */
|
||||
private final class MediaPlaylistBundle
|
||||
implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable {
|
||||
private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
|
||||
|
||||
private static final String BLOCK_MSN_PARAM = "_HLS_msn";
|
||||
private static final String SKIP_PARAM = "_HLS_skip";
|
||||
|
||||
private final Uri playlistUrl;
|
||||
private final Loader mediaPlaylistLoader;
|
||||
@ -502,7 +506,12 @@ public final class DefaultHlsPlaylistTracker
|
||||
mediaPlaylistLoader.release();
|
||||
}
|
||||
|
||||
public void loadPlaylist() {
|
||||
/**
|
||||
* Loads the playlist.
|
||||
*
|
||||
* @param requestUri The URI to be used for loading the playlist.
|
||||
*/
|
||||
public void loadPlaylist(Uri requestUri) {
|
||||
excludeUntilMs = 0;
|
||||
if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) {
|
||||
// Load already pending, in progress, or a fatal error has been encountered. Do nothing.
|
||||
@ -511,9 +520,14 @@ public final class DefaultHlsPlaylistTracker
|
||||
long currentTimeMs = SystemClock.elapsedRealtime();
|
||||
if (currentTimeMs < earliestNextLoadTimeMs) {
|
||||
loadPending = true;
|
||||
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
|
||||
playlistRefreshHandler.postDelayed(
|
||||
() -> {
|
||||
loadPending = false;
|
||||
loadPlaylistImmediately(requestUri);
|
||||
},
|
||||
earliestNextLoadTimeMs - currentTimeMs);
|
||||
} else {
|
||||
loadPlaylistImmediately();
|
||||
loadPlaylistImmediately(requestUri);
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,6 +599,19 @@ public final class DefaultHlsPlaylistTracker
|
||||
elapsedRealtimeMs,
|
||||
loadDurationMs,
|
||||
loadable.bytesLoaded());
|
||||
boolean isBlockingRequest = loadable.getUri().getQueryParameter(BLOCK_MSN_PARAM) != null;
|
||||
if (isBlockingRequest && error instanceof HttpDataSource.InvalidResponseCodeException) {
|
||||
int responseCode = ((HttpDataSource.InvalidResponseCodeException) error).responseCode;
|
||||
if (responseCode == 400 || responseCode == 503) {
|
||||
// Intercept bad request and service unavailable to force a full, non-blocking request
|
||||
// (see RFC 8216, section 6.2.5.2).
|
||||
earliestNextLoadTimeMs = SystemClock.elapsedRealtime();
|
||||
loadPlaylist(/* requestUri= */ playlistUrl);
|
||||
castNonNull(eventDispatcher)
|
||||
.loadError(loadEventInfo, loadable.type, error, /* wasCanceled= */ true);
|
||||
return Loader.DONT_RETRY;
|
||||
}
|
||||
}
|
||||
MediaLoadData mediaLoadData = new MediaLoadData(loadable.type);
|
||||
LoadErrorInfo loadErrorInfo =
|
||||
new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount);
|
||||
@ -616,21 +643,13 @@ public final class DefaultHlsPlaylistTracker
|
||||
return loadErrorAction;
|
||||
}
|
||||
|
||||
// Runnable implementation.
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
loadPending = false;
|
||||
loadPlaylistImmediately();
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void loadPlaylistImmediately() {
|
||||
private void loadPlaylistImmediately(Uri playlistRequestUri) {
|
||||
ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable =
|
||||
new ParsingLoadable<>(
|
||||
mediaPlaylistDataSource,
|
||||
getMediaPlaylistUriForRequest(playlistUrl, playlistSnapshot),
|
||||
playlistRequestUri,
|
||||
C.DATA_TYPE_MANIFEST,
|
||||
mediaPlaylistParser);
|
||||
long elapsedRealtime =
|
||||
@ -685,31 +704,42 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do not allow the playlist to load again within the target duration if we obtained a new
|
||||
// snapshot, or half the target duration otherwise.
|
||||
earliestNextLoadTimeMs =
|
||||
currentTimeMs
|
||||
+ C.usToMs(
|
||||
playlistSnapshot != oldPlaylist
|
||||
? playlistSnapshot.targetDurationUs
|
||||
: (playlistSnapshot.targetDurationUs / 2));
|
||||
long durationUntilNextLoadUs = 0L;
|
||||
if (!playlistSnapshot.serverControl.canBlockReload) {
|
||||
// If blocking requests are not supported, do not allow the playlist to load again within
|
||||
// the target duration if we obtained a new snapshot, or half the target duration otherwise.
|
||||
durationUntilNextLoadUs =
|
||||
playlistSnapshot != oldPlaylist
|
||||
? playlistSnapshot.targetDurationUs
|
||||
: (playlistSnapshot.targetDurationUs / 2);
|
||||
}
|
||||
earliestNextLoadTimeMs = currentTimeMs + C.usToMs(durationUntilNextLoadUs);
|
||||
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
|
||||
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
|
||||
// the primary.
|
||||
if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {
|
||||
loadPlaylist();
|
||||
loadPlaylist(getMediaPlaylistUriForReload());
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getMediaPlaylistUriForRequest(
|
||||
Uri playlistUri, @Nullable HlsMediaPlaylist currentMediaPlaylist) {
|
||||
if (currentMediaPlaylist == null
|
||||
|| currentMediaPlaylist.serverControl.skipUntilUs == C.TIME_UNSET) {
|
||||
return playlistUri;
|
||||
private Uri getMediaPlaylistUriForReload() {
|
||||
if (playlistSnapshot == null
|
||||
|| (playlistSnapshot.serverControl.skipUntilUs == C.TIME_UNSET
|
||||
&& !playlistSnapshot.serverControl.canBlockReload)) {
|
||||
return playlistUrl;
|
||||
}
|
||||
Uri.Builder uriBuilder = playlistUrl.buildUpon();
|
||||
if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) {
|
||||
uriBuilder.appendQueryParameter(
|
||||
SKIP_PARAM, playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
|
||||
}
|
||||
if (playlistSnapshot.serverControl.canBlockReload) {
|
||||
long reloadMediaSequence =
|
||||
playlistSnapshot.mediaSequence
|
||||
+ playlistSnapshot.segments.size()
|
||||
+ playlistSnapshot.skippedSegmentCount;
|
||||
uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(reloadMediaSequence));
|
||||
}
|
||||
Uri.Builder uriBuilder = playlistUri.buildUpon();
|
||||
uriBuilder.appendQueryParameter(
|
||||
"_HLS_skip", currentMediaPlaylist.serverControl.canSkipDateRanges ? "v2" : "YES");
|
||||
return uriBuilder.build();
|
||||
}
|
||||
|
||||
|
@ -15,32 +15,28 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSet;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okio.Buffer;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@ -64,24 +60,51 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
"media/m3u8/live_low_latency_media_can_not_skip";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT =
|
||||
"media/m3u8/live_low_latency_media_can_not_skip_next";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_next";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD =
|
||||
"media/m3u8/live_low_latency_media_can_skip_until_and_block_reload";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT =
|
||||
"media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT_SKIPPED =
|
||||
"media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next_skipped";
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
private int enqueueCounter;
|
||||
private int assertedRequestCounter;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockWebServer = new MockWebServer();
|
||||
enqueueCounter = 0;
|
||||
assertedRequestCounter = 0;
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
assertThat(assertedRequestCounter).isEqualTo(enqueueCounter);
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_playlistCanNotSkip_requestsFullUpdate() throws IOException, TimeoutException {
|
||||
|
||||
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
|
||||
Queue<DataSource> dataSourceQueue = new ArrayDeque<>();
|
||||
dataSourceQueue.add(new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MASTER)));
|
||||
dataSourceQueue.add(
|
||||
new DataSourceList(
|
||||
new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP)),
|
||||
new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT))));
|
||||
public void start_playlistCanNotSkip_requestsFullUpdate()
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {"master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
/* dataSourceFactory= */ dataSourceQueue::remove,
|
||||
masterPlaylistUri,
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
HlsMediaPlaylist firstFullPlaylist = mediaPlaylists.get(0);
|
||||
assertThat(firstFullPlaylist.mediaSequence).isEqualTo(10);
|
||||
assertThat(firstFullPlaylist.segments.get(0).url).isEqualTo("fileSequence10.ts");
|
||||
@ -98,22 +121,23 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
|
||||
@Test
|
||||
public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments()
|
||||
throws IOException, TimeoutException {
|
||||
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
|
||||
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8");
|
||||
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES");
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER))
|
||||
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL))
|
||||
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet),
|
||||
masterPlaylistUri,
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0);
|
||||
assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10);
|
||||
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
|
||||
@ -131,24 +155,23 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
|
||||
@Test
|
||||
public void start_playlistCanSkip_missingSegments_correctedMediaSequence()
|
||||
throws IOException, TimeoutException {
|
||||
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
|
||||
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8");
|
||||
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES");
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER))
|
||||
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL))
|
||||
.setData(
|
||||
mediaPlaylistSkippedUri,
|
||||
getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING));
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet),
|
||||
masterPlaylistUri,
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0);
|
||||
assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10);
|
||||
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
|
||||
@ -160,23 +183,23 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
|
||||
@Test
|
||||
public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2()
|
||||
throws IOException, TimeoutException {
|
||||
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
|
||||
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8");
|
||||
// Expect _HLS_skip parameter with value v2.
|
||||
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=v2");
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER))
|
||||
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES))
|
||||
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet),
|
||||
masterPlaylistUri,
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
// Finding the media sequence of the second playlist request asserts that the second request has
|
||||
// been made with the correct uri parameter appended.
|
||||
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
|
||||
@ -184,29 +207,104 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
|
||||
@Test
|
||||
public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams()
|
||||
throws IOException, TimeoutException {
|
||||
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8");
|
||||
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8?param1=1¶m2=2");
|
||||
// Expect _HLS_skip parameter appended with an ampersand.
|
||||
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "&_HLS_skip=YES");
|
||||
FakeDataSet fakeDataSet =
|
||||
new FakeDataSet()
|
||||
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM))
|
||||
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL))
|
||||
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8",
|
||||
"/media0/playlist.m3u8?param1=1¶m2=2",
|
||||
"/media0/playlist.m3u8?param1=1¶m2=2&_HLS_skip=YES"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet),
|
||||
masterPlaylistUri,
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
// Finding the media sequence of the second playlist request asserts that the second request has
|
||||
// been made with the original uri parameters preserved and the additional param concatenated
|
||||
// correctly.
|
||||
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_playlistCanBlockReload_requestBlockingReloadWithCorrectMediaSequence()
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_msn=14"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 2);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
|
||||
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest()
|
||||
throws IOException, TimeoutException, InterruptedException {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/master.m3u8",
|
||||
"/media0/playlist.m3u8",
|
||||
"/media0/playlist.m3u8?_HLS_skip=YES&_HLS_msn=16",
|
||||
"/media0/playlist.m3u8",
|
||||
"/media0/playlist.m3u8?_HLS_skip=YES&_HLS_msn=17"
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD),
|
||||
new MockResponse().setResponseCode(400),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT_SKIPPED));
|
||||
|
||||
List<HlsMediaPlaylist> mediaPlaylists =
|
||||
runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
/* dataSourceFactory= */ new DefaultHttpDataSourceFactory(),
|
||||
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
|
||||
/* awaitedMediaPlaylistCount= */ 3);
|
||||
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
|
||||
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
|
||||
assertThat(mediaPlaylists.get(2).mediaSequence).isEqualTo(12);
|
||||
}
|
||||
|
||||
private List<HttpUrl> enqueueWebServerResponses(String[] paths, MockResponse... mockResponses) {
|
||||
assertThat(paths).hasLength(mockResponses.length);
|
||||
for (MockResponse mockResponse : mockResponses) {
|
||||
enqueueCounter++;
|
||||
mockWebServer.enqueue(mockResponse);
|
||||
}
|
||||
List<HttpUrl> urls = new ArrayList<>();
|
||||
for (String path : paths) {
|
||||
urls.add(mockWebServer.url(path));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private void assertRequestUrlsCalled(List<HttpUrl> httpUrls) throws InterruptedException {
|
||||
for (HttpUrl url : httpUrls) {
|
||||
assertedRequestCounter++;
|
||||
assertThat(url.toString()).endsWith(mockWebServer.takeRequest().getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private static List<HlsMediaPlaylist> runPlaylistTrackerAndCollectMediaPlaylists(
|
||||
DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount)
|
||||
throws TimeoutException {
|
||||
@ -227,70 +325,17 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
playlistCounter.addAndGet(1);
|
||||
});
|
||||
|
||||
RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() == awaitedMediaPlaylistCount);
|
||||
RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() >= awaitedMediaPlaylistCount);
|
||||
|
||||
defaultHlsPlaylistTracker.stop();
|
||||
return mediaPlaylists;
|
||||
}
|
||||
|
||||
private static MockResponse getMockResponse(String assetFile) throws IOException {
|
||||
return new MockResponse().setResponseCode(200).setBody(new Buffer().write(getBytes(assetFile)));
|
||||
}
|
||||
|
||||
private static byte[] getBytes(String filename) throws IOException {
|
||||
return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), filename);
|
||||
}
|
||||
|
||||
private static final class DataSourceList implements DataSource {
|
||||
|
||||
private final DataSource[] dataSources;
|
||||
|
||||
private DataSource delegate;
|
||||
private int index;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param dataSources The data sources to delegate to.
|
||||
*/
|
||||
public DataSourceList(DataSource... dataSources) {
|
||||
checkArgument(dataSources.length > 0);
|
||||
this.dataSources = dataSources;
|
||||
delegate = dataSources[index++];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTransferListener(TransferListener transferListener) {
|
||||
for (DataSource dataSource : dataSources) {
|
||||
dataSource.addTransferListener(transferListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws IOException {
|
||||
checkState(index <= dataSources.length);
|
||||
return delegate.open(dataSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int readLength) throws IOException {
|
||||
return delegate.read(buffer, offset, readLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Uri getUri() {
|
||||
return delegate.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getResponseHeaders() {
|
||||
return delegate.getResponseHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
delegate.close();
|
||||
if (index < dataSources.length) {
|
||||
delegate = dataSources[index];
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_block_reload
vendored
Normal file
13
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_block_reload
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
#EXTINF:4.00000,
|
||||
fileSequence10.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.ts
|
13
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_block_reload_next
vendored
Normal file
13
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_block_reload_next
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:11
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence14.ts
|
@ -1,4 +1,5 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-SKIP-DATERANGES=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
@ -14,4 +15,3 @@ fileSequence13.ts
|
||||
fileSequence14.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence15.ts
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-SKIP-DATERANGES=YES
|
||||
|
@ -1,4 +1,5 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-MEDIA-SEQUENCE:11
|
||||
@ -11,4 +12,3 @@ fileSequence14.ts
|
||||
fileSequence15.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence16.ts
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
|
@ -1,4 +1,5 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:9
|
||||
#EXT-X-MEDIA-SEQUENCE:20
|
||||
@ -11,4 +12,3 @@ fileSequence23.ts
|
||||
fileSequence24.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence25.ts
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
|
@ -1,4 +1,5 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
@ -14,4 +15,3 @@ fileSequence13.ts
|
||||
fileSequence14.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence15.ts
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
|
||||
|
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload
vendored
Normal file
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
#EXTINF:4.00000,
|
||||
fileSequence10.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence14.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence15.ts
|
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next
vendored
Normal file
17
testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:11
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence14.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence15.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence16.ts
|
@ -0,0 +1,14 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-SKIP:SKIPPED-SEGMENTS=2
|
||||
#EXT-X-MEDIA-SEQUENCE:12
|
||||
#EXTINF:4.00000,
|
||||
fileSequence14.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence15.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence16.ts
|
||||
#EXTINF:4.00000,
|
||||
fileSequence17.ts
|
Loading…
x
Reference in New Issue
Block a user