• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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