public final class

ImaAdsLoader

extends java.lang.Object

implements AdsLoader

 java.lang.Object

↳androidx.media3.exoplayer.ima.ImaAdsLoader

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer-ima', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer-ima
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer-ima:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Overview

AdsLoader using the IMA SDK. All methods must be called on the main thread.

The player instance that will play the loaded ads must be set before playback using ImaAdsLoader.setPlayer(Player). If the ads loader is no longer required, it must be released by calling ImaAdsLoader.release().

See IMA's Support and compatibility page for information on compatible ad tag formats. Pass the ad tag URI when setting media item playback properties (if using the media item API) or as a DataSpec when constructing the AdsMediaSource (if using media sources directly). For the latter case, please note that this implementation delegates loading of the data spec to the IMA SDK, so range and headers specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data scheme data specs, for example, by constructing the data spec using a URI generated via Util.getDataUriForString(String, String).

The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This means that any overlay views that obstruct the ad overlay but are essential for playback need to be registered via the AdViewProvider passed to the AdsMediaSource. See the IMA SDK Open Measurement documentation for more information.

Summary

Methods
public voidfocusSkipButton()

Moves UI focus to the skip button (or other interactive elements), if currently shown.

public AdDisplayContainergetAdDisplayContainer()

Returns the used by this loader, or null if ads have not been requested yet.

public com.google.ads.interactivemedia.v3.api.AdsLoadergetAdsLoader()

Returns the underlying com.google.ads.interactivemedia.v3.api.AdsLoader wrapped by this instance, or null if ads have not been requested yet.

public voidhandlePrepareComplete(AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup)

public voidhandlePrepareError(AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup, java.io.IOException exception)

public voidrelease()

public voidrequestAds(DataSpec adTagDataSpec, java.lang.Object adsId, ViewGroup adViewGroup)

Requests ads, if they have not already been requested.

public voidsetPlayer(Player player)

public voidsetSupportedContentTypes(int[] contentTypes[])

public voidskipAd()

Skips the current ad.

public voidstart(AdsMediaSource adsMediaSource, DataSpec adTagDataSpec, java.lang.Object adsId, AdViewProvider adViewProvider, AdsLoader.EventListener eventListener)

public voidstop(AdsMediaSource adsMediaSource, AdsLoader.EventListener eventListener)

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader()

Returns the underlying com.google.ads.interactivemedia.v3.api.AdsLoader wrapped by this instance, or null if ads have not been requested yet.

public AdDisplayContainer getAdDisplayContainer()

Returns the used by this loader, or null if ads have not been requested yet.

Note: any video controls overlays registered via will be unregistered automatically when the media source detaches from this instance. It is therefore necessary to re-register views each time the ads loader is reused. Alternatively, provide overlay views via the AdViewProvider when creating the media source to benefit from automatic registration.

public void requestAds(DataSpec adTagDataSpec, java.lang.Object adsId, ViewGroup adViewGroup)

Requests ads, if they have not already been requested. Must be called on the main thread.

Ads will be requested automatically when the player is prepared if this method has not been called, so it is only necessary to call this method if you want to request ads before preparing the player.

Parameters:

adTagDataSpec: The data specification of the ad tag to load. See class javadoc for information about compatible ad tag formats.
adsId: A opaque identifier for the ad playback state across start/stop calls.
adViewGroup: A ViewGroup on top of the player that will show any ad UI, or null if playing audio-only ads.

public void skipAd()

Skips the current ad.

This method is intended for apps that play audio-only ads and so need to provide their own UI for users to skip skippable ads. Apps showing video ads should not call this method, as the IMA SDK provides the UI to skip ads in the ad view group passed via AdViewProvider.

public void focusSkipButton()

Moves UI focus to the skip button (or other interactive elements), if currently shown. See .

public void setPlayer(Player player)

public void setSupportedContentTypes(int[] contentTypes[])

public void start(AdsMediaSource adsMediaSource, DataSpec adTagDataSpec, java.lang.Object adsId, AdViewProvider adViewProvider, AdsLoader.EventListener eventListener)

public void stop(AdsMediaSource adsMediaSource, AdsLoader.EventListener eventListener)

public void release()

public void handlePrepareComplete(AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup)

public void handlePrepareError(AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup, java.io.IOException exception)

Source

/*
 * Copyright (C) 2017 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 androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.exoplayer.ima.ImaUtil.BITRATE_UNSET;
import static androidx.media3.exoplayer.ima.ImaUtil.TIMEOUT_UNSET;
import static androidx.media3.exoplayer.ima.ImaUtil.getImaLooper;

import android.content.Context;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AdViewProvider;
import androidx.media3.common.C;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.source.ads.AdsMediaSource;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
import com.google.ads.interactivemedia.v3.api.AdsManager;
import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
import com.google.ads.interactivemedia.v3.api.AdsRequest;
import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
import com.google.ads.interactivemedia.v3.api.FriendlyObstruction;
import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose;
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
import com.google.ads.interactivemedia.v3.api.UiElement;
import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

/**
 * {@link AdsLoader} using the IMA SDK. All methods must be called on the main thread.
 *
 * <p>The player instance that will play the loaded ads must be set before playback using {@link
 * #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
 * {@link #release()}.
 *
 * <p>See <a
 * href="https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility">IMA's
 * Support and compatibility page</a> for information on compatible ad tag formats. Pass the ad tag
 * URI when setting media item playback properties (if using the media item API) or as a {@link
 * DataSpec} when constructing the {@link AdsMediaSource} (if using media sources directly). For the
 * latter case, please note that this implementation delegates loading of the data spec to the IMA
 * SDK, so range and headers specifications will be ignored in ad tag URIs. Literal ads responses
 * can be encoded as data scheme data specs, for example, by constructing the data spec using a URI
 * generated via {@link Util#getDataUriForString(String, String)}.
 *
 * <p>The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
 * means that any overlay views that obstruct the ad overlay but are essential for playback need to
 * be registered via the {@link AdViewProvider} passed to the {@link AdsMediaSource}. See the <a
 * href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/omsdk">IMA
 * SDK Open Measurement documentation</a> for more information.
 */
public final class ImaAdsLoader implements AdsLoader {

  static {
    MediaLibraryInfo.registerModule("media3.exoplayer.ima");
  }

  /** Builder for {@link ImaAdsLoader}. */
  public static final class Builder {

    /**
     * The default duration in milliseconds for which the player must buffer while preloading an ad
     * group before that ad group is skipped and marked as having failed to load.
     *
     * <p>This value should be large enough not to trigger discarding the ad when it actually might
     * load soon, but small enough so that user is not waiting for too long.
     *
     * @see #setAdPreloadTimeoutMs(long)
     */
    @UnstableApi public static final long DEFAULT_AD_PRELOAD_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND;

    private final Context context;

    @Nullable private ImaSdkSettings imaSdkSettings;
    @Nullable private AdErrorListener adErrorListener;
    @Nullable private AdEventListener adEventListener;
    @Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback;
    @Nullable private List<String> adMediaMimeTypes;
    @Nullable private Set<UiElement> adUiElements;
    @Nullable private Collection<CompanionAdSlot> companionAdSlots;
    @Nullable private Boolean enableContinuousPlayback;
    private long adPreloadTimeoutMs;
    private int vastLoadTimeoutMs;
    private int mediaLoadTimeoutMs;
    private int mediaBitrate;
    private boolean focusSkipButtonWhenAvailable;
    private boolean playAdBeforeStartPosition;
    private boolean debugModeEnabled;
    private ImaUtil.ImaFactory imaFactory;

    /**
     * Creates a new builder for {@link ImaAdsLoader}.
     *
     * @param context The context;
     */
    public Builder(Context context) {
      this.context = checkNotNull(context).getApplicationContext();
      adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS;
      vastLoadTimeoutMs = TIMEOUT_UNSET;
      mediaLoadTimeoutMs = TIMEOUT_UNSET;
      mediaBitrate = BITRATE_UNSET;
      focusSkipButtonWhenAvailable = true;
      playAdBeforeStartPosition = true;
      imaFactory = new DefaultImaFactory();
    }

    /**
     * Sets the IMA SDK settings. The provided settings instance's player type and version fields
     * may be overwritten.
     *
     * <p>If this method is not called the default settings will be used.
     *
     * @param imaSdkSettings The {@link ImaSdkSettings}.
     * @return This builder, for convenience.
     */
    @UnstableApi
    public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
      this.imaSdkSettings = checkNotNull(imaSdkSettings);
      return this;
    }

    /**
     * Sets a listener for ad errors that will be passed to {@link
     * com.google.ads.interactivemedia.v3.api.AdsLoader#addAdErrorListener(AdErrorListener)} and
     * {@link AdsManager#addAdErrorListener(AdErrorListener)}.
     *
     * @param adErrorListener The ad error listener.
     * @return This builder, for convenience.
     */
    @UnstableApi
    public Builder setAdErrorListener(AdErrorListener adErrorListener) {
      this.adErrorListener = checkNotNull(adErrorListener);
      return this;
    }

    /**
     * Sets a listener for ad events that will be passed to {@link
     * AdsManager#addAdEventListener(AdEventListener)}.
     *
     * @param adEventListener The ad event listener.
     * @return This builder, for convenience.
     */
    @UnstableApi
    public Builder setAdEventListener(AdEventListener adEventListener) {
      this.adEventListener = checkNotNull(adEventListener);
      return this;
    }

    /**
     * Sets a callback to receive video ad player events. Note that these events are handled
     * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new
     * implementations should generally use events from the top-level {@link Player} listeners
     * instead of setting a callback via this method.
     *
     * @param videoAdPlayerCallback The callback to receive video ad player events.
     * @return This builder, for convenience.
     * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback
     */
    @UnstableApi
    public Builder setVideoAdPlayerCallback(
        VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) {
      this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback);
      return this;
    }

    /**
     * Sets the ad UI elements to be rendered by the IMA SDK.
     *
     * @param adUiElements The ad UI elements to be rendered by the IMA SDK.
     * @return This builder, for convenience.
     * @see AdsRenderingSettings#setUiElements(Set)
     */
    @UnstableApi
    public Builder setAdUiElements(Set<UiElement> adUiElements) {
      this.adUiElements = ImmutableSet.copyOf(checkNotNull(adUiElements));
      return this;
    }

    /**
     * Sets the slots to use for companion ads, if they are present in the loaded ad.
     *
     * @param companionAdSlots The slots to use for companion ads.
     * @return This builder, for convenience.
     * @see AdDisplayContainer#setCompanionSlots(Collection)
     */
    @UnstableApi
    public Builder setCompanionAdSlots(Collection<CompanionAdSlot> companionAdSlots) {
      this.companionAdSlots = ImmutableList.copyOf(checkNotNull(companionAdSlots));
      return this;
    }

    /**
     * Sets the MIME types to prioritize for linear ad media. If not specified, MIME types supported
     * by the {@link MediaSource.Factory adMediaSourceFactory} used to construct the {@link
     * AdsMediaSource} will be used.
     *
     * @param adMediaMimeTypes The MIME types to prioritize for linear ad media. May contain {@link
     *     MimeTypes#APPLICATION_MPD}, {@link MimeTypes#APPLICATION_M3U8}, {@link
     *     MimeTypes#VIDEO_MP4}, {@link MimeTypes#VIDEO_WEBM}, {@link MimeTypes#VIDEO_H263}, {@link
     *     MimeTypes#AUDIO_MP4} and {@link MimeTypes#AUDIO_MPEG}.
     * @return This builder, for convenience.
     * @see AdsRenderingSettings#setMimeTypes(List)
     */
    @UnstableApi
    public Builder setAdMediaMimeTypes(List<String> adMediaMimeTypes) {
      this.adMediaMimeTypes = ImmutableList.copyOf(checkNotNull(adMediaMimeTypes));
      return this;
    }

    /**
     * Sets whether to enable continuous playback. Pass {@code true} if content videos will be
     * played continuously, similar to a TV broadcast. This setting may modify the ads request but
     * does not affect ad playback behavior. The requested value is unknown by default.
     *
     * @param enableContinuousPlayback Whether to enable continuous playback.
     * @return This builder, for convenience.
     * @see AdsRequest#setContinuousPlayback(boolean)
     */
    @UnstableApi
    public Builder setEnableContinuousPlayback(boolean enableContinuousPlayback) {
      this.enableContinuousPlayback = enableContinuousPlayback;
      return this;
    }

    /**
     * Sets the duration in milliseconds for which the player must buffer while preloading an ad
     * group before that ad group is skipped and marked as having failed to load. Pass {@link
     * C#TIME_UNSET} if there should be no such timeout. The default value is {@value
     * #DEFAULT_AD_PRELOAD_TIMEOUT_MS} ms.
     *
     * <p>The purpose of this timeout is to avoid playback getting stuck in the unexpected case that
     * the IMA SDK does not load an ad break based on the player's reported content position.
     *
     * @param adPreloadTimeoutMs The timeout buffering duration in milliseconds, or {@link
     *     C#TIME_UNSET} for no timeout.
     * @return This builder, for convenience.
     */
    @UnstableApi
    public Builder setAdPreloadTimeoutMs(long adPreloadTimeoutMs) {
      checkArgument(adPreloadTimeoutMs == C.TIME_UNSET || adPreloadTimeoutMs > 0);
      this.adPreloadTimeoutMs = adPreloadTimeoutMs;
      return this;
    }

    /**
     * Sets the VAST load timeout, in milliseconds.
     *
     * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds.
     * @return This builder, for convenience.
     * @see AdsRequest#setVastLoadTimeout(float)
     */
    @UnstableApi
    public Builder setVastLoadTimeoutMs(@IntRange(from = 1) int vastLoadTimeoutMs) {
      checkArgument(vastLoadTimeoutMs > 0);
      this.vastLoadTimeoutMs = vastLoadTimeoutMs;
      return this;
    }

    /**
     * Sets the ad media load timeout, in milliseconds.
     *
     * @param mediaLoadTimeoutMs The ad media load timeout, in milliseconds.
     * @return This builder, for convenience.
     * @see AdsRenderingSettings#setLoadVideoTimeout(int)
     */
    @UnstableApi
    public Builder setMediaLoadTimeoutMs(@IntRange(from = 1) int mediaLoadTimeoutMs) {
      checkArgument(mediaLoadTimeoutMs > 0);
      this.mediaLoadTimeoutMs = mediaLoadTimeoutMs;
      return this;
    }

    /**
     * Sets the media maximum recommended bitrate for ads, in bps.
     *
     * @param bitrate The media maximum recommended bitrate for ads, in bps.
     * @return This builder, for convenience.
     * @see AdsRenderingSettings#setBitrateKbps(int)
     */
    @UnstableApi
    public Builder setMaxMediaBitrate(@IntRange(from = 1) int bitrate) {
      checkArgument(bitrate > 0);
      this.mediaBitrate = bitrate;
      return this;
    }

    /**
     * Sets whether to focus the skip button (when available) on Android TV devices. The default
     * setting is {@code true}.
     *
     * @param focusSkipButtonWhenAvailable Whether to focus the skip button (when available) on
     *     Android TV devices.
     * @return This builder, for convenience.
     * @see AdsRenderingSettings#setFocusSkipButtonWhenAvailable(boolean)
     */
    @UnstableApi
    public Builder setFocusSkipButtonWhenAvailable(boolean focusSkipButtonWhenAvailable) {
      this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable;
      return this;
    }

    /**
     * Sets whether to play an ad before the start position when beginning playback. If {@code
     * true}, an ad will be played if there is one at or before the start position. If {@code
     * false}, an ad will be played only if there is one exactly at the start position. The default
     * setting is {@code true}.
     *
     * @param playAdBeforeStartPosition Whether to play an ad before the start position when
     *     beginning playback.
     * @return This builder, for convenience.
     */
    @UnstableApi
    public Builder setPlayAdBeforeStartPosition(boolean playAdBeforeStartPosition) {
      this.playAdBeforeStartPosition = playAdBeforeStartPosition;
      return this;
    }

    /**
     * Sets whether to enable outputting verbose logs for the IMA extension and IMA SDK. The default
     * value is {@code false}. This setting is intended for debugging only, and should not be
     * enabled in production applications.
     *
     * @param debugModeEnabled Whether to enable outputting verbose logs for the IMA extension and
     *     IMA SDK.
     * @return This builder, for convenience.
     * @see ImaSdkSettings#setDebugMode(boolean)
     */
    @UnstableApi
    public Builder setDebugModeEnabled(boolean debugModeEnabled) {
      this.debugModeEnabled = debugModeEnabled;
      return this;
    }

    @VisibleForTesting
    /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) {
      this.imaFactory = checkNotNull(imaFactory);
      return this;
    }

    /** Returns a new {@link ImaAdsLoader}. */
    public ImaAdsLoader build() {
      return new ImaAdsLoader(
          context,
          new ImaUtil.Configuration(
              adPreloadTimeoutMs,
              vastLoadTimeoutMs,
              mediaLoadTimeoutMs,
              focusSkipButtonWhenAvailable,
              playAdBeforeStartPosition,
              mediaBitrate,
              enableContinuousPlayback,
              adMediaMimeTypes,
              adUiElements,
              companionAdSlots,
              adErrorListener,
              adEventListener,
              videoAdPlayerCallback,
              imaSdkSettings,
              debugModeEnabled),
          imaFactory);
    }
  }

  private final ImaUtil.Configuration configuration;
  private final Context context;
  private final ImaUtil.ImaFactory imaFactory;
  private final PlayerListenerImpl playerListener;
  private final HashMap<Object, AdTagLoader> adTagLoaderByAdsId;
  private final HashMap<AdsMediaSource, AdTagLoader> adTagLoaderByAdsMediaSource;
  private final Timeline.Period period;
  private final Timeline.Window window;

  private boolean wasSetPlayerCalled;
  @Nullable private Player nextPlayer;
  private List<String> supportedMimeTypes;
  @Nullable private Player player;
  @Nullable private AdTagLoader currentAdTagLoader;

  private ImaAdsLoader(
      Context context, ImaUtil.Configuration configuration, ImaUtil.ImaFactory imaFactory) {
    this.context = context.getApplicationContext();
    this.configuration = configuration;
    this.imaFactory = imaFactory;
    playerListener = new PlayerListenerImpl();
    supportedMimeTypes = ImmutableList.of();
    adTagLoaderByAdsId = new HashMap<>();
    adTagLoaderByAdsMediaSource = new HashMap<>();
    period = new Timeline.Period();
    window = new Timeline.Window();
  }

  /**
   * Returns the underlying {@link com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by this
   * instance, or {@code null} if ads have not been requested yet.
   */
  @UnstableApi
  @Nullable
  public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() {
    return currentAdTagLoader != null ? currentAdTagLoader.getAdsLoader() : null;
  }

  /**
   * Returns the {@link AdDisplayContainer} used by this loader, or {@code null} if ads have not
   * been requested yet.
   *
   * <p>Note: any video controls overlays registered via {@link
   * AdDisplayContainer#registerFriendlyObstruction(FriendlyObstruction)} will be unregistered
   * automatically when the media source detaches from this instance. It is therefore necessary to
   * re-register views each time the ads loader is reused. Alternatively, provide overlay views via
   * the {@link AdViewProvider} when creating the media source to benefit from automatic
   * registration.
   */
  @UnstableApi
  @Nullable
  public AdDisplayContainer getAdDisplayContainer() {
    return currentAdTagLoader != null ? currentAdTagLoader.getAdDisplayContainer() : null;
  }

  /**
   * Requests ads, if they have not already been requested. Must be called on the main thread.
   *
   * <p>Ads will be requested automatically when the player is prepared if this method has not been
   * called, so it is only necessary to call this method if you want to request ads before preparing
   * the player.
   *
   * @param adTagDataSpec The data specification of the ad tag to load. See class javadoc for
   *     information about compatible ad tag formats.
   * @param adsId A opaque identifier for the ad playback state across start/stop calls.
   * @param adViewGroup A {@link ViewGroup} on top of the player that will show any ad UI, or {@code
   *     null} if playing audio-only ads.
   */
  @UnstableApi
  public void requestAds(DataSpec adTagDataSpec, Object adsId, @Nullable ViewGroup adViewGroup) {
    if (!adTagLoaderByAdsId.containsKey(adsId)) {
      AdTagLoader adTagLoader =
          new AdTagLoader(
              context,
              configuration,
              imaFactory,
              supportedMimeTypes,
              adTagDataSpec,
              adsId,
              adViewGroup);
      adTagLoaderByAdsId.put(adsId, adTagLoader);
    }
  }

  /**
   * Skips the current ad.
   *
   * <p>This method is intended for apps that play audio-only ads and so need to provide their own
   * UI for users to skip skippable ads. Apps showing video ads should not call this method, as the
   * IMA SDK provides the UI to skip ads in the ad view group passed via {@link AdViewProvider}.
   */
  @UnstableApi
  public void skipAd() {
    if (currentAdTagLoader != null) {
      currentAdTagLoader.skipAd();
    }
  }

  /**
   * Moves UI focus to the skip button (or other interactive elements), if currently shown. See
   * {@link AdsManager#focus()}.
   */
  @UnstableApi
  public void focusSkipButton() {
    if (currentAdTagLoader != null) {
      currentAdTagLoader.focusSkipButton();
    }
  }

  // AdsLoader implementation.

  @Override
  public void setPlayer(@Nullable Player player) {
    checkState(Looper.myLooper() == getImaLooper());
    checkState(player == null || player.getApplicationLooper() == getImaLooper());
    nextPlayer = player;
    wasSetPlayerCalled = true;
  }

  @UnstableApi
  @Override
  public void setSupportedContentTypes(@C.ContentType int... contentTypes) {
    List<String> supportedMimeTypes = new ArrayList<>();
    for (@C.ContentType int contentType : contentTypes) {
      // IMA does not support Smooth Streaming ad media.
      if (contentType == C.TYPE_DASH) {
        supportedMimeTypes.add(MimeTypes.APPLICATION_MPD);
      } else if (contentType == C.TYPE_HLS) {
        supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8);
      } else if (contentType == C.TYPE_OTHER) {
        supportedMimeTypes.addAll(
            Arrays.asList(
                MimeTypes.VIDEO_MP4,
                MimeTypes.VIDEO_WEBM,
                MimeTypes.VIDEO_H263,
                MimeTypes.AUDIO_MP4,
                MimeTypes.AUDIO_MPEG));
      }
    }
    this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
  }

  @UnstableApi
  @Override
  public void start(
      AdsMediaSource adsMediaSource,
      DataSpec adTagDataSpec,
      Object adsId,
      AdViewProvider adViewProvider,
      EventListener eventListener) {
    checkState(
        wasSetPlayerCalled, "Set player using adsLoader.setPlayer before preparing the player.");
    if (adTagLoaderByAdsMediaSource.isEmpty()) {
      player = nextPlayer;
      @Nullable Player player = this.player;
      if (player == null) {
        return;
      }
      player.addListener(playerListener);
    }

    @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
    if (adTagLoader == null) {
      requestAds(adTagDataSpec, adsId, adViewProvider.getAdViewGroup());
      adTagLoader = adTagLoaderByAdsId.get(adsId);
    }
    adTagLoaderByAdsMediaSource.put(adsMediaSource, checkNotNull(adTagLoader));
    adTagLoader.addListenerWithAdView(eventListener, adViewProvider);
    maybeUpdateCurrentAdTagLoader();
  }

  @UnstableApi
  @Override
  public void stop(AdsMediaSource adsMediaSource, EventListener eventListener) {
    @Nullable AdTagLoader removedAdTagLoader = adTagLoaderByAdsMediaSource.remove(adsMediaSource);
    maybeUpdateCurrentAdTagLoader();
    if (removedAdTagLoader != null) {
      removedAdTagLoader.removeListener(eventListener);
    }

    if (player != null && adTagLoaderByAdsMediaSource.isEmpty()) {
      player.removeListener(playerListener);
      player = null;
    }
  }

  @Override
  public void release() {
    if (player != null) {
      player.removeListener(playerListener);
      player = null;
      maybeUpdateCurrentAdTagLoader();
    }
    nextPlayer = null;

    for (AdTagLoader adTagLoader : adTagLoaderByAdsMediaSource.values()) {
      adTagLoader.release();
    }
    adTagLoaderByAdsMediaSource.clear();

    for (AdTagLoader adTagLoader : adTagLoaderByAdsId.values()) {
      adTagLoader.release();
    }
    adTagLoaderByAdsId.clear();
  }

  @UnstableApi
  @Override
  public void handlePrepareComplete(
      AdsMediaSource adsMediaSource, int adGroupIndex, int adIndexInAdGroup) {
    if (player == null) {
      return;
    }
    checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource))
        .handlePrepareComplete(adGroupIndex, adIndexInAdGroup);
  }

  @UnstableApi
  @Override
  public void handlePrepareError(
      AdsMediaSource adsMediaSource,
      int adGroupIndex,
      int adIndexInAdGroup,
      IOException exception) {
    if (player == null) {
      return;
    }
    checkNotNull(adTagLoaderByAdsMediaSource.get(adsMediaSource))
        .handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
  }

  // Internal methods.

  private void maybeUpdateCurrentAdTagLoader() {
    @Nullable AdTagLoader oldAdTagLoader = currentAdTagLoader;
    @Nullable AdTagLoader newAdTagLoader = getCurrentAdTagLoader();
    if (!Util.areEqual(oldAdTagLoader, newAdTagLoader)) {
      if (oldAdTagLoader != null) {
        oldAdTagLoader.deactivate();
      }
      currentAdTagLoader = newAdTagLoader;
      if (newAdTagLoader != null) {
        newAdTagLoader.activate(checkNotNull(player));
      }
    }
  }

  @Nullable
  private AdTagLoader getCurrentAdTagLoader() {
    @Nullable Player player = this.player;
    if (player == null) {
      return null;
    }
    Timeline timeline = player.getCurrentTimeline();
    if (timeline.isEmpty()) {
      return null;
    }
    int periodIndex = player.getCurrentPeriodIndex();
    @Nullable Object adsId = timeline.getPeriod(periodIndex, period).getAdsId();
    if (adsId == null) {
      return null;
    }
    @Nullable AdTagLoader adTagLoader = adTagLoaderByAdsId.get(adsId);
    if (adTagLoader == null || !adTagLoaderByAdsMediaSource.containsValue(adTagLoader)) {
      return null;
    }
    return adTagLoader;
  }

  private void maybePreloadNextPeriodAds() {
    @Nullable Player player = ImaAdsLoader.this.player;
    if (player == null) {
      return;
    }
    Timeline timeline = player.getCurrentTimeline();
    if (timeline.isEmpty()) {
      return;
    }
    int nextPeriodIndex =
        timeline.getNextPeriodIndex(
            player.getCurrentPeriodIndex(),
            period,
            window,
            player.getRepeatMode(),
            player.getShuffleModeEnabled());
    if (nextPeriodIndex == C.INDEX_UNSET) {
      return;
    }
    timeline.getPeriod(nextPeriodIndex, period);
    @Nullable Object nextAdsId = period.getAdsId();
    if (nextAdsId == null) {
      return;
    }
    @Nullable AdTagLoader nextAdTagLoader = adTagLoaderByAdsId.get(nextAdsId);
    if (nextAdTagLoader == null || nextAdTagLoader == currentAdTagLoader) {
      return;
    }
    long periodPositionUs =
        timeline.getPeriodPositionUs(
                window, period, period.windowIndex, /* windowPositionUs= */ C.TIME_UNSET)
            .second;
    nextAdTagLoader.maybePreloadAds(Util.usToMs(periodPositionUs), Util.usToMs(period.durationUs));
  }

  private final class PlayerListenerImpl implements Player.Listener {

    @Override
    public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
      if (timeline.isEmpty()) {
        // The player is being reset or contains no media.
        return;
      }
      maybeUpdateCurrentAdTagLoader();
      maybePreloadNextPeriodAds();
    }

    @Override
    public void onPositionDiscontinuity(
        Player.PositionInfo oldPosition,
        Player.PositionInfo newPosition,
        @Player.DiscontinuityReason int reason) {
      maybeUpdateCurrentAdTagLoader();
      maybePreloadNextPeriodAds();
    }

    @Override
    public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
      maybePreloadNextPeriodAds();
    }

    @Override
    public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) {
      maybePreloadNextPeriodAds();
    }
  }

  /**
   * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link
   * ImaSdkFactory}.
   */
  private static final class DefaultImaFactory implements ImaUtil.ImaFactory {
    @Override
    public ImaSdkSettings createImaSdkSettings() {
      ImaSdkSettings settings = ImaSdkFactory.getInstance().createImaSdkSettings();
      settings.setLanguage(Util.getSystemLanguageCodes()[0]);
      return settings;
    }

    @Override
    public AdsRenderingSettings createAdsRenderingSettings() {
      return ImaSdkFactory.getInstance().createAdsRenderingSettings();
    }

    @Override
    public AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player) {
      return ImaSdkFactory.createAdDisplayContainer(container, player);
    }

    @Override
    public AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player) {
      return ImaSdkFactory.createAudioAdDisplayContainer(context, player);
    }

    // The reasonDetail parameter to createFriendlyObstruction is annotated @Nullable but the
    // annotation is not kept in the obfuscated dependency.
    @SuppressWarnings("nullness:argument")
    @Override
    public FriendlyObstruction createFriendlyObstruction(
        View view,
        FriendlyObstructionPurpose friendlyObstructionPurpose,
        @Nullable String reasonDetail) {
      return ImaSdkFactory.getInstance()
          .createFriendlyObstruction(view, friendlyObstructionPurpose, reasonDetail);
    }

    @Override
    public AdsRequest createAdsRequest() {
      return ImaSdkFactory.getInstance().createAdsRequest();
    }

    @Override
    public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader(
        Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) {
      return ImaSdkFactory.getInstance()
          .createAdsLoader(context, imaSdkSettings, adDisplayContainer);
    }
  }
}