From 40101094ba9c3e166af183dd446bda81b7f64914 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 12 Feb 2020 10:57:37 +0000 Subject: [PATCH] Add option to send messages at end of stream. This simplifies sending messages to the end of a stream. This was already possible, but users needed to wait until the duration is known before sending the message. This duration resolution can happen as part of the message position resolution. PiperOrigin-RevId: 294626984 --- RELEASENOTES.md | 1 + .../exoplayer2/ExoPlayerImplInternal.java | 78 +++++++++++++------ .../android/exoplayer2/PlayerMessage.java | 10 ++- .../android/exoplayer2/ExoPlayerTest.java | 41 +++++++--- 4 files changed, 96 insertions(+), 34 deletions(-) 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