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 e50eae7ca5..9e6ebd7caf 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.analytics; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.os.Looper; +import android.util.SparseArray; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -80,6 +81,7 @@ public class AnalyticsCollector private final Period period; private final Window window; private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + private final SparseArray eventTimes; private ListenerSet listeners; private @MonotonicNonNull Player player; @@ -100,6 +102,7 @@ public class AnalyticsCollector period = new Period(); window = new Window(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(period); + eventTimes = new SparseArray<>(); } /** @@ -133,7 +136,12 @@ public class AnalyticsCollector this.player == null || mediaPeriodQueueTracker.mediaPeriodQueue.isEmpty()); this.player = checkNotNull(player); listeners = - listeners.copy(looper, (listener, eventFlags) -> listener.onEvents(player, eventFlags)); + listeners.copy( + looper, + (listener, events) -> { + events.setEventTimes(eventTimes); + listener.onEvents(player, events); + }); } /** @@ -160,8 +168,8 @@ public class AnalyticsCollector if (!isSeeking) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); isSeeking = true; - listeners.sendEvent( - /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekStarted(eventTime)); + sendEvent( + eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekStarted(eventTime)); } } @@ -175,8 +183,10 @@ public class AnalyticsCollector @Override public final void onMetadata(Metadata metadata) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( - AnalyticsListener.EVENT_METADATA, listener -> listener.onMetadata(eventTime, metadata)); + sendEvent( + eventTime, + AnalyticsListener.EVENT_METADATA, + listener -> listener.onMetadata(eventTime, metadata)); } // AudioRendererEventListener implementation. @@ -185,7 +195,8 @@ public class AnalyticsCollector @Override public final void onAudioEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_ENABLED, listener -> { listener.onAudioEnabled(eventTime, counters); @@ -198,7 +209,8 @@ public class AnalyticsCollector public final void onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_DECODER_INITIALIZED, listener -> { listener.onAudioDecoderInitialized(eventTime, decoderName, initializationDurationMs); @@ -212,7 +224,8 @@ public class AnalyticsCollector public final void onAudioInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_INPUT_FORMAT_CHANGED, listener -> { listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); @@ -223,7 +236,8 @@ public class AnalyticsCollector @Override public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_POSITION_ADVANCING, listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs)); } @@ -232,7 +246,8 @@ public class AnalyticsCollector public final void onAudioUnderrun( int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_UNDERRUN, listener -> listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); @@ -241,7 +256,8 @@ public class AnalyticsCollector @Override public final void onAudioDecoderReleased(String decoderName) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_DECODER_RELEASED, listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); } @@ -250,7 +266,8 @@ public class AnalyticsCollector @Override public final void onAudioDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_DISABLED, listener -> { listener.onAudioDisabled(eventTime, counters); @@ -263,7 +280,8 @@ public class AnalyticsCollector @Override public final void onAudioSessionId(int audioSessionId) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_SESSION_ID, listener -> listener.onAudioSessionId(eventTime, audioSessionId)); } @@ -271,7 +289,8 @@ public class AnalyticsCollector @Override public void onAudioAttributesChanged(AudioAttributes audioAttributes) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_ATTRIBUTES_CHANGED, listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes)); } @@ -279,7 +298,8 @@ public class AnalyticsCollector @Override public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED, listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled)); } @@ -287,7 +307,8 @@ public class AnalyticsCollector @Override public void onAudioSinkError(Exception audioSinkError) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_AUDIO_SINK_ERROR, listener -> listener.onAudioSinkError(eventTime, audioSinkError)); } @@ -295,7 +316,8 @@ public class AnalyticsCollector @Override public void onVolumeChanged(float audioVolume) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(eventTime, audioVolume)); } @@ -306,7 +328,8 @@ public class AnalyticsCollector @Override public final void onVideoEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_ENABLED, listener -> { listener.onVideoEnabled(eventTime, counters); @@ -319,7 +342,8 @@ public class AnalyticsCollector public final void onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_DECODER_INITIALIZED, listener -> { listener.onVideoDecoderInitialized(eventTime, decoderName, initializationDurationMs); @@ -333,7 +357,8 @@ public class AnalyticsCollector public final void onVideoInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_INPUT_FORMAT_CHANGED, listener -> { listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); @@ -344,7 +369,8 @@ public class AnalyticsCollector @Override public final void onDroppedFrames(int count, long elapsedMs) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_DROPPED_VIDEO_FRAMES, listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs)); } @@ -352,7 +378,8 @@ public class AnalyticsCollector @Override public final void onVideoDecoderReleased(String decoderName) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_DECODER_RELEASED, listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); } @@ -361,7 +388,8 @@ public class AnalyticsCollector @Override public final void onVideoDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_DISABLED, listener -> { listener.onVideoDisabled(eventTime, counters); @@ -372,7 +400,8 @@ public class AnalyticsCollector @Override public final void onRenderedFirstFrame(@Nullable Surface surface) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_RENDERED_FIRST_FRAME, listener -> listener.onRenderedFirstFrame(eventTime, surface)); } @@ -380,7 +409,8 @@ public class AnalyticsCollector @Override public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_FRAME_PROCESSING_OFFSET, listener -> listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount)); @@ -397,7 +427,8 @@ public class AnalyticsCollector public final void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED, listener -> listener.onVideoSizeChanged( @@ -407,7 +438,8 @@ public class AnalyticsCollector @Override public void onSurfaceSizeChanged(int width, int height) { EventTime eventTime = generateReadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_SURFACE_SIZE_CHANGED, listener -> listener.onSurfaceSizeChanged(eventTime, width, height)); } @@ -421,7 +453,8 @@ public class AnalyticsCollector LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_LOAD_STARTED, listener -> listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData)); } @@ -433,7 +466,8 @@ public class AnalyticsCollector LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_LOAD_COMPLETED, listener -> listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData)); } @@ -445,7 +479,8 @@ public class AnalyticsCollector LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_LOAD_CANCELED, listener -> listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData)); } @@ -459,7 +494,8 @@ public class AnalyticsCollector IOException error, boolean wasCanceled) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_LOAD_ERROR, listener -> listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled)); @@ -469,7 +505,8 @@ public class AnalyticsCollector public final void onUpstreamDiscarded( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_UPSTREAM_DISCARDED, listener -> listener.onUpstreamDiscarded(eventTime, mediaLoadData)); } @@ -478,7 +515,8 @@ public class AnalyticsCollector public final void onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_DOWNSTREAM_FORMAT_CHANGED, listener -> listener.onDownstreamFormatChanged(eventTime, mediaLoadData)); } @@ -493,7 +531,8 @@ public class AnalyticsCollector public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(eventTime, reason)); } @@ -502,7 +541,8 @@ public class AnalyticsCollector public final void onMediaItemTransition( @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(eventTime, mediaItem, reason)); } @@ -511,7 +551,8 @@ public class AnalyticsCollector public final void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(eventTime, trackGroups, trackSelections)); } @@ -519,7 +560,8 @@ public class AnalyticsCollector @Override public final void onStaticMetadataChanged(List metadataList) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_STATIC_METADATA_CHANGED, listener -> listener.onStaticMetadataChanged(eventTime, metadataList)); } @@ -527,7 +569,8 @@ public class AnalyticsCollector @Override public final void onIsLoadingChanged(boolean isLoading) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_IS_LOADING_CHANGED, listener -> listener.onIsLoadingChanged(eventTime, isLoading)); } @@ -536,7 +579,8 @@ public class AnalyticsCollector @Override public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState)); } @@ -544,7 +588,8 @@ public class AnalyticsCollector @Override public final void onPlaybackStateChanged(@Player.State int state) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED, listener -> listener.onPlaybackStateChanged(eventTime, state)); } @@ -553,7 +598,8 @@ public class AnalyticsCollector public final void onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED, listener -> listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason)); } @@ -562,7 +608,8 @@ public class AnalyticsCollector public void onPlaybackSuppressionReasonChanged( @PlaybackSuppressionReason int playbackSuppressionReason) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED, listener -> listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason)); @@ -571,7 +618,8 @@ public class AnalyticsCollector @Override public void onIsPlayingChanged(boolean isPlaying) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(eventTime, isPlaying)); } @@ -579,7 +627,8 @@ public class AnalyticsCollector @Override public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(eventTime, repeatMode)); } @@ -587,7 +636,8 @@ public class AnalyticsCollector @Override public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_SHUFFLE_MODE_ENABLED_CHANGED, listener -> listener.onShuffleModeChanged(eventTime, shuffleModeEnabled)); } @@ -598,8 +648,10 @@ public class AnalyticsCollector error.mediaPeriodId != null ? generateEventTime(error.mediaPeriodId) : generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( - AnalyticsListener.EVENT_PLAYER_ERROR, listener -> listener.onPlayerError(eventTime, error)); + sendEvent( + eventTime, + AnalyticsListener.EVENT_PLAYER_ERROR, + listener -> listener.onPlayerError(eventTime, error)); } @Override @@ -609,7 +661,8 @@ public class AnalyticsCollector } mediaPeriodQueueTracker.onPositionDiscontinuity(checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_POSITION_DISCONTINUITY, listener -> listener.onPositionDiscontinuity(eventTime, reason)); } @@ -617,7 +670,8 @@ public class AnalyticsCollector @Override public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_PLAYBACK_PARAMETERS_CHANGED, listener -> listener.onPlaybackParametersChanged(eventTime, playbackParameters)); } @@ -626,8 +680,8 @@ public class AnalyticsCollector @Override public final void onSeekProcessed() { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - listeners.sendEvent( - /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); + sendEvent( + eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); } // BandwidthMeter.Listener implementation. @@ -635,7 +689,8 @@ public class AnalyticsCollector @Override public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { EventTime eventTime = generateLoadingMediaPeriodEventTime(); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_BANDWIDTH_ESTIMATE, listener -> listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate)); } @@ -645,7 +700,8 @@ public class AnalyticsCollector @Override public final void onDrmSessionAcquired(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_DRM_SESSION_ACQUIRED, listener -> listener.onDrmSessionAcquired(eventTime)); } @@ -653,15 +709,18 @@ public class AnalyticsCollector @Override public final void onDrmKeysLoaded(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( - AnalyticsListener.EVENT_DRM_KEYS_LOADED, listener -> listener.onDrmKeysLoaded(eventTime)); + sendEvent( + eventTime, + 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( + sendEvent( + eventTime, AnalyticsListener.EVENT_DRM_SESSION_MANAGER_ERROR, listener -> listener.onDrmSessionManagerError(eventTime, error)); } @@ -669,7 +728,8 @@ public class AnalyticsCollector @Override public final void onDrmKeysRestored(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( + sendEvent( + eventTime, AnalyticsListener.EVENT_DRM_KEYS_RESTORED, listener -> listener.onDrmKeysRestored(eventTime)); } @@ -677,20 +737,31 @@ public class AnalyticsCollector @Override public final void onDrmKeysRemoved(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - listeners.sendEvent( - AnalyticsListener.EVENT_DRM_KEYS_REMOVED, listener -> listener.onDrmKeysRemoved(eventTime)); + sendEvent( + eventTime, + 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( + sendEvent( + eventTime, AnalyticsListener.EVENT_DRM_SESSION_RELEASED, listener -> listener.onDrmSessionReleased(eventTime)); } // Internal methods. + private void sendEvent( + EventTime eventTime, + @AnalyticsListener.EventFlags int eventFlag, + ListenerSet.Event eventInvocation) { + eventTimes.put(eventFlag, eventTime); + listeners.sendEvent(eventFlag, eventInvocation); + } + /** 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 fa62caa5c5..c0d12ce3b5 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,10 @@ */ package com.google.android.exoplayer2.analytics; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.os.Looper; +import android.util.SparseArray; import android.view.Surface; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -63,6 +66,38 @@ public interface AnalyticsListener { /** A set of {@link EventFlags}. */ final class Events extends MutableFlags { + + private final SparseArray eventTimes; + + /** Creates the set of event flags. */ + public Events() { + eventTimes = new SparseArray<>(/* initialCapacity= */ 0); + } + + /** + * Returns the {@link EventTime} for the specified event. + * + * @param event The {@link EventFlags event}. + * @return The {@link EventTime} of this event. + */ + public EventTime getEventTime(@EventFlags int event) { + return checkNotNull(eventTimes.get(event)); + } + + /** + * Sets the {@link EventTime} values for events recorded in this set. + * + * @param eventTimes A map from {@link EventFlags} to {@link EventTime}. Must at least contain + * all the events recorded in this set. + */ + public void setEventTimes(SparseArray eventTimes) { + this.eventTimes.clear(); + for (int i = 0; i < size(); i++) { + @EventFlags int eventFlag = get(i); + this.eventTimes.append(eventFlag, checkNotNull(eventTimes.get(eventFlag))); + } + } + /** * Returns whether the given event occurred. * 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 6e251d4cde..a853c81042 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 @@ -8849,7 +8849,7 @@ public final class ExoPlayerTest { } @Test - public void onStateChangedFlags_correspondToListenerCalls() throws Exception { + public void onEvents_correspondToListenerCalls() throws Exception { ExoPlayer player = new TestExoPlayerBuilder(context).build(); EventListener listener = mock(EventListener.class); player.addListener(listener); @@ -8897,13 +8897,14 @@ public final class ExoPlayerTest { 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). + // combined together in onEvents 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(); + player.release(); // Verify that all callbacks have been called at least once. verify(listener, atLeastOnce()).onTimelineChanged(any(), anyInt()); @@ -8920,7 +8921,7 @@ public final class ExoPlayerTest { verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean()); verify(listener, atLeastOnce()).onPlayerError(any()); - // Verify all the same events have been recorded with onStateChanged. + // Verify all the same events have been recorded with onEvents. verify(listener, atLeastOnce()).onEvents(eq(player), eventCaptor.capture()); List allEvents = eventCaptor.getAllValues(); assertThat(containsEvent(allEvents, Player.EVENT_TIMELINE_CHANGED)).isTrue(); 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 5b539b48e5..ac079b99a3 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 @@ -28,11 +28,19 @@ import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_DR 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_IS_PLAYING_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_CANCELED; import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_COMPLETED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_ERROR; import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_LOAD_STARTED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_MEDIA_ITEM_TRANSITION; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_PLAYBACK_PARAMETERS_CHANGED; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_PLAYBACK_STATE_CHANGED; import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_PLAYER_ERROR; +import static com.google.android.exoplayer2.analytics.AnalyticsListener.EVENT_PLAY_WHEN_READY_CHANGED; 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_STATIC_METADATA_CHANGED; 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; @@ -45,11 +53,18 @@ import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSample import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import android.os.Looper; +import android.util.SparseArray; import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -57,6 +72,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; @@ -72,6 +88,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm; import com.google.android.exoplayer2.drm.MediaDrmCallback; import com.google.android.exoplayer2.drm.MediaDrmCallbackException; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -89,9 +106,11 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeVideoRenderer; +import com.google.android.exoplayer2.testutil.TestExoPlayerBuilder; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -103,8 +122,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mockito; +import org.robolectric.shadows.ShadowLooper; /** Integration test for {@link AnalyticsCollector}. */ @RunWith(AndroidJUnit4.class) @@ -1601,6 +1622,264 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1); } + @Test + public void onEvents_isReportedWithCorrectEventTimes() throws Exception { + SimpleExoPlayer player = + new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build(); + AnalyticsListener listener = mock(AnalyticsListener.class); + Format[] formats = + new Format[] { + new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build(), + new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build() + }; + player.addAnalyticsListener(listener); + + // Trigger some simultaneous events. + player.setMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), formats)); + player.seekTo(2_000); + player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f)); + ShadowLooper.runMainLooperToNextTask(); + + // Move to another item and fail with a third one to trigger events with different EventTimes. + player.prepare(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY); + player.addMediaSource(new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), formats)); + player.play(); + TestPlayerRunHelper.runUntilPositionDiscontinuity( + player, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4")); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE); + ShadowLooper.runMainLooperToNextTask(); + player.release(); + + // Verify that expected individual callbacks have been called and capture EventTimes. + ArgumentCaptor individualTimelineChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onTimelineChanged(individualTimelineChangedEventTimes.capture(), anyInt()); + ArgumentCaptor individualMediaItemTransitionEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onMediaItemTransition(individualMediaItemTransitionEventTimes.capture(), any(), anyInt()); + ArgumentCaptor individualPositionDiscontinuityEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onPositionDiscontinuity(individualPositionDiscontinuityEventTimes.capture(), anyInt()); + ArgumentCaptor individualPlaybackStateChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onPlaybackStateChanged(individualPlaybackStateChangedEventTimes.capture(), anyInt()); + ArgumentCaptor individualIsLoadingChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onIsLoadingChanged(individualIsLoadingChangedEventTimes.capture(), anyBoolean()); + ArgumentCaptor individualTracksChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onTracksChanged(individualTracksChangedEventTimes.capture(), any(), any()); + ArgumentCaptor individualPlayWhenReadyChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onPlayWhenReadyChanged( + individualPlayWhenReadyChangedEventTimes.capture(), anyBoolean(), anyInt()); + ArgumentCaptor individualIsPlayingChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onIsPlayingChanged(individualIsPlayingChangedEventTimes.capture(), anyBoolean()); + ArgumentCaptor individualPlayerErrorEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()).onPlayerError(individualPlayerErrorEventTimes.capture(), any()); + ArgumentCaptor individualPlaybackParametersChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onPlaybackParametersChanged( + individualPlaybackParametersChangedEventTimes.capture(), any()); + ArgumentCaptor individualLoadStartedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onLoadStarted(individualLoadStartedEventTimes.capture(), any(), any()); + ArgumentCaptor individualLoadCompletedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onLoadCompleted(individualLoadCompletedEventTimes.capture(), any(), any()); + ArgumentCaptor individualLoadErrorEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onLoadError(individualLoadErrorEventTimes.capture(), any(), any(), any(), anyBoolean()); + ArgumentCaptor individualVideoEnabledEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoEnabled(individualVideoEnabledEventTimes.capture(), any()); + ArgumentCaptor individualAudioEnabledEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onAudioEnabled(individualAudioEnabledEventTimes.capture(), any()); + ArgumentCaptor individualStaticMetadataChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onStaticMetadataChanged(individualStaticMetadataChangedEventTimes.capture(), any()); + ArgumentCaptor individualDownstreamFormatChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onDownstreamFormatChanged(individualDownstreamFormatChangedEventTimes.capture(), any()); + ArgumentCaptor individualVideoInputFormatChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoInputFormatChanged( + individualVideoInputFormatChangedEventTimes.capture(), any(), any()); + ArgumentCaptor individualAudioInputFormatChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onAudioInputFormatChanged( + individualAudioInputFormatChangedEventTimes.capture(), any(), any()); + ArgumentCaptor individualVideoDecoderInitializedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoDecoderInitialized( + individualVideoDecoderInitializedEventTimes.capture(), any(), anyLong()); + ArgumentCaptor individualAudioDecoderInitializedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onAudioDecoderInitialized( + individualAudioDecoderInitializedEventTimes.capture(), any(), anyLong()); + ArgumentCaptor individualVideoDisabledEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoDisabled(individualVideoDisabledEventTimes.capture(), any()); + ArgumentCaptor individualAudioDisabledEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onAudioDisabled(individualAudioDisabledEventTimes.capture(), any()); + ArgumentCaptor individualRenderedFirstFrameEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onRenderedFirstFrame(individualRenderedFirstFrameEventTimes.capture(), any()); + ArgumentCaptor individualVideoSizeChangedEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoSizeChanged( + individualVideoSizeChangedEventTimes.capture(), + anyInt(), + anyInt(), + anyInt(), + anyFloat()); + ArgumentCaptor individualAudioPositionAdvancingEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onAudioPositionAdvancing(individualAudioPositionAdvancingEventTimes.capture(), anyLong()); + ArgumentCaptor individualVideoProcessingOffsetEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onVideoFrameProcessingOffset( + individualVideoProcessingOffsetEventTimes.capture(), anyLong(), anyInt()); + ArgumentCaptor individualDroppedFramesEventTimes = + ArgumentCaptor.forClass(AnalyticsListener.EventTime.class); + verify(listener, atLeastOnce()) + .onDroppedVideoFrames(individualDroppedFramesEventTimes.capture(), anyInt(), anyLong()); + + // Verify the EventTimes reported with onEvents are a non-empty subset of the individual + // callback EventTimes. We can only assert they are a non-empty subset because there may be + // multiple events of the same type arriving in the same message queue iteration. + ArgumentCaptor eventsCaptor = + ArgumentCaptor.forClass(AnalyticsListener.Events.class); + verify(listener, atLeastOnce()).onEvents(eq(player), eventsCaptor.capture()); + SparseArray> onEventsEventTimes = new SparseArray<>(); + for (AnalyticsListener.Events events : eventsCaptor.getAllValues()) { + for (int i = 0; i < events.size(); i++) { + @AnalyticsListener.EventFlags int event = events.get(i); + if (onEventsEventTimes.get(event) == null) { + onEventsEventTimes.put(event, new ArrayList<>()); + } + onEventsEventTimes.get(event).add(events.getEventTime(event)); + } + } + // SparseArray.get returns null if the key doesn't exist, thus verifying the sets are non-empty. + assertThat(individualTimelineChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_TIMELINE_CHANGED)) + .inOrder(); + assertThat(individualMediaItemTransitionEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_MEDIA_ITEM_TRANSITION)) + .inOrder(); + assertThat(individualPositionDiscontinuityEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_POSITION_DISCONTINUITY)) + .inOrder(); + assertThat(individualPlaybackStateChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_PLAYBACK_STATE_CHANGED)) + .inOrder(); + assertThat(individualIsLoadingChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_IS_LOADING_CHANGED)) + .inOrder(); + assertThat(individualTracksChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_TRACKS_CHANGED)) + .inOrder(); + assertThat(individualPlayWhenReadyChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_PLAY_WHEN_READY_CHANGED)) + .inOrder(); + assertThat(individualIsPlayingChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_IS_PLAYING_CHANGED)) + .inOrder(); + assertThat(individualPlayerErrorEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_PLAYER_ERROR)) + .inOrder(); + assertThat(individualPlaybackParametersChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_PLAYBACK_PARAMETERS_CHANGED)) + .inOrder(); + assertThat(individualLoadStartedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_LOAD_STARTED)) + .inOrder(); + assertThat(individualLoadCompletedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_LOAD_COMPLETED)) + .inOrder(); + assertThat(individualLoadErrorEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_LOAD_ERROR)) + .inOrder(); + assertThat(individualVideoEnabledEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_ENABLED)) + .inOrder(); + assertThat(individualAudioEnabledEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_AUDIO_ENABLED)) + .inOrder(); + assertThat(individualStaticMetadataChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_STATIC_METADATA_CHANGED)) + .inOrder(); + assertThat(individualDownstreamFormatChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .inOrder(); + assertThat(individualVideoInputFormatChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_INPUT_FORMAT_CHANGED)) + .inOrder(); + assertThat(individualAudioInputFormatChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_AUDIO_INPUT_FORMAT_CHANGED)) + .inOrder(); + assertThat(individualVideoDecoderInitializedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_DECODER_INITIALIZED)) + .inOrder(); + assertThat(individualAudioDecoderInitializedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_AUDIO_DECODER_INITIALIZED)) + .inOrder(); + assertThat(individualVideoDisabledEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_DISABLED)) + .inOrder(); + assertThat(individualAudioDisabledEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_AUDIO_DISABLED)) + .inOrder(); + assertThat(individualRenderedFirstFrameEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_RENDERED_FIRST_FRAME)) + .inOrder(); + assertThat(individualVideoSizeChangedEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_SIZE_CHANGED)) + .inOrder(); + assertThat(individualAudioPositionAdvancingEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_AUDIO_POSITION_ADVANCING)) + .inOrder(); + assertThat(individualVideoProcessingOffsetEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_VIDEO_FRAME_PROCESSING_OFFSET)) + .inOrder(); + assertThat(individualDroppedFramesEventTimes.getAllValues()) + .containsAtLeastElementsIn(onEventsEventTimes.get(EVENT_DROPPED_VIDEO_FRAMES)) + .inOrder(); + } + private void populateEventIds(Timeline timeline) { period0 = new EventWindowAndPeriodId(