Add reason to onTimelineChanged.

Currently onTimelineChanged doesn't allow to distinguish easily between the
different reasons why it's being called. Especially, finding
out whether a new media source has been prepared or the current source
refreshed dynamically was impossible without tightly coupling the player
operations with the listener.

The new reasons provide this disdinction by either indicating a newly
initialized media source, a dynamic update to an existing timeline
or manifest, or a reset of the player (which usually results in an
empty timeline).

The original onTimelineChanged method without reason is kept in the
DefaultEventListener as deprecated to prevent the need to update all
existing listeners in one go.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=176478701
This commit is contained in:
tonihei 2017-11-21 01:33:48 -08:00 committed by Oliver Woodman
parent 13b595ed39
commit aac53cac56
13 changed files with 136 additions and 21 deletions

View File

@ -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 ###

View File

@ -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 "?";
}
}
}

View File

@ -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<MediaChannelResult> 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);
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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).
* <p>
* When a position discontinuity occurs as a result of a change to the timeline this method is
* <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
* <em>not</em> 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

View File

@ -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();

View File

@ -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);

View File

@ -318,6 +318,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener {
private final CountDownLatch endedCountDownLatch;
private final LinkedList<Timeline> timelines;
private final LinkedList<Object> manifests;
private final ArrayList<Integer> timelineChangeReasons;
private final LinkedList<Integer> periodIndices;
private final ArrayList<Integer> 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

View File

@ -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();
}
}