mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
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:
parent
05e55f37eb
commit
a06a670d63
4 changed files with 107 additions and 79 deletions
|
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
minLoadableRetryCount,
|
minLoadableRetryCount,
|
||||||
periodEventDispatcher,
|
periodEventDispatcher,
|
||||||
elapsedRealtimeOffsetMs,
|
elapsedRealtimeOffsetMs,
|
||||||
loaderErrorThrower,
|
manifestLoadErrorThrower,
|
||||||
allocator,
|
allocator,
|
||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
playerEmsgCallback);
|
playerEmsgCallback);
|
||||||
|
|
@ -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,36 +593,43 @@ public final class DashMediaSource implements MediaSource {
|
||||||
removedPeriodCount++;
|
removedPeriodCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After discarding old periods, we should never have more periods than listed in the new
|
if (newManifest.dynamic) {
|
||||||
// manifest. That would mean that a previously announced period is no longer advertised. If
|
boolean isManifestStale = false;
|
||||||
// this condition occurs, assume that we are hitting a manifest server that is out of sync and
|
if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) {
|
||||||
// behind, discard this manifest, and try again later.
|
// After discarding old periods, we should never have more periods than listed in the new
|
||||||
if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) {
|
// manifest. That would mean that a previously announced period is no longer advertised. If
|
||||||
Log.w(TAG, "Loaded out of sync manifest");
|
// this condition occurs, assume that we are hitting a manifest server that is out of sync
|
||||||
scheduleManifestRefresh();
|
// and
|
||||||
return;
|
// 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
|
if (isManifestStale) {
|
||||||
// expired, or it's dynamic and we know the presentation has ended), then ignore it and load
|
if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
|
||||||
// again up to a specified number of times.
|
scheduleManifestRefresh(getManifestLoadRetryDelayMillis());
|
||||||
if (newManifest.dynamic
|
} else {
|
||||||
&& (dynamicMediaPresentationEnded
|
manifestFatalError = new DashManifestStaleException();
|
||||||
|| newManifest.publishTimeMs <= expiredManifestPublishTimeUs)) {
|
}
|
||||||
Log.w(
|
|
||||||
TAG,
|
|
||||||
"Loaded stale dynamic manifest: "
|
|
||||||
+ newManifest.publishTimeMs
|
|
||||||
+ ", "
|
|
||||||
+ dynamicMediaPresentationEnded
|
|
||||||
+ ", "
|
|
||||||
+ expiredManifestPublishTimeUs);
|
|
||||||
if (staleManifestReloadAttempt++ < minLoadableRetryCount) {
|
|
||||||
startLoadingManifest();
|
|
||||||
return;
|
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 {
|
||||||
}
|
}
|
||||||
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();
|
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() {
|
private void scheduleManifestRefresh(long delayUntilNextLoadMs) {
|
||||||
if (!manifest.dynamic) {
|
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
|
||||||
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 startLoadingManifest() {
|
private void startLoadingManifest() {
|
||||||
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (DashManifestExpiredException e) {
|
|
||||||
fatalError = e;
|
|
||||||
return;
|
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
* @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 {
|
||||||
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 {
|
||||||
*
|
*
|
||||||
* @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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue