AbstractConcatenatedTimeline.java
/*
* Copyright (C) 2017 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 android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.source.ShuffleOrder;
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
@UnstableApi
public abstract class AbstractConcatenatedTimeline extends Timeline {
private final int childCount;
private final ShuffleOrder shuffleOrder;
private final boolean isAtomic;
/**
* Returns UID of child timeline from a concatenated period UID.
*
* @param concatenatedUid UID of a period in a concatenated timeline.
* @return UID of the child timeline this period belongs to.
*/
@SuppressWarnings("nullness:return")
public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) {
return ((Pair<?, ?>) concatenatedUid).first;
}
/**
* Returns UID of the period in the child timeline from a concatenated period UID.
*
* @param concatenatedUid UID of a period in a concatenated timeline.
* @return UID of the period in the child timeline.
*/
@SuppressWarnings("nullness:return")
public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) {
return ((Pair<?, ?>) concatenatedUid).second;
}
/**
* Returns a concatenated UID for a period or window in a child timeline.
*
* @param childTimelineUid UID of the child timeline this period or window belongs to.
* @param childPeriodOrWindowUid UID of the period or window in the child timeline.
* @return UID of the period or window in the concatenated timeline.
*/
public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodOrWindowUid) {
return Pair.create(childTimelineUid, childPeriodOrWindowUid);
}
/**
* Sets up a concatenated timeline with a shuffle order of child timelines.
*
* @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a
* single item for repeating and shuffling.
* @param shuffleOrder A shuffle order of child timelines. The number of child timelines must
* match the number of elements in the shuffle order.
*/
public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) {
this.isAtomic = isAtomic;
this.shuffleOrder = shuffleOrder;
this.childCount = shuffleOrder.getLength();
}
@Override
public int getNextWindowIndex(
int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
if (isAtomic) {
// Adapt repeat and shuffle mode to atomic concatenation.
repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
shuffleModeEnabled = false;
}
// Find next window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int nextWindowIndexInChild =
getTimelineByChildIndex(childIndex)
.getNextWindowIndex(
windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled);
if (nextWindowIndexInChild != C.INDEX_UNSET) {
return firstWindowIndexInChild + nextWindowIndexInChild;
}
// If not found, find first window of next non-empty child.
int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled);
while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) {
nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled);
}
if (nextChildIndex != C.INDEX_UNSET) {
return getFirstWindowIndexByChildIndex(nextChildIndex)
+ getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled);
}
// If not found, this is the last window.
if (repeatMode == Player.REPEAT_MODE_ALL) {
return getFirstWindowIndex(shuffleModeEnabled);
}
return C.INDEX_UNSET;
}
@Override
public int getPreviousWindowIndex(
int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
if (isAtomic) {
// Adapt repeat and shuffle mode to atomic concatenation.
repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
shuffleModeEnabled = false;
}
// Find previous window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int previousWindowIndexInChild =
getTimelineByChildIndex(childIndex)
.getPreviousWindowIndex(
windowIndex - firstWindowIndexInChild,
repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode,
shuffleModeEnabled);
if (previousWindowIndexInChild != C.INDEX_UNSET) {
return firstWindowIndexInChild + previousWindowIndexInChild;
}
// If not found, find last window of previous non-empty child.
int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled);
while (previousChildIndex != C.INDEX_UNSET
&& getTimelineByChildIndex(previousChildIndex).isEmpty()) {
previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled);
}
if (previousChildIndex != C.INDEX_UNSET) {
return getFirstWindowIndexByChildIndex(previousChildIndex)
+ getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled);
}
// If not found, this is the first window.
if (repeatMode == Player.REPEAT_MODE_ALL) {
return getLastWindowIndex(shuffleModeEnabled);
}
return C.INDEX_UNSET;
}
@Override
public int getLastWindowIndex(boolean shuffleModeEnabled) {
if (childCount == 0) {
return C.INDEX_UNSET;
}
if (isAtomic) {
shuffleModeEnabled = false;
}
// Find last non-empty child.
int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1;
while (getTimelineByChildIndex(lastChildIndex).isEmpty()) {
lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled);
if (lastChildIndex == C.INDEX_UNSET) {
// All children are empty.
return C.INDEX_UNSET;
}
}
return getFirstWindowIndexByChildIndex(lastChildIndex)
+ getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled);
}
@Override
public int getFirstWindowIndex(boolean shuffleModeEnabled) {
if (childCount == 0) {
return C.INDEX_UNSET;
}
if (isAtomic) {
shuffleModeEnabled = false;
}
// Find first non-empty child.
int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0;
while (getTimelineByChildIndex(firstChildIndex).isEmpty()) {
firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled);
if (firstChildIndex == C.INDEX_UNSET) {
// All children are empty.
return C.INDEX_UNSET;
}
}
return getFirstWindowIndexByChildIndex(firstChildIndex)
+ getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled);
}
@Override
public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
getTimelineByChildIndex(childIndex)
.getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs);
Object childUid = getChildUidByChildIndex(childIndex);
// Don't create new objects if the child is using SINGLE_WINDOW_UID.
window.uid =
Window.SINGLE_WINDOW_UID.equals(window.uid)
? childUid
: getConcatenatedUid(childUid, window.uid);
window.firstPeriodIndex += firstPeriodIndexInChild;
window.lastPeriodIndex += firstPeriodIndexInChild;
return window;
}
@Override
public final Period getPeriodByUid(Object periodUid, Period period) {
Object childUid = getChildTimelineUidFromConcatenatedUid(periodUid);
Object childPeriodUid = getChildPeriodUidFromConcatenatedUid(periodUid);
int childIndex = getChildIndexByChildUid(childUid);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
getTimelineByChildIndex(childIndex).getPeriodByUid(childPeriodUid, period);
period.windowIndex += firstWindowIndexInChild;
period.uid = periodUid;
return period;
}
@Override
public final Period getPeriod(int periodIndex, Period period, boolean setIds) {
int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
getTimelineByChildIndex(childIndex)
.getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
period.windowIndex += firstWindowIndexInChild;
if (setIds) {
period.uid =
getConcatenatedUid(
getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid));
}
return period;
}
@Override
public final int getIndexOfPeriod(Object uid) {
if (!(uid instanceof Pair)) {
return C.INDEX_UNSET;
}
Object childUid = getChildTimelineUidFromConcatenatedUid(uid);
Object childPeriodUid = getChildPeriodUidFromConcatenatedUid(uid);
int childIndex = getChildIndexByChildUid(childUid);
if (childIndex == C.INDEX_UNSET) {
return C.INDEX_UNSET;
}
int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(childPeriodUid);
return periodIndexInChild == C.INDEX_UNSET
? C.INDEX_UNSET
: getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild;
}
@Override
public final Object getUidOfPeriod(int periodIndex) {
int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);
Object periodUidInChild =
getTimelineByChildIndex(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild);
return getConcatenatedUid(getChildUidByChildIndex(childIndex), periodUidInChild);
}
/**
* Returns the index of the child timeline containing the given period index.
*
* @param periodIndex A valid period index within the bounds of the timeline.
*/
protected abstract int getChildIndexByPeriodIndex(int periodIndex);
/**
* Returns the index of the child timeline containing the given window index.
*
* @param windowIndex A valid window index within the bounds of the timeline.
*/
protected abstract int getChildIndexByWindowIndex(int windowIndex);
/**
* Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not
* found.
*
* @param childUid A child UID.
* @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found.
*/
protected abstract int getChildIndexByChildUid(Object childUid);
/**
* Returns the child timeline for the child with the given index.
*
* @param childIndex A valid child index within the bounds of the timeline.
*/
protected abstract Timeline getTimelineByChildIndex(int childIndex);
/**
* Returns the first period index belonging to the child timeline with the given index.
*
* @param childIndex A valid child index within the bounds of the timeline.
*/
protected abstract int getFirstPeriodIndexByChildIndex(int childIndex);
/**
* Returns the first window index belonging to the child timeline with the given index.
*
* @param childIndex A valid child index within the bounds of the timeline.
*/
protected abstract int getFirstWindowIndexByChildIndex(int childIndex);
/**
* Returns the UID of the child timeline with the given index.
*
* @param childIndex A valid child index within the bounds of the timeline.
*/
protected abstract Object getChildUidByChildIndex(int childIndex);
private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) {
return shuffleModeEnabled
? shuffleOrder.getNextIndex(childIndex)
: childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;
}
private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) {
return shuffleModeEnabled
? shuffleOrder.getPreviousIndex(childIndex)
: childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET;
}
}