mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Rollback of 4ad4e3e4fc
*** Original commit ***
Rollback of 3b22db33ba
*** Original commit ***
add top-level playlist API to ExoPlayer
Public design doc:
https://docs.google.com/document/d/11h0S91KI5TB3NNZUtsCzg0S7r6nyTnF_tDZZAtmY93g
Issue: #6161
***
***
PiperOrigin-RevId: 275276158
This commit is contained in:
parent
9ec2d2fb36
commit
36f8bd78f7
34 changed files with 4313 additions and 772 deletions
|
|
@ -50,7 +50,6 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep
|
||||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
import com.google.android.exoplayer2.source.MediaSourceFactory;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
|
@ -79,6 +78,8 @@ import java.lang.reflect.Constructor;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||||
public class PlayerActivity extends AppCompatActivity
|
public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
@ -140,7 +141,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
|
|
||||||
private DataSource.Factory dataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
private MediaSource mediaSource;
|
private List<MediaSource> mediaSources;
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
private DefaultTrackSelector.Parameters trackSelectorParameters;
|
||||||
private DebugTextViewHelper debugViewHelper;
|
private DebugTextViewHelper debugViewHelper;
|
||||||
|
|
@ -342,12 +343,10 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
private void initializePlayer() {
|
private void initializePlayer() {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
Intent intent = getIntent();
|
Intent intent = getIntent();
|
||||||
|
mediaSources = createTopLevelMediaSources(intent);
|
||||||
mediaSource = createTopLevelMediaSource(intent);
|
if (mediaSources.isEmpty()) {
|
||||||
if (mediaSource == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSelection.Factory trackSelectionFactory;
|
TrackSelection.Factory trackSelectionFactory;
|
||||||
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
String abrAlgorithm = intent.getStringExtra(ABR_ALGORITHM_EXTRA);
|
||||||
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
if (abrAlgorithm == null || ABR_ALGORITHM_DEFAULT.equals(abrAlgorithm)) {
|
||||||
|
|
@ -388,12 +387,12 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
if (haveStartPosition) {
|
if (haveStartPosition) {
|
||||||
player.seekTo(startWindow, startPosition);
|
player.seekTo(startWindow, startPosition);
|
||||||
}
|
}
|
||||||
player.prepare(mediaSource, !haveStartPosition, false);
|
player.setMediaItems(mediaSources, /* resetPosition= */ !haveStartPosition);
|
||||||
|
player.prepare();
|
||||||
updateButtonVisibility();
|
updateButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
private List<MediaSource> createTopLevelMediaSources(Intent intent) {
|
||||||
private MediaSource createTopLevelMediaSource(Intent intent) {
|
|
||||||
String action = intent.getAction();
|
String action = intent.getAction();
|
||||||
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
boolean actionIsListView = ACTION_VIEW_LIST.equals(action);
|
||||||
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
if (!actionIsListView && !ACTION_VIEW.equals(action)) {
|
||||||
|
|
@ -421,34 +420,30 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSource[] mediaSources = new MediaSource[samples.length];
|
List<MediaSource> mediaSources = new ArrayList<>();
|
||||||
for (int i = 0; i < samples.length; i++) {
|
for (UriSample sample : samples) {
|
||||||
mediaSources[i] = createLeafMediaSource(samples[i]);
|
mediaSources.add(createLeafMediaSource(sample));
|
||||||
}
|
}
|
||||||
MediaSource mediaSource =
|
if (seenAdsTagUri && mediaSources.size() == 1) {
|
||||||
mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources);
|
|
||||||
|
|
||||||
if (seenAdsTagUri) {
|
|
||||||
Uri adTagUri = samples[0].adTagUri;
|
Uri adTagUri = samples[0].adTagUri;
|
||||||
if (actionIsListView) {
|
if (!adTagUri.equals(loadedAdTagUri)) {
|
||||||
showToast(R.string.unsupported_ads_in_concatenation);
|
releaseAdsLoader();
|
||||||
} else {
|
loadedAdTagUri = adTagUri;
|
||||||
if (!adTagUri.equals(loadedAdTagUri)) {
|
|
||||||
releaseAdsLoader();
|
|
||||||
loadedAdTagUri = adTagUri;
|
|
||||||
}
|
|
||||||
MediaSource adsMediaSource = createAdsMediaSource(mediaSource, adTagUri);
|
|
||||||
if (adsMediaSource != null) {
|
|
||||||
mediaSource = adsMediaSource;
|
|
||||||
} else {
|
|
||||||
showToast(R.string.ima_not_loaded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
MediaSource adsMediaSource = createAdsMediaSource(mediaSources.get(0), adTagUri);
|
||||||
|
if (adsMediaSource != null) {
|
||||||
|
mediaSources.set(0, adsMediaSource);
|
||||||
|
} else {
|
||||||
|
showToast(R.string.ima_not_loaded);
|
||||||
|
}
|
||||||
|
} else if (seenAdsTagUri && mediaSources.size() > 1) {
|
||||||
|
showToast(R.string.unsupported_ads_in_concatenation);
|
||||||
|
releaseAdsLoader();
|
||||||
} else {
|
} else {
|
||||||
releaseAdsLoader();
|
releaseAdsLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaSource;
|
return mediaSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource createLeafMediaSource(UriSample parameters) {
|
private MediaSource createLeafMediaSource(UriSample parameters) {
|
||||||
|
|
@ -535,7 +530,7 @@ public class PlayerActivity extends AppCompatActivity
|
||||||
debugViewHelper = null;
|
debugViewHelper = null;
|
||||||
player.release();
|
player.release();
|
||||||
player = null;
|
player = null;
|
||||||
mediaSource = null;
|
mediaSources = null;
|
||||||
trackSelector = null;
|
trackSelector = null;
|
||||||
}
|
}
|
||||||
if (adsLoader != null) {
|
if (adsLoader != null) {
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@ public final class CastPlayer extends BasePlayer {
|
||||||
private int pendingSeekCount;
|
private int pendingSeekCount;
|
||||||
private int pendingSeekWindowIndex;
|
private int pendingSeekWindowIndex;
|
||||||
private long pendingSeekPositionMs;
|
private long pendingSeekPositionMs;
|
||||||
private boolean waitingForInitialTimeline;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param castContext The context from which the cast session is obtained.
|
* @param castContext The context from which the cast session is obtained.
|
||||||
|
|
@ -174,7 +173,6 @@ public final class CastPlayer extends BasePlayer {
|
||||||
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) {
|
||||||
if (remoteMediaClient != null) {
|
if (remoteMediaClient != null) {
|
||||||
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
positionMs = positionMs != C.TIME_UNSET ? positionMs : 0;
|
||||||
waitingForInitialTimeline = true;
|
|
||||||
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode),
|
||||||
positionMs, null);
|
positionMs, null);
|
||||||
}
|
}
|
||||||
|
|
@ -619,15 +617,13 @@ public final class CastPlayer extends BasePlayer {
|
||||||
|
|
||||||
private void updateTimelineAndNotifyIfChanged() {
|
private void updateTimelineAndNotifyIfChanged() {
|
||||||
if (updateTimeline()) {
|
if (updateTimeline()) {
|
||||||
@Player.TimelineChangeReason
|
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
|
||||||
int reason =
|
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
|
||||||
waitingForInitialTimeline
|
|
||||||
? Player.TIMELINE_CHANGE_REASON_PREPARED
|
|
||||||
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
|
||||||
waitingForInitialTimeline = false;
|
|
||||||
notificationsBatch.add(
|
notificationsBatch.add(
|
||||||
new ListenerNotificationTask(
|
new ListenerNotificationTask(
|
||||||
listener -> listener.onTimelineChanged(currentTimeline, reason)));
|
listener ->
|
||||||
|
listener.onTimelineChanged(
|
||||||
|
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import java.util.ArrayList;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final Timeline timeline;
|
private final Timeline timeline;
|
||||||
|
|
||||||
private boolean prepared;
|
|
||||||
@Player.State private int state;
|
@Player.State private int state;
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
private long position;
|
private long position;
|
||||||
|
|
@ -47,13 +46,17 @@ import java.util.ArrayList;
|
||||||
timeline = Timeline.EMPTY;
|
timeline = Timeline.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the timeline on this fake player, which notifies listeners with the changed timeline. */
|
/**
|
||||||
public void updateTimeline(Timeline timeline) {
|
* Sets the timeline on this fake player, which notifies listeners with the changed timeline and
|
||||||
|
* the given timeline change reason.
|
||||||
|
*
|
||||||
|
* @param timeline The new timeline.
|
||||||
|
* @param timelineChangeReason The reason for the timeline change.
|
||||||
|
*/
|
||||||
|
public void updateTimeline(Timeline timeline, @TimelineChangeReason int timelineChangeReason) {
|
||||||
for (Player.EventListener listener : listeners) {
|
for (Player.EventListener listener : listeners) {
|
||||||
listener.onTimelineChanged(
|
listener.onTimelineChanged(timeline, timelineChangeReason);
|
||||||
timeline, prepared ? TIMELINE_CHANGE_REASON_DYNAMIC : TIMELINE_CHANGE_REASON_PREPARED);
|
|
||||||
}
|
}
|
||||||
prepared = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,9 @@ public class ImaAdsLoaderTest {
|
||||||
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
public void onAdPlaybackState(AdPlaybackState adPlaybackState) {
|
||||||
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
|
adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
|
||||||
this.adPlaybackState = adPlaybackState;
|
this.adPlaybackState = adPlaybackState;
|
||||||
fakeExoPlayer.updateTimeline(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
|
fakeExoPlayer.updateTimeline(
|
||||||
|
new SinglePeriodAdTimeline(contentTimeline, adPlaybackState),
|
||||||
|
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,14 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2.source;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.Timeline;
|
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
|
||||||
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
|
/** Abstract base class for the concatenation of one or more {@link Timeline}s. */
|
||||||
/* package */ abstract class AbstractConcatenatedTimeline extends Timeline {
|
public abstract class AbstractConcatenatedTimeline extends Timeline {
|
||||||
|
|
||||||
private final int childCount;
|
private final int childCount;
|
||||||
private final ShuffleOrder shuffleOrder;
|
private final ShuffleOrder shuffleOrder;
|
||||||
|
|
@ -28,6 +28,7 @@ import com.google.android.exoplayer2.source.LoopingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||||
import com.google.android.exoplayer2.text.TextRenderer;
|
import com.google.android.exoplayer2.text.TextRenderer;
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||||
|
|
@ -39,6 +40,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
import com.google.android.exoplayer2.util.Clock;
|
import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link
|
* An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link
|
||||||
|
|
@ -139,7 +141,7 @@ public interface ExoPlayer extends Player {
|
||||||
private LoadControl loadControl;
|
private LoadControl loadControl;
|
||||||
private BandwidthMeter bandwidthMeter;
|
private BandwidthMeter bandwidthMeter;
|
||||||
private Looper looper;
|
private Looper looper;
|
||||||
private AnalyticsCollector analyticsCollector;
|
@Nullable private AnalyticsCollector analyticsCollector;
|
||||||
private boolean useLazyPreparation;
|
private boolean useLazyPreparation;
|
||||||
private boolean buildCalled;
|
private boolean buildCalled;
|
||||||
|
|
||||||
|
|
@ -170,7 +172,7 @@ public interface ExoPlayer extends Player {
|
||||||
new DefaultLoadControl(),
|
new DefaultLoadControl(),
|
||||||
DefaultBandwidthMeter.getSingletonInstance(context),
|
DefaultBandwidthMeter.getSingletonInstance(context),
|
||||||
Util.getLooper(),
|
Util.getLooper(),
|
||||||
new AnalyticsCollector(Clock.DEFAULT),
|
/* analyticsCollector= */ null,
|
||||||
/* useLazyPreparation= */ true,
|
/* useLazyPreparation= */ true,
|
||||||
Clock.DEFAULT);
|
Clock.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
@ -197,7 +199,7 @@ public interface ExoPlayer extends Player {
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
Looper looper,
|
Looper looper,
|
||||||
AnalyticsCollector analyticsCollector,
|
@Nullable AnalyticsCollector analyticsCollector,
|
||||||
boolean useLazyPreparation,
|
boolean useLazyPreparation,
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
Assertions.checkArgument(renderers.length > 0);
|
Assertions.checkArgument(renderers.length > 0);
|
||||||
|
|
@ -318,38 +320,156 @@ public interface ExoPlayer extends Player {
|
||||||
Assertions.checkState(!buildCalled);
|
Assertions.checkState(!buildCalled);
|
||||||
buildCalled = true;
|
buildCalled = true;
|
||||||
return new ExoPlayerImpl(
|
return new ExoPlayerImpl(
|
||||||
renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
|
renderers,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
bandwidthMeter,
|
||||||
|
analyticsCollector,
|
||||||
|
useLazyPreparation,
|
||||||
|
clock,
|
||||||
|
looper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the {@link Looper} associated with the playback thread. */
|
/** Returns the {@link Looper} associated with the playback thread. */
|
||||||
Looper getPlaybackLooper();
|
Looper getPlaybackLooper();
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #prepare()} instead. */
|
||||||
* Retries a failed or stopped playback. Does nothing if the player has been reset, or if playback
|
@Deprecated
|
||||||
* has not failed or been stopped.
|
|
||||||
*/
|
|
||||||
void retry();
|
void retry();
|
||||||
|
|
||||||
/**
|
/** @deprecated Use {@link #setMediaItem(MediaSource)} and {@link #prepare()} instead. */
|
||||||
* Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code
|
@Deprecated
|
||||||
* prepare(mediaSource, true, true)}.
|
|
||||||
*/
|
|
||||||
void prepare(MediaSource mediaSource);
|
void prepare(MediaSource mediaSource);
|
||||||
|
|
||||||
|
/** @deprecated Use {@link #setMediaItems(List, int, long)} and {@link #prepare()} instead. */
|
||||||
|
@Deprecated
|
||||||
|
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
|
||||||
|
|
||||||
|
/** Prepares the player. */
|
||||||
|
void prepare();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
|
* Clears the playlist and adds the specified {@link MediaSource MediaSources}.
|
||||||
* position the default position in the first {@link Timeline.Window}.
|
|
||||||
*
|
*
|
||||||
* @param mediaSource The {@link MediaSource} to play.
|
* @param mediaItems The new {@link MediaSource MediaSources}.
|
||||||
|
*/
|
||||||
|
void setMediaItems(List<MediaSource> mediaItems);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the playlist and adds the specified {@link MediaSource MediaSources}.
|
||||||
|
*
|
||||||
|
* @param mediaItems The new {@link MediaSource MediaSources}.
|
||||||
* @param resetPosition Whether the playback position should be reset to the default position in
|
* @param resetPosition Whether the playback position should be reset to the default position in
|
||||||
* the first {@link Timeline.Window}. If false, playback will start from the position defined
|
* the first {@link Timeline.Window}. If false, playback will start from the position defined
|
||||||
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
|
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
|
||||||
* @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
|
|
||||||
* Should be true unless the player is being prepared to play the same media as it was playing
|
|
||||||
* previously (e.g. if playback failed and is being retried).
|
|
||||||
*/
|
*/
|
||||||
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
|
void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the playlist and adds the specified {@link MediaSource MediaSources}.
|
||||||
|
*
|
||||||
|
* @param mediaItems The new {@link MediaSource MediaSources}.
|
||||||
|
* @param startWindowIndex The window index to start playback from. If {@link C#INDEX_UNSET} is
|
||||||
|
* passed, the current position is not reset.
|
||||||
|
* @param startPositionMs The position in milliseconds to start playback from. If {@link
|
||||||
|
* C#TIME_UNSET} is passed, the default position of the given window is used. In any case, if
|
||||||
|
* {@code startWindowIndex} is set to {@link C#INDEX_UNSET}, this parameter is ignored and the
|
||||||
|
* position is not reset at all.
|
||||||
|
*/
|
||||||
|
void setMediaItems(List<MediaSource> mediaItems, int startWindowIndex, long startPositionMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the playlist and adds the specified {@link MediaSource}.
|
||||||
|
*
|
||||||
|
* @param mediaItem The new {@link MediaSource}.
|
||||||
|
*/
|
||||||
|
void setMediaItem(MediaSource mediaItem);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the playlist and adds the specified {@link MediaSource}.
|
||||||
|
*
|
||||||
|
* @param mediaItem The new {@link MediaSource}.
|
||||||
|
* @param startPositionMs The position in milliseconds to start playback from.
|
||||||
|
*/
|
||||||
|
void setMediaItem(MediaSource mediaItem, long startPositionMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a media item to the end of the playlist.
|
||||||
|
*
|
||||||
|
* @param mediaSource The {@link MediaSource} to add.
|
||||||
|
*/
|
||||||
|
void addMediaItem(MediaSource mediaSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a media item at the given index of the playlist.
|
||||||
|
*
|
||||||
|
* @param index The index at which to add the item.
|
||||||
|
* @param mediaSource The {@link MediaSource} to add.
|
||||||
|
*/
|
||||||
|
void addMediaItem(int index, MediaSource mediaSource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a list of media items to the end of the playlist.
|
||||||
|
*
|
||||||
|
* @param mediaSources The {@link MediaSource MediaSources} to add.
|
||||||
|
*/
|
||||||
|
void addMediaItems(List<MediaSource> mediaSources);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a list of media items at the given index of the playlist.
|
||||||
|
*
|
||||||
|
* @param index The index at which to add the media items.
|
||||||
|
* @param mediaSources The {@link MediaSource MediaSources} to add.
|
||||||
|
*/
|
||||||
|
void addMediaItems(int index, List<MediaSource> mediaSources);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the media item at the current index to the new index.
|
||||||
|
*
|
||||||
|
* @param currentIndex The current index of the media item to move.
|
||||||
|
* @param newIndex The new index of the media item. If the new index is larger than the size of
|
||||||
|
* the playlist the item is moved to the end of the playlist.
|
||||||
|
*/
|
||||||
|
void moveMediaItem(int currentIndex, int newIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the media item range to the new index.
|
||||||
|
*
|
||||||
|
* @param fromIndex The start of the range to move.
|
||||||
|
* @param toIndex The first item not to be included in the range (exclusive).
|
||||||
|
* @param newIndex The new index of the first media item of the range. If the new index is larger
|
||||||
|
* than the size of the remaining playlist after removing the range, the range is moved to the
|
||||||
|
* end of the playlist.
|
||||||
|
*/
|
||||||
|
void moveMediaItems(int fromIndex, int toIndex, int newIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the media item at the given index of the playlist.
|
||||||
|
*
|
||||||
|
* @param index The index at which to remove the media item.
|
||||||
|
* @return The removed {@link MediaSource} or null if no item exists at the given index.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
MediaSource removeMediaItem(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a range of media items from the playlist.
|
||||||
|
*
|
||||||
|
* @param fromIndex The index at which to start removing media items.
|
||||||
|
* @param toIndex The index of the first item to be kept (exclusive).
|
||||||
|
*/
|
||||||
|
void removeMediaItems(int fromIndex, int toIndex);
|
||||||
|
|
||||||
|
/** Clears the playlist. */
|
||||||
|
void clearMediaItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the shuffle order.
|
||||||
|
*
|
||||||
|
* @param shuffleOrder The shuffle order.
|
||||||
|
*/
|
||||||
|
void setShuffleOrder(ShuffleOrder shuffleOrder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message
|
* Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,7 @@ public final class ExoPlayerFactory {
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
bandwidthMeter,
|
bandwidthMeter,
|
||||||
analyticsCollector,
|
analyticsCollector,
|
||||||
|
/* useLazyPreparation= */ true,
|
||||||
Clock.DEFAULT,
|
Clock.DEFAULT,
|
||||||
looper);
|
looper);
|
||||||
}
|
}
|
||||||
|
|
@ -344,6 +345,13 @@ public final class ExoPlayerFactory {
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
Looper looper) {
|
Looper looper) {
|
||||||
return new ExoPlayerImpl(
|
return new ExoPlayerImpl(
|
||||||
renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper);
|
renderers,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
bandwidthMeter,
|
||||||
|
/* analyticsCollector= */ null,
|
||||||
|
/* useLazyPreparation= */ true,
|
||||||
|
Clock.DEFAULT,
|
||||||
|
looper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ import android.os.Message;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.PlayerMessage.Target;
|
import com.google.android.exoplayer2.PlayerMessage.Target;
|
||||||
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
|
|
@ -35,6 +37,9 @@ import com.google.android.exoplayer2.util.Clock;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,19 +66,20 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
private final CopyOnWriteArrayList<ListenerHolder> listeners;
|
||||||
private final Timeline.Period period;
|
private final Timeline.Period period;
|
||||||
private final ArrayDeque<Runnable> pendingListenerNotifications;
|
private final ArrayDeque<Runnable> pendingListenerNotifications;
|
||||||
|
private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
|
||||||
|
private final boolean useLazyPreparation;
|
||||||
|
|
||||||
private MediaSource mediaSource;
|
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
@PlaybackSuppressionReason private int playbackSuppressionReason;
|
@PlaybackSuppressionReason private int playbackSuppressionReason;
|
||||||
@RepeatMode private int repeatMode;
|
@RepeatMode private int repeatMode;
|
||||||
private boolean shuffleModeEnabled;
|
private boolean shuffleModeEnabled;
|
||||||
private int pendingOperationAcks;
|
private int pendingOperationAcks;
|
||||||
private boolean hasPendingPrepare;
|
|
||||||
private boolean hasPendingSeek;
|
private boolean hasPendingSeek;
|
||||||
private boolean foregroundMode;
|
private boolean foregroundMode;
|
||||||
private int pendingSetPlaybackParametersAcks;
|
private int pendingSetPlaybackParametersAcks;
|
||||||
private PlaybackParameters playbackParameters;
|
private PlaybackParameters playbackParameters;
|
||||||
private SeekParameters seekParameters;
|
private SeekParameters seekParameters;
|
||||||
|
private ShuffleOrder shuffleOrder;
|
||||||
|
|
||||||
// Playback information when there is no pending seek/set source operation.
|
// Playback information when there is no pending seek/set source operation.
|
||||||
private PlaybackInfo playbackInfo;
|
private PlaybackInfo playbackInfo;
|
||||||
|
|
@ -90,6 +96,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||||
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
||||||
|
* @param analyticsCollector The {@link AnalyticsCollector} that will be used by the instance.
|
||||||
|
* @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
|
||||||
|
* loads and other initial preparation steps happen immediately. If true, these initial
|
||||||
|
* preparations are triggered only when the player starts buffering the media.
|
||||||
* @param clock The {@link Clock} that will be used by the instance.
|
* @param clock The {@link Clock} that will be used by the instance.
|
||||||
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
* used to call listeners on.
|
* used to call listeners on.
|
||||||
|
|
@ -100,6 +110,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
|
@Nullable AnalyticsCollector analyticsCollector,
|
||||||
|
boolean useLazyPreparation,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
Looper looper) {
|
Looper looper) {
|
||||||
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
|
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
|
||||||
|
|
@ -107,10 +119,13 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
Assertions.checkState(renderers.length > 0);
|
Assertions.checkState(renderers.length > 0);
|
||||||
this.renderers = Assertions.checkNotNull(renderers);
|
this.renderers = Assertions.checkNotNull(renderers);
|
||||||
this.trackSelector = Assertions.checkNotNull(trackSelector);
|
this.trackSelector = Assertions.checkNotNull(trackSelector);
|
||||||
this.playWhenReady = false;
|
this.useLazyPreparation = useLazyPreparation;
|
||||||
this.repeatMode = Player.REPEAT_MODE_OFF;
|
playWhenReady = false;
|
||||||
this.shuffleModeEnabled = false;
|
repeatMode = Player.REPEAT_MODE_OFF;
|
||||||
this.listeners = new CopyOnWriteArrayList<>();
|
shuffleModeEnabled = false;
|
||||||
|
listeners = new CopyOnWriteArrayList<>();
|
||||||
|
mediaSourceHolders = new ArrayList<>();
|
||||||
|
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
|
||||||
emptyTrackSelectorResult =
|
emptyTrackSelectorResult =
|
||||||
new TrackSelectorResult(
|
new TrackSelectorResult(
|
||||||
new RendererConfiguration[renderers.length],
|
new RendererConfiguration[renderers.length],
|
||||||
|
|
@ -120,6 +135,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
playbackParameters = PlaybackParameters.DEFAULT;
|
playbackParameters = PlaybackParameters.DEFAULT;
|
||||||
seekParameters = SeekParameters.DEFAULT;
|
seekParameters = SeekParameters.DEFAULT;
|
||||||
playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
|
playbackSuppressionReason = PLAYBACK_SUPPRESSION_REASON_NONE;
|
||||||
|
maskingWindowIndex = C.INDEX_UNSET;
|
||||||
eventHandler =
|
eventHandler =
|
||||||
new Handler(looper) {
|
new Handler(looper) {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -129,6 +145,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
};
|
};
|
||||||
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
|
playbackInfo = PlaybackInfo.createDummy(/* startPositionUs= */ 0, emptyTrackSelectorResult);
|
||||||
pendingListenerNotifications = new ArrayDeque<>();
|
pendingListenerNotifications = new ArrayDeque<>();
|
||||||
|
if (analyticsCollector != null) {
|
||||||
|
analyticsCollector.setPlayer(this);
|
||||||
|
}
|
||||||
internalPlayer =
|
internalPlayer =
|
||||||
new ExoPlayerImplInternal(
|
new ExoPlayerImplInternal(
|
||||||
renderers,
|
renderers,
|
||||||
|
|
@ -139,6 +158,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
playWhenReady,
|
playWhenReady,
|
||||||
repeatMode,
|
repeatMode,
|
||||||
shuffleModeEnabled,
|
shuffleModeEnabled,
|
||||||
|
analyticsCollector,
|
||||||
eventHandler,
|
eventHandler,
|
||||||
clock);
|
clock);
|
||||||
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
|
internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
|
||||||
|
|
@ -212,41 +232,162 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public void retry() {
|
public void retry() {
|
||||||
if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) {
|
prepare();
|
||||||
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare() {
|
||||||
|
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepare(MediaSource mediaSource) {
|
|
||||||
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
|
||||||
this.mediaSource = mediaSource;
|
|
||||||
PlaybackInfo playbackInfo =
|
PlaybackInfo playbackInfo =
|
||||||
getResetPlaybackInfo(
|
getResetPlaybackInfo(
|
||||||
resetPosition,
|
/* clearPlaylist= */ false,
|
||||||
resetState,
|
|
||||||
/* resetError= */ true,
|
/* resetError= */ true,
|
||||||
/* playbackState= */ Player.STATE_BUFFERING);
|
/* playbackState= */ this.playbackInfo.timeline.isEmpty()
|
||||||
|
? Player.STATE_ENDED
|
||||||
|
: Player.STATE_BUFFERING);
|
||||||
// Trigger internal prepare first before updating the playback info and notifying external
|
// Trigger internal prepare first before updating the playback info and notifying external
|
||||||
// listeners to ensure that new operations issued in the listener notifications reach the
|
// listeners to ensure that new operations issued in the listener notifications reach the
|
||||||
// player after this prepare. The internal player can't change the playback info immediately
|
// player after this prepare. The internal player can't change the playback info immediately
|
||||||
// because it uses a callback.
|
// because it uses a callback.
|
||||||
hasPendingPrepare = true;
|
|
||||||
pendingOperationAcks++;
|
pendingOperationAcks++;
|
||||||
internalPlayer.prepare(mediaSource, resetPosition, resetState);
|
internalPlayer.prepare();
|
||||||
updatePlaybackInfo(
|
updatePlaybackInfo(
|
||||||
playbackInfo,
|
playbackInfo,
|
||||||
/* positionDiscontinuity= */ false,
|
/* positionDiscontinuity= */ false,
|
||||||
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
||||||
TIMELINE_CHANGE_REASON_RESET,
|
/* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
|
||||||
/* seekProcessed= */ false);
|
/* seekProcessed= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public void prepare(MediaSource mediaSource) {
|
||||||
|
setMediaItem(mediaSource);
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
||||||
|
setMediaItem(
|
||||||
|
mediaSource, /* startPositionMs= */ resetPosition ? C.TIME_UNSET : getCurrentPosition());
|
||||||
|
prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem) {
|
||||||
|
setMediaItems(Collections.singletonList(mediaItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem, long startPositionMs) {
|
||||||
|
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaSource> mediaItems) {
|
||||||
|
setMediaItems(
|
||||||
|
mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs */ C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
|
||||||
|
setMediaItemsInternal(
|
||||||
|
mediaItems,
|
||||||
|
/* startWindowIndex= */ C.INDEX_UNSET,
|
||||||
|
/* startPositionMs= */ C.TIME_UNSET,
|
||||||
|
/* resetToDefaultPosition= */ resetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(
|
||||||
|
List<MediaSource> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||||
|
setMediaItemsInternal(
|
||||||
|
mediaItems, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(MediaSource mediaSource) {
|
||||||
|
addMediaItems(Collections.singletonList(mediaSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(int index, MediaSource mediaSource) {
|
||||||
|
addMediaItems(index, Collections.singletonList(mediaSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(List<MediaSource> mediaSources) {
|
||||||
|
addMediaItems(/* index= */ mediaSourceHolders.size(), mediaSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(int index, List<MediaSource> mediaSources) {
|
||||||
|
Assertions.checkArgument(index >= 0);
|
||||||
|
pendingOperationAcks++;
|
||||||
|
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
|
||||||
|
Timeline timeline = maskTimeline();
|
||||||
|
internalPlayer.addMediaItems(index, holders, shuffleOrder);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource removeMediaItem(int index) {
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSourceHolders =
|
||||||
|
removeMediaItemsInternal(/* fromIndex= */ index, /* toIndex= */ index + 1);
|
||||||
|
return mediaSourceHolders.isEmpty() ? null : mediaSourceHolders.get(0).mediaSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||||
|
Assertions.checkArgument(toIndex > fromIndex);
|
||||||
|
removeMediaItemsInternal(fromIndex, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItem(int currentIndex, int newIndex) {
|
||||||
|
Assertions.checkArgument(currentIndex != newIndex);
|
||||||
|
moveMediaItems(/* fromIndex= */ currentIndex, /* toIndex= */ currentIndex + 1, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
|
||||||
|
Assertions.checkArgument(
|
||||||
|
fromIndex >= 0
|
||||||
|
&& fromIndex <= toIndex
|
||||||
|
&& toIndex <= mediaSourceHolders.size()
|
||||||
|
&& newFromIndex >= 0);
|
||||||
|
pendingOperationAcks++;
|
||||||
|
newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex));
|
||||||
|
Playlist.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
|
||||||
|
Timeline timeline = maskTimeline();
|
||||||
|
internalPlayer.moveMediaItems(fromIndex, toIndex, newFromIndex, shuffleOrder);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearMediaItems() {
|
||||||
|
if (mediaSourceHolders.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
pendingOperationAcks++;
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
Timeline timeline = maskTimeline();
|
||||||
|
internalPlayer.setShuffleOrder(shuffleOrder);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
|
|
@ -408,13 +549,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop(boolean reset) {
|
public void stop(boolean reset) {
|
||||||
if (reset) {
|
|
||||||
mediaSource = null;
|
|
||||||
}
|
|
||||||
PlaybackInfo playbackInfo =
|
PlaybackInfo playbackInfo =
|
||||||
getResetPlaybackInfo(
|
getResetPlaybackInfo(
|
||||||
/* resetPosition= */ reset,
|
/* clearPlaylist= */ reset,
|
||||||
/* resetState= */ reset,
|
|
||||||
/* resetError= */ reset,
|
/* resetError= */ reset,
|
||||||
/* playbackState= */ Player.STATE_IDLE);
|
/* playbackState= */ Player.STATE_IDLE);
|
||||||
// Trigger internal stop first before updating the playback info and notifying external
|
// Trigger internal stop first before updating the playback info and notifying external
|
||||||
|
|
@ -427,7 +564,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
playbackInfo,
|
playbackInfo,
|
||||||
/* positionDiscontinuity= */ false,
|
/* positionDiscontinuity= */ false,
|
||||||
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
|
||||||
TIMELINE_CHANGE_REASON_RESET,
|
TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
||||||
/* seekProcessed= */ false);
|
/* seekProcessed= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,13 +573,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
|
Log.i(TAG, "Release " + Integer.toHexString(System.identityHashCode(this)) + " ["
|
||||||
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
|
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "] ["
|
||||||
+ ExoPlayerLibraryInfo.registeredModules() + "]");
|
+ ExoPlayerLibraryInfo.registeredModules() + "]");
|
||||||
mediaSource = null;
|
|
||||||
internalPlayer.release();
|
internalPlayer.release();
|
||||||
eventHandler.removeCallbacksAndMessages(null);
|
eventHandler.removeCallbacksAndMessages(null);
|
||||||
playbackInfo =
|
playbackInfo =
|
||||||
getResetPlaybackInfo(
|
getResetPlaybackInfo(
|
||||||
/* resetPosition= */ false,
|
/* clearPlaylist= */ false,
|
||||||
/* resetState= */ false,
|
|
||||||
/* resetError= */ false,
|
/* resetError= */ false,
|
||||||
/* playbackState= */ Player.STATE_IDLE);
|
/* playbackState= */ Player.STATE_IDLE);
|
||||||
}
|
}
|
||||||
|
|
@ -468,12 +603,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCurrentWindowIndex() {
|
public int getCurrentWindowIndex() {
|
||||||
if (shouldMaskPosition()) {
|
int currentWindowIndex = getCurrentWindowIndexInternal();
|
||||||
return maskingWindowIndex;
|
return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
|
||||||
} else {
|
|
||||||
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
|
|
||||||
.windowIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -590,10 +721,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
// Not private so it can be called from an inner class without going through a thunk method.
|
// Not private so it can be called from an inner class without going through a thunk method.
|
||||||
/* package */ void handleEvent(Message msg) {
|
/* package */ void handleEvent(Message msg) {
|
||||||
|
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:
|
case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:
|
||||||
handlePlaybackInfo(
|
handlePlaybackInfo(
|
||||||
(PlaybackInfo) msg.obj,
|
/* playbackInfo= */ (PlaybackInfo) msg.obj,
|
||||||
/* operationAcks= */ msg.arg1,
|
/* operationAcks= */ msg.arg1,
|
||||||
/* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
|
/* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
|
||||||
/* positionDiscontinuityReason= */ msg.arg2);
|
/* positionDiscontinuityReason= */ msg.arg2);
|
||||||
|
|
@ -606,6 +738,15 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getCurrentWindowIndexInternal() {
|
||||||
|
if (shouldMaskPosition()) {
|
||||||
|
return maskingWindowIndex;
|
||||||
|
} else {
|
||||||
|
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
|
||||||
|
.windowIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handlePlaybackParameters(
|
private void handlePlaybackParameters(
|
||||||
PlaybackParameters playbackParameters, boolean operationAck) {
|
PlaybackParameters playbackParameters, boolean operationAck) {
|
||||||
if (operationAck) {
|
if (operationAck) {
|
||||||
|
|
@ -638,33 +779,27 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
|
if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) {
|
||||||
// Update the masking variables, which are used when the timeline becomes empty.
|
// Update the masking variables, which are used when the timeline becomes empty.
|
||||||
maskingPeriodIndex = 0;
|
maskingPeriodIndex = 0;
|
||||||
maskingWindowIndex = 0;
|
maskingWindowIndex = C.INDEX_UNSET;
|
||||||
maskingWindowPositionMs = 0;
|
maskingWindowPositionMs = 0;
|
||||||
}
|
}
|
||||||
@Player.TimelineChangeReason
|
|
||||||
int timelineChangeReason =
|
|
||||||
hasPendingPrepare
|
|
||||||
? Player.TIMELINE_CHANGE_REASON_PREPARED
|
|
||||||
: Player.TIMELINE_CHANGE_REASON_DYNAMIC;
|
|
||||||
boolean seekProcessed = hasPendingSeek;
|
boolean seekProcessed = hasPendingSeek;
|
||||||
hasPendingPrepare = false;
|
|
||||||
hasPendingSeek = false;
|
hasPendingSeek = false;
|
||||||
updatePlaybackInfo(
|
updatePlaybackInfo(
|
||||||
playbackInfo,
|
playbackInfo,
|
||||||
positionDiscontinuity,
|
positionDiscontinuity,
|
||||||
positionDiscontinuityReason,
|
positionDiscontinuityReason,
|
||||||
timelineChangeReason,
|
TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
|
||||||
seekProcessed);
|
seekProcessed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlaybackInfo getResetPlaybackInfo(
|
private PlaybackInfo getResetPlaybackInfo(
|
||||||
boolean resetPosition,
|
boolean clearPlaylist, boolean resetError, @Player.State int playbackState) {
|
||||||
boolean resetState,
|
if (clearPlaylist) {
|
||||||
boolean resetError,
|
// Reset list of media source holders which are used for creating the masking timeline.
|
||||||
@Player.State int playbackState) {
|
removeMediaSourceHolders(
|
||||||
if (resetPosition) {
|
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
|
||||||
maskingWindowIndex = 0;
|
maskingWindowIndex = C.INDEX_UNSET;
|
||||||
maskingPeriodIndex = 0;
|
maskingPeriodIndex = 0;
|
||||||
maskingWindowPositionMs = 0;
|
maskingWindowPositionMs = 0;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -672,24 +807,22 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
maskingPeriodIndex = getCurrentPeriodIndex();
|
maskingPeriodIndex = getCurrentPeriodIndex();
|
||||||
maskingWindowPositionMs = getCurrentPosition();
|
maskingWindowPositionMs = getCurrentPosition();
|
||||||
}
|
}
|
||||||
// Also reset period-based PlaybackInfo positions if resetting the state.
|
|
||||||
resetPosition = resetPosition || resetState;
|
|
||||||
MediaPeriodId mediaPeriodId =
|
MediaPeriodId mediaPeriodId =
|
||||||
resetPosition
|
clearPlaylist
|
||||||
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
|
? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period)
|
||||||
: playbackInfo.periodId;
|
: playbackInfo.periodId;
|
||||||
long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs;
|
long startPositionUs = clearPlaylist ? 0 : playbackInfo.positionUs;
|
||||||
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
|
long contentPositionUs = clearPlaylist ? C.TIME_UNSET : playbackInfo.contentPositionUs;
|
||||||
return new PlaybackInfo(
|
return new PlaybackInfo(
|
||||||
resetState ? Timeline.EMPTY : playbackInfo.timeline,
|
clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline,
|
||||||
mediaPeriodId,
|
mediaPeriodId,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
contentPositionUs,
|
contentPositionUs,
|
||||||
playbackState,
|
playbackState,
|
||||||
resetError ? null : playbackInfo.playbackError,
|
resetError ? null : playbackInfo.playbackError,
|
||||||
/* isLoading= */ false,
|
/* isLoading= */ false,
|
||||||
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
|
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
|
||||||
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
|
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
|
||||||
mediaPeriodId,
|
mediaPeriodId,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
/* totalBufferedDurationUs= */ 0,
|
/* totalBufferedDurationUs= */ 0,
|
||||||
|
|
@ -699,8 +832,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
private void updatePlaybackInfo(
|
private void updatePlaybackInfo(
|
||||||
PlaybackInfo playbackInfo,
|
PlaybackInfo playbackInfo,
|
||||||
boolean positionDiscontinuity,
|
boolean positionDiscontinuity,
|
||||||
@Player.DiscontinuityReason int positionDiscontinuityReason,
|
@DiscontinuityReason int positionDiscontinuityReason,
|
||||||
@Player.TimelineChangeReason int timelineChangeReason,
|
@TimelineChangeReason int timelineChangeReason,
|
||||||
boolean seekProcessed) {
|
boolean seekProcessed) {
|
||||||
boolean previousIsPlaying = isPlaying();
|
boolean previousIsPlaying = isPlaying();
|
||||||
// Assign playback info immediately such that all getters return the right values.
|
// Assign playback info immediately such that all getters return the right values.
|
||||||
|
|
@ -721,6 +854,80 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
/* isPlayingChanged= */ previousIsPlaying != isPlaying));
|
/* isPlayingChanged= */ previousIsPlaying != isPlaying));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setMediaItemsInternal(
|
||||||
|
List<MediaSource> mediaItems,
|
||||||
|
int startWindowIndex,
|
||||||
|
long startPositionMs,
|
||||||
|
boolean resetToDefaultPosition) {
|
||||||
|
int currentWindowIndex = getCurrentWindowIndexInternal();
|
||||||
|
long currentPositionMs = getCurrentPosition();
|
||||||
|
pendingOperationAcks++;
|
||||||
|
if (!mediaSourceHolders.isEmpty()) {
|
||||||
|
removeMediaSourceHolders(
|
||||||
|
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size());
|
||||||
|
}
|
||||||
|
List<Playlist.MediaSourceHolder> holders = addMediaSourceHolders(/* index= */ 0, mediaItems);
|
||||||
|
Timeline timeline = maskTimeline();
|
||||||
|
if (resetToDefaultPosition) {
|
||||||
|
startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
|
||||||
|
startPositionMs = C.TIME_UNSET;
|
||||||
|
} else if (startWindowIndex == C.INDEX_UNSET) {
|
||||||
|
startWindowIndex = currentWindowIndex;
|
||||||
|
startPositionMs = currentPositionMs;
|
||||||
|
}
|
||||||
|
internalPlayer.setMediaItems(
|
||||||
|
holders, startWindowIndex, C.msToUs(startPositionMs), shuffleOrder);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Playlist.MediaSourceHolder> addMediaSourceHolders(
|
||||||
|
int index, List<MediaSource> mediaSources) {
|
||||||
|
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
|
||||||
|
for (int i = 0; i < mediaSources.size(); i++) {
|
||||||
|
Playlist.MediaSourceHolder holder =
|
||||||
|
new Playlist.MediaSourceHolder(mediaSources.get(i), useLazyPreparation);
|
||||||
|
holders.add(holder);
|
||||||
|
mediaSourceHolders.add(i + index, holder);
|
||||||
|
}
|
||||||
|
shuffleOrder =
|
||||||
|
shuffleOrder.cloneAndInsert(
|
||||||
|
/* insertionIndex= */ index, /* insertionCount= */ holders.size());
|
||||||
|
return holders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Playlist.MediaSourceHolder> removeMediaItemsInternal(int fromIndex, int toIndex) {
|
||||||
|
Assertions.checkArgument(
|
||||||
|
fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size());
|
||||||
|
pendingOperationAcks++;
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSourceHolders =
|
||||||
|
removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex);
|
||||||
|
Timeline timeline = maskTimeline();
|
||||||
|
internalPlayer.removeMediaItems(fromIndex, toIndex, shuffleOrder);
|
||||||
|
notifyListeners(
|
||||||
|
listener -> listener.onTimelineChanged(timeline, TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
|
||||||
|
return mediaSourceHolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Playlist.MediaSourceHolder> removeMediaSourceHolders(
|
||||||
|
int fromIndex, int toIndexExclusive) {
|
||||||
|
List<Playlist.MediaSourceHolder> removed = new ArrayList<>();
|
||||||
|
for (int i = toIndexExclusive - 1; i >= fromIndex; i--) {
|
||||||
|
removed.add(mediaSourceHolders.remove(i));
|
||||||
|
}
|
||||||
|
shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive);
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timeline maskTimeline() {
|
||||||
|
playbackInfo =
|
||||||
|
playbackInfo.copyWithTimeline(
|
||||||
|
mediaSourceHolders.isEmpty()
|
||||||
|
? Timeline.EMPTY
|
||||||
|
: new Playlist.PlaylistTimeline(mediaSourceHolders, shuffleOrder));
|
||||||
|
return playbackInfo.timeline;
|
||||||
|
}
|
||||||
|
|
||||||
private void notifyListeners(ListenerInvocation listenerInvocation) {
|
private void notifyListeners(ListenerInvocation listenerInvocation) {
|
||||||
CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
|
CopyOnWriteArrayList<ListenerHolder> listenerSnapshot = new CopyOnWriteArrayList<>(listeners);
|
||||||
notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
|
notifyListeners(() -> invokeAll(listenerSnapshot, listenerInvocation));
|
||||||
|
|
@ -756,7 +963,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
private final TrackSelector trackSelector;
|
private final TrackSelector trackSelector;
|
||||||
private final boolean positionDiscontinuity;
|
private final boolean positionDiscontinuity;
|
||||||
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
|
private final @Player.DiscontinuityReason int positionDiscontinuityReason;
|
||||||
private final @Player.TimelineChangeReason int timelineChangeReason;
|
private final int timelineChangeReason;
|
||||||
private final boolean seekProcessed;
|
private final boolean seekProcessed;
|
||||||
private final boolean playbackStateChanged;
|
private final boolean playbackStateChanged;
|
||||||
private final boolean playbackErrorChanged;
|
private final boolean playbackErrorChanged;
|
||||||
|
|
@ -790,15 +997,16 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
playbackErrorChanged =
|
playbackErrorChanged =
|
||||||
previousPlaybackInfo.playbackError != playbackInfo.playbackError
|
previousPlaybackInfo.playbackError != playbackInfo.playbackError
|
||||||
&& playbackInfo.playbackError != null;
|
&& playbackInfo.playbackError != null;
|
||||||
timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline;
|
|
||||||
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
|
isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading;
|
||||||
|
timelineChanged =
|
||||||
|
!Util.areTimelinesSame(previousPlaybackInfo.timeline, playbackInfo.timeline);
|
||||||
trackSelectorResultChanged =
|
trackSelectorResultChanged =
|
||||||
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
|
previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
|
if (timelineChanged) {
|
||||||
invokeAll(
|
invokeAll(
|
||||||
listenerSnapshot,
|
listenerSnapshot,
|
||||||
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
|
listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason));
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
|
import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
|
||||||
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
import com.google.android.exoplayer2.Player.DiscontinuityReason;
|
||||||
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
|
|
||||||
import com.google.android.exoplayer2.source.SampleStream;
|
import com.google.android.exoplayer2.source.SampleStream;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||||
|
|
@ -45,6 +45,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
|
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
|
||||||
|
|
@ -52,7 +53,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
implements Handler.Callback,
|
implements Handler.Callback,
|
||||||
MediaPeriod.Callback,
|
MediaPeriod.Callback,
|
||||||
TrackSelector.InvalidationListener,
|
TrackSelector.InvalidationListener,
|
||||||
MediaSourceCaller,
|
Playlist.PlaylistInfoRefreshListener,
|
||||||
PlaybackParameterListener,
|
PlaybackParameterListener,
|
||||||
PlayerMessage.Sender {
|
PlayerMessage.Sender {
|
||||||
|
|
||||||
|
|
@ -71,16 +72,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private static final int MSG_SET_SEEK_PARAMETERS = 5;
|
private static final int MSG_SET_SEEK_PARAMETERS = 5;
|
||||||
private static final int MSG_STOP = 6;
|
private static final int MSG_STOP = 6;
|
||||||
private static final int MSG_RELEASE = 7;
|
private static final int MSG_RELEASE = 7;
|
||||||
private static final int MSG_REFRESH_SOURCE_INFO = 8;
|
private static final int MSG_PERIOD_PREPARED = 8;
|
||||||
private static final int MSG_PERIOD_PREPARED = 9;
|
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
|
||||||
private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;
|
private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
|
||||||
private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
|
private static final int MSG_SET_REPEAT_MODE = 11;
|
||||||
private static final int MSG_SET_REPEAT_MODE = 12;
|
private static final int MSG_SET_SHUFFLE_ENABLED = 12;
|
||||||
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
|
private static final int MSG_SET_FOREGROUND_MODE = 13;
|
||||||
private static final int MSG_SET_FOREGROUND_MODE = 14;
|
private static final int MSG_SEND_MESSAGE = 14;
|
||||||
private static final int MSG_SEND_MESSAGE = 15;
|
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
|
||||||
private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16;
|
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 16;
|
||||||
private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17;
|
private static final int MSG_SET_MEDIA_ITEMS = 17;
|
||||||
|
private static final int MSG_ADD_MEDIA_ITEMS = 18;
|
||||||
|
private static final int MSG_MOVE_MEDIA_ITEMS = 19;
|
||||||
|
private static final int MSG_REMOVE_MEDIA_ITEMS = 20;
|
||||||
|
private static final int MSG_SET_SHUFFLE_ORDER = 21;
|
||||||
|
private static final int MSG_PLAYLIST_UPDATE_REQUESTED = 22;
|
||||||
|
|
||||||
private static final int ACTIVE_INTERVAL_MS = 10;
|
private static final int ACTIVE_INTERVAL_MS = 10;
|
||||||
private static final int IDLE_INTERVAL_MS = 1000;
|
private static final int IDLE_INTERVAL_MS = 1000;
|
||||||
|
|
@ -103,12 +109,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private final ArrayList<PendingMessageInfo> pendingMessages;
|
private final ArrayList<PendingMessageInfo> pendingMessages;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final MediaPeriodQueue queue;
|
private final MediaPeriodQueue queue;
|
||||||
|
private final Playlist playlist;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private SeekParameters seekParameters;
|
private SeekParameters seekParameters;
|
||||||
|
|
||||||
private PlaybackInfo playbackInfo;
|
private PlaybackInfo playbackInfo;
|
||||||
private MediaSource mediaSource;
|
|
||||||
private Renderer[] enabledRenderers;
|
private Renderer[] enabledRenderers;
|
||||||
private boolean released;
|
private boolean released;
|
||||||
private boolean playWhenReady;
|
private boolean playWhenReady;
|
||||||
|
|
@ -118,8 +124,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private boolean shuffleModeEnabled;
|
private boolean shuffleModeEnabled;
|
||||||
private boolean foregroundMode;
|
private boolean foregroundMode;
|
||||||
|
|
||||||
private int pendingPrepareCount;
|
@Nullable private SeekPosition pendingInitialSeekPosition;
|
||||||
private SeekPosition pendingInitialSeekPosition;
|
|
||||||
private long rendererPositionUs;
|
private long rendererPositionUs;
|
||||||
private int nextPendingMessageIndex;
|
private int nextPendingMessageIndex;
|
||||||
|
|
||||||
|
|
@ -132,6 +137,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
boolean playWhenReady,
|
boolean playWhenReady,
|
||||||
@Player.RepeatMode int repeatMode,
|
@Player.RepeatMode int repeatMode,
|
||||||
boolean shuffleModeEnabled,
|
boolean shuffleModeEnabled,
|
||||||
|
@Nullable AnalyticsCollector analyticsCollector,
|
||||||
Handler eventHandler,
|
Handler eventHandler,
|
||||||
Clock clock) {
|
Clock clock) {
|
||||||
this.renderers = renderers;
|
this.renderers = renderers;
|
||||||
|
|
@ -171,12 +177,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO);
|
new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO);
|
||||||
internalPlaybackThread.start();
|
internalPlaybackThread.start();
|
||||||
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
|
handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
|
||||||
|
playlist = new Playlist(this);
|
||||||
|
if (analyticsCollector != null) {
|
||||||
|
playlist.setAnalyticsCollector(eventHandler, analyticsCollector);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
public void prepare() {
|
||||||
handler
|
handler.obtainMessage(MSG_PREPARE).sendToTarget();
|
||||||
.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, resetState ? 1 : 0, mediaSource)
|
|
||||||
.sendToTarget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
|
|
@ -209,6 +217,48 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
|
handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMediaItems(
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSources,
|
||||||
|
int windowIndex,
|
||||||
|
long positionUs,
|
||||||
|
ShuffleOrder shuffleOrder) {
|
||||||
|
handler
|
||||||
|
.obtainMessage(
|
||||||
|
MSG_SET_MEDIA_ITEMS,
|
||||||
|
new PlaylistUpdateMessage(mediaSources, shuffleOrder, windowIndex, positionUs))
|
||||||
|
.sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMediaItems(
|
||||||
|
int index, List<Playlist.MediaSourceHolder> mediaSources, ShuffleOrder shuffleOrder) {
|
||||||
|
handler
|
||||||
|
.obtainMessage(
|
||||||
|
MSG_ADD_MEDIA_ITEMS,
|
||||||
|
index,
|
||||||
|
/* ignored */ 0,
|
||||||
|
new PlaylistUpdateMessage(
|
||||||
|
mediaSources,
|
||||||
|
shuffleOrder,
|
||||||
|
/* windowIndex= */ C.INDEX_UNSET,
|
||||||
|
/* positionUs= */ C.TIME_UNSET))
|
||||||
|
.sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeMediaItems(int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
handler.obtainMessage(MSG_REMOVE_MEDIA_ITEMS, fromIndex, toIndex, shuffleOrder).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveMediaItems(
|
||||||
|
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
MoveMediaItemsMessage moveMediaItemsMessage =
|
||||||
|
new MoveMediaItemsMessage(fromIndex, toIndex, newFromIndex, shuffleOrder);
|
||||||
|
handler.obtainMessage(MSG_MOVE_MEDIA_ITEMS, moveMediaItemsMessage).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
handler.obtainMessage(MSG_SET_SHUFFLE_ORDER, shuffleOrder).sendToTarget();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void sendMessage(PlayerMessage message) {
|
public synchronized void sendMessage(PlayerMessage message) {
|
||||||
if (released || !internalPlaybackThread.isAlive()) {
|
if (released || !internalPlaybackThread.isAlive()) {
|
||||||
|
|
@ -268,13 +318,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
return internalPlaybackThread.getLooper();
|
return internalPlaybackThread.getLooper();
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaSource.MediaSourceCaller implementation.
|
// Playlist.PlaylistInfoRefreshListener implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) {
|
public void onPlaylistUpdateRequested() {
|
||||||
handler
|
handler.sendEmptyMessage(MSG_PLAYLIST_UPDATE_REQUESTED);
|
||||||
.obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline))
|
|
||||||
.sendToTarget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaPeriod.Callback implementation.
|
// MediaPeriod.Callback implementation.
|
||||||
|
|
@ -306,14 +354,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
// Handler.Callback implementation.
|
// Handler.Callback implementation.
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public boolean handleMessage(Message msg) {
|
public boolean handleMessage(Message msg) {
|
||||||
try {
|
try {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
case MSG_PREPARE:
|
case MSG_PREPARE:
|
||||||
prepareInternal(
|
prepareInternal();
|
||||||
(MediaSource) msg.obj,
|
|
||||||
/* resetPosition= */ msg.arg1 != 0,
|
|
||||||
/* resetState= */ msg.arg2 != 0);
|
|
||||||
break;
|
break;
|
||||||
case MSG_SET_PLAY_WHEN_READY:
|
case MSG_SET_PLAY_WHEN_READY:
|
||||||
setPlayWhenReadyInternal(msg.arg1 != 0);
|
setPlayWhenReadyInternal(msg.arg1 != 0);
|
||||||
|
|
@ -349,9 +395,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
case MSG_PERIOD_PREPARED:
|
case MSG_PERIOD_PREPARED:
|
||||||
handlePeriodPrepared((MediaPeriod) msg.obj);
|
handlePeriodPrepared((MediaPeriod) msg.obj);
|
||||||
break;
|
break;
|
||||||
case MSG_REFRESH_SOURCE_INFO:
|
|
||||||
handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
|
|
||||||
break;
|
|
||||||
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
|
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
|
||||||
handleContinueLoadingRequested((MediaPeriod) msg.obj);
|
handleContinueLoadingRequested((MediaPeriod) msg.obj);
|
||||||
break;
|
break;
|
||||||
|
|
@ -368,6 +411,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
|
case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
|
||||||
sendMessageToTargetThread((PlayerMessage) msg.obj);
|
sendMessageToTargetThread((PlayerMessage) msg.obj);
|
||||||
break;
|
break;
|
||||||
|
case MSG_SET_MEDIA_ITEMS:
|
||||||
|
setMediaItemsInternal((PlaylistUpdateMessage) msg.obj);
|
||||||
|
break;
|
||||||
|
case MSG_ADD_MEDIA_ITEMS:
|
||||||
|
addMediaItemsInternal((PlaylistUpdateMessage) msg.obj, msg.arg1);
|
||||||
|
break;
|
||||||
|
case MSG_MOVE_MEDIA_ITEMS:
|
||||||
|
moveMediaItemsInternal((MoveMediaItemsMessage) msg.obj);
|
||||||
|
break;
|
||||||
|
case MSG_REMOVE_MEDIA_ITEMS:
|
||||||
|
removeMediaItemsInternal(msg.arg1, msg.arg2, (ShuffleOrder) msg.obj);
|
||||||
|
break;
|
||||||
|
case MSG_SET_SHUFFLE_ORDER:
|
||||||
|
setShuffleOrderInternal((ShuffleOrder) msg.obj);
|
||||||
|
break;
|
||||||
|
case MSG_PLAYLIST_UPDATE_REQUESTED:
|
||||||
|
playlistUpdateRequestedInternal();
|
||||||
|
break;
|
||||||
case MSG_RELEASE:
|
case MSG_RELEASE:
|
||||||
releaseInternal();
|
releaseInternal();
|
||||||
// Return immediately to not send playback info updates after release.
|
// Return immediately to not send playback info updates after release.
|
||||||
|
|
@ -431,21 +492,77 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
private void prepareInternal() {
|
||||||
pendingPrepareCount++;
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
resetInternal(
|
resetInternal(
|
||||||
/* resetRenderers= */ false,
|
/* resetRenderers= */ false,
|
||||||
/* releaseMediaSource= */ true,
|
/* resetPosition= */ false,
|
||||||
resetPosition,
|
/* releasePlaylist= */ false,
|
||||||
resetState,
|
/* clearPlaylist= */ false,
|
||||||
/* resetError= */ true);
|
/* resetError= */ true);
|
||||||
loadControl.onPrepared();
|
loadControl.onPrepared();
|
||||||
this.mediaSource = mediaSource;
|
setState(playbackInfo.timeline.isEmpty() ? Player.STATE_ENDED : Player.STATE_BUFFERING);
|
||||||
setState(Player.STATE_BUFFERING);
|
playlist.prepare(bandwidthMeter.getTransferListener());
|
||||||
mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener());
|
|
||||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setMediaItemsInternal(PlaylistUpdateMessage playlistUpdateMessage)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
if (playlistUpdateMessage.windowIndex != C.INDEX_UNSET) {
|
||||||
|
pendingInitialSeekPosition =
|
||||||
|
new SeekPosition(
|
||||||
|
new Playlist.PlaylistTimeline(
|
||||||
|
playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder),
|
||||||
|
playlistUpdateMessage.windowIndex,
|
||||||
|
playlistUpdateMessage.positionUs);
|
||||||
|
}
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.setMediaSources(
|
||||||
|
playlistUpdateMessage.mediaSourceHolders, playlistUpdateMessage.shuffleOrder);
|
||||||
|
handlePlaylistInfoRefreshed(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMediaItemsInternal(PlaylistUpdateMessage addMessage, int insertionIndex)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.addMediaSources(
|
||||||
|
insertionIndex == C.INDEX_UNSET ? playlist.getSize() : insertionIndex,
|
||||||
|
addMessage.mediaSourceHolders,
|
||||||
|
addMessage.shuffleOrder);
|
||||||
|
handlePlaylistInfoRefreshed(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveMediaItemsInternal(MoveMediaItemsMessage moveMediaItemsMessage)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
moveMediaItemsMessage.fromIndex,
|
||||||
|
moveMediaItemsMessage.toIndex,
|
||||||
|
moveMediaItemsMessage.newFromIndex,
|
||||||
|
moveMediaItemsMessage.shuffleOrder);
|
||||||
|
handlePlaylistInfoRefreshed(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeMediaItemsInternal(int fromIndex, int toIndex, ShuffleOrder shuffleOrder)
|
||||||
|
throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
Timeline timeline = playlist.removeMediaSourceRange(fromIndex, toIndex, shuffleOrder);
|
||||||
|
handlePlaylistInfoRefreshed(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playlistUpdateRequestedInternal() throws ExoPlaybackException {
|
||||||
|
handlePlaylistInfoRefreshed(playlist.createTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setShuffleOrderInternal(ShuffleOrder shuffleOrder) throws ExoPlaybackException {
|
||||||
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
Timeline timeline = playlist.setShuffleOrder(shuffleOrder);
|
||||||
|
handlePlaylistInfoRefreshed(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
|
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
|
||||||
rebuffering = false;
|
rebuffering = false;
|
||||||
this.playWhenReady = playWhenReady;
|
this.playWhenReady = playWhenReady;
|
||||||
|
|
@ -663,6 +780,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
long periodPositionUs;
|
long periodPositionUs;
|
||||||
long contentPositionUs;
|
long contentPositionUs;
|
||||||
boolean seekPositionAdjusted;
|
boolean seekPositionAdjusted;
|
||||||
|
@Nullable
|
||||||
Pair<Object, Long> resolvedSeekPosition =
|
Pair<Object, Long> resolvedSeekPosition =
|
||||||
resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
|
resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
|
||||||
if (resolvedSeekPosition == null) {
|
if (resolvedSeekPosition == null) {
|
||||||
|
|
@ -687,7 +805,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mediaSource == null || pendingPrepareCount > 0) {
|
if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
|
||||||
// Save seek position for later, as we are still waiting for a prepared source.
|
// Save seek position for later, as we are still waiting for a prepared source.
|
||||||
pendingInitialSeekPosition = seekPosition;
|
pendingInitialSeekPosition = seekPosition;
|
||||||
} else if (periodPositionUs == C.TIME_UNSET) {
|
} else if (periodPositionUs == C.TIME_UNSET) {
|
||||||
|
|
@ -695,9 +813,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
setState(Player.STATE_ENDED);
|
setState(Player.STATE_ENDED);
|
||||||
resetInternal(
|
resetInternal(
|
||||||
/* resetRenderers= */ false,
|
/* resetRenderers= */ false,
|
||||||
/* releaseMediaSource= */ false,
|
|
||||||
/* resetPosition= */ true,
|
/* resetPosition= */ true,
|
||||||
/* resetState= */ false,
|
/* releasePlaylist= */ false,
|
||||||
|
/* clearPlaylist= */ false,
|
||||||
/* resetError= */ true);
|
/* resetError= */ true);
|
||||||
} else {
|
} else {
|
||||||
// Execute the seek in the current media periods.
|
// Execute the seek in the current media periods.
|
||||||
|
|
@ -844,13 +962,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
|
boolean forceResetRenderers, boolean resetPositionAndState, boolean acknowledgeStop) {
|
||||||
resetInternal(
|
resetInternal(
|
||||||
/* resetRenderers= */ forceResetRenderers || !foregroundMode,
|
/* resetRenderers= */ forceResetRenderers || !foregroundMode,
|
||||||
/* releaseMediaSource= */ true,
|
|
||||||
/* resetPosition= */ resetPositionAndState,
|
/* resetPosition= */ resetPositionAndState,
|
||||||
/* resetState= */ resetPositionAndState,
|
/* releasePlaylist= */ true,
|
||||||
|
/* clearPlaylist= */ resetPositionAndState,
|
||||||
/* resetError= */ resetPositionAndState);
|
/* resetError= */ resetPositionAndState);
|
||||||
playbackInfoUpdate.incrementPendingOperationAcks(
|
playbackInfoUpdate.incrementPendingOperationAcks(acknowledgeStop ? 1 : 0);
|
||||||
pendingPrepareCount + (acknowledgeStop ? 1 : 0));
|
|
||||||
pendingPrepareCount = 0;
|
|
||||||
loadControl.onStopped();
|
loadControl.onStopped();
|
||||||
setState(Player.STATE_IDLE);
|
setState(Player.STATE_IDLE);
|
||||||
}
|
}
|
||||||
|
|
@ -858,9 +974,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private void releaseInternal() {
|
private void releaseInternal() {
|
||||||
resetInternal(
|
resetInternal(
|
||||||
/* resetRenderers= */ true,
|
/* resetRenderers= */ true,
|
||||||
/* releaseMediaSource= */ true,
|
|
||||||
/* resetPosition= */ true,
|
/* resetPosition= */ true,
|
||||||
/* resetState= */ true,
|
/* releasePlaylist= */ true,
|
||||||
|
/* clearPlaylist= */ true,
|
||||||
/* resetError= */ false);
|
/* resetError= */ false);
|
||||||
loadControl.onReleased();
|
loadControl.onReleased();
|
||||||
setState(Player.STATE_IDLE);
|
setState(Player.STATE_IDLE);
|
||||||
|
|
@ -873,9 +989,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
private void resetInternal(
|
private void resetInternal(
|
||||||
boolean resetRenderers,
|
boolean resetRenderers,
|
||||||
boolean releaseMediaSource,
|
|
||||||
boolean resetPosition,
|
boolean resetPosition,
|
||||||
boolean resetState,
|
boolean releasePlaylist,
|
||||||
|
boolean clearPlaylist,
|
||||||
boolean resetError) {
|
boolean resetError) {
|
||||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||||
rebuffering = false;
|
rebuffering = false;
|
||||||
|
|
@ -903,8 +1019,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
if (resetPosition) {
|
if (resetPosition) {
|
||||||
pendingInitialSeekPosition = null;
|
pendingInitialSeekPosition = null;
|
||||||
} else if (resetState) {
|
} else if (clearPlaylist) {
|
||||||
// When resetting the state, also reset the period-based PlaybackInfo position and convert
|
// When clearing the playlist, also reset the period-based PlaybackInfo position and convert
|
||||||
// existing position to initial seek instead.
|
// existing position to initial seek instead.
|
||||||
resetPosition = true;
|
resetPosition = true;
|
||||||
if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
|
if (pendingInitialSeekPosition == null && !playbackInfo.timeline.isEmpty()) {
|
||||||
|
|
@ -915,10 +1031,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.clear(/* keepFrontPeriodUid= */ !resetState);
|
queue.clear(/* keepFrontPeriodUid= */ !clearPlaylist);
|
||||||
shouldContinueLoading = false;
|
shouldContinueLoading = false;
|
||||||
if (resetState) {
|
if (clearPlaylist) {
|
||||||
queue.setTimeline(Timeline.EMPTY);
|
queue.setTimeline(playlist.clear(/* shuffleOrder= */ null));
|
||||||
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
|
for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
|
||||||
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
|
pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
|
||||||
}
|
}
|
||||||
|
|
@ -934,24 +1050,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
|
long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs;
|
||||||
playbackInfo =
|
playbackInfo =
|
||||||
new PlaybackInfo(
|
new PlaybackInfo(
|
||||||
resetState ? Timeline.EMPTY : playbackInfo.timeline,
|
clearPlaylist ? Timeline.EMPTY : playbackInfo.timeline,
|
||||||
mediaPeriodId,
|
mediaPeriodId,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
contentPositionUs,
|
contentPositionUs,
|
||||||
playbackInfo.playbackState,
|
playbackInfo.playbackState,
|
||||||
resetError ? null : playbackInfo.playbackError,
|
resetError ? null : playbackInfo.playbackError,
|
||||||
/* isLoading= */ false,
|
/* isLoading= */ false,
|
||||||
resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
|
clearPlaylist ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
|
||||||
resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
|
clearPlaylist ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
|
||||||
mediaPeriodId,
|
mediaPeriodId,
|
||||||
startPositionUs,
|
startPositionUs,
|
||||||
/* totalBufferedDurationUs= */ 0,
|
/* totalBufferedDurationUs= */ 0,
|
||||||
startPositionUs);
|
startPositionUs);
|
||||||
if (releaseMediaSource) {
|
if (releasePlaylist) {
|
||||||
if (mediaSource != null) {
|
playlist.release();
|
||||||
mediaSource.releaseSource(/* caller= */ this);
|
|
||||||
mediaSource = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -959,7 +1072,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
if (message.getPositionMs() == C.TIME_UNSET) {
|
if (message.getPositionMs() == C.TIME_UNSET) {
|
||||||
// If no delivery time is specified, trigger immediate message delivery.
|
// If no delivery time is specified, trigger immediate message delivery.
|
||||||
sendMessageToTarget(message);
|
sendMessageToTarget(message);
|
||||||
} else if (mediaSource == null || pendingPrepareCount > 0) {
|
} else if (playbackInfo.timeline.isEmpty()) {
|
||||||
// Still waiting for initial timeline to resolve position.
|
// Still waiting for initial timeline to resolve position.
|
||||||
pendingMessages.add(new PendingMessageInfo(message));
|
pendingMessages.add(new PendingMessageInfo(message));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1279,20 +1392,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
playlist.maybeThrowSourceInfoRefreshError();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo)
|
private void handlePlaylistInfoRefreshed(Timeline timeline) throws ExoPlaybackException {
|
||||||
throws ExoPlaybackException {
|
|
||||||
if (sourceRefreshInfo.source != mediaSource) {
|
|
||||||
// Stale event.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
|
|
||||||
pendingPrepareCount = 0;
|
|
||||||
|
|
||||||
Timeline oldTimeline = playbackInfo.timeline;
|
Timeline oldTimeline = playbackInfo.timeline;
|
||||||
Timeline timeline = sourceRefreshInfo.timeline;
|
|
||||||
queue.setTimeline(timeline);
|
queue.setTimeline(timeline);
|
||||||
playbackInfo = playbackInfo.copyWithTimeline(timeline);
|
playbackInfo = playbackInfo.copyWithTimeline(timeline);
|
||||||
resolvePendingMessagePositions();
|
resolvePendingMessagePositions();
|
||||||
|
|
@ -1303,13 +1407,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
long newContentPositionUs = oldContentPositionUs;
|
long newContentPositionUs = oldContentPositionUs;
|
||||||
if (pendingInitialSeekPosition != null) {
|
if (pendingInitialSeekPosition != null) {
|
||||||
// Resolve initial seek position.
|
// Resolve initial seek position.
|
||||||
|
@Nullable
|
||||||
Pair<Object, Long> periodPosition =
|
Pair<Object, Long> periodPosition =
|
||||||
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
|
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
|
||||||
pendingInitialSeekPosition = null;
|
boolean keepInitialSeekPosition = periodPosition == null && timeline.isEmpty();
|
||||||
|
pendingInitialSeekPosition = keepInitialSeekPosition ? pendingInitialSeekPosition : null;
|
||||||
if (periodPosition == null) {
|
if (periodPosition == null) {
|
||||||
// The seek position was valid for the timeline that it was performed into, but the
|
// The seek position was valid for the timeline that it was performed into, but the
|
||||||
// timeline has changed and a suitable seek position could not be resolved in the new one.
|
// timeline has changed and a suitable seek position could not be resolved in the new one.
|
||||||
handleSourceInfoRefreshEndedPlayback();
|
handleSourceInfoRefreshEndedPlayback(/* resetPosition= */ !keepInitialSeekPosition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newContentPositionUs = periodPosition.second;
|
newContentPositionUs = periodPosition.second;
|
||||||
|
|
@ -1330,7 +1436,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);
|
Object newPeriodUid = resolveSubsequentPeriod(newPeriodId.periodUid, oldTimeline, timeline);
|
||||||
if (newPeriodUid == null) {
|
if (newPeriodUid == null) {
|
||||||
// We failed to resolve a suitable restart position.
|
// We failed to resolve a suitable restart position.
|
||||||
handleSourceInfoRefreshEndedPlayback();
|
handleSourceInfoRefreshEndedPlayback(/* resetPosition= */ true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We resolved a subsequent period. Start at the default position in the corresponding window.
|
// We resolved a subsequent period. Start at the default position in the corresponding window.
|
||||||
|
|
@ -1403,16 +1509,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
return maxReadPositionUs;
|
return maxReadPositionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSourceInfoRefreshEndedPlayback() {
|
private void handleSourceInfoRefreshEndedPlayback(boolean resetPosition) {
|
||||||
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
if (playbackInfo.playbackState != Player.STATE_IDLE) {
|
||||||
setState(Player.STATE_ENDED);
|
setState(Player.STATE_ENDED);
|
||||||
}
|
}
|
||||||
// Reset, but retain the source so that it can still be used should a seek occur.
|
// Reset, but retain the playlist so that it can still be used should a seek occur.
|
||||||
resetInternal(
|
resetInternal(
|
||||||
/* resetRenderers= */ false,
|
/* resetRenderers= */ false,
|
||||||
/* releaseMediaSource= */ false,
|
/* resetPosition= */ resetPosition,
|
||||||
/* resetPosition= */ true,
|
/* releasePlaylist= */ false,
|
||||||
/* resetState= */ false,
|
/* clearPlaylist= */ false,
|
||||||
/* resetError= */ true);
|
/* resetError= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1455,6 +1561,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
* @throws IllegalSeekPositionException If the window index of the seek position is outside the
|
* @throws IllegalSeekPositionException If the window index of the seek position is outside the
|
||||||
* bounds of the timeline.
|
* bounds of the timeline.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
private Pair<Object, Long> resolveSeekPosition(
|
private Pair<Object, Long> resolveSeekPosition(
|
||||||
SeekPosition seekPosition, boolean trySubsequentPeriods) {
|
SeekPosition seekPosition, boolean trySubsequentPeriods) {
|
||||||
Timeline timeline = playbackInfo.timeline;
|
Timeline timeline = playbackInfo.timeline;
|
||||||
|
|
@ -1511,13 +1618,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePeriods() throws ExoPlaybackException, IOException {
|
private void updatePeriods() throws ExoPlaybackException, IOException {
|
||||||
if (mediaSource == null) {
|
if (playbackInfo.timeline.isEmpty() || !playlist.isPrepared()) {
|
||||||
// The player has no media source yet.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pendingPrepareCount > 0) {
|
|
||||||
// We're waiting to get information about periods.
|
// We're waiting to get information about periods.
|
||||||
mediaSource.maybeThrowSourceInfoRefreshError();
|
playlist.maybeThrowSourceInfoRefreshError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
maybeUpdateLoadingPeriod();
|
maybeUpdateLoadingPeriod();
|
||||||
|
|
@ -1537,7 +1640,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
rendererCapabilities,
|
rendererCapabilities,
|
||||||
trackSelector,
|
trackSelector,
|
||||||
loadControl.getAllocator(),
|
loadControl.getAllocator(),
|
||||||
mediaSource,
|
playlist,
|
||||||
info,
|
info,
|
||||||
emptyTrackSelectorResult);
|
emptyTrackSelectorResult);
|
||||||
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
|
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
|
||||||
|
|
@ -1556,7 +1659,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
|
private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
|
||||||
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
|
@Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
|
||||||
if (readingPeriodHolder == null) {
|
if (readingPeriodHolder == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1986,14 +2089,38 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class MediaSourceRefreshInfo {
|
private static final class PlaylistUpdateMessage {
|
||||||
|
|
||||||
public final MediaSource source;
|
private final List<Playlist.MediaSourceHolder> mediaSourceHolders;
|
||||||
public final Timeline timeline;
|
private final ShuffleOrder shuffleOrder;
|
||||||
|
private final int windowIndex;
|
||||||
|
private final long positionUs;
|
||||||
|
|
||||||
public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) {
|
private PlaylistUpdateMessage(
|
||||||
this.source = source;
|
List<Playlist.MediaSourceHolder> mediaSourceHolders,
|
||||||
this.timeline = timeline;
|
ShuffleOrder shuffleOrder,
|
||||||
|
int windowIndex,
|
||||||
|
long positionUs) {
|
||||||
|
this.mediaSourceHolders = mediaSourceHolders;
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
this.windowIndex = windowIndex;
|
||||||
|
this.positionUs = positionUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MoveMediaItemsMessage {
|
||||||
|
|
||||||
|
public final int fromIndex;
|
||||||
|
public final int toIndex;
|
||||||
|
public final int newFromIndex;
|
||||||
|
public final ShuffleOrder shuffleOrder;
|
||||||
|
|
||||||
|
public MoveMediaItemsMessage(
|
||||||
|
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
this.fromIndex = fromIndex;
|
||||||
|
this.toIndex = toIndex;
|
||||||
|
this.newFromIndex = newFromIndex;
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2002,7 +2129,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
private PlaybackInfo lastPlaybackInfo;
|
private PlaybackInfo lastPlaybackInfo;
|
||||||
private int operationAcks;
|
private int operationAcks;
|
||||||
private boolean positionDiscontinuity;
|
private boolean positionDiscontinuity;
|
||||||
private @DiscontinuityReason int discontinuityReason;
|
@DiscontinuityReason private int discontinuityReason;
|
||||||
|
|
||||||
public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
|
public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
|
||||||
return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
|
return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
|
||||||
|
|
@ -2030,5 +2157,4 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
this.discontinuityReason = discontinuityReason;
|
this.discontinuityReason = discontinuityReason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
|
import com.google.android.exoplayer2.source.ClippingMediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.EmptySampleStream;
|
import com.google.android.exoplayer2.source.EmptySampleStream;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.source.SampleStream;
|
import com.google.android.exoplayer2.source.SampleStream;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
|
|
@ -56,7 +55,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
private final boolean[] mayRetainStreamFlags;
|
private final boolean[] mayRetainStreamFlags;
|
||||||
private final RendererCapabilities[] rendererCapabilities;
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
private final TrackSelector trackSelector;
|
private final TrackSelector trackSelector;
|
||||||
private final MediaSource mediaSource;
|
private final Playlist playlist;
|
||||||
|
|
||||||
@Nullable private MediaPeriodHolder next;
|
@Nullable private MediaPeriodHolder next;
|
||||||
private TrackGroupArray trackGroups;
|
private TrackGroupArray trackGroups;
|
||||||
|
|
@ -70,7 +69,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
* @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
|
* @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds.
|
||||||
* @param trackSelector The track selector.
|
* @param trackSelector The track selector.
|
||||||
* @param allocator The allocator.
|
* @param allocator The allocator.
|
||||||
* @param mediaSource The media source that produced the media period.
|
* @param playlist The playlist.
|
||||||
* @param info Information used to identify this media period in its timeline period.
|
* @param info Information used to identify this media period in its timeline period.
|
||||||
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
|
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
|
||||||
* renderer.
|
* renderer.
|
||||||
|
|
@ -80,13 +79,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
long rendererPositionOffsetUs,
|
long rendererPositionOffsetUs,
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
MediaSource mediaSource,
|
Playlist playlist,
|
||||||
MediaPeriodInfo info,
|
MediaPeriodInfo info,
|
||||||
TrackSelectorResult emptyTrackSelectorResult) {
|
TrackSelectorResult emptyTrackSelectorResult) {
|
||||||
this.rendererCapabilities = rendererCapabilities;
|
this.rendererCapabilities = rendererCapabilities;
|
||||||
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
this.rendererPositionOffsetUs = rendererPositionOffsetUs;
|
||||||
this.trackSelector = trackSelector;
|
this.trackSelector = trackSelector;
|
||||||
this.mediaSource = mediaSource;
|
this.playlist = playlist;
|
||||||
this.uid = info.id.periodUid;
|
this.uid = info.id.periodUid;
|
||||||
this.info = info;
|
this.info = info;
|
||||||
this.trackGroups = TrackGroupArray.EMPTY;
|
this.trackGroups = TrackGroupArray.EMPTY;
|
||||||
|
|
@ -94,8 +93,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
sampleStreams = new SampleStream[rendererCapabilities.length];
|
sampleStreams = new SampleStream[rendererCapabilities.length];
|
||||||
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
mayRetainStreamFlags = new boolean[rendererCapabilities.length];
|
||||||
mediaPeriod =
|
mediaPeriod =
|
||||||
createMediaPeriod(
|
createMediaPeriod(info.id, playlist, allocator, info.startPositionUs, info.endPositionUs);
|
||||||
info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -305,7 +303,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
/** Releases the media period. No other method should be called after the release. */
|
/** Releases the media period. No other method should be called after the release. */
|
||||||
public void release() {
|
public void release() {
|
||||||
disableTrackSelectionsInResult();
|
disableTrackSelectionsInResult();
|
||||||
releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod);
|
releaseMediaPeriod(info.endPositionUs, playlist, mediaPeriod);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -402,11 +400,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
/** Returns a media period corresponding to the given {@code id}. */
|
/** Returns a media period corresponding to the given {@code id}. */
|
||||||
private static MediaPeriod createMediaPeriod(
|
private static MediaPeriod createMediaPeriod(
|
||||||
MediaPeriodId id,
|
MediaPeriodId id,
|
||||||
MediaSource mediaSource,
|
Playlist playlist,
|
||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
long startPositionUs,
|
long startPositionUs,
|
||||||
long endPositionUs) {
|
long endPositionUs) {
|
||||||
MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs);
|
MediaPeriod mediaPeriod = playlist.createPeriod(id, allocator, startPositionUs);
|
||||||
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||||
mediaPeriod =
|
mediaPeriod =
|
||||||
new ClippingMediaPeriod(
|
new ClippingMediaPeriod(
|
||||||
|
|
@ -417,12 +415,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
|
/** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */
|
||||||
private static void releaseMediaPeriod(
|
private static void releaseMediaPeriod(
|
||||||
long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) {
|
long endPositionUs, Playlist playlist, MediaPeriod mediaPeriod) {
|
||||||
try {
|
try {
|
||||||
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) {
|
||||||
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
|
playlist.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
|
||||||
} else {
|
} else {
|
||||||
mediaSource.releasePeriod(mediaPeriod);
|
playlist.releasePeriod(mediaPeriod);
|
||||||
}
|
}
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
// There's nothing we can do.
|
// There's nothing we can do.
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||||
|
|
@ -134,7 +133,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
* @param rendererCapabilities The renderer capabilities.
|
* @param rendererCapabilities The renderer capabilities.
|
||||||
* @param trackSelector The track selector.
|
* @param trackSelector The track selector.
|
||||||
* @param allocator The allocator.
|
* @param allocator The allocator.
|
||||||
* @param mediaSource The media source that produced the media period.
|
* @param playlist The playlist.
|
||||||
* @param info Information used to identify this media period in its timeline period.
|
* @param info Information used to identify this media period in its timeline period.
|
||||||
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
|
* @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each
|
||||||
* renderer.
|
* renderer.
|
||||||
|
|
@ -143,7 +142,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
RendererCapabilities[] rendererCapabilities,
|
RendererCapabilities[] rendererCapabilities,
|
||||||
TrackSelector trackSelector,
|
TrackSelector trackSelector,
|
||||||
Allocator allocator,
|
Allocator allocator,
|
||||||
MediaSource mediaSource,
|
Playlist playlist,
|
||||||
MediaPeriodInfo info,
|
MediaPeriodInfo info,
|
||||||
TrackSelectorResult emptyTrackSelectorResult) {
|
TrackSelectorResult emptyTrackSelectorResult) {
|
||||||
long rendererPositionOffsetUs =
|
long rendererPositionOffsetUs =
|
||||||
|
|
@ -158,7 +157,7 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||||
rendererPositionOffsetUs,
|
rendererPositionOffsetUs,
|
||||||
trackSelector,
|
trackSelector,
|
||||||
allocator,
|
allocator,
|
||||||
mediaSource,
|
playlist,
|
||||||
info,
|
info,
|
||||||
emptyTrackSelectorResult);
|
emptyTrackSelectorResult);
|
||||||
if (loading != null) {
|
if (loading != null) {
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,8 @@ public interface Player {
|
||||||
* {@link #onPositionDiscontinuity(int)}.
|
* {@link #onPositionDiscontinuity(int)}.
|
||||||
*
|
*
|
||||||
* @param timeline The latest timeline. Never null, but may be empty.
|
* @param timeline The latest timeline. Never null, but may be empty.
|
||||||
* @param manifest The latest manifest. May be null.
|
* @param manifest The latest manifest in case the timeline has a single window only. Always
|
||||||
|
* null if the timeline has more than a single window.
|
||||||
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
|
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
|
||||||
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
|
* @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be
|
||||||
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
|
* accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex,
|
||||||
|
|
@ -603,25 +604,17 @@ public interface Player {
|
||||||
int DISCONTINUITY_REASON_INTERNAL = 4;
|
int DISCONTINUITY_REASON_INTERNAL = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link
|
* Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED} or {@link
|
||||||
* #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}.
|
* #TIMELINE_CHANGE_REASON_SOURCE_UPDATE}.
|
||||||
*/
|
*/
|
||||||
@Documented
|
@Documented
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
@IntDef({
|
@IntDef({TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, TIMELINE_CHANGE_REASON_SOURCE_UPDATE})
|
||||||
TIMELINE_CHANGE_REASON_PREPARED,
|
|
||||||
TIMELINE_CHANGE_REASON_RESET,
|
|
||||||
TIMELINE_CHANGE_REASON_DYNAMIC
|
|
||||||
})
|
|
||||||
@interface TimelineChangeReason {}
|
@interface TimelineChangeReason {}
|
||||||
/** Timeline and manifest changed as a result of a player initialization with new media. */
|
/** Timeline changed as a result of a change of the playlist items or the order of the items. */
|
||||||
int TIMELINE_CHANGE_REASON_PREPARED = 0;
|
int TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED = 0;
|
||||||
/** Timeline and manifest changed as a result of a player reset. */
|
/** Timeline changed as a result of a dynamic update introduced by the played media. */
|
||||||
int TIMELINE_CHANGE_REASON_RESET = 1;
|
int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1;
|
||||||
/**
|
|
||||||
* Timeline or manifest changed as a result of an dynamic update introduced by the played media.
|
|
||||||
*/
|
|
||||||
int TIMELINE_CHANGE_REASON_DYNAMIC = 2;
|
|
||||||
|
|
||||||
/** Returns the component of this player for audio output, or null if audio is not supported. */
|
/** Returns the component of this player for audio output, or null if audio is not supported. */
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,707 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 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;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
|
import com.google.android.exoplayer2.source.MaskingMediaPeriod;
|
||||||
|
import com.google.android.exoplayer2.source.MaskingMediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||||
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
|
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
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.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
|
||||||
|
* during playback. It is valid for the same {@link MediaSource} instance to be present more than
|
||||||
|
* once in the playlist.
|
||||||
|
*
|
||||||
|
* <p>With the exception of the constructor, all methods are called on the playback thread.
|
||||||
|
*/
|
||||||
|
/* package */ class Playlist {
|
||||||
|
|
||||||
|
/** Listener for source events. */
|
||||||
|
public interface PlaylistInfoRefreshListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the timeline of a media item has changed and a new timeline that reflects the
|
||||||
|
* current playlist state needs to be created by calling {@link #createTimeline()}.
|
||||||
|
*
|
||||||
|
* <p>Called on the playback thread.
|
||||||
|
*/
|
||||||
|
void onPlaylistUpdateRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<MediaSourceHolder> mediaSourceHolders;
|
||||||
|
private final Map<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
|
||||||
|
private final Map<Object, MediaSourceHolder> mediaSourceByUid;
|
||||||
|
private final PlaylistInfoRefreshListener playlistInfoListener;
|
||||||
|
private final MediaSourceEventListener.EventDispatcher eventDispatcher;
|
||||||
|
private final HashMap<Playlist.MediaSourceHolder, MediaSourceAndListener> childSources;
|
||||||
|
private final Set<MediaSourceHolder> enabledMediaSourceHolders;
|
||||||
|
|
||||||
|
private ShuffleOrder shuffleOrder;
|
||||||
|
private boolean isPrepared;
|
||||||
|
|
||||||
|
@Nullable private TransferListener mediaTransferListener;
|
||||||
|
|
||||||
|
@SuppressWarnings("initialization")
|
||||||
|
public Playlist(PlaylistInfoRefreshListener listener) {
|
||||||
|
playlistInfoListener = listener;
|
||||||
|
shuffleOrder = new DefaultShuffleOrder(0);
|
||||||
|
mediaSourceByMediaPeriod = new IdentityHashMap<>();
|
||||||
|
mediaSourceByUid = new HashMap<>();
|
||||||
|
mediaSourceHolders = new ArrayList<>();
|
||||||
|
eventDispatcher = new MediaSourceEventListener.EventDispatcher();
|
||||||
|
childSources = new HashMap<>();
|
||||||
|
enabledMediaSourceHolders = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the media sources replacing any sources previously contained in the playlist.
|
||||||
|
*
|
||||||
|
* @param holders The list of {@link MediaSourceHolder}s to set.
|
||||||
|
* @param shuffleOrder The new shuffle order.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
*/
|
||||||
|
public final Timeline setMediaSources(
|
||||||
|
List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
|
||||||
|
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size());
|
||||||
|
return addMediaSources(/* index= */ this.mediaSourceHolders.size(), holders, shuffleOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple {@link MediaSourceHolder}s to the playlist.
|
||||||
|
*
|
||||||
|
* @param index The index at which the new {@link MediaSourceHolder}s will be inserted. This index
|
||||||
|
* must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
|
* @param holders A list of {@link MediaSourceHolder}s to be added.
|
||||||
|
* @param shuffleOrder The new shuffle order.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
*/
|
||||||
|
public final Timeline addMediaSources(
|
||||||
|
int index, List<MediaSourceHolder> holders, ShuffleOrder shuffleOrder) {
|
||||||
|
if (!holders.isEmpty()) {
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
for (int insertionIndex = index; insertionIndex < index + holders.size(); insertionIndex++) {
|
||||||
|
MediaSourceHolder holder = holders.get(insertionIndex - index);
|
||||||
|
if (insertionIndex > 0) {
|
||||||
|
MediaSourceHolder previousHolder = mediaSourceHolders.get(insertionIndex - 1);
|
||||||
|
Timeline previousTimeline = previousHolder.mediaSource.getTimeline();
|
||||||
|
holder.reset(
|
||||||
|
/* firstWindowInChildIndex= */ previousHolder.firstWindowIndexInChild
|
||||||
|
+ previousTimeline.getWindowCount());
|
||||||
|
} else {
|
||||||
|
holder.reset(/* firstWindowIndexInChild= */ 0);
|
||||||
|
}
|
||||||
|
Timeline newTimeline = holder.mediaSource.getTimeline();
|
||||||
|
correctOffsets(
|
||||||
|
/* startIndex= */ insertionIndex,
|
||||||
|
/* windowOffsetUpdate= */ newTimeline.getWindowCount());
|
||||||
|
mediaSourceHolders.add(insertionIndex, holder);
|
||||||
|
mediaSourceByUid.put(holder.uid, holder);
|
||||||
|
if (isPrepared) {
|
||||||
|
prepareChildSource(holder);
|
||||||
|
if (mediaSourceByMediaPeriod.isEmpty()) {
|
||||||
|
enabledMediaSourceHolders.add(holder);
|
||||||
|
} else {
|
||||||
|
disableChildSource(holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a range of {@link MediaSourceHolder}s from the playlist, by specifying an initial index
|
||||||
|
* (included) and a final index (excluded).
|
||||||
|
*
|
||||||
|
* <p>Note: when specified range is empty, no actual media source is removed and no exception is
|
||||||
|
* thrown.
|
||||||
|
*
|
||||||
|
* @param fromIndex The initial range index, pointing to the first media source that will be
|
||||||
|
* removed. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
|
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||||
|
* untouched. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
|
* @param shuffleOrder The new shuffle order.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||||
|
* {@code toIndex} > {@link #getSize()}, {@code fromIndex} > {@code toIndex}
|
||||||
|
*/
|
||||||
|
public final Timeline removeMediaSourceRange(
|
||||||
|
int fromIndex, int toIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
Assertions.checkArgument(fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize());
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
removeMediaSourcesInternal(fromIndex, toIndex);
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves an existing media source within the playlist.
|
||||||
|
*
|
||||||
|
* @param currentIndex The current index of the media source in the playlist. This index must be
|
||||||
|
* in the range of 0 <= index < {@link #getSize()}.
|
||||||
|
* @param newIndex The target index of the media source in the playlist. This index must be in the
|
||||||
|
* range of 0 <= index < {@link #getSize()}.
|
||||||
|
* @param shuffleOrder The new shuffle order.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
* @throws IllegalArgumentException When an index is invalid, i.e. {@code currentIndex} < 0,
|
||||||
|
* {@code currentIndex} >= {@link #getSize()}, {@code newIndex} < 0
|
||||||
|
*/
|
||||||
|
public final Timeline moveMediaSource(int currentIndex, int newIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
return moveMediaSourceRange(currentIndex, currentIndex + 1, newIndex, shuffleOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a range of media sources within the playlist.
|
||||||
|
*
|
||||||
|
* <p>Note: when specified range is empty or the from index equals the new from index, no actual
|
||||||
|
* media source is moved and no exception is thrown.
|
||||||
|
*
|
||||||
|
* @param fromIndex The initial range index, pointing to the first media source of the range that
|
||||||
|
* will be moved. This index must be in the range of 0 <= index <= {@link #getSize()}.
|
||||||
|
* @param toIndex The final range index, pointing to the first media source that will be left
|
||||||
|
* untouched. This index must be larger or equals than {@code fromIndex}.
|
||||||
|
* @param newFromIndex The target index of the first media source of the range that will be moved.
|
||||||
|
* @param shuffleOrder The new shuffle order.
|
||||||
|
* @return The new {@link Timeline}.
|
||||||
|
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} < 0,
|
||||||
|
* {@code toIndex} < {@code fromIndex}, {@code fromIndex} > {@code toIndex}, {@code
|
||||||
|
* newFromIndex} < 0
|
||||||
|
*/
|
||||||
|
public Timeline moveMediaSourceRange(
|
||||||
|
int fromIndex, int toIndex, int newFromIndex, ShuffleOrder shuffleOrder) {
|
||||||
|
Assertions.checkArgument(
|
||||||
|
fromIndex >= 0 && fromIndex <= toIndex && toIndex <= getSize() && newFromIndex >= 0);
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
if (fromIndex == toIndex || fromIndex == newFromIndex) {
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
int startIndex = Math.min(fromIndex, newFromIndex);
|
||||||
|
int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1;
|
||||||
|
int endIndex = Math.max(newEndIndex, toIndex - 1);
|
||||||
|
int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild;
|
||||||
|
moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex);
|
||||||
|
for (int i = startIndex; i <= endIndex; i++) {
|
||||||
|
MediaSourceHolder holder = mediaSourceHolders.get(i);
|
||||||
|
holder.firstWindowIndexInChild = windowOffset;
|
||||||
|
windowOffset += holder.mediaSource.getTimeline().getWindowCount();
|
||||||
|
}
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the playlist. */
|
||||||
|
public final Timeline clear(@Nullable ShuffleOrder shuffleOrder) {
|
||||||
|
this.shuffleOrder = shuffleOrder != null ? shuffleOrder : this.shuffleOrder.cloneAndClear();
|
||||||
|
removeMediaSourcesInternal(/* fromIndex= */ 0, /* toIndex= */ getSize());
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the playlist is prepared. */
|
||||||
|
public final boolean isPrepared() {
|
||||||
|
return isPrepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the number of media sources in the playlist. */
|
||||||
|
public final int getSize() {
|
||||||
|
return mediaSourceHolders.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link AnalyticsCollector}.
|
||||||
|
*
|
||||||
|
* @param handler The handler on which to call the collector.
|
||||||
|
* @param analyticsCollector The analytics collector.
|
||||||
|
*/
|
||||||
|
public final void setAnalyticsCollector(Handler handler, AnalyticsCollector analyticsCollector) {
|
||||||
|
eventDispatcher.addEventListener(handler, analyticsCollector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new shuffle order to use when shuffling the child media sources.
|
||||||
|
*
|
||||||
|
* @param shuffleOrder A {@link ShuffleOrder}.
|
||||||
|
*/
|
||||||
|
public final Timeline setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
int size = getSize();
|
||||||
|
if (shuffleOrder.getLength() != size) {
|
||||||
|
shuffleOrder =
|
||||||
|
shuffleOrder
|
||||||
|
.cloneAndClear()
|
||||||
|
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
|
||||||
|
}
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
return createTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prepares the playlist. */
|
||||||
|
public final void prepare(@Nullable TransferListener mediaTransferListener) {
|
||||||
|
Assertions.checkState(!isPrepared);
|
||||||
|
this.mediaTransferListener = mediaTransferListener;
|
||||||
|
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||||
|
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||||
|
prepareChildSource(mediaSourceHolder);
|
||||||
|
enabledMediaSourceHolders.add(mediaSourceHolder);
|
||||||
|
}
|
||||||
|
isPrepared = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link MediaPeriod} identified by {@code periodId}.
|
||||||
|
*
|
||||||
|
* @param id The identifier of the period.
|
||||||
|
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
|
||||||
|
* @param startPositionUs The expected start position, in microseconds.
|
||||||
|
* @return A new {@link MediaPeriod}.
|
||||||
|
*/
|
||||||
|
public MediaPeriod createPeriod(
|
||||||
|
MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs) {
|
||||||
|
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
|
||||||
|
MediaSource.MediaPeriodId childMediaPeriodId =
|
||||||
|
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
|
||||||
|
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
|
||||||
|
enableMediaSource(holder);
|
||||||
|
holder.activeMediaPeriodIds.add(childMediaPeriodId);
|
||||||
|
MediaPeriod mediaPeriod =
|
||||||
|
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
|
||||||
|
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
|
||||||
|
disableUnusedMediaSources();
|
||||||
|
return mediaPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the period.
|
||||||
|
*
|
||||||
|
* @param mediaPeriod The period to release.
|
||||||
|
*/
|
||||||
|
public final void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
|
MediaSourceHolder holder =
|
||||||
|
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
|
||||||
|
holder.mediaSource.releasePeriod(mediaPeriod);
|
||||||
|
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
|
||||||
|
if (!mediaSourceByMediaPeriod.isEmpty()) {
|
||||||
|
disableUnusedMediaSources();
|
||||||
|
}
|
||||||
|
maybeReleaseChildSource(holder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases the playlist. */
|
||||||
|
public final void release() {
|
||||||
|
for (MediaSourceAndListener childSource : childSources.values()) {
|
||||||
|
childSource.mediaSource.releaseSource(childSource.caller);
|
||||||
|
childSource.mediaSource.removeEventListener(childSource.eventListener);
|
||||||
|
}
|
||||||
|
childSources.clear();
|
||||||
|
enabledMediaSourceHolders.clear();
|
||||||
|
isPrepared = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throws any pending error encountered while loading or refreshing. */
|
||||||
|
public final void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||||
|
for (MediaSourceAndListener childSource : childSources.values()) {
|
||||||
|
childSource.mediaSource.maybeThrowSourceInfoRefreshError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a timeline reflecting the current state of the playlist. */
|
||||||
|
public final Timeline createTimeline() {
|
||||||
|
if (mediaSourceHolders.isEmpty()) {
|
||||||
|
return Timeline.EMPTY;
|
||||||
|
}
|
||||||
|
int windowOffset = 0;
|
||||||
|
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||||
|
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||||
|
mediaSourceHolder.firstWindowIndexInChild = windowOffset;
|
||||||
|
windowOffset += mediaSourceHolder.mediaSource.getTimeline().getWindowCount();
|
||||||
|
}
|
||||||
|
return new PlaylistTimeline(mediaSourceHolders, shuffleOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private void enableMediaSource(MediaSourceHolder mediaSourceHolder) {
|
||||||
|
enabledMediaSourceHolders.add(mediaSourceHolder);
|
||||||
|
@Nullable MediaSourceAndListener enabledChild = childSources.get(mediaSourceHolder);
|
||||||
|
if (enabledChild != null) {
|
||||||
|
enabledChild.mediaSource.enable(enabledChild.caller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableUnusedMediaSources() {
|
||||||
|
Iterator<MediaSourceHolder> iterator = enabledMediaSourceHolders.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
MediaSourceHolder holder = iterator.next();
|
||||||
|
if (holder.activeMediaPeriodIds.isEmpty()) {
|
||||||
|
disableChildSource(holder);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableChildSource(MediaSourceHolder holder) {
|
||||||
|
@Nullable MediaSourceAndListener disabledChild = childSources.get(holder);
|
||||||
|
if (disabledChild != null) {
|
||||||
|
disabledChild.mediaSource.disable(disabledChild.caller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeMediaSourcesInternal(int fromIndex, int toIndex) {
|
||||||
|
for (int index = toIndex - 1; index >= fromIndex; index--) {
|
||||||
|
MediaSourceHolder holder = mediaSourceHolders.remove(index);
|
||||||
|
mediaSourceByUid.remove(holder.uid);
|
||||||
|
Timeline oldTimeline = holder.mediaSource.getTimeline();
|
||||||
|
correctOffsets(
|
||||||
|
/* startIndex= */ index, /* windowOffsetUpdate= */ -oldTimeline.getWindowCount());
|
||||||
|
holder.isRemoved = true;
|
||||||
|
if (isPrepared) {
|
||||||
|
maybeReleaseChildSource(holder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void correctOffsets(int startIndex, int windowOffsetUpdate) {
|
||||||
|
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
|
||||||
|
MediaSourceHolder mediaSourceHolder = mediaSourceHolders.get(i);
|
||||||
|
mediaSourceHolder.firstWindowIndexInChild += windowOffsetUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods to manage child sources.
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
|
||||||
|
MediaSourceHolder mediaSourceHolder, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||||
|
for (int i = 0; i < mediaSourceHolder.activeMediaPeriodIds.size(); i++) {
|
||||||
|
// Ensure the reported media period id has the same window sequence number as the one created
|
||||||
|
// by this media source. Otherwise it does not belong to this child source.
|
||||||
|
if (mediaSourceHolder.activeMediaPeriodIds.get(i).windowSequenceNumber
|
||||||
|
== mediaPeriodId.windowSequenceNumber) {
|
||||||
|
Object periodUid = getPeriodUid(mediaSourceHolder, mediaPeriodId.periodUid);
|
||||||
|
return mediaPeriodId.copyWithPeriodUid(periodUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getWindowIndexForChildWindowIndex(
|
||||||
|
MediaSourceHolder mediaSourceHolder, int windowIndex) {
|
||||||
|
return windowIndex + mediaSourceHolder.firstWindowIndexInChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareChildSource(MediaSourceHolder holder) {
|
||||||
|
MediaSource mediaSource = holder.mediaSource;
|
||||||
|
MediaSource.MediaSourceCaller caller =
|
||||||
|
(source, timeline) -> playlistInfoListener.onPlaylistUpdateRequested();
|
||||||
|
MediaSourceEventListener eventListener = new ForwardingEventListener(holder);
|
||||||
|
childSources.put(holder, new MediaSourceAndListener(mediaSource, caller, eventListener));
|
||||||
|
mediaSource.addEventListener(new Handler(), eventListener);
|
||||||
|
mediaSource.prepareSource(caller, mediaTransferListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
|
||||||
|
// Release if the source has been removed from the playlist and no periods are still active.
|
||||||
|
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
|
||||||
|
MediaSourceAndListener removedChild =
|
||||||
|
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
|
||||||
|
removedChild.mediaSource.releaseSource(removedChild.caller);
|
||||||
|
removedChild.mediaSource.removeEventListener(removedChild.eventListener);
|
||||||
|
enabledMediaSourceHolders.remove(mediaSourceHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return uid of media source holder from period uid of concatenated source. */
|
||||||
|
private static Object getMediaSourceHolderUid(Object periodUid) {
|
||||||
|
return PlaylistTimeline.getChildTimelineUidFromConcatenatedUid(periodUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return uid of child period from period uid of concatenated source. */
|
||||||
|
private static Object getChildPeriodUid(Object periodUid) {
|
||||||
|
return PlaylistTimeline.getChildPeriodUidFromConcatenatedUid(periodUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) {
|
||||||
|
return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ static void moveMediaSourceHolders(
|
||||||
|
List<MediaSourceHolder> mediaSourceHolders, int fromIndex, int toIndex, int newFromIndex) {
|
||||||
|
MediaSourceHolder[] removedItems = new MediaSourceHolder[toIndex - fromIndex];
|
||||||
|
for (int i = removedItems.length - 1; i >= 0; i--) {
|
||||||
|
removedItems[i] = mediaSourceHolders.remove(fromIndex + i);
|
||||||
|
}
|
||||||
|
mediaSourceHolders.addAll(
|
||||||
|
Math.min(newFromIndex, mediaSourceHolders.size()), Arrays.asList(removedItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Data class to hold playlist media sources together with meta data needed to process them. */
|
||||||
|
/* package */ static final class MediaSourceHolder {
|
||||||
|
|
||||||
|
public final MaskingMediaSource mediaSource;
|
||||||
|
public final Object uid;
|
||||||
|
public final List<MediaSource.MediaPeriodId> activeMediaPeriodIds;
|
||||||
|
|
||||||
|
public int firstWindowIndexInChild;
|
||||||
|
public boolean isRemoved;
|
||||||
|
|
||||||
|
public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) {
|
||||||
|
this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation);
|
||||||
|
this.activeMediaPeriodIds = new ArrayList<>();
|
||||||
|
this.uid = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(int firstWindowIndexInChild) {
|
||||||
|
this.firstWindowIndexInChild = firstWindowIndexInChild;
|
||||||
|
this.isRemoved = false;
|
||||||
|
this.activeMediaPeriodIds.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Timeline exposing concatenated timelines of playlist media sources. */
|
||||||
|
/* package */ static final class PlaylistTimeline extends AbstractConcatenatedTimeline {
|
||||||
|
|
||||||
|
private final int windowCount;
|
||||||
|
private final int periodCount;
|
||||||
|
private final int[] firstPeriodInChildIndices;
|
||||||
|
private final int[] firstWindowInChildIndices;
|
||||||
|
private final Timeline[] timelines;
|
||||||
|
private final Object[] uids;
|
||||||
|
private final HashMap<Object, Integer> childIndexByUid;
|
||||||
|
|
||||||
|
public PlaylistTimeline(
|
||||||
|
Collection<MediaSourceHolder> mediaSourceHolders, ShuffleOrder shuffleOrder) {
|
||||||
|
super(/* isAtomic= */ false, shuffleOrder);
|
||||||
|
int childCount = mediaSourceHolders.size();
|
||||||
|
firstPeriodInChildIndices = new int[childCount];
|
||||||
|
firstWindowInChildIndices = new int[childCount];
|
||||||
|
timelines = new Timeline[childCount];
|
||||||
|
uids = new Object[childCount];
|
||||||
|
childIndexByUid = new HashMap<>();
|
||||||
|
int index = 0;
|
||||||
|
int windowCount = 0;
|
||||||
|
int periodCount = 0;
|
||||||
|
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||||
|
timelines[index] = mediaSourceHolder.mediaSource.getTimeline();
|
||||||
|
firstWindowInChildIndices[index] = windowCount;
|
||||||
|
firstPeriodInChildIndices[index] = periodCount;
|
||||||
|
windowCount += timelines[index].getWindowCount();
|
||||||
|
periodCount += timelines[index].getPeriodCount();
|
||||||
|
uids[index] = mediaSourceHolder.uid;
|
||||||
|
childIndexByUid.put(uids[index], index++);
|
||||||
|
}
|
||||||
|
this.windowCount = windowCount;
|
||||||
|
this.periodCount = periodCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getChildIndexByPeriodIndex(int periodIndex) {
|
||||||
|
return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getChildIndexByWindowIndex(int windowIndex) {
|
||||||
|
return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getChildIndexByChildUid(Object childUid) {
|
||||||
|
Integer index = childIndexByUid.get(childUid);
|
||||||
|
return index == null ? C.INDEX_UNSET : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Timeline getTimelineByChildIndex(int childIndex) {
|
||||||
|
return timelines[childIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getFirstPeriodIndexByChildIndex(int childIndex) {
|
||||||
|
return firstPeriodInChildIndices[childIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getFirstWindowIndexByChildIndex(int childIndex) {
|
||||||
|
return firstWindowInChildIndices[childIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object getChildUidByChildIndex(int childIndex) {
|
||||||
|
return uids[childIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWindowCount() {
|
||||||
|
return windowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPeriodCount() {
|
||||||
|
return periodCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MediaSourceAndListener {
|
||||||
|
|
||||||
|
public final MediaSource mediaSource;
|
||||||
|
public final MediaSource.MediaSourceCaller caller;
|
||||||
|
public final MediaSourceEventListener eventListener;
|
||||||
|
|
||||||
|
public MediaSourceAndListener(
|
||||||
|
MediaSource mediaSource,
|
||||||
|
MediaSource.MediaSourceCaller caller,
|
||||||
|
MediaSourceEventListener eventListener) {
|
||||||
|
this.mediaSource = mediaSource;
|
||||||
|
this.caller = caller;
|
||||||
|
this.eventListener = eventListener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ForwardingEventListener implements MediaSourceEventListener {
|
||||||
|
|
||||||
|
private final Playlist.MediaSourceHolder id;
|
||||||
|
private EventDispatcher eventDispatcher;
|
||||||
|
|
||||||
|
public ForwardingEventListener(Playlist.MediaSourceHolder id) {
|
||||||
|
eventDispatcher = Playlist.this.eventDispatcher;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMediaPeriodCreated(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.mediaPeriodCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMediaPeriodReleased(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.mediaPeriodReleased();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadStarted(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventData,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.loadStarted(loadEventData, mediaLoadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventData,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.loadCompleted(loadEventData, mediaLoadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventData,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.loadCanceled(loadEventData, mediaLoadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadError(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
LoadEventInfo loadEventData,
|
||||||
|
MediaLoadData mediaLoadData,
|
||||||
|
IOException error,
|
||||||
|
boolean wasCanceled) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReadingStarted(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.readingStarted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpstreamDiscarded(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.upstreamDiscarded(mediaLoadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownstreamFormatChanged(
|
||||||
|
int windowIndex,
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId,
|
||||||
|
MediaLoadData mediaLoadData) {
|
||||||
|
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) {
|
||||||
|
eventDispatcher.downstreamFormatChanged(mediaLoadData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates the event dispatcher and returns whether the event should be dispatched. */
|
||||||
|
private boolean maybeUpdateEventDispatcher(
|
||||||
|
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
|
||||||
|
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
|
||||||
|
if (childMediaPeriodId != null) {
|
||||||
|
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
|
||||||
|
if (mediaPeriodId == null) {
|
||||||
|
// Media period not found. Ignore event.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
|
||||||
|
if (eventDispatcher.windowIndex != windowIndex
|
||||||
|
|| !Util.areEqual(eventDispatcher.mediaPeriodId, mediaPeriodId)) {
|
||||||
|
eventDispatcher =
|
||||||
|
Playlist.this.eventDispatcher.withParameters(
|
||||||
|
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
import com.google.android.exoplayer2.metadata.Metadata;
|
import com.google.android.exoplayer2.metadata.Metadata;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
import com.google.android.exoplayer2.metadata.MetadataOutput;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.text.Cue;
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
import com.google.android.exoplayer2.text.TextOutput;
|
import com.google.android.exoplayer2.text.TextOutput;
|
||||||
|
|
@ -163,7 +164,9 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
* @param bandwidthMeter A {@link BandwidthMeter}.
|
* @param bandwidthMeter A {@link BandwidthMeter}.
|
||||||
* @param looper A {@link Looper} that must be used for all calls to the player.
|
* @param looper A {@link Looper} that must be used for all calls to the player.
|
||||||
* @param analyticsCollector An {@link AnalyticsCollector}.
|
* @param analyticsCollector An {@link AnalyticsCollector}.
|
||||||
* @param useLazyPreparation Whether media sources should be initialized lazily.
|
* @param useLazyPreparation Whether playlist items should be prepared lazily. If false, all
|
||||||
|
* initial preparation steps (e.g., manifest loads) happen immediately. If true, these
|
||||||
|
* initial preparations are triggered only when the player starts buffering the media.
|
||||||
* @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}.
|
* @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}.
|
||||||
*/
|
*/
|
||||||
public Builder(
|
public Builder(
|
||||||
|
|
@ -300,6 +303,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
loadControl,
|
loadControl,
|
||||||
bandwidthMeter,
|
bandwidthMeter,
|
||||||
analyticsCollector,
|
analyticsCollector,
|
||||||
|
useLazyPreparation,
|
||||||
clock,
|
clock,
|
||||||
looper);
|
looper);
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +345,6 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
private int audioSessionId;
|
private int audioSessionId;
|
||||||
private AudioAttributes audioAttributes;
|
private AudioAttributes audioAttributes;
|
||||||
private float audioVolume;
|
private float audioVolume;
|
||||||
@Nullable private MediaSource mediaSource;
|
|
||||||
private List<Cue> currentCues;
|
private List<Cue> currentCues;
|
||||||
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
|
||||||
@Nullable private CameraMotionListener cameraMotionListener;
|
@Nullable private CameraMotionListener cameraMotionListener;
|
||||||
|
|
@ -357,6 +360,9 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
||||||
* @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will
|
* @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will
|
||||||
* collect and forward all player events.
|
* collect and forward all player events.
|
||||||
|
* @param useLazyPreparation Whether playlist items are prepared lazily. If false, all manifest
|
||||||
|
* loads and other initial preparation steps happen immediately. If true, these initial
|
||||||
|
* preparations are triggered only when the player starts buffering the media.
|
||||||
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
|
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
|
||||||
* Clock#DEFAULT}, unless the player is being used from a test.
|
* Clock#DEFAULT}, unless the player is being used from a test.
|
||||||
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
||||||
|
|
@ -370,6 +376,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
AnalyticsCollector analyticsCollector,
|
AnalyticsCollector analyticsCollector,
|
||||||
|
boolean useLazyPreparation,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
Looper looper) {
|
Looper looper) {
|
||||||
this(
|
this(
|
||||||
|
|
@ -380,26 +387,14 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
DrmSessionManager.getDummyDrmSessionManager(),
|
DrmSessionManager.getDummyDrmSessionManager(),
|
||||||
bandwidthMeter,
|
bandwidthMeter,
|
||||||
analyticsCollector,
|
analyticsCollector,
|
||||||
|
useLazyPreparation,
|
||||||
clock,
|
clock,
|
||||||
looper);
|
looper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context A {@link Context}.
|
|
||||||
* @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
|
|
||||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
|
||||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
|
||||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
|
|
||||||
* will not be used for DRM protected playbacks.
|
|
||||||
* @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance.
|
|
||||||
* @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all
|
|
||||||
* player events.
|
|
||||||
* @param clock The {@link Clock} that will be used by the instance. Should always be {@link
|
|
||||||
* Clock#DEFAULT}, unless the player is being used from a test.
|
|
||||||
* @param looper The {@link Looper} which must be used for all calls to the player and which is
|
|
||||||
* used to call listeners on.
|
|
||||||
* @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl,
|
* @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl,
|
||||||
* BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link
|
* BandwidthMeter, AnalyticsCollector, boolean, Clock, Looper)} instead, and pass the {@link
|
||||||
* DrmSessionManager} to the {@link MediaSource} factories.
|
* DrmSessionManager} to the {@link MediaSource} factories.
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
|
@ -411,6 +406,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
@Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||||
BandwidthMeter bandwidthMeter,
|
BandwidthMeter bandwidthMeter,
|
||||||
AnalyticsCollector analyticsCollector,
|
AnalyticsCollector analyticsCollector,
|
||||||
|
boolean useLazyPreparation,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
Looper looper) {
|
Looper looper) {
|
||||||
this.bandwidthMeter = bandwidthMeter;
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
|
|
@ -441,7 +437,15 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
|
|
||||||
// Build the player and associated objects.
|
// Build the player and associated objects.
|
||||||
player =
|
player =
|
||||||
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
|
new ExoPlayerImpl(
|
||||||
|
renderers,
|
||||||
|
trackSelector,
|
||||||
|
loadControl,
|
||||||
|
bandwidthMeter,
|
||||||
|
analyticsCollector,
|
||||||
|
useLazyPreparation,
|
||||||
|
clock,
|
||||||
|
looper);
|
||||||
analyticsCollector.setPlayer(player);
|
analyticsCollector.setPlayer(player);
|
||||||
addListener(analyticsCollector);
|
addListener(analyticsCollector);
|
||||||
addListener(componentListener);
|
addListener(componentListener);
|
||||||
|
|
@ -1130,32 +1134,133 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public void retry() {
|
public void retry() {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
if (mediaSource != null
|
prepare();
|
||||||
&& (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) {
|
|
||||||
prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
public void prepare() {
|
||||||
|
verifyApplicationThread();
|
||||||
|
@AudioFocusManager.PlayerCommand
|
||||||
|
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
|
||||||
|
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
|
||||||
|
player.prepare();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Deprecated
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public void prepare(MediaSource mediaSource) {
|
public void prepare(MediaSource mediaSource) {
|
||||||
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
|
prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Deprecated
|
||||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
if (this.mediaSource != null) {
|
setMediaItems(
|
||||||
this.mediaSource.removeEventListener(analyticsCollector);
|
Collections.singletonList(mediaSource),
|
||||||
analyticsCollector.resetForNewMediaSource();
|
/* startWindowIndex= */ resetPosition ? 0 : C.INDEX_UNSET,
|
||||||
}
|
/* startPositionMs= */ C.TIME_UNSET);
|
||||||
this.mediaSource = mediaSource;
|
prepare();
|
||||||
mediaSource.addEventListener(eventHandler, analyticsCollector);
|
}
|
||||||
@AudioFocusManager.PlayerCommand
|
|
||||||
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady());
|
@Override
|
||||||
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
|
public void setMediaItems(List<MediaSource> mediaItems) {
|
||||||
player.prepare(mediaSource, resetPosition, resetState);
|
verifyApplicationThread();
|
||||||
|
analyticsCollector.resetForNewPlaylist();
|
||||||
|
player.setMediaItems(mediaItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
analyticsCollector.resetForNewPlaylist();
|
||||||
|
player.setMediaItems(mediaItems, resetPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(
|
||||||
|
List<MediaSource> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
analyticsCollector.resetForNewPlaylist();
|
||||||
|
player.setMediaItems(mediaItems, startWindowIndex, startPositionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
analyticsCollector.resetForNewPlaylist();
|
||||||
|
player.setMediaItem(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem, long startPositionMs) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
analyticsCollector.resetForNewPlaylist();
|
||||||
|
player.setMediaItem(mediaItem, startPositionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(MediaSource mediaSource) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.addMediaItem(mediaSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(int index, MediaSource mediaSource) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.addMediaItem(index, mediaSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(List<MediaSource> mediaSources) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.addMediaItems(mediaSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(int index, List<MediaSource> mediaSources) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.addMediaItems(index, mediaSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItem(int currentIndex, int newIndex) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.moveMediaItem(currentIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.moveMediaItems(fromIndex, toIndex, newIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource removeMediaItem(int index) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
return player.removeMediaItem(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.removeMediaItems(fromIndex, toIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearMediaItems() {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.clearMediaItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
verifyApplicationThread();
|
||||||
|
player.setShuffleOrder(shuffleOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1235,6 +1340,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setForegroundMode(boolean foregroundMode) {
|
public void setForegroundMode(boolean foregroundMode) {
|
||||||
|
verifyApplicationThread();
|
||||||
player.setForegroundMode(foregroundMode);
|
player.setForegroundMode(foregroundMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1242,13 +1348,6 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
public void stop(boolean reset) {
|
public void stop(boolean reset) {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
player.stop(reset);
|
player.stop(reset);
|
||||||
if (mediaSource != null) {
|
|
||||||
mediaSource.removeEventListener(analyticsCollector);
|
|
||||||
analyticsCollector.resetForNewMediaSource();
|
|
||||||
if (reset) {
|
|
||||||
mediaSource = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
audioFocusManager.handleStop();
|
audioFocusManager.handleStop();
|
||||||
currentCues = Collections.emptyList();
|
currentCues = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
@ -1266,10 +1365,6 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
}
|
}
|
||||||
surface = null;
|
surface = null;
|
||||||
}
|
}
|
||||||
if (mediaSource != null) {
|
|
||||||
mediaSource.removeEventListener(analyticsCollector);
|
|
||||||
mediaSource = null;
|
|
||||||
}
|
|
||||||
if (isPriorityTaskManagerRegistered) {
|
if (isPriorityTaskManagerRegistered) {
|
||||||
Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
|
Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK);
|
||||||
isPriorityTaskManagerRegistered = false;
|
isPriorityTaskManagerRegistered = false;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.util.Assertions;
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flexible representation of the structure of media. A timeline is able to represent the
|
* A flexible representation of the structure of media. A timeline is able to represent the
|
||||||
|
|
@ -278,6 +279,48 @@ public abstract class Timeline {
|
||||||
return positionInFirstPeriodUs;
|
return positionInFirstPeriodUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || !getClass().equals(obj.getClass())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Window that = (Window) obj;
|
||||||
|
return Util.areEqual(uid, that.uid)
|
||||||
|
&& Util.areEqual(tag, that.tag)
|
||||||
|
&& Util.areEqual(manifest, that.manifest)
|
||||||
|
&& presentationStartTimeMs == that.presentationStartTimeMs
|
||||||
|
&& windowStartTimeMs == that.windowStartTimeMs
|
||||||
|
&& isSeekable == that.isSeekable
|
||||||
|
&& isDynamic == that.isDynamic
|
||||||
|
&& isLive == that.isLive
|
||||||
|
&& defaultPositionUs == that.defaultPositionUs
|
||||||
|
&& durationUs == that.durationUs
|
||||||
|
&& firstPeriodIndex == that.firstPeriodIndex
|
||||||
|
&& lastPeriodIndex == that.lastPeriodIndex
|
||||||
|
&& positionInFirstPeriodUs == that.positionInFirstPeriodUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = 7;
|
||||||
|
result = 31 * result + uid.hashCode();
|
||||||
|
result = 31 * result + (tag == null ? 0 : tag.hashCode());
|
||||||
|
result = 31 * result + (manifest == null ? 0 : manifest.hashCode());
|
||||||
|
result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32));
|
||||||
|
result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32));
|
||||||
|
result = 31 * result + (isSeekable ? 1 : 0);
|
||||||
|
result = 31 * result + (isDynamic ? 1 : 0);
|
||||||
|
result = 31 * result + (isLive ? 1 : 0);
|
||||||
|
result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32));
|
||||||
|
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
|
||||||
|
result = 31 * result + firstPeriodIndex;
|
||||||
|
result = 31 * result + lastPeriodIndex;
|
||||||
|
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -534,6 +577,34 @@ public abstract class Timeline {
|
||||||
return adPlaybackState.adResumePositionUs;
|
return adPlaybackState.adResumePositionUs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || !getClass().equals(obj.getClass())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Period that = (Period) obj;
|
||||||
|
return Util.areEqual(id, that.id)
|
||||||
|
&& Util.areEqual(uid, that.uid)
|
||||||
|
&& windowIndex == that.windowIndex
|
||||||
|
&& durationUs == that.durationUs
|
||||||
|
&& positionInWindowUs == that.positionInWindowUs
|
||||||
|
&& Util.areEqual(adPlaybackState, that.adPlaybackState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = 7;
|
||||||
|
result = 31 * result + (id == null ? 0 : id.hashCode());
|
||||||
|
result = 31 * result + (uid == null ? 0 : uid.hashCode());
|
||||||
|
result = 31 * result + windowIndex;
|
||||||
|
result = 31 * result + (int) (durationUs ^ (durationUs >>> 32));
|
||||||
|
result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32));
|
||||||
|
result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An empty timeline. */
|
/** An empty timeline. */
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,8 @@ public class AnalyticsCollector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Resets the analytics collector for a new playlist. */
|
||||||
* Resets the analytics collector for a new media source. Should be called before the player is
|
public final void resetForNewPlaylist() {
|
||||||
* prepared with a new media source.
|
|
||||||
*/
|
|
||||||
public final void resetForNewMediaSource() {
|
|
||||||
// Copying the list is needed because onMediaPeriodReleased will modify the list.
|
// Copying the list is needed because onMediaPeriodReleased will modify the list.
|
||||||
List<MediaPeriodInfo> mediaPeriodInfos =
|
List<MediaPeriodInfo> mediaPeriodInfos =
|
||||||
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
|
new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue);
|
||||||
|
|
@ -804,9 +801,13 @@ public class AnalyticsCollector
|
||||||
|
|
||||||
/** Updates the queue with a newly created media period. */
|
/** Updates the queue with a newly created media period. */
|
||||||
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
|
public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {
|
||||||
boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET;
|
int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid);
|
||||||
|
boolean isInTimeline = periodIndex != C.INDEX_UNSET;
|
||||||
MediaPeriodInfo mediaPeriodInfo =
|
MediaPeriodInfo mediaPeriodInfo =
|
||||||
new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex);
|
new MediaPeriodInfo(
|
||||||
|
mediaPeriodId,
|
||||||
|
isInTimeline ? timeline : Timeline.EMPTY,
|
||||||
|
isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex);
|
||||||
mediaPeriodInfoQueue.add(mediaPeriodInfo);
|
mediaPeriodInfoQueue.add(mediaPeriodInfo);
|
||||||
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
|
mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo);
|
||||||
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
|
lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0);
|
||||||
|
|
@ -822,7 +823,7 @@ public class AnalyticsCollector
|
||||||
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
|
public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId) {
|
||||||
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
|
MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId);
|
||||||
if (mediaPeriodInfo == null) {
|
if (mediaPeriodInfo == null) {
|
||||||
// The media period has already been removed from the queue in resetForNewMediaSource().
|
// The media period has already been removed from the queue in resetForNewPlaylist().
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
mediaPeriodInfoQueue.remove(mediaPeriodInfo);
|
mediaPeriodInfoQueue.remove(mediaPeriodInfo);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder;
|
||||||
|
|
@ -139,6 +140,23 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
|
||||||
addMediaSources(Arrays.asList(mediaSources));
|
addMediaSources(Arrays.asList(mediaSources));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Timeline getInitialTimeline() {
|
||||||
|
ShuffleOrder shuffleOrder =
|
||||||
|
this.shuffleOrder.getLength() != mediaSourcesPublic.size()
|
||||||
|
? this.shuffleOrder
|
||||||
|
.cloneAndClear()
|
||||||
|
.cloneAndInsert(
|
||||||
|
/* insertionIndex= */ 0, /* insertionCount= */ mediaSourcesPublic.size())
|
||||||
|
: this.shuffleOrder;
|
||||||
|
return new ConcatenatedTimeline(mediaSourcesPublic, shuffleOrder, isAtomic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSingleWindow() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends a {@link MediaSource} to the playlist.
|
* Appends a {@link MediaSource} to the playlist.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package com.google.android.exoplayer2.source;
|
package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.AbstractConcatenatedTimeline;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
|
||||||
|
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.Timeline.Window;
|
import com.google.android.exoplayer2.Timeline.Window;
|
||||||
|
|
@ -43,6 +44,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
@Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher;
|
@Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher;
|
||||||
private boolean hasStartedPreparing;
|
private boolean hasStartedPreparing;
|
||||||
private boolean isPrepared;
|
private boolean isPrepared;
|
||||||
|
private boolean hasRealTimeline;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the masking media source.
|
* Creates the masking media source.
|
||||||
|
|
@ -54,14 +56,22 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
*/
|
*/
|
||||||
public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
|
public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSource = mediaSource;
|
||||||
this.useLazyPreparation = useLazyPreparation;
|
this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow();
|
||||||
window = new Timeline.Window();
|
window = new Timeline.Window();
|
||||||
period = new Timeline.Period();
|
period = new Timeline.Period();
|
||||||
timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());
|
Timeline initialTimeline = mediaSource.getInitialTimeline();
|
||||||
|
if (initialTimeline != null) {
|
||||||
|
timeline =
|
||||||
|
MaskingTimeline.createWithRealTimeline(
|
||||||
|
initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null);
|
||||||
|
hasRealTimeline = true;
|
||||||
|
} else {
|
||||||
|
timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the {@link Timeline}. */
|
/** Returns the {@link Timeline}. */
|
||||||
public Timeline getTimeline() {
|
public synchronized Timeline getTimeline() {
|
||||||
return timeline;
|
return timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,14 +139,16 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChildSourceInfoRefreshed(
|
protected synchronized void onChildSourceInfoRefreshed(
|
||||||
Void id, MediaSource mediaSource, Timeline newTimeline) {
|
Void id, MediaSource mediaSource, Timeline newTimeline) {
|
||||||
if (isPrepared) {
|
if (isPrepared) {
|
||||||
timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
|
timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
|
||||||
} else if (newTimeline.isEmpty()) {
|
} else if (newTimeline.isEmpty()) {
|
||||||
timeline =
|
timeline =
|
||||||
MaskingTimeline.createWithRealTimeline(
|
hasRealTimeline
|
||||||
newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);
|
? timeline.cloneWithUpdatedTimeline(newTimeline)
|
||||||
|
: MaskingTimeline.createWithRealTimeline(
|
||||||
|
newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID);
|
||||||
} else {
|
} else {
|
||||||
// Determine first period and the start position.
|
// Determine first period and the start position.
|
||||||
// This will be:
|
// This will be:
|
||||||
|
|
@ -164,7 +176,10 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
||||||
Object periodUid = periodPosition.first;
|
Object periodUid = periodPosition.first;
|
||||||
long periodPositionUs = periodPosition.second;
|
long periodPositionUs = periodPosition.second;
|
||||||
timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
|
timeline =
|
||||||
|
hasRealTimeline
|
||||||
|
? timeline.cloneWithUpdatedTimeline(newTimeline)
|
||||||
|
: MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
|
||||||
if (unpreparedMaskingMediaPeriod != null) {
|
if (unpreparedMaskingMediaPeriod != null) {
|
||||||
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
|
MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
|
||||||
maskingPeriod.overridePreparePositionUs(periodPositionUs);
|
maskingPeriod.overridePreparePositionUs(periodPositionUs);
|
||||||
|
|
@ -173,6 +188,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
maskingPeriod.createPeriod(idInSource);
|
maskingPeriod.createPeriod(idInSource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
hasRealTimeline = true;
|
||||||
isPrepared = true;
|
isPrepared = true;
|
||||||
refreshSourceInfo(this.timeline);
|
refreshSourceInfo(this.timeline);
|
||||||
}
|
}
|
||||||
|
|
@ -193,13 +209,15 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object getInternalPeriodUid(Object externalPeriodUid) {
|
private Object getInternalPeriodUid(Object externalPeriodUid) {
|
||||||
return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)
|
return timeline.replacedInternalPeriodUid != null
|
||||||
|
&& externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID)
|
||||||
? timeline.replacedInternalPeriodUid
|
? timeline.replacedInternalPeriodUid
|
||||||
: externalPeriodUid;
|
: externalPeriodUid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object getExternalPeriodUid(Object internalPeriodUid) {
|
private Object getExternalPeriodUid(Object internalPeriodUid) {
|
||||||
return timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
|
return timeline.replacedInternalPeriodUid != null
|
||||||
|
&& timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
|
||||||
? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID
|
? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID
|
||||||
: internalPeriodUid;
|
: internalPeriodUid;
|
||||||
}
|
}
|
||||||
|
|
@ -212,8 +230,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
|
|
||||||
public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object();
|
public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object();
|
||||||
|
|
||||||
private final Object replacedInternalWindowUid;
|
@Nullable private final Object replacedInternalWindowUid;
|
||||||
private final Object replacedInternalPeriodUid;
|
@Nullable private final Object replacedInternalPeriodUid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an instance with a dummy timeline using the provided window tag.
|
* Returns an instance with a dummy timeline using the provided window tag.
|
||||||
|
|
@ -236,12 +254,14 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
* assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}.
|
* assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}.
|
||||||
*/
|
*/
|
||||||
public static MaskingTimeline createWithRealTimeline(
|
public static MaskingTimeline createWithRealTimeline(
|
||||||
Timeline timeline, Object firstWindowUid, Object firstPeriodUid) {
|
Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) {
|
||||||
return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
|
return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MaskingTimeline(
|
private MaskingTimeline(
|
||||||
Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) {
|
Timeline timeline,
|
||||||
|
@Nullable Object replacedInternalWindowUid,
|
||||||
|
@Nullable Object replacedInternalPeriodUid) {
|
||||||
super(timeline);
|
super(timeline);
|
||||||
this.replacedInternalWindowUid = replacedInternalWindowUid;
|
this.replacedInternalWindowUid = replacedInternalWindowUid;
|
||||||
this.replacedInternalPeriodUid = replacedInternalPeriodUid;
|
this.replacedInternalPeriodUid = replacedInternalPeriodUid;
|
||||||
|
|
@ -282,7 +302,9 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
@Override
|
@Override
|
||||||
public int getIndexOfPeriod(Object uid) {
|
public int getIndexOfPeriod(Object uid) {
|
||||||
return timeline.getIndexOfPeriod(
|
return timeline.getIndexOfPeriod(
|
||||||
DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid);
|
DUMMY_EXTERNAL_PERIOD_UID.equals(uid) && replacedInternalPeriodUid != null
|
||||||
|
? replacedInternalPeriodUid
|
||||||
|
: uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -293,7 +315,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
|
/** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
|
||||||
private static final class DummyTimeline extends Timeline {
|
@VisibleForTesting
|
||||||
|
public static final class DummyTimeline extends Timeline {
|
||||||
|
|
||||||
@Nullable private final Object tag;
|
@Nullable private final Object tag;
|
||||||
|
|
||||||
|
|
@ -333,8 +356,8 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
||||||
@Override
|
@Override
|
||||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||||
return period.set(
|
return period.set(
|
||||||
/* id= */ 0,
|
/* id= */ setIds ? 0 : null,
|
||||||
/* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID,
|
/* uid= */ setIds ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID : null,
|
||||||
/* windowIndex= */ 0,
|
/* windowIndex= */ 0,
|
||||||
/* durationUs = */ C.TIME_UNSET,
|
/* durationUs = */ C.TIME_UNSET,
|
||||||
/* positionInWindowUs= */ 0);
|
/* positionInWindowUs= */ 0);
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,33 @@ public interface MediaSource {
|
||||||
*/
|
*/
|
||||||
void removeEventListener(MediaSourceEventListener eventListener);
|
void removeEventListener(MediaSourceEventListener eventListener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the initial dummy timeline that is returned immediately when the real timeline is not
|
||||||
|
* yet known, or null to let the player create an initial timeline.
|
||||||
|
*
|
||||||
|
* <p>The initial timeline must use the same uids for windows and periods that the real timeline
|
||||||
|
* will use. It also must provide windows which are marked as dynamic to indicate that the window
|
||||||
|
* is expected to change when the real timeline arrives.
|
||||||
|
*
|
||||||
|
* <p>Any media source which has multiple windows should typically provide such an initial
|
||||||
|
* timeline to make sure the player reports the correct number of windows immediately.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
default Timeline getInitialTimeline() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the media source is guaranteed to never have zero or more than one window.
|
||||||
|
*
|
||||||
|
* <p>The default implementation returns {@code true}.
|
||||||
|
*
|
||||||
|
* @return true if the source has exactly one window.
|
||||||
|
*/
|
||||||
|
default boolean isSingleWindow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the tag set on the media source, or null if none was set. */
|
/** Returns the tag set on the media source, or null if none was set. */
|
||||||
@Nullable
|
@Nullable
|
||||||
default Object getTag() {
|
default Object getTag() {
|
||||||
|
|
|
||||||
|
|
@ -630,12 +630,10 @@ public class EventLogger implements AnalyticsListener {
|
||||||
|
|
||||||
private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {
|
private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) {
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case Player.TIMELINE_CHANGE_REASON_PREPARED:
|
case Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE:
|
||||||
return "PREPARED";
|
return "SOURCE_UPDATE";
|
||||||
case Player.TIMELINE_CHANGE_REASON_RESET:
|
case Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED:
|
||||||
return "RESET";
|
return "PLAYLIST_CHANGED";
|
||||||
case Player.TIMELINE_CHANGE_REASON_DYNAMIC:
|
|
||||||
return "DYNAMIC";
|
|
||||||
default:
|
default:
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import com.google.android.exoplayer2.Renderer;
|
||||||
import com.google.android.exoplayer2.RendererCapabilities;
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
import com.google.android.exoplayer2.RenderersFactory;
|
import com.google.android.exoplayer2.RenderersFactory;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||||
|
|
@ -2037,6 +2038,42 @@ public final class Util {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the timelines are the same.
|
||||||
|
*
|
||||||
|
* @param firstTimeline The first {@link Timeline}.
|
||||||
|
* @param secondTimeline The second {@link Timeline} to compare with.
|
||||||
|
* @return {@code true} if the both timelines are the same.
|
||||||
|
*/
|
||||||
|
public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) {
|
||||||
|
if (firstTimeline == secondTimeline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount()
|
||||||
|
|| secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Timeline.Window firstWindow = new Timeline.Window();
|
||||||
|
Timeline.Period firstPeriod = new Timeline.Period();
|
||||||
|
Timeline.Window secondWindow = new Timeline.Window();
|
||||||
|
Timeline.Period secondPeriod = new Timeline.Period();
|
||||||
|
for (int i = 0; i < firstTimeline.getWindowCount(); i++) {
|
||||||
|
if (!firstTimeline
|
||||||
|
.getWindow(i, firstWindow)
|
||||||
|
.equals(secondTimeline.getWindow(i, secondWindow))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < firstTimeline.getPeriodCount(); i++) {
|
||||||
|
if (!firstTimeline
|
||||||
|
.getPeriod(i, firstPeriod, /* setIds= */ true)
|
||||||
|
.equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ true))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static HashMap<String, String> createIso3ToIso2Map() {
|
private static HashMap<String, String> createIso3ToIso2Map() {
|
||||||
String[] iso2Languages = Locale.getISOLanguages();
|
String[] iso2Languages = Locale.getISOLanguages();
|
||||||
HashMap<String, String> iso3ToIso2 =
|
HashMap<String, String> iso3ToIso2 =
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -21,15 +21,17 @@ import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
|
||||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||||
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
|
||||||
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
|
import com.google.android.exoplayer2.source.ads.SinglePeriodAdTimeline;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||||
import com.google.android.exoplayer2.upstream.Allocator;
|
import com.google.android.exoplayer2.upstream.Allocator;
|
||||||
|
import java.util.Collections;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -50,19 +52,20 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
private MediaPeriodQueue mediaPeriodQueue;
|
private MediaPeriodQueue mediaPeriodQueue;
|
||||||
private AdPlaybackState adPlaybackState;
|
private AdPlaybackState adPlaybackState;
|
||||||
private Timeline timeline;
|
|
||||||
private Object periodUid;
|
private Object periodUid;
|
||||||
|
|
||||||
private PlaybackInfo playbackInfo;
|
private PlaybackInfo playbackInfo;
|
||||||
private RendererCapabilities[] rendererCapabilities;
|
private RendererCapabilities[] rendererCapabilities;
|
||||||
private TrackSelector trackSelector;
|
private TrackSelector trackSelector;
|
||||||
private Allocator allocator;
|
private Allocator allocator;
|
||||||
private MediaSource mediaSource;
|
private Playlist playlist;
|
||||||
|
private FakeMediaSource fakeMediaSource;
|
||||||
|
private Playlist.MediaSourceHolder mediaSourceHolder;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
mediaPeriodQueue = new MediaPeriodQueue();
|
mediaPeriodQueue = new MediaPeriodQueue();
|
||||||
mediaSource = mock(MediaSource.class);
|
playlist = mock(Playlist.class);
|
||||||
rendererCapabilities = new RendererCapabilities[0];
|
rendererCapabilities = new RendererCapabilities[0];
|
||||||
trackSelector = mock(TrackSelector.class);
|
trackSelector = mock(TrackSelector.class);
|
||||||
allocator = mock(Allocator.class);
|
allocator = mock(Allocator.class);
|
||||||
|
|
@ -70,7 +73,7 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
|
public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() {
|
||||||
setupTimeline(/* initialPositionUs= */ 0);
|
setupTimeline();
|
||||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
/* endPositionUs= */ C.TIME_UNSET,
|
/* endPositionUs= */ C.TIME_UNSET,
|
||||||
|
|
@ -81,7 +84,7 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
|
public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() {
|
||||||
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0);
|
setupTimeline(/* adGroupTimesUs= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
|
assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0);
|
||||||
advance();
|
advance();
|
||||||
|
|
@ -95,10 +98,7 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {
|
public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() {
|
||||||
setupTimeline(
|
setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US);
|
|
||||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
/* endPositionUs= */ FIRST_AD_START_TIME_US,
|
/* endPositionUs= */ FIRST_AD_START_TIME_US,
|
||||||
|
|
@ -133,10 +133,7 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {
|
public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() {
|
||||||
setupTimeline(
|
setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE);
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
C.TIME_END_OF_SOURCE);
|
|
||||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
/* endPositionUs= */ FIRST_AD_START_TIME_US,
|
/* endPositionUs= */ FIRST_AD_START_TIME_US,
|
||||||
|
|
@ -169,7 +166,7 @@ public final class MediaPeriodQueueTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
|
public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() {
|
||||||
setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE);
|
setupTimeline(/* adGroupTimesUs= */ C.TIME_END_OF_SOURCE);
|
||||||
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
/* endPositionUs= */ C.TIME_END_OF_SOURCE,
|
/* endPositionUs= */ C.TIME_END_OF_SOURCE,
|
||||||
|
|
@ -189,10 +186,7 @@ public final class MediaPeriodQueueTest {
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
setupTimeline(
|
setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
enqueueNext(); // Content before first ad.
|
enqueueNext(); // Content before first ad.
|
||||||
|
|
@ -202,10 +196,8 @@ public final class MediaPeriodQueueTest {
|
||||||
enqueueNext(); // Second ad.
|
enqueueNext(); // Second ad.
|
||||||
|
|
||||||
// Change position of second ad (= change duration of content between ads).
|
// Change position of second ad (= change duration of content between ads).
|
||||||
setupTimeline(
|
updateAdPlaybackStateAndTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US + 1);
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US + 1);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
boolean changeHandled =
|
boolean changeHandled =
|
||||||
|
|
@ -219,10 +211,7 @@ public final class MediaPeriodQueueTest {
|
||||||
@Test
|
@Test
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
setupTimeline(
|
setupTimeline(/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US);
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
enqueueNext(); // Content before first ad.
|
enqueueNext(); // Content before first ad.
|
||||||
|
|
@ -233,10 +222,8 @@ public final class MediaPeriodQueueTest {
|
||||||
advanceReading(); // Reading first ad.
|
advanceReading(); // Reading first ad.
|
||||||
|
|
||||||
// Change position of first ad (= change duration of content before first ad).
|
// Change position of first ad (= change duration of content before first ad).
|
||||||
setupTimeline(
|
updateAdPlaybackStateAndTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, SECOND_AD_START_TIME_US);
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1,
|
|
||||||
SECOND_AD_START_TIME_US);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
boolean changeHandled =
|
boolean changeHandled =
|
||||||
|
|
@ -251,7 +238,6 @@ public final class MediaPeriodQueueTest {
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
setupTimeline(
|
setupTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
||||||
SECOND_AD_START_TIME_US);
|
SECOND_AD_START_TIME_US);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
|
|
@ -265,10 +251,8 @@ public final class MediaPeriodQueueTest {
|
||||||
advanceReading(); // Reading content between ads.
|
advanceReading(); // Reading content between ads.
|
||||||
|
|
||||||
// Change position of second ad (= change duration of content between ads).
|
// Change position of second ad (= change duration of content between ads).
|
||||||
setupTimeline(
|
updateAdPlaybackStateAndTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US - 1000);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;
|
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US;
|
||||||
|
|
@ -285,7 +269,6 @@ public final class MediaPeriodQueueTest {
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
setupTimeline(
|
setupTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
||||||
SECOND_AD_START_TIME_US);
|
SECOND_AD_START_TIME_US);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
|
|
@ -299,10 +282,8 @@ public final class MediaPeriodQueueTest {
|
||||||
advanceReading(); // Reading content between ads.
|
advanceReading(); // Reading content between ads.
|
||||||
|
|
||||||
// Change position of second ad (= change duration of content between ads).
|
// Change position of second ad (= change duration of content between ads).
|
||||||
setupTimeline(
|
updateAdPlaybackStateAndTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US - 1000);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;
|
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US;
|
||||||
|
|
@ -319,7 +300,6 @@ public final class MediaPeriodQueueTest {
|
||||||
public void
|
public void
|
||||||
updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() {
|
||||||
setupTimeline(
|
setupTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
||||||
SECOND_AD_START_TIME_US);
|
SECOND_AD_START_TIME_US);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
|
|
@ -333,10 +313,8 @@ public final class MediaPeriodQueueTest {
|
||||||
advanceReading(); // Reading content between ads.
|
advanceReading(); // Reading content between ads.
|
||||||
|
|
||||||
// Change position of second ad (= change duration of content between ads).
|
// Change position of second ad (= change duration of content between ads).
|
||||||
setupTimeline(
|
updateAdPlaybackStateAndTimeline(
|
||||||
/* initialPositionUs= */ 0,
|
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
|
||||||
/* adGroupTimesUs= */ FIRST_AD_START_TIME_US,
|
|
||||||
SECOND_AD_START_TIME_US - 1000);
|
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
setAdGroupLoaded(/* adGroupIndex= */ 0);
|
||||||
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
setAdGroupLoaded(/* adGroupIndex= */ 1);
|
||||||
boolean changeHandled =
|
boolean changeHandled =
|
||||||
|
|
@ -347,16 +325,25 @@ public final class MediaPeriodQueueTest {
|
||||||
assertThat(getQueueLength()).isEqualTo(3);
|
assertThat(getQueueLength()).isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) {
|
private void setupTimeline(long... adGroupTimesUs) {
|
||||||
adPlaybackState =
|
adPlaybackState =
|
||||||
new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
|
new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
|
||||||
timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
|
|
||||||
|
// Create a media source holder.
|
||||||
|
SinglePeriodAdTimeline adTimeline =
|
||||||
|
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
|
||||||
|
fakeMediaSource = new FakeMediaSource(adTimeline);
|
||||||
|
mediaSourceHolder = new Playlist.MediaSourceHolder(fakeMediaSource, false);
|
||||||
|
mediaSourceHolder.mediaSource.prepareSourceInternal(/* mediaTransferListener */ null);
|
||||||
|
|
||||||
|
Timeline timeline = createPlaylistTimeline();
|
||||||
periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
|
periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0);
|
||||||
mediaPeriodQueue.setTimeline(timeline);
|
mediaPeriodQueue.setTimeline(timeline);
|
||||||
|
|
||||||
playbackInfo =
|
playbackInfo =
|
||||||
new PlaybackInfo(
|
new PlaybackInfo(
|
||||||
timeline,
|
timeline,
|
||||||
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, initialPositionUs),
|
mediaPeriodQueue.resolveMediaPeriodIdForAds(periodUid, /* positionUs= */ 0),
|
||||||
/* startPositionUs= */ 0,
|
/* startPositionUs= */ 0,
|
||||||
/* contentPositionUs= */ 0,
|
/* contentPositionUs= */ 0,
|
||||||
Player.STATE_READY,
|
Player.STATE_READY,
|
||||||
|
|
@ -370,6 +357,25 @@ public final class MediaPeriodQueueTest {
|
||||||
/* positionUs= */ 0);
|
/* positionUs= */ 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateAdPlaybackStateAndTimeline(long... adGroupTimesUs) {
|
||||||
|
adPlaybackState =
|
||||||
|
new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US);
|
||||||
|
updateTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimeline() {
|
||||||
|
SinglePeriodAdTimeline adTimeline =
|
||||||
|
new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
|
||||||
|
fakeMediaSource.setNewSourceInfo(adTimeline, /* manifest */ null);
|
||||||
|
mediaPeriodQueue.setTimeline(createPlaylistTimeline());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Playlist.PlaylistTimeline createPlaylistTimeline() {
|
||||||
|
return new Playlist.PlaylistTimeline(
|
||||||
|
Collections.singleton(mediaSourceHolder),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
|
||||||
|
}
|
||||||
|
|
||||||
private void advance() {
|
private void advance() {
|
||||||
enqueueNext();
|
enqueueNext();
|
||||||
if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) {
|
if (mediaPeriodQueue.getLoadingPeriod() != mediaPeriodQueue.getPlayingPeriod()) {
|
||||||
|
|
@ -390,7 +396,7 @@ public final class MediaPeriodQueueTest {
|
||||||
rendererCapabilities,
|
rendererCapabilities,
|
||||||
trackSelector,
|
trackSelector,
|
||||||
allocator,
|
allocator,
|
||||||
mediaSource,
|
playlist,
|
||||||
getNextMediaPeriodInfo(),
|
getNextMediaPeriodInfo(),
|
||||||
new TrackSelectorResult(
|
new TrackSelectorResult(
|
||||||
new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
|
new RendererConfiguration[0], new TrackSelection[0], /* info= */ null));
|
||||||
|
|
@ -422,11 +428,6 @@ public final class MediaPeriodQueueTest {
|
||||||
updateTimeline();
|
updateTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTimeline() {
|
|
||||||
timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState);
|
|
||||||
mediaPeriodQueue.setTimeline(timeline);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod(
|
||||||
long startPositionUs,
|
long startPositionUs,
|
||||||
long endPositionUs,
|
long endPositionUs,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,510 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 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;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
|
||||||
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link Playlist}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class PlaylistTest {
|
||||||
|
|
||||||
|
private static final int PLAYLIST_SIZE = 4;
|
||||||
|
|
||||||
|
private Playlist playlist;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
playlist = new Playlist(mock(Playlist.PlaylistInfoRefreshListener.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyPlaylist_expectConstantTimelineInstanceEMPTY() {
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
|
||||||
|
List<Playlist.MediaSourceHolder> fakeHolders = createFakeHolders();
|
||||||
|
|
||||||
|
Timeline timeline = playlist.setMediaSources(fakeHolders, shuffleOrder);
|
||||||
|
assertNotSame(timeline, Timeline.EMPTY);
|
||||||
|
|
||||||
|
// Remove all media sources.
|
||||||
|
timeline =
|
||||||
|
playlist.removeMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ timeline.getWindowCount(), shuffleOrder);
|
||||||
|
assertSame(timeline, Timeline.EMPTY);
|
||||||
|
|
||||||
|
timeline = playlist.setMediaSources(fakeHolders, shuffleOrder);
|
||||||
|
assertNotSame(timeline, Timeline.EMPTY);
|
||||||
|
// Clear.
|
||||||
|
timeline = playlist.clear(shuffleOrder);
|
||||||
|
assertSame(timeline, Timeline.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrepareAndReprepareAfterRelease_expectSourcePreparationAfterPlaylistPrepare() {
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
playlist.setMediaSources(
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2));
|
||||||
|
// Verify prepare is called once on prepare.
|
||||||
|
verify(mockMediaSource1, times(0))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(0))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
assertThat(playlist.isPrepared()).isTrue();
|
||||||
|
// Verify prepare is called once on prepare.
|
||||||
|
verify(mockMediaSource1, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
|
||||||
|
playlist.release();
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
// Verify prepare is called a second time on re-prepare.
|
||||||
|
verify(mockMediaSource1, times(2))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(2))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetMediaSources_playlistUnprepared_notUsingLazyPreparation() {
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2);
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSources =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||||
|
Timeline timeline = playlist.setMediaSources(mediaSources, shuffleOrder);
|
||||||
|
|
||||||
|
assertThat(timeline.getWindowCount()).isEqualTo(2);
|
||||||
|
assertThat(playlist.getSize()).isEqualTo(2);
|
||||||
|
|
||||||
|
// Assert holder offsets have been set properly
|
||||||
|
for (int i = 0; i < mediaSources.size(); i++) {
|
||||||
|
Playlist.MediaSourceHolder mediaSourceHolder = mediaSources.get(i);
|
||||||
|
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||||
|
assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set media items again. The second holder is re-used.
|
||||||
|
List<Playlist.MediaSourceHolder> moreMediaSources =
|
||||||
|
createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class));
|
||||||
|
moreMediaSources.add(mediaSources.get(1));
|
||||||
|
timeline = playlist.setMediaSources(moreMediaSources, shuffleOrder);
|
||||||
|
|
||||||
|
assertThat(playlist.getSize()).isEqualTo(2);
|
||||||
|
assertThat(timeline.getWindowCount()).isEqualTo(2);
|
||||||
|
for (int i = 0; i < moreMediaSources.size(); i++) {
|
||||||
|
Playlist.MediaSourceHolder mediaSourceHolder = moreMediaSources.get(i);
|
||||||
|
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||||
|
assertThat(mediaSourceHolder.firstWindowIndexInChild).isEqualTo(i);
|
||||||
|
}
|
||||||
|
// Expect removed holders and sources to be removed without releasing.
|
||||||
|
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSources.get(0).isRemoved).isTrue();
|
||||||
|
// Expect re-used holder and source not to be removed.
|
||||||
|
verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSources.get(1).isRemoved).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetMediaSources_playlistPrepared_notUsingLazyPreparation() {
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2);
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSources =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||||
|
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
playlist.setMediaSources(mediaSources, shuffleOrder);
|
||||||
|
|
||||||
|
// Verify sources are prepared.
|
||||||
|
verify(mockMediaSource1, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
|
||||||
|
// Set media items again. The second holder is re-used.
|
||||||
|
List<Playlist.MediaSourceHolder> moreMediaSources =
|
||||||
|
createFakeHoldersWithSources(/* useLazyPreparation= */ false, mock(MediaSource.class));
|
||||||
|
moreMediaSources.add(mediaSources.get(1));
|
||||||
|
playlist.setMediaSources(moreMediaSources, shuffleOrder);
|
||||||
|
|
||||||
|
// Expect removed holders and sources to be removed and released.
|
||||||
|
verify(mockMediaSource1, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSources.get(0).isRemoved).isTrue();
|
||||||
|
// Expect re-used holder and source not to be removed but released.
|
||||||
|
verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSources.get(1).isRemoved).isFalse();
|
||||||
|
verify(mockMediaSource2, times(2))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddMediaSources_playlistUnprepared_notUsingLazyPreparation_expectUnprepared() {
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
List<Playlist.MediaSourceHolder> mediaSources =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, mediaSources, new ShuffleOrder.DefaultShuffleOrder(2));
|
||||||
|
|
||||||
|
assertThat(playlist.getSize()).isEqualTo(2);
|
||||||
|
// Verify lazy initialization does not call prepare on sources.
|
||||||
|
verify(mockMediaSource1, times(0))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(0))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
|
||||||
|
for (int i = 0; i < mediaSources.size(); i++) {
|
||||||
|
assertThat(mediaSources.get(i).firstWindowIndexInChild).isEqualTo(i);
|
||||||
|
assertThat(mediaSources.get(i).isRemoved).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add for more sources in between.
|
||||||
|
List<Playlist.MediaSourceHolder> moreMediaSources = createFakeHolders();
|
||||||
|
playlist.addMediaSources(
|
||||||
|
/* index= */ 1, moreMediaSources, new ShuffleOrder.DefaultShuffleOrder(/* length= */ 3));
|
||||||
|
|
||||||
|
assertThat(mediaSources.get(0).firstWindowIndexInChild).isEqualTo(0);
|
||||||
|
assertThat(moreMediaSources.get(0).firstWindowIndexInChild).isEqualTo(1);
|
||||||
|
assertThat(moreMediaSources.get(3).firstWindowIndexInChild).isEqualTo(4);
|
||||||
|
assertThat(mediaSources.get(1).firstWindowIndexInChild).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddMediaSources_playlistPrepared_notUsingLazyPreparation_expectPrepared() {
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
playlist.addMediaSources(
|
||||||
|
/* index= */ 0,
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 2));
|
||||||
|
|
||||||
|
// Verify prepare is called on sources when added.
|
||||||
|
verify(mockMediaSource1, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
verify(mockMediaSource2, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMoveMediaSources() {
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||||
|
List<Playlist.MediaSourceHolder> holders = createFakeHolders();
|
||||||
|
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||||
|
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
playlist.moveMediaSource(/* currentIndex= */ 0, /* newIndex= */ 3, shuffleOrder);
|
||||||
|
assertFirstWindowInChildIndices(holders, 3, 0, 1, 2);
|
||||||
|
playlist.moveMediaSource(/* currentIndex= */ 3, /* newIndex= */ 0, shuffleOrder);
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder);
|
||||||
|
assertFirstWindowInChildIndices(holders, 2, 3, 0, 1);
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 2, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder);
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, /* newFromIndex= */ 2, shuffleOrder);
|
||||||
|
assertFirstWindowInChildIndices(holders, 2, 3, 0, 1);
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 2, /* toIndex= */ 3, /* newFromIndex= */ 0, shuffleOrder);
|
||||||
|
assertFirstWindowInChildIndices(holders, 0, 3, 1, 2);
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 3, /* toIndex= */ 4, /* newFromIndex= */ 1, shuffleOrder);
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
|
||||||
|
// No-ops.
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 4, /* newFromIndex= */ 0, shuffleOrder);
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 0, /* newFromIndex= */ 3, shuffleOrder);
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveMediaSources_whenUnprepared_expectNoRelease() {
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource3 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource4 = mock(MediaSource.class);
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||||
|
|
||||||
|
List<Playlist.MediaSourceHolder> holders =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false,
|
||||||
|
mockMediaSource1,
|
||||||
|
mockMediaSource2,
|
||||||
|
mockMediaSource3,
|
||||||
|
mockMediaSource4);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||||
|
playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder);
|
||||||
|
|
||||||
|
assertThat(playlist.getSize()).isEqualTo(2);
|
||||||
|
Playlist.MediaSourceHolder removedHolder1 = holders.remove(1);
|
||||||
|
Playlist.MediaSourceHolder removedHolder2 = holders.remove(1);
|
||||||
|
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
assertThat(removedHolder1.isRemoved).isTrue();
|
||||||
|
assertThat(removedHolder2.isRemoved).isTrue();
|
||||||
|
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource2, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource3, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveMediaSources_whenPrepared_expectRelease() {
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource3 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource4 = mock(MediaSource.class);
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||||
|
|
||||||
|
List<Playlist.MediaSourceHolder> holders =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false,
|
||||||
|
mockMediaSource1,
|
||||||
|
mockMediaSource2,
|
||||||
|
mockMediaSource3,
|
||||||
|
mockMediaSource4);
|
||||||
|
playlist.prepare(/* mediaTransferListener */ null);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, holders, shuffleOrder);
|
||||||
|
playlist.removeMediaSourceRange(/* fromIndex= */ 1, /* toIndex= */ 3, shuffleOrder);
|
||||||
|
|
||||||
|
assertThat(playlist.getSize()).isEqualTo(2);
|
||||||
|
holders.remove(2);
|
||||||
|
holders.remove(1);
|
||||||
|
|
||||||
|
assertDefaultFirstWindowInChildIndexOrder(holders);
|
||||||
|
verify(mockMediaSource1, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource2, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource3, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
verify(mockMediaSource4, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRelease_playlistUnprepared_expectSourcesNotReleased() {
|
||||||
|
MediaSource mockMediaSource = mock(MediaSource.class);
|
||||||
|
Playlist.MediaSourceHolder mediaSourceHolder =
|
||||||
|
new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false);
|
||||||
|
|
||||||
|
playlist.setMediaSources(
|
||||||
|
Collections.singletonList(mediaSourceHolder),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
|
||||||
|
verify(mockMediaSource, times(0))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
playlist.release();
|
||||||
|
verify(mockMediaSource, times(0)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRelease_playlistPrepared_expectSourcesReleasedNotRemoved() {
|
||||||
|
MediaSource mockMediaSource = mock(MediaSource.class);
|
||||||
|
Playlist.MediaSourceHolder mediaSourceHolder =
|
||||||
|
new Playlist.MediaSourceHolder(mockMediaSource, /* useLazyPreparation= */ false);
|
||||||
|
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
playlist.setMediaSources(
|
||||||
|
Collections.singletonList(mediaSourceHolder),
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 1));
|
||||||
|
verify(mockMediaSource, times(1))
|
||||||
|
.prepareSource(
|
||||||
|
any(MediaSource.MediaSourceCaller.class), /* mediaTransferListener= */ isNull());
|
||||||
|
playlist.release();
|
||||||
|
verify(mockMediaSource, times(1)).releaseSource(any(MediaSource.MediaSourceCaller.class));
|
||||||
|
assertThat(mediaSourceHolder.isRemoved).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testClearPlaylist_expectSourcesReleasedAndRemoved() {
|
||||||
|
ShuffleOrder.DefaultShuffleOrder shuffleOrder =
|
||||||
|
new ShuffleOrder.DefaultShuffleOrder(/* length= */ 4);
|
||||||
|
MediaSource mockMediaSource1 = mock(MediaSource.class);
|
||||||
|
MediaSource mockMediaSource2 = mock(MediaSource.class);
|
||||||
|
List<Playlist.MediaSourceHolder> holders =
|
||||||
|
createFakeHoldersWithSources(
|
||||||
|
/* useLazyPreparation= */ false, mockMediaSource1, mockMediaSource2);
|
||||||
|
playlist.setMediaSources(holders, shuffleOrder);
|
||||||
|
playlist.prepare(/* mediaTransferListener= */ null);
|
||||||
|
|
||||||
|
Timeline timeline = playlist.clear(shuffleOrder);
|
||||||
|
assertThat(timeline.isEmpty()).isTrue();
|
||||||
|
assertThat(holders.get(0).isRemoved).isTrue();
|
||||||
|
assertThat(holders.get(1).isRemoved).isTrue();
|
||||||
|
verify(mockMediaSource1, times(1)).releaseSource(any());
|
||||||
|
verify(mockMediaSource2, times(1)).releaseSource(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.setMediaSources(createFakeHolders(), new FakeShuffleOrder(/* length=*/ 4));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.addMediaSources(
|
||||||
|
/* index= */ 0, createFakeHolders(), new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMoveMediaSources_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.moveMediaSource(
|
||||||
|
/* currentIndex= */ 0, /* newIndex= */ 1, new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.moveMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0,
|
||||||
|
/* toIndex= */ 2,
|
||||||
|
/* newFromIndex= */ 2,
|
||||||
|
new FakeShuffleOrder(PLAYLIST_SIZE));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRemoveMediaSourceRange_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
ShuffleOrder shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE);
|
||||||
|
playlist.addMediaSources(/* index= */ 0, createFakeHolders(), shuffleOrder);
|
||||||
|
Timeline timeline =
|
||||||
|
playlist.removeMediaSourceRange(
|
||||||
|
/* fromIndex= */ 0, /* toIndex= */ 2, new FakeShuffleOrder(/* length= */ 2));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetShuffleOrder_expectTimelineUsesCustomShuffleOrder() {
|
||||||
|
playlist.setMediaSources(
|
||||||
|
createFakeHolders(), new ShuffleOrder.DefaultShuffleOrder(/* length= */ PLAYLIST_SIZE));
|
||||||
|
assertTimelineUsesFakeShuffleOrder(
|
||||||
|
playlist.setShuffleOrder(new FakeShuffleOrder(PLAYLIST_SIZE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal methods.
|
||||||
|
|
||||||
|
private static void assertTimelineUsesFakeShuffleOrder(Timeline timeline) {
|
||||||
|
assertThat(
|
||||||
|
timeline.getNextWindowIndex(
|
||||||
|
/* windowIndex= */ 0, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true))
|
||||||
|
.isEqualTo(-1);
|
||||||
|
assertThat(
|
||||||
|
timeline.getPreviousWindowIndex(
|
||||||
|
/* windowIndex= */ timeline.getWindowCount() - 1,
|
||||||
|
Player.REPEAT_MODE_OFF,
|
||||||
|
/* shuffleModeEnabled= */ true))
|
||||||
|
.isEqualTo(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertDefaultFirstWindowInChildIndexOrder(
|
||||||
|
List<Playlist.MediaSourceHolder> holders) {
|
||||||
|
int[] indices = new int[holders.size()];
|
||||||
|
for (int i = 0; i < indices.length; i++) {
|
||||||
|
indices[i] = i;
|
||||||
|
}
|
||||||
|
assertFirstWindowInChildIndices(holders, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertFirstWindowInChildIndices(
|
||||||
|
List<Playlist.MediaSourceHolder> holders, int... firstWindowInChildIndices) {
|
||||||
|
assertThat(holders).hasSize(firstWindowInChildIndices.length);
|
||||||
|
for (int i = 0; i < holders.size(); i++) {
|
||||||
|
assertThat(holders.get(i).firstWindowIndexInChild).isEqualTo(firstWindowInChildIndices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Playlist.MediaSourceHolder> createFakeHolders() {
|
||||||
|
MediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(1));
|
||||||
|
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
|
||||||
|
for (int i = 0; i < PLAYLIST_SIZE; i++) {
|
||||||
|
holders.add(new Playlist.MediaSourceHolder(fakeMediaSource, /* useLazyPreparation= */ true));
|
||||||
|
}
|
||||||
|
return holders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Playlist.MediaSourceHolder> createFakeHoldersWithSources(
|
||||||
|
boolean useLazyPreparation, MediaSource... sources) {
|
||||||
|
List<Playlist.MediaSourceHolder> holders = new ArrayList<>();
|
||||||
|
for (MediaSource mediaSource : sources) {
|
||||||
|
holders.add(
|
||||||
|
new Playlist.MediaSourceHolder(
|
||||||
|
mediaSource, /* useLazyPreparation= */ useLazyPreparation));
|
||||||
|
}
|
||||||
|
return holders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer2;
|
package com.google.android.exoplayer2;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||||
|
|
@ -58,4 +60,148 @@ public class TimelineTest {
|
||||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0);
|
||||||
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWindowEquals() {
|
||||||
|
Timeline.Window window = new Timeline.Window();
|
||||||
|
assertThat(window).isEqualTo(new Timeline.Window());
|
||||||
|
|
||||||
|
Timeline.Window otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.tag = new Object();
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.manifest = new Object();
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.presentationStartTimeMs = C.TIME_UNSET;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.windowStartTimeMs = C.TIME_UNSET;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.isSeekable = true;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.isDynamic = true;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.isLive = true;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.defaultPositionUs = C.TIME_UNSET;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.durationUs = C.TIME_UNSET;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.firstPeriodIndex = 1;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.lastPeriodIndex = 1;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
otherWindow = new Timeline.Window();
|
||||||
|
otherWindow.positionInFirstPeriodUs = C.TIME_UNSET;
|
||||||
|
assertThat(window).isNotEqualTo(otherWindow);
|
||||||
|
|
||||||
|
window.uid = new Object();
|
||||||
|
window.tag = new Object();
|
||||||
|
window.manifest = new Object();
|
||||||
|
window.presentationStartTimeMs = C.TIME_UNSET;
|
||||||
|
window.windowStartTimeMs = C.TIME_UNSET;
|
||||||
|
window.isSeekable = true;
|
||||||
|
window.isDynamic = true;
|
||||||
|
window.isLive = true;
|
||||||
|
window.defaultPositionUs = C.TIME_UNSET;
|
||||||
|
window.durationUs = C.TIME_UNSET;
|
||||||
|
window.firstPeriodIndex = 1;
|
||||||
|
window.lastPeriodIndex = 1;
|
||||||
|
window.positionInFirstPeriodUs = C.TIME_UNSET;
|
||||||
|
otherWindow =
|
||||||
|
otherWindow.set(
|
||||||
|
window.uid,
|
||||||
|
window.tag,
|
||||||
|
window.manifest,
|
||||||
|
window.presentationStartTimeMs,
|
||||||
|
window.windowStartTimeMs,
|
||||||
|
window.isSeekable,
|
||||||
|
window.isDynamic,
|
||||||
|
window.isLive,
|
||||||
|
window.defaultPositionUs,
|
||||||
|
window.durationUs,
|
||||||
|
window.firstPeriodIndex,
|
||||||
|
window.lastPeriodIndex,
|
||||||
|
window.positionInFirstPeriodUs);
|
||||||
|
assertThat(window).isEqualTo(otherWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWindowHashCode() {
|
||||||
|
Timeline.Window window = new Timeline.Window();
|
||||||
|
Timeline.Window otherWindow = new Timeline.Window();
|
||||||
|
assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode());
|
||||||
|
|
||||||
|
window.tag = new Object();
|
||||||
|
assertThat(window.hashCode()).isNotEqualTo(otherWindow.hashCode());
|
||||||
|
otherWindow.tag = window.tag;
|
||||||
|
assertThat(window.hashCode()).isEqualTo(otherWindow.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPeriodEquals() {
|
||||||
|
Timeline.Period period = new Timeline.Period();
|
||||||
|
assertThat(period).isEqualTo(new Timeline.Period());
|
||||||
|
|
||||||
|
Timeline.Period otherPeriod = new Timeline.Period();
|
||||||
|
otherPeriod.id = new Object();
|
||||||
|
assertThat(period).isNotEqualTo(otherPeriod);
|
||||||
|
|
||||||
|
otherPeriod = new Timeline.Period();
|
||||||
|
otherPeriod.uid = new Object();
|
||||||
|
assertThat(period).isNotEqualTo(otherPeriod);
|
||||||
|
|
||||||
|
otherPeriod = new Timeline.Period();
|
||||||
|
otherPeriod.windowIndex = 12;
|
||||||
|
assertThat(period).isNotEqualTo(otherPeriod);
|
||||||
|
|
||||||
|
otherPeriod = new Timeline.Period();
|
||||||
|
otherPeriod.durationUs = 11L;
|
||||||
|
assertThat(period).isNotEqualTo(otherPeriod);
|
||||||
|
|
||||||
|
otherPeriod = new Timeline.Period();
|
||||||
|
period.id = new Object();
|
||||||
|
period.uid = new Object();
|
||||||
|
period.windowIndex = 1;
|
||||||
|
period.durationUs = 123L;
|
||||||
|
otherPeriod =
|
||||||
|
otherPeriod.set(
|
||||||
|
period.id,
|
||||||
|
period.uid,
|
||||||
|
period.windowIndex,
|
||||||
|
period.durationUs,
|
||||||
|
/* positionInWindowUs= */ 0);
|
||||||
|
assertThat(period).isEqualTo(otherPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPeriodHashCode() {
|
||||||
|
Timeline.Period period = new Timeline.Period();
|
||||||
|
Timeline.Period otherPeriod = new Timeline.Period();
|
||||||
|
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
|
||||||
|
|
||||||
|
period.windowIndex = 12;
|
||||||
|
assertThat(period.hashCode()).isNotEqualTo(otherPeriod.hashCode());
|
||||||
|
otherPeriod.windowIndex = period.windowIndex;
|
||||||
|
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
||||||
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
|
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
|
||||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
||||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;
|
|
||||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||||
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||||
|
|
@ -133,24 +132,29 @@ public final class AnalyticsCollectorTest {
|
||||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */);
|
WINDOW_0 /* setPlayWhenReady */, WINDOW_0 /* BUFFERING */, WINDOW_0 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
|
||||||
listener.assertNoMoreEvents();
|
listener.assertNoMoreEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSinglePeriod() throws Exception {
|
public void testSinglePeriod() throws Exception {
|
||||||
FakeMediaSource mediaSource =
|
FakeMediaSource mediaSource =
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
|
new FakeMediaSource(
|
||||||
|
SINGLE_PERIOD_TIMELINE,
|
||||||
|
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
|
||||||
|
ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||||
|
|
||||||
populateEventIds(SINGLE_PERIOD_TIMELINE);
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
WINDOW_0 /* setPlayWhenReady */,
|
WINDOW_0 /* setPlayWhenReady */,
|
||||||
WINDOW_0 /* BUFFERING */,
|
WINDOW_0 /* BUFFERING */,
|
||||||
period0 /* READY */,
|
period0 /* READY */,
|
||||||
period0 /* ENDED */);
|
period0 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, WINDOW_0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
||||||
.containsExactly(period0 /* started */, period0 /* stopped */);
|
.containsExactly(period0 /* started */, period0 /* stopped */);
|
||||||
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);
|
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED)).containsExactly(period0);
|
||||||
|
|
@ -179,9 +183,14 @@ public final class AnalyticsCollectorTest {
|
||||||
public void testAutomaticPeriodTransition() throws Exception {
|
public void testAutomaticPeriodTransition() throws Exception {
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ConcatenatingMediaSource(
|
new ConcatenatingMediaSource(
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT),
|
|
||||||
new FakeMediaSource(
|
new FakeMediaSource(
|
||||||
SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT));
|
SINGLE_PERIOD_TIMELINE,
|
||||||
|
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
|
||||||
|
ExoPlayerTestRunner.Builder.AUDIO_FORMAT),
|
||||||
|
new FakeMediaSource(
|
||||||
|
SINGLE_PERIOD_TIMELINE,
|
||||||
|
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
|
||||||
|
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||||
|
|
||||||
populateEventIds(listener.lastReportedTimeline);
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
|
|
@ -191,7 +200,8 @@ public final class AnalyticsCollectorTest {
|
||||||
WINDOW_0 /* BUFFERING */,
|
WINDOW_0 /* BUFFERING */,
|
||||||
period0 /* READY */,
|
period0 /* READY */,
|
||||||
period1 /* ENDED */);
|
period1 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
||||||
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
||||||
.containsExactly(period0, period0, period0, period0);
|
.containsExactly(period0, period0, period0, period0);
|
||||||
|
|
@ -233,8 +243,8 @@ public final class AnalyticsCollectorTest {
|
||||||
public void testPeriodTransitionWithRendererChange() throws Exception {
|
public void testPeriodTransitionWithRendererChange() throws Exception {
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ConcatenatingMediaSource(
|
new ConcatenatingMediaSource(
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource);
|
||||||
|
|
||||||
populateEventIds(listener.lastReportedTimeline);
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
|
|
@ -246,7 +256,8 @@ public final class AnalyticsCollectorTest {
|
||||||
period1 /* BUFFERING */,
|
period1 /* BUFFERING */,
|
||||||
period1 /* READY */,
|
period1 /* READY */,
|
||||||
period1 /* ENDED */);
|
period1 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
||||||
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
||||||
.containsExactly(period0, period0, period0, period0);
|
.containsExactly(period0, period0, period0, period0);
|
||||||
|
|
@ -286,8 +297,8 @@ public final class AnalyticsCollectorTest {
|
||||||
public void testSeekToOtherPeriod() throws Exception {
|
public void testSeekToOtherPeriod() throws Exception {
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ConcatenatingMediaSource(
|
new ConcatenatingMediaSource(
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.AUDIO_FORMAT));
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
|
||||||
ActionSchedule actionSchedule =
|
ActionSchedule actionSchedule =
|
||||||
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
||||||
.pause()
|
.pause()
|
||||||
|
|
@ -308,7 +319,8 @@ public final class AnalyticsCollectorTest {
|
||||||
period1 /* READY */,
|
period1 /* READY */,
|
||||||
period1 /* setPlayWhenReady=true */,
|
period1 /* setPlayWhenReady=true */,
|
||||||
period1 /* ENDED */);
|
period1 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY)).containsExactly(period1);
|
||||||
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
||||||
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);
|
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period1);
|
||||||
|
|
@ -350,9 +362,11 @@ public final class AnalyticsCollectorTest {
|
||||||
public void testSeekBackAfterReadingAhead() throws Exception {
|
public void testSeekBackAfterReadingAhead() throws Exception {
|
||||||
MediaSource mediaSource =
|
MediaSource mediaSource =
|
||||||
new ConcatenatingMediaSource(
|
new ConcatenatingMediaSource(
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT),
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT),
|
||||||
new FakeMediaSource(
|
new FakeMediaSource(
|
||||||
SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT));
|
SINGLE_PERIOD_TIMELINE,
|
||||||
|
ExoPlayerTestRunner.Builder.VIDEO_FORMAT,
|
||||||
|
ExoPlayerTestRunner.Builder.AUDIO_FORMAT));
|
||||||
long periodDurationMs =
|
long periodDurationMs =
|
||||||
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
|
SINGLE_PERIOD_TIMELINE.getWindow(/* windowIndex= */ 0, new Window()).getDurationMs();
|
||||||
ActionSchedule actionSchedule =
|
ActionSchedule actionSchedule =
|
||||||
|
|
@ -380,7 +394,8 @@ public final class AnalyticsCollectorTest {
|
||||||
period1Seq2 /* BUFFERING */,
|
period1Seq2 /* BUFFERING */,
|
||||||
period1Seq2 /* READY */,
|
period1Seq2 /* READY */,
|
||||||
period1Seq2 /* ENDED */);
|
period1Seq2 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(WINDOW_0 /* PLAYLIST_CHANGED */, period0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
|
assertThat(listener.getEvents(EVENT_POSITION_DISCONTINUITY))
|
||||||
.containsExactly(period0, period1Seq2);
|
.containsExactly(period0, period1Seq2);
|
||||||
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
||||||
|
|
@ -428,18 +443,28 @@ public final class AnalyticsCollectorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPrepareNewSource() throws Exception {
|
public void testPrepareNewSource() throws Exception {
|
||||||
MediaSource mediaSource1 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
|
MediaSource mediaSource1 =
|
||||||
MediaSource mediaSource2 = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
|
||||||
|
MediaSource mediaSource2 =
|
||||||
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
|
||||||
ActionSchedule actionSchedule =
|
ActionSchedule actionSchedule =
|
||||||
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
||||||
.pause()
|
.pause()
|
||||||
.waitForPlaybackState(Player.STATE_READY)
|
.waitForPlaybackState(Player.STATE_READY)
|
||||||
.prepareSource(mediaSource2)
|
.setMediaItems(/* resetPosition= */ false, mediaSource2)
|
||||||
.play()
|
.play()
|
||||||
.build();
|
.build();
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource1, actionSchedule);
|
||||||
|
|
||||||
populateEventIds(SINGLE_PERIOD_TIMELINE);
|
// Populate all event ids with last timeline (after second prepare).
|
||||||
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
|
// Populate event id of period 0, sequence 0 with timeline of initial preparation.
|
||||||
|
period0Seq0 =
|
||||||
|
new EventWindowAndPeriodId(
|
||||||
|
/* windowIndex= */ 0,
|
||||||
|
new MediaPeriodId(
|
||||||
|
listener.reportedTimelines.get(1).getUidOfPeriod(/* periodIndex= */ 0),
|
||||||
|
/* windowSequenceNumber= */ 0));
|
||||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
WINDOW_0 /* setPlayWhenReady=true */,
|
WINDOW_0 /* setPlayWhenReady=true */,
|
||||||
|
|
@ -451,12 +476,16 @@ public final class AnalyticsCollectorTest {
|
||||||
period0Seq1 /* READY */,
|
period0Seq1 /* READY */,
|
||||||
period0Seq1 /* ENDED */);
|
period0Seq1 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
.containsExactly(WINDOW_0 /* prepared */, WINDOW_0 /* reset */, WINDOW_0 /* prepared */);
|
.containsExactly(
|
||||||
|
WINDOW_0 /* PLAYLIST_CHANGE */,
|
||||||
|
WINDOW_0 /* DYNAMIC */,
|
||||||
|
WINDOW_0 /* PLAYLIST_CHANGE */,
|
||||||
|
WINDOW_0 /* DYNAMIC */);
|
||||||
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
||||||
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);
|
.containsExactly(period0Seq0, period0Seq0, period0Seq1, period0Seq1);
|
||||||
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
|
assertThat(listener.getEvents(EVENT_TRACKS_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
period0Seq0 /* prepared */, WINDOW_0 /* reset */, period0Seq1 /* prepared */);
|
period0Seq0 /* prepared */, WINDOW_0 /* setMediaItems */, period0Seq1 /* prepared */);
|
||||||
assertThat(listener.getEvents(EVENT_LOAD_STARTED))
|
assertThat(listener.getEvents(EVENT_LOAD_STARTED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
WINDOW_0 /* manifest */,
|
WINDOW_0 /* manifest */,
|
||||||
|
|
@ -490,19 +519,20 @@ public final class AnalyticsCollectorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReprepareAfterError() throws Exception {
|
public void testReprepareAfterError() throws Exception {
|
||||||
MediaSource mediaSource = new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
|
MediaSource mediaSource =
|
||||||
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
|
||||||
ActionSchedule actionSchedule =
|
ActionSchedule actionSchedule =
|
||||||
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
new ActionSchedule.Builder("AnalyticsCollectorTest")
|
||||||
.waitForPlaybackState(Player.STATE_READY)
|
.waitForPlaybackState(Player.STATE_READY)
|
||||||
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
|
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
|
||||||
.waitForPlaybackState(Player.STATE_IDLE)
|
.waitForPlaybackState(Player.STATE_IDLE)
|
||||||
.seek(/* positionMs= */ 0)
|
.seek(/* positionMs= */ 0)
|
||||||
.prepareSource(mediaSource, /* resetPosition= */ false, /* resetState= */ false)
|
.prepare()
|
||||||
.waitForPlaybackState(Player.STATE_ENDED)
|
.waitForPlaybackState(Player.STATE_ENDED)
|
||||||
.build();
|
.build();
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||||
|
|
||||||
populateEventIds(SINGLE_PERIOD_TIMELINE);
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
assertThat(listener.getEvents(EVENT_PLAYER_STATE_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
WINDOW_0 /* setPlayWhenReady=true */,
|
WINDOW_0 /* setPlayWhenReady=true */,
|
||||||
|
|
@ -556,7 +586,7 @@ public final class AnalyticsCollectorTest {
|
||||||
@Test
|
@Test
|
||||||
public void testDynamicTimelineChange() throws Exception {
|
public void testDynamicTimelineChange() throws Exception {
|
||||||
MediaSource childMediaSource =
|
MediaSource childMediaSource =
|
||||||
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, Builder.VIDEO_FORMAT);
|
new FakeMediaSource(SINGLE_PERIOD_TIMELINE, ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
|
||||||
final ConcatenatingMediaSource concatenatedMediaSource =
|
final ConcatenatingMediaSource concatenatedMediaSource =
|
||||||
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
|
new ConcatenatingMediaSource(childMediaSource, childMediaSource);
|
||||||
long periodDurationMs =
|
long periodDurationMs =
|
||||||
|
|
@ -588,7 +618,11 @@ public final class AnalyticsCollectorTest {
|
||||||
period1Seq0 /* setPlayWhenReady=true */,
|
period1Seq0 /* setPlayWhenReady=true */,
|
||||||
period1Seq0 /* BUFFERING */,
|
period1Seq0 /* BUFFERING */,
|
||||||
period1Seq0 /* ENDED */);
|
period1Seq0 /* ENDED */);
|
||||||
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED)).containsExactly(WINDOW_0, period1Seq0);
|
assertThat(listener.getEvents(EVENT_TIMELINE_CHANGED))
|
||||||
|
.containsExactly(
|
||||||
|
WINDOW_0 /* PLAYLIST_CHANGED */,
|
||||||
|
window0Period1Seq0 /* DYNAMIC (concatenated timeline replaces dummy) */,
|
||||||
|
period1Seq0 /* DYNAMIC (child sources in concatenating source moved) */);
|
||||||
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
assertThat(listener.getEvents(EVENT_LOADING_CHANGED))
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
|
window0Period1Seq0, window0Period1Seq0, window0Period1Seq0, window0Period1Seq0);
|
||||||
|
|
@ -642,7 +676,7 @@ public final class AnalyticsCollectorTest {
|
||||||
.build();
|
.build();
|
||||||
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
|
||||||
|
|
||||||
populateEventIds(SINGLE_PERIOD_TIMELINE);
|
populateEventIds(listener.lastReportedTimeline);
|
||||||
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
assertThat(listener.getEvents(EVENT_SEEK_STARTED)).containsExactly(period0);
|
||||||
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0);
|
assertThat(listener.getEvents(EVENT_SEEK_PROCESSED)).containsExactly(period0);
|
||||||
}
|
}
|
||||||
|
|
@ -709,7 +743,7 @@ public final class AnalyticsCollectorTest {
|
||||||
TestAnalyticsListener listener = new TestAnalyticsListener();
|
TestAnalyticsListener listener = new TestAnalyticsListener();
|
||||||
try {
|
try {
|
||||||
new ExoPlayerTestRunner.Builder()
|
new ExoPlayerTestRunner.Builder()
|
||||||
.setMediaSource(mediaSource)
|
.setMediaSources(mediaSource)
|
||||||
.setRenderersFactory(renderersFactory)
|
.setRenderersFactory(renderersFactory)
|
||||||
.setAnalyticsListener(listener)
|
.setAnalyticsListener(listener)
|
||||||
.setActionSchedule(actionSchedule)
|
.setActionSchedule(actionSchedule)
|
||||||
|
|
@ -731,7 +765,7 @@ public final class AnalyticsCollectorTest {
|
||||||
private boolean renderedFirstFrame;
|
private boolean renderedFirstFrame;
|
||||||
|
|
||||||
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
|
public FakeVideoRenderer(Handler handler, VideoRendererEventListener eventListener) {
|
||||||
super(Builder.VIDEO_FORMAT);
|
super(ExoPlayerTestRunner.Builder.VIDEO_FORMAT);
|
||||||
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
|
eventDispatcher = new VideoRendererEventListener.EventDispatcher(handler, eventListener);
|
||||||
decoderCounters = new DecoderCounters();
|
decoderCounters = new DecoderCounters();
|
||||||
}
|
}
|
||||||
|
|
@ -789,7 +823,7 @@ public final class AnalyticsCollectorTest {
|
||||||
private boolean notifiedAudioSessionId;
|
private boolean notifiedAudioSessionId;
|
||||||
|
|
||||||
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
|
public FakeAudioRenderer(Handler handler, AudioRendererEventListener eventListener) {
|
||||||
super(Builder.AUDIO_FORMAT);
|
super(ExoPlayerTestRunner.Builder.AUDIO_FORMAT);
|
||||||
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
|
eventDispatcher = new AudioRendererEventListener.EventDispatcher(handler, eventListener);
|
||||||
decoderCounters = new DecoderCounters();
|
decoderCounters = new DecoderCounters();
|
||||||
}
|
}
|
||||||
|
|
@ -873,10 +907,12 @@ public final class AnalyticsCollectorTest {
|
||||||
|
|
||||||
public Timeline lastReportedTimeline;
|
public Timeline lastReportedTimeline;
|
||||||
|
|
||||||
|
private final List<Timeline> reportedTimelines;
|
||||||
private final ArrayList<ReportedEvent> reportedEvents;
|
private final ArrayList<ReportedEvent> reportedEvents;
|
||||||
|
|
||||||
public TestAnalyticsListener() {
|
public TestAnalyticsListener() {
|
||||||
reportedEvents = new ArrayList<>();
|
reportedEvents = new ArrayList<>();
|
||||||
|
reportedTimelines = new ArrayList<>();
|
||||||
lastReportedTimeline = Timeline.EMPTY;
|
lastReportedTimeline = Timeline.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -906,6 +942,7 @@ public final class AnalyticsCollectorTest {
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(EventTime eventTime, int reason) {
|
public void onTimelineChanged(EventTime eventTime, int reason) {
|
||||||
lastReportedTimeline = eventTime.timeline;
|
lastReportedTimeline = eventTime.timeline;
|
||||||
|
reportedTimelines.add(eventTime.timeline);
|
||||||
reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime));
|
reportedEvents.add(new ReportedEvent(EVENT_TIMELINE_CHANGED, eventTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.test.core.app.ApplicationProvider;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.ExoPlayer;
|
import com.google.android.exoplayer2.ExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.IllegalSeekPositionException;
|
||||||
import com.google.android.exoplayer2.PlaybackParameters;
|
import com.google.android.exoplayer2.PlaybackParameters;
|
||||||
import com.google.android.exoplayer2.Player;
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.PlayerMessage;
|
import com.google.android.exoplayer2.PlayerMessage;
|
||||||
|
|
@ -31,6 +32,7 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode;
|
import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode;
|
||||||
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
|
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable;
|
||||||
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
|
import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget;
|
||||||
|
|
@ -39,6 +41,8 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet
|
||||||
import com.google.android.exoplayer2.util.ConditionVariable;
|
import com.google.android.exoplayer2.util.ConditionVariable;
|
||||||
import com.google.android.exoplayer2.util.HandlerWrapper;
|
import com.google.android.exoplayer2.util.HandlerWrapper;
|
||||||
import com.google.android.exoplayer2.util.Log;
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/** Base class for actions to perform during playback tests. */
|
/** Base class for actions to perform during playback tests. */
|
||||||
public abstract class Action {
|
public abstract class Action {
|
||||||
|
|
@ -115,6 +119,7 @@ public abstract class Action {
|
||||||
|
|
||||||
private final Integer windowIndex;
|
private final Integer windowIndex;
|
||||||
private final long positionMs;
|
private final long positionMs;
|
||||||
|
private final boolean catchIllegalSeekException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action calls {@link Player#seekTo(long)}.
|
* Action calls {@link Player#seekTo(long)}.
|
||||||
|
|
@ -126,6 +131,7 @@ public abstract class Action {
|
||||||
super(tag, "Seek:" + positionMs);
|
super(tag, "Seek:" + positionMs);
|
||||||
this.windowIndex = null;
|
this.windowIndex = null;
|
||||||
this.positionMs = positionMs;
|
this.positionMs = positionMs;
|
||||||
|
catchIllegalSeekException = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -134,24 +140,191 @@ public abstract class Action {
|
||||||
* @param tag A tag to use for logging.
|
* @param tag A tag to use for logging.
|
||||||
* @param windowIndex The window to seek to.
|
* @param windowIndex The window to seek to.
|
||||||
* @param positionMs The seek position.
|
* @param positionMs The seek position.
|
||||||
|
* @param catchIllegalSeekException Whether {@link IllegalSeekPositionException} should be
|
||||||
|
* silently caught or not.
|
||||||
*/
|
*/
|
||||||
public Seek(String tag, int windowIndex, long positionMs) {
|
public Seek(String tag, int windowIndex, long positionMs, boolean catchIllegalSeekException) {
|
||||||
super(tag, "Seek:" + positionMs);
|
super(tag, "Seek:" + positionMs);
|
||||||
this.windowIndex = windowIndex;
|
this.windowIndex = windowIndex;
|
||||||
this.positionMs = positionMs;
|
this.positionMs = positionMs;
|
||||||
|
this.catchIllegalSeekException = catchIllegalSeekException;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doActionImpl(
|
protected void doActionImpl(
|
||||||
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
if (windowIndex == null) {
|
try {
|
||||||
player.seekTo(positionMs);
|
if (windowIndex == null) {
|
||||||
} else {
|
player.seekTo(positionMs);
|
||||||
player.seekTo(windowIndex, positionMs);
|
} else {
|
||||||
|
player.seekTo(windowIndex, positionMs);
|
||||||
|
}
|
||||||
|
} catch (IllegalSeekPositionException e) {
|
||||||
|
if (!catchIllegalSeekException) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#setMediaItems(List, int, long)}. */
|
||||||
|
public static final class SetMediaItems extends Action {
|
||||||
|
|
||||||
|
private final int windowIndex;
|
||||||
|
private final long positionMs;
|
||||||
|
private final MediaSource[] mediaSources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param windowIndex The window index to start playback from.
|
||||||
|
* @param positionMs The position in milliseconds to start playback from.
|
||||||
|
* @param mediaSources The media sources to populate the playlist with.
|
||||||
|
*/
|
||||||
|
public SetMediaItems(
|
||||||
|
String tag, int windowIndex, long positionMs, MediaSource... mediaSources) {
|
||||||
|
super(tag, "SetMediaItems");
|
||||||
|
this.windowIndex = windowIndex;
|
||||||
|
this.positionMs = positionMs;
|
||||||
|
this.mediaSources = mediaSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.setMediaItems(Arrays.asList(mediaSources), windowIndex, positionMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#addMediaItems(List)}. */
|
||||||
|
public static final class AddMediaItems extends Action {
|
||||||
|
|
||||||
|
private final MediaSource[] mediaSources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param mediaSources The media sources to be added to the playlist.
|
||||||
|
*/
|
||||||
|
public AddMediaItems(String tag, MediaSource... mediaSources) {
|
||||||
|
super(tag, /* description= */ "AddMediaItems");
|
||||||
|
this.mediaSources = mediaSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.addMediaItems(Arrays.asList(mediaSources));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#setMediaItems(List, boolean)}. */
|
||||||
|
public static final class SetMediaItemsResetPosition extends Action {
|
||||||
|
|
||||||
|
private final boolean resetPosition;
|
||||||
|
private final MediaSource[] mediaSources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param resetPosition Whether the position should be reset.
|
||||||
|
* @param mediaSources The media sources to populate the playlist with.
|
||||||
|
*/
|
||||||
|
public SetMediaItemsResetPosition(
|
||||||
|
String tag, boolean resetPosition, MediaSource... mediaSources) {
|
||||||
|
super(tag, "SetMediaItems");
|
||||||
|
this.resetPosition = resetPosition;
|
||||||
|
this.mediaSources = mediaSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.setMediaItems(Arrays.asList(mediaSources), resetPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#moveMediaItem(int, int)}. */
|
||||||
|
public static class MoveMediaItem extends Action {
|
||||||
|
|
||||||
|
private final int currentIndex;
|
||||||
|
private final int newIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param currentIndex The current index of the media item.
|
||||||
|
* @param newIndex The new index of the media item.
|
||||||
|
*/
|
||||||
|
public MoveMediaItem(String tag, int currentIndex, int newIndex) {
|
||||||
|
super(tag, "MoveMediaItem");
|
||||||
|
this.currentIndex = currentIndex;
|
||||||
|
this.newIndex = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.moveMediaItem(currentIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#removeMediaItem(int)}. */
|
||||||
|
public static class RemoveMediaItem extends Action {
|
||||||
|
|
||||||
|
private final int index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param index The index of the item to remove.
|
||||||
|
*/
|
||||||
|
public RemoveMediaItem(String tag, int index) {
|
||||||
|
super(tag, "RemoveMediaItem");
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.removeMediaItem(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#removeMediaItems(int, int)}. */
|
||||||
|
public static class RemoveMediaItems extends Action {
|
||||||
|
|
||||||
|
private final int fromIndex;
|
||||||
|
private final int toIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param fromIndex The start if the range of media items to remove.
|
||||||
|
* @param toIndex The end of the range of media items to remove (exclusive).
|
||||||
|
*/
|
||||||
|
public RemoveMediaItems(String tag, int fromIndex, int toIndex) {
|
||||||
|
super(tag, "RemoveMediaItem");
|
||||||
|
this.fromIndex = fromIndex;
|
||||||
|
this.toIndex = toIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.removeMediaItems(fromIndex, toIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls {@link SimpleExoPlayer#clearMediaItems()}}. */
|
||||||
|
public static class ClearMediaItems extends Action {
|
||||||
|
|
||||||
|
/** @param tag A tag to use for logging. */
|
||||||
|
public ClearMediaItems(String tag) {
|
||||||
|
super(tag, "ClearMediaItems");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.clearMediaItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */
|
/** Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */
|
||||||
public static final class Stop extends Action {
|
public static final class Stop extends Action {
|
||||||
|
|
||||||
|
|
@ -210,7 +383,6 @@ public abstract class Action {
|
||||||
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
player.setPlayWhenReady(playWhenReady);
|
player.setPlayWhenReady(playWhenReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Broadcasts an {@link Intent}. */
|
/** Broadcasts an {@link Intent}. */
|
||||||
|
|
@ -316,52 +488,31 @@ public abstract class Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calls {@link ExoPlayer#prepare(MediaSource)}. */
|
/** Calls {@link ExoPlayer#prepare()}. */
|
||||||
public static final class PrepareSource extends Action {
|
public static final class Prepare extends Action {
|
||||||
|
/** @param tag A tag to use for logging. */
|
||||||
private final MediaSource mediaSource;
|
public Prepare(String tag) {
|
||||||
private final boolean resetPosition;
|
super(tag, "Prepare");
|
||||||
private final boolean resetState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tag A tag to use for logging.
|
|
||||||
* @param mediaSource The {@link MediaSource} to prepare the player with.
|
|
||||||
*/
|
|
||||||
public PrepareSource(String tag, MediaSource mediaSource) {
|
|
||||||
this(tag, mediaSource, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tag A tag to use for logging.
|
|
||||||
* @param mediaSource The {@link MediaSource} to prepare the player with.
|
|
||||||
* @param resetPosition Whether the player's position should be reset.
|
|
||||||
*/
|
|
||||||
public PrepareSource(
|
|
||||||
String tag, MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
|
||||||
super(tag, "PrepareSource");
|
|
||||||
this.mediaSource = mediaSource;
|
|
||||||
this.resetPosition = resetPosition;
|
|
||||||
this.resetState = resetState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doActionImpl(
|
protected void doActionImpl(
|
||||||
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
player.prepare(mediaSource, resetPosition, resetState);
|
player.prepare();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calls {@link Player#setRepeatMode(int)}. */
|
/** Calls {@link Player#setRepeatMode(int)}. */
|
||||||
public static final class SetRepeatMode extends Action {
|
public static final class SetRepeatMode extends Action {
|
||||||
|
|
||||||
private final @Player.RepeatMode int repeatMode;
|
@Player.RepeatMode private final int repeatMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param tag A tag to use for logging.
|
* @param tag A tag to use for logging.
|
||||||
* @param repeatMode The repeat mode.
|
* @param repeatMode The repeat mode.
|
||||||
*/
|
*/
|
||||||
public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) {
|
public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) {
|
||||||
super(tag, "SetRepeatMode:" + repeatMode);
|
super(tag, "SetRepeatMode: " + repeatMode);
|
||||||
this.repeatMode = repeatMode;
|
this.repeatMode = repeatMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,6 +523,27 @@ public abstract class Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Calls {@link ExoPlayer#setShuffleOrder(ShuffleOrder)} . */
|
||||||
|
public static final class SetShuffleOrder extends Action {
|
||||||
|
|
||||||
|
private final ShuffleOrder shuffleOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
* @param shuffleOrder The shuffle order.
|
||||||
|
*/
|
||||||
|
public SetShuffleOrder(String tag, ShuffleOrder shuffleOrder) {
|
||||||
|
super(tag, "SetShufflerOrder");
|
||||||
|
this.shuffleOrder = shuffleOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doActionImpl(
|
||||||
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
|
player.setShuffleOrder(shuffleOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Calls {@link Player#setShuffleModeEnabled(boolean)}. */
|
/** Calls {@link Player#setShuffleModeEnabled(boolean)}. */
|
||||||
public static final class SetShuffleModeEnabled extends Action {
|
public static final class SetShuffleModeEnabled extends Action {
|
||||||
|
|
||||||
|
|
@ -382,7 +554,7 @@ public abstract class Action {
|
||||||
* @param shuffleModeEnabled Whether shuffling is enabled.
|
* @param shuffleModeEnabled Whether shuffling is enabled.
|
||||||
*/
|
*/
|
||||||
public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) {
|
public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) {
|
||||||
super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled);
|
super(tag, "SetShuffleModeEnabled: " + shuffleModeEnabled);
|
||||||
this.shuffleModeEnabled = shuffleModeEnabled;
|
this.shuffleModeEnabled = shuffleModeEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,7 +641,6 @@ public abstract class Action {
|
||||||
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) {
|
||||||
player.setPlaybackParameters(playbackParameters);
|
player.setPlaybackParameters(playbackParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Throws a playback exception on the playback thread. */
|
/** Throws a playback exception on the playback thread. */
|
||||||
|
|
@ -566,18 +737,35 @@ public abstract class Action {
|
||||||
/** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */
|
/** Waits for {@link Player.EventListener#onTimelineChanged(Timeline, int)}. */
|
||||||
public static final class WaitForTimelineChanged extends Action {
|
public static final class WaitForTimelineChanged extends Action {
|
||||||
|
|
||||||
@Nullable private final Timeline expectedTimeline;
|
private final Timeline expectedTimeline;
|
||||||
|
private final boolean ignoreExpectedReason;
|
||||||
|
@Player.TimelineChangeReason private final int expectedReason;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates action waiting for a timeline change.
|
* Creates action waiting for a timeline change for a given reason.
|
||||||
*
|
*
|
||||||
* @param tag A tag to use for logging.
|
* @param tag A tag to use for logging.
|
||||||
* @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline
|
* @param expectedTimeline The expected timeline or null if any timeline change is relevant.
|
||||||
* change.
|
* @param expectedReason The expected timeline change reason.
|
||||||
*/
|
*/
|
||||||
public WaitForTimelineChanged(String tag, @Nullable Timeline expectedTimeline) {
|
public WaitForTimelineChanged(
|
||||||
|
String tag, Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
|
||||||
super(tag, "WaitForTimelineChanged");
|
super(tag, "WaitForTimelineChanged");
|
||||||
this.expectedTimeline = expectedTimeline;
|
this.expectedTimeline = expectedTimeline;
|
||||||
|
this.ignoreExpectedReason = false;
|
||||||
|
this.expectedReason = expectedReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates action waiting for any timeline change for any reason.
|
||||||
|
*
|
||||||
|
* @param tag A tag to use for logging.
|
||||||
|
*/
|
||||||
|
public WaitForTimelineChanged(String tag) {
|
||||||
|
super(tag, "WaitForTimelineChanged");
|
||||||
|
this.expectedTimeline = null;
|
||||||
|
this.ignoreExpectedReason = true;
|
||||||
|
this.expectedReason = Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -595,7 +783,9 @@ public abstract class Action {
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(
|
public void onTimelineChanged(
|
||||||
Timeline timeline, @Player.TimelineChangeReason int reason) {
|
Timeline timeline, @Player.TimelineChangeReason int reason) {
|
||||||
if (expectedTimeline == null || timeline.equals(expectedTimeline)) {
|
if ((expectedTimeline == null
|
||||||
|
|| TestUtil.areTimelinesSame(expectedTimeline, timeline))
|
||||||
|
&& (ignoreExpectedReason || expectedReason == reason)) {
|
||||||
player.removeListener(this);
|
player.removeListener(this);
|
||||||
nextAction.schedule(player, trackSelector, surface, handler);
|
nextAction.schedule(player, trackSelector, surface, handler);
|
||||||
}
|
}
|
||||||
|
|
@ -783,7 +973,7 @@ public abstract class Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Calls {@link Runnable#run()}. */
|
/** Calls {@code Runnable.run()}. */
|
||||||
public static final class ExecuteRunnable extends Action {
|
public static final class ExecuteRunnable extends Action {
|
||||||
|
|
||||||
private final Runnable runnable;
|
private final Runnable runnable;
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,10 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface;
|
import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface;
|
||||||
import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;
|
import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable;
|
||||||
import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition;
|
import com.google.android.exoplayer2.testutil.Action.PlayUntilPosition;
|
||||||
import com.google.android.exoplayer2.testutil.Action.PrepareSource;
|
|
||||||
import com.google.android.exoplayer2.testutil.Action.Seek;
|
import com.google.android.exoplayer2.testutil.Action.Seek;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SendBroadcast;
|
import com.google.android.exoplayer2.testutil.Action.SendBroadcast;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SendMessages;
|
import com.google.android.exoplayer2.testutil.Action.SendMessages;
|
||||||
|
|
@ -42,6 +42,7 @@ import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;
|
import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
|
import com.google.android.exoplayer2.testutil.Action.SetRepeatMode;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
|
import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled;
|
||||||
|
import com.google.android.exoplayer2.testutil.Action.SetShuffleOrder;
|
||||||
import com.google.android.exoplayer2.testutil.Action.SetVideoSurface;
|
import com.google.android.exoplayer2.testutil.Action.SetVideoSurface;
|
||||||
import com.google.android.exoplayer2.testutil.Action.Stop;
|
import com.google.android.exoplayer2.testutil.Action.Stop;
|
||||||
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
|
import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException;
|
||||||
|
|
@ -173,7 +174,19 @@ public final class ActionSchedule {
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
public Builder seek(int windowIndex, long positionMs) {
|
public Builder seek(int windowIndex, long positionMs) {
|
||||||
return apply(new Seek(tag, windowIndex, positionMs));
|
return apply(new Seek(tag, windowIndex, positionMs, /* catchIllegalSeekException= */ false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a seek action to be executed.
|
||||||
|
*
|
||||||
|
* @param windowIndex The window to seek to.
|
||||||
|
* @param positionMs The seek position.
|
||||||
|
* @param catchIllegalSeekException Whether an illegal seek position should be caught or not.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder seek(int windowIndex, long positionMs, boolean catchIllegalSeekException) {
|
||||||
|
return apply(new Seek(tag, windowIndex, positionMs, catchIllegalSeekException));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -314,23 +327,99 @@ public final class ActionSchedule {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a new source preparation action.
|
* Schedules a set media items action to be executed.
|
||||||
*
|
*
|
||||||
|
* @param windowIndex The window index to start playback from or {@link C#INDEX_UNSET} if the
|
||||||
|
* playback position should not be reset.
|
||||||
|
* @param positionMs The position in milliseconds from where playback should start. If {@link
|
||||||
|
* C#TIME_UNSET} is passed the default position is used. In any case, if {@code windowIndex}
|
||||||
|
* is set to {@link C#INDEX_UNSET} the position is not reset at all and this parameter is
|
||||||
|
* ignored.
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
public Builder prepareSource(MediaSource mediaSource) {
|
public Builder setMediaItems(int windowIndex, long positionMs, MediaSource... sources) {
|
||||||
return apply(new PrepareSource(tag, mediaSource));
|
return apply(new Action.SetMediaItems(tag, windowIndex, positionMs, sources));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a new source preparation action.
|
* Schedules a set media items action to be executed.
|
||||||
*
|
*
|
||||||
* @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean)
|
* @param resetPosition Whether the playback position should be reset.
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
public Builder prepareSource(
|
public Builder setMediaItems(boolean resetPosition, MediaSource... sources) {
|
||||||
MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
return apply(new Action.SetMediaItemsResetPosition(tag, resetPosition, sources));
|
||||||
return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState));
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a set media items action to be executed.
|
||||||
|
*
|
||||||
|
* @param mediaSources The media sources to add.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder setMediaItems(MediaSource... mediaSources) {
|
||||||
|
return apply(
|
||||||
|
new Action.SetMediaItems(
|
||||||
|
tag, /* windowIndex= */ C.INDEX_UNSET, /* positionMs= */ C.TIME_UNSET, mediaSources));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Schedules a add media items action to be executed.
|
||||||
|
*
|
||||||
|
* @param mediaSources The media sources to add.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder addMediaItems(MediaSource... mediaSources) {
|
||||||
|
return apply(new Action.AddMediaItems(tag, mediaSources));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a move media item action to be executed.
|
||||||
|
*
|
||||||
|
* @param currentIndex The current index of the item to move.
|
||||||
|
* @param newIndex The index after the item has been moved.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder moveMediaItem(int currentIndex, int newIndex) {
|
||||||
|
return apply(new Action.MoveMediaItem(tag, currentIndex, newIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a remove media item action to be executed.
|
||||||
|
*
|
||||||
|
* @param index The index of the media item to be removed.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder removeMediaItem(int index) {
|
||||||
|
return apply(new Action.RemoveMediaItem(tag, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a remove media items action to be executed.
|
||||||
|
*
|
||||||
|
* @param fromIndex The start of the range of media items to be removed.
|
||||||
|
* @param toIndex The end of the range of media items to be removed (exclusive).
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder removeMediaItems(int fromIndex, int toIndex) {
|
||||||
|
return apply(new Action.RemoveMediaItems(tag, fromIndex, toIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a prepare action to be executed.
|
||||||
|
*
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder prepare() {
|
||||||
|
return apply(new Action.Prepare(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a clear media items action to be created.
|
||||||
|
*
|
||||||
|
* @return The builder. for convenience,
|
||||||
|
*/
|
||||||
|
public Builder clearMediaItems() {
|
||||||
|
return apply(new Action.ClearMediaItems(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -343,7 +432,17 @@ public final class ActionSchedule {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a shuffle setting action.
|
* Schedules a set shuffle order action to be executed.
|
||||||
|
*
|
||||||
|
* @param shuffleOrder The shuffle order.
|
||||||
|
* @return The builder, for convenience.
|
||||||
|
*/
|
||||||
|
public Builder setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
return apply(new SetShuffleOrder(tag, shuffleOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a shuffle setting action to be executed.
|
||||||
*
|
*
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
|
|
@ -405,18 +504,19 @@ public final class ActionSchedule {
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
public Builder waitForTimelineChanged() {
|
public Builder waitForTimelineChanged() {
|
||||||
return apply(new WaitForTimelineChanged(tag, /* expectedTimeline= */ null));
|
return apply(new WaitForTimelineChanged(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules a delay until the timeline changed to a specified expected timeline.
|
* Schedules a delay until the timeline changed to a specified expected timeline.
|
||||||
*
|
*
|
||||||
* @param expectedTimeline The expected timeline to wait for. If null, wait for any timeline
|
* @param expectedTimeline The expected timeline.
|
||||||
* change.
|
* @param expectedReason The expected reason of the timeline change.
|
||||||
* @return The builder, for convenience.
|
* @return The builder, for convenience.
|
||||||
*/
|
*/
|
||||||
public Builder waitForTimelineChanged(Timeline expectedTimeline) {
|
public Builder waitForTimelineChanged(
|
||||||
return apply(new WaitForTimelineChanged(tag, expectedTimeline));
|
Timeline expectedTimeline, @Player.TimelineChangeReason int expectedReason) {
|
||||||
|
return apply(new WaitForTimelineChanged(tag, expectedTimeline, expectedReason));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,8 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest {
|
||||||
pendingSchedule = null;
|
pendingSchedule = null;
|
||||||
}
|
}
|
||||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = buildDrmSessionManager(userAgent);
|
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = buildDrmSessionManager(userAgent);
|
||||||
player.prepare(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager));
|
player.setMediaItem(buildSource(host, Util.getUserAgent(host, userAgent), drmSessionManager));
|
||||||
|
player.prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
package com.google.android.exoplayer2.testutil;
|
package com.google.android.exoplayer2.testutil;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static junit.framework.TestCase.assertTrue;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
|
|
@ -43,6 +45,7 @@ import com.google.android.exoplayer2.util.HandlerWrapper;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
@ -71,8 +74,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
private Timeline timeline;
|
private Timeline timeline;
|
||||||
|
private List<MediaSource> mediaSources;
|
||||||
private Object manifest;
|
private Object manifest;
|
||||||
private MediaSource mediaSource;
|
|
||||||
private DefaultTrackSelector trackSelector;
|
private DefaultTrackSelector trackSelector;
|
||||||
private LoadControl loadControl;
|
private LoadControl loadControl;
|
||||||
private BandwidthMeter bandwidthMeter;
|
private BandwidthMeter bandwidthMeter;
|
||||||
|
|
@ -83,19 +86,28 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
private Player.EventListener eventListener;
|
private Player.EventListener eventListener;
|
||||||
private AnalyticsListener analyticsListener;
|
private AnalyticsListener analyticsListener;
|
||||||
private Integer expectedPlayerEndedCount;
|
private Integer expectedPlayerEndedCount;
|
||||||
|
private boolean useLazyPreparation;
|
||||||
|
private int initialWindowIndex;
|
||||||
|
private long initialPositionMs;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
mediaSources = new ArrayList<>();
|
||||||
|
initialWindowIndex = C.INDEX_UNSET;
|
||||||
|
initialPositionMs = C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The
|
* Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The
|
||||||
* default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link
|
* default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of {@link
|
||||||
* FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is
|
* FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the timeline is
|
||||||
* not allowed after a call to {@link #setMediaSource(MediaSource)}.
|
* not allowed after a call to {@link #setMediaSources(MediaSource...)}.
|
||||||
*
|
*
|
||||||
* @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test
|
* @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test
|
||||||
* runner.
|
* runner.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
public Builder setTimeline(Timeline timeline) {
|
public Builder setTimeline(Timeline timeline) {
|
||||||
assertThat(mediaSource).isNull();
|
assertThat(mediaSources).isEmpty();
|
||||||
this.timeline = timeline;
|
this.timeline = timeline;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -103,30 +115,54 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
/**
|
/**
|
||||||
* Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value
|
* Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value
|
||||||
* is null. Setting the manifest is not allowed after a call to {@link
|
* is null. Setting the manifest is not allowed after a call to {@link
|
||||||
* #setMediaSource(MediaSource)}.
|
* #setMediaSources(MediaSource...)}.
|
||||||
*
|
*
|
||||||
* @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner.
|
* @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
public Builder setManifest(Object manifest) {
|
public Builder setManifest(Object manifest) {
|
||||||
assertThat(mediaSource).isNull();
|
assertThat(mediaSources).isEmpty();
|
||||||
this.manifest = manifest;
|
this.manifest = manifest;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a {@link MediaSource} to be used by the test runner. The default value is a {@link
|
* Seeks before setting the media sources and preparing the player.
|
||||||
* FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)}
|
|
||||||
* and {@link #setManifest(Object)}. Setting the media source is not allowed after calls to
|
|
||||||
* {@link #setTimeline(Timeline)} and/or {@link #setManifest(Object)}.
|
|
||||||
*
|
*
|
||||||
* @param mediaSource A {@link MediaSource} to be used by the test runner.
|
* @param windowIndex The window index to seek to.
|
||||||
|
* @param positionMs The position in milliseconds to seek to.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
*/
|
*/
|
||||||
public Builder setMediaSource(MediaSource mediaSource) {
|
public Builder initialSeek(int windowIndex, int positionMs) {
|
||||||
|
this.initialWindowIndex = windowIndex;
|
||||||
|
this.initialPositionMs = positionMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link MediaSource}s to be used by the test runner. The default value is a {@link
|
||||||
|
* FakeMediaSource} with the timeline and manifest provided by {@link #setTimeline(Timeline)}
|
||||||
|
* and {@link #setManifest(Object)}. Setting media sources is not allowed after calls to {@link
|
||||||
|
* #setTimeline(Timeline)} and/or {@link #setManifest(Object)}.
|
||||||
|
*
|
||||||
|
* @param mediaSources The {@link MediaSource}s to be used by the test runner.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
public Builder setMediaSources(MediaSource... mediaSources) {
|
||||||
assertThat(timeline).isNull();
|
assertThat(timeline).isNull();
|
||||||
assertThat(manifest).isNull();
|
assertThat(manifest).isNull();
|
||||||
this.mediaSource = mediaSource;
|
this.mediaSources = Arrays.asList(mediaSources);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to use lazy preparation.
|
||||||
|
*
|
||||||
|
* @param useLazyPreparation Whether to use lazy preparation.
|
||||||
|
* @return This builder.
|
||||||
|
*/
|
||||||
|
public Builder setUseLazyPreparation(boolean useLazyPreparation) {
|
||||||
|
this.useLazyPreparation = useLazyPreparation;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +206,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
* Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media
|
* Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media
|
||||||
* periods and for setting up a {@link FakeRenderer}. The default value is a single {@link
|
* periods and for setting up a {@link FakeRenderer}. The default value is a single {@link
|
||||||
* #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source
|
* #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media source
|
||||||
* with {@link #setMediaSource(MediaSource)} and renderers with {@link
|
* with {@link #setMediaSources(MediaSource...)} and renderers with {@link
|
||||||
* #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.
|
* #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set.
|
||||||
*
|
*
|
||||||
* @param supportedFormats A list of supported {@link Format}s.
|
* @param supportedFormats A list of supported {@link Format}s.
|
||||||
|
|
@ -224,7 +260,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets an {@link ActionSchedule} to be run by the test runner. The first action will be
|
* Sets an {@link ActionSchedule} to be run by the test runner. The first action will be
|
||||||
* executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}.
|
* executed immediately before {@link SimpleExoPlayer#prepare()}.
|
||||||
*
|
*
|
||||||
* @param actionSchedule An {@link ActionSchedule} to be used by the test runner.
|
* @param actionSchedule An {@link ActionSchedule} to be used by the test runner.
|
||||||
* @return This builder.
|
* @return This builder.
|
||||||
|
|
@ -305,11 +341,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
if (clock == null) {
|
if (clock == null) {
|
||||||
clock = new AutoAdvancingFakeClock();
|
clock = new AutoAdvancingFakeClock();
|
||||||
}
|
}
|
||||||
if (mediaSource == null) {
|
if (mediaSources.isEmpty()) {
|
||||||
if (timeline == null) {
|
if (timeline == null) {
|
||||||
timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
|
timeline = new FakeTimeline(/* windowCount= */ 1, manifest);
|
||||||
}
|
}
|
||||||
mediaSource = new FakeMediaSource(timeline, supportedFormats);
|
mediaSources.add(new FakeMediaSource(timeline, supportedFormats));
|
||||||
}
|
}
|
||||||
if (expectedPlayerEndedCount == null) {
|
if (expectedPlayerEndedCount == null) {
|
||||||
expectedPlayerEndedCount = 1;
|
expectedPlayerEndedCount = 1;
|
||||||
|
|
@ -317,7 +353,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
return new ExoPlayerTestRunner(
|
return new ExoPlayerTestRunner(
|
||||||
context,
|
context,
|
||||||
clock,
|
clock,
|
||||||
mediaSource,
|
initialWindowIndex,
|
||||||
|
initialPositionMs,
|
||||||
|
mediaSources,
|
||||||
|
useLazyPreparation,
|
||||||
renderersFactory,
|
renderersFactory,
|
||||||
trackSelector,
|
trackSelector,
|
||||||
loadControl,
|
loadControl,
|
||||||
|
|
@ -331,7 +370,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final MediaSource mediaSource;
|
private final int initialWindowIndex;
|
||||||
|
private final long initialPositionMs;
|
||||||
|
private final List<MediaSource> mediaSources;
|
||||||
private final RenderersFactory renderersFactory;
|
private final RenderersFactory renderersFactory;
|
||||||
private final DefaultTrackSelector trackSelector;
|
private final DefaultTrackSelector trackSelector;
|
||||||
private final LoadControl loadControl;
|
private final LoadControl loadControl;
|
||||||
|
|
@ -348,6 +389,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
private final ArrayList<Integer> timelineChangeReasons;
|
private final ArrayList<Integer> timelineChangeReasons;
|
||||||
private final ArrayList<Integer> periodIndices;
|
private final ArrayList<Integer> periodIndices;
|
||||||
private final ArrayList<Integer> discontinuityReasons;
|
private final ArrayList<Integer> discontinuityReasons;
|
||||||
|
private final ArrayList<Integer> playbackStates;
|
||||||
|
private final boolean useLazyPreparation;
|
||||||
|
|
||||||
private SimpleExoPlayer player;
|
private SimpleExoPlayer player;
|
||||||
private Exception exception;
|
private Exception exception;
|
||||||
|
|
@ -357,7 +400,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
private ExoPlayerTestRunner(
|
private ExoPlayerTestRunner(
|
||||||
Context context,
|
Context context,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
MediaSource mediaSource,
|
int initialWindowIndex,
|
||||||
|
long initialPositionMs,
|
||||||
|
List<MediaSource> mediaSources,
|
||||||
|
boolean useLazyPreparation,
|
||||||
RenderersFactory renderersFactory,
|
RenderersFactory renderersFactory,
|
||||||
DefaultTrackSelector trackSelector,
|
DefaultTrackSelector trackSelector,
|
||||||
LoadControl loadControl,
|
LoadControl loadControl,
|
||||||
|
|
@ -368,7 +414,10 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
int expectedPlayerEndedCount) {
|
int expectedPlayerEndedCount) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.mediaSource = mediaSource;
|
this.initialWindowIndex = initialWindowIndex;
|
||||||
|
this.initialPositionMs = initialPositionMs;
|
||||||
|
this.mediaSources = mediaSources;
|
||||||
|
this.useLazyPreparation = useLazyPreparation;
|
||||||
this.renderersFactory = renderersFactory;
|
this.renderersFactory = renderersFactory;
|
||||||
this.trackSelector = trackSelector;
|
this.trackSelector = trackSelector;
|
||||||
this.loadControl = loadControl;
|
this.loadControl = loadControl;
|
||||||
|
|
@ -380,6 +429,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
this.timelineChangeReasons = new ArrayList<>();
|
this.timelineChangeReasons = new ArrayList<>();
|
||||||
this.periodIndices = new ArrayList<>();
|
this.periodIndices = new ArrayList<>();
|
||||||
this.discontinuityReasons = new ArrayList<>();
|
this.discontinuityReasons = new ArrayList<>();
|
||||||
|
this.playbackStates = new ArrayList<>();
|
||||||
this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
|
this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount);
|
||||||
this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
|
this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0);
|
||||||
this.playerThread = new HandlerThread("ExoPlayerTest thread");
|
this.playerThread = new HandlerThread("ExoPlayerTest thread");
|
||||||
|
|
@ -418,6 +468,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
.setBandwidthMeter(bandwidthMeter)
|
.setBandwidthMeter(bandwidthMeter)
|
||||||
.setAnalyticsCollector(new AnalyticsCollector(clock))
|
.setAnalyticsCollector(new AnalyticsCollector(clock))
|
||||||
.setClock(clock)
|
.setClock(clock)
|
||||||
|
.setUseLazyPreparation(useLazyPreparation)
|
||||||
.setLooper(Looper.myLooper())
|
.setLooper(Looper.myLooper())
|
||||||
.build();
|
.build();
|
||||||
player.addListener(ExoPlayerTestRunner.this);
|
player.addListener(ExoPlayerTestRunner.this);
|
||||||
|
|
@ -431,7 +482,13 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
if (actionSchedule != null) {
|
if (actionSchedule != null) {
|
||||||
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
|
actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this);
|
||||||
}
|
}
|
||||||
player.prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false);
|
if (initialWindowIndex != C.INDEX_UNSET) {
|
||||||
|
player.seekTo(initialWindowIndex, initialPositionMs);
|
||||||
|
}
|
||||||
|
player.setMediaItems(mediaSources, /* resetPosition= */ false);
|
||||||
|
if (doPrepare) {
|
||||||
|
player.prepare();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleException(e);
|
handleException(e);
|
||||||
}
|
}
|
||||||
|
|
@ -483,12 +540,16 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline,
|
* Asserts that the timelines reported by {@link Player.EventListener#onTimelineChanged(Timeline,
|
||||||
* int)} are equal to the provided timelines.
|
* int)} are the same to the provided timelines. This assert differs from testing equality by not
|
||||||
|
* comparing period ids which may be different due to id mapping of child source period ids.
|
||||||
*
|
*
|
||||||
* @param timelines A list of expected {@link Timeline}s.
|
* @param timelines A list of expected {@link Timeline}s.
|
||||||
*/
|
*/
|
||||||
public void assertTimelinesEqual(Timeline... timelines) {
|
public void assertTimelinesSame(Timeline... timelines) {
|
||||||
assertThat(this.timelines).containsExactlyElementsIn(Arrays.asList(timelines)).inOrder();
|
assertThat(this.timelines).hasSize(timelines.length);
|
||||||
|
for (int i = 0; i < timelines.length; i++) {
|
||||||
|
assertTrue(TestUtil.areTimelinesSame(timelines[i], this.timelines.get(i)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -500,6 +561,15 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder();
|
assertThat(timelineChangeReasons).containsExactlyElementsIn(Arrays.asList(reasons)).inOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the playback states reported by {@link
|
||||||
|
* Player.EventListener#onPlayerStateChanged(boolean, int)} are equal to the provided playback
|
||||||
|
* states.
|
||||||
|
*/
|
||||||
|
public void assertPlaybackStatesEqual(Integer... states) {
|
||||||
|
assertThat(playbackStates).containsExactlyElementsIn(Arrays.asList(states)).inOrder();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the last track group array reported by {@link
|
* Asserts that the last track group array reported by {@link
|
||||||
* Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the
|
* Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to the
|
||||||
|
|
@ -575,10 +645,12 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
|
public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
|
||||||
timelines.add(timeline);
|
|
||||||
timelineChangeReasons.add(reason);
|
timelineChangeReasons.add(reason);
|
||||||
if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) {
|
timelines.add(timeline);
|
||||||
periodIndices.add(player.getCurrentPeriodIndex());
|
int currentIndex = player.getCurrentPeriodIndex();
|
||||||
|
if (periodIndices.isEmpty() || periodIndices.get(periodIndices.size() - 1) != currentIndex) {
|
||||||
|
// Ignore timeline changes that do not change the period index.
|
||||||
|
periodIndices.add(currentIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -589,6 +661,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener, ActionSc
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {
|
||||||
|
playbackStates.add(playbackState);
|
||||||
playerWasPrepared |= playbackState != Player.STATE_IDLE;
|
playerWasPrepared |= playbackState != Player.STATE_IDLE;
|
||||||
if (playbackState == Player.STATE_ENDED
|
if (playbackState == Player.STATE_ENDED
|
||||||
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
|
|| (playbackState == Player.STATE_IDLE && playerWasPrepared)) {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ public final class FakeTimeline extends Timeline {
|
||||||
public final Object id;
|
public final Object id;
|
||||||
public final boolean isSeekable;
|
public final boolean isSeekable;
|
||||||
public final boolean isDynamic;
|
public final boolean isDynamic;
|
||||||
|
public final boolean isLive;
|
||||||
public final long durationUs;
|
public final long durationUs;
|
||||||
public final AdPlaybackState adPlaybackState;
|
public final AdPlaybackState adPlaybackState;
|
||||||
|
|
||||||
|
|
@ -99,10 +100,41 @@ public final class FakeTimeline extends Timeline {
|
||||||
boolean isDynamic,
|
boolean isDynamic,
|
||||||
long durationUs,
|
long durationUs,
|
||||||
AdPlaybackState adPlaybackState) {
|
AdPlaybackState adPlaybackState) {
|
||||||
|
this(
|
||||||
|
periodCount,
|
||||||
|
id,
|
||||||
|
isSeekable,
|
||||||
|
isDynamic,
|
||||||
|
/* isLive= */ isDynamic,
|
||||||
|
durationUs,
|
||||||
|
adPlaybackState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a window definition with ad groups.
|
||||||
|
*
|
||||||
|
* @param periodCount The number of periods in the window. Each period get an equal slice of the
|
||||||
|
* total window duration.
|
||||||
|
* @param id The UID of the window.
|
||||||
|
* @param isSeekable Whether the window is seekable.
|
||||||
|
* @param isDynamic Whether the window is dynamic.
|
||||||
|
* @param isLive Whether the window is live.
|
||||||
|
* @param durationUs The duration of the window in microseconds.
|
||||||
|
* @param adPlaybackState The ad playback state.
|
||||||
|
*/
|
||||||
|
public TimelineWindowDefinition(
|
||||||
|
int periodCount,
|
||||||
|
Object id,
|
||||||
|
boolean isSeekable,
|
||||||
|
boolean isDynamic,
|
||||||
|
boolean isLive,
|
||||||
|
long durationUs,
|
||||||
|
AdPlaybackState adPlaybackState) {
|
||||||
this.periodCount = periodCount;
|
this.periodCount = periodCount;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.isSeekable = isSeekable;
|
this.isSeekable = isSeekable;
|
||||||
this.isDynamic = isDynamic;
|
this.isDynamic = isDynamic;
|
||||||
|
this.isLive = isLive;
|
||||||
this.durationUs = durationUs;
|
this.durationUs = durationUs;
|
||||||
this.adPlaybackState = adPlaybackState;
|
this.adPlaybackState = adPlaybackState;
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +221,7 @@ public final class FakeTimeline extends Timeline {
|
||||||
/* windowStartTimeMs= */ C.TIME_UNSET,
|
/* windowStartTimeMs= */ C.TIME_UNSET,
|
||||||
windowDefinition.isSeekable,
|
windowDefinition.isSeekable,
|
||||||
windowDefinition.isDynamic,
|
windowDefinition.isDynamic,
|
||||||
/* isLive= */ windowDefinition.isDynamic,
|
windowDefinition.isLive,
|
||||||
/* defaultPositionUs= */ 0,
|
/* defaultPositionUs= */ 0,
|
||||||
windowDefinition.durationUs,
|
windowDefinition.durationUs,
|
||||||
periodOffsets[windowIndex],
|
periodOffsets[windowIndex],
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ import com.google.android.exoplayer2.PlayerMessage;
|
||||||
import com.google.android.exoplayer2.SeekParameters;
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.source.MediaSource;
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException}
|
* An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException}
|
||||||
|
|
@ -96,6 +98,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepare(MediaSource mediaSource) {
|
public void prepare(MediaSource mediaSource) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
@ -106,6 +113,77 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaSource> mediaItems, boolean resetPosition) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(List<MediaSource> mediaItems) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItems(
|
||||||
|
List<MediaSource> mediaItems, int startWindowIndex, long startPositionMs) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem, long startPositionMs) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMediaItem(MediaSource mediaItem) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(MediaSource mediaSource) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItem(int index, MediaSource mediaSource) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(List<MediaSource> mediaSources) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addMediaItems(int index, List<MediaSource> mediaSources) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItem(int currentIndex, int newIndex) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MediaSource removeMediaItem(int index) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeMediaItems(int fromIndex, int toIndex) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearMediaItems() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setPlayWhenReady(boolean playWhenReady) {
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
@ -126,6 +204,11 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.database.DatabaseProvider;
|
import com.google.android.exoplayer2.database.DatabaseProvider;
|
||||||
import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
|
import com.google.android.exoplayer2.database.DefaultDatabaseProvider;
|
||||||
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
|
||||||
|
|
@ -398,4 +399,62 @@ public class TestUtil {
|
||||||
}
|
}
|
||||||
return new DefaultExtractorInput(dataSource, position, length);
|
return new DefaultExtractorInput(dataSource, position, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the timelines are the same (does not compare {@link Timeline.Window#uid} and
|
||||||
|
* {@link Timeline.Period#uid}).
|
||||||
|
*
|
||||||
|
* @param firstTimeline The first {@link Timeline}.
|
||||||
|
* @param secondTimeline The second {@link Timeline} to compare with.
|
||||||
|
* @return {@code true} if both timelines are the same.
|
||||||
|
*/
|
||||||
|
public static boolean areTimelinesSame(Timeline firstTimeline, Timeline secondTimeline) {
|
||||||
|
if (firstTimeline == secondTimeline) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (secondTimeline.getWindowCount() != firstTimeline.getWindowCount()
|
||||||
|
|| secondTimeline.getPeriodCount() != firstTimeline.getPeriodCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Timeline.Window firstWindow = new Timeline.Window();
|
||||||
|
Timeline.Period firstPeriod = new Timeline.Period();
|
||||||
|
Timeline.Window secondWindow = new Timeline.Window();
|
||||||
|
Timeline.Period secondPeriod = new Timeline.Period();
|
||||||
|
for (int i = 0; i < firstTimeline.getWindowCount(); i++) {
|
||||||
|
if (!areWindowsSame(
|
||||||
|
firstTimeline.getWindow(i, firstWindow), secondTimeline.getWindow(i, secondWindow))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < firstTimeline.getPeriodCount(); i++) {
|
||||||
|
if (!firstTimeline
|
||||||
|
.getPeriod(i, firstPeriod, /* setIds= */ false)
|
||||||
|
.equals(secondTimeline.getPeriod(i, secondPeriod, /* setIds= */ false))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the windows are the same. This comparison does not compare the uid.
|
||||||
|
*
|
||||||
|
* @param first The first {@link Timeline.Window}.
|
||||||
|
* @param second The second {@link Timeline.Window}.
|
||||||
|
* @return true if both windows are the same.
|
||||||
|
*/
|
||||||
|
private static boolean areWindowsSame(Timeline.Window first, Timeline.Window second) {
|
||||||
|
return Util.areEqual(first.tag, second.tag)
|
||||||
|
&& Util.areEqual(first.manifest, second.manifest)
|
||||||
|
&& first.presentationStartTimeMs == second.presentationStartTimeMs
|
||||||
|
&& first.windowStartTimeMs == second.windowStartTimeMs
|
||||||
|
&& first.isSeekable == second.isSeekable
|
||||||
|
&& first.isDynamic == second.isDynamic
|
||||||
|
&& first.isLive == second.isLive
|
||||||
|
&& first.defaultPositionUs == second.defaultPositionUs
|
||||||
|
&& first.durationUs == second.durationUs
|
||||||
|
&& first.firstPeriodIndex == second.firstPeriodIndex
|
||||||
|
&& first.lastPeriodIndex == second.lastPeriodIndex
|
||||||
|
&& first.positionInFirstPeriodUs == second.positionInFirstPeriodUs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue