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 29b8967587..9fa9257459 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 @@ -76,6 +76,7 @@ public class AnalyticsCollector private final MediaPeriodQueueTracker mediaPeriodQueueTracker; private @MonotonicNonNull Player player; + private boolean isSeeking; /** * Creates an analytics collector. @@ -126,9 +127,9 @@ public class AnalyticsCollector * adjusts its state and position to the seek. */ public final void notifySeekStarted() { - if (!mediaPeriodQueueTracker.isSeeking()) { + if (!isSeeking) { EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); - mediaPeriodQueueTracker.onSeekStarted(); + isSeeking = true; for (AnalyticsListener listener : listeners) { listener.onSeekStarted(eventTime); } @@ -309,7 +310,8 @@ public class AnalyticsCollector @Override public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId); + mediaPeriodQueueTracker.onMediaPeriodCreated( + windowIndex, mediaPeriodId, Assertions.checkNotNull(player)); EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); for (AnalyticsListener listener : listeners) { listener.onMediaPeriodCreated(eventTime); @@ -319,7 +321,8 @@ public class AnalyticsCollector @Override public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); - if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) { + if (mediaPeriodQueueTracker.onMediaPeriodReleased( + mediaPeriodId, Assertions.checkNotNull(player))) { for (AnalyticsListener listener : listeners) { listener.onMediaPeriodReleased(eventTime); } @@ -411,7 +414,7 @@ public class AnalyticsCollector @Override public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { - mediaPeriodQueueTracker.onTimelineChanged(timeline); + mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onTimelineChanged(eventTime, reason); @@ -478,7 +481,7 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } @@ -486,6 +489,7 @@ public class AnalyticsCollector @Override public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + mediaPeriodQueueTracker.onPositionDiscontinuity(Assertions.checkNotNull(player)); EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPositionDiscontinuity(eventTime, reason); @@ -502,8 +506,8 @@ public class AnalyticsCollector @Override public final void onSeekProcessed() { - if (mediaPeriodQueueTracker.isSeeking()) { - mediaPeriodQueueTracker.onSeekProcessed(); + if (isSeeking) { + isSeeking = false; EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onSeekProcessed(eventTime); @@ -620,13 +624,10 @@ public class AnalyticsCollector Assertions.checkNotNull(player); if (mediaPeriodInfo == null) { int windowIndex = player.getCurrentWindowIndex(); - mediaPeriodInfo = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); - if (mediaPeriodInfo == null) { - Timeline timeline = player.getCurrentTimeline(); - boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); - return generateEventTime( - windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); - } + Timeline timeline = player.getCurrentTimeline(); + boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); + return generateEventTime( + windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); } return generateEventTime( mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); @@ -673,10 +674,10 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - @Nullable private MediaPeriodInfo playingMediaPeriod; + @Nullable private MediaPeriodInfo currentPlayerMediaPeriod; + private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod; @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; - private boolean isSeeking; public MediaPeriodQueueTracker() { mediaPeriodInfoQueue = new ArrayList<>(); @@ -689,14 +690,11 @@ public class AnalyticsCollector * Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of * the player. * - *

May be null if no matching media period has been created yet or the player is currently - * masking its state. + *

May be null if no matching media period has been created yet. */ @Nullable public MediaPeriodInfo getCurrentPlayerMediaPeriod() { - return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking - ? null - : mediaPeriodInfoQueue.get(0); + return currentPlayerMediaPeriod; } /** @@ -739,35 +737,13 @@ public class AnalyticsCollector return mediaPeriodIdToInfo.get(mediaPeriodId); } - /** Returns whether the player is currently seeking. */ - public boolean isSeeking() { - return isSeeking; - } - - /** - * Tries to find an existing media period info from the specified window index. Only returns a - * non-null media period info if there is a unique, unambiguous match. - */ - @Nullable - public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { - MediaPeriodInfo match = null; - for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { - MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); - int periodIndex = timeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); - if (periodIndex != C.INDEX_UNSET - && timeline.getPeriod(periodIndex, period).windowIndex == windowIndex) { - if (match != null) { - // Ambiguous match. - return null; - } - match = info; - } - } - return match; + /** Updates the queue with a reported position discontinuity. */ + public void onPositionDiscontinuity(Player player) { + currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); } /** Updates the queue with a reported timeline change. */ - public void onTimelineChanged(Timeline timeline) { + public void onTimelineChanged(Timeline timeline, Player player) { for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { MediaPeriodInfo newMediaPeriodInfo = updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline); @@ -781,20 +757,11 @@ public class AnalyticsCollector playingMediaPeriod = mediaPeriodInfoQueue.get(0); } this.timeline = timeline; - } - - /** Updates the queue with a reported start of seek. */ - public void onSeekStarted() { - isSeeking = true; - } - - /** Updates the queue with a reported processed seek. */ - public void onSeekProcessed() { - isSeeking = false; + currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); } /** Updates the queue with a newly created media period. */ - public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { + public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) { int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); boolean isInTimeline = periodIndex != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = @@ -805,14 +772,17 @@ public class AnalyticsCollector mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); playingMediaPeriod = mediaPeriodInfoQueue.get(0); + if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { + currentPlayerMediaPeriod = playingMediaPeriod; + } } /** * Updates the queue with a released media period. Returns whether the media period was still in * the queue. */ - public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) { - MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); + public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId, Player player) { + @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); if (mediaPeriodInfo == null) { // The media period has already been removed from the queue in resetForNewPlaylist(). return false; @@ -824,6 +794,9 @@ public class AnalyticsCollector if (!mediaPeriodInfoQueue.isEmpty()) { playingMediaPeriod = mediaPeriodInfoQueue.get(0); } + if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { + currentPlayerMediaPeriod = playingMediaPeriod; + } return true; } @@ -832,6 +805,99 @@ public class AnalyticsCollector readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); } + @Nullable + private MediaPeriodInfo findMatchingMediaPeriodInQueue(Player player) { + Timeline playerTimeline = player.getCurrentTimeline(); + int playerPeriodIndex = player.getCurrentPeriodIndex(); + @Nullable + Object playerPeriodUid = + playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); + int playerNextAdGroupIndex = + player.isPlayingAd() || playerTimeline.isEmpty() + ? C.INDEX_UNSET + : playerTimeline + .getPeriod(playerPeriodIndex, period) + .getAdGroupIndexAfterPositionUs(C.msToUs(player.getCurrentPosition())); + for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodInfoQueue.get(i); + if (isMatchingMediaPeriod( + mediaPeriodInfo, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex)) { + return mediaPeriodInfo; + } + } + if (mediaPeriodInfoQueue.isEmpty() && playingMediaPeriod != null) { + if (isMatchingMediaPeriod( + playingMediaPeriod, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex)) { + return playingMediaPeriod; + } + } + return null; + } + + private boolean isMatchingPlayingMediaPeriod(Player player) { + if (playingMediaPeriod == null) { + return false; + } + Timeline playerTimeline = player.getCurrentTimeline(); + int playerPeriodIndex = player.getCurrentPeriodIndex(); + @Nullable + Object playerPeriodUid = + playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); + int playerNextAdGroupIndex = + player.isPlayingAd() || playerTimeline.isEmpty() + ? C.INDEX_UNSET + : playerTimeline + .getPeriod(playerPeriodIndex, period) + .getAdGroupIndexAfterPositionUs(C.msToUs(player.getCurrentPosition())); + return isMatchingMediaPeriod( + playingMediaPeriod, + playerTimeline, + player.getCurrentWindowIndex(), + playerPeriodUid, + player.isPlayingAd(), + player.getCurrentAdGroupIndex(), + player.getCurrentAdIndexInAdGroup(), + playerNextAdGroupIndex); + } + + private static boolean isMatchingMediaPeriod( + MediaPeriodInfo mediaPeriodInfo, + Timeline playerTimeline, + int playerWindowIndex, + @Nullable Object playerPeriodUid, + boolean isPlayingAd, + int playerAdGroupIndex, + int playerAdIndexInAdGroup, + int playerNextAdGroupIndex) { + if (mediaPeriodInfo.timeline.isEmpty() + || !mediaPeriodInfo.timeline.equals(playerTimeline) + || mediaPeriodInfo.windowIndex != playerWindowIndex + || !mediaPeriodInfo.mediaPeriodId.periodUid.equals(playerPeriodUid)) { + return false; + } + // Timeline period matches. Still need to check ad information. + return (isPlayingAd + && mediaPeriodInfo.mediaPeriodId.adGroupIndex == playerAdGroupIndex + && mediaPeriodInfo.mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup) + || (!isPlayingAd + && mediaPeriodInfo.mediaPeriodId.adGroupIndex == C.INDEX_UNSET + && mediaPeriodInfo.mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex); + } + private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( MediaPeriodInfo info, Timeline newTimeline) { int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8149e63f2c..36809c4ee7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -25,7 +25,6 @@ import android.content.Context; import android.content.Intent; import android.graphics.SurfaceTexture; import android.media.AudioManager; -import android.net.Uri; import android.os.Looper; import android.view.Surface; import androidx.annotation.Nullable; @@ -2969,10 +2968,7 @@ public final class ExoPlayerTest { @Test public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception { AdPlaybackState adPlaybackState = - FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs= */ 0) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, Uri.parse("https://ad1")) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, Uri.parse("https://ad2")) - .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, Uri.parse("https://ad3")); + FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0); Timeline fakeTimeline = new FakeTimeline( new TimelineWindowDefinition( @@ -4060,15 +4056,14 @@ public final class ExoPlayerTest { } }) .build(); - ExoPlayerTestRunner testRunner = - new ExoPlayerTestRunner.Builder() - .setMediaSources(concatenatingMediaSource) - .initialSeek(seekToWindowIndex, 5000) - .setActionSchedule(actionSchedule) - .build(context) - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); + new ExoPlayerTestRunner.Builder() + .setMediaSources(concatenatingMediaSource) + .initialSeek(seekToWindowIndex, 5000) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); assertArrayEquals(new long[] {5_000}, currentPlaybackPositions); assertArrayEquals(new int[] {seekToWindowIndex}, currentWindowIndices); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java index 547a0be459..ced210d727 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/AnalyticsCollectorTest.java @@ -23,6 +23,7 @@ import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; @@ -41,12 +42,14 @@ import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; 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.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -54,6 +57,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.LooperMode; @@ -133,7 +138,7 @@ public final class AnalyticsCollectorTest { .containsExactly( WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); listener.assertNoMoreEvents(); } @@ -154,7 +159,7 @@ public final class AnalyticsCollectorTest { period0 /* READY */, period0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0 /* started */, period0 /* stopped */); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0); @@ -201,7 +206,7 @@ public final class AnalyticsCollectorTest { period0 /* READY */, period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -257,7 +262,7 @@ public final class AnalyticsCollectorTest { period1 /* READY */, period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0, period0, period0, period0); @@ -320,7 +325,7 @@ public final class AnalyticsCollectorTest { period1 /* setPlayWhenReady=true */, period1 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1); @@ -395,7 +400,7 @@ public final class AnalyticsCollectorTest { period1Seq2 /* READY */, period1Seq2 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) - .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */); + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) .containsExactly(period0, period1Seq2); assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0); @@ -477,9 +482,9 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */, + WINDOW_0 /* SOURCE_UPDATE */, WINDOW_0 /* PLAYLIST_CHANGE */, - WINDOW_0 /* DYNAMIC */); + WINDOW_0 /* SOURCE_UPDATE */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1); assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) @@ -522,11 +527,13 @@ public final class AnalyticsCollectorTest { new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() .waitForPlaybackState(Player.STATE_READY) .throwPlaybackException(ExoPlaybackException.createForSource(new IOException())) .waitForPlaybackState(Player.STATE_IDLE) .seek(/* positionMs= */ 0) .prepare() + .play() .waitForPlaybackState(Player.STATE_ENDED) .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); @@ -535,17 +542,19 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) .containsExactly( WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, WINDOW_0 /* BUFFERING */, period0Seq0 /* READY */, - WINDOW_0 /* IDLE */, - WINDOW_0 /* BUFFERING */, + period0Seq0 /* IDLE */, + period0Seq0 /* BUFFERING */, + period0Seq0 /* setPlayWhenReady=true */, period0Seq0 /* READY */, period0Seq0 /* ENDED */); assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */); - assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(WINDOW_0); - assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(WINDOW_0); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0); @@ -573,8 +582,7 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); - assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) - .containsExactly(period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq0); assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) .containsExactly(period0Seq0, period0Seq0); assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) @@ -620,8 +628,8 @@ public final class AnalyticsCollectorTest { assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) .containsExactly( WINDOW_0 /* PLAYLIST_CHANGED */, - window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */, - period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */); + window0Period1Seq0 /* SOURCE_UPDATE (concatenated timeline replaces dummy) */, + period1Seq0 /* SOURCE_UPDATE (child sources in concatenating source moved) */); assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) .containsExactly( window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0); @@ -656,6 +664,411 @@ public final class AnalyticsCollectorTest { listener.assertNoMoreEvents(); } + @Test + public void testPlaylistOperations() throws Exception { + MediaSource fakeMediaSource = + new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .addMediaSources(fakeMediaSource) + // Wait until second period has fully loaded to assert loading events without flakiness. + .waitForIsLoading(true) + .waitForIsLoading(false) + .removeMediaItem(/* index= */ 0) + .play() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + // Populate event ids with second to last timeline that still contained both periods. + populateEventIds(listener.reportedTimelines.get(listener.reportedTimelines.size() - 2)); + // Expect the second period with window index 0 and increased window sequence after the removal + // moved the period to another window index. + period0Seq1 = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0), + /* windowSequenceNumber= */ 1)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + period0Seq0 /* READY */, + period0Seq1 /* BUFFERING */, + period0Seq1 /* setPlayWhenReady=true */, + period0Seq1 /* READY */, + period0Seq1 /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + WINDOW_0 /* SOURCE_UPDATE (first item) */, + period0Seq0 /* PLAYLIST_CHANGED (add) */, + period0Seq0 /* SOURCE_UPDATE (second item) */, + period0Seq1 /* PLAYLIST_CHANGED (remove) */); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly(WINDOW_0 /* manifest */, period0Seq0 /* media */, period1Seq1 /* media */); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly(WINDOW_0 /* manifest */, period0Seq0 /* media */, period1Seq1 /* media */); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(period0Seq0, period1Seq1); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_READING_STARTED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_INIT)).containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(period0Seq0); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(period0Seq1); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(period0Seq0, period0Seq1); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(period0Seq0, period0Seq1); + listener.assertNoMoreEvents(); + } + + @Test + public void testAdPlayback() throws Exception { + long contentDurationsUs = 10 * C.MICROS_PER_SECOND; + AtomicReference adPlaybackState = + new AtomicReference<>( + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ + 0, + 5 * C.MICROS_PER_SECOND, + C.TIME_END_OF_SOURCE) + .withContentDurationUs(contentDurationsUs)); + AtomicInteger playedAdCount = new AtomicInteger(0); + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + contentDurationsUs, + adPlaybackState.get())); + FakeMediaSource fakeMediaSource = + new FakeMediaSource(adTimeline, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.addListener( + new Player.EventListener() { + @Override + public void onPositionDiscontinuity( + @Player.DiscontinuityReason int reason) { + if (!player.isPlayingAd() + && reason == Player.DISCONTINUITY_REASON_AD_INSERTION) { + // Finished playing ad. Marked as played. + adPlaybackState.set( + adPlaybackState + .get() + .withPlayedAd( + playedAdCount.getAndIncrement(), + /* adIndexInAdGroup= */ 0)); + fakeMediaSource.setNewSourceInfo( + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs =*/ 10 * C.MICROS_PER_SECOND, + adPlaybackState.get())), + /* newManifest= */ null); + } + } + }); + } + }) + .pause() + .waitForPlaybackState(Player.STATE_READY) + // Wait in each content part to ensure previously triggered events get a chance to be + // delivered. This prevents flakiness caused by playback progressing too fast. + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 3_000) + .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 8_000) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + // Wait for final timeline change that marks post-roll played. + .waitForTimelineChanged() + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); + EventWindowAndPeriodId prerollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId midrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 1, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId postrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 2, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId contentAfterPreroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 1)); + EventWindowAndPeriodId contentAfterMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 2)); + EventWindowAndPeriodId contentAfterPostroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + prerollAd /* READY */, + prerollAd /* setPlayWhenReady=true */, + contentAfterPreroll /* setPlayWhenReady=false */, + contentAfterPreroll /* setPlayWhenReady=true */, + contentAfterMidroll /* setPlayWhenReady=false */, + contentAfterMidroll /* setPlayWhenReady=true */, + contentAfterPostroll /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly( + WINDOW_0 /* PLAYLIST_CHANGED */, + WINDOW_0 /* SOURCE_UPDATE (initial) */, + contentAfterPreroll /* SOURCE_UPDATE (played preroll) */, + contentAfterMidroll /* SOURCE_UPDATE (played midroll) */, + contentAfterPostroll /* SOURCE_UPDATE (played postroll) */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly( + contentAfterPreroll, midrollAd, contentAfterMidroll, postrollAd, contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly( + prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, prerollAd, + prerollAd, prerollAd, prerollAd, prerollAd); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* content manifest */, + WINDOW_0 /* preroll manifest */, + prerollAd, + contentAfterPreroll, + WINDOW_0 /* midroll manifest */, + midrollAd, + contentAfterMidroll, + WINDOW_0 /* postroll manifest */, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* content manifest */, + WINDOW_0 /* preroll manifest */, + prerollAd, + contentAfterPreroll, + WINDOW_0 /* midroll manifest */, + midrollAd, + contentAfterMidroll, + WINDOW_0 /* postroll manifest */, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly( + prerollAd, contentAfterPreroll, midrollAd, contentAfterMidroll, postrollAd); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)).containsExactly(prerollAd); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly( + prerollAd, + contentAfterPreroll, + midrollAd, + contentAfterMidroll, + postrollAd, + contentAfterPostroll); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)) + .containsExactly(contentAfterPreroll, contentAfterMidroll, contentAfterPostroll); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)).containsExactly(prerollAd); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)).containsExactly(prerollAd); + listener.assertNoMoreEvents(); + } + + @Test + public void testSeekAfterMidroll() throws Exception { + Timeline adTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + 10 * C.MICROS_PER_SECOND, + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 5 * C.MICROS_PER_SECOND))); + FakeMediaSource fakeMediaSource = + new FakeMediaSource(adTimeline, ExoPlayerTestRunner.Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("AnalyticsCollectorTest") + .pause() + // Ensure everything is preloaded. + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + .waitForIsLoading(true) + .waitForIsLoading(false) + // Seek behind the midroll. + .seek(6 * C.MICROS_PER_SECOND) + .play() + .waitForPlaybackState(Player.STATE_ENDED) + .build(); + TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule); + + Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0); + EventWindowAndPeriodId midrollAd = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 0)); + EventWindowAndPeriodId contentBeforeMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ 0)); + EventWindowAndPeriodId contentAfterMidroll = + new EventWindowAndPeriodId( + /* windowIndex= */ 0, + new MediaPeriodId( + periodUid, /* windowSequenceNumber= */ 0, /* nextAdGroupIndex= */ C.INDEX_UNSET)); + assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED)) + .containsExactly( + WINDOW_0 /* setPlayWhenReady=true */, + WINDOW_0 /* setPlayWhenReady=false */, + WINDOW_0 /* BUFFERING */, + contentBeforeMidroll /* READY */, + contentAfterMidroll /* setPlayWhenReady=true */, + midrollAd /* BUFFERING */, + midrollAd /* READY */, + contentAfterMidroll /* ENDED */); + assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)) + .containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */); + assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)) + .containsExactly( + contentAfterMidroll /* seek */, + midrollAd /* seek adjustment */, + contentAfterMidroll /* ad transition */); + assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(contentBeforeMidroll); + assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(midrollAd); + assertThat(listener.getEvents(EVENT_LOADING_CHANGED)) + .containsExactly( + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + contentBeforeMidroll, + midrollAd, + midrollAd); + assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_LOAD_STARTED)) + .containsExactly( + WINDOW_0 /* content manifest */, + contentBeforeMidroll, + midrollAd, + contentAfterMidroll, + contentAfterMidroll); + assertThat(listener.getEvents(EVENT_LOAD_COMPLETED)) + .containsExactly( + WINDOW_0 /* content manifest */, + contentBeforeMidroll, + midrollAd, + contentAfterMidroll, + contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DOWNSTREAM_FORMAT_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_CREATED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_MEDIA_PERIOD_RELEASED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_READING_STARTED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_ENABLED)) + .containsExactly(contentBeforeMidroll, midrollAd); + assertThat(listener.getEvents(EVENT_DECODER_INIT)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_FORMAT_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd, contentAfterMidroll); + assertThat(listener.getEvents(EVENT_DECODER_DISABLED)).containsExactly(contentBeforeMidroll); + assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES)).containsExactly(contentAfterMidroll); + assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED)) + .containsExactly(contentBeforeMidroll, midrollAd); + assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME)) + .containsExactly(contentBeforeMidroll, midrollAd); + listener.assertNoMoreEvents(); + } + @Test public void testNotifyExternalEvents() throws Exception { MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 8160dc3147..6b738ec075 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.net.Uri; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -160,11 +161,19 @@ public final class FakeTimeline extends Timeline { AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs); long[][] adDurationsUs = new long[adGroupCount][]; for (int i = 0; i < adGroupCount; i++) { - adPlaybackState = adPlaybackState.withAdCount(i, adsPerAdGroup); + adPlaybackState = adPlaybackState.withAdCount(/* adGroupIndex= */ i, adsPerAdGroup); + for (int j = 0; j < adsPerAdGroup; j++) { + adPlaybackState = + adPlaybackState.withAdUri( + /* adGroupIndex= */ i, + /* adIndexInAdGroup= */ j, + Uri.parse("https://ad/" + i + "/" + j)); + } adDurationsUs[i] = new long[adsPerAdGroup]; Arrays.fill(adDurationsUs[i], AD_DURATION_US); } adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); + return adPlaybackState; }