Commit e7b76354 by bachinger Committed by Oliver Woodman

Add Player.EventListener.onMediaItemTransition

PiperOrigin-RevId: 321218451
parent e486dc60
......@@ -31,9 +31,10 @@
* Add `play` and `pause` methods to `Player`.
* Add `Player.getCurrentLiveOffset` to conveniently return the live
offset.
* Add `Player.onPlayWhenReadyChanged` with reasons.
* Add `Player.onPlaybackStateChanged` and deprecate
`Player.onPlayerStateChanged`.
* Add `Player.EventListener.onPlayWhenReadyChanged` with reasons.
* Add `Player.EventListener.onPlaybackStateChanged` and deprecate
`Player.EventListener.onPlayerStateChanged`.
* Add `Player.EventListener.onMediaItemTransition` with reasons.
* Add `Player.setAudioSessionId` to set the session ID attached to the
`AudioTrack`.
* Deprecate and rename `getPlaybackError` to `getPlayerError` for
......@@ -242,9 +243,8 @@
* Cast extension: Implement playlist API and deprecate the old queue
manipulation API.
* IMA extension:
* Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the
media load timeout
([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* Upgrade to IMA SDK 3.19.4, bringing in a fix for setting the media load
timeout ([#7170](https://github.com/google/ExoPlayer/issues/7170)).
* Migrate to new 'friendly obstruction' IMA SDK APIs, and allow apps to
register a purpose and detail reason for overlay views via
`AdsLoader.AdViewProvider`.
......
......@@ -990,6 +990,22 @@ import java.util.concurrent.TimeoutException;
// Assign playback info immediately such that all getters return the right values.
PlaybackInfo previousPlaybackInfo = this.playbackInfo;
this.playbackInfo = playbackInfo;
Pair<Boolean, Integer> mediaItemTransitionInfo =
evaluateMediaItemTransitionReason(
playbackInfo,
previousPlaybackInfo,
positionDiscontinuity,
positionDiscontinuityReason,
!previousPlaybackInfo.timeline.equals(playbackInfo.timeline));
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
@Nullable MediaItem newMediaItem = null;
if (mediaItemTransitioned && !playbackInfo.timeline.isEmpty()) {
int windowIndex =
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period).windowIndex;
newMediaItem = playbackInfo.timeline.getWindow(windowIndex, window).mediaItem;
}
notifyListeners(
new PlaybackInfoUpdate(
playbackInfo,
......@@ -999,10 +1015,58 @@ import java.util.concurrent.TimeoutException;
positionDiscontinuity,
positionDiscontinuityReason,
timelineChangeReason,
mediaItemTransitioned,
mediaItemTransitionReason,
newMediaItem,
playWhenReadyChangeReason,
seekProcessed));
}
private Pair<Boolean, Integer> evaluateMediaItemTransitionReason(
PlaybackInfo playbackInfo,
PlaybackInfo oldPlaybackInfo,
boolean positionDiscontinuity,
int positionDiscontinuityReason,
boolean timelineChanged) {
Timeline oldTimeline = oldPlaybackInfo.timeline;
Timeline newTimeline = playbackInfo.timeline;
if (newTimeline.isEmpty() && oldTimeline.isEmpty()) {
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET);
} else if (newTimeline.isEmpty() != oldTimeline.isEmpty()) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
int oldWindowIndex =
oldTimeline.getPeriodByUid(oldPlaybackInfo.periodId.periodUid, period).windowIndex;
Object oldWindowUid = oldTimeline.getWindow(oldWindowIndex, window).uid;
int newWindowIndex =
newTimeline.getPeriodByUid(playbackInfo.periodId.periodUid, period).windowIndex;
Object newWindowUid = newTimeline.getWindow(newWindowIndex, window).uid;
int firstPeriodIndexInNewWindow = window.firstPeriodIndex;
if (!oldWindowUid.equals(newWindowUid)) {
@Player.MediaItemTransitionReason int transitionReason;
if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_PERIOD_TRANSITION) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_AUTO;
} else if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_SEEK;
} else if (timelineChanged) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
} else {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_SKIP;
}
return new Pair<>(/* isTransitioning */ true, transitionReason);
} else if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_PERIOD_TRANSITION
&& newTimeline.getIndexOfPeriod(playbackInfo.periodId.periodUid)
== firstPeriodIndexInNewWindow) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_REPEAT);
}
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET);
}
private void setMediaSourcesInternal(
List<MediaSource> mediaSources,
int startWindowIndex,
......@@ -1388,16 +1452,19 @@ import java.util.concurrent.TimeoutException;
private final boolean positionDiscontinuity;
@DiscontinuityReason private final int positionDiscontinuityReason;
@TimelineChangeReason private final int timelineChangeReason;
private final boolean mediaItemTransitioned;
private final int mediaItemTransitionReason;
@Nullable private final MediaItem mediaItem;
@PlayWhenReadyChangeReason private final int playWhenReadyChangeReason;
private final boolean seekProcessed;
private final boolean playbackStateChanged;
private final boolean playbackErrorChanged;
private final boolean timelineChanged;
private final boolean isLoadingChanged;
private final boolean timelineChanged;
private final boolean trackSelectorResultChanged;
private final boolean isPlayingChanged;
private final boolean playWhenReadyChanged;
private final boolean playbackSuppressionReasonChanged;
private final boolean isPlayingChanged;
public PlaybackInfoUpdate(
PlaybackInfo playbackInfo,
......@@ -1407,6 +1474,9 @@ import java.util.concurrent.TimeoutException;
boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason,
@TimelineChangeReason int timelineChangeReason,
boolean mediaItemTransitioned,
@MediaItemTransitionReason int mediaItemTransitionReason,
@Nullable MediaItem mediaItem,
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
boolean seekProcessed) {
this.playbackInfo = playbackInfo;
......@@ -1415,6 +1485,9 @@ import java.util.concurrent.TimeoutException;
this.positionDiscontinuity = positionDiscontinuity;
this.positionDiscontinuityReason = positionDiscontinuityReason;
this.timelineChangeReason = timelineChangeReason;
this.mediaItemTransitioned = mediaItemTransitioned;
this.mediaItemTransitionReason = mediaItemTransitionReason;
this.mediaItem = mediaItem;
this.playWhenReadyChangeReason = playWhenReadyChangeReason;
this.seekProcessed = seekProcessed;
playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState;
......@@ -1444,6 +1517,11 @@ import java.util.concurrent.TimeoutException;
listenerSnapshot,
listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
}
if (mediaItemTransitioned) {
invokeAll(
listenerSnapshot,
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
}
if (playbackErrorChanged) {
invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError));
}
......
......@@ -471,6 +471,15 @@ public interface Player {
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {}
/**
* Called when playback transitions to a different media item.
*
* @param mediaItem The {@link MediaItem}. May be null if the timeline becomes empty.
* @param reason The reason for the transition.
*/
default void onMediaItemTransition(
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {}
/**
* Called when the available or selected tracks change.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
......@@ -766,6 +775,32 @@ public interface Player {
/** Timeline changed as a result of a dynamic update introduced by the played media. */
int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1;
/** Reasons for media item transitions. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MEDIA_ITEM_TRANSITION_REASON_REPEAT,
MEDIA_ITEM_TRANSITION_REASON_AUTO,
MEDIA_ITEM_TRANSITION_REASON_SEEK,
MEDIA_ITEM_TRANSITION_REASON_SKIP,
MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED
})
@interface MediaItemTransitionReason {}
/** The media item has been repeated. */
int MEDIA_ITEM_TRANSITION_REASON_REPEAT = 0;
/** Playback has automatically transitioned to the next media item. */
int MEDIA_ITEM_TRANSITION_REASON_AUTO = 1;
/** A seek to another media item has occurred. */
int MEDIA_ITEM_TRANSITION_REASON_SEEK = 2;
/** Playback skipped to a new media item (for example after failure). */
int MEDIA_ITEM_TRANSITION_REASON_SKIP = 3;
/**
* The current media item has changed because of a modification of the timeline. This can either
* be if the period previously being played has been removed, or when the timeline becomes
* non-empty after being empty.
*/
int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 4;
/** The default playback speed. */
float DEFAULT_PLAYBACK_SPEED = 1.0f;
......
......@@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
......@@ -456,6 +457,15 @@ public class AnalyticsCollector
}
@Override
public final void onMediaItemTransition(
@Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
for (AnalyticsListener listener : listeners) {
listener.onMediaItemTransition(eventTime, mediaItem, reason);
}
}
@Override
public final void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
......
......@@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
......@@ -208,6 +209,18 @@ public interface AnalyticsListener {
default void onTimelineChanged(EventTime eventTime, @TimelineChangeReason int reason) {}
/**
* Called when playback transitions to a different media item.
*
* @param eventTime The event time.
* @param mediaItem The media item.
* @param reason The reason for the media item transition.
*/
default void onMediaItemTransition(
EventTime eventTime,
@Nullable MediaItem mediaItem,
@Player.MediaItemTransitionReason int reason) {}
/**
* Called when a position discontinuity occurred.
*
* @param eventTime The event time.
......
......@@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.PlaybackSuppressionReason;
......@@ -197,6 +198,19 @@ public class EventLogger implements AnalyticsListener {
}
@Override
public void onMediaItemTransition(
EventTime eventTime, @Nullable MediaItem mediaItem, int reason) {
logd(
"mediaItem ["
+ getEventTimeString(eventTime)
+ ", "
+ (mediaItem == null ? "null" : "mediaId=" + mediaItem.mediaId)
+ ", reason="
+ getMediaItemTransitionReasonString(reason)
+ "]");
}
@Override
public void onPlayerError(EventTime eventTime, ExoPlaybackException e) {
loge(eventTime, "playerFailed", e);
}
......@@ -648,6 +662,24 @@ public class EventLogger implements AnalyticsListener {
}
}
private static String getMediaItemTransitionReasonString(
@Player.MediaItemTransitionReason int reason) {
switch (reason) {
case Player.MEDIA_ITEM_TRANSITION_REASON_AUTO:
return "AUTO";
case Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED:
return "PLAYLIST_CHANGED";
case Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT:
return "REPEAT";
case Player.MEDIA_ITEM_TRANSITION_REASON_SEEK:
return "SEEK";
case Player.MEDIA_ITEM_TRANSITION_REASON_SKIP:
return "SKIP";
default:
return "?";
}
}
private static String getPlaybackSuppressionReasonString(
@PlaybackSuppressionReason int playbackSuppressionReason) {
switch (playbackSuppressionReason) {
......
......@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.RenderersFactory;
......@@ -356,6 +357,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
private final CountDownLatch actionScheduleFinishedCountDownLatch;
private final ArrayList<Timeline> timelines;
private final ArrayList<Integer> timelineChangeReasons;
private final ArrayList<MediaItem> mediaItems;
private final ArrayList<Integer> mediaItemTransitionReasons;
private final ArrayList<Integer> periodIndices;
private final ArrayList<Integer> discontinuityReasons;
private final ArrayList<Integer> playbackStates;
......@@ -387,6 +390,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
this.analyticsListener = analyticsListener;
timelines = new ArrayList<>();
timelineChangeReasons = new ArrayList<>();
mediaItems = new ArrayList<>();
mediaItemTransitionReasons = new ArrayList<>();
periodIndices = new ArrayList<>();
discontinuityReasons = new ArrayList<>();
playbackStates = new ArrayList<>();
......@@ -526,11 +531,33 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
/**
* Asserts that the media items reported by {@link
* Player.EventListener#onMediaItemTransition(MediaItem, int)} are the same as the provided media
* items.
*
* @param mediaItems A list of expected {@link MediaItem media items}.
*/
public void assertMediaItemsTransitionedSame(MediaItem... mediaItems) {
assertThat(this.mediaItems).containsExactlyElementsIn(mediaItems).inOrder();
}
/**
* Asserts that the media item transition reasons reported by {@link
* Player.EventListener#onMediaItemTransition(MediaItem, int)} are the same as the provided
* reasons.
*
* @param reasons A list of expected transition reasons.
*/
public void assertMediaItemsTransitionReasonsEqual(Integer... reasons) {
assertThat(this.mediaItemTransitionReasons).containsExactlyElementsIn(reasons).inOrder();
}
/**
* Asserts that the playback states reported by {@link
* Player.EventListener#onPlaybackStateChanged(int)} are equal to the provided playback states.
*/
public void assertPlaybackStatesEqual(Integer... states) {
assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder();
assertThat(playbackStates).containsExactlyElementsIn(states).inOrder();
}
/**
......@@ -618,6 +645,13 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
}
@Override
public void onMediaItemTransition(
@Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) {
mediaItems.add(mediaItem);
mediaItemTransitionReasons.add(reason);
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
this.trackGroups = trackGroups;
}
......
......@@ -166,10 +166,53 @@ public final class FakeTimeline extends Timeline {
long defaultPositionUs,
long windowOffsetInFirstPeriodUs,
AdPlaybackState adPlaybackState) {
this(
periodCount,
id,
isSeekable,
isDynamic,
isLive,
isPlaceholder,
durationUs,
defaultPositionUs,
windowOffsetInFirstPeriodUs,
adPlaybackState,
FAKE_MEDIA_ITEM.buildUpon().setTag(id).build());
}
/**
* Creates a window definition with ad groups and a custom media item.
*
* @param periodCount The number of periods in the window. Each period get an equal slice of the
* total window duration.
* @param id The UID of the window.
* @param isSeekable Whether the window is seekable.
* @param isDynamic Whether the window is dynamic.
* @param isLive Whether the window is live.
* @param isPlaceholder Whether the window is a placeholder.
* @param durationUs The duration of the window in microseconds.
* @param defaultPositionUs The default position of the window in microseconds.
* @param windowOffsetInFirstPeriodUs The offset of the window in the first period, in
* microseconds.
* @param adPlaybackState The ad playback state.
* @param mediaItem The media item to include in the timeline.
*/
public TimelineWindowDefinition(
int periodCount,
Object id,
boolean isSeekable,
boolean isDynamic,
boolean isLive,
boolean isPlaceholder,
long durationUs,
long defaultPositionUs,
long windowOffsetInFirstPeriodUs,
AdPlaybackState adPlaybackState,
MediaItem mediaItem) {
Assertions.checkArgument(durationUs != C.TIME_UNSET || periodCount == 1);
this.periodCount = periodCount;
this.id = id;
this.mediaItem = FAKE_MEDIA_ITEM.buildUpon().setTag(id).build();
this.mediaItem = mediaItem;
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.isLive = isLive;
......
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