summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java848
1 files changed, 848 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
new file mode 100644
index 0000000000..eb9eaae2cf
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -0,0 +1,848 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Pair;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.ArrayDeque;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}.
+ */
+/* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer {
+
+ private static final String TAG = "ExoPlayerImpl";
+
+ /**
+ * This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}
+ * when the player does not have any track selection made (such as when player is reset, or when
+ * player seeks to an unprepared period). It will not be used as result of any {@link
+ * TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}
+ * operation.
+ */
+ /* package */ final TrackSelectorResult emptyTrackSelectorResult;
+
+ private final Renderer[] renderers;
+ private final TrackSelector trackSelector;
+ private final Handler eventHandler;
+ private final ExoPlayerImplInternal internalPlayer;
+ private final Handler internalPlayerHandler;
+ private final CopyOnWriteArrayList<ListenerHolder> listeners;
+ private final Timeline.Period period;
+ private final ArrayDeque<Runnable> pendingListenerNotifications;
+
+ private MediaSource mediaSource;
+ private boolean playWhenReady;
+ @PlaybackSuppressionReason private int playbackSuppressionReason;
+ @RepeatMode private int repeatMode;
+ private boolean shuffleModeEnabled;
+ private int pendingOperationAcks;
+ private boolean hasPendingPrepare;
+ private boolean hasPendingSeek;
+ private boolean foregroundMode;
+ private int pendingSetPlaybackParametersAcks;
+ private PlaybackParameters playbackParameters;
+ private SeekParameters seekParameters;
+
+ // Playback information when there is no pending seek/set source operation.
+ private PlaybackInfo playbackInfo;
+
+ // Playback information when there is a pending seek/set source operation.
+ private int maskingWindowIndex;
+ private int maskingPeriodIndex;
+ private long maskingWindowPositionMs;
+
+ /**
+ * Constructs an instance. Must be called from a thread that has an associated {@link Looper}.
+ *
+ * @param renderers The {@link Renderer}s that will be used by the instance.
+ * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+ * @param loadControl The {@link LoadControl} that will be used by the instance.
+ * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
+ * @param clock The {@link Clock} that will be used by the instance.
+ * @param looper The {@link Looper} which must be used for all calls to the player and which is
+ * used to call listeners on.
+ */
+ @SuppressLint("HandlerLeak")
+ public ExoPlayerImpl(
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ BandwidthMeter bandwidthMeter,
+ Clock clock,
+ Looper looper) {
+ Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
+ Assertions.checkState(renderers.length > 0);
+ this.renderers = Assertions.checkNotNull(renderers);
+ this.trackSelector = Assertions.checkNotNull(trackSelector);
+ this.playWhenReady = false;
+ this.repeatMode = Player.REPEAT_MODE_OFF;
+ this.shuffleModeEnabled = false;
+ this.listeners = new CopyOnWriteArrayList<>();
+ emptyTrackSelectorResult =
+ new TrackSelectorResult(
+ new RendererConfiguration[renderers.length],
+ new TrackSelection[renderers.length],
+ null);
+ period = new Timeline.Period();
+ playbackParameters = PlaybackParameters.DEFAULT;
+ seekParameters = SeekParameters.DEFAULT;
+ playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
+ eventHandler =
+ new Handler(looper) {
+ @Override
+ public void handleMessage(Message msg) {
+ ExoPlayerImpl.this.handleEvent(msg);
+ }
+ };
+ playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
+ pendingListenerNotifications = new ArrayDeque<>();
+ internalPlayer =
+ new ExoPlayerImplInternal(
+ renderers,
+ trackSelector,
+ emptyTrackSelectorResult,
+ loadControl,
+ bandwidthMeter,
+ playWhenReady,
+ repeatMode,
+ shuffleModeEnabled,
+ eventHandler,
+ clock);
+ internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
+ }
+
+ @Override
+ @Nullable
+ public AudioComponent getAudioComponent() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public VideoComponent getVideoComponent() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public TextComponent getTextComponent() {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public MetadataComponent getMetadataComponent() {
+ return null;
+ }
+
+ @Override
+ public Looper getPlaybackLooper() {
+ return internalPlayer.getPlaybackLooper();
+ }
+
+ @Override
+ public Looper getApplicationLooper() {
+ return eventHandler.getLooper();
+ }
+
+ @Override
+ public void addListener(Player.EventListener listener) {
+ listeners.addIfAbsent(new ListenerHolder(listener));
+ }
+
+ @Override
+ public void removeListener(Player.EventListener listener) {
+ for (ListenerHolder listenerHolder : listeners) {
+ if (listenerHolder.listener.equals(listener)) {
+ listenerHolder.release();
+ listeners.remove(listenerHolder);
+ }
+ }
+ }
+
+ @Override
+ @State
+ public int getPlaybackState() {
+ return playbackInfo.playbackState;
+ }
+
+ @Override
+ @PlaybackSuppressionReason
+ public int getPlaybackSuppressionReason() {
+ return playbackSuppressionReason;
+ }
+
+ @Override
+ @Nullable
+ public ExoPlaybackException getPlaybackError() {
+ return playbackInfo.playbackError;
+ }
+
+ @Override
+ public void retry() {
+ if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) {
+ prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
+ }
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource) {
+ prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
+ }
+
+ @Override
+ public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+ this.mediaSource = mediaSource;
+ PlaybackInfo playbackInfo =
+ getResetPlaybackInfo(
+ resetPosition,
+ resetState,
+ /* resetError= */ true,
+ /* 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, resetState);
+ updatePlaybackInfo(
+ playbackInfo,
+ /* positionDiscontinuity= */ false,
+ /* ignored */ DISCONTINUITY_REASON_INTERNAL,
+ TIMELINE_CHANGE_REASON_RESET,
+ /* seekProcessed= */ false);
+ }
+
+
+ @Override
+ public void setPlayWhenReady(boolean playWhenReady) {
+ setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE);
+ }
+
+ public void setPlayWhenReady(
+ boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) {
+ boolean oldIsPlaying = isPlaying();
+ boolean oldInternalPlayWhenReady =
+ this.playWhenReady && this.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
+ boolean internalPlayWhenReady =
+ playWhenReady && playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE;
+ if (oldInternalPlayWhenReady != internalPlayWhenReady) {
+ internalPlayer.setPlayWhenReady(internalPlayWhenReady);
+ }
+ boolean playWhenReadyChanged = this.playWhenReady != playWhenReady;
+ boolean suppressionReasonChanged = this.playbackSuppressionReason != playbackSuppressionReason;
+ this.playWhenReady = playWhenReady;
+ this.playbackSuppressionReason = playbackSuppressionReason;
+ boolean isPlaying = isPlaying();
+ boolean isPlayingChanged = oldIsPlaying != isPlaying;
+ if (playWhenReadyChanged || suppressionReasonChanged || isPlayingChanged) {
+ int playbackState = playbackInfo.playbackState;
+ notifyListeners(
+ listener -> {
+ if (playWhenReadyChanged) {
+ listener.onPlayerStateChanged(playWhenReady, playbackState);
+ }
+ if (suppressionReasonChanged) {
+ listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
+ }
+ if (isPlayingChanged) {
+ listener.onIsPlayingChanged(isPlaying);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean getPlayWhenReady() {
+ return playWhenReady;
+ }
+
+ @Override
+ public void setRepeatMode(@RepeatMode int repeatMode) {
+ if (this.repeatMode != repeatMode) {
+ this.repeatMode = repeatMode;
+ internalPlayer.setRepeatMode(repeatMode);
+ notifyListeners(listener -> listener.onRepeatModeChanged(repeatMode));
+ }
+ }
+
+ @Override
+ public @RepeatMode int getRepeatMode() {
+ return repeatMode;
+ }
+
+ @Override
+ public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ if (this.shuffleModeEnabled != shuffleModeEnabled) {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
+ notifyListeners(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
+ }
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ return shuffleModeEnabled;
+ }
+
+ @Override
+ public boolean isLoading() {
+ return playbackInfo.isLoading;
+ }
+
+ @Override
+ public void seekTo(int windowIndex, long positionMs) {
+ Timeline timeline = playbackInfo.timeline;
+ 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");
+ eventHandler
+ .obtainMessage(
+ ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED,
+ /* operationAcks */ 1,
+ /* positionDiscontinuityReason */ C.INDEX_UNSET,
+ playbackInfo)
+ .sendToTarget();
+ return;
+ }
+ maskingWindowIndex = windowIndex;
+ if (timeline.isEmpty()) {
+ maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs;
+ maskingPeriodIndex = 0;
+ } else {
+ long windowPositionUs = positionMs == C.TIME_UNSET
+ ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);
+ Pair<Object, Long> periodUidAndPosition =
+ timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
+ maskingWindowPositionMs = C.usToMs(windowPositionUs);
+ maskingPeriodIndex = timeline.getIndexOfPeriod(periodUidAndPosition.first);
+ }
+ internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
+ notifyListeners(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
+ }
+
+ @Override
+ public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+ if (playbackParameters == null) {
+ playbackParameters = PlaybackParameters.DEFAULT;
+ }
+ if (this.playbackParameters.equals(playbackParameters)) {
+ return;
+ }
+ pendingSetPlaybackParametersAcks++;
+ this.playbackParameters = playbackParameters;
+ internalPlayer.setPlaybackParameters(playbackParameters);
+ PlaybackParameters playbackParametersToNotify = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify));
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return playbackParameters;
+ }
+
+ @Override
+ public void setSeekParameters(@Nullable SeekParameters seekParameters) {
+ if (seekParameters == null) {
+ seekParameters = SeekParameters.DEFAULT;
+ }
+ if (!this.seekParameters.equals(seekParameters)) {
+ this.seekParameters = seekParameters;
+ internalPlayer.setSeekParameters(seekParameters);
+ }
+ }
+
+ @Override
+ public SeekParameters getSeekParameters() {
+ return seekParameters;
+ }
+
+ @Override
+ public void setForegroundMode(boolean foregroundMode) {
+ if (this.foregroundMode != foregroundMode) {
+ this.foregroundMode = foregroundMode;
+ internalPlayer.setForegroundMode(foregroundMode);
+ }
+ }
+
+ @Override
+ public void stop(boolean reset) {
+ if (reset) {
+ mediaSource = null;
+ }
+ PlaybackInfo playbackInfo =
+ getResetPlaybackInfo(
+ /* resetPosition= */ reset,
+ /* resetState= */ reset,
+ /* resetError= */ 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
+ public void release() {
+ Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
+ + ExoPlayerLibraryInfo.registeredModules() + "]");
+ mediaSource = null;
+ internalPlayer.release();
+ eventHandler.removeCallbacksAndMessages(null);
+ playbackInfo =
+ getResetPlaybackInfo(
+ /* resetPosition= */ false,
+ /* resetState= */ false,
+ /* resetError= */ false,
+ /* playbackState= */ Player.STATE_IDLE);
+ }
+
+ @Override
+ public PlayerMessage createMessage(Target target) {
+ return new PlayerMessage(
+ internalPlayer,
+ target,
+ playbackInfo.timeline,
+ getCurrentWindowIndex(),
+ internalPlayerHandler);
+ }
+
+ @Override
+ public int getCurrentPeriodIndex() {
+ if (shouldMaskPosition()) {
+ return maskingPeriodIndex;
+ } else {
+ return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);
+ }
+ }
+
+ @Override
+ public int getCurrentWindowIndex() {
+ if (shouldMaskPosition()) {
+ return maskingWindowIndex;
+ } else {
+ return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
+ .windowIndex;
+ }
+ }
+
+ @Override
+ public long getDuration() {
+ if (isPlayingAd()) {
+ MediaPeriodId periodId = playbackInfo.periodId;
+ playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
+ long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup);
+ return C.usToMs(adDurationUs);
+ }
+ return getContentDuration();
+ }
+
+ @Override
+ public long getCurrentPosition() {
+ if (shouldMaskPosition()) {
+ return maskingWindowPositionMs;
+ } else if (playbackInfo.periodId.isAd()) {
+ return C.usToMs(playbackInfo.positionUs);
+ } else {
+ return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs);
+ }
+ }
+
+ @Override
+ public long getBufferedPosition() {
+ if (isPlayingAd()) {
+ return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)
+ ? C.usToMs(playbackInfo.bufferedPositionUs)
+ : getDuration();
+ }
+ return getContentBufferedPosition();
+ }
+
+ @Override
+ public long getTotalBufferedDuration() {
+ return C.usToMs(playbackInfo.totalBufferedDurationUs);
+ }
+
+ @Override
+ public boolean isPlayingAd() {
+ return !shouldMaskPosition() && playbackInfo.periodId.isAd();
+ }
+
+ @Override
+ public int getCurrentAdGroupIndex() {
+ return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getCurrentAdIndexInAdGroup() {
+ return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
+ }
+
+ @Override
+ public long getContentPosition() {
+ if (isPlayingAd()) {
+ playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
+ return playbackInfo.contentPositionUs == C.TIME_UNSET
+ ? playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDefaultPositionMs()
+ : period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs);
+ } else {
+ return getCurrentPosition();
+ }
+ }
+
+ @Override
+ public long getContentBufferedPosition() {
+ if (shouldMaskPosition()) {
+ return maskingWindowPositionMs;
+ }
+ if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber
+ != playbackInfo.periodId.windowSequenceNumber) {
+ return playbackInfo.timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+ }
+ long contentBufferedPositionUs = playbackInfo.bufferedPositionUs;
+ if (playbackInfo.loadingMediaPeriodId.isAd()) {
+ Timeline.Period loadingPeriod =
+ playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period);
+ contentBufferedPositionUs =
+ loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex);
+ if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) {
+ contentBufferedPositionUs = loadingPeriod.durationUs;
+ }
+ }
+ return periodPositionUsToWindowPositionMs(
+ playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs);
+ }
+
+ @Override
+ public int getRendererCount() {
+ return renderers.length;
+ }
+
+ @Override
+ public int getRendererType(int index) {
+ return renderers[index].getTrackType();
+ }
+
+ @Override
+ public TrackGroupArray getCurrentTrackGroups() {
+ return playbackInfo.trackGroups;
+ }
+
+ @Override
+ public TrackSelectionArray getCurrentTrackSelections() {
+ return playbackInfo.trackSelectorResult.selections;
+ }
+
+ @Override
+ public Timeline getCurrentTimeline() {
+ return playbackInfo.timeline;
+ }
+
+ // 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_PLAYBACK_INFO_CHANGED:
+ handlePlaybackInfo(
+ (PlaybackInfo) msg.obj,
+ /* operationAcks= */ msg.arg1,
+ /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
+ /* positionDiscontinuityReason= */ msg.arg2);
+ break;
+ case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:
+ handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private void handlePlaybackParameters(
+ PlaybackParameters playbackParameters, boolean operationAck) {
+ if (operationAck) {
+ pendingSetPlaybackParametersAcks--;
+ }
+ if (pendingSetPlaybackParametersAcks == 0) {
+ if (!this.playbackParameters.equals(playbackParameters)) {
+ this.playbackParameters = playbackParameters;
+ notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters));
+ }
+ }
+ }
+
+ private void handlePlaybackInfo(
+ PlaybackInfo playbackInfo,
+ int operationAcks,
+ boolean positionDiscontinuity,
+ @DiscontinuityReason int positionDiscontinuityReason) {
+ pendingOperationAcks -= operationAcks;
+ if (pendingOperationAcks == 0) {
+ if (playbackInfo.startPositionUs == C.TIME_UNSET) {
+ // Replace internal unset start position with externally visible start position of zero.
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ /* positionUs= */ 0,
+ playbackInfo.contentPositionUs,
+ playbackInfo.totalBufferedDurationUs);
+ }
+ if (!this.playbackInfo.timeline.isEmpty() && 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 PlaybackInfo getResetPlaybackInfo(
+ boolean resetPosition,
+ boolean resetState,
+ boolean resetError,
+ @Player.State int playbackState) {
+ if (resetPosition) {
+ maskingWindowIndex = 0;
+ maskingPeriodIndex = 0;
+ maskingWindowPositionMs = 0;
+ } else {
+ maskingWindowIndex = getCurrentWindowIndex();
+ maskingPeriodIndex = getCurrentPeriodIndex();
+ maskingWindowPositionMs = getCurrentPosition();
+ }
+ // Also reset period-based PlaybackInfo positions if resetting the state.
+ resetPosition = resetPosition || resetState;
+ MediaPeriodId mediaPeriodId =
+ resetPosition
+ ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
+ : playbackInfo.periodId;
+ long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
+ long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
+ return new PlaybackInfo(
+ resetState ? Timeline.EMPTY : playbackInfo.timeline,
+ mediaPeriodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ resetError ? null : playbackInfo.playbackError,
+ /* isLoading= */ false,
+ resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
+ mediaPeriodId,
+ startPositionUs,
+ /* totalBufferedDurationUs= */ 0,
+ startPositionUs);
+ }
+
+ private void updatePlaybackInfo(
+ PlaybackInfo playbackInfo,
+ boolean positionDiscontinuity,
+ @Player.DiscontinuityReason int positionDiscontinuityReason,
+ @Player.TimelineChangeReason int timelineChangeReason,
+ boolean seekProcessed) {
+ boolean previousIsPlaying = isPlaying();
+ // Assign playback info immediately such that all getters return the right values.
+ PlaybackInfo previousPlaybackInfo = this.playbackInfo;
+ this.playbackInfo = playbackInfo;
+ boolean isPlaying = isPlaying();
+ notifyListeners(
+ new PlaybackInfoUpdate(
+ playbackInfo,
+ previousPlaybackInfo,
+ listeners,
+ trackSelector,
+ positionDiscontinuity,
+ positionDiscontinuityReason,
+ timelineChangeReason,
+ seekProcessed,
+ playWhenReady,
+ /* isPlayingChanged= */ previousIsPlaying != isPlaying));
+ }
+
+ private void notifyListeners(ListenerInvocation listenerInvocation) {
+ CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
+ notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
+ }
+
+ private void notifyListeners(Runnable listenerNotificationRunnable) {
+ boolean isRunningRecursiveListenerNotification = !pendingListenerNotifications.isEmpty();
+ pendingListenerNotifications.addLast(listenerNotificationRunnable);
+ if (isRunningRecursiveListenerNotification) {
+ return;
+ }
+ while (!pendingListenerNotifications.isEmpty()) {
+ pendingListenerNotifications.peekFirst().run();
+ pendingListenerNotifications.removeFirst();
+ }
+ }
+
+ private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) {
+ long positionMs = C.usToMs(positionUs);
+ playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
+ positionMs += period.getPositionInWindowMs();
+ return positionMs;
+ }
+
+ private boolean shouldMaskPosition() {
+ return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
+ }
+
+ private static final class PlaybackInfoUpdate implements Runnable {
+
+ private final PlaybackInfo playbackInfo;
+ private final CopyOnWriteArrayList<ListenerHolder> listenerSnapshot;
+ private final TrackSelector trackSelector;
+ private final boolean positionDiscontinuity;
+ private final @Player.DiscontinuityReason int positionDiscontinuityReason;
+ private final @Player.TimelineChangeReason int timelineChangeReason;
+ private final boolean seekProcessed;
+ private final boolean playbackStateChanged;
+ private final boolean playbackErrorChanged;
+ private final boolean timelineChanged;
+ private final boolean isLoadingChanged;
+ private final boolean trackSelectorResultChanged;
+ private final boolean playWhenReady;
+ private final boolean isPlayingChanged;
+
+ public PlaybackInfoUpdate(
+ PlaybackInfo playbackInfo,
+ PlaybackInfo previousPlaybackInfo,
+ CopyOnWriteArrayList<ListenerHolder> listeners,
+ TrackSelector trackSelector,
+ boolean positionDiscontinuity,
+ @DiscontinuityReason int positionDiscontinuityReason,
+ @TimelineChangeReason int timelineChangeReason,
+ boolean seekProcessed,
+ boolean playWhenReady,
+ boolean isPlayingChanged) {
+ this.playbackInfo = playbackInfo;
+ this.listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
+ this.trackSelector = trackSelector;
+ this.positionDiscontinuity = positionDiscontinuity;
+ this.positionDiscontinuityReason = positionDiscontinuityReason;
+ this.timelineChangeReason = timelineChangeReason;
+ this.seekProcessed = seekProcessed;
+ this.playWhenReady = playWhenReady;
+ this.isPlayingChanged = isPlayingChanged;
+ playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
+ playbackErrorChanged =
+ previousPlaybackInfo.playbackError != playbackInfo.playbackError
+ && playbackInfo.playbackError != null;
+ timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
+ isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
+ trackSelectorResultChanged =
+ previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
+ }
+
+ @Override
+ public void run() {
+ if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
+ }
+ if (positionDiscontinuity) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
+ }
+ if (playbackErrorChanged) {
+ invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError));
+ }
+ if (trackSelectorResultChanged) {
+ trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
+ invokeAll(
+ listenerSnapshot,
+ listener ->
+ listener.onTracksChanged(
+ playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections));
+ }
+ if (isLoadingChanged) {
+ invokeAll(listenerSnapshot, listener -> listener.onLoadingChanged(playbackInfo.isLoading));
+ }
+ if (playbackStateChanged) {
+ invokeAll(
+ listenerSnapshot,
+ listener -> listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState));
+ }
+ if (isPlayingChanged) {
+ invokeAll(
+ listenerSnapshot,
+ listener ->
+ listener.onIsPlayingChanged(playbackInfo.playbackState == Player.STATE_READY));
+ }
+ if (seekProcessed) {
+ invokeAll(listenerSnapshot, EventListener::onSeekProcessed);
+ }
+ }
+ }
+
+ private static void invokeAll(
+ CopyOnWriteArrayList<ListenerHolder> listeners, ListenerInvocation listenerInvocation) {
+ for (ListenerHolder listenerHolder : listeners) {
+ listenerHolder.invoke(listenerInvocation);
+ }
+ }
+}