diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index ce1a58822c..a860249478 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -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. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 8dfea1e511..545b8f5155 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -139,6 +139,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { - private final MediaSource childSource; + private final MaskingMediaSource maskingMediaSource; private final int loopCount; private final Map childMediaPeriodIdToMediaPeriodId; private final Map mediaPeriodToChildMediaPeriodId; @@ -58,7 +58,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { */ 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 { @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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 891cb351c1..031d50e7d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -43,6 +43,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { @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 { */ 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 { } @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 { 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 { maskingPeriod.createPeriod(idInSource); } } + hasRealTimeline = true; isPrepared = true; refreshSourceInfo(this.timeline); } @@ -193,13 +208,15 @@ public final class MaskingMediaSource extends CompositeMediaSource { } 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 { 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 { * 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 { @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 { @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 { @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); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 5ee980d01f..f6dd4d79a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -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. + * + *

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. + * + *

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. + * + *

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() { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index d6e65cb34d..5110ad411c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -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()); + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 401fcf8034..c8c7190007 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -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],