Commit a06a670d by hoangtc Committed by Oliver Woodman

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
parent 05e55f37
...@@ -17,5 +17,5 @@ package com.google.android.exoplayer2.source.dash; ...@@ -17,5 +17,5 @@ package com.google.android.exoplayer2.source.dash;
import java.io.IOException; import java.io.IOException;
/** Thrown when a live playback's manifest is expired and a new manifest could not be loaded. */ /** Thrown when a live playback's manifest is stale and a new manifest could not be loaded. */
public final class DashManifestExpiredException extends IOException {} public final class DashManifestStaleException extends IOException {}
...@@ -284,7 +284,8 @@ public final class DashMediaSource implements MediaSource { ...@@ -284,7 +284,8 @@ public final class DashMediaSource implements MediaSource {
private Listener sourceListener; private Listener sourceListener;
private DataSource dataSource; private DataSource dataSource;
private Loader loader; private Loader loader;
private LoaderErrorThrower loaderErrorThrower; private LoaderErrorThrower manifestLoadErrorThrower;
private IOException manifestFatalError;
private Handler handler; private Handler handler;
private Uri manifestUri; private Uri manifestUri;
...@@ -493,12 +494,12 @@ public final class DashMediaSource implements MediaSource { ...@@ -493,12 +494,12 @@ public final class DashMediaSource implements MediaSource {
Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE);
sourceListener = listener; sourceListener = listener;
if (sideloadedManifest) { if (sideloadedManifest) {
loaderErrorThrower = new LoaderErrorThrower.Dummy(); manifestLoadErrorThrower = new LoaderErrorThrower.Dummy();
processManifest(false); processManifest(false);
} else { } else {
dataSource = manifestDataSourceFactory.createDataSource(); dataSource = manifestDataSourceFactory.createDataSource();
loader = new Loader("Loader:DashMediaSource"); loader = new Loader("Loader:DashMediaSource");
loaderErrorThrower = loader; manifestLoadErrorThrower = new ManifestLoadErrorThrower();
handler = new Handler(); handler = new Handler();
startLoadingManifest(); startLoadingManifest();
} }
...@@ -506,7 +507,7 @@ public final class DashMediaSource implements MediaSource { ...@@ -506,7 +507,7 @@ public final class DashMediaSource implements MediaSource {
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
loaderErrorThrower.maybeThrowError(); manifestLoadErrorThrower.maybeThrowError();
} }
@Override @Override
...@@ -523,7 +524,7 @@ public final class DashMediaSource implements MediaSource { ...@@ -523,7 +524,7 @@ public final class DashMediaSource implements MediaSource {
minLoadableRetryCount, minLoadableRetryCount,
periodEventDispatcher, periodEventDispatcher,
elapsedRealtimeOffsetMs, elapsedRealtimeOffsetMs,
loaderErrorThrower, manifestLoadErrorThrower,
allocator, allocator,
compositeSequenceableLoaderFactory, compositeSequenceableLoaderFactory,
playerEmsgCallback); playerEmsgCallback);
...@@ -542,7 +543,7 @@ public final class DashMediaSource implements MediaSource { ...@@ -542,7 +543,7 @@ public final class DashMediaSource implements MediaSource {
public void releaseSource() { public void releaseSource() {
manifestLoadPending = false; manifestLoadPending = false;
dataSource = null; dataSource = null;
loaderErrorThrower = null; manifestLoadErrorThrower = null;
if (loader != null) { if (loader != null) {
loader.release(); loader.release();
loader = null; loader = null;
...@@ -592,22 +593,21 @@ public final class DashMediaSource implements MediaSource { ...@@ -592,22 +593,21 @@ public final class DashMediaSource implements MediaSource {
removedPeriodCount++; removedPeriodCount++;
} }
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 // 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 // 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 // this condition occurs, assume that we are hitting a manifest server that is out of sync
// behind, discard this manifest, and try again later. // and
if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) { // behind.
Log.w(TAG, "Loaded out of sync manifest"); Log.w(TAG, "Loaded out of sync manifest");
scheduleManifestRefresh(); isManifestStale = true;
return; } else if (dynamicMediaPresentationEnded
} || newManifest.publishTimeMs <= expiredManifestPublishTimeUs) {
// If we receive a dynamic manifest that's older than expected (i.e. its publish time has // 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 // expired, or it's dynamic and we know the presentation has ended), then this manifest is
// again up to a specified number of times. // stale.
if (newManifest.dynamic
&& (dynamicMediaPresentationEnded
|| newManifest.publishTimeMs <= expiredManifestPublishTimeUs)) {
Log.w( Log.w(
TAG, TAG,
"Loaded stale dynamic manifest: " "Loaded stale dynamic manifest: "
...@@ -616,12 +616,20 @@ public final class DashMediaSource implements MediaSource { ...@@ -616,12 +616,20 @@ public final class DashMediaSource implements MediaSource {
+ dynamicMediaPresentationEnded + dynamicMediaPresentationEnded
+ ", " + ", "
+ expiredManifestPublishTimeUs); + expiredManifestPublishTimeUs);
isManifestStale = true;
}
if (isManifestStale) {
if (staleManifestReloadAttempt++ < minLoadableRetryCount) { if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
startLoadingManifest(); scheduleManifestRefresh(getManifestLoadRetryDelayMillis());
return; } else {
manifestFatalError = new DashManifestStaleException();
} }
return;
} }
staleManifestReloadAttempt = 0; staleManifestReloadAttempt = 0;
}
manifest = newManifest; manifest = newManifest;
manifestLoadPending &= manifest.dynamic; manifestLoadPending &= manifest.dynamic;
...@@ -804,28 +812,26 @@ public final class DashMediaSource implements MediaSource { ...@@ -804,28 +812,26 @@ public final class DashMediaSource implements MediaSource {
} }
if (manifestLoadPending) { if (manifestLoadPending) {
startLoadingManifest(); startLoadingManifest();
} else if (scheduleRefresh) { } else if (scheduleRefresh && manifest.dynamic) {
// Schedule an explicit refresh if needed. // Schedule an explicit refresh if needed.
scheduleManifestRefresh();
}
}
}
private void scheduleManifestRefresh() {
if (!manifest.dynamic) {
return;
}
long minUpdatePeriodMs = manifest.minUpdatePeriodMs; long minUpdatePeriodMs = manifest.minUpdatePeriodMs;
if (minUpdatePeriodMs == 0) { if (minUpdatePeriodMs == 0) {
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where // 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 // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is
// explicit signaling in the stream, according to: // explicit signaling in the stream, according to:
// http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service
minUpdatePeriodMs = 5000; minUpdatePeriodMs = 5000;
} }
long nextLoadTimestamp = manifestLoadStartTimestampMs + minUpdatePeriodMs; long nextLoadTimestampMs = manifestLoadStartTimestampMs + minUpdatePeriodMs;
long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); long delayUntilNextLoadMs =
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); Math.max(0, nextLoadTimestampMs - SystemClock.elapsedRealtime());
scheduleManifestRefresh(delayUntilNextLoadMs);
}
}
}
private void scheduleManifestRefresh(long delayUntilNextLoadMs) {
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
} }
private void startLoadingManifest() { private void startLoadingManifest() {
...@@ -845,6 +851,10 @@ public final class DashMediaSource implements MediaSource { ...@@ -845,6 +851,10 @@ public final class DashMediaSource implements MediaSource {
minLoadableRetryCount); minLoadableRetryCount);
} }
private long getManifestLoadRetryDelayMillis() {
return Math.min((staleManifestReloadAttempt - 1) * 1000, 5000);
}
private <T> void startLoading(ParsingLoadable<T> loadable, private <T> void startLoading(ParsingLoadable<T> loadable,
Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) { Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
...@@ -1125,4 +1135,29 @@ public final class DashMediaSource implements MediaSource { ...@@ -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;
}
}
}
} }
...@@ -246,16 +246,13 @@ public class DefaultDashChunkSource implements DashChunkSource { ...@@ -246,16 +246,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
C.msToUs(manifest.availabilityStartTimeMs) C.msToUs(manifest.availabilityStartTimeMs)
+ C.msToUs(manifest.getPeriod(periodIndex).startMs) + C.msToUs(manifest.getPeriod(periodIndex).startMs)
+ loadPositionUs; + loadPositionUs;
try {
if (playerTrackEmsgHandler != null if (playerTrackEmsgHandler != null
&& playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk( && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk(
presentationPositionUs)) { presentationPositionUs)) {
return; return;
} }
} catch (DashManifestExpiredException e) {
fatalError = e;
return;
}
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs);
RepresentationHolder representationHolder = RepresentationHolder representationHolder =
......
...@@ -36,6 +36,7 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifest; ...@@ -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.upstream.Allocator;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException; import java.io.IOException;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
...@@ -92,7 +93,6 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -92,7 +93,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private long lastLoadedChunkEndTimeBeforeRefreshUs; private long lastLoadedChunkEndTimeBeforeRefreshUs;
private boolean isWaitingForManifestRefresh; private boolean isWaitingForManifestRefresh;
private boolean released; private boolean released;
private DashManifestExpiredException fatalError;
/** /**
* @param manifest The initial manifest. * @param manifest The initial manifest.
...@@ -119,26 +119,13 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -119,26 +119,13 @@ public final class PlayerEmsgHandler implements Handler.Callback {
* @param newManifest The updated manifest. * @param newManifest The updated manifest.
*/ */
public void updateManifest(DashManifest newManifest) { public void updateManifest(DashManifest newManifest) {
if (isManifestStale(newManifest)) {
fatalError = new DashManifestExpiredException();
}
isWaitingForManifestRefresh = false; isWaitingForManifestRefresh = false;
expiredManifestPublishTimeUs = C.TIME_UNSET; expiredManifestPublishTimeUs = C.TIME_UNSET;
this.manifest = newManifest; this.manifest = newManifest;
removePreviouslyExpiredManifestPublishTimeValues();
} }
private boolean isManifestStale(DashManifest manifest) { /* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
return manifest.dynamic
&& (dynamicMediaPresentationEnded
|| manifest.publishTimeMs <= expiredManifestPublishTimeUs);
}
/* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)
throws DashManifestExpiredException {
if (fatalError != null) {
throw fatalError;
}
if (!manifest.dynamic) { if (!manifest.dynamic) {
return false; return false;
} }
...@@ -273,6 +260,18 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -273,6 +260,18 @@ public final class PlayerEmsgHandler implements Handler.Callback {
return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); 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() { private void notifyManifestPublishTimeExpired() {
playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs);
} }
...@@ -351,11 +350,8 @@ public final class PlayerEmsgHandler implements Handler.Callback { ...@@ -351,11 +350,8 @@ public final class PlayerEmsgHandler implements Handler.Callback {
* *
* @param presentationPositionUs The next load position in presentation time. * @param presentationPositionUs The next load position in presentation time.
* @return True if manifest refresh has been requested, false otherwise. * @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) public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
throws DashManifestExpiredException {
return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk( return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk(
presentationPositionUs); presentationPositionUs);
} }
......
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