diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ecc967f016c..27644c609c8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.9.2 ### +* DASH: Fix detecting the end of live events + ([#4780](https://github.com/google/ExoPlayer/issues/4780)). * Include channel count in audio capabilities check ([#4690](https://github.com/google/ExoPlayer/issues/4690)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 1639920aaaa..bb7f027726a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -139,9 +139,12 @@ public static final class Window { */ public boolean isSeekable; - /** - * Whether this window may change when the timeline is updated. - */ + // TODO: Split this to better describe which parts of the window might change. For example it + // should be possible to individually determine whether the start and end positions of the + // window may change relative to the underlying periods. For an example of where it's useful to + // know that the end position is fixed whilst the start position may still change, see: + // https://github.com/google/ExoPlayer/issues/4780. + /** Whether this window may change when the timeline is updated. */ public boolean isDynamic; /** 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 0c05be873a8..8ee859b8bdd 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 @@ -376,7 +376,6 @@ public int[] getSupportedTypes() { private int staleManifestReloadAttempt; private long expiredManifestPublishTimeUs; - private boolean dynamicMediaPresentationEnded; private int firstPeriodId; @@ -679,7 +678,6 @@ public void releaseSourceInternal() { elapsedRealtimeOffsetMs = 0; staleManifestReloadAttempt = 0; expiredManifestPublishTimeUs = C.TIME_UNSET; - dynamicMediaPresentationEnded = false; firstPeriodId = 0; periodsById.clear(); } @@ -691,10 +689,6 @@ public void releaseSourceInternal() { startLoadingManifest(); } - /* package */ void onDashLiveMediaPresentationEndSignalEncountered() { - this.dynamicMediaPresentationEnded = true; - } - /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { if (this.expiredManifestPublishTimeUs == C.TIME_UNSET || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) { @@ -734,9 +728,8 @@ public void releaseSourceInternal() { // behind. Log.w(TAG, "Loaded out of sync manifest"); isManifestStale = true; - } else if (dynamicMediaPresentationEnded - || (expiredManifestPublishTimeUs != C.TIME_UNSET - && newManifest.publishTimeMs * 1000 <= expiredManifestPublishTimeUs)) { + } else if (expiredManifestPublishTimeUs != C.TIME_UNSET + && newManifest.publishTimeMs * 1000 <= 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. @@ -745,8 +738,6 @@ public void releaseSourceInternal() { "Loaded stale dynamic manifest: " + newManifest.publishTimeMs + ", " - + dynamicMediaPresentationEnded - + ", " + expiredManifestPublishTimeUs); isManifestStale = true; } @@ -763,7 +754,6 @@ public void releaseSourceInternal() { staleManifestReloadAttempt = 0; } - manifest = newManifest; manifestLoadPending &= manifest.dynamic; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; @@ -1170,12 +1160,16 @@ public Window getWindow( long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); Object tag = setTag ? windowTag : null; + boolean isDynamic = + manifest.dynamic + && manifest.minUpdatePeriodMs != C.TIME_UNSET + && manifest.durationMs == C.TIME_UNSET; return window.set( tag, presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, - manifest.dynamic, + isDynamic, windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, @@ -1253,11 +1247,6 @@ public void onDashManifestRefreshRequested() { public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); } - - @Override - public void onDashLiveMediaPresentationEndSignalEncountered() { - DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered(); - } } private final class ManifestCallback implements Loader.Callback> { 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 1ea25ecc36b..5e20fb769ce 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 @@ -318,9 +318,12 @@ public void getNextChunk( } } + long periodDurationUs = representationHolder.periodDurationUs; + boolean periodEnded = periodDurationUs != C.TIME_UNSET; + if (representationHolder.getSegmentCount() == 0) { // The index doesn't define any segments. - out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + out.endOfStream = periodEnded; return; } @@ -343,17 +346,15 @@ public void getNextChunk( fatalError = new BehindLiveWindowException(); return; } + if (segmentNum > lastAvailableSegmentNum || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { - // The segment is beyond the end of the period. We know the period will not be extended if the - // manifest is static, or if there's a period after this one. - out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + // The segment is beyond the end of the period. + out.endOfStream = periodEnded; return; } - long periodDurationUs = representationHolder.periodDurationUs; - if (periodDurationUs != C.TIME_UNSET - && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) { + if (periodEnded && representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) { // The period duration clips the period to a position before the segment. out.endOfStream = true; return; 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 e657d126bf2..be299308a94 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 @@ -60,8 +60,7 @@ */ public final class PlayerEmsgHandler implements Handler.Callback { - private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1; - private static final int EMSG_MANIFEST_EXPIRED = 2; + private static final int EMSG_MANIFEST_EXPIRED = 1; /** Callbacks for player emsg events encountered during DASH live stream. */ public interface PlayerEmsgCallback { @@ -75,9 +74,6 @@ public interface PlayerEmsgCallback { * @param expiredManifestPublishTimeUs The manifest publish time that has been expired. */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs); - - /** Called when a media presentation end signal is encountered during live stream. * */ - void onDashLiveMediaPresentationEndSignalEncountered(); } private final Allocator allocator; @@ -88,7 +84,6 @@ public interface PlayerEmsgCallback { private DashManifest manifest; - private boolean dynamicMediaPresentationEnded; private long expiredManifestPublishTimeUs; private long lastLoadedChunkEndTimeUs; private long lastLoadedChunkEndTimeBeforeRefreshUs; @@ -134,21 +129,15 @@ public void updateManifest(DashManifest newManifest) { return true; } boolean manifestRefreshNeeded = false; - if (dynamicMediaPresentationEnded) { - // The manifest we have is dynamic, but we know a non-dynamic one representing the final state - // should be available. - manifestRefreshNeeded = true; - } else { - // Find the smallest publishTime (greater than or equal to the current manifest's publish - // time) that has a corresponding expiry time. - Map.Entry expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); - if (expiredEntry != null) { - long expiredPointUs = expiredEntry.getValue(); - if (expiredPointUs < presentationPositionUs) { - expiredManifestPublishTimeUs = expiredEntry.getKey(); - notifyManifestPublishTimeExpired(); - manifestRefreshNeeded = true; - } + // Find the smallest publishTime (greater than or equal to the current manifest's publish time) + // that has a corresponding expiry time. + Map.Entry expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); + if (expiredEntry != null) { + long expiredPointUs = expiredEntry.getValue(); + if (expiredPointUs < presentationPositionUs) { + expiredManifestPublishTimeUs = expiredEntry.getKey(); + notifyManifestPublishTimeExpired(); + manifestRefreshNeeded = true; } } if (manifestRefreshNeeded) { @@ -221,9 +210,6 @@ public boolean handleMessage(Message message) { return true; } switch (message.what) { - case (EMSG_MEDIA_PRESENTATION_ENDED): - handleMediaPresentationEndedMessageEncountered(); - return true; case (EMSG_MANIFEST_EXPIRED): ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; handleManifestExpiredMessage( @@ -248,11 +234,6 @@ private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublish } } - private void handleMediaPresentationEndedMessageEncountered() { - dynamicMediaPresentationEnded = true; - notifySourceMediaPresentationEnded(); - } - private @Nullable Map.Entry ceilingExpiryEntryForPublishTime(long publishTimeMs) { return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); } @@ -273,10 +254,6 @@ private void notifyManifestPublishTimeExpired() { playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); } - private void notifySourceMediaPresentationEnded() { - playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered(); - } - /** Requests DASH media manifest to be refreshed if necessary. */ private void maybeNotifyDashManifestRefreshNeeded() { if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET @@ -298,12 +275,6 @@ private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) { } } - private static boolean isMessageSignalingMediaPresentationEnded(EventMessage eventMessage) { - // According to section 4.5.2.1 DASH-IF IOP, if both presentation time delta and event duration - // are zero, the media presentation is ended. - return eventMessage.presentationTimeUs == 0 && eventMessage.durationMs == 0; - } - /** Handles emsg messages for a specific track for the player. */ public final class PlayerTrackEmsgHandler implements TrackOutput { @@ -413,16 +384,7 @@ private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) { if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) { return; } - - if (isMessageSignalingMediaPresentationEnded(eventMessage)) { - onMediaPresentationEndedMessageEncountered(); - } else { - onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg); - } - } - - private void onMediaPresentationEndedMessageEncountered() { - handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED)); + onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg); } private void onManifestExpiredMessageEncountered(