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 dad891718e..a227aa3575 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 @@ -56,18 +56,15 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 0); + Timeline timeline = Timeline.EMPTY; FakeRenderer renderer = new FakeRenderer(); - // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline - // update happens after the transition to STATE_ENDED and the test runner may already have been - // stopped. Remove action schedule as soon as state changes are part of the masking and the - // correct order of events is restored. - ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlayEmptyTimeline") - .waitForTimelineChanged(timeline) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) - .build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setRenderers(renderer) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); @@ -307,21 +304,28 @@ public final class ExoPlayerTest extends TestCase { public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek before timeline preparation started. Expect immediate seek processed while - // the player is still in STATE_IDLE. - .pause().seek(5) - // Wait until the media source starts preparing and issue more initial seeks. Expect only - // one seek processed after the source has been prepared. - .waitForPlaybackState(Player.STATE_BUFFERING).seek(2).seek(10) - // Wait until media source prepared and re-seek to same position. Expect a seek processed - // while still being in STATE_READY. - .waitForPlaybackState(Player.STATE_READY).seek(10) - // Start playback and wait until playback reaches second window. - .play().waitForPositionDiscontinuity() - // Seek twice in concession, expecting the first seek to be replaced (and thus except only - // on seek processed callback). - .seek(5).seek(60).build(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekProcessedCallback") + // Initial seek. Expect immediate seek processed. + .pause() + .seek(5) + .waitForSeekProcessed() + // Multiple overlapping seeks while the player is still preparing. Expect only one seek + // processed. + .seek(2) + .seek(10) + // Wait until media source prepared and re-seek to same position. Expect a seek + // processed while still being in STATE_READY. + .waitForPlaybackState(Player.STATE_READY) + .seek(10) + // Start playback and wait until playback reaches second window. + .play() + .waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced (and thus except + // only on seek processed callback). + .seek(5) + .seek(60) + .build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); Player.EventListener eventListener = new Player.DefaultEventListener() { private int currentPlaybackState = Player.STATE_IDLE; @@ -340,7 +344,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertEquals(4, playbackStatesWhenSeekProcessed.size()); - assertEquals(Player.STATE_IDLE, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(1)); assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(2)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); @@ -804,19 +808,24 @@ public final class ExoPlayerTest extends TestCase { public void testStopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") - .waitForPlaybackState(Player.STATE_BUFFERING) - .stop(true) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .seek(0) + .stop(true) + .waitForSeekProcessed() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertNoPositionDiscontinuities(); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { @@ -855,8 +864,9 @@ public final class ExoPlayerTest extends TestCase { .waitForPlaybackState(Player.STATE_IDLE) .prepareSource( new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ false, + /* resetPosition= */ true, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() 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 3fe6cc6eed..2869a7668e 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 @@ -42,24 +42,19 @@ import java.util.concurrent.CopyOnWriteArraySet; private final Renderer[] renderers; private final TrackSelector trackSelector; - private final TrackSelectionArray emptyTrackSelections; + private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; - private boolean tracksSelected; private boolean playWhenReady; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int playbackState; - private int pendingSeekAcks; - private int pendingPrepareOrStopAcks; - private boolean waitingForInitialTimeline; - private boolean isLoading; - private TrackGroupArray trackGroups; - private TrackSelectionArray trackSelections; + private int pendingOperationAcks; + private boolean hasPendingPrepare; + private boolean hasPendingSeek; private PlaybackParameters playbackParameters; // Playback information when there is no pending seek/set source operation. @@ -87,13 +82,16 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = false; this.repeatMode = Player.REPEAT_MODE_OFF; this.shuffleModeEnabled = false; - this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); - emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); + emptyTrackSelectorResult = + new TrackSelectorResult( + TrackGroupArray.EMPTY, + new boolean[renderers.length], + new TrackSelectionArray(new TrackSelection[renderers.length]), + null, + new RendererConfiguration[renderers.length]); window = new Timeline.Window(); period = new Timeline.Period(); - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); eventHandler = new Handler(eventLooper) { @@ -102,9 +100,19 @@ import java.util.concurrent.CopyOnWriteArraySet; ExoPlayerImpl.this.handleEvent(msg); } }; - playbackInfo = new PlaybackInfo(Timeline.EMPTY, null, 0, 0); - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - repeatMode, shuffleModeEnabled, eventHandler, this); + playbackInfo = + new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult); + internalPlayer = + new ExoPlayerImplInternal( + renderers, + trackSelector, + emptyTrackSelectorResult, + loadControl, + playWhenReady, + repeatMode, + shuffleModeEnabled, + eventHandler, + this); } @Override @@ -124,7 +132,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getPlaybackState() { - return playbackState; + return playbackInfo.playbackState; } @Override @@ -134,10 +142,22 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - waitingForInitialTimeline = true; - pendingPrepareOrStopAcks++; - reset(resetPosition, resetState); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; internalPlayer.prepare(mediaSource, resetPosition); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -146,7 +166,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); } } } @@ -190,7 +210,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isLoading() { - return isLoading; + return playbackInfo.isLoading; } @Override @@ -214,19 +234,22 @@ import java.util.concurrent.CopyOnWriteArraySet; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } + hasPendingSeek = true; + pendingOperationAcks++; if (isPlayingAd()) { // TODO: Investigate adding support for seeking during ads. This is complicated to do in // general because the midroll ad preceding the seek destination must be played before the // content position can be played, if a different ad is playing at the moment. Log.w(TAG, "seekTo ignored because an ad is playing"); - if (pendingSeekAcks == 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); - } - } + eventHandler + .obtainMessage( + ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, + /* operationAcks */ 1, + /* positionDiscontinuityReason */ C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); return; } - pendingSeekAcks++; maskingWindowIndex = windowIndex; if (timeline.isEmpty()) { maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; @@ -273,9 +296,23 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop(boolean reset) { - pendingPrepareOrStopAcks++; - reset(/* resetPosition= */ reset, /* resetState= */ reset); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* resetPosition= */ reset, + /* resetState= */ reset, + /* playbackState= */ Player.STATE_IDLE); + // Trigger internal stop first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this stop. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; internalPlayer.stop(reset); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -421,12 +458,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackGroupArray getCurrentTrackGroups() { - return trackGroups; + return playbackInfo.trackSelectorResult.groups; } @Override public TrackSelectionArray getCurrentTrackSelections() { - return trackSelections; + return playbackInfo.trackSelectorResult.selections; } @Override @@ -442,51 +479,14 @@ import java.util.concurrent.CopyOnWriteArraySet; // Not private so it can be called from an inner class without going through a thunk method. /* package */ void handleEvent(Message msg) { switch (msg.what) { - case ExoPlayerImplInternal.MSG_STATE_CHANGED: { - playbackState = msg.arg1; - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } + case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: + handlePlaybackInfo( + (PlaybackInfo) msg.obj, + /* operationAcks= */ msg.arg1, + /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, + /* positionDiscontinuityReason= */ msg.arg2); break; - } - case ExoPlayerImplInternal.MSG_LOADING_CHANGED: { - isLoading = msg.arg1 != 0; - for (Player.EventListener listener : listeners) { - listener.onLoadingChanged(isLoading); - } - break; - } - case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareOrStopAcks = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, 0, false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL); - break; - } - case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareOrStopAcks == 0) { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - break; - } - case ExoPlayerImplInternal.MSG_SEEK_ACK: { - boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, - DISCONTINUITY_REASON_SEEK_ADJUSTMENT); - break; - } - case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - @DiscontinuityReason int discontinuityReason = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); - break; - } - case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; if (!this.playbackParameters.equals(playbackParameters)) { this.playbackParameters = playbackParameters; @@ -495,24 +495,24 @@ import java.util.concurrent.CopyOnWriteArraySet; } } break; - } - case ExoPlayerImplInternal.MSG_ERROR: { + case ExoPlayerImplInternal.MSG_ERROR: ExoPlaybackException exception = (ExoPlaybackException) msg.obj; for (Player.EventListener listener : listeners) { listener.onPlayerError(exception); } break; - } default: throw new IllegalStateException(); } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, - boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { - pendingPrepareOrStopAcks -= prepareOrStopAcks; - pendingSeekAcks -= seekAcks; - if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { + private void handlePlaybackInfo( + PlaybackInfo playbackInfo, + int operationAcks, + boolean positionDiscontinuity, + @DiscontinuityReason int positionDiscontinuityReason) { + pendingOperationAcks -= operationAcks; + if (pendingOperationAcks == 0) { if (playbackInfo.timeline == null) { // Replace internal null timeline with externally visible empty timeline. playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest); @@ -523,37 +523,32 @@ import java.util.concurrent.CopyOnWriteArraySet; playbackInfo.fromNewPosition( playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); } - boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline - || this.playbackInfo.manifest != playbackInfo.manifest; - this.playbackInfo = playbackInfo; - if (timelineOrManifestChanged || waitingForInitialTimeline) { - if (playbackInfo.timeline.isEmpty()) { - // Update the masking variables, which are used when the timeline becomes empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; - } - @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, reason); - } - } - if (positionDiscontinuity) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(positionDiscontinuityReason); - } - } - } - if (pendingSeekAcks == 0 && seekAcks > 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); + if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare) + && playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; + hasPendingSeek = false; + updatePlaybackInfo( + playbackInfo, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed); } } - private void reset(boolean resetPosition, boolean resetState) { + private PlaybackInfo getResetPlaybackInfo( + boolean resetPosition, boolean resetState, int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; @@ -563,22 +558,62 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } + return new PlaybackInfo( + resetState ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + playbackInfo.periodId, + playbackInfo.startPositionUs, + playbackInfo.contentPositionUs, + playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + } + + private void updatePlaybackInfo( + PlaybackInfo newPlaybackInfo, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed) { + boolean timelineOrManifestChanged = + playbackInfo.timeline != newPlaybackInfo.timeline + || playbackInfo.manifest != newPlaybackInfo.manifest; + boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; + boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; + boolean trackSelectorResultChanged = + this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; + playbackInfo = newPlaybackInfo; + if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } + } + if (positionDiscontinuity) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + } + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged( + playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections); + } + } + if (isLoadingChanged) { + for (Player.EventListener listener : listeners) { + listener.onLoadingChanged(playbackInfo.isLoading); + } + } + if (playbackStateChanged) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); + } + } + if (seekProcessed) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); } } } @@ -593,7 +628,6 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; } - } 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 a1fe8c09c5..b52696533d 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 @@ -27,6 +27,7 @@ import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -51,14 +52,9 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages - public static final int MSG_STATE_CHANGED = 0; - public static final int MSG_LOADING_CHANGED = 1; - public static final int MSG_TRACKS_CHANGED = 2; - public static final int MSG_SEEK_ACK = 3; - public static final int MSG_POSITION_DISCONTINUITY = 4; - public static final int MSG_SOURCE_INFO_REFRESHED = 5; - public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 6; - public static final int MSG_ERROR = 7; + public static final int MSG_PLAYBACK_INFO_CHANGED = 0; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; + public static final int MSG_ERROR = 2; // Internal messages private static final int MSG_PREPARE = 0; @@ -99,6 +95,7 @@ import java.io.IOException; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; private final Handler handler; private final HandlerThread internalPlaybackThread; @@ -110,6 +107,7 @@ import java.io.IOException; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; + private final PlaybackInfoUpdate playbackInfoUpdate; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,8 +118,6 @@ import java.io.IOException; private boolean released; private boolean playWhenReady; private boolean rebuffering; - private boolean isLoading; - private int state; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; private int customMessagesSent; @@ -136,24 +132,34 @@ import java.io.IOException; private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder playingPeriodHolder; - public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled, Handler eventHandler, ExoPlayer player) { + public ExoPlayerImplInternal( + Renderer[] renderers, + TrackSelector trackSelector, + TrackSelectorResult emptyTrackSelectorResult, + LoadControl loadControl, + boolean playWhenReady, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Handler eventHandler, + ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; + this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; - this.state = Player.STATE_IDLE; this.player = player; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); seekParameters = SeekParameters.DEFAULT; - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); + playbackInfo = + new PlaybackInfo( + /* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -305,84 +311,99 @@ import java.io.IOException; switch (msg.what) { case MSG_PREPARE: prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); - return true; + break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); - return true; + break; case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); - return true; + break; case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); - return true; + break; case MSG_DO_SOME_WORK: doSomeWork(); - return true; + break; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); - return true; + break; case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); - return true; + break; case MSG_SET_SEEK_PARAMETERS: setSeekParametersInternal((SeekParameters) msg.obj); - return true; + break; case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); - return true; + break; case MSG_RELEASE: releaseInternal(); - return true; + break; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); - return true; + break; case MSG_REFRESH_SOURCE_INFO: handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); - return true; + break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); - return true; + break; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); - return true; + break; case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); - return true; + break; default: return false; } + maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { Log.e(TAG, "Source error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } + return true; } // Private methods. private void setState(int state) { - if (this.state != state) { - this.state = state; - eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); + if (playbackInfo.playbackState != state) { + playbackInfo = playbackInfo.copyWithPlaybackState(state); } } private void setIsLoading(boolean isLoading) { - if (this.isLoading != isLoading) { - this.isLoading = isLoading; - eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget(); + if (playbackInfo.isLoading != isLoading) { + playbackInfo = playbackInfo.copyWithIsLoading(isLoading); + } + } + + private void maybeNotifyPlaybackInfoChanged() { + if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { + eventHandler + .obtainMessage( + MSG_PLAYBACK_INFO_CHANGED, + playbackInfoUpdate.operationAcks, + playbackInfoUpdate.positionDiscontinuity + ? playbackInfoUpdate.discontinuityReason + : C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); + playbackInfoUpdate.reset(playbackInfo); } } @@ -403,10 +424,10 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == Player.STATE_READY) { + if (playbackInfo.playbackState == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } @@ -474,10 +495,9 @@ import java.io.IOException; MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, - playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfo = + playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } } @@ -511,8 +531,7 @@ import java.io.IOException; if (periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); @@ -575,7 +594,7 @@ import java.io.IOException; && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( @@ -587,7 +606,7 @@ import java.io.IOException; startRenderers(); } } - } else if (state == Player.STATE_READY) { + } else if (playbackInfo.playbackState == Player.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { @@ -597,15 +616,16 @@ import java.io.IOException; } } - if (state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) { + if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + || playbackInfo.playbackState == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) { + } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -626,12 +646,10 @@ import java.io.IOException; } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); Timeline timeline = playbackInfo.timeline; if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; - eventHandler - .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 0, 0, playbackInfo) - .sendToTarget(); return; } @@ -679,8 +697,9 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); } } finally { - eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) - .sendToTarget(); + if (seekPositionAdjusted) { + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } } } @@ -779,7 +798,9 @@ import java.io.IOException; private void stopInternal(boolean reset, boolean acknowledgeStop) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - notifySourceInfoRefresh(acknowledgeStop); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -817,25 +838,29 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = + timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; if (resetPosition) { - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline == null || timeline.isEmpty() - ? 0 - : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; pendingInitialSeekPosition = null; - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); - playbackInfo = playbackInfo.copyWithTimeline(null, null); } + playbackInfo = + new PlaybackInfo( + resetState ? null : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + resetPosition ? new MediaPeriodId(firstPeriodIndex) : playbackInfo.periodId, + // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. + resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs, + resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, + playbackInfo.playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -849,7 +874,8 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -909,11 +935,11 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { + if (playbackInfo.playbackState != Player.STATE_ENDED + && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } @@ -936,8 +962,7 @@ import java.io.IOException; } } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult) - .sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -954,7 +979,7 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - if (state != Player.STATE_ENDED) { + if (playbackInfo.playbackState != Player.STATE_ENDED) { maybeContinueLoading(); updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); @@ -1010,6 +1035,8 @@ import java.io.IOException; playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); if (oldTimeline == null) { + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; @@ -1024,7 +1051,6 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1038,10 +1064,7 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(); } - } else { - notifySourceInfoRefresh(); } return; } @@ -1050,7 +1073,6 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { - notifySourceInfoRefresh(); return; } Object playingPeriodUid = periodHolder == null @@ -1090,7 +1112,6 @@ import java.io.IOException; MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET); - notifySourceInfoRefresh(); return; } @@ -1107,14 +1128,12 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs); - notifySourceInfoRefresh(); return; } } if (periodHolder == null) { // We don't have any period holders, so we're done. - notifySourceInfoRefresh(); return; } @@ -1152,8 +1171,6 @@ import java.io.IOException; break; } } - - notifySourceInfoRefresh(); } private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { @@ -1172,18 +1189,6 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - notifySourceInfoRefresh(); - } - - private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(/* acknowledgeStop= */ false); - } - - private void notifySourceInfoRefresh(boolean acknowledgeStop) { - int prepareOrStopAcks = pendingPrepareCount + (acknowledgeStop ? 1 : 0); - pendingPrepareCount = 0; - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) - .sendToTarget(); } /** @@ -1287,7 +1292,7 @@ import java.io.IOException; if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); - } else if (loadingPeriodHolder != null && !isLoading) { + } else if (loadingPeriodHolder != null && !playbackInfo.isLoading) { maybeContinueLoading(); } @@ -1305,9 +1310,8 @@ import java.io.IOException; setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { @@ -1488,7 +1492,7 @@ import java.io.IOException; } playingPeriodHolder = periodHolder; - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1514,7 +1518,7 @@ import java.io.IOException; rendererIndex); Format[] formats = getFormats(newSelection); // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == Player.STATE_READY; + boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !wasRendererEnabled && playing; // Enable the renderer. @@ -1805,7 +1809,40 @@ import java.io.IOException; this.timeline = timeline; this.manifest = manifest; } + } + private static final class PlaybackInfoUpdate { + + private PlaybackInfo lastPlaybackInfo; + private int operationAcks; + private boolean positionDiscontinuity; + private @DiscontinuityReason int discontinuityReason; + + public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { + return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; + } + + public void reset(PlaybackInfo playbackInfo) { + lastPlaybackInfo = playbackInfo; + operationAcks = 0; + positionDiscontinuity = false; + } + + public void incrementPendingOperationAcks(int operationAcks) { + this.operationAcks += operationAcks; + } + + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { + if (positionDiscontinuity + && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { + // We always prefer non-internal discontinuity reasons. We also assume that we won't report + // more than one non-internal discontinuity per message iteration. + Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); + return; + } + positionDiscontinuity = true; + this.discontinuityReason = discontinuityReason; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index a2ffa43c4b..65392ba269 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -15,35 +15,59 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * Information about an ongoing playback. */ /* package */ final class PlaybackInfo { - public final Timeline timeline; - public final Object manifest; + public final @Nullable Timeline timeline; + public final @Nullable Object manifest; public final MediaPeriodId periodId; public final long startPositionUs; public final long contentPositionUs; + public final int playbackState; + public final boolean isLoading; + public final TrackSelectorResult trackSelectorResult; public volatile long positionUs; public volatile long bufferedPositionUs; - public PlaybackInfo(Timeline timeline, Object manifest, int periodIndex, long startPositionUs) { - this(timeline, manifest, new MediaPeriodId(periodIndex), startPositionUs, C.TIME_UNSET); + public PlaybackInfo( + @Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) { + this( + timeline, + /* manifest= */ null, + new MediaPeriodId(0), + startPositionUs, + /* contentPositionUs =*/ C.TIME_UNSET, + Player.STATE_IDLE, + /* isLoading= */ false, + trackSelectorResult); } - public PlaybackInfo(Timeline timeline, Object manifest, MediaPeriodId periodId, - long startPositionUs, long contentPositionUs) { + public PlaybackInfo( + @Nullable Timeline timeline, + @Nullable Object manifest, + MediaPeriodId periodId, + long startPositionUs, + long contentPositionUs, + int playbackState, + boolean isLoading, + TrackSelectorResult trackSelectorResult) { this.timeline = timeline; this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; - positionUs = startPositionUs; - bufferedPositionUs = startPositionUs; + this.positionUs = startPositionUs; + this.bufferedPositionUs = startPositionUs; + this.playbackState = playbackState; + this.isLoading = isLoading; + this.trackSelectorResult = trackSelectorResult; } public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs, @@ -53,19 +77,88 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { - return new PlaybackInfo(timeline, manifest, periodId, startPositionUs, contentPositionUs); + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, - periodId.copyWithPeriodIndex(periodIndex), startPositionUs, contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, periodId, startPositionUs, - contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithPlaybackState(int playbackState) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithIsLoading(boolean isLoading) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 801f5b9584..68adc32395 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -74,7 +74,7 @@ public final class TrackSelectorResult { * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { - if (other == null) { + if (other == null || other.selections.length != selections.length) { return false; } for (int i = 0; i < selections.length; i++) {