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() {} }