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.
|
||||
* Fix issue in `PlaybackStatsListener` where spurious `PlaybackStats` are
|
||||
created after the playlist is cleared.
|
||||
* Add `MediaSource.canUpdateMediaItem` and `MediaSource.updateMediaItem`
|
||||
to accept `MediaItem` updates after creation via
|
||||
`Player.replaceMediaItem(s)`.
|
||||
* Transformer:
|
||||
* Parse EXIF rotation data for image inputs.
|
||||
* Remove `TransformationRequest.HdrMode` annotation type and its
|
||||
|
|
|
|||
|
|
@ -1505,6 +1505,27 @@ public interface ExoPlayer extends Player {
|
|||
@UnstableApi
|
||||
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
|
||||
* 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.audio.AudioRendererEventListener;
|
||||
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
||||
import androidx.media3.exoplayer.source.MaskingMediaSource;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder;
|
||||
import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem;
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||
import androidx.media3.exoplayer.text.TextOutput;
|
||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||
|
|
@ -753,6 +755,11 @@ import java.util.concurrent.TimeoutException;
|
|||
return;
|
||||
}
|
||||
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);
|
||||
if (mediaSourceHolderSnapshots.isEmpty()) {
|
||||
// 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();
|
||||
checkState(timelines.size() == mediaSourceHolderSnapshots.size());
|
||||
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;
|
||||
|
|
@ -2000,7 +2007,6 @@ import java.util.concurrent.TimeoutException;
|
|||
repeatCurrentMediaItem);
|
||||
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
|
||||
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
|
||||
MediaMetadata newMediaMetadata = mediaMetadata;
|
||||
@Nullable MediaItem mediaItem = null;
|
||||
if (mediaItemTransitioned) {
|
||||
if (!newPlaybackInfo.timeline.isEmpty()) {
|
||||
|
|
@ -2011,15 +2017,14 @@ import java.util.concurrent.TimeoutException;
|
|||
}
|
||||
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
|
||||
}
|
||||
if (mediaItemTransitioned
|
||||
|| !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
||||
if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
|
||||
staticAndDynamicMediaMetadata =
|
||||
staticAndDynamicMediaMetadata
|
||||
.buildUpon()
|
||||
.populateFromMetadata(newPlaybackInfo.staticMetadata)
|
||||
.build();
|
||||
newMediaMetadata = buildUpdatedMediaMetadata();
|
||||
}
|
||||
MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata();
|
||||
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
|
||||
mediaMetadata = newMediaMetadata;
|
||||
boolean playWhenReadyChanged =
|
||||
|
|
@ -2361,7 +2366,7 @@ import java.util.concurrent.TimeoutException;
|
|||
new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation);
|
||||
holders.add(holder);
|
||||
mediaSourceHolderSnapshots.add(
|
||||
i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource.getTimeline()));
|
||||
i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource));
|
||||
}
|
||||
shuffleOrder =
|
||||
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) {
|
||||
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
|
||||
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
|
||||
|
|
@ -2919,12 +2961,14 @@ import java.util.concurrent.TimeoutException;
|
|||
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
|
||||
|
||||
private final Object uid;
|
||||
private final MediaSource mediaSource;
|
||||
|
||||
private Timeline timeline;
|
||||
|
||||
public MediaSourceHolderSnapshot(Object uid, Timeline timeline) {
|
||||
public MediaSourceHolderSnapshot(Object uid, MaskingMediaSource mediaSource) {
|
||||
this.uid = uid;
|
||||
this.timeline = timeline;
|
||||
this.mediaSource = mediaSource;
|
||||
this.timeline = mediaSource.getTimeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -2936,6 +2980,10 @@ import java.util.concurrent.TimeoutException;
|
|||
public Timeline getTimeline() {
|
||||
return timeline;
|
||||
}
|
||||
|
||||
public void updateTimeline(Timeline timeline) {
|
||||
this.timeline = timeline;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ComponentListener
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.media3.common.AdPlaybackState;
|
|||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.IllegalSeekPositionException;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.ParserException;
|
||||
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_ATTEMPT_RENDERER_ERROR_RECOVERY = 25;
|
||||
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 IDLE_INTERVAL_MS = 1000;
|
||||
|
|
@ -407,6 +409,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
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
|
||||
public synchronized void sendMessage(PlayerMessage message) {
|
||||
if (released || !playbackLooper.getThread().isAlive()) {
|
||||
|
|
@ -500,6 +509,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
|
||||
// Handler.Callback implementation.
|
||||
|
||||
@SuppressWarnings("unchecked") // Casting message payload types.
|
||||
@Override
|
||||
public boolean handleMessage(Message msg) {
|
||||
try {
|
||||
|
|
@ -587,6 +597,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
case MSG_RENDERER_CAPABILITIES_CHANGED:
|
||||
reselectTracksInternalAndSeek();
|
||||
break;
|
||||
case MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS:
|
||||
updateMediaSourcesWithMediaItemsInternal(msg.arg1, msg.arg2, (List<MediaItem>) msg.obj);
|
||||
break;
|
||||
case MSG_RELEASE:
|
||||
releaseInternal();
|
||||
// Return immediately to not send playback info updates after release.
|
||||
|
|
@ -810,6 +823,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
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) {
|
||||
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
|
||||
while (periodHolder != null) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import static java.lang.Math.min;
|
|||
import android.os.Handler;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.HandlerWrapper;
|
||||
|
|
@ -250,6 +251,27 @@ import java.util.Set;
|
|||
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. */
|
||||
public Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
|
||||
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
|
||||
|
|
|
|||
|
|
@ -77,6 +77,23 @@ public final class MaskingMediaSource extends WrappingMediaSource {
|
|||
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
|
||||
public void prepareSourceInternal() {
|
||||
if (!useLazyPreparation) {
|
||||
|
|
|
|||
|
|
@ -280,6 +280,34 @@ public interface MediaSource {
|
|||
@UnstableApi
|
||||
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)}
|
||||
* instead.
|
||||
|
|
|
|||
|
|
@ -167,6 +167,16 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
|
|||
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
|
||||
protected void prepareSourceInternal(@Nullable TransferListener 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>
|
||||
* <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
|
||||
* 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
|
||||
|
|
@ -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
|
||||
* used before the child source is prepared.
|
||||
*
|
||||
* @see MediaSource#getMediaItem()
|
||||
*/
|
||||
@Override
|
||||
public MediaItem 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}.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -191,6 +191,16 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
return contentMediaSource.getMediaItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||
return contentMediaSource.canUpdateMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMediaItem(MediaItem mediaItem) {
|
||||
contentMediaSource.updateMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
super.prepareSourceInternal(mediaTransferListener);
|
||||
|
|
|
|||
|
|
@ -219,6 +219,16 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
|
|||
return mediaSource.getMediaItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||
return mediaSource.canUpdateMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMediaItem(MediaItem mediaItem) {
|
||||
mediaSource.updateMediaItem(mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||
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.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.content.Context;
|
||||
|
|
@ -13140,6 +13141,202 @@ public final class ExoPlayerTest {
|
|||
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
|
||||
* the Wear OS.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import androidx.media3.test.utils.FakeMediaSource;
|
|||
import androidx.media3.test.utils.FakeShuffleOrder;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
|
@ -508,6 +509,40 @@ public class MediaSourceListTest {
|
|||
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.
|
||||
|
||||
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.MediaSource;
|
||||
import androidx.media3.exoplayer.source.MediaSourceEventListener;
|
||||
import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem;
|
||||
import androidx.media3.exoplayer.source.TrackGroupArray;
|
||||
import androidx.media3.exoplayer.upstream.Allocator;
|
||||
import androidx.media3.test.utils.FakeMediaPeriod.TrackDataFactory;
|
||||
|
|
@ -98,6 +99,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
|
||||
private boolean canUpdateMediaItems;
|
||||
private boolean preparationAllowed;
|
||||
private @MonotonicNonNull Timeline timeline;
|
||||
private boolean preparedSource;
|
||||
|
|
@ -168,6 +170,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
this.drmSessionManager = drmSessionManager;
|
||||
this.trackDataFactory = trackDataFactory;
|
||||
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
|
||||
protected Timeline getTimeline() {
|
||||
return timeline;
|
||||
|
|
@ -198,6 +210,22 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
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
|
||||
@Nullable
|
||||
public Timeline getInitialTimeline() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue