Merge pull request #7517 from google/dev-v2-r2.11.6

r2.11.6
This commit is contained in:
Oliver Woodman 2020-06-24 00:39:54 +01:00 committed by GitHub
commit 1a5b304b1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1079 additions and 460 deletions

View file

@ -1,5 +1,27 @@
# Release notes #
### 2.11.6 (2020-06-24) ###
* UI: Prevent `PlayerView` from temporarily hiding the video surface when
seeking to an unprepared period within the current window. For example when
seeking over an ad group, or to the next period in a multi-period DASH
stream ([#5507](https://github.com/google/ExoPlayer/issues/5507)).
* IMA extension:
* Add option to skip ads before the start position.
* Catch unexpected errors in `stopAd` to avoid a crash
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix a bug that caused playback to be stuck buffering on resuming from
the background after all ads had played to the end
([#7508](https://github.com/google/ExoPlayer/issues/7508)).
* Fix a bug where the number of ads in an ad group couldn't change
([#7477](https://github.com/google/ExoPlayer/issues/7477)).
* Work around unexpected `pauseAd`/`stopAd` for ads that have preloaded
on seeking to another position
([#7492](https://github.com/google/ExoPlayer/issues/7492)).
* Fix incorrect rounding of ad cue points.
* Fix handling of postrolls preloading
([#7518](https://github.com/google/ExoPlayer/issues/7518)).
### 2.11.5 (2020-06-05) ###
* Improve the smoothness of video playback immediately after starting, seeking
@ -16,10 +38,10 @@
([#7306](https://github.com/google/ExoPlayer/issues/7306)).
* Fix issue in `AudioTrackPositionTracker` that could cause negative positions
to be reported at the start of playback and immediately after seeking
([#7456](https://github.com/google/ExoPlayer/issues/7456).
([#7456](https://github.com/google/ExoPlayer/issues/7456)).
* Fix further cases where downloads would sometimes not resume after their
network requirements are met
([#7453](https://github.com/google/ExoPlayer/issues/7453).
([#7453](https://github.com/google/ExoPlayer/issues/7453)).
* DASH:
* Merge trick play adaptation sets (i.e., adaptation sets marked with
`http://dashif.org/guidelines/trickmode`) into the same `TrackGroup` as
@ -99,10 +121,10 @@
`DefaultAudioSink` constructor
([#7134](https://github.com/google/ExoPlayer/issues/7134)).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671).
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671)).
* Fix case where another app spuriously holding transient audio focus could
prevent ExoPlayer from acquiring audio focus for an indefinite period of
time ([#7182](https://github.com/google/ExoPlayer/issues/7182).
time ([#7182](https://github.com/google/ExoPlayer/issues/7182)).
* Fix case where the player volume could be permanently ducked if audio focus
was released whilst ducking.
* Fix playback of WAV files with trailing non-media bytes
@ -1022,7 +1044,7 @@
([#4492](https://github.com/google/ExoPlayer/issues/4492) and
[#4634](https://github.com/google/ExoPlayer/issues/4634)).
* Fix issue where removing looping media from a playlist throws an exception
([#4871](https://github.com/google/ExoPlayer/issues/4871).
([#4871](https://github.com/google/ExoPlayer/issues/4871)).
* Fix issue where the preferred audio or text track would not be selected if
mapped onto a secondary renderer of the corresponding type
([#4711](http://github.com/google/ExoPlayer/issues/4711)).
@ -1439,7 +1461,7 @@
resources when the playback thread has quit by the time the loading task has
completed.
* ID3: Better handle malformed ID3 data
([#3792](https://github.com/google/ExoPlayer/issues/3792).
([#3792](https://github.com/google/ExoPlayer/issues/3792)).
* Support 14-bit mode and little endianness in DTS PES packets
([#3340](https://github.com/google/ExoPlayer/issues/3340)).
* Demo app: Add ability to download not DRM protected content.

View file

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.11.5'
releaseVersionCode = 2011005
releaseVersion = '2.11.6'
releaseVersionCode = 2011006
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved

View file

@ -476,6 +476,11 @@
"name": "VMAP full, empty, full midrolls",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/empty-midroll-2"
},
{
"name": "VMAP midroll at 1765 s",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/frame-counter-one-hour.mp4",
"ad_tag_uri": "https://vastsynthesizer.appspot.com/midroll-large"
}
]
},

View file

@ -0,0 +1,56 @@
/*
* Copyright (C) 2020 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.ext.ima;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import java.util.Arrays;
import java.util.List;
/**
* Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data.
*/
/* package */ final class AdPlaybackStateFactory {
private AdPlaybackStateFactory() {}
/**
* Construct an {@link AdPlaybackState} from the provided {@code cuePoints}.
*
* @param cuePoints The cue points of the ads in seconds.
* @return The {@link AdPlaybackState}.
*/
public static AdPlaybackState fromCuePoints(List<Float> cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
return new AdPlaybackState(/* adGroupTimesUs...= */ 0);
}
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
if (cuePoint == -1.0) {
adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
} else {
adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint);
}
}
// Cue points may be out of order, so sort them.
Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return new AdPlaybackState(adGroupTimesUs);
}
}

View file

@ -17,10 +17,12 @@ package com.google.android.exoplayer2.ext.ima;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -42,6 +44,8 @@ import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo;
import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Player;
@ -56,6 +60,7 @@ import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline;
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.time.Duration;
@ -91,8 +96,7 @@ public final class ImaAdsLoaderTest {
private static final Uri TEST_URI = Uri.EMPTY;
private static final AdMediaInfo TEST_AD_MEDIA_INFO = new AdMediaInfo(TEST_URI.toString());
private static final long TEST_AD_DURATION_US = 5 * C.MICROS_PER_SECOND;
private static final long[][] ADS_DURATIONS_US = new long[][] {{TEST_AD_DURATION_US}};
private static final Float[] PREROLL_CUE_POINTS_SECONDS = new Float[] {0f};
private static final ImmutableList<Float> PREROLL_CUE_POINTS_SECONDS = ImmutableList.of(0f);
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@ -111,6 +115,9 @@ public final class ImaAdsLoaderTest {
private ViewGroup adViewGroup;
private View adOverlayView;
private AdsLoader.AdViewProvider adViewProvider;
private AdEvent.AdEventListener adEventListener;
private ContentProgressProvider contentProgressProvider;
private VideoAdPlayer videoAdPlayer;
private TestAdsLoaderListener adsLoaderListener;
private FakePlayer fakeExoPlayer;
private ImaAdsLoader imaAdsLoader;
@ -144,14 +151,14 @@ public final class ImaAdsLoaderTest {
@Test
public void builder_overridesPlayerType() {
when(mockImaSdkSettings.getPlayerType()).thenReturn("test player type");
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
verify(mockImaSdkSettings).setPlayerType("google/exo.ext.ima");
}
@Test
public void start_setsAdUiViewGroup() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdDisplayContainer, atLeastOnce()).setAdContainer(adViewGroup);
@ -161,7 +168,7 @@ public final class ImaAdsLoaderTest {
@Test
public void start_withPlaceholderContent_initializedAdsLoader() {
Timeline placeholderTimeline = new DummyTimeline(/* tag= */ null);
setupPlayback(placeholderTimeline, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(placeholderTimeline, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
// We'll only create the rendering settings when initializing the ads loader.
@ -170,26 +177,27 @@ public final class ImaAdsLoaderTest {
@Test
public void start_updatesAdPlaybackState() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ 0)
.withAdDurationsUs(ADS_DURATIONS_US)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void startAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
}
@Test
public void startAndCallbacksAfterRelease() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Request ads in order to get a reference to the ad event listener.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.release();
imaAdsLoader.start(adsLoaderListener, adViewProvider);
fakeExoPlayer.setPlayingContentPosition(/* position= */ 0);
@ -200,47 +208,47 @@ public final class ImaAdsLoaderTest {
// when using Robolectric and accessing VideoProgressUpdate.VIDEO_TIME_NOT_READY, due to the IMA
// SDK being proguarded.
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.pauseAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO);
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError(
/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
}
@Test
public void playback_withPrerollAd_marksAdAsPlayed() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
// Load the preroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
imaAdsLoader.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.LOADED, mockPrerollSingleAd));
videoAdPlayer.loadAd(TEST_AD_MEDIA_INFO, mockAdPodInfo);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_PAUSE_REQUESTED, mockPrerollSingleAd));
// Play the preroll ad.
imaAdsLoader.playAd(TEST_AD_MEDIA_INFO);
videoAdPlayer.playAd(TEST_AD_MEDIA_INFO);
fakeExoPlayer.setPlayingAdPosition(
/* adGroupIndex= */ 0,
/* adIndexInAdGroup= */ 0,
/* position= */ 0,
/* contentPosition= */ 0);
fakeExoPlayer.setState(Player.STATE_READY, true);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.STARTED, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.FIRST_QUARTILE, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.MIDPOINT, mockPrerollSingleAd));
adEventListener.onAdEvent(getAdEvent(AdEventType.THIRD_QUARTILE, mockPrerollSingleAd));
// Play the content.
fakeExoPlayer.setPlayingContentPosition(0);
imaAdsLoader.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
// Verify that the preroll ad has been marked as played.
assertThat(adsLoaderListener.adPlaybackState)
@ -249,24 +257,24 @@ public final class ImaAdsLoaderTest {
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, /* uri= */ TEST_URI)
.withAdDurationsUs(ADS_DURATIONS_US)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withAdResumePositionUs(/* adResumePositionUs= */ 0));
}
@Test
public void playback_withPostrollFetchError_marksAdAsInErrorState() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, new Float[] {-1f});
setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f));
// Simulate loading an empty postroll ad.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.onAdEvent(mockPostrollFetchErrorAdEvent);
adEventListener.onAdEvent(mockPostrollFetchErrorAdEvent);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@ -275,10 +283,9 @@ public final class ImaAdsLoaderTest {
public void playback_withAdNotPreloadingBeforeTimeout_hasNoError() {
// Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback(
CONTENT_TIMELINE,
ADS_DURATIONS_US,
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
long adGroupTimeUs = adGroupPositionInWindowUs;
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -286,23 +293,21 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance before the timeout and simulating polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(1));
imaAdsLoader.getContentProgress();
contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US));
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void playback_withAdNotPreloadingAfterTimeout_hasErrorAdGroup() {
// Simulate an ad at 2 seconds.
long adGroupPositionInWindowUs = 2 * C.MICROS_PER_SECOND;
setupPlayback(
CONTENT_TIMELINE,
ADS_DURATIONS_US,
new Float[] {(float) adGroupPositionInWindowUs / C.MICROS_PER_SECOND});
long adGroupTimeUs = adGroupPositionInWindowUs;
ImmutableList<Float> cuePoints = ImmutableList.of((float) adGroupTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
// Advance playback to just before the midroll and simulate buffering.
imaAdsLoader.start(adsLoaderListener, adViewProvider);
@ -310,20 +315,296 @@ public final class ImaAdsLoaderTest {
fakeExoPlayer.setState(Player.STATE_BUFFERING, /* playWhenReady= */ true);
// Advance past the timeout and simulate polling content progress.
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
imaAdsLoader.getContentProgress();
contentProgressProvider.getContentProgress();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
new AdPlaybackState(/* adGroupTimesUs...= */ adGroupPositionInWindowUs)
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdDurationsUs(ADS_DURATIONS_US)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}})
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0));
}
@Test
public void resumePlaybackBeforeMidroll_playsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtMidroll_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackAfterMidroll_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackBeforeSecondMidroll_playsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble());
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtSecondMidroll_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(CONTENT_TIMELINE, cuePoints);
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackBeforeMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtMidroll_withoutPlayAdBeforeStartPosition_skipsPreroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = midrollPeriodTimeUs / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void resumePlaybackAfterMidroll_withoutPlayAdBeforeStartPosition_skipsMidroll() {
long midrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long midrollPeriodTimeUs = midrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(0f, (float) midrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(midrollWindowTimeUs) + 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
verify(mockAdsManager).destroy();
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withSkippedAdGroup(/* adGroupIndex= */ 1));
}
@Test
public void
resumePlaybackBeforeSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs) - 1_000);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withSkippedAdGroup(/* adGroupIndex= */ 0)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US));
}
@Test
public void resumePlaybackAtSecondMidroll_withoutPlayAdBeforeStartPosition_skipsFirstMidroll() {
long firstMidrollWindowTimeUs = 2 * C.MICROS_PER_SECOND;
long firstMidrollPeriodTimeUs = firstMidrollWindowTimeUs;
long secondMidrollWindowTimeUs = 4 * C.MICROS_PER_SECOND;
long secondMidrollPeriodTimeUs = secondMidrollWindowTimeUs;
ImmutableList<Float> cuePoints =
ImmutableList.of(
(float) firstMidrollPeriodTimeUs / C.MICROS_PER_SECOND,
(float) secondMidrollPeriodTimeUs / C.MICROS_PER_SECOND);
setupPlayback(
CONTENT_TIMELINE,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setPlayAdBeforeStartPosition(false)
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI));
fakeExoPlayer.setPlayingContentPosition(C.usToMs(secondMidrollWindowTimeUs));
imaAdsLoader.start(adsLoaderListener, adViewProvider);
ArgumentCaptor<Double> playAdsAfterTimeCaptor = ArgumentCaptor.forClass(Double.class);
verify(mockAdsRenderingSettings).setPlayAdsAfterTime(playAdsAfterTimeCaptor.capture());
double expectedPlayAdsAfterTimeUs = (firstMidrollPeriodTimeUs + secondMidrollPeriodTimeUs) / 2d;
assertThat(playAdsAfterTimeCaptor.getValue())
.isWithin(0.1d)
.of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND);
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withSkippedAdGroup(/* adGroupIndex= */ 0));
}
@Test
public void stop_unregistersAllVideoControlOverlays() {
setupPlayback(CONTENT_TIMELINE, ADS_DURATIONS_US, PREROLL_CUE_POINTS_SECONDS);
setupPlayback(CONTENT_TIMELINE, PREROLL_CUE_POINTS_SECONDS);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
imaAdsLoader.requestAds(adViewGroup);
imaAdsLoader.stop();
@ -333,15 +614,71 @@ public final class ImaAdsLoaderTest {
inOrder.verify(mockAdDisplayContainer).unregisterAllVideoControlsOverlays();
}
private void setupPlayback(Timeline contentTimeline, long[][] adDurationsUs, Float[] cuePoints) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline, adDurationsUs);
when(mockAdsManager.getAdCuePoints()).thenReturn(Arrays.asList(cuePoints));
imaAdsLoader =
@Test
public void loadAd_withLargeAdCuePoint_updatesAdPlaybackStateWithLoadedAd() {
float midrollTimeSecs = 1_765f;
ImmutableList<Float> cuePoints = ImmutableList.of(midrollTimeSecs);
setupPlayback(CONTENT_TIMELINE, cuePoints);
imaAdsLoader.start(adsLoaderListener, adViewProvider);
videoAdPlayer.loadAd(
TEST_AD_MEDIA_INFO,
new AdPodInfo() {
@Override
public int getTotalAds() {
return 1;
}
@Override
public int getAdPosition() {
return 1;
}
@Override
public boolean isBumper() {
return false;
}
@Override
public double getMaxDuration() {
return 0;
}
@Override
public int getPodIndex() {
return 0;
}
@Override
public double getTimeOffset() {
return midrollTimeSecs;
}
});
assertThat(adsLoaderListener.adPlaybackState)
.isEqualTo(
AdPlaybackStateFactory.fromCuePoints(cuePoints)
.withContentDurationUs(CONTENT_PERIOD_DURATION_US)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}));
}
private void setupPlayback(Timeline contentTimeline, List<Float> cuePoints) {
setupPlayback(
contentTimeline,
cuePoints,
new ImaAdsLoader.Builder(ApplicationProvider.getApplicationContext())
.setImaFactory(mockImaFactory)
.setImaSdkSettings(mockImaSdkSettings)
.buildForAdTag(TEST_URI);
.buildForAdTag(TEST_URI));
}
private void setupPlayback(
Timeline contentTimeline, List<Float> cuePoints, ImaAdsLoader imaAdsLoader) {
fakeExoPlayer = new FakePlayer();
adsLoaderListener = new TestAdsLoaderListener(fakeExoPlayer, contentTimeline);
when(mockAdsManager.getAdCuePoints()).thenReturn(cuePoints);
this.imaAdsLoader = imaAdsLoader;
imaAdsLoader.setPlayer(fakeExoPlayer);
}
@ -349,9 +686,11 @@ public final class ImaAdsLoaderTest {
ArgumentCaptor<Object> userRequestContextCaptor = ArgumentCaptor.forClass(Object.class);
doNothing().when(mockAdsRequest).setUserRequestContext(userRequestContextCaptor.capture());
when(mockAdsRequest.getUserRequestContext())
.thenAnswer((Answer<Object>) invocation -> userRequestContextCaptor.getValue());
.thenAnswer(invocation -> userRequestContextCaptor.getValue());
List<com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener> adsLoadedListeners =
new ArrayList<>();
// Deliberately don't handle removeAdsLoadedListener to allow testing behavior if the IMA SDK
// invokes callbacks after release.
doAnswer(
invocation -> {
adsLoadedListeners.add(invocation.getArgument(0));
@ -359,13 +698,6 @@ public final class ImaAdsLoaderTest {
})
.when(mockAdsLoader)
.addAdsLoadedListener(any());
doAnswer(
invocation -> {
adsLoadedListeners.remove(invocation.getArgument(0));
return null;
})
.when(mockAdsLoader)
.removeAdsLoadedListener(any());
when(mockAdsManagerLoadedEvent.getAdsManager()).thenReturn(mockAdsManager);
when(mockAdsManagerLoadedEvent.getUserRequestContext())
.thenAnswer(invocation -> mockAdsRequest.getUserRequestContext());
@ -381,6 +713,30 @@ public final class ImaAdsLoaderTest {
.when(mockAdsLoader)
.requestAds(mockAdsRequest);
doAnswer(
invocation -> {
adEventListener = invocation.getArgument(0);
return null;
})
.when(mockAdsManager)
.addAdEventListener(any());
doAnswer(
invocation -> {
contentProgressProvider = invocation.getArgument(0);
return null;
})
.when(mockAdsRequest)
.setContentProgressProvider(any());
doAnswer(
invocation -> {
videoAdPlayer = invocation.getArgument(0);
return null;
})
.when(mockAdDisplayContainer)
.setPlayer(any());
when(mockImaFactory.createAdDisplayContainer()).thenReturn(mockAdDisplayContainer);
when(mockImaFactory.createAdsRenderingSettings()).thenReturn(mockAdsRenderingSettings);
when(mockImaFactory.createAdsRequest()).thenReturn(mockAdsRequest);
@ -422,19 +778,21 @@ public final class ImaAdsLoaderTest {
private final FakePlayer fakeExoPlayer;
private final Timeline contentTimeline;
private final long[][] adDurationsUs;
public AdPlaybackState adPlaybackState;
public TestAdsLoaderListener(
FakePlayer fakeExoPlayer, Timeline contentTimeline, long[][] adDurationsUs) {
public TestAdsLoaderListener(FakePlayer fakeExoPlayer, Timeline contentTimeline) {
this.fakeExoPlayer = fakeExoPlayer;
this.contentTimeline = contentTimeline;
this.adDurationsUs = adDurationsUs;
}
@Override
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
long[][] adDurationsUs = new long[adPlaybackState.adGroupCount][];
for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) {
adDurationsUs[adGroupIndex] = new long[adPlaybackState.adGroups[adGroupIndex].uris.length];
Arrays.fill(adDurationsUs[adGroupIndex], TEST_AD_DURATION_US);
}
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
this.adPlaybackState = adPlaybackState;
fakeExoPlayer.updateTimeline(

View file

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.11.5";
public static final String VERSION = "2.11.6";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.5";
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.6";
/**
* The version of the library expressed as an integer, for example 1002003.
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2011005;
public static final int VERSION_INT = 2011006;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -124,13 +124,9 @@ public final class AdPlaybackState {
return result;
}
/**
* Returns a new instance with the ad count set to {@code count}. This method may only be called
* if this instance's ad count has not yet been specified.
*/
/** Returns a new instance with the ad count set to {@code count}. */
@CheckResult
public AdGroup withAdCount(int count) {
Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
@NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
@ -139,17 +135,11 @@ public final class AdPlaybackState {
/**
* Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
* marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
* #AD_STATE_UNAVAILABLE}, which is the default state.
*
* <p>This instance's ad count may be unknown, in which case {@code index} must be less than the
* ad count specified later. Otherwise, {@code index} must be less than the current ad count.
* marked as {@link #AD_STATE_AVAILABLE}.
*/
@CheckResult
public AdGroup withAdUri(Uri uri, int index) {
Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
long[] durationsUs =
this.durationsUs.length == states.length
? this.durationsUs
@ -280,7 +270,9 @@ public final class AdPlaybackState {
public final AdGroup[] adGroups;
/** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
public final long adResumePositionUs;
/** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
/**
* The duration of the content period in microseconds, if known. {@link C#TIME_UNSET} otherwise.
*/
public final long contentDurationUs;
/**
@ -489,6 +481,54 @@ public final class AdPlaybackState {
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("AdPlaybackState(adResumePositionUs=");
sb.append(adResumePositionUs);
sb.append(", adGroups=[");
for (int i = 0; i < adGroups.length; i++) {
sb.append("adGroup(timeUs=");
sb.append(adGroupTimesUs[i]);
sb.append(", ads=[");
for (int j = 0; j < adGroups[i].states.length; j++) {
sb.append("ad(state=");
switch (adGroups[i].states[j]) {
case AD_STATE_UNAVAILABLE:
sb.append('_');
break;
case AD_STATE_ERROR:
sb.append('!');
break;
case AD_STATE_AVAILABLE:
sb.append('R');
break;
case AD_STATE_PLAYED:
sb.append('P');
break;
case AD_STATE_SKIPPED:
sb.append('S');
break;
default:
sb.append('?');
break;
}
sb.append(", durationUs=");
sb.append(adGroups[i].durationsUs[j]);
sb.append(')');
if (j < adGroups[i].states.length - 1) {
sb.append(", ");
}
}
sb.append("])");
if (i < adGroups.length - 1) {
sb.append(", ");
}
}
sb.append("])");
return sb.toString();
}
private boolean isPositionBeforeAdGroup(
long positionUs, long periodDurationUs, int adGroupIndex) {
if (positionUs == C.TIME_END_OF_SOURCE) {

View file

@ -44,23 +44,16 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline {
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
long durationUs =
period.durationUs == C.TIME_UNSET ? adPlaybackState.contentDurationUs : period.durationUs;
period.set(
period.id,
period.uid,
period.windowIndex,
period.durationUs,
durationUs,
period.getPositionInWindowUs(),
adPlaybackState);
return period;
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
window = super.getWindow(windowIndex, window, defaultPositionProjectionUs);
if (window.durationUs == C.TIME_UNSET) {
window.durationUs = adPlaybackState.contentDurationUs;
}
return window;
}
}

View file

@ -155,12 +155,22 @@ public final class MimeTypes {
if (mimeType == null) {
return false;
}
// TODO: Consider adding additional audio MIME types here.
// TODO: Add additional audio MIME types. Also consider evaluating based on Format rather than
// just MIME type, since in some cases the property is true for a subset of the profiles
// belonging to a single MIME type. If we do this, we should move the method to a different
// class. See [Internal ref: http://go/exo-audio-format-random-access].
switch (mimeType) {
case AUDIO_AAC:
case AUDIO_MPEG:
case AUDIO_MPEG_L1:
case AUDIO_MPEG_L2:
case AUDIO_RAW:
case AUDIO_ALAW:
case AUDIO_MLAW:
case AUDIO_OPUS:
case AUDIO_FLAC:
case AUDIO_AC3:
case AUDIO_E_AC3:
case AUDIO_E_AC3_JOC:
return true;
default:
return false;

View file

@ -48,6 +48,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
@ -1506,6 +1508,13 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
SingleTapListener,
PlayerControlView.VisibilityListener {
private final Period period;
private @Nullable Object lastPeriodUidWithTracks;
public ComponentListener() {
period = new Period();
}
// TextOutput implementation
@Override
@ -1554,6 +1563,29 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
// Suppress the update if transitioning to an unprepared period within the same window. This
// is necessary to avoid closing the shutter when such a transition occurs. See:
// https://github.com/google/ExoPlayer/issues/5507.
Player player = Assertions.checkNotNull(PlayerView.this.player);
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
lastPeriodUidWithTracks = null;
} else if (!player.getCurrentTrackGroups().isEmpty()) {
lastPeriodUidWithTracks =
timeline.getPeriod(player.getCurrentPeriodIndex(), period, /* setIds= */ true).uid;
} else if (lastPeriodUidWithTracks != null) {
int lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(lastPeriodUidWithTracks);
if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {
int lastWindowIndexWithTracks =
timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex;
if (player.getCurrentWindowIndex() == lastWindowIndexWithTracks) {
// We're in the same window. Suppress the update.
return;
}
}
lastPeriodUidWithTracks = null;
}
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}