Remove restriction from ConcatenatingMediaSource2

The class currently disallows offsets of periods in their windows
except for the very first window. This is not necessary because
we can use TimeOffsetMediaPeriod to eliminate the offset if needed.
This makes the class more useful for many use cases, in particular
for using it with ClippingMediaSource.

Issue: google/ExoPlayer#11226
PiperOrigin-RevId: 563702120
This commit is contained in:
tonihei 2023-09-08 03:29:05 -07:00 committed by Copybara-Service
parent 763dddfbd4
commit 5a1322c9f9
4 changed files with 211 additions and 34 deletions

View File

@ -18,6 +18,9 @@
([#612](https://github.com/androidx/media/issues/612)). ([#612](https://github.com/androidx/media/issues/612)).
* Add `MediaPeriodId` parameter to * Add `MediaPeriodId` parameter to
`CompositeMediaSource.getMediaTimeForChildMediaTime`. `CompositeMediaSource.getMediaTimeForChildMediaTime`.
* Support `ClippingMediaSource` (and other sources with period/window time
offsets) in `ConcatenatingMediaSource2`
([#11226](https://github.com/google/ExoPlayer/issues/11226)).
* Transformer: * Transformer:
* Changed `frameRate` and `durationUs` parameters of * Changed `frameRate` and `durationUs` parameters of
`SampleConsumer.queueInputBitmap` to `TimestampIterator`. `SampleConsumer.queueInputBitmap` to `TimestampIterator`.

View File

@ -37,19 +37,21 @@ import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.upstream.Allocator; import androidx.media3.exoplayer.upstream.Allocator;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
/** /**
* Concatenates multiple {@link MediaSource MediaSources}, combining everything in one single {@link * Concatenates multiple {@link MediaSource MediaSources}, combining everything in one single {@link
* Timeline.Window}. * Timeline.Window}.
* *
* <p>This class can only be used under the following conditions: * <p>This class can be used under the following conditions:
* *
* <ul> * <ul>
* <li>All sources must be non-empty. * <li>All sources must be non-empty.
* <li>All {@link Timeline.Window Windows} defined by the sources, except the first, must have an * <li>The {@link Timeline.Window#getPositionInFirstPeriodUs() period offset} in all windows
* {@link Timeline.Window#getPositionInFirstPeriodUs() period offset} of zero. This excludes, * (except for the first one) must not change during the lifetime of this media source. This
* for example, live streams or {@link ClippingMediaSource} with a non-zero start position. * excludes, for example, live streams with moving live windows or dynamic updates to the
* clipping start time of a {@link ClippingMediaSource}.
* </ul> * </ul>
*/ */
@UnstableApi @UnstableApi
@ -155,6 +157,13 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
checkStateNotNull( checkStateNotNull(
mediaSourceFactory, mediaSourceFactory,
"Must use useDefaultMediaSourceFactory or setMediaSourceFactory first."); "Must use useDefaultMediaSourceFactory or setMediaSourceFactory first.");
if (initialPlaceholderDurationMs == C.TIME_UNSET
&& mediaItem.clippingConfiguration.endPositionMs != C.TIME_END_OF_SOURCE) {
// If the item is going to be clipped, we can provide a placeholder duration automatically.
initialPlaceholderDurationMs =
mediaItem.clippingConfiguration.endPositionMs
- mediaItem.clippingConfiguration.startPositionMs;
}
return add(mediaSourceFactory.createMediaSource(mediaItem), initialPlaceholderDurationMs); return add(mediaSourceFactory.createMediaSource(mediaItem), initialPlaceholderDurationMs);
} }
@ -277,8 +286,15 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
id.windowSequenceNumber, mediaSourceHolders.size(), holder.index)); id.windowSequenceNumber, mediaSourceHolders.size(), holder.index));
enableChildSource(holder.index); enableChildSource(holder.index);
holder.activeMediaPeriods++; holder.activeMediaPeriods++;
long timeOffsetUs =
id.isAd()
? 0
: checkNotNull(holder.periodTimeOffsetsByUid.get(childMediaPeriodId.periodUid));
MediaPeriod mediaPeriod = MediaPeriod mediaPeriod =
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); new TimeOffsetMediaPeriod(
holder.mediaSource.createPeriod(
childMediaPeriodId, allocator, startPositionUs - timeOffsetUs),
timeOffsetUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder); mediaSourceByMediaPeriod.put(mediaPeriod, holder);
disableUnusedMediaSources(); disableUnusedMediaSources();
return mediaPeriod; return mediaPeriod;
@ -287,7 +303,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod)); MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
holder.mediaSource.releasePeriod(mediaPeriod); holder.mediaSource.releasePeriod(((TimeOffsetMediaPeriod) mediaPeriod).getWrappedMediaPeriod());
holder.activeMediaPeriods--; holder.activeMediaPeriods--;
if (!mediaSourceByMediaPeriod.isEmpty()) { if (!mediaSourceByMediaPeriod.isEmpty()) {
disableUnusedMediaSources(); disableUnusedMediaSources();
@ -336,6 +352,21 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
return 0; return 0;
} }
@Override
protected long getMediaTimeForChildMediaTime(
Integer childSourceId, long mediaTimeMs, @Nullable MediaPeriodId mediaPeriodId) {
if (mediaTimeMs == C.TIME_UNSET || mediaPeriodId == null || mediaPeriodId.isAd()) {
return mediaTimeMs;
}
@Nullable
Long timeOffsetUs =
mediaSourceHolders.get(childSourceId).periodTimeOffsetsByUid.get(mediaPeriodId.periodUid);
if (timeOffsetUs == null) {
return mediaTimeMs;
}
return mediaTimeMs + Util.usToMs(timeOffsetUs);
}
private boolean handleMessage(Message msg) { private boolean handleMessage(Message msg) {
if (msg.what == MSG_UPDATE_TIMELINE) { if (msg.what == MSG_UPDATE_TIMELINE) {
updateTimeline(); updateTimeline();
@ -383,13 +414,15 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
boolean manifestsAreIdentical = true; boolean manifestsAreIdentical = true;
boolean hasInitialManifest = false; boolean hasInitialManifest = false;
@Nullable Object initialManifest = null; @Nullable Object initialManifest = null;
for (int i = 0; i < mediaSourceHolders.size(); i++) { int mediaSourceHoldersCount = mediaSourceHolders.size();
for (int i = 0; i < mediaSourceHoldersCount; i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i); MediaSourceHolder holder = mediaSourceHolders.get(i);
Timeline timeline = holder.mediaSource.getTimeline(); Timeline timeline = holder.mediaSource.getTimeline();
checkArgument(!timeline.isEmpty(), "Can't concatenate empty child Timeline."); checkArgument(!timeline.isEmpty(), "Can't concatenate empty child Timeline.");
timelinesBuilder.add(timeline); timelinesBuilder.add(timeline);
firstPeriodIndicesBuilder.add(periodCount); firstPeriodIndicesBuilder.add(periodCount);
periodCount += timeline.getPeriodCount(); int periodCountInMediaSourceHolder = timeline.getPeriodCount();
periodCount += periodCountInMediaSourceHolder;
for (int j = 0; j < timeline.getWindowCount(); j++) { for (int j = 0; j < timeline.getWindowCount(); j++) {
timeline.getWindow(/* windowIndex= */ j, window); timeline.getWindow(/* windowIndex= */ j, window);
if (!hasInitialManifest) { if (!hasInitialManifest) {
@ -411,31 +444,38 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
if (holder.index == 0 && j == 0) { if (holder.index == 0 && j == 0) {
defaultPositionUs = window.defaultPositionUs; defaultPositionUs = window.defaultPositionUs;
nextPeriodOffsetInWindowUs = -window.positionInFirstPeriodUs; nextPeriodOffsetInWindowUs = -window.positionInFirstPeriodUs;
} else {
checkArgument(
window.positionInFirstPeriodUs == 0,
"Can't concatenate windows. A window has a non-zero offset in a period.");
} }
// Assume placeholder windows are seekable to not prevent seeking in other periods. // Assume placeholder windows are seekable to not prevent seeking in other periods.
isSeekable &= window.isSeekable || window.isPlaceholder; isSeekable &= window.isSeekable || window.isPlaceholder;
isDynamic |= window.isDynamic; isDynamic |= window.isDynamic;
}
int childPeriodCount = timeline.getPeriodCount(); for (int k = window.firstPeriodIndex; k <= window.lastPeriodIndex; k++) {
for (int j = 0; j < childPeriodCount; j++) { periodOffsetsInWindowUsBuilder.add(nextPeriodOffsetInWindowUs);
periodOffsetsInWindowUsBuilder.add(nextPeriodOffsetInWindowUs); timeline.getPeriod(/* periodIndex= */ k, period, /* setIds= */ true);
timeline.getPeriod(/* periodIndex= */ j, period); long periodDurationUs = period.durationUs;
long periodDurationUs = period.durationUs; if (periodDurationUs == C.TIME_UNSET) {
if (periodDurationUs == C.TIME_UNSET) { checkArgument(
window.firstPeriodIndex == window.lastPeriodIndex,
"Can't apply placeholder duration to multiple periods with unknown duration "
+ "in a single window.");
periodDurationUs = windowDurationUs + window.positionInFirstPeriodUs;
}
long timeOffsetUsForPeriod = 0;
boolean isFirstPeriodInNonFirstWindow =
k == window.firstPeriodIndex && (holder.index != 0 || j != 0);
if (isFirstPeriodInNonFirstWindow && periodDurationUs != C.TIME_UNSET) {
timeOffsetUsForPeriod = -window.positionInFirstPeriodUs;
periodDurationUs += timeOffsetUsForPeriod;
}
Object periodUid = checkNotNull(period.uid);
checkArgument( checkArgument(
childPeriodCount == 1, holder.activeMediaPeriods == 0
"Can't concatenate multiple periods with unknown duration in one window."); || !holder.periodTimeOffsetsByUid.containsKey(periodUid)
long windowDurationUs = || holder.periodTimeOffsetsByUid.get(periodUid).equals(timeOffsetUsForPeriod),
window.durationUs != C.TIME_UNSET "Can't handle windows with changing offset in first period.");
? window.durationUs holder.periodTimeOffsetsByUid.put(periodUid, timeOffsetUsForPeriod);
: holder.initialPlaceholderDurationUs; nextPeriodOffsetInWindowUs += periodDurationUs;
periodDurationUs = windowDurationUs + window.positionInFirstPeriodUs;
} }
nextPeriodOffsetInWindowUs += periodDurationUs;
} }
} }
return new ConcatenatedTimeline( return new ConcatenatedTimeline(
@ -492,6 +532,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
public final MaskingMediaSource mediaSource; public final MaskingMediaSource mediaSource;
public final int index; public final int index;
public final long initialPlaceholderDurationUs; public final long initialPlaceholderDurationUs;
public final HashMap<Object, Long> periodTimeOffsetsByUid;
public int activeMediaPeriods; public int activeMediaPeriods;
@ -500,6 +541,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
this.mediaSource = new MaskingMediaSource(mediaSource, /* useLazyPreparation= */ false); this.mediaSource = new MaskingMediaSource(mediaSource, /* useLazyPreparation= */ false);
this.index = index; this.index = index;
this.initialPlaceholderDurationUs = initialPlaceholderDurationUs; this.initialPlaceholderDurationUs = initialPlaceholderDurationUs;
this.periodTimeOffsetsByUid = new HashMap<>();
} }
} }
@ -547,8 +589,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
} }
@Override @Override
public final Window getWindow( public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
int windowIndex, Window window, long defaultPositionProjectionUs) {
return window.set( return window.set(
Window.SINGLE_WINDOW_UID, Window.SINGLE_WINDOW_UID,
mediaItem, mediaItem,
@ -567,7 +608,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
} }
@Override @Override
public final Period getPeriodByUid(Object periodUid, Period period) { public Period getPeriodByUid(Object periodUid, Period period) {
int childIndex = getChildIndex(periodUid); int childIndex = getChildIndex(periodUid);
Object childPeriodUid = getChildPeriodUid(periodUid); Object childPeriodUid = getChildPeriodUid(periodUid);
Timeline timeline = timelines.get(childIndex); Timeline timeline = timelines.get(childIndex);
@ -576,17 +617,19 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
timeline.getPeriodByUid(childPeriodUid, period); timeline.getPeriodByUid(childPeriodUid, period);
period.windowIndex = 0; period.windowIndex = 0;
period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex); period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex);
period.durationUs = getPeriodDurationUs(period, periodIndex);
period.uid = periodUid; period.uid = periodUid;
return period; return period;
} }
@Override @Override
public final Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
int childIndex = getChildIndexByPeriodIndex(periodIndex); int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex); int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex);
timelines.get(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); timelines.get(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
period.windowIndex = 0; period.windowIndex = 0;
period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex); period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex);
period.durationUs = getPeriodDurationUs(period, periodIndex);
if (setIds) { if (setIds) {
period.uid = getPeriodUid(childIndex, checkNotNull(period.uid)); period.uid = getPeriodUid(childIndex, checkNotNull(period.uid));
} }
@ -594,7 +637,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
} }
@Override @Override
public final int getIndexOfPeriod(Object uid) { public int getIndexOfPeriod(Object uid) {
if (!(uid instanceof Pair) || !(((Pair<?, ?>) uid).first instanceof Integer)) { if (!(uid instanceof Pair) || !(((Pair<?, ?>) uid).first instanceof Integer)) {
return C.INDEX_UNSET; return C.INDEX_UNSET;
} }
@ -607,7 +650,7 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
} }
@Override @Override
public final Object getUidOfPeriod(int periodIndex) { public Object getUidOfPeriod(int periodIndex) {
int childIndex = getChildIndexByPeriodIndex(periodIndex); int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex); int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex);
Object periodUidInChild = Object periodUidInChild =
@ -619,5 +662,18 @@ public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Intege
return Util.binarySearchFloor( return Util.binarySearchFloor(
firstPeriodIndices, periodIndex + 1, /* inclusive= */ false, /* stayInBounds= */ false); firstPeriodIndices, periodIndex + 1, /* inclusive= */ false, /* stayInBounds= */ false);
} }
private long getPeriodDurationUs(Timeline.Period childPeriod, int periodIndex) {
// Keep unset duration, but force duration to match offset of next period otherwise.
if (childPeriod.durationUs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
long periodStartTimeInWindowUs = periodOffsetsInWindowUs.get(periodIndex);
long periodEndTimeInWindowUs =
periodIndex == periodOffsetsInWindowUs.size() - 1
? durationUs
: periodOffsetsInWindowUs.get(periodIndex + 1);
return periodEndTimeInWindowUs - periodStartTimeInWindowUs;
}
} }
} }

View File

@ -22,6 +22,7 @@ import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ConcatenatingMediaSource2;
import androidx.media3.test.utils.CapturingRenderersFactory; import androidx.media3.test.utils.CapturingRenderersFactory;
import androidx.media3.test.utils.DumpFileAsserts; import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock; import androidx.media3.test.utils.FakeClock;
@ -71,7 +72,7 @@ public final class ClippingPlaylistPlaybackTest {
ShadowMediaCodecConfig.forAllSupportedMimeTypes(); ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Test @Test
public void test() throws Exception { public void playbackWithClippingMediaSources() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext(); Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory = CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext); new CapturingRenderersFactory(applicationContext);
@ -113,6 +114,61 @@ public final class ClippingPlaylistPlaybackTest {
"playbackdumps/clipping/" + firstItemConfig.name + "_" + secondItemConfig.name + ".dump"); "playbackdumps/clipping/" + firstItemConfig.name + "_" + secondItemConfig.name + ".dump");
} }
@Test
public void playbackWithClippingMediaSourcesInConcatenatingMediaSource2() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaSource(
new ConcatenatingMediaSource2.Builder()
.useDefaultMediaSourceFactory(applicationContext)
.add(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(firstItemConfig.startMs)
.setEndPositionMs(firstItemConfig.endMs)
.build())
.build(),
/* initialPlaceholderDurationMs= */ firstItemConfig.endMs == C.TIME_END_OF_SOURCE
? 1
: C.TIME_UNSET)
.add(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(secondItemConfig.startMs)
.setEndPositionMs(secondItemConfig.endMs)
.build())
.build(),
/* initialPlaceholderDurationMs= */ secondItemConfig.endMs == C.TIME_END_OF_SOURCE
? 1
: C.TIME_UNSET)
.build());
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
// Intentionally uses the same dump files as for the test above because the renderer output
// should not be affected by combining all sources in a ConcatenatingMediaSource2.
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
"playbackdumps/clipping/" + firstItemConfig.name + "_" + secondItemConfig.name + ".dump");
}
private static final class ClippingConfig { private static final class ClippingConfig {
public final String name; public final String name;

View File

@ -123,6 +123,68 @@ public final class ConcatenatingMediaSource2Test {
/* manifest= */ null) /* manifest= */ null)
.withAdPlaybackState(/* periodIndex= */ 4, adPlaybackState))); .withAdPlaybackState(/* periodIndex= */ 4, adPlaybackState)));
// Second full example with additional offsets in all other windows (including multiple windows
// per source and a single last period with an offset).
builder.add(
new TestConfig(
"offset_in_multiple_windows_and_ads",
buildConcatenatingMediaSource(
buildMediaSource(
buildWindow(
/* periodCount= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationMs= */ 1000,
/* defaultPositionMs= */ 123,
/* windowOffsetInFirstPeriodMs= */ 50),
buildWindow(
/* periodCount= */ 2,
/* isSeekable= */ false,
/* isDynamic= */ false,
/* durationMs= */ 2500,
/* defaultPositionMs= */ 234,
/* windowOffsetInFirstPeriodMs= */ 300)),
buildMediaSource(
buildWindow(
/* periodCount= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationMs= */ 500,
/* defaultPositionMs= */ 234,
/* windowOffsetInFirstPeriodMs= */ 100,
adPlaybackState)),
buildMediaSource(
buildWindow(
/* periodCount= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationMs= */ 1200,
/* defaultPositionMs= */ 234,
/* windowOffsetInFirstPeriodMs= */ 250)),
buildMediaSource(
buildWindow(
/* periodCount= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationMs= */ 600,
/* defaultPositionMs= */ 234,
/* windowOffsetInFirstPeriodMs= */ 200))),
/* expectedAdDiscontinuities= */ 3,
new ExpectedTimelineData(
/* isSeekable= */ false,
/* isDynamic= */ false,
/* defaultPositionMs= */ 123,
/* periodDurationsMs= */ new long[] {550, 500, 1250, 1250, 500, 600, 600, 600},
/* periodOffsetsInWindowMs= */ new long[] {
-50, 500, 1000, 2250, 3500, 4000, 4600, 5200
},
/* periodIsPlaceholder= */ new boolean[] {
false, false, false, false, false, false, false, false
},
/* windowDurationMs= */ 5800,
/* manifest= */ null)
.withAdPlaybackState(/* periodIndex= */ 4, adPlaybackState)));
builder.add( builder.add(
new TestConfig( new TestConfig(
"multipleMediaSource_sameManifest", "multipleMediaSource_sameManifest",