add default methods isSingleWindow and getInitialTimeline to MediaSource interface

PiperOrigin-RevId: 277695826
This commit is contained in:
bachinger 2019-10-31 11:32:47 +00:00 committed by Oliver Woodman
parent df251ad1be
commit 01a4cf98d5
7 changed files with 352 additions and 25 deletions

View file

@ -19,6 +19,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* A flexible representation of the structure of media. A timeline is able to represent the
@ -278,6 +279,48 @@ public abstract class Timeline {
return positionInFirstPeriodUs;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Window that = (Window) obj;
return Util.areEqual(uid, that.uid)
&& Util.areEqual(tag, that.tag)
&& Util.areEqual(manifest, that.manifest)
&& presentationStartTimeMs == that.presentationStartTimeMs
&& windowStartTimeMs == that.windowStartTimeMs
&& isSeekable == that.isSeekable
&& isDynamic == that.isDynamic
&& isLive == that.isLive
&& defaultPositionUs == that.defaultPositionUs
&& durationUs == that.durationUs
&& firstPeriodIndex == that.firstPeriodIndex
&& lastPeriodIndex == that.lastPeriodIndex
&& positionInFirstPeriodUs == that.positionInFirstPeriodUs;
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + uid.hashCode();
result = 31 * result + (tag == null ? 0 : tag.hashCode());
result = 31 * result + (manifest == null ? 0 : manifest.hashCode());
result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32));
result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32));
result = 31 * result + (isSeekable ? 1 : 0);
result = 31 * result + (isDynamic ? 1 : 0);
result = 31 * result + (isLive ? 1 : 0);
result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32));
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
result = 31 * result + firstPeriodIndex;
result = 31 * result + lastPeriodIndex;
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
return result;
}
}
/**
@ -534,6 +577,34 @@ public abstract class Timeline {
return adPlaybackState.adResumePositionUs;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
Period that = (Period) obj;
return Util.areEqual(id, that.id)
&& Util.areEqual(uid, that.uid)
&& windowIndex == that.windowIndex
&& durationUs == that.durationUs
&& positionInWindowUs == that.positionInWindowUs
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + (id == null ? 0 : id.hashCode());
result = 31 * result + (uid == null ? 0 : uid.hashCode());
result = 31 * result + windowIndex;
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32));
result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode());
return result;
}
}
/** An empty timeline. */

View file

@ -139,6 +139,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
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.
*

View file

@ -35,7 +35,7 @@ import java.util.Map;
*/
public final class LoopingMediaSource extends CompositeMediaSource<Void> {
private final MediaSource childSource;
private final MaskingMediaSource maskingMediaSource;
private final int loopCount;
private final Map<MediaPeriodId, MediaPeriodId> childMediaPeriodIdToMediaPeriodId;
private final Map<MediaPeriod, MediaPeriodId> mediaPeriodToChildMediaPeriodId;
@ -58,7 +58,7 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
*/
public LoopingMediaSource(MediaSource childSource, int loopCount) {
Assertions.checkArgument(loopCount > 0);
this.childSource = childSource;
this.maskingMediaSource = new MaskingMediaSource(childSource, /* useLazyPreparation= */ false);
this.loopCount = loopCount;
childMediaPeriodIdToMediaPeriodId = new HashMap<>();
mediaPeriodToChildMediaPeriodId = new HashMap<>();
@ -67,32 +67,45 @@ public final class LoopingMediaSource extends CompositeMediaSource<Void> {
@Override
@Nullable
public Object getTag() {
return childSource.getTag();
return maskingMediaSource.getTag();
}
@Nullable
@Override
public Timeline getInitialTimeline() {
return loopCount != Integer.MAX_VALUE
? new LoopingTimeline(maskingMediaSource.getTimeline(), loopCount)
: new InfinitelyLoopingTimeline(maskingMediaSource.getTimeline());
}
@Override
public boolean isSingleWindow() {
return false;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
prepareChildSource(/* id= */ null, childSource);
prepareChildSource(/* id= */ null, maskingMediaSource);
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
if (loopCount == Integer.MAX_VALUE) {
return childSource.createPeriod(id, allocator, startPositionUs);
return maskingMediaSource.createPeriod(id, allocator, startPositionUs);
}
Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid);
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid);
childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id);
MediaPeriod mediaPeriod =
childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
maskingMediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId);
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
childSource.releasePeriod(mediaPeriod);
maskingMediaSource.releasePeriod(mediaPeriod);
MediaPeriodId childMediaPeriodId = mediaPeriodToChildMediaPeriodId.remove(mediaPeriod);
if (childMediaPeriodId != null) {
childMediaPeriodIdToMediaPeriodId.remove(childMediaPeriodId);

View file

@ -43,6 +43,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher;
private boolean hasStartedPreparing;
private boolean isPrepared;
private boolean hasRealTimeline;
/**
* Creates the masking media source.
@ -54,14 +55,22 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
*/
public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
this.mediaSource = mediaSource;
this.useLazyPreparation = useLazyPreparation;
this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow();
window = new Timeline.Window();
period = new Timeline.Period();
timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());
Timeline initialTimeline = mediaSource.getInitialTimeline();
if (initialTimeline != null) {
timeline =
MaskingTimeline.createWithRealTimeline(
initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null);
hasRealTimeline = true;
} else {
timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());
}
}
/** Returns the {@link Timeline}. */
public Timeline getTimeline() {
public synchronized Timeline getTimeline() {
return timeline;
}
@ -129,14 +138,16 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
@Override
protected void onChildSourceInfoRefreshed(
protected synchronized void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline newTimeline) {
if (isPrepared) {
timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
} else if (newTimeline.isEmpty()) {
timeline =
MaskingTimeline.createWithRealTimeline(
newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);
hasRealTimeline
? timeline.cloneWithUpdatedTimeline(newTimeline)
: MaskingTimeline.createWithRealTimeline(
newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);
} else {
// Determine first period and the start position.
// This will be:
@ -164,7 +175,10 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
window, period, /* windowIndex= */ 0, windowStartPositionUs);
Object periodUid = periodPosition.first;
long periodPositionUs = periodPosition.second;
timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
timeline =
hasRealTimeline
? timeline.cloneWithUpdatedTimeline(newTimeline)
: MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
if (unpreparedMaskingMediaPeriod != null) {
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
maskingPeriod.overridePreparePositionUs(periodPositionUs);
@ -173,6 +187,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
maskingPeriod.createPeriod(idInSource);
}
}
hasRealTimeline = true;
isPrepared = true;
refreshSourceInfo(this.timeline);
}
@ -193,13 +208,15 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
private Object getInternalPeriodUid(Object externalPeriodUid) {
return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)
return timeline.replacedInternalPeriodUid != null
&& externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)
? timeline.replacedInternalPeriodUid
: externalPeriodUid;
}
private Object getExternalPeriodUid(Object internalPeriodUid) {
return timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
return timeline.replacedInternalPeriodUid != null
&& timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID
: internalPeriodUid;
}
@ -212,8 +229,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object();
private final Object replacedInternalWindowUid;
private final Object replacedInternalPeriodUid;
@Nullable private final Object replacedInternalWindowUid;
@Nullable private final Object replacedInternalPeriodUid;
/**
* Returns an instance with a dummy timeline using the provided window tag.
@ -236,12 +253,14 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
* assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}.
*/
public static MaskingTimeline createWithRealTimeline(
Timeline timeline, Object firstWindowUid, Object firstPeriodUid) {
Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) {
return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
}
private MaskingTimeline(
Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) {
Timeline timeline,
@Nullable Object replacedInternalWindowUid,
@Nullable Object replacedInternalPeriodUid) {
super(timeline);
this.replacedInternalWindowUid = replacedInternalWindowUid;
this.replacedInternalPeriodUid = replacedInternalPeriodUid;
@ -273,7 +292,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
if (Util.areEqual(period.uid, replacedInternalPeriodUid)) {
if (Util.areEqual(period.uid, replacedInternalPeriodUid) && setIds) {
period.uid = DUMMY_EXTERNAL_PERIOD_UID;
}
return period;
@ -282,7 +301,9 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(
DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid);
DUMMY_EXTERNAL_PERIOD_UID.equals(uid) && replacedInternalPeriodUid != null
? replacedInternalPeriodUid
: uid);
}
@Override
@ -333,8 +354,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
return period.set(
/* id= */ 0,
/* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID,
/* id= */ setIds ? 0 : null,
/* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null,
/* windowIndex= */ 0,
/* durationUs = */ C.TIME_UNSET,
/* positionInWindowUs= */ 0);

View file

@ -228,6 +228,33 @@ public interface MediaSource {
*/
void removeEventListener(MediaSourceEventListener eventListener);
/**
* Returns the initial dummy timeline that is returned immediately when the real timeline is not
* yet known, or null to let the player create an initial timeline.
*
* <p>The initial timeline must use the same uids for windows and periods that the real timeline
* will use. It also must provide windows which are marked as dynamic to indicate that the window
* is expected to change when the real timeline arrives.
*
* <p>Any media source which has multiple windows should typically provide such an initial
* timeline to make sure the player reports the correct number of windows immediately.
*/
@Nullable
default Timeline getInitialTimeline() {
return null;
}
/**
* Returns true if the media source is guaranteed to never have zero or more than one window.
*
* <p>The default implementation returns {@code true}.
*
* @return true if the source has exactly one window.
*/
default boolean isSingleWindow() {
return true;
}
/** Returns the tag set on the media source, or null if none was set. */
@Nullable
default Object getTag() {

View file

@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
@ -58,4 +60,148 @@ public class TimelineTest {
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
}
@Test
public void testWindowEquals() {
Timeline.Window window = new Timeline.Window();
assertThat(window).isEqualTo(new Timeline.Window());
Timeline.Window otherWindow = new Timeline.Window();
otherWindow.tag = new Object();
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.manifest = new Object();
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.presentationStartTimeMs = C.TIME_UNSET;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.windowStartTimeMs = C.TIME_UNSET;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.isSeekable = true;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.isDynamic = true;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.isLive = true;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.defaultPositionUs = C.TIME_UNSET;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.durationUs = C.TIME_UNSET;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.firstPeriodIndex = 1;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.lastPeriodIndex = 1;
assertThat(window).isNotEqualTo(otherWindow);
otherWindow = new Timeline.Window();
otherWindow.positionInFirstPeriodUs = C.TIME_UNSET;
assertThat(window).isNotEqualTo(otherWindow);
window.uid = new Object();
window.tag = new Object();
window.manifest = new Object();
window.presentationStartTimeMs = C.TIME_UNSET;
window.windowStartTimeMs = C.TIME_UNSET;
window.isSeekable = true;
window.isDynamic = true;
window.isLive = true;
window.defaultPositionUs = C.TIME_UNSET;
window.durationUs = C.TIME_UNSET;
window.firstPeriodIndex = 1;
window.lastPeriodIndex = 1;
window.positionInFirstPeriodUs = C.TIME_UNSET;
otherWindow =
otherWindow.set(
window.uid,
window.tag,
window.manifest,
window.presentationStartTimeMs,
window.windowStartTimeMs,
window.isSeekable,
window.isDynamic,
window.isLive,
window.defaultPositionUs,
window.durationUs,
window.firstPeriodIndex,
window.lastPeriodIndex,
window.positionInFirstPeriodUs);
assertThat(window).isEqualTo(otherWindow);
}
@Test
public void testWindowHashCode() {
Timeline.Window window = new Timeline.Window();
Timeline.Window otherWindow = new Timeline.Window();
assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode());
window.tag = new Object();
assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode());
otherWindow.tag = window.tag;
assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode());
}
@Test
public void testPeriodEquals() {
Timeline.Period period = new Timeline.Period();
assertThat(period).isEqualTo(new Timeline.Period());
Timeline.Period otherPeriod = new Timeline.Period();
otherPeriod.id = new Object();
assertThat(period).isNotEqualTo(otherPeriod);
otherPeriod = new Timeline.Period();
otherPeriod.uid = new Object();
assertThat(period).isNotEqualTo(otherPeriod);
otherPeriod = new Timeline.Period();
otherPeriod.windowIndex = 12;
assertThat(period).isNotEqualTo(otherPeriod);
otherPeriod = new Timeline.Period();
otherPeriod.durationUs = 11L;
assertThat(period).isNotEqualTo(otherPeriod);
otherPeriod = new Timeline.Period();
period.id = new Object();
period.uid = new Object();
period.windowIndex = 1;
period.durationUs = 123L;
otherPeriod =
otherPeriod.set(
period.id,
period.uid,
period.windowIndex,
period.durationUs,
/* positionInWindowUs= */ 0);
assertThat(period).isEqualTo(otherPeriod);
}
@Test
public void testPeriodHashCode() {
Timeline.Period period = new Timeline.Period();
Timeline.Period otherPeriod = new Timeline.Period();
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
period.windowIndex = 12;
assertThat(period.hashCode()).isNotEqualTo(otherPeriod.hashCode());
otherPeriod.windowIndex = period.windowIndex;
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
}
}

View file

@ -40,6 +40,7 @@ public final class FakeTimeline extends Timeline {
public final Object id;
public final boolean isSeekable;
public final boolean isDynamic;
public final boolean isLive;
public final long durationUs;
public final AdPlaybackState adPlaybackState;
@ -99,10 +100,41 @@ public final class FakeTimeline extends Timeline {
boolean isDynamic,
long durationUs,
AdPlaybackState adPlaybackState) {
this(
periodCount,
id,
isSeekable,
isDynamic,
/* isLive= */ isDynamic,
durationUs,
adPlaybackState);
}
/**
* Creates a window definition with ad groups.
*
* @param periodCount The number of periods in the window. Each period get an equal slice of the
* total window duration.
* @param id The UID of the window.
* @param isSeekable Whether the window is seekable.
* @param isDynamic Whether the window is dynamic.
* @param isLive Whether the window is live.
* @param durationUs The duration of the window in microseconds.
* @param adPlaybackState The ad playback state.
*/
public TimelineWindowDefinition(
int periodCount,
Object id,
boolean isSeekable,
boolean isDynamic,
boolean isLive,
long durationUs,
AdPlaybackState adPlaybackState) {
this.periodCount = periodCount;
this.id = id;
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.isLive = isLive;
this.durationUs = durationUs;
this.adPlaybackState = adPlaybackState;
}
@ -189,7 +221,7 @@ public final class FakeTimeline extends Timeline {
/* windowStartTimeMs= */ C.TIME_UNSET,
windowDefinition.isSeekable,
windowDefinition.isDynamic,
/* isLive= */ windowDefinition.isDynamic,
windowDefinition.isLive,
/* defaultPositionUs= */ 0,
windowDefinition.durationUs,
periodOffsets[windowIndex],