Commit 86f03b7b by bachinger Committed by Ian Baker

Do not select unprepared media period in getMediaPeriodForEvent

There is a race with the ad period preparation having completed
and `onDownstreamFormatChanged` being called when a live stream
is joined in an ad period. In this case the stream event metadata
of the period is immediately emitted and causing an ad media period
being created that is selected in `getMediaPeriodForEvent` before
being prepared (1 out of 4).

Using an `isPrepared` flag makes sure we don't hand out the media
period to early in `getMediaPeriodForEvent`.

PiperOrigin-RevId: 522340046
parent 04e007bf
...@@ -685,6 +685,9 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -685,6 +685,9 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
if (mediaLoadData != null && mediaLoadData.mediaStartTimeMs != C.TIME_UNSET) { if (mediaLoadData != null && mediaLoadData.mediaStartTimeMs != C.TIME_UNSET) {
for (int i = 0; i < mediaPeriods.size(); i++) { for (int i = 0; i < mediaPeriods.size(); i++) {
MediaPeriodImpl mediaPeriod = mediaPeriods.get(i); MediaPeriodImpl mediaPeriod = mediaPeriods.get(i);
if (!mediaPeriod.isPrepared) {
continue;
}
long startTimeInPeriodUs = long startTimeInPeriodUs =
getMediaPeriodPositionUs( getMediaPeriodPositionUs(
Util.msToUs(mediaLoadData.mediaStartTimeMs), Util.msToUs(mediaLoadData.mediaStartTimeMs),
...@@ -703,7 +706,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -703,7 +706,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
mediaPeriod.lastStartPositionUs = positionUs; mediaPeriod.lastStartPositionUs = positionUs;
if (hasStartedPreparing) { if (hasStartedPreparing) {
if (isPrepared) { if (isPrepared) {
checkNotNull(mediaPeriod.callback).onPrepared(mediaPeriod); mediaPeriod.onPrepared();
} }
return; return;
} }
...@@ -920,10 +923,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -920,10 +923,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
public void onPrepared(MediaPeriod actualMediaPeriod) { public void onPrepared(MediaPeriod actualMediaPeriod) {
isPrepared = true; isPrepared = true;
for (int i = 0; i < mediaPeriods.size(); i++) { for (int i = 0; i < mediaPeriods.size(); i++) {
MediaPeriodImpl mediaPeriod = mediaPeriods.get(i); mediaPeriods.get(i).onPrepared();
if (mediaPeriod.callback != null) {
mediaPeriod.callback.onPrepared(mediaPeriod);
}
} }
} }
...@@ -1101,6 +1101,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -1101,6 +1101,7 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
public @MonotonicNonNull Callback callback; public @MonotonicNonNull Callback callback;
public long lastStartPositionUs; public long lastStartPositionUs;
public boolean[] hasNotifiedDownstreamFormatChange; public boolean[] hasNotifiedDownstreamFormatChange;
public boolean isPrepared;
public MediaPeriodImpl( public MediaPeriodImpl(
SharedMediaPeriod sharedPeriod, SharedMediaPeriod sharedPeriod,
...@@ -1114,6 +1115,14 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource ...@@ -1114,6 +1115,14 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
hasNotifiedDownstreamFormatChange = new boolean[0]; hasNotifiedDownstreamFormatChange = new boolean[0];
} }
/** Called when the preparation has completed. */
public void onPrepared() {
if (callback != null) {
callback.onPrepared(this);
}
isPrepared = true;
}
@Override @Override
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
this.callback = callback; this.callback = callback;
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.ads; package com.google.android.exoplayer2.source.ads;
import static com.google.android.exoplayer2.C.DATA_TYPE_MEDIA;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilPosition;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
...@@ -33,12 +34,15 @@ import static org.mockito.Mockito.verify; ...@@ -33,12 +34,15 @@ import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.util.Pair; import android.util.Pair;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
...@@ -47,12 +51,19 @@ import com.google.android.exoplayer2.analytics.PlayerId; ...@@ -47,12 +51,19 @@ import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.robolectric.PlaybackOutput; import com.google.android.exoplayer2.robolectric.PlaybackOutput;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.DumpFileAsserts;
import com.google.android.exoplayer2.testutil.FakeClock; import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
...@@ -151,6 +162,125 @@ public final class ServerSideAdInsertionMediaSourceTest { ...@@ -151,6 +162,125 @@ public final class ServerSideAdInsertionMediaSourceTest {
} }
@Test @Test
public void createPeriod_unpreparedAdMediaPeriodImplReplacesContentPeriod_adPeriodNotSelected()
throws Exception {
DefaultAllocator allocator =
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024);
MediaPeriod.Callback callback =
new MediaPeriod.Callback() {
@Override
public void onPrepared(MediaPeriod mediaPeriod) {}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {}
};
AdPlaybackState adPlaybackState =
new AdPlaybackState("adsId").withLivePostrollPlaceholderAppended();
FakeTimeline wrappedTimeline =
new FakeTimeline(
new FakeTimeline.TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000L,
/* defaultPositionUs= */ 3_000_000L,
/* windowOffsetInFirstPeriodUs= */ 0L,
AdPlaybackState.NONE));
ServerSideAdInsertionMediaSource mediaSource =
new ServerSideAdInsertionMediaSource(
new FakeMediaSource(wrappedTimeline), /* adPlaybackStateUpdater= */ null);
AtomicReference<Timeline> timelineReference = new AtomicReference<>();
AtomicReference<MediaSource.MediaPeriodId> mediaPeriodIdReference = new AtomicReference<>();
mediaSource.setAdPlaybackStates(
ImmutableMap.of(new Pair<>(0, 0), adPlaybackState), wrappedTimeline);
mediaSource.addEventListener(
new Handler(Util.getCurrentOrMainLooper()),
new MediaSourceEventListener() {
@Override
public void onDownstreamFormatChanged(
int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) {
mediaPeriodIdReference.set(mediaPeriodId);
}
});
mediaSource.prepareSource(
(source, timeline) -> timelineReference.set(timeline),
/* mediaTransferListener= */ null,
PlayerId.UNSET);
runMainLooperUntil(() -> timelineReference.get() != null);
Timeline firstTimeline = timelineReference.get();
MediaSource.MediaPeriodId mediaPeriodId1 =
new MediaSource.MediaPeriodId(
new Pair<>(0, 0), /* windowSequenceNumber= */ 0L, /* nextAdGroupIndex= */ 0);
MediaSource.MediaPeriodId mediaPeriodId2 =
new MediaSource.MediaPeriodId(
new Pair<>(0, 0),
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* windowSequenceNumber= */ 0L);
// Create and prepare the first period.
MediaPeriod mediaPeriod1 =
mediaSource.createPeriod(mediaPeriodId1, allocator, /* startPositionUs= */ 0L);
mediaPeriod1.prepare(callback, /* positionUs= */ 0L);
// Update the playback state to turn the content period into an ad period.
adPlaybackState =
adPlaybackState
.withNewAdGroup(/* adGroupIndex= */ 0, /* adGroupTimeUs= */ 0L)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true)
.withAdCount(/* adGroupIndex= */ 0, 1)
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, 10_000_000L)
.withAdDurationsUs(/* adGroupIndex= */ 0, 10_000_000L);
mediaSource.setAdPlaybackStates(
ImmutableMap.of(new Pair<>(0, 0), adPlaybackState), wrappedTimeline);
runMainLooperUntil(() -> !timelineReference.get().equals(firstTimeline));
// Create the second period that is tied to the same SharedMediaPeriod internally.
mediaSource.createPeriod(mediaPeriodId2, allocator, /* startPositionUs= */ 0L);
// Issue a onDownstreamFormatChanged event for mediaPeriodId1. The SharedPeriod selects in
// `getMediaPeriodForEvent` from the following `MediaPeriodImpl`s for
// MediaLoadData.mediaStartTimeMs=0 to 10_000_00.
// [
// isPrepared: true,
// startPositionMs: 0,
// endPositionMs: 0,
// adGroupIndex: -1,
// adIndexInAdGroup: -1,
// nextAdGroupIndex: 0,
// ],
// [
// isPrepared: false,
// startPositionMs: 0,
// endPositionMs: 10_000_000,
// adGroupIndex: 0,
// adIndexInAdGroup: 0,
// nextAdGroupIndex: -1,
// ]
MediaLoadData mediaLoadData =
new MediaLoadData(
/* dataType= */ DATA_TYPE_MEDIA,
C.TRACK_TYPE_VIDEO,
new Format.Builder().build(),
C.SELECTION_REASON_INITIAL,
/* trackSelectionData= */ null,
/* mediaStartTimeMs= */ 123L,
/* mediaEndTimeMs= */ 10_000_000L);
mediaSource.onDownstreamFormatChanged(/* windowIndex= */ 0, mediaPeriodId1, mediaLoadData);
runMainLooperUntil(
() -> mediaPeriodId1.equals(mediaPeriodIdReference.get()),
/* timeoutMs= */ 500L,
Clock.DEFAULT);
assertThat(mediaPeriodIdReference.get()).isEqualTo(mediaPeriodId1);
}
@Test
public void timeline_liveSinglePeriodWithUnsetPeriodDuration_containsAdsDefinedInAdPlaybackState() public void timeline_liveSinglePeriodWithUnsetPeriodDuration_containsAdsDefinedInAdPlaybackState()
throws Exception { throws Exception {
Timeline wrappedTimeline = Timeline wrappedTimeline =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment