Add ServerSideInsertedAdsMediaSource.
This media source wraps another source and publishes a Timeline with ads. The created MediaPeriods for ad and content are mapped back to the original stream to allow seamless playback. PiperOrigin-RevId: 374836091
This commit is contained in:
parent
1a6f36d4a4
commit
d08303d05d
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,388 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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.ads;
|
||||||
|
|
||||||
|
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.runUntilPendingCommandsAreFullyHandled;
|
||||||
|
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
|
||||||
|
import static com.google.android.exoplayer2.source.ads.ServerSideInsertedAdsUtil.addAdGroupToAdPlaybackState;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.view.Surface;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.analytics.AnalyticsListener;
|
||||||
|
import com.google.android.exoplayer2.robolectric.PlaybackOutput;
|
||||||
|
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
|
||||||
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
|
||||||
|
import com.google.android.exoplayer2.testutil.DumpFileAsserts;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeClock;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
/** Unit test for {@link ServerSideInsertedAdsMediaSource}. */
|
||||||
|
// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved.
|
||||||
|
@Config(sdk = 29)
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class ServerSideInsertedAdMediaSourceTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShadowMediaCodecConfig mediaCodecConfig =
|
||||||
|
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
|
||||||
|
|
||||||
|
private static final String TEST_ASSET = "asset:///media/mp4/sample.mp4";
|
||||||
|
private static final String TEST_ASSET_DUMP = "playbackdumps/mp4/sample.mp4.dump";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timeline_containsAdsDefinedInAdPlaybackState() throws Exception {
|
||||||
|
FakeTimeline wrappedTimeline =
|
||||||
|
new FakeTimeline(
|
||||||
|
new FakeTimeline.TimelineWindowDefinition(
|
||||||
|
/* periodCount= */ 1,
|
||||||
|
/* id= */ 0,
|
||||||
|
/* isSeekable= */ true,
|
||||||
|
/* isDynamic= */ true,
|
||||||
|
/* isLive= */ true,
|
||||||
|
/* isPlaceholder= */ false,
|
||||||
|
/* durationUs= */ 10_000_000,
|
||||||
|
/* defaultPositionUs= */ 3_000_000,
|
||||||
|
/* windowOffsetInFirstPeriodUs= */ 42_000_000L,
|
||||||
|
AdPlaybackState.NONE));
|
||||||
|
ServerSideInsertedAdsMediaSource mediaSource =
|
||||||
|
new ServerSideInsertedAdsMediaSource(new FakeMediaSource(wrappedTimeline));
|
||||||
|
// Test with one ad group before the window, and the window starting within the second ad group.
|
||||||
|
AdPlaybackState adPlaybackState =
|
||||||
|
new AdPlaybackState(
|
||||||
|
/* adsId= */ new Object(), /* adGroupTimesUs...= */
|
||||||
|
15_000_000,
|
||||||
|
41_500_000,
|
||||||
|
42_200_000)
|
||||||
|
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true)
|
||||||
|
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
|
||||||
|
.withIsServerSideInserted(/* adGroupIndex= */ 2, /* isServerSideInserted= */ true)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
|
||||||
|
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
|
||||||
|
.withAdDurationsUs(/* adGroupIndex= */ 0, /* adDurationsUs...= */ 500_000)
|
||||||
|
.withAdDurationsUs(/* adGroupIndex= */ 1, /* adDurationsUs...= */ 300_000, 100_000)
|
||||||
|
.withAdDurationsUs(/* adGroupIndex= */ 2, /* adDurationsUs...= */ 400_000)
|
||||||
|
.withContentResumeOffsetUs(/* adGroupIndex= */ 0, /* contentResumeOffsetUs= */ 100_000)
|
||||||
|
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 400_000)
|
||||||
|
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 200_000);
|
||||||
|
AtomicReference<Timeline> timelineReference = new AtomicReference<>();
|
||||||
|
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
mediaSource.prepareSource(
|
||||||
|
(source, timeline) -> timelineReference.set(timeline), /* mediaTransferListener= */ null);
|
||||||
|
runMainLooperUntil(() -> timelineReference.get() != null);
|
||||||
|
|
||||||
|
Timeline timeline = timelineReference.get();
|
||||||
|
assertThat(timeline.getPeriodCount()).isEqualTo(1);
|
||||||
|
Timeline.Period period = timeline.getPeriod(/* periodIndex= */ 0, new Timeline.Period());
|
||||||
|
assertThat(period.getAdGroupCount()).isEqualTo(3);
|
||||||
|
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 0)).isEqualTo(1);
|
||||||
|
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 1)).isEqualTo(2);
|
||||||
|
assertThat(period.getAdCountInAdGroup(/* adGroupIndex= */ 2)).isEqualTo(1);
|
||||||
|
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 0)).isEqualTo(15_000_000);
|
||||||
|
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 1)).isEqualTo(41_500_000);
|
||||||
|
assertThat(period.getAdGroupTimeUs(/* adGroupIndex= */ 2)).isEqualTo(42_200_000);
|
||||||
|
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0))
|
||||||
|
.isEqualTo(500_000);
|
||||||
|
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0))
|
||||||
|
.isEqualTo(300_000);
|
||||||
|
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1))
|
||||||
|
.isEqualTo(100_000);
|
||||||
|
assertThat(period.getAdDurationUs(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0))
|
||||||
|
.isEqualTo(400_000);
|
||||||
|
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 0)).isEqualTo(100_000);
|
||||||
|
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 1)).isEqualTo(400_000);
|
||||||
|
assertThat(period.getContentResumeOffsetUs(/* adGroupIndex= */ 2)).isEqualTo(200_000);
|
||||||
|
// windowDurationUs + windowOffsetInFirstPeriodUs - sum(adDurations) + sum(contentResumeOffsets)
|
||||||
|
assertThat(period.getDurationUs()).isEqualTo(51_400_000);
|
||||||
|
// positionInWindowUs + sum(adDurationsBeforeWindow) - sum(contentResumeOffsetsBeforeWindow)
|
||||||
|
assertThat(period.getPositionInWindowUs()).isEqualTo(-41_600_000);
|
||||||
|
Timeline.Window window = timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window());
|
||||||
|
assertThat(window.positionInFirstPeriodUs).isEqualTo(41_600_000);
|
||||||
|
// windowDurationUs - sum(adDurationsInWindow) + sum(applicableContentResumeOffsetUs)
|
||||||
|
assertThat(window.durationUs).isEqualTo(9_800_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithPredefinedAds_playsSuccessfulWithoutRendererResets() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context);
|
||||||
|
SimpleExoPlayer player =
|
||||||
|
new SimpleExoPlayer.Builder(context, renderersFactory)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.build();
|
||||||
|
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||||
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
|
|
||||||
|
ServerSideInsertedAdsMediaSource mediaSource =
|
||||||
|
new ServerSideInsertedAdsMediaSource(
|
||||||
|
new DefaultMediaSourceFactory(context)
|
||||||
|
.createMediaSource(MediaItem.fromUri(TEST_ASSET)));
|
||||||
|
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object());
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 0,
|
||||||
|
/* toPositionUs= */ 200_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 400_000,
|
||||||
|
/* toPositionUs= */ 700_000,
|
||||||
|
/* contentResumeOffsetUs= */ 1_000_000);
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 900_000,
|
||||||
|
/* toPositionUs= */ 1_000_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
|
||||||
|
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||||
|
player.addAnalyticsListener(listener);
|
||||||
|
player.setMediaSource(mediaSource);
|
||||||
|
player.prepare();
|
||||||
|
player.play();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
// Assert all samples have been played.
|
||||||
|
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
|
||||||
|
// Assert playback has been reported with ads: [ad0][content][ad1][content][ad2][content]
|
||||||
|
// 6*2(audio+video) format changes, 5 discontinuities between parts.
|
||||||
|
verify(listener, times(5))
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
|
||||||
|
verify(listener, times(12)).onDownstreamFormatChanged(any(), any());
|
||||||
|
// Assert renderers played through without reset (=decoders have been enabled only once).
|
||||||
|
verify(listener).onVideoEnabled(any(), any());
|
||||||
|
verify(listener).onAudioEnabled(any(), any());
|
||||||
|
// Assert playback progression was smooth (=no unexpected delays that cause audio to underrun)
|
||||||
|
verify(listener, never()).onAudioUnderrun(any(), anyInt(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithNewlyInsertedAds_playsSuccessfulWithoutRendererResets() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context);
|
||||||
|
SimpleExoPlayer player =
|
||||||
|
new SimpleExoPlayer.Builder(context, renderersFactory)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.build();
|
||||||
|
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||||
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
|
|
||||||
|
ServerSideInsertedAdsMediaSource mediaSource =
|
||||||
|
new ServerSideInsertedAdsMediaSource(
|
||||||
|
new DefaultMediaSourceFactory(context)
|
||||||
|
.createMediaSource(MediaItem.fromUri(TEST_ASSET)));
|
||||||
|
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object());
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 900_000,
|
||||||
|
/* toPositionUs= */ 1_000_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
|
||||||
|
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||||
|
player.addAnalyticsListener(listener);
|
||||||
|
player.setMediaSource(mediaSource);
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
// Add ad at the current playback position during playback.
|
||||||
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 0,
|
||||||
|
/* toPositionUs= */ 500_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
// Assert all samples have been played.
|
||||||
|
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
|
||||||
|
// Assert playback has been reported with ads: [content][ad0][content][ad1][content]
|
||||||
|
// 5*2(audio+video) format changes, 4 discontinuities between parts.
|
||||||
|
verify(listener, times(4))
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
|
||||||
|
verify(listener, times(10)).onDownstreamFormatChanged(any(), any());
|
||||||
|
// Assert renderers played through without reset (=decoders have been enabled only once).
|
||||||
|
verify(listener).onVideoEnabled(any(), any());
|
||||||
|
verify(listener).onAudioEnabled(any(), any());
|
||||||
|
// Assert playback progression was smooth (=no unexpected delays that cause audio to underrun)
|
||||||
|
verify(listener, never()).onAudioUnderrun(any(), anyInt(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithAdditionalAdsInAdGroup_playsSuccessfulWithoutRendererResets()
|
||||||
|
throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
CapturingRenderersFactory renderersFactory = new CapturingRenderersFactory(context);
|
||||||
|
SimpleExoPlayer player =
|
||||||
|
new SimpleExoPlayer.Builder(context, renderersFactory)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.build();
|
||||||
|
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||||
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
|
|
||||||
|
ServerSideInsertedAdsMediaSource mediaSource =
|
||||||
|
new ServerSideInsertedAdsMediaSource(
|
||||||
|
new DefaultMediaSourceFactory(context)
|
||||||
|
.createMediaSource(MediaItem.fromUri(TEST_ASSET)));
|
||||||
|
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object());
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 0,
|
||||||
|
/* toPositionUs= */ 500_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
|
||||||
|
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||||
|
player.addAnalyticsListener(listener);
|
||||||
|
player.setMediaSource(mediaSource);
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
// Wait until playback is ready with first ad and then replace by 3 ads.
|
||||||
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
|
adPlaybackState =
|
||||||
|
adPlaybackState
|
||||||
|
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3)
|
||||||
|
.withAdDurationsUs(
|
||||||
|
/* adGroupIndex= */ 0, /* adDurationsUs...= */ 50_000, 250_000, 200_000);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
|
||||||
|
player.play();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
// Assert all samples have been played.
|
||||||
|
DumpFileAsserts.assertOutput(context, playbackOutput, TEST_ASSET_DUMP);
|
||||||
|
// Assert playback has been reported with ads: [ad0][ad1][ad2][content]
|
||||||
|
// 4*2(audio+video) format changes, 3 discontinuities between parts.
|
||||||
|
verify(listener, times(3))
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
|
||||||
|
verify(listener, times(8)).onDownstreamFormatChanged(any(), any());
|
||||||
|
// Assert renderers played through without reset (=decoders have been enabled only once).
|
||||||
|
verify(listener).onVideoEnabled(any(), any());
|
||||||
|
verify(listener).onAudioEnabled(any(), any());
|
||||||
|
// Assert playback progression was smooth (=no unexpected delays that cause audio to underrun)
|
||||||
|
verify(listener, never()).onAudioUnderrun(any(), anyInt(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void playbackWithSeek_isHandledCorrectly() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
SimpleExoPlayer player =
|
||||||
|
new SimpleExoPlayer.Builder(context)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.build();
|
||||||
|
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
|
||||||
|
|
||||||
|
ServerSideInsertedAdsMediaSource mediaSource =
|
||||||
|
new ServerSideInsertedAdsMediaSource(
|
||||||
|
new DefaultMediaSourceFactory(context)
|
||||||
|
.createMediaSource(MediaItem.fromUri(TEST_ASSET)));
|
||||||
|
AdPlaybackState adPlaybackState = new AdPlaybackState(/* adsId= */ new Object());
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 0,
|
||||||
|
/* toPositionUs= */ 100_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 600_000,
|
||||||
|
/* toPositionUs= */ 700_000,
|
||||||
|
/* contentResumeOffsetUs= */ 1_000_000);
|
||||||
|
adPlaybackState =
|
||||||
|
addAdGroupToAdPlaybackState(
|
||||||
|
adPlaybackState,
|
||||||
|
/* fromPositionUs= */ 900_000,
|
||||||
|
/* toPositionUs= */ 1_000_000,
|
||||||
|
/* contentResumeOffsetUs= */ 0);
|
||||||
|
mediaSource.setAdPlaybackState(adPlaybackState);
|
||||||
|
|
||||||
|
AnalyticsListener listener = mock(AnalyticsListener.class);
|
||||||
|
player.addAnalyticsListener(listener);
|
||||||
|
player.setMediaSource(mediaSource);
|
||||||
|
player.prepare();
|
||||||
|
// Play to the first content part, then seek past the midroll.
|
||||||
|
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100);
|
||||||
|
player.seekTo(/* positionMs= */ 1_600);
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
long positionAfterSeekMs = player.getCurrentPosition();
|
||||||
|
long contentPositionAfterSeekMs = player.getContentPosition();
|
||||||
|
player.play();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
|
player.release();
|
||||||
|
|
||||||
|
// Assert playback has been reported with ads: [ad0][content] seek [ad1][content][ad2][content]
|
||||||
|
// 6*2(audio+video) format changes, 4 auto-transitions between parts, 1 seek with adjustment.
|
||||||
|
verify(listener, times(4))
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
|
||||||
|
verify(listener, times(1))
|
||||||
|
.onPositionDiscontinuity(any(), any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK));
|
||||||
|
verify(listener, times(1))
|
||||||
|
.onPositionDiscontinuity(
|
||||||
|
any(), any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT));
|
||||||
|
verify(listener, times(12)).onDownstreamFormatChanged(any(), any());
|
||||||
|
assertThat(contentPositionAfterSeekMs).isEqualTo(1_600);
|
||||||
|
assertThat(positionAfterSeekMs).isEqualTo(0); // Beginning of second ad.
|
||||||
|
// Assert renderers played through without reset, except for the seek.
|
||||||
|
verify(listener, times(2)).onVideoEnabled(any(), any());
|
||||||
|
verify(listener, times(2)).onAudioEnabled(any(), any());
|
||||||
|
// Assert playback progression was smooth (=no unexpected delays that cause audio to underrun)
|
||||||
|
verify(listener, never()).onAudioUnderrun(any(), anyInt(), anyLong(), anyLong());
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user