diff options
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.java | 848 |
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); + } + } +} |