diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 390deac933..8f15fb8789 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -45,8 +45,11 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.CopyOnWriteArrayList; /** * {@link Player} implementation that communicates with a Cast receiver app. @@ -86,8 +89,10 @@ public final class CastPlayer extends BasePlayer { private final StatusListener statusListener; private final SeekResultCallback seekResultCallback; - // Listeners. - private final CopyOnWriteArraySet listeners; + // Listeners and notification. + private final CopyOnWriteArrayList listeners; + private final ArrayList notificationsBatch; + private final ArrayDeque ongoingNotificationsTasks; private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. @@ -113,7 +118,9 @@ public final class CastPlayer extends BasePlayer { period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); - listeners = new CopyOnWriteArraySet<>(); + listeners = new CopyOnWriteArrayList<>(); + notificationsBatch = new ArrayList<>(); + ongoingNotificationsTasks = new ArrayDeque<>(); SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); @@ -296,12 +303,17 @@ public final class CastPlayer extends BasePlayer { @Override public void addListener(EventListener listener) { - listeners.add(listener); + listeners.addIfAbsent(new ListenerHolder(listener)); } @Override public void removeListener(EventListener listener) { - listeners.remove(listener); + for (ListenerHolder listenerHolder : listeners) { + if (listenerHolder.listener.equals(listener)) { + listenerHolder.release(); + listeners.remove(listenerHolder); + } + } } @Override @@ -348,14 +360,13 @@ public final class CastPlayer extends BasePlayer { pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK))); } else if (pendingSeekCount == 0) { - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); } + flushNotifications(); } @Override @@ -531,30 +542,31 @@ public final class CastPlayer extends BasePlayer { || this.playWhenReady != playWhenReady) { this.playbackState = playbackState; this.playWhenReady = playWhenReady; - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(this.playWhenReady, this.playbackState); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onPlayerStateChanged(this.playWhenReady, this.playbackState))); } @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); if (this.repeatMode != repeatMode) { this.repeatMode = repeatMode; - for (EventListener listener : listeners) { - listener.onRepeatModeChanged(repeatMode); - } + notificationsBatch.add( + new ListenerNotificationTask(listener -> listener.onRepeatModeChanged(this.repeatMode))); } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; - for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION))); } if (updateTracksAndSelections()) { - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection))); } maybeUpdateTimelineAndNotify(); + flushNotifications(); } private void maybeUpdateTimelineAndNotify() { @@ -562,9 +574,10 @@ public final class CastPlayer extends BasePlayer { @Player.TimelineChangeReason int reason = waitingForInitialTimeline ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; waitingForInitialTimeline = false; - for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null, reason); - } + notificationsBatch.add( + new ListenerNotificationTask( + listener -> + listener.onTimelineChanged(currentTimeline, /* manifest= */ null, reason))); } } @@ -827,7 +840,23 @@ public final class CastPlayer extends BasePlayer { } - // Result callbacks hooks. + // Internal methods. + + private void flushNotifications() { + boolean recursiveNotification = !ongoingNotificationsTasks.isEmpty(); + ongoingNotificationsTasks.addAll(notificationsBatch); + notificationsBatch.clear(); + if (recursiveNotification) { + // This will be handled once the current notification task is finished. + return; + } + while (!ongoingNotificationsTasks.isEmpty()) { + ongoingNotificationsTasks.peekFirst().execute(); + ongoingNotificationsTasks.removeFirst(); + } + } + + // Internal classes. private final class SeekResultCallback implements ResultCallback { @@ -841,9 +870,25 @@ public final class CastPlayer extends BasePlayer { if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - for (EventListener listener : listeners) { - listener.onSeekProcessed(); - } + notificationsBatch.add(new ListenerNotificationTask(EventListener::onSeekProcessed)); + flushNotifications(); + } + } + } + + private final class ListenerNotificationTask { + + private final Iterator listenersSnapshot; + private final ListenerInvocation listenerInvocation; + + private ListenerNotificationTask(ListenerInvocation listenerInvocation) { + this.listenersSnapshot = listeners.iterator(); + this.listenerInvocation = listenerInvocation; + } + + public void execute() { + while (listenersSnapshot.hasNext()) { + listenersSnapshot.next().invoke(listenerInvocation); } } }