Commit dc4148d5 by bachinger Committed by Oliver Woodman

Add positions and new reasons to onPositionDiscontinuity

PiperOrigin-RevId: 364861539
parent f19ab4aa
Showing with 1586 additions and 192 deletions
......@@ -2,6 +2,17 @@
### dev-v2 (not yet released)
* Core Library:
* Add position info of the old and the new position as arguments to
`EventListener.onPositionDiscontinuity`. Add the new reasons
`DISCONTINUITY_REASON_SKIP` and `DISCONTINUITY_REASON_REMOVE` and rename
`DISCONTINUITY_REASON_PERIOD_TRANSITION` to
`DISCONTINUITY_REASON_AUTO_TRANSITION`. Remove
`DISCONTINUITY_REASON_AD_INSERTION` for which
`DISCONTINUITY_REASON_AUTO_TRANSITION` is used instead. Deprecate the
`onPositionDiscontinuity(int)` callback
([#6163](https://github.com/google/ExoPlayer/issues/6163),
[#4768](https://github.com/google/ExoPlayer/issues/4768)).
* UI:
* Add builder for `PlayerNotificationManager`.
* Add group setting to `PlayerNotificationManager`.
......
......@@ -460,6 +460,7 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
// TODO(b/181262841): call new onPositionDiscontinuity callback
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
......@@ -630,6 +631,8 @@ public final class CastPlayer extends BasePlayer {
// Internal methods.
// Call deprecated callbacks.
@SuppressWarnings("deprecation")
private void updateInternalStateAndNotifyIfChanged() {
if (remoteMediaClient == null) {
// There is no session. We leave the state of the player as it is now.
......@@ -648,9 +651,10 @@ public final class CastPlayer extends BasePlayer {
int currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
// TODO(b/181262841): call new onPositionDiscontinuity callback
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION));
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AUTO_TRANSITION));
listeners.queueEvent(
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener ->
......
......@@ -468,7 +468,10 @@ import java.util.Map;
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
handleTimelineOrPositionChanged();
}
......
......@@ -613,7 +613,10 @@ public final class ImaAdsLoader implements Player.EventListener, AdsLoader {
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
maybeUpdateCurrentAdTagLoader();
maybePreloadNextPeriodAds();
}
......
......@@ -29,6 +29,8 @@ import com.google.android.exoplayer2.util.ListenerSet;
private final ListenerSet<EventListener> listeners;
private final Timeline.Period period;
private final Object windowUid = new Object();
private final Object periodUid = new Object();
private Timeline timeline;
@Player.State private int state;
......@@ -65,6 +67,16 @@ import com.google.android.exoplayer2.util.ListenerSet;
*/
public void setPlayingContentPosition(int periodIndex, long positionMs) {
boolean notify = isPlayingAd;
PositionInfo oldPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
periodUid,
/* periodIndex= */ 0,
this.positionMs,
this.contentPositionMs,
this.adGroupIndex,
this.adIndexInAdGroup);
isPlayingAd = false;
adGroupIndex = C.INDEX_UNSET;
adIndexInAdGroup = C.INDEX_UNSET;
......@@ -72,9 +84,21 @@ import com.google.android.exoplayer2.util.ListenerSet;
this.positionMs = positionMs;
contentPositionMs = positionMs;
if (notify) {
PositionInfo newPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
periodUid,
/* periodIndex= */ 0,
positionMs,
this.contentPositionMs,
this.adGroupIndex,
this.adIndexInAdGroup);
listeners.sendEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
listener ->
listener.onPositionDiscontinuity(
oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
}
}
......@@ -90,6 +114,16 @@ import com.google.android.exoplayer2.util.ListenerSet;
long positionMs,
long contentPositionMs) {
boolean notify = !isPlayingAd || this.adIndexInAdGroup != adIndexInAdGroup;
PositionInfo oldPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
periodUid,
/* periodIndex= */ 0,
this.positionMs,
this.contentPositionMs,
this.adGroupIndex,
this.adIndexInAdGroup);
isPlayingAd = true;
this.periodIndex = periodIndex;
this.adGroupIndex = adGroupIndex;
......@@ -97,9 +131,21 @@ import com.google.android.exoplayer2.util.ListenerSet;
this.positionMs = positionMs;
this.contentPositionMs = contentPositionMs;
if (notify) {
PositionInfo newPosition =
new PositionInfo(
windowUid,
/* windowIndex= */ 0,
periodUid,
/* periodIndex= */ 0,
positionMs,
contentPositionMs,
adGroupIndex,
adIndexInAdGroup);
listeners.sendEvent(
EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_AD_INSERTION));
listener ->
listener.onPositionDiscontinuity(
oldPosition, newPosition, DISCONTINUITY_REASON_AUTO_TRANSITION));
}
}
......
......@@ -281,7 +281,26 @@ public final class ImaAdsLoaderTest {
videoAdPlayer.pauseAd(TEST_AD_MEDIA_INFO);
videoAdPlayer.stopAd(TEST_AD_MEDIA_INFO);
imaAdsLoader.onPlayerError(ExoPlaybackException.createForSource(new IOException()));
imaAdsLoader.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK);
imaAdsLoader.onPositionDiscontinuity(
new Player.PositionInfo(
/* windowUid= */ new Object(),
/* windowIndex= */ 0,
/* periodUid= */ new Object(),
/* periodIndex= */ 0,
/* positionMs= */ 10_000,
/* contentPositionMs= */ 0,
/* adGroupIndex= */ -1,
/* adIndexInAdGroup= */ -1),
new Player.PositionInfo(
/* windowUid= */ new Object(),
/* windowIndex= */ 1,
/* periodUid= */ new Object(),
/* periodIndex= */ 0,
/* positionMs= */ 20_000,
/* contentPositionMs= */ 0,
/* adGroupIndex= */ -1,
/* adIndexInAdGroup= */ -1),
Player.DISCONTINUITY_REASON_SEEK);
adEventListener.onAdEvent(getAdEvent(AdEventType.CONTENT_RESUME_REQUESTED, /* ad= */ null));
imaAdsLoader.handlePrepareError(
adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, new IOException());
......
......@@ -306,7 +306,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@DiscontinuityReason int reason) {
Callback callback = getCallback();
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
......
......@@ -437,7 +437,7 @@ import java.util.List;
case Player.STATE_READY:
if (!prepared) {
prepared = true;
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
listener.onPrepared(
Assertions.checkNotNull(getCurrentMediaItem()), player.getBufferedPercentage());
}
......@@ -517,9 +517,11 @@ import java.util.List;
int currentWindowIndex = getCurrentMediaItemIndex();
if (this.currentWindowIndex != currentWindowIndex) {
this.currentWindowIndex = currentWindowIndex;
androidx.media2.common.MediaItem currentMediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
listener.onCurrentMediaItemChanged(currentMediaItem);
if (currentWindowIndex != C.INDEX_UNSET) {
androidx.media2.common.MediaItem currentMediaItem =
Assertions.checkNotNull(getCurrentMediaItem());
listener.onCurrentMediaItemChanged(currentMediaItem);
}
} else {
listener.onSeekCompleted();
}
......@@ -597,7 +599,10 @@ import java.util.List;
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
handlePositionDiscontinuity(reason);
}
......
......@@ -15,7 +15,6 @@
*/
package com.google.android.exoplayer2;
import android.content.Context;
import android.os.Looper;
import android.view.Surface;
......@@ -40,6 +39,7 @@ import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.exoplayer2.video.spherical.CameraMotionListener;
import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -396,10 +396,9 @@ public interface Player {
/**
* Called when the timeline has been refreshed.
*
* <p>Note that if the timeline has changed then a position discontinuity may also have
* occurred. For example, the current period index may have changed as a result of periods being
* added or removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity(int)}.
* <p>Note that the current window or period index may change as a result of a timeline change.
* If playback can't continue smoothly because of this timeline change, a separate {@link
* #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} callback will be triggered.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
......@@ -576,21 +575,27 @@ public interface Player {
default void onPlayerError(ExoPlaybackException error) {}
/**
* Called when a position discontinuity occurs without a change to the timeline. A position
* discontinuity occurs when the current window or period index changes (as a result of playback
* transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
* @deprecated Use {@link #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} instead.
*/
@Deprecated
default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
/**
* Called when a position discontinuity occurs.
*
* <p>When a position discontinuity occurs as a result of a change to the timeline this method
* is <em>not</em> called. {@link #onTimelineChanged(Timeline, int)} is called in this case.
* <p>A position discontinuity occurs when the playing period changes, the playback position
* jumps within the period currently being played, or when the playing period has been skipped
* or removed.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param oldPosition The position before the discontinuity.
* @param newPosition The position after the discontinuity.
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
default void onPositionDiscontinuity(@DiscontinuityReason int reason) {}
default void onPositionDiscontinuity(
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {}
/**
* Called when the current playback parameters change. The playback parameters may change due to
......@@ -607,7 +612,8 @@ public interface Player {
/**
* @deprecated Seeks are processed without delay. Listen to {@link
* #onPositionDiscontinuity(int)} with reason {@link #DISCONTINUITY_REASON_SEEK} instead.
* #onPositionDiscontinuity(PositionInfo, PositionInfo, int)} with reason {@link
* #DISCONTINUITY_REASON_SEEK} instead.
*/
@Deprecated
default void onSeekProcessed() {}
......@@ -698,6 +704,94 @@ public interface Player {
}
}
/** Position info describing a playback position involved in a discontinuity. */
final class PositionInfo {
/**
* The UID of the window, or {@code null}, if the timeline is {@link Timeline#isEmpty() empty}.
*/
@Nullable public final Object windowUid;
/** The window index. */
public final int windowIndex;
/**
* The UID of the period, or {@code null}, if the timeline is {@link Timeline#isEmpty() empty}.
*/
@Nullable public final Object periodUid;
/** The period index. */
public final int periodIndex;
/** The playback position, in milliseconds. */
public final long positionMs;
/**
* The content position, in milliseconds.
*
* <p>If {@link #adGroupIndex} is {@link C#INDEX_UNSET}, this is the same as {@link
* #positionMs}.
*/
public final long contentPositionMs;
/**
* The ad group index if the playback position is within an ad, {@link C#INDEX_UNSET} otherwise.
*/
public final int adGroupIndex;
/**
* The index of the ad within the ad group if the playback position is within an ad, {@link
* C#INDEX_UNSET} otherwise.
*/
public final int adIndexInAdGroup;
/** Creates an instance. */
public PositionInfo(
@Nullable Object windowUid,
int windowIndex,
@Nullable Object periodUid,
int periodIndex,
long positionMs,
long contentPositionMs,
int adGroupIndex,
int adIndexInAdGroup) {
this.windowUid = windowUid;
this.windowIndex = windowIndex;
this.periodUid = periodUid;
this.periodIndex = periodIndex;
this.positionMs = positionMs;
this.contentPositionMs = contentPositionMs;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PositionInfo that = (PositionInfo) o;
return windowIndex == that.windowIndex
&& periodIndex == that.periodIndex
&& positionMs == that.positionMs
&& contentPositionMs == that.contentPositionMs
&& adGroupIndex == that.adGroupIndex
&& adIndexInAdGroup == that.adIndexInAdGroup
&& Objects.equal(windowUid, that.windowUid)
&& Objects.equal(periodUid, that.periodUid);
}
@Override
public int hashCode() {
return Objects.hashCode(
windowUid,
windowIndex,
periodUid,
periodIndex,
windowIndex,
positionMs,
contentPositionMs,
adGroupIndex,
adIndexInAdGroup);
}
}
/**
* A set of {@link Command commands}.
*
......@@ -933,25 +1027,30 @@ public interface Player {
int REPEAT_MODE_ALL = 2;
/**
* Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_PERIOD_TRANSITION},
* Reasons for position discontinuities. One of {@link #DISCONTINUITY_REASON_AUTO_TRANSITION},
* {@link #DISCONTINUITY_REASON_SEEK}, {@link #DISCONTINUITY_REASON_SEEK_ADJUSTMENT}, {@link
* #DISCONTINUITY_REASON_AD_INSERTION} or {@link #DISCONTINUITY_REASON_INTERNAL}.
* #DISCONTINUITY_REASON_SKIP}, {@link #DISCONTINUITY_REASON_REMOVE} or {@link
* #DISCONTINUITY_REASON_INTERNAL}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DISCONTINUITY_REASON_PERIOD_TRANSITION,
DISCONTINUITY_REASON_AUTO_TRANSITION,
DISCONTINUITY_REASON_SEEK,
DISCONTINUITY_REASON_SEEK_ADJUSTMENT,
DISCONTINUITY_REASON_AD_INSERTION,
DISCONTINUITY_REASON_SKIP,
DISCONTINUITY_REASON_REMOVE,
DISCONTINUITY_REASON_INTERNAL
})
@interface DiscontinuityReason {}
/**
* Automatic playback transition from one period in the timeline to the next. The period index may
* be the same as it was before the discontinuity in case the current period is repeated.
*
* <p>This reason also indicates an automatic transition from the content period to an inserted ad
* period or vice versa.
*/
int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0;
int DISCONTINUITY_REASON_AUTO_TRANSITION = 0;
/** Seek within the current period or to another period. */
int DISCONTINUITY_REASON_SEEK = 1;
/**
......@@ -959,10 +1058,12 @@ public interface Player {
* permitted to be inexact.
*/
int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2;
/** Discontinuity to or from an ad within one period in the timeline. */
int DISCONTINUITY_REASON_AD_INSERTION = 3;
/** Discontinuity introduced by a skipped period (for instance a skipped ad). */
int DISCONTINUITY_REASON_SKIP = 3;
/** Discontinuity caused by the removal of the current period from the {@link Timeline}. */
int DISCONTINUITY_REASON_REMOVE = 4;
/** Discontinuity introduced internally by the source. */
int DISCONTINUITY_REASON_INTERNAL = 4;
int DISCONTINUITY_REASON_INTERNAL = 5;
/**
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link
......@@ -1053,7 +1154,10 @@ public interface Player {
int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = 10;
/** {@link #getPlayerError()} changed. */
int EVENT_PLAYER_ERROR = 11;
/** A position discontinuity occurred. See {@link EventListener#onPositionDiscontinuity(int)}. */
/**
* A position discontinuity occurred. See {@link
* EventListener#onPositionDiscontinuity(PositionInfo, PositionInfo, int)}.
*/
int EVENT_POSITION_DISCONTINUITY = 12;
/** {@link #getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13;
......
......@@ -87,8 +87,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
@RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
private int pendingOperationAcks;
private boolean hasPendingDiscontinuity;
@DiscontinuityReason private int pendingDiscontinuityReason;
private boolean pendingDiscontinuity;
@PlayWhenReadyChangeReason private int pendingPlayWhenReadyChangeReason;
private boolean foregroundMode;
private SeekParameters seekParameters;
......@@ -367,11 +367,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.prepare();
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
/**
......@@ -479,24 +481,30 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.addMediaSources(index, holders, shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
toIndex = min(toIndex, mediaSourceHolderSnapshots.size());
PlaybackInfo playbackInfo = removeMediaItemsInternal(fromIndex, toIndex);
PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex);
boolean positionDiscontinuity =
!newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid);
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ Player.DISCONTINUITY_REASON_INTERNAL,
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
positionDiscontinuity,
Player.DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -519,11 +527,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -540,11 +550,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.setShuffleOrder(shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -583,11 +595,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
playWhenReadyChangeReason,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -656,7 +670,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : Player.STATE_BUFFERING;
PlaybackInfo newPlaybackInfo = this.playbackInfo.copyWithPlaybackState(newPlaybackState);
int oldMaskingWindowIndex = getCurrentWindowIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
......@@ -665,11 +680,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ true);
/* seekProcessed= */ true,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingWindowIndex);
}
@Override
......@@ -685,11 +702,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer.setPlaybackParameters(playbackParameters);
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -758,13 +777,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
pendingOperationAcks++;
internalPlayer.stop();
boolean positionDiscontinuity =
playbackInfo.timeline.isEmpty() && !this.playbackInfo.timeline.isEmpty();
updatePlaybackInfo(
playbackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
positionDiscontinuity,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(playbackInfo),
/* ignored */ C.INDEX_UNSET);
}
@Override
......@@ -839,13 +862,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public long getCurrentPosition() {
if (playbackInfo.timeline.isEmpty()) {
return maskingWindowPositionMs;
} else if (playbackInfo.periodId.isAd()) {
return C.usToMs(playbackInfo.positionUs);
} else {
return periodPositionUsToWindowPositionMs(playbackInfo.periodId, playbackInfo.positionUs);
}
return C.usToMs(getCurrentPositionUsInternal(playbackInfo));
}
@Override
......@@ -909,8 +926,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
contentBufferedPositionUs = loadingPeriod.durationUs;
}
}
return periodPositionUsToWindowPositionMs(
playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs);
return C.usToMs(
periodPositionUsToWindowPositionUs(
playbackInfo.timeline, playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs));
}
@Override
......@@ -958,6 +976,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
private long getCurrentPositionUsInternal(PlaybackInfo playbackInfo) {
if (playbackInfo.timeline.isEmpty()) {
return C.msToUs(maskingWindowPositionMs);
} else if (playbackInfo.periodId.isAd()) {
return playbackInfo.positionUs;
} else {
return periodPositionUsToWindowPositionUs(
playbackInfo.timeline, playbackInfo.periodId, playbackInfo.positionUs);
}
}
private List<MediaSource> createMediaSources(List<MediaItem> mediaItems) {
List<MediaSource> mediaSources = new ArrayList<>();
for (int i = 0; i < mediaItems.size(); i++) {
......@@ -969,8 +998,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
private void handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate) {
pendingOperationAcks -= playbackInfoUpdate.operationAcks;
if (playbackInfoUpdate.positionDiscontinuity) {
hasPendingDiscontinuity = true;
pendingDiscontinuityReason = playbackInfoUpdate.discontinuityReason;
pendingDiscontinuity = true;
}
if (playbackInfoUpdate.hasPlayWhenReadyChangeReason) {
pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason;
......@@ -991,15 +1020,33 @@ import java.util.concurrent.CopyOnWriteArraySet;
mediaSourceHolderSnapshots.get(i).timeline = timelines.get(i);
}
}
boolean positionDiscontinuity = hasPendingDiscontinuity;
hasPendingDiscontinuity = false;
boolean positionDiscontinuity = false;
long discontinuityWindowStartPositionUs = C.TIME_UNSET;
if (pendingDiscontinuity) {
positionDiscontinuity =
!playbackInfoUpdate.playbackInfo.periodId.equals(playbackInfo.periodId)
|| playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs
!= playbackInfo.positionUs;
if (positionDiscontinuity) {
discontinuityWindowStartPositionUs =
newTimeline.isEmpty() || playbackInfoUpdate.playbackInfo.periodId.isAd()
? playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs
: periodPositionUsToWindowPositionUs(
newTimeline,
playbackInfoUpdate.playbackInfo.periodId,
playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs);
}
}
pendingDiscontinuity = false;
updatePlaybackInfo(
playbackInfoUpdate.playbackInfo,
positionDiscontinuity,
pendingDiscontinuityReason,
TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
pendingPlayWhenReadyChangeReason,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
positionDiscontinuity,
pendingDiscontinuityReason,
discontinuityWindowStartPositionUs,
/* ignored */ C.INDEX_UNSET);
}
}
......@@ -1007,11 +1054,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
@SuppressWarnings("deprecation")
private void updatePlaybackInfo(
PlaybackInfo playbackInfo,
boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason,
@TimelineChangeReason int timelineChangeReason,
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
boolean seekProcessed) {
boolean seekProcessed,
boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason,
long discontinuityWindowStartPositionUs,
int oldMaskingWindowIndex) {
// Assign playback info immediately such that all getters return the right values, but keep
// snapshot of previous and new state so that listener invocations are triggered correctly.
PlaybackInfo previousPlaybackInfo = this.playbackInfo;
......@@ -1042,9 +1092,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
});
}
if (positionDiscontinuity) {
PositionInfo previousPositionInfo =
getPreviousPositionInfo(
positionDiscontinuityReason, previousPlaybackInfo, oldMaskingWindowIndex);
PositionInfo positionInfo = getPositionInfo(discontinuityWindowStartPositionUs);
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
listener -> {
listener.onPositionDiscontinuity(positionDiscontinuityReason);
listener.onPositionDiscontinuity(
previousPositionInfo, positionInfo, positionDiscontinuityReason);
});
}
if (mediaItemTransitioned) {
@Nullable final MediaItem mediaItem;
......@@ -1144,6 +1202,93 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
private PositionInfo getPreviousPositionInfo(
@DiscontinuityReason int positionDiscontinuityReason,
PlaybackInfo oldPlaybackInfo,
int oldMaskingWindowIndex) {
@Nullable Object oldWindowUid = null;
@Nullable Object oldPeriodUid = null;
int oldWindowIndex = oldMaskingWindowIndex;
int oldPeriodIndex = C.INDEX_UNSET;
Timeline.Period oldPeriod = new Timeline.Period();
if (!oldPlaybackInfo.timeline.isEmpty()) {
oldPeriodUid = oldPlaybackInfo.periodId.periodUid;
oldPlaybackInfo.timeline.getPeriodByUid(oldPeriodUid, oldPeriod);
oldWindowIndex = oldPeriod.windowIndex;
oldPeriodIndex = oldPlaybackInfo.timeline.getIndexOfPeriod(oldPeriodUid);
oldWindowUid = oldPlaybackInfo.timeline.getWindow(oldWindowIndex, window).uid;
}
long oldPositionUs;
long oldContentPositionUs;
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
oldPositionUs = oldPeriod.positionInWindowUs + oldPeriod.durationUs;
oldContentPositionUs = oldPositionUs;
if (oldPlaybackInfo.periodId.isAd()) {
// The old position is the end of the previous ad.
oldPositionUs =
oldPeriod.getAdDurationUs(
oldPlaybackInfo.periodId.adGroupIndex, oldPlaybackInfo.periodId.adIndexInAdGroup);
// The ad cue point is stored in the old requested content position.
oldContentPositionUs = getRequestedContentPositionUs(oldPlaybackInfo);
} else if (oldPlaybackInfo.periodId.nextAdGroupIndex != C.INDEX_UNSET
&& playbackInfo.periodId.isAd()) {
// If it's a transition from content to an ad in the same window, the old position is the
// ad cue point that is the same as current content position.
oldPositionUs = getRequestedContentPositionUs(playbackInfo);
oldContentPositionUs = oldPositionUs;
}
} else if (oldPlaybackInfo.periodId.isAd()) {
oldPositionUs = oldPlaybackInfo.positionUs;
oldContentPositionUs = getRequestedContentPositionUs(oldPlaybackInfo);
} else {
oldPositionUs = oldPeriod.positionInWindowUs + oldPlaybackInfo.positionUs;
oldContentPositionUs = oldPositionUs;
}
return new PositionInfo(
oldWindowUid,
oldWindowIndex,
oldPeriodUid,
oldPeriodIndex,
C.usToMs(oldPositionUs),
C.usToMs(oldContentPositionUs),
oldPlaybackInfo.periodId.adGroupIndex,
oldPlaybackInfo.periodId.adIndexInAdGroup);
}
private PositionInfo getPositionInfo(long discontinuityWindowStartPositionUs) {
@Nullable Object newWindowUid = null;
@Nullable Object newPeriodUid = null;
int newWindowIndex = getCurrentWindowIndex();
int newPeriodIndex = C.INDEX_UNSET;
if (!playbackInfo.timeline.isEmpty()) {
newPeriodUid = playbackInfo.periodId.periodUid;
playbackInfo.timeline.getPeriodByUid(newPeriodUid, period);
newPeriodIndex = playbackInfo.timeline.getIndexOfPeriod(newPeriodUid);
newWindowUid = playbackInfo.timeline.getWindow(newWindowIndex, window).uid;
}
long positionMs = C.usToMs(discontinuityWindowStartPositionUs);
return new PositionInfo(
newWindowUid,
newWindowIndex,
newPeriodUid,
newPeriodIndex,
positionMs,
/* contentPositionMs= */ playbackInfo.periodId.isAd()
? C.usToMs(getRequestedContentPositionUs(playbackInfo))
: positionMs,
playbackInfo.periodId.adGroupIndex,
playbackInfo.periodId.adIndexInAdGroup);
}
private static long getRequestedContentPositionUs(PlaybackInfo playbackInfo) {
Timeline.Window window = new Timeline.Window();
Timeline.Period period = new Timeline.Period();
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
return playbackInfo.requestedContentPositionUs == C.TIME_UNSET
? playbackInfo.timeline.getWindow(period.windowIndex, window).getDefaultPositionUs()
: period.getPositionInWindowUs() + playbackInfo.requestedContentPositionUs;
}
private Pair<Boolean, Integer> evaluateMediaItemTransitionReason(
PlaybackInfo playbackInfo,
PlaybackInfo oldPlaybackInfo,
......@@ -1169,7 +1314,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (!oldWindowUid.equals(newWindowUid)) {
@Player.MediaItemTransitionReason int transitionReason;
if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_PERIOD_TRANSITION) {
&& positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_AUTO;
} else if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK) {
......@@ -1182,7 +1327,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
return new Pair<>(/* isTransitioning */ true, transitionReason);
} else if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_PERIOD_TRANSITION
&& positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION
&& newTimeline.getIndexOfPeriod(playbackInfo.periodId.periodUid)
== firstPeriodIndexInNewWindow) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_REPEAT);
......@@ -1245,13 +1390,18 @@ import java.util.concurrent.CopyOnWriteArraySet;
newPlaybackInfo = newPlaybackInfo.copyWithPlaybackState(maskingPlaybackState);
internalPlayer.setMediaSources(
holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder);
boolean positionDiscontinuity =
!playbackInfo.periodId.periodUid.equals(newPlaybackInfo.periodId.periodUid)
&& !playbackInfo.timeline.isEmpty();
updatePlaybackInfo(
newPlaybackInfo,
/* positionDiscontinuity= */ false,
/* ignored */ Player.DISCONTINUITY_REASON_INTERNAL,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ false);
/* seekProcessed= */ false,
/* positionDiscontinuity= */ positionDiscontinuity,
Player.DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET);
}
private List<MediaSourceList.MediaSourceHolder> addMediaSourceHolders(
......@@ -1319,11 +1469,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (timeline.isEmpty()) {
// Reset periodId and loadingPeriodId.
MediaPeriodId dummyMediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline();
long positionUs = C.msToUs(maskingWindowPositionMs);
playbackInfo =
playbackInfo.copyWithNewPosition(
dummyMediaPeriodId,
/* positionUs= */ C.msToUs(maskingWindowPositionMs),
/* requestedContentPositionUs= */ C.msToUs(maskingWindowPositionMs),
positionUs,
/* requestedContentPositionUs= */ positionUs,
/* discontinuityStartPositionUs= */ positionUs,
/* totalBufferedDurationUs= */ 0,
TrackGroupArray.EMPTY,
emptyTrackSelectorResult,
......@@ -1352,6 +1504,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
newPeriodId,
/* positionUs= */ newContentPositionUs,
/* requestedContentPositionUs= */ newContentPositionUs,
/* discontinuityStartPositionUs= */ newContentPositionUs,
/* totalBufferedDurationUs= */ 0,
playingPeriodChanged ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
playingPeriodChanged ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
......@@ -1377,6 +1530,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
newPeriodId,
/* positionUs= */ playbackInfo.positionUs,
/* requestedContentPositionUs= */ playbackInfo.positionUs,
playbackInfo.discontinuityStartPositionUs,
/* totalBufferedDurationUs= */ maskedBufferedPositionUs - playbackInfo.positionUs,
playbackInfo.trackGroups,
playbackInfo.trackSelectorResult,
......@@ -1400,6 +1554,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
newPeriodId,
/* positionUs= */ newContentPositionUs,
/* requestedContentPositionUs= */ newContentPositionUs,
/* discontinuityStartPositionUs= */ newContentPositionUs,
maskedTotalBufferedDurationUs,
playbackInfo.trackGroups,
playbackInfo.trackSelectorResult,
......@@ -1468,11 +1623,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
return timeline.getPeriodPosition(window, period, windowIndex, C.msToUs(windowPositionMs));
}
private long periodPositionUsToWindowPositionMs(MediaPeriodId periodId, long positionUs) {
long positionMs = C.usToMs(positionUs);
playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
positionMs += period.getPositionInWindowMs();
return positionMs;
private long periodPositionUsToWindowPositionUs(
Timeline timeline, MediaPeriodId periodId, long positionUs) {
timeline.getPeriodByUid(periodId.periodUid, period);
positionUs += period.getPositionInWindowUs();
return positionUs;
}
private static boolean isPlaying(PlaybackInfo playbackInfo) {
......
......@@ -668,7 +668,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
mediaSourceList.setMediaSources(
mediaSourceListUpdateMessage.mediaSourceHolders,
mediaSourceListUpdateMessage.shuffleOrder);
handleMediaSourceListInfoRefreshed(timeline);
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void addMediaItemsInternal(MediaSourceListUpdateMessage addMessage, int insertionIndex)
......@@ -679,7 +679,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
insertionIndex == C.INDEX_UNSET ? mediaSourceList.getSize() : insertionIndex,
addMessage.mediaSourceHolders,
addMessage.shuffleOrder);
handleMediaSourceListInfoRefreshed(timeline);
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)
......@@ -691,24 +691,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
moveMediaItemsMessage.toIndex,
moveMediaItemsMessage.newFromIndex,
moveMediaItemsMessage.shuffleOrder);
handleMediaSourceListInfoRefreshed(timeline);
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)
throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline = mediaSourceList.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder);
handleMediaSourceListInfoRefreshed(timeline);
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void mediaSourceListUpdateRequestedInternal() throws ExoPlaybackException {
handleMediaSourceListInfoRefreshed(mediaSourceList.createTimeline());
handleMediaSourceListInfoRefreshed(
mediaSourceList.createTimeline(), /* isSourceRefresh= */ true);
}
private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException {
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
Timeline timeline = mediaSourceList.setShuffleOrder(shuffleOrder);
handleMediaSourceListInfoRefreshed(timeline);
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
}
private void notifyTrackSelectionPlayWhenReadyChanged(boolean playWhenReady) {
......@@ -803,10 +804,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (newPositionUs != playbackInfo.positionUs) {
playbackInfo =
handlePositionDiscontinuity(
periodId, newPositionUs, playbackInfo.requestedContentPositionUs);
if (sendDiscontinuity) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
periodId,
newPositionUs,
playbackInfo.requestedContentPositionUs,
playbackInfo.discontinuityStartPositionUs,
sendDiscontinuity,
Player.DISCONTINUITY_REASON_INTERNAL);
}
}
......@@ -852,9 +855,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo =
handlePositionDiscontinuity(
playbackInfo.periodId,
discontinuityPositionUs,
playbackInfo.requestedContentPositionUs);
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
/* positionUs= */ discontinuityPositionUs,
playbackInfo.requestedContentPositionUs,
/* discontinuityStartPositionUs= */ discontinuityPositionUs,
/* reportDiscontinuity= */ true,
Player.DISCONTINUITY_REASON_INTERNAL);
}
} else {
rendererPositionUs =
......@@ -1166,10 +1171,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
} finally {
playbackInfo =
handlePositionDiscontinuity(periodId, periodPositionUs, requestedContentPositionUs);
if (seekPositionAdjusted) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
handlePositionDiscontinuity(
periodId,
periodPositionUs,
requestedContentPositionUs,
/* discontinuityStartPositionUs= */ periodPositionUs,
/* reportDiscontinuity= */ seekPositionAdjusted,
Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
}
......@@ -1385,6 +1393,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo.timeline,
mediaPeriodId,
requestedContentPositionUs,
/* discontinuityStartPositionUs= */ startPositionUs,
playbackInfo.playbackState,
resetError ? null : playbackInfo.playbackError,
/* isLoading= */ false,
......@@ -1395,9 +1404,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo.playWhenReady,
playbackInfo.playbackSuppressionReason,
playbackInfo.playbackParameters,
startPositionUs,
/* bufferedPositionUs= */ startPositionUs,
/* totalBufferedDurationUs= */ 0,
startPositionUs,
/* positionUs= */ startPositionUs,
offloadSchedulingEnabled,
/* sleepingForOffload= */ false);
if (releaseMediaSourceList) {
......@@ -1634,12 +1643,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
long periodPositionUs =
playingPeriodHolder.applyTrackSelection(
newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
boolean hasDiscontinuity =
playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs;
playbackInfo =
handlePositionDiscontinuity(
playbackInfo.periodId, periodPositionUs, playbackInfo.requestedContentPositionUs);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
playbackInfo.periodId,
periodPositionUs,
playbackInfo.requestedContentPositionUs,
playbackInfo.discontinuityStartPositionUs,
hasDiscontinuity,
Player.DISCONTINUITY_REASON_INTERNAL);
if (hasDiscontinuity) {
resetRendererPosition(periodPositionUs);
}
......@@ -1742,7 +1757,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|| !shouldPlayWhenReady());
}
private void handleMediaSourceListInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
private void handleMediaSourceListInfoRefreshed(Timeline timeline, boolean isSourceRefresh)
throws ExoPlaybackException {
PositionUpdateForPlaylistChange positionUpdate =
resolvePositionForPlaylistChange(
timeline,
......@@ -1759,7 +1775,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
long newPositionUs = positionUpdate.periodPositionUs;
boolean periodPositionChanged =
!playbackInfo.periodId.equals(newPeriodId) || newPositionUs != playbackInfo.positionUs;
try {
if (positionUpdate.endPlayback) {
if (playbackInfo.playbackState != Player.STATE_IDLE) {
......@@ -1800,8 +1815,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
: C.TIME_UNSET);
if (periodPositionChanged
|| newRequestedContentPositionUs != playbackInfo.requestedContentPositionUs) {
Object oldPeriodUid = playbackInfo.periodId.periodUid;
Timeline oldTimeline = playbackInfo.timeline;
boolean reportDiscontinuity =
periodPositionChanged
&& isSourceRefresh
&& !oldTimeline.isEmpty()
&& !oldTimeline.getWindow(
oldTimeline.getPeriodByUid(oldPeriodUid, period).windowIndex, window)
.isPlaceholder;
playbackInfo =
handlePositionDiscontinuity(newPeriodId, newPositionUs, newRequestedContentPositionUs);
handlePositionDiscontinuity(
newPeriodId,
newPositionUs,
newRequestedContentPositionUs,
playbackInfo.discontinuityStartPositionUs,
reportDiscontinuity,
timeline.getIndexOfPeriod(oldPeriodUid) == C.INDEX_UNSET
? Player.DISCONTINUITY_REASON_REMOVE
: Player.DISCONTINUITY_REASON_SKIP);
}
resetPendingPauseAtEndOfPeriod();
resolvePendingMessagePositions(
......@@ -2049,12 +2081,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
handlePositionDiscontinuity(
newPlayingPeriodHolder.info.id,
newPlayingPeriodHolder.info.startPositionUs,
newPlayingPeriodHolder.info.requestedContentPositionUs);
int discontinuityReason =
oldPlayingPeriodHolder.info.isLastInTimelinePeriod
? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
: Player.DISCONTINUITY_REASON_AD_INSERTION;
playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
newPlayingPeriodHolder.info.requestedContentPositionUs,
/* discontinuityStartPositionUs= */ newPlayingPeriodHolder.info.startPositionUs,
/* reportDiscontinuity= */ true,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
updateLivePlaybackSpeedControl(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ newPlayingPeriodHolder.info.id,
......@@ -2140,7 +2170,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
handlePositionDiscontinuity(
playbackInfo.periodId,
loadingPeriodHolder.info.startPositionUs,
playbackInfo.requestedContentPositionUs);
playbackInfo.requestedContentPositionUs,
loadingPeriodHolder.info.startPositionUs,
/* reportDiscontinuity= */ false,
/* ignored */ Player.DISCONTINUITY_REASON_INTERNAL);
}
maybeContinueLoading();
}
......@@ -2232,7 +2265,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
@CheckResult
private PlaybackInfo handlePositionDiscontinuity(
MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) {
MediaPeriodId mediaPeriodId,
long positionUs,
long contentPositionUs,
long discontinuityStartPositionUs,
boolean reportDiscontinuity,
@DiscontinuityReason int discontinuityReason) {
deliverPendingMessageAtStartPositionRequired =
deliverPendingMessageAtStartPositionRequired
|| positionUs != playbackInfo.positionUs
......@@ -2264,11 +2302,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
trackSelectorResult = emptyTrackSelectorResult;
staticMetadata = ImmutableList.of();
}
if (reportDiscontinuity) {
playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
}
return playbackInfo.copyWithNewPosition(
mediaPeriodId,
positionUs,
contentPositionUs,
discontinuityStartPositionUs,
getTotalBufferedDurationUs(),
trackGroupArray,
trackSelectorResult,
......
......@@ -50,6 +50,8 @@ import java.util.List;
* suspended content.
*/
public final long requestedContentPositionUs;
/** The start position after a reported position discontinuity, in microseconds. */
public final long discontinuityStartPositionUs;
/** The current playback state. One of the {@link Player}.STATE_ constants. */
@Player.State public final int playbackState;
/** The current playback error, or null if this is not an error state. */
......@@ -104,6 +106,7 @@ import java.util.List;
Timeline.EMPTY,
PLACEHOLDER_MEDIA_PERIOD_ID,
/* requestedContentPositionUs= */ C.TIME_UNSET,
/* discontinuityStartPositionUs= */ 0,
Player.STATE_IDLE,
/* playbackError= */ null,
/* isLoading= */ false,
......@@ -147,6 +150,7 @@ import java.util.List;
Timeline timeline,
MediaPeriodId periodId,
long requestedContentPositionUs,
long discontinuityStartPositionUs,
@Player.State int playbackState,
@Nullable ExoPlaybackException playbackError,
boolean isLoading,
......@@ -165,6 +169,7 @@ import java.util.List;
this.timeline = timeline;
this.periodId = periodId;
this.requestedContentPositionUs = requestedContentPositionUs;
this.discontinuityStartPositionUs = discontinuityStartPositionUs;
this.playbackState = playbackState;
this.playbackError = playbackError;
this.isLoading = isLoading;
......@@ -207,6 +212,7 @@ import java.util.List;
MediaPeriodId periodId,
long positionUs,
long requestedContentPositionUs,
long discontinuityStartPositionUs,
long totalBufferedDurationUs,
TrackGroupArray trackGroups,
TrackSelectorResult trackSelectorResult,
......@@ -215,6 +221,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -244,6 +251,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -273,6 +281,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -302,6 +311,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -331,6 +341,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -360,6 +371,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -393,6 +405,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -422,6 +435,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -452,6 +466,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......@@ -481,6 +496,7 @@ import java.util.List;
timeline,
periodId,
requestedContentPositionUs,
discontinuityStartPositionUs,
playbackState,
playbackError,
isLoading,
......
......@@ -706,8 +706,13 @@ public class AnalyticsCollector
listener -> listener.onPlayerError(eventTime, error));
}
// Calling deprecated callback.
@SuppressWarnings("deprecation")
@Override
public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public final void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
isSeeking = false;
}
......@@ -716,7 +721,10 @@ public class AnalyticsCollector
sendEvent(
eventTime,
AnalyticsListener.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(eventTime, reason));
listener -> {
listener.onPositionDiscontinuity(eventTime, reason);
listener.onPositionDiscontinuity(eventTime, oldPosition, newPosition, reason);
});
}
@Override
......
......@@ -236,7 +236,7 @@ public interface AnalyticsListener {
int EVENT_PLAYER_ERROR = Player.EVENT_PLAYER_ERROR;
/**
* A position discontinuity occurred. See {@link
* Player.EventListener#onPositionDiscontinuity(int)}.
* Player.EventListener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)}.
*/
int EVENT_POSITION_DISCONTINUITY = Player.EVENT_POSITION_DISCONTINUITY;
/** {@link Player#getPlaybackParameters()} changed. */
......@@ -532,12 +532,25 @@ public interface AnalyticsListener {
@Player.MediaItemTransitionReason int reason) {}
/**
* @deprecated Use {@link #onPositionDiscontinuity(EventTime, Player.PositionInfo,
* Player.PositionInfo, int)} instead.
*/
@Deprecated
default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}
/**
* Called when a position discontinuity occurred.
*
* @param eventTime The event time.
* @param oldPosition The position before the discontinuity.
* @param newPosition The position after the discontinuity.
* @param reason The reason for the position discontinuity.
*/
default void onPositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason) {}
default void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@DiscontinuityReason int reason) {}
/**
* Called when a seek operation started.
......
......@@ -191,9 +191,7 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
public synchronized void updateSessionsWithDiscontinuity(
EventTime eventTime, @DiscontinuityReason int reason) {
Assertions.checkNotNull(listener);
boolean hasAutomaticTransition =
reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
|| reason == Player.DISCONTINUITY_REASON_AD_INSERTION;
boolean hasAutomaticTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
Iterator<SessionDescriptor> iterator = sessions.values().iterator();
while (iterator.hasNext()) {
SessionDescriptor session = iterator.next();
......
......@@ -89,7 +89,10 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable {
}
@Override
public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public final void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
updateAndPost();
}
......
......@@ -141,8 +141,50 @@ public class EventLogger implements AnalyticsListener {
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) {
logd(eventTime, "positionDiscontinuity", getDiscontinuityReasonString(reason));
public void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
StringBuilder builder = new StringBuilder();
builder
.append("reason=")
.append(getDiscontinuityReasonString(reason))
.append(", PositionInfo:old [")
.append("window=")
.append(oldPosition.windowIndex)
.append(", period=")
.append(oldPosition.periodIndex)
.append(", pos=")
.append(oldPosition.positionMs);
if (oldPosition.adGroupIndex != C.INDEX_UNSET) {
builder
.append(", contentPos=")
.append(oldPosition.contentPositionMs)
.append(", adGroup=")
.append(oldPosition.adGroupIndex)
.append(", ad=")
.append(oldPosition.adIndexInAdGroup);
}
builder
.append("], PositionInfo:new [")
.append("window=")
.append(newPosition.windowIndex)
.append(", period=")
.append(newPosition.periodIndex)
.append(", pos=")
.append(newPosition.positionMs);
if (newPosition.adGroupIndex != C.INDEX_UNSET) {
builder
.append(", contentPos=")
.append(newPosition.contentPositionMs)
.append(", adGroup=")
.append(newPosition.adGroupIndex)
.append(", ad=")
.append(newPosition.adIndexInAdGroup);
}
builder.append("]");
logd(eventTime, "positionDiscontinuity", builder.toString());
}
@Override
......@@ -658,14 +700,16 @@ public class EventLogger implements AnalyticsListener {
private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) {
switch (reason) {
case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION:
return "PERIOD_TRANSITION";
case Player.DISCONTINUITY_REASON_AUTO_TRANSITION:
return "AUTO_TRANSITION";
case Player.DISCONTINUITY_REASON_SEEK:
return "SEEK";
case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
return "SEEK_ADJUSTMENT";
case Player.DISCONTINUITY_REASON_AD_INSERTION:
return "AD_INSERTION";
case Player.DISCONTINUITY_REASON_REMOVE:
return "REMOVE";
case Player.DISCONTINUITY_REASON_SKIP:
return "SKIP";
case Player.DISCONTINUITY_REASON_INTERNAL:
return "INTERNAL";
default:
......
......@@ -49,6 +49,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
......@@ -132,6 +133,7 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import java.io.IOException;
import java.util.ArrayList;
......@@ -210,6 +212,7 @@ public final class ExoPlayerTest {
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(renderer.getFormatsRead()).isEmpty();
assertThat(renderer.sampleBufferReadCount).isEqualTo(0);
assertThat(renderer.isEnded).isFalse();
......@@ -244,6 +247,7 @@ public final class ExoPlayerTest {
.onTracksChanged(
eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt());
assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(renderer.sampleBufferReadCount).isEqualTo(1);
assertThat(renderer.isEnded).isTrue();
......@@ -268,14 +272,14 @@ public final class ExoPlayerTest {
.verify(mockEventListener)
.onTimelineChanged(
argThat(noUid(new FakeMediaSource.InitialTimeline(timeline))),
eq(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION));
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(mockEventListener)
.onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockEventListener, times(2))
.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(renderer.getFormatsRead())
.containsExactly(
ExoPlayerTestRunner.VIDEO_FORMAT,
......@@ -313,7 +317,7 @@ public final class ExoPlayerTest {
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockEventListener, times(99))
.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(renderer.getFormatsRead()).hasSize(100);
assertThat(renderer.sampleBufferReadCount).isEqualTo(100);
assertThat(renderer.isEnded).isTrue();
......@@ -397,7 +401,7 @@ public final class ExoPlayerTest {
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockEventListener, times(2))
.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
assertThat(audioRenderer.positionResetCount).isEqualTo(1);
assertThat(videoRenderer.isEnded).isTrue();
assertThat(audioRenderer.isEnded).isTrue();
......@@ -441,7 +445,6 @@ public final class ExoPlayerTest {
// second source was suppressed as we replace it with the third source before the update
// arrives.
InOrder inOrder = inOrder(mockEventListener);
inOrder.verify(mockEventListener, never()).onPositionDiscontinuity(anyInt());
inOrder
.verify(mockEventListener)
.onTimelineChanged(
......@@ -452,12 +455,23 @@ public final class ExoPlayerTest {
.onTimelineChanged(
argThat(noUid(firstTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
.verify(mockEventListener, times(2))
.verify(mockEventListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockEventListener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(mockEventListener)
.onTimelineChanged(
argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(mockEventListener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(mockEventListener)
.onTimelineChanged(
argThat(noUid(thirdTimeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder
......@@ -542,11 +556,11 @@ public final class ExoPlayerTest {
.blockUntilEnded(TIMEOUT_MS);
testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION,
Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(renderer.isEnded).isTrue();
}
......@@ -603,7 +617,7 @@ public final class ExoPlayerTest {
.start()
.blockUntilEnded(TIMEOUT_MS);
// There is still one discontinuity from content to content for the failed ad insertion.
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AD_INSERTION);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
}
@Test
......@@ -1267,7 +1281,8 @@ public final class ExoPlayerTest {
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_REMOVE);
assertThat(currentWindowIndex[0]).isEqualTo(1);
assertThat(currentPosition[0]).isGreaterThan(0);
......@@ -2616,8 +2631,7 @@ public final class ExoPlayerTest {
@Test
public void timelineUpdateWithNewMidrollAdCuePoint_dropsPrebufferedPeriod() throws Exception {
Timeline timeline1 =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0));
Timeline timeline1 = new FakeTimeline(TimelineWindowDefinition.createPlaceholder(/* tag= */ 0));
AdPlaybackState adPlaybackStateWithMidroll =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1,
......@@ -2653,9 +2667,10 @@ public final class ExoPlayerTest {
testRunner.assertTimelineChangeReasonsEqual(
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
testRunner.assertPlayedPeriodIndices(0);
testRunner.assertPositionDiscontinuityReasonsEqual(
Player.DISCONTINUITY_REASON_AUTO_TRANSITION, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(4);
assertThat(mediaSource.getCreatedMediaPeriods().get(0).nextAdGroupIndex)
.isEqualTo(C.INDEX_UNSET);
......@@ -2718,7 +2733,7 @@ public final class ExoPlayerTest {
// When the ad finishes, the player position should be at or after the requested seek position.
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_AD_INSERTION);
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(player.getCurrentPosition()).isAtLeast(seekPositionMs);
}
......@@ -3019,7 +3034,7 @@ public final class ExoPlayerTest {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
positionAtDiscontinuityMs.set(playerReference.get().getCurrentPosition());
clockAtDiscontinuityMs.set(clock.elapsedRealtime());
}
......@@ -3242,7 +3257,7 @@ public final class ExoPlayerTest {
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (bufferedPositionAtFirstDiscontinuityMs.get() == C.TIME_UNSET) {
bufferedPositionAtFirstDiscontinuityMs.set(
playerReference.get().getBufferedPosition());
......@@ -3296,7 +3311,7 @@ public final class ExoPlayerTest {
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
......@@ -3349,7 +3364,7 @@ public final class ExoPlayerTest {
new EventListener() {
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
contentStartPositionMs.set(playerReference.get().getContentPosition());
}
}
......@@ -7584,7 +7599,7 @@ public final class ExoPlayerTest {
firstMediaSource.setNewSourceInfo(timelineWithOffsets);
// Wait until player transitions to second source (which also has non-zero offsets).
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
assertThat(player.getCurrentWindowIndex()).isEqualTo(1);
player.release();
......@@ -7680,7 +7695,7 @@ public final class ExoPlayerTest {
@Override
public void onPositionDiscontinuity(int reason) {
if (reason == Player.DISCONTINUITY_REASON_SEEK
|| reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|| reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
currentMediaItems.add(playerHolder[0].getCurrentMediaItem());
}
}
......@@ -9271,7 +9286,7 @@ public final class ExoPlayerTest {
verify(listener).onTimelineChanged(any(), anyInt());
verify(listener).onMediaItemTransition(any(), anyInt());
verify(listener).onPositionDiscontinuity(anyInt());
verify(listener).onPositionDiscontinuity(any(), any(), anyInt());
verify(listener).onPlaybackParametersChanged(any());
ArgumentCaptor<Player.Events> eventCaptor = ArgumentCaptor.forClass(Player.Events.class);
verify(listener).onEvents(eq(player), eventCaptor.capture());
......@@ -9312,7 +9327,7 @@ public final class ExoPlayerTest {
// Verify that all callbacks have been called at least once.
verify(listener, atLeastOnce()).onTimelineChanged(any(), anyInt());
verify(listener, atLeastOnce()).onMediaItemTransition(any(), anyInt());
verify(listener, atLeastOnce()).onPositionDiscontinuity(anyInt());
verify(listener, atLeastOnce()).onPositionDiscontinuity(any(), any(), anyInt());
verify(listener, atLeastOnce()).onPlaybackParametersChanged(any());
verify(listener, atLeastOnce()).onRepeatModeChanged(anyInt());
verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean());
......@@ -9342,6 +9357,877 @@ public final class ExoPlayerTest {
assertThat(containsEvent(allEvents, Player.EVENT_PLAYER_ERROR)).isTrue();
}
@Test
public void play_withPreMidAndPostRollAd_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 2,
/* adGroupTimesUs...= */ 0,
7 * C.MICROS_PER_SECOND,
C.TIME_END_OF_SOURCE);
TimelineWindowDefinition adTimeline =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(new FakeMediaSource(new FakeTimeline(adTimeline)));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(
any(), any(), not(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)));
verify(listener, times(8))
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
// first ad group (pre-roll)
// starts with ad to ad transition
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(1);
// ad to content transition
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
// second add group (mid-roll)
assertThat(oldPositions.get(2).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(7000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(2).windowIndex).isEqualTo(0);
assertThat(newPositions.get(2).positionMs).isEqualTo(0);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(1);
assertThat(newPositions.get(2).adIndexInAdGroup).isEqualTo(0);
// ad to ad transition
assertThat(oldPositions.get(3).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(3).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(3).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(1);
assertThat(oldPositions.get(3).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(3).windowIndex).isEqualTo(0);
assertThat(newPositions.get(3).positionMs).isEqualTo(0);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(1);
assertThat(newPositions.get(3).adIndexInAdGroup).isEqualTo(1);
// ad to content transition
assertThat(oldPositions.get(4).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(4).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(4).contentPositionMs).isEqualTo(7000);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(1);
assertThat(oldPositions.get(4).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(4).windowIndex).isEqualTo(0);
assertThat(newPositions.get(4).positionMs).isEqualTo(7000);
assertThat(newPositions.get(4).contentPositionMs).isEqualTo(7000);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(4).adIndexInAdGroup).isEqualTo(-1);
// third add group (post-roll)
assertThat(oldPositions.get(5).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(5).positionMs).isEqualTo(10000);
assertThat(oldPositions.get(5).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(5).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(5).windowIndex).isEqualTo(0);
assertThat(newPositions.get(5).positionMs).isEqualTo(0);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(10000);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(2);
assertThat(newPositions.get(5).adIndexInAdGroup).isEqualTo(0);
// ad to ad transition
assertThat(oldPositions.get(6).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(6).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(6).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(6).adGroupIndex).isEqualTo(2);
assertThat(oldPositions.get(6).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(6).windowIndex).isEqualTo(0);
assertThat(newPositions.get(6).positionMs).isEqualTo(0);
assertThat(newPositions.get(6).contentPositionMs).isEqualTo(10000);
assertThat(newPositions.get(6).adGroupIndex).isEqualTo(2);
assertThat(newPositions.get(6).adIndexInAdGroup).isEqualTo(1);
// post roll ad to end of content transition
assertThat(oldPositions.get(7).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(7).positionMs).isEqualTo(5000);
assertThat(oldPositions.get(7).contentPositionMs).isEqualTo(10000);
assertThat(oldPositions.get(7).adGroupIndex).isEqualTo(2);
assertThat(oldPositions.get(7).adIndexInAdGroup).isEqualTo(1);
assertThat(newPositions.get(7).windowIndex).isEqualTo(0);
assertThat(newPositions.get(7).positionMs).isEqualTo(9999);
assertThat(newPositions.get(7).contentPositionMs).isEqualTo(9999);
assertThat(newPositions.get(7).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(7).adIndexInAdGroup).isEqualTo(-1);
player.release();
}
@Test
public void seekTo_seekOverMidRoll_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 2 * C.MICROS_PER_SECOND);
TimelineWindowDefinition adTimeline =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(new FakeMediaSource(new FakeTimeline(adTimeline)));
player.prepare();
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 1000);
player.seekTo(/* positionMs= */ 8_000);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
verify(listener, never())
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_REMOVE));
verify(listener, never())
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SKIP));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// SEEK behind mid roll
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(980L, 1_000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(980L, 1_000L));
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(8_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(8_000);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
// SEEK_ADJUSTMENT back to ad
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(8_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(8_000);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(8000);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(0);
// AUTO_TRANSITION back to content
assertThat(oldPositions.get(2).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(2).positionMs).isEqualTo(5_000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(8_000);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(2).windowIndex).isEqualTo(0);
assertThat(newPositions.get(2).positionMs).isEqualTo(8_000);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(8_000);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(2).adIndexInAdGroup).isEqualTo(-1);
player.release();
}
@Test
public void play_multiItemPlaylistWidthAds_callsOnDiscontinuityCorrectly() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
AdPlaybackState postRollAdPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE);
TimelineWindowDefinition postRollWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 20 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
postRollAdPlaybackState);
AdPlaybackState preRollAdPlaybackState =
FakeTimeline.createAdPlaybackState(/* adsPerAdGroup= */ 1, /* adGroupTimesUs...= */ 0);
TimelineWindowDefinition preRollWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 25 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
preRollAdPlaybackState);
player.setMediaSources(
Lists.newArrayList(
new FakeMediaSource(),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 15 * C.MICROS_PER_SECOND))),
new FakeMediaSource(new FakeTimeline(postRollWindow)),
new FakeMediaSource(new FakeTimeline(preRollWindow))));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(
any(), any(), not(eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION)));
verify(listener, times(6))
.onPositionDiscontinuity(
oldPosition.capture(),
newPosition.capture(),
eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
// from first to second window
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
Window window = new Window();
assertThat(oldPositions.get(0).windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(0, window).uid);
assertThat(newPositions.get(0).windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(1, window).uid);
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isEqualTo(10_000);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(10_000);
assertThat(oldPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(0).windowIndex).isEqualTo(1);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(0).adIndexInAdGroup).isEqualTo(-1);
// from second window to third
assertThat(oldPositions.get(1).windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(1, window).uid);
assertThat(newPositions.get(1).windowUid)
.isEqualTo(player.getCurrentTimeline().getWindow(2, window).uid);
assertThat(oldPositions.get(1).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(15_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(15_000);
assertThat(oldPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(1).windowIndex).isEqualTo(2);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(1).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(1).adIndexInAdGroup).isEqualTo(-1);
// from third window content to post roll ad
assertThat(oldPositions.get(2).windowIndex).isEqualTo(2);
assertThat(oldPositions.get(2).windowUid).isEqualTo(newPositions.get(2).windowUid);
assertThat(oldPositions.get(2).positionMs).isEqualTo(20_000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(20_000);
assertThat(oldPositions.get(2).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(2).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(2).windowIndex).isEqualTo(2);
assertThat(newPositions.get(2).positionMs).isEqualTo(0);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(20_000);
assertThat(newPositions.get(2).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(2).adIndexInAdGroup).isEqualTo(0);
// from third window post roll to third window content end
assertThat(oldPositions.get(3).windowUid).isEqualTo(newPositions.get(2).windowUid);
assertThat(oldPositions.get(3).windowIndex).isEqualTo(2);
assertThat(oldPositions.get(3).positionMs).isEqualTo(5_000);
assertThat(oldPositions.get(3).contentPositionMs).isEqualTo(20_000);
assertThat(oldPositions.get(3).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(3).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(3).windowUid).isEqualTo(oldPositions.get(3).windowUid);
assertThat(newPositions.get(3).windowIndex).isEqualTo(2);
assertThat(newPositions.get(3).positionMs).isEqualTo(19_999);
assertThat(newPositions.get(3).contentPositionMs).isEqualTo(19_999);
assertThat(newPositions.get(3).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(3).adIndexInAdGroup).isEqualTo(-1);
// from third window content end to fourth window pre roll ad
assertThat(oldPositions.get(4).windowUid).isEqualTo(newPositions.get(3).windowUid);
assertThat(oldPositions.get(4).windowIndex).isEqualTo(2);
assertThat(oldPositions.get(4).positionMs).isEqualTo(20_000);
assertThat(oldPositions.get(4).contentPositionMs).isEqualTo(20_000);
assertThat(oldPositions.get(4).adGroupIndex).isEqualTo(-1);
assertThat(oldPositions.get(4).adIndexInAdGroup).isEqualTo(-1);
assertThat(newPositions.get(4).windowUid).isNotEqualTo(oldPositions.get(4).windowUid);
assertThat(newPositions.get(4).windowIndex).isEqualTo(3);
assertThat(newPositions.get(4).positionMs).isEqualTo(0);
assertThat(newPositions.get(4).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(4).adGroupIndex).isEqualTo(0);
assertThat(newPositions.get(4).adIndexInAdGroup).isEqualTo(0);
// from fourth window pre roll ad to fourth window content
assertThat(oldPositions.get(5).windowUid).isEqualTo(newPositions.get(4).windowUid);
assertThat(oldPositions.get(5).windowIndex).isEqualTo(3);
assertThat(oldPositions.get(5).positionMs).isEqualTo(5_000);
assertThat(oldPositions.get(5).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(5).adGroupIndex).isEqualTo(0);
assertThat(oldPositions.get(5).adIndexInAdGroup).isEqualTo(0);
assertThat(newPositions.get(5).windowUid).isEqualTo(oldPositions.get(5).windowUid);
assertThat(newPositions.get(5).windowIndex).isEqualTo(3);
assertThat(newPositions.get(5).positionMs).isEqualTo(0);
assertThat(newPositions.get(5).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(5).adGroupIndex).isEqualTo(-1);
assertThat(newPositions.get(5).adIndexInAdGroup).isEqualTo(-1);
player.release();
}
@Test
public void setMediaSources_removesPlayingPeriod_callsOnPositionDiscontinuity() throws Exception {
FakeMediaSource secondMediaSource =
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 15 * C.MICROS_PER_SECOND)));
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.setMediaSources(Lists.newArrayList(secondMediaSource, secondMediaSource));
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void removeMediaItems_removesPlayingPeriod_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSources(
Lists.newArrayList(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND)))));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.removeMediaItem(/* index= */ 1);
player.seekTo(/* positionMs= */ 0);
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ 2 * C.MILLIS_PER_SECOND);
// Removing the last item resets the position to 0 with an empty timeline.
player.removeMediaItem(0);
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isIn(Range.closed(1980L, 2000L));
assertThat(oldPositions.get(1).contentPositionMs).isIn(Range.closed(1980L, 2000L));
assertThat(newPositions.get(1).windowUid).isNull();
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void
concatenatingMediaSourceRemoveMediaSource_removesPlayingPeriod_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 6 * C.MICROS_PER_SECOND))));
player.addMediaSource(concatenatingMediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
concatenatingMediaSource.removeMediaSource(1);
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
concatenatingMediaSource.removeMediaSource(1);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowIndex).isEqualTo(1);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(oldPositions.get(1).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(0);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(0);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void
concatenatingMediaSourceRemoveMediaSourceWithSeek_overridesRemoval_callsOnPositionDiscontinuity()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
ConcatenatingMediaSource concatenatingMediaSource =
new ConcatenatingMediaSource(
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 1,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 8 * C.MICROS_PER_SECOND))),
new FakeMediaSource(
new FakeTimeline(
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 2,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* durationUs= */ 6 * C.MICROS_PER_SECOND))));
player.addMediaSource(concatenatingMediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 1, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
concatenatingMediaSource.removeMediaSource(1);
player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234);
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
concatenatingMediaSource.removeMediaSource(0);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
// SEEK overrides concatenating media source modification.
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
// This fails once out of a hundred test runs due to a race condition whether the seek or the
// removal arrives first in EPI.
// inOrder.verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(1234);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(1234);
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(1234);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(1234);
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(1234);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1234);
player.release();
}
@Test
public void seekTo_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSources(Lists.newArrayList(new FakeMediaSource(), new FakeMediaSource()));
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.seekTo(/* positionMs= */ 7 * C.MILLIS_PER_SECOND);
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ C.MILLIS_PER_SECOND);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowUid).isEqualTo(newPositions.get(0).windowUid);
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(7_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).windowUid).isNotEqualTo(newPositions.get(1).windowUid);
assertThat(oldPositions.get(1).positionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(7_000);
assertThat(newPositions.get(1).windowIndex).isEqualTo(1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1_000);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1_000);
player.release();
}
@Test
public void seekTo_whenTimelineEmpty_callsOnPositionDiscontinuity() {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.seekTo(/* positionMs= */ 7 * C.MILLIS_PER_SECOND);
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ C.MILLIS_PER_SECOND);
player.seekTo(/* positionMs= */ 5 * C.MILLIS_PER_SECOND);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_SEEK)));
verify(listener, times(3))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// a seek from initial state to masked seek position
assertThat(oldPositions.get(0).windowUid).isNull();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isEqualTo(0);
assertThat(oldPositions.get(0).contentPositionMs).isEqualTo(0);
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).windowUid).isNull();
assertThat(newPositions.get(0).positionMs).isEqualTo(7_000);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(7_000);
// a seek from masked seek position to another masked position across windows
assertThat(oldPositions.get(1).windowUid).isNull();
assertThat(oldPositions.get(1).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(1).positionMs).isEqualTo(7_000);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(7_000);
assertThat(newPositions.get(1).windowUid).isNull();
assertThat(newPositions.get(1).windowIndex).isEqualTo(1);
assertThat(newPositions.get(1).positionMs).isEqualTo(1_000);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(1_000);
// a seek from masked seek position to another masked position within window
assertThat(oldPositions.get(2).windowUid).isNull();
assertThat(oldPositions.get(2).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(2).positionMs).isEqualTo(1_000);
assertThat(oldPositions.get(2).contentPositionMs).isEqualTo(1_000);
assertThat(newPositions.get(2).windowUid).isNull();
assertThat(newPositions.get(2).windowIndex).isEqualTo(1);
assertThat(newPositions.get(2).positionMs).isEqualTo(5_000);
assertThat(newPositions.get(2).contentPositionMs).isEqualTo(5_000);
player.release();
}
@Test
public void stop_doesNotCallOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.stop();
verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
player.release();
}
// Tests deprecated stop(boolean reset)
@SuppressWarnings("deprecation")
@Test
public void stop_withResetRemovesPlayingPeriod_callsOnPositionDiscontinuity() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSource(new FakeMediaSource());
player.prepare();
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ 5 * C.MILLIS_PER_SECOND);
player.stop(/* reset= */ true);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
verify(listener, never())
.onPositionDiscontinuity(any(), any(), not(eq(Player.DISCONTINUITY_REASON_REMOVE)));
verify(listener)
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_REMOVE));
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
assertThat(oldPositions.get(0).windowIndex).isEqualTo(0);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(4980L, 5000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(4980L, 5000L));
assertThat(newPositions.get(0).windowUid).isNull();
assertThat(newPositions.get(0).windowIndex).isEqualTo(0);
assertThat(newPositions.get(0).positionMs).isEqualTo(0);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(0);
player.release();
}
@Test
public void seekTo_cancelsSourceDiscontinuity_callsOnPositionDiscontinuity() throws Exception {
Timeline timeline1 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2));
final Timeline timeline2 =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3));
final FakeMediaSource mediaSource =
new FakeMediaSource(timeline1, ExoPlayerTestRunner.VIDEO_FORMAT);
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
player.setMediaSource(mediaSource);
player.prepare();
TestPlayerRunHelper.playUntilPosition(player, /* windowIndex= */ 1, /* positionMs= */ 2000);
player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 2122);
// This causes a DISCONTINUITY_REASON_REMOVE between pending operations that needs to be
// cancelled by the seek below.
mediaSource.setNewSourceInfo(timeline2);
player.play();
player.seekTo(/* windowIndex= */ 0, /* positionMs= */ 2222);
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
ArgumentCaptor<Player.PositionInfo> oldPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
ArgumentCaptor<Player.PositionInfo> newPosition =
ArgumentCaptor.forClass(Player.PositionInfo.class);
InOrder inOrder = inOrder(listener);
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder
.verify(listener, times(2))
.onPositionDiscontinuity(
oldPosition.capture(), newPosition.capture(), eq(Player.DISCONTINUITY_REASON_SEEK));
inOrder
.verify(listener)
.onPositionDiscontinuity(any(), any(), eq(Player.DISCONTINUITY_REASON_AUTO_TRANSITION));
inOrder.verify(listener, never()).onPositionDiscontinuity(any(), any(), anyInt());
List<Player.PositionInfo> oldPositions = oldPosition.getAllValues();
List<Player.PositionInfo> newPositions = newPosition.getAllValues();
// First seek
assertThat(oldPositions.get(0).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(0).positionMs).isIn(Range.closed(1980L, 2000L));
assertThat(oldPositions.get(0).contentPositionMs).isIn(Range.closed(1980L, 2000L));
assertThat(newPositions.get(0).windowIndex).isEqualTo(1);
assertThat(newPositions.get(0).positionMs).isEqualTo(2122);
assertThat(newPositions.get(0).contentPositionMs).isEqualTo(2122);
// Second seek.
assertThat(oldPositions.get(1).windowIndex).isEqualTo(1);
assertThat(oldPositions.get(1).positionMs).isEqualTo(2122);
assertThat(oldPositions.get(1).contentPositionMs).isEqualTo(2122);
assertThat(newPositions.get(1).windowIndex).isEqualTo(0);
assertThat(newPositions.get(1).positionMs).isEqualTo(2222);
assertThat(newPositions.get(1).contentPositionMs).isEqualTo(2222);
player.release();
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......
......@@ -430,6 +430,7 @@ public final class MediaPeriodQueueTest {
mediaPeriodQueue.resolveMediaPeriodIdForAds(
playlistTimeline, firstPeriodUid, /* positionUs= */ 0),
/* requestedContentPositionUs= */ C.TIME_UNSET,
/* discontinuityStartPositionUs= */ 0,
Player.STATE_READY,
/* playbackError= */ null,
/* isLoading= */ false,
......
......@@ -662,6 +662,8 @@ public final class AnalyticsCollectorTest {
period0Seq0 /* SOURCE_UPDATE */,
WINDOW_0 /* PLAYLIST_CHANGE */,
period0Seq1 /* SOURCE_UPDATE */);
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(WINDOW_0 /* REMOVE */);
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1)
.inOrder();
......@@ -937,6 +939,9 @@ public final class AnalyticsCollectorTest {
period0Seq0 /* SOURCE_UPDATE (second item) */,
period0Seq1 /* PLAYLIST_CHANGED (remove) */)
.inOrder();
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
.containsExactly(period0Seq1 /* REMOVE */)
.inOrder();
assertThat(listener.getEvents(EVENT_IS_LOADING_CHANGED))
.containsExactly(period0Seq0, period0Seq0, period0Seq0, period0Seq0);
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
......@@ -1037,9 +1042,11 @@ public final class AnalyticsCollectorTest {
new Player.EventListener() {
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
if (!player.isPlayingAd()
&& reason == Player.DISCONTINUITY_REASON_AD_INSERTION) {
&& reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
// Finished playing ad. Marked as played.
adPlaybackState.set(
adPlaybackState
......@@ -1651,7 +1658,7 @@ public final class AnalyticsCollectorTest {
player.addMediaSource(new FakeMediaSource(new FakeTimeline(), formats));
player.play();
TestPlayerRunHelper.runUntilPositionDiscontinuity(
player, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
player, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE);
ShadowLooper.runMainLooperToNextTask();
......@@ -2085,7 +2092,11 @@ public final class AnalyticsCollectorTest {
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
public void onPositionDiscontinuity(
EventTime eventTime,
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
int reason) {
reportedEvents.add(new ReportedEvent(EVENT_POSITION_DISCONTINUITY, eventTime));
}
......
......@@ -461,9 +461,9 @@ public final class DefaultPlaybackSessionManagerTest {
sessionManager.updateSessionsWithTimelineChange(contentEventTime1);
sessionManager.updateSessions(adEventTime1);
sessionManager.updateSessionsWithDiscontinuity(
adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION);
adEventTime1, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
sessionManager.updateSessionsWithDiscontinuity(
contentEventTime2, Player.DISCONTINUITY_REASON_AD_INSERTION);
contentEventTime2, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
String adSessionId2 =
sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId);
......@@ -751,7 +751,7 @@ public final class DefaultPlaybackSessionManagerTest {
sessionManager.updateSessions(eventTime2);
sessionManager.updateSessionsWithDiscontinuity(
eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
eventTime2, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockListener).onSessionCreated(eq(eventTime1), anyString());
verify(mockListener).onSessionActive(eq(eventTime1), anyString());
......@@ -781,7 +781,7 @@ public final class DefaultPlaybackSessionManagerTest {
sessionManager.getSessionForMediaPeriodId(timeline, eventTime2.mediaPeriodId);
sessionManager.updateSessionsWithDiscontinuity(
eventTime2, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION);
eventTime2, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockListener).onSessionCreated(eventTime1, sessionId1);
verify(mockListener).onSessionActive(eventTime1, sessionId1);
......@@ -960,7 +960,7 @@ public final class DefaultPlaybackSessionManagerTest {
adTimeline, contentEventTimeDuringPreroll.mediaPeriodId);
sessionManager.updateSessionsWithDiscontinuity(
contentEventTimeBetweenAds, Player.DISCONTINUITY_REASON_AD_INSERTION);
contentEventTimeBetweenAds, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
InOrder inOrder = inOrder(mockListener);
inOrder.verify(mockListener).onSessionCreated(contentEventTimeDuringPreroll, contentSessionId);
......@@ -1025,7 +1025,7 @@ public final class DefaultPlaybackSessionManagerTest {
sessionManager.updateSessions(adEventTime2);
sessionManager.updateSessionsWithDiscontinuity(
adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION);
adEventTime1, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
verify(mockListener, never()).onSessionFinished(any(), anyString(), anyBoolean());
}
......@@ -1083,7 +1083,7 @@ public final class DefaultPlaybackSessionManagerTest {
sessionManager.getSessionForMediaPeriodId(adTimeline, adEventTime2.mediaPeriodId);
sessionManager.updateSessionsWithDiscontinuity(
adEventTime1, Player.DISCONTINUITY_REASON_AD_INSERTION);
adEventTime1, Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
sessionManager.updateSessionsWithDiscontinuity(adEventTime2, Player.DISCONTINUITY_REASON_SEEK);
verify(mockListener).onSessionCreated(eq(contentEventTime), anyString());
......
......@@ -1625,7 +1625,10 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@DiscontinuityReason int reason) {
if (isPlayingAd() && controllerHideDuringAds) {
hideController();
}
......
......@@ -1647,7 +1647,10 @@ public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewPro
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@DiscontinuityReason int reason) {
if (isPlayingAd() && controllerHideDuringAds) {
hideController();
}
......
......@@ -158,8 +158,8 @@ public class TestPlayerRunHelper {
/**
* Runs tasks of the main {@link Looper} until a {@link
* Player.EventListener#onPositionDiscontinuity} callback with the specified {@link
* Player.DiscontinuityReason} occurred.
* Player.EventListener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)}
* callback with the specified {@link Player.DiscontinuityReason} occurred.
*
* @param player The {@link Player}.
* @param expectedReason The expected {@link Player.DiscontinuityReason}.
......@@ -173,7 +173,8 @@ public class TestPlayerRunHelper {
Player.EventListener listener =
new Player.EventListener() {
@Override
public void onPositionDiscontinuity(int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
if (reason == expectedReason) {
receivedCallback.set(true);
player.removeListener(this);
......
......@@ -803,7 +803,10 @@ public abstract class Action {
}
}
/** Waits for {@link Player.EventListener#onPositionDiscontinuity(int)}. */
/**
* Waits for {@link Player.EventListener#onPositionDiscontinuity(Player.PositionInfo,
* Player.PositionInfo, int)}.
*/
public static final class WaitForPositionDiscontinuity extends Action {
/** @param tag A tag to use for logging. */
......@@ -824,7 +827,10 @@ public abstract class Action {
player.addListener(
new Player.EventListener() {
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
player.removeListener(this);
nextAction.schedule(player, trackSelector, surface, handler);
}
......
......@@ -575,7 +575,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
/**
* Asserts that {@link Player.EventListener#onPositionDiscontinuity(int)} was not called.
* Asserts that {@link Player.EventListener#onPositionDiscontinuity(Player.PositionInfo,
* Player.PositionInfo, int)} was not called.
*/
public void assertNoPositionDiscontinuities() {
assertThat(discontinuityReasons).isEmpty();
......@@ -583,7 +584,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
/**
* Asserts that the discontinuity reasons reported by {@link
* Player.EventListener#onPositionDiscontinuity(int)} are equal to the provided values.
* Player.EventListener#onPositionDiscontinuity(Player.PositionInfo, Player.PositionInfo, int)}
* are equal to the provided values.
*
* @param discontinuityReasons The expected discontinuity reasons.
*/
......@@ -676,10 +678,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
@Override
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
@Player.DiscontinuityReason int reason) {
discontinuityReasons.add(reason);
int currentIndex = player.getCurrentPeriodIndex();
if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
if ((reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
&& oldPosition.adGroupIndex != C.INDEX_UNSET
&& newPosition.adGroupIndex != C.INDEX_UNSET)
|| periodIndices.isEmpty()
|| periodIndices.get(periodIndices.size() - 1) != currentIndex) {
// Ignore seek or internal discontinuities within a period.
......
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