diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java index acf2eb1fb6..396e5f6257 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java @@ -22,7 +22,6 @@ import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import androidx.test.platform.app.InstrumentationRegistry; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.rules.ExternalResource; @@ -52,14 +51,13 @@ public class PlayerTestRule extends ExternalResource { AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - exoPlayer = new SimpleExoPlayer.Builder(context).setLooper(Looper.myLooper()).build(); - ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(); - TimelinePlaylistManager manager = - new TimelinePlaylistManager(context, concatenatingMediaSource); - ConcatenatingMediaSourcePlaybackPreparer playbackPreparer = - new ConcatenatingMediaSourcePlaybackPreparer(exoPlayer, concatenatingMediaSource); - sessionPlayerConnector = - new SessionPlayerConnector(exoPlayer, manager, playbackPreparer); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(context); + exoPlayer = + new SimpleExoPlayer.Builder(context) + .setLooper(Looper.myLooper()) + .setMediaSourceFactory(converter) + .build(); + sessionPlayerConnector = new SessionPlayerConnector(exoPlayer, converter); }); } diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java index 3ebd92c7db..033e649dc4 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnectorTest.java @@ -30,6 +30,7 @@ import android.content.Context; import android.content.res.Resources; import android.media.AudioManager; import android.os.Build; +import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media.AudioAttributesCompat; @@ -53,7 +54,6 @@ import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.media2.test.R; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.common.util.concurrent.ListenableFuture; import java.io.IOException; import java.util.ArrayList; @@ -176,6 +176,10 @@ public class SessionPlayerConnectorTest { @LargeTest @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) public void play_withCustomControlDispatcher_isSkipped() throws Exception { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + ControlDispatcher controlDispatcher = new DefaultControlDispatcher() { @Override @@ -183,17 +187,22 @@ public class SessionPlayerConnectorTest { return false; } }; - SimpleExoPlayer simpleExoPlayer = playerTestRule.getSimpleExoPlayer(); - ConcatenatingMediaSource concatenatingMediaSource = new ConcatenatingMediaSource(); - TimelinePlaylistManager timelinePlaylistManager = - new TimelinePlaylistManager(context, concatenatingMediaSource); - ConcatenatingMediaSourcePlaybackPreparer playbackPreparer = - new ConcatenatingMediaSourcePlaybackPreparer(simpleExoPlayer, concatenatingMediaSource); - - try (SessionPlayerConnector player = - new SessionPlayerConnector( - simpleExoPlayer, timelinePlaylistManager, playbackPreparer, controlDispatcher)) { - assertPlayerResult(player.play(), RESULT_INFO_SKIPPED); + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(context); + SimpleExoPlayer simpleExoPlayer = null; + try { + simpleExoPlayer = + new SimpleExoPlayer.Builder(context) + .setLooper(Looper.myLooper()) + .setMediaSourceFactory(converter) + .build(); + try (SessionPlayerConnector player = + new SessionPlayerConnector(simpleExoPlayer, converter, controlDispatcher)) { + assertPlayerResult(player.play(), RESULT_INFO_SKIPPED); + } + } finally { + if (simpleExoPlayer != null) { + simpleExoPlayer.release(); + } } } diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/ConcatenatingMediaSourcePlaybackPreparer.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/ConcatenatingMediaSourcePlaybackPreparer.java deleted file mode 100644 index bb1129dc83..0000000000 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/ConcatenatingMediaSourcePlaybackPreparer.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2019 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 com.google.android.exoplayer2.ext.media2; - -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlaybackPreparer; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.util.Assertions; - -/** Prepares an {@link ExoPlayer} instance with a {@link ConcatenatingMediaSource}. */ -public final class ConcatenatingMediaSourcePlaybackPreparer implements PlaybackPreparer { - - private final ExoPlayer exoPlayer; - private final ConcatenatingMediaSource concatenatingMediaSource; - - /** - * Creates a concatenating media source playback preparer. - * - * @param exoPlayer The player to prepare. - * @param concatenatingMediaSource The concatenating media source with which to prepare the - * player. - */ - public ConcatenatingMediaSourcePlaybackPreparer( - ExoPlayer exoPlayer, ConcatenatingMediaSource concatenatingMediaSource) { - this.exoPlayer = exoPlayer; - this.concatenatingMediaSource = Assertions.checkNotNull(concatenatingMediaSource); - } - - @Override - public void preparePlayback() { - exoPlayer.setMediaSource(concatenatingMediaSource); - exoPlayer.prepare(); - } -} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java new file mode 100644 index 0000000000..96a8f7d05a --- /dev/null +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -0,0 +1,197 @@ +/* + * Copyright 2020 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 com.google.android.exoplayer2.ext.media2; + +import android.content.Context; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.media2.common.FileMediaItem; +import androidx.media2.common.MediaMetadata; +import androidx.media2.common.UriMediaItem; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.source.ClippingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceFactory; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** Default implementation of both {@link MediaItemConverter} and {@link MediaSourceFactory}. */ +public final class DefaultMediaItemConverter implements MediaItemConverter, MediaSourceFactory { + private static final int[] SUPPORTED_TYPES = + new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + private final Context context; + private final DataSource.Factory dataSourceFactory; + + /** + * Default constructor with {@link DefaultDataSourceFactory}. + * + * @param context The context. + */ + public DefaultMediaItemConverter(Context context) { + this( + context, + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); + } + + /** + * Default constructor with {@link DataSource.Factory}. + * + * @param context The {@link Context}. + * @param dataSourceFactory The {@link DataSource.Factory} to create {@link MediaSource} from + * {@link MediaItem ExoPlayer MediaItem}. + */ + public DefaultMediaItemConverter(Context context, DataSource.Factory dataSourceFactory) { + this.context = Assertions.checkNotNull(context); + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); + } + + // Implements MediaItemConverter + + @Override + public MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { + if (androidXMediaItem instanceof FileMediaItem) { + throw new IllegalStateException("FileMediaItem isn't supported"); + } + + com.google.android.exoplayer2.MediaItem.Builder exoplayerMediaItemBuilder = + new com.google.android.exoplayer2.MediaItem.Builder(); + + // Set mediaItem as tag for creating MediaSource via MediaSourceFactory methods. + exoplayerMediaItemBuilder.setTag(androidXMediaItem); + + // Media id or Uri must be present. Get it from androidx.MediaItem if possible. + Uri uri = null; + String mediaId = null; + if (androidXMediaItem instanceof UriMediaItem) { + UriMediaItem uriMediaItem = (UriMediaItem) androidXMediaItem; + uri = uriMediaItem.getUri(); + } + MediaMetadata metadata = androidXMediaItem.getMetadata(); + if (metadata != null) { + mediaId = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + String uriString = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI); + if (uriString != null) { + uri = Uri.parse(uriString); + } + } + if (uri == null) { + // Generate a Uri to make it non-null. If not, tag will be ignored. + uri = Uri.parse("exoplayer://" + androidXMediaItem.hashCode()); + } + exoplayerMediaItemBuilder.setUri(uri); + exoplayerMediaItemBuilder.setMediaId(mediaId); + + // These are actually aren't needed, because MediaSource will be generated only via tag. + // However, fills in the exoplayer2.MediaItem's fields as much as possible just in case. + if (androidXMediaItem.getStartPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + exoplayerMediaItemBuilder.setClipStartPositionMs(androidXMediaItem.getStartPosition()); + exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); + } + if (androidXMediaItem.getEndPosition() != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + exoplayerMediaItemBuilder.setClipEndPositionMs(androidXMediaItem.getEndPosition()); + exoplayerMediaItemBuilder.setClipRelativeToDefaultPosition(true); + } + + return exoplayerMediaItemBuilder.build(); + } + + @Override + public androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem) { + Assertions.checkNotNull(exoplayerMediaItem); + MediaItem.PlaybackProperties playbackProperties = + Assertions.checkNotNull(exoplayerMediaItem.playbackProperties); + Object tag = playbackProperties.tag; + if (!(tag instanceof androidx.media2.common.MediaItem)) { + throw new IllegalStateException( + "DefaultMediaItemConverter cannot understand " + + exoplayerMediaItem + + ". Unexpected tag " + + tag + + " in PlaybackProperties"); + } + return (androidx.media2.common.MediaItem) tag; + } + + // Implements MediaSourceFactory + + @Override + public MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager) { + // No-op + return this; + } + + @Override + public MediaSourceFactory setLoadErrorHandlingPolicy( + @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + // No-op + return this; + } + + @Override + public int[] getSupportedTypes() { + return SUPPORTED_TYPES; + } + + @Override + public MediaSource createMediaSource(com.google.android.exoplayer2.MediaItem exoplayerMediaItem) { + Assertions.checkNotNull( + exoplayerMediaItem.playbackProperties, + "DefaultMediaItemConverter cannot understand " + + exoplayerMediaItem + + ". PlaybackProperties is missing."); + Object tag = exoplayerMediaItem.playbackProperties.tag; + if (!(tag instanceof androidx.media2.common.MediaItem)) { + throw new IllegalStateException( + "DefaultMediaItemConverter cannot understand " + + exoplayerMediaItem + + ". Unexpected tag " + + tag + + " in PlaybackProperties"); + } + androidx.media2.common.MediaItem androidXMediaItem = (androidx.media2.common.MediaItem) tag; + + // Create a source for the item. + MediaSource mediaSource = + Utils.createUnclippedMediaSource(context, dataSourceFactory, androidXMediaItem); + + // Apply clipping if needed. + long startPosition = androidXMediaItem.getStartPosition(); + long endPosition = androidXMediaItem.getEndPosition(); + if (startPosition != 0L || endPosition != androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + if (endPosition == androidx.media2.common.MediaItem.POSITION_UNKNOWN) { + endPosition = C.TIME_END_OF_SOURCE; + } + // Disable the initial discontinuity to give seamless transitions to clips. + mediaSource = + new ClippingMediaSource( + mediaSource, + C.msToUs(startPosition), + C.msToUs(endPosition), + /* enableInitialDiscontinuity= */ false, + /* allowDynamicClippingUpdates= */ false, + /* relativeToDefaultPosition= */ true); + } + return mediaSource; + } +} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java new file mode 100644 index 0000000000..34a3d1c314 --- /dev/null +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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 com.google.android.exoplayer2.ext.media2; + +import com.google.android.exoplayer2.MediaItem; + +/** + * Converter for between {@link MediaItem AndroidX MediaItem} and {@link + * com.google.android.exoplayer2.MediaItem ExoPlayer MediaItem}. + */ +public interface MediaItemConverter { + /** + * Converts {@link androidx.media2.common.MediaItem AndroidX MediaItem} to {@link MediaItem + * ExoPlayer MediaItem}. + */ + MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem androidXMediaItem); + + /** + * Converts {@link MediaItem ExoPlayer MediaItem} to {@link androidx.media2.common.MediaItem + * AndroidX MediaItem}. + */ + androidx.media2.common.MediaItem convertToAndroidXMediaItem(MediaItem exoplayerMediaItem); +} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java index 7e3e3b4ac5..875ab918d8 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlayerWrapper.java @@ -18,19 +18,25 @@ package com.google.android.exoplayer2.ext.media2; import androidx.annotation.IntRange; import androidx.annotation.Nullable; +import androidx.core.util.ObjectsCompat; import androidx.media.AudioAttributesCompat; -import androidx.media2.common.MediaItem; +import androidx.media2.common.CallbackMediaItem; import androidx.media2.common.MediaMetadata; import androidx.media2.common.SessionPlayer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.PlaybackPreparer; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -38,6 +44,7 @@ import java.util.List; * the {@link SessionPlayer} API. */ /* package */ final class PlayerWrapper { + private static final String TAG = "PlayerWrapper"; /** Listener for player wrapper events. */ public interface Listener { @@ -49,28 +56,30 @@ import java.util.List; void onPlayerStateChanged(/* @SessionPlayer.PlayerState */ int playerState); /** Called when the player is prepared. */ - void onPrepared(MediaItem mediaItem, int bufferingPercentage); + void onPrepared(androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); /** Called when a seek request has completed. */ void onSeekCompleted(); /** Called when the player rebuffers. */ - void onBufferingStarted(MediaItem mediaItem); + void onBufferingStarted(androidx.media2.common.MediaItem androidXMediaItem); /** Called when the player becomes ready again after rebuffering. */ - void onBufferingEnded(MediaItem mediaItem, int bufferingPercentage); + void onBufferingEnded( + androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); /** Called periodically with the player's buffered position as a percentage. */ - void onBufferingUpdate(MediaItem mediaItem, int bufferingPercentage); + void onBufferingUpdate( + androidx.media2.common.MediaItem androidXMediaItem, int bufferingPercentage); /** Called when current media item is changed. */ - void onCurrentMediaItemChanged(MediaItem mediaItem); + void onCurrentMediaItemChanged(androidx.media2.common.MediaItem androidXMediaItem); /** Called when playback of the item list has ended. */ void onPlaybackEnded(); /** Called when the player encounters an error. */ - void onError(@Nullable MediaItem mediaItem); + void onError(@Nullable androidx.media2.common.MediaItem androidXMediaItem); /** Called when the playlist is changed */ void onPlaylistChanged(); @@ -95,35 +104,36 @@ import java.util.List; private final Runnable pollBufferRunnable; private final Player player; - private final PlaylistManager playlistManager; - private final PlaybackPreparer playbackPreparer; + private final MediaItemConverter mediaItemConverter; private final ControlDispatcher controlDispatcher; private final ComponentListener componentListener; + private final List cachedPlaylist; + @Nullable private MediaMetadata playlistMetadata; + private final List cachedMediaItems; + private boolean prepared; private boolean rebuffering; private int currentWindowIndex; + private boolean loggedUnexpectedTimelineChanges; + private boolean ignoreTimelineUpdates; /** * Creates a new ExoPlayer wrapper. * - * @param listener A listener for player wrapper events. - * @param player The player to handle commands - * @param playlistManager The playlist manager to handle playlist commands - * @param playbackPreparer The playback preparer to prepare - * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching - * changes to the player. + * @param listener A {@link Listener}. + * @param player The {@link Player}. + * @param mediaItemConverter The {@link MediaItemConverter}. + * @param controlDispatcher A {@link ControlDispatcher}. */ PlayerWrapper( Listener listener, Player player, - PlaylistManager playlistManager, - PlaybackPreparer playbackPreparer, + MediaItemConverter mediaItemConverter, ControlDispatcher controlDispatcher) { this.listener = listener; this.player = player; - this.playlistManager = playlistManager; - this.playbackPreparer = playbackPreparer; + this.mediaItemConverter = mediaItemConverter; this.controlDispatcher = controlDispatcher; componentListener = new ComponentListener(); @@ -136,51 +146,119 @@ import java.util.List; handler = new PlayerHandler(player.getApplicationLooper()); pollBufferRunnable = new PollBufferRunnable(); + cachedPlaylist = new ArrayList<>(); + cachedMediaItems = new ArrayList<>(); currentWindowIndex = C.INDEX_UNSET; } - public boolean setMediaItem(MediaItem mediaItem) { - boolean handled = playlistManager.setMediaItem(player, mediaItem); - if (handled) { - currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player); - } - return handled; + public boolean setMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { + return setPlaylist(Collections.singletonList(androidXMediaItem), /* metadata= */ null); } - public boolean setPlaylist(List playlist, @Nullable MediaMetadata metadata) { - boolean handled = playlistManager.setPlaylist(player, playlist, metadata); - if (handled) { - currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player); + public boolean setPlaylist( + List playlist, @Nullable MediaMetadata metadata) { + // Check for duplication. + for (int i = 0; i < playlist.size(); i++) { + androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i); + Assertions.checkArgument(playlist.indexOf(androidXMediaItem) == i); } - return handled; + + this.cachedPlaylist.clear(); + this.cachedPlaylist.addAll(playlist); + this.playlistMetadata = metadata; + this.cachedMediaItems.clear(); + List exoplayerMediaItems = new ArrayList<>(); + for (int i = 0; i < playlist.size(); i++) { + androidx.media2.common.MediaItem androidXMediaItem = playlist.get(i); + MediaItem exoplayerMediaItem = + Assertions.checkNotNull( + mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); + exoplayerMediaItems.add(exoplayerMediaItem); + } + this.cachedMediaItems.addAll(exoplayerMediaItems); + + player.setMediaItems(exoplayerMediaItems, /* resetPosition= */ true); + + currentWindowIndex = getCurrentMediaItemIndex(); + return true; } - public boolean addPlaylistItem(int index, MediaItem item) { - return playlistManager.addPlaylistItem(player, index, item); + public boolean addPlaylistItem(int index, androidx.media2.common.MediaItem androidXMediaItem) { + Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem)); + index = Util.constrainValue(index, 0, cachedPlaylist.size()); + + cachedPlaylist.add(index, androidXMediaItem); + MediaItem exoplayerMediaItem = + Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); + cachedMediaItems.add(index, exoplayerMediaItem); + player.addMediaItem(index, exoplayerMediaItem); + return true; } public boolean removePlaylistItem(@IntRange(from = 0) int index) { - return playlistManager.removePlaylistItem(player, index); + androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.remove(index); + releaseMediaItem(androidXMediaItemToRemove); + cachedMediaItems.remove(index); + player.removeMediaItem(index); + return true; } - public boolean replacePlaylistItem(int index, MediaItem item) { - return playlistManager.replacePlaylistItem(player, index, item); + public boolean replacePlaylistItem( + int index, androidx.media2.common.MediaItem androidXMediaItem) { + Assertions.checkArgument(!cachedPlaylist.contains(androidXMediaItem)); + index = Util.constrainValue(index, 0, cachedPlaylist.size()); + + androidx.media2.common.MediaItem androidXMediaItemToRemove = cachedPlaylist.get(index); + cachedPlaylist.set(index, androidXMediaItem); + releaseMediaItem(androidXMediaItemToRemove); + MediaItem exoplayerMediaItemToAdd = + Assertions.checkNotNull(mediaItemConverter.convertToExoPlayerMediaItem(androidXMediaItem)); + cachedMediaItems.set(index, exoplayerMediaItemToAdd); + + ignoreTimelineUpdates = true; + player.removeMediaItem(index); + ignoreTimelineUpdates = false; + player.addMediaItem(index, exoplayerMediaItemToAdd); + return true; } public boolean skipToPreviousPlaylistItem() { - return playlistManager.skipToPreviousPlaylistItem(player, controlDispatcher); + Timeline timeline = player.getCurrentTimeline(); + Assertions.checkState(!timeline.isEmpty()); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET) { + return controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET); + } + return false; } public boolean skipToNextPlaylistItem() { - return playlistManager.skipToNextPlaylistItem(player, controlDispatcher); + Timeline timeline = player.getCurrentTimeline(); + Assertions.checkState(!timeline.isEmpty()); + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + return controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET); + } + return false; } public boolean skipToPlaylistItem(@IntRange(from = 0) int index) { - return playlistManager.skipToPlaylistItem(player, controlDispatcher, index); + Timeline timeline = player.getCurrentTimeline(); + Assertions.checkState(!timeline.isEmpty()); + // Use checkState() instead of checkIndex() for throwing IllegalStateException. + // checkIndex() throws IndexOutOfBoundsException which maps the RESULT_ERROR_BAD_VALUE + // but RESULT_ERROR_INVALID_STATE with IllegalStateException is expected here. + Assertions.checkState(0 <= index && index < timeline.getWindowCount()); + int windowIndex = player.getCurrentWindowIndex(); + if (windowIndex != index) { + return controlDispatcher.dispatchSeekTo(player, index, C.TIME_UNSET); + } + return false; } public boolean updatePlaylistMetadata(@Nullable MediaMetadata metadata) { - return playlistManager.updatePlaylistMetadata(player, metadata); + this.playlistMetadata = metadata; + return true; } public boolean setRepeatMode(int repeatMode) { @@ -194,13 +272,13 @@ import java.util.List; } @Nullable - public List getPlaylist() { - return playlistManager.getPlaylist(player); + public List getCachedPlaylist() { + return new ArrayList<>(cachedPlaylist); } @Nullable public MediaMetadata getPlaylistMetadata() { - return playlistManager.getPlaylistMetadata(player); + return playlistMetadata; } public int getRepeatMode() { @@ -212,7 +290,7 @@ import java.util.List; } public int getCurrentMediaItemIndex() { - return playlistManager.getCurrentMediaItemIndex(player); + return cachedPlaylist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex(); } public int getPreviousMediaItemIndex() { @@ -224,21 +302,22 @@ import java.util.List; } @Nullable - public MediaItem getCurrentMediaItem() { - return playlistManager.getCurrentMediaItem(player); + public androidx.media2.common.MediaItem getCurrentMediaItem() { + int index = getCurrentMediaItemIndex(); + return (index != C.INDEX_UNSET) ? cachedPlaylist.get(index) : null; } public boolean prepare() { if (prepared) { return false; } - playbackPreparer.preparePlayback(); + player.prepare(); return true; } public boolean play() { if (player.getPlaybackState() == Player.STATE_ENDED) { - int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player); + int currentWindowIndex = getCurrentMediaItemIndex(); boolean seekHandled = controlDispatcher.dispatchSeekTo(player, currentWindowIndex, /* positionMs= */ 0); if (!seekHandled) { @@ -263,7 +342,7 @@ import java.util.List; } public boolean seekTo(long position) { - int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player); + int currentWindowIndex = getCurrentMediaItemIndex(); return controlDispatcher.dispatchSeekTo(player, currentWindowIndex, position); } @@ -348,7 +427,7 @@ import java.util.List; } public boolean canSkipToPlaylistItem() { - @Nullable List playlist = getPlaylist(); + @Nullable List playlist = getCachedPlaylist(); return playlist != null && playlist.size() > 1; } @@ -394,11 +473,11 @@ import java.util.List; } private void handlePositionDiscontinuity(@Player.DiscontinuityReason int reason) { - int currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player); + int currentWindowIndex = getCurrentMediaItemIndex(); if (this.currentWindowIndex != currentWindowIndex) { this.currentWindowIndex = currentWindowIndex; - MediaItem currentMediaItem = - Assertions.checkNotNull(playlistManager.getCurrentMediaItem(player)); + androidx.media2.common.MediaItem currentMediaItem = + Assertions.checkNotNull(getCurrentMediaItem()); listener.onCurrentMediaItemChanged(currentMediaItem); } else { listener.onSeekCompleted(); @@ -422,42 +501,90 @@ import java.util.List; listener.onPlaybackSpeedChanged(playbackSpeed); } - private void handleTimelineChanged() { - playlistManager.onTimelineChanged(player); + private void handleTimelineChanged(Timeline timeline) { + if (ignoreTimelineUpdates) { + return; + } + updateCachedPlaylistAndMediaItems(timeline); listener.onPlaylistChanged(); } + // Update cached playlist, if the ExoPlayer Player's Timeline is unexpectedly changed without + // using SessionPlayer interface. + private void updateCachedPlaylistAndMediaItems(Timeline currentTimeline) { + // Check whether ExoPlayer media items are the same as expected. + Timeline.Window window = new Timeline.Window(); + int windowCount = currentTimeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + currentTimeline.getWindow(i, window); + if (i >= cachedMediaItems.size() + || !ObjectsCompat.equals(cachedMediaItems.get(i), window.mediaItem)) { + if (!loggedUnexpectedTimelineChanges) { + Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt."); + loggedUnexpectedTimelineChanges = true; + } + + androidx.media2.common.MediaItem oldAndroidXMediaItem = cachedPlaylist.get(i); + releaseMediaItem(oldAndroidXMediaItem); + + androidx.media2.common.MediaItem androidXMediaItem = + Assertions.checkNotNull( + mediaItemConverter.convertToAndroidXMediaItem(window.mediaItem)); + if (i < cachedMediaItems.size()) { + cachedMediaItems.set(i, window.mediaItem); + cachedPlaylist.set(i, androidXMediaItem); + } else { + cachedMediaItems.add(window.mediaItem); + cachedPlaylist.add(androidXMediaItem); + } + } + } + if (cachedMediaItems.size() > windowCount) { + if (!loggedUnexpectedTimelineChanges) { + Log.w(TAG, "Timeline was unexpectedly changed. Playlist will be rebuilt."); + loggedUnexpectedTimelineChanges = true; + } + while (cachedMediaItems.size() > windowCount) { + cachedMediaItems.remove(windowCount); + cachedPlaylist.remove(windowCount); + } + } + } + private void handleAudioAttributesChanged(AudioAttributes audioAttributes) { listener.onAudioAttributesChanged(Utils.getAudioAttributesCompat(audioAttributes)); } private void updateBufferingAndScheduleNextPollBuffer() { - MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem()); - listener.onBufferingUpdate(mediaItem, player.getBufferedPercentage()); + androidx.media2.common.MediaItem androidXMediaItem = + Assertions.checkNotNull(getCurrentMediaItem()); + listener.onBufferingUpdate(androidXMediaItem, player.getBufferedPercentage()); handler.removeCallbacks(pollBufferRunnable); handler.postDelayed(pollBufferRunnable, POLL_BUFFER_INTERVAL_MS); } private void maybeNotifyBufferingEvents() { - MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem()); + androidx.media2.common.MediaItem androidXMediaItem = + Assertions.checkNotNull(getCurrentMediaItem()); if (prepared && !rebuffering) { rebuffering = true; - listener.onBufferingStarted(mediaItem); + listener.onBufferingStarted(androidXMediaItem); } } private void maybeNotifyReadyEvents() { - MediaItem mediaItem = Assertions.checkNotNull(getCurrentMediaItem()); + androidx.media2.common.MediaItem androidXMediaItem = + Assertions.checkNotNull(getCurrentMediaItem()); boolean prepareComplete = !prepared; if (prepareComplete) { prepared = true; handlePositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); listener.onPlayerStateChanged(SessionPlayer.PLAYER_STATE_PAUSED); - listener.onPrepared(mediaItem, player.getBufferedPercentage()); + listener.onPrepared(androidXMediaItem, player.getBufferedPercentage()); } if (rebuffering) { rebuffering = false; - listener.onBufferingEnded(mediaItem, player.getBufferedPercentage()); + listener.onBufferingEnded(androidXMediaItem, player.getBufferedPercentage()); } } @@ -469,6 +596,16 @@ import java.util.List; } } + private void releaseMediaItem(androidx.media2.common.MediaItem androidXMediaItem) { + try { + if (androidXMediaItem instanceof CallbackMediaItem) { + ((CallbackMediaItem) androidXMediaItem).getDataSourceCallback().close(); + } + } catch (IOException e) { + Log.w(TAG, "Error releasing media item " + androidXMediaItem, e); + } + } + private final class ComponentListener implements Player.EventListener, AudioListener { // Player.EventListener implementation. @@ -510,7 +647,7 @@ import java.util.List; @Override public void onTimelineChanged(Timeline timeline, int reason) { - handleTimelineChanged(); + handleTimelineChanged(timeline); } // AudioListener implementation. diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlaylistManager.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlaylistManager.java deleted file mode 100644 index 0eab1bc3a3..0000000000 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/PlaylistManager.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2019 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 com.google.android.exoplayer2.ext.media2; - -import androidx.annotation.Nullable; -import androidx.media2.common.MediaItem; -import androidx.media2.common.MediaMetadata; -import androidx.media2.common.SessionPlayer; -import com.google.android.exoplayer2.ControlDispatcher; -import com.google.android.exoplayer2.Player; -import java.util.List; - -/** Interface that handles playlist edit and navigation operations. */ -public interface PlaylistManager { - /** - * See {@link SessionPlayer#setPlaylist(List, MediaMetadata)}. - * - * @param player The player used to build SessionPlayer together. - * @param playlist A list of {@link MediaItem} objects to set as a play list. - * @param metadata The metadata of the playlist. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean setPlaylist(Player player, List playlist, @Nullable MediaMetadata metadata); - - /** - * See {@link SessionPlayer#addPlaylistItem(int, MediaItem)}. - * - * @param player The player used to build SessionPlayer together. - * @param index The index of the item you want to add in the playlist. - * @param mediaItem The media item you want to add. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean addPlaylistItem(Player player, int index, MediaItem mediaItem); - - /** - * See {@link SessionPlayer#removePlaylistItem(int)}. - * - * @param player The player used to build SessionPlayer together. - * @param index The index of the item you want to remove in the playlist. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean removePlaylistItem(Player player, int index); - - /** - * See {@link SessionPlayer#replacePlaylistItem(int, MediaItem)}. - * - * @param player The player used to build SessionPlayer together. - * @param mediaItem The media item you want to replace with. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean replacePlaylistItem(Player player, int index, MediaItem mediaItem); - - /** - * See {@link SessionPlayer#setMediaItem(MediaItem)}. - * - * @param player The player used to build SessionPlayer together. - * @param mediaItem The media item you want to set. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean setMediaItem(Player player, MediaItem mediaItem); - - /** - * See {@link SessionPlayer#updatePlaylistMetadata(MediaMetadata)}. - * - * @param player The player used to build SessionPlayer together. - * @param metadata The metadata of the playlist. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean updatePlaylistMetadata(Player player, @Nullable MediaMetadata metadata); - - /** - * See {@link SessionPlayer#skipToNextPlaylistItem()}. - * - * @param player The player used to build SessionPlayer together. - * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching - * changes to the player. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean skipToNextPlaylistItem(Player player, ControlDispatcher controlDispatcher); - - /** - * See {@link SessionPlayer#skipToPreviousPlaylistItem()}. - * - * @param player The player used to build SessionPlayer together. - * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching - * changes to the player. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean skipToPreviousPlaylistItem(Player player, ControlDispatcher controlDispatcher); - - /** - * See {@link SessionPlayer#skipToPlaylistItem(int)}. - * - * @param player The player used to build SessionPlayer together. - * @param controlDispatcher A {@link ControlDispatcher} that should be used for dispatching - * changes to the player. - * @return true if the operation was dispatched. False if suppressed. - */ - boolean skipToPlaylistItem(Player player, ControlDispatcher controlDispatcher, int index); - - /** - * See {@link SessionPlayer#getCurrentMediaItemIndex()}. - * - * @param player The player used to build SessionPlayer together. - * @return The current media item index - */ - int getCurrentMediaItemIndex(Player player); - - /** - * See {@link SessionPlayer#getCurrentMediaItem()}. - * - * @param player The player used to build SessionPlayer together. - * @return The current media item index - */ - @Nullable - MediaItem getCurrentMediaItem(Player player); - - /** - * See {@link SessionPlayer#setPlaylist(List, MediaMetadata)}. - * - * @param player The player used to build SessionPlayer together. - * @return The playlist. - */ - @Nullable - List getPlaylist(Player player); - - /** - * See {@link SessionPlayer#getPlaylistMetadata()}. - * - * @param player The player used to build SessionPlayer together. - * @return The metadata of the playlist. - */ - @Nullable - MediaMetadata getPlaylistMetadata(Player player); - - /** - * Called when the player's timeline is changed. - * - * @param player The player used to build SessionPlayer together. - */ - void onTimelineChanged(Player player); -} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java index f97dbc79e2..41a696932a 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/SessionPlayerConnector.java @@ -31,7 +31,6 @@ import androidx.media2.common.SessionPlayer; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.Assertions; import com.google.common.util.concurrent.ListenableFuture; @@ -101,30 +100,23 @@ public final class SessionPlayerConnector extends SessionPlayer { * Creates an instance using {@link DefaultControlDispatcher} to dispatch player commands. * * @param player The player to wrap. - * @param playlistManager The {@link PlaylistManager}. - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param mediaItemConverter The {@link MediaItemConverter}. */ - public SessionPlayerConnector( - Player player, PlaylistManager playlistManager, PlaybackPreparer playbackPreparer) { - this(player, playlistManager, playbackPreparer, new DefaultControlDispatcher()); + public SessionPlayerConnector(Player player, MediaItemConverter mediaItemConverter) { + this(player, mediaItemConverter, new DefaultControlDispatcher()); } /** * Creates an instance using the provided {@link ControlDispatcher} to dispatch player commands. * * @param player The player to wrap. - * @param playlistManager The {@link PlaylistManager}. - * @param playbackPreparer The {@link PlaybackPreparer}. + * @param mediaItemConverter The {@link MediaItemConverter}. * @param controlDispatcher The {@link ControlDispatcher}. */ public SessionPlayerConnector( - Player player, - PlaylistManager playlistManager, - PlaybackPreparer playbackPreparer, - ControlDispatcher controlDispatcher) { + Player player, MediaItemConverter mediaItemConverter, ControlDispatcher controlDispatcher) { Assertions.checkNotNull(player); - Assertions.checkNotNull(playlistManager); - Assertions.checkNotNull(playbackPreparer); + Assertions.checkNotNull(mediaItemConverter); Assertions.checkNotNull(controlDispatcher); state = PLAYER_STATE_IDLE; @@ -132,8 +124,7 @@ public final class SessionPlayerConnector extends SessionPlayer { taskHandlerExecutor = taskHandler::postOrRun; ExoPlayerWrapperListener playerListener = new ExoPlayerWrapperListener(); PlayerWrapper playerWrapper = - new PlayerWrapper( - playerListener, player, playlistManager, playbackPreparer, controlDispatcher); + new PlayerWrapper(playerListener, player, mediaItemConverter, controlDispatcher); this.player = playerWrapper; playerCommandQueue = new PlayerCommandQueue(this.player, taskHandler); @@ -393,7 +384,7 @@ public final class SessionPlayerConnector extends SessionPlayer { @Override @Nullable public List getPlaylist() { - return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getPlaylist); + return runPlayerCallableBlockingWithNullOnException(/* callable= */ player::getCachedPlaylist); } @Override @@ -566,7 +557,7 @@ public final class SessionPlayerConnector extends SessionPlayer { } private void handlePlaylistChangedOnHandler() { - List currentPlaylist = player.getPlaylist(); + List currentPlaylist = player.getCachedPlaylist(); boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist); this.currentPlaylist = currentPlaylist; MediaMetadata playlistMetadata = player.getPlaylistMetadata(); diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/TimelinePlaylistManager.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/TimelinePlaylistManager.java deleted file mode 100644 index 70d87a9665..0000000000 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/TimelinePlaylistManager.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright 2019 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 com.google.android.exoplayer2.ext.media2; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.Nullable; -import androidx.media2.common.CallbackMediaItem; -import androidx.media2.common.MediaItem; -import androidx.media2.common.MediaMetadata; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ControlDispatcher; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ClippingMediaSource; -import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * A default {@link PlaylistManager} implementation based on {@link ConcatenatingMediaSource} that - * maps a {@link MediaItem} in the playlist to a {@link MediaSource}. - * - *

When it's used, {@link Player}'s Timeline shouldn't be changed directly, and should be only - * changed via {@link TimelinePlaylistManager}. If it's not, internal playlist would be out of sync - * with the actual {@link Timeline}. If you need to change Timeline directly, build your own {@link - * PlaylistManager} instead. - */ -public class TimelinePlaylistManager implements PlaylistManager { - private static final String TAG = "TimelinePlaylistManager"; - - private final MediaSourceFactory sourceFactory; - private final ConcatenatingMediaSource concatenatingMediaSource; - private final List playlist; - @Nullable private MediaMetadata playlistMetadata; - private boolean loggedUnexpectedTimelineChanges; - - /** Factory to create {@link MediaSource}s. */ - public interface MediaSourceFactory { - /** - * Creates a {@link MediaSource} for the given {@link MediaItem}. - * - * @param mediaItem The {@link MediaItem} to create a media source for. - * @return A {@link MediaSource} or {@code null} if no source can be created for the given - * description. - */ - @Nullable - MediaSource createMediaSource(MediaItem mediaItem); - } - - /** - * Default implementation of the {@link MediaSourceFactory}. - * - *

This doesn't support the {@link androidx.media2.common.FileMediaItem}. - */ - public static final class DefaultMediaSourceFactory implements MediaSourceFactory { - private final Context context; - private final DataSource.Factory dataSourceFactory; - - /** - * Default constructor with {@link DefaultDataSourceFactory}. - * - * @param context The context. - */ - public DefaultMediaSourceFactory(Context context) { - this( - context, - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); - } - - /** - * Default constructor with {@link DataSource.Factory}. - * - * @param context The context. - * @param dataSourceFactory The dataSourceFactory to create {@link MediaSource} from {@link - * MediaItem}. - */ - public DefaultMediaSourceFactory(Context context, DataSource.Factory dataSourceFactory) { - this.context = Assertions.checkNotNull(context); - this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); - } - - @Override - public MediaSource createMediaSource(MediaItem mediaItem) { - // Create a source for the item. - MediaSource mediaSource = - Utils.createUnclippedMediaSource(context, dataSourceFactory, mediaItem); - - // Apply clipping if needed. - long startPosition = mediaItem.getStartPosition(); - long endPosition = mediaItem.getEndPosition(); - if (startPosition != 0L || endPosition != MediaItem.POSITION_UNKNOWN) { - if (endPosition == MediaItem.POSITION_UNKNOWN) { - endPosition = C.TIME_END_OF_SOURCE; - } - // Disable the initial discontinuity to give seamless transitions to clips. - mediaSource = - new ClippingMediaSource( - mediaSource, - C.msToUs(startPosition), - C.msToUs(endPosition), - /* enableInitialDiscontinuity= */ false, - /* allowDynamicClippingUpdates= */ false, - /* relativeToDefaultPosition= */ true); - } - - return mediaSource; - } - } - - /** - * Creates a new {@link TimelinePlaylistManager} with the {@link DefaultMediaSourceFactory}. - * - * @param context The context. - * @param concatenatingMediaSource The {@link ConcatenatingMediaSource} to manipulate. - */ - public TimelinePlaylistManager( - Context context, ConcatenatingMediaSource concatenatingMediaSource) { - this(concatenatingMediaSource, new DefaultMediaSourceFactory(context)); - } - - /** - * Creates a new {@link TimelinePlaylistManager} with a given mediaSourceFactory. - * - * @param concatenatingMediaSource The {@link ConcatenatingMediaSource} to manipulate. - * @param sourceFactory The {@link MediaSourceFactory} to build media sources. - */ - public TimelinePlaylistManager( - ConcatenatingMediaSource concatenatingMediaSource, MediaSourceFactory sourceFactory) { - this.concatenatingMediaSource = concatenatingMediaSource; - this.sourceFactory = sourceFactory; - this.playlist = new ArrayList<>(); - } - - @Override - public boolean setPlaylist( - Player player, List playlist, @Nullable MediaMetadata metadata) { - // Check for duplication. - for (int i = 0; i < playlist.size(); i++) { - MediaItem mediaItem = playlist.get(i); - Assertions.checkArgument(playlist.indexOf(mediaItem) == i); - } - for (MediaItem mediaItem : this.playlist) { - if (!playlist.contains(mediaItem)) { - releaseMediaItem(mediaItem); - } - } - this.playlist.clear(); - this.playlist.addAll(playlist); - this.playlistMetadata = metadata; - - concatenatingMediaSource.clear(); - - List mediaSources = new ArrayList<>(); - for (int i = 0; i < playlist.size(); i++) { - MediaItem mediaItem = playlist.get(i); - MediaSource mediaSource = createMediaSource(mediaItem); - mediaSources.add(mediaSource); - } - concatenatingMediaSource.addMediaSources(mediaSources); - return true; - } - - @Override - public boolean addPlaylistItem(Player player, int index, MediaItem mediaItem) { - Assertions.checkArgument(!playlist.contains(mediaItem)); - index = Util.constrainValue(index, 0, playlist.size()); - - playlist.add(index, mediaItem); - MediaSource mediaSource = createMediaSource(mediaItem); - concatenatingMediaSource.addMediaSource(index, mediaSource); - return true; - } - - @Override - public boolean removePlaylistItem(Player player, int index) { - MediaItem mediaItemToRemove = playlist.remove(index); - releaseMediaItem(mediaItemToRemove); - concatenatingMediaSource.removeMediaSource(index); - return true; - } - - @Override - public boolean replacePlaylistItem(Player player, int index, MediaItem mediaItem) { - Assertions.checkArgument(!playlist.contains(mediaItem)); - index = Util.constrainValue(index, 0, playlist.size()); - - MediaItem mediaItemToRemove = playlist.get(index); - playlist.set(index, mediaItem); - releaseMediaItem(mediaItemToRemove); - - MediaSource mediaSourceToAdd = createMediaSource(mediaItem); - concatenatingMediaSource.removeMediaSource(index); - concatenatingMediaSource.addMediaSource(index, mediaSourceToAdd); - return true; - } - - @Override - public boolean setMediaItem(Player player, MediaItem mediaItem) { - List playlist = new ArrayList<>(); - playlist.add(mediaItem); - return setPlaylist(player, playlist, /* metadata */ null); - } - - @Override - public boolean updatePlaylistMetadata(Player player, @Nullable MediaMetadata metadata) { - this.playlistMetadata = metadata; - return true; - } - - @Override - public boolean skipToNextPlaylistItem(Player player, ControlDispatcher controlDispatcher) { - Timeline timeline = player.getCurrentTimeline(); - Assertions.checkState(!timeline.isEmpty()); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - return controlDispatcher.dispatchSeekTo(player, nextWindowIndex, C.TIME_UNSET); - } - return false; - } - - @Override - public boolean skipToPreviousPlaylistItem(Player player, ControlDispatcher controlDispatcher) { - Timeline timeline = player.getCurrentTimeline(); - Assertions.checkState(!timeline.isEmpty()); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET) { - return controlDispatcher.dispatchSeekTo(player, previousWindowIndex, C.TIME_UNSET); - } - return false; - } - - @Override - public boolean skipToPlaylistItem(Player player, ControlDispatcher controlDispatcher, int index) { - Timeline timeline = player.getCurrentTimeline(); - Assertions.checkState(!timeline.isEmpty()); - // Use checkState() instead of checkIndex() for throwing IllegalStateException. - // checkIndex() throws IndexOutOfBoundsException which maps the RESULT_ERROR_BAD_VALUE - // but RESULT_ERROR_INVALID_STATE with IllegalStateException is expected here. - Assertions.checkState(0 <= index && index < timeline.getWindowCount()); - int windowIndex = player.getCurrentWindowIndex(); - if (windowIndex != index) { - return controlDispatcher.dispatchSeekTo(player, index, C.TIME_UNSET); - } - return false; - } - - @Override - public int getCurrentMediaItemIndex(Player player) { - return playlist.isEmpty() ? C.INDEX_UNSET : player.getCurrentWindowIndex(); - } - - @Override - @Nullable - public MediaItem getCurrentMediaItem(Player player) { - int index = getCurrentMediaItemIndex(player); - return (index != C.INDEX_UNSET) ? playlist.get(index) : null; - } - - @Override - public List getPlaylist(Player player) { - return new ArrayList<>(playlist); - } - - @Override - @Nullable - public MediaMetadata getPlaylistMetadata(Player player) { - return playlistMetadata; - } - - @Override - public void onTimelineChanged(Player player) { - checkTimelineWindowCountEqualsToPlaylistSize(player); - } - - private void releaseMediaItem(MediaItem mediaItem) { - try { - if (mediaItem instanceof CallbackMediaItem) { - ((CallbackMediaItem) mediaItem).getDataSourceCallback().close(); - } - } catch (IOException e) { - Log.w(TAG, "Error releasing media item " + mediaItem, e); - } - } - - private MediaSource createMediaSource(MediaItem mediaItem) { - return Assertions.checkNotNull( - sourceFactory.createMediaSource(mediaItem), - "createMediaSource() failed, mediaItem=" + mediaItem); - } - - // Check whether Timeline's window count matches with the playlist size, and leave log for - // mismatch. It's to check whether the Timeline and playlist are out of sync or not at the best - // effort. - private void checkTimelineWindowCountEqualsToPlaylistSize(Player player) { - if (player.getPlaybackState() == Player.STATE_IDLE) { - // Cannot do check in STATE_IDLE, because Timeline isn't available. - return; - } - Timeline timeline = player.getCurrentTimeline(); - if ((playlist == null && timeline.getWindowCount() == 1) - || (playlist != null && playlist.size() == timeline.getWindowCount())) { - return; - } - if (!loggedUnexpectedTimelineChanges) { - Log.w(TAG, "Timeline is unexpectedly changed. Playlist can be out of sync."); - loggedUnexpectedTimelineChanges = true; - } - } -} diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/Utils.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/Utils.java index cd86bc18dd..cb2441d9c6 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/Utils.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/Utils.java @@ -54,9 +54,9 @@ import com.google.android.exoplayer2.util.Util; * as the tag of the source. */ public static MediaSource createUnclippedMediaSource( - Context context, DataSource.Factory dataSourceFactory, MediaItem mediaItem) { - if (mediaItem instanceof UriMediaItem) { - Uri uri = ((UriMediaItem) mediaItem).getUri(); + Context context, DataSource.Factory dataSourceFactory, MediaItem androidXMediaItem) { + if (androidXMediaItem instanceof UriMediaItem) { + Uri uri = ((UriMediaItem) androidXMediaItem).getUri(); if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { String path = Assertions.checkNotNull(uri.getPath()); int resourceIdentifier; @@ -74,16 +74,16 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkState(resourceIdentifier != 0); uri = RawResourceDataSource.buildRawResourceUri(resourceIdentifier); } - return createMediaSource(uri, dataSourceFactory, /* tag= */ mediaItem); - } else if (mediaItem instanceof CallbackMediaItem) { - CallbackMediaItem callbackMediaItem = (CallbackMediaItem) mediaItem; + return createMediaSource(uri, dataSourceFactory, /* tag= */ androidXMediaItem); + } else if (androidXMediaItem instanceof CallbackMediaItem) { + CallbackMediaItem callbackMediaItem = (CallbackMediaItem) androidXMediaItem; dataSourceFactory = DataSourceCallbackDataSource.getFactory(callbackMediaItem.getDataSourceCallback()); return new ProgressiveMediaSource.Factory(dataSourceFactory, sExtractorsFactory) .createMediaSource( new com.google.android.exoplayer2.MediaItem.Builder() .setUri(Uri.EMPTY) - .setTag(mediaItem) + .setTag(androidXMediaItem) .build()); } else { throw new IllegalStateException();