Commit c04dd8b3 by bachinger Committed by Andrew Lewis

Use blocking HLS media playlist reload for segments

Issue: #5011
PiperOrigin-RevId: 340477795
parent 5fd1601f
...@@ -32,6 +32,7 @@ dependencies { ...@@ -32,6 +32,7 @@ dependencies {
testImplementation project(modulePrefix + 'robolectricutils') testImplementation project(modulePrefix + 'robolectricutils')
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testdata') testImplementation project(modulePrefix + 'testdata')
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; 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 static java.lang.Math.max;
import android.net.Uri; import android.net.Uri;
...@@ -31,6 +32,7 @@ import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; ...@@ -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.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource; 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;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.LoadErrorInfo;
import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader;
...@@ -216,7 +218,7 @@ public final class DefaultHlsPlaylistTracker ...@@ -216,7 +218,7 @@ public final class DefaultHlsPlaylistTracker
@Override @Override
public void refreshPlaylist(Uri url) { public void refreshPlaylist(Uri url) {
playlistBundles.get(url).loadPlaylist(); playlistBundles.get(url).loadPlaylist(url);
} }
@Override @Override
...@@ -241,7 +243,6 @@ public final class DefaultHlsPlaylistTracker ...@@ -241,7 +243,6 @@ public final class DefaultHlsPlaylistTracker
mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist); mediaPlaylistParser = playlistParserFactory.createPlaylistParser(masterPlaylist);
primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url; primaryMediaPlaylistUrl = masterPlaylist.variants.get(0).url;
createBundles(masterPlaylist.mediaPlaylistUrls); createBundles(masterPlaylist.mediaPlaylistUrls);
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
LoadEventInfo loadEventInfo = LoadEventInfo loadEventInfo =
new LoadEventInfo( new LoadEventInfo(
loadable.loadTaskId, loadable.loadTaskId,
...@@ -251,11 +252,12 @@ public final class DefaultHlsPlaylistTracker ...@@ -251,11 +252,12 @@ public final class DefaultHlsPlaylistTracker
elapsedRealtimeMs, elapsedRealtimeMs,
loadDurationMs, loadDurationMs,
loadable.bytesLoaded()); loadable.bytesLoaded());
MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryMediaPlaylistUrl);
if (isMediaPlaylist) { if (isMediaPlaylist) {
// We don't need to load the playlist again. We can use the same result. // We don't need to load the playlist again. We can use the same result.
primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo); primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result, loadEventInfo);
} else { } else {
primaryBundle.loadPlaylist(); primaryBundle.loadPlaylist(primaryMediaPlaylistUrl);
} }
loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId); loadErrorHandlingPolicy.onLoadTaskConcluded(loadable.loadTaskId);
eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST); eventDispatcher.loadCompleted(loadEventInfo, C.DATA_TYPE_MANIFEST);
...@@ -320,7 +322,7 @@ public final class DefaultHlsPlaylistTracker ...@@ -320,7 +322,7 @@ public final class DefaultHlsPlaylistTracker
MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url); MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i).url);
if (currentTimeMs > bundle.excludeUntilMs) { if (currentTimeMs > bundle.excludeUntilMs) {
primaryMediaPlaylistUrl = bundle.playlistUrl; primaryMediaPlaylistUrl = bundle.playlistUrl;
bundle.loadPlaylist(); bundle.loadPlaylist(primaryMediaPlaylistUrl);
return true; return true;
} }
} }
...@@ -336,7 +338,7 @@ public final class DefaultHlsPlaylistTracker ...@@ -336,7 +338,7 @@ public final class DefaultHlsPlaylistTracker
return; return;
} }
primaryMediaPlaylistUrl = url; 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. */ /** Returns whether any of the variants in the master playlist have the specified playlist URL. */
...@@ -460,8 +462,10 @@ public final class DefaultHlsPlaylistTracker ...@@ -460,8 +462,10 @@ public final class DefaultHlsPlaylistTracker
} }
/** Holds all information related to a specific Media Playlist. */ /** Holds all information related to a specific Media Playlist. */
private final class MediaPlaylistBundle private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
implements Loader.Callback<ParsingLoadable<HlsPlaylist>>, Runnable {
private static final String BLOCK_MSN_PARAM = "_HLS_msn";
private static final String SKIP_PARAM = "_HLS_skip";
private final Uri playlistUrl; private final Uri playlistUrl;
private final Loader mediaPlaylistLoader; private final Loader mediaPlaylistLoader;
...@@ -502,7 +506,12 @@ public final class DefaultHlsPlaylistTracker ...@@ -502,7 +506,12 @@ public final class DefaultHlsPlaylistTracker
mediaPlaylistLoader.release(); 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; excludeUntilMs = 0;
if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) { if (loadPending || mediaPlaylistLoader.isLoading() || mediaPlaylistLoader.hasFatalError()) {
// Load already pending, in progress, or a fatal error has been encountered. Do nothing. // Load already pending, in progress, or a fatal error has been encountered. Do nothing.
...@@ -511,9 +520,14 @@ public final class DefaultHlsPlaylistTracker ...@@ -511,9 +520,14 @@ public final class DefaultHlsPlaylistTracker
long currentTimeMs = SystemClock.elapsedRealtime(); long currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < earliestNextLoadTimeMs) { if (currentTimeMs < earliestNextLoadTimeMs) {
loadPending = true; loadPending = true;
playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); playlistRefreshHandler.postDelayed(
() -> {
loadPending = false;
loadPlaylistImmediately(requestUri);
},
earliestNextLoadTimeMs - currentTimeMs);
} else { } else {
loadPlaylistImmediately(); loadPlaylistImmediately(requestUri);
} }
} }
...@@ -585,6 +599,19 @@ public final class DefaultHlsPlaylistTracker ...@@ -585,6 +599,19 @@ public final class DefaultHlsPlaylistTracker
elapsedRealtimeMs, elapsedRealtimeMs,
loadDurationMs, loadDurationMs,
loadable.bytesLoaded()); 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); MediaLoadData mediaLoadData = new MediaLoadData(loadable.type);
LoadErrorInfo loadErrorInfo = LoadErrorInfo loadErrorInfo =
new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount); new LoadErrorInfo(loadEventInfo, mediaLoadData, error, errorCount);
...@@ -616,21 +643,13 @@ public final class DefaultHlsPlaylistTracker ...@@ -616,21 +643,13 @@ public final class DefaultHlsPlaylistTracker
return loadErrorAction; return loadErrorAction;
} }
// Runnable implementation.
@Override
public void run() {
loadPending = false;
loadPlaylistImmediately();
}
// Internal methods. // Internal methods.
private void loadPlaylistImmediately() { private void loadPlaylistImmediately(Uri playlistRequestUri) {
ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable = ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable =
new ParsingLoadable<>( new ParsingLoadable<>(
mediaPlaylistDataSource, mediaPlaylistDataSource,
getMediaPlaylistUriForRequest(playlistUrl, playlistSnapshot), playlistRequestUri,
C.DATA_TYPE_MANIFEST, C.DATA_TYPE_MANIFEST,
mediaPlaylistParser); mediaPlaylistParser);
long elapsedRealtime = long elapsedRealtime =
...@@ -685,31 +704,42 @@ public final class DefaultHlsPlaylistTracker ...@@ -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 long durationUntilNextLoadUs = 0L;
// snapshot, or half the target duration otherwise. if (!playlistSnapshot.serverControl.canBlockReload) {
earliestNextLoadTimeMs = // If blocking requests are not supported, do not allow the playlist to load again within
currentTimeMs // the target duration if we obtained a new snapshot, or half the target duration otherwise.
+ C.usToMs( durationUntilNextLoadUs =
playlistSnapshot != oldPlaylist playlistSnapshot != oldPlaylist
? playlistSnapshot.targetDurationUs ? playlistSnapshot.targetDurationUs
: (playlistSnapshot.targetDurationUs / 2)); : (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 // 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 // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// the primary. // the primary.
if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) { if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {
loadPlaylist(); loadPlaylist(getMediaPlaylistUriForReload());
} }
} }
private Uri getMediaPlaylistUriForRequest( private Uri getMediaPlaylistUriForReload() {
Uri playlistUri, @Nullable HlsMediaPlaylist currentMediaPlaylist) { if (playlistSnapshot == null
if (currentMediaPlaylist == null || (playlistSnapshot.serverControl.skipUntilUs == C.TIME_UNSET
|| currentMediaPlaylist.serverControl.skipUntilUs == C.TIME_UNSET) { && !playlistSnapshot.serverControl.canBlockReload)) {
return playlistUri; 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(); return uriBuilder.build();
} }
......
...@@ -15,32 +15,28 @@ ...@@ -15,32 +15,28 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; 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 static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.MediaSourceEventListener; 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.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
import com.google.android.exoplayer2.upstream.DataSource; 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.DefaultLoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; 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.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -64,24 +60,51 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -64,24 +60,51 @@ public class DefaultHlsPlaylistTrackerTest {
"media/m3u8/live_low_latency_media_can_not_skip"; "media/m3u8/live_low_latency_media_can_not_skip";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT = private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT =
"media/m3u8/live_low_latency_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;
}
@Test @After
public void start_playlistCanNotSkip_requestsFullUpdate() throws IOException, TimeoutException { public void tearDown() throws IOException {
assertThat(assertedRequestCounter).isEqualTo(enqueueCounter);
mockWebServer.shutdown();
}
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); @Test
Queue<DataSource> dataSourceQueue = new ArrayDeque<>(); public void start_playlistCanNotSkip_requestsFullUpdate()
dataSourceQueue.add(new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MASTER))); throws IOException, TimeoutException, InterruptedException {
dataSourceQueue.add( List<HttpUrl> httpUrls =
new DataSourceList( enqueueWebServerResponses(
new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP)), new String[] {"master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8"},
new ByteArrayDataSource(getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_NOT_SKIP_NEXT)))); 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 = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
/* dataSourceFactory= */ dataSourceQueue::remove, new DefaultHttpDataSourceFactory(),
masterPlaylistUri, Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
HlsMediaPlaylist firstFullPlaylist = mediaPlaylists.get(0); HlsMediaPlaylist firstFullPlaylist = mediaPlaylists.get(0);
assertThat(firstFullPlaylist.mediaSequence).isEqualTo(10); assertThat(firstFullPlaylist.mediaSequence).isEqualTo(10);
assertThat(firstFullPlaylist.segments.get(0).url).isEqualTo("fileSequence10.ts"); assertThat(firstFullPlaylist.segments.get(0).url).isEqualTo("fileSequence10.ts");
...@@ -98,22 +121,23 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -98,22 +121,23 @@ public class DefaultHlsPlaylistTrackerTest {
@Test @Test
public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments() public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments()
throws IOException, TimeoutException { throws IOException, TimeoutException, InterruptedException {
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); List<HttpUrl> httpUrls =
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); enqueueWebServerResponses(
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES"); new String[] {
FakeDataSet fakeDataSet = "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
new FakeDataSet() },
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), new DefaultHttpDataSourceFactory(),
masterPlaylistUri, Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0); HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0);
assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10); assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10);
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
...@@ -131,24 +155,23 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -131,24 +155,23 @@ public class DefaultHlsPlaylistTrackerTest {
@Test @Test
public void start_playlistCanSkip_missingSegments_correctedMediaSequence() public void start_playlistCanSkip_missingSegments_correctedMediaSequence()
throws IOException, TimeoutException { throws IOException, TimeoutException, InterruptedException {
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); List<HttpUrl> httpUrls =
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); enqueueWebServerResponses(
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=YES"); new String[] {
FakeDataSet fakeDataSet = "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
new FakeDataSet() },
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
.setData( getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING));
mediaPlaylistSkippedUri,
getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), new DefaultHttpDataSourceFactory(),
masterPlaylistUri, Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0); HlsMediaPlaylist initialPlaylistWithAllSegments = mediaPlaylists.get(0);
assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10); assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10);
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6); assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
...@@ -160,23 +183,23 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -160,23 +183,23 @@ public class DefaultHlsPlaylistTrackerTest {
@Test @Test
public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2() public void start_playlistCanSkipDataRanges_requestsDeltaUpdateV2()
throws IOException, TimeoutException { throws IOException, TimeoutException, InterruptedException {
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); List<HttpUrl> httpUrls =
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8"); enqueueWebServerResponses(
// Expect _HLS_skip parameter with value v2. new String[] {
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "?_HLS_skip=v2"); "/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=v2"
FakeDataSet fakeDataSet = },
new FakeDataSet() getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER)) getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES),
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES)) getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), new DefaultHttpDataSourceFactory(),
masterPlaylistUri, Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
// Finding the media sequence of the second playlist request asserts that the second request has // Finding the media sequence of the second playlist request asserts that the second request has
// been made with the correct uri parameter appended. // been made with the correct uri parameter appended.
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11); assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
...@@ -184,29 +207,104 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -184,29 +207,104 @@ public class DefaultHlsPlaylistTrackerTest {
@Test @Test
public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams() public void start_playlistCanSkipAndUriWithParams_preservesOriginalParams()
throws IOException, TimeoutException { throws IOException, TimeoutException, InterruptedException {
Uri masterPlaylistUri = Uri.parse("fake://foo.bar/master.m3u8"); List<HttpUrl> httpUrls =
Uri mediaPlaylistUri = Uri.parse("fake://foo.bar/media0/playlist.m3u8?param1=1&param2=2"); enqueueWebServerResponses(
// Expect _HLS_skip parameter appended with an ampersand. new String[] {
Uri mediaPlaylistSkippedUri = Uri.parse(mediaPlaylistUri + "&_HLS_skip=YES"); "/master.m3u8",
FakeDataSet fakeDataSet = "/media0/playlist.m3u8?param1=1&param2=2",
new FakeDataSet() "/media0/playlist.m3u8?param1=1&param2=2&_HLS_skip=YES"
.setData(masterPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM)) },
.setData(mediaPlaylistUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL)) getMockResponse(SAMPLE_M3U8_LIVE_MASTER_MEDIA_URI_WITH_PARAM),
.setData(mediaPlaylistSkippedUri, getBytes(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED)); getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED));
List<HlsMediaPlaylist> mediaPlaylists = List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists( runPlaylistTrackerAndCollectMediaPlaylists(
new FakeDataSource.Factory().setFakeDataSet(fakeDataSet), new DefaultHttpDataSourceFactory(),
masterPlaylistUri, Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2); /* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
// Finding the media sequence of the second playlist request asserts that the second request has // 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 // been made with the original uri parameters preserved and the additional param concatenated
// correctly. // correctly.
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11); 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( private static List<HlsMediaPlaylist> runPlaylistTrackerAndCollectMediaPlaylists(
DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount) DataSource.Factory dataSourceFactory, Uri masterPlaylistUri, int awaitedMediaPlaylistCount)
throws TimeoutException { throws TimeoutException {
...@@ -227,70 +325,17 @@ public class DefaultHlsPlaylistTrackerTest { ...@@ -227,70 +325,17 @@ public class DefaultHlsPlaylistTrackerTest {
playlistCounter.addAndGet(1); playlistCounter.addAndGet(1);
}); });
RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() == awaitedMediaPlaylistCount); RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() >= awaitedMediaPlaylistCount);
defaultHlsPlaylistTracker.stop(); defaultHlsPlaylistTracker.stop();
return mediaPlaylists; return mediaPlaylists;
} }
private static byte[] getBytes(String filename) throws IOException { private static MockResponse getMockResponse(String assetFile) throws IOException {
return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), filename); return new MockResponse().setResponseCode(200).setBody(new Buffer().write(getBytes(assetFile)));
} }
private static final class DataSourceList implements DataSource { private static byte[] getBytes(String filename) throws IOException {
return TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), filename);
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++;
}
} }
} }
#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
#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
#EXTM3U #EXTM3U
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-SKIP-DATERANGES=YES
#EXT-X-TARGETDURATION:4 #EXT-X-TARGETDURATION:4
#EXT-X-VERSION:3 #EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10 #EXT-X-MEDIA-SEQUENCE:10
...@@ -14,4 +15,3 @@ fileSequence13.ts ...@@ -14,4 +15,3 @@ fileSequence13.ts
fileSequence14.ts fileSequence14.ts
#EXTINF:4.00000, #EXTINF:4.00000,
fileSequence15.ts fileSequence15.ts
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-SKIP-DATERANGES=YES
#EXTM3U #EXTM3U
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXT-X-TARGETDURATION:4 #EXT-X-TARGETDURATION:4
#EXT-X-VERSION:9 #EXT-X-VERSION:9
#EXT-X-MEDIA-SEQUENCE:11 #EXT-X-MEDIA-SEQUENCE:11
...@@ -11,4 +12,3 @@ fileSequence14.ts ...@@ -11,4 +12,3 @@ fileSequence14.ts
fileSequence15.ts fileSequence15.ts
#EXTINF:4.00000, #EXTINF:4.00000,
fileSequence16.ts fileSequence16.ts
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXTM3U #EXTM3U
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXT-X-TARGETDURATION:4 #EXT-X-TARGETDURATION:4
#EXT-X-VERSION:9 #EXT-X-VERSION:9
#EXT-X-MEDIA-SEQUENCE:20 #EXT-X-MEDIA-SEQUENCE:20
...@@ -11,4 +12,3 @@ fileSequence23.ts ...@@ -11,4 +12,3 @@ fileSequence23.ts
fileSequence24.ts fileSequence24.ts
#EXTINF:4.00000, #EXTINF:4.00000,
fileSequence25.ts fileSequence25.ts
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXTM3U #EXTM3U
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXT-X-TARGETDURATION:4 #EXT-X-TARGETDURATION:4
#EXT-X-VERSION:3 #EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10 #EXT-X-MEDIA-SEQUENCE:10
...@@ -14,4 +15,3 @@ fileSequence13.ts ...@@ -14,4 +15,3 @@ fileSequence13.ts
fileSequence14.ts fileSequence14.ts
#EXTINF:4.00000, #EXTINF:4.00000,
fileSequence15.ts fileSequence15.ts
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#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
#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
#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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment