1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.exoplayer2.testutil; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.content.Context; 22 import android.os.Looper; 23 import androidx.annotation.Nullable; 24 import com.google.android.exoplayer2.DefaultLoadControl; 25 import com.google.android.exoplayer2.ExoPlaybackException; 26 import com.google.android.exoplayer2.ExoPlayer; 27 import com.google.android.exoplayer2.LoadControl; 28 import com.google.android.exoplayer2.Player; 29 import com.google.android.exoplayer2.Renderer; 30 import com.google.android.exoplayer2.RenderersFactory; 31 import com.google.android.exoplayer2.SimpleExoPlayer; 32 import com.google.android.exoplayer2.Timeline; 33 import com.google.android.exoplayer2.analytics.AnalyticsCollector; 34 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 35 import com.google.android.exoplayer2.upstream.BandwidthMeter; 36 import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; 37 import com.google.android.exoplayer2.util.Assertions; 38 import com.google.android.exoplayer2.util.Clock; 39 import com.google.android.exoplayer2.util.Supplier; 40 import com.google.android.exoplayer2.util.Util; 41 import com.google.android.exoplayer2.video.VideoListener; 42 import java.lang.reflect.InvocationTargetException; 43 import java.lang.reflect.Method; 44 import java.util.concurrent.atomic.AtomicBoolean; 45 import java.util.concurrent.atomic.AtomicReference; 46 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 47 48 /** 49 * Utilities to write unit/integration tests with a SimpleExoPlayer instance that uses fake 50 * components. 51 */ 52 public class TestExoPlayer { 53 54 /** Reflectively call Robolectric ShadowLooper#runOneTask. */ 55 private static final Object shadowLooper; 56 57 private static final Method runOneTaskMethod; 58 59 static { 60 try { 61 Class<?> clazz = Class.forName("org.robolectric.Shadows"); 62 Method shadowOfMethod = 63 Assertions.checkNotNull(clazz.getDeclaredMethod("shadowOf", Looper.class)); 64 shadowLooper = 65 Assertions.checkNotNull(shadowOfMethod.invoke(new Object(), Looper.getMainLooper())); 66 runOneTaskMethod = shadowLooper.getClass().getDeclaredMethod("runOneTask"); 67 } catch (Exception e) { 68 throw new RuntimeException(e); 69 } 70 } 71 72 /** A builder of {@link SimpleExoPlayer} instances for testing. */ 73 public static class Builder { 74 75 private final Context context; 76 private Clock clock; 77 private DefaultTrackSelector trackSelector; 78 private LoadControl loadControl; 79 private BandwidthMeter bandwidthMeter; 80 @Nullable private Renderer[] renderers; 81 @Nullable private RenderersFactory renderersFactory; 82 private boolean useLazyPreparation; 83 private boolean throwWhenStuckBuffering; 84 private @MonotonicNonNull Looper looper; 85 Builder(Context context)86 public Builder(Context context) { 87 this.context = context; 88 clock = new AutoAdvancingFakeClock(); 89 trackSelector = new DefaultTrackSelector(context); 90 loadControl = new DefaultLoadControl(); 91 bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); 92 @Nullable Looper myLooper = Looper.myLooper(); 93 if (myLooper != null) { 94 looper = myLooper; 95 } 96 } 97 98 /** 99 * Sets whether to use lazy preparation. 100 * 101 * @param useLazyPreparation Whether to use lazy preparation. 102 * @return This builder. 103 */ setUseLazyPreparation(boolean useLazyPreparation)104 public Builder setUseLazyPreparation(boolean useLazyPreparation) { 105 this.useLazyPreparation = useLazyPreparation; 106 return this; 107 } 108 109 /** Returns whether the player will use lazy preparation. */ getUseLazyPreparation()110 public boolean getUseLazyPreparation() { 111 return useLazyPreparation; 112 } 113 114 /** 115 * Sets a {@link DefaultTrackSelector}. The default value is a {@link DefaultTrackSelector} in 116 * its initial configuration. 117 * 118 * @param trackSelector The {@link DefaultTrackSelector} to be used by the player. 119 * @return This builder. 120 */ setTrackSelector(DefaultTrackSelector trackSelector)121 public Builder setTrackSelector(DefaultTrackSelector trackSelector) { 122 Assertions.checkNotNull(trackSelector); 123 this.trackSelector = trackSelector; 124 return this; 125 } 126 127 /** Returns the track selector used by the player. */ getTrackSelector()128 public DefaultTrackSelector getTrackSelector() { 129 return trackSelector; 130 } 131 132 /** 133 * Sets a {@link LoadControl} to be used by the player. The default value is a {@link 134 * DefaultLoadControl}. 135 * 136 * @param loadControl The {@link LoadControl} to be used by the player. 137 * @return This builder. 138 */ setLoadControl(LoadControl loadControl)139 public Builder setLoadControl(LoadControl loadControl) { 140 this.loadControl = loadControl; 141 return this; 142 } 143 144 /** Returns the {@link LoadControl} that will be used by the player. */ getLoadControl()145 public LoadControl getLoadControl() { 146 return loadControl; 147 } 148 149 /** 150 * Sets the {@link BandwidthMeter}. The default value is a {@link DefaultBandwidthMeter} in its 151 * default configuration. 152 * 153 * @param bandwidthMeter The {@link BandwidthMeter} to be used by the player. 154 * @return This builder. 155 */ setBandwidthMeter(BandwidthMeter bandwidthMeter)156 public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { 157 Assertions.checkNotNull(bandwidthMeter); 158 this.bandwidthMeter = bandwidthMeter; 159 return this; 160 } 161 162 /** Returns the bandwidth meter used by the player. */ getBandwidthMeter()163 public BandwidthMeter getBandwidthMeter() { 164 return bandwidthMeter; 165 } 166 167 /** 168 * Sets the {@link Renderer}s. If not set, the player will use a {@link FakeVideoRenderer} and a 169 * {@link FakeAudioRenderer}. Setting the renderers is not allowed after a call to {@link 170 * #setRenderersFactory(RenderersFactory)}. 171 * 172 * @param renderers A list of {@link Renderer}s to be used by the player. 173 * @return This builder. 174 */ setRenderers(Renderer... renderers)175 public Builder setRenderers(Renderer... renderers) { 176 assertThat(renderersFactory).isNull(); 177 this.renderers = renderers; 178 return this; 179 } 180 181 /** 182 * Returns the {@link Renderer Renderers} that have been set with {@link #setRenderers} or null 183 * if no {@link Renderer Renderers} have been explicitly set. Note that these renderers may not 184 * be the ones used by the built player, for example if a {@link #setRenderersFactory Renderer 185 * factory} has been set. 186 */ 187 @Nullable getRenderers()188 public Renderer[] getRenderers() { 189 return renderers; 190 } 191 192 /** 193 * Sets the {@link RenderersFactory}. The default factory creates all renderers set by {@link 194 * #setRenderers(Renderer...)}. Setting the renderer factory is not allowed after a call to 195 * {@link #setRenderers(Renderer...)}. 196 * 197 * @param renderersFactory A {@link RenderersFactory} to be used by the player. 198 * @return This builder. 199 */ setRenderersFactory(RenderersFactory renderersFactory)200 public Builder setRenderersFactory(RenderersFactory renderersFactory) { 201 assertThat(renderers).isNull(); 202 this.renderersFactory = renderersFactory; 203 return this; 204 } 205 206 /** 207 * Returns the {@link RenderersFactory} that has been set with {@link #setRenderersFactory} or 208 * null if no factory has been explicitly set. 209 */ 210 @Nullable getRenderersFactory()211 public RenderersFactory getRenderersFactory() { 212 return renderersFactory; 213 } 214 215 /** 216 * Sets the {@link Clock} to be used by the player. The default value is a {@link 217 * AutoAdvancingFakeClock}. 218 * 219 * @param clock A {@link Clock} to be used by the player. 220 * @return This builder. 221 */ setClock(Clock clock)222 public Builder setClock(Clock clock) { 223 assertThat(clock).isNotNull(); 224 this.clock = clock; 225 return this; 226 } 227 228 /** Returns the clock used by the player. */ getClock()229 public Clock getClock() { 230 return clock; 231 } 232 233 /** 234 * Sets the {@link Looper} to be used by the player. 235 * 236 * @param looper The {@link Looper} to be used by the player. 237 * @return This builder. 238 */ setLooper(Looper looper)239 public Builder setLooper(Looper looper) { 240 this.looper = looper; 241 return this; 242 } 243 244 /** 245 * Returns the {@link Looper} that will be used by the player, or null if no {@link Looper} has 246 * been set yet and no default is available. 247 */ 248 @Nullable getLooper()249 public Looper getLooper() { 250 return looper; 251 } 252 253 /** 254 * Sets whether the player should throw when it detects it's stuck buffering. 255 * 256 * <p>This method is experimental, and will be renamed or removed in a future release. 257 * 258 * @param throwWhenStuckBuffering Whether to throw when the player detects it's stuck buffering. 259 * @return This builder. 260 */ experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering)261 public Builder experimental_setThrowWhenStuckBuffering(boolean throwWhenStuckBuffering) { 262 this.throwWhenStuckBuffering = throwWhenStuckBuffering; 263 return this; 264 } 265 266 /** 267 * Builds an {@link SimpleExoPlayer} using the provided values or their defaults. 268 * 269 * @return The built {@link ExoPlayerTestRunner}. 270 */ build()271 public SimpleExoPlayer build() { 272 Assertions.checkNotNull( 273 looper, "TestExoPlayer builder run on a thread without Looper and no Looper specified."); 274 // Do not update renderersFactory and renderers here, otherwise their getters may 275 // return different values before and after build() is called, making them confusing. 276 RenderersFactory playerRenderersFactory = renderersFactory; 277 if (playerRenderersFactory == null) { 278 playerRenderersFactory = 279 (eventHandler, 280 videoRendererEventListener, 281 audioRendererEventListener, 282 textRendererOutput, 283 metadataRendererOutput) -> 284 renderers != null 285 ? renderers 286 : new Renderer[] { 287 new FakeVideoRenderer(eventHandler, videoRendererEventListener), 288 new FakeAudioRenderer(eventHandler, audioRendererEventListener) 289 }; 290 } 291 292 return new SimpleExoPlayer.Builder(context, playerRenderersFactory) 293 .setTrackSelector(trackSelector) 294 .setLoadControl(loadControl) 295 .setBandwidthMeter(bandwidthMeter) 296 .setAnalyticsCollector(new AnalyticsCollector(clock)) 297 .setClock(clock) 298 .setUseLazyPreparation(useLazyPreparation) 299 .setLooper(looper) 300 .experimental_setThrowWhenStuckBuffering(throwWhenStuckBuffering) 301 .build(); 302 } 303 } 304 TestExoPlayer()305 private TestExoPlayer() {} 306 307 /** 308 * Run tasks of the main {@link Looper} until the {@code player}'s state reaches the {@code 309 * expectedState}. 310 */ runUntilPlaybackState(Player player, @Player.State int expectedState)311 public static void runUntilPlaybackState(Player player, @Player.State int expectedState) { 312 verifyMainTestThread(player); 313 if (player.getPlaybackState() == expectedState) { 314 return; 315 } 316 317 AtomicBoolean receivedExpectedState = new AtomicBoolean(false); 318 Player.EventListener listener = 319 new Player.EventListener() { 320 @Override 321 public void onPlaybackStateChanged(int state) { 322 if (state == expectedState) { 323 receivedExpectedState.set(true); 324 } 325 } 326 }; 327 player.addListener(listener); 328 runUntil(() -> receivedExpectedState.get()); 329 player.removeListener(listener); 330 } 331 332 /** 333 * Run tasks of the main {@link Looper} until the {@code player} calls the {@link 334 * Player.EventListener#onPlaybackSpeedChanged} callback with that matches {@code 335 * expectedPlayWhenReady}. 336 */ runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady)337 public static void runUntilPlayWhenReady(Player player, boolean expectedPlayWhenReady) { 338 verifyMainTestThread(player); 339 if (player.getPlayWhenReady() == expectedPlayWhenReady) { 340 return; 341 } 342 343 AtomicBoolean receivedExpectedPlayWhenReady = new AtomicBoolean(false); 344 Player.EventListener listener = 345 new Player.EventListener() { 346 @Override 347 public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { 348 if (playWhenReady == expectedPlayWhenReady) { 349 receivedExpectedPlayWhenReady.set(true); 350 } 351 player.removeListener(this); 352 } 353 }; 354 player.addListener(listener); 355 runUntil(() -> receivedExpectedPlayWhenReady.get()); 356 } 357 358 /** 359 * Run tasks of the main {@link Looper} until the {@code player} calls the {@link 360 * Player.EventListener#onTimelineChanged} callback. 361 * 362 * @param player The {@link Player}. 363 * @param expectedTimeline A specific {@link Timeline} to wait for, or null if any timeline is 364 * accepted. 365 * @return The received {@link Timeline}. 366 */ runUntilTimelineChanged( Player player, @Nullable Timeline expectedTimeline)367 public static Timeline runUntilTimelineChanged( 368 Player player, @Nullable Timeline expectedTimeline) { 369 verifyMainTestThread(player); 370 371 if (expectedTimeline != null && expectedTimeline.equals(player.getCurrentTimeline())) { 372 return expectedTimeline; 373 } 374 375 AtomicReference<Timeline> receivedTimeline = new AtomicReference<>(); 376 Player.EventListener listener = 377 new Player.EventListener() { 378 @Override 379 public void onTimelineChanged(Timeline timeline, int reason) { 380 if (expectedTimeline == null || expectedTimeline.equals(timeline)) { 381 receivedTimeline.set(timeline); 382 } 383 player.removeListener(this); 384 } 385 }; 386 player.addListener(listener); 387 runUntil(() -> receivedTimeline.get() != null); 388 return receivedTimeline.get(); 389 } 390 391 /** 392 * Run tasks of the main {@link Looper} until the {@code player} calls the {@link 393 * Player.EventListener#onPositionDiscontinuity} callback with the specified {@link 394 * Player.DiscontinuityReason}. 395 */ runUntilPositionDiscontinuity( Player player, @Player.DiscontinuityReason int expectedReason)396 public static void runUntilPositionDiscontinuity( 397 Player player, @Player.DiscontinuityReason int expectedReason) { 398 AtomicBoolean receivedCallback = new AtomicBoolean(false); 399 Player.EventListener listener = 400 new Player.EventListener() { 401 @Override 402 public void onPositionDiscontinuity(int reason) { 403 if (reason == expectedReason) { 404 receivedCallback.set(true); 405 player.removeListener(this); 406 } 407 } 408 }; 409 player.addListener(listener); 410 runUntil(() -> receivedCallback.get()); 411 } 412 413 /** 414 * Run tasks of the main {@link Looper} until the {@code player} calls the {@link 415 * Player.EventListener#onPlayerError} callback. 416 * 417 * @param player The {@link Player}. 418 * @return The raised error. 419 */ runUntilError(Player player)420 public static ExoPlaybackException runUntilError(Player player) { 421 verifyMainTestThread(player); 422 AtomicReference<ExoPlaybackException> receivedError = new AtomicReference<>(); 423 Player.EventListener listener = 424 new Player.EventListener() { 425 @Override 426 public void onPlayerError(ExoPlaybackException error) { 427 receivedError.set(error); 428 player.removeListener(this); 429 } 430 }; 431 player.addListener(listener); 432 runUntil(() -> receivedError.get() != null); 433 return receivedError.get(); 434 } 435 436 /** 437 * Run tasks of the main {@link Looper} until the {@code player} calls the {@link 438 * com.google.android.exoplayer2.video.VideoRendererEventListener#onRenderedFirstFrame} callback. 439 */ runUntilRenderedFirstFrame(SimpleExoPlayer player)440 public static void runUntilRenderedFirstFrame(SimpleExoPlayer player) { 441 verifyMainTestThread(player); 442 AtomicBoolean receivedCallback = new AtomicBoolean(false); 443 VideoListener listener = 444 new VideoListener() { 445 @Override 446 public void onRenderedFirstFrame() { 447 receivedCallback.set(true); 448 player.removeVideoListener(this); 449 } 450 }; 451 player.addVideoListener(listener); 452 runUntil(() -> receivedCallback.get()); 453 } 454 455 /** 456 * Runs tasks of the main {@link Looper} until the {@code player} handled all previously issued 457 * commands completely on the internal playback thread. 458 */ runUntilPendingCommandsAreFullyHandled(ExoPlayer player)459 public static void runUntilPendingCommandsAreFullyHandled(ExoPlayer player) { 460 verifyMainTestThread(player); 461 // Send message to player that will arrive after all other pending commands. Thus, the message 462 // execution on the app thread will also happen after all other pending command 463 // acknowledgements have arrived back on the app thread. 464 AtomicBoolean receivedMessageCallback = new AtomicBoolean(false); 465 player 466 .createMessage((type, data) -> receivedMessageCallback.set(true)) 467 .setHandler(Util.createHandler()) 468 .send(); 469 runUntil(() -> receivedMessageCallback.get()); 470 } 471 472 /** Run tasks of the main {@link Looper} until the {@code condition} returns {@code true}. */ runUntil(Supplier<Boolean> condition)473 public static void runUntil(Supplier<Boolean> condition) { 474 verifyMainTestThread(); 475 476 try { 477 while (!condition.get()) { 478 runOneTaskMethod.invoke(shadowLooper); 479 } 480 } catch (IllegalAccessException e) { 481 throw new IllegalStateException(e); 482 } catch (InvocationTargetException e) { 483 throw new IllegalStateException(e); 484 } 485 } 486 verifyMainTestThread(Player player)487 private static void verifyMainTestThread(Player player) { 488 if (Looper.myLooper() != Looper.getMainLooper() 489 || player.getApplicationLooper() != Looper.getMainLooper()) { 490 throw new IllegalStateException(); 491 } 492 } 493 verifyMainTestThread()494 private static void verifyMainTestThread() { 495 if (Looper.myLooper() != Looper.getMainLooper()) { 496 throw new IllegalStateException(); 497 } 498 } 499 } 500