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.source.chunk; 17 18 import android.os.Looper; 19 import androidx.annotation.Nullable; 20 import com.google.android.exoplayer2.C; 21 import com.google.android.exoplayer2.Format; 22 import com.google.android.exoplayer2.FormatHolder; 23 import com.google.android.exoplayer2.SeekParameters; 24 import com.google.android.exoplayer2.decoder.DecoderInputBuffer; 25 import com.google.android.exoplayer2.drm.DrmSession; 26 import com.google.android.exoplayer2.drm.DrmSessionManager; 27 import com.google.android.exoplayer2.source.LoadEventInfo; 28 import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 29 import com.google.android.exoplayer2.source.SampleQueue; 30 import com.google.android.exoplayer2.source.SampleStream; 31 import com.google.android.exoplayer2.source.SequenceableLoader; 32 import com.google.android.exoplayer2.upstream.Allocator; 33 import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; 34 import com.google.android.exoplayer2.upstream.Loader; 35 import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; 36 import com.google.android.exoplayer2.util.Assertions; 37 import com.google.android.exoplayer2.util.Log; 38 import com.google.android.exoplayer2.util.Util; 39 import java.io.IOException; 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.List; 43 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 44 45 /** 46 * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}. 47 * May also be configured to expose additional embedded {@link SampleStream}s. 48 */ 49 public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader, 50 Loader.Callback<Chunk>, Loader.ReleaseCallback { 51 52 /** A callback to be notified when a sample stream has finished being released. */ 53 public interface ReleaseCallback<T extends ChunkSource> { 54 55 /** 56 * Called when the {@link ChunkSampleStream} has finished being released. 57 * 58 * @param chunkSampleStream The released sample stream. 59 */ onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream)60 void onSampleStreamReleased(ChunkSampleStream<T> chunkSampleStream); 61 } 62 63 private static final String TAG = "ChunkSampleStream"; 64 65 public final int primaryTrackType; 66 67 private final int[] embeddedTrackTypes; 68 private final Format[] embeddedTrackFormats; 69 private final boolean[] embeddedTracksSelected; 70 private final T chunkSource; 71 private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback; 72 private final EventDispatcher eventDispatcher; 73 private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; 74 private final Loader loader; 75 private final ChunkHolder nextChunkHolder; 76 private final ArrayList<BaseMediaChunk> mediaChunks; 77 private final List<BaseMediaChunk> readOnlyMediaChunks; 78 private final SampleQueue primarySampleQueue; 79 private final SampleQueue[] embeddedSampleQueues; 80 private final BaseMediaChunkOutput chunkOutput; 81 82 private @MonotonicNonNull Format primaryDownstreamTrackFormat; 83 @Nullable private ReleaseCallback<T> releaseCallback; 84 private long pendingResetPositionUs; 85 private long lastSeekPositionUs; 86 private int nextNotifyPrimaryFormatMediaChunkIndex; 87 88 /* package */ long decodeOnlyUntilPositionUs; 89 /* package */ boolean loadingFinished; 90 91 /** 92 * Constructs an instance. 93 * 94 * @param primaryTrackType The type of the primary track. One of the {@link C} {@code 95 * TRACK_TYPE_*} constants. 96 * @param embeddedTrackTypes The types of any embedded tracks, or null. 97 * @param embeddedTrackFormats The formats of the embedded tracks, or null. 98 * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. 99 * @param callback An {@link Callback} for the stream. 100 * @param allocator An {@link Allocator} from which allocations can be obtained. 101 * @param positionUs The position from which to start loading media. 102 * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} 103 * from. 104 * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. 105 * @param eventDispatcher A dispatcher to notify of events. 106 */ ChunkSampleStream( int primaryTrackType, @Nullable int[] embeddedTrackTypes, @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback<ChunkSampleStream<T>> callback, Allocator allocator, long positionUs, DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher)107 public ChunkSampleStream( 108 int primaryTrackType, 109 @Nullable int[] embeddedTrackTypes, 110 @Nullable Format[] embeddedTrackFormats, 111 T chunkSource, 112 Callback<ChunkSampleStream<T>> callback, 113 Allocator allocator, 114 long positionUs, 115 DrmSessionManager drmSessionManager, 116 LoadErrorHandlingPolicy loadErrorHandlingPolicy, 117 EventDispatcher eventDispatcher) { 118 this.primaryTrackType = primaryTrackType; 119 this.embeddedTrackTypes = embeddedTrackTypes == null ? new int[0] : embeddedTrackTypes; 120 this.embeddedTrackFormats = embeddedTrackFormats == null ? new Format[0] : embeddedTrackFormats; 121 this.chunkSource = chunkSource; 122 this.callback = callback; 123 this.eventDispatcher = eventDispatcher; 124 this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; 125 loader = new Loader("Loader:ChunkSampleStream"); 126 nextChunkHolder = new ChunkHolder(); 127 mediaChunks = new ArrayList<>(); 128 readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); 129 130 int embeddedTrackCount = this.embeddedTrackTypes.length; 131 embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; 132 embeddedTracksSelected = new boolean[embeddedTrackCount]; 133 int[] trackTypes = new int[1 + embeddedTrackCount]; 134 SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; 135 136 primarySampleQueue = 137 new SampleQueue( 138 allocator, 139 /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), 140 drmSessionManager, 141 eventDispatcher); 142 trackTypes[0] = primaryTrackType; 143 sampleQueues[0] = primarySampleQueue; 144 145 for (int i = 0; i < embeddedTrackCount; i++) { 146 SampleQueue sampleQueue = 147 new SampleQueue( 148 allocator, 149 /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), 150 DrmSessionManager.getDummyDrmSessionManager(), 151 eventDispatcher); 152 embeddedSampleQueues[i] = sampleQueue; 153 sampleQueues[i + 1] = sampleQueue; 154 trackTypes[i + 1] = this.embeddedTrackTypes[i]; 155 } 156 157 chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); 158 pendingResetPositionUs = positionUs; 159 lastSeekPositionUs = positionUs; 160 } 161 162 /** 163 * Discards buffered media up to the specified position. 164 * 165 * @param positionUs The position to discard up to, in microseconds. 166 * @param toKeyframe If true then for each track discards samples up to the keyframe before or at 167 * the specified position, rather than any sample before or at that position. 168 */ discardBuffer(long positionUs, boolean toKeyframe)169 public void discardBuffer(long positionUs, boolean toKeyframe) { 170 if (isPendingReset()) { 171 return; 172 } 173 int oldFirstSampleIndex = primarySampleQueue.getFirstIndex(); 174 primarySampleQueue.discardTo(positionUs, toKeyframe, true); 175 int newFirstSampleIndex = primarySampleQueue.getFirstIndex(); 176 if (newFirstSampleIndex > oldFirstSampleIndex) { 177 long discardToUs = primarySampleQueue.getFirstTimestampUs(); 178 for (int i = 0; i < embeddedSampleQueues.length; i++) { 179 embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]); 180 } 181 } 182 discardDownstreamMediaChunks(newFirstSampleIndex); 183 } 184 185 /** 186 * Selects the embedded track, returning a new {@link EmbeddedSampleStream} from which the track's 187 * samples can be consumed. {@link EmbeddedSampleStream#release()} must be called on the returned 188 * stream when the track is no longer required, and before calling this method again to obtain 189 * another stream for the same track. 190 * 191 * @param positionUs The current playback position in microseconds. 192 * @param trackType The type of the embedded track to enable. 193 * @return The {@link EmbeddedSampleStream} for the embedded track. 194 */ selectEmbeddedTrack(long positionUs, int trackType)195 public EmbeddedSampleStream selectEmbeddedTrack(long positionUs, int trackType) { 196 for (int i = 0; i < embeddedSampleQueues.length; i++) { 197 if (embeddedTrackTypes[i] == trackType) { 198 Assertions.checkState(!embeddedTracksSelected[i]); 199 embeddedTracksSelected[i] = true; 200 embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); 201 return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); 202 } 203 } 204 // Should never happen. 205 throw new IllegalStateException(); 206 } 207 208 /** 209 * Returns the {@link ChunkSource} used by this stream. 210 */ getChunkSource()211 public T getChunkSource() { 212 return chunkSource; 213 } 214 215 /** 216 * Returns an estimate of the position up to which data is buffered. 217 * 218 * @return An estimate of the absolute position in microseconds up to which data is buffered, or 219 * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. 220 */ 221 @Override getBufferedPositionUs()222 public long getBufferedPositionUs() { 223 if (loadingFinished) { 224 return C.TIME_END_OF_SOURCE; 225 } else if (isPendingReset()) { 226 return pendingResetPositionUs; 227 } else { 228 long bufferedPositionUs = lastSeekPositionUs; 229 BaseMediaChunk lastMediaChunk = getLastMediaChunk(); 230 BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk 231 : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; 232 if (lastCompletedMediaChunk != null) { 233 bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); 234 } 235 return Math.max(bufferedPositionUs, primarySampleQueue.getLargestQueuedTimestampUs()); 236 } 237 } 238 239 /** 240 * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used 241 * as sync points. 242 * 243 * @param positionUs The seek position in microseconds. 244 * @param seekParameters Parameters that control how the seek is performed. 245 * @return The adjusted seek position, in microseconds. 246 */ getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters)247 public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { 248 return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); 249 } 250 251 /** 252 * Seeks to the specified position in microseconds. 253 * 254 * @param positionUs The seek position in microseconds. 255 */ seekToUs(long positionUs)256 public void seekToUs(long positionUs) { 257 lastSeekPositionUs = positionUs; 258 if (isPendingReset()) { 259 // A reset is already pending. We only need to update its position. 260 pendingResetPositionUs = positionUs; 261 return; 262 } 263 264 // Detect whether the seek is to the start of a chunk that's at least partially buffered. 265 @Nullable BaseMediaChunk seekToMediaChunk = null; 266 for (int i = 0; i < mediaChunks.size(); i++) { 267 BaseMediaChunk mediaChunk = mediaChunks.get(i); 268 long mediaChunkStartTimeUs = mediaChunk.startTimeUs; 269 if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) { 270 seekToMediaChunk = mediaChunk; 271 break; 272 } else if (mediaChunkStartTimeUs > positionUs) { 273 // We're not going to find a chunk with a matching start time. 274 break; 275 } 276 } 277 278 // See if we can seek inside the primary sample queue. 279 boolean seekInsideBuffer; 280 if (seekToMediaChunk != null) { 281 // When seeking to the start of a chunk we use the index of the first sample in the chunk 282 // rather than the seek position. This ensures we seek to the keyframe at the start of the 283 // chunk even if the sample timestamps are slightly offset from the chunk start times. 284 seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); 285 decodeOnlyUntilPositionUs = 0; 286 } else { 287 seekInsideBuffer = 288 primarySampleQueue.seekTo( 289 positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); 290 decodeOnlyUntilPositionUs = lastSeekPositionUs; 291 } 292 293 if (seekInsideBuffer) { 294 // We can seek inside the buffer. 295 nextNotifyPrimaryFormatMediaChunkIndex = 296 primarySampleIndexToMediaChunkIndex( 297 primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0); 298 // Seek the embedded sample queues. 299 for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { 300 embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); 301 } 302 } else { 303 // We can't seek inside the buffer, and so need to reset. 304 pendingResetPositionUs = positionUs; 305 loadingFinished = false; 306 mediaChunks.clear(); 307 nextNotifyPrimaryFormatMediaChunkIndex = 0; 308 if (loader.isLoading()) { 309 loader.cancelLoading(); 310 } else { 311 loader.clearFatalError(); 312 primarySampleQueue.reset(); 313 for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { 314 embeddedSampleQueue.reset(); 315 } 316 } 317 } 318 } 319 320 /** 321 * Releases the stream. 322 * 323 * <p>This method should be called when the stream is no longer required. Either this method or 324 * {@link #release(ReleaseCallback)} can be used to release this stream. 325 */ release()326 public void release() { 327 release(null); 328 } 329 330 /** 331 * Releases the stream. 332 * 333 * <p>This method should be called when the stream is no longer required. Either this method or 334 * {@link #release()} can be used to release this stream. 335 * 336 * @param callback An optional callback to be called on the loading thread once the loader has 337 * been released. 338 */ release(@ullable ReleaseCallback<T> callback)339 public void release(@Nullable ReleaseCallback<T> callback) { 340 this.releaseCallback = callback; 341 // Discard as much as we can synchronously. 342 primarySampleQueue.preRelease(); 343 for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { 344 embeddedSampleQueue.preRelease(); 345 } 346 loader.release(this); 347 } 348 349 @Override onLoaderReleased()350 public void onLoaderReleased() { 351 primarySampleQueue.release(); 352 for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { 353 embeddedSampleQueue.release(); 354 } 355 if (releaseCallback != null) { 356 releaseCallback.onSampleStreamReleased(this); 357 } 358 } 359 360 // SampleStream implementation. 361 362 @Override isReady()363 public boolean isReady() { 364 return !isPendingReset() && primarySampleQueue.isReady(loadingFinished); 365 } 366 367 @Override maybeThrowError()368 public void maybeThrowError() throws IOException { 369 loader.maybeThrowError(); 370 primarySampleQueue.maybeThrowError(); 371 if (!loader.isLoading()) { 372 chunkSource.maybeThrowError(); 373 } 374 } 375 376 @Override readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired)377 public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 378 boolean formatRequired) { 379 if (isPendingReset()) { 380 return C.RESULT_NOTHING_READ; 381 } 382 maybeNotifyPrimaryTrackFormatChanged(); 383 384 return primarySampleQueue.read( 385 formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); 386 } 387 388 @Override skipData(long positionUs)389 public int skipData(long positionUs) { 390 if (isPendingReset()) { 391 return 0; 392 } 393 int skipCount; 394 if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { 395 skipCount = primarySampleQueue.advanceToEnd(); 396 } else { 397 skipCount = primarySampleQueue.advanceTo(positionUs); 398 } 399 maybeNotifyPrimaryTrackFormatChanged(); 400 return skipCount; 401 } 402 403 // Loader.Callback implementation. 404 405 @Override onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs)406 public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) { 407 chunkSource.onChunkLoadCompleted(loadable); 408 eventDispatcher.loadCompleted( 409 new LoadEventInfo( 410 loadable.dataSpec, 411 loadable.getUri(), 412 loadable.getResponseHeaders(), 413 elapsedRealtimeMs, 414 loadDurationMs, 415 loadable.bytesLoaded()), 416 loadable.type, 417 primaryTrackType, 418 loadable.trackFormat, 419 loadable.trackSelectionReason, 420 loadable.trackSelectionData, 421 loadable.startTimeUs, 422 loadable.endTimeUs); 423 callback.onContinueLoadingRequested(this); 424 } 425 426 @Override onLoadCanceled( Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released)427 public void onLoadCanceled( 428 Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { 429 eventDispatcher.loadCanceled( 430 new LoadEventInfo( 431 loadable.dataSpec, 432 loadable.getUri(), 433 loadable.getResponseHeaders(), 434 elapsedRealtimeMs, 435 loadDurationMs, 436 loadable.bytesLoaded()), 437 loadable.type, 438 primaryTrackType, 439 loadable.trackFormat, 440 loadable.trackSelectionReason, 441 loadable.trackSelectionData, 442 loadable.startTimeUs, 443 loadable.endTimeUs); 444 if (!released) { 445 primarySampleQueue.reset(); 446 for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { 447 embeddedSampleQueue.reset(); 448 } 449 callback.onContinueLoadingRequested(this); 450 } 451 } 452 453 @Override onLoadError( Chunk loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error, int errorCount)454 public LoadErrorAction onLoadError( 455 Chunk loadable, 456 long elapsedRealtimeMs, 457 long loadDurationMs, 458 IOException error, 459 int errorCount) { 460 long bytesLoaded = loadable.bytesLoaded(); 461 boolean isMediaChunk = isMediaChunk(loadable); 462 int lastChunkIndex = mediaChunks.size() - 1; 463 boolean cancelable = 464 bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); 465 long blacklistDurationMs = 466 cancelable 467 ? loadErrorHandlingPolicy.getBlacklistDurationMsFor( 468 loadable.type, loadDurationMs, error, errorCount) 469 : C.TIME_UNSET; 470 @Nullable LoadErrorAction loadErrorAction = null; 471 if (chunkSource.onChunkLoadError(loadable, cancelable, error, blacklistDurationMs)) { 472 if (cancelable) { 473 loadErrorAction = Loader.DONT_RETRY; 474 if (isMediaChunk) { 475 BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); 476 Assertions.checkState(removed == loadable); 477 if (mediaChunks.isEmpty()) { 478 pendingResetPositionUs = lastSeekPositionUs; 479 } 480 } 481 } else { 482 Log.w(TAG, "Ignoring attempt to cancel non-cancelable load."); 483 } 484 } 485 486 if (loadErrorAction == null) { 487 // The load was not cancelled. Either the load must be retried or the error propagated. 488 long retryDelayMs = 489 loadErrorHandlingPolicy.getRetryDelayMsFor( 490 loadable.type, loadDurationMs, error, errorCount); 491 loadErrorAction = 492 retryDelayMs != C.TIME_UNSET 493 ? Loader.createRetryAction(/* resetErrorCount= */ false, retryDelayMs) 494 : Loader.DONT_RETRY_FATAL; 495 } 496 497 boolean canceled = !loadErrorAction.isRetry(); 498 eventDispatcher.loadError( 499 new LoadEventInfo( 500 loadable.dataSpec, 501 loadable.getUri(), 502 loadable.getResponseHeaders(), 503 elapsedRealtimeMs, 504 loadDurationMs, 505 bytesLoaded), 506 loadable.type, 507 primaryTrackType, 508 loadable.trackFormat, 509 loadable.trackSelectionReason, 510 loadable.trackSelectionData, 511 loadable.startTimeUs, 512 loadable.endTimeUs, 513 error, 514 canceled); 515 if (canceled) { 516 callback.onContinueLoadingRequested(this); 517 } 518 return loadErrorAction; 519 } 520 521 // SequenceableLoader implementation 522 523 @Override continueLoading(long positionUs)524 public boolean continueLoading(long positionUs) { 525 if (loadingFinished || loader.isLoading() || loader.hasFatalError()) { 526 return false; 527 } 528 529 boolean pendingReset = isPendingReset(); 530 List<BaseMediaChunk> chunkQueue; 531 long loadPositionUs; 532 if (pendingReset) { 533 chunkQueue = Collections.emptyList(); 534 loadPositionUs = pendingResetPositionUs; 535 } else { 536 chunkQueue = readOnlyMediaChunks; 537 loadPositionUs = getLastMediaChunk().endTimeUs; 538 } 539 chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); 540 boolean endOfStream = nextChunkHolder.endOfStream; 541 @Nullable Chunk loadable = nextChunkHolder.chunk; 542 nextChunkHolder.clear(); 543 544 if (endOfStream) { 545 pendingResetPositionUs = C.TIME_UNSET; 546 loadingFinished = true; 547 return true; 548 } 549 550 if (loadable == null) { 551 return false; 552 } 553 554 if (isMediaChunk(loadable)) { 555 BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; 556 if (pendingReset) { 557 boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; 558 // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. 559 decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; 560 pendingResetPositionUs = C.TIME_UNSET; 561 } 562 mediaChunk.init(chunkOutput); 563 mediaChunks.add(mediaChunk); 564 } else if (loadable instanceof InitializationChunk) { 565 ((InitializationChunk) loadable).init(chunkOutput); 566 } 567 long elapsedRealtimeMs = 568 loader.startLoading( 569 loadable, this, loadErrorHandlingPolicy.getMinimumLoadableRetryCount(loadable.type)); 570 eventDispatcher.loadStarted( 571 new LoadEventInfo(loadable.dataSpec, elapsedRealtimeMs), 572 loadable.type, 573 primaryTrackType, 574 loadable.trackFormat, 575 loadable.trackSelectionReason, 576 loadable.trackSelectionData, 577 loadable.startTimeUs, 578 loadable.endTimeUs); 579 return true; 580 } 581 582 @Override isLoading()583 public boolean isLoading() { 584 return loader.isLoading(); 585 } 586 587 @Override getNextLoadPositionUs()588 public long getNextLoadPositionUs() { 589 if (isPendingReset()) { 590 return pendingResetPositionUs; 591 } else { 592 return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; 593 } 594 } 595 596 @Override reevaluateBuffer(long positionUs)597 public void reevaluateBuffer(long positionUs) { 598 if (loader.isLoading() || loader.hasFatalError() || isPendingReset()) { 599 return; 600 } 601 602 int currentQueueSize = mediaChunks.size(); 603 int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); 604 if (currentQueueSize <= preferredQueueSize) { 605 return; 606 } 607 608 int newQueueSize = currentQueueSize; 609 for (int i = preferredQueueSize; i < currentQueueSize; i++) { 610 if (!haveReadFromMediaChunk(i)) { 611 newQueueSize = i; 612 break; 613 } 614 } 615 if (newQueueSize == currentQueueSize) { 616 return; 617 } 618 619 long endTimeUs = getLastMediaChunk().endTimeUs; 620 BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); 621 if (mediaChunks.isEmpty()) { 622 pendingResetPositionUs = lastSeekPositionUs; 623 } 624 loadingFinished = false; 625 eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); 626 } 627 628 // Internal methods 629 isMediaChunk(Chunk chunk)630 private boolean isMediaChunk(Chunk chunk) { 631 return chunk instanceof BaseMediaChunk; 632 } 633 634 /** Returns whether samples have been read from media chunk at given index. */ haveReadFromMediaChunk(int mediaChunkIndex)635 private boolean haveReadFromMediaChunk(int mediaChunkIndex) { 636 BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); 637 if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) { 638 return true; 639 } 640 for (int i = 0; i < embeddedSampleQueues.length; i++) { 641 if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) { 642 return true; 643 } 644 } 645 return false; 646 } 647 isPendingReset()648 /* package */ boolean isPendingReset() { 649 return pendingResetPositionUs != C.TIME_UNSET; 650 } 651 discardDownstreamMediaChunks(int discardToSampleIndex)652 private void discardDownstreamMediaChunks(int discardToSampleIndex) { 653 int discardToMediaChunkIndex = 654 primarySampleIndexToMediaChunkIndex(discardToSampleIndex, /* minChunkIndex= */ 0); 655 // Don't discard any chunks that we haven't reported the primary format change for yet. 656 discardToMediaChunkIndex = 657 Math.min(discardToMediaChunkIndex, nextNotifyPrimaryFormatMediaChunkIndex); 658 if (discardToMediaChunkIndex > 0) { 659 Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex); 660 nextNotifyPrimaryFormatMediaChunkIndex -= discardToMediaChunkIndex; 661 } 662 } 663 maybeNotifyPrimaryTrackFormatChanged()664 private void maybeNotifyPrimaryTrackFormatChanged() { 665 int readSampleIndex = primarySampleQueue.getReadIndex(); 666 int notifyToMediaChunkIndex = 667 primarySampleIndexToMediaChunkIndex( 668 readSampleIndex, /* minChunkIndex= */ nextNotifyPrimaryFormatMediaChunkIndex - 1); 669 while (nextNotifyPrimaryFormatMediaChunkIndex <= notifyToMediaChunkIndex) { 670 maybeNotifyPrimaryTrackFormatChanged(nextNotifyPrimaryFormatMediaChunkIndex++); 671 } 672 } 673 maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex)674 private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) { 675 BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex); 676 Format trackFormat = currentChunk.trackFormat; 677 if (!trackFormat.equals(primaryDownstreamTrackFormat)) { 678 eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, 679 currentChunk.trackSelectionReason, currentChunk.trackSelectionData, 680 currentChunk.startTimeUs); 681 } 682 primaryDownstreamTrackFormat = trackFormat; 683 } 684 685 /** 686 * Returns the media chunk index corresponding to a given primary sample index. 687 * 688 * @param primarySampleIndex The primary sample index for which the corresponding media chunk 689 * index is required. 690 * @param minChunkIndex A minimum chunk index from which to start searching, or -1 if no hint can 691 * be provided. 692 * @return The index of the media chunk corresponding to the sample index, or -1 if the list of 693 * media chunks is empty, or {@code minChunkIndex} if the sample precedes the first chunk in 694 * the search (i.e. the chunk at {@code minChunkIndex}, or at index 0 if {@code minChunkIndex} 695 * is -1. 696 */ primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex)697 private int primarySampleIndexToMediaChunkIndex(int primarySampleIndex, int minChunkIndex) { 698 for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) { 699 if (mediaChunks.get(i).getFirstSampleIndex(0) > primarySampleIndex) { 700 return i - 1; 701 } 702 } 703 return mediaChunks.size() - 1; 704 } 705 getLastMediaChunk()706 private BaseMediaChunk getLastMediaChunk() { 707 return mediaChunks.get(mediaChunks.size() - 1); 708 } 709 710 /** 711 * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample 712 * queues. 713 * 714 * @param chunkIndex The index of the first chunk to discard. 715 * @return The chunk at given index. 716 */ discardUpstreamMediaChunksFromIndex(int chunkIndex)717 private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) { 718 BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex); 719 Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size()); 720 nextNotifyPrimaryFormatMediaChunkIndex = 721 Math.max(nextNotifyPrimaryFormatMediaChunkIndex, mediaChunks.size()); 722 primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); 723 for (int i = 0; i < embeddedSampleQueues.length; i++) { 724 embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); 725 } 726 return firstRemovedChunk; 727 } 728 729 /** 730 * A {@link SampleStream} embedded in a {@link ChunkSampleStream}. 731 */ 732 public final class EmbeddedSampleStream implements SampleStream { 733 734 public final ChunkSampleStream<T> parent; 735 736 private final SampleQueue sampleQueue; 737 private final int index; 738 739 private boolean notifiedDownstreamFormat; 740 EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index)741 public EmbeddedSampleStream(ChunkSampleStream<T> parent, SampleQueue sampleQueue, int index) { 742 this.parent = parent; 743 this.sampleQueue = sampleQueue; 744 this.index = index; 745 } 746 747 @Override isReady()748 public boolean isReady() { 749 return !isPendingReset() && sampleQueue.isReady(loadingFinished); 750 } 751 752 @Override skipData(long positionUs)753 public int skipData(long positionUs) { 754 if (isPendingReset()) { 755 return 0; 756 } 757 maybeNotifyDownstreamFormat(); 758 int skipCount; 759 if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { 760 skipCount = sampleQueue.advanceToEnd(); 761 } else { 762 skipCount = sampleQueue.advanceTo(positionUs); 763 } 764 return skipCount; 765 } 766 767 @Override maybeThrowError()768 public void maybeThrowError() { 769 // Do nothing. Errors will be thrown from the primary stream. 770 } 771 772 @Override readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired)773 public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, 774 boolean formatRequired) { 775 if (isPendingReset()) { 776 return C.RESULT_NOTHING_READ; 777 } 778 maybeNotifyDownstreamFormat(); 779 return sampleQueue.read( 780 formatHolder, 781 buffer, 782 formatRequired, 783 loadingFinished, 784 decodeOnlyUntilPositionUs); 785 } 786 release()787 public void release() { 788 Assertions.checkState(embeddedTracksSelected[index]); 789 embeddedTracksSelected[index] = false; 790 } 791 maybeNotifyDownstreamFormat()792 private void maybeNotifyDownstreamFormat() { 793 if (!notifiedDownstreamFormat) { 794 eventDispatcher.downstreamFormatChanged( 795 embeddedTrackTypes[index], 796 embeddedTrackFormats[index], 797 C.SELECTION_REASON_UNKNOWN, 798 /* trackSelectionData= */ null, 799 lastSeekPositionUs); 800 notifiedDownstreamFormat = true; 801 } 802 } 803 } 804 805 } 806