1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2; 17 18 import android.os.Handler; 19 import android.os.HandlerThread; 20 import android.os.Looper; 21 import android.os.Message; 22 import android.os.Process; 23 import android.os.SystemClock; 24 import android.util.Pair; 25 import androidx.annotation.CheckResult; 26 import androidx.annotation.Nullable; 27 import com.google.android.exoplayer2.DefaultMediaClock.PlaybackSpeedListener; 28 import com.google.android.exoplayer2.Player.DiscontinuityReason; 29 import com.google.android.exoplayer2.Player.PlayWhenReadyChangeReason; 30 import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; 31 import com.google.android.exoplayer2.Player.RepeatMode; 32 import com.google.android.exoplayer2.analytics.AnalyticsCollector; 33 import com.google.android.exoplayer2.source.MediaPeriod; 34 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 35 import com.google.android.exoplayer2.source.SampleStream; 36 import com.google.android.exoplayer2.source.ShuffleOrder; 37 import com.google.android.exoplayer2.source.TrackGroupArray; 38 import com.google.android.exoplayer2.trackselection.TrackSelection; 39 import com.google.android.exoplayer2.trackselection.TrackSelector; 40 import com.google.android.exoplayer2.trackselection.TrackSelectorResult; 41 import com.google.android.exoplayer2.upstream.BandwidthMeter; 42 import com.google.android.exoplayer2.util.Assertions; 43 import com.google.android.exoplayer2.util.Clock; 44 import com.google.android.exoplayer2.util.HandlerWrapper; 45 import com.google.android.exoplayer2.util.Log; 46 import com.google.android.exoplayer2.util.TraceUtil; 47 import com.google.android.exoplayer2.util.Util; 48 import java.io.IOException; 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.List; 52 import java.util.concurrent.atomic.AtomicBoolean; 53 54 /** Implements the internal behavior of {@link ExoPlayerImpl}. */ 55 /* package */ final class ExoPlayerImplInternal 56 implements Handler.Callback, 57 MediaPeriod.Callback, 58 TrackSelector.InvalidationListener, 59 MediaSourceList.MediaSourceListInfoRefreshListener, 60 PlaybackSpeedListener, 61 PlayerMessage.Sender { 62 63 private static final String TAG = "ExoPlayerImplInternal"; 64 65 // External messages 66 public static final int MSG_PLAYBACK_INFO_CHANGED = 0; 67 public static final int MSG_PLAYBACK_SPEED_CHANGED = 1; 68 69 // Internal messages 70 private static final int MSG_PREPARE = 0; 71 private static final int MSG_SET_PLAY_WHEN_READY = 1; 72 private static final int MSG_DO_SOME_WORK = 2; 73 private static final int MSG_SEEK_TO = 3; 74 private static final int MSG_SET_PLAYBACK_SPEED = 4; 75 private static final int MSG_SET_SEEK_PARAMETERS = 5; 76 private static final int MSG_STOP = 6; 77 private static final int MSG_RELEASE = 7; 78 private static final int MSG_PERIOD_PREPARED = 8; 79 private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9; 80 private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; 81 private static final int MSG_SET_REPEAT_MODE = 11; 82 private static final int MSG_SET_SHUFFLE_ENABLED = 12; 83 private static final int MSG_SET_FOREGROUND_MODE = 13; 84 private static final int MSG_SEND_MESSAGE = 14; 85 private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15; 86 private static final int MSG_PLAYBACK_SPEED_CHANGED_INTERNAL = 16; 87 private static final int MSG_SET_MEDIA_SOURCES = 17; 88 private static final int MSG_ADD_MEDIA_SOURCES = 18; 89 private static final int MSG_MOVE_MEDIA_SOURCES = 19; 90 private static final int MSG_REMOVE_MEDIA_SOURCES = 20; 91 private static final int MSG_SET_SHUFFLE_ORDER = 21; 92 private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22; 93 private static final int MSG_SET_PAUSE_AT_END_OF_WINDOW = 23; 94 95 private static final int ACTIVE_INTERVAL_MS = 10; 96 private static final int IDLE_INTERVAL_MS = 1000; 97 98 private final Renderer[] renderers; 99 private final RendererCapabilities[] rendererCapabilities; 100 private final TrackSelector trackSelector; 101 private final TrackSelectorResult emptyTrackSelectorResult; 102 private final LoadControl loadControl; 103 private final BandwidthMeter bandwidthMeter; 104 private final HandlerWrapper handler; 105 private final HandlerThread internalPlaybackThread; 106 private final Handler eventHandler; 107 private final Timeline.Window window; 108 private final Timeline.Period period; 109 private final long backBufferDurationUs; 110 private final boolean retainBackBufferFromKeyframe; 111 private final DefaultMediaClock mediaClock; 112 private final ArrayList<PendingMessageInfo> pendingMessages; 113 private final Clock clock; 114 private final MediaPeriodQueue queue; 115 private final MediaSourceList mediaSourceList; 116 117 @SuppressWarnings("unused") 118 private SeekParameters seekParameters; 119 120 private PlaybackInfo playbackInfo; 121 private PlaybackInfoUpdate playbackInfoUpdate; 122 private boolean released; 123 private boolean pauseAtEndOfWindow; 124 private boolean pendingPauseAtEndOfPeriod; 125 private boolean rebuffering; 126 private boolean shouldContinueLoading; 127 @Player.RepeatMode private int repeatMode; 128 private boolean shuffleModeEnabled; 129 private boolean foregroundMode; 130 131 private int enabledRendererCount; 132 @Nullable private SeekPosition pendingInitialSeekPosition; 133 private long rendererPositionUs; 134 private int nextPendingMessageIndex; 135 private boolean deliverPendingMessageAtStartPositionRequired; 136 137 private long releaseTimeoutMs; 138 private boolean throwWhenStuckBuffering; 139 ExoPlayerImplInternal( Renderer[] renderers, TrackSelector trackSelector, TrackSelectorResult emptyTrackSelectorResult, LoadControl loadControl, BandwidthMeter bandwidthMeter, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, @Nullable AnalyticsCollector analyticsCollector, Handler eventHandler, Clock clock)140 public ExoPlayerImplInternal( 141 Renderer[] renderers, 142 TrackSelector trackSelector, 143 TrackSelectorResult emptyTrackSelectorResult, 144 LoadControl loadControl, 145 BandwidthMeter bandwidthMeter, 146 @Player.RepeatMode int repeatMode, 147 boolean shuffleModeEnabled, 148 @Nullable AnalyticsCollector analyticsCollector, 149 Handler eventHandler, 150 Clock clock) { 151 this.renderers = renderers; 152 this.trackSelector = trackSelector; 153 this.emptyTrackSelectorResult = emptyTrackSelectorResult; 154 this.loadControl = loadControl; 155 this.bandwidthMeter = bandwidthMeter; 156 this.repeatMode = repeatMode; 157 this.shuffleModeEnabled = shuffleModeEnabled; 158 this.eventHandler = eventHandler; 159 this.clock = clock; 160 this.queue = new MediaPeriodQueue(); 161 162 backBufferDurationUs = loadControl.getBackBufferDurationUs(); 163 retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); 164 165 seekParameters = SeekParameters.DEFAULT; 166 playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult); 167 playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); 168 rendererCapabilities = new RendererCapabilities[renderers.length]; 169 for (int i = 0; i < renderers.length; i++) { 170 renderers[i].setIndex(i); 171 rendererCapabilities[i] = renderers[i].getCapabilities(); 172 } 173 mediaClock = new DefaultMediaClock(this, clock); 174 pendingMessages = new ArrayList<>(); 175 window = new Timeline.Window(); 176 period = new Timeline.Period(); 177 trackSelector.init(/* listener= */ this, bandwidthMeter); 178 179 // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can 180 // not normally change to this priority" is incorrect. 181 internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); 182 internalPlaybackThread.start(); 183 handler = clock.createHandler(internalPlaybackThread.getLooper(), this); 184 deliverPendingMessageAtStartPositionRequired = true; 185 mediaSourceList = new MediaSourceList(this); 186 if (analyticsCollector != null) { 187 mediaSourceList.setAnalyticsCollector(eventHandler, analyticsCollector); 188 } 189 } 190 experimental_setReleaseTimeoutMs(long releaseTimeoutMs)191 public void experimental_setReleaseTimeoutMs(long releaseTimeoutMs) { 192 this.releaseTimeoutMs = releaseTimeoutMs; 193 } 194 experimental_throwWhenStuckBuffering()195 public void experimental_throwWhenStuckBuffering() { 196 throwWhenStuckBuffering = true; 197 } 198 prepare()199 public void prepare() { 200 handler.obtainMessage(MSG_PREPARE).sendToTarget(); 201 } 202 setPlayWhenReady( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason)203 public void setPlayWhenReady( 204 boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason) { 205 handler 206 .obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, playbackSuppressionReason) 207 .sendToTarget(); 208 } 209 setPauseAtEndOfWindow(boolean pauseAtEndOfWindow)210 public void setPauseAtEndOfWindow(boolean pauseAtEndOfWindow) { 211 handler 212 .obtainMessage(MSG_SET_PAUSE_AT_END_OF_WINDOW, pauseAtEndOfWindow ? 1 : 0, /* ignored */ 0) 213 .sendToTarget(); 214 } 215 setRepeatMode(@layer.RepeatMode int repeatMode)216 public void setRepeatMode(@Player.RepeatMode int repeatMode) { 217 handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); 218 } 219 setShuffleModeEnabled(boolean shuffleModeEnabled)220 public void setShuffleModeEnabled(boolean shuffleModeEnabled) { 221 handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget(); 222 } 223 seekTo(Timeline timeline, int windowIndex, long positionUs)224 public void seekTo(Timeline timeline, int windowIndex, long positionUs) { 225 handler 226 .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) 227 .sendToTarget(); 228 } 229 setPlaybackSpeed(float playbackSpeed)230 public void setPlaybackSpeed(float playbackSpeed) { 231 handler.obtainMessage(MSG_SET_PLAYBACK_SPEED, playbackSpeed).sendToTarget(); 232 } 233 setSeekParameters(SeekParameters seekParameters)234 public void setSeekParameters(SeekParameters seekParameters) { 235 handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget(); 236 } 237 stop(boolean reset)238 public void stop(boolean reset) { 239 handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); 240 } 241 setMediaSources( List<MediaSourceList.MediaSourceHolder> mediaSources, int windowIndex, long positionUs, ShuffleOrder shuffleOrder)242 public void setMediaSources( 243 List<MediaSourceList.MediaSourceHolder> mediaSources, 244 int windowIndex, 245 long positionUs, 246 ShuffleOrder shuffleOrder) { 247 handler 248 .obtainMessage( 249 MSG_SET_MEDIA_SOURCES, 250 new MediaSourceListUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs)) 251 .sendToTarget(); 252 } 253 addMediaSources( int index, List<MediaSourceList.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder)254 public void addMediaSources( 255 int index, List<MediaSourceList.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) { 256 handler 257 .obtainMessage( 258 MSG_ADD_MEDIA_SOURCES, 259 index, 260 /* ignored */ 0, 261 new MediaSourceListUpdateMessage( 262 mediaSources, 263 shuffleOrder, 264 /* windowIndex= */ C.INDEX_UNSET, 265 /* positionUs= */ C.TIME_UNSET)) 266 .sendToTarget(); 267 } 268 removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)269 public void removeMediaSources(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) { 270 handler 271 .obtainMessage(MSG_REMOVE_MEDIA_SOURCES, fromIndex, toIndex, shuffleOrder) 272 .sendToTarget(); 273 } 274 moveMediaSources( int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder)275 public void moveMediaSources( 276 int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { 277 MoveMediaItemsMessage moveMediaItemsMessage = 278 new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder); 279 handler.obtainMessage(MSG_MOVE_MEDIA_SOURCES, moveMediaItemsMessage).sendToTarget(); 280 } 281 setShuffleOrder(ShuffleOrder shuffleOrder)282 public void setShuffleOrder(ShuffleOrder shuffleOrder) { 283 handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget(); 284 } 285 286 @Override sendMessage(PlayerMessage message)287 public synchronized void sendMessage(PlayerMessage message) { 288 if (released || !internalPlaybackThread.isAlive()) { 289 Log.w(TAG, "Ignoring messages sent after release."); 290 message.markAsProcessed(/* isDelivered= */ false); 291 return; 292 } 293 handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget(); 294 } 295 setForegroundMode(boolean foregroundMode)296 public synchronized void setForegroundMode(boolean foregroundMode) { 297 if (released || !internalPlaybackThread.isAlive()) { 298 return; 299 } 300 if (foregroundMode) { 301 handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget(); 302 } else { 303 AtomicBoolean processedFlag = new AtomicBoolean(); 304 handler 305 .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag) 306 .sendToTarget(); 307 boolean wasInterrupted = false; 308 while (!processedFlag.get()) { 309 try { 310 wait(); 311 } catch (InterruptedException e) { 312 wasInterrupted = true; 313 } 314 } 315 if (wasInterrupted) { 316 // Restore the interrupted status. 317 Thread.currentThread().interrupt(); 318 } 319 } 320 } 321 release()322 public synchronized boolean release() { 323 if (released || !internalPlaybackThread.isAlive()) { 324 return true; 325 } 326 327 handler.sendEmptyMessage(MSG_RELEASE); 328 try { 329 if (releaseTimeoutMs > 0) { 330 waitUntilReleased(releaseTimeoutMs); 331 } else { 332 waitUntilReleased(); 333 } 334 } catch (InterruptedException e) { 335 Thread.currentThread().interrupt(); 336 } 337 338 return released; 339 } 340 getPlaybackLooper()341 public Looper getPlaybackLooper() { 342 return internalPlaybackThread.getLooper(); 343 } 344 345 // Playlist.PlaylistInfoRefreshListener implementation. 346 347 @Override onPlaylistUpdateRequested()348 public void onPlaylistUpdateRequested() { 349 handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED); 350 } 351 352 // MediaPeriod.Callback implementation. 353 354 @Override onPrepared(MediaPeriod source)355 public void onPrepared(MediaPeriod source) { 356 handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget(); 357 } 358 359 @Override onContinueLoadingRequested(MediaPeriod source)360 public void onContinueLoadingRequested(MediaPeriod source) { 361 handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget(); 362 } 363 364 // TrackSelector.InvalidationListener implementation. 365 366 @Override onTrackSelectionsInvalidated()367 public void onTrackSelectionsInvalidated() { 368 handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); 369 } 370 371 // DefaultMediaClock.PlaybackSpeedListener implementation. 372 373 @Override onPlaybackSpeedChanged(float playbackSpeed)374 public void onPlaybackSpeedChanged(float playbackSpeed) { 375 sendPlaybackSpeedChangedInternal(playbackSpeed, /* acknowledgeCommand= */ false); 376 } 377 378 // Handler.Callback implementation. 379 380 @Override handleMessage(Message msg)381 public boolean handleMessage(Message msg) { 382 try { 383 switch (msg.what) { 384 case MSG_PREPARE: 385 prepareInternal(); 386 break; 387 case MSG_SET_PLAY_WHEN_READY: 388 setPlayWhenReadyInternal( 389 /* playWhenReady= */ msg.arg1 != 0, 390 /* playbackSuppressionReason= */ msg.arg2, 391 /* operationAck= */ true, 392 Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); 393 break; 394 case MSG_SET_REPEAT_MODE: 395 setRepeatModeInternal(msg.arg1); 396 break; 397 case MSG_SET_SHUFFLE_ENABLED: 398 setShuffleModeEnabledInternal(msg.arg1 != 0); 399 break; 400 case MSG_DO_SOME_WORK: 401 doSomeWork(); 402 break; 403 case MSG_SEEK_TO: 404 seekToInternal((SeekPosition) msg.obj); 405 break; 406 case MSG_SET_PLAYBACK_SPEED: 407 setPlaybackSpeedInternal((Float) msg.obj); 408 break; 409 case MSG_SET_SEEK_PARAMETERS: 410 setSeekParametersInternal((SeekParameters) msg.obj); 411 break; 412 case MSG_SET_FOREGROUND_MODE: 413 setForegroundModeInternal( 414 /* foregroundMode= */ msg.arg1 != 0, /* processedFlag= */ (AtomicBoolean) msg.obj); 415 break; 416 case MSG_STOP: 417 stopInternal( 418 /* forceResetRenderers= */ false, 419 /* resetPositionAndState= */ msg.arg1 != 0, 420 /* acknowledgeStop= */ true); 421 break; 422 case MSG_PERIOD_PREPARED: 423 handlePeriodPrepared((MediaPeriod) msg.obj); 424 break; 425 case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: 426 handleContinueLoadingRequested((MediaPeriod) msg.obj); 427 break; 428 case MSG_TRACK_SELECTION_INVALIDATED: 429 reselectTracksInternal(); 430 break; 431 case MSG_PLAYBACK_SPEED_CHANGED_INTERNAL: 432 handlePlaybackSpeed((Float) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); 433 break; 434 case MSG_SEND_MESSAGE: 435 sendMessageInternal((PlayerMessage) msg.obj); 436 break; 437 case MSG_SEND_MESSAGE_TO_TARGET_THREAD: 438 sendMessageToTargetThread((PlayerMessage) msg.obj); 439 break; 440 case MSG_SET_MEDIA_SOURCES: 441 setMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj); 442 break; 443 case MSG_ADD_MEDIA_SOURCES: 444 addMediaItemsInternal((MediaSourceListUpdateMessage) msg.obj, msg.arg1); 445 break; 446 case MSG_MOVE_MEDIA_SOURCES: 447 moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj); 448 break; 449 case MSG_REMOVE_MEDIA_SOURCES: 450 removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj); 451 break; 452 case MSG_SET_SHUFFLE_ORDER: 453 setShuffleOrderInternal((ShuffleOrder) msg.obj); 454 break; 455 case MSG_PLAYLIST_UPDATE_REQUESTED: 456 mediaSourceListUpdateRequestedInternal(); 457 break; 458 case MSG_SET_PAUSE_AT_END_OF_WINDOW: 459 setPauseAtEndOfWindowInternal(msg.arg1 != 0); 460 break; 461 case MSG_RELEASE: 462 releaseInternal(); 463 // Return immediately to not send playback info updates after release. 464 return true; 465 default: 466 return false; 467 } 468 maybeNotifyPlaybackInfoChanged(); 469 } catch (ExoPlaybackException e) { 470 Log.e(TAG, "Playback error", e); 471 stopInternal( 472 /* forceResetRenderers= */ true, 473 /* resetPositionAndState= */ false, 474 /* acknowledgeStop= */ false); 475 playbackInfo = playbackInfo.copyWithPlaybackError(e); 476 maybeNotifyPlaybackInfoChanged(); 477 } catch (IOException e) { 478 ExoPlaybackException error = ExoPlaybackException.createForSource(e); 479 Log.e(TAG, "Playback error", error); 480 stopInternal( 481 /* forceResetRenderers= */ false, 482 /* resetPositionAndState= */ false, 483 /* acknowledgeStop= */ false); 484 playbackInfo = playbackInfo.copyWithPlaybackError(error); 485 maybeNotifyPlaybackInfoChanged(); 486 } catch (RuntimeException | OutOfMemoryError e) { 487 ExoPlaybackException error = 488 e instanceof OutOfMemoryError 489 ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) 490 : ExoPlaybackException.createForUnexpected((RuntimeException) e); 491 Log.e(TAG, "Playback error", error); 492 stopInternal( 493 /* forceResetRenderers= */ true, 494 /* resetPositionAndState= */ false, 495 /* acknowledgeStop= */ false); 496 playbackInfo = playbackInfo.copyWithPlaybackError(error); 497 maybeNotifyPlaybackInfoChanged(); 498 } 499 return true; 500 } 501 502 // Private methods. 503 504 /** 505 * Blocks the current thread until {@link #releaseInternal()} is executed on the playback Thread. 506 * 507 * <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to 508 * complete, this method will delay throwing the {@link InterruptedException} to ensure that the 509 * underlying resources have been released, and will an {@link InterruptedException} <b>after</b> 510 * {@link #releaseInternal()} is complete. 511 * 512 * @throws {@link InterruptedException} if the current Thread was interrupted while waiting for 513 * {@link #releaseInternal()} to complete. 514 */ waitUntilReleased()515 private synchronized void waitUntilReleased() throws InterruptedException { 516 InterruptedException interruptedException = null; 517 while (!released) { 518 try { 519 wait(); 520 } catch (InterruptedException e) { 521 interruptedException = e; 522 } 523 } 524 525 if (interruptedException != null) { 526 throw interruptedException; 527 } 528 } 529 530 /** 531 * Blocks the current thread until {@link #releaseInternal()} is performed on the playback Thread 532 * or the specified amount of time has elapsed. 533 * 534 * <p>If the current thread is interrupted while waiting for {@link #releaseInternal()} to 535 * complete, this method will delay throwing the {@link InterruptedException} to ensure that the 536 * underlying resources have been released or the operation timed out, and will throw an {@link 537 * InterruptedException} afterwards. 538 * 539 * @param timeoutMs the time in milliseconds to wait for {@link #releaseInternal()} to complete. 540 * @throws {@link InterruptedException} if the current Thread was interrupted while waiting for 541 * {@link #releaseInternal()} to complete. 542 */ waitUntilReleased(long timeoutMs)543 private synchronized void waitUntilReleased(long timeoutMs) throws InterruptedException { 544 long deadlineMs = clock.elapsedRealtime() + timeoutMs; 545 long remainingMs = timeoutMs; 546 InterruptedException interruptedException = null; 547 while (!released && remainingMs > 0) { 548 try { 549 wait(remainingMs); 550 } catch (InterruptedException e) { 551 interruptedException = e; 552 } 553 remainingMs = deadlineMs - clock.elapsedRealtime(); 554 } 555 556 if (interruptedException != null) { 557 throw interruptedException; 558 } 559 } 560 setState(int state)561 private void setState(int state) { 562 if (playbackInfo.playbackState != state) { 563 playbackInfo = playbackInfo.copyWithPlaybackState(state); 564 } 565 } 566 maybeNotifyPlaybackInfoChanged()567 private void maybeNotifyPlaybackInfoChanged() { 568 playbackInfoUpdate.setPlaybackInfo(playbackInfo); 569 if (playbackInfoUpdate.hasPendingChange) { 570 eventHandler.obtainMessage(MSG_PLAYBACK_INFO_CHANGED, playbackInfoUpdate).sendToTarget(); 571 playbackInfoUpdate = new PlaybackInfoUpdate(playbackInfo); 572 } 573 } 574 prepareInternal()575 private void prepareInternal() { 576 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 577 resetInternal( 578 /* resetRenderers= */ false, 579 /* resetPosition= */ false, 580 /* releaseMediaSourceList= */ false, 581 /* clearMediaSourceList= */ false, 582 /* resetError= */ true); 583 loadControl.onPrepared(); 584 setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING); 585 mediaSourceList.prepare(bandwidthMeter.getTransferListener()); 586 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 587 } 588 setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage)589 private void setMediaItemsInternal(MediaSourceListUpdateMessage mediaSourceListUpdateMessage) 590 throws ExoPlaybackException { 591 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 592 if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) { 593 pendingInitialSeekPosition = 594 new SeekPosition( 595 new MediaSourceList.PlaylistTimeline( 596 mediaSourceListUpdateMessage.mediaSourceHolders, 597 mediaSourceListUpdateMessage.shuffleOrder), 598 mediaSourceListUpdateMessage.windowIndex, 599 mediaSourceListUpdateMessage.positionUs); 600 } 601 Timeline timeline = 602 mediaSourceList.setMediaSources( 603 mediaSourceListUpdateMessage.mediaSourceHolders, 604 mediaSourceListUpdateMessage.shuffleOrder); 605 handleMediaSourceListInfoRefreshed(timeline); 606 } 607 addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex)608 private void addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex) 609 throws ExoPlaybackException { 610 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 611 Timeline timeline = 612 mediaSourceList.addMediaSources( 613 insertionIndex == C.INDEX_UNSET ? mediaSourceList.getSize() : insertionIndex, 614 addMessage.mediaSourceHolders, 615 addMessage.shuffleOrder); 616 handleMediaSourceListInfoRefreshed(timeline); 617 } 618 moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)619 private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage) 620 throws ExoPlaybackException { 621 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 622 Timeline timeline = 623 mediaSourceList.moveMediaSourceRange( 624 moveMediaItemsMessage.fromIndex, 625 moveMediaItemsMessage.toIndex, 626 moveMediaItemsMessage.newFromIndex, 627 moveMediaItemsMessage.shuffleOrder); 628 handleMediaSourceListInfoRefreshed(timeline); 629 } 630 removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)631 private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) 632 throws ExoPlaybackException { 633 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 634 Timeline timeline = mediaSourceList.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder); 635 handleMediaSourceListInfoRefreshed(timeline); 636 } 637 mediaSourceListUpdateRequestedInternal()638 private void mediaSourceListUpdateRequestedInternal() throws ExoPlaybackException { 639 handleMediaSourceListInfoRefreshed(mediaSourceList.createTimeline()); 640 } 641 setShuffleOrderInternal(ShuffleOrder shuffleOrder)642 private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException { 643 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 644 Timeline timeline = mediaSourceList.setShuffleOrder(shuffleOrder); 645 handleMediaSourceListInfoRefreshed(timeline); 646 } 647 setPlayWhenReadyInternal( boolean playWhenReady, @PlaybackSuppressionReason int playbackSuppressionReason, boolean operationAck, @Player.PlayWhenReadyChangeReason int reason)648 private void setPlayWhenReadyInternal( 649 boolean playWhenReady, 650 @PlaybackSuppressionReason int playbackSuppressionReason, 651 boolean operationAck, 652 @Player.PlayWhenReadyChangeReason int reason) 653 throws ExoPlaybackException { 654 playbackInfoUpdate.incrementPendingOperationAcks(operationAck ? 1 : 0); 655 playbackInfoUpdate.setPlayWhenReadyChangeReason(reason); 656 playbackInfo = playbackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason); 657 rebuffering = false; 658 if (!shouldPlayWhenReady()) { 659 stopRenderers(); 660 updatePlaybackPositions(); 661 } else { 662 if (playbackInfo.playbackState == Player.STATE_READY) { 663 startRenderers(); 664 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 665 } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { 666 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 667 } 668 } 669 } 670 setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow)671 private void setPauseAtEndOfWindowInternal(boolean pauseAtEndOfWindow) 672 throws ExoPlaybackException { 673 this.pauseAtEndOfWindow = pauseAtEndOfWindow; 674 if (queue.getReadingPeriod() != queue.getPlayingPeriod()) { 675 seekToCurrentPosition(/* sendDiscontinuity= */ true); 676 } 677 resetPendingPauseAtEndOfPeriod(); 678 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 679 } 680 setRepeatModeInternal(@layer.RepeatMode int repeatMode)681 private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) 682 throws ExoPlaybackException { 683 this.repeatMode = repeatMode; 684 if (!queue.updateRepeatMode(playbackInfo.timeline, repeatMode)) { 685 seekToCurrentPosition(/* sendDiscontinuity= */ true); 686 } 687 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 688 } 689 setShuffleModeEnabledInternal(boolean shuffleModeEnabled)690 private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) 691 throws ExoPlaybackException { 692 this.shuffleModeEnabled = shuffleModeEnabled; 693 if (!queue.updateShuffleModeEnabled(playbackInfo.timeline, shuffleModeEnabled)) { 694 seekToCurrentPosition(/* sendDiscontinuity= */ true); 695 } 696 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 697 } 698 seekToCurrentPosition(boolean sendDiscontinuity)699 private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException { 700 // Renderers may have read from a period that's been removed. Seek back to the current 701 // position of the playing period to make sure none of the removed period is played. 702 MediaPeriodId periodId = queue.getPlayingPeriod().info.id; 703 long newPositionUs = 704 seekToPeriodPosition( 705 periodId, 706 playbackInfo.positionUs, 707 /* forceDisableRenderers= */ true, 708 /* forceBufferingState= */ false); 709 if (newPositionUs != playbackInfo.positionUs) { 710 playbackInfo = 711 handlePositionDiscontinuity( 712 periodId, newPositionUs, playbackInfo.requestedContentPositionUs); 713 if (sendDiscontinuity) { 714 playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); 715 } 716 } 717 } 718 startRenderers()719 private void startRenderers() throws ExoPlaybackException { 720 rebuffering = false; 721 mediaClock.start(); 722 for (Renderer renderer : renderers) { 723 if (isRendererEnabled(renderer)) { 724 renderer.start(); 725 } 726 } 727 } 728 stopRenderers()729 private void stopRenderers() throws ExoPlaybackException { 730 mediaClock.stop(); 731 for (Renderer renderer : renderers) { 732 if (isRendererEnabled(renderer)) { 733 ensureStopped(renderer); 734 } 735 } 736 } 737 updatePlaybackPositions()738 private void updatePlaybackPositions() throws ExoPlaybackException { 739 MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 740 if (playingPeriodHolder == null) { 741 return; 742 } 743 744 // Update the playback position. 745 long discontinuityPositionUs = 746 playingPeriodHolder.prepared 747 ? playingPeriodHolder.mediaPeriod.readDiscontinuity() 748 : C.TIME_UNSET; 749 if (discontinuityPositionUs != C.TIME_UNSET) { 750 resetRendererPosition(discontinuityPositionUs); 751 // A MediaPeriod may report a discontinuity at the current playback position to ensure the 752 // renderers are flushed. Only report the discontinuity externally if the position changed. 753 if (discontinuityPositionUs != playbackInfo.positionUs) { 754 playbackInfo = 755 handlePositionDiscontinuity( 756 playbackInfo.periodId, 757 discontinuityPositionUs, 758 playbackInfo.requestedContentPositionUs); 759 playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); 760 } 761 } else { 762 rendererPositionUs = 763 mediaClock.syncAndGetPositionUs( 764 /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); 765 long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); 766 maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); 767 playbackInfo.positionUs = periodPositionUs; 768 } 769 770 // Update the buffered position and total buffered duration. 771 MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); 772 playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs(); 773 playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs(); 774 } 775 doSomeWork()776 private void doSomeWork() throws ExoPlaybackException, IOException { 777 long operationStartTimeMs = clock.uptimeMillis(); 778 updatePeriods(); 779 780 if (playbackInfo.playbackState == Player.STATE_IDLE 781 || playbackInfo.playbackState == Player.STATE_ENDED) { 782 // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume. 783 handler.removeMessages(MSG_DO_SOME_WORK); 784 return; 785 } 786 787 @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 788 if (playingPeriodHolder == null) { 789 // We're still waiting until the playing period is available. 790 scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); 791 return; 792 } 793 794 TraceUtil.beginSection("doSomeWork"); 795 796 updatePlaybackPositions(); 797 798 boolean renderersEnded = true; 799 boolean renderersAllowPlayback = true; 800 if (playingPeriodHolder.prepared) { 801 long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; 802 playingPeriodHolder.mediaPeriod.discardBuffer( 803 playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); 804 for (int i = 0; i < renderers.length; i++) { 805 Renderer renderer = renderers[i]; 806 if (!isRendererEnabled(renderer)) { 807 continue; 808 } 809 // TODO: Each renderer should return the maximum delay before which it wishes to be called 810 // again. The minimum of these values should then be used as the delay before the next 811 // invocation of this method. 812 renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); 813 renderersEnded = renderersEnded && renderer.isEnded(); 814 // Determine whether the renderer allows playback to continue. Playback can continue if the 815 // renderer is ready or ended. Also continue playback if the renderer is reading ahead into 816 // the next stream or is waiting for the next stream. This is to avoid getting stuck if 817 // tracks in the current period have uneven durations and are still being read by another 818 // renderer. See: https://github.com/google/ExoPlayer/issues/1874. 819 boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); 820 boolean isWaitingForNextStream = 821 !isReadingAhead 822 && playingPeriodHolder.getNext() != null 823 && renderer.hasReadStreamToEnd(); 824 boolean allowsPlayback = 825 isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); 826 renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; 827 if (!allowsPlayback) { 828 renderer.maybeThrowStreamError(); 829 } 830 } 831 } else { 832 playingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); 833 } 834 835 long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; 836 boolean finishedRendering = 837 renderersEnded 838 && playingPeriodHolder.prepared 839 && (playingPeriodDurationUs == C.TIME_UNSET 840 || playingPeriodDurationUs <= playbackInfo.positionUs); 841 if (finishedRendering && pendingPauseAtEndOfPeriod) { 842 pendingPauseAtEndOfPeriod = false; 843 setPlayWhenReadyInternal( 844 /* playWhenReady= */ false, 845 playbackInfo.playbackSuppressionReason, 846 /* operationAck= */ false, 847 Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM); 848 } 849 if (finishedRendering && playingPeriodHolder.info.isFinal) { 850 setState(Player.STATE_ENDED); 851 stopRenderers(); 852 } else if (playbackInfo.playbackState == Player.STATE_BUFFERING 853 && shouldTransitionToReadyState(renderersAllowPlayback)) { 854 setState(Player.STATE_READY); 855 if (shouldPlayWhenReady()) { 856 startRenderers(); 857 } 858 } else if (playbackInfo.playbackState == Player.STATE_READY 859 && !(enabledRendererCount == 0 ? isTimelineReady() : renderersAllowPlayback)) { 860 rebuffering = shouldPlayWhenReady(); 861 setState(Player.STATE_BUFFERING); 862 stopRenderers(); 863 } 864 865 if (playbackInfo.playbackState == Player.STATE_BUFFERING) { 866 for (int i = 0; i < renderers.length; i++) { 867 if (isRendererEnabled(renderers[i]) 868 && renderers[i].getStream() == playingPeriodHolder.sampleStreams[i]) { 869 renderers[i].maybeThrowStreamError(); 870 } 871 } 872 if (throwWhenStuckBuffering 873 && !playbackInfo.isLoading 874 && playbackInfo.totalBufferedDurationUs < 500_000 875 && isLoadingPossible()) { 876 // Throw if the LoadControl prevents loading even if the buffer is empty or almost empty. We 877 // can't compare against 0 to account for small differences between the renderer position 878 // and buffered position in the media at the point where playback gets stuck. 879 throw new IllegalStateException("Playback stuck buffering and not loading"); 880 } 881 } 882 883 if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY) 884 || playbackInfo.playbackState == Player.STATE_BUFFERING) { 885 scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); 886 } else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { 887 scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); 888 } else { 889 handler.removeMessages(MSG_DO_SOME_WORK); 890 } 891 892 TraceUtil.endSection(); 893 } 894 scheduleNextWork(long thisOperationStartTimeMs, long intervalMs)895 private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { 896 handler.removeMessages(MSG_DO_SOME_WORK); 897 handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); 898 } 899 seekToInternal(SeekPosition seekPosition)900 private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { 901 playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); 902 903 MediaPeriodId periodId; 904 long periodPositionUs; 905 long requestedContentPosition; 906 boolean seekPositionAdjusted; 907 @Nullable 908 Pair<Object, Long> resolvedSeekPosition = 909 resolveSeekPosition( 910 playbackInfo.timeline, 911 seekPosition, 912 /* trySubsequentPeriods= */ true, 913 repeatMode, 914 shuffleModeEnabled, 915 window, 916 period); 917 if (resolvedSeekPosition == null) { 918 // The seek position was valid for the timeline that it was performed into, but the 919 // timeline has changed or is not ready and a suitable seek position could not be resolved. 920 Pair<MediaPeriodId, Long> firstPeriodAndPosition = 921 getDummyFirstMediaPeriodPosition(playbackInfo.timeline); 922 periodId = firstPeriodAndPosition.first; 923 periodPositionUs = firstPeriodAndPosition.second; 924 requestedContentPosition = C.TIME_UNSET; 925 seekPositionAdjusted = !playbackInfo.timeline.isEmpty(); 926 } else { 927 // Update the resolved seek position to take ads into account. 928 Object periodUid = resolvedSeekPosition.first; 929 long resolvedContentPosition = resolvedSeekPosition.second; 930 requestedContentPosition = 931 seekPosition.windowPositionUs == C.TIME_UNSET ? C.TIME_UNSET : resolvedContentPosition; 932 periodId = 933 queue.resolveMediaPeriodIdForAds( 934 playbackInfo.timeline, periodUid, resolvedContentPosition); 935 if (periodId.isAd()) { 936 playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period); 937 periodPositionUs = 938 period.getFirstAdIndexToPlay(periodId.adGroupIndex) == periodId.adIndexInAdGroup 939 ? period.getAdResumePositionUs() 940 : 0; 941 seekPositionAdjusted = true; 942 } else { 943 periodPositionUs = resolvedContentPosition; 944 seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; 945 } 946 } 947 948 try { 949 if (playbackInfo.timeline.isEmpty()) { 950 // Save seek position for later, as we are still waiting for a prepared source. 951 pendingInitialSeekPosition = seekPosition; 952 } else if (resolvedSeekPosition == null) { 953 // End playback, as we didn't manage to find a valid seek position. 954 if (playbackInfo.playbackState != Player.STATE_IDLE) { 955 setState(Player.STATE_ENDED); 956 } 957 resetInternal( 958 /* resetRenderers= */ false, 959 /* resetPosition= */ true, 960 /* releaseMediaSourceList= */ false, 961 /* clearMediaSourceList= */ false, 962 /* resetError= */ true); 963 } else { 964 // Execute the seek in the current media periods. 965 long newPeriodPositionUs = periodPositionUs; 966 if (periodId.equals(playbackInfo.periodId)) { 967 MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 968 if (playingPeriodHolder != null 969 && playingPeriodHolder.prepared 970 && newPeriodPositionUs != 0) { 971 newPeriodPositionUs = 972 playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( 973 newPeriodPositionUs, seekParameters); 974 } 975 if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs) 976 && (playbackInfo.playbackState == Player.STATE_BUFFERING 977 || playbackInfo.playbackState == Player.STATE_READY)) { 978 // Seek will be performed to the current position. Do nothing. 979 periodPositionUs = playbackInfo.positionUs; 980 return; 981 } 982 } 983 newPeriodPositionUs = 984 seekToPeriodPosition( 985 periodId, 986 newPeriodPositionUs, 987 /* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED); 988 seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; 989 periodPositionUs = newPeriodPositionUs; 990 } 991 } finally { 992 playbackInfo = 993 handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPosition); 994 if (seekPositionAdjusted) { 995 playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); 996 } 997 } 998 } 999 seekToPeriodPosition( MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState)1000 private long seekToPeriodPosition( 1001 MediaPeriodId periodId, long periodPositionUs, boolean forceBufferingState) 1002 throws ExoPlaybackException { 1003 // Force disable renderers if they are reading from a period other than the one being played. 1004 return seekToPeriodPosition( 1005 periodId, 1006 periodPositionUs, 1007 queue.getPlayingPeriod() != queue.getReadingPeriod(), 1008 forceBufferingState); 1009 } 1010 seekToPeriodPosition( MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers, boolean forceBufferingState)1011 private long seekToPeriodPosition( 1012 MediaPeriodId periodId, 1013 long periodPositionUs, 1014 boolean forceDisableRenderers, 1015 boolean forceBufferingState) 1016 throws ExoPlaybackException { 1017 stopRenderers(); 1018 rebuffering = false; 1019 if (forceBufferingState || playbackInfo.playbackState == Player.STATE_READY) { 1020 setState(Player.STATE_BUFFERING); 1021 } 1022 1023 // Find the requested period if it already exists. 1024 @Nullable MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); 1025 @Nullable MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; 1026 while (newPlayingPeriodHolder != null) { 1027 if (periodId.equals(newPlayingPeriodHolder.info.id)) { 1028 break; 1029 } 1030 newPlayingPeriodHolder = newPlayingPeriodHolder.getNext(); 1031 } 1032 1033 // Disable all renderers if the period being played is changing, if the seek results in negative 1034 // renderer timestamps, or if forced. 1035 if (forceDisableRenderers 1036 || oldPlayingPeriodHolder != newPlayingPeriodHolder 1037 || (newPlayingPeriodHolder != null 1038 && newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) { 1039 for (Renderer renderer : renderers) { 1040 disableRenderer(renderer); 1041 } 1042 if (newPlayingPeriodHolder != null) { 1043 // Update the queue and reenable renderers if the requested media period already exists. 1044 while (queue.getPlayingPeriod() != newPlayingPeriodHolder) { 1045 queue.advancePlayingPeriod(); 1046 } 1047 queue.removeAfter(newPlayingPeriodHolder); 1048 newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0); 1049 enableRenderers(); 1050 } 1051 } 1052 1053 // Do the actual seeking. 1054 if (newPlayingPeriodHolder != null) { 1055 queue.removeAfter(newPlayingPeriodHolder); 1056 if (!newPlayingPeriodHolder.prepared) { 1057 newPlayingPeriodHolder.info = 1058 newPlayingPeriodHolder.info.copyWithStartPositionUs(periodPositionUs); 1059 } else { 1060 if (newPlayingPeriodHolder.info.durationUs != C.TIME_UNSET 1061 && periodPositionUs >= newPlayingPeriodHolder.info.durationUs) { 1062 // Make sure seek position doesn't exceed period duration. 1063 periodPositionUs = Math.max(0, newPlayingPeriodHolder.info.durationUs - 1); 1064 } 1065 if (newPlayingPeriodHolder.hasEnabledTracks) { 1066 periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); 1067 newPlayingPeriodHolder.mediaPeriod.discardBuffer( 1068 periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); 1069 } 1070 } 1071 resetRendererPosition(periodPositionUs); 1072 maybeContinueLoading(); 1073 } else { 1074 // New period has not been prepared. 1075 queue.clear(); 1076 resetRendererPosition(periodPositionUs); 1077 } 1078 1079 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 1080 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 1081 return periodPositionUs; 1082 } 1083 resetRendererPosition(long periodPositionUs)1084 private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { 1085 MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod(); 1086 rendererPositionUs = 1087 playingMediaPeriod == null 1088 ? periodPositionUs 1089 : playingMediaPeriod.toRendererTime(periodPositionUs); 1090 mediaClock.resetPosition(rendererPositionUs); 1091 for (Renderer renderer : renderers) { 1092 if (isRendererEnabled(renderer)) { 1093 renderer.resetPosition(rendererPositionUs); 1094 } 1095 } 1096 notifyTrackSelectionDiscontinuity(); 1097 } 1098 setPlaybackSpeedInternal(float playbackSpeed)1099 private void setPlaybackSpeedInternal(float playbackSpeed) { 1100 mediaClock.setPlaybackSpeed(playbackSpeed); 1101 sendPlaybackSpeedChangedInternal(mediaClock.getPlaybackSpeed(), /* acknowledgeCommand= */ true); 1102 } 1103 setSeekParametersInternal(SeekParameters seekParameters)1104 private void setSeekParametersInternal(SeekParameters seekParameters) { 1105 this.seekParameters = seekParameters; 1106 } 1107 setForegroundModeInternal( boolean foregroundMode, @Nullable AtomicBoolean processedFlag)1108 private void setForegroundModeInternal( 1109 boolean foregroundMode, @Nullable AtomicBoolean processedFlag) { 1110 if (this.foregroundMode != foregroundMode) { 1111 this.foregroundMode = foregroundMode; 1112 if (!foregroundMode) { 1113 for (Renderer renderer : renderers) { 1114 if (!isRendererEnabled(renderer)) { 1115 renderer.reset(); 1116 } 1117 } 1118 } 1119 } 1120 if (processedFlag != null) { 1121 synchronized (this) { 1122 processedFlag.set(true); 1123 notifyAll(); 1124 } 1125 } 1126 } 1127 stopInternal( boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop)1128 private void stopInternal( 1129 boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) { 1130 resetInternal( 1131 /* resetRenderers= */ forceResetRenderers || !foregroundMode, 1132 /* resetPosition= */ resetPositionAndState, 1133 /* releaseMediaSourceList= */ true, 1134 /* clearMediaSourceList= */ resetPositionAndState, 1135 /* resetError= */ resetPositionAndState); 1136 playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0); 1137 loadControl.onStopped(); 1138 setState(Player.STATE_IDLE); 1139 } 1140 releaseInternal()1141 private void releaseInternal() { 1142 resetInternal( 1143 /* resetRenderers= */ true, 1144 /* resetPosition= */ true, 1145 /* releaseMediaSourceList= */ true, 1146 /* clearMediaSourceList= */ true, 1147 /* resetError= */ false); 1148 loadControl.onReleased(); 1149 setState(Player.STATE_IDLE); 1150 internalPlaybackThread.quit(); 1151 synchronized (this) { 1152 released = true; 1153 notifyAll(); 1154 } 1155 } 1156 resetInternal( boolean resetRenderers, boolean resetPosition, boolean releaseMediaSourceList, boolean clearMediaSourceList, boolean resetError)1157 private void resetInternal( 1158 boolean resetRenderers, 1159 boolean resetPosition, 1160 boolean releaseMediaSourceList, 1161 boolean clearMediaSourceList, 1162 boolean resetError) { 1163 handler.removeMessages(MSG_DO_SOME_WORK); 1164 rebuffering = false; 1165 mediaClock.stop(); 1166 rendererPositionUs = 0; 1167 for (Renderer renderer : renderers) { 1168 try { 1169 disableRenderer(renderer); 1170 } catch (ExoPlaybackException | RuntimeException e) { 1171 // There's nothing we can do. 1172 Log.e(TAG, "Disable failed.", e); 1173 } 1174 } 1175 if (resetRenderers) { 1176 for (Renderer renderer : renderers) { 1177 try { 1178 renderer.reset(); 1179 } catch (RuntimeException e) { 1180 // There's nothing we can do. 1181 Log.e(TAG, "Reset failed.", e); 1182 } 1183 } 1184 } 1185 enabledRendererCount = 0; 1186 1187 Timeline timeline = playbackInfo.timeline; 1188 if (clearMediaSourceList) { 1189 timeline = mediaSourceList.clear(/* shuffleOrder= */ null); 1190 for (PendingMessageInfo pendingMessageInfo : pendingMessages) { 1191 pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); 1192 } 1193 pendingMessages.clear(); 1194 nextPendingMessageIndex = 0; 1195 resetPosition = true; 1196 } 1197 MediaPeriodId mediaPeriodId = playbackInfo.periodId; 1198 long startPositionUs = playbackInfo.positionUs; 1199 long requestedContentPositionUs = 1200 shouldUseRequestedContentPosition(playbackInfo, period, window) 1201 ? playbackInfo.requestedContentPositionUs 1202 : playbackInfo.positionUs; 1203 boolean resetTrackInfo = clearMediaSourceList; 1204 if (resetPosition) { 1205 pendingInitialSeekPosition = null; 1206 Pair<MediaPeriodId, Long> firstPeriodAndPosition = getDummyFirstMediaPeriodPosition(timeline); 1207 mediaPeriodId = firstPeriodAndPosition.first; 1208 startPositionUs = firstPeriodAndPosition.second; 1209 requestedContentPositionUs = C.TIME_UNSET; 1210 if (!mediaPeriodId.equals(playbackInfo.periodId)) { 1211 resetTrackInfo = true; 1212 } 1213 } 1214 1215 queue.clear(); 1216 shouldContinueLoading = false; 1217 1218 playbackInfo = 1219 new PlaybackInfo( 1220 timeline, 1221 mediaPeriodId, 1222 requestedContentPositionUs, 1223 playbackInfo.playbackState, 1224 resetError ? null : playbackInfo.playbackError, 1225 /* isLoading= */ false, 1226 resetTrackInfo ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, 1227 resetTrackInfo ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, 1228 mediaPeriodId, 1229 playbackInfo.playWhenReady, 1230 playbackInfo.playbackSuppressionReason, 1231 startPositionUs, 1232 /* totalBufferedDurationUs= */ 0, 1233 startPositionUs); 1234 if (releaseMediaSourceList) { 1235 mediaSourceList.release(); 1236 } 1237 } 1238 getDummyFirstMediaPeriodPosition(Timeline timeline)1239 private Pair<MediaPeriodId, Long> getDummyFirstMediaPeriodPosition(Timeline timeline) { 1240 if (timeline.isEmpty()) { 1241 return Pair.create(PlaybackInfo.getDummyPeriodForEmptyTimeline(), 0L); 1242 } 1243 int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 1244 Pair<Object, Long> firstPeriodAndPosition = 1245 timeline.getPeriodPosition( 1246 window, period, firstWindowIndex, /* windowPositionUs= */ C.TIME_UNSET); 1247 // Add ad metadata if any and propagate the window sequence number to new period id. 1248 MediaPeriodId firstPeriodId = 1249 queue.resolveMediaPeriodIdForAds( 1250 timeline, firstPeriodAndPosition.first, /* positionUs= */ 0); 1251 long positionUs = firstPeriodAndPosition.second; 1252 if (firstPeriodId.isAd()) { 1253 timeline.getPeriodByUid(firstPeriodId.periodUid, period); 1254 positionUs = 1255 firstPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(firstPeriodId.adGroupIndex) 1256 ? period.getAdResumePositionUs() 1257 : 0; 1258 } 1259 return Pair.create(firstPeriodId, positionUs); 1260 } 1261 sendMessageInternal(PlayerMessage message)1262 private void sendMessageInternal(PlayerMessage message) throws ExoPlaybackException { 1263 if (message.getPositionMs() == C.TIME_UNSET) { 1264 // If no delivery time is specified, trigger immediate message delivery. 1265 sendMessageToTarget(message); 1266 } else if (playbackInfo.timeline.isEmpty()) { 1267 // Still waiting for initial timeline to resolve position. 1268 pendingMessages.add(new PendingMessageInfo(message)); 1269 } else { 1270 PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message); 1271 if (resolvePendingMessagePosition( 1272 pendingMessageInfo, 1273 /* newTimeline= */ playbackInfo.timeline, 1274 /* previousTimeline= */ playbackInfo.timeline, 1275 repeatMode, 1276 shuffleModeEnabled, 1277 window, 1278 period)) { 1279 pendingMessages.add(pendingMessageInfo); 1280 // Ensure new message is inserted according to playback order. 1281 Collections.sort(pendingMessages); 1282 } else { 1283 message.markAsProcessed(/* isDelivered= */ false); 1284 } 1285 } 1286 } 1287 sendMessageToTarget(PlayerMessage message)1288 private void sendMessageToTarget(PlayerMessage message) throws ExoPlaybackException { 1289 if (message.getHandler().getLooper() == handler.getLooper()) { 1290 deliverMessage(message); 1291 if (playbackInfo.playbackState == Player.STATE_READY 1292 || playbackInfo.playbackState == Player.STATE_BUFFERING) { 1293 // The message may have caused something to change that now requires us to do work. 1294 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 1295 } 1296 } else { 1297 handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget(); 1298 } 1299 } 1300 sendMessageToTargetThread(final PlayerMessage message)1301 private void sendMessageToTargetThread(final PlayerMessage message) { 1302 Handler handler = message.getHandler(); 1303 if (!handler.getLooper().getThread().isAlive()) { 1304 Log.w("TAG", "Trying to send message on a dead thread."); 1305 message.markAsProcessed(/* isDelivered= */ false); 1306 return; 1307 } 1308 handler.post( 1309 () -> { 1310 try { 1311 deliverMessage(message); 1312 } catch (ExoPlaybackException e) { 1313 Log.e(TAG, "Unexpected error delivering message on external thread.", e); 1314 throw new RuntimeException(e); 1315 } 1316 }); 1317 } 1318 deliverMessage(PlayerMessage message)1319 private void deliverMessage(PlayerMessage message) throws ExoPlaybackException { 1320 if (message.isCanceled()) { 1321 return; 1322 } 1323 try { 1324 message.getTarget().handleMessage(message.getType(), message.getPayload()); 1325 } finally { 1326 message.markAsProcessed(/* isDelivered= */ true); 1327 } 1328 } 1329 resolvePendingMessagePositions(Timeline newTimeline, Timeline previousTimeline)1330 private void resolvePendingMessagePositions(Timeline newTimeline, Timeline previousTimeline) { 1331 if (newTimeline.isEmpty() && previousTimeline.isEmpty()) { 1332 // Keep all messages unresolved until we have a non-empty timeline. 1333 return; 1334 } 1335 for (int i = pendingMessages.size() - 1; i >= 0; i--) { 1336 if (!resolvePendingMessagePosition( 1337 pendingMessages.get(i), 1338 newTimeline, 1339 previousTimeline, 1340 repeatMode, 1341 shuffleModeEnabled, 1342 window, 1343 period)) { 1344 // Unable to resolve a new position for the message. Remove it. 1345 pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false); 1346 pendingMessages.remove(i); 1347 } 1348 } 1349 // Re-sort messages by playback order. 1350 Collections.sort(pendingMessages); 1351 } 1352 maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs)1353 private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) 1354 throws ExoPlaybackException { 1355 if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) { 1356 return; 1357 } 1358 // If this is the first call after resetting the renderer position, include oldPeriodPositionUs 1359 // in potential trigger positions, but make sure we deliver it only once. 1360 if (deliverPendingMessageAtStartPositionRequired) { 1361 oldPeriodPositionUs--; 1362 deliverPendingMessageAtStartPositionRequired = false; 1363 } 1364 1365 // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) 1366 int currentPeriodIndex = 1367 playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); 1368 PendingMessageInfo previousInfo = 1369 nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; 1370 while (previousInfo != null 1371 && (previousInfo.resolvedPeriodIndex > currentPeriodIndex 1372 || (previousInfo.resolvedPeriodIndex == currentPeriodIndex 1373 && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { 1374 nextPendingMessageIndex--; 1375 previousInfo = 1376 nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; 1377 } 1378 PendingMessageInfo nextInfo = 1379 nextPendingMessageIndex < pendingMessages.size() 1380 ? pendingMessages.get(nextPendingMessageIndex) 1381 : null; 1382 while (nextInfo != null 1383 && nextInfo.resolvedPeriodUid != null 1384 && (nextInfo.resolvedPeriodIndex < currentPeriodIndex 1385 || (nextInfo.resolvedPeriodIndex == currentPeriodIndex 1386 && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { 1387 nextPendingMessageIndex++; 1388 nextInfo = 1389 nextPendingMessageIndex < pendingMessages.size() 1390 ? pendingMessages.get(nextPendingMessageIndex) 1391 : null; 1392 } 1393 // Check if any message falls within the covered time span. 1394 while (nextInfo != null 1395 && nextInfo.resolvedPeriodUid != null 1396 && nextInfo.resolvedPeriodIndex == currentPeriodIndex 1397 && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs 1398 && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { 1399 try { 1400 sendMessageToTarget(nextInfo.message); 1401 } finally { 1402 if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { 1403 pendingMessages.remove(nextPendingMessageIndex); 1404 } else { 1405 nextPendingMessageIndex++; 1406 } 1407 } 1408 nextInfo = 1409 nextPendingMessageIndex < pendingMessages.size() 1410 ? pendingMessages.get(nextPendingMessageIndex) 1411 : null; 1412 } 1413 } 1414 1415 private void ensureStopped(Renderer renderer) throws ExoPlaybackException { 1416 if (renderer.getState() == Renderer.STATE_STARTED) { 1417 renderer.stop(); 1418 } 1419 } 1420 1421 private void disableRenderer(Renderer renderer) throws ExoPlaybackException { 1422 if (!isRendererEnabled(renderer)) { 1423 return; 1424 } 1425 mediaClock.onRendererDisabled(renderer); 1426 ensureStopped(renderer); 1427 renderer.disable(); 1428 enabledRendererCount--; 1429 } 1430 1431 private void reselectTracksInternal() throws ExoPlaybackException { 1432 float playbackSpeed = mediaClock.getPlaybackSpeed(); 1433 // Reselect tracks on each period in turn, until the selection changes. 1434 MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); 1435 MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); 1436 boolean selectionsChangedForReadPeriod = true; 1437 TrackSelectorResult newTrackSelectorResult; 1438 while (true) { 1439 if (periodHolder == null || !periodHolder.prepared) { 1440 // The reselection did not change any prepared periods. 1441 return; 1442 } 1443 newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline); 1444 if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) { 1445 // Selected tracks have changed for this period. 1446 break; 1447 } 1448 if (periodHolder == readingPeriodHolder) { 1449 // The track reselection didn't affect any period that has been read. 1450 selectionsChangedForReadPeriod = false; 1451 } 1452 periodHolder = periodHolder.getNext(); 1453 } 1454 1455 if (selectionsChangedForReadPeriod) { 1456 // Update streams and rebuffer for the new selection, recreating all streams if reading ahead. 1457 MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 1458 boolean recreateStreams = queue.removeAfter(playingPeriodHolder); 1459 1460 boolean[] streamResetFlags = new boolean[renderers.length]; 1461 long periodPositionUs = 1462 playingPeriodHolder.applyTrackSelection( 1463 newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags); 1464 playbackInfo = 1465 handlePositionDiscontinuity( 1466 playbackInfo.periodId, periodPositionUs, playbackInfo.requestedContentPositionUs); 1467 if (playbackInfo.playbackState != Player.STATE_ENDED 1468 && periodPositionUs != playbackInfo.positionUs) { 1469 playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); 1470 resetRendererPosition(periodPositionUs); 1471 } 1472 1473 boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; 1474 for (int i = 0; i < renderers.length; i++) { 1475 Renderer renderer = renderers[i]; 1476 rendererWasEnabledFlags[i] = isRendererEnabled(renderer); 1477 SampleStream sampleStream = playingPeriodHolder.sampleStreams[i]; 1478 if (rendererWasEnabledFlags[i]) { 1479 if (sampleStream != renderer.getStream()) { 1480 // We need to disable the renderer. 1481 disableRenderer(renderer); 1482 } else if (streamResetFlags[i]) { 1483 // The renderer will continue to consume from its current stream, but needs to be reset. 1484 renderer.resetPosition(rendererPositionUs); 1485 } 1486 } 1487 } 1488 enableRenderers(rendererWasEnabledFlags); 1489 } else { 1490 // Release and re-prepare/buffer periods after the one whose selection changed. 1491 queue.removeAfter(periodHolder); 1492 if (periodHolder.prepared) { 1493 long loadingPeriodPositionUs = 1494 Math.max( 1495 periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); 1496 periodHolder.applyTrackSelection(newTrackSelectorResult, loadingPeriodPositionUs, false); 1497 } 1498 } 1499 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ true); 1500 if (playbackInfo.playbackState != Player.STATE_ENDED) { 1501 maybeContinueLoading(); 1502 updatePlaybackPositions(); 1503 handler.sendEmptyMessage(MSG_DO_SOME_WORK); 1504 } 1505 } 1506 1507 private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { 1508 MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); 1509 while (periodHolder != null) { 1510 TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); 1511 for (TrackSelection trackSelection : trackSelections) { 1512 if (trackSelection != null) { 1513 trackSelection.onPlaybackSpeed(playbackSpeed); 1514 } 1515 } 1516 periodHolder = periodHolder.getNext(); 1517 } 1518 } 1519 1520 private void notifyTrackSelectionDiscontinuity() { 1521 MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); 1522 while (periodHolder != null) { 1523 TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); 1524 for (TrackSelection trackSelection : trackSelections) { 1525 if (trackSelection != null) { 1526 trackSelection.onDiscontinuity(); 1527 } 1528 } 1529 periodHolder = periodHolder.getNext(); 1530 } 1531 } 1532 1533 private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) { 1534 if (enabledRendererCount == 0) { 1535 // If there are no enabled renderers, determine whether we're ready based on the timeline. 1536 return isTimelineReady(); 1537 } 1538 if (!renderersReadyOrEnded) { 1539 return false; 1540 } 1541 if (!playbackInfo.isLoading) { 1542 // Renderers are ready and we're not loading. Transition to ready, since the alternative is 1543 // getting stuck waiting for additional media that's not being loaded. 1544 return true; 1545 } 1546 // Renderers are ready and we're loading. Ask the LoadControl whether to transition. 1547 MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); 1548 boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal; 1549 return bufferedToEnd 1550 || loadControl.shouldStartPlayback( 1551 getTotalBufferedDurationUs(), mediaClock.getPlaybackSpeed(), rebuffering); 1552 } 1553 1554 private boolean isTimelineReady() { 1555 MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 1556 long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; 1557 return playingPeriodHolder.prepared 1558 && (playingPeriodDurationUs == C.TIME_UNSET 1559 || playbackInfo.positionUs < playingPeriodDurationUs 1560 || !shouldPlayWhenReady()); 1561 } 1562 1563 private void maybeThrowSourceInfoRefreshError() throws IOException { 1564 MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); 1565 if (loadingPeriodHolder != null) { 1566 // Defer throwing until we read all available media periods. 1567 for (Renderer renderer : renderers) { 1568 if (isRendererEnabled(renderer) && !renderer.hasReadStreamToEnd()) { 1569 return; 1570 } 1571 } 1572 } 1573 mediaSourceList.maybeThrowSourceInfoRefreshError(); 1574 } 1575 1576 private void handleMediaSourceListInfoRefreshed(Timeline timeline) throws ExoPlaybackException { 1577 PositionUpdateForPlaylistChange positionUpdate = 1578 resolvePositionForPlaylistChange( 1579 timeline, 1580 playbackInfo, 1581 pendingInitialSeekPosition, 1582 queue, 1583 repeatMode, 1584 shuffleModeEnabled, 1585 window, 1586 period); 1587 MediaPeriodId newPeriodId = positionUpdate.periodId; 1588 long newRequestedContentPositionUs = positionUpdate.requestedContentPositionUs; 1589 boolean forceBufferingState = positionUpdate.forceBufferingState; 1590 long newPositionUs = positionUpdate.periodPositionUs; 1591 boolean periodPositionChanged = 1592 !playbackInfo.periodId.equals(newPeriodId) || newPositionUs != playbackInfo.positionUs; 1593 1594 try { 1595 if (positionUpdate.endPlayback) { 1596 if (playbackInfo.playbackState != Player.STATE_IDLE) { 1597 setState(Player.STATE_ENDED); 1598 } 1599 resetInternal( 1600 /* resetRenderers= */ false, 1601 /* resetPosition= */ false, 1602 /* releaseMediaSourceList= */ false, 1603 /* clearMediaSourceList= */ false, 1604 /* resetError= */ true); 1605 } 1606 if (!periodPositionChanged) { 1607 // We can keep the current playing period. Update the rest of the queued periods. 1608 if (!queue.updateQueuedPeriods( 1609 timeline, rendererPositionUs, getMaxRendererReadPositionUs())) { 1610 seekToCurrentPosition(/* sendDiscontinuity= */ false); 1611 } 1612 } else if (!timeline.isEmpty()) { 1613 // Something changed. Seek to new start position. 1614 @Nullable MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); 1615 while (periodHolder != null) { 1616 // Update the new playing media period info if it already exists. 1617 if (periodHolder.info.id.equals(newPeriodId)) { 1618 periodHolder.info = queue.getUpdatedMediaPeriodInfo(timeline, periodHolder.info); 1619 } 1620 periodHolder = periodHolder.getNext(); 1621 } 1622 newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState); 1623 } 1624 } finally { 1625 if (periodPositionChanged 1626 || newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) { 1627 playbackInfo = 1628 handlePositionDiscontinuity(newPeriodId, newPositionUs, newRequestedContentPositionUs); 1629 } 1630 resetPendingPauseAtEndOfPeriod(); 1631 resolvePendingMessagePositions( 1632 /* newTimeline= */ timeline, /* previousTimeline= */ playbackInfo.timeline); 1633 playbackInfo = playbackInfo.copyWithTimeline(timeline); 1634 if (!timeline.isEmpty()) { 1635 // Retain pending seek position only while the timeline is still empty. 1636 pendingInitialSeekPosition = null; 1637 } 1638 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 1639 } 1640 } 1641 1642 private long getMaxRendererReadPositionUs() { 1643 MediaPeriodHolder readingHolder = queue.getReadingPeriod(); 1644 if (readingHolder == null) { 1645 return 0; 1646 } 1647 long maxReadPositionUs = readingHolder.getRendererOffset(); 1648 if (!readingHolder.prepared) { 1649 return maxReadPositionUs; 1650 } 1651 for (int i = 0; i < renderers.length; i++) { 1652 if (!isRendererEnabled(renderers[i]) 1653 || renderers[i].getStream() != readingHolder.sampleStreams[i]) { 1654 // Ignore disabled renderers and renderers with sample streams from previous periods. 1655 continue; 1656 } 1657 long readingPositionUs = renderers[i].getReadingPositionUs(); 1658 if (readingPositionUs == C.TIME_END_OF_SOURCE) { 1659 return C.TIME_END_OF_SOURCE; 1660 } else { 1661 maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs); 1662 } 1663 } 1664 return maxReadPositionUs; 1665 } 1666 1667 private void updatePeriods() throws ExoPlaybackException, IOException { 1668 if (playbackInfo.timeline.isEmpty() || !mediaSourceList.isPrepared()) { 1669 // We're waiting to get information about periods. 1670 mediaSourceList.maybeThrowSourceInfoRefreshError(); 1671 return; 1672 } 1673 maybeUpdateLoadingPeriod(); 1674 maybeUpdateReadingPeriod(); 1675 maybeUpdateReadingRenderers(); 1676 maybeUpdatePlayingPeriod(); 1677 } 1678 1679 private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException { 1680 queue.reevaluateBuffer(rendererPositionUs); 1681 if (queue.shouldLoadNextMediaPeriod()) { 1682 MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); 1683 if (info == null) { 1684 maybeThrowSourceInfoRefreshError(); 1685 } else { 1686 MediaPeriodHolder mediaPeriodHolder = 1687 queue.enqueueNextMediaPeriodHolder( 1688 rendererCapabilities, 1689 trackSelector, 1690 loadControl.getAllocator(), 1691 mediaSourceList, 1692 info, 1693 emptyTrackSelectorResult); 1694 mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); 1695 if (queue.getPlayingPeriod() == mediaPeriodHolder) { 1696 resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime()); 1697 } 1698 handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); 1699 } 1700 } 1701 if (shouldContinueLoading) { 1702 // We should still be loading, except in the case that it's no longer possible (i.e., because 1703 // we've loaded the current playlist to the end). 1704 shouldContinueLoading = isLoadingPossible(); 1705 updateIsLoading(); 1706 } else { 1707 maybeContinueLoading(); 1708 } 1709 } 1710 1711 private void maybeUpdateReadingPeriod() { 1712 @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); 1713 if (readingPeriodHolder == null) { 1714 return; 1715 } 1716 1717 if (readingPeriodHolder.getNext() == null || pendingPauseAtEndOfPeriod) { 1718 // We don't have a successor to advance the reading period to or we want to let them end 1719 // intentionally to pause at the end of the period. 1720 if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) { 1721 for (int i = 0; i < renderers.length; i++) { 1722 Renderer renderer = renderers[i]; 1723 SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; 1724 // Defer setting the stream as final until the renderer has actually consumed the whole 1725 // stream in case of playlist changes that cause the stream to be no longer final. 1726 if (sampleStream != null 1727 && renderer.getStream() == sampleStream 1728 && renderer.hasReadStreamToEnd()) { 1729 renderer.setCurrentStreamFinal(); 1730 } 1731 } 1732 } 1733 return; 1734 } 1735 1736 if (!hasReadingPeriodFinishedReading()) { 1737 return; 1738 } 1739 1740 if (!readingPeriodHolder.getNext().prepared 1741 && rendererPositionUs < readingPeriodHolder.getNext().getStartPositionRendererTime()) { 1742 // The successor is not prepared yet and playback hasn't reached the transition point. 1743 return; 1744 } 1745 1746 TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); 1747 readingPeriodHolder = queue.advanceReadingPeriod(); 1748 TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); 1749 1750 if (readingPeriodHolder.prepared 1751 && readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { 1752 // The new period starts with a discontinuity, so the renderers will play out all data, then 1753 // be disabled and re-enabled when they start playing the next period. 1754 setAllRendererStreamsFinal(); 1755 return; 1756 } 1757 for (int i = 0; i < renderers.length; i++) { 1758 boolean oldRendererEnabled = oldTrackSelectorResult.isRendererEnabled(i); 1759 boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); 1760 if (oldRendererEnabled && !renderers[i].isCurrentStreamFinal()) { 1761 boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; 1762 RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; 1763 RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; 1764 if (!newRendererEnabled || !newConfig.equals(oldConfig) || isNoSampleRenderer) { 1765 // The renderer will be disabled when transitioning to playing the next period, because 1766 // there's no new selection, or because a configuration change is required, or because 1767 // it's a no-sample renderer for which rendererOffsetUs should be updated only when 1768 // starting to play the next period. Mark the SampleStream as final to play out any 1769 // remaining data. 1770 renderers[i].setCurrentStreamFinal(); 1771 } 1772 } 1773 } 1774 } 1775 1776 private void maybeUpdateReadingRenderers() throws ExoPlaybackException { 1777 @Nullable MediaPeriodHolder readingPeriod = queue.getReadingPeriod(); 1778 if (readingPeriod == null 1779 || queue.getPlayingPeriod() == readingPeriod 1780 || readingPeriod.allRenderersEnabled) { 1781 // Not reading ahead or all renderers updated. 1782 return; 1783 } 1784 if (replaceStreamsOrDisableRendererForTransition()) { 1785 enableRenderers(); 1786 } 1787 } 1788 1789 private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException { 1790 MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); 1791 TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); 1792 boolean needsToWaitForRendererToEnd = false; 1793 for (int i = 0; i < renderers.length; i++) { 1794 Renderer renderer = renderers[i]; 1795 if (!isRendererEnabled(renderer)) { 1796 continue; 1797 } 1798 boolean rendererIsReadingOldStream = 1799 renderer.getStream() != readingPeriodHolder.sampleStreams[i]; 1800 boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i); 1801 if (rendererShouldBeEnabled && !rendererIsReadingOldStream) { 1802 // All done. 1803 continue; 1804 } 1805 if (!renderer.isCurrentStreamFinal()) { 1806 // The renderer stream is not final, so we can replace the sample streams immediately. 1807 Format[] formats = getFormats(newTrackSelectorResult.selections.get(i)); 1808 renderer.replaceStream( 1809 formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); 1810 } else if (renderer.isEnded()) { 1811 // The renderer has finished playback, so we can disable it now. 1812 disableRenderer(renderer); 1813 } else { 1814 // We need to wait until rendering finished before disabling the renderer. 1815 needsToWaitForRendererToEnd = true; 1816 } 1817 } 1818 return !needsToWaitForRendererToEnd; 1819 } 1820 1821 private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { 1822 boolean advancedPlayingPeriod = false; 1823 while (shouldAdvancePlayingPeriod()) { 1824 if (advancedPlayingPeriod) { 1825 // If we advance more than one period at a time, notify listeners after each update. 1826 maybeNotifyPlaybackInfoChanged(); 1827 } 1828 MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); 1829 MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); 1830 playbackInfo = 1831 handlePositionDiscontinuity( 1832 newPlayingPeriodHolder.info.id, 1833 newPlayingPeriodHolder.info.startPositionUs, 1834 newPlayingPeriodHolder.info.requestedContentPositionUs); 1835 int discontinuityReason = 1836 oldPlayingPeriodHolder.info.isLastInTimelinePeriod 1837 ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION 1838 : Player.DISCONTINUITY_REASON_AD_INSERTION; 1839 playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); 1840 resetPendingPauseAtEndOfPeriod(); 1841 updatePlaybackPositions(); 1842 advancedPlayingPeriod = true; 1843 } 1844 } 1845 1846 private void resetPendingPauseAtEndOfPeriod() { 1847 @Nullable MediaPeriodHolder playingPeriod = queue.getPlayingPeriod(); 1848 pendingPauseAtEndOfPeriod = 1849 playingPeriod != null && playingPeriod.info.isLastInTimelineWindow && pauseAtEndOfWindow; 1850 } 1851 1852 private boolean shouldAdvancePlayingPeriod() { 1853 if (!shouldPlayWhenReady()) { 1854 return false; 1855 } 1856 if (pendingPauseAtEndOfPeriod) { 1857 return false; 1858 } 1859 MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 1860 if (playingPeriodHolder == null) { 1861 return false; 1862 } 1863 MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); 1864 return nextPlayingPeriodHolder != null 1865 && rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime() 1866 && nextPlayingPeriodHolder.allRenderersEnabled; 1867 } 1868 1869 private boolean hasReadingPeriodFinishedReading() { 1870 MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); 1871 if (!readingPeriodHolder.prepared) { 1872 return false; 1873 } 1874 for (int i = 0; i < renderers.length; i++) { 1875 Renderer renderer = renderers[i]; 1876 SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; 1877 if (renderer.getStream() != sampleStream 1878 || (sampleStream != null && !renderer.hasReadStreamToEnd())) { 1879 // The current reading period is still being read by at least one renderer. 1880 return false; 1881 } 1882 } 1883 return true; 1884 } 1885 1886 private void setAllRendererStreamsFinal() { 1887 for (Renderer renderer : renderers) { 1888 if (renderer.getStream() != null) { 1889 renderer.setCurrentStreamFinal(); 1890 } 1891 } 1892 } 1893 1894 private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { 1895 if (!queue.isLoading(mediaPeriod)) { 1896 // Stale event. 1897 return; 1898 } 1899 MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); 1900 loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackSpeed(), playbackInfo.timeline); 1901 updateLoadControlTrackSelection( 1902 loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); 1903 if (loadingPeriodHolder == queue.getPlayingPeriod()) { 1904 // This is the first prepared period, so update the position and the renderers. 1905 resetRendererPosition(loadingPeriodHolder.info.startPositionUs); 1906 enableRenderers(); 1907 playbackInfo = 1908 handlePositionDiscontinuity( 1909 playbackInfo.periodId, 1910 loadingPeriodHolder.info.startPositionUs, 1911 playbackInfo.requestedContentPositionUs); 1912 } 1913 maybeContinueLoading(); 1914 } 1915 1916 private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) { 1917 if (!queue.isLoading(mediaPeriod)) { 1918 // Stale event. 1919 return; 1920 } 1921 queue.reevaluateBuffer(rendererPositionUs); 1922 maybeContinueLoading(); 1923 } 1924 1925 private void handlePlaybackSpeed(float playbackSpeed, boolean acknowledgeCommand) 1926 throws ExoPlaybackException { 1927 eventHandler 1928 .obtainMessage(MSG_PLAYBACK_SPEED_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackSpeed) 1929 .sendToTarget(); 1930 updateTrackSelectionPlaybackSpeed(playbackSpeed); 1931 for (Renderer renderer : renderers) { 1932 if (renderer != null) { 1933 renderer.setOperatingRate(playbackSpeed); 1934 } 1935 } 1936 } 1937 1938 private void maybeContinueLoading() { 1939 shouldContinueLoading = shouldContinueLoading(); 1940 if (shouldContinueLoading) { 1941 queue.getLoadingPeriod().continueLoading(rendererPositionUs); 1942 } 1943 updateIsLoading(); 1944 } 1945 1946 private boolean shouldContinueLoading() { 1947 if (!isLoadingPossible()) { 1948 return false; 1949 } 1950 MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); 1951 long bufferedDurationUs = 1952 getTotalBufferedDurationUs(loadingPeriodHolder.getNextLoadPositionUs()); 1953 long playbackPositionUs = 1954 loadingPeriodHolder == queue.getPlayingPeriod() 1955 ? loadingPeriodHolder.toPeriodTime(rendererPositionUs) 1956 : loadingPeriodHolder.toPeriodTime(rendererPositionUs) 1957 - loadingPeriodHolder.info.startPositionUs; 1958 return loadControl.shouldContinueLoading( 1959 playbackPositionUs, bufferedDurationUs, mediaClock.getPlaybackSpeed()); 1960 } 1961 1962 private boolean isLoadingPossible() { 1963 MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); 1964 if (loadingPeriodHolder == null) { 1965 return false; 1966 } 1967 long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); 1968 if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { 1969 return false; 1970 } 1971 return true; 1972 } 1973 1974 private void updateIsLoading() { 1975 MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); 1976 boolean isLoading = 1977 shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading()); 1978 if (isLoading != playbackInfo.isLoading) { 1979 playbackInfo = playbackInfo.copyWithIsLoading(isLoading); 1980 } 1981 } 1982 1983 @CheckResult 1984 private PlaybackInfo handlePositionDiscontinuity( 1985 MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) { 1986 deliverPendingMessageAtStartPositionRequired = 1987 deliverPendingMessageAtStartPositionRequired 1988 || positionUs != playbackInfo.positionUs 1989 || !mediaPeriodId.equals(playbackInfo.periodId); 1990 resetPendingPauseAtEndOfPeriod(); 1991 TrackGroupArray trackGroupArray = playbackInfo.trackGroups; 1992 TrackSelectorResult trackSelectorResult = playbackInfo.trackSelectorResult; 1993 if (mediaSourceList.isPrepared()) { 1994 @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); 1995 trackGroupArray = 1996 playingPeriodHolder == null 1997 ? TrackGroupArray.EMPTY 1998 : playingPeriodHolder.getTrackGroups(); 1999 trackSelectorResult = 2000 playingPeriodHolder == null 2001 ? emptyTrackSelectorResult 2002 : playingPeriodHolder.getTrackSelectorResult(); 2003 } else if (!mediaPeriodId.equals(playbackInfo.periodId)) { 2004 // Reset previously kept track info if unprepared and the period changes. 2005 trackGroupArray = TrackGroupArray.EMPTY; 2006 trackSelectorResult = emptyTrackSelectorResult; 2007 } 2008 return playbackInfo.copyWithNewPosition( 2009 mediaPeriodId, 2010 positionUs, 2011 contentPositionUs, 2012 getTotalBufferedDurationUs(), 2013 trackGroupArray, 2014 trackSelectorResult); 2015 } 2016 2017 private void enableRenderers() throws ExoPlaybackException { 2018 enableRenderers(/* rendererWasEnabledFlags= */ new boolean[renderers.length]); 2019 } 2020 2021 private void enableRenderers(boolean[] rendererWasEnabledFlags) throws ExoPlaybackException { 2022 MediaPeriodHolder readingMediaPeriod = queue.getReadingPeriod(); 2023 TrackSelectorResult trackSelectorResult = readingMediaPeriod.getTrackSelectorResult(); 2024 // Reset all disabled renderers before enabling any new ones. This makes sure resources released 2025 // by the disabled renderers will be available to renderers that are being enabled. 2026 for (int i = 0; i < renderers.length; i++) { 2027 if (!trackSelectorResult.isRendererEnabled(i)) { 2028 renderers[i].reset(); 2029 } 2030 } 2031 // Enable the renderers. 2032 for (int i = 0; i < renderers.length; i++) { 2033 if (trackSelectorResult.isRendererEnabled(i)) { 2034 enableRenderer(i, rendererWasEnabledFlags[i]); 2035 } 2036 } 2037 readingMediaPeriod.allRenderersEnabled = true; 2038 } 2039 2040 private void enableRenderer(int rendererIndex, boolean wasRendererEnabled) 2041 throws ExoPlaybackException { 2042 Renderer renderer = renderers[rendererIndex]; 2043 if (isRendererEnabled(renderer)) { 2044 return; 2045 } 2046 MediaPeriodHolder periodHolder = queue.getReadingPeriod(); 2047 boolean mayRenderStartOfStream = periodHolder == queue.getPlayingPeriod(); 2048 TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult(); 2049 RendererConfiguration rendererConfiguration = 2050 trackSelectorResult.rendererConfigurations[rendererIndex]; 2051 TrackSelection newSelection = trackSelectorResult.selections.get(rendererIndex); 2052 Format[] formats = getFormats(newSelection); 2053 // The renderer needs enabling with its new track selection. 2054 boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY; 2055 // Consider as joining only if the renderer was previously disabled. 2056 boolean joining = !wasRendererEnabled && playing; 2057 // Enable the renderer. 2058 enabledRendererCount++; 2059 renderer.enable( 2060 rendererConfiguration, 2061 formats, 2062 periodHolder.sampleStreams[rendererIndex], 2063 rendererPositionUs, 2064 joining, 2065 mayRenderStartOfStream, 2066 periodHolder.getRendererOffset()); 2067 mediaClock.onRendererEnabled(renderer); 2068 // Start the renderer if playing. 2069 if (playing) { 2070 renderer.start(); 2071 } 2072 } 2073 2074 private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) { 2075 MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod(); 2076 MediaPeriodId loadingMediaPeriodId = 2077 loadingMediaPeriodHolder == null ? playbackInfo.periodId : loadingMediaPeriodHolder.info.id; 2078 boolean loadingMediaPeriodChanged = 2079 !playbackInfo.loadingMediaPeriodId.equals(loadingMediaPeriodId); 2080 if (loadingMediaPeriodChanged) { 2081 playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId); 2082 } 2083 playbackInfo.bufferedPositionUs = 2084 loadingMediaPeriodHolder == null 2085 ? playbackInfo.positionUs 2086 : loadingMediaPeriodHolder.getBufferedPositionUs(); 2087 playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs(); 2088 if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged) 2089 && loadingMediaPeriodHolder != null 2090 && loadingMediaPeriodHolder.prepared) { 2091 updateLoadControlTrackSelection( 2092 loadingMediaPeriodHolder.getTrackGroups(), 2093 loadingMediaPeriodHolder.getTrackSelectorResult()); 2094 } 2095 } 2096 2097 private long getTotalBufferedDurationUs() { 2098 return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs); 2099 } 2100 2101 private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) { 2102 MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); 2103 if (loadingPeriodHolder == null) { 2104 return 0; 2105 } 2106 long totalBufferedDurationUs = 2107 bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs); 2108 return Math.max(0, totalBufferedDurationUs); 2109 } 2110 2111 private void updateLoadControlTrackSelection( 2112 TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { 2113 loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); 2114 } 2115 2116 private void sendPlaybackSpeedChangedInternal(float playbackSpeed, boolean acknowledgeCommand) { 2117 handler 2118 .obtainMessage( 2119 MSG_PLAYBACK_SPEED_CHANGED_INTERNAL, acknowledgeCommand ? 1 : 0, 0, playbackSpeed) 2120 .sendToTarget(); 2121 } 2122 2123 private boolean shouldPlayWhenReady() { 2124 return playbackInfo.playWhenReady 2125 && playbackInfo.playbackSuppressionReason == Player.PLAYBACK_SUPPRESSION_REASON_NONE; 2126 } 2127 2128 private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( 2129 Timeline timeline, 2130 PlaybackInfo playbackInfo, 2131 @Nullable SeekPosition pendingInitialSeekPosition, 2132 MediaPeriodQueue queue, 2133 @RepeatMode int repeatMode, 2134 boolean shuffleModeEnabled, 2135 Timeline.Window window, 2136 Timeline.Period period) { 2137 if (timeline.isEmpty()) { 2138 return new PositionUpdateForPlaylistChange( 2139 PlaybackInfo.getDummyPeriodForEmptyTimeline(), 2140 /* periodPositionUs= */ 0, 2141 /* requestedContentPositionUs= */ C.TIME_UNSET, 2142 /* forceBufferingState= */ false, 2143 /* endPlayback= */ true); 2144 } 2145 MediaPeriodId oldPeriodId = playbackInfo.periodId; 2146 Object newPeriodUid = oldPeriodId.periodUid; 2147 boolean shouldUseRequestedContentPosition = 2148 shouldUseRequestedContentPosition(playbackInfo, period, window); 2149 long oldContentPositionUs = 2150 shouldUseRequestedContentPosition 2151 ? playbackInfo.requestedContentPositionUs 2152 : playbackInfo.positionUs; 2153 long newContentPositionUs = oldContentPositionUs; 2154 int startAtDefaultPositionWindowIndex = C.INDEX_UNSET; 2155 boolean forceBufferingState = false; 2156 boolean endPlayback = false; 2157 if (pendingInitialSeekPosition != null) { 2158 // Resolve initial seek position. 2159 @Nullable 2160 Pair<Object, Long> periodPosition = 2161 resolveSeekPosition( 2162 timeline, 2163 pendingInitialSeekPosition, 2164 /* trySubsequentPeriods= */ true, 2165 repeatMode, 2166 shuffleModeEnabled, 2167 window, 2168 period); 2169 if (periodPosition == null) { 2170 // The initial seek in the empty old timeline is invalid in the new timeline. 2171 endPlayback = true; 2172 startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 2173 } else { 2174 // The pending seek has been resolved successfully in the new timeline. 2175 if (pendingInitialSeekPosition.windowPositionUs == C.TIME_UNSET) { 2176 startAtDefaultPositionWindowIndex = 2177 timeline.getPeriodByUid(periodPosition.first, period).windowIndex; 2178 } else { 2179 newPeriodUid = periodPosition.first; 2180 newContentPositionUs = periodPosition.second; 2181 } 2182 forceBufferingState = playbackInfo.playbackState == Player.STATE_ENDED; 2183 } 2184 } else if (playbackInfo.timeline.isEmpty()) { 2185 // Resolve to default position if the old timeline is empty and no seek is requested above. 2186 startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 2187 } else if (timeline.getIndexOfPeriod(newPeriodUid) == C.INDEX_UNSET) { 2188 // The current period isn't in the new timeline. Attempt to resolve a subsequent period whose 2189 // window we can restart from. 2190 @Nullable 2191 Object subsequentPeriodUid = 2192 resolveSubsequentPeriod( 2193 window, 2194 period, 2195 repeatMode, 2196 shuffleModeEnabled, 2197 newPeriodUid, 2198 playbackInfo.timeline, 2199 timeline); 2200 if (subsequentPeriodUid == null) { 2201 // We failed to resolve a suitable restart position but the timeline is not empty. 2202 endPlayback = true; 2203 startAtDefaultPositionWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); 2204 } else { 2205 // We resolved a subsequent period. Start at the default position in the corresponding 2206 // window. 2207 startAtDefaultPositionWindowIndex = 2208 timeline.getPeriodByUid(subsequentPeriodUid, period).windowIndex; 2209 } 2210 } else if (shouldUseRequestedContentPosition) { 2211 // We previously requested a content position, but haven't used it yet. Re-resolve the 2212 // requested window position to the period uid and position in case they changed. 2213 if (oldContentPositionUs == C.TIME_UNSET) { 2214 startAtDefaultPositionWindowIndex = 2215 timeline.getPeriodByUid(newPeriodUid, period).windowIndex; 2216 } else { 2217 playbackInfo.timeline.getPeriodByUid(oldPeriodId.periodUid, period); 2218 long windowPositionUs = oldContentPositionUs + period.getPositionInWindowUs(); 2219 int windowIndex = timeline.getPeriodByUid(newPeriodUid, period).windowIndex; 2220 Pair<Object, Long> periodPosition = 2221 timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); 2222 newPeriodUid = periodPosition.first; 2223 newContentPositionUs = periodPosition.second; 2224 } 2225 } 2226 2227 // Set period uid for default positions and resolve position for ad resolution. 2228 long contentPositionForAdResolutionUs = newContentPositionUs; 2229 if (startAtDefaultPositionWindowIndex != C.INDEX_UNSET) { 2230 Pair<Object, Long> defaultPosition = 2231 timeline.getPeriodPosition( 2232 window, 2233 period, 2234 startAtDefaultPositionWindowIndex, 2235 /* windowPositionUs= */ C.TIME_UNSET); 2236 newPeriodUid = defaultPosition.first; 2237 contentPositionForAdResolutionUs = defaultPosition.second; 2238 newContentPositionUs = C.TIME_UNSET; 2239 } 2240 2241 // Ensure ad insertion metadata is up to date. 2242 MediaPeriodId periodIdWithAds = 2243 queue.resolveMediaPeriodIdForAds(timeline, newPeriodUid, contentPositionForAdResolutionUs); 2244 boolean oldAndNewPeriodIdAreSame = 2245 oldPeriodId.periodUid.equals(newPeriodUid) 2246 && !oldPeriodId.isAd() 2247 && !periodIdWithAds.isAd(); 2248 // Drop update if we keep playing the same content (MediaPeriod.periodUid are identical) and 2249 // only MediaPeriodId.nextAdGroupIndex may have changed. This postpones a potential 2250 // discontinuity until we reach the former next ad group position. 2251 MediaPeriodId newPeriodId = oldAndNewPeriodIdAreSame ? oldPeriodId : periodIdWithAds; 2252 2253 long periodPositionUs = contentPositionForAdResolutionUs; 2254 if (newPeriodId.isAd()) { 2255 if (newPeriodId.equals(oldPeriodId)) { 2256 periodPositionUs = playbackInfo.positionUs; 2257 } else { 2258 timeline.getPeriodByUid(newPeriodId.periodUid, period); 2259 periodPositionUs = 2260 newPeriodId.adIndexInAdGroup == period.getFirstAdIndexToPlay(newPeriodId.adGroupIndex) 2261 ? period.getAdResumePositionUs() 2262 : 0; 2263 } 2264 } 2265 2266 return new PositionUpdateForPlaylistChange( 2267 newPeriodId, periodPositionUs, newContentPositionUs, forceBufferingState, endPlayback); 2268 } 2269 2270 private static boolean shouldUseRequestedContentPosition( 2271 PlaybackInfo playbackInfo, Timeline.Period period, Timeline.Window window) { 2272 // Only use the actual position as content position if it's not an ad and we already have 2273 // prepared media information. Otherwise use the requested position. 2274 MediaPeriodId periodId = playbackInfo.periodId; 2275 Timeline timeline = playbackInfo.timeline; 2276 return periodId.isAd() 2277 || timeline.isEmpty() 2278 || timeline.getWindow( 2279 timeline.getPeriodByUid(periodId.periodUid, period).windowIndex, window) 2280 .isPlaceholder; 2281 } 2282 2283 /** 2284 * Updates pending message to a new timeline. 2285 * 2286 * @param pendingMessageInfo The pending message. 2287 * @param newTimeline The new timeline. 2288 * @param previousTimeline The previous timeline used to set the message positions. 2289 * @param repeatMode The current repeat mode. 2290 * @param shuffleModeEnabled The current shuffle mode. 2291 * @param window A scratch window. 2292 * @param period A scratch period. 2293 * @return Whether the message position could be resolved to the current timeline. 2294 */ 2295 private static boolean resolvePendingMessagePosition( 2296 PendingMessageInfo pendingMessageInfo, 2297 Timeline newTimeline, 2298 Timeline previousTimeline, 2299 @Player.RepeatMode int repeatMode, 2300 boolean shuffleModeEnabled, 2301 Timeline.Window window, 2302 Timeline.Period period) { 2303 if (pendingMessageInfo.resolvedPeriodUid == null) { 2304 // Position is still unresolved. Try to find window in new timeline. 2305 long requestPositionUs = 2306 pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE 2307 ? C.TIME_UNSET 2308 : C.msToUs(pendingMessageInfo.message.getPositionMs()); 2309 @Nullable 2310 Pair<Object, Long> periodPosition = 2311 resolveSeekPosition( 2312 newTimeline, 2313 new SeekPosition( 2314 pendingMessageInfo.message.getTimeline(), 2315 pendingMessageInfo.message.getWindowIndex(), 2316 requestPositionUs), 2317 /* trySubsequentPeriods= */ false, 2318 repeatMode, 2319 shuffleModeEnabled, 2320 window, 2321 period); 2322 if (periodPosition == null) { 2323 return false; 2324 } 2325 pendingMessageInfo.setResolvedPosition( 2326 /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), 2327 /* periodTimeUs= */ periodPosition.second, 2328 /* periodUid= */ periodPosition.first); 2329 if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { 2330 resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); 2331 } 2332 return true; 2333 } 2334 // Position has been resolved for a previous timeline. Try to find the updated period index. 2335 int index = newTimeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid); 2336 if (index == C.INDEX_UNSET) { 2337 return false; 2338 } 2339 if (pendingMessageInfo.message.getPositionMs() == C.TIME_END_OF_SOURCE) { 2340 // Re-resolve end of stream in case the duration changed. 2341 resolvePendingMessageEndOfStreamPosition(newTimeline, pendingMessageInfo, window, period); 2342 return true; 2343 } 2344 pendingMessageInfo.resolvedPeriodIndex = index; 2345 previousTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period); 2346 if (previousTimeline.getWindow(period.windowIndex, window).isPlaceholder) { 2347 // The position needs to be re-resolved because the window in the previous timeline wasn't 2348 // fully prepared. 2349 long windowPositionUs = 2350 pendingMessageInfo.resolvedPeriodTimeUs + period.getPositionInWindowUs(); 2351 int windowIndex = 2352 newTimeline.getPeriodByUid(pendingMessageInfo.resolvedPeriodUid, period).windowIndex; 2353 Pair<Object, Long> periodPosition = 2354 newTimeline.getPeriodPosition(window, period, windowIndex, windowPositionUs); 2355 pendingMessageInfo.setResolvedPosition( 2356 /* periodIndex= */ newTimeline.getIndexOfPeriod(periodPosition.first), 2357 /* periodTimeUs= */ periodPosition.second, 2358 /* periodUid= */ periodPosition.first); 2359 } 2360 return true; 2361 } 2362 2363 private static void resolvePendingMessageEndOfStreamPosition( 2364 Timeline timeline, 2365 PendingMessageInfo messageInfo, 2366 Timeline.Window window, 2367 Timeline.Period period) { 2368 int windowIndex = timeline.getPeriodByUid(messageInfo.resolvedPeriodUid, period).windowIndex; 2369 int lastPeriodIndex = timeline.getWindow(windowIndex, window).lastPeriodIndex; 2370 Object lastPeriodUid = timeline.getPeriod(lastPeriodIndex, period, /* setIds= */ true).uid; 2371 long positionUs = period.durationUs != C.TIME_UNSET ? period.durationUs - 1 : Long.MAX_VALUE; 2372 messageInfo.setResolvedPosition(lastPeriodIndex, positionUs, lastPeriodUid); 2373 } 2374 2375 /** 2376 * Converts a {@link SeekPosition} into the corresponding (periodUid, periodPositionUs) for the 2377 * internal timeline. 2378 * 2379 * @param seekPosition The position to resolve. 2380 * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching 2381 * period if the original period is no longer available. 2382 * @return The resolved position, or null if resolution was not successful. 2383 * @throws IllegalSeekPositionException If the window index of the seek position is outside the 2384 * bounds of the timeline. 2385 */ 2386 @Nullable 2387 private static Pair<Object, Long> resolveSeekPosition( 2388 Timeline timeline, 2389 SeekPosition seekPosition, 2390 boolean trySubsequentPeriods, 2391 @RepeatMode int repeatMode, 2392 boolean shuffleModeEnabled, 2393 Timeline.Window window, 2394 Timeline.Period period) { 2395 Timeline seekTimeline = seekPosition.timeline; 2396 if (timeline.isEmpty()) { 2397 // We don't have a valid timeline yet, so we can't resolve the position. 2398 return null; 2399 } 2400 if (seekTimeline.isEmpty()) { 2401 // The application performed a blind seek with an empty timeline (most likely based on 2402 // knowledge of what the future timeline will be). Use the internal timeline. 2403 seekTimeline = timeline; 2404 } 2405 // Map the SeekPosition to a position in the corresponding timeline. 2406 Pair<Object, Long> periodPosition; 2407 try { 2408 periodPosition = 2409 seekTimeline.getPeriodPosition( 2410 window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); 2411 } catch (IndexOutOfBoundsException e) { 2412 // The window index of the seek position was outside the bounds of the timeline. 2413 return null; 2414 } 2415 if (timeline.equals(seekTimeline)) { 2416 // Our internal timeline is the seek timeline, so the mapped position is correct. 2417 return periodPosition; 2418 } 2419 // Attempt to find the mapped period in the internal timeline. 2420 int periodIndex = timeline.getIndexOfPeriod(periodPosition.first); 2421 if (periodIndex != C.INDEX_UNSET) { 2422 // We successfully located the period in the internal timeline. 2423 seekTimeline.getPeriodByUid(periodPosition.first, period); 2424 if (seekTimeline.getWindow(period.windowIndex, window).isPlaceholder) { 2425 // The seek timeline was using a placeholder, so we need to re-resolve using the updated 2426 // timeline in case the resolved position changed. 2427 int newWindowIndex = timeline.getPeriodByUid(periodPosition.first, period).windowIndex; 2428 periodPosition = 2429 timeline.getPeriodPosition( 2430 window, period, newWindowIndex, seekPosition.windowPositionUs); 2431 } 2432 return periodPosition; 2433 } 2434 if (trySubsequentPeriods) { 2435 // Try and find a subsequent period from the seek timeline in the internal timeline. 2436 @Nullable 2437 Object periodUid = 2438 resolveSubsequentPeriod( 2439 window, 2440 period, 2441 repeatMode, 2442 shuffleModeEnabled, 2443 periodPosition.first, 2444 seekTimeline, 2445 timeline); 2446 if (periodUid != null) { 2447 // We found one. Use the default position of the corresponding window. 2448 return timeline.getPeriodPosition( 2449 window, 2450 period, 2451 timeline.getPeriodByUid(periodUid, period).windowIndex, 2452 /* windowPositionUs= */ C.TIME_UNSET); 2453 } 2454 } 2455 // We didn't find one. Give up. 2456 return null; 2457 } 2458 2459 /** 2460 * Given a period index into an old timeline, finds the first subsequent period that also exists 2461 * in a new timeline. The uid of this period in the new timeline is returned. 2462 * 2463 * @param window A {@link Timeline.Window} to be used internally. 2464 * @param period A {@link Timeline.Period} to be used internally. 2465 * @param repeatMode The repeat mode to use. 2466 * @param shuffleModeEnabled Whether the shuffle mode is enabled. 2467 * @param oldPeriodUid The index of the period in the old timeline. 2468 * @param oldTimeline The old timeline. 2469 * @param newTimeline The new timeline. 2470 * @return The uid in the new timeline of the first subsequent period, or null if no such period 2471 * was found. 2472 */ 2473 /* package */ static @Nullable Object resolveSubsequentPeriod( 2474 Timeline.Window window, 2475 Timeline.Period period, 2476 @Player.RepeatMode int repeatMode, 2477 boolean shuffleModeEnabled, 2478 Object oldPeriodUid, 2479 Timeline oldTimeline, 2480 Timeline newTimeline) { 2481 int oldPeriodIndex = oldTimeline.getIndexOfPeriod(oldPeriodUid); 2482 int newPeriodIndex = C.INDEX_UNSET; 2483 int maxIterations = oldTimeline.getPeriodCount(); 2484 for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { 2485 oldPeriodIndex = 2486 oldTimeline.getNextPeriodIndex( 2487 oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); 2488 if (oldPeriodIndex == C.INDEX_UNSET) { 2489 // We've reached the end of the old timeline. 2490 break; 2491 } 2492 newPeriodIndex = newTimeline.getIndexOfPeriod(oldTimeline.getUidOfPeriod(oldPeriodIndex)); 2493 } 2494 return newPeriodIndex == C.INDEX_UNSET ? null : newTimeline.getUidOfPeriod(newPeriodIndex); 2495 } 2496 2497 private static Format[] getFormats(TrackSelection newSelection) { 2498 // Build an array of formats contained by the selection. 2499 int length = newSelection != null ? newSelection.length() : 0; 2500 Format[] formats = new Format[length]; 2501 for (int i = 0; i < length; i++) { 2502 formats[i] = newSelection.getFormat(i); 2503 } 2504 return formats; 2505 } 2506 2507 private static boolean isRendererEnabled(Renderer renderer) { 2508 return renderer.getState() != Renderer.STATE_DISABLED; 2509 } 2510 2511 private static final class SeekPosition { 2512 2513 public final Timeline timeline; 2514 public final int windowIndex; 2515 public final long windowPositionUs; 2516 2517 public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) { 2518 this.timeline = timeline; 2519 this.windowIndex = windowIndex; 2520 this.windowPositionUs = windowPositionUs; 2521 } 2522 } 2523 2524 private static final class PositionUpdateForPlaylistChange { 2525 public final MediaPeriodId periodId; 2526 public final long periodPositionUs; 2527 public final long requestedContentPositionUs; 2528 public final boolean forceBufferingState; 2529 public final boolean endPlayback; 2530 2531 public PositionUpdateForPlaylistChange( 2532 MediaPeriodId periodId, 2533 long periodPositionUs, 2534 long requestedContentPositionUs, 2535 boolean forceBufferingState, 2536 boolean endPlayback) { 2537 this.periodId = periodId; 2538 this.periodPositionUs = periodPositionUs; 2539 this.requestedContentPositionUs = requestedContentPositionUs; 2540 this.forceBufferingState = forceBufferingState; 2541 this.endPlayback = endPlayback; 2542 } 2543 } 2544 2545 private static final class PendingMessageInfo implements Comparable<PendingMessageInfo> { 2546 2547 public final PlayerMessage message; 2548 2549 public int resolvedPeriodIndex; 2550 public long resolvedPeriodTimeUs; 2551 @Nullable public Object resolvedPeriodUid; 2552 2553 public PendingMessageInfo(PlayerMessage message) { 2554 this.message = message; 2555 } 2556 2557 public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { 2558 resolvedPeriodIndex = periodIndex; 2559 resolvedPeriodTimeUs = periodTimeUs; 2560 resolvedPeriodUid = periodUid; 2561 } 2562 2563 @Override 2564 public int compareTo(PendingMessageInfo other) { 2565 if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { 2566 // PendingMessageInfos with a resolved period position are always smaller. 2567 return resolvedPeriodUid != null ? -1 : 1; 2568 } 2569 if (resolvedPeriodUid == null) { 2570 // Don't sort message with unresolved positions. 2571 return 0; 2572 } 2573 // Sort resolved media times by period index and then by period position. 2574 int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; 2575 if (comparePeriodIndex != 0) { 2576 return comparePeriodIndex; 2577 } 2578 return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); 2579 } 2580 } 2581 2582 private static final class MediaSourceListUpdateMessage { 2583 2584 private final List<MediaSourceList.MediaSourceHolder> mediaSourceHolders; 2585 private final ShuffleOrder shuffleOrder; 2586 private final int windowIndex; 2587 private final long positionUs; 2588 2589 private MediaSourceListUpdateMessage( 2590 List<MediaSourceList.MediaSourceHolder> mediaSourceHolders, 2591 ShuffleOrder shuffleOrder, 2592 int windowIndex, 2593 long positionUs) { 2594 this.mediaSourceHolders = mediaSourceHolders; 2595 this.shuffleOrder = shuffleOrder; 2596 this.windowIndex = windowIndex; 2597 this.positionUs = positionUs; 2598 } 2599 } 2600 2601 private static class MoveMediaItemsMessage { 2602 2603 public final int fromIndex; 2604 public final int toIndex; 2605 public final int newFromIndex; 2606 public final ShuffleOrder shuffleOrder; 2607 2608 public MoveMediaItemsMessage( 2609 int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) { 2610 this.fromIndex = fromIndex; 2611 this.toIndex = toIndex; 2612 this.newFromIndex = newFromIndex; 2613 this.shuffleOrder = shuffleOrder; 2614 } 2615 } 2616 2617 /* package */ static final class PlaybackInfoUpdate { 2618 2619 private boolean hasPendingChange; 2620 2621 public PlaybackInfo playbackInfo; 2622 public int operationAcks; 2623 public boolean positionDiscontinuity; 2624 @DiscontinuityReason public int discontinuityReason; 2625 public boolean hasPlayWhenReadyChangeReason; 2626 @PlayWhenReadyChangeReason public int playWhenReadyChangeReason; 2627 2628 public PlaybackInfoUpdate(PlaybackInfo playbackInfo) { 2629 this.playbackInfo = playbackInfo; 2630 } 2631 2632 public void incrementPendingOperationAcks(int operationAcks) { 2633 hasPendingChange |= operationAcks > 0; 2634 this.operationAcks += operationAcks; 2635 } 2636 2637 public void setPlaybackInfo(PlaybackInfo playbackInfo) { 2638 hasPendingChange |= this.playbackInfo != playbackInfo; 2639 this.playbackInfo = playbackInfo; 2640 } 2641 2642 public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { 2643 if (positionDiscontinuity 2644 && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { 2645 // We always prefer non-internal discontinuity reasons. We also assume that we won't report 2646 // more than one non-internal discontinuity per message iteration. 2647 Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); 2648 return; 2649 } 2650 hasPendingChange = true; 2651 positionDiscontinuity = true; 2652 this.discontinuityReason = discontinuityReason; 2653 } 2654 2655 public void setPlayWhenReadyChangeReason( 2656 @PlayWhenReadyChangeReason int playWhenReadyChangeReason) { 2657 hasPendingChange = true; 2658 this.hasPlayWhenReadyChangeReason = true; 2659 this.playWhenReadyChangeReason = playWhenReadyChangeReason; 2660 } 2661 } 2662 } 2663