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 <= index <= {@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 <= index <= {@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 <= index <= {@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} < 0, 163 * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@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 <= index < {@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 <= index < {@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} < 0, 183 * {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 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 <= index <= {@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} < 0, 203 * {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code 204 * newFromIndex} < 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