diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index 3c76c7bbeb..b9e3f86096 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -64,6 +64,7 @@ import java.lang.reflect.Method; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -2186,6 +2187,24 @@ public final class Util { : SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs; } + /** + * Moves the elements starting at {@code fromIndex} to {@code newFromIndex}. + * + * @param items The list of which to move elements. + * @param fromIndex The index at which the items to move start. + * @param toIndex The index up to which elements should be moved (exclusive). + * @param newFromIndex The new from index. + */ + public static void moveItems( + List items, int fromIndex, int toIndex, int newFromIndex) { + ArrayDeque removedItems = new ArrayDeque<>(); + int removedItemsLength = toIndex - fromIndex; + for (int i = removedItemsLength - 1; i >= 0; i--) { + removedItems.addFirst(items.remove(fromIndex + i)); + } + items.addAll(Math.min(newFromIndex, items.size()), removedItems); + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index cf3fd51201..d551e8d6d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; import android.annotation.SuppressLint; import android.os.Handler; @@ -71,7 +72,7 @@ import java.util.concurrent.TimeoutException; private final CopyOnWriteArrayList listeners; private final Timeline.Period period; private final ArrayDeque pendingListenerNotifications; - private final List mediaSourceHolders; + private final List mediaSourceHolderSnapshots; private final boolean useLazyPreparation; private final MediaSourceFactory mediaSourceFactory; @@ -130,7 +131,7 @@ import java.util.concurrent.TimeoutException; Looper applicationLooper) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); - Assertions.checkState(renderers.length > 0); + checkState(renderers.length > 0); this.renderers = checkNotNull(renderers); this.trackSelector = checkNotNull(trackSelector); this.mediaSourceFactory = mediaSourceFactory; @@ -139,7 +140,7 @@ import java.util.concurrent.TimeoutException; this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems; repeatMode = Player.REPEAT_MODE_OFF; listeners = new CopyOnWriteArrayList<>(); - mediaSourceHolders = new ArrayList<>(); + mediaSourceHolderSnapshots = new ArrayList<>(); shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0); emptyTrackSelectorResult = new TrackSelectorResult( @@ -387,7 +388,7 @@ import java.util.concurrent.TimeoutException; @Override public void addMediaItems(List mediaItems) { - addMediaItems(/* index= */ mediaSourceHolders.size(), mediaItems); + addMediaItems(/* index= */ mediaSourceHolderSnapshots.size(), mediaItems); } @Override @@ -407,7 +408,7 @@ import java.util.concurrent.TimeoutException; @Override public void addMediaSources(List mediaSources) { - addMediaSources(/* index= */ mediaSourceHolders.size(), mediaSources); + addMediaSources(/* index= */ mediaSourceHolderSnapshots.size(), mediaSources); } @Override @@ -442,14 +443,15 @@ import java.util.concurrent.TimeoutException; Assertions.checkArgument( fromIndex >= 0 && fromIndex <= toIndex - && toIndex <= mediaSourceHolders.size() + && toIndex <= mediaSourceHolderSnapshots.size() && newFromIndex >= 0); int currentWindowIndex = getCurrentWindowIndex(); long currentPositionMs = getCurrentPosition(); Timeline oldTimeline = getCurrentTimeline(); pendingOperationAcks++; - newFromIndex = Math.min(newFromIndex, mediaSourceHolders.size() - (toIndex - fromIndex)); - MediaSourceList.moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); + newFromIndex = + Math.min(newFromIndex, mediaSourceHolderSnapshots.size() - (toIndex - fromIndex)); + Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex); PlaybackInfo playbackInfo = maskTimelineAndWindowIndex(currentWindowIndex, currentPositionMs, oldTimeline); internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder); @@ -464,10 +466,10 @@ import java.util.concurrent.TimeoutException; @Override public void clearMediaItems() { - if (mediaSourceHolders.isEmpty()) { + if (mediaSourceHolderSnapshots.isEmpty()) { return; } - removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolders.size()); + removeMediaItemsInternal(/* fromIndex= */ 0, /* toIndex= */ mediaSourceHolderSnapshots.size()); } @Override @@ -624,7 +626,7 @@ import java.util.concurrent.TimeoutException; @SuppressWarnings("deprecation") @Override public void setPlaybackSpeed(float playbackSpeed) { - Assertions.checkState(playbackSpeed > 0); + checkState(playbackSpeed > 0); if (this.playbackSpeed == playbackSpeed) { return; } @@ -918,11 +920,18 @@ import java.util.concurrent.TimeoutException; pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason; } if (pendingOperationAcks == 0) { - if (!this.playbackInfo.timeline.isEmpty() - && playbackInfoUpdate.playbackInfo.timeline.isEmpty()) { + Timeline newTimeline = playbackInfoUpdate.playbackInfo.timeline; + if (!this.playbackInfo.timeline.isEmpty() && newTimeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. resetMaskingPosition(); } + if (!newTimeline.isEmpty()) { + List timelines = ((PlaylistTimeline) newTimeline).getChildTimelines(); + checkState(timelines.size() == mediaSourceHolderSnapshots.size()); + for (int i = 0; i < timelines.size(); i++) { + mediaSourceHolderSnapshots.get(i).timeline = timelines.get(i); + } + } boolean positionDiscontinuity = hasPendingDiscontinuity; hasPendingDiscontinuity = false; updatePlaybackInfo( @@ -940,7 +949,7 @@ import java.util.concurrent.TimeoutException; if (clearPlaylist) { // Reset list of media source holders which are used for creating the masking timeline. removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolderSnapshots.size()); resetMaskingPosition(); } else { maskWithCurrentPosition(); @@ -1004,9 +1013,9 @@ import java.util.concurrent.TimeoutException; int currentWindowIndex = getCurrentWindowIndexInternal(); long currentPositionMs = getCurrentPosition(); pendingOperationAcks++; - if (!mediaSourceHolders.isEmpty()) { + if (!mediaSourceHolderSnapshots.isEmpty()) { removeMediaSourceHolders( - /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolders.size()); + /* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolderSnapshots.size()); } List holders = addMediaSourceHolders(/* index= */ 0, mediaSources); @@ -1055,7 +1064,8 @@ import java.util.concurrent.TimeoutException; MediaSourceList.MediaSourceHolder holder = new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation); holders.add(holder); - mediaSourceHolders.add(i + index, holder); + mediaSourceHolderSnapshots.add( + i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource.getTimeline())); } shuffleOrder = shuffleOrder.cloneAndInsert( @@ -1065,11 +1075,11 @@ import java.util.concurrent.TimeoutException; private void removeMediaItemsInternal(int fromIndex, int toIndex) { Assertions.checkArgument( - fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolders.size()); + fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolderSnapshots.size()); int currentWindowIndex = getCurrentWindowIndex(); long currentPositionMs = getCurrentPosition(); Timeline oldTimeline = getCurrentTimeline(); - int currentMediaSourceCount = mediaSourceHolders.size(); + int currentMediaSourceCount = mediaSourceHolderSnapshots.size(); pendingOperationAcks++; removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex); PlaybackInfo playbackInfo = @@ -1094,17 +1104,14 @@ import java.util.concurrent.TimeoutException; /* seekProcessed= */ false); } - private List removeMediaSourceHolders( - int fromIndex, int toIndexExclusive) { - List removed = new ArrayList<>(); + private void removeMediaSourceHolders(int fromIndex, int toIndexExclusive) { for (int i = toIndexExclusive - 1; i >= fromIndex; i--) { - removed.add(mediaSourceHolders.remove(i)); + mediaSourceHolderSnapshots.remove(i); } shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive); - if (mediaSourceHolders.isEmpty()) { + if (mediaSourceHolderSnapshots.isEmpty()) { hasAdsMediaSource = false; } - return removed; } /** @@ -1123,7 +1130,7 @@ import java.util.concurrent.TimeoutException; throw new IllegalStateException(); } int sizeAfterModification = - mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolders.size()); + mediaSources.size() + (mediaSourceReplacement ? 0 : mediaSourceHolderSnapshots.size()); for (int i = 0; i < mediaSources.size(); i++) { MediaSource mediaSource = checkNotNull(mediaSources.get(i)); if (mediaSource instanceof AdsMediaSource) { @@ -1139,9 +1146,9 @@ import java.util.concurrent.TimeoutException; private PlaybackInfo maskTimeline() { return playbackInfo.copyWithTimeline( - mediaSourceHolders.isEmpty() + mediaSourceHolderSnapshots.isEmpty() ? Timeline.EMPTY - : new MediaSourceList.PlaylistTimeline(mediaSourceHolders, shuffleOrder)); + : new PlaylistTimeline(mediaSourceHolderSnapshots, shuffleOrder)); } private PlaybackInfo maskTimelineAndWindowIndex( @@ -1395,4 +1402,26 @@ import java.util.concurrent.TimeoutException; listenerHolder.invoke(listenerInvocation); } } + + private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder { + + private final Object uid; + + private Timeline timeline; + + public MediaSourceHolderSnapshot(Object uid, Timeline timeline) { + this.uid = uid; + this.timeline = timeline; + } + + @Override + public Object getUid() { + return uid; + } + + @Override + public Timeline getTimeline() { + return timeline; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b02df9f050..2afb87758c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -596,7 +596,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (mediaSourceListUpdateMessage.windowIndex != C.INDEX_UNSET) { pendingInitialSeekPosition = new SeekPosition( - new MediaSourceList.PlaylistTimeline( + new PlaylistTimeline( mediaSourceListUpdateMessage.mediaSourceHolders, mediaSourceListUpdateMessage.shuffleOrder), mediaSourceListUpdateMessage.windowIndex, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceInfoHolder.java new file mode 100644 index 0000000000..f8624995ad --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceInfoHolder.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import com.google.android.exoplayer2.source.MediaSource; + +/** A holder of information about a {@link MediaSource}. */ +/* package */ interface MediaSourceInfoHolder { + + /** Returns the uid of the {@link MediaSourceList.MediaSourceHolder}. */ + Object getUid(); + + /** Returns the timeline. */ + Timeline getTimeline(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java index 518f7bc6cb..3456909460 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaSourceList.java @@ -35,8 +35,6 @@ import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -234,7 +232,7 @@ import java.util.Set; int newEndIndex = newFromIndex + (toIndex - fromIndex) - 1; int endIndex = Math.max(newEndIndex, toIndex - 1); int windowOffset = mediaSourceHolders.get(startIndex).firstWindowIndexInChild; - moveMediaSourceHolders(mediaSourceHolders, fromIndex, toIndex, newFromIndex); + Util.moveItems(mediaSourceHolders, fromIndex, toIndex, newFromIndex); for (int i = startIndex; i <= endIndex; i++) { MediaSourceHolder holder = mediaSourceHolders.get(i); holder.firstWindowIndexInChild = windowOffset; @@ -472,18 +470,8 @@ import java.util.Set; return PlaylistTimeline.getConcatenatedUid(holder.uid, childPeriodUid); } - /* package */ static void moveMediaSourceHolders( - List 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 { + /* package */ static final class MediaSourceHolder implements MediaSourceInfoHolder { public final MaskingMediaSource mediaSource; public final Object uid; @@ -503,88 +491,15 @@ import java.util.Set; 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 childIndexByUid; - - public PlaylistTimeline( - Collection 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 + public Object getUid() { + return uid; } @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; + public Timeline getTimeline() { + return mediaSource.getTimeline(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaylistTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaylistTimeline.java new file mode 100644 index 0000000000..3b93041348 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaylistTimeline.java @@ -0,0 +1,113 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import com.google.android.exoplayer2.source.ShuffleOrder; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** Timeline exposing concatenated timelines of playlist media sources. */ +/* package */ 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 childIndexByUid; + + /** Creates an instance. */ + public PlaylistTimeline( + Collection mediaSourceInfoHolders, + ShuffleOrder shuffleOrder) { + super(/* isAtomic= */ false, shuffleOrder); + int childCount = mediaSourceInfoHolders.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 (MediaSourceInfoHolder mediaSourceInfoHolder : mediaSourceInfoHolders) { + timelines[index] = mediaSourceInfoHolder.getTimeline(); + firstWindowInChildIndices[index] = windowCount; + firstPeriodInChildIndices[index] = periodCount; + windowCount += timelines[index].getWindowCount(); + periodCount += timelines[index].getPeriodCount(); + uids[index] = mediaSourceInfoHolder.getUid(); + childIndexByUid.put(uids[index], index++); + } + this.windowCount = windowCount; + this.periodCount = periodCount; + } + + /** Returns the child timelines. */ + /* package */ List getChildTimelines() { + return Arrays.asList(timelines); + } + + @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; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 01ad02017d..13aa70fa9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -72,7 +72,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } /** Returns the {@link Timeline}. */ - public synchronized Timeline getTimeline() { + public Timeline getTimeline() { return timeline; } @@ -150,7 +150,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { } @Override - protected synchronized void onChildSourceInfoRefreshed( + protected void onChildSourceInfoRefreshed( Void id, MediaSource mediaSource, Timeline newTimeline) { @Nullable MediaPeriodId idForMaskingPeriodPreparation = null; if (isPrepared) {