Gradle dependencies
compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-exoplayer
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
Concatenates multiple MediaSource instances. The list of MediaSource instances
can be modified during playback. It is valid for the same MediaSource instance to be
present more than once in the concatenation. Access to this class is thread-safe.
Summary
Methods |
---|
public synchronized void | addMediaSource(int index, MediaSource mediaSource)
Adds a MediaSource to the playlist. |
public synchronized void | addMediaSource(int index, MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)
Adds a MediaSource to the playlist and executes a custom action on completion. |
public synchronized void | addMediaSource(MediaSource mediaSource)
Appends a MediaSource to the playlist. |
public synchronized void | addMediaSource(MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)
Appends a MediaSource to the playlist and executes a custom action on completion. |
public synchronized void | addMediaSources(java.util.Collection<MediaSource> mediaSources)
Appends multiple MediaSources to the playlist. |
public synchronized void | addMediaSources(java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)
Appends multiple MediaSources to the playlist and executes a custom action on
completion. |
public synchronized void | addMediaSources(int index, java.util.Collection<MediaSource> mediaSources)
Adds multiple MediaSources to the playlist. |
public synchronized void | addMediaSources(int index, java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)
Adds multiple MediaSources to the playlist and executes a custom action on completion. |
public synchronized void | clear()
Clears the playlist. |
public synchronized void | clear(Handler handler, java.lang.Runnable onCompletionAction)
Clears the playlist and executes a custom action on completion. |
public MediaPeriod | createPeriod(MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)
|
protected void | disableInternal()
Disables the source, see MediaSource.disable(MediaSource.MediaSourceCaller). |
protected void | enableInternal()
Enables the source, see MediaSource.enable(MediaSource.MediaSourceCaller). |
public synchronized Timeline | getInitialTimeline()
|
public MediaItem | getMediaItem()
|
protected MediaSource.MediaPeriodId | getMediaPeriodIdForChildMediaPeriodId(androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId)
|
public synchronized MediaSource | getMediaSource(int index)
Returns the MediaSource at a specified index. |
public synchronized int | getSize()
Returns the number of media sources in the playlist. |
protected int | getWindowIndexForChildWindowIndex(androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder mediaSourceHolder, int windowIndex)
|
public boolean | isSingleWindow()
|
public synchronized void | moveMediaSource(int currentIndex, int newIndex)
Moves an existing MediaSource within the playlist. |
public synchronized void | moveMediaSource(int currentIndex, int newIndex, Handler handler, java.lang.Runnable onCompletionAction)
Moves an existing MediaSource within the playlist and executes a custom action on
completion. |
protected void | onChildSourceInfoRefreshed(androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline)
|
protected abstract void | prepareSourceInternal(TransferListener mediaTransferListener)
Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId). |
public void | releasePeriod(MediaPeriod mediaPeriod)
|
protected abstract void | releaseSourceInternal()
Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller). |
public synchronized MediaSource | removeMediaSource(int index)
Removes a MediaSource from the playlist. |
public synchronized MediaSource | removeMediaSource(int index, Handler handler, java.lang.Runnable onCompletionAction)
Removes a MediaSource from the playlist and executes a custom action on completion. |
public synchronized void | removeMediaSourceRange(int fromIndex, int toIndex)
Removes a range of MediaSources from the playlist, by specifying an initial index
(included) and a final index (excluded). |
public synchronized void | removeMediaSourceRange(int fromIndex, int toIndex, Handler handler, java.lang.Runnable onCompletionAction)
Removes a range of MediaSources from the playlist, by specifying an initial index
(included) and a final index (excluded), and executes a custom action on completion. |
public synchronized void | setShuffleOrder(ShuffleOrder shuffleOrder)
Sets a new shuffle order to use when shuffling the child media sources. |
public synchronized void | setShuffleOrder(ShuffleOrder shuffleOrder, Handler handler, java.lang.Runnable onCompletionAction)
Sets a new shuffle order to use when shuffling the child media sources. |
from CompositeMediaSource<T> | disableChildSource, enableChildSource, getMediaPeriodIdForChildMediaPeriodId, getMediaTimeForChildMediaTime, getWindowIndexForChildWindowIndex, maybeThrowSourceInfoRefreshError, onChildSourceInfoRefreshed, prepareChildSource, releaseChildSource |
from BaseMediaSource | addDrmEventListener, addEventListener, createDrmEventDispatcher, createDrmEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, disable, enable, getPlayerId, isEnabled, prepareSource, prepareSource, prepareSourceCalled, refreshSourceInfo, releaseSource, removeDrmEventListener, removeEventListener, setPlayerId |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Constructors
public
ConcatenatingMediaSource(
MediaSource mediaSources[])
Parameters:
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.
public
ConcatenatingMediaSource(boolean isAtomic,
MediaSource mediaSources[])
Parameters:
isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated
as a single item for repeating and shuffling.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.
Parameters:
isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated
as a single item for repeating and shuffling.
shuffleOrder: The ShuffleOrder to use when shuffling the child media sources.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.
public
ConcatenatingMediaSource(boolean isAtomic, boolean useLazyPreparation,
ShuffleOrder shuffleOrder,
MediaSource mediaSources[])
Parameters:
isAtomic: Whether the concatenating media source will be treated as atomic, i.e., treated
as a single item for repeating and shuffling.
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.
shuffleOrder: The ShuffleOrder to use when shuffling the child media sources.
mediaSources: The MediaSources to concatenate. It is valid for the same MediaSource instance to be present more than once in the array.
Methods
public synchronized
Timeline getInitialTimeline()
public boolean
isSingleWindow()
public synchronized void
addMediaSource(
MediaSource mediaSource)
Appends a MediaSource to the playlist.
Parameters:
mediaSource: The MediaSource to be added to the list.
public synchronized void
addMediaSource(
MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)
Appends a MediaSource to the playlist and executes a custom action on completion.
Parameters:
mediaSource: The MediaSource to be added to the list.
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
source has been added to the playlist.
public synchronized void
addMediaSource(int index,
MediaSource mediaSource)
Adds a MediaSource to the playlist.
Parameters:
index: The index at which the new MediaSource will be inserted. This index must
be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSource: The MediaSource to be added to the list.
public synchronized void
addMediaSource(int index,
MediaSource mediaSource, Handler handler, java.lang.Runnable onCompletionAction)
Adds a MediaSource to the playlist and executes a custom action on completion.
Parameters:
index: The index at which the new MediaSource will be inserted. This index must
be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSource: The MediaSource to be added to the list.
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
source has been added to the playlist.
public synchronized void
addMediaSources(java.util.Collection<MediaSource> mediaSources)
Appends multiple MediaSources to the playlist.
Parameters:
mediaSources: A collection of MediaSources 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(java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)
Appends multiple MediaSources to the playlist and executes a custom action on
completion.
Parameters:
mediaSources: A collection of MediaSources to be added to the list. The media
sources are added in the order in which they appear in this collection.
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
sources have been added to the playlist.
public synchronized void
addMediaSources(int index, java.util.Collection<MediaSource> mediaSources)
Adds multiple MediaSources to the playlist.
Parameters:
index: The index at which the new MediaSources will be inserted. This index must
be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSources: A collection of MediaSources 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, java.util.Collection<MediaSource> mediaSources, Handler handler, java.lang.Runnable onCompletionAction)
Adds multiple MediaSources to the playlist and executes a custom action on completion.
Parameters:
index: The index at which the new MediaSources will be inserted. This index must
be in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
mediaSources: A collection of MediaSources to be added to the list. The media
sources are added in the order in which they appear in this collection.
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
sources have been added to the playlist.
public synchronized
MediaSource removeMediaSource(int index)
Removes a MediaSource from the playlist.
Note: If you want to move the instance, it's preferable to use ConcatenatingMediaSource.moveMediaSource(int, int) instead.
Note: If you want to remove a set of contiguous sources, it's preferable to use ConcatenatingMediaSource.removeMediaSourceRange(int, int) instead.
Parameters:
index: The index at which the media source will be removed. This index must be in the
range of 0 <= index < ConcatenatingMediaSource.getSize().
Returns:
The removed MediaSource.
public synchronized
MediaSource removeMediaSource(int index, Handler handler, java.lang.Runnable onCompletionAction)
Removes a MediaSource from the playlist and executes a custom action on completion.
Note: If you want to move the instance, it's preferable to use ConcatenatingMediaSource.moveMediaSource(int, int, Handler, Runnable) instead.
Note: If you want to remove a set of contiguous sources, it's preferable to use ConcatenatingMediaSource.removeMediaSourceRange(int, int, Handler, Runnable) instead.
Parameters:
index: The index at which the media source will be removed. This index must be in the
range of 0 <= index < ConcatenatingMediaSource.getSize().
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
source has been removed from the playlist.
Returns:
The removed MediaSource.
public synchronized void
removeMediaSourceRange(int fromIndex, int toIndex)
Removes a range of MediaSources from the playlist, by specifying an initial index
(included) and a final index (excluded).
Note: when specified range is empty, no actual media source is removed and no exception is
thrown.
Parameters:
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 <= ConcatenatingMediaSource.getSize().
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 <= ConcatenatingMediaSource.getSize().
public synchronized void
removeMediaSourceRange(int fromIndex, int toIndex, Handler handler, java.lang.Runnable onCompletionAction)
Removes a range of MediaSources from the playlist, by specifying an initial index
(included) and a final index (excluded), and executes a custom action on completion.
Note: when specified range is empty, no actual media source is removed and no exception is
thrown.
Parameters:
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 <= ConcatenatingMediaSource.getSize().
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 <= ConcatenatingMediaSource.getSize().
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
source range has been removed from the playlist.
public synchronized void
moveMediaSource(int currentIndex, int newIndex)
Moves an existing MediaSource within the playlist.
Parameters:
currentIndex: The current index of the media source in the playlist. This index must be
in the range of 0 <= index < ConcatenatingMediaSource.getSize().
newIndex: The target index of the media source in the playlist. This index must be in the
range of 0 <= index < ConcatenatingMediaSource.getSize().
public synchronized void
moveMediaSource(int currentIndex, int newIndex, Handler handler, java.lang.Runnable onCompletionAction)
Moves an existing MediaSource within the playlist and executes a custom action on
completion.
Parameters:
currentIndex: The current index of the media source in the playlist. This index must be
in the range of 0 <= index < ConcatenatingMediaSource.getSize().
newIndex: The target index of the media source in the playlist. This index must be in the
range of 0 <= index < ConcatenatingMediaSource.getSize().
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the media
source has been moved.
public synchronized void
clear()
Clears the playlist.
public synchronized void
clear(Handler handler, java.lang.Runnable onCompletionAction)
Clears the playlist and executes a custom action on completion.
Parameters:
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the playlist
has been cleared.
public synchronized int
getSize()
Returns the number of media sources in the playlist.
public synchronized
MediaSource getMediaSource(int index)
Returns the MediaSource at a specified index.
Parameters:
index: An index in the range of 0 <= index <= ConcatenatingMediaSource.getSize().
Returns:
The MediaSource at this index.
public synchronized void
setShuffleOrder(
ShuffleOrder shuffleOrder)
Sets a new shuffle order to use when shuffling the child media sources.
Parameters:
shuffleOrder: A ShuffleOrder.
public synchronized void
setShuffleOrder(
ShuffleOrder shuffleOrder, Handler handler, java.lang.Runnable onCompletionAction)
Sets a new shuffle order to use when shuffling the child media sources.
Parameters:
shuffleOrder: A ShuffleOrder.
handler: The Handler
to run onCompletionAction.
onCompletionAction: A java.lang.Runnable
which is executed immediately after the shuffle
order has been changed.
protected abstract void
prepareSourceInternal(
TransferListener mediaTransferListener)
Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId). This method is called at most once until the next call to BaseMediaSource.releaseSourceInternal().
Parameters:
mediaTransferListener: The transfer listener which should be informed of any media data
transfers. May be null if no listener is available. Note that this listener should usually
be only informed of transfers related to the media loads and not of auxiliary loads for
manifests and other data.
protected void
enableInternal()
Enables the source, see MediaSource.enable(MediaSource.MediaSourceCaller).
protected void
disableInternal()
Disables the source, see MediaSource.disable(MediaSource.MediaSourceCaller).
protected abstract void
releaseSourceInternal()
Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller). This method is called
exactly once after each call to BaseMediaSource.prepareSourceInternal(TransferListener).
protected void
onChildSourceInfoRefreshed(androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder mediaSourceHolder,
MediaSource mediaSource,
Timeline timeline)
protected int
getWindowIndexForChildWindowIndex(androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder mediaSourceHolder, int windowIndex)
Source
/*
* 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 androidx.media3.exoplayer.source;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.AbstractConcatenatedTimeline;
import androidx.media3.exoplayer.source.ConcatenatingMediaSource.MediaSourceHolder;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.exoplayer.upstream.Allocator;
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} instances. The list of {@link MediaSource} instances
* 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.
*
* @deprecated Use playlist modification methods like {@link
* androidx.media3.common.Player#addMediaItem} instead. To combine multiple sources together as
* one item or for further wrapping in other {@link MediaSource} instances, use {@link
* ConcatenatingMediaSource2} instead.
*/
@Deprecated
@UnstableApi
public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHolder> {
private static final int MSG_ADD = 1;
private static final int MSG_REMOVE = 2;
private static final int MSG_MOVE = 3;
private static final int MSG_SET_SHUFFLE_ORDER = 4;
private static final int MSG_UPDATE_TIMELINE = 5;
private static final int MSG_ON_COMPLETION = 6;
private static final MediaItem PLACEHOLDER_MEDIA_ITEM =
new MediaItem.Builder().setUri(Uri.EMPTY).build();
// 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 IdentityHashMap<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
public MediaItem getMediaItem() {
// This method is actually never called because getInitialTimeline is implemented and hence the
// MaskingMediaSource does not need to create a placeholder timeline for this media source.
return PLACEHOLDER_MEDIA_ITEM;
}
@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 FakeMediaSource(), 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);
}
/**
* {@inheritDoc}
*
* @hide
*/
@Override
protected void onChildSourceInfoRefreshed(
MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, Timeline timeline) {
updateMediaSourceInternal(mediaSourceHolder, timeline);
}
/**
* {@inheritDoc}
*
* @hide
*/
@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;
}
/**
* {@inheritDoc}
*
* @hide
*/
@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 = min(currentIndex, newIndex);
int endIndex = 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;
}
}
/** A media source which does nothing and does not support creating periods. */
private static final class FakeMediaSource extends BaseMediaSource {
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
// Do nothing.
}
@Override
public MediaItem getMediaItem() {
return PLACEHOLDER_MEDIA_ITEM;
}
@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);
}
}
}