Use same logic for DASH manifest reloading for all cases when manifest is invalid.

When a loaded DASH manifest is invalid (either some periods were removed
illegally, or a manifest for a live event is stale), we will retry using 1
logic:
- Retry loading with back-off up-to a limit.
- Throw a DashManifestExpiredException() if we exceed retry limit.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182770028
This commit is contained in:
hoangtc 2018-01-22 06:29:51 -08:00 committed by Oliver Woodman
parent 05e55f37eb
commit a06a670d63
4 changed files with 107 additions and 79 deletions

View File

@ -17,5 +17,5 @@ package com.google.android.exoplayer2.source.dash;
import java.io.IOException;
/** Thrown when a live playback's manifest is expired and a new manifest could not be loaded. */
public final class DashManifestExpiredException extends IOException {}
/** Thrown when a live playback's manifest is stale and a new manifest could not be loaded. */
public final class DashManifestStaleException extends IOException {}

View File

@ -284,7 +284,8 @@ public final class DashMediaSource implements MediaSource {
private Listener sourceListener;
private DataSource dataSource;
private Loader loader;
private LoaderErrorThrower loaderErrorThrower;
private LoaderErrorThrower manifestLoadErrorThrower;
private IOException manifestFatalError;
private Handler handler;
private Uri manifestUri;
@ -493,12 +494,12 @@ public final class DashMediaSource implements MediaSource {
Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE);
sourceListener = listener;
if (sideloadedManifest) {
loaderErrorThrower = new LoaderErrorThrower.Dummy();
manifestLoadErrorThrower = new LoaderErrorThrower.Dummy();
processManifest(false);
} else {
dataSource = manifestDataSourceFactory.createDataSource();
loader = new Loader("Loader:DashMediaSource");
loaderErrorThrower = loader;
manifestLoadErrorThrower = new ManifestLoadErrorThrower();
handler = new Handler();
startLoadingManifest();
}
@ -506,7 +507,7 @@ public final class DashMediaSource implements MediaSource {
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
loaderErrorThrower.maybeThrowError();
manifestLoadErrorThrower.maybeThrowError();
}
@Override
@ -523,7 +524,7 @@ public final class DashMediaSource implements MediaSource {
minLoadableRetryCount,
periodEventDispatcher,
elapsedRealtimeOffsetMs,
loaderErrorThrower,
manifestLoadErrorThrower,
allocator,
compositeSequenceableLoaderFactory,
playerEmsgCallback);
@ -542,7 +543,7 @@ public final class DashMediaSource implements MediaSource {
public void releaseSource() {
manifestLoadPending = false;
dataSource = null;
loaderErrorThrower = null;
manifestLoadErrorThrower = null;
if (loader != null) {
loader.release();
loader = null;
@ -592,36 +593,43 @@ public final class DashMediaSource implements MediaSource {
removedPeriodCount++;
}
// After discarding old periods, we should never have more periods than listed in the new
// manifest. That would mean that a previously announced period is no longer advertised. If
// this condition occurs, assume that we are hitting a manifest server that is out of sync and
// behind, discard this manifest, and try again later.
if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) {
Log.w(TAG, "Loaded out of sync manifest");
scheduleManifestRefresh();
return;
}
if (newManifest.dynamic) {
boolean isManifestStale = false;
if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) {
// After discarding old periods, we should never have more periods than listed in the new
// manifest. That would mean that a previously announced period is no longer advertised. If
// this condition occurs, assume that we are hitting a manifest server that is out of sync
// and
// behind.
Log.w(TAG, "Loaded out of sync manifest");
isManifestStale = true;
} else if (dynamicMediaPresentationEnded
|| newManifest.publishTimeMs <= expiredManifestPublishTimeUs) {
// If we receive a dynamic manifest that's older than expected (i.e. its publish time has
// expired, or it's dynamic and we know the presentation has ended), then this manifest is
// stale.
Log.w(
TAG,
"Loaded stale dynamic manifest: "
+ newManifest.publishTimeMs
+ ", "
+ dynamicMediaPresentationEnded
+ ", "
+ expiredManifestPublishTimeUs);
isManifestStale = true;
}
// If we receive a dynamic manifest that's older than expected (i.e. its publish time has
// expired, or it's dynamic and we know the presentation has ended), then ignore it and load
// again up to a specified number of times.
if (newManifest.dynamic
&& (dynamicMediaPresentationEnded
|| newManifest.publishTimeMs <= expiredManifestPublishTimeUs)) {
Log.w(
TAG,
"Loaded stale dynamic manifest: "
+ newManifest.publishTimeMs
+ ", "
+ dynamicMediaPresentationEnded
+ ", "
+ expiredManifestPublishTimeUs);
if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
startLoadingManifest();
if (isManifestStale) {
if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
scheduleManifestRefresh(getManifestLoadRetryDelayMillis());
} else {
manifestFatalError = new DashManifestStaleException();
}
return;
}
staleManifestReloadAttempt = 0;
}
staleManifestReloadAttempt = 0;
manifest = newManifest;
manifestLoadPending &= manifest.dynamic;
@ -804,28 +812,26 @@ public final class DashMediaSource implements MediaSource {
}
if (manifestLoadPending) {
startLoadingManifest();
} else if (scheduleRefresh) {
} else if (scheduleRefresh && manifest.dynamic) {
// Schedule an explicit refresh if needed.
scheduleManifestRefresh();
long minUpdatePeriodMs = manifest.minUpdatePeriodMs;
if (minUpdatePeriodMs == 0) {
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
// minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is
// explicit signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service
minUpdatePeriodMs = 5000;
}
long nextLoadTimestampMs = manifestLoadStartTimestampMs + minUpdatePeriodMs;
long delayUntilNextLoadMs =
Math.max(0, nextLoadTimestampMs - SystemClock.elapsedRealtime());
scheduleManifestRefresh(delayUntilNextLoadMs);
}
}
}
private void scheduleManifestRefresh() {
if (!manifest.dynamic) {
return;
}
long minUpdatePeriodMs = manifest.minUpdatePeriodMs;
if (minUpdatePeriodMs == 0) {
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
// minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is
// explicit signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
minUpdatePeriodMs = 5000;
}
long nextLoadTimestamp = manifestLoadStartTimestampMs + minUpdatePeriodMs;
long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime());
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad);
private void scheduleManifestRefresh(long delayUntilNextLoadMs) {
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
}
private void startLoadingManifest() {
@ -845,6 +851,10 @@ public final class DashMediaSource implements MediaSource {
minLoadableRetryCount);
}
private long getManifestLoadRetryDelayMillis() {
return Math.min((staleManifestReloadAttempt - 1) * 1000, 5000);
}
private <T> void startLoading(ParsingLoadable<T> loadable,
Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
@ -1125,4 +1135,29 @@ public final class DashMediaSource implements MediaSource {
}
}
/**
* A {@link LoaderErrorThrower} that throws fatal {@link IOException} that has occurred during
* manifest loading from the manifest {@code loader}, or exception with the loaded manifest.
*/
/* package */ final class ManifestLoadErrorThrower implements LoaderErrorThrower {
@Override
public void maybeThrowError() throws IOException {
loader.maybeThrowError();
maybeThrowManifestError();
}
@Override
public void maybeThrowError(int minRetryCount) throws IOException {
loader.maybeThrowError(minRetryCount);
maybeThrowManifestError();
}
private void maybeThrowManifestError() throws IOException {
if (manifestFatalError != null) {
throw manifestFatalError;
}
}
}
}

View File

@ -246,16 +246,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
C.msToUs(manifest.availabilityStartTimeMs)
+ C.msToUs(manifest.getPeriod(periodIndex).startMs)
+ loadPositionUs;
try {
if (playerTrackEmsgHandler != null
&& playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(
presentationPositionUs)) {
return;
}
} catch (DashManifestExpiredException e) {
fatalError = e;
if (playerTrackEmsgHandler != null
&& playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(
presentationPositionUs)) {
return;
}
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs);
RepresentationHolder representationHolder =

View File

@ -36,6 +36,7 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
@ -92,7 +93,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private long lastLoadedChunkEndTimeBeforeRefreshUs;
private boolean isWaitingForManifestRefresh;
private boolean released;
private DashManifestExpiredException fatalError;
/**
* @param manifest The initial manifest.
@ -119,26 +119,13 @@ public final class PlayerEmsgHandler implements Handler.Callback {
* @param newManifest The updated manifest.
*/
public void updateManifest(DashManifest newManifest) {
if (isManifestStale(newManifest)) {
fatalError = new DashManifestExpiredException();
}
isWaitingForManifestRefresh = false;
expiredManifestPublishTimeUs = C.TIME_UNSET;
this.manifest = newManifest;
removePreviouslyExpiredManifestPublishTimeValues();
}
private boolean isManifestStale(DashManifest manifest) {
return manifest.dynamic
&& (dynamicMediaPresentationEnded
|| manifest.publishTimeMs <= expiredManifestPublishTimeUs);
}
/* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)
throws DashManifestExpiredException {
if (fatalError != null) {
throw fatalError;
}
/* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
if (!manifest.dynamic) {
return false;
}
@ -273,6 +260,18 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs);
}
private void removePreviouslyExpiredManifestPublishTimeValues() {
for (Iterator<Map.Entry<Long, Long>> it =
manifestPublishTimeToExpiryTimeUs.entrySet().iterator();
it.hasNext(); ) {
Map.Entry<Long, Long> entry = it.next();
long expiredManifestPublishTime = entry.getKey();
if (expiredManifestPublishTime < manifest.publishTimeMs) {
it.remove();
}
}
}
private void notifyManifestPublishTimeExpired() {
playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
}
@ -351,11 +350,8 @@ public final class PlayerEmsgHandler implements Handler.Callback {
*
* @param presentationPositionUs The next load position in presentation time.
* @return True if manifest refresh has been requested, false otherwise.
* @throws DashManifestExpiredException If the current DASH manifest is expired, but a new
* manifest could not be loaded.
*/
public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)
throws DashManifestExpiredException {
public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk(
presentationPositionUs);
}