mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
PlayerEmsgHandler: Track stream max chunk times separately
Issue: #8408 PiperOrigin-RevId: 350786430
This commit is contained in:
parent
c1529c46d8
commit
a7379ee658
3 changed files with 77 additions and 90 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue