Use blocking HLS media playlist reload for segments

Issue: #5011
PiperOrigin-RevId: 340477795
This commit is contained in:
bachinger 2020-11-03 18:46:11 +00:00 committed by Andrew Lewis
parent 5fd1601f91
commit c04dd8b328
12 changed files with 319 additions and 169 deletions

View File

@ -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
}

View File

@ -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();
}

View File

@ -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&param2=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&param2=2",
"/media0/playlist.m3u8?param1=1&param2=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++;
}
}
}

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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