diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index c6f478c67c0..0b4fb8b296d 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -83,6 +83,9 @@
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
* Enforce playback speed of 1.0 during ad playback
([#9018](https://github.com/google/ExoPlayer/issues/9018)).
+ * Add support for
+ [IMA Dynamic Ad Insertion (DAI)](https://support.google.com/admanager/answer/6147120)
+ ([#8213](https://github.com/google/ExoPlayer/issues/8213)).
* DASH:
* Support the `forced-subtitle` track role
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java
new file mode 100644
index 00000000000..0c2aa28003d
--- /dev/null
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaServerSideAdInsertionMediaSource.java
@@ -0,0 +1,1126 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.ext.ima;
+
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.expandAdGroupPlaceholder;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.splitAdPlaybackStateForPeriods;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationAndPropagate;
+import static com.google.android.exoplayer2.ext.ima.ImaUtil.updateAdDurationInAdGroup;
+import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.addAdGroupToAdPlaybackState;
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+import static com.google.android.exoplayer2.util.Assertions.checkState;
+import static com.google.android.exoplayer2.util.Util.secToUs;
+import static com.google.android.exoplayer2.util.Util.sum;
+import static com.google.android.exoplayer2.util.Util.usToMs;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.view.ViewGroup;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import com.google.ads.interactivemedia.v3.api.Ad;
+import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
+import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
+import com.google.ads.interactivemedia.v3.api.AdEvent;
+import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener;
+import com.google.ads.interactivemedia.v3.api.AdPodInfo;
+import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener;
+import com.google.ads.interactivemedia.v3.api.AdsManager;
+import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent;
+import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings;
+import com.google.ads.interactivemedia.v3.api.CompanionAdSlot;
+import com.google.ads.interactivemedia.v3.api.CuePoint;
+import com.google.ads.interactivemedia.v3.api.ImaSdkFactory;
+import com.google.ads.interactivemedia.v3.api.ImaSdkSettings;
+import com.google.ads.interactivemedia.v3.api.StreamDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.StreamManager;
+import com.google.ads.interactivemedia.v3.api.StreamRequest;
+import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate;
+import com.google.ads.interactivemedia.v3.api.player.VideoStreamPlayer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.emsg.EventMessage;
+import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
+import com.google.android.exoplayer2.source.CompositeMediaSource;
+import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
+import com.google.android.exoplayer2.source.ForwardingTimeline;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource;
+import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionMediaSource.AdPlaybackStateUpdater;
+import com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil;
+import com.google.android.exoplayer2.ui.AdOverlayInfo;
+import com.google.android.exoplayer2.ui.AdViewProvider;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.upstream.TransferListener;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.android.exoplayer2.util.Util;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * MediaSource for IMA server side inserted ad streams.
+ *
+ *
TODO(bachinger) add code snippet from PlayerActivity
+ */
+public final class ImaServerSideAdInsertionMediaSource extends CompositeMediaSource {
+
+ /**
+ * Factory for creating {@link ImaServerSideAdInsertionMediaSource
+ * ImaServerSideAdInsertionMediaSources}.
+ *
+ *
Apps can use the {@link ImaServerSideAdInsertionMediaSource.Factory} to customized the
+ * {@link DefaultMediaSourceFactory} that is used to build a player:
+ *
+ *
TODO(bachinger) add code snippet from PlayerActivity
+ */
+ public static final class Factory implements MediaSource.Factory {
+
+ private final AdsLoader adsLoader;
+ private final MediaSource.Factory contentMediaSourceFactory;
+
+ /**
+ * Creates a new factory for {@link ImaServerSideAdInsertionMediaSource
+ * ImaServerSideAdInsertionMediaSources}.
+ *
+ * @param adsLoader The {@link AdsLoader}.
+ * @param contentMediaSourceFactory The content media source factory to create content sources.
+ */
+ public Factory(AdsLoader adsLoader, MediaSource.Factory contentMediaSourceFactory) {
+ this.adsLoader = adsLoader;
+ this.contentMediaSourceFactory = contentMediaSourceFactory;
+ }
+
+ @Override
+ public MediaSource.Factory setLoadErrorHandlingPolicy(
+ @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
+ contentMediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
+ return this;
+ }
+
+ @Override
+ public MediaSource.Factory setDrmSessionManagerProvider(
+ @Nullable DrmSessionManagerProvider drmSessionManagerProvider) {
+ contentMediaSourceFactory.setDrmSessionManagerProvider(drmSessionManagerProvider);
+ return this;
+ }
+
+ @Override
+ public int[] getSupportedTypes() {
+ return contentMediaSourceFactory.getSupportedTypes();
+ }
+
+ @Override
+ public MediaSource createMediaSource(MediaItem mediaItem) {
+ Player player = checkNotNull(adsLoader.player);
+ StreamPlayer streamPlayer = new StreamPlayer(player, mediaItem);
+ ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance();
+ StreamDisplayContainer streamDisplayContainer =
+ createStreamDisplayContainer(imaSdkFactory, adsLoader.configuration, streamPlayer);
+ com.google.ads.interactivemedia.v3.api.AdsLoader imaAdsLoader =
+ imaSdkFactory.createAdsLoader(
+ adsLoader.context, adsLoader.configuration.imaSdkSettings, streamDisplayContainer);
+ ImaServerSideAdInsertionMediaSource mediaSource =
+ new ImaServerSideAdInsertionMediaSource(
+ mediaItem,
+ player,
+ imaAdsLoader,
+ streamPlayer,
+ contentMediaSourceFactory,
+ adsLoader.configuration.applicationAdEventListener,
+ adsLoader.configuration.applicationAdErrorListener);
+ adsLoader.addMediaSourceResources(mediaSource, streamPlayer, imaAdsLoader);
+ return mediaSource;
+ }
+ }
+
+ /** An ads loader for IMA server side ad insertion streams. */
+ public static final class AdsLoader {
+
+ /** Builder for building an {@link AdsLoader}. */
+ public static final class Builder {
+
+ private final Context context;
+ private final AdViewProvider adViewProvider;
+
+ @Nullable private ImaSdkSettings imaSdkSettings;
+ @Nullable private AdEventListener adEventListener;
+ @Nullable private AdErrorEvent.AdErrorListener adErrorListener;
+ private ImmutableList companionAdSlots;
+
+ /**
+ * Creates an instance.
+ *
+ * @param context A context.
+ * @param adViewProvider A provider for {@link ViewGroup} instances.
+ */
+ public Builder(Context context, AdViewProvider adViewProvider) {
+ this.context = context;
+ this.adViewProvider = adViewProvider;
+ companionAdSlots = ImmutableList.of();
+ }
+
+ /**
+ * Sets the IMA SDK settings.
+ *
+ *
If this method is not called the default settings will be used.
+ *
+ * @param imaSdkSettings The {@link ImaSdkSettings}.
+ * @return This builder, for convenience.
+ */
+ public AdsLoader.Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) {
+ this.imaSdkSettings = imaSdkSettings;
+ return this;
+ }
+
+ /**
+ * Sets the optional {@link AdEventListener} that will be passed to {@link
+ * AdsManager#addAdEventListener(AdEventListener)}.
+ *
+ * @param adEventListener The ad event listener.
+ * @return This builder, for convenience.
+ */
+ public AdsLoader.Builder setAdEventListener(AdEventListener adEventListener) {
+ this.adEventListener = adEventListener;
+ return this;
+ }
+
+ /**
+ * Sets the optional {@link AdErrorEvent.AdErrorListener} that will be passed to {@link
+ * AdsManager#addAdErrorListener(AdErrorEvent.AdErrorListener)}.
+ *
+ * @param adErrorListener The {@link AdErrorEvent.AdErrorListener}.
+ * @return This builder, for convenience.
+ */
+ public AdsLoader.Builder setAdErrorListener(AdErrorEvent.AdErrorListener adErrorListener) {
+ this.adErrorListener = adErrorListener;
+ 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)
+ */
+ public AdsLoader.Builder setCompanionAdSlots(Collection companionAdSlots) {
+ this.companionAdSlots = ImmutableList.copyOf(companionAdSlots);
+ return this;
+ }
+
+ /** Returns a new {@link AdsLoader}. */
+ public AdsLoader build() {
+ @Nullable ImaSdkSettings imaSdkSettings = this.imaSdkSettings;
+ if (imaSdkSettings == null) {
+ imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
+ imaSdkSettings.setLanguage(Util.getSystemLanguageCodes()[0]);
+ }
+ ImaUtil.ServerSideAdInsertionConfiguration configuration =
+ new ImaUtil.ServerSideAdInsertionConfiguration(
+ adViewProvider,
+ imaSdkSettings,
+ adEventListener,
+ adErrorListener,
+ companionAdSlots,
+ imaSdkSettings.isDebugMode());
+ return new AdsLoader(context, configuration);
+ }
+ }
+
+ private final ImaUtil.ServerSideAdInsertionConfiguration configuration;
+ private final Context context;
+ private final Map
+ mediaSourceResources;
+
+ @Nullable private Player player;
+
+ private AdsLoader(Context context, ImaUtil.ServerSideAdInsertionConfiguration configuration) {
+ this.context = context.getApplicationContext();
+ this.configuration = configuration;
+ mediaSourceResources = new HashMap<>();
+ }
+
+ /**
+ * Sets the player.
+ *
+ *
This method needs to be called before adding server side ad insertion media items to the
+ * player.
+ */
+ public void setPlayer(Player player) {
+ this.player = player;
+ }
+
+ public void addMediaSourceResources(
+ ImaServerSideAdInsertionMediaSource mediaSource,
+ StreamPlayer streamPlayer,
+ com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
+ mediaSourceResources.put(mediaSource, new MediaSourceResourceHolder(streamPlayer, adsLoader));
+ }
+
+ /** Releases resources when the ads loader is no longer needed. */
+ public void release() {
+ for (MediaSourceResourceHolder resourceHolder : mediaSourceResources.values()) {
+ resourceHolder.streamPlayer.release();
+ resourceHolder.adsLoader.release();
+ }
+ mediaSourceResources.clear();
+ }
+
+ private static final class MediaSourceResourceHolder {
+ public final StreamPlayer streamPlayer;
+ public final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+
+ private MediaSourceResourceHolder(
+ StreamPlayer streamPlayer, com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader) {
+ this.streamPlayer = streamPlayer;
+ this.adsLoader = adsLoader;
+ }
+ }
+ }
+
+ private final MediaItem mediaItem;
+ private final Player player;
+ private final MediaSource.Factory contentMediaSourceFactory;
+ private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader;
+ @Nullable private final AdEventListener applicationAdEventListener;
+ @Nullable private final AdErrorListener applicationAdErrorListener;
+ private final ServerSideAdInsertionStreamRequest streamRequest;
+ private final StreamPlayer streamPlayer;
+ private final Handler mainHandler;
+ private final ComponentListener componentListener;
+
+ @Nullable private Loader loader;
+ @Nullable private StreamManager streamManager;
+ @Nullable private ServerSideAdInsertionMediaSource serverSideAdInsertionMediaSource;
+ @Nullable private IOException loadError;
+ private @MonotonicNonNull Timeline contentTimeline;
+ private AdPlaybackState adPlaybackState;
+
+ private ImaServerSideAdInsertionMediaSource(
+ MediaItem mediaItem,
+ Player player,
+ com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader,
+ StreamPlayer streamPlayer,
+ MediaSource.Factory contentMediaSourceFactory,
+ @Nullable AdEventListener applicationAdEventListener,
+ @Nullable AdErrorEvent.AdErrorListener applicationAdErrorListener) {
+ this.mediaItem = mediaItem;
+ this.player = player;
+ this.adsLoader = adsLoader;
+ this.streamPlayer = streamPlayer;
+ this.contentMediaSourceFactory = contentMediaSourceFactory;
+ this.applicationAdEventListener = applicationAdEventListener;
+ this.applicationAdErrorListener = applicationAdErrorListener;
+ componentListener = new ComponentListener();
+ adPlaybackState = AdPlaybackState.NONE;
+ mainHandler = Util.createHandlerForCurrentLooper();
+ Uri streamRequestUri = checkNotNull(mediaItem.localConfiguration).uri;
+ streamRequest = ServerSideAdInsertionStreamRequest.fromUri(streamRequestUri);
+ }
+
+ @Override
+ public MediaItem getMediaItem() {
+ return mediaItem;
+ }
+
+ @Override
+ public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
+ super.prepareSourceInternal(mediaTransferListener);
+ if (loader == null) {
+ Loader loader = new Loader("ImaServerSideAdInsertionMediaSource");
+ player.addListener(componentListener);
+ StreamManagerLoadable streamManagerLoadable =
+ new StreamManagerLoadable(
+ adsLoader,
+ streamRequest.getStreamRequest(),
+ streamPlayer,
+ applicationAdErrorListener,
+ streamRequest.loadVideoTimeoutMs);
+ loader.startLoading(
+ streamManagerLoadable,
+ new StreamManagerLoadableCallback(),
+ /* defaultMinRetryCount= */ 0);
+ this.loader = loader;
+ }
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Void id, MediaSource mediaSource, Timeline newTimeline) {
+ refreshSourceInfo(
+ new ForwardingTimeline(newTimeline) {
+ @Override
+ public Window getWindow(
+ int windowIndex, Window window, long defaultPositionProjectionUs) {
+ newTimeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
+ window.mediaItem = mediaItem;
+ return window;
+ }
+ });
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ return checkNotNull(serverSideAdInsertionMediaSource)
+ .createPeriod(id, allocator, startPositionUs);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ checkNotNull(serverSideAdInsertionMediaSource).releasePeriod(mediaPeriod);
+ }
+
+ @Override
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ super.maybeThrowSourceInfoRefreshError();
+ if (loadError != null) {
+ IOException loadError = this.loadError;
+ this.loadError = null;
+ throw loadError;
+ }
+ }
+
+ @Override
+ protected void releaseSourceInternal() {
+ super.releaseSourceInternal();
+ if (loader != null) {
+ loader.release();
+ player.removeListener(componentListener);
+ mainHandler.post(() -> setStreamManager(/* streamManager= */ null));
+ loader = null;
+ }
+ }
+
+ // Internal methods (called on the main thread).
+
+ @MainThread
+ private void setStreamManager(@Nullable StreamManager streamManager) {
+ if (this.streamManager == streamManager) {
+ return;
+ }
+ if (this.streamManager != null) {
+ if (applicationAdEventListener != null) {
+ this.streamManager.removeAdEventListener(applicationAdEventListener);
+ }
+ if (applicationAdErrorListener != null) {
+ this.streamManager.removeAdErrorListener(applicationAdErrorListener);
+ }
+ this.streamManager.removeAdEventListener(componentListener);
+ this.streamManager.destroy();
+ this.streamManager = null;
+ }
+ this.streamManager = streamManager;
+ if (streamManager != null) {
+ streamManager.addAdEventListener(componentListener);
+ if (applicationAdEventListener != null) {
+ streamManager.addAdEventListener(applicationAdEventListener);
+ }
+ if (applicationAdErrorListener != null) {
+ streamManager.addAdErrorListener(applicationAdErrorListener);
+ }
+ }
+ }
+
+ @MainThread
+ private void setAdPlaybackState(AdPlaybackState adPlaybackState) {
+ if (adPlaybackState.equals(this.adPlaybackState)) {
+ return;
+ }
+ this.adPlaybackState = adPlaybackState;
+ invalidateServerSideAdInsertionAdPlaybackState();
+ }
+
+ @MainThread
+ private void setContentTimeline(Timeline contentTimeline) {
+ if (contentTimeline.equals(this.contentTimeline)) {
+ return;
+ }
+ this.contentTimeline = contentTimeline;
+ invalidateServerSideAdInsertionAdPlaybackState();
+ }
+
+ @MainThread
+ private void invalidateServerSideAdInsertionAdPlaybackState() {
+ if (!adPlaybackState.equals(AdPlaybackState.NONE) && contentTimeline != null) {
+ ImmutableMap