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 bdf55fe40d..c72bed1b5b 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 @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; @@ -71,8 +70,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceByUid; private final boolean isAtomic; private final boolean useLazyPreparation; - private final Timeline.Window window; - private final Timeline.Period period; private boolean timelineUpdateScheduled; private Set nextTimelineUpdateOnCompletionActions; @@ -136,8 +133,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -435,33 +430,21 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders = new ArrayList<>(mediaSources.size()); for (MediaSource mediaSource : mediaSources) { - mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); } mediaSourcesPublic.addAll(index, mediaSourceHolders); if (playbackThreadHandler != null && !mediaSources.isEmpty()) { @@ -728,30 +711,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0) { MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); newMediaSourceHolder.reset( - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount()); + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } - correctOffsets( - newIndex, /* childIndexUpdate= */ 1, newMediaSourceHolder.timeline.getWindowCount()); + Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); + correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); - if (!useLazyPreparation) { - newMediaSourceHolder.hasStartedPreparing = true; - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); - } + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); } private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { if (mediaSourceHolder == null) { throw new IllegalArgumentException(); } - DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; - if (deferredTimeline.getTimeline() == timeline) { - return; - } if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); int windowOffsetUpdate = @@ -762,61 +738,13 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource periodPosition = - timeline.getPeriodPosition(window, period, /* windowIndex= */ 0, windowStartPositionUs); - Object periodUid = periodPosition.first; - long periodPositionUs = periodPosition.second; - mediaSourceHolder.timeline = DeferredTimeline.createWithRealTimeline(timeline, periodUid); - if (deferredMediaPeriod != null) { - deferredMediaPeriod.overridePreparePositionUs(periodPositionUs); - MediaPeriodId idInSource = - deferredMediaPeriod.id.copyWithPeriodUid( - getChildPeriodUid(mediaSourceHolder, deferredMediaPeriod.id.periodUid)); - deferredMediaPeriod.createPeriod(idInSource); - } - } - mediaSourceHolder.isPrepared = true; scheduleTimelineUpdate(); } private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.timeline; + Timeline oldTimeline = holder.mediaSource.getTimeline(); correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); @@ -831,7 +759,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public boolean hasStartedPreparing; - public boolean isPrepared; public boolean isRemoved; - public MediaSourceHolder(MediaSource mediaSource) { - this.mediaSource = mediaSource; - this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag()); - this.activeMediaPeriods = new ArrayList<>(); + 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.hasStartedPreparing = false; - this.isPrepared = false; this.isRemoved = false; - this.activeMediaPeriods.clear(); + this.activeMediaPeriodIds.clear(); } } @@ -944,7 +859,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + private boolean hasStartedPreparing; + private boolean isPrepared; + + /** + * Creates the masking media source. + * + * @param mediaSource A {@link MediaSource}. + * @param useLazyPreparation Whether the {@code mediaSource} is 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. + */ + public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = mediaSource; + this.useLazyPreparation = useLazyPreparation; + window = new Timeline.Window(); + period = new Timeline.Period(); + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); + } + + /** Returns the {@link Timeline}. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + if (!useLazyPreparation) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + + @Nullable + @Override + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. Source info refresh errors will be thrown when calling + // MaskingMediaPeriod.maybeThrowPrepareError. + } + + @Override + public MaskingMediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + if (isPrepared) { + MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid)); + mediaPeriod.createPeriod(idInSource); + } else { + // We should have at most one media period while source is unprepared because the duration is + // unset and we don't load beyond periods with unset duration. We need to figure out how to + // handle the prepare positions of multiple deferred media periods, should that ever change. + unpreparedMaskingMediaPeriod = mediaPeriod; + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + unpreparedMaskingMediaPeriod = null; + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline, @Nullable Object manifest) { + if (isPrepared) { + timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + } else if (newTimeline.isEmpty()) { + timeline = + MaskingTimeline.createWithRealTimeline(newTimeline, MaskingTimeline.DUMMY_EXTERNAL_ID); + } else { + // Determine first period and the start position. + // This will be: + // 1. The default window start position if no deferred period has been created yet. + // 2. The non-zero prepare position of the deferred period under the assumption that this is + // a non-zero initial seek position in the window. + // 3. The default window start position if the deferred period has a prepare position of zero + // under the assumption that the prepare position of zero was used because it's the + // default position of the DummyTimeline window. Note that this will override an + // intentional seek to zero for a window with a non-zero default position. This is + // unlikely to be a problem as a non-zero default position usually only occurs for live + // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions + // anyway. + newTimeline.getWindow(/* windowIndex= */ 0, window); + long windowStartPositionUs = window.getDefaultPositionUs(); + if (unpreparedMaskingMediaPeriod != null) { + long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); + if (periodPreparePositionUs != 0) { + windowStartPositionUs = periodPreparePositionUs; + } + } + Pair periodPosition = + newTimeline.getPeriodPosition( + window, period, /* windowIndex= */ 0, windowStartPositionUs); + Object periodUid = periodPosition.first; + long periodPositionUs = periodPosition.second; + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, periodUid); + if (unpreparedMaskingMediaPeriod != null) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + unpreparedMaskingMediaPeriod = null; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline, manifest); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + private Object getInternalPeriodUid(Object externalPeriodUid) { + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_ID) + ? timeline.replacedInternalId + : externalPeriodUid; + } + + private Object getExternalPeriodUid(Object internalPeriodUid) { + return timeline.replacedInternalId.equals(internalPeriodUid) + ? MaskingTimeline.DUMMY_EXTERNAL_ID + : internalPeriodUid; + } + + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a + * MaskingTimeline is used to keep the originally assigned dummy period ID. + */ + private static final class MaskingTimeline extends ForwardingTimeline { + + public static final Object DUMMY_EXTERNAL_ID = new Object(); + + private final Object replacedInternalId; + + /** + * Returns an instance with a dummy timeline using the provided window tag. + * + * @param windowTag A window tag. + */ + public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) { + return new MaskingTimeline(new DummyTimeline(windowTag), DUMMY_EXTERNAL_ID); + } + + /** + * Returns an instance with a real timeline, replacing the provided period ID with the already + * assigned dummy period ID. + * + * @param timeline The real timeline. + * @param firstPeriodUid The period UID in the timeline which will be replaced by the already + * assigned dummy period UID. + */ + public static MaskingTimeline createWithRealTimeline(Timeline timeline, Object firstPeriodUid) { + return new MaskingTimeline(timeline, firstPeriodUid); + } + + private MaskingTimeline(Timeline timeline, Object replacedInternalId) { + super(timeline); + this.replacedInternalId = replacedInternalId; + } + + /** + * Returns a copy with an updated timeline. This keeps the existing period replacement. + * + * @param timeline The new timeline. + */ + public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) { + return new MaskingTimeline(timeline, replacedInternalId); + } + + /** Returns the wrapped timeline. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedInternalId)) { + period.uid = DUMMY_EXTERNAL_ID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(DUMMY_EXTERNAL_ID.equals(uid) ? replacedInternalId : uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Object uid = timeline.getUidOfPeriod(periodIndex); + return Util.areEqual(uid, replacedInternalId) ? DUMMY_EXTERNAL_ID : uid; + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Nullable private final Object tag; + + public DummyTimeline(@Nullable Object tag) { + this.tag = tag; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + return window.set( + tag, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_ID, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == MaskingTimeline.DUMMY_EXTERNAL_ID ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return MaskingTimeline.DUMMY_EXTERNAL_ID; + } + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 8137289555..5187addec3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -279,13 +279,6 @@ public final class ConcatenatingMediaSourceTest { CountDownLatch preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); assertThat(preparedCondition.getCount()).isEqualTo(1); - // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = - testRunner.createPeriod( - new MediaPeriodId( - timeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0)); - testRunner.releasePeriod(secondLazyPeriod); - // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. testRunner.runOnPlaybackThread(