mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Add MediaSource.canUpdateMediaItem/updateMediaItem
This allows MediaSources to accept MediaItem updates after creation. This CL adds the handling and plumbing logic in `ExoPlayerImpl`, `ExoPlayerImplInternal`, `MediaSourceList` and `MaskingMediaSource`. It also updates all forwarding/wrapping sources to forward these calls to their wrapped instance. The actual functionality is only added to `FakeMediaSource` instances in tests so far. PiperOrigin-RevId: 545450210
This commit is contained in:
parent
c33a17d89c
commit
aa57d48347
15 changed files with 534 additions and 12 deletions
|
|
@ -24,6 +24,9 @@
|
||||||
reason will be removed when a suitable output is connected.
|
reason will be removed when a suitable output is connected.
|
||||||
* Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are
|
* Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are
|
||||||
created after the playlist is cleared.
|
created after the playlist is cleared.
|
||||||
|
* Add `MediaSource.canUpdateMediaItem` and `MediaSource.updateMediaItem`
|
||||||
|
to accept `MediaItem` updates after creation via
|
||||||
|
`Player.replaceMediaItem(s)`.
|
||||||
* Transformer:
|
* Transformer:
|
||||||
* Parse EXIF rotation data for image inputs.
|
* Parse EXIF rotation data for image inputs.
|
||||||
* Remove `TransformationRequest.HdrMode` annotation type and its
|
* Remove `TransformationRequest.HdrMode` annotation type and its
|
||||||
|
|
|
||||||
|
|
@ -1505,6 +1505,27 @@ public interface ExoPlayer extends Player {
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
void setShuffleOrder(ShuffleOrder shuffleOrder);
|
void setShuffleOrder(ShuffleOrder shuffleOrder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>ExoPlayer will keep the existing {@link MediaSource} for this {@link MediaItem} if
|
||||||
|
* {@linkplain MediaSource#canUpdateMediaItem supported} by the {@link MediaSource}. If the
|
||||||
|
* current item is replaced, this will also not interrupt the ongoing playback.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void replaceMediaItem(int index, MediaItem mediaItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>ExoPlayer will keep the existing {@link MediaSource} instances for the new {@link MediaItem
|
||||||
|
* MediaItems} if {@linkplain MediaSource#canUpdateMediaItem supported} by all of these {@link
|
||||||
|
* MediaSource} instances. If the current item is replaced, this will also not interrupt the
|
||||||
|
* ongoing playback.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the attributes for audio playback, used by the underlying audio track. If not set, the
|
* Sets the attributes for audio playback, used by the underlying audio track. If not set, the
|
||||||
* default audio attributes will be used. They are suitable for general media playback.
|
* default audio attributes will be used. They are suitable for general media playback.
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,11 @@ import androidx.media3.exoplayer.analytics.MediaMetricsListener;
|
||||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
||||||
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
||||||
|
import androidx.media3.exoplayer.source.MaskingMediaSource;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||||
import androidx.media3.exoplayer.source.ShuffleOrder;
|
import androidx.media3.exoplayer.source.ShuffleOrder;
|
||||||
|
import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem;
|
||||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||||
import androidx.media3.exoplayer.text.TextOutput;
|
import androidx.media3.exoplayer.text.TextOutput;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
|
@ -753,6 +755,11 @@ import java.util.concurrent.TimeoutException;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toIndex = min(toIndex, playlistSize);
|
toIndex = min(toIndex, playlistSize);
|
||||||
|
if (canUpdateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems)) {
|
||||||
|
// Update MediaSources directly without creating new ones if possible.
|
||||||
|
updateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<MediaSource> mediaSources = createMediaSources(mediaItems);
|
List<MediaSource> mediaSources = createMediaSources(mediaItems);
|
||||||
if (mediaSourceHolderSnapshots.isEmpty()) {
|
if (mediaSourceHolderSnapshots.isEmpty()) {
|
||||||
// Handle initial items in a playlist as a set operation to ensure state changes and initial
|
// Handle initial items in a playlist as a set operation to ensure state changes and initial
|
||||||
|
|
@ -1938,7 +1945,7 @@ import java.util.concurrent.TimeoutException;
|
||||||
List<Timeline> timelines = ((PlaylistTimeline) newTimeline).getChildTimelines();
|
List<Timeline> timelines = ((PlaylistTimeline) newTimeline).getChildTimelines();
|
||||||
checkState(timelines.size() == mediaSourceHolderSnapshots.size());
|
checkState(timelines.size() == mediaSourceHolderSnapshots.size());
|
||||||
for (int i = 0; i < timelines.size(); i++) {
|
for (int i = 0; i < timelines.size(); i++) {
|
||||||
mediaSourceHolderSnapshots.get(i).timeline = timelines.get(i);
|
mediaSourceHolderSnapshots.get(i).updateTimeline(timelines.get(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boolean positionDiscontinuity = false;
|
boolean positionDiscontinuity = false;
|
||||||
|
|
@ -2000,7 +2007,6 @@ import java.util.concurrent.TimeoutException;
|
||||||
repeatCurrentMediaItem);
|
repeatCurrentMediaItem);
|
||||||
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
|
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
|
||||||
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
|
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
|
||||||
MediaMetadata newMediaMetadata = mediaMetadata;
|
|
||||||
@Nullable MediaItem mediaItem = null;
|
@Nullable MediaItem mediaItem = null;
|
||||||
if (mediaItemTransitioned) {
|
if (mediaItemTransitioned) {
|
||||||
if (!newPlaybackInfo.timeline.isEmpty()) {
|
if (!newPlaybackInfo.timeline.isEmpty()) {
|
||||||
|
|
@ -2011,15 +2017,14 @@ import java.util.concurrent.TimeoutException;
|
||||||
}
|
}
|
||||||
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
|
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
|
||||||
}
|
}
|
||||||
if (mediaItemTransitioned
|
if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
||||||
|| !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
|
||||||
staticAndDynamicMediaMetadata =
|
staticAndDynamicMediaMetadata =
|
||||||
staticAndDynamicMediaMetadata
|
staticAndDynamicMediaMetadata
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.populateFromMetadata(newPlaybackInfo.staticMetadata)
|
.populateFromMetadata(newPlaybackInfo.staticMetadata)
|
||||||
.build();
|
.build();
|
||||||
newMediaMetadata = buildUpdatedMediaMetadata();
|
|
||||||
}
|
}
|
||||||
|
MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata();
|
||||||
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
|
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
|
||||||
mediaMetadata = newMediaMetadata;
|
mediaMetadata = newMediaMetadata;
|
||||||
boolean playWhenReadyChanged =
|
boolean playWhenReadyChanged =
|
||||||
|
|
@ -2361,7 +2366,7 @@ import java.util.concurrent.TimeoutException;
|
||||||
new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation);
|
new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation);
|
||||||
holders.add(holder);
|
holders.add(holder);
|
||||||
mediaSourceHolderSnapshots.add(
|
mediaSourceHolderSnapshots.add(
|
||||||
i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource.getTimeline()));
|
i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource));
|
||||||
}
|
}
|
||||||
shuffleOrder =
|
shuffleOrder =
|
||||||
shuffleOrder.cloneAndInsert(
|
shuffleOrder.cloneAndInsert(
|
||||||
|
|
@ -2903,6 +2908,43 @@ import java.util.concurrent.TimeoutException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canUpdateMediaSourcesWithMediaItems(
|
||||||
|
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
|
||||||
|
if (toIndex - fromIndex != mediaItems.size()) {
|
||||||
|
// Number of items doesn't match.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = fromIndex; i < toIndex; i++) {
|
||||||
|
MediaSource mediaSource = mediaSourceHolderSnapshots.get(i).mediaSource;
|
||||||
|
if (!mediaSource.canUpdateMediaItem(mediaItems.get(i - fromIndex))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMediaSourcesWithMediaItems(
|
||||||
|
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
|
||||||
|
pendingOperationAcks++;
|
||||||
|
internalPlayer.updateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems);
|
||||||
|
for (int i = fromIndex; i < toIndex; i++) {
|
||||||
|
MediaSourceHolderSnapshot snapshot = mediaSourceHolderSnapshots.get(i);
|
||||||
|
snapshot.updateTimeline(
|
||||||
|
new TimelineWithUpdatedMediaItem(snapshot.getTimeline(), mediaItems.get(i - fromIndex)));
|
||||||
|
}
|
||||||
|
Timeline newTimeline = createMaskingTimeline();
|
||||||
|
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithTimeline(newTimeline);
|
||||||
|
updatePlaybackInfo(
|
||||||
|
newPlaybackInfo,
|
||||||
|
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
||||||
|
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
|
||||||
|
/* ignored */ false,
|
||||||
|
/* ignored */ DISCONTINUITY_REASON_REMOVE,
|
||||||
|
/* ignored */ C.TIME_UNSET,
|
||||||
|
/* ignored */ C.INDEX_UNSET,
|
||||||
|
/* ignored */ false);
|
||||||
|
}
|
||||||
|
|
||||||
private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) {
|
private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) {
|
||||||
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
|
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
|
||||||
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
|
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
|
||||||
|
|
@ -2919,12 +2961,14 @@ import java.util.concurrent.TimeoutException;
|
||||||
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
|
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
|
||||||
|
|
||||||
private final Object uid;
|
private final Object uid;
|
||||||
|
private final MediaSource mediaSource;
|
||||||
|
|
||||||
private Timeline timeline;
|
private Timeline timeline;
|
||||||
|
|
||||||
public MediaSourceHolderSnapshot(Object uid, Timeline timeline) {
|
public MediaSourceHolderSnapshot(Object uid, MaskingMediaSource mediaSource) {
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.timeline = timeline;
|
this.mediaSource = mediaSource;
|
||||||
|
this.timeline = mediaSource.getTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -2936,6 +2980,10 @@ import java.util.concurrent.TimeoutException;
|
||||||
public Timeline getTimeline() {
|
public Timeline getTimeline() {
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateTimeline(Timeline timeline) {
|
||||||
|
this.timeline = timeline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ComponentListener
|
private final class ComponentListener
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.media3.common.AdPlaybackState;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.IllegalSeekPositionException;
|
import androidx.media3.common.IllegalSeekPositionException;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.Metadata;
|
import androidx.media3.common.Metadata;
|
||||||
import androidx.media3.common.ParserException;
|
import androidx.media3.common.ParserException;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
|
|
@ -166,6 +167,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private static final int MSG_SET_OFFLOAD_SCHEDULING_ENABLED = 24;
|
private static final int MSG_SET_OFFLOAD_SCHEDULING_ENABLED = 24;
|
||||||
private static final int MSG_ATTEMPT_RENDERER_ERROR_RECOVERY = 25;
|
private static final int MSG_ATTEMPT_RENDERER_ERROR_RECOVERY = 25;
|
||||||
private static final int MSG_RENDERER_CAPABILITIES_CHANGED = 26;
|
private static final int MSG_RENDERER_CAPABILITIES_CHANGED = 26;
|
||||||
|
private static final int MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS = 27;
|
||||||
|
|
||||||
private static final int ACTIVE_INTERVAL_MS = 10;
|
private static final int ACTIVE_INTERVAL_MS = 10;
|
||||||
private static final int IDLE_INTERVAL_MS = 1000;
|
private static final int IDLE_INTERVAL_MS = 1000;
|
||||||
|
|
@ -407,6 +409,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget();
|
handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateMediaSourcesWithMediaItems(
|
||||||
|
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
|
||||||
|
handler
|
||||||
|
.obtainMessage(MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS, fromIndex, toIndex, mediaItems)
|
||||||
|
.sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void sendMessage(PlayerMessage message) {
|
public synchronized void sendMessage(PlayerMessage message) {
|
||||||
if (released || !playbackLooper.getThread().isAlive()) {
|
if (released || !playbackLooper.getThread().isAlive()) {
|
||||||
|
|
@ -500,6 +509,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
// Handler.Callback implementation.
|
// Handler.Callback implementation.
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") // Casting message payload types.
|
||||||
@Override
|
@Override
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -587,6 +597,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
case MSG_RENDERER_CAPABILITIES_CHANGED:
|
case MSG_RENDERER_CAPABILITIES_CHANGED:
|
||||||
reselectTracksInternalAndSeek();
|
reselectTracksInternalAndSeek();
|
||||||
break;
|
break;
|
||||||
|
case MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS:
|
||||||
|
updateMediaSourcesWithMediaItemsInternal(msg.arg1, msg.arg2, (List<MediaItem>) msg.obj);
|
||||||
|
break;
|
||||||
case MSG_RELEASE:
|
case MSG_RELEASE:
|
||||||
releaseInternal();
|
releaseInternal();
|
||||||
// Return immediately to not send playback info updates after release.
|
// Return immediately to not send playback info updates after release.
|
||||||
|
|
@ -810,6 +823,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
|
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateMediaSourcesWithMediaItemsInternal(
|
||||||
|
int fromIndex, int toIndex, List<MediaItem> mediaItems) throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
Timeline timeline =
|
||||||
|
mediaSourceList.updateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems);
|
||||||
|
handleMediaSourceListInfoRefreshed(timeline, /* isSourceRefresh= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
private void notifyTrackSelectionPlayWhenReadyChanged(boolean playWhenReady) {
|
private void notifyTrackSelectionPlayWhenReadyChanged(boolean playWhenReady) {
|
||||||
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
|
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
|
||||||
while (periodHolder != null) {
|
while (periodHolder != null) {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import static java.lang.Math.min;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.Timeline;
|
import androidx.media3.common.Timeline;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.HandlerWrapper;
|
import androidx.media3.common.util.HandlerWrapper;
|
||||||
|
|
@ -250,6 +251,27 @@ import java.util.Set;
|
||||||
return createTimeline();
|
return createTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the specified media sources with new {@link MediaItem media items}.
|
||||||
|
*
|
||||||
|
* @param fromIndex The index of the first media source to update. This index must be in the range
|
||||||
|
* of 0 <= index <= {@link #getSize()}.
|
||||||
|
* @param toIndex The index after the last media source to update. This index must be in the range
|
||||||
|
* of {@code fromIndex} <= index <= {@link #getSize()}.
|
||||||
|
* @param mediaItems The new {@link MediaItem media items} for the specified range of media
|
||||||
|
* sources. Must have a size of {@code toIndex - fromIndex}.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
*/
|
||||||
|
public Timeline updateMediaSourcesWithMediaItems(
|
||||||
|
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
|
||||||
|
Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
|
||||||
|
Assertions.checkArgument(mediaItems.size() == toIndex - fromIndex);
|
||||||
|
for (int i = fromIndex; i < toIndex; i++) {
|
||||||
|
mediaSourceHolders.get(i).mediaSource.updateMediaItem(mediaItems.get(i - fromIndex));
|
||||||
|
}
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
/** Clears the playlist. */
|
/** Clears the playlist. */
|
||||||
public Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
|
public Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
|
||||||
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
|
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,23 @@ public final class MaskingMediaSource extends WrappingMediaSource {
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return mediaSource.canUpdateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
if (hasRealTimeline) {
|
||||||
|
timeline =
|
||||||
|
timeline.cloneWithUpdatedTimeline(
|
||||||
|
new TimelineWithUpdatedMediaItem(timeline.timeline, mediaItem));
|
||||||
|
} else {
|
||||||
|
timeline = MaskingTimeline.createWithPlaceholderTimeline(mediaItem);
|
||||||
|
}
|
||||||
|
mediaSource.updateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepareSourceInternal() {
|
public void prepareSourceInternal() {
|
||||||
if (!useLazyPreparation) {
|
if (!useLazyPreparation) {
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,34 @@ public interface MediaSource {
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
MediaItem getMediaItem();
|
MediaItem getMediaItem();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the {@link MediaItem} for this source can be updated with the provided item.
|
||||||
|
*
|
||||||
|
* <p>Should not be called directly from application code.
|
||||||
|
*
|
||||||
|
* <p>This method must be called on the application thread.
|
||||||
|
*
|
||||||
|
* @param mediaItem The new {@link MediaItem}.
|
||||||
|
* @return Whether the source can be updated using this item.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
default boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the {@link MediaItem} for this source.
|
||||||
|
*
|
||||||
|
* <p>Should not be called directly from application code.
|
||||||
|
*
|
||||||
|
* <p>This method must be called on the playback thread and only if {@link #canUpdateMediaItem}
|
||||||
|
* returns {@code true} for the new {@link MediaItem}.
|
||||||
|
*
|
||||||
|
* @param mediaItem The new {@link MediaItem}.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
default void updateMediaItem(MediaItem mediaItem) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Implement {@link #prepareSource(MediaSourceCaller, TransferListener, PlayerId)}
|
* @deprecated Implement {@link #prepareSource(MediaSourceCaller, TransferListener, PlayerId)}
|
||||||
* instead.
|
* instead.
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,16 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
||||||
return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : PLACEHOLDER_MEDIA_ITEM;
|
return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : PLACEHOLDER_MEDIA_ITEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return mediaSources.length > 0 && mediaSources[0].canUpdateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
mediaSources[0].updateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
super.prepareSourceInternal(mediaTransferListener);
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/** A {@link Timeline} that overrides the {@link MediaItem}. */
|
||||||
|
@UnstableApi
|
||||||
|
public final class TimelineWithUpdatedMediaItem extends ForwardingTimeline {
|
||||||
|
|
||||||
|
private final MediaItem updatedMediaItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the timeline.
|
||||||
|
*
|
||||||
|
* @param timeline The wrapped {@link Timeline}.
|
||||||
|
* @param mediaItem The {@link MediaItem} that replaced the original one in {@code timeline}.
|
||||||
|
*/
|
||||||
|
public TimelineWithUpdatedMediaItem(Timeline timeline, MediaItem mediaItem) {
|
||||||
|
super(timeline);
|
||||||
|
this.updatedMediaItem = mediaItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation") // Setting deprecated field for backward compatibility.
|
||||||
|
@Override
|
||||||
|
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
|
||||||
|
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
|
||||||
|
window.mediaItem = updatedMediaItem;
|
||||||
|
window.tag =
|
||||||
|
updatedMediaItem.localConfiguration != null
|
||||||
|
? updatedMediaItem.localConfiguration.tag
|
||||||
|
: null;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,9 @@ import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link #getMediaItem()}: Amend the {@link MediaItem} for this media source. This is only
|
* <li>{@link #getMediaItem()}: Amend the {@link MediaItem} for this media source. This is only
|
||||||
* used before the child source is prepared.
|
* used before the child source is prepared. You may also want to override {@link
|
||||||
|
* #canUpdateMediaItem} or {@link #updateMediaItem} to intercept further updates to the {@link
|
||||||
|
* MediaItem}.
|
||||||
* <li>{@link #onChildSourceInfoRefreshed(Timeline)}: Called whenever the child source's {@link
|
* <li>{@link #onChildSourceInfoRefreshed(Timeline)}: Called whenever the child source's {@link
|
||||||
* Timeline} changed. This {@link Timeline} can be amended if needed, for example using {@link
|
* Timeline} changed. This {@link Timeline} can be amended if needed, for example using {@link
|
||||||
* ForwardingTimeline}. The {@link Timeline} for the wrapping source needs to be published
|
* ForwardingTimeline}. The {@link Timeline} for the wrapping source needs to be published
|
||||||
|
|
@ -88,18 +90,38 @@ public abstract class WrappingMediaSource extends CompositeMediaSource<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link MediaItem} for this media source.
|
* {@inheritDoc}
|
||||||
*
|
*
|
||||||
* <p>This method can be overridden to amend the {@link MediaItem} of the child source. It is only
|
* <p>This method can be overridden to amend the {@link MediaItem} of the child source. It is only
|
||||||
* used before the child source is prepared.
|
* used before the child source is prepared.
|
||||||
*
|
|
||||||
* @see MediaSource#getMediaItem()
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public MediaItem getMediaItem() {
|
public MediaItem getMediaItem() {
|
||||||
return mediaSource.getMediaItem();
|
return mediaSource.getMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>This method can be overridden to change whether the {@link MediaItem} of the child source
|
||||||
|
* can be updated.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return mediaSource.canUpdateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>This method can be overridden to change how the {@link MediaItem} of the child source is
|
||||||
|
* updated.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
mediaSource.updateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the requested {@link MediaPeriod}.
|
* Creates the requested {@link MediaPeriod}.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
||||||
return contentMediaSource.getMediaItem();
|
return contentMediaSource.getMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return contentMediaSource.canUpdateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
contentMediaSource.updateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
super.prepareSourceInternal(mediaTransferListener);
|
super.prepareSourceInternal(mediaTransferListener);
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,16 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
||||||
return mediaSource.getMediaItem();
|
return mediaSource.getMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return mediaSource.canUpdateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
mediaSource.updateMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
Handler handler = Util.createHandlerForCurrentLooper();
|
Handler handler = Util.createHandlerForCurrentLooper();
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
import static org.robolectric.Shadows.shadowOf;
|
import static org.robolectric.Shadows.shadowOf;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
@ -13140,6 +13141,202 @@ public final class ExoPlayerTest {
|
||||||
player.release();
|
player.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replaceMediaItems_withOnlyPartiallyReplaceableItemsInMediaSource_createsNewItems() {
|
||||||
|
FakeMediaSource sourceWithoutReplaceableItem = new FakeMediaSource();
|
||||||
|
sourceWithoutReplaceableItem.setCanUpdateMediaItems(false);
|
||||||
|
FakeMediaSource sourceWithReplaceableItem = new FakeMediaSource();
|
||||||
|
sourceWithReplaceableItem.setCanUpdateMediaItems(true);
|
||||||
|
MediaSource.Factory mockFactory = mock(MediaSource.Factory.class);
|
||||||
|
when(mockFactory.createMediaSource(any())).thenReturn(new FakeMediaSource());
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).setMediaSourceFactory(mockFactory).build();
|
||||||
|
player.addMediaSources(
|
||||||
|
ImmutableList.of(sourceWithoutReplaceableItem, sourceWithReplaceableItem));
|
||||||
|
MediaItem mediaItem0 = new MediaItem.Builder().setMediaId("0").build();
|
||||||
|
MediaItem mediaItem1 = new MediaItem.Builder().setMediaId("1").build();
|
||||||
|
|
||||||
|
player.replaceMediaItems(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem0, mediaItem1));
|
||||||
|
|
||||||
|
verify(mockFactory).createMediaSource(mediaItem0);
|
||||||
|
verify(mockFactory).createMediaSource(mediaItem1);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
replaceMediaItems_withReplaceableItemsInMediaSourceBeforePreparation_updatesMediaItems()
|
||||||
|
throws Exception {
|
||||||
|
FakeMediaSource unaffectedSource = new FakeMediaSource();
|
||||||
|
FakeMediaSource source0 = new FakeMediaSource();
|
||||||
|
source0.setCanUpdateMediaItems(true);
|
||||||
|
FakeMediaSource source1 = new FakeMediaSource();
|
||||||
|
source1.setCanUpdateMediaItems(true);
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
player.addMediaSources(ImmutableList.of(source0, source1, unaffectedSource));
|
||||||
|
MediaItem unaffectedMediaItem = unaffectedSource.getMediaItem();
|
||||||
|
MediaItem mediaItem0 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("0")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("0").build())
|
||||||
|
.build();
|
||||||
|
MediaItem mediaItem1 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("1")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("1").build())
|
||||||
|
.build();
|
||||||
|
Player.Listener listener = mock(Player.Listener.class);
|
||||||
|
player.addListener(listener);
|
||||||
|
|
||||||
|
player.replaceMediaItems(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem0, mediaItem1));
|
||||||
|
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener)
|
||||||
|
.onTimelineChanged(
|
||||||
|
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||||
|
verify(listener).onMediaMetadataChanged(new MediaMetadata.Builder().setTitle("0").build());
|
||||||
|
verifyNoMoreInteractions(listener);
|
||||||
|
|
||||||
|
// Verify that preparing the source keeps the updated item.
|
||||||
|
reset(listener);
|
||||||
|
player.prepare();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
|
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener)
|
||||||
|
.onTimelineChanged(
|
||||||
|
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||||
|
verify(listener, never()).onMediaMetadataChanged(any());
|
||||||
|
verify(listener, never()).onMediaItemTransition(any(), anyInt());
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
replaceMediaItems_withReplaceableItemsInMediaSourceImmediatelyAfterPreparation_updatesMediaItems()
|
||||||
|
throws Exception {
|
||||||
|
FakeMediaSource unaffectedSource = new FakeMediaSource();
|
||||||
|
FakeMediaSource source0 = new FakeMediaSource();
|
||||||
|
source0.setCanUpdateMediaItems(true);
|
||||||
|
FakeMediaSource source1 = new FakeMediaSource();
|
||||||
|
source1.setCanUpdateMediaItems(true);
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
player.addMediaSources(ImmutableList.of(source0, source1, unaffectedSource));
|
||||||
|
MediaItem unaffectedMediaItem = unaffectedSource.getMediaItem();
|
||||||
|
MediaItem mediaItem0 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("0")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("0").build())
|
||||||
|
.build();
|
||||||
|
MediaItem mediaItem1 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("1")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("1").build())
|
||||||
|
.build();
|
||||||
|
Player.Listener listener = mock(Player.Listener.class);
|
||||||
|
|
||||||
|
player.prepare();
|
||||||
|
player.addListener(listener);
|
||||||
|
player.replaceMediaItems(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem0, mediaItem1));
|
||||||
|
|
||||||
|
// Immediate updates from the prepare and replace operations.
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener)
|
||||||
|
.onTimelineChanged(
|
||||||
|
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||||
|
verify(listener).onMediaMetadataChanged(new MediaMetadata.Builder().setTitle("0").build());
|
||||||
|
verifyNoMoreInteractions(listener);
|
||||||
|
|
||||||
|
// Verify that the preparation finished without another MediaItem or metadata update.
|
||||||
|
reset(listener);
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener)
|
||||||
|
.onTimelineChanged(
|
||||||
|
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||||
|
verify(listener, never()).onMediaMetadataChanged(any());
|
||||||
|
verify(listener, never()).onMediaItemTransition(any(), anyInt());
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void
|
||||||
|
replaceMediaItems_withReplaceableItemsInMediaSourceDuringPlayback_updatesMediaItemsWithoutInterruption()
|
||||||
|
throws Exception {
|
||||||
|
FakeMediaSource unaffectedSource = new FakeMediaSource();
|
||||||
|
FakeMediaSource source0 = new FakeMediaSource();
|
||||||
|
source0.setCanUpdateMediaItems(true);
|
||||||
|
FakeMediaSource source1 = new FakeMediaSource();
|
||||||
|
source1.setCanUpdateMediaItems(true);
|
||||||
|
ExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||||
|
player.addMediaSources(ImmutableList.of(source0, source1, unaffectedSource));
|
||||||
|
MediaItem unaffectedMediaItem = unaffectedSource.getMediaItem();
|
||||||
|
MediaItem mediaItem0 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("0")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("0").build())
|
||||||
|
.build();
|
||||||
|
MediaItem mediaItem1 =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setMediaId("1")
|
||||||
|
.setMediaMetadata(new MediaMetadata.Builder().setTitle("1").build())
|
||||||
|
.build();
|
||||||
|
player.prepare();
|
||||||
|
runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
|
player.play();
|
||||||
|
Player.Listener listener = mock(Player.Listener.class);
|
||||||
|
player.addListener(listener);
|
||||||
|
|
||||||
|
player.replaceMediaItems(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, ImmutableList.of(mediaItem0, mediaItem1));
|
||||||
|
|
||||||
|
// Immediate updates from the replace operation.
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener)
|
||||||
|
.onTimelineChanged(
|
||||||
|
player.getCurrentTimeline(), Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
|
||||||
|
verify(listener).onMediaMetadataChanged(new MediaMetadata.Builder().setTitle("0").build());
|
||||||
|
verifyNoMoreInteractions(listener);
|
||||||
|
|
||||||
|
// Verify that the update causes no immediate interruption.
|
||||||
|
reset(listener);
|
||||||
|
runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
assertThat(player.getMediaItemAt(0)).isEqualTo(mediaItem0);
|
||||||
|
assertThat(player.getMediaItemAt(1)).isEqualTo(mediaItem1);
|
||||||
|
assertThat(player.getMediaItemAt(2)).isEqualTo(unaffectedMediaItem);
|
||||||
|
verify(listener, never()).onTimelineChanged(any(), anyInt());
|
||||||
|
verify(listener, never()).onMediaMetadataChanged(any());
|
||||||
|
verify(listener, never()).onMediaItemTransition(any(), anyInt());
|
||||||
|
verify(listener, never()).onPlaybackStateChanged(Player.STATE_BUFFERING);
|
||||||
|
|
||||||
|
// Verify that playback finishes with the expected item and metadata transitions.
|
||||||
|
reset(listener);
|
||||||
|
runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
|
|
||||||
|
verify(listener, never()).onTimelineChanged(any(), anyInt());
|
||||||
|
verify(listener, times(2)).onMediaMetadataChanged(any());
|
||||||
|
verify(listener).onMediaMetadataChanged(new MediaMetadata.Builder().setTitle("1").build());
|
||||||
|
verify(listener).onMediaMetadataChanged(MediaMetadata.EMPTY);
|
||||||
|
verify(listener, times(2)).onMediaItemTransition(any(), anyInt());
|
||||||
|
verify(listener).onMediaItemTransition(mediaItem1, Player.MEDIA_ITEM_TRANSITION_REASON_AUTO);
|
||||||
|
verify(listener)
|
||||||
|
.onMediaItemTransition(unaffectedMediaItem, Player.MEDIA_ITEM_TRANSITION_REASON_AUTO);
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests playback suppression for playback with only unsuitable outputs (e.g. builtin speaker) on
|
* Tests playback suppression for playback with only unsuitable outputs (e.g. builtin speaker) on
|
||||||
* the Wear OS.
|
* the Wear OS.
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.media3.test.utils.FakeMediaSource;
|
||||||
import androidx.media3.test.utils.FakeShuffleOrder;
|
import androidx.media3.test.utils.FakeShuffleOrder;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -508,6 +509,40 @@ public class MediaSourceListTest {
|
||||||
mediaSourceList.setShuffleOrder(new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE)));
|
mediaSourceList.setShuffleOrder(new FakeShuffleOrder(MEDIA_SOURCE_LIST_SIZE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateMediaSourcesWithMediaItems_updatesMediaItemsForPreparedAndPlaceholderSources() {
|
||||||
|
FakeMediaSource unaffectedSource = new FakeMediaSource();
|
||||||
|
FakeMediaSource preparedSource = new FakeMediaSource();
|
||||||
|
preparedSource.setCanUpdateMediaItems(true);
|
||||||
|
preparedSource.setAllowPreparation(true);
|
||||||
|
FakeMediaSource unpreparedSource = new FakeMediaSource();
|
||||||
|
unpreparedSource.setCanUpdateMediaItems(true);
|
||||||
|
unpreparedSource.setAllowPreparation(false);
|
||||||
|
mediaSourceList.setMediaSources(
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, unaffectedSource, preparedSource, unpreparedSource),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3));
|
||||||
|
mediaSourceList.prepare(/* mediaTransferListener= */ null);
|
||||||
|
MediaItem unaffectedMediaItem = unaffectedSource.getMediaItem();
|
||||||
|
MediaItem updatedItem1 = new MediaItem.Builder().setMediaId("1").build();
|
||||||
|
MediaItem updatedItem2 = new MediaItem.Builder().setMediaId("2").build();
|
||||||
|
|
||||||
|
Timeline timeline =
|
||||||
|
mediaSourceList.updateMediaSourcesWithMediaItems(
|
||||||
|
/* fromIndex= */ 1, /* toIndex= */ 3, ImmutableList.of(updatedItem1, updatedItem2));
|
||||||
|
|
||||||
|
assertThat(timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).mediaItem)
|
||||||
|
.isEqualTo(unaffectedMediaItem);
|
||||||
|
assertThat(timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).mediaItem)
|
||||||
|
.isEqualTo(updatedItem1);
|
||||||
|
assertThat(timeline.getWindow(/* windowIndex= */ 1, new Timeline.Window()).isPlaceholder)
|
||||||
|
.isFalse();
|
||||||
|
assertThat(timeline.getWindow(/* windowIndex= */ 2, new Timeline.Window()).mediaItem)
|
||||||
|
.isEqualTo(updatedItem2);
|
||||||
|
assertThat(timeline.getWindow(/* windowIndex= */ 2, new Timeline.Window()).isPlaceholder)
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
// Internal methods.
|
// Internal methods.
|
||||||
|
|
||||||
private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) {
|
private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import androidx.media3.exoplayer.source.MediaLoadData;
|
||||||
import androidx.media3.exoplayer.source.MediaPeriod;
|
import androidx.media3.exoplayer.source.MediaPeriod;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||||
|
import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem;
|
||||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||||
import androidx.media3.exoplayer.upstream.Allocator;
|
import androidx.media3.exoplayer.upstream.Allocator;
|
||||||
import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory;
|
import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory;
|
||||||
|
|
@ -98,6 +99,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
||||||
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
||||||
private final DrmSessionManager drmSessionManager;
|
private final DrmSessionManager drmSessionManager;
|
||||||
|
|
||||||
|
private boolean canUpdateMediaItems;
|
||||||
private boolean preparationAllowed;
|
private boolean preparationAllowed;
|
||||||
private @MonotonicNonNull Timeline timeline;
|
private @MonotonicNonNull Timeline timeline;
|
||||||
private boolean preparedSource;
|
private boolean preparedSource;
|
||||||
|
|
@ -168,6 +170,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.trackDataFactory = trackDataFactory;
|
this.trackDataFactory = trackDataFactory;
|
||||||
preparationAllowed = true;
|
preparationAllowed = true;
|
||||||
|
canUpdateMediaItems = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -185,6 +188,15 @@ public class FakeMediaSource extends BaseMediaSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the source allows to update its {@link MediaItem} via {@link #updateMediaItem}.
|
||||||
|
*
|
||||||
|
* @param canUpdateMediaItems Whether a {@link MediaItem} update is possible.
|
||||||
|
*/
|
||||||
|
public void setCanUpdateMediaItems(boolean canUpdateMediaItems) {
|
||||||
|
this.canUpdateMediaItems = canUpdateMediaItems;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected Timeline getTimeline() {
|
protected Timeline getTimeline() {
|
||||||
return timeline;
|
return timeline;
|
||||||
|
|
@ -198,6 +210,22 @@ public class FakeMediaSource extends BaseMediaSource {
|
||||||
return timeline.getWindow(0, new Timeline.Window()).mediaItem;
|
return timeline.getWindow(0, new Timeline.Window()).mediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
|
return canUpdateMediaItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMediaItem(MediaItem mediaItem) {
|
||||||
|
if (timeline == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timeline = new TimelineWithUpdatedMediaItem(timeline, mediaItem);
|
||||||
|
if (preparedSource && preparationAllowed) {
|
||||||
|
refreshSourceInfo(timeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Timeline getInitialTimeline() {
|
public Timeline getInitialTimeline() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue