diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index d7e22f00ec..7ee06c7c0f 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -406,7 +406,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay private void updateResumePosition() { resumeWindow = player.getCurrentWindowIndex(); - resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition()) + resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getContentPosition()) : C.TIME_UNSET; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 93ffc0dfc6..477577c476 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -536,6 +536,11 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { throw new UnsupportedOperationException(); } + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + @Override public boolean handleMessage(Message msg) { ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 067cb9fa3a..84b4c8bcf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -563,4 +563,11 @@ public interface ExoPlayer { */ int getCurrentAdIndexInAdGroup(); + /** + * If {@link #isPlayingAd()} returns {@code true}, returns the content position that will be + * played once all ads in the ad group have finished playing, in milliseconds. If there is no ad + * playing, the returned position is the same as that returned by {@link #getCurrentPosition()}. + */ + long getContentPosition(); + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 96dd0bd113..f977bbb942 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -367,6 +367,16 @@ import java.util.concurrent.CopyOnWriteArraySet; return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; } + @Override + public long getContentPosition() { + if (isPlayingAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + return period.getPositionInWindowMs() + C.usToMs(playbackInfo.contentPositionUs); + } else { + return getCurrentPosition(); + } + } + @Override public int getRendererCount() { return renderers.length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f6a0bdd08e..7d92a6a85c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -54,6 +54,7 @@ import java.io.IOException; public final MediaPeriodId periodId; public final long startPositionUs; + public final long contentPositionUs; public volatile long positionUs; public volatile long bufferedPositionUs; @@ -63,14 +64,20 @@ import java.io.IOException; } public PlaybackInfo(MediaPeriodId periodId, long startPositionUs) { + this(periodId, startPositionUs, C.TIME_UNSET); + } + + public PlaybackInfo(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { this.periodId = periodId; this.startPositionUs = startPositionUs; + this.contentPositionUs = contentPositionUs; positionUs = startPositionUs; bufferedPositionUs = startPositionUs; } - public PlaybackInfo copyWithPeriodId(MediaPeriodId periodId) { - PlaybackInfo playbackInfo = new PlaybackInfo(periodId.periodIndex, startPositionUs); + public PlaybackInfo copyWithPeriodIndex(int periodIndex) { + PlaybackInfo playbackInfo = new PlaybackInfo(periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, contentPositionUs); playbackInfo.positionUs = positionUs; playbackInfo.bufferedPositionUs = bufferedPositionUs; return playbackInfo; @@ -486,7 +493,7 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(periodId, newPositionUs); + playbackInfo = new PlaybackInfo(periodId, newPositionUs, playbackInfo.contentPositionUs); } } @@ -663,11 +670,11 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; + long contentPositionUs = periodPositionUs; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); if (periodId.isAd()) { seekPositionAdjusted = true; - // TODO: Resume content at periodPositionUs after the ad plays. periodPositionUs = 0; } try { @@ -680,7 +687,7 @@ import java.io.IOException; seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = new PlaybackInfo(periodId, periodPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodPositionUs, contentPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) .sendToTarget(); } @@ -985,16 +992,19 @@ import java.io.IOException; long positionUs = periodPosition.second; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds( - defaultPosition.first, defaultPosition.second); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : defaultPosition.second); + int periodIndex = defaultPosition.first; + long startPositionUs = defaultPosition.second; + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + startPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, + startPositionUs); } } @@ -1047,8 +1057,7 @@ import java.io.IOException; // The current period is in the new timeline. Update the holder and playbackInfo. periodHolder = updatePeriodInfo(periodHolder, periodIndex); if (periodIndex != playbackInfo.periodId.periodIndex) { - playbackInfo = - playbackInfo.copyWithPeriodId(playbackInfo.periodId.copyWithPeriodIndex(periodIndex)); + playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } // If there are subsequent holders, update the index for each of them. If we find a holder @@ -1070,7 +1079,8 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. long newPositionUs = seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs); - playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs); + playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, newPositionUs, + playbackInfo.contentPositionUs); } else { // Update the loading period to be the last period that's still valid, and release all // subsequent periods. @@ -1223,7 +1233,7 @@ import java.io.IOException; playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = new PlaybackInfo(playingPeriodHolder.info.id, - playingPeriodHolder.info.startPositionUs); + playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java index d2b9c2fefe..6050d6faf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -47,6 +47,11 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period. */ public final long endPositionUs; + /** + * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET} + * otherwise. + */ + public final long contentPositionUs; /** * The duration of the media to play within the media period, in microseconds, or * {@link C#TIME_UNSET} if not known. @@ -64,10 +69,11 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public final boolean isFinal; private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs, - long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { + long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { this.id = id; this.startPositionUs = startPositionUs; this.endPositionUs = endPositionUs; + this.contentPositionUs = contentPositionUs; this.durationUs = durationUs; this.isLastInTimelinePeriod = isLastInTimelinePeriod; this.isFinal = isFinal; @@ -79,14 +85,14 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; */ public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) { return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs, - endPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); } /** * Returns a copy of this instance with the start position set to the specified value. */ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { - return new MediaPeriodInfo(id, startPositionUs, endPositionUs, durationUs, + return new MediaPeriodInfo(id, startPositionUs, endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); } @@ -127,7 +133,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. */ public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) { - return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.startPositionUs); + return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.contentPositionUs, + playbackInfo.startPositionUs); } /** @@ -173,8 +180,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; } else { startPositionUs = 0; } - return getMediaPeriodInfo(resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs), - startPositionUs); + MediaPeriodId periodId = resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs); + return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); } MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; @@ -190,18 +197,23 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // Play the next ad in the ad group if it's available. return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex, - nextAdIndexInAdGroup); + nextAdIndexInAdGroup, currentMediaPeriodInfo.contentPositionUs); } else { // Play content from the ad group position. - return getMediaPeriodInfo(new MediaPeriodId(currentPeriodId.periodIndex), - period.getAdGroupTimeUs(currentAdGroupIndex)); + int nextAdGroupIndex = + period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs); + long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); + return getMediaPeriodInfoForContent(currentPeriodId.periodIndex, + currentMediaPeriodInfo.contentPositionUs, endUs); } } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { // Play the next ad group if it's available. int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); return !period.isAdAvailable(nextAdGroupIndex, 0) ? null - : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0); + : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0, + currentMediaPeriodInfo.endPositionUs); } else { // Check if the postroll ad should be played. int adGroupCount = period.getAdGroupCount(); @@ -211,7 +223,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; || !period.isAdAvailable(adGroupCount - 1, 0)) { return null; } - return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0); + return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0, + currentMediaPeriodInfo.endPositionUs); } } @@ -223,8 +236,12 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) { timeline.getPeriod(periodIndex, period); int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); - return adGroupIndex == C.INDEX_UNSET ? new MediaPeriodId(periodIndex) - : new MediaPeriodId(periodIndex, adGroupIndex, 0); + if (adGroupIndex == C.INDEX_UNSET) { + return new MediaPeriodId(periodIndex); + } else { + int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex); + return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + } } /** @@ -255,17 +272,19 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; long durationUs = newId.isAd() ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup) : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs); - return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, durationUs, isLastInPeriod, - isLastInTimeline); + return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, info.contentPositionUs, + durationUs, isLastInPeriod, isLastInTimeline); } - private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long startPositionUs) { + private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long contentPositionUs, + long startPositionUs) { timeline.getPeriod(id.periodIndex, period); if (id.isAd()) { if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) { return null; } - return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup); + return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, + contentPositionUs); } else { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE @@ -275,14 +294,14 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; } private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex, - int adIndexInAdGroup) { + int adIndexInAdGroup, long contentPositionUs) { MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = timeline.getPeriod(id.periodIndex, period) .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); - return new MediaPeriodInfo(id, 0, C.TIME_END_OF_SOURCE, durationUs, isLastInPeriod, - isLastInTimeline); + return new MediaPeriodInfo(id, 0, C.TIME_END_OF_SOURCE, contentPositionUs, durationUs, + isLastInPeriod, isLastInTimeline); } private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs, @@ -292,7 +311,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); timeline.getPeriod(id.periodIndex, period); long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; - return new MediaPeriodInfo(id, startPositionUs, endUs, durationUs, isLastInPeriod, + return new MediaPeriodInfo(id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 054d3e38b9..9d163007fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -715,6 +715,11 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getCurrentAdIndexInAdGroup(); } + @Override + public long getContentPosition() { + return player.getContentPosition(); + } + // Internal methods. private void removeSurfaceCallbacks() { 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 de66b5775b..3514aa3bbd 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 @@ -378,6 +378,16 @@ public abstract class Timeline { return adGroupTimesUs[adGroupIndex]; } + /** + * Returns the number of ads that have been played in the specified ad group in the period. + * + * @param adGroupIndex The ad group index. + * @return The number of ads that have been played. + */ + public int getPlayedAdCount(int adGroupIndex) { + return adsPlayedCounts[adGroupIndex]; + } + /** * Returns whether the ad group at index {@code adGroupIndex} has been played. *