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