Prevent parallel timeline access in MaskingMediaSource

The list of MediaSourceHolder in ExoPlayerImpl is only maintained to be able to create a PlaylistTimeline for masking. By keeping only the id and a snapshot of the timeline of the MediaSourceHolder in ExoPlayerImpl, parallel access is prevented and we still have sufficient information to create the masking timeline.

PiperOrigin-RevId: 319003837
This commit is contained in:
bachinger 2020-06-30 13:37:54 +01:00 committed by Oliver Woodman
parent a3bbcf3395
commit 20820800f3
7 changed files with 227 additions and 123 deletions

View file

@ -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 <T extends Object> void moveItems(
List<T> items, int fromIndex, int toIndex, int newFromIndex) {
ArrayDeque<T> 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 {

View file

@ -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<ListenerHolder> listeners;
private final Timeline.Period period;
private final ArrayDeque<Runnable> pendingListenerNotifications;
private final List<MediaSourceList.MediaSourceHolder> mediaSourceHolders;
private final List<MediaSourceHolderSnapshot> 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<MediaItem> 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<MediaSource> 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<Timeline> timelines = ((PlaylistTimeline) newTimeline).getChildTimelines();
checkState(timelines.size() == mediaSourceHolderSnapshots.size());
for (int i = 0; i < timelines.size(); i++) {
mediaSourceHolderSnapshots.get(i).timeline = timelines.get(i);
}
}
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<MediaSourceList.MediaSourceHolder> 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<MediaSourceList.MediaSourceHolder> removeMediaSourceHolders(
int fromIndex, int toIndexExclusive) {
List<MediaSourceList.MediaSourceHolder> 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;
}
}
}

View file

@ -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,

View file

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

View file

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

View file

@ -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<Object, Integer> childIndexByUid;
/** Creates an instance. */
public PlaylistTimeline(
Collection<? extends MediaSourceInfoHolder> 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<Timeline> 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;
}
}

View file

@ -72,7 +72,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
/** Returns the {@link Timeline}. */
public synchronized Timeline getTimeline() {
public Timeline getTimeline() {
return timeline;
}
@ -150,7 +150,7 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
}
@Override
protected synchronized void onChildSourceInfoRefreshed(
protected void onChildSourceInfoRefreshed(
Void id, MediaSource mediaSource, Timeline newTimeline) {
@Nullable MediaPeriodId idForMaskingPeriodPreparation = null;
if (isPrepared) {