From 92dd708ef8fa4598b8e14cdf40e30fa601e58b0c Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 16 Apr 2018 02:23:22 -0700 Subject: [PATCH] Automated g4 rollback of changelist 192816182. *** Reason for rollback *** Added the missing initialization to Timeline.EMPTY. *** Original change description *** Automated g4 rollback of changelist 192742299. *** Reason for rollback *** Culprit for b/78018932. *** Original change description *** Auto-register analytics collector in SimpleExoPlayer. This automatically registers and deregisters an analytics collector in SimpleExoPlayer. Doing this also allows to write integration tests checking whether the reported window indices and media period ids are correct. *** *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=193006701 --- RELEASENOTES.md | 2 + .../android/exoplayer2/ExoPlayerFactory.java | 22 + .../android/exoplayer2/SimpleExoPlayer.java | 97 +- .../analytics/AnalyticsCollector.java | 28 +- .../analytics/AnalyticsCollectorTest.java | 1144 +++++++++++++++++ .../testutil/ExoPlayerTestRunner.java | 29 +- .../exoplayer2/testutil/FakeRenderer.java | 7 + 7 files changed, 1321 insertions(+), 8 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9bc9753e15..8966812010 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,8 @@ * Moved initial bitrate estimate from `AdaptiveTrackSelection` to `DefaultBandwidthMeter`. * Updated default max buffer length in `DefaultLoadControl`. +* Added `AnalyticsListener` interface which can be registered in + `SimpleExoPlayer` to receive detailed meta data for each ExoPlayer event. * UI components: * Add support for listening to `AspectRatioFrameLayout`'s aspect ratio update ([#3736](https://github.com/google/ExoPlayer/issues/3736)). 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 b89968e168..8095ed9c64 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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2; import android.content.Context; import android.support.annotation.Nullable; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -175,6 +176,27 @@ public final class ExoPlayerFactory { return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl, drmSessionManager); } + /** + * Creates a {@link SimpleExoPlayer} instance. + * + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + */ + public static SimpleExoPlayer newSimpleInstance( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory) { + return new SimpleExoPlayer( + renderersFactory, trackSelector, loadControl, drmSessionManager, analyticsCollectorFactory); + } + /** * Creates an {@link ExoPlayer} instance. * 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 e6979b4a60..b998027eb3 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 @@ -27,9 +27,12 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; @@ -63,6 +66,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player protected final Renderer[] renderers; private final ExoPlayer player; + private final Handler eventHandler; private final ComponentListener componentListener; private final CopyOnWriteArraySet videoListeners; @@ -70,6 +74,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; + private final AnalyticsCollector analyticsCollector; private Format videoFormat; private Format audioFormat; @@ -85,6 +90,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; + private MediaSource mediaSource; /** * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. @@ -98,7 +104,12 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager) { - this(renderersFactory, trackSelector, loadControl, drmSessionManager, Clock.DEFAULT); + this( + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + new AnalyticsCollector.Factory()); } /** @@ -107,6 +118,32 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player * @param loadControl The {@link LoadControl} that will be used by the instance. * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory) { + this( + renderersFactory, + trackSelector, + loadControl, + drmSessionManager, + analyticsCollectorFactory, + Clock.DEFAULT); + } + + /** + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that + * will collect and forward all player events. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. */ @@ -115,6 +152,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, + AnalyticsCollector.Factory analyticsCollectorFactory, Clock clock) { componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); @@ -123,7 +161,7 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player videoDebugListeners = new CopyOnWriteArraySet<>(); audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); - Handler eventHandler = new Handler(eventLooper); + eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers( eventHandler, @@ -141,6 +179,14 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player // Build the player and associated objects. player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); + analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); + addListener(analyticsCollector); + addVideoDebugListener(analyticsCollector); + addAudioDebugListener(analyticsCollector); + addMetadataOutput(analyticsCollector); + if (drmSessionManager instanceof DefaultDrmSessionManager) { + ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); + } } @Override @@ -283,6 +329,29 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player return Util.getStreamTypeForAudioUsage(audioAttributes.usage); } + /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */ + public AnalyticsCollector getAnalyticsCollector() { + return analyticsCollector; + } + + /** + * Adds an {@link AnalyticsListener} to receive analytics events. + * + * @param listener The listener to be added. + */ + public void addAnalyticsListener(AnalyticsListener listener) { + analyticsCollector.addListener(listener); + } + + /** + * Removes an {@link AnalyticsListener}. + * + * @param listener The listener to be removed. + */ + public void removeAnalyticsListener(AnalyticsListener listener) { + analyticsCollector.removeListener(listener); + } + /** * Sets the attributes for audio playback, used by the underlying audio track. If not set, the * default audio attributes will be used. They are suitable for general media playback. @@ -586,11 +655,19 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void prepare(MediaSource mediaSource) { - player.prepare(mediaSource); + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); } @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (this.mediaSource != mediaSource) { + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + mediaSource.addEventListener(eventHandler, analyticsCollector); + this.mediaSource = mediaSource; + } player.prepare(mediaSource, resetPosition, resetState); } @@ -631,21 +708,25 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void seekToDefaultPosition() { + analyticsCollector.notifySeekStarted(); player.seekToDefaultPosition(); } @Override public void seekToDefaultPosition(int windowIndex) { + analyticsCollector.notifySeekStarted(); player.seekToDefaultPosition(windowIndex); } @Override public void seekTo(long positionMs) { + analyticsCollector.notifySeekStarted(); player.seekTo(positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { + analyticsCollector.notifySeekStarted(); player.seekTo(windowIndex, positionMs); } @@ -671,12 +752,17 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player @Override public void stop() { - player.stop(); + stop(/* reset= */ false); } @Override public void stop(boolean reset) { player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + analyticsCollector.resetForNewMediaSource(); + } } @Override @@ -689,6 +775,9 @@ public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player } surface = null; } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + } } @Override 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 3a937a832d..4397b945b4 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 @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; @@ -157,6 +158,22 @@ public class AnalyticsCollector } } + /** + * Resets the analytics collector for a new media source. Should be called before the player is + * prepared with a new media source. + */ + public final void resetForNewMediaSource() { + // Copying the list is needed because onMediaPeriodReleased will modify the list. + List activeMediaPeriods = + new ArrayList<>(mediaPeriodQueueTracker.activeMediaPeriods); + Timeline timeline = mediaPeriodQueueTracker.timeline; + for (MediaPeriodId mediaPeriod : activeMediaPeriods) { + int windowIndex = + timeline.isEmpty() ? 0 : timeline.getPeriod(mediaPeriod.periodIndex, period).windowIndex; + onMediaPeriodReleased(windowIndex, mediaPeriod); + } + } + // MetadataOutput implementation. @Override @@ -631,6 +648,9 @@ public class AnalyticsCollector /** Keeps track of the active media periods and currently playing and reading media period. */ private static final class MediaPeriodQueueTracker { + // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue + // changes, which would hopefully remove the need to track the queue here. + private final ArrayList activeMediaPeriods; private final Period period; @@ -642,6 +662,7 @@ public class AnalyticsCollector public MediaPeriodQueueTracker() { activeMediaPeriods = new ArrayList<>(); period = new Period(); + timeline = Timeline.EMPTY; } /** @@ -763,13 +784,14 @@ public class AnalyticsCollector } private void updateLastReportedPlayingMediaPeriod() { - lastReportedPlayingMediaPeriod = - activeMediaPeriods.isEmpty() ? null : activeMediaPeriods.get(0); + if (!activeMediaPeriods.isEmpty()) { + lastReportedPlayingMediaPeriod = activeMediaPeriods.get(0); + } } private MediaPeriodId updateMediaPeriodIdToNewTimeline( MediaPeriodId mediaPeriodId, Timeline newTimeline) { - if (newTimeline.isEmpty()) { + if (newTimeline.isEmpty() || timeline.isEmpty()) { return mediaPeriodId; } Object uid = timeline.getPeriod(mediaPeriodId.periodIndex, period, /* setIds= */ true).uid; 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 new file mode 100644 index 0000000000..2320563750 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -0,0 +1,1144 @@ +/* + * Copyright (C) 2018 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.analytics; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataOutput; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** Integration test for {@link AnalyticsCollector}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class 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_MEDIA_PERIOD_CREATED = 17; + private static final int EVENT_MEDIA_PERIOD_RELEASED = 18; + private static final int EVENT_READING_STARTED = 19; + private static final int EVENT_BANDWIDTH_ESTIMATE = 20; + private static final int EVENT_VIEWPORT_SIZE_CHANGED = 21; + private static final int EVENT_NETWORK_TYPE_CHANGED = 22; + private static final int EVENT_METADATA = 23; + private static final int EVENT_DECODER_ENABLED = 24; + private static final int EVENT_DECODER_INIT = 25; + private static final int EVENT_DECODER_FORMAT_CHANGED = 26; + private static final int EVENT_DECODER_DISABLED = 27; + private static final int EVENT_AUDIO_SESSION_ID = 28; + private static final int EVENT_AUDIO_UNDERRUN = 29; + private static final int EVENT_DROPPED_VIDEO_FRAMES = 30; + private static final int EVENT_VIDEO_SIZE_CHANGED = 31; + private static final int EVENT_RENDERED_FIRST_FRAME = 32; + private static final int EVENT_AD_LOAD_ERROR = 33; + private static final int EVENT_INTERNAL_AD_LOAD_ERROR = 34; + private static final int EVENT_AD_CLICKED = 35; + private static final int EVENT_AD_TAPPED = 36; + private static final int EVENT_DRM_KEYS_LOADED = 37; + private static final int EVENT_DRM_ERROR = 38; + private static final int EVENT_DRM_KEYS_RESTORED = 39; + private static final int EVENT_DRM_KEYS_REMOVED = 40; + + private static final int TIMEOUT_MS = 10000; + private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline(/* windowCount= */ 1); + private static final EventWindowAndPeriodId WINDOW_0 = + new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null); + private static final EventWindowAndPeriodId WINDOW_1 = + new EventWindowAndPeriodId(/* windowIndex= */ 1, /* mediaPeriodId= */ null); + private static final EventWindowAndPeriodId PERIOD_0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + private static final EventWindowAndPeriodId PERIOD_1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + private static final EventWindowAndPeriodId PERIOD_0_SEQ_0 = PERIOD_0; + private static final EventWindowAndPeriodId PERIOD_1_SEQ_1 = PERIOD_1; + private static final EventWindowAndPeriodId PERIOD_0_SEQ_1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + private static final EventWindowAndPeriodId PERIOD_1_SEQ_0 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + private static final EventWindowAndPeriodId PERIOD_1_SEQ_2 = + new EventWindowAndPeriodId( + /* windowIndex= */ 1, + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 2)); + + @Test + public void testEmptyTimeline() throws Exception { + FakeMediaSource mediaSource = + new FakeMediaSource( + Timeline.EMPTY, /* manifest= */ null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSinglePeriod() throws Exception { + FakeMediaSource mediaSource = + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0 /* started */, PERIOD_0 /* stopped */); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly(WINDOW_0 /* manifest */, PERIOD_0 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testAutomaticPeriodTransition() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT)); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* audio */, PERIOD_0 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly( + PERIOD_0 /* audio */, PERIOD_0 /* video */, PERIOD_1 /* audio */, PERIOD_1 /* video */); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testPeriodTransitionWithRendererChange() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady */, + WINDOW_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_1 /* BUFFERING */, + PERIOD_1 /* READY */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSeekToOtherPeriod() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.AUDIO_FORMAT)); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0 /* READY */, + PERIOD_1 /* BUFFERING */, + PERIOD_1 /* READY */, + PERIOD_1 /* setPlayWhenReady=true */, + PERIOD_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_1); + List loadingEvents = listener.getEvents(EVENT_LOADING_CHANGED); + assertThat(loadingEvents).hasSize(4); + assertThat(loadingEvents).containsAllOf(PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(PERIOD_0, PERIOD_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0 /* video */, PERIOD_1 /* audio */); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testSeekBackAfterReadingAhead() throws Exception { + MediaSource mediaSource = + new ConcatenatingMediaSource( + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT), + new FakeMediaSource( + SINGLE_PERIOD_TIMELINE, + /* manifest= */ null, + Builder.VIDEO_FORMAT, + Builder.AUDIO_FORMAT)); + long periodDurationMs = + SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) + .seek(/* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_READY) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0 /* READY */, + PERIOD_0 /* setPlayWhenReady=true */, + PERIOD_0 /* setPlayWhenReady=false */, + PERIOD_0 /* BUFFERING */, + PERIOD_0 /* READY */, + PERIOD_0 /* setPlayWhenReady=true */, + PERIOD_1_SEQ_2 /* BUFFERING */, + PERIOD_1_SEQ_2 /* READY */, + PERIOD_1_SEQ_2 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0, PERIOD_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1_SEQ_1 /* media */, + PERIOD_1_SEQ_2 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0 /* media */, + WINDOW_1 /* manifest */, + PERIOD_1_SEQ_1 /* media */, + PERIOD_1_SEQ_2 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_1, PERIOD_1_SEQ_2, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_AUDIO_SESSION_ID)).containsExactly(PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(PERIOD_0, PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0, PERIOD_1_SEQ_2); + listener.assertNoMoreEvents(); + } + + @Test + public void testPrepareNewSource() throws Exception { + MediaSource mediaSource1 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + MediaSource mediaSource2 = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .prepareSource(mediaSource2) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0_SEQ_0 /* READY */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=true */, + PERIOD_0_SEQ_1 /* READY */, + PERIOD_0_SEQ_1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_1, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly( + PERIOD_0_SEQ_0 /* prepared */, WINDOW_0 /* reset */, PERIOD_0_SEQ_1 /* prepared */); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_1); + listener.assertNoMoreEvents(); + } + + @Test + public void testReprepareAfterError() throws Exception { + MediaSource mediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .waitForPlaybackState(Player.STATE_READY) + .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + PERIOD_0_SEQ_0 /* READY */, + WINDOW_0 /* IDLE */, + WINDOW_0 /* BUFFERING */, + PERIOD_0_SEQ_0 /* READY */, + PERIOD_0_SEQ_0 /* ENDED */); + // assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)).doesNotContain(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */, + WINDOW_0 /* manifest */, + PERIOD_0_SEQ_0 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testDynamicTimelineChange() throws Exception { + MediaSource childMediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null, Builder.VIDEO_FORMAT); + final ConcatenatingMediaSource concatenatedMediaSource = + new ConcatenatingMediaSource(childMediaSource, childMediaSource); + long periodDurationMs = + SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + // Ensure second period is already being read from. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ periodDurationMs) + .executeRunnable( + new Runnable() { + @Override + public void run() { + concatenatedMediaSource.moveMediaSource( + /* currentIndex= */ 0, /* newIndex= */ 1); + } + }) + .waitForTimelineChanged(/* expectedTimeline= */ null) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* BUFFERING */, + WINDOW_0 /* setPlayWhenReady=false */, + PERIOD_0_SEQ_0 /* READY */, + PERIOD_0_SEQ_0 /* setPlayWhenReady=true */, + PERIOD_0_SEQ_0 /* setPlayWhenReady=false */, + PERIOD_1_SEQ_0 /* setPlayWhenReady=true */, + PERIOD_1_SEQ_0 /* BUFFERING */, + PERIOD_1_SEQ_0 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0, PERIOD_1_SEQ_0); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* manifest */, PERIOD_0_SEQ_0 /* media */, PERIOD_1_SEQ_1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(PERIOD_0_SEQ_1); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(PERIOD_0_SEQ_0, PERIOD_1_SEQ_1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(PERIOD_0_SEQ_0); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(PERIOD_0_SEQ_0); + listener.assertNoMoreEvents(); + } + + @Test + public void testNotifyExternalEvents() throws Exception { + MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, /* manifest= */ null); + final NetworkInfo networkInfo = + ((ConnectivityManager) + RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE)) + .getActiveNetworkInfo(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.getAnalyticsCollector().notifyNetworkTypeChanged(networkInfo); + player + .getAnalyticsCollector() + .notifyViewportSizeChanged(/* width= */ 320, /* height= */ 240); + player.getAnalyticsCollector().notifySeekStarted(); + } + }) + .seek(/* positionMs= */ 0) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); + + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_VIEWPORT_SIZE_CHANGED)).containsExactly(PERIOD_0); + assertThat(listener.getEvents(EVENT_NETWORK_TYPE_CHANGED)).containsExactly(PERIOD_0); + } + + private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception { + return runAnalyticsTest(mediaSource, /* actionSchedule= */ null); + } + + private static TestAnalyticsListener runAnalyticsTest( + MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception { + RenderersFactory renderersFactory = + new RenderersFactory() { + @Override + public Renderer[] createRenderers( + Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput, + @Nullable DrmSessionManager drmSessionManager) { + return new Renderer[] { + new FakeVideoRenderer(eventHandler, videoRendererEventListener), + new FakeAudioRenderer(eventHandler, audioRendererEventListener) + }; + } + }; + TestAnalyticsListener listener = new TestAnalyticsListener(); + try { + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderersFactory(renderersFactory) + .setAnalyticsListener(listener) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + } catch (ExoPlaybackException e) { + // Ignore ExoPlaybackException as these may be expected. + } + return listener; + } + + private static final class FakeVideoRenderer extends FakeRenderer { + + private final VideoRendererEventListener.EventDispatcher eventDispatcher; + private final DecoderCounters decoderCounters; + private Format format; + private boolean renderedFirstFrame; + + public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { + super(Builder.VIDEO_FORMAT); + eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); + decoderCounters = new DecoderCounters(); + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + super.onEnabled(joining); + eventDispatcher.enabled(decoderCounters); + renderedFirstFrame = false; + } + + @Override + protected void onStopped() throws ExoPlaybackException { + super.onStopped(); + eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0); + } + + @Override + protected void onDisabled() { + super.onDisabled(); + eventDispatcher.disabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + renderedFirstFrame = false; + } + + @Override + protected void onFormatChanged(Format format) { + eventDispatcher.inputFormatChanged(format); + eventDispatcher.decoderInitialized( + /* decoderName= */ "fake.video.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0); + this.format = format; + } + + @Override + protected void onBufferRead() { + if (!renderedFirstFrame) { + eventDispatcher.videoSizeChanged( + format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio); + eventDispatcher.renderedFirstFrame(/* surface= */ null); + renderedFirstFrame = true; + } + } + } + + private static final class FakeAudioRenderer extends FakeRenderer { + + private final AudioRendererEventListener.EventDispatcher eventDispatcher; + private final DecoderCounters decoderCounters; + private boolean notifiedAudioSessionId; + + public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { + super(Builder.AUDIO_FORMAT); + eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); + decoderCounters = new DecoderCounters(); + } + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + super.onEnabled(joining); + eventDispatcher.enabled(decoderCounters); + notifiedAudioSessionId = false; + } + + @Override + protected void onDisabled() { + super.onDisabled(); + eventDispatcher.disabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + super.onPositionReset(positionUs, joining); + } + + @Override + protected void onFormatChanged(Format format) { + eventDispatcher.inputFormatChanged(format); + eventDispatcher.decoderInitialized( + /* decoderName= */ "fake.audio.decoder", + /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), + /* initializationDurationMs= */ 0); + } + + @Override + protected void onBufferRead() { + if (!notifiedAudioSessionId) { + eventDispatcher.audioSessionId(/* audioSessionId= */ 0); + notifiedAudioSessionId = true; + } + } + } + + private static final class EventWindowAndPeriodId { + + private final int windowIndex; + private final @Nullable MediaPeriodId mediaPeriodId; + + public EventWindowAndPeriodId(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + this.windowIndex = windowIndex; + this.mediaPeriodId = mediaPeriodId; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof EventWindowAndPeriodId)) { + return false; + } + EventWindowAndPeriodId event = (EventWindowAndPeriodId) other; + return windowIndex == event.windowIndex && Util.areEqual(mediaPeriodId, event.mediaPeriodId); + } + + @Override + public String toString() { + return mediaPeriodId != null + ? "Event{" + + "window=" + + windowIndex + + ", period=" + + mediaPeriodId.periodIndex + + ", sequence=" + + mediaPeriodId.windowSequenceNumber + + '}' + : "Event{" + "window=" + windowIndex + ", period = null}"; + } + + @Override + public int hashCode() { + return 31 * windowIndex + (mediaPeriodId == null ? 0 : mediaPeriodId.hashCode()); + } + } + + private static final class TestAnalyticsListener implements AnalyticsListener { + + private final ArrayList reportedEvents; + + public TestAnalyticsListener() { + reportedEvents = new ArrayList<>(); + } + + public List getEvents(int eventType) { + ArrayList eventTimes = new ArrayList<>(); + Iterator eventIterator = reportedEvents.iterator(); + while (eventIterator.hasNext()) { + ReportedEvent event = eventIterator.next(); + if (event.eventType == eventType) { + eventTimes.add(event.eventWindowAndPeriodId); + eventIterator.remove(); + } + } + return eventTimes; + } + + public void assertNoMoreEvents() { + assertThat(reportedEvents).isEmpty(); + } + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, int playbackState) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYER_STATE_CHANGED, eventTime)); + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime)); + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + reportedEvents.add(new ReportedEvent(EVENT_POSITION_DISCONTINUITY, eventTime)); + } + + @Override + public void onSeekStarted(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_SEEK_STARTED, eventTime)); + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_SEEK_PROCESSED, eventTime)); + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYBACK_PARAMETERS_CHANGED, eventTime)); + } + + @Override + public void onRepeatModeChanged(EventTime eventTime, int repeatMode) { + reportedEvents.add(new ReportedEvent(EVENT_REPEAT_MODE_CHANGED, eventTime)); + } + + @Override + public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) { + reportedEvents.add(new ReportedEvent(EVENT_SHUFFLE_MODE_CHANGED, eventTime)); + } + + @Override + public void onLoadingChanged(EventTime eventTime, boolean isLoading) { + reportedEvents.add(new ReportedEvent(EVENT_LOADING_CHANGED, eventTime)); + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + reportedEvents.add(new ReportedEvent(EVENT_PLAYER_ERROR, eventTime)); + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + reportedEvents.add(new ReportedEvent(EVENT_TRACKS_CHANGED, eventTime)); + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_STARTED, eventTime)); + } + + @Override + public void onLoadCompleted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_COMPLETED, eventTime)); + } + + @Override + public void onLoadCanceled( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_CANCELED, eventTime)); + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + reportedEvents.add(new ReportedEvent(EVENT_LOAD_ERROR, eventTime)); + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_DOWNSTREAM_FORMAT_CHANGED, eventTime)); + } + + @Override + public void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) { + reportedEvents.add(new ReportedEvent(EVENT_UPSTREAM_DISCARDED, eventTime)); + } + + @Override + public void onMediaPeriodCreated(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_CREATED, eventTime)); + } + + @Override + public void onMediaPeriodReleased(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_MEDIA_PERIOD_RELEASED, eventTime)); + } + + @Override + public void onReadingStarted(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_READING_STARTED, eventTime)); + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + reportedEvents.add(new ReportedEvent(EVENT_BANDWIDTH_ESTIMATE, eventTime)); + } + + @Override + public void onViewportSizeChange(EventTime eventTime, int width, int height) { + reportedEvents.add(new ReportedEvent(EVENT_VIEWPORT_SIZE_CHANGED, eventTime)); + } + + @Override + public void onNetworkTypeChanged(EventTime eventTime, @Nullable NetworkInfo networkInfo) { + reportedEvents.add(new ReportedEvent(EVENT_NETWORK_TYPE_CHANGED, eventTime)); + } + + @Override + public void onMetadata(EventTime eventTime, Metadata metadata) { + reportedEvents.add(new ReportedEvent(EVENT_METADATA, eventTime)); + } + + @Override + public void onDecoderEnabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_ENABLED, eventTime)); + } + + @Override + public void onDecoderInitialized( + EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_INIT, eventTime)); + } + + @Override + public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_FORMAT_CHANGED, eventTime)); + } + + @Override + public void onDecoderDisabled( + EventTime eventTime, int trackType, DecoderCounters decoderCounters) { + reportedEvents.add(new ReportedEvent(EVENT_DECODER_DISABLED, eventTime)); + } + + @Override + public void onAudioSessionId(EventTime eventTime, int audioSessionId) { + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_SESSION_ID, eventTime)); + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + reportedEvents.add(new ReportedEvent(EVENT_AUDIO_UNDERRUN, eventTime)); + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + reportedEvents.add(new ReportedEvent(EVENT_DROPPED_VIDEO_FRAMES, eventTime)); + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + reportedEvents.add(new ReportedEvent(EVENT_VIDEO_SIZE_CHANGED, eventTime)); + } + + @Override + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { + reportedEvents.add(new ReportedEvent(EVENT_RENDERED_FIRST_FRAME, eventTime)); + } + + @Override + public void onAdLoadError(EventTime eventTime, IOException error) { + reportedEvents.add(new ReportedEvent(EVENT_AD_LOAD_ERROR, eventTime)); + } + + @Override + public void onInternalAdLoadError(EventTime eventTime, RuntimeException error) { + reportedEvents.add(new ReportedEvent(EVENT_INTERNAL_AD_LOAD_ERROR, eventTime)); + } + + @Override + public void onAdClicked(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_AD_CLICKED, eventTime)); + } + + @Override + public void onAdTapped(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_AD_TAPPED, eventTime)); + } + + @Override + public void onDrmKeysLoaded(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_LOADED, eventTime)); + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_ERROR, eventTime)); + } + + @Override + public void onDrmKeysRestored(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_RESTORED, eventTime)); + } + + @Override + public void onDrmKeysRemoved(EventTime eventTime) { + reportedEvents.add(new ReportedEvent(EVENT_DRM_KEYS_REMOVED, eventTime)); + } + + private static final class ReportedEvent { + + public final int eventType; + public final EventWindowAndPeriodId eventWindowAndPeriodId; + + public ReportedEvent(int eventType, EventTime eventTime) { + this.eventType = eventType; + this.eventWindowAndPeriodId = + new EventWindowAndPeriodId(eventTime.windowIndex, eventTime.mediaPeriodId); + } + } + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 6855ca4cc3..cf7470b80a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -86,6 +88,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private Player.EventListener eventListener; private VideoRendererEventListener videoRendererEventListener; private AudioRendererEventListener audioRendererEventListener; + private AnalyticsListener analyticsListener; private Integer expectedPlayerEndedCount; /** @@ -261,6 +264,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets an {@link AnalyticsListener} to be registered. + * + * @param analyticsListener An {@link AnalyticsListener} to be registered. + * @return This builder. + */ + public Builder setAnalyticsListener(AnalyticsListener analyticsListener) { + this.analyticsListener = analyticsListener; + return this; + } + /** * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} * or {@link Player#STATE_IDLE}. The default is 1. This affects how long @@ -330,6 +344,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener eventListener, videoRendererEventListener, audioRendererEventListener, + analyticsListener, expectedPlayerEndedCount); } } @@ -343,6 +358,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final @Nullable Player.EventListener eventListener; private final @Nullable VideoRendererEventListener videoRendererEventListener; private final @Nullable AudioRendererEventListener audioRendererEventListener; + private final @Nullable AnalyticsListener analyticsListener; private final HandlerThread playerThread; private final HandlerWrapper handler; @@ -369,6 +385,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Nullable Player.EventListener eventListener, @Nullable VideoRendererEventListener videoRendererEventListener, @Nullable AudioRendererEventListener audioRendererEventListener, + @Nullable AnalyticsListener analyticsListener, int expectedPlayerEndedCount) { this.clock = clock; this.mediaSource = mediaSource; @@ -379,6 +396,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.eventListener = eventListener; this.videoRendererEventListener = videoRendererEventListener; this.audioRendererEventListener = audioRendererEventListener; + this.analyticsListener = analyticsListener; this.timelines = new ArrayList<>(); this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); @@ -417,6 +435,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (audioRendererEventListener != null) { player.addAudioDebugListener(audioRendererEventListener); } + if (analyticsListener != null) { + player.addAnalyticsListener(analyticsListener); + } player.setPlayWhenReady(true); if (actionSchedule != null) { actionSchedule.start( @@ -636,7 +657,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener TrackSelector trackSelector, LoadControl loadControl, Clock clock) { - super(renderersFactory, trackSelector, loadControl, /* drmSessionManager= */ null, clock); + super( + renderersFactory, + trackSelector, + loadControl, + /* drmSessionManager= */ null, + new AnalyticsCollector.Factory(), + clock); } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index 171e237fd1..0d65d7fcc7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -85,6 +85,7 @@ public class FakeRenderer extends BaseRenderer { if (result == C.RESULT_FORMAT_READ) { formatReadCount++; assertThat(expectedFormats).contains(formatHolder.format); + onFormatChanged(formatHolder.format); } else if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { isEnded = true; @@ -92,6 +93,7 @@ public class FakeRenderer extends BaseRenderer { } lastSamplePositionUs = buffer.timeUs; sampleBufferReadCount++; + onBufferRead(); } else { Assertions.checkState(result == C.RESULT_NOTHING_READ); return; @@ -115,4 +117,9 @@ public class FakeRenderer extends BaseRenderer { ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; } + /** Called when the renderer reads a new format. */ + protected void onFormatChanged(Format format) {} + + /** Called when the renderer read a sample from the buffer. */ + protected void onBufferRead() {} }