diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java index faa4dc7680..6baf54e974 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -28,17 +28,22 @@ import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationAndPropagate import static androidx.media3.exoplayer.ima.ImaUtil.updateAdDurationInAdGroup; import static androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState; import static java.lang.Math.min; +import static java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.util.Pair; import android.view.ViewGroup; +import androidx.annotation.IntDef; import androidx.annotation.MainThread; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.AdOverlayInfo; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdViewProvider; +import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Metadata; @@ -88,6 +93,10 @@ import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -285,13 +294,67 @@ public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSou } /** The state of the {@link AdsLoader}. */ - public static class State { + public static class State implements Bundleable { private final ImmutableMap adPlaybackStates; - private State(ImmutableMap adPlaybackStates) { + @VisibleForTesting + /* package */ State(ImmutableMap adPlaybackStates) { this.adPlaybackStates = adPlaybackStates; } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof State)) { + return false; + } + State state = (State) o; + return adPlaybackStates.equals(state.adPlaybackStates); + } + + @Override + public int hashCode() { + return adPlaybackStates.hashCode(); + } + + // Bundleable implementation. + + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({FIELD_AD_PLAYBACK_STATES}) + private @interface FieldNumber {} + + private static final int FIELD_AD_PLAYBACK_STATES = 1; + + @Override + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putSerializable(keyForField(FIELD_AD_PLAYBACK_STATES), adPlaybackStates); + return bundle; + } + + /** Object that can restore {@link AdsLoader.State} from a {@link Bundle}. */ + public static final Bundleable.Creator CREATOR = State::fromBundle; + + @SuppressWarnings("unchecked") + private static State fromBundle(Bundle bundle) { + @Nullable + Map adPlaybackStateMap = + (Map) + bundle.getSerializable(keyForField(FIELD_AD_PLAYBACK_STATES)); + return new State( + adPlaybackStateMap != null + ? ImmutableMap.copyOf(adPlaybackStateMap) + : ImmutableMap.of()); + } + + private static String keyForField(@FieldNumber int field) { + return Integer.toString(field, Character.MAX_RADIX); + } } private final ImaUtil.ServerSideAdInsertionConfiguration configuration; diff --git a/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSourceTest.java b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSourceTest.java new file mode 100644 index 0000000000..981ac9adb3 --- /dev/null +++ b/libraries/exoplayer_ima/src/test/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSourceTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 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 androidx.media3.exoplayer.ima; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.AdPlaybackState; +import androidx.media3.common.C; +import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource.AdsLoader.State; +import androidx.media3.exoplayer.source.ads.ServerSideAdInsertionUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link ImaServerSideAdInsertionMediaSource}. */ +@RunWith(AndroidJUnit4.class) +public class ImaServerSideAdInsertionMediaSourceTest { + + @Test + public void adsLoaderStateToBundle_marshallAndUnmarshalling_resultIsEqual() { + AdPlaybackState firstAdPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + new AdPlaybackState("adsId1"), + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 10, + /* adDurationsUs...= */ 5_000_000, + 10_000_000, + 20_000_000); + AdPlaybackState secondAdPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + new AdPlaybackState("adsId2"), + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 10, + /* adDurationsUs...= */ 10_000_000) + .withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + AdPlaybackState thirdAdPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + new AdPlaybackState("adsId3"), + /* fromPositionUs= */ C.TIME_END_OF_SOURCE, + /* contentResumeOffsetUs= */ 10, + /* adDurationsUs...= */ 10_000_000); + thirdAdPlaybackState = + ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState( + thirdAdPlaybackState, + /* fromPositionUs= */ 0, + /* contentResumeOffsetUs= */ 10, + /* adDurationsUs...= */ 10_000_000) + .withRemovedAdGroupCount(1); + State state = + new State( + ImmutableMap.builder() + .put("adsId1", firstAdPlaybackState) + .put("adsId2", secondAdPlaybackState) + .put("adsId3", thirdAdPlaybackState) + .buildOrThrow()); + + assertThat(State.CREATOR.fromBundle(state.toBundle())).isEqualTo(state); + } +}