diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 38aed91cc5..9bff079475 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,7 @@ * Move player message-related constants from `C` to `Renderer`, to avoid having the constants class depend on player/renderer classes. * Split out `common` and `extractor` submodules. + * Allow to explicitly send `PlayerMessage`s at the end of a stream. * Add `DataSpec.Builder` and deprecate most `DataSpec` constructors. * Add `DataSpec.customData` to allow applications to pass custom data through `DataSource` chains. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 79dbb3ac14..bd66e33053 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -2241,6 +2241,10 @@ import java.util.concurrent.atomic.AtomicBoolean; Timeline.Period period) { if (pendingMessageInfo.resolvedPeriodUid == null) { // Position is still unresolved. Try to find window in new timeline. + long requestPositionUs = + pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE + ? C.TIME_UNSET + : C.msToUs(pendingMessageInfo.message.getPositionMs()); @Nullable Pair periodPosition = resolveSeekPosition( @@ -2248,7 +2252,7 @@ import java.util.concurrent.atomic.AtomicBoolean; new SeekPosition( pendingMessageInfo.message.getTimeline(), pendingMessageInfo.message.getWindowIndex(), - C.msToUs(pendingMessageInfo.message.getPositionMs())), + requestPositionUs), /* trySubsequentPeriods= */ false, repeatMode, shuffleModeEnabled, @@ -2261,32 +2265,62 @@ import java.util.concurrent.atomic.AtomicBoolean; /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), /* periodTimeUs= */ periodPosition.second, /* periodUid= */ periodPosition.first); - } else { - // Position has been resolved for a previous timeline. Try to find the updated period index. - int index = newTimeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); - if (index == C.INDEX_UNSET) { - return false; - } - pendingMessageInfo.resolvedPeriodIndex = index; - previousTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period); - if (previousTimeline.getWindow(period.windowIndex, window).isPlaceholder) { - // The position needs to be re-resolved because the window in the previous timeline wasn't - // fully prepared. - long windowPositionUs = - pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs(); - int windowIndex = - newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex; - Pair periodPosition = - newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); - pendingMessageInfo.setResolvedPosition( - /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), - /* periodTimeUs= */ periodPosition.second, - /* periodUid= */ periodPosition.first); + if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { + resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); } + return true; + } + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = newTimeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { + // Re-resolve end of stream in case the duration changed. + resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); + return true; + } + pendingMessageInfo.resolvedPeriodIndex = index; + previousTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period); + if (previousTimeline.getWindow(period.windowIndex, window).isPlaceholder) { + // The position needs to be re-resolved because the window in the previous timeline wasn't + // fully prepared. + long windowPositionUs = + pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs(); + int windowIndex = + newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex; + Pair periodPosition = + newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); + pendingMessageInfo.setResolvedPosition( + /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), + /* periodTimeUs= */ periodPosition.second, + /* periodUid= */ periodPosition.first); } return true; } + private static void resolvePendingMessageEndOfStreamPosition( + Timeline timeline, + PendingMessageInfo messageInfo, + Timeline.Window window, + Timeline.Period period) { + int windowIndex = timeline.getPeriodByUid(messageInfo.resolvedPeriodUid, period).windowIndex; + timeline.getWindow(windowIndex, window); + if (!window.isDynamic && window.durationUs != C.TIME_UNSET) { + Pair periodPosition = + timeline.getPeriodPosition(window, period, windowIndex, window.durationUs); + messageInfo.setResolvedPosition( + /* periodIndex= */ timeline.getIndexOfPeriod(periodPosition.first), + /* periodTimeUs= */ periodPosition.second, + /* periodUid= */ periodPosition.first); + } else { + int lastPeriodIndex = timeline.getWindow(windowIndex, window).lastPeriodIndex; + Object lastPeriodUid = timeline.getUidOfPeriod(lastPeriodIndex); + messageInfo.setResolvedPosition( + lastPeriodIndex, /* periodTimeUs= */ Long.MAX_VALUE, lastPeriodUid); + } + } + /** * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the * internal timeline. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index e7ade7b68b..be7c7ce973 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -162,7 +162,9 @@ public final class PlayerMessage { /** * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, - * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. If {@link + * C#TIME_END_OF_SOURCE}, the message will be delivered at the end of the window at {@link + * #getWindowIndex()}. */ public long getPositionMs() { return positionMs; @@ -172,7 +174,8 @@ public final class PlayerMessage { * Sets a position in the current window at which the message will be delivered. * * @param positionMs The position in the current window at which the message will be sent, in - * milliseconds. + * milliseconds, or {@link C#TIME_END_OF_SOURCE} to deliver the message at the end of the + * current window. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ @@ -187,7 +190,8 @@ public final class PlayerMessage { * * @param windowIndex The index of the window at which the message will be sent. * @param positionMs The position in the window with index {@code windowIndex} at which the - * message will be sent, in milliseconds. + * message will be sent, in milliseconds, or {@link C#TIME_END_OF_SOURCE} to deliver the + * message at the end of the window with index {@code windowIndex}. * @return This message. * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not * empty and the provided window index is not within the bounds of the timeline. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 927c51b468..99f72f1e96 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -1878,18 +1878,33 @@ public final class ExoPlayerTest { public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriodResolved = + new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriodUnresolved = + new PositionGrabbingMessageTarget(); PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriodResolved = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriodUnresolved = + new PositionGrabbingMessageTarget(); long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSendMessagesAtStartAndEndOfPeriod") .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) - .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage( + targetEndMiddlePeriodResolved, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage( + targetEndMiddlePeriodUnresolved, + /* windowIndex= */ 0, + /* positionMs= */ C.TIME_END_OF_SOURCE) .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) - .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) - .waitForMessage(targetEndLastPeriod) + .sendMessage( + targetEndLastPeriodResolved, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + .sendMessage( + targetEndLastPeriodUnresolved, + /* windowIndex= */ 1, + /* positionMs= */ C.TIME_END_OF_SOURCE) + .waitForMessage(targetEndLastPeriodUnresolved) .build(); new Builder() .setTimeline(timeline) @@ -1900,12 +1915,20 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); assertThat(targetStartFirstPeriod.windowIndex).isEqualTo(0); assertThat(targetStartFirstPeriod.positionMs).isAtLeast(0L); - assertThat(targetEndMiddlePeriod.windowIndex).isEqualTo(0); - assertThat(targetEndMiddlePeriod.positionMs).isAtLeast(duration1Ms); + assertThat(targetEndMiddlePeriodResolved.windowIndex).isEqualTo(0); + assertThat(targetEndMiddlePeriodResolved.positionMs).isAtLeast(duration1Ms); + assertThat(targetEndMiddlePeriodUnresolved.windowIndex).isEqualTo(0); + assertThat(targetEndMiddlePeriodUnresolved.positionMs).isAtLeast(duration1Ms); + assertThat(targetEndMiddlePeriodResolved.positionMs) + .isEqualTo(targetEndMiddlePeriodUnresolved.positionMs); assertThat(targetStartMiddlePeriod.windowIndex).isEqualTo(1); assertThat(targetStartMiddlePeriod.positionMs).isAtLeast(0L); - assertThat(targetEndLastPeriod.windowIndex).isEqualTo(1); - assertThat(targetEndLastPeriod.positionMs).isAtLeast(duration2Ms); + assertThat(targetEndLastPeriodResolved.windowIndex).isEqualTo(1); + assertThat(targetEndLastPeriodResolved.positionMs).isAtLeast(duration2Ms); + assertThat(targetEndLastPeriodUnresolved.windowIndex).isEqualTo(1); + assertThat(targetEndLastPeriodUnresolved.positionMs).isAtLeast(duration2Ms); + assertThat(targetEndLastPeriodResolved.positionMs) + .isEqualTo(targetEndLastPeriodUnresolved.positionMs); } @Test