Add Player.EventListener.onMediaItemTransition

PiperOrigin-RevId: 321218451
This commit is contained in:
bachinger 2020-07-14 21:04:30 +01:00 committed by Oliver Woodman
parent e486dc602c
commit e7b76354b9
9 changed files with 578 additions and 10 deletions

View file

@ -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`.

View file

@ -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));
}

View file

@ -470,6 +470,15 @@ public interface Player {
default void onTimelineChanged(
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.
*
@ -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;

View file

@ -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;
@ -455,6 +456,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) {

View file

@ -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;
@ -207,6 +208,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.
*

View file

@ -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;
@ -196,6 +197,19 @@ public class EventLogger implements AnalyticsListener {
logd("]");
}
@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) {

View file

@ -7886,6 +7886,329 @@ public final class ExoPlayerTest {
assertThat(initialMediaItems).containsExactlyElementsIn(currentMediaItems);
}
@Test
public void setMediaSources_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource = factory.setTag("1").createMediaSource();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(mediaSource.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void setMediaSources_replaceWithSameMediaItem_notifiesMediaItemTransition()
throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource = factory.setTag("1").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.setMediaSources(mediaSource)
.waitForPlaybackState(Player.STATE_READY)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(
mediaSource.getMediaItem(), mediaSource.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void automaticWindowTransition_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.build()
.start()
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(
mediaSource1.getMediaItem(), mediaSource2.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO);
}
@Test
public void clearMediaItem_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 1, /* positionMs= */ 2000)
.clearMediaItems()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(
mediaSource1.getMediaItem(), mediaSource2.getMediaItem(), null);
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void seekTo_otherWindow_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_READY)
.seek(/* windowIndex= */ 1, /* positionMs= */ 2000)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(
mediaSource1.getMediaItem(), mediaSource2.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK);
}
@Test
public void seekTo_sameWindow_doesNotNotifyMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)
.seek(/* windowIndex= */ 0, /* positionMs= */ 20_000)
.stop()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(mediaSource1.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void repeat_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.setRepeatMode(Player.REPEAT_MODE_ONE);
}
})
.play()
.waitForPositionDiscontinuity()
.waitForPositionDiscontinuity()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
player.setRepeatMode(Player.REPEAT_MODE_OFF);
}
})
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(
mediaSource1.getMediaItem(),
mediaSource1.getMediaItem(),
mediaSource1.getMediaItem(),
mediaSource2.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT,
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT,
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO);
}
@Test
public void stop_withReset_notifiesMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)
.stop(/* reset= */ true)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(mediaSource1.getMediaItem(), null);
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED,
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void stop_withoutReset_doesNotNotifyMediaItemTransition() throws Exception {
SilenceMediaSource.Factory factory =
new SilenceMediaSource.Factory().setDurationUs(C.msToUs(100_000));
SilenceMediaSource mediaSource1 = factory.setTag("1").createMediaSource();
SilenceMediaSource mediaSource2 = factory.setTag("2").createMediaSource();
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)
.stop(/* reset= */ false)
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource1, mediaSource2)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(mediaSource1.getMediaItem());
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
@Test
public void timelineRefresh_withModifiedMediaItem_doesNotNotifyMediaItemTransition()
throws Exception {
MediaItem initialMediaItem = FakeTimeline.FAKE_MEDIA_ITEM.buildUpon().setTag(0).build();
TimelineWindowDefinition initialWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
initialMediaItem);
TimelineWindowDefinition secondWindow =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10_000_000,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
AdPlaybackState.NONE,
initialMediaItem.buildUpon().setTag(1).build());
FakeTimeline timeline = new FakeTimeline(initialWindow);
FakeTimeline newTimeline = new FakeTimeline(secondWindow);
FakeMediaSource mediaSource = new FakeMediaSource(timeline);
ActionSchedule actionSchedule =
new ActionSchedule.Builder(TAG)
.pause()
.waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 2000)
.waitForPlayWhenReady(false)
.executeRunnable(
() -> {
mediaSource.setNewSourceInfo(newTimeline);
})
.play()
.build();
ExoPlayerTestRunner exoPlayerTestRunner =
new ExoPlayerTestRunner.Builder(context)
.setMediaSources(mediaSource)
.setActionSchedule(actionSchedule)
.build()
.start()
.blockUntilActionScheduleFinished(TIMEOUT_MS)
.blockUntilEnded(TIMEOUT_MS);
exoPlayerTestRunner.assertTimelinesSame(placeholderTimeline, timeline, newTimeline);
exoPlayerTestRunner.assertMediaItemsTransitionReasonsEqual(
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
exoPlayerTestRunner.assertMediaItemsTransitionedSame(initialMediaItem);
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {

View file

@ -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<>();
@ -525,12 +530,34 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder();
}
/**
* 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();
}
/**
@ -617,6 +644,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;

View file

@ -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;