Skip to content

Commit

Permalink
Detect playlist stuck and playlist reset conditions in HLS
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AquilesCanta authored and ojw28 committed Jul 5, 2017
1 parent dda3616 commit a046633
Showing 1 changed file with 72 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@
*/
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.
*/
Expand Down Expand Up @@ -75,6 +107,11 @@ public interface PlaylistEventListener {

}

/**
* 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.
Expand Down Expand Up @@ -213,14 +250,14 @@ public void maybeThrowPrimaryPlaylistRefreshError() throws IOException {
}

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

/**
Expand Down Expand Up @@ -441,9 +478,11 @@ private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadab

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;
Expand Down Expand Up @@ -483,6 +522,13 @@ public void loadPlaylist() {
}
}

public void maybeThrowPlaylistRefreshError() throws IOException {
mediaPlaylistLoader.maybeThrowError();
if (playlistError != null) {
throw playlistError;
}
}

// Loader.Callback implementation.

@Override
Expand All @@ -494,8 +540,7 @@ public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedR
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.");
}
}

Expand All @@ -517,10 +562,7 @@ public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealti
}
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;
Expand All @@ -538,14 +580,28 @@ public void run() {

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) {
Expand All @@ -554,6 +610,12 @@ private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
}
}

private void blacklistPlaylist() {
blacklistUntilMs = SystemClock.elapsedRealtime()
+ ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
notifyPlaylistBlacklisting(playlistUrl, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
}

}

}

12 comments on commit a046633

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi, how can we handle the time o be reloaded when reset from server?

@AquilesCanta
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand the question. Could you rephrase and maybe provide an example or two?

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can we manage the time to reload the channel after it is stucked such ass PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT and PRIMARY_URL_KEEPALIVE_MS and DEFAULT_TRACK_BLACKLIST_MS

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since when reset from server it takes a long time to play

@AquilesCanta
Copy link
Contributor Author

@AquilesCanta AquilesCanta commented on a046633 Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you restart the server it takes time to throw PlaylistResetException? That should happen as soon as the playlist gets loaded.

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the time needed cant be handled to minimize it

@hadinajem
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What he means is that:
on iOS when the streaming channel is restarted, the player plays the stream as soon as the channel goes back online (delay of 10 seconds max)
What can be modified here to replicate the same behaviour with exoplayer noting that no exceptions are thrown or errors are thrown by exoplayer to be able to detect the problem

@hadinajem
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also kindly note that the stream restarts by exoplayer after one minute
So my question here is, how and what parameters can be modified to reduce this one minute interval

@AquilesCanta
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The referred constants are unrelated to server reset detection. ExoPlayer detects playlist resets by tracking media sequence numbers. The condition for server resets is:

media sequence of the last snapshot > (media sequence of the new snapshot + number of segments in the new snapshot)

E.g:

Say all playlists have 10 segments, and the last snapshot grabbed from the server has media sequence 15. A playlist reset exception will be thrown if and only if the new snapshot has media sequence 4 or lower. Otherwise, it is going to be stuck in the buffering state until the playlist starts offering new segments. I suppose this is what is happening? (This was not explained in the comments above).

I assume what you want is to customize the playlist reset detection condition to be more aggressive? It is not currently possible, but could be. If this is the case, please file a new issue providing clear explanation of what the problem is, including expected and actual behavior. Also, it is usually easier for us to understand if behavior is described in terms of player states and events.

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to minimize the time needed to the channel to be restarted which is arround 1 min when channel on server is restarted?

@AquilesCanta
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you kindly file an issue to prevent this discussion from getting lost in time?

@TarifHatoum
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi when restarting the channel from server the channel on exoplayer is stucked or take a long time to be restarted is there a way to make it faster to reload on client side when restarted from server?

Please sign in to comment.