/*
* Copyright (C) 2019 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;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MaskingMediaPeriod;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
import androidx.media3.exoplayer.upstream.Allocator;
import java.io.IOException;
import java.util.ArrayList;
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 playlist.
*
* <p>With the exception of the constructor, all methods are called on the playback thread.
*/
/* package */ final class MediaSourceList {
/** Listener for source events. */
public interface MediaSourceListInfoRefreshListener {
/**
* Called when the timeline of a media item has changed and a new timeline that reflects the
* current playlist state needs to be created by calling {@link #createTimeline()}.
*
* <p>Called on the playback thread.
*/
void onPlaylistUpdateRequested();
}
private static final String TAG = "MediaSourceList";
private final PlayerId playerId;
private final List<MediaSourceHolder> mediaSourceHolders;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
private final Set<MediaSourceHolder> enabledMediaSourceHolders;
private ShuffleOrder shuffleOrder;
private boolean isPrepared;
@Nullable private TransferListener mediaTransferListener;
/**
* Creates the media source list.
*
* @param listener The {@link MediaSourceListInfoRefreshListener} to be informed of timeline
* changes.
* @param analyticsCollector An {@link AnalyticsCollector} to be registered for media source
* events.
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
* on.
* @param playerId The {@link PlayerId} of the player using this list.
*/
public MediaSourceList(
MediaSourceListInfoRefreshListener listener,
AnalyticsCollector analyticsCollector,
Handler analyticsCollectorHandler,
PlayerId playerId) {
this.playerId = playerId;
mediaSourceListInfoListener = listener;
shuffleOrder = new DefaultShuffleOrder(0);
mediaSourceByMediaPeriod = new IdentityHashMap<>();
mediaSourceByUid = new HashMap<>();
mediaSourceHolders = new ArrayList<>();
mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher();
drmEventDispatcher = new DrmSessionEventListener.EventDispatcher();
childSources = new HashMap<>();
enabledMediaSourceHolders = new HashSet<>();
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
}
/**
* Sets the media sources replacing any sources previously contained in the playlist.
*
* @param holders The list of {@link MediaSourceHolder}s to set.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
*/
public Timeline setMediaSources(List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder);
}
/**
* Adds multiple {@link MediaSourceHolder}s to the playlist.
*
* @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
* must be in the range of 0 <= index <= {@link #getSize()}.
* @param holders A list of {@link MediaSourceHolder}s to be added.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
*/
public Timeline addMediaSources(
int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
if (!holders.isEmpty()) {
this.shuffleOrder = shuffleOrder;
for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) {
MediaSourceHolder holder = holders.get(insertionIndex - index);
if (insertionIndex > 0) {
MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1);
Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
holder.reset(
/* firstWindowIndexInChild= */ previousHolder.firstWindowIndexInChild
+ previousTimeline.getWindowCount());
} else {
holder.reset(/* firstWindowIndexInChild= */ 0);
}
Timeline newTimeline = holder.mediaSource.getTimeline();
correctOffsets(
/* startIndex= */ insertionIndex,
/* windowOffsetUpdate= */ newTimeline.getWindowCount());
mediaSourceHolders.add(insertionIndex, holder);
mediaSourceByUid.put(holder.uid, holder);
if (isPrepared) {
prepareChildSource(holder);
if (mediaSourceByMediaPeriod.isEmpty()) {
enabledMediaSourceHolders.add(holder);
} else {
disableChildSource(holder);
}
}
}
}
return createTimeline();
}
/**
* Removes a range of {@link MediaSourceHolder}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()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
*/
public Timeline removeMediaSourceRange(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
this.shuffleOrder = shuffleOrder;
removeMediaSourcesInternal(fromIndex, toIndex);
return createTimeline();
}
/**
* Moves an existing media source 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()}.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0,
* {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0
*/
public Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) {
return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder);
}
/**
* Moves a range of media sources within the playlist.
*
* <p>Note: when specified range is empty or the from index equals the new from index, no actual
* media source is moved and no exception is thrown.
*
* @param fromIndex The initial range index, pointing to the first media source of the range that
* will be moved. 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 larger or equals than {@code fromIndex}.
* @param newFromIndex The target index of the first media source of the range that will be moved.
* @param shuffleOrder The new shuffle order.
* @return The new {@link Timeline}.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
* {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code
* newFromIndex} < 0
*/
public Timeline moveMediaSourceRange(
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
Assertions.checkArgument(
fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0);
this.shuffleOrder = shuffleOrder;
if (fromIndex == toIndex || fromIndex == newFromIndex) {
return createTimeline();
}
int startIndex = min(fromIndex, newFromIndex);
int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1;
int endIndex = max(newEndIndex, toIndex - 1);
int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
Util.moveItems(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
for (int i = startIndex; i <= endIndex; i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
holder.firstWindowIndexInChild = windowOffset;
windowOffset += holder.mediaSource.getTimeline().getWindowCount();
}
return createTimeline();
}
/** Clears the playlist. */
public Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize());
return createTimeline();
}
/** Whether the playlist is prepared. */
public boolean isPrepared() {
return isPrepared;
}
/** Returns the number of media sources in the playlist. */
public int getSize() {
return mediaSourceHolders.size();
}
/**
* Sets a new shuffle order to use when shuffling the child media sources.
*
* @param shuffleOrder A {@link ShuffleOrder}.
*/
public Timeline setShuffleOrder(ShuffleOrder shuffleOrder) {
int size = getSize();
if (shuffleOrder.getLength() != size) {
shuffleOrder =
shuffleOrder
.cloneAndClear()
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
}
this.shuffleOrder = shuffleOrder;
return createTimeline();
}
/** Prepares the playlist. */
public void prepare(@Nullable TransferListener mediaTransferListener) {
Assertions.checkState(!isPrepared);
this.mediaTransferListener = mediaTransferListener;
for (int i = 0; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
prepareChildSource(mediaSourceHolder);
enabledMediaSourceHolders.add(mediaSourceHolder);
}
isPrepared = true;
}
/**
* Returns a new {@link MediaPeriod} identified by {@code periodId}.
*
* @param id The identifier of the period.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param startPositionUs The expected start position, in microseconds.
* @return A new {@link MediaPeriod}.
*/
public MediaPeriod createPeriod(
MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) {
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaSource.MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
enableMediaSource(holder);
holder.activeMediaPeriodIds.add(childMediaPeriodId);
MediaPeriod mediaPeriod =
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
disableUnusedMediaSources();
return mediaPeriod;
}
/**
* Releases the period.
*
* @param mediaPeriod The period to release.
*/
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);
}
/** Releases the playlist. */
public void release() {
for (MediaSourceAndListener childSource : childSources.values()) {
try {
childSource.mediaSource.releaseSource(childSource.caller);
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Failed to release child source.", e);
}
childSource.mediaSource.removeEventListener(childSource.eventListener);
childSource.mediaSource.removeDrmEventListener(childSource.eventListener);
}
childSources.clear();
enabledMediaSourceHolders.clear();
isPrepared = false;
}
/** Creates a timeline reflecting the current state of the playlist. */
public Timeline createTimeline() {
if (mediaSourceHolders.isEmpty()) {
return Timeline.EMPTY;
}
int windowOffset = 0;
for (int i = 0; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
mediaSourceHolder.firstWindowIndexInChild = windowOffset;
windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount();
}
return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
}
// Internal methods.
private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
enabledMediaSourceHolders.add(mediaSourceHolder);
@Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder);
if (enabledChild != null) {
enabledChild.mediaSource.enable(enabledChild.caller);
}
}
private void disableUnusedMediaSources() {
Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
while (iterator.hasNext()) {
MediaSourceHolder holder = iterator.next();
if (holder.activeMediaPeriodIds.isEmpty()) {
disableChildSource(holder);
iterator.remove();
}
}
}
private void disableChildSource(MediaSourceHolder holder) {
@Nullable MediaSourceAndListener disabledChild = childSources.get(holder);
if (disabledChild != null) {
disabledChild.mediaSource.disable(disabledChild.caller);
}
}
private void removeMediaSourcesInternal(int fromIndex, int toIndex) {
for (int index = toIndex - 1; index >= fromIndex; index--) {
MediaSourceHolder holder = mediaSourceHolders.remove(index);
mediaSourceByUid.remove(holder.uid);
Timeline oldTimeline = holder.mediaSource.getTimeline();
correctOffsets(
/* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount());
holder.isRemoved = true;
if (isPrepared) {
maybeReleaseChildSource(holder);
}
}
}
private void correctOffsets(int startIndex, int windowOffsetUpdate) {
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate;
}
}
// Internal methods to manage child sources.
@Nullable
private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
MediaSourceHolder mediaSourceHolder, MediaSource.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;
}
private static int getWindowIndexForChildWindowIndex(
MediaSourceHolder mediaSourceHolder, int windowIndex) {
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
}
private void prepareChildSource(MediaSourceHolder holder) {
MediaSource mediaSource = holder.mediaSource;
MediaSource.MediaSourceCaller caller =
(source, timeline) -> mediaSourceListInfoListener.onPlaylistUpdateRequested();
ForwardingEventListener eventListener = new ForwardingEventListener(holder);
childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
mediaSource.addEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
mediaSource.addDrmEventListener(Util.createHandlerForCurrentOrMainLooper(), eventListener);
mediaSource.prepareSource(caller, mediaTransferListener, playerId);
}
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()) {
MediaSourceAndListener removedChild =
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
removedChild.mediaSource.releaseSource(removedChild.caller);
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener);
enabledMediaSourceHolders.remove(mediaSourceHolder);
}
}
/** Return uid of media source holder from period uid of concatenated source. */
private static Object getMediaSourceHolderUid(Object periodUid) {
return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
}
/** Return uid of child period from period uid of concatenated source. */
private static Object getChildPeriodUid(Object periodUid) {
return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
}
private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
}
/** Data class to hold playlist media sources together with meta data needed to process them. */
/* package */ static final class MediaSourceHolder implements MediaSourceInfoHolder {
public final MaskingMediaSource mediaSource;
public final Object uid;
public final List<MediaSource.MediaPeriodId> activeMediaPeriodIds;
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 firstWindowIndexInChild) {
this.firstWindowIndexInChild = firstWindowIndexInChild;
this.isRemoved = false;
this.activeMediaPeriodIds.clear();
}
@Override
public Object getUid() {
return uid;
}
@Override
public Timeline getTimeline() {
return mediaSource.getTimeline();
}
}
private static final class MediaSourceAndListener {
public final MediaSource mediaSource;
public final MediaSource.MediaSourceCaller caller;
public final ForwardingEventListener eventListener;
public MediaSourceAndListener(
MediaSource mediaSource,
MediaSource.MediaSourceCaller caller,
ForwardingEventListener eventListener) {
this.mediaSource = mediaSource;
this.caller = caller;
this.eventListener = eventListener;
}
}
private final class ForwardingEventListener
implements MediaSourceEventListener, DrmSessionEventListener {
private final MediaSourceList.MediaSourceHolder id;
private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private DrmSessionEventListener.EventDispatcher drmEventDispatcher;
public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher;
drmEventDispatcher = MediaSourceList.this.drmEventDispatcher;
this.id = id;
}
// MediaSourceEventListener implementation
@Override
public void onLoadStarted(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData);
}
}
@Override
public void onLoadCompleted(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData);
}
}
@Override
public void onLoadCanceled(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData);
}
}
@Override
public void onLoadError(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
}
}
@Override
public void onUpstreamDiscarded(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData);
}
}
@Override
public void onDownstreamFormatChanged(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData);
}
}
// DrmSessionEventListener implementation
@Override
public void onDrmSessionAcquired(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
@DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionAcquired(state);
}
}
@Override
public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysLoaded();
}
}
@Override
public void onDrmSessionManagerError(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionManagerError(error);
}
}
@Override
public void onDrmKeysRestored(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysRestored();
}
}
@Override
public void onDrmKeysRemoved(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmKeysRemoved();
}
}
@Override
public void onDrmSessionReleased(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
drmEventDispatcher.drmSessionReleased();
}
}
/** Updates the event dispatcher and returns whether the event should be dispatched. */
private boolean maybeUpdateEventDispatcher(
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
if (childMediaPeriodId != null) {
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
if (mediaPeriodId == null) {
// Media period not found. Ignore event.
return false;
}
}
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
if (mediaSourceEventDispatcher.windowIndex != windowIndex
|| !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) {
mediaSourceEventDispatcher =
MediaSourceList.this.mediaSourceEventDispatcher.withParameters(
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
}
if (drmEventDispatcher.windowIndex != windowIndex
|| !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) {
drmEventDispatcher =
MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId);
}
return true;
}
}
}