Move MediaSource masking code into separate class.

The masking logic for unprepared MediaSources is currently part of
ConcatanatingMediaSource. Moving it to its own class nicely separates the
code responsibilities and allows reuse.

PiperOrigin-RevId: 256360904
This commit is contained in:
tonihei 2019-07-03 15:07:55 +01:00 committed by Toni
parent d8c29e8211
commit 0145edb60d
3 changed files with 344 additions and 251 deletions

View file

@ -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<MediaSo
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
private final boolean isAtomic;
private final boolean useLazyPreparation;
private final Timeline.Window window;
private final Timeline.Period period;
private boolean timelineUpdateScheduled;
private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
@ -136,8 +133,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
this.pendingOnCompletionActions = new HashSet<>();
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<MediaSo
}
}
@Override
@SuppressWarnings("MissingSuperCall")
public void maybeThrowSourceInfoRefreshError() throws IOException {
// Do nothing. Source info refresh errors of the individual sources will be thrown when calling
// DeferredMediaPeriod.maybeThrowPrepareError.
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
MediaSourceHolder holder = mediaSourceByUid.get(mediaSourceHolderUid);
if (holder == null) {
// Stale event. The media source has already been removed.
holder = new MediaSourceHolder(new DummyMediaSource());
holder.hasStartedPreparing = true;
}
MaskingMediaPeriod mediaPeriod =
new MaskingMediaPeriod(holder.mediaSource, id, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
holder.activeMediaPeriods.add(mediaPeriod);
if (!holder.hasStartedPreparing) {
holder.hasStartedPreparing = true;
holder = new MediaSourceHolder(new DummyMediaSource(), useLazyPreparation);
holder.isRemoved = true;
prepareChildSource(holder, holder.mediaSource);
} else if (holder.isPrepared) {
MediaPeriodId idInSource = id.copyWithPeriodUid(getChildPeriodUid(holder, id.periodUid));
mediaPeriod.createPeriod(idInSource);
}
holder.activeMediaPeriodIds.add(childMediaPeriodId);
MediaPeriod mediaPeriod =
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
return mediaPeriod;
}
@ -469,8 +452,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder =
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
((MaskingMediaPeriod) mediaPeriod).releasePeriod();
holder.activeMediaPeriods.remove(mediaPeriod);
holder.mediaSource.releasePeriod(mediaPeriod);
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
maybeReleaseChildSource(holder);
}
@ -502,10 +485,10 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
@Nullable
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
MediaSourceHolder mediaSourceHolder, MediaPeriodId mediaPeriodId) {
for (int i = 0; i < mediaSourceHolder.activeMediaPeriods.size(); i++) {
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.activeMediaPeriods.get(i).id.windowSequenceNumber
if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
== mediaPeriodId.windowSequenceNumber) {
Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
return mediaPeriodId.copyWithPeriodUid(periodUid);
@ -535,7 +518,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
}
List<MediaSourceHolder> 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<MediaSo
private void addMediaSourceInternal(int newIndex, MediaSourceHolder newMediaSourceHolder) {
if (newIndex > 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<MediaSo
mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate);
}
}
if (mediaSourceHolder.isPrepared) {
mediaSourceHolder.timeline = deferredTimeline.cloneWithUpdatedTimeline(timeline);
} else if (timeline.isEmpty()) {
mediaSourceHolder.timeline =
DeferredTimeline.createWithRealTimeline(timeline, DeferredTimeline.DUMMY_ID);
} else {
// We should have at most one deferred media period for the DummyTimeline 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.
Assertions.checkState(mediaSourceHolder.activeMediaPeriods.size() <= 1);
MaskingMediaPeriod deferredMediaPeriod =
mediaSourceHolder.activeMediaPeriods.isEmpty()
? null
: mediaSourceHolder.activeMediaPeriods.get(0);
// 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.
timeline.getWindow(/* windowIndex= */ 0, window);
long windowStartPositionUs = window.getDefaultPositionUs();
if (deferredMediaPeriod != null) {
long periodPreparePositionUs = deferredMediaPeriod.getPreparePositionUs();
if (periodPreparePositionUs != 0) {
windowStartPositionUs = periodPreparePositionUs;
}
}
Pair<Object, Long> 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<MediaSo
MediaSourceHolder holder = mediaSourceHolders.get(i);
holder.childIndex = i;
holder.firstWindowIndexInChild = windowOffset;
windowOffset += holder.timeline.getWindowCount();
windowOffset += holder.mediaSource.getTimeline().getWindowCount();
}
}
@ -846,11 +774,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
}
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
// Release if the source has been removed from the playlist, but only if it has been previously
// prepared and only if we are not waiting for an existing media period to be released.
if (mediaSourceHolder.isRemoved
&& mediaSourceHolder.hasStartedPreparing
&& mediaSourceHolder.activeMediaPeriods.isEmpty()) {
// Release if the source has been removed from the playlist and no periods are still active.
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
releaseChildSource(mediaSourceHolder);
}
}
@ -861,46 +786,36 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
}
/** Return uid of child period from period uid of concatenated source. */
private static Object getChildPeriodUid(MediaSourceHolder holder, Object periodUid) {
Object childUid = ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
return childUid.equals(DeferredTimeline.DUMMY_ID) ? holder.timeline.replacedId : childUid;
private static Object getChildPeriodUid(Object periodUid) {
return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
}
private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
if (holder.timeline.replacedId.equals(childPeriodUid)) {
childPeriodUid = DeferredTimeline.DUMMY_ID;
}
return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
}
/** Data class to hold playlist media sources together with meta data needed to process them. */
/* package */ static final class MediaSourceHolder {
public final MediaSource mediaSource;
public final MaskingMediaSource mediaSource;
public final Object uid;
public final List<MaskingMediaPeriod> activeMediaPeriods;
public final List<MediaPeriodId> 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<MediaSo
int windowCount = 0;
int periodCount = 0;
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
timelines[index] = mediaSourceHolder.timeline;
timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
firstWindowInChildIndices[index] = windowCount;
firstPeriodInChildIndices[index] = periodCount;
windowCount += timelines[index].getWindowCount();
@ -1003,135 +918,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
}
}
/**
* Timeline used as placeholder for an unprepared media source. After preparation, a
* DeferredTimeline is used to keep the originally assigned dummy period ID.
*/
private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private final Object replacedId;
/**
* Returns an instance with a dummy timeline using the provided window tag.
*
* @param windowTag A window tag.
*/
public static DeferredTimeline createWithDummyTimeline(@Nullable Object windowTag) {
return new DeferredTimeline(new DummyTimeline(windowTag), DUMMY_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 DeferredTimeline createWithRealTimeline(
Timeline timeline, Object firstPeriodUid) {
return new DeferredTimeline(timeline, firstPeriodUid);
}
private DeferredTimeline(Timeline timeline, Object replacedId) {
super(timeline);
this.replacedId = replacedId;
}
/**
* Returns a copy with an updated timeline. This keeps the existing period replacement.
*
* @param timeline The new timeline.
*/
public DeferredTimeline cloneWithUpdatedTimeline(Timeline timeline) {
return new DeferredTimeline(timeline, replacedId);
}
/** Returns 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, replacedId)) {
period.uid = DUMMY_ID;
}
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid);
}
@Override
public Object getUidOfPeriod(int periodIndex) {
Object uid = timeline.getUidOfPeriod(periodIndex);
return Util.areEqual(uid, replacedId) ? DUMMY_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= */ DeferredTimeline.DUMMY_ID,
/* windowIndex= */ 0,
/* durationUs = */ C.TIME_UNSET,
/* positionInWindowUs= */ 0);
}
@Override
public int getIndexOfPeriod(Object uid) {
return uid == DeferredTimeline.DUMMY_ID ? 0 : C.INDEX_UNSET;
}
@Override
public Object getUidOfPeriod(int periodIndex) {
return DeferredTimeline.DUMMY_ID;
}
}
/** Dummy media source which does nothing and does not support creating periods. */
private static final class DummyMediaSource extends BaseMediaSource {

View file

@ -0,0 +1,314 @@
/*
* 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 com.google.android.exoplayer2.source;
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.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
/**
* A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media
* structure is known.
*/
public final class MaskingMediaSource extends CompositeMediaSource<Void> {
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<Object, Long> 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;
}
}
}

View file

@ -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(