Detect playlist stuck and playlist reset conditions in HLS

This CL aims that the player fails upon:

- Playlist that don't change in a suspiciously long time,
  which might mean there are server side issues.
- Playlist with a media sequence lower that its last snapshot
  and no overlapping segments.

This two error conditions are propagated through the renderer,
but not through MediaSource#maybeThrowSourceInfoRefreshError.

Issue:#2872

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160899995
This commit is contained in:
aquilescanta 2017-07-04 09:38:57 -07:00 committed by Oliver Woodman
parent dda3616f5a
commit a04663372f

View file

@ -40,6 +40,38 @@ import java.util.List;
*/
public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
/**
* Thrown when a playlist is considered to be stuck due to a server side error.
*/
public static final class PlaylistStuckException extends IOException {
/**
* The url of the stuck playlist.
*/
public final String url;
private PlaylistStuckException(String url) {
this.url = url;
}
}
/**
* Thrown when the media sequence of a new snapshot indicates the server has reset.
*/
public static final class PlaylistResetException extends IOException {
/**
* The url of the reset playlist.
*/
public final String url;
private PlaylistResetException(String url) {
this.url = url;
}
}
/**
* Listener for primary playlist changes.
*/
@ -75,6 +107,11 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
/**
* Coefficient applied on the target duration of a playlist to determine the amount of time after
* which an unchanging playlist is considered stuck.
*/
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5;
/**
* The minimum number of milliseconds that a url is kept as primary url, if no
* {@link #getPlaylistSnapshot} call is made for that url.
@ -213,14 +250,14 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
/**
* If the playlist is having trouble loading the playlist referenced by the given {@link HlsUrl},
* this method throws the underlying error.
* If the playlist is having trouble refreshing the playlist referenced by the given
* {@link HlsUrl}, this method throws the underlying error.
*
* @param url The {@link HlsUrl}.
* @throws IOException The underyling error.
*/
public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException {
playlistBundles.get(url).mediaPlaylistLoader.maybeThrowError();
playlistBundles.get(url).maybeThrowPlaylistRefreshError();
}
/**
@ -441,9 +478,11 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private HlsMediaPlaylist playlistSnapshot;
private long lastSnapshotLoadMs;
private long lastSnapshotChangeMs;
private long lastSnapshotAccessTimeMs;
private long blacklistUntilMs;
private boolean pendingRefresh;
private IOException playlistError;
public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) {
this.playlistUrl = playlistUrl;
@ -483,6 +522,13 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
}
public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}
// Loader.Callback implementation.
@Override
@ -494,8 +540,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
} else {
onLoadError(loadable, elapsedRealtimeMs, loadDurationMs,
new ParserException("Loaded playlist has unexpected type."));
playlistError = new ParserException("Loaded playlist has unexpected type.");
}
}
@ -517,10 +562,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
boolean shouldRetry = true;
if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) {
blacklistUntilMs =
SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
notifyPlaylistBlacklisting(playlistUrl,
ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
blacklistPlaylist();
shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
}
return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY;
@ -538,14 +580,28 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
HlsMediaPlaylist oldPlaylist = playlistSnapshot;
lastSnapshotLoadMs = SystemClock.elapsedRealtime();
long currentTimeMs = SystemClock.elapsedRealtime();
lastSnapshotLoadMs = currentTimeMs;
playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
long refreshDelayUs = C.TIME_UNSET;
if (playlistSnapshot != oldPlaylist) {
playlistError = null;
lastSnapshotChangeMs = currentTimeMs;
if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) {
refreshDelayUs = playlistSnapshot.targetDurationUs;
}
} else if (!playlistSnapshot.hasEndTag) {
if (currentTimeMs - lastSnapshotChangeMs
> C.usToMs(playlistSnapshot.targetDurationUs)
* PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) {
// The playlist seems to be stuck, we blacklist it.
playlistError = new PlaylistStuckException(playlistUrl.url);
blacklistPlaylist();
} else if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size()
< playlistSnapshot.mediaSequence) {
// The media sequence has jumped backwards. The server has likely reset.
playlistError = new PlaylistResetException(playlistUrl.url);
}
refreshDelayUs = playlistSnapshot.targetDurationUs / 2;
}
if (refreshDelayUs != C.TIME_UNSET) {
@ -554,6 +610,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
}
private void blacklistPlaylist() {
blacklistUntilMs = SystemClock.elapsedRealtime()
+ ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
notifyPlaylistBlacklisting(playlistUrl, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
}
}
}