From 4fd6d670c6a8a2d9552f5c7d49f7181901fbccb7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 19 Nov 2021 14:02:12 +0000 Subject: [PATCH] Add method to reset ad group from final states to be playable again. The player will not play ads in final states (played, skipped, error) again. To allow ads loader customizations to play ads again, we can add a method that resets the state back to available or unavailable (depending on whether we have the URI for the ad). Issue: google/ExoPlayer#9615 PiperOrigin-RevId: 411042842 --- RELEASENOTES.md | 4 ++ .../source/ads/AdPlaybackState.java | 35 ++++++++++++ .../source/ads/AdPlaybackStateTest.java | 57 +++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index db92059e09..655ed9f7c4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,10 @@ When a `DrmSessionManager` is used by an app in a custom `MediaSource`, the `playbackLooper` needs to be passed to `DrmSessionManager.setPlayer` instead. +* IMA: + * Add a method to `AdPlaybackState` to allow resetting an ad group so that + it can be played again + ([#9615](https://github.com/google/ExoPlayer/issues/9615)). ### 2.16.1 (2021-11-18) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index e296d013d5..46a1e6526b 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -300,6 +300,28 @@ public final class AdPlaybackState implements Bundleable { timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted); } + /** + * Returns an instance with all ads in final states (played, skipped, error) reset to either + * available or unavailable, which allows to play them again. + */ + @CheckResult + public AdGroup withAllAdsReset() { + if (count == C.LENGTH_UNSET) { + return this; + } + int count = this.states.length; + @AdState int[] states = Arrays.copyOf(this.states, count); + for (int i = 0; i < count; i++) { + if (states[i] == AD_STATE_PLAYED + || states[i] == AD_STATE_SKIPPED + || states[i] == AD_STATE_ERROR) { + states[i] = uris[i] == null ? AD_STATE_UNAVAILABLE : AD_STATE_AVAILABLE; + } + } + return new AdGroup( + timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted); + } + @CheckResult private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) { int oldStateCount = states.length; @@ -783,6 +805,19 @@ public final class AdPlaybackState implements Bundleable { adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); } + /** + * Returns an instance with all ads in the specified ad group reset from final states (played, + * skipped, error) to either available or unavailable, which allows to play them again. + */ + @CheckResult + public AdPlaybackState withResetAdGroup(@IntRange(from = 0) int adGroupIndex) { + int adjustedIndex = adGroupIndex - removedAdGroupCount; + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); + adGroups[adjustedIndex] = adGroups[adjustedIndex].withAllAdsReset(); + return new AdPlaybackState( + adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index 90e28934b0..1d9176b618 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -16,7 +16,10 @@ package com.google.android.exoplayer2.source.ads; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_AVAILABLE; +import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_ERROR; import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_PLAYED; +import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_SKIPPED; +import static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @@ -254,6 +257,60 @@ public class AdPlaybackStateTest { assertThat(state.getAdGroup(1).count).isEqualTo(0); } + @Test + public void withResetAdGroup_beforeSetAdCount_doesNothing() { + AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US); + + state = state.withResetAdGroup(/* adGroupIndex= */ 1); + + assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET); + } + + @Test + public void withResetAdGroup_resetsAdsInFinalStates() { + AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US); + state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 5); + state = + state.withAdDurationsUs( + /* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY); + state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY); + state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2); + state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3); + state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4); + // Verify setup. + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states) + .asList() + .containsExactly( + AD_STATE_UNAVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_PLAYED, + AD_STATE_SKIPPED, + AD_STATE_ERROR) + .inOrder(); + + state = state.withResetAdGroup(/* adGroupIndex= */ 1); + + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).states) + .asList() + .containsExactly( + AD_STATE_UNAVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE, + AD_STATE_AVAILABLE) + .inOrder(); + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris) + .asList() + .containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY) + .inOrder(); + assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs) + .asList() + .containsExactly(1_000L, 2_000L, 3_000L, 4_000L, 5_000L); + } + @Test public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() { AdPlaybackState originalState =