From e5e903eb8ac11a0fc003e92811c8959581e270ba Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 23 Nov 2020 11:27:12 +0000 Subject: [PATCH] Add EventTimes to AnalyticsListener.onEvents The EventTime wasn't part of the onEvents callbacks so far because the individual events may have different event times (e.g. if they relate to different media periods). By adding a query method to obtain the EventTime by event type, we can solve this issue. #exofixit PiperOrigin-RevId: 343818819 --- .../analytics/AnalyticsCollector.java | 191 ++++++++---- .../analytics/AnalyticsListener.java | 35 +++ .../android/exoplayer2/ExoPlayerTest.java | 7 +- .../analytics/AnalyticsCollectorTest.java | 279 ++++++++++++++++++ 4 files changed, 449 insertions(+), 63 deletions(-) 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(