Remove flakiness from DefaultAnalyticsCollectorTest
Our FakeClock generally makes sure that playback tests are fully deterministic. However, this fails if the test uses blocking waits with clock.onThreadBlocked and where relevant Handlers are created without using the clock. To fix the flakiness, we can make the following adjustments: - Use TestExoPlayerBuilder instead of legacy ExoPlayerTestRunner to avoid onThreadBlocked calls. This also makes the tests more readable. - Use clock to create Handler for FakeVideoRenderer and FakeAudioRenderer. Ideally, this should be passed through RenderersFactory, but it's too disruptive given this is a public API. - Use clock for MediaSourceList and MediaPeriodQueue update handler. PiperOrigin-RevId: 490907495
This commit is contained in:
parent
fed53362c9
commit
6abc94a8b7
@ -281,7 +281,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
deliverPendingMessageAtStartPositionRequired = true;
|
||||
|
||||
Handler eventHandler = new Handler(applicationLooper);
|
||||
HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
|
||||
queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
|
||||
mediaSourceList =
|
||||
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
|
||||
|
@ -26,6 +26,7 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Player.RepeatMode;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
|
||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||
@ -71,7 +72,7 @@ import com.google.common.collect.ImmutableList;
|
||||
private final Timeline.Period period;
|
||||
private final Timeline.Window window;
|
||||
private final AnalyticsCollector analyticsCollector;
|
||||
private final Handler analyticsCollectorHandler;
|
||||
private final HandlerWrapper analyticsCollectorHandler;
|
||||
|
||||
private long nextWindowSequenceNumber;
|
||||
private @RepeatMode int repeatMode;
|
||||
@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
|
||||
* on.
|
||||
*/
|
||||
public MediaPeriodQueue(
|
||||
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
|
||||
AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
|
||||
this.analyticsCollector = analyticsCollector;
|
||||
this.analyticsCollectorHandler = analyticsCollectorHandler;
|
||||
period = new Timeline.Period();
|
||||
|
@ -15,13 +15,16 @@
|
||||
*/
|
||||
package androidx.media3.exoplayer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
@ -48,6 +51,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
|
||||
/**
|
||||
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
||||
@ -77,11 +81,10 @@ import java.util.Set;
|
||||
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
|
||||
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
|
||||
private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
|
||||
private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
|
||||
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
|
||||
private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
|
||||
private final Set<MediaSourceHolder> enabledMediaSourceHolders;
|
||||
|
||||
private final AnalyticsCollector eventListener;
|
||||
private final HandlerWrapper eventHandler;
|
||||
private ShuffleOrder shuffleOrder;
|
||||
private boolean isPrepared;
|
||||
|
||||
@ -101,7 +104,7 @@ import java.util.Set;
|
||||
public MediaSourceList(
|
||||
MediaSourceListInfoRefreshListener listener,
|
||||
AnalyticsCollector analyticsCollector,
|
||||
Handler analyticsCollectorHandler,
|
||||
HandlerWrapper analyticsCollectorHandler,
|
||||
PlayerId playerId) {
|
||||
this.playerId = playerId;
|
||||
mediaSourceListInfoListener = listener;
|
||||
@ -109,12 +112,10 @@ import java.util.Set;
|
||||
mediaSourceByMediaPeriod = new IdentityHashMap<>();
|
||||
mediaSourceByUid = new HashMap<>();
|
||||
mediaSourceHolders = new ArrayList<>();
|
||||
mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher();
|
||||
drmEventDispatcher = new DrmSessionEventListener.EventDispatcher();
|
||||
eventListener = analyticsCollector;
|
||||
eventHandler = analyticsCollectorHandler;
|
||||
childSources = new HashMap<>();
|
||||
enabledMediaSourceHolders = new HashSet<>();
|
||||
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
|
||||
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -308,7 +309,7 @@ import java.util.Set;
|
||||
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||
MediaSource.MediaPeriodId childMediaPeriodId =
|
||||
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
|
||||
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
|
||||
MediaSourceHolder holder = checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
|
||||
enableMediaSource(holder);
|
||||
holder.activeMediaPeriodIds.add(childMediaPeriodId);
|
||||
MediaPeriod mediaPeriod =
|
||||
@ -324,8 +325,7 @@ import java.util.Set;
|
||||
* @param mediaPeriod The period to release.
|
||||
*/
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
MediaSourceHolder holder =
|
||||
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
|
||||
MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
|
||||
holder.mediaSource.releasePeriod(mediaPeriod);
|
||||
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
|
||||
if (!mediaSourceByMediaPeriod.isEmpty()) {
|
||||
@ -450,8 +450,7 @@ import java.util.Set;
|
||||
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
|
||||
// Release if the source has been removed from the playlist and no periods are still active.
|
||||
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
|
||||
MediaSourceAndListener removedChild =
|
||||
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
|
||||
MediaSourceAndListener removedChild = checkNotNull(childSources.remove(mediaSourceHolder));
|
||||
removedChild.mediaSource.releaseSource(removedChild.caller);
|
||||
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
|
||||
removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener);
|
||||
@ -526,12 +525,8 @@ import java.util.Set;
|
||||
implements MediaSourceEventListener, DrmSessionEventListener {
|
||||
|
||||
private final MediaSourceList.MediaSourceHolder id;
|
||||
private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
|
||||
private DrmSessionEventListener.EventDispatcher drmEventDispatcher;
|
||||
|
||||
public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
|
||||
mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher;
|
||||
drmEventDispatcher = MediaSourceList.this.drmEventDispatcher;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@ -543,8 +538,14 @@ import java.util.Set;
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onLoadStarted(
|
||||
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,8 +555,14 @@ import java.util.Set;
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onLoadCompleted(
|
||||
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
|
||||
}
|
||||
}
|
||||
|
||||
@ -565,8 +572,14 @@ import java.util.Set;
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
LoadEventInfo loadEventData,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onLoadCanceled(
|
||||
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
|
||||
}
|
||||
}
|
||||
|
||||
@ -578,8 +591,19 @@ import java.util.Set;
|
||||
MediaLoadData mediaLoadData,
|
||||
IOException error,
|
||||
boolean wasCanceled) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onLoadError(
|
||||
eventParameters.first,
|
||||
eventParameters.second,
|
||||
loadEventData,
|
||||
mediaLoadData,
|
||||
error,
|
||||
wasCanceled));
|
||||
}
|
||||
}
|
||||
|
||||
@ -588,8 +612,14 @@ import java.util.Set;
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onUpstreamDiscarded(
|
||||
eventParameters.first, checkNotNull(eventParameters.second), mediaLoadData));
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,8 +628,14 @@ import java.util.Set;
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
MediaLoadData mediaLoadData) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onDownstreamFormatChanged(
|
||||
eventParameters.first, eventParameters.second, mediaLoadData));
|
||||
}
|
||||
}
|
||||
|
||||
@ -610,75 +646,94 @@ import java.util.Set;
|
||||
int windowIndex,
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||
@DrmSession.State int state) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmSessionAcquired(state);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onDrmSessionAcquired(
|
||||
eventParameters.first, eventParameters.second, state));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmKeysLoaded();
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() -> eventListener.onDrmKeysLoaded(eventParameters.first, eventParameters.second));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmSessionManagerError(error);
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onDrmSessionManagerError(
|
||||
eventParameters.first, eventParameters.second, error));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRestored(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmKeysRestored();
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() -> eventListener.onDrmKeysRestored(eventParameters.first, eventParameters.second));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRemoved(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmKeysRemoved();
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() -> eventListener.onDrmKeysRemoved(eventParameters.first, eventParameters.second));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionReleased(
|
||||
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
|
||||
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||
drmEventDispatcher.drmSessionReleased();
|
||||
@Nullable
|
||||
Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
|
||||
getEventParameters(windowIndex, mediaPeriodId);
|
||||
if (eventParameters != null) {
|
||||
eventHandler.post(
|
||||
() ->
|
||||
eventListener.onDrmSessionReleased(eventParameters.first, eventParameters.second));
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the event dispatcher and returns whether the event should be dispatched. */
|
||||
private boolean maybeUpdateEventDispatcher(
|
||||
/** Updates the event parameters and returns whether the event should be dispatched. */
|
||||
@Nullable
|
||||
private Pair<Integer, MediaSource.@NullableType MediaPeriodId> getEventParameters(
|
||||
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
|
||||
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
|
||||
if (childMediaPeriodId != null) {
|
||||
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
|
||||
if (mediaPeriodId == null) {
|
||||
// Media period not found. Ignore event.
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
|
||||
if (mediaSourceEventDispatcher.windowIndex != windowIndex
|
||||
|| !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) {
|
||||
mediaSourceEventDispatcher =
|
||||
MediaSourceList.this.mediaSourceEventDispatcher.withParameters(
|
||||
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
|
||||
}
|
||||
if (drmEventDispatcher.windowIndex != windowIndex
|
||||
|| !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) {
|
||||
drmEventDispatcher =
|
||||
MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId);
|
||||
}
|
||||
return true;
|
||||
return Pair.create(windowIndex, mediaPeriodId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ import androidx.media3.common.TrackGroup;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.SystemClock;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
||||
@ -11897,7 +11898,11 @@ public final class ExoPlayerTest {
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setRenderersFactory(
|
||||
(handler, videoListener, audioListener, textOutput, metadataOutput) -> {
|
||||
videoRenderer.set(new FakeVideoRenderer(handler, videoListener));
|
||||
videoRenderer.set(
|
||||
new FakeVideoRenderer(
|
||||
SystemClock.DEFAULT.createHandler(
|
||||
handler.getLooper(), /* callback= */ null),
|
||||
videoListener));
|
||||
return new Renderer[] {videoRenderer.get()};
|
||||
})
|
||||
.build();
|
||||
@ -12034,7 +12039,12 @@ public final class ExoPlayerTest {
|
||||
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
|
||||
.setRenderersFactory(
|
||||
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
|
||||
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
|
||||
new Renderer[] {
|
||||
new FakeVideoRenderer(
|
||||
SystemClock.DEFAULT.createHandler(
|
||||
handler.getLooper(), /* callback= */ null),
|
||||
videoListener)
|
||||
})
|
||||
.build();
|
||||
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||
player.addAnalyticsListener(listener);
|
||||
@ -12059,7 +12069,12 @@ public final class ExoPlayerTest {
|
||||
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
|
||||
.setRenderersFactory(
|
||||
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
|
||||
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
|
||||
new Renderer[] {
|
||||
new FakeVideoRenderer(
|
||||
SystemClock.DEFAULT.createHandler(
|
||||
handler.getLooper(), /* callback= */ null),
|
||||
videoListener)
|
||||
})
|
||||
.build();
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
player.addListener(listener);
|
||||
|
@ -25,7 +25,6 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.AdPlaybackState;
|
||||
@ -36,6 +35,7 @@ import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
|
||||
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
|
||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||
@ -97,13 +97,14 @@ public final class MediaPeriodQueueTest {
|
||||
analyticsCollector.setPlayer(
|
||||
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(),
|
||||
Looper.getMainLooper());
|
||||
mediaPeriodQueue =
|
||||
new MediaPeriodQueue(analyticsCollector, new Handler(Looper.getMainLooper()));
|
||||
HandlerWrapper handler =
|
||||
Clock.DEFAULT.createHandler(Looper.getMainLooper(), /* callback= */ null);
|
||||
mediaPeriodQueue = new MediaPeriodQueue(analyticsCollector, handler);
|
||||
mediaSourceList =
|
||||
new MediaSourceList(
|
||||
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
|
||||
analyticsCollector,
|
||||
new Handler(Looper.getMainLooper()),
|
||||
handler,
|
||||
PlayerId.UNSET);
|
||||
rendererCapabilities = new RendererCapabilities[0];
|
||||
trackSelector = mock(TrackSelector.class);
|
||||
|
@ -67,7 +67,7 @@ public class MediaSourceListTest {
|
||||
new MediaSourceList(
|
||||
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
|
||||
analyticsCollector,
|
||||
Util.createHandlerForCurrentOrMainLooper(),
|
||||
Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null),
|
||||
PlayerId.UNSET);
|
||||
}
|
||||
|
||||
|
@ -49,6 +49,12 @@ import static androidx.media3.exoplayer.analytics.AnalyticsListener.EVENT_VIDEO_
|
||||
import static androidx.media3.exoplayer.analytics.AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED;
|
||||
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
||||
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.playUntilPosition;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilError;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilIsLoading;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
||||
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilTimelineChanged;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
@ -63,6 +69,8 @@ import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.robolectric.shadows.ShadowLooper.idleMainLooper;
|
||||
import static org.robolectric.shadows.ShadowLooper.runMainLooperToNextTask;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.os.Looper;
|
||||
@ -85,6 +93,7 @@ import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.exoplayer.DecoderCounters;
|
||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
@ -102,8 +111,6 @@ import androidx.media3.exoplayer.source.LoadEventInfo;
|
||||
import androidx.media3.exoplayer.source.MediaLoadData;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||
import androidx.media3.test.utils.ActionSchedule;
|
||||
import androidx.media3.test.utils.ActionSchedule.PlayerRunnable;
|
||||
import androidx.media3.test.utils.ExoPlayerTestRunner;
|
||||
import androidx.media3.test.utils.FakeAudioRenderer;
|
||||
import androidx.media3.test.utils.FakeClock;
|
||||
@ -132,14 +139,11 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Integration test for {@link DefaultAnalyticsCollector}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class DefaultAnalyticsCollectorTest {
|
||||
|
||||
private static final String TAG = "DefaultAnalyticsCollectorTest";
|
||||
|
||||
// Deprecated event constants.
|
||||
private static final long EVENT_PLAYER_STATE_CHANGED = 1L << 63;
|
||||
private static final long EVENT_SEEK_STARTED = 1L << 62;
|
||||
@ -167,7 +171,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
private static final Format VIDEO_FORMAT_DRM_1 =
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT.buildUpon().setDrmInitData(DRM_DATA_1).build();
|
||||
|
||||
private static final int TIMEOUT_MS = 10_000;
|
||||
private static final Timeline SINGLE_PERIOD_TIMELINE = new FakeTimeline();
|
||||
private static final EventWindowAndPeriodId WINDOW_0 =
|
||||
new EventWindowAndPeriodId(/* windowIndex= */ 0, /* mediaPeriodId= */ null);
|
||||
@ -217,7 +220,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
FakeMediaSource mediaSource =
|
||||
new FakeMediaSource(
|
||||
Timeline.EMPTY, ExoPlayerTestRunner.VIDEO_FORMAT, ExoPlayerTestRunner.AUDIO_FORMAT);
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
@ -236,7 +246,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
SINGLE_PERIOD_TIMELINE,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT,
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT);
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
@ -247,7 +264,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
period0 /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* SOURCE_UPDATE */)
|
||||
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
|
||||
.containsExactly(period0 /* started */, period0 /* stopped */)
|
||||
@ -297,7 +314,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
SINGLE_PERIOD_TIMELINE,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT,
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
@ -378,7 +402,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
new ConcatenatingMediaSource(
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT),
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
@ -449,23 +480,23 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT,
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT),
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
// Wait until second period has fully loaded to assert loading events without flakiness.
|
||||
.waitForIsLoading(true)
|
||||
.waitForIsLoading(false)
|
||||
.seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
// Wait until second period has fully loaded to assert loading events.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* setPlayWhenReady=true */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
WINDOW_0 /* setPlayWhenReady=false */,
|
||||
period0 /* READY */,
|
||||
period1 /* BUFFERING */,
|
||||
period1 /* setPlayWhenReady=true */,
|
||||
@ -542,23 +573,24 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
SINGLE_PERIOD_TIMELINE,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT,
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
long periodDurationMs =
|
||||
long windowDurationMs =
|
||||
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.playUntilPosition(/* mediaItemIndex= */ 0, periodDurationMs)
|
||||
.seekAndWait(/* positionMs= */ 0)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, windowDurationMs - 100);
|
||||
player.seekTo(/* positionMs= */ 0);
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* setPlayWhenReady=true */,
|
||||
WINDOW_0 /* setPlayWhenReady=false */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
period0 /* READY */,
|
||||
period0 /* setPlayWhenReady=true */,
|
||||
@ -653,17 +685,19 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
MediaSource mediaSource2 =
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.setMediaSources(/* resetPosition= */ false, mediaSource2)
|
||||
.waitForTimelineChanged()
|
||||
// Wait until loading started to prevent flakiness caused by loading finishing too fast.
|
||||
.waitForIsLoading(true)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(mediaSource1);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.setMediaSource(mediaSource2, /* resetPosition= */ false);
|
||||
runUntilTimelineChanged(player);
|
||||
// Wait until loading started to assert loading events.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
// Populate all event ids with last timeline (after second prepare).
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
@ -676,9 +710,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
/* windowSequenceNumber= */ 0));
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* setPlayWhenReady=true */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
WINDOW_0 /* setPlayWhenReady=false */,
|
||||
period0Seq0 /* READY */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
period0Seq1 /* setPlayWhenReady=true */,
|
||||
@ -688,9 +720,9 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* PLAYLIST_CHANGE */,
|
||||
period0Seq0 /* SOURCE_UPDATE */,
|
||||
WINDOW_0 /* SOURCE_UPDATE */,
|
||||
WINDOW_0 /* PLAYLIST_CHANGE */,
|
||||
period0Seq1 /* SOURCE_UPDATE */);
|
||||
WINDOW_0 /* SOURCE_UPDATE */);
|
||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
|
||||
.containsExactly(WINDOW_0 /* REMOVE */);
|
||||
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
|
||||
@ -753,28 +785,31 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
public void reprepareAfterError() throws Exception {
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.throwPlaybackException(
|
||||
ExoPlaybackException.createForSource(
|
||||
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED))
|
||||
.waitForPlaybackState(Player.STATE_IDLE)
|
||||
.seek(/* positionMs= */ 0)
|
||||
.prepare()
|
||||
// Wait until loading started to assert loading events without flakiness.
|
||||
.waitForIsLoading(true)
|
||||
.play()
|
||||
.waitForPlaybackState(Player.STATE_ENDED)
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player
|
||||
.createMessage(
|
||||
(message, payload) -> {
|
||||
throw ExoPlaybackException.createForSource(
|
||||
new IOException(), PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
})
|
||||
.send();
|
||||
runUntilError(player);
|
||||
player.seekTo(/* positionMs= */ 0);
|
||||
player.prepare();
|
||||
// Wait until loading started to assert loading events.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* setPlayWhenReady=true */,
|
||||
WINDOW_0 /* setPlayWhenReady=false */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
period0Seq0 /* READY */,
|
||||
period0Seq0 /* IDLE */,
|
||||
@ -784,7 +819,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
period0Seq0 /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(WINDOW_0 /* prepared */, period0Seq0 /* prepared */);
|
||||
.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);
|
||||
@ -835,36 +870,33 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
final ConcatenatingMediaSource concatenatedMediaSource =
|
||||
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
|
||||
long periodDurationMs =
|
||||
long windowDurationMs =
|
||||
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
// Ensure second period is already being read from.
|
||||
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ periodDurationMs)
|
||||
.executeRunnable(
|
||||
() ->
|
||||
concatenatedMediaSource.moveMediaSource(
|
||||
/* currentIndex= */ 0, /* newIndex= */ 1))
|
||||
.waitForTimelineChanged()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(concatenatedMediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(concatenatedMediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
// Ensure second period is already being read from.
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ windowDurationMs - 100);
|
||||
concatenatedMediaSource.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 1);
|
||||
runUntilTimelineChanged(player);
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* setPlayWhenReady=true */,
|
||||
WINDOW_0 /* setPlayWhenReady=false */,
|
||||
WINDOW_0 /* BUFFERING */,
|
||||
window0Period1Seq0 /* READY */,
|
||||
window0Period1Seq0 /* setPlayWhenReady=true */,
|
||||
window0Period1Seq0 /* setPlayWhenReady=false */,
|
||||
period1Seq0 /* setPlayWhenReady=true */,
|
||||
period1Seq0 /* BUFFERING */,
|
||||
period1Seq0 /* READY */,
|
||||
period1Seq0 /* setPlayWhenReady=true */,
|
||||
period1Seq0 /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
@ -926,20 +958,22 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
public void playlistOperations() throws Exception {
|
||||
MediaSource fakeMediaSource =
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.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)
|
||||
.waitForPlaybackState(Player.STATE_BUFFERING)
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(fakeMediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.addMediaSource(fakeMediaSource);
|
||||
// Wait until second period has fully loaded to assert loading events.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
player.removeMediaItem(/* index= */ 0);
|
||||
runUntilPlaybackState(player, Player.STATE_BUFFERING);
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
// Populate event ids with second to last timeline that still contained both periods.
|
||||
populateEventIds(listener.reportedTimelines.get(listener.reportedTimelines.size() - 2));
|
||||
@ -953,8 +987,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
/* 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 */,
|
||||
@ -965,7 +997,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* PLAYLIST_CHANGED */,
|
||||
period0Seq0 /* SOURCE_UPDATE (first item) */,
|
||||
WINDOW_0 /* SOURCE_UPDATE (first item) */,
|
||||
period0Seq0 /* PLAYLIST_CHANGED (add) */,
|
||||
period0Seq0 /* SOURCE_UPDATE (second item) */,
|
||||
period0Seq1 /* PLAYLIST_CHANGED (remove) */)
|
||||
@ -1063,60 +1095,53 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
}
|
||||
},
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.executeRunnable(
|
||||
new PlayerRunnable() {
|
||||
@Override
|
||||
public void run(ExoPlayer player) {
|
||||
player.addListener(
|
||||
new Player.Listener() {
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
if (!player.isPlayingAd()
|
||||
&& reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
// Finished playing ad. Marked as played.
|
||||
adPlaybackState.set(
|
||||
adPlaybackState
|
||||
.get()
|
||||
.withPlayedAd(
|
||||
/* adGroupIndex= */ playedAdCount.getAndIncrement(),
|
||||
/* adIndexInAdGroup= */ 0));
|
||||
fakeMediaSource.setNewSourceInfo(
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
contentDurationsUs,
|
||||
adPlaybackState.get())),
|
||||
/* sendManifestLoadEvents= */ false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.pause()
|
||||
// Ensure everything is preloaded.
|
||||
.waitForIsLoading(true)
|
||||
.waitForIsLoading(false)
|
||||
.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(/* mediaItemIndex= */ 0, /* positionMs= */ 3_000)
|
||||
.waitForPendingPlayerCommands()
|
||||
.playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8_000)
|
||||
.waitForPendingPlayerCommands()
|
||||
.play()
|
||||
.waitForPlaybackState(Player.STATE_ENDED)
|
||||
// Wait for final timeline change that marks post-roll played.
|
||||
.waitForTimelineChanged()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
player.addListener(
|
||||
new Player.Listener() {
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
Player.PositionInfo oldPosition,
|
||||
Player.PositionInfo newPosition,
|
||||
@Player.DiscontinuityReason int reason) {
|
||||
if (!player.isPlayingAd() && reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
// Finished playing ad. Marked as played.
|
||||
adPlaybackState.set(
|
||||
adPlaybackState
|
||||
.get()
|
||||
.withPlayedAd(
|
||||
/* adGroupIndex= */ playedAdCount.getAndIncrement(),
|
||||
/* adIndexInAdGroup= */ 0));
|
||||
fakeMediaSource.setNewSourceInfo(
|
||||
new FakeTimeline(
|
||||
new TimelineWindowDefinition(
|
||||
/* periodCount= */ 1,
|
||||
/* id= */ 0,
|
||||
/* isSeekable= */ true,
|
||||
/* isDynamic= */ false,
|
||||
contentDurationsUs,
|
||||
adPlaybackState.get())),
|
||||
/* sendManifestLoadEvents= */ false);
|
||||
}
|
||||
}
|
||||
});
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(fakeMediaSource);
|
||||
player.prepare();
|
||||
// Ensure everything is preloaded.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
// Wait in each content part to ensure previously triggered events get a chance to be delivered.
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 3_000);
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 8_000);
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
// Wait for final timeline change that marks post-roll played.
|
||||
runUntilTimelineChanged(player);
|
||||
|
||||
Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0);
|
||||
EventWindowAndPeriodId prerollAd =
|
||||
@ -1158,8 +1183,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
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 */,
|
||||
@ -1172,7 +1195,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(
|
||||
WINDOW_0 /* PLAYLIST_CHANGED */,
|
||||
prerollAd /* SOURCE_UPDATE (initial) */,
|
||||
WINDOW_0 /* SOURCE_UPDATE (initial) */,
|
||||
contentAfterPreroll /* SOURCE_UPDATE (played preroll) */,
|
||||
contentAfterMidroll /* SOURCE_UPDATE (played midroll) */,
|
||||
contentAfterPostroll /* SOURCE_UPDATE (played postroll) */)
|
||||
@ -1322,20 +1345,21 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
}
|
||||
},
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
// Ensure everything is preloaded.
|
||||
.waitForIsLoading(true)
|
||||
.waitForIsLoading(false)
|
||||
// Seek behind the midroll.
|
||||
.seek(6 * C.MICROS_PER_SECOND)
|
||||
// Wait until loading started again to assert loading events without flakiness.
|
||||
.waitForIsLoading(true)
|
||||
.play()
|
||||
.waitForPlaybackState(Player.STATE_ENDED)
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(fakeMediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(fakeMediaSource);
|
||||
player.prepare();
|
||||
// Ensure everything is preloaded.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
// Seek behind the midroll.
|
||||
player.seekTo(/* positionMs= */ 6_000);
|
||||
// Wait until loading started again to assert loading events.
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
Object periodUid = listener.lastReportedTimeline.getUidOfPeriod(/* periodIndex= */ 0);
|
||||
EventWindowAndPeriodId midrollAd =
|
||||
@ -1357,8 +1381,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
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 /* BUFFERING */,
|
||||
@ -1367,7 +1389,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
contentAfterMidroll /* ENDED */)
|
||||
.inOrder();
|
||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, contentBeforeMidroll /* SOURCE_UPDATE */);
|
||||
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* SOURCE_UPDATE */);
|
||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
|
||||
.containsExactly(
|
||||
contentAfterMidroll /* seek */,
|
||||
@ -1435,21 +1457,17 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
@Test
|
||||
public void notifyExternalEvents() throws Exception {
|
||||
MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE);
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.waitForPlaybackState(Player.STATE_READY)
|
||||
.executeRunnable(
|
||||
new PlayerRunnable() {
|
||||
@Override
|
||||
public void run(ExoPlayer player) {
|
||||
player.getAnalyticsCollector().notifySeekStarted();
|
||||
}
|
||||
})
|
||||
.seek(/* positionMs= */ 0)
|
||||
.play()
|
||||
.build();
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.getAnalyticsCollector().notifySeekStarted();
|
||||
player.seekTo(/* positionMs= */ 0);
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
||||
@ -1460,7 +1478,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
public void drmEvents_singlePeriod() throws Exception {
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, drmSessionManager, VIDEO_FORMAT_DRM_1);
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
|
||||
@ -1488,18 +1513,21 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1),
|
||||
new FakeMediaSource(
|
||||
SINGLE_PERIOD_TIMELINE, blockingDrmSessionManager, VIDEO_FORMAT_DRM_1));
|
||||
TestAnalyticsListener listener =
|
||||
runAnalyticsTest(
|
||||
mediaSource,
|
||||
// Wait for the media to be fully buffered before unblocking the DRM key request. This
|
||||
// ensures both periods report the same load event (because period1's DRM session is
|
||||
// already preacquired by the time the key load completes).
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.waitForIsLoading(false)
|
||||
.waitForIsLoading(true)
|
||||
.waitForIsLoading(false)
|
||||
.executeRunnable(mediaDrmCallback.keyCondition::open)
|
||||
.build());
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
// Wait for the media to be fully buffered before unblocking the DRM key request. This
|
||||
// ensures both periods report the same load event (because period1's DRM session is
|
||||
// already preacquired by the time the key load completes).
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ true);
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
mediaDrmCallback.keyCondition.open();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
|
||||
@ -1525,7 +1553,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
SINGLE_PERIOD_TIMELINE,
|
||||
drmSessionManager,
|
||||
VIDEO_FORMAT_DRM_1.buildUpon().setDrmInitData(DRM_DATA_2).build()));
|
||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).isEmpty();
|
||||
@ -1552,13 +1587,16 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
.build(mediaDrmCallback);
|
||||
MediaSource mediaSource =
|
||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, failingDrmSessionManager, VIDEO_FORMAT_DRM_1);
|
||||
TestAnalyticsListener listener =
|
||||
runAnalyticsTest(
|
||||
mediaSource,
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.waitForIsLoading(false)
|
||||
.executeRunnable(mediaDrmCallback.keyCondition::open)
|
||||
.build());
|
||||
ExoPlayer player = setupPlayer();
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
player.play();
|
||||
player.setMediaSource(mediaSource);
|
||||
player.prepare();
|
||||
runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
mediaDrmCallback.keyCondition.open();
|
||||
runUntilError(player);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_DRM_SESSION_MANAGER_ERROR)).containsExactly(period0);
|
||||
@ -1588,12 +1626,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
}
|
||||
}
|
||||
};
|
||||
ExoPlayer player = setupPlayer(renderersFactory);
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
TestAnalyticsListener listener =
|
||||
runAnalyticsTest(
|
||||
new ConcatenatingMediaSource(source0, source1),
|
||||
/* actionSchedule= */ null,
|
||||
renderersFactory);
|
||||
player.play();
|
||||
player.setMediaSource(new ConcatenatingMediaSource(source0, source1));
|
||||
player.prepare();
|
||||
runUntilError(player);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
|
||||
@ -1622,12 +1662,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
}
|
||||
}
|
||||
};
|
||||
ExoPlayer player = setupPlayer(renderersFactory);
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
TestAnalyticsListener listener =
|
||||
runAnalyticsTest(
|
||||
new ConcatenatingMediaSource(source0, source1),
|
||||
/* actionSchedule= */ null,
|
||||
renderersFactory);
|
||||
player.play();
|
||||
player.setMediaSource(new ConcatenatingMediaSource(source0, source1));
|
||||
player.prepare();
|
||||
runUntilError(player);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
|
||||
@ -1660,12 +1702,14 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
}
|
||||
}
|
||||
};
|
||||
ExoPlayer player = setupPlayer(renderersFactory);
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
player.addAnalyticsListener(listener);
|
||||
|
||||
TestAnalyticsListener listener =
|
||||
runAnalyticsTest(
|
||||
new ConcatenatingMediaSource(source, source),
|
||||
/* actionSchedule= */ null,
|
||||
renderersFactory);
|
||||
player.play();
|
||||
player.setMediaSource(new ConcatenatingMediaSource(source, source));
|
||||
player.prepare();
|
||||
runUntilError(player);
|
||||
|
||||
populateEventIds(listener.lastReportedTimeline);
|
||||
assertThat(listener.getEvents(EVENT_PLAYER_ERROR)).containsExactly(period1);
|
||||
@ -1673,11 +1717,7 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
|
||||
@Test
|
||||
public void onEvents_isReportedWithCorrectEventTimes() throws Exception {
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).build();
|
||||
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
|
||||
player.setVideoSurface(surface);
|
||||
|
||||
ExoPlayer player = setupPlayer();
|
||||
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||
Format[] formats =
|
||||
new Format[] {
|
||||
@ -1690,20 +1730,18 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
player.setMediaSource(new FakeMediaSource(new FakeTimeline(), formats));
|
||||
player.seekTo(2_000);
|
||||
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
|
||||
runMainLooperToNextTask();
|
||||
// Move to another item and fail with a third one to trigger events with different EventTimes.
|
||||
player.prepare();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player.addMediaSource(new FakeMediaSource(new FakeTimeline(), formats));
|
||||
player.play();
|
||||
TestPlayerRunHelper.runUntilPositionDiscontinuity(
|
||||
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
|
||||
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
|
||||
TestPlayerRunHelper.runUntilError(player);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
runUntilError(player);
|
||||
runMainLooperToNextTask();
|
||||
player.release();
|
||||
surface.release();
|
||||
|
||||
// Verify that expected individual callbacks have been called and capture EventTimes.
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> individualTimelineChangedEventTimes =
|
||||
@ -1928,48 +1966,6 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
private void populateEventIds(Timeline timeline) {
|
||||
period0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
|
||||
period0Seq0 = period0;
|
||||
period0Seq1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
|
||||
window1Period0Seq1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
|
||||
if (timeline.getPeriodCount() > 1) {
|
||||
period1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
|
||||
period1Seq1 = period1;
|
||||
period1Seq0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
|
||||
period1Seq2 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2));
|
||||
window0Period1Seq0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void recursiveListenerInvocation_arrivesInCorrectOrder() {
|
||||
AnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector(Clock.DEFAULT);
|
||||
@ -2027,13 +2023,12 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
exoPlayer.setMediaSource(
|
||||
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
|
||||
exoPlayer.prepare();
|
||||
TestPlayerRunHelper.runUntilPlaybackState(exoPlayer, Player.STATE_READY);
|
||||
|
||||
runUntilPlaybackState(exoPlayer, Player.STATE_READY);
|
||||
// Release and add delay on releasing thread to verify timestamps of events.
|
||||
exoPlayer.release();
|
||||
long releaseTimeMs = fakeClock.currentTimeMillis();
|
||||
fakeClock.advanceTime(1);
|
||||
ShadowLooper.idleMainLooper();
|
||||
idleMainLooper();
|
||||
|
||||
// Verify video disable events and release events arrived in order.
|
||||
ArgumentCaptor<AnalyticsListener.EventTime> videoDisabledEventTime =
|
||||
@ -2059,49 +2054,79 @@ public final class DefaultAnalyticsCollectorTest {
|
||||
assertThat(releasedEventTime.getValue().realtimeMs).isGreaterThan(videoDisableTimeMs);
|
||||
}
|
||||
|
||||
private static TestAnalyticsListener runAnalyticsTest(MediaSource mediaSource) throws Exception {
|
||||
return runAnalyticsTest(mediaSource, /* actionSchedule= */ null);
|
||||
private void populateEventIds(Timeline timeline) {
|
||||
period0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
|
||||
period0Seq0 = period0;
|
||||
period0Seq1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
|
||||
window1Period0Seq1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 1));
|
||||
if (timeline.getPeriodCount() > 1) {
|
||||
period1 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
|
||||
period1Seq1 = period1;
|
||||
period1Seq0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
|
||||
period1Seq2 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 1,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2));
|
||||
window0Period1Seq0 =
|
||||
new EventWindowAndPeriodId(
|
||||
/* windowIndex= */ 0,
|
||||
new MediaPeriodId(
|
||||
timeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 0));
|
||||
}
|
||||
}
|
||||
|
||||
private static TestAnalyticsListener runAnalyticsTest(
|
||||
MediaSource mediaSource, @Nullable ActionSchedule actionSchedule) throws Exception {
|
||||
RenderersFactory renderersFactory =
|
||||
(eventHandler,
|
||||
private static ExoPlayer setupPlayer() {
|
||||
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
return setupPlayer(
|
||||
/* renderersFactory= */ (eventHandler,
|
||||
videoRendererEventListener,
|
||||
audioRendererEventListener,
|
||||
textRendererOutput,
|
||||
metadataRendererOutput) ->
|
||||
new Renderer[] {
|
||||
new FakeVideoRenderer(eventHandler, videoRendererEventListener),
|
||||
new FakeAudioRenderer(eventHandler, audioRendererEventListener)
|
||||
};
|
||||
return runAnalyticsTest(mediaSource, actionSchedule, renderersFactory);
|
||||
metadataRendererOutput) -> {
|
||||
HandlerWrapper clockAwareHandler =
|
||||
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
|
||||
return new Renderer[] {
|
||||
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
|
||||
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
|
||||
};
|
||||
},
|
||||
clock);
|
||||
}
|
||||
|
||||
private static TestAnalyticsListener runAnalyticsTest(
|
||||
MediaSource mediaSource,
|
||||
@Nullable ActionSchedule actionSchedule,
|
||||
RenderersFactory renderersFactory)
|
||||
throws Exception {
|
||||
private static ExoPlayer setupPlayer(RenderersFactory renderersFactory) {
|
||||
return setupPlayer(renderersFactory, new FakeClock(/* isAutoAdvancing= */ true));
|
||||
}
|
||||
|
||||
private static ExoPlayer setupPlayer(RenderersFactory renderersFactory, Clock clock) {
|
||||
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 0));
|
||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||
try {
|
||||
new ExoPlayerTestRunner.Builder(ApplicationProvider.getApplicationContext())
|
||||
.setMediaSources(mediaSource)
|
||||
.setRenderersFactory(renderersFactory)
|
||||
.setVideoSurface(surface)
|
||||
.setAnalyticsListener(listener)
|
||||
.setActionSchedule(actionSchedule)
|
||||
.build()
|
||||
.start()
|
||||
.blockUntilActionScheduleFinished(TIMEOUT_MS)
|
||||
.blockUntilEnded(TIMEOUT_MS);
|
||||
} catch (ExoPlaybackException e) {
|
||||
// Ignore ExoPlaybackException as these may be expected.
|
||||
} finally {
|
||||
surface.release();
|
||||
}
|
||||
return listener;
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
|
||||
.setClock(clock)
|
||||
.setRenderersFactory(renderersFactory)
|
||||
.build();
|
||||
player.setVideoSurface(surface);
|
||||
return player;
|
||||
}
|
||||
|
||||
private static final class EventWindowAndPeriodId {
|
||||
|
@ -16,10 +16,10 @@
|
||||
|
||||
package androidx.media3.test.utils;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.DecoderCounters;
|
||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
@ -29,13 +29,15 @@ import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
||||
@UnstableApi
|
||||
public class FakeAudioRenderer extends FakeRenderer {
|
||||
|
||||
private final AudioRendererEventListener.EventDispatcher eventDispatcher;
|
||||
private final HandlerWrapper handler;
|
||||
private final AudioRendererEventListener eventListener;
|
||||
private final DecoderCounters decoderCounters;
|
||||
private boolean notifiedPositionAdvancing;
|
||||
|
||||
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
|
||||
public FakeAudioRenderer(HandlerWrapper handler, AudioRendererEventListener eventListener) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
|
||||
this.handler = handler;
|
||||
this.eventListener = eventListener;
|
||||
decoderCounters = new DecoderCounters();
|
||||
}
|
||||
|
||||
@ -43,30 +45,33 @@ public class FakeAudioRenderer extends FakeRenderer {
|
||||
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
|
||||
throws ExoPlaybackException {
|
||||
super.onEnabled(joining, mayRenderStartOfStream);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
handler.post(() -> eventListener.onAudioEnabled(decoderCounters));
|
||||
notifiedPositionAdvancing = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
super.onDisabled();
|
||||
eventDispatcher.disabled(decoderCounters);
|
||||
handler.post(() -> eventListener.onAudioDisabled(decoderCounters));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFormatChanged(Format format) {
|
||||
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null);
|
||||
eventDispatcher.decoderInitialized(
|
||||
/* decoderName= */ "fake.audio.decoder",
|
||||
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
|
||||
/* initializationDurationMs= */ 0);
|
||||
handler.post(
|
||||
() -> eventListener.onAudioInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
|
||||
handler.post(
|
||||
() ->
|
||||
eventListener.onAudioDecoderInitialized(
|
||||
/* decoderName= */ "fake.audio.decoder",
|
||||
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
|
||||
/* initializationDurationMs= */ 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
|
||||
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
|
||||
if (shouldProcess && !notifiedPositionAdvancing) {
|
||||
eventDispatcher.positionAdvancing(System.currentTimeMillis());
|
||||
handler.post(() -> eventListener.onAudioPositionAdvancing(System.currentTimeMillis()));
|
||||
notifiedPositionAdvancing = true;
|
||||
}
|
||||
return shouldProcess;
|
||||
|
@ -16,13 +16,13 @@
|
||||
|
||||
package androidx.media3.test.utils;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.VideoSize;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.DecoderCounters;
|
||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
@ -34,7 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@UnstableApi
|
||||
public class FakeVideoRenderer extends FakeRenderer {
|
||||
|
||||
private final VideoRendererEventListener.EventDispatcher eventDispatcher;
|
||||
private final HandlerWrapper handler;
|
||||
private final VideoRendererEventListener eventListener;
|
||||
private final DecoderCounters decoderCounters;
|
||||
private @MonotonicNonNull Format format;
|
||||
@Nullable private Object output;
|
||||
@ -43,9 +44,10 @@ public class FakeVideoRenderer extends FakeRenderer {
|
||||
private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
|
||||
private boolean renderedFirstFrameAfterEnable;
|
||||
|
||||
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
|
||||
public FakeVideoRenderer(HandlerWrapper handler, VideoRendererEventListener eventListener) {
|
||||
super(C.TRACK_TYPE_VIDEO);
|
||||
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
|
||||
this.handler = handler;
|
||||
this.eventListener = eventListener;
|
||||
decoderCounters = new DecoderCounters();
|
||||
}
|
||||
|
||||
@ -53,7 +55,7 @@ public class FakeVideoRenderer extends FakeRenderer {
|
||||
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
|
||||
throws ExoPlaybackException {
|
||||
super.onEnabled(joining, mayRenderStartOfStream);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
handler.post(() -> eventListener.onVideoEnabled(decoderCounters));
|
||||
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
|
||||
renderedFirstFrameAfterEnable = false;
|
||||
}
|
||||
@ -69,15 +71,17 @@ public class FakeVideoRenderer extends FakeRenderer {
|
||||
@Override
|
||||
protected void onStopped() {
|
||||
super.onStopped();
|
||||
eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0);
|
||||
eventDispatcher.reportVideoFrameProcessingOffset(
|
||||
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10);
|
||||
handler.post(() -> eventListener.onDroppedFrames(/* count= */ 0, /* elapsedMs= */ 0));
|
||||
handler.post(
|
||||
() ->
|
||||
eventListener.onVideoFrameProcessingOffset(
|
||||
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
super.onDisabled();
|
||||
eventDispatcher.disabled(decoderCounters);
|
||||
handler.post(() -> eventListener.onVideoDisabled(decoderCounters));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -88,11 +92,14 @@ public class FakeVideoRenderer extends FakeRenderer {
|
||||
|
||||
@Override
|
||||
protected void onFormatChanged(Format format) {
|
||||
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null);
|
||||
eventDispatcher.decoderInitialized(
|
||||
/* decoderName= */ "fake.video.decoder",
|
||||
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
|
||||
/* initializationDurationMs= */ 0);
|
||||
handler.post(
|
||||
() -> eventListener.onVideoInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
|
||||
handler.post(
|
||||
() ->
|
||||
eventListener.onVideoDecoderInitialized(
|
||||
/* decoderName= */ "fake.video.decoder",
|
||||
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
|
||||
/* initializationDurationMs= */ 0));
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
@ -133,10 +140,18 @@ public class FakeVideoRenderer extends FakeRenderer {
|
||||
@Nullable Object output = this.output;
|
||||
if (shouldProcess && !renderedFirstFrameAfterReset && output != null) {
|
||||
@MonotonicNonNull Format format = Assertions.checkNotNull(this.format);
|
||||
eventDispatcher.videoSizeChanged(
|
||||
new VideoSize(
|
||||
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio));
|
||||
eventDispatcher.renderedFirstFrame(output);
|
||||
handler.post(
|
||||
() ->
|
||||
eventListener.onVideoSizeChanged(
|
||||
new VideoSize(
|
||||
format.width,
|
||||
format.height,
|
||||
format.rotationDegrees,
|
||||
format.pixelWidthHeightRatio)));
|
||||
handler.post(
|
||||
() ->
|
||||
eventListener.onRenderedFirstFrame(
|
||||
output, /* renderTimeMs= */ SystemClock.elapsedRealtime()));
|
||||
renderedFirstFrameAfterReset = true;
|
||||
renderedFirstFrameAfterEnable = true;
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
@ -299,13 +300,16 @@ public class TestExoPlayerBuilder {
|
||||
videoRendererEventListener,
|
||||
audioRendererEventListener,
|
||||
textRendererOutput,
|
||||
metadataRendererOutput) ->
|
||||
renderers != null
|
||||
? renderers
|
||||
: new Renderer[] {
|
||||
new FakeVideoRenderer(eventHandler, videoRendererEventListener),
|
||||
new FakeAudioRenderer(eventHandler, audioRendererEventListener)
|
||||
};
|
||||
metadataRendererOutput) -> {
|
||||
HandlerWrapper clockAwareHandler =
|
||||
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
|
||||
return renderers != null
|
||||
? renderers
|
||||
: new Renderer[] {
|
||||
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
|
||||
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
ExoPlayer.Builder builder =
|
||||
|
@ -91,6 +91,30 @@ public class TestPlayerRunHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs tasks of the main {@link Looper} until {@link Player#isLoading()} matches the expected
|
||||
* value or a playback error occurs.
|
||||
*
|
||||
* <p>If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
|
||||
*
|
||||
* @param player The {@link Player}.
|
||||
* @param expectedIsLoading The expected value for {@link Player#isLoading()}.
|
||||
* @throws TimeoutException If the {@link RobolectricUtil#DEFAULT_TIMEOUT_MS default timeout} is
|
||||
* exceeded.
|
||||
*/
|
||||
public static void runUntilIsLoading(Player player, boolean expectedIsLoading)
|
||||
throws TimeoutException {
|
||||
verifyMainTestThread(player);
|
||||
if (player instanceof ExoPlayer) {
|
||||
verifyPlaybackThreadIsAlive((ExoPlayer) player);
|
||||
}
|
||||
runMainLooperUntil(
|
||||
() -> player.isLoading() == expectedIsLoading || player.getPlayerError() != null);
|
||||
if (player.getPlayerError() != null) {
|
||||
throw new IllegalStateException(player.getPlayerError());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
|
||||
* expected timeline or a playback error occurs.
|
||||
|
Loading…
x
Reference in New Issue
Block a user