Migrate to use Player's top level playlist API

Being specific, this includes following changes
  - Remove PlaylistManager and TimelinePlaylistManager
    and use Player's playlist API directly.
  - Replace ConcatenatingMediaSource uses with
    ExoPlayer MediaItem.
  - Replace PlaybackPreparer uses with Player#prepare()
  - Add MediaItemConverter for developers to customize
    converting AndroidX MediaItems to ExoPlayer MediaItems
    and vice-versa.
  - Add DefaultMediaItemConverter for providing default
    implementation of both MediaItemConverter
    and MediaSourceFactory.

Note that removing PlaylistManager loses the ability
to suppress individual playlist API. But decided to remove
for simpler API set. The feature can be added back later
via explicit request.

PiperOrigin-RevId: 326463492
This commit is contained in:
jaewan 2020-08-13 17:36:59 +01:00 committed by kim-vde
parent 1b9992cf7a
commit e6bf7bd0ff
10 changed files with 478 additions and 644 deletions

View File

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

View File

@ -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,18 +187,23 @@ 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);
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, timelinePlaylistManager, playbackPreparer, controlDispatcher)) {
new SessionPlayerConnector(simpleExoPlayer, converter, controlDispatcher)) {
assertPlayerResult(player.play(), RESULT_INFO_SKIPPED);
}
} finally {
if (simpleExoPlayer != null) {
simpleExoPlayer.release();
}
}
}
@Test

View File

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

View File

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

View File

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

View File

@ -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<androidx.media2.common.MediaItem> cachedPlaylist;
@Nullable private MediaMetadata playlistMetadata;
private final List<MediaItem> 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<MediaItem> playlist, @Nullable MediaMetadata metadata) {
boolean handled = playlistManager.setPlaylist(player, playlist, metadata);
if (handled) {
currentWindowIndex = playlistManager.getCurrentMediaItemIndex(player);
}
return handled;
public boolean setPlaylist(
List<androidx.media2.common.MediaItem> 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);
}
public boolean addPlaylistItem(int index, MediaItem item) {
return playlistManager.addPlaylistItem(player, index, item);
this.cachedPlaylist.clear();
this.cachedPlaylist.addAll(playlist);
this.playlistMetadata = metadata;
this.cachedMediaItems.clear();
List<MediaItem> 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, 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<MediaItem> getPlaylist() {
return playlistManager.getPlaylist(player);
public List<androidx.media2.common.MediaItem> 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<MediaItem> playlist = getPlaylist();
@Nullable List<androidx.media2.common.MediaItem> 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.

View File

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

View File

@ -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<MediaItem> 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<MediaItem> currentPlaylist = player.getPlaylist();
List<MediaItem> currentPlaylist = player.getCachedPlaylist();
boolean notifyCurrentPlaylist = !ObjectsCompat.equals(this.currentPlaylist, currentPlaylist);
this.currentPlaylist = currentPlaylist;
MediaMetadata playlistMetadata = player.getPlaylistMetadata();

View File

@ -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}.
*
* <p>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<MediaItem> 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}.
*
* <p>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<MediaItem> 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<MediaSource> 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<MediaItem> 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<MediaItem> 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;
}
}
}

View File

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