Commit 1f815db3 by andrewlewis Committed by Oliver Woodman

Switch the IMA extension to use in-period ads

This also adds support for seeking in periods with midroll ads.

Remove Timeline.Period.isAd.

Issue: #2617

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=160510702
parent 6509dce6
...@@ -36,9 +36,6 @@ section of the app. ...@@ -36,9 +36,6 @@ section of the app.
This is a preview version with some known issues: 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 * 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 sometimes delayed, meaning that midroll ads take a long time to start and the
ad overlay does not show immediately. ad overlay does not show immediately.
......
/*
* 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<Boolean> isAd;
private final ArrayList<Ad> ads;
private final ArrayList<Long> startTimesUs;
private final ArrayList<Long> endTimesUs;
private final ArrayList<Object> 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;
}
}
/*
* 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);
}
}
...@@ -483,7 +483,7 @@ public final class ExoPlayerTest extends TestCase { ...@@ -483,7 +483,7 @@ public final class ExoPlayerTest extends TestCase {
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
Object id = setIds ? periodIndex : null; 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 @Override
......
...@@ -63,7 +63,7 @@ public class TimelineTest extends TestCase { ...@@ -63,7 +63,7 @@ public class TimelineTest extends TestCase {
@Override @Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { 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 @Override
......
...@@ -758,24 +758,24 @@ public final class C { ...@@ -758,24 +758,24 @@ public final class C {
/** /**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving * 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. * @param timeUs The time in microseconds.
* @return The corresponding time in milliseconds. * @return The corresponding time in milliseconds.
*/ */
public static long usToMs(long timeUs) { 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 * 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. * @param timeMs The time in milliseconds.
* @return The corresponding time in microseconds. * @return The corresponding time in microseconds.
*/ */
public static long msToUs(long timeMs) { 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);
} }
/** /**
......
...@@ -264,13 +264,6 @@ public abstract class Timeline { ...@@ -264,13 +264,6 @@ public abstract class Timeline {
*/ */
public long durationUs; 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 positionInWindowUs;
private long[] adGroupTimesUs; private long[] adGroupTimesUs;
private boolean[] hasPlayedAdGroup; private boolean[] hasPlayedAdGroup;
...@@ -289,12 +282,11 @@ public abstract class Timeline { ...@@ -289,12 +282,11 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of * @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 * the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window. * period is not within the window.
* @param isAd Whether this period is an ad.
* @return This period, for convenience. * @return This period, for convenience.
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd) { long positionInWindowUs) {
return set(id, uid, windowIndex, durationUs, positionInWindowUs, isAd, null, null, null, null, return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null,
null); null);
} }
...@@ -309,7 +301,6 @@ public abstract class Timeline { ...@@ -309,7 +301,6 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of * @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 * the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window. * 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 * @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 * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
* the period has a postroll ad. * the period has a postroll ad.
...@@ -322,14 +313,13 @@ public abstract class Timeline { ...@@ -322,14 +313,13 @@ public abstract class Timeline {
* @return This period, for convenience. * @return This period, for convenience.
*/ */
public Period set(Object id, Object uid, int windowIndex, long durationUs, public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs, boolean isAd, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, long positionInWindowUs, long[] adGroupTimesUs, boolean[] hasPlayedAdGroup, int[] adCounts,
int[] adCounts, boolean[][] isAdAvailable, long[][] adDurationsUs) { boolean[][] isAdAvailable, long[][] adDurationsUs) {
this.id = id; this.id = id;
this.uid = uid; this.uid = uid;
this.windowIndex = windowIndex; this.windowIndex = windowIndex;
this.durationUs = durationUs; this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs; this.positionInWindowUs = positionInWindowUs;
this.isAd = isAd;
this.adGroupTimesUs = adGroupTimesUs; this.adGroupTimesUs = adGroupTimesUs;
this.hasPlayedAdGroup = hasPlayedAdGroup; this.hasPlayedAdGroup = hasPlayedAdGroup;
this.adCounts = adCounts; this.adCounts = adCounts;
......
...@@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline { ...@@ -99,7 +99,7 @@ public final class SinglePeriodTimeline extends Timeline {
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Assertions.checkIndex(periodIndex, 0, 1); Assertions.checkIndex(periodIndex, 0, 1);
Object id = setIds ? ID : null; Object id = setIds ? ID : null;
return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
} }
@Override @Override
......
...@@ -653,7 +653,7 @@ public final class DashMediaSource implements MediaSource { ...@@ -653,7 +653,7 @@ public final class DashMediaSource implements MediaSource {
+ Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
- offsetInFirstPeriodUs, false); - offsetInFirstPeriodUs);
} }
@Override @Override
......
...@@ -78,13 +78,13 @@ public interface TimeBar { ...@@ -78,13 +78,13 @@ public interface TimeBar {
void setDuration(long duration); 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 * @param adGroupTimesMs An array where the first {@code adGroupCount} elements are the times of
* ad breaks in milliseconds. May be {@code null} if there are no ad breaks. * ad groups in milliseconds. May be {@code null} if there are no ad groups.
* @param adBreakCount The number of ad breaks. * @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. * Listener for scrubbing events.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment