diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6438cbdd68..c01f2c29ee 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* Added a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9233b016f5..473a0d3441 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -116,10 +116,12 @@ import java.util.Locale; } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); - Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); + Log.d(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount + + ", reason=" + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getPeriod(i, period); Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); @@ -507,4 +509,18 @@ import java.util.Locale; return "?"; } } + + private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; + default: + return "?"; + } + } + } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 9a8986409a..32e064e834 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -116,6 +116,7 @@ public final class CastPlayer implements Player { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; + private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -170,6 +171,7 @@ public final class CastPlayer implements Player { public PendingResult loadItems(MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { + waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -556,8 +558,11 @@ public final class CastPlayer implements Player { private void maybeUpdateTimelineAndNotify() { if (updateTimeline()) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null); + listener.onTimelineChanged(currentTimeline, null, reason); } } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5b61db0264..fe6a6d6196 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -523,9 +523,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Player.EventListener implementation. @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline.isEmpty()) { - // The player is being re-prepared and this source will be released. + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { + if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { + // The player is being reset and this source will be released. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 510ed9cf4f..c9ed54398e 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -258,7 +259,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { Callback callback = getCallback(); callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index aa007ea1d6..d80487f2bd 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -628,7 +628,8 @@ public final class MediaSessionConnector { private int currentWindowCount; @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index f0f1c23c2b..59a58a4912 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; @@ -59,7 +61,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(); + testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); @@ -78,6 +80,7 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -97,6 +100,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); @@ -210,6 +214,8 @@ public final class ExoPlayerTest extends TestCase { // info refresh from the second source was suppressed as we re-prepared with the third source. testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -243,6 +249,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertTrue(renderer.isEnded); } @@ -513,4 +520,25 @@ public final class ExoPlayerTest extends TestCase { assertEquals(3, numSelectionsEnabled); } + public void testDynamicTimelineChangeReason() throws Exception { + Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") + .waitForTimelineChanged(timeline1) + .executeRunnable(new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline2, null); + } + }) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline1, timeline2); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_DYNAMIC); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ff00f9de91..77131f5ded 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -56,6 +56,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; + private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; @@ -146,7 +147,8 @@ import java.util.concurrent.CopyOnWriteArraySet; if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); } } if (tracksSelected) { @@ -159,6 +161,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + waitingForInitialTimeline = true; pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -532,9 +535,12 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } - if (timelineOrManifestChanged) { + if (timelineOrManifestChanged || waitingForInitialTimeline) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, reason); } } if (positionDiscontinuity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index dc703f924a..77fced0832 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -59,8 +59,9 @@ public interface Player { * * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - void onTimelineChanged(Timeline timeline, Object manifest); + void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); /** * Called when the available or selected tracks change. @@ -118,7 +119,8 @@ public interface Player { * when the source introduces a discontinuity internally). *

* When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. + * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this + * case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -149,8 +151,10 @@ public interface Player { abstract class DefaultEventListener implements EventListener { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { + // Call deprecated version. Otherwise, do nothing. + onTimelineChanged(timeline, manifest); } @Override @@ -198,6 +202,15 @@ public interface Player { // Do nothing. } + /** + * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)} + * instead. + */ + @Deprecated + public void onTimelineChanged(Timeline timeline, Object manifest) { + // Do nothing. + } + } /** @@ -264,6 +277,26 @@ public interface Player { */ int DISCONTINUITY_REASON_INTERNAL = 3; + /** + * Reasons for timeline and/or manifest changes. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TIMELINE_CHANGE_REASON_PREPARED, TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC}) + public @interface TimelineChangeReason {} + /** + * Timeline and manifest changed as a result of a player initialization with new media. + */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** + * Timeline and manifest changed as a result of a player reset. + */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a96ed3a622..751a6c81a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -699,6 +699,8 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); break; + default: + // Never happens. } repeatToggleButton.setVisibility(View.VISIBLE); } @@ -1098,7 +1100,8 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 2abe521883..357d69df38 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -304,7 +304,7 @@ public abstract class Action { } /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. + * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. */ public static final class WaitForTimelineChanged extends Action { @@ -327,7 +327,8 @@ public abstract class Action { } Player.EventListener listener = new Player.DefaultEventListener() { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { if (timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 6730bf1c7f..638ad9e12d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -318,6 +318,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final CountDownLatch endedCountDownLatch; private final LinkedList timelines; private final LinkedList manifests; + private final ArrayList timelineChangeReasons; private final LinkedList periodIndices; private final ArrayList discontinuityReasons; @@ -338,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.eventListener = eventListener; this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); + this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new LinkedList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); @@ -430,6 +432,18 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the timeline change reasons reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timeline change reasons. + */ + public void assertTimelineChangeReasonsEqual(@Player.TimelineChangeReason int... reasons) { + Assert.assertEquals(reasons.length, timelineChangeReasons.size()); + for (int i = 0; i < reasons.length; i++) { + Assert.assertEquals(reasons[i], (int) timelineChangeReasons.get(i)); + } + } + /** * Asserts that the last track group array reported by * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to @@ -507,9 +521,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Player.EventListener @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { timelines.add(timeline); manifests.add(manifest); + timelineChangeReasons.add(reason); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 4a5beb0501..6dc9cf7fd8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -340,7 +340,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; FakeExoPlayer.this.timeline = timeline; FakeExoPlayer.this.manifest = manifest; - eventListener.onTimelineChanged(timeline, manifest); + eventListener.onTimelineChanged(timeline, manifest, + Player.TIMELINE_CHANGE_REASON_PREPARED); waitForNotification.open(); } }