diff --git a/extensions/ima/README.md b/extensions/ima/README.md index aaae44edcf..9ef37170d7 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -36,9 +36,6 @@ section of the app. This is a preview version with some known issues: -* Seeking is not yet ad aware. This means that it's possible to seek back into - ads that have already been played, and also seek past midroll ads without - them being played. Seeking will be made ad aware for the first stable release. * Midroll ads are not yet fully supported. `playAd` and `AD_STARTED` events are sometimes delayed, meaning that midroll ads take a long time to start and the ad overlay does not show immediately. diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java deleted file mode 100644 index 1f8008ed10..0000000000 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdTimeline.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * 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 com.google.android.exoplayer2.ext.ima; - -import android.util.Pair; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; - -/** - * A {@link Timeline} for {@link ImaAdsMediaSource}. - */ -/* package */ final class AdTimeline extends Timeline { - - private static final Object AD_ID = new Object(); - - /** - * Builder for ad timelines. - */ - public static final class Builder { - - private final Timeline contentTimeline; - private final long contentDurationUs; - private final ArrayList isAd; - private final ArrayList ads; - private final ArrayList startTimesUs; - private final ArrayList endTimesUs; - private final ArrayList uids; - - /** - * Creates a new ad timeline builder using the specified {@code contentTimeline} as the timeline - * of the content within which to insert ad breaks. - * - * @param contentTimeline The timeline of the content within which to insert ad breaks. - */ - public Builder(Timeline contentTimeline) { - this.contentTimeline = contentTimeline; - contentDurationUs = contentTimeline.getPeriod(0, new Period()).durationUs; - isAd = new ArrayList<>(); - ads = new ArrayList<>(); - startTimesUs = new ArrayList<>(); - endTimesUs = new ArrayList<>(); - uids = new ArrayList<>(); - } - - /** - * Adds an ad period. Each individual ad in an ad pod is represented by a separate ad period. - * - * @param ad The {@link Ad} instance representing the ad break, or {@code null} if not known. - * @param adBreakIndex The index of the ad break that contains the ad in the timeline. - * @param adIndexInAdBreak The index of the ad in its ad break. - * @param durationUs The duration of the ad, in microseconds. May be {@link C#TIME_UNSET}. - * @return The builder. - */ - public Builder addAdPeriod(Ad ad, int adBreakIndex, int adIndexInAdBreak, long durationUs) { - isAd.add(true); - ads.add(ad); - startTimesUs.add(0L); - endTimesUs.add(durationUs); - uids.add(Pair.create(adBreakIndex, adIndexInAdBreak)); - return this; - } - - /** - * Adds a content period. - * - * @param startTimeUs The start time of the period relative to the start of the content - * timeline, in microseconds. - * @param endTimeUs The end time of the period relative to the start of the content timeline, in - * microseconds. May be {@link C#TIME_UNSET} to include the rest of the content. - * @return The builder. - */ - public Builder addContent(long startTimeUs, long endTimeUs) { - ads.add(null); - isAd.add(false); - startTimesUs.add(startTimeUs); - endTimesUs.add(endTimeUs == C.TIME_UNSET ? contentDurationUs : endTimeUs); - uids.add(Pair.create(startTimeUs, endTimeUs)); - return this; - } - - /** - * Builds and returns the ad timeline. - */ - public AdTimeline build() { - int periodCount = uids.size(); - Assertions.checkState(periodCount > 0); - Ad[] ads = new Ad[periodCount]; - boolean[] isAd = new boolean[periodCount]; - long[] startTimesUs = new long[periodCount]; - long[] endTimesUs = new long[periodCount]; - for (int i = 0; i < periodCount; i++) { - ads[i] = this.ads.get(i); - isAd[i] = this.isAd.get(i); - startTimesUs[i] = this.startTimesUs.get(i); - endTimesUs[i] = this.endTimesUs.get(i); - } - Object[] uids = this.uids.toArray(new Object[periodCount]); - return new AdTimeline(contentTimeline, isAd, ads, startTimesUs, endTimesUs, uids); - } - - } - - private final Period contentPeriod; - private final Window contentWindow; - private final boolean[] isAd; - private final Ad[] ads; - private final long[] startTimesUs; - private final long[] endTimesUs; - private final Object[] uids; - - private AdTimeline(Timeline contentTimeline, boolean[] isAd, Ad[] ads, long[] startTimesUs, - long[] endTimesUs, Object[] uids) { - contentWindow = contentTimeline.getWindow(0, new Window(), true); - contentPeriod = contentTimeline.getPeriod(0, new Period(), true); - this.isAd = isAd; - this.ads = ads; - this.startTimesUs = startTimesUs; - this.endTimesUs = endTimesUs; - this.uids = uids; - } - - /** - * Returns whether the period at {@code index} contains ad media. - */ - public boolean isPeriodAd(int index) { - return isAd[index]; - } - - /** - * Returns the duration of the content within which ads have been inserted, in microseconds. - */ - public long getContentDurationUs() { - return contentPeriod.durationUs; - } - - /** - * Returns the start time of the period at {@code periodIndex} relative to the start of the - * content, in microseconds. - * - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content - * period. - */ - public long getContentStartTimeUs(int periodIndex) { - Assertions.checkArgument(!isAd[periodIndex]); - return startTimesUs[periodIndex]; - } - - /** - * Returns the end time of the period at {@code periodIndex} relative to the start of the content, - * in microseconds. - * - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not a content - * period. - */ - public long getContentEndTimeUs(int periodIndex) { - Assertions.checkArgument(!isAd[periodIndex]); - return endTimesUs[periodIndex]; - } - - /** - * Returns the index of the ad break to which the period at {@code periodIndex} belongs. - * - * @param periodIndex The period index. - * @return The index of the ad break to which the period belongs. - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. - */ - public int getAdBreakIndex(int periodIndex) { - Assertions.checkArgument(isAd[periodIndex]); - int adBreakIndex = 0; - for (int i = 1; i < periodIndex; i++) { - if (!isAd[i] && isAd[i - 1]) { - adBreakIndex++; - } - } - return adBreakIndex; - } - - /** - * Returns the index of the ad at {@code periodIndex} in its ad break. - * - * @param periodIndex The period index. - * @return The index of the ad at {@code periodIndex} in its ad break. - * @throws IllegalArgumentException Thrown if the period at {@code periodIndex} is not an ad. - */ - public int getAdIndexInAdBreak(int periodIndex) { - Assertions.checkArgument(isAd[periodIndex]); - int adIndex = 0; - for (int i = 0; i < periodIndex; i++) { - if (isAd[i]) { - adIndex++; - } else { - adIndex = 0; - } - } - return adIndex; - } - - @Override - public int getWindowCount() { - return uids.length; - } - - @Override - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; - } - return super.getNextWindowIndex(windowIndex, repeatMode); - } - - @Override - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; - } - return super.getPreviousWindowIndex(windowIndex, repeatMode); - } - - @Override - public Window getWindow(int index, Window window, boolean setIds, - long defaultPositionProjectionUs) { - long startTimeUs = startTimesUs[index]; - long durationUs = endTimesUs[index] - startTimeUs; - if (isAd[index]) { - window.set(ads[index], C.TIME_UNSET, C.TIME_UNSET, false, false, 0L, durationUs, index, index, - 0L); - } else { - window.set(contentWindow.id, contentWindow.presentationStartTimeMs + C.usToMs(startTimeUs), - contentWindow.windowStartTimeMs + C.usToMs(startTimeUs), contentWindow.isSeekable, false, - 0L, durationUs, index, index, 0L); - } - return window; - } - - @Override - public int getPeriodCount() { - return uids.length; - } - - @Override - public Period getPeriod(int index, Period period, boolean setIds) { - Object id = setIds ? (isAd[index] ? AD_ID : contentPeriod.id) : null; - return period.set(id, uids[index], index, endTimesUs[index] - startTimesUs[index], 0, - isAd[index]); - } - - @Override - public int getIndexOfPeriod(Object uid) { - for (int i = 0; i < uids.length; i++) { - if (Util.areEqual(uid, uids[i])) { - return i; - } - } - return C.INDEX_UNSET; - } - -} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index c4b626e355..0b14f16256 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -66,36 +66,35 @@ import java.util.List; public interface EventListener { /** - * Called when the timestamps of ad breaks are known. + * Called when the times of ad groups are known. * - * @param adBreakTimesUs The times of ad breaks, in microseconds. + * @param adGroupTimesUs The times of ad groups, in microseconds. */ - void onAdBreakTimesUsLoaded(long[] adBreakTimesUs); + void onAdGroupTimesUsLoaded(long[] adGroupTimesUs); + + /** + * Called when an ad group has been played to the end. + * + * @param adGroupIndex The index of the ad group. + */ + void onAdGroupPlayedToEnd(int adGroupIndex); /** * Called when the URI for the media of an ad has been loaded. * - * @param adBreakIndex The index of the ad break containing the ad with the media URI. - * @param adIndexInAdBreak The index of the ad in its ad break. + * @param adGroupIndex The index of the ad group containing the ad with the media URI. + * @param adIndexInAdGroup The index of the ad in its ad group. * @param uri The URI for the ad's media. */ - void onUriLoaded(int adBreakIndex, int adIndexInAdBreak, Uri uri); + void onAdUriLoaded(int adGroupIndex, int adIndexInAdGroup, Uri uri); /** - * Called when the {@link Ad} instance for a specified ad has been loaded. + * Called when an ad group has loaded. * - * @param adBreakIndex The index of the ad break containing the ad. - * @param adIndexInAdBreak The index of the ad in its ad break. - * @param ad The {@link Ad} instance for the ad. + * @param adGroupIndex The index of the ad group containing the ad. + * @param adCountInAdGroup The number of ads in the ad group. */ - void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad); - - /** - * Called when the specified ad break has been played to the end. - * - * @param adBreakIndex The index of the ad break. - */ - void onAdBreakPlayedToEnd(int adBreakIndex); + void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup); /** * Called when there was an error loading ads. @@ -127,51 +126,45 @@ import java.util.List; private final AdsLoader adsLoader; private AdsManager adsManager; - private AdTimeline adTimeline; + private long[] adGroupTimesUs; + private int[] adsLoadedInAdGroup; + private Timeline timeline; private long contentDurationMs; - private int lastContentPeriodIndex; - - private int playerPeriodIndex; private boolean released; // Fields tracking IMA's state. /** - * The index of the current ad break that IMA is loading. + * The index of the current ad group that IMA is loading. */ - private int adBreakIndex; + private int adGroupIndex; /** - * The index of the ad within its ad break, in {@link #loadAd(String)}. - */ - private int adIndexInAdBreak; - /** - * The total number of ads in the current ad break, or {@link C#INDEX_UNSET} if unknown. - */ - private int adCountInAdBreak; - - /** - * Tracks the period currently being played in IMA's model of playback. - */ - private int imaPeriodIndex; - /** - * Whether the period at {@link #imaPeriodIndex} is an ad. - */ - private boolean isAdDisplayed; - /** - * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. - */ - private boolean sentContentComplete; - /** - * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #playAd()} and not + * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #playAd()} and not * {@link #stopAd()}. */ private boolean playingAd; /** - * If {@link #isAdDisplayed} is set, stores whether IMA has called {@link #pauseAd()} since a - * preceding call to {@link #playAd()} for the current ad. + * If {@link #playingAdGroupIndex} is set, stores whether IMA has called {@link #pauseAd()} since + * a preceding call to {@link #playAd()} for the current ad. */ private boolean pausedInAd; + /** + * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + */ + private boolean sentContentComplete; + + // Fields tracking the player/loader state. + + /** + * If the player is playing an ad, stores the ad group index. {@link C#INDEX_UNSET} otherwise. + */ + private int playingAdGroupIndex; + /** + * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} + * otherwise. + */ + private int playingAdIndexInAdGroup; /** * If a content period has finished but IMA has not yet sent an ad event with * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of @@ -179,6 +172,14 @@ import java.util.List; * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressElapsedRealtimeMs; + /** + * Stores the pending content position when a seek operation was intercepted to play an ad. + */ + private long pendingContentPositionMs; + /** + * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. + */ + private boolean sentPendingContentPositionMs; /** * Creates a new IMA ads loader. @@ -190,8 +191,7 @@ import java.util.List; * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. - * @param player The player instance that will play the loaded ad schedule. The player's timeline - * must be an {@link AdTimeline} matching the loaded ad schedule. + * @param player The player instance that will play the loaded ad schedule. * @param eventListener Listener for ad loader events. */ public ImaAdsLoader(Context context, Uri adTagUri, ViewGroup adUiViewGroup, @@ -201,9 +201,10 @@ import java.util.List; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); - lastContentPeriodIndex = C.INDEX_UNSET; - adCountInAdBreak = C.INDEX_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + pendingContentPositionMs = C.TIME_UNSET; + adGroupIndex = C.INDEX_UNSET; + contentDurationMs = C.TIME_UNSET; player.addListener(this); @@ -262,13 +263,16 @@ import java.util.List; Log.d(TAG, "Initialized without preloading"); } } - eventListener.onAdBreakTimesUsLoaded(getAdBreakTimesUs(adsManager.getAdCuePoints())); + adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adsLoadedInAdGroup = new int[adGroupTimesUs.length]; + eventListener.onAdGroupTimesUsLoaded(adGroupTimesUs); } // AdEvent.AdEventListener implementation. @Override public void onAdEvent(AdEvent adEvent) { + Ad ad = adEvent.getAd(); if (DEBUG) { Log.d(TAG, "onAdEvent " + adEvent.getType()); } @@ -278,20 +282,18 @@ import java.util.List; } switch (adEvent.getType()) { case LOADED: - adsManager.start(); - break; - case STARTED: - // Note: This event is sometimes delivered several seconds after playAd is called. - // See [Internal: b/37775441]. - Ad ad = adEvent.getAd(); + // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. AdPodInfo adPodInfo = ad.getAdPodInfo(); - adCountInAdBreak = adPodInfo.getTotalAds(); + int podIndex = adPodInfo.getPodIndex(); + adGroupIndex = podIndex == -1 ? adGroupTimesUs.length - 1 : podIndex; int adPosition = adPodInfo.getAdPosition(); - eventListener.onAdLoaded(adBreakIndex, adPosition - 1, ad); + int adCountInAdGroup = adPodInfo.getTotalAds(); + adsManager.start(); if (DEBUG) { - Log.d(TAG, "Started ad " + adPosition + " of " + adCountInAdBreak + " in ad break " - + adBreakIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " + + adGroupIndex); } + eventListener.onAdGroupLoaded(adGroupIndex, adCountInAdGroup); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -325,40 +327,41 @@ import java.util.List; @Override public VideoProgressUpdate getContentProgress() { - if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { - long contentEndTimeMs = C.usToMs(adTimeline.getContentEndTimeUs(imaPeriodIndex)); - long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - return new VideoProgressUpdate(contentEndTimeMs + elapsedSinceEndMs, contentDurationMs); + if (pendingContentPositionMs != C.TIME_UNSET) { + sentPendingContentPositionMs = true; + return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs); } - - if (adTimeline == null || isAdDisplayed || imaPeriodIndex != playerPeriodIndex - || contentDurationMs == C.TIME_UNSET) { + if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { + long adGroupTimeMs = C.usToMs(adGroupTimesUs[adGroupIndex]); + if (adGroupTimeMs == C.TIME_END_OF_SOURCE) { + adGroupTimeMs = contentDurationMs; + } + long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; + return new VideoProgressUpdate(adGroupTimeMs + elapsedSinceEndMs, contentDurationMs); + } + if (player.isPlayingAd() || contentDurationMs == C.TIME_UNSET) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } - checkForContentComplete(); - long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) - + player.getCurrentPosition(); - return new VideoProgressUpdate(positionMs, contentDurationMs); + return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); } // VideoAdPlayer implementation. @Override public VideoProgressUpdate getAdProgress() { - if (adTimeline == null || !isAdDisplayed || imaPeriodIndex != playerPeriodIndex - || adTimeline.getPeriod(imaPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { + if (!player.isPlayingAd()) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } - return new VideoProgressUpdate(player.getCurrentPosition(), period.getDurationMs()); + return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); } @Override public void loadAd(String adUriString) { + int adIndexInAdGroup = adsLoadedInAdGroup[adGroupIndex]++; if (DEBUG) { - Log.d(TAG, "loadAd at index " + adIndexInAdBreak + " in ad break " + adBreakIndex); + Log.d(TAG, "loadAd at index " + adIndexInAdGroup + " in ad group " + adGroupIndex); } - eventListener.onUriLoaded(adBreakIndex, adIndexInAdBreak, Uri.parse(adUriString)); - adIndexInAdBreak++; + eventListener.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); } @Override @@ -376,7 +379,6 @@ import java.util.List; if (DEBUG) { Log.d(TAG, "playAd"); } - Assertions.checkState(isAdDisplayed); if (playingAd && !pausedInAd) { // Work around an issue where IMA does not always call stopAd before resuming content. // See [Internal: b/38354028]. @@ -443,18 +445,12 @@ import java.util.List; // The player is being re-prepared and this source will be released. return; } - if (adTimeline == null) { - // TODO: Handle initial seeks after the first period. - isAdDisplayed = timeline.getPeriod(0, period).isAd; - imaPeriodIndex = 0; - player.seekTo(0, 0); - } - adTimeline = (AdTimeline) timeline; - contentDurationMs = C.usToMs(adTimeline.getContentDurationUs()); - lastContentPeriodIndex = adTimeline.getPeriodCount() - 1; - while (adTimeline.isPeriodAd(lastContentPeriodIndex)) { - // All timelines have at least one content period. - lastContentPeriodIndex--; + Assertions.checkArgument(timeline.getPeriodCount() == 1); + this.timeline = timeline; + contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); + if (player.isPlayingAd()) { + playingAdGroupIndex = player.getCurrentAdGroupIndex(); + playingAdIndexInAdGroup = player.getCurrentAdIndexInAdGroup(); } } @@ -470,9 +466,9 @@ import java.util.List; @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { + if (!playingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { checkForContentComplete(); - } else if (playbackState == ExoPlayer.STATE_ENDED && isAdDisplayed) { + } else if (playingAd && playbackState == ExoPlayer.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (VideoAdPlayerCallback callback : adCallbacks) { @@ -488,7 +484,7 @@ import java.util.List; @Override public void onPlayerError(ExoPlaybackException error) { - if (isAdDisplayed && adTimeline.isPeriodAd(playerPeriodIndex)) { + if (player.isPlayingAd()) { for (VideoAdPlayerCallback callback : adCallbacks) { callback.onError(); } @@ -497,23 +493,41 @@ import java.util.List; @Override public void onPositionDiscontinuity() { - if (player.getCurrentPeriodIndex() == playerPeriodIndex + 1) { - if (isAdDisplayed) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); - } - } else { - player.setPlayWhenReady(false); - if (imaPeriodIndex == playerPeriodIndex) { - // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (!player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) { + long positionUs = C.msToUs(player.getCurrentPosition()); + int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = player.getCurrentPosition(); + } + return; + } + + boolean adFinished = (!player.isPlayingAd() && playingAdGroupIndex != C.INDEX_UNSET) + || (player.isPlayingAd() && playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup()); + if (adFinished) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + + if (player.isPlayingAd() && playingAdGroupIndex == C.INDEX_UNSET) { + player.setPlayWhenReady(false); + // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + if (adGroupIndex == adGroupTimesUs.length - 1) { + adsLoader.contentComplete(); + if (DEBUG) { + Log.d(TAG, "adsLoader.contentComplete"); } } } - playerPeriodIndex = player.getCurrentPeriodIndex(); + boolean isPlayingAd = player.isPlayingAd(); + playingAdGroupIndex = isPlayingAd ? player.getCurrentAdGroupIndex() : C.INDEX_UNSET; + playingAdIndexInAdGroup = isPlayingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } @Override @@ -527,70 +541,42 @@ import java.util.List; * Resumes the player, ensuring the current period is a content period by seeking if necessary. */ private void resumeContentInternal() { - if (adTimeline != null) { - if (imaPeriodIndex < lastContentPeriodIndex) { - if (playingAd) { - // Work around an issue where IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. - if (DEBUG) { - Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); - } - stopAdInternal(); + if (contentDurationMs != C.TIME_UNSET) { + if (playingAd) { + // Work around an issue where IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028]. + if (DEBUG) { + Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); } - while (adTimeline.isPeriodAd(imaPeriodIndex)) { - imaPeriodIndex++; - } - synchronizePlayerToIma(); + stopAdInternal(); } } player.setPlayWhenReady(true); + clearFlags(); } - /** - * Pauses the player, and ensures that the current period is an ad period by seeking if necessary. - */ private void pauseContentInternal() { + if (sentPendingContentPositionMs) { + pendingContentPositionMs = C.TIME_UNSET; + sentPendingContentPositionMs = false; + } // IMA is requesting to pause content, so stop faking the content position. fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - if (adTimeline != null && !isAdDisplayed) { - // Seek to the next ad. - while (!adTimeline.isPeriodAd(imaPeriodIndex)) { - imaPeriodIndex++; - } - synchronizePlayerToIma(); - } else { - // IMA is sending an initial CONTENT_PAUSE_REQUESTED before a pre-roll ad. - Assertions.checkState(playerPeriodIndex == 0 && imaPeriodIndex == 0); - } player.setPlayWhenReady(false); + clearFlags(); } - /** - * Stops the currently playing ad, seeking to the next content period if there is one. May only be - * called when {@link #playingAd} is {@code true}. - */ private void stopAdInternal() { Assertions.checkState(playingAd); - if (imaPeriodIndex != adTimeline.getPeriodCount() - 1) { - player.setPlayWhenReady(false); - imaPeriodIndex++; - if (!adTimeline.isPeriodAd(imaPeriodIndex)) { - eventListener.onAdBreakPlayedToEnd(adBreakIndex); - adBreakIndex++; - adIndexInAdBreak = 0; - } - synchronizePlayerToIma(); - } else { - eventListener.onAdBreakPlayedToEnd(adTimeline.getAdBreakIndex(imaPeriodIndex)); + player.setPlayWhenReady(false); + if (!player.isPlayingAd()) { + eventListener.onAdGroupPlayedToEnd(adGroupIndex); + adGroupIndex = C.INDEX_UNSET; } + clearFlags(); } - private void synchronizePlayerToIma() { - if (playerPeriodIndex != imaPeriodIndex) { - player.seekTo(imaPeriodIndex, 0); - } - - isAdDisplayed = adTimeline.isPeriodAd(imaPeriodIndex); + private void clearFlags() { // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until // the content is resumed. playingAd = false; @@ -598,14 +584,9 @@ import java.util.List; } private void checkForContentComplete() { - if (adTimeline == null || isAdDisplayed || sentContentComplete) { - return; - } - long positionMs = C.usToMs(adTimeline.getContentStartTimeUs(imaPeriodIndex)) - + player.getCurrentPosition(); - if (playerPeriodIndex == lastContentPeriodIndex - && positionMs + END_OF_CONTENT_POSITION_THRESHOLD_MS - >= C.usToMs(adTimeline.getContentEndTimeUs(playerPeriodIndex))) { + if (contentDurationMs != C.TIME_UNSET + && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + && !sentContentComplete) { adsLoader.contentComplete(); if (DEBUG) { Log.d(TAG, "adsLoader.contentComplete"); @@ -614,19 +595,20 @@ import java.util.List; } } - private static long[] getAdBreakTimesUs(List cuePoints) { + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad break. + // If no cue points are specified, there is a preroll ad. return new long[] {0}; } int count = cuePoints.size(); - long[] adBreakTimesUs = new long[count]; + long[] adGroupTimesUs = new long[count]; for (int i = 0; i < count; i++) { double cuePoint = cuePoints.get(i); - adBreakTimesUs[i] = cuePoint == -1.0 ? C.TIME_UNSET : (long) (C.MICROS_PER_SECOND * cuePoint); + adGroupTimesUs[i] = + cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint); } - return adBreakTimesUs; + return adGroupTimesUs; } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index ea6aaaf01c..5e96bd26dc 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -20,20 +20,14 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.view.ViewGroup; -import com.google.ads.interactivemedia.v3.api.Ad; -import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -56,7 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource { private final ImaSdkSettings imaSdkSettings; private final Handler mainHandler; private final AdListener adLoaderListener; - private final Map mediaSourceByMediaPeriod; + private final Map adMediaSourceByMediaPeriod; + private final Timeline.Period period; private Handler playerHandler; private ExoPlayer player; @@ -65,13 +60,12 @@ public final class ImaAdsMediaSource implements MediaSource { // Accessed on the player thread. private Timeline contentTimeline; private Object contentManifest; - private long[] adBreakTimesUs; - private boolean[] playedAdBreak; - private Ad[][] adBreakAds; - private Timeline[][] adBreakTimelines; - private MediaSource[][] adBreakMediaSources; - private DeferredMediaPeriod[][] adBreakDeferredMediaPeriods; - private AdTimeline timeline; + private long[] adGroupTimesUs; + private boolean[] hasPlayedAdGroup; + private int[] adCounts; + private MediaSource[][] adGroupMediaSources; + private boolean[][] isAdAvailable; + private long[][] adDurationsUs; private MediaSource.Listener listener; private IOException adLoadError; @@ -120,8 +114,11 @@ public final class ImaAdsMediaSource implements MediaSource { this.imaSdkSettings = imaSdkSettings; mainHandler = new Handler(Looper.getMainLooper()); adLoaderListener = new AdListener(); - mediaSourceByMediaPeriod = new HashMap<>(); - adBreakMediaSources = new MediaSource[0][]; + adMediaSourceByMediaPeriod = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + isAdAvailable = new boolean[0][]; + adDurationsUs = new long[0][]; } @Override @@ -151,7 +148,7 @@ public final class ImaAdsMediaSource implements MediaSource { throw adLoadError; } contentMediaSource.maybeThrowSourceInfoRefreshError(); - for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { mediaSource.maybeThrowSourceInfoRefreshError(); } @@ -160,49 +157,23 @@ public final class ImaAdsMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int index = id.periodIndex; - if (timeline.isPeriodAd(index)) { - int adBreakIndex = timeline.getAdBreakIndex(index); - int adIndexInAdBreak = timeline.getAdIndexInAdBreak(index); - if (adIndexInAdBreak >= adBreakMediaSources[adBreakIndex].length) { - DeferredMediaPeriod deferredPeriod = new DeferredMediaPeriod(0, allocator); - if (adIndexInAdBreak >= adBreakDeferredMediaPeriods[adBreakIndex].length) { - adBreakDeferredMediaPeriods[adBreakIndex] = Arrays.copyOf( - adBreakDeferredMediaPeriods[adBreakIndex], adIndexInAdBreak + 1); - } - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] = deferredPeriod; - return deferredPeriod; - } - - MediaSource adBreakMediaSource = adBreakMediaSources[adBreakIndex][adIndexInAdBreak]; - MediaPeriod adBreakMediaPeriod = - adBreakMediaSource.createPeriod(new MediaPeriodId(0), allocator); - mediaSourceByMediaPeriod.put(adBreakMediaPeriod, adBreakMediaSource); - return adBreakMediaPeriod; + if (id.isAd()) { + MediaSource mediaSource = adGroupMediaSources[id.adGroupIndex][id.adIndexInAdGroup]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); + adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); + return mediaPeriod; } else { - long startUs = timeline.getContentStartTimeUs(index); - long endUs = timeline.getContentEndTimeUs(index); - MediaPeriod contentMediaPeriod = - contentMediaSource.createPeriod(new MediaPeriodId(0), allocator); - ClippingMediaPeriod clippingPeriod = new ClippingMediaPeriod(contentMediaPeriod, true); - clippingPeriod.setClipping(startUs, endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : endUs); - mediaSourceByMediaPeriod.put(contentMediaPeriod, contentMediaSource); - return clippingPeriod; + return contentMediaSource.createPeriod(id, allocator); } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (mediaPeriod instanceof DeferredMediaPeriod) { - mediaPeriod = ((DeferredMediaPeriod) mediaPeriod).mediaPeriod; - if (mediaPeriod == null) { - // Nothing to do. - return; - } - } else if (mediaPeriod instanceof ClippingMediaPeriod) { - mediaPeriod = ((ClippingMediaPeriod) mediaPeriod).mediaPeriod; + if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { + adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } else { + contentMediaSource.releasePeriod(mediaPeriod); } - mediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); } @Override @@ -210,7 +181,7 @@ public final class ImaAdsMediaSource implements MediaSource { released = true; adLoadError = null; contentMediaSource.releaseSource(); - for (MediaSource[] mediaSources : adBreakMediaSources) { + for (MediaSource[] mediaSources : adGroupMediaSources) { for (MediaSource mediaSource : mediaSources) { mediaSource.releaseSource(); } @@ -229,19 +200,19 @@ public final class ImaAdsMediaSource implements MediaSource { // Internal methods. - private void onAdBreakTimesUsLoaded(long[] adBreakTimesUs) { - Assertions.checkState(this.adBreakTimesUs == null); - this.adBreakTimesUs = adBreakTimesUs; - int adBreakCount = adBreakTimesUs.length; - adBreakAds = new Ad[adBreakCount][]; - Arrays.fill(adBreakAds, new Ad[0]); - adBreakTimelines = new Timeline[adBreakCount][]; - Arrays.fill(adBreakTimelines, new Timeline[0]); - adBreakMediaSources = new MediaSource[adBreakCount][]; - Arrays.fill(adBreakMediaSources, new MediaSource[0]); - adBreakDeferredMediaPeriods = new DeferredMediaPeriod[adBreakCount][]; - Arrays.fill(adBreakDeferredMediaPeriods, new DeferredMediaPeriod[0]); - playedAdBreak = new boolean[adBreakCount]; + private void onAdGroupTimesUsLoaded(long[] adGroupTimesUs) { + Assertions.checkState(this.adGroupTimesUs == null); + int adGroupCount = adGroupTimesUs.length; + this.adGroupTimesUs = adGroupTimesUs; + hasPlayedAdGroup = new boolean[adGroupCount]; + adCounts = new int[adGroupCount]; + Arrays.fill(adCounts, C.LENGTH_UNSET); + adGroupMediaSources = new MediaSource[adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + isAdAvailable = new boolean[adGroupCount][]; + Arrays.fill(isAdAvailable, new boolean[0]); + adDurationsUs = new long[adGroupCount][]; + Arrays.fill(adDurationsUs, new long[0]); maybeUpdateSourceInfo(); } @@ -251,98 +222,51 @@ public final class ImaAdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, Uri uri) { + private void onAdGroupPlayedToEnd(int adGroupIndex) { + hasPlayedAdGroup[adGroupIndex] = true; + maybeUpdateSourceInfo(); + } + + private void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, Uri uri) { MediaSource adMediaSource = new ExtractorMediaSource(uri, dataSourceFactory, new DefaultExtractorsFactory(), mainHandler, adLoaderListener); - if (adBreakMediaSources[adBreakIndex].length <= adIndexInAdBreak) { - int adCount = adIndexInAdBreak + 1; - adBreakMediaSources[adBreakIndex] = Arrays.copyOf(adBreakMediaSources[adBreakIndex], adCount); - adBreakTimelines[adBreakIndex] = Arrays.copyOf(adBreakTimelines[adBreakIndex], adCount); - } - adBreakMediaSources[adBreakIndex][adIndexInAdBreak] = adMediaSource; - if (adIndexInAdBreak < adBreakDeferredMediaPeriods[adBreakIndex].length - && adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak] != null) { - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].setMediaSource( - adBreakMediaSources[adBreakIndex][adIndexInAdBreak]); - mediaSourceByMediaPeriod.put( - adBreakDeferredMediaPeriods[adBreakIndex][adIndexInAdBreak].mediaPeriod, adMediaSource); + int oldAdCount = adGroupMediaSources[adGroupIndex].length; + if (adIndexInAdGroup >= oldAdCount) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + isAdAvailable[adGroupIndex] = Arrays.copyOf(isAdAvailable[adGroupIndex], adCount); + adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); + Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; + isAdAvailable[adGroupIndex][adIndexInAdGroup] = true; adMediaSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - onAdSourceInfoRefreshed(adBreakIndex, adIndexInAdBreak, timeline); + onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); } }); } - private void onAdSourceInfoRefreshed(int adBreakIndex, int adIndexInAdBreak, Timeline timeline) { - adBreakTimelines[adBreakIndex][adIndexInAdBreak] = timeline; + private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); maybeUpdateSourceInfo(); } - private void onAdLoaded(int adBreakIndex, int adIndexInAdBreak, Ad ad) { - if (adBreakAds[adBreakIndex].length <= adIndexInAdBreak) { - int adCount = adIndexInAdBreak + 1; - adBreakAds[adBreakIndex] = Arrays.copyOf(adBreakAds[adBreakIndex], adCount); + private void onAdGroupLoaded(int adGroupIndex, int adCountInAdGroup) { + if (adCounts[adGroupIndex] == C.LENGTH_UNSET) { + adCounts[adGroupIndex] = adCountInAdGroup; + maybeUpdateSourceInfo(); } - adBreakAds[adBreakIndex][adIndexInAdBreak] = ad; - maybeUpdateSourceInfo(); } private void maybeUpdateSourceInfo() { - if (adBreakTimesUs == null || contentTimeline == null) { - // We don't have enough information to start building the timeline yet. - return; + if (adGroupTimesUs != null && contentTimeline != null) { + SinglePeriodAdTimeline timeline = new SinglePeriodAdTimeline(contentTimeline, adGroupTimesUs, + hasPlayedAdGroup, adCounts, isAdAvailable, adDurationsUs); + listener.onSourceInfoRefreshed(timeline, contentManifest); } - - AdTimeline.Builder builder = new AdTimeline.Builder(contentTimeline); - int count = adBreakTimesUs.length; - boolean preroll = adBreakTimesUs[0] == 0; - boolean postroll = adBreakTimesUs[count - 1] == C.TIME_UNSET; - int midrollCount = count - (preroll ? 1 : 0) - (postroll ? 1 : 0); - - int adBreakIndex = 0; - long contentTimeUs = 0; - if (preroll) { - addAdBreak(builder, adBreakIndex++); - } - for (int i = 0; i < midrollCount; i++) { - long startTimeUs = contentTimeUs; - contentTimeUs = adBreakTimesUs[adBreakIndex]; - builder.addContent(startTimeUs, contentTimeUs); - addAdBreak(builder, adBreakIndex++); - } - builder.addContent(contentTimeUs, C.TIME_UNSET); - if (postroll) { - addAdBreak(builder, adBreakIndex); - } - - timeline = builder.build(); - listener.onSourceInfoRefreshed(timeline, contentManifest); - } - - private void addAdBreak(AdTimeline.Builder builder, int adBreakIndex) { - int adCount = adBreakMediaSources[adBreakIndex].length; - AdPodInfo adPodInfo = null; - for (int adIndex = 0; adIndex < adCount; adIndex++) { - Timeline adTimeline = adBreakTimelines[adBreakIndex][adIndex]; - long adDurationUs = adTimeline != null - ? adTimeline.getPeriod(0, new Timeline.Period()).getDurationUs() : C.TIME_UNSET; - Ad ad = adIndex < adBreakAds[adBreakIndex].length - ? adBreakAds[adBreakIndex][adIndex] : null; - builder.addAdPeriod(ad, adBreakIndex, adIndex, adDurationUs); - if (ad != null) { - adPodInfo = ad.getAdPodInfo(); - } - } - if (adPodInfo == null || adPodInfo.getTotalAds() > adCount) { - // We don't know how many ads are in the ad break, or they have not loaded yet. - builder.addAdPeriod(null, adBreakIndex, adCount, C.TIME_UNSET); - } - } - - private void onAdBreakPlayedToEnd(int adBreakIndex) { - playedAdBreak[adBreakIndex] = true; } /** @@ -352,7 +276,7 @@ public final class ImaAdsMediaSource implements MediaSource { ExtractorMediaSource.EventListener { @Override - public void onAdBreakTimesUsLoaded(final long[] adBreakTimesUs) { + public void onAdGroupTimesUsLoaded(final long[] adGroupTimesUs) { if (released) { return; } @@ -362,13 +286,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdBreakTimesUsLoaded(adBreakTimesUs); + ImaAdsMediaSource.this.onAdGroupTimesUsLoaded(adGroupTimesUs); } }); } @Override - public void onUriLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Uri uri) { + public void onAdGroupPlayedToEnd(final int adGroupIndex) { if (released) { return; } @@ -378,13 +302,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdUriLoaded(adBreakIndex, adIndexInAdBreak, uri); + ImaAdsMediaSource.this.onAdGroupPlayedToEnd(adGroupIndex); } }); } @Override - public void onAdLoaded(final int adBreakIndex, final int adIndexInAdBreak, final Ad ad) { + public void onAdUriLoaded(final int adGroupIndex, final int adIndexInAdGroup, final Uri uri) { if (released) { return; } @@ -394,13 +318,13 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdLoaded(adBreakIndex, adIndexInAdBreak, ad); + ImaAdsMediaSource.this.onAdUriLoaded(adGroupIndex, adIndexInAdGroup, uri); } }); } @Override - public void onAdBreakPlayedToEnd(final int adBreakIndex) { + public void onAdGroupLoaded(final int adGroupIndex, final int adCountInAdGroup) { if (released) { return; } @@ -410,7 +334,7 @@ public final class ImaAdsMediaSource implements MediaSource { if (released) { return; } - ImaAdsMediaSource.this.onAdBreakPlayedToEnd(adBreakIndex); + ImaAdsMediaSource.this.onAdGroupLoaded(adGroupIndex, adCountInAdGroup); } }); } @@ -433,99 +357,4 @@ public final class ImaAdsMediaSource implements MediaSource { } - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - private final int index; - private final Allocator allocator; - - public MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private long positionUs; - - public DeferredMediaPeriod(int index, Allocator allocator) { - this.index = index; - this.allocator = allocator; - } - - public void setMediaSource(MediaSource mediaSource) { - mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(index), allocator); - if (callback != null) { - mediaPeriod.prepare(this, positionUs); - } - } - - @Override - public void prepare(Callback callback, long positionUs) { - this.callback = callback; - this.positionUs = positionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, positionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs) { - // Do nothing. - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - // MediaPeriod.Callback implementation. - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - Assertions.checkArgument(this.mediaPeriod == mediaPeriod); - callback.onPrepared(this); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { - Assertions.checkArgument(this.mediaPeriod == mediaPeriod); - callback.onContinueLoadingRequested(this); - } - - } - } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java new file mode 100644 index 0000000000..78d3bb9e73 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -0,0 +1,92 @@ +/* + * 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 com.google.android.exoplayer2.ext.ima; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; + +/** + * A {@link Timeline} for sources that have ads. + */ +public final class SinglePeriodAdTimeline extends Timeline { + + private final Timeline contentTimeline; + private final long[] adGroupTimesUs; + private final boolean[] hasPlayedAdGroup; + private final int[] adCounts; + private final boolean[][] isAdAvailable; + private final long[][] adDurationsUs; + + /** + * Creates a new timeline with a single period containing the specified ads. + * + * @param contentTimeline The timeline of the content alongside which ads will be played. It must + * have one window and one period. + * @param adGroupTimesUs The times of ad groups relative to the start of the period, in + * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that + * the period has a postroll ad. + * @param hasPlayedAdGroup Whether each ad group has been played. + * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} + * if the number of ads is not yet known. + * @param isAdAvailable Whether each ad in each ad group is available. + * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element + * may be {@link C#TIME_UNSET} if the duration is not yet known. + */ + public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, + boolean[] hasPlayedAdGroup, int[] adCounts, boolean[][] isAdAvailable, + long[][] adDurationsUs) { + Assertions.checkState(contentTimeline.getPeriodCount() == 1); + Assertions.checkState(contentTimeline.getWindowCount() == 1); + this.contentTimeline = contentTimeline; + this.adGroupTimesUs = adGroupTimesUs; + this.hasPlayedAdGroup = hasPlayedAdGroup; + this.adCounts = adCounts; + this.isAdAvailable = isAdAvailable; + this.adDurationsUs = adDurationsUs; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + contentTimeline.getPeriod(periodIndex, period, setIds); + period.set(period.id, period.uid, period.windowIndex, period.durationUs, + period.getPositionInWindowUs(), adGroupTimesUs, hasPlayedAdGroup, adCounts, + isAdAvailable, adDurationsUs); + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return contentTimeline.getIndexOfPeriod(uid); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8d137fa71e..7bbb6f9306 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -483,7 +483,7 @@ public final class ExoPlayerTest extends TestCase { public Period getPeriod(int periodIndex, Period period, boolean setIds) { TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); + return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); } @Override diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index 15763ae66d..8b0504253b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -63,7 +63,7 @@ public class TimelineTest extends TestCase { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0, false); + return period.set(new int[] { id, periodIndex }, null, 0, WINDOW_DURATION_US, 0); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 62afbc98a7..e8c47d9811 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -758,24 +758,24 @@ public final class C { /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} and {@link #TIME_END_OF_SOURCE} values. * * @param timeUs The time in microseconds. * @return The corresponding time in milliseconds. */ public static long usToMs(long timeUs) { - return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000); + return (timeUs == TIME_UNSET || timeUs == TIME_END_OF_SOURCE) ? timeUs : (timeUs / 1000); } /** * Converts a time in milliseconds to the corresponding time in microseconds, preserving - * {@link #TIME_UNSET} values. + * {@link #TIME_UNSET} values and {@link #TIME_END_OF_SOURCE} values. * * @param timeMs The time in milliseconds. * @return The corresponding time in microseconds. */ public static long msToUs(long timeMs) { - return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); + return (timeMs == TIME_UNSET || timeMs == TIME_END_OF_SOURCE) ? timeMs : (timeMs * 1000); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index a8f66231c1..19e66f9031 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -264,13 +264,6 @@ public abstract class Timeline { */ public long durationUs; - // TODO: Remove this flag now that in-period ads are supported. - - /** - * Whether this period contains an ad. - */ - public boolean isAd; - private long positionInWindowUs; private long[] adGroupTimesUs; private boolean[] hasPlayedAdGroup; @@ -289,12 +282,11 @@ public abstract class Timeline { * @param positionInWindowUs The position of the start of this period relative to the start of * the window to which it belongs, in milliseconds. May be negative if the start of the * period is not within the window. - * @param isAd Whether this period is an ad. * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd) { - return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null, + long positionInWindowUs) { + return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null, null); } @@ -309,7 +301,6 @@ public abstract class Timeline { * @param positionInWindowUs The position of the start of this period relative to the start of * the window to which it belongs, in milliseconds. May be negative if the start of the * period is not within the window. - * @param isAd Whether this period is an ad. * @param adGroupTimesUs The times of ad groups relative to the start of the period, in * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that * the period has a postroll ad. @@ -322,14 +313,13 @@ public abstract class Timeline { * @return This period, for convenience. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, - int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) { + long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts, + boolean[][] isAdAvailable, long[][] adDurationsUs) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; - this.isAd = isAd; this.adGroupTimesUs = adGroupTimesUs; this.hasPlayedAdGroup = hasPlayedAdGroup; this.adCounts = adCounts; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 447839392e..ae367ef14c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); } @Override diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index f1d5ad96fa..e17f1d26e7 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -653,7 +653,7 @@ public final class DashMediaSource implements MediaSource { + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs, false); + - offsetInFirstPeriodUs); } @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java index 215688083d..44a7687089 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -78,13 +78,13 @@ public interface TimeBar { void setDuration(long duration); /** - * Sets the times of ad breaks. + * Sets the times of ad groups. * - * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of - * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. - * @param adBreakCount The number of ad breaks. + * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of + * ad groups in milliseconds. May be {@code null} if there are no ad groups. + * @param adGroupCount The number of ad groups. */ - void setAdGroupTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + void setAdGroupTimesMs(@Nullable long[] adGroupTimesMs, int adGroupCount); /** * Listener for scrubbing events.