From a06a670d63803b216e4f39af9a3d6c64b144af35 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 22 Jan 2018 06:29:51 -0800 Subject: [PATCH] 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 --- ...n.java => DashManifestStaleException.java} | 4 +- .../source/dash/DashMediaSource.java | 133 +++++++++++------- .../source/dash/DefaultDashChunkSource.java | 13 +- .../source/dash/PlayerEmsgHandler.java | 36 +++-- 4 files changed, 107 insertions(+), 79 deletions(-) rename library/dash/src/main/java/com/google/android/exoplayer2/source/dash/{DashManifestExpiredException.java => DashManifestStaleException.java} (80%) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestStaleException.java similarity index 80% rename from library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java rename to library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestStaleException.java index 2af847467c..7d946cb105 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestStaleException.java @@ -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 {} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 593cf4f231..f4b175540c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -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 void startLoading(ParsingLoadable loadable, Loader.Callback> 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; + } + } + } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 4635a08a3c..b93338df35 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -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 = diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index bdcfef24c1..affeeafe50 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -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> it = + manifestPublishTimeToExpiryTimeUs.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry 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); }