Commit 7d62943b by tonihei Committed by Rohit Singh

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
parent 7fffe657
......@@ -273,7 +273,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);
......
......@@ -30,6 +30,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.common.collect.ImmutableList;
/**
......@@ -69,7 +70,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;
......@@ -89,7 +90,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();
......
......@@ -151,6 +151,7 @@ import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.SystemClock;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
......@@ -11887,7 +11888,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();
......@@ -12024,7 +12029,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);
......@@ -12049,7 +12059,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.test.core.app.ApplicationProvider;
......@@ -48,6 +47,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
......@@ -91,13 +91,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);
......
......@@ -64,7 +64,7 @@ public class MediaSourceListTest {
new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
analyticsCollector,
Util.createHandlerForCurrentOrMainLooper(),
Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null),
PlayerId.UNSET);
}
......
......@@ -90,6 +90,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.
*
......
......@@ -16,24 +16,26 @@
package com.google.android.exoplayer2.testutil;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.HandlerWrapper;
/** A {@link FakeRenderer} that supports {@link C#TRACK_TYPE_AUDIO}. */
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();
}
......@@ -41,30 +43,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,7 +16,6 @@
package com.google.android.exoplayer2.testutil;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
......@@ -25,6 +24,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoSize;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
......@@ -32,7 +32,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link FakeRenderer} that supports {@link C#TRACK_TYPE_VIDEO}. */
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;
......@@ -41,9 +42,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();
}
......@@ -51,7 +53,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;
}
......@@ -67,15 +69,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
......@@ -86,11 +90,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;
}
......@@ -131,10 +138,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;
}
......
......@@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
......@@ -297,13 +298,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 =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment