| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.android.exoplayer2.source; |
| |
| import android.os.Handler; |
| import android.os.Message; |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.AbstractConcatenatedTimeline; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.Timeline; |
| import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; |
| import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; |
| import com.google.android.exoplayer2.upstream.Allocator; |
| import com.google.android.exoplayer2.upstream.TransferListener; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.Util; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified |
| * during playback. It is valid for the same {@link MediaSource} instance to be present more than |
| * once in the concatenation. Access to this class is thread-safe. |
| */ |
| public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> { |
| |
| private static final int MSG_ADD = 0; |
| private static final int MSG_REMOVE = 1; |
| private static final int MSG_MOVE = 2; |
| private static final int MSG_SET_SHUFFLE_ORDER = 3; |
| private static final int MSG_UPDATE_TIMELINE = 4; |
| private static final int MSG_ON_COMPLETION = 5; |
| |
| // Accessed on any thread. |
| @GuardedBy("this") |
| private final List<MediaSourceHolder> mediaSourcesPublic; |
| |
| @GuardedBy("this") |
| private final Set<HandlerAndRunnable> pendingOnCompletionActions; |
| |
| @GuardedBy("this") |
| @Nullable |
| private Handler playbackThreadHandler; |
| |
| // Accessed on the playback thread only. |
| private final List<MediaSourceHolder> mediaSourceHolders; |
| private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod; |
| private final Map<Object, MediaSourceHolder> mediaSourceByUid; |
| private final Set<MediaSourceHolder> enabledMediaSourceHolders; |
| private final boolean isAtomic; |
| private final boolean useLazyPreparation; |
| |
| private boolean timelineUpdateScheduled; |
| private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions; |
| private ShuffleOrder shuffleOrder; |
| |
| /** |
| * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same |
| * {@link MediaSource} instance to be present more than once in the array. |
| */ |
| public ConcatenatingMediaSource(MediaSource... mediaSources) { |
| this(/* isAtomic= */ false, mediaSources); |
| } |
| |
| /** |
| * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated |
| * as a single item for repeating and shuffling. |
| * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link |
| * MediaSource} instance to be present more than once in the array. |
| */ |
| public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { |
| this(isAtomic, new DefaultShuffleOrder(0), mediaSources); |
| } |
| |
| /** |
| * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated |
| * as a single item for repeating and shuffling. |
| * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. |
| * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link |
| * MediaSource} instance to be present more than once in the array. |
| */ |
| public ConcatenatingMediaSource( |
| boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) { |
| this(isAtomic, /* useLazyPreparation= */ false, shuffleOrder, mediaSources); |
| } |
| |
| /** |
| * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated |
| * as a single item for repeating and shuffling. |
| * @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest |
| * loads and other initial preparation steps happen immediately. If true, these initial |
| * preparations are triggered only when the player starts buffering the media. |
| * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. |
| * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link |
| * MediaSource} instance to be present more than once in the array. |
| */ |
| @SuppressWarnings("initialization") |
| public ConcatenatingMediaSource( |
| boolean isAtomic, |
| boolean useLazyPreparation, |
| ShuffleOrder shuffleOrder, |
| MediaSource... mediaSources) { |
| for (MediaSource mediaSource : mediaSources) { |
| Assertions.checkNotNull(mediaSource); |
| } |
| this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; |
| this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); |
| this.mediaSourceByUid = new HashMap<>(); |
| this.mediaSourcesPublic = new ArrayList<>(); |
| this.mediaSourceHolders = new ArrayList<>(); |
| this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); |
| this.pendingOnCompletionActions = new HashSet<>(); |
| this.enabledMediaSourceHolders = new HashSet<>(); |
| this.isAtomic = isAtomic; |
| this.useLazyPreparation = useLazyPreparation; |
| addMediaSources(Arrays.asList(mediaSources)); |
| } |
| |
| @Override |
| public synchronized Timeline getInitialTimeline() { |
| ShuffleOrder shuffleOrder = |
| this.shuffleOrder.getLength() != mediaSourcesPublic.size() |
| ? this.shuffleOrder |
| .cloneAndClear() |
| .cloneAndInsert( |
| /* insertionIndex= */ 0, /* insertionCount= */ mediaSourcesPublic.size()) |
| : this.shuffleOrder; |
| return new ConcatenatedTimeline(mediaSourcesPublic, shuffleOrder, isAtomic); |
| } |
| |
| @Override |
| public boolean isSingleWindow() { |
| return false; |
| } |
| |
| /** |
| * Appends a {@link MediaSource} to the playlist. |
| * |
| * @param mediaSource The {@link MediaSource} to be added to the list. |
| */ |
| public synchronized void addMediaSource(MediaSource mediaSource) { |
| addMediaSource(mediaSourcesPublic.size(), mediaSource); |
| } |
| |
| /** |
| * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. |
| * |
| * @param mediaSource The {@link MediaSource} to be added to the list. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * source has been added to the playlist. |
| */ |
| public synchronized void addMediaSource( |
| MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { |
| addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction); |
| } |
| |
| /** |
| * Adds a {@link MediaSource} to the playlist. |
| * |
| * @param index The index at which the new {@link MediaSource} will be inserted. This index must |
| * be in the range of 0 <= index <= {@link #getSize()}. |
| * @param mediaSource The {@link MediaSource} to be added to the list. |
| */ |
| public synchronized void addMediaSource(int index, MediaSource mediaSource) { |
| addPublicMediaSources( |
| index, |
| Collections.singletonList(mediaSource), |
| /* handler= */ null, |
| /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. |
| * |
| * @param index The index at which the new {@link MediaSource} will be inserted. This index must |
| * be in the range of 0 <= index <= {@link #getSize()}. |
| * @param mediaSource The {@link MediaSource} to be added to the list. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * source has been added to the playlist. |
| */ |
| public synchronized void addMediaSource( |
| int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) { |
| addPublicMediaSources( |
| index, Collections.singletonList(mediaSource), handler, onCompletionAction); |
| } |
| |
| /** |
| * Appends multiple {@link MediaSource}s to the playlist. |
| * |
| * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media |
| * sources are added in the order in which they appear in this collection. |
| */ |
| public synchronized void addMediaSources(Collection<MediaSource> mediaSources) { |
| addPublicMediaSources( |
| mediaSourcesPublic.size(), |
| mediaSources, |
| /* handler= */ null, |
| /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on |
| * completion. |
| * |
| * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media |
| * sources are added in the order in which they appear in this collection. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * sources have been added to the playlist. |
| */ |
| public synchronized void addMediaSources( |
| Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) { |
| addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); |
| } |
| |
| /** |
| * Adds multiple {@link MediaSource}s to the playlist. |
| * |
| * @param index The index at which the new {@link MediaSource}s will be inserted. This index must |
| * be in the range of 0 <= index <= {@link #getSize()}. |
| * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media |
| * sources are added in the order in which they appear in this collection. |
| */ |
| public synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) { |
| addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. |
| * |
| * @param index The index at which the new {@link MediaSource}s will be inserted. This index must |
| * be in the range of 0 <= index <= {@link #getSize()}. |
| * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media |
| * sources are added in the order in which they appear in this collection. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * sources have been added to the playlist. |
| */ |
| public synchronized void addMediaSources( |
| int index, |
| Collection<MediaSource> mediaSources, |
| Handler handler, |
| Runnable onCompletionAction) { |
| addPublicMediaSources(index, mediaSources, handler, onCompletionAction); |
| } |
| |
| /** |
| * Removes a {@link MediaSource} from the playlist. |
| * |
| * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, |
| * int)} instead. |
| * |
| * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link |
| * #removeMediaSourceRange(int, int)} instead. |
| * |
| * @param index The index at which the media source will be removed. This index must be in the |
| * range of 0 <= index < {@link #getSize()}. |
| * @return The removed {@link MediaSource}. |
| */ |
| public synchronized MediaSource removeMediaSource(int index) { |
| MediaSource removedMediaSource = getMediaSource(index); |
| removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null); |
| return removedMediaSource; |
| } |
| |
| /** |
| * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. |
| * |
| * <p>Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, |
| * int, Handler, Runnable)} instead. |
| * |
| * <p>Note: If you want to remove a set of contiguous sources, it's preferable to use {@link |
| * #removeMediaSourceRange(int, int, Handler, Runnable)} instead. |
| * |
| * @param index The index at which the media source will be removed. This index must be in the |
| * range of 0 <= index < {@link #getSize()}. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * source has been removed from the playlist. |
| * @return The removed {@link MediaSource}. |
| */ |
| public synchronized MediaSource removeMediaSource( |
| int index, Handler handler, Runnable onCompletionAction) { |
| MediaSource removedMediaSource = getMediaSource(index); |
| removePublicMediaSources(index, index + 1, handler, onCompletionAction); |
| return removedMediaSource; |
| } |
| |
| /** |
| * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index |
| * (included) and a final index (excluded). |
| * |
| * <p>Note: when specified range is empty, no actual media source is removed and no exception is |
| * thrown. |
| * |
| * @param fromIndex The initial range index, pointing to the first media source that will be |
| * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. |
| * @param toIndex The final range index, pointing to the first media source that will be left |
| * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. |
| * @throws IndexOutOfBoundsException When the range is malformed, i.e. {@code fromIndex} < 0, |
| * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} |
| */ |
| public synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { |
| removePublicMediaSources( |
| fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Removes a range of {@link MediaSource}s from the playlist, by specifying an initial index |
| * (included) and a final index (excluded), and executes a custom action on completion. |
| * |
| * <p>Note: when specified range is empty, no actual media source is removed and no exception is |
| * thrown. |
| * |
| * @param fromIndex The initial range index, pointing to the first media source that will be |
| * removed. This index must be in the range of 0 <= index <= {@link #getSize()}. |
| * @param toIndex The final range index, pointing to the first media source that will be left |
| * untouched. This index must be in the range of 0 <= index <= {@link #getSize()}. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * source range has been removed from the playlist. |
| * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0, |
| * {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex} |
| */ |
| public synchronized void removeMediaSourceRange( |
| int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) { |
| removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction); |
| } |
| |
| /** |
| * Moves an existing {@link MediaSource} within the playlist. |
| * |
| * @param currentIndex The current index of the media source in the playlist. This index must be |
| * in the range of 0 <= index < {@link #getSize()}. |
| * @param newIndex The target index of the media source in the playlist. This index must be in the |
| * range of 0 <= index < {@link #getSize()}. |
| */ |
| public synchronized void moveMediaSource(int currentIndex, int newIndex) { |
| movePublicMediaSource( |
| currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Moves an existing {@link MediaSource} within the playlist and executes a custom action on |
| * completion. |
| * |
| * @param currentIndex The current index of the media source in the playlist. This index must be |
| * in the range of 0 <= index < {@link #getSize()}. |
| * @param newIndex The target index of the media source in the playlist. This index must be in the |
| * range of 0 <= index < {@link #getSize()}. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the media |
| * source has been moved. |
| */ |
| public synchronized void moveMediaSource( |
| int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) { |
| movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction); |
| } |
| |
| /** Clears the playlist. */ |
| public synchronized void clear() { |
| removeMediaSourceRange(0, getSize()); |
| } |
| |
| /** |
| * Clears the playlist and executes a custom action on completion. |
| * |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist |
| * has been cleared. |
| */ |
| public synchronized void clear(Handler handler, Runnable onCompletionAction) { |
| removeMediaSourceRange(0, getSize(), handler, onCompletionAction); |
| } |
| |
| /** Returns the number of media sources in the playlist. */ |
| public synchronized int getSize() { |
| return mediaSourcesPublic.size(); |
| } |
| |
| /** |
| * Returns the {@link MediaSource} at a specified index. |
| * |
| * @param index An index in the range of 0 <= index <= {@link #getSize()}. |
| * @return The {@link MediaSource} at this index. |
| */ |
| public synchronized MediaSource getMediaSource(int index) { |
| return mediaSourcesPublic.get(index).mediaSource; |
| } |
| |
| /** |
| * Sets a new shuffle order to use when shuffling the child media sources. |
| * |
| * @param shuffleOrder A {@link ShuffleOrder}. |
| */ |
| public synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { |
| setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null); |
| } |
| |
| /** |
| * Sets a new shuffle order to use when shuffling the child media sources. |
| * |
| * @param shuffleOrder A {@link ShuffleOrder}. |
| * @param handler The {@link Handler} to run {@code onCompletionAction}. |
| * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle |
| * order has been changed. |
| */ |
| public synchronized void setShuffleOrder( |
| ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) { |
| setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction); |
| } |
| |
| // CompositeMediaSource implementation. |
| |
| @Override |
| @Nullable |
| public Object getTag() { |
| return null; |
| } |
| |
| @Override |
| protected synchronized void prepareSourceInternal( |
| @Nullable TransferListener mediaTransferListener) { |
| super.prepareSourceInternal(mediaTransferListener); |
| playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); |
| if (mediaSourcesPublic.isEmpty()) { |
| updateTimelineAndScheduleOnCompletionActions(); |
| } else { |
| shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); |
| addMediaSourcesInternal(0, mediaSourcesPublic); |
| scheduleTimelineUpdate(); |
| } |
| } |
| |
| @SuppressWarnings("MissingSuperCall") |
| @Override |
| protected void enableInternal() { |
| // Suppress enabling all child sources here as they can be lazily enabled when creating periods. |
| } |
| |
| @Override |
| public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { |
| Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); |
| MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); |
| @Nullable MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid); |
| if (holder == null) { |
| // Stale event. The media source has already been removed. |
| holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation); |
| holder.isRemoved = true; |
| prepareChildSource(holder, holder.mediaSource); |
| } |
| enableMediaSource(holder); |
| holder.activeMediaPeriodIds.add(childMediaPeriodId); |
| MediaPeriod mediaPeriod = |
| holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); |
| mediaSourceByMediaPeriod.put(mediaPeriod, holder); |
| disableUnusedMediaSources(); |
| return mediaPeriod; |
| } |
| |
| @Override |
| public void releasePeriod(MediaPeriod mediaPeriod) { |
| MediaSourceHolder holder = |
| Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); |
| holder.mediaSource.releasePeriod(mediaPeriod); |
| holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); |
| if (!mediaSourceByMediaPeriod.isEmpty()) { |
| disableUnusedMediaSources(); |
| } |
| maybeReleaseChildSource(holder); |
| } |
| |
| @Override |
| protected void disableInternal() { |
| super.disableInternal(); |
| enabledMediaSourceHolders.clear(); |
| } |
| |
| @Override |
| protected synchronized void releaseSourceInternal() { |
| super.releaseSourceInternal(); |
| mediaSourceHolders.clear(); |
| enabledMediaSourceHolders.clear(); |
| mediaSourceByUid.clear(); |
| shuffleOrder = shuffleOrder.cloneAndClear(); |
| if (playbackThreadHandler != null) { |
| playbackThreadHandler.removeCallbacksAndMessages(null); |
| playbackThreadHandler = null; |
| } |
| timelineUpdateScheduled = false; |
| nextTimelineUpdateOnCompletionActions.clear(); |
| dispatchOnCompletionActions(pendingOnCompletionActions); |
| } |
| |
| @Override |
| protected void onChildSourceInfoRefreshed( |
| MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) { |
| updateMediaSourceInternal(mediaSourceHolder, timeline); |
| } |
| |
| @Override |
| @Nullable |
| protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( |
| MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) { |
| for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) { |
| // Ensure the reported media period id has the same window sequence number as the one created |
| // by this media source. Otherwise it does not belong to this child source. |
| if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber |
| == mediaPeriodId.windowSequenceNumber) { |
| Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid); |
| return mediaPeriodId.copyWithPeriodUid(periodUid); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| protected int getWindowIndexForChildWindowIndex( |
| MediaSourceHolder mediaSourceHolder, int windowIndex) { |
| return windowIndex + mediaSourceHolder.firstWindowIndexInChild; |
| } |
| |
| // Internal methods. Called from any thread. |
| |
| @GuardedBy("this") |
| private void addPublicMediaSources( |
| int index, |
| Collection<MediaSource> mediaSources, |
| @Nullable Handler handler, |
| @Nullable Runnable onCompletionAction) { |
| Assertions.checkArgument((handler == null) == (onCompletionAction == null)); |
| @Nullable Handler playbackThreadHandler = this.playbackThreadHandler; |
| for (MediaSource mediaSource : mediaSources) { |
| Assertions.checkNotNull(mediaSource); |
| } |
| List<MediaSourceHolder> mediaSourceHolders = new ArrayList<>(mediaSources.size()); |
| for (MediaSource mediaSource : mediaSources) { |
| mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); |
| } |
| mediaSourcesPublic.addAll(index, mediaSourceHolders); |
| if (playbackThreadHandler != null && !mediaSources.isEmpty()) { |
| @Nullable |
| HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); |
| playbackThreadHandler |
| .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction)) |
| .sendToTarget(); |
| } else if (onCompletionAction != null && handler != null) { |
| handler.post(onCompletionAction); |
| } |
| } |
| |
| @GuardedBy("this") |
| private void removePublicMediaSources( |
| int fromIndex, |
| int toIndex, |
| @Nullable Handler handler, |
| @Nullable Runnable onCompletionAction) { |
| Assertions.checkArgument((handler == null) == (onCompletionAction == null)); |
| @Nullable Handler playbackThreadHandler = this.playbackThreadHandler; |
| Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); |
| if (playbackThreadHandler != null) { |
| @Nullable |
| HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); |
| playbackThreadHandler |
| .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction)) |
| .sendToTarget(); |
| } else if (onCompletionAction != null && handler != null) { |
| handler.post(onCompletionAction); |
| } |
| } |
| |
| @GuardedBy("this") |
| private void movePublicMediaSource( |
| int currentIndex, |
| int newIndex, |
| @Nullable Handler handler, |
| @Nullable Runnable onCompletionAction) { |
| Assertions.checkArgument((handler == null) == (onCompletionAction == null)); |
| @Nullable Handler playbackThreadHandler = this.playbackThreadHandler; |
| mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); |
| if (playbackThreadHandler != null) { |
| @Nullable |
| HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); |
| playbackThreadHandler |
| .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction)) |
| .sendToTarget(); |
| } else if (onCompletionAction != null && handler != null) { |
| handler.post(onCompletionAction); |
| } |
| } |
| |
| @GuardedBy("this") |
| private void setPublicShuffleOrder( |
| ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) { |
| Assertions.checkArgument((handler == null) == (onCompletionAction == null)); |
| @Nullable Handler playbackThreadHandler = this.playbackThreadHandler; |
| if (playbackThreadHandler != null) { |
| int size = getSize(); |
| if (shuffleOrder.getLength() != size) { |
| shuffleOrder = |
| shuffleOrder |
| .cloneAndClear() |
| .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); |
| } |
| @Nullable |
| HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction); |
| playbackThreadHandler |
| .obtainMessage( |
| MSG_SET_SHUFFLE_ORDER, |
| new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction)) |
| .sendToTarget(); |
| } else { |
| this.shuffleOrder = |
| shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; |
| if (onCompletionAction != null && handler != null) { |
| handler.post(onCompletionAction); |
| } |
| } |
| } |
| |
| @GuardedBy("this") |
| @Nullable |
| private HandlerAndRunnable createOnCompletionAction( |
| @Nullable Handler handler, @Nullable Runnable runnable) { |
| if (handler == null || runnable == null) { |
| return null; |
| } |
| HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable); |
| pendingOnCompletionActions.add(handlerAndRunnable); |
| return handlerAndRunnable; |
| } |
| |
| // Internal methods. Called on the playback thread. |
| |
| @SuppressWarnings("unchecked") |
| private boolean handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_ADD: |
| MessageData<Collection<MediaSourceHolder>> addMessage = |
| (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj); |
| shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); |
| addMediaSourcesInternal(addMessage.index, addMessage.customData); |
| scheduleTimelineUpdate(addMessage.onCompletionAction); |
| break; |
| case MSG_REMOVE: |
| MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); |
| int fromIndex = removeMessage.index; |
| int toIndex = removeMessage.customData; |
| if (fromIndex == 0 && toIndex == shuffleOrder.getLength()) { |
| shuffleOrder = shuffleOrder.cloneAndClear(); |
| } else { |
| shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndex); |
| } |
| for (int index = toIndex - 1; index >= fromIndex; index--) { |
| removeMediaSourceInternal(index); |
| } |
| scheduleTimelineUpdate(removeMessage.onCompletionAction); |
| break; |
| case MSG_MOVE: |
| MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); |
| shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); |
| shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); |
| moveMediaSourceInternal(moveMessage.index, moveMessage.customData); |
| scheduleTimelineUpdate(moveMessage.onCompletionAction); |
| break; |
| case MSG_SET_SHUFFLE_ORDER: |
| MessageData<ShuffleOrder> shuffleOrderMessage = |
| (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj); |
| shuffleOrder = shuffleOrderMessage.customData; |
| scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction); |
| break; |
| case MSG_UPDATE_TIMELINE: |
| updateTimelineAndScheduleOnCompletionActions(); |
| break; |
| case MSG_ON_COMPLETION: |
| Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj); |
| dispatchOnCompletionActions(actions); |
| break; |
| default: |
| throw new IllegalStateException(); |
| } |
| return true; |
| } |
| |
| private void scheduleTimelineUpdate() { |
| scheduleTimelineUpdate(/* onCompletionAction= */ null); |
| } |
| |
| private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) { |
| if (!timelineUpdateScheduled) { |
| getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget(); |
| timelineUpdateScheduled = true; |
| } |
| if (onCompletionAction != null) { |
| nextTimelineUpdateOnCompletionActions.add(onCompletionAction); |
| } |
| } |
| |
| private void updateTimelineAndScheduleOnCompletionActions() { |
| timelineUpdateScheduled = false; |
| Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions; |
| nextTimelineUpdateOnCompletionActions = new HashSet<>(); |
| refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); |
| getPlaybackThreadHandlerOnPlaybackThread() |
| .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) |
| .sendToTarget(); |
| } |
| |
| @SuppressWarnings("GuardedBy") |
| private Handler getPlaybackThreadHandlerOnPlaybackThread() { |
| // Write access to this value happens on the playback thread only, so playback thread reads |
| // don't need to be synchronized. |
| return Assertions.checkNotNull(playbackThreadHandler); |
| } |
| |
| private synchronized void dispatchOnCompletionActions( |
| Set<HandlerAndRunnable> onCompletionActions) { |
| for (HandlerAndRunnable pendingAction : onCompletionActions) { |
| pendingAction.dispatch(); |
| } |
| pendingOnCompletionActions.removeAll(onCompletionActions); |
| } |
| |
| private void addMediaSourcesInternal( |
| int index, Collection<MediaSourceHolder> mediaSourceHolders) { |
| for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { |
| addMediaSourceInternal(index++, mediaSourceHolder); |
| } |
| } |
| |
| private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) { |
| if (newIndex > 0) { |
| MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); |
| Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); |
| newMediaSourceHolder.reset( |
| newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); |
| } else { |
| newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); |
| } |
| Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); |
| correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); |
| mediaSourceHolders.add(newIndex, newMediaSourceHolder); |
| mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); |
| prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); |
| if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) { |
| enabledMediaSourceHolders.add(newMediaSourceHolder); |
| } else { |
| disableChildSource(newMediaSourceHolder); |
| } |
| } |
| |
| private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { |
| if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { |
| MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); |
| int windowOffsetUpdate = |
| timeline.getWindowCount() |
| - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild); |
| if (windowOffsetUpdate != 0) { |
| correctOffsets( |
| mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); |
| } |
| } |
| scheduleTimelineUpdate(); |
| } |
| |
| private void removeMediaSourceInternal(int index) { |
| MediaSourceHolder holder = mediaSourceHolders.remove(index); |
| mediaSourceByUid.remove(holder.uid); |
| Timeline oldTimeline = holder.mediaSource.getTimeline(); |
| correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); |
| holder.isRemoved = true; |
| maybeReleaseChildSource(holder); |
| } |
| |
| private void moveMediaSourceInternal(int currentIndex, int newIndex) { |
| int startIndex = Math.min(currentIndex, newIndex); |
| int endIndex = Math.max(currentIndex, newIndex); |
| int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; |
| mediaSourceHolders.add(newIndex, mediaSourceHolders.remove(currentIndex)); |
| for (int i = startIndex; i <= endIndex; i++) { |
| MediaSourceHolder holder = mediaSourceHolders.get(i); |
| holder.childIndex = i; |
| holder.firstWindowIndexInChild = windowOffset; |
| windowOffset += holder.mediaSource.getTimeline().getWindowCount(); |
| } |
| } |
| |
| private void correctOffsets(int startIndex, int childIndexUpdate, int windowOffsetUpdate) { |
| // TODO: Replace window index with uid in reporting to get rid of this inefficient method and |
| // the childIndex and firstWindowIndexInChild variables. |
| for (int i = startIndex; i < mediaSourceHolders.size(); i++) { |
| MediaSourceHolder holder = mediaSourceHolders.get(i); |
| holder.childIndex += childIndexUpdate; |
| holder.firstWindowIndexInChild += windowOffsetUpdate; |
| } |
| } |
| |
| private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { |
| // Release if the source has been removed from the playlist and no periods are still active. |
| if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { |
| enabledMediaSourceHolders.remove(mediaSourceHolder); |
| releaseChildSource(mediaSourceHolder); |
| } |
| } |
| |
| private void enableMediaSource(MediaSourceHolder mediaSourceHolder) { |
| enabledMediaSourceHolders.add(mediaSourceHolder); |
| enableChildSource(mediaSourceHolder); |
| } |
| |
| private void disableUnusedMediaSources() { |
| Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator(); |
| while (iterator.hasNext()) { |
| MediaSourceHolder holder = iterator.next(); |
| if (holder.activeMediaPeriodIds.isEmpty()) { |
| disableChildSource(holder); |
| iterator.remove(); |
| } |
| } |
| } |
| |
| /** Return uid of media source holder from period uid of concatenated source. */ |
| private static Object getMediaSourceHolderUid(Object periodUid) { |
| return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); |
| } |
| |
| /** Return uid of child period from period uid of concatenated source. */ |
| private static Object getChildPeriodUid(Object periodUid) { |
| return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); |
| } |
| |
| private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { |
| return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid); |
| } |
| |
| /** Data class to hold playlist media sources together with meta data needed to process them. */ |
| /* package */ static final class MediaSourceHolder { |
| |
| public final MaskingMediaSource mediaSource; |
| public final Object uid; |
| public final List<MediaPeriodId> activeMediaPeriodIds; |
| |
| public int childIndex; |
| public int firstWindowIndexInChild; |
| public boolean isRemoved; |
| |
| public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { |
| this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); |
| this.activeMediaPeriodIds = new ArrayList<>(); |
| this.uid = new Object(); |
| } |
| |
| public void reset(int childIndex, int firstWindowIndexInChild) { |
| this.childIndex = childIndex; |
| this.firstWindowIndexInChild = firstWindowIndexInChild; |
| this.isRemoved = false; |
| this.activeMediaPeriodIds.clear(); |
| } |
| } |
| |
| /** Message used to post actions from app thread to playback thread. */ |
| private static final class MessageData<T> { |
| |
| public final int index; |
| public final T customData; |
| @Nullable public final HandlerAndRunnable onCompletionAction; |
| |
| public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) { |
| this.index = index; |
| this.customData = customData; |
| this.onCompletionAction = onCompletionAction; |
| } |
| } |
| |
| /** Timeline exposing concatenated timelines of playlist media sources. */ |
| private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { |
| |
| private final int windowCount; |
| private final int periodCount; |
| private final int[] firstPeriodInChildIndices; |
| private final int[] firstWindowInChildIndices; |
| private final Timeline[] timelines; |
| private final Object[] uids; |
| private final HashMap<Object, Integer> childIndexByUid; |
| |
| public ConcatenatedTimeline( |
| Collection<MediaSourceHolder> mediaSourceHolders, |
| ShuffleOrder shuffleOrder, |
| boolean isAtomic) { |
| super(isAtomic, shuffleOrder); |
| int childCount = mediaSourceHolders.size(); |
| firstPeriodInChildIndices = new int[childCount]; |
| firstWindowInChildIndices = new int[childCount]; |
| timelines = new Timeline[childCount]; |
| uids = new Object[childCount]; |
| childIndexByUid = new HashMap<>(); |
| int index = 0; |
| int windowCount = 0; |
| int periodCount = 0; |
| for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { |
| timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); |
| firstWindowInChildIndices[index] = windowCount; |
| firstPeriodInChildIndices[index] = periodCount; |
| windowCount += timelines[index].getWindowCount(); |
| periodCount += timelines[index].getPeriodCount(); |
| uids[index] = mediaSourceHolder.uid; |
| childIndexByUid.put(uids[index], index++); |
| } |
| this.windowCount = windowCount; |
| this.periodCount = periodCount; |
| } |
| |
| @Override |
| protected int getChildIndexByPeriodIndex(int periodIndex) { |
| return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); |
| } |
| |
| @Override |
| protected int getChildIndexByWindowIndex(int windowIndex) { |
| return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); |
| } |
| |
| @Override |
| protected int getChildIndexByChildUid(Object childUid) { |
| @Nullable Integer index = childIndexByUid.get(childUid); |
| return index == null ? C.INDEX_UNSET : index; |
| } |
| |
| @Override |
| protected Timeline getTimelineByChildIndex(int childIndex) { |
| return timelines[childIndex]; |
| } |
| |
| @Override |
| protected int getFirstPeriodIndexByChildIndex(int childIndex) { |
| return firstPeriodInChildIndices[childIndex]; |
| } |
| |
| @Override |
| protected int getFirstWindowIndexByChildIndex(int childIndex) { |
| return firstWindowInChildIndices[childIndex]; |
| } |
| |
| @Override |
| protected Object getChildUidByChildIndex(int childIndex) { |
| return uids[childIndex]; |
| } |
| |
| @Override |
| public int getWindowCount() { |
| return windowCount; |
| } |
| |
| @Override |
| public int getPeriodCount() { |
| return periodCount; |
| } |
| } |
| |
| /** Dummy media source which does nothing and does not support creating periods. */ |
| private static final class DummyMediaSource extends BaseMediaSource { |
| |
| @Override |
| protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { |
| // Do nothing. |
| } |
| |
| @Override |
| @Nullable |
| public Object getTag() { |
| return null; |
| } |
| |
| @Override |
| protected void releaseSourceInternal() { |
| // Do nothing. |
| } |
| |
| @Override |
| public void maybeThrowSourceInfoRefreshError() { |
| // Do nothing. |
| } |
| |
| @Override |
| public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public void releasePeriod(MediaPeriod mediaPeriod) { |
| // Do nothing. |
| } |
| } |
| |
| private static final class HandlerAndRunnable { |
| |
| private final Handler handler; |
| private final Runnable runnable; |
| |
| public HandlerAndRunnable(Handler handler, Runnable runnable) { |
| this.handler = handler; |
| this.runnable = runnable; |
| } |
| |
| public void dispatch() { |
| handler.post(runnable); |
| } |
| } |
| } |
| |