PlayerEmsgHandler: Track stream max chunk times separately

Issue: #8408
PiperOrigin-RevId: 350786430
This commit is contained in:
olly 2021-01-08 18:11:20 +00:00 committed by Oliver Woodman
parent c1529c46d8
commit a7379ee658
3 changed files with 77 additions and 90 deletions

View file

@ -64,6 +64,9 @@
* Support low-latency DASH playback (`availabilityTimeOffset` and * Support low-latency DASH playback (`availabilityTimeOffset` and
`ServiceDescription` tags) `ServiceDescription` tags)
([#4904](https://github.com/google/ExoPlayer/issues/4904)). ([#4904](https://github.com/google/ExoPlayer/issues/4904)).
* Improve logic for determining whether to refresh the manifest when a
chunk load error occurs in a live streams that contains EMSG data
([#8408](https://github.com/google/ExoPlayer/issues/8408)).
* HLS: * HLS:
* Support playlist delta updates, blocking playlist reloads and rendition * Support playlist delta updates, blocking playlist reloads and rendition
reports. reports.

View file

@ -429,8 +429,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
if (!cancelable) { if (!cancelable) {
return false; return false;
} }
if (playerTrackEmsgHandler != null if (playerTrackEmsgHandler != null && playerTrackEmsgHandler.onChunkLoadError(chunk)) {
&& playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) {
return true; return true;
} }
// Workaround for missing segment at the end of the period // Workaround for missing segment at the end of the period

View file

@ -85,8 +85,7 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private DashManifest manifest; private DashManifest manifest;
private long expiredManifestPublishTimeUs; private long expiredManifestPublishTimeUs;
private long lastLoadedChunkEndTimeUs; private boolean chunkLoadedCompletedSinceLastManifestRefreshRequest;
private long lastLoadedChunkEndTimeBeforeRefreshUs;
private boolean isWaitingForManifestRefresh; private boolean isWaitingForManifestRefresh;
private boolean released; private boolean released;
@ -105,8 +104,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
manifestPublishTimeToExpiryTimeUs = new TreeMap<>(); manifestPublishTimeToExpiryTimeUs = new TreeMap<>();
handler = Util.createHandlerForCurrentLooper(/* callback= */ this); handler = Util.createHandlerForCurrentLooper(/* callback= */ this);
decoder = new EventMessageDecoder(); decoder = new EventMessageDecoder();
lastLoadedChunkEndTimeUs = C.TIME_UNSET;
lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET;
} }
/** /**
@ -121,78 +118,6 @@ public final class PlayerEmsgHandler implements Handler.Callback {
removePreviouslyExpiredManifestPublishTimeValues(); removePreviouslyExpiredManifestPublishTimeValues();
} }
/* package */ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
if (!manifest.dynamic) {
return false;
}
if (isWaitingForManifestRefresh) {
return true;
}
boolean manifestRefreshNeeded = false;
// Find the smallest publishTime (greater than or equal to the current manifest's publish time)
// that has a corresponding expiry time.
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
if (expiredEntry != null) {
long expiredPointUs = expiredEntry.getValue();
if (expiredPointUs < presentationPositionUs) {
expiredManifestPublishTimeUs = expiredEntry.getKey();
notifyManifestPublishTimeExpired();
manifestRefreshNeeded = true;
}
}
if (manifestRefreshNeeded) {
maybeNotifyDashManifestRefreshNeeded();
}
return manifestRefreshNeeded;
}
/**
* For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that
* signals end-of-stream or Manifest expiry, which results in load error. In this case, we should
* notify the Dash media source to refresh its manifest.
*
* @param chunk The chunk whose load encountered the error.
* @return True if manifest refresh has been requested, false otherwise.
*/
/* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) {
if (!manifest.dynamic) {
return false;
}
if (isWaitingForManifestRefresh) {
return true;
}
boolean isAfterForwardSeek =
lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs;
if (isAfterForwardSeek) {
// if we are after a forward seek, and the playback is dynamic with embedded emsg stream,
// there's a chance that we have seek over the emsg messages, in which case we should ask
// media source for a refresh.
maybeNotifyDashManifestRefreshNeeded();
return true;
}
return false;
}
/**
* Called when the a new chunk in the current media stream has been loaded.
*
* @param chunk The chunk whose load has been completed.
*/
/* package */ void onChunkLoadCompleted(Chunk chunk) {
if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) {
lastLoadedChunkEndTimeUs = chunk.endTimeUs;
}
}
/**
* Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the
* player.
*/
public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) {
return "urn:mpeg:dash:event:2012".equals(schemeIdUri)
&& ("1".equals(value) || "2".equals(value) || "3".equals(value));
}
/** Returns a {@link TrackOutput} that emsg messages could be written to. */ /** Returns a {@link TrackOutput} that emsg messages could be written to. */
public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() {
return new PlayerTrackEmsgHandler(allocator); return new PlayerTrackEmsgHandler(allocator);
@ -223,6 +148,52 @@ public final class PlayerEmsgHandler implements Handler.Callback {
// Internal methods. // Internal methods.
/* package */ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) {
if (!manifest.dynamic) {
return false;
}
if (isWaitingForManifestRefresh) {
return true;
}
boolean manifestRefreshNeeded = false;
// Find the smallest publishTime (greater than or equal to the current manifest's publish time)
// that has a corresponding expiry time.
Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs);
if (expiredEntry != null) {
long expiredPointUs = expiredEntry.getValue();
if (expiredPointUs < presentationPositionUs) {
expiredManifestPublishTimeUs = expiredEntry.getKey();
notifyManifestPublishTimeExpired();
manifestRefreshNeeded = true;
}
}
if (manifestRefreshNeeded) {
maybeNotifyDashManifestRefreshNeeded();
}
return manifestRefreshNeeded;
}
/* package */ void onChunkLoadCompleted(Chunk chunk) {
chunkLoadedCompletedSinceLastManifestRefreshRequest = true;
}
/* package */ boolean onChunkLoadError(boolean isForwardSeek) {
if (!manifest.dynamic) {
return false;
}
if (isWaitingForManifestRefresh) {
return true;
}
if (isForwardSeek) {
// If a forward seek has occurred, there's a chance that the seek has skipped EMSGs signalling
// end-of-stream or manifest expiration. We must assume that the manifest might need to be
// refreshed.
maybeNotifyDashManifestRefreshNeeded();
return true;
}
return false;
}
private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) { private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) {
Long previousExpiryTimeUs = manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); Long previousExpiryTimeUs = manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg);
if (previousExpiryTimeUs == null) { if (previousExpiryTimeUs == null) {
@ -256,13 +227,12 @@ public final class PlayerEmsgHandler implements Handler.Callback {
/** Requests DASH media manifest to be refreshed if necessary. */ /** Requests DASH media manifest to be refreshed if necessary. */
private void maybeNotifyDashManifestRefreshNeeded() { private void maybeNotifyDashManifestRefreshNeeded() {
if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET if (!chunkLoadedCompletedSinceLastManifestRefreshRequest) {
&& lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) { // Don't request a refresh unless some progress has been made.
// Already requested manifest refresh.
return; return;
} }
isWaitingForManifestRefresh = true; isWaitingForManifestRefresh = true;
lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs; chunkLoadedCompletedSinceLastManifestRefreshRequest = false;
playerEmsgCallback.onDashManifestRefreshRequested(); playerEmsgCallback.onDashManifestRefreshRequested();
} }
@ -275,6 +245,15 @@ public final class PlayerEmsgHandler implements Handler.Callback {
} }
} }
/**
* Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the
* player.
*/
private static boolean isPlayerEmsgEvent(String schemeIdUri, String value) {
return "urn:mpeg:dash:event:2012".equals(schemeIdUri)
&& ("1".equals(value) || "2".equals(value) || "3".equals(value));
}
/** Handles emsg messages for a specific track for the player. */ /** Handles emsg messages for a specific track for the player. */
public final class PlayerTrackEmsgHandler implements TrackOutput { public final class PlayerTrackEmsgHandler implements TrackOutput {
@ -282,10 +261,13 @@ public final class PlayerEmsgHandler implements Handler.Callback {
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final MetadataInputBuffer buffer; private final MetadataInputBuffer buffer;
private long maxLoadedChunkEndTimeUs;
/* package */ PlayerTrackEmsgHandler(Allocator allocator) { /* package */ PlayerTrackEmsgHandler(Allocator allocator) {
this.sampleQueue = SampleQueue.createWithoutDrm(allocator); this.sampleQueue = SampleQueue.createWithoutDrm(allocator);
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
buffer = new MetadataInputBuffer(); buffer = new MetadataInputBuffer();
maxLoadedChunkEndTimeUs = C.TIME_UNSET;
} }
@Override @Override
@ -325,24 +307,27 @@ public final class PlayerEmsgHandler implements Handler.Callback {
} }
/** /**
* Called when the a new chunk in the current media stream has been loaded. * Called when a chunk load has been completed.
* *
* @param chunk The chunk whose load has been completed. * @param chunk The chunk whose load has been completed.
*/ */
public void onChunkLoadCompleted(Chunk chunk) { public void onChunkLoadCompleted(Chunk chunk) {
if (maxLoadedChunkEndTimeUs == C.TIME_UNSET || chunk.endTimeUs > maxLoadedChunkEndTimeUs) {
maxLoadedChunkEndTimeUs = chunk.endTimeUs;
}
PlayerEmsgHandler.this.onChunkLoadCompleted(chunk); PlayerEmsgHandler.this.onChunkLoadCompleted(chunk);
} }
/** /**
* For live streaming with emsg event stream, forward seeking can seek pass the emsg messages * Called when a chunk load has encountered an error.
* that signals end-of-stream or Manifest expiry, which results in load error. In this case, we
* should notify the Dash media source to refresh its manifest.
* *
* @param chunk The chunk whose load encountered the error. * @param chunk The chunk whose load encountered an error.
* @return True if manifest refresh has been requested, false otherwise. * @return Whether a manifest refresh has been requested.
*/ */
public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { public boolean onChunkLoadError(Chunk chunk) {
return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk); boolean isAfterForwardSeek =
maxLoadedChunkEndTimeUs != C.TIME_UNSET && maxLoadedChunkEndTimeUs < chunk.startTimeUs;
return PlayerEmsgHandler.this.onChunkLoadError(isAfterForwardSeek);
} }
/** Release this track emsg handler. It should not be reused after this call. */ /** Release this track emsg handler. It should not be reused after this call. */