Commit 6abc94a8 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 fed53362
...@@ -281,7 +281,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -281,7 +281,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
deliverPendingMessageAtStartPositionRequired = true; deliverPendingMessageAtStartPositionRequired = true;
Handler eventHandler = new Handler(applicationLooper); HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler); queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList = mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
......
...@@ -26,6 +26,7 @@ import androidx.media3.common.C; ...@@ -26,6 +26,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player.RepeatMode; import androidx.media3.common.Player.RepeatMode;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
...@@ -71,7 +72,7 @@ import com.google.common.collect.ImmutableList; ...@@ -71,7 +72,7 @@ import com.google.common.collect.ImmutableList;
private final Timeline.Period period; private final Timeline.Period period;
private final Timeline.Window window; private final Timeline.Window window;
private final AnalyticsCollector analyticsCollector; private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler; private final HandlerWrapper analyticsCollectorHandler;
private long nextWindowSequenceNumber; private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode; private @RepeatMode int repeatMode;
...@@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList; ...@@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
* on. * on.
*/ */
public MediaPeriodQueue( public MediaPeriodQueue(
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) { AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector; this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler; this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period(); period = new Timeline.Period();
......
...@@ -112,6 +112,7 @@ import androidx.media3.common.TrackGroup; ...@@ -112,6 +112,7 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.analytics.AnalyticsListener;
...@@ -11897,7 +11898,11 @@ public final class ExoPlayerTest { ...@@ -11897,7 +11898,11 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(context) new TestExoPlayerBuilder(context)
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> { (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()}; return new Renderer[] {videoRenderer.get()};
}) })
.build(); .build();
...@@ -12034,7 +12039,12 @@ public final class ExoPlayerTest { ...@@ -12034,7 +12039,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> (handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build(); .build();
AnalyticsListener listener = mock(AnalyticsListener.class); AnalyticsListener listener = mock(AnalyticsListener.class);
player.addAnalyticsListener(listener); player.addAnalyticsListener(listener);
...@@ -12059,7 +12069,12 @@ public final class ExoPlayerTest { ...@@ -12059,7 +12069,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> (handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build(); .build();
Player.Listener listener = mock(Player.Listener.class); Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener); player.addListener(listener);
......
...@@ -25,7 +25,6 @@ import static org.mockito.Mockito.mock; ...@@ -25,7 +25,6 @@ import static org.mockito.Mockito.mock;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Pair; import android.util.Pair;
import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdPlaybackState;
...@@ -36,6 +35,7 @@ import androidx.media3.common.Player; ...@@ -36,6 +35,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector; import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
...@@ -97,13 +97,14 @@ public final class MediaPeriodQueueTest { ...@@ -97,13 +97,14 @@ public final class MediaPeriodQueueTest {
analyticsCollector.setPlayer( analyticsCollector.setPlayer(
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(), new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(),
Looper.getMainLooper()); Looper.getMainLooper());
mediaPeriodQueue = HandlerWrapper handler =
new MediaPeriodQueue(analyticsCollector, new Handler(Looper.getMainLooper())); Clock.DEFAULT.createHandler(Looper.getMainLooper(), /* callback= */ null);
mediaPeriodQueue = new MediaPeriodQueue(analyticsCollector, handler);
mediaSourceList = mediaSourceList =
new MediaSourceList( new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
analyticsCollector, analyticsCollector,
new Handler(Looper.getMainLooper()), handler,
PlayerId.UNSET); PlayerId.UNSET);
rendererCapabilities = new RendererCapabilities[0]; rendererCapabilities = new RendererCapabilities[0];
trackSelector = mock(TrackSelector.class); trackSelector = mock(TrackSelector.class);
......
...@@ -67,7 +67,7 @@ public class MediaSourceListTest { ...@@ -67,7 +67,7 @@ public class MediaSourceListTest {
new MediaSourceList( new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class), mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
analyticsCollector, analyticsCollector,
Util.createHandlerForCurrentOrMainLooper(), Clock.DEFAULT.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null),
PlayerId.UNSET); PlayerId.UNSET);
} }
......
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
package androidx.media3.test.utils; package androidx.media3.test.utils;
import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
...@@ -29,13 +29,15 @@ import androidx.media3.exoplayer.audio.AudioRendererEventListener; ...@@ -29,13 +29,15 @@ import androidx.media3.exoplayer.audio.AudioRendererEventListener;
@UnstableApi @UnstableApi
public class FakeAudioRenderer extends FakeRenderer { public class FakeAudioRenderer extends FakeRenderer {
private final AudioRendererEventListener.EventDispatcher eventDispatcher; private final HandlerWrapper handler;
private final AudioRendererEventListener eventListener;
private final DecoderCounters decoderCounters; private final DecoderCounters decoderCounters;
private boolean notifiedPositionAdvancing; private boolean notifiedPositionAdvancing;
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) { public FakeAudioRenderer(HandlerWrapper handler, AudioRendererEventListener eventListener) {
super(C.TRACK_TYPE_AUDIO); super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener); this.handler = handler;
this.eventListener = eventListener;
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -43,30 +45,33 @@ public class FakeAudioRenderer extends FakeRenderer { ...@@ -43,30 +45,33 @@ public class FakeAudioRenderer extends FakeRenderer {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException { throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream); super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters); handler.post(() -> eventListener.onAudioEnabled(decoderCounters));
notifiedPositionAdvancing = false; notifiedPositionAdvancing = false;
} }
@Override @Override
protected void onDisabled() { protected void onDisabled() {
super.onDisabled(); super.onDisabled();
eventDispatcher.disabled(decoderCounters); handler.post(() -> eventListener.onAudioDisabled(decoderCounters));
} }
@Override @Override
protected void onFormatChanged(Format format) { protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); handler.post(
eventDispatcher.decoderInitialized( () -> eventListener.onAudioInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
/* decoderName= */ "fake.audio.decoder", handler.post(
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(), () ->
/* initializationDurationMs= */ 0); eventListener.onAudioDecoderInitialized(
/* decoderName= */ "fake.audio.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0));
} }
@Override @Override
protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) { protected boolean shouldProcessBuffer(long bufferTimeUs, long playbackPositionUs) {
boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs); boolean shouldProcess = super.shouldProcessBuffer(bufferTimeUs, playbackPositionUs);
if (shouldProcess && !notifiedPositionAdvancing) { if (shouldProcess && !notifiedPositionAdvancing) {
eventDispatcher.positionAdvancing(System.currentTimeMillis()); handler.post(() -> eventListener.onAudioPositionAdvancing(System.currentTimeMillis()));
notifiedPositionAdvancing = true; notifiedPositionAdvancing = true;
} }
return shouldProcess; return shouldProcess;
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
package androidx.media3.test.utils; package androidx.media3.test.utils;
import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
...@@ -34,7 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -34,7 +34,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@UnstableApi @UnstableApi
public class FakeVideoRenderer extends FakeRenderer { public class FakeVideoRenderer extends FakeRenderer {
private final VideoRendererEventListener.EventDispatcher eventDispatcher; private final HandlerWrapper handler;
private final VideoRendererEventListener eventListener;
private final DecoderCounters decoderCounters; private final DecoderCounters decoderCounters;
private @MonotonicNonNull Format format; private @MonotonicNonNull Format format;
@Nullable private Object output; @Nullable private Object output;
...@@ -43,9 +44,10 @@ public class FakeVideoRenderer extends FakeRenderer { ...@@ -43,9 +44,10 @@ public class FakeVideoRenderer extends FakeRenderer {
private boolean mayRenderFirstFrameAfterEnableIfNotStarted; private boolean mayRenderFirstFrameAfterEnableIfNotStarted;
private boolean renderedFirstFrameAfterEnable; private boolean renderedFirstFrameAfterEnable;
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) { public FakeVideoRenderer(HandlerWrapper handler, VideoRendererEventListener eventListener) {
super(C.TRACK_TYPE_VIDEO); super(C.TRACK_TYPE_VIDEO);
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener); this.handler = handler;
this.eventListener = eventListener;
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
} }
...@@ -53,7 +55,7 @@ public class FakeVideoRenderer extends FakeRenderer { ...@@ -53,7 +55,7 @@ public class FakeVideoRenderer extends FakeRenderer {
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException { throws ExoPlaybackException {
super.onEnabled(joining, mayRenderStartOfStream); super.onEnabled(joining, mayRenderStartOfStream);
eventDispatcher.enabled(decoderCounters); handler.post(() -> eventListener.onVideoEnabled(decoderCounters));
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream; mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false; renderedFirstFrameAfterEnable = false;
} }
...@@ -69,15 +71,17 @@ public class FakeVideoRenderer extends FakeRenderer { ...@@ -69,15 +71,17 @@ public class FakeVideoRenderer extends FakeRenderer {
@Override @Override
protected void onStopped() { protected void onStopped() {
super.onStopped(); super.onStopped();
eventDispatcher.droppedFrames(/* droppedFrameCount= */ 0, /* elapsedMs= */ 0); handler.post(() -> eventListener.onDroppedFrames(/* count= */ 0, /* elapsedMs= */ 0));
eventDispatcher.reportVideoFrameProcessingOffset( handler.post(
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10); () ->
eventListener.onVideoFrameProcessingOffset(
/* totalProcessingOffsetUs= */ 400000, /* frameCount= */ 10));
} }
@Override @Override
protected void onDisabled() { protected void onDisabled() {
super.onDisabled(); super.onDisabled();
eventDispatcher.disabled(decoderCounters); handler.post(() -> eventListener.onVideoDisabled(decoderCounters));
} }
@Override @Override
...@@ -88,11 +92,14 @@ public class FakeVideoRenderer extends FakeRenderer { ...@@ -88,11 +92,14 @@ public class FakeVideoRenderer extends FakeRenderer {
@Override @Override
protected void onFormatChanged(Format format) { protected void onFormatChanged(Format format) {
eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); handler.post(
eventDispatcher.decoderInitialized( () -> eventListener.onVideoInputFormatChanged(format, /* decoderReuseEvaluation= */ null));
/* decoderName= */ "fake.video.decoder", handler.post(
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(), () ->
/* initializationDurationMs= */ 0); eventListener.onVideoDecoderInitialized(
/* decoderName= */ "fake.video.decoder",
/* initializedTimestampMs= */ SystemClock.elapsedRealtime(),
/* initializationDurationMs= */ 0));
this.format = format; this.format = format;
} }
...@@ -133,10 +140,18 @@ public class FakeVideoRenderer extends FakeRenderer { ...@@ -133,10 +140,18 @@ public class FakeVideoRenderer extends FakeRenderer {
@Nullable Object output = this.output; @Nullable Object output = this.output;
if (shouldProcess && !renderedFirstFrameAfterReset && output != null) { if (shouldProcess && !renderedFirstFrameAfterReset && output != null) {
@MonotonicNonNull Format format = Assertions.checkNotNull(this.format); @MonotonicNonNull Format format = Assertions.checkNotNull(this.format);
eventDispatcher.videoSizeChanged( handler.post(
new VideoSize( () ->
format.width, format.height, format.rotationDegrees, format.pixelWidthHeightRatio)); eventListener.onVideoSizeChanged(
eventDispatcher.renderedFirstFrame(output); new VideoSize(
format.width,
format.height,
format.rotationDegrees,
format.pixelWidthHeightRatio)));
handler.post(
() ->
eventListener.onRenderedFirstFrame(
output, /* renderTimeMs= */ SystemClock.elapsedRealtime()));
renderedFirstFrameAfterReset = true; renderedFirstFrameAfterReset = true;
renderedFirstFrameAfterEnable = true; renderedFirstFrameAfterEnable = true;
} }
......
...@@ -23,6 +23,7 @@ import androidx.annotation.Nullable; ...@@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
...@@ -299,13 +300,16 @@ public class TestExoPlayerBuilder { ...@@ -299,13 +300,16 @@ public class TestExoPlayerBuilder {
videoRendererEventListener, videoRendererEventListener,
audioRendererEventListener, audioRendererEventListener,
textRendererOutput, textRendererOutput,
metadataRendererOutput) -> metadataRendererOutput) -> {
renderers != null HandlerWrapper clockAwareHandler =
? renderers clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
: new Renderer[] { return renderers != null
new FakeVideoRenderer(eventHandler, videoRendererEventListener), ? renderers
new FakeAudioRenderer(eventHandler, audioRendererEventListener) : new Renderer[] {
}; new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
};
} }
ExoPlayer.Builder builder = ExoPlayer.Builder builder =
......
...@@ -92,6 +92,30 @@ public class TestPlayerRunHelper { ...@@ -92,6 +92,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 * Runs tasks of the main {@link Looper} until {@link Player#getCurrentTimeline()} matches the
* expected timeline or a playback error occurs. * expected timeline or a playback error occurs.
* *
......
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