• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.analytics.AnalyticsCollector;
21 import com.google.android.exoplayer2.drm.DrmSessionEventListener;
22 import com.google.android.exoplayer2.source.LoadEventInfo;
23 import com.google.android.exoplayer2.source.MaskingMediaPeriod;
24 import com.google.android.exoplayer2.source.MaskingMediaSource;
25 import com.google.android.exoplayer2.source.MediaLoadData;
26 import com.google.android.exoplayer2.source.MediaPeriod;
27 import com.google.android.exoplayer2.source.MediaSource;
28 import com.google.android.exoplayer2.source.MediaSourceEventListener;
29 import com.google.android.exoplayer2.source.ShuffleOrder;
30 import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
31 import com.google.android.exoplayer2.upstream.Allocator;
32 import com.google.android.exoplayer2.upstream.TransferListener;
33 import com.google.android.exoplayer2.util.Assertions;
34 import com.google.android.exoplayer2.util.Util;
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.IdentityHashMap;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 
47 /**
48  * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
49  * during playback. It is valid for the same {@link MediaSource} instance to be present more than
50  * once in the playlist.
51  *
52  * <p>With the exception of the constructor, all methods are called on the playback thread.
53  */
54 /* package */ class MediaSourceList {
55 
56   /** Listener for source events. */
57   public interface MediaSourceListInfoRefreshListener {
58 
59     /**
60      * Called when the timeline of a media item has changed and a new timeline that reflects the
61      * current playlist state needs to be created by calling {@link #createTimeline()}.
62      *
63      * <p>Called on the playback thread.
64      */
onPlaylistUpdateRequested()65     void onPlaylistUpdateRequested();
66   }
67 
68   private final List<MediaSourceHolder> mediaSourceHolders;
69   private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
70   private final Map<Object, MediaSourceHolder> mediaSourceByUid;
71   private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
72   private final MediaSourceEventListener.EventDispatcher eventDispatcher;
73   private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
74   private final Set<MediaSourceHolder> enabledMediaSourceHolders;
75 
76   private ShuffleOrder shuffleOrder;
77   private boolean isPrepared;
78 
79   @Nullable private TransferListener mediaTransferListener;
80 
81   @SuppressWarnings("initialization")
MediaSourceList(MediaSourceListInfoRefreshListener listener)82   public MediaSourceList(MediaSourceListInfoRefreshListener listener) {
83     mediaSourceListInfoListener = listener;
84     shuffleOrder = new DefaultShuffleOrder(0);
85     mediaSourceByMediaPeriod = new IdentityHashMap<>();
86     mediaSourceByUid = new HashMap<>();
87     mediaSourceHolders = new ArrayList<>();
88     eventDispatcher = new MediaSourceEventListener.EventDispatcher();
89     childSources = new HashMap<>();
90     enabledMediaSourceHolders = new HashSet<>();
91   }
92 
93   /**
94    * Sets the media sources replacing any sources previously contained in the playlist.
95    *
96    * @param holders The list of {@link MediaSourceHolder}s to set.
97    * @param shuffleOrder The new shuffle order.
98    * @return The new {@link Timeline}.
99    */
setMediaSources( List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder)100   public final Timeline setMediaSources(
101       List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
102     removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
103     return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder);
104   }
105 
106   /**
107    * Adds multiple {@link MediaSourceHolder}s to the playlist.
108    *
109    * @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
110    *     must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
111    * @param holders A list of {@link MediaSourceHolder}s to be added.
112    * @param shuffleOrder The new shuffle order.
113    * @return The new {@link Timeline}.
114    */
addMediaSources( int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder)115   public final Timeline addMediaSources(
116       int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
117     if (!holders.isEmpty()) {
118       this.shuffleOrder = shuffleOrder;
119       for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) {
120         MediaSourceHolder holder = holders.get(insertionIndex - index);
121         if (insertionIndex > 0) {
122           MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1);
123           Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
124           holder.reset(
125               /* firstWindowIndexInChild= */ previousHolder.firstWindowIndexInChild
126                   + previousTimeline.getWindowCount());
127         } else {
128           holder.reset(/* firstWindowIndexInChild= */ 0);
129         }
130         Timeline newTimeline = holder.mediaSource.getTimeline();
131         correctOffsets(
132             /* startIndex= */ insertionIndex,
133             /* windowOffsetUpdate= */ newTimeline.getWindowCount());
134         mediaSourceHolders.add(insertionIndex, holder);
135         mediaSourceByUid.put(holder.uid, holder);
136         if (isPrepared) {
137           prepareChildSource(holder);
138           if (mediaSourceByMediaPeriod.isEmpty()) {
139             enabledMediaSourceHolders.add(holder);
140           } else {
141             disableChildSource(holder);
142           }
143         }
144       }
145     }
146     return createTimeline();
147   }
148 
149   /**
150    * Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index
151    * (included) and a final index (excluded).
152    *
153    * <p>Note: when specified range is empty, no actual media source is removed and no exception is
154    * thrown.
155    *
156    * @param fromIndex The initial range index, pointing to the first media source that will be
157    *     removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
158    * @param toIndex The final range index, pointing to the first media source that will be left
159    *     untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
160    * @param shuffleOrder The new shuffle order.
161    * @return The new {@link Timeline}.
162    * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
163    *     {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
164    */
removeMediaSourceRange( int fromIndex, int toIndex, ShuffleOrder shuffleOrder)165   public final Timeline removeMediaSourceRange(
166       int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
167     Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
168     this.shuffleOrder = shuffleOrder;
169     removeMediaSourcesInternal(fromIndex, toIndex);
170     return createTimeline();
171   }
172 
173   /**
174    * Moves an existing media source within the playlist.
175    *
176    * @param currentIndex The current index of the media source in the playlist. This index must be
177    *     in the range of 0 &lt;= index &lt; {@link #getSize()}.
178    * @param newIndex The target index of the media source in the playlist. This index must be in the
179    *     range of 0 &lt;= index &lt; {@link #getSize()}.
180    * @param shuffleOrder The new shuffle order.
181    * @return The new {@link Timeline}.
182    * @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} &lt; 0,
183    *     {@code currentIndex} &gt;= {@link #getSize()}, {@code newIndex} &lt; 0
184    */
moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder)185   public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) {
186     return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder);
187   }
188 
189   /**
190    * Moves a range of media sources within the playlist.
191    *
192    * <p>Note: when specified range is empty or the from index equals the new from index, no actual
193    * media source is moved and no exception is thrown.
194    *
195    * @param fromIndex The initial range index, pointing to the first media source of the range that
196    *     will be moved. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
197    * @param toIndex The final range index, pointing to the first media source that will be left
198    *     untouched. This index must be larger or equals than {@code fromIndex}.
199    * @param newFromIndex The target index of the first media source of the range that will be moved.
200    * @param shuffleOrder The new shuffle order.
201    * @return The new {@link Timeline}.
202    * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
203    *     {@code toIndex} &lt; {@code fromIndex}, {@code fromIndex} &gt; {@code toIndex}, {@code
204    *     newFromIndex} &lt; 0
205    */
moveMediaSourceRange( int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder)206   public Timeline moveMediaSourceRange(
207       int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
208     Assertions.checkArgument(
209         fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0);
210     this.shuffleOrder = shuffleOrder;
211     if (fromIndex == toIndex || fromIndex == newFromIndex) {
212       return createTimeline();
213     }
214     int startIndex = Math.min(fromIndex, newFromIndex);
215     int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1;
216     int endIndex = Math.max(newEndIndex, toIndex - 1);
217     int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
218     moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
219     for (int i = startIndex; i <= endIndex; i++) {
220       MediaSourceHolder holder = mediaSourceHolders.get(i);
221       holder.firstWindowIndexInChild = windowOffset;
222       windowOffset += holder.mediaSource.getTimeline().getWindowCount();
223     }
224     return createTimeline();
225   }
226 
227   /** Clears the playlist. */
clear(@ullable ShuffleOrder shuffleOrder)228   public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
229     this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
230     removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize());
231     return createTimeline();
232   }
233 
234   /** Whether the playlist is prepared. */
isPrepared()235   public final boolean isPrepared() {
236     return isPrepared;
237   }
238 
239   /** Returns the number of media sources in the playlist. */
getSize()240   public final int getSize() {
241     return mediaSourceHolders.size();
242   }
243 
244   /**
245    * Sets the {@link AnalyticsCollector}.
246    *
247    * @param handler The handler on which to call the collector.
248    * @param analyticsCollector The analytics collector.
249    */
setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector)250   public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) {
251     eventDispatcher.addEventListener(handler, analyticsCollector, MediaSourceEventListener.class);
252     eventDispatcher.addEventListener(handler, analyticsCollector, DrmSessionEventListener.class);
253   }
254 
255   /**
256    * Sets a new shuffle order to use when shuffling the child media sources.
257    *
258    * @param shuffleOrder A {@link ShuffleOrder}.
259    */
setShuffleOrder(ShuffleOrder shuffleOrder)260   public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) {
261     int size = getSize();
262     if (shuffleOrder.getLength() != size) {
263       shuffleOrder =
264           shuffleOrder
265               .cloneAndClear()
266               .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
267     }
268     this.shuffleOrder = shuffleOrder;
269     return createTimeline();
270   }
271 
272   /** Prepares the playlist. */
prepare(@ullable TransferListener mediaTransferListener)273   public final void prepare(@Nullable TransferListener mediaTransferListener) {
274     Assertions.checkState(!isPrepared);
275     this.mediaTransferListener = mediaTransferListener;
276     for (int i = 0; i < mediaSourceHolders.size(); i++) {
277       MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
278       prepareChildSource(mediaSourceHolder);
279       enabledMediaSourceHolders.add(mediaSourceHolder);
280     }
281     isPrepared = true;
282   }
283 
284   /**
285    * Returns a new {@link MediaPeriod} identified by {@code periodId}.
286    *
287    * @param id The identifier of the period.
288    * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
289    * @param startPositionUs The expected start position, in microseconds.
290    * @return A new {@link MediaPeriod}.
291    */
createPeriod( MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)292   public MediaPeriod createPeriod(
293       MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) {
294     Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
295     MediaSource.MediaPeriodId childMediaPeriodId =
296         id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
297     MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
298     enableMediaSource(holder);
299     holder.activeMediaPeriodIds.add(childMediaPeriodId);
300     MediaPeriod mediaPeriod =
301         holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
302     mediaSourceByMediaPeriod.put(mediaPeriod, holder);
303     disableUnusedMediaSources();
304     return mediaPeriod;
305   }
306 
307   /**
308    * Releases the period.
309    *
310    * @param mediaPeriod The period to release.
311    */
releasePeriod(MediaPeriod mediaPeriod)312   public final void releasePeriod(MediaPeriod mediaPeriod) {
313     MediaSourceHolder holder =
314         Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
315     holder.mediaSource.releasePeriod(mediaPeriod);
316     holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
317     if (!mediaSourceByMediaPeriod.isEmpty()) {
318       disableUnusedMediaSources();
319     }
320     maybeReleaseChildSource(holder);
321   }
322 
323   /** Releases the playlist. */
release()324   public final void release() {
325     for (MediaSourceAndListener childSource : childSources.values()) {
326       childSource.mediaSource.releaseSource(childSource.caller);
327       childSource.mediaSource.removeEventListener(childSource.eventListener);
328     }
329     childSources.clear();
330     enabledMediaSourceHolders.clear();
331     isPrepared = false;
332   }
333 
334   /** Throws any pending error encountered while loading or refreshing. */
maybeThrowSourceInfoRefreshError()335   public final void maybeThrowSourceInfoRefreshError() throws IOException {
336     for (MediaSourceAndListener childSource : childSources.values()) {
337       childSource.mediaSource.maybeThrowSourceInfoRefreshError();
338     }
339   }
340 
341   /** Creates a timeline reflecting the current state of the playlist. */
createTimeline()342   public final Timeline createTimeline() {
343     if (mediaSourceHolders.isEmpty()) {
344       return Timeline.EMPTY;
345     }
346     int windowOffset = 0;
347     for (int i = 0; i < mediaSourceHolders.size(); i++) {
348       MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
349       mediaSourceHolder.firstWindowIndexInChild = windowOffset;
350       windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount();
351     }
352     return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
353   }
354 
355   // Internal methods.
356 
enableMediaSource(MediaSourceHolder mediaSourceHolder)357   private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
358     enabledMediaSourceHolders.add(mediaSourceHolder);
359     @Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder);
360     if (enabledChild != null) {
361       enabledChild.mediaSource.enable(enabledChild.caller);
362     }
363   }
364 
disableUnusedMediaSources()365   private void disableUnusedMediaSources() {
366     Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
367     while (iterator.hasNext()) {
368       MediaSourceHolder holder = iterator.next();
369       if (holder.activeMediaPeriodIds.isEmpty()) {
370         disableChildSource(holder);
371         iterator.remove();
372       }
373     }
374   }
375 
disableChildSource(MediaSourceHolder holder)376   private void disableChildSource(MediaSourceHolder holder) {
377     @Nullable MediaSourceAndListener disabledChild = childSources.get(holder);
378     if (disabledChild != null) {
379       disabledChild.mediaSource.disable(disabledChild.caller);
380     }
381   }
382 
removeMediaSourcesInternal(int fromIndex, int toIndex)383   private void removeMediaSourcesInternal(int fromIndex, int toIndex) {
384     for (int index = toIndex - 1; index >= fromIndex; index--) {
385       MediaSourceHolder holder = mediaSourceHolders.remove(index);
386       mediaSourceByUid.remove(holder.uid);
387       Timeline oldTimeline = holder.mediaSource.getTimeline();
388       correctOffsets(
389           /* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount());
390       holder.isRemoved = true;
391       if (isPrepared) {
392         maybeReleaseChildSource(holder);
393       }
394     }
395   }
396 
correctOffsets(int startIndex, int windowOffsetUpdate)397   private void correctOffsets(int startIndex, int windowOffsetUpdate) {
398     for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
399       MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
400       mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate;
401     }
402   }
403 
404   // Internal methods to manage child sources.
405 
406   @Nullable
getMediaPeriodIdForChildMediaPeriodId( MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId)407   private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
408       MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) {
409     for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
410       // Ensure the reported media period id has the same window sequence number as the one created
411       // by this media source. Otherwise it does not belong to this child source.
412       if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
413           == mediaPeriodId.windowSequenceNumber) {
414         Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
415         return mediaPeriodId.copyWithPeriodUid(periodUid);
416       }
417     }
418     return null;
419   }
420 
getWindowIndexForChildWindowIndex( MediaSourceHolder mediaSourceHolder, int windowIndex)421   private static int getWindowIndexForChildWindowIndex(
422       MediaSourceHolder mediaSourceHolder, int windowIndex) {
423     return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
424   }
425 
prepareChildSource(MediaSourceHolder holder)426   private void prepareChildSource(MediaSourceHolder holder) {
427     MediaSource mediaSource = holder.mediaSource;
428     MediaSource.MediaSourceCaller caller =
429         (source, timeline) -> mediaSourceListInfoListener.onPlaylistUpdateRequested();
430     ForwardingEventListener eventListener = new ForwardingEventListener(holder);
431     childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
432     mediaSource.addEventListener(Util.createHandler(), eventListener);
433     mediaSource.addDrmEventListener(Util.createHandler(), eventListener);
434     mediaSource.prepareSource(caller, mediaTransferListener);
435   }
436 
maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder)437   private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
438     // Release if the source has been removed from the playlist and no periods are still active.
439     if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
440       MediaSourceAndListener removedChild =
441           Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
442       removedChild.mediaSource.releaseSource(removedChild.caller);
443       removedChild.mediaSource.removeEventListener(removedChild.eventListener);
444       enabledMediaSourceHolders.remove(mediaSourceHolder);
445     }
446   }
447 
448   /** Return uid of media source holder from period uid of concatenated source. */
getMediaSourceHolderUid(Object periodUid)449   private static Object getMediaSourceHolderUid(Object periodUid) {
450     return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
451   }
452 
453   /** Return uid of child period from period uid of concatenated source. */
getChildPeriodUid(Object periodUid)454   private static Object getChildPeriodUid(Object periodUid) {
455     return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
456   }
457 
getPeriodUid(MediaSourceHolder holder, Object childPeriodUid)458   private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
459     return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
460   }
461 
moveMediaSourceHolders( List<MediaSourceHolder> mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex)462   /* package */ static void moveMediaSourceHolders(
463       List<MediaSourceHolder> mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) {
464     MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex];
465     for (int i = removedItems.length - 1; i >= 0; i--) {
466       removedItems[i] = mediaSourceHolders.remove(fromIndex + i);
467     }
468     mediaSourceHolders.addAll(
469         Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems));
470   }
471 
472   /** Data class to hold playlist media sources together with meta data needed to process them. */
473   /* package */ static final class MediaSourceHolder {
474 
475     public final MaskingMediaSource mediaSource;
476     public final Object uid;
477     public final List<MediaSource.MediaPeriodId> activeMediaPeriodIds;
478 
479     public int firstWindowIndexInChild;
480     public boolean isRemoved;
481 
MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation)482     public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
483       this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
484       this.activeMediaPeriodIds = new ArrayList<>();
485       this.uid = new Object();
486     }
487 
reset(int firstWindowIndexInChild)488     public void reset(int firstWindowIndexInChild) {
489       this.firstWindowIndexInChild = firstWindowIndexInChild;
490       this.isRemoved = false;
491       this.activeMediaPeriodIds.clear();
492     }
493   }
494 
495   /** Timeline exposing concatenated timelines of playlist media sources. */
496   /* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline {
497 
498     private final int windowCount;
499     private final int periodCount;
500     private final int[] firstPeriodInChildIndices;
501     private final int[] firstWindowInChildIndices;
502     private final Timeline[] timelines;
503     private final Object[] uids;
504     private final HashMap<Object, Integer> childIndexByUid;
505 
PlaylistTimeline( Collection<MediaSourceHolder> mediaSourceHolders, ShuffleOrder shuffleOrder)506     public PlaylistTimeline(
507         Collection<MediaSourceHolder> mediaSourceHolders, ShuffleOrder shuffleOrder) {
508       super(/* isAtomic= */ false, shuffleOrder);
509       int childCount = mediaSourceHolders.size();
510       firstPeriodInChildIndices = new int[childCount];
511       firstWindowInChildIndices = new int[childCount];
512       timelines = new Timeline[childCount];
513       uids = new Object[childCount];
514       childIndexByUid = new HashMap<>();
515       int index = 0;
516       int windowCount = 0;
517       int periodCount = 0;
518       for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
519         timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
520         firstWindowInChildIndices[index] = windowCount;
521         firstPeriodInChildIndices[index] = periodCount;
522         windowCount += timelines[index].getWindowCount();
523         periodCount += timelines[index].getPeriodCount();
524         uids[index] = mediaSourceHolder.uid;
525         childIndexByUid.put(uids[index], index++);
526       }
527       this.windowCount = windowCount;
528       this.periodCount = periodCount;
529     }
530 
531     @Override
getChildIndexByPeriodIndex(int periodIndex)532     protected int getChildIndexByPeriodIndex(int periodIndex) {
533       return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
534     }
535 
536     @Override
getChildIndexByWindowIndex(int windowIndex)537     protected int getChildIndexByWindowIndex(int windowIndex) {
538       return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
539     }
540 
541     @Override
getChildIndexByChildUid(Object childUid)542     protected int getChildIndexByChildUid(Object childUid) {
543       Integer index = childIndexByUid.get(childUid);
544       return index == null ? C.INDEX_UNSET : index;
545     }
546 
547     @Override
getTimelineByChildIndex(int childIndex)548     protected Timeline getTimelineByChildIndex(int childIndex) {
549       return timelines[childIndex];
550     }
551 
552     @Override
getFirstPeriodIndexByChildIndex(int childIndex)553     protected int getFirstPeriodIndexByChildIndex(int childIndex) {
554       return firstPeriodInChildIndices[childIndex];
555     }
556 
557     @Override
getFirstWindowIndexByChildIndex(int childIndex)558     protected int getFirstWindowIndexByChildIndex(int childIndex) {
559       return firstWindowInChildIndices[childIndex];
560     }
561 
562     @Override
getChildUidByChildIndex(int childIndex)563     protected Object getChildUidByChildIndex(int childIndex) {
564       return uids[childIndex];
565     }
566 
567     @Override
getWindowCount()568     public int getWindowCount() {
569       return windowCount;
570     }
571 
572     @Override
getPeriodCount()573     public int getPeriodCount() {
574       return periodCount;
575     }
576   }
577 
578   private static final class MediaSourceAndListener {
579 
580     public final MediaSource mediaSource;
581     public final MediaSource.MediaSourceCaller caller;
582     public final MediaSourceEventListener eventListener;
583 
MediaSourceAndListener( MediaSource mediaSource, MediaSource.MediaSourceCaller caller, MediaSourceEventListener eventListener)584     public MediaSourceAndListener(
585         MediaSource mediaSource,
586         MediaSource.MediaSourceCaller caller,
587         MediaSourceEventListener eventListener) {
588       this.mediaSource = mediaSource;
589       this.caller = caller;
590       this.eventListener = eventListener;
591     }
592   }
593 
594   private final class ForwardingEventListener
595       implements MediaSourceEventListener, DrmSessionEventListener {
596 
597     private final MediaSourceList.MediaSourceHolder id;
598     private EventDispatcher eventDispatcher;
599 
ForwardingEventListener(MediaSourceList.MediaSourceHolder id)600     public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
601       eventDispatcher = MediaSourceList.this.eventDispatcher;
602       this.id = id;
603     }
604 
605     // MediaSourceEventListener implementation
606 
607     @Override
onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)608     public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
609       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
610         eventDispatcher.mediaPeriodCreated();
611       }
612     }
613 
614     @Override
onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)615     public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
616       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
617         eventDispatcher.mediaPeriodReleased();
618       }
619     }
620 
621     @Override
onLoadStarted( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData)622     public void onLoadStarted(
623         int windowIndex,
624         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
625         LoadEventInfo loadEventData,
626         MediaLoadData mediaLoadData) {
627       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
628         eventDispatcher.loadStarted(loadEventData, mediaLoadData);
629       }
630     }
631 
632     @Override
onLoadCompleted( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData)633     public void onLoadCompleted(
634         int windowIndex,
635         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
636         LoadEventInfo loadEventData,
637         MediaLoadData mediaLoadData) {
638       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
639         eventDispatcher.loadCompleted(loadEventData, mediaLoadData);
640       }
641     }
642 
643     @Override
onLoadCanceled( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData)644     public void onLoadCanceled(
645         int windowIndex,
646         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
647         LoadEventInfo loadEventData,
648         MediaLoadData mediaLoadData) {
649       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
650         eventDispatcher.loadCanceled(loadEventData, mediaLoadData);
651       }
652     }
653 
654     @Override
onLoadError( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, LoadEventInfo loadEventData, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled)655     public void onLoadError(
656         int windowIndex,
657         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
658         LoadEventInfo loadEventData,
659         MediaLoadData mediaLoadData,
660         IOException error,
661         boolean wasCanceled) {
662       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
663         eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
664       }
665     }
666 
667     @Override
onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId)668     public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
669       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
670         eventDispatcher.readingStarted();
671       }
672     }
673 
674     @Override
onUpstreamDiscarded( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)675     public void onUpstreamDiscarded(
676         int windowIndex,
677         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
678         MediaLoadData mediaLoadData) {
679       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
680         eventDispatcher.upstreamDiscarded(mediaLoadData);
681       }
682     }
683 
684     @Override
onDownstreamFormatChanged( int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)685     public void onDownstreamFormatChanged(
686         int windowIndex,
687         @Nullable MediaSource.MediaPeriodId mediaPeriodId,
688         MediaLoadData mediaLoadData) {
689       if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
690         eventDispatcher.downstreamFormatChanged(mediaLoadData);
691       }
692     }
693 
694     // DrmSessionEventListener implementation
695 
696     @Override
onDrmSessionAcquired()697     public void onDrmSessionAcquired() {
698       eventDispatcher.dispatch(
699           (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired(),
700           DrmSessionEventListener.class);
701     }
702 
703     @Override
onDrmKeysLoaded()704     public void onDrmKeysLoaded() {
705       eventDispatcher.dispatch(
706           (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(),
707           DrmSessionEventListener.class);
708     }
709 
710     @Override
onDrmSessionManagerError(Exception error)711     public void onDrmSessionManagerError(Exception error) {
712       eventDispatcher.dispatch(
713           (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(error),
714           DrmSessionEventListener.class);
715     }
716 
717     @Override
onDrmKeysRestored()718     public void onDrmKeysRestored() {
719       eventDispatcher.dispatch(
720           (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored(),
721           DrmSessionEventListener.class);
722     }
723 
724     @Override
onDrmKeysRemoved()725     public void onDrmKeysRemoved() {
726       eventDispatcher.dispatch(
727           (listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRemoved(),
728           DrmSessionEventListener.class);
729     }
730 
731     @Override
onDrmSessionReleased()732     public void onDrmSessionReleased() {
733       eventDispatcher.dispatch(
734           (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionReleased(),
735           DrmSessionEventListener.class);
736     }
737 
738     /** Updates the event dispatcher and returns whether the event should be dispatched. */
maybeUpdateEventDispatcher( int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId)739     private boolean maybeUpdateEventDispatcher(
740         int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
741       @Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
742       if (childMediaPeriodId != null) {
743         mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
744         if (mediaPeriodId == null) {
745           // Media period not found. Ignore event.
746           return false;
747         }
748       }
749       int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
750       if (eventDispatcher.windowIndex != windowIndex
751           || !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) {
752         eventDispatcher =
753             MediaSourceList.this.eventDispatcher.withParameters(
754                 windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
755       }
756       return true;
757     }
758   }
759 }
760