mirror of
https://github.com/samsonjs/media.git
synced 2026-03-29 10:05:48 +00:00
Add Player.EventListener.onMediaItemTransition
PiperOrigin-RevId: 321218451
This commit is contained in:
parent
e486dc602c
commit
e7b76354b9
9 changed files with 578 additions and 10 deletions
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue