diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6ab25118f9..3be8af6763 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,11 @@ * Fix issue that could cause playback to freeze when selecting tracks, if extension audio renderers are being used ([#8203](https://github.com/google/ExoPlayer/issues/8203)). + * Add `onEvents` callback to `Player.EventListener` and + `AnalyticsListener` to notify when all simultaneous state changes have + been handled and the values reported through callbacks are again + completely consistent with the values obtained from the `Player` + getters. * Track selection: * Add option to specify multiple preferred audio or text languages. * Forward `Timeline` and `MediaPeriodId` to `TrackSelection.Factory`. diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 12cc108019..7bc9fadf42 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -96,7 +96,7 @@ public final class CastPlayer extends BasePlayer { private final SeekResultCallback seekResultCallback; // Listeners and notification. - private final ListenerSet listeners; + private final ListenerSet listeners; @Nullable private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. @@ -135,7 +135,11 @@ public final class CastPlayer extends BasePlayer { period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); - listeners = new ListenerSet<>(); + listeners = + new ListenerSet<>( + Looper.getMainLooper(), + Player.Events::new, + (listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags)); playWhenReady = new StateHolder<>(false); repeatMode = new StateHolder<>(REPEAT_MODE_OFF); @@ -445,9 +449,11 @@ public final class CastPlayer extends BasePlayer { pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; - listeners.queueEvent(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); + listeners.queueEvent( + Player.EVENT_POSITION_DISCONTINUITY, + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK)); } else if (pendingSeekCount == 0) { - listeners.queueEvent(EventListener::onSeekProcessed); + listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed); } listeners.flushEvents(); } @@ -647,7 +653,8 @@ public final class CastPlayer extends BasePlayer { updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null); boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value; if (wasPlaying != isPlaying) { - listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying)); + listeners.queueEvent( + Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying)); } updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); updateTimelineAndNotifyIfChanged(); @@ -664,10 +671,12 @@ public final class CastPlayer extends BasePlayer { if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; listeners.queueEvent( + Player.EVENT_POSITION_DISCONTINUITY, listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION)); } if (updateTracksAndSelectionsAndNotifyIfChanged()) { listeners.queueEvent( + Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection)); } listeners.flushEvents(); @@ -710,6 +719,7 @@ public final class CastPlayer extends BasePlayer { // TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and // TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553]. listeners.queueEvent( + Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged( currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); @@ -831,7 +841,8 @@ public final class CastPlayer extends BasePlayer { private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) { if (this.repeatMode.value != repeatMode) { this.repeatMode.value = repeatMode; - listeners.queueEvent(listener -> listener.onRepeatModeChanged(repeatMode)); + listeners.queueEvent( + Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode)); } } @@ -846,15 +857,18 @@ public final class CastPlayer extends BasePlayer { this.playbackState = playbackState; this.playWhenReady.value = playWhenReady; listeners.queueEvent( - listener -> { - listener.onPlayerStateChanged(playWhenReady, playbackState); - if (playbackStateChanged) { - listener.onPlaybackStateChanged(playbackState); - } - if (playWhenReadyChanged) { - listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason); - } - }); + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onPlayerStateChanged(playWhenReady, playbackState)); + if (playbackStateChanged) { + listeners.queueEvent( + Player.EVENT_PLAYBACK_STATE_CHANGED, + listener -> listener.onPlaybackStateChanged(playbackState)); + } + if (playWhenReadyChanged) { + listeners.queueEvent( + Player.EVENT_PLAY_WHEN_READY_CHANGED, + listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason)); + } } } @@ -1072,7 +1086,7 @@ public final class CastPlayer extends BasePlayer { if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - listeners.sendEvent(EventListener::onSeekProcessed); + listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 8513db808e..63b31b23ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -454,7 +454,8 @@ public interface ExoPlayer extends Player { releaseTimeoutMs, pauseAtEndOfMediaItems, clock, - looper); + looper, + /* wrappingPlayer= */ null); player.experimentalSetForegroundModeTimeoutMs(setForegroundModeTimeoutMs); if (!throwWhenStuckBuffering) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 9a208a94ae..40c3473abd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -260,6 +260,7 @@ public final class ExoPlayerFactory { ExoPlayer.DEFAULT_RELEASE_TIMEOUT_MS, /* pauseAtEndOfMediaItems= */ false, Clock.DEFAULT, - applicationLooper); + applicationLooper, + /* wrappingPlayer= */ null); } } 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 b87f587200..ace5b411ee 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 @@ -73,7 +73,7 @@ import java.util.concurrent.TimeoutException; private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener; private final ExoPlayerImplInternal internalPlayer; private final Handler internalPlayerHandler; - private final ListenerSet listeners; + private final ListenerSet listeners; private final Timeline.Period period; private final List mediaSourceHolderSnapshots; private final boolean useLazyPreparation; @@ -121,6 +121,8 @@ import java.util.concurrent.TimeoutException; * @param clock The {@link Clock}. * @param applicationLooper The {@link Looper} that must be used for all calls to the player and * which is used to call listeners on. + * @param wrappingPlayer The {@link Player} wrapping this one if applicable. This player instance + * should be used for all externally visible callbacks. */ @SuppressLint("HandlerLeak") public ExoPlayerImpl( @@ -136,7 +138,8 @@ import java.util.concurrent.TimeoutException; long releaseTimeoutMs, boolean pauseAtEndOfMediaItems, Clock clock, - Looper applicationLooper) { + Looper applicationLooper, + @Nullable Player wrappingPlayer) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); checkState(renderers.length > 0); @@ -150,7 +153,12 @@ import java.util.concurrent.TimeoutException; this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; this.applicationLooper = applicationLooper; repeatMode = Player.REPEAT_MODE_OFF; - listeners = new ListenerSet<>(); + Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this; + listeners = + new ListenerSet<>( + applicationLooper, + Player.Events::new, + (listener, eventFlags) -> listener.onEvents(playerForListeners, eventFlags)); mediaSourceHolderSnapshots = new ArrayList<>(); shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); emptyTrackSelectorResult = @@ -166,7 +174,7 @@ import java.util.concurrent.TimeoutException; playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate)); playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); if (analyticsCollector != null) { - analyticsCollector.setPlayer(this); + analyticsCollector.setPlayer(playerForListeners, applicationLooper); addListener(analyticsCollector); bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector); } @@ -565,7 +573,8 @@ import java.util.concurrent.TimeoutException; if (this.repeatMode != repeatMode) { this.repeatMode = repeatMode; internalPlayer.setRepeatMode(repeatMode); - listeners.sendEvent(listener -> listener.onRepeatModeChanged(repeatMode)); + listeners.sendEvent( + Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode)); } } @@ -579,7 +588,9 @@ import java.util.concurrent.TimeoutException; if (this.shuffleModeEnabled != shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; internalPlayer.setShuffleModeEnabled(shuffleModeEnabled); - listeners.sendEvent(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)); + listeners.sendEvent( + Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled)); } } @@ -729,6 +740,7 @@ import java.util.concurrent.TimeoutException; + ExoPlayerLibraryInfo.registeredModules() + "]"); if (!internalPlayer.release()) { listeners.sendEvent( + Player.EVENT_PLAYER_ERROR, listener -> listener.onPlayerError( ExoPlaybackException.createForTimeout( @@ -974,10 +986,12 @@ import java.util.concurrent.TimeoutException; int mediaItemTransitionReason = mediaItemTransitionInfo.second; if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) { listeners.queueEvent( + Player.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason)); } if (positionDiscontinuity) { listeners.queueEvent( + Player.EVENT_POSITION_DISCONTINUITY, listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason)); } if (mediaItemTransitioned) { @@ -991,39 +1005,49 @@ import java.util.concurrent.TimeoutException; mediaItem = null; } listeners.queueEvent( + Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason)); } if (previousPlaybackInfo.playbackError != newPlaybackInfo.playbackError && newPlaybackInfo.playbackError != null) { - listeners.queueEvent(listener -> listener.onPlayerError(newPlaybackInfo.playbackError)); + listeners.queueEvent( + Player.EVENT_PLAYER_ERROR, + listener -> listener.onPlayerError(newPlaybackInfo.playbackError)); } if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) { trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info); listeners.queueEvent( + Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged( newPlaybackInfo.trackGroups, newPlaybackInfo.trackSelectorResult.selections)); } if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) { listeners.queueEvent( + Player.EVENT_STATIC_METADATA_CHANGED, listener -> listener.onStaticMetadataChanged(newPlaybackInfo.staticMetadata)); } if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) { - listeners.queueEvent(listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading)); + listeners.queueEvent( + Player.EVENT_IS_LOADING_CHANGED, + listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading)); } if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState || previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) { listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onPlayerStateChanged( newPlaybackInfo.playWhenReady, newPlaybackInfo.playbackState)); } if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState) { listeners.queueEvent( + Player.EVENT_PLAYBACK_STATE_CHANGED, listener -> listener.onPlaybackStateChanged(newPlaybackInfo.playbackState)); } if (previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) { listeners.queueEvent( + Player.EVENT_PLAY_WHEN_READY_CHANGED, listener -> listener.onPlayWhenReadyChanged( newPlaybackInfo.playWhenReady, playWhenReadyChangeReason)); @@ -1031,28 +1055,34 @@ import java.util.concurrent.TimeoutException; if (previousPlaybackInfo.playbackSuppressionReason != newPlaybackInfo.playbackSuppressionReason) { listeners.queueEvent( + Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, listener -> listener.onPlaybackSuppressionReasonChanged( newPlaybackInfo.playbackSuppressionReason)); } if (isPlaying(previousPlaybackInfo) != isPlaying(newPlaybackInfo)) { - listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo))); + listeners.queueEvent( + Player.EVENT_IS_PLAYING_CHANGED, + listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo))); } if (!previousPlaybackInfo.playbackParameters.equals(newPlaybackInfo.playbackParameters)) { listeners.queueEvent( + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED, listener -> listener.onPlaybackParametersChanged(newPlaybackInfo.playbackParameters)); } if (seekProcessed) { - listeners.queueEvent(EventListener::onSeekProcessed); + listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed); } if (previousPlaybackInfo.offloadSchedulingEnabled != newPlaybackInfo.offloadSchedulingEnabled) { listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onExperimentalOffloadSchedulingEnabledChanged( newPlaybackInfo.offloadSchedulingEnabled)); } if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) { listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 6ee35b25e1..1117fe65bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.MutableFlags; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; @@ -421,8 +422,13 @@ public interface Player { } /** - * Listener of changes in player state. All methods have no-op default implementations to allow - * selective overrides. + * Listener of changes in player state. + * + *

All methods have no-op default implementations to allow selective overrides. + * + *

Listeners can choose to implement individual events (e.g. {@link + * #onIsPlayingChanged(boolean)}) or {@link #onEvents(Player, Events)}, which is called after one + * or more events occurred together. */ interface EventListener { @@ -621,12 +627,41 @@ public interface Player { *

This method is experimental, and will be renamed or removed in a future release. */ default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {} + /** * Called when the player has started or finished sleeping for offload. * *

This method is experimental, and will be renamed or removed in a future release. */ default void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) {} + + /** + * Called when one or more player states changed. + * + *

State changes and events that happen within one {@link Looper} message queue iteration are + * reported together and only after all individual callbacks were triggered. + * + *

Listeners should prefer this method over individual callbacks in the following cases: + * + *

    + *
  • They intend to use multiple state values together (e.g. using {@link + * #getCurrentWindowIndex()} to query in {@link #getCurrentTimeline()}). + *
  • The same logic should be triggered for multiple events (e.g. when updating a UI for + * both {@link #onPlaybackStateChanged(int)} and {@link #onPlayWhenReadyChanged(boolean, + * int)}). + *
  • They need access to the {@link Player} object to trigger further events (e.g. to call + * {@link Player#seekTo(long)} after a {@link #onMediaItemTransition(MediaItem, int)}). + *
  • They are interested in events that logically happened together (e.g {@link + * #onPlaybackStateChanged(int)} to {@link #STATE_BUFFERING} because of {@link + * #onMediaItemTransition(MediaItem, int)}). + *
+ * + * @param player The {@link Player} whose state changed. Use the getters to obtain the latest + * states. + * @param events The {@link Events} that happened in this iteration, indicating which player + * states changed. + */ + default void onEvents(Player player, Events events) {} } /** @@ -663,6 +698,37 @@ public interface Player { } } + /** A set of {@link EventFlags}. */ + final class Events extends MutableFlags { + /** + * Returns whether the given event occurred. + * + * @param event The {@link EventFlags event}. + * @return Whether the event occurred. + */ + @Override + public boolean contains(@EventFlags int event) { + // Overridden to add IntDef compiler enforcement and new JavaDoc. + return super.contains(event); + } + + /** + * Returns the {@link EventFlags event} at the given index. + * + *

Although index-based access is possible, it doesn't imply a particular order of these + * events. + * + * @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive). + * @return The {@link EventFlags event} at the given index. + */ + @Override + @EventFlags + public int get(int index) { + // Overridden to add IntDef compiler enforcement and new JavaDoc. + return super.get(index); + } + } + /** * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or * {@link #STATE_ENDED}. @@ -802,7 +868,11 @@ public interface Player { /** Timeline changed as a result of a dynamic update introduced by the played media. */ int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; - /** Reasons for media item transitions. */ + /** + * Reasons for media item transitions. One of {@link #MEDIA_ITEM_TRANSITION_REASON_REPEAT}, {@link + * #MEDIA_ITEM_TRANSITION_REASON_AUTO}, {@link #MEDIA_ITEM_TRANSITION_REASON_SEEK} or {@link + * #MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED}. + */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -825,6 +895,59 @@ public interface Player { */ int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3; + /** + * Events that can be reported via {@link EventListener#onEvents(Player, Events)}. + * + *

One of the {@link Player}{@code .EVENT_*} flags. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + EVENT_TIMELINE_CHANGED, + EVENT_MEDIA_ITEM_TRANSITION, + EVENT_TRACKS_CHANGED, + EVENT_STATIC_METADATA_CHANGED, + EVENT_IS_LOADING_CHANGED, + EVENT_PLAYBACK_STATE_CHANGED, + EVENT_PLAY_WHEN_READY_CHANGED, + EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + EVENT_IS_PLAYING_CHANGED, + EVENT_REPEAT_MODE_CHANGED, + EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + EVENT_PLAYER_ERROR, + EVENT_POSITION_DISCONTINUITY, + EVENT_PLAYBACK_PARAMETERS_CHANGED + }) + @interface EventFlags {} + /** {@link #getCurrentTimeline()} changed. */ + int EVENT_TIMELINE_CHANGED = 0; + /** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */ + int EVENT_MEDIA_ITEM_TRANSITION = 1; + /** {@link #getCurrentTrackGroups()} or {@link #getCurrentTrackSelections()} changed. */ + int EVENT_TRACKS_CHANGED = 2; + /** {@link #getCurrentStaticMetadata()} changed. */ + int EVENT_STATIC_METADATA_CHANGED = 3; + /** {@link #isLoading()} ()} changed. */ + int EVENT_IS_LOADING_CHANGED = 4; + /** {@link #getPlaybackState()} changed. */ + int EVENT_PLAYBACK_STATE_CHANGED = 5; + /** {@link #getPlayWhenReady()} changed. */ + int EVENT_PLAY_WHEN_READY_CHANGED = 6; + /** {@link #getPlaybackSuppressionReason()} changed. */ + int EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED = 7; + /** {@link #isPlaying()} changed. */ + int EVENT_IS_PLAYING_CHANGED = 8; + /** {@link #getRepeatMode()} changed. */ + int EVENT_REPEAT_MODE_CHANGED = 9; + /** {@link #getShuffleModeEnabled()} changed. */ + int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = 10; + /** {@link #getPlayerError()} changed. */ + int EVENT_PLAYER_ERROR = 11; + /** A position discontinuity occurred. See {@link EventListener#onPositionDiscontinuity(int)}. */ + int EVENT_POSITION_DISCONTINUITY = 12; + /** {@link #getPlaybackParameters()} changed. */ + int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13; + /** Returns the component of this player for audio output, or null if audio is not supported. */ @Nullable AudioComponent getAudioComponent(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e82e0dcba8..f2872a0876 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -701,7 +701,8 @@ public class SimpleExoPlayer extends BasePlayer builder.releaseTimeoutMs, builder.pauseAtEndOfMediaItems, builder.clock, - builder.looper); + builder.looper, + /* wrappingPlayer= */ this); player.addListener(componentListener); videoDebugListeners.add(analyticsCollector); videoListeners.add(analyticsCollector); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index a11953421b..e50eae7ca5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.analytics; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -48,6 +49,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.ListenerSet; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.common.base.Objects; @@ -74,12 +76,12 @@ public class AnalyticsCollector VideoListener, AudioListener { - private final ListenerSet listeners; private final Clock clock; private final Period period; private final Window window; private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + private ListenerSet listeners; private @MonotonicNonNull Player player; private boolean isSeeking; @@ -90,7 +92,11 @@ public class AnalyticsCollector */ public AnalyticsCollector(Clock clock) { this.clock = checkNotNull(clock); - listeners = new ListenerSet<>(); + listeners = + new ListenerSet<>( + Util.getCurrentOrMainLooper(), + AnalyticsListener.Events::new, + (listener, eventFlags) -> {}); period = new Period(); window = new Window(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(period); @@ -120,11 +126,14 @@ public class AnalyticsCollector * yet or the current player is idle. * * @param player The {@link Player} for which data will be collected. + * @param looper The {@link Looper} used for listener callbacks. */ - public void setPlayer(Player player) { + public void setPlayer(Player player, Looper looper) { Assertions.checkState( this.player == null || mediaPeriodQueueTracker.mediaPeriodQueue.isEmpty()); this.player = checkNotNull(player); + listeners = + listeners.copy(looper, (listener, eventFlags) -> listener.onEvents(player, eventFlags)); } /** @@ -151,7 +160,8 @@ public class AnalyticsCollector if (!isSeeking) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); isSeeking = true; - listeners.sendEvent(listener -> listener.onSeekStarted(eventTime)); + listeners.sendEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekStarted(eventTime)); } } @@ -165,7 +175,8 @@ public class AnalyticsCollector @Override public final void onMetadata(Metadata metadata) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onMetadata(eventTime, metadata)); + listeners.sendEvent( + AnalyticsListener.EVENT_METADATA, listener -> listener.onMetadata(eventTime, metadata)); } // AudioRendererEventListener implementation. @@ -175,6 +186,7 @@ public class AnalyticsCollector public final void onAudioEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_ENABLED, listener -> { listener.onAudioEnabled(eventTime, counters); listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); @@ -187,6 +199,7 @@ public class AnalyticsCollector String decoderName, long initializedTimestampMs, long initializationDurationMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED, listener -> { listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs); listener.onDecoderInitialized( @@ -200,6 +213,7 @@ public class AnalyticsCollector Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED, listener -> { listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); @@ -210,6 +224,7 @@ public class AnalyticsCollector public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_POSITION_ADVANCING, listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs)); } @@ -218,6 +233,7 @@ public class AnalyticsCollector int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_UNDERRUN, listener -> listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } @@ -225,7 +241,9 @@ public class AnalyticsCollector @Override public final void onAudioDecoderReleased(String decoderName) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); + listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_DECODER_RELEASED, + listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); } @SuppressWarnings("deprecation") @@ -233,6 +251,7 @@ public class AnalyticsCollector public final void onAudioDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_DISABLED, listener -> { listener.onAudioDisabled(eventTime, counters); listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); @@ -244,32 +263,41 @@ public class AnalyticsCollector @Override public final void onAudioSessionId(int audioSessionId) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onAudioSessionId(eventTime, audioSessionId)); + listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_SESSION_ID, + listener -> listener.onAudioSessionId(eventTime, audioSessionId)); } @Override public void onAudioAttributesChanged(AudioAttributes audioAttributes) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes)); + listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_ATTRIBUTES_CHANGED, + listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes)); } @Override public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED, listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled)); } @Override public void onAudioSinkError(Exception audioSinkError) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onAudioSinkError(eventTime, audioSinkError)); + listeners.sendEvent( + AnalyticsListener.EVENT_AUDIO_SINK_ERROR, + listener -> listener.onAudioSinkError(eventTime, audioSinkError)); } @Override public void onVolumeChanged(float audioVolume) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onVolumeChanged(eventTime, audioVolume)); + listeners.sendEvent( + AnalyticsListener.EVENT_VOLUME_CHANGED, + listener -> listener.onVolumeChanged(eventTime, audioVolume)); } // VideoRendererEventListener implementation. @@ -279,6 +307,7 @@ public class AnalyticsCollector public final void onVideoEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_ENABLED, listener -> { listener.onVideoEnabled(eventTime, counters); listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); @@ -291,6 +320,7 @@ public class AnalyticsCollector String decoderName, long initializedTimestampMs, long initializationDurationMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED, listener -> { listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs); listener.onDecoderInitialized( @@ -304,6 +334,7 @@ public class AnalyticsCollector Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED, listener -> { listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); @@ -313,13 +344,17 @@ public class AnalyticsCollector @Override public final void onDroppedFrames(int count, long elapsedMs) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs)); + listeners.sendEvent( + AnalyticsListener.EVENT_DROPPED_VIDEO_FRAMES, + listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs)); } @Override public final void onVideoDecoderReleased(String decoderName) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); + listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_DECODER_RELEASED, + listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); } @SuppressWarnings("deprecation") @@ -327,6 +362,7 @@ public class AnalyticsCollector public final void onVideoDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_DISABLED, listener -> { listener.onVideoDisabled(eventTime, counters); listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); @@ -336,13 +372,16 @@ public class AnalyticsCollector @Override public final void onRenderedFirstFrame(@Nullable Surface surface) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onRenderedFirstFrame(eventTime, surface)); + listeners.sendEvent( + AnalyticsListener.EVENT_RENDERED_FIRST_FRAME, + listener -> listener.onRenderedFirstFrame(eventTime, surface)); } @Override public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_FRAME_PROCESSING_OFFSET, listener -> listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount)); } @@ -359,6 +398,7 @@ public class AnalyticsCollector int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED, listener -> listener.onVideoSizeChanged( eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); @@ -367,7 +407,9 @@ public class AnalyticsCollector @Override public void onSurfaceSizeChanged(int width, int height) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onSurfaceSizeChanged(eventTime, width, height)); + listeners.sendEvent( + AnalyticsListener.EVENT_SURFACE_SIZE_CHANGED, + listener -> listener.onSurfaceSizeChanged(eventTime, width, height)); } // MediaSourceEventListener implementation. @@ -380,6 +422,7 @@ public class AnalyticsCollector MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); listeners.sendEvent( + AnalyticsListener.EVENT_LOAD_STARTED, listener -> listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData)); } @@ -391,6 +434,7 @@ public class AnalyticsCollector MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); listeners.sendEvent( + AnalyticsListener.EVENT_LOAD_COMPLETED, listener -> listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData)); } @@ -402,6 +446,7 @@ public class AnalyticsCollector MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); listeners.sendEvent( + AnalyticsListener.EVENT_LOAD_CANCELED, listener -> listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData)); } @@ -415,6 +460,7 @@ public class AnalyticsCollector boolean wasCanceled) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); listeners.sendEvent( + AnalyticsListener.EVENT_LOAD_ERROR, listener -> listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled)); } @@ -423,34 +469,42 @@ public class AnalyticsCollector public final void onUpstreamDiscarded( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onUpstreamDiscarded(eventTime, mediaLoadData)); + listeners.sendEvent( + AnalyticsListener.EVENT_UPSTREAM_DISCARDED, + listener -> listener.onUpstreamDiscarded(eventTime, mediaLoadData)); } @Override public final void onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDownstreamFormatChanged(eventTime, mediaLoadData)); + listeners.sendEvent( + AnalyticsListener.EVENT_DOWNSTREAM_FORMAT_CHANGED, + listener -> listener.onDownstreamFormatChanged(eventTime, mediaLoadData)); } // Player.EventListener implementation. - // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous - // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of - // having slightly different real times. + // TODO: Use Player.EventListener.onEvents to know when a set of simultaneous callbacks finished. + // This helps to assign exactly the same EventTime to all of them instead of having slightly + // different real times. @Override public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onTimelineChanged(eventTime, reason)); + listeners.sendEvent( + AnalyticsListener.EVENT_TIMELINE_CHANGED, + listener -> listener.onTimelineChanged(eventTime, reason)); } @Override public final void onMediaItemTransition( @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onMediaItemTransition(eventTime, mediaItem, reason)); + listeners.sendEvent( + AnalyticsListener.EVENT_MEDIA_ITEM_TRANSITION, + listener -> listener.onMediaItemTransition(eventTime, mediaItem, reason)); } @Override @@ -458,19 +512,24 @@ public class AnalyticsCollector TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(eventTime, trackGroups, trackSelections)); } @Override public final void onStaticMetadataChanged(List metadataList) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onStaticMetadataChanged(eventTime, metadataList)); + listeners.sendEvent( + AnalyticsListener.EVENT_STATIC_METADATA_CHANGED, + listener -> listener.onStaticMetadataChanged(eventTime, metadataList)); } @Override public final void onIsLoadingChanged(boolean isLoading) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onIsLoadingChanged(eventTime, isLoading)); + listeners.sendEvent( + AnalyticsListener.EVENT_IS_LOADING_CHANGED, + listener -> listener.onIsLoadingChanged(eventTime, isLoading)); } @SuppressWarnings("deprecation") @@ -478,13 +537,16 @@ public class AnalyticsCollector public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); listeners.sendEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState)); } @Override public final void onPlaybackStateChanged(@Player.State int state) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onPlaybackStateChanged(eventTime, state)); + listeners.sendEvent( + AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED, + listener -> listener.onPlaybackStateChanged(eventTime, state)); } @Override @@ -492,6 +554,7 @@ public class AnalyticsCollector boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED, listener -> listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason)); } @@ -500,6 +563,7 @@ public class AnalyticsCollector @PlaybackSuppressionReason int playbackSuppressionReason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, listener -> listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason)); } @@ -507,19 +571,25 @@ public class AnalyticsCollector @Override public void onIsPlayingChanged(boolean isPlaying) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onIsPlayingChanged(eventTime, isPlaying)); + listeners.sendEvent( + AnalyticsListener.EVENT_IS_PLAYING_CHANGED, + listener -> listener.onIsPlayingChanged(eventTime, isPlaying)); } @Override public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onRepeatModeChanged(eventTime, repeatMode)); + listeners.sendEvent( + AnalyticsListener.EVENT_REPEAT_MODE_CHANGED, + listener -> listener.onRepeatModeChanged(eventTime, repeatMode)); } @Override public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onShuffleModeChanged(eventTime, shuffleModeEnabled)); + listeners.sendEvent( + AnalyticsListener.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + listener -> listener.onShuffleModeChanged(eventTime, shuffleModeEnabled)); } @Override @@ -528,7 +598,8 @@ public class AnalyticsCollector error.mediaPeriodId != null ? generateEventTime(error.mediaPeriodId) : generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onPlayerError(eventTime, error)); + listeners.sendEvent( + AnalyticsListener.EVENT_PLAYER_ERROR, listener -> listener.onPlayerError(eventTime, error)); } @Override @@ -538,13 +609,16 @@ public class AnalyticsCollector } mediaPeriodQueueTracker.onPositionDiscontinuity(checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onPositionDiscontinuity(eventTime, reason)); + listeners.sendEvent( + AnalyticsListener.EVENT_POSITION_DISCONTINUITY, + listener -> listener.onPositionDiscontinuity(eventTime, reason)); } @Override public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_PLAYBACK_PARAMETERS_CHANGED, listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters)); } @@ -552,7 +626,8 @@ public class AnalyticsCollector @Override public final void onSeekProcessed() { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent(listener -> listener.onSeekProcessed(eventTime)); + listeners.sendEvent( + /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); } // BandwidthMeter.Listener implementation. @@ -561,6 +636,7 @@ public class AnalyticsCollector public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { EventTime eventTime = generateLoadingMediaPeriodEventTime(); listeners.sendEvent( + AnalyticsListener.EVENT_BANDWIDTH_ESTIMATE, listener -> listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate)); } @@ -569,43 +645,52 @@ public class AnalyticsCollector @Override public final void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmSessionAcquired(eventTime)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED, + listener -> listener.onDrmSessionAcquired(eventTime)); } @Override public final void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmKeysLoaded(eventTime)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_KEYS_LOADED, listener -> listener.onDrmKeysLoaded(eventTime)); } @Override public final void onDrmSessionManagerError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, Exception error) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmSessionManagerError(eventTime, error)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_SESSION_MANAGER_ERROR, + listener -> listener.onDrmSessionManagerError(eventTime, error)); } @Override public final void onDrmKeysRestored(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmKeysRestored(eventTime)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_KEYS_RESTORED, + listener -> listener.onDrmKeysRestored(eventTime)); } @Override public final void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmKeysRemoved(eventTime)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_KEYS_REMOVED, listener -> listener.onDrmKeysRemoved(eventTime)); } @Override public final void onDrmSessionReleased(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent(listener -> listener.onDrmSessionReleased(eventTime)); + listeners.sendEvent( + AnalyticsListener.EVENT_DRM_SESSION_RELEASED, + listener -> listener.onDrmSessionReleased(eventTime)); } // Internal methods. - /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */ @RequiresNonNull("player") protected EventTime generateEventTime( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 87c6308990..3e49d422b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.analytics; +import android.os.Looper; import android.view.Surface; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -37,8 +39,12 @@ import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.MutableFlags; import com.google.common.base.Objects; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -48,9 +54,215 @@ import java.util.List; * time at the time of the event. * *

All methods have no-op default implementations to allow selective overrides. + * + *

Listeners can choose to implement individual events (e.g. {@link + * #onIsPlayingChanged(EventTime, boolean)}) or {@link #onEvents(Player, Events)}, which is called + * after one or more events occurred together. */ public interface AnalyticsListener { + /** A set of {@link EventFlags}. */ + final class Events extends MutableFlags { + /** + * Returns whether the given event occurred. + * + * @param event The {@link EventFlags event}. + * @return Whether the event occurred. + */ + @Override + public boolean contains(@EventFlags int event) { + // Overridden to add IntDef compiler enforcement and new JavaDoc. + return super.contains(event); + } + + /** + * Returns the {@link EventFlags event} at the given index. + * + *

Although index-based access is possible, it doesn't imply a particular order of these + * events. + * + * @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive). + * @return The {@link EventFlags event} at the given index. + */ + @Override + @EventFlags + public int get(int index) { + // Overridden to add IntDef compiler enforcement and new JavaDoc. + return super.get(index); + } + } + + /** + * Events that can be reported via {@link #onEvents(Player, Events)}. + * + *

One of the {@link AnalyticsListener}{@code .EVENT_*} flags. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + EVENT_TIMELINE_CHANGED, + EVENT_MEDIA_ITEM_TRANSITION, + EVENT_TRACKS_CHANGED, + EVENT_STATIC_METADATA_CHANGED, + EVENT_IS_LOADING_CHANGED, + EVENT_PLAYBACK_STATE_CHANGED, + EVENT_PLAY_WHEN_READY_CHANGED, + EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, + EVENT_IS_PLAYING_CHANGED, + EVENT_REPEAT_MODE_CHANGED, + EVENT_SHUFFLE_MODE_ENABLED_CHANGED, + EVENT_PLAYER_ERROR, + EVENT_POSITION_DISCONTINUITY, + EVENT_PLAYBACK_PARAMETERS_CHANGED, + EVENT_LOAD_STARTED, + EVENT_LOAD_COMPLETED, + EVENT_LOAD_CANCELED, + EVENT_LOAD_ERROR, + EVENT_DOWNSTREAM_FORMAT_CHANGED, + EVENT_UPSTREAM_DISCARDED, + EVENT_BANDWIDTH_ESTIMATE, + EVENT_METADATA, + EVENT_AUDIO_ENABLED, + EVENT_AUDIO_DECODER_INITIALIZED, + EVENT_AUDIO_INPUT_FORMAT_CHANGED, + EVENT_AUDIO_POSITION_ADVANCING, + EVENT_AUDIO_UNDERRUN, + EVENT_AUDIO_DECODER_RELEASED, + EVENT_AUDIO_DISABLED, + EVENT_AUDIO_SESSION_ID, + EVENT_AUDIO_ATTRIBUTES_CHANGED, + EVENT_SKIP_SILENCE_ENABLED_CHANGED, + EVENT_AUDIO_SINK_ERROR, + EVENT_VOLUME_CHANGED, + EVENT_VIDEO_ENABLED, + EVENT_VIDEO_DECODER_INITIALIZED, + EVENT_VIDEO_INPUT_FORMAT_CHANGED, + EVENT_DROPPED_VIDEO_FRAMES, + EVENT_VIDEO_DECODER_RELEASED, + EVENT_VIDEO_DISABLED, + EVENT_VIDEO_FRAME_PROCESSING_OFFSET, + EVENT_RENDERED_FIRST_FRAME, + EVENT_VIDEO_SIZE_CHANGED, + EVENT_SURFACE_SIZE_CHANGED, + EVENT_DRM_SESSION_ACQUIRED, + EVENT_DRM_KEYS_LOADED, + EVENT_DRM_SESSION_MANAGER_ERROR, + EVENT_DRM_KEYS_RESTORED, + EVENT_DRM_KEYS_REMOVED, + EVENT_DRM_SESSION_RELEASED + }) + @interface EventFlags {} + /** {@link Player#getCurrentTimeline()} changed. */ + int EVENT_TIMELINE_CHANGED = Player.EVENT_TIMELINE_CHANGED; + /** + * {@link Player#getCurrentMediaItem()} changed or the player started repeating the current item. + */ + int EVENT_MEDIA_ITEM_TRANSITION = Player.EVENT_MEDIA_ITEM_TRANSITION; + /** + * {@link Player#getCurrentTrackGroups()} or {@link Player#getCurrentTrackSelections()} changed. + */ + int EVENT_TRACKS_CHANGED = Player.EVENT_TRACKS_CHANGED; + /** {@link Player#getCurrentStaticMetadata()} changed. */ + int EVENT_STATIC_METADATA_CHANGED = Player.EVENT_STATIC_METADATA_CHANGED; + /** {@link Player#isLoading()} ()} changed. */ + int EVENT_IS_LOADING_CHANGED = Player.EVENT_IS_LOADING_CHANGED; + /** {@link Player#getPlaybackState()} changed. */ + int EVENT_PLAYBACK_STATE_CHANGED = Player.EVENT_PLAYBACK_STATE_CHANGED; + /** {@link Player#getPlayWhenReady()} changed. */ + int EVENT_PLAY_WHEN_READY_CHANGED = Player.EVENT_PLAY_WHEN_READY_CHANGED; + /** {@link Player#getPlaybackSuppressionReason()} changed. */ + int EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED = Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED; + /** {@link Player#isPlaying()} changed. */ + int EVENT_IS_PLAYING_CHANGED = Player.EVENT_IS_PLAYING_CHANGED; + /** {@link Player#getRepeatMode()} changed. */ + int EVENT_REPEAT_MODE_CHANGED = Player.EVENT_REPEAT_MODE_CHANGED; + /** {@link Player#getShuffleModeEnabled()} changed. */ + int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED; + /** {@link Player#getPlayerError()} changed. */ + int EVENT_PLAYER_ERROR = Player.EVENT_PLAYER_ERROR; + /** + * A position discontinuity occurred. See {@link + * Player.EventListener#onPositionDiscontinuity(int)}. + */ + int EVENT_POSITION_DISCONTINUITY = Player.EVENT_POSITION_DISCONTINUITY; + /** {@link Player#getPlaybackParameters()} changed. */ + int EVENT_PLAYBACK_PARAMETERS_CHANGED = Player.EVENT_PLAYBACK_PARAMETERS_CHANGED; + /** A source started loading data. */ + int EVENT_LOAD_STARTED = 1000; // Intentional gap to leave space for new Player events + /** A source started completed loading data. */ + int EVENT_LOAD_COMPLETED = 1001; + /** A source canceled loading data. */ + int EVENT_LOAD_CANCELED = 1002; + /** A source had a non-fatal error loading data. */ + int EVENT_LOAD_ERROR = 1003; + /** The downstream format sent to renderers changed. */ + int EVENT_DOWNSTREAM_FORMAT_CHANGED = 1004; + /** Data was removed from the end of the media buffer. */ + int EVENT_UPSTREAM_DISCARDED = 1005; + /** The bandwidth estimate has been updated. */ + int EVENT_BANDWIDTH_ESTIMATE = 1006; + /** Metadata associated with the current playback time was reported. */ + int EVENT_METADATA = 1007; + /** An audio renderer was enabled. */ + int EVENT_AUDIO_ENABLED = 1008; + /** An audio renderer created a decoder. */ + int EVENT_AUDIO_DECODER_INITIALIZED = 1009; + /** The format consumed by an audio renderer changed. */ + int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 1010; + /** The audio position has increased for the first time since the last pause or position reset. */ + int EVENT_AUDIO_POSITION_ADVANCING = 1011; + /** An audio underrun occurred. */ + int EVENT_AUDIO_UNDERRUN = 1012; + /** An audio renderer released a decoder. */ + int EVENT_AUDIO_DECODER_RELEASED = 1013; + /** An audio renderer was disabled. */ + int EVENT_AUDIO_DISABLED = 1014; + /** An audio session id was set. */ + int EVENT_AUDIO_SESSION_ID = 1015; + /** Audio attributes changed. */ + int EVENT_AUDIO_ATTRIBUTES_CHANGED = 1016; + /** Skipping silences was enabled or disabled in the audio stream. */ + int EVENT_SKIP_SILENCE_ENABLED_CHANGED = 1017; + /** The audio sink encountered a non-fatal error. */ + int EVENT_AUDIO_SINK_ERROR = 1018; + /** The volume changed. */ + int EVENT_VOLUME_CHANGED = 1019; + /** A video renderer was enabled. */ + int EVENT_VIDEO_ENABLED = 1020; + /** A video renderer created a decoder. */ + int EVENT_VIDEO_DECODER_INITIALIZED = 1021; + /** The format consumed by a video renderer changed. */ + int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 1022; + /** Video frames have been dropped. */ + int EVENT_DROPPED_VIDEO_FRAMES = 1023; + /** A video renderer released a decoder. */ + int EVENT_VIDEO_DECODER_RELEASED = 1024; + /** A video renderer was disabled. */ + int EVENT_VIDEO_DISABLED = 1025; + /** Video frame processing offset data has been reported. */ + int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 1026; + /** + * The first frame has been rendered since setting the surface, since the renderer was reset or + * since the stream changed. + */ + int EVENT_RENDERED_FIRST_FRAME = 1027; + /** The video size changed. */ + int EVENT_VIDEO_SIZE_CHANGED = 1028; + /** The surface size changed. */ + int EVENT_SURFACE_SIZE_CHANGED = 1029; + /** A DRM session has been acquired. */ + int EVENT_DRM_SESSION_ACQUIRED = 1030; + /** DRM keys were loaded. */ + int EVENT_DRM_KEYS_LOADED = 1031; + /** A non-fatal DRM session manager error occurred. */ + int EVENT_DRM_SESSION_MANAGER_ERROR = 1032; + /** DRM keys were restored. */ + int EVENT_DRM_KEYS_RESTORED = 1033; + /** DRM keys were removed. */ + int EVENT_DRM_KEYS_REMOVED = 1034; + /** A DRM session has been released. */ + int EVENT_DRM_SESSION_RELEASED = 1035; + /** Time information of an event. */ final class EventTime { @@ -753,4 +965,31 @@ public interface AnalyticsListener { * @param eventTime The event time. */ default void onDrmSessionReleased(EventTime eventTime) {} + + /** + * Called after one or more events occurred. + * + *

State changes and events that happen within one {@link Looper} message queue iteration are + * reported together and only after all individual callbacks were triggered. + * + *

Listeners should prefer this method over individual callbacks in the following cases: + * + *

    + *
  • They intend to use multiple state values together (e.g. using {@link + * Player#getCurrentWindowIndex()} to query in {@link Player#getCurrentTimeline()}). + *
  • The same logic should be triggered for multiple events (e.g. when updating a UI for both + * {@link #onPlaybackStateChanged(EventTime, int)} and {@link + * #onPlayWhenReadyChanged(EventTime, boolean, int)}). + *
  • They need access to the {@link Player} object to trigger further events (e.g. to call + * {@link Player#seekTo(long)} after a {@link + * AnalyticsListener#onMediaItemTransition(EventTime, MediaItem, int)}). + *
  • They are interested in events that logically happened together (e.g {@link + * #onPlaybackStateChanged(EventTime, int)} to {@link Player#STATE_BUFFERING} because of + * {@link #onMediaItemTransition(EventTime, MediaItem, int)}). + *
+ * + * @param player The {@link Player}. + * @param events The {@link Events} that occurred in this iteration. + */ + default void onEvents(Player player, Events events) {} } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java index 499e05acf0..f98ff33f6d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java @@ -15,7 +15,13 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.common.base.Supplier; import java.util.ArrayDeque; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nonnull; @@ -30,8 +36,9 @@ import javax.annotation.Nonnull; * was enqueued and haven't been removed since. * * @param The listener type. + * @param The {@link MutableFlags} type used to indicate which events occurred. */ -public final class ListenerSet { +public final class ListenerSet { /** * An event sent to a listener. @@ -44,17 +51,84 @@ public final class ListenerSet { void invoke(T listener); } - private final CopyOnWriteArraySet> listeners; + /** + * An event sent to a listener when all other events sent during one {@link Looper} message queue + * iteration were handled by the listener. + * + * @param The listener type. + * @param The {@link MutableFlags} type used to indicate which events occurred. + */ + public interface IterationFinishedEvent { + + /** + * Invokes the iteration finished event. + * + * @param listener The listener to invoke the event on. + * @param eventFlags The combined event flags of all events sent in this iteration. + */ + void invoke(T listener, E eventFlags); + } + + private static final int MSG_ITERATION_FINISHED = 0; + + private final Handler iterationFinishedHandler; + private final Supplier eventFlagsSupplier; + private final IterationFinishedEvent iterationFinishedEvent; + private final CopyOnWriteArraySet> listeners; private final ArrayDeque flushingEvents; private final ArrayDeque queuedEvents; private boolean released; - /** Creates the listener set. */ - public ListenerSet() { - listeners = new CopyOnWriteArraySet<>(); + /** + * Creates a new listener set. + * + * @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used + * to call all other methods of this class. + * @param eventFlagsSupplier A {@link Supplier} for new instances of {@link E the event flags + * type}. + * @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent + * during one {@link Looper} message queue iteration were handled by the listeners. + */ + public ListenerSet( + Looper looper, + Supplier eventFlagsSupplier, + IterationFinishedEvent iterationFinishedEvent) { + this( + /* listeners= */ new CopyOnWriteArraySet<>(), + looper, + eventFlagsSupplier, + iterationFinishedEvent); + } + + private ListenerSet( + CopyOnWriteArraySet> listeners, + Looper looper, + Supplier eventFlagsSupplier, + IterationFinishedEvent iterationFinishedEvent) { + this.listeners = listeners; + this.eventFlagsSupplier = eventFlagsSupplier; + this.iterationFinishedEvent = iterationFinishedEvent; flushingEvents = new ArrayDeque<>(); queuedEvents = new ArrayDeque<>(); + // It's safe to use "this" because we don't send a message before exiting the constructor. + @SuppressWarnings("methodref.receiver.bound.invalid") + Handler handler = Util.createHandler(looper, this::handleIterationFinished); + iterationFinishedHandler = handler; + } + + /** + * Copies the listener set. + * + * @param looper The new {@link Looper} for the copied listener set. + * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events + * sent during one {@link Looper} message queue iteration were handled by the listeners. + * @return The copied listener set. + */ + @CheckResult + public ListenerSet copy( + Looper looper, IterationFinishedEvent iterationFinishedEvent) { + return new ListenerSet<>(listeners, looper, eventFlagsSupplier, iterationFinishedEvent); } /** @@ -69,7 +143,7 @@ public final class ListenerSet { return; } Assertions.checkNotNull(listener); - listeners.add(new ListenerHolder(listener)); + listeners.add(new ListenerHolder<>(listener, eventFlagsSupplier)); } /** @@ -80,7 +154,7 @@ public final class ListenerSet { * @param listener The listener to be removed. */ public void remove(T listener) { - for (ListenerHolder listenerHolder : listeners) { + for (ListenerHolder listenerHolder : listeners) { if (listenerHolder.listener.equals(listener)) { listenerHolder.release(); listeners.remove(listenerHolder); @@ -91,20 +165,29 @@ public final class ListenerSet { /** * Adds an event that is sent to the listeners when {@link #flushEvents} is called. * + * @param eventFlag An integer indicating the type of the event, or {@link C#INDEX_UNSET} to not + * report this event with a flag. * @param event The event. */ - public void queueEvent(Event event) { - CopyOnWriteArraySet> listenerSnapshot = new CopyOnWriteArraySet<>(listeners); + public void queueEvent(int eventFlag, Event event) { + CopyOnWriteArraySet> listenerSnapshot = + new CopyOnWriteArraySet<>(listeners); queuedEvents.add( () -> { - for (ListenerHolder holder : listenerSnapshot) { - holder.invoke(event); + for (ListenerHolder holder : listenerSnapshot) { + holder.invoke(eventFlag, event); } }); } - /** Notifies listeners of events previously enqueued with {@link #queueEvent(Event)}. */ + /** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */ public void flushEvents() { + if (queuedEvents.isEmpty()) { + return; + } + if (!iterationFinishedHandler.hasMessages(MSG_ITERATION_FINISHED)) { + iterationFinishedHandler.obtainMessage(MSG_ITERATION_FINISHED).sendToTarget(); + } boolean recursiveFlushInProgress = !flushingEvents.isEmpty(); flushingEvents.addAll(queuedEvents); queuedEvents.clear(); @@ -119,13 +202,15 @@ public final class ListenerSet { } /** - * {@link #queueEvent(Event) Queues} a single event and immediately {@link #flushEvents() flushes} - * the event queue to notify all listeners. + * {@link #queueEvent(int, Event) Queues} a single event and immediately {@link #flushEvents() + * flushes} the event queue to notify all listeners. * + * @param eventFlag An integer flag indicating the type of the event, or {@link C#INDEX_UNSET} to + * not report this event with a flag. * @param event The event. */ - public void sendEvent(Event event) { - queueEvent(event); + public void sendEvent(int eventFlag, Event event) { + queueEvent(eventFlag, event); flushEvents(); } @@ -135,30 +220,59 @@ public final class ListenerSet { *

This will ensure no events are sent to any listener after this method has been called. */ public void release() { - for (ListenerHolder listenerHolder : listeners) { + for (ListenerHolder listenerHolder : listeners) { listenerHolder.release(); } listeners.clear(); released = true; } - private static final class ListenerHolder { + private boolean handleIterationFinished(Message message) { + for (ListenerHolder holder : listeners) { + holder.iterationFinished(eventFlagsSupplier, iterationFinishedEvent); + if (iterationFinishedHandler.hasMessages(MSG_ITERATION_FINISHED)) { + // The invocation above triggered new events (and thus scheduled a new message). We need to + // stop here because this new message will take care of informing every listener about the + // new update (including the ones already called here). + break; + } + } + return true; + } + + private static final class ListenerHolder { @Nonnull public final T listener; + private E eventsFlags; private boolean released; - public ListenerHolder(@Nonnull T listener) { + public ListenerHolder(@Nonnull T listener, Supplier eventFlagSupplier) { this.listener = listener; + this.eventsFlags = eventFlagSupplier.get(); } public void release() { released = true; } - public void invoke(Event event) { + public void invoke(int eventFlag, Event event) { if (!released) { event.invoke(listener); + if (eventFlag != C.INDEX_UNSET) { + eventsFlags.add(eventFlag); + } + } + } + + public void iterationFinished( + Supplier eventFlagSupplier, IterationFinishedEvent event) { + if (!released) { + // Reset flags before invoking the listener to ensure we keep all new flags that are set by + // recursive events triggered from this callback. + E flagToNotify = eventsFlags; + eventsFlags = eventFlagSupplier.get(); + event.invoke(listener, flagToNotify); } } @@ -170,7 +284,7 @@ public final class ListenerSet { if (other == null || getClass() != other.getClass()) { return false; } - return listener.equals(((ListenerHolder) other).listener); + return listener.equals(((ListenerHolder) other).listener); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MutableFlags.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MutableFlags.java new file mode 100644 index 0000000000..eb3afba2a1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MutableFlags.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 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 com.google.android.exoplayer2.util; + +import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; + +/** + * A set of integer flags. + * + *

Intended for usages where the number of flags may exceed 32 and can no longer be represented + * by an IntDef. + */ +public class MutableFlags { + + private final SparseBooleanArray flags; + + /** Creates the set of flags. */ + public MutableFlags() { + flags = new SparseBooleanArray(); + } + + /** Clears all previously set flags. */ + public void clear() { + flags.clear(); + } + + /** + * Adds a flag to the set. + * + * @param flag The flag to add. + */ + public void add(int flag) { + flags.append(flag, /* value= */ true); + } + + /** + * Returns whether the set contains the given flag. + * + * @param flag The flag. + * @return Whether the set contains the flag. + */ + public boolean contains(int flag) { + return flags.get(flag); + } + + /** Returns the number of flags in this set. */ + public int size() { + return flags.size(); + } + + /** + * Returns the flag at the given index. + * + * @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive). + * @return The flag at the given index. + * @throws IllegalArgumentException If index is outside the allowed range. + */ + public int get(int index) { + Assertions.checkArgument(index >= 0 && index < size()); + return flags.keyAt(index); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MutableFlags)) { + return false; + } + MutableFlags that = (MutableFlags) o; + return flags.equals(that.flags); + } + + @Override + public int hashCode() { + return flags.hashCode(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index e4f17b351d..6e251d4cde 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,9 +28,11 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -57,6 +59,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.BinaryFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.source.ClippingMediaSource; @@ -110,6 +113,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Range; import java.io.IOException; @@ -132,6 +136,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mockito; import org.robolectric.shadows.ShadowAudioManager; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link ExoPlayer}. */ @RunWith(AndroidJUnit4.class) @@ -8843,6 +8848,96 @@ public final class ExoPlayerTest { assertThat(liveOffsetAtEnd).isIn(Range.closed(11_900L, 12_100L)); } + @Test + public void onStateChangedFlags_correspondToListenerCalls() throws Exception { + ExoPlayer player = new TestExoPlayerBuilder(context).build(); + EventListener listener = mock(EventListener.class); + player.addListener(listener); + Format formatWithStaticMetadata = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H264) + .setMetadata(new Metadata(new BinaryFrame(/* id= */ "", /* data= */ new byte[0]))) + .build(); + + // Set multiple values together. + player.setMediaSource( + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), formatWithStaticMetadata)); + player.seekTo(2_000); + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)); + ShadowLooper.runMainLooperToNextTask(); + + verify(listener).onTimelineChanged(any(), anyInt()); + verify(listener).onMediaItemTransition(any(), anyInt()); + verify(listener).onPositionDiscontinuity(anyInt()); + verify(listener).onPlaybackParametersChanged(any()); + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Player.Events.class); + verify(listener).onEvents(eq(player), eventCaptor.capture()); + Player.Events events = eventCaptor.getValue(); + assertThat(events.contains(Player.EVENT_TIMELINE_CHANGED)).isTrue(); + assertThat(events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue(); + assertThat(events.contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue(); + assertThat(events.contains(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue(); + + // Set values recursively. + player.addListener( + new EventListener() { + @Override + public void onRepeatModeChanged(int repeatMode) { + player.setShuffleModeEnabled(true); + } + }); + player.setRepeatMode(Player.REPEAT_MODE_ONE); + ShadowLooper.runMainLooperToNextTask(); + + verify(listener).onRepeatModeChanged(anyInt()); + verify(listener).onShuffleModeEnabledChanged(anyBoolean()); + verify(listener, times(2)).onEvents(eq(player), eventCaptor.capture()); + events = Iterables.getLast(eventCaptor.getAllValues()); + assertThat(events.contains(Player.EVENT_REPEAT_MODE_CHANGED)).isTrue(); + assertThat(events.contains(Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue(); + + // Ensure all other events are called (even though we can't control how exactly they are + // combined together in onStateChanged calls). + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + player.play(); + player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4")); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE); + ShadowLooper.runMainLooperToNextTask(); + + // Verify that all callbacks have been called at least once. + verify(listener, atLeastOnce()).onTimelineChanged(any(), anyInt()); + verify(listener, atLeastOnce()).onMediaItemTransition(any(), anyInt()); + verify(listener, atLeastOnce()).onPositionDiscontinuity(anyInt()); + verify(listener, atLeastOnce()).onPlaybackParametersChanged(any()); + verify(listener, atLeastOnce()).onRepeatModeChanged(anyInt()); + verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean()); + verify(listener, atLeastOnce()).onPlaybackStateChanged(anyInt()); + verify(listener, atLeastOnce()).onIsLoadingChanged(anyBoolean()); + verify(listener, atLeastOnce()).onTracksChanged(any(), any()); + verify(listener, atLeastOnce()).onStaticMetadataChanged(any()); + verify(listener, atLeastOnce()).onPlayWhenReadyChanged(anyBoolean(), anyInt()); + verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean()); + verify(listener, atLeastOnce()).onPlayerError(any()); + + // Verify all the same events have been recorded with onStateChanged. + verify(listener, atLeastOnce()).onEvents(eq(player), eventCaptor.capture()); + List allEvents = eventCaptor.getAllValues(); + assertThat(containsEvent(allEvents, Player.EVENT_TIMELINE_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_POSITION_DISCONTINUITY)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_REPEAT_MODE_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_STATE_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_IS_LOADING_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_TRACKS_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_STATIC_METADATA_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_PLAY_WHEN_READY_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_IS_PLAYING_CHANGED)).isTrue(); + assertThat(containsEvent(allEvents, Player.EVENT_PLAYER_ERROR)).isTrue(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { @@ -8870,6 +8965,16 @@ public final class ExoPlayerTest { shadowOf(Looper.getMainLooper()).idle(); } + private static boolean containsEvent( + List eventsList, @Player.EventFlags int event) { + for (Player.Events events : eventsList) { + if (events.contains(event)) { + return true; + } + } + return false; + } + // Internal classes. /* {@link FakeRenderer} that can sleep and be woken-up. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 4a66f45a12..5b539b48e5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -15,6 +15,32 @@ */ package com.google.android.exoplayer2.analytics; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_DISABLED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_ENABLED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_POSITION_ADVANCING; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_AUDIO_SESSION_ID; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DOWNSTREAM_FORMAT_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DRM_KEYS_LOADED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DRM_SESSION_MANAGER_ERROR; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DRM_SESSION_RELEASED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DROPPED_VIDEO_FRAMES; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_IS_LOADING_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_COMPLETED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_STARTED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_PLAYER_ERROR; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_POSITION_DISCONTINUITY; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_RENDERED_FIRST_FRAME; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_TIMELINE_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_TRACKS_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_DISABLED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_ENABLED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_FRAME_PROCESSING_OFFSET; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; import static com.google.common.truth.Truth.assertThat; @@ -23,6 +49,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -85,51 +112,14 @@ public final class AnalyticsCollectorTest { private static final String TAG = "AnalyticsCollectorTest"; - private static final int EVENT_PLAYER_STATE_CHANGED = 0; - private static final int EVENT_TIMELINE_CHANGED = 1; - private static final int EVENT_POSITION_DISCONTINUITY = 2; - private static final int EVENT_SEEK_STARTED = 3; - private static final int EVENT_SEEK_PROCESSED = 4; - private static final int EVENT_PLAYBACK_PARAMETERS_CHANGED = 5; - private static final int EVENT_REPEAT_MODE_CHANGED = 6; - private static final int EVENT_SHUFFLE_MODE_CHANGED = 7; - private static final int EVENT_LOADING_CHANGED = 8; - private static final int EVENT_PLAYER_ERROR = 9; - private static final int EVENT_TRACKS_CHANGED = 10; - private static final int EVENT_LOAD_STARTED = 11; - private static final int EVENT_LOAD_COMPLETED = 12; - private static final int EVENT_LOAD_CANCELED = 13; - private static final int EVENT_LOAD_ERROR = 14; - private static final int EVENT_DOWNSTREAM_FORMAT_CHANGED = 15; - private static final int EVENT_UPSTREAM_DISCARDED = 16; - private static final int EVENT_BANDWIDTH_ESTIMATE = 17; - private static final int EVENT_SURFACE_SIZE_CHANGED = 18; - private static final int EVENT_METADATA = 19; - private static final int EVENT_DECODER_ENABLED = 20; - private static final int EVENT_DECODER_INIT = 21; - private static final int EVENT_DECODER_FORMAT_CHANGED = 22; - private static final int EVENT_DECODER_DISABLED = 23; - private static final int EVENT_AUDIO_ENABLED = 24; - private static final int EVENT_AUDIO_DECODER_INIT = 25; - private static final int EVENT_AUDIO_INPUT_FORMAT_CHANGED = 26; - private static final int EVENT_AUDIO_DISABLED = 27; - private static final int EVENT_AUDIO_SESSION_ID = 28; - private static final int EVENT_AUDIO_POSITION_ADVANCING = 29; - private static final int EVENT_AUDIO_UNDERRUN = 30; - private static final int EVENT_VIDEO_ENABLED = 31; - private static final int EVENT_VIDEO_DECODER_INIT = 32; - private static final int EVENT_VIDEO_INPUT_FORMAT_CHANGED = 33; - private static final int EVENT_DROPPED_FRAMES = 34; - private static final int EVENT_VIDEO_DISABLED = 35; - private static final int EVENT_RENDERED_FIRST_FRAME = 36; - private static final int EVENT_VIDEO_FRAME_PROCESSING_OFFSET = 37; - private static final int EVENT_VIDEO_SIZE_CHANGED = 38; - private static final int EVENT_DRM_KEYS_LOADED = 39; - private static final int EVENT_DRM_ERROR = 40; - private static final int EVENT_DRM_KEYS_RESTORED = 41; - private static final int EVENT_DRM_KEYS_REMOVED = 42; - private static final int EVENT_DRM_SESSION_ACQUIRED = 43; - private static final int EVENT_DRM_SESSION_RELEASED = 44; + // Deprecated event constants. + private static final long EVENT_PLAYER_STATE_CHANGED = 1L << 63; + private static final long EVENT_SEEK_STARTED = 1L << 62; + private static final long EVENT_SEEK_PROCESSED = 1L << 61; + private static final long EVENT_DECODER_ENABLED = 1L << 60; + private static final long EVENT_DECODER_INIT = 1L << 59; + private static final long EVENT_DECODER_FORMAT_CHANGED = 1L << 58; + private static final long EVENT_DECODER_DISABLED = 1L << 57; private static final UUID DRM_SCHEME_UUID = UUID.nameUUIDFromBytes(TestUtil.createByteArray(7, 8, 9)); @@ -209,7 +199,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */) .inOrder(); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */) .inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -232,14 +222,14 @@ public final class AnalyticsCollectorTest { .containsExactly(period0 /* audio */, period0 /* video */) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INITIALIZED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period0); @@ -272,7 +262,7 @@ public final class AnalyticsCollectorTest { .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */) .inOrder(); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0) .inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -308,7 +298,7 @@ public final class AnalyticsCollectorTest { period0 /* audio */, period0 /* video */, period1 /* audio */, period1 /* video */) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INITIALIZED)) .containsExactly(period0, period1) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)) @@ -317,13 +307,13 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period0); assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0, period1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0, period1) .inOrder(); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0, period1) .inOrder(); @@ -354,7 +344,7 @@ public final class AnalyticsCollectorTest { .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */) .inOrder(); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0) .inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -388,15 +378,15 @@ public final class AnalyticsCollectorTest { .inOrder(); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0 /* video */); assertThat(listener.getEvents(EVENT_AUDIO_ENABLED)).containsExactly(period1); - assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)).containsExactly(period1); + assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INITIALIZED)).containsExactly(period1); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)).containsExactly(period1); assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(period1); assertThat(listener.getEvents(EVENT_AUDIO_POSITION_ADVANCING)).containsExactly(period1); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)).containsExactly(period0); @@ -443,7 +433,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); - List loadingEvents = listener.getEvents(EVENT_LOADING_CHANGED); + List loadingEvents = listener.getEvents(EVENT_IS_LOADING_CHANGED); assertThat(loadingEvents).hasSize(4); assertThat(loadingEvents).containsAtLeast(period0, period0).inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -479,7 +469,7 @@ public final class AnalyticsCollectorTest { .containsExactly(period0 /* video */, period0 /* audio */) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_ENABLED)).containsExactly(period0, period1).inOrder(); - assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INITIALIZED)) .containsExactly(period0, period1) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)) @@ -493,7 +483,7 @@ public final class AnalyticsCollectorTest { .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(period0); @@ -544,7 +534,7 @@ public final class AnalyticsCollectorTest { .inOrder(); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0, period0, period0) .inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -582,7 +572,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_AUDIO_ENABLED)) .containsExactly(period1, period1Seq2) .inOrder(); - assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_AUDIO_DECODER_INITIALIZED)) .containsExactly(period1Seq1, period1Seq2) .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_INPUT_FORMAT_CHANGED)) @@ -596,14 +586,14 @@ public final class AnalyticsCollectorTest { .inOrder(); assertThat(listener.getEvents(EVENT_AUDIO_DISABLED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0, period0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0, period1Seq1, period1Seq2) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0, period1Seq1, period1Seq2) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)) + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) .containsExactly(period0, period1Seq2) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) @@ -662,7 +652,7 @@ public final class AnalyticsCollectorTest { WINDOW_0 /* SOURCE_UPDATE */, WINDOW_0 /* PLAYLIST_CHANGE */, WINDOW_0 /* SOURCE_UPDATE */); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -699,14 +689,14 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period0Seq1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); @@ -755,7 +745,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0Seq0, period0Seq0); @@ -781,12 +771,12 @@ public final class AnalyticsCollectorTest { .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(period0Seq0, period0Seq0); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) @@ -840,7 +830,7 @@ public final class AnalyticsCollectorTest { window0Period1Seq0 /* SOURCE_UPDATE (concatenated timeline replaces placeholder) */, period1Seq0 /* SOURCE_UPDATE (child sources in concatenating source moved) */) .inOrder(); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(window0Period1Seq0); @@ -868,14 +858,14 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)) .containsExactly(window0Period1Seq0, window0Period1Seq0) .inOrder(); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(window0Period1Seq0, window1Period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(window0Period1Seq0, window1Period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(window0Period1Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)) + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) .containsExactly(window0Period1Seq0, period1Seq0) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) @@ -938,7 +928,7 @@ public final class AnalyticsCollectorTest { period0Seq0 /* SOURCE_UPDATE (second item) */, period0Seq1 /* PLAYLIST_CHANGED (remove) */) .inOrder(); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) .containsExactly(period0Seq0, period0Seq1, period0Seq1) @@ -966,14 +956,14 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)) .containsExactly(period0Seq0, period0Seq1, period0Seq1) .inOrder(); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(period0Seq0, period0Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(period0Seq1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0Seq0, period0Seq1) .inOrder(); @@ -1150,7 +1140,7 @@ public final class AnalyticsCollectorTest { .containsExactly( contentAfterPreroll, midrollAd, contentAfterMidroll, postrollAd, contentAfterPostroll) .inOrder(); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly( prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd) @@ -1213,7 +1203,7 @@ public final class AnalyticsCollectorTest { contentAfterPostroll) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)).containsExactly(prerollAd); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly( prerollAd, contentAfterPreroll, @@ -1231,7 +1221,7 @@ public final class AnalyticsCollectorTest { postrollAd, contentAfterPostroll) .inOrder(); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)) + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) .containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) @@ -1347,7 +1337,7 @@ public final class AnalyticsCollectorTest { .inOrder(); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(contentBeforeMidroll); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(contentAfterMidroll); - assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED)) .containsExactly( contentBeforeMidroll, contentBeforeMidroll, @@ -1392,14 +1382,14 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_VIDEO_ENABLED)) .containsExactly(contentBeforeMidroll, midrollAd) .inOrder(); - assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INIT)) + assertThat(listener.getEvents(EVENT_VIDEO_DECODER_INITIALIZED)) .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll) .inOrder(); assertThat(listener.getEvents(EVENT_VIDEO_DISABLED)).containsExactly(contentBeforeMidroll); - assertThat(listener.getEvents(EVENT_DROPPED_FRAMES)).containsExactly(contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(contentAfterMidroll); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll) .inOrder(); @@ -1442,7 +1432,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); - assertThat(listener.getEvents(EVENT_DRM_ERROR)).isEmpty(); + assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); assertThat(listener.getEvents(EVENT_DRM_SESSION_ACQUIRED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_DRM_KEYS_LOADED)).containsExactly(period0); // The release event is lost because it's posted to "ExoPlayerTest thread" after that thread @@ -1459,7 +1449,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); - assertThat(listener.getEvents(EVENT_DRM_ERROR)).isEmpty(); + assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); assertThat(listener.getEvents(EVENT_DRM_SESSION_ACQUIRED)) .containsExactly(period0, period1) .inOrder(); @@ -1481,7 +1471,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); - assertThat(listener.getEvents(EVENT_DRM_ERROR)).isEmpty(); + assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty(); assertThat(listener.getEvents(EVENT_DRM_SESSION_ACQUIRED)) .containsExactly(period0, period1) .inOrder(); @@ -1502,7 +1492,7 @@ public final class AnalyticsCollectorTest { TestAnalyticsListener listener = runAnalyticsTest(mediaSource); populateEventIds(listener.lastReportedTimeline); - assertThat(listener.getEvents(EVENT_DRM_ERROR)).containsExactly(period0); + assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0); } @@ -1657,7 +1647,8 @@ public final class AnalyticsCollectorTest { public void recursiveListenerInvocation_arrivesInCorrectOrder() { AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT); analyticsCollector.setPlayer( - new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build()); + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), + Looper.myLooper()); AnalyticsListener listener1 = mock(AnalyticsListener.class); AnalyticsListener listener2 = spy( @@ -1785,7 +1776,7 @@ public final class AnalyticsCollectorTest { lastReportedTimeline = Timeline.EMPTY; } - public List getEvents(int eventType) { + public List getEvents(long eventType) { ArrayList eventTimes = new ArrayList<>(); Iterator eventIterator = reportedEvents.iterator(); while (eventIterator.hasNext()) { @@ -1845,12 +1836,12 @@ public final class AnalyticsCollectorTest { @Override public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { - reportedEvents.add(new ReportedEvent(EVENT_SHUFFLE_MODE_CHANGED, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_SHUFFLE_MODE_ENABLED_CHANGED, eventTime)); } @Override public void onIsLoadingChanged(EventTime eventTime, boolean isLoading) { - reportedEvents.add(new ReportedEvent(EVENT_LOADING_CHANGED, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_IS_LOADING_CHANGED, eventTime)); } @Override @@ -1953,7 +1944,7 @@ public final class AnalyticsCollectorTest { @Override public void onAudioDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) { - reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INIT, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_DECODER_INITIALIZED, eventTime)); } @Override @@ -1990,7 +1981,7 @@ public final class AnalyticsCollectorTest { @Override public void onVideoDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) { - reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INIT, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_VIDEO_DECODER_INITIALIZED, eventTime)); } @Override @@ -2000,7 +1991,7 @@ public final class AnalyticsCollectorTest { @Override public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { - reportedEvents.add(new ReportedEvent(EVENT_DROPPED_FRAMES, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_DROPPED_VIDEO_FRAMES, eventTime)); } @Override @@ -2015,7 +2006,7 @@ public final class AnalyticsCollectorTest { } @Override - public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { + public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) { reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime)); } @@ -2041,7 +2032,7 @@ public final class AnalyticsCollectorTest { @Override public void onDrmSessionManagerError(EventTime eventTime, Exception error) { - reportedEvents.add(new ReportedEvent(EVENT_DRM_ERROR, eventTime)); + reportedEvents.add(new ReportedEvent(EVENT_DRM_SESSION_MANAGER_ERROR, eventTime)); } @Override @@ -2061,10 +2052,10 @@ public final class AnalyticsCollectorTest { private static final class ReportedEvent { - public final int eventType; + public final long eventType; public final EventWindowAndPeriodId eventWindowAndPeriodId; - public ReportedEvent(int eventType, EventTime eventTime) { + public ReportedEvent(long eventType, EventTime eventTime) { this.eventType = eventType; this.eventWindowAndPeriodId = new EventWindowAndPeriodId(eventTime.windowIndex, eventTime.mediaPeriodId); @@ -2072,7 +2063,12 @@ public final class AnalyticsCollectorTest { @Override public String toString() { - return "{" + "type=" + eventType + ", windowAndPeriodId=" + eventWindowAndPeriodId + '}'; + return "{" + + "type=" + + Long.numberOfTrailingZeros(eventType) + + ", windowAndPeriodId=" + + eventWindowAndPeriodId + + '}'; } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java index aaa299ecd7..604e0d5a0a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ListenerSetTest.java @@ -22,39 +22,49 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.os.Looper; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mockito; +import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link ListenerSet}. */ @RunWith(AndroidJUnit4.class) public class ListenerSetTest { + private static final int EVENT_ID_1 = 0; + private static final int EVENT_ID_2 = 1; + private static final int EVENT_ID_3 = 2; + @Test - public void queueEvent_isNotSentWithoutFlush() { - ListenerSet listenerSet = new ListenerSet<>(); + public void queueEvent_withoutFlush_sendsNoEvents() { + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener = mock(TestListener.class); listenerSet.add(listener); - listenerSet.queueEvent(TestListener::callback1); - listenerSet.queueEvent(TestListener::callback2); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); + ShadowLooper.runMainLooperToNextTask(); verifyNoMoreInteractions(listener); } @Test public void flushEvents_sendsPreviouslyQueuedEventsToAllListeners() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener1 = mock(TestListener.class); TestListener listener2 = mock(TestListener.class); listenerSet.add(listener1); listenerSet.add(listener2); - listenerSet.queueEvent(TestListener::callback1); - listenerSet.queueEvent(TestListener::callback2); - listenerSet.queueEvent(TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); listenerSet.flushEvents(); InOrder inOrder = Mockito.inOrder(listener1, listener2); @@ -69,14 +79,15 @@ public class ListenerSetTest { @Test public void flushEvents_recursive_sendsEventsInCorrectOrder() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); // Listener1 sends callback3 recursively when receiving callback1. TestListener listener1 = spy( new TestListener() { @Override public void callback1() { - listenerSet.queueEvent(TestListener::callback3); + listenerSet.queueEvent(EVENT_ID_3, TestListener::callback3); listenerSet.flushEvents(); } }); @@ -84,8 +95,8 @@ public class ListenerSetTest { listenerSet.add(listener1); listenerSet.add(listener2); - listenerSet.queueEvent(TestListener::callback1); - listenerSet.queueEvent(TestListener::callback2); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); listenerSet.flushEvents(); InOrder inOrder = Mockito.inOrder(listener1, listener2); @@ -98,9 +109,121 @@ public class ListenerSetTest { inOrder.verifyNoMoreInteractions(); } + @Test + public void + flushEvents_withMultipleMessageQueueIterations_sendsIterationFinishedEventPerIteration() { + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); + // Listener1 sends callback1 recursively when receiving callback3. + TestListener listener1 = + spy( + new TestListener() { + @Override + public void callback3() { + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); + } + }); + TestListener listener2 = mock(TestListener.class); + listenerSet.add(listener1); + listenerSet.add(listener2); + + // Iteration with single flush. + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); + listenerSet.flushEvents(); + ShadowLooper.runMainLooperToNextTask(); + + // Iteration with multiple flushes. + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); + listenerSet.flushEvents(); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.flushEvents(); + ShadowLooper.runMainLooperToNextTask(); + + // Iteration with recursive call. + listenerSet.sendEvent(EVENT_ID_3, TestListener::callback3); + ShadowLooper.runMainLooperToNextTask(); + + InOrder inOrder = Mockito.inOrder(listener1, listener2); + inOrder.verify(listener1).callback2(); + inOrder.verify(listener2).callback2(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener2).callback1(); + inOrder.verify(listener1).callback2(); + inOrder.verify(listener2).callback2(); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener2).callback1(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_2)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_2)); + inOrder.verify(listener1).callback3(); + inOrder.verify(listener2).callback3(); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener2).callback1(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_3)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_3)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void flushEvents_calledFromIterationFinishedCallback_restartsIterationFinishedEvents() { + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); + // Listener2 sends callback1 recursively when receiving the iteration finished event. + TestListener listener2 = + spy( + new TestListener() { + boolean eventSent; + + @Override + public void iterationFinished(Flags flags) { + if (!eventSent) { + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); + eventSent = true; + } + } + }); + TestListener listener1 = mock(TestListener.class); + TestListener listener3 = mock(TestListener.class); + listenerSet.add(listener1); + listenerSet.add(listener2); + listenerSet.add(listener3); + + listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2); + ShadowLooper.runMainLooperToNextTask(); + + InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3); + inOrder.verify(listener1).callback2(); + inOrder.verify(listener2).callback2(); + inOrder.verify(listener3).callback2(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener2).callback1(); + inOrder.verify(listener3).callback1(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_1)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_1)); + inOrder.verify(listener3).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_2)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void flushEvents_withUnsetEventFlag_doesNotThrow() { + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); + + listenerSet.queueEvent(/* eventFlag= */ C.INDEX_UNSET, TestListener::callback1); + listenerSet.flushEvents(); + ShadowLooper.runMainLooperToNextTask(); + + // Asserts that negative event flag (INDEX_UNSET) can be used without throwing. + } + @Test public void add_withRecursion_onlyReceivesUpdatesForFutureEvents() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener2 = mock(TestListener.class); // Listener1 adds listener2 recursively. TestListener listener1 = @@ -112,38 +235,52 @@ public class ListenerSetTest { } }); - listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); listenerSet.add(listener1); // This should add listener2, but the event should not be received yet as it happened before // listener2 was added. - listenerSet.sendEvent(TestListener::callback1); - listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2); + ShadowLooper.runMainLooperToNextTask(); - verify(listener1, times(2)).callback1(); - verify(listener2).callback1(); + InOrder inOrder = Mockito.inOrder(listener1, listener2); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener1).callback2(); + inOrder.verify(listener2).callback2(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_2)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verifyNoMoreInteractions(); } @Test public void add_withQueueing_onlyReceivesUpdatesForFutureEvents() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener1 = mock(TestListener.class); TestListener listener2 = mock(TestListener.class); // This event is flushed after listener2 was added, but shouldn't be sent to listener2 because // the event itself occurred before the listener was added. listenerSet.add(listener1); - listenerSet.queueEvent(TestListener::callback2); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); listenerSet.add(listener2); - listenerSet.queueEvent(TestListener::callback2); + listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2); listenerSet.flushEvents(); + ShadowLooper.runMainLooperToNextTask(); - verify(listener1, times(2)).callback2(); - verify(listener2).callback2(); + InOrder inOrder = Mockito.inOrder(listener1, listener2); + inOrder.verify(listener1).callback1(); + inOrder.verify(listener1).callback2(); + inOrder.verify(listener2).callback2(); + inOrder.verify(listener1).iterationFinished(Flags.create(EVENT_ID_1, EVENT_ID_2)); + inOrder.verify(listener2).iterationFinished(Flags.create(EVENT_ID_2)); + inOrder.verifyNoMoreInteractions(); } @Test public void remove_withRecursion_stopsReceivingEventsImmediately() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener2 = mock(TestListener.class); // Listener1 removes listener2 recursively. TestListener listener1 = @@ -158,35 +295,40 @@ public class ListenerSetTest { listenerSet.add(listener2); // Listener2 shouldn't even get this event as it's removed before the event can be invoked. - listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); listenerSet.remove(listener1); - listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); + ShadowLooper.runMainLooperToNextTask(); verify(listener1).callback1(); - verify(listener2, never()).callback1(); + verifyNoMoreInteractions(listener1, listener2); } @Test public void remove_withQueueing_stopsReceivingEventsImmediately() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener1 = mock(TestListener.class); TestListener listener2 = mock(TestListener.class); listenerSet.add(listener1); listenerSet.add(listener2); // Listener1 shouldn't even get this event as it's removed before the event can be invoked. - listenerSet.queueEvent(TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); listenerSet.remove(listener1); - listenerSet.queueEvent(TestListener::callback1); + listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1); listenerSet.flushEvents(); + ShadowLooper.runMainLooperToNextTask(); - verify(listener1, never()).callback1(); verify(listener2, times(2)).callback1(); + verify(listener2).iterationFinished(Flags.create(EVENT_ID_1)); + verifyNoMoreInteractions(listener1, listener2); } @Test public void release_stopsForwardingEventsImmediately() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener2 = mock(TestListener.class); // Listener1 releases the set from within the callback. TestListener listener1 = @@ -201,23 +343,23 @@ public class ListenerSetTest { listenerSet.add(listener2); // Listener2 shouldn't even get this event as it's released before the event can be invoked. - listenerSet.sendEvent(TestListener::callback1); - listenerSet.sendEvent(TestListener::callback2); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2); + ShadowLooper.runMainLooperToNextTask(); verify(listener1).callback1(); - verify(listener2, never()).callback1(); - verify(listener1, never()).callback2(); - verify(listener2, never()).callback2(); + verifyNoMoreInteractions(listener1, listener2); } @Test public void release_preventsRegisteringNewListeners() { - ListenerSet listenerSet = new ListenerSet<>(); + ListenerSet listenerSet = + new ListenerSet<>(Looper.myLooper(), Flags::new, TestListener::iterationFinished); TestListener listener = mock(TestListener.class); listenerSet.release(); listenerSet.add(listener); - listenerSet.sendEvent(TestListener::callback1); + listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1); verify(listener, never()).callback1(); } @@ -228,5 +370,18 @@ public class ListenerSetTest { default void callback2() {} default void callback3() {} + + default void iterationFinished(Flags flags) {} + } + + private static final class Flags extends MutableFlags { + + public static Flags create(int... flagValues) { + Flags flags = new Flags(); + for (int value : flagValues) { + flags.add(value); + } + return flags; + } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MutableFlagsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/MutableFlagsTest.java new file mode 100644 index 0000000000..1e68f80476 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/MutableFlagsTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2020 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 com.google.android.exoplayer2.util; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit test for {@link MutableFlags}. */ +@RunWith(AndroidJUnit4.class) +public final class MutableFlagsTest { + + @Test + public void contains_withoutAdd_returnsFalseForAllValues() { + MutableFlags flags = new MutableFlags(); + + assertThat(flags.contains(/* flag= */ -1234)).isFalse(); + assertThat(flags.contains(/* flag= */ 0)).isFalse(); + assertThat(flags.contains(/* flag= */ 2)).isFalse(); + assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isFalse(); + } + + @Test + public void contains_afterAdd_returnsTrueForAddedValues() { + MutableFlags flags = new MutableFlags(); + + flags.add(/* flag= */ -1234); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 2); + flags.add(/* flag= */ Integer.MAX_VALUE); + + assertThat(flags.contains(/* flag= */ -1235)).isFalse(); + assertThat(flags.contains(/* flag= */ -1234)).isTrue(); + assertThat(flags.contains(/* flag= */ 0)).isTrue(); + assertThat(flags.contains(/* flag= */ 1)).isFalse(); + assertThat(flags.contains(/* flag= */ 2)).isTrue(); + assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE - 1)).isFalse(); + assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isTrue(); + } + + @Test + public void contains_afterClear_returnsFalseForAllValues() { + MutableFlags flags = new MutableFlags(); + flags.add(/* flag= */ -1234); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 2); + flags.add(/* flag= */ Integer.MAX_VALUE); + + flags.clear(); + + assertThat(flags.contains(/* flag= */ -1234)).isFalse(); + assertThat(flags.contains(/* flag= */ 0)).isFalse(); + assertThat(flags.contains(/* flag= */ 2)).isFalse(); + assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isFalse(); + } + + @Test + public void size_withoutAdd_returnsZero() { + MutableFlags flags = new MutableFlags(); + + assertThat(flags.size()).isEqualTo(0); + } + + @Test + public void size_afterAdd_returnsNumberUniqueOfElements() { + MutableFlags flags = new MutableFlags(); + + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 123); + flags.add(/* flag= */ 123); + + assertThat(flags.size()).isEqualTo(2); + } + + @Test + public void size_afterClear_returnsZero() { + MutableFlags flags = new MutableFlags(); + + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 123); + flags.clear(); + + assertThat(flags.size()).isEqualTo(0); + } + + @Test + public void get_withNegativeIndex_throwsIllegalArgumentException() { + MutableFlags flags = new MutableFlags(); + + assertThrows(IllegalArgumentException.class, () -> flags.get(/* index= */ -1)); + } + + @Test + public void get_withIndexExceedingSize_throwsIllegalArgumentException() { + MutableFlags flags = new MutableFlags(); + + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 123); + + assertThrows(IllegalArgumentException.class, () -> flags.get(/* index= */ 2)); + } + + @Test + public void get_afterAdd_returnsAllUniqueValues() { + MutableFlags flags = new MutableFlags(); + + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 0); + flags.add(/* flag= */ 123); + flags.add(/* flag= */ 123); + flags.add(/* flag= */ 456); + + List values = new ArrayList<>(); + for (int i = 0; i < flags.size(); i++) { + values.add(flags.get(i)); + } + assertThat(values).containsExactly(0, 123, 456); + } +}