*** Original commit ***

Fix some logic in AnalyticsCollector.

All events issued from ExoPlayerImpl (i.e. Player.EventListener events) currently
try to use the media period data from the playing media period as set in the
playback thread queue. This is only correct as long as there no pending masking
operations in ExoPlayerImpl. That's why we currently disable this whenever the timeline
is empty or a seek is pending. Since adding all the playlist API methods to the player,
this is no longer the right choice. Moreover,...

***

PiperOrigin-RevId: 290312118
This commit is contained in:
bachinger 2020-01-17 19:45:58 +00:00 committed by Ian Baker
parent 4cf614c639
commit 72437e4442
4 changed files with 93 additions and 566 deletions

View File

@ -76,7 +76,6 @@ public class AnalyticsCollector
private final MediaPeriodQueueTracker mediaPeriodQueueTracker;
private @MonotonicNonNull Player player;
private boolean isSeeking;
/**
* Creates an analytics collector.
@ -127,9 +126,9 @@ public class AnalyticsCollector
* adjusts its state and position to the seek.
*/
public final void notifySeekStarted() {
if (!isSeeking) {
if (!mediaPeriodQueueTracker.isSeeking()) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
isSeeking = true;
mediaPeriodQueueTracker.onSeekStarted();
for (AnalyticsListener listener : listeners) {
listener.onSeekStarted(eventTime);
}
@ -310,8 +309,7 @@ public class AnalyticsCollector
@Override
public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
mediaPeriodQueueTracker.onMediaPeriodCreated(
windowIndex, mediaPeriodId, Assertions.checkNotNull(player));
mediaPeriodQueueTracker.onMediaPeriodCreated(windowIndex, mediaPeriodId);
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodCreated(eventTime);
@ -321,8 +319,7 @@ public class AnalyticsCollector
@Override
public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {
EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId);
if (mediaPeriodQueueTracker.onMediaPeriodReleased(
mediaPeriodId, Assertions.checkNotNull(player))) {
if (mediaPeriodQueueTracker.onMediaPeriodReleased(mediaPeriodId)) {
for (AnalyticsListener listener : listeners) {
listener.onMediaPeriodReleased(eventTime);
}
@ -414,7 +411,7 @@ public class AnalyticsCollector
@Override
public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player));
mediaPeriodQueueTracker.onTimelineChanged(timeline);
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onTimelineChanged(eventTime, reason);
@ -481,7 +478,7 @@ public class AnalyticsCollector
@Override
public final void onPlayerError(ExoPlaybackException error) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
EventTime eventTime = generatePlayingMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onPlayerError(eventTime, error);
}
@ -489,7 +486,6 @@ 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);
@ -506,8 +502,8 @@ public class AnalyticsCollector
@Override
public final void onSeekProcessed() {
if (isSeeking) {
isSeeking = false;
if (mediaPeriodQueueTracker.isSeeking()) {
mediaPeriodQueueTracker.onSeekProcessed();
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onSeekProcessed(eventTime);
@ -624,10 +620,13 @@ public class AnalyticsCollector
Assertions.checkNotNull(player);
if (mediaPeriodInfo == null) {
int windowIndex = player.getCurrentWindowIndex();
Timeline timeline = player.getCurrentTimeline();
boolean windowIsInTimeline = windowIndex < timeline.getWindowCount();
return generateEventTime(
windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
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);
}
}
return generateEventTime(
mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId);
@ -674,10 +673,10 @@ public class AnalyticsCollector
private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo;
private final Period period;
@Nullable private MediaPeriodInfo currentPlayerMediaPeriod;
private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod;
@Nullable private MediaPeriodInfo playingMediaPeriod;
@Nullable private MediaPeriodInfo readingMediaPeriod;
private Timeline timeline;
private boolean isSeeking;
public MediaPeriodQueueTracker() {
mediaPeriodInfoQueue = new ArrayList<>();
@ -690,11 +689,14 @@ public class AnalyticsCollector
* Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of
* the player.
*
* <p>May be null if no matching media period has been created yet.
* <p>May be null if no matching media period has been created yet or the player is currently
* masking its state.
*/
@Nullable
public MediaPeriodInfo getCurrentPlayerMediaPeriod() {
return currentPlayerMediaPeriod;
return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking
? null
: mediaPeriodInfoQueue.get(0);
}
/**
@ -737,13 +739,35 @@ public class AnalyticsCollector
return mediaPeriodIdToInfo.get(mediaPeriodId);
}
/** Updates the queue with a reported position discontinuity. */
public void onPositionDiscontinuity(Player player) {
currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player);
/** 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 timeline change. */
public void onTimelineChanged(Timeline timeline, Player player) {
public void onTimelineChanged(Timeline timeline) {
for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) {
MediaPeriodInfo newMediaPeriodInfo =
updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline);
@ -757,11 +781,20 @@ public class AnalyticsCollector
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
this.timeline = timeline;
currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player);
}
/** 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;
}
/** Updates the queue with a newly created media period. */
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) {
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
boolean isInTimeline = periodIndex != C.INDEX_UNSET;
MediaPeriodInfo mediaPeriodInfo =
@ -772,17 +805,14 @@ 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, Player player) {
@Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
if (mediaPeriodInfo == null) {
// The media period has already been removed from the queue in resetForNewPlaylist().
return false;
@ -794,9 +824,6 @@ public class AnalyticsCollector
if (!mediaPeriodInfoQueue.isEmpty()) {
playingMediaPeriod = mediaPeriodInfoQueue.get(0);
}
if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) {
currentPlayerMediaPeriod = playingMediaPeriod;
}
return true;
}
@ -805,99 +832,6 @@ 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);

View File

@ -25,6 +25,7 @@ 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;
@ -2968,7 +2969,10 @@ public final class ExoPlayerTest {
@Test
public void contentWithInitialSeekPositionAfterPrerollAdStartsAtSeekPosition() throws Exception {
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 3, /* adGroupTimesUs...= */ 0);
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"));
Timeline fakeTimeline =
new FakeTimeline(
new TimelineWindowDefinition(
@ -4056,14 +4060,15 @@ public final class ExoPlayerTest {
}
})
.build();
new ExoPlayerTestRunner.Builder()
.setMediaSources(concatenatingMediaSource)
.initialSeek(seekToWindowIndex, 5000)
.setActionSchedule(actionSchedule)
.build(context)
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
ExoPlayerTestRunner testRunner =
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);
}

View File

@ -23,7 +23,6 @@ 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;
@ -42,14 +41,12 @@ 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;
@ -57,8 +54,6 @@ 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;
@ -138,7 +133,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 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
listener.assertNoMoreEvents();
}
@ -159,7 +154,7 @@ public final class AnalyticsCollectorTest {
period0 /* READY */,
period0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0 /* started */, period0 /* stopped */);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);
@ -206,7 +201,7 @@ public final class AnalyticsCollectorTest {
period0 /* READY */,
period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0);
@ -262,7 +257,7 @@ public final class AnalyticsCollectorTest {
period1 /* READY */,
period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0, period0, period0, period0);
@ -325,7 +320,7 @@ public final class AnalyticsCollectorTest {
period1 /* setPlayWhenReady=true */,
period1 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);
@ -400,7 +395,7 @@ public final class AnalyticsCollectorTest {
period1Seq2 /* READY */,
period1Seq2 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */);
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(period0, period1Seq2);
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
@ -482,9 +477,9 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* SOURCE_UPDATE */,
WINDOW_0 /* DYNAMIC */,
WINDOW_0 /* PLAYLIST_CHANGE */,
WINDOW_0 /* SOURCE_UPDATE */);
WINDOW_0 /* DYNAMIC */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
@ -527,13 +522,11 @@ 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);
@ -542,19 +535,17 @@ 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 */,
period0Seq0 /* IDLE */,
period0Seq0 /* BUFFERING */,
period0Seq0 /* setPlayWhenReady=true */,
WINDOW_0 /* IDLE */,
WINDOW_0 /* BUFFERING */,
period0Seq0 /* READY */,
period0Seq0 /* ENDED */);
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* prepared */);
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_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_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period0Seq0);
@ -582,7 +573,8 @@ 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);
assertThat(listener.getEvents(EVENT_DROPPED_VIDEO_FRAMES))
.containsExactly(period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_VIDEO_SIZE_CHANGED))
.containsExactly(period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_RENDERED_FIRST_FRAME))
@ -628,8 +620,8 @@ public final class AnalyticsCollectorTest {
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
.containsExactly(
WINDOW_0 /* PLAYLIST_CHANGED */,
window0Period1Seq0 /* SOURCE_UPDATE (concatenated timeline replaces dummy) */,
period1Seq0 /* SOURCE_UPDATE (child sources in concatenating source moved) */);
window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */,
period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */);
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
.containsExactly(
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
@ -664,401 +656,6 @@ 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> 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)
.play()
.waitForPlaybackState(Player.STATE_ENDED)
.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 */,
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(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);

View File

@ -15,7 +15,6 @@
*/
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;
@ -161,19 +160,11 @@ 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(/* adGroupIndex= */ i, adsPerAdGroup);
for (int j = 0; j < adsPerAdGroup; j++) {
adPlaybackState =
adPlaybackState.withAdUri(
/* adGroupIndex= */ i,
/* adIndexInAdGroup= */ j,
Uri.parse("https://ad/" + i + "/" + j));
}
adPlaybackState = adPlaybackState.withAdCount(i, adsPerAdGroup);
adDurationsUs[i] = new long[adsPerAdGroup];
Arrays.fill(adDurationsUs[i], AD_DURATION_US);
}
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
return adPlaybackState;
}