Commit bd631a6c by tonihei Committed by Oliver Woodman

Add onEvents callback.

This callback allows listeners to know when all simultanous changes
have been handled and the values reported through callbacks are
again completely consistent with value obtained from Player
getter calls.

PiperOrigin-RevId: 343476639
parent a6b53d24
......@@ -26,6 +26,11 @@
* Fix issue that could cause playback to freeze when selecting tracks, if
extension audio renderers are being used
([#8203](https://github.com/google/ExoPlayer/issues/8203)).
* Add `onEvents` callback to `Player.EventListener` and
`AnalyticsListener` to notify when all simultaneous state changes have
been handled and the values reported through callbacks are again
completely consistent with the values obtained from the `Player`
getters.
* Track selection:
* Add option to specify multiple preferred audio or text languages.
* Forward `Timeline` and `MediaPeriodId` to `TrackSelection.Factory`.
......
......@@ -96,7 +96,7 @@ public final class CastPlayer extends BasePlayer {
private final SeekResultCallback seekResultCallback;
// Listeners and notification.
private final ListenerSet<Player.EventListener> listeners;
private final ListenerSet<Player.EventListener, Player.Events> listeners;
@Nullable private SessionAvailabilityListener sessionAvailabilityListener;
// Internal state.
......@@ -135,7 +135,11 @@ public final class CastPlayer extends BasePlayer {
period = new Timeline.Period();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
listeners = new ListenerSet<>();
listeners =
new ListenerSet<>(
Looper.getMainLooper(),
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(/* player= */ this, eventFlags));
playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
......@@ -445,9 +449,11 @@ public final class CastPlayer extends BasePlayer {
pendingSeekCount++;
pendingSeekWindowIndex = windowIndex;
pendingSeekPositionMs = positionMs;
listeners.queueEvent(listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK));
} else if (pendingSeekCount == 0) {
listeners.queueEvent(EventListener::onSeekProcessed);
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
listeners.flushEvents();
}
......@@ -647,7 +653,8 @@ public final class CastPlayer extends BasePlayer {
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
boolean isPlaying = playbackState == Player.STATE_READY && playWhenReady.value;
if (wasPlaying != isPlaying) {
listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying));
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
}
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updateTimelineAndNotifyIfChanged();
......@@ -664,10 +671,12 @@ public final class CastPlayer extends BasePlayer {
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex;
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION));
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
}
listeners.flushEvents();
......@@ -710,6 +719,7 @@ public final class CastPlayer extends BasePlayer {
// TODO: Differentiate TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED and
// TIMELINE_CHANGE_REASON_SOURCE_UPDATE [see internal: b/65152553].
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
listener ->
listener.onTimelineChanged(
currentTimeline, Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
......@@ -831,7 +841,8 @@ public final class CastPlayer extends BasePlayer {
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
if (this.repeatMode.value != repeatMode) {
this.repeatMode.value = repeatMode;
listeners.queueEvent(listener -> listener.onRepeatModeChanged(repeatMode));
listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
}
}
......@@ -846,15 +857,18 @@ public final class CastPlayer extends BasePlayer {
this.playbackState = playbackState;
this.playWhenReady.value = playWhenReady;
listeners.queueEvent(
listener -> {
listener.onPlayerStateChanged(playWhenReady, playbackState);
if (playbackStateChanged) {
listener.onPlaybackStateChanged(playbackState);
}
if (playWhenReadyChanged) {
listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason);
}
});
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
if (playbackStateChanged) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> listener.onPlaybackStateChanged(playbackState));
}
if (playWhenReadyChanged) {
listeners.queueEvent(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
listener -> listener.onPlayWhenReadyChanged(playWhenReady, playWhenReadyChangeReason));
}
}
}
......@@ -1072,7 +1086,7 @@ public final class CastPlayer extends BasePlayer {
if (--pendingSeekCount == 0) {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
listeners.sendEvent(EventListener::onSeekProcessed);
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
}
}
......
......@@ -454,7 +454,8 @@ public interface ExoPlayer extends Player {
releaseTimeoutMs,
pauseAtEndOfMediaItems,
clock,
looper);
looper,
/* wrappingPlayer= */ null);
player.experimentalSetForegroundModeTimeoutMs(setForegroundModeTimeoutMs);
if (!throwWhenStuckBuffering) {
......
......@@ -260,6 +260,7 @@ public final class ExoPlayerFactory {
ExoPlayer.DEFAULT_RELEASE_TIMEOUT_MS,
/* pauseAtEndOfMediaItems= */ false,
Clock.DEFAULT,
applicationLooper);
applicationLooper,
/* wrappingPlayer= */ null);
}
}
......@@ -73,7 +73,7 @@ import java.util.concurrent.TimeoutException;
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
private final ExoPlayerImplInternal internalPlayer;
private final Handler internalPlayerHandler;
private final ListenerSet<Player.EventListener> listeners;
private final ListenerSet<Player.EventListener, Player.Events> listeners;
private final Timeline.Period period;
private final List<MediaSourceHolderSnapshot> mediaSourceHolderSnapshots;
private final boolean useLazyPreparation;
......@@ -121,6 +121,8 @@ import java.util.concurrent.TimeoutException;
* @param clock The {@link Clock}.
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
* which is used to call listeners on.
* @param wrappingPlayer The {@link Player} wrapping this one if applicable. This player instance
* should be used for all externally visible callbacks.
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(
......@@ -136,7 +138,8 @@ import java.util.concurrent.TimeoutException;
long releaseTimeoutMs,
boolean pauseAtEndOfMediaItems,
Clock clock,
Looper applicationLooper) {
Looper applicationLooper,
@Nullable Player wrappingPlayer) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
checkState(renderers.length > 0);
......@@ -150,7 +153,12 @@ import java.util.concurrent.TimeoutException;
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
this.applicationLooper = applicationLooper;
repeatMode = Player.REPEAT_MODE_OFF;
listeners = new ListenerSet<>();
Player playerForListeners = wrappingPlayer != null ? wrappingPlayer : this;
listeners =
new ListenerSet<>(
applicationLooper,
Player.Events::new,
(listener, eventFlags) -> listener.onEvents(playerForListeners, eventFlags));
mediaSourceHolderSnapshots = new ArrayList<>();
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
emptyTrackSelectorResult =
......@@ -166,7 +174,7 @@ import java.util.concurrent.TimeoutException;
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
if (analyticsCollector != null) {
analyticsCollector.setPlayer(this);
analyticsCollector.setPlayer(playerForListeners, applicationLooper);
addListener(analyticsCollector);
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
}
......@@ -565,7 +573,8 @@ import java.util.concurrent.TimeoutException;
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode);
listeners.sendEvent(listener -> listener.onRepeatModeChanged(repeatMode));
listeners.sendEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
}
}
......@@ -579,7 +588,9 @@ import java.util.concurrent.TimeoutException;
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
listeners.sendEvent(listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
listeners.sendEvent(
Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
}
}
......@@ -729,6 +740,7 @@ import java.util.concurrent.TimeoutException;
+ ExoPlayerLibraryInfo.registeredModules() + "]");
if (!internalPlayer.release()) {
listeners.sendEvent(
Player.EVENT_PLAYER_ERROR,
listener ->
listener.onPlayerError(
ExoPlaybackException.createForTimeout(
......@@ -974,10 +986,12 @@ import java.util.concurrent.TimeoutException;
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
if (!previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline)) {
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason));
}
if (positionDiscontinuity) {
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason));
}
if (mediaItemTransitioned) {
......@@ -991,39 +1005,49 @@ import java.util.concurrent.TimeoutException;
mediaItem = null;
}
listeners.queueEvent(
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener -> listener.onMediaItemTransition(mediaItem, mediaItemTransitionReason));
}
if (previousPlaybackInfo.playbackError != newPlaybackInfo.playbackError
&& newPlaybackInfo.playbackError != null) {
listeners.queueEvent(listener -> listener.onPlayerError(newPlaybackInfo.playbackError));
listeners.queueEvent(
Player.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerError(newPlaybackInfo.playbackError));
}
if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) {
trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info);
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener ->
listener.onTracksChanged(
newPlaybackInfo.trackGroups, newPlaybackInfo.trackSelectorResult.selections));
}
if (!previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
listeners.queueEvent(
Player.EVENT_STATIC_METADATA_CHANGED,
listener -> listener.onStaticMetadataChanged(newPlaybackInfo.staticMetadata));
}
if (previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading) {
listeners.queueEvent(listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading));
listeners.queueEvent(
Player.EVENT_IS_LOADING_CHANGED,
listener -> listener.onIsLoadingChanged(newPlaybackInfo.isLoading));
}
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState
|| previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onPlayerStateChanged(
newPlaybackInfo.playWhenReady, newPlaybackInfo.playbackState));
}
if (previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> listener.onPlaybackStateChanged(newPlaybackInfo.playbackState));
}
if (previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady) {
listeners.queueEvent(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
listener ->
listener.onPlayWhenReadyChanged(
newPlaybackInfo.playWhenReady, playWhenReadyChangeReason));
......@@ -1031,28 +1055,34 @@ import java.util.concurrent.TimeoutException;
if (previousPlaybackInfo.playbackSuppressionReason
!= newPlaybackInfo.playbackSuppressionReason) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
listener ->
listener.onPlaybackSuppressionReasonChanged(
newPlaybackInfo.playbackSuppressionReason));
}
if (isPlaying(previousPlaybackInfo) != isPlaying(newPlaybackInfo)) {
listeners.queueEvent(listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo)));
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED,
listener -> listener.onIsPlayingChanged(isPlaying(newPlaybackInfo)));
}
if (!previousPlaybackInfo.playbackParameters.equals(newPlaybackInfo.playbackParameters)) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED,
listener -> listener.onPlaybackParametersChanged(newPlaybackInfo.playbackParameters));
}
if (seekProcessed) {
listeners.queueEvent(EventListener::onSeekProcessed);
listeners.queueEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
}
if (previousPlaybackInfo.offloadSchedulingEnabled != newPlaybackInfo.offloadSchedulingEnabled) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onExperimentalOffloadSchedulingEnabledChanged(
newPlaybackInfo.offloadSchedulingEnabled));
}
if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onExperimentalSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload));
}
......
......@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.util.MutableFlags;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer;
import com.google.android.exoplayer2.video.VideoFrameMetadataListener;
......@@ -421,8 +422,13 @@ public interface Player {
}
/**
* Listener of changes in player state. All methods have no-op default implementations to allow
* selective overrides.
* Listener of changes in player state.
*
* <p>All methods have no-op default implementations to allow selective overrides.
*
* <p>Listeners can choose to implement individual events (e.g. {@link
* #onIsPlayingChanged(boolean)}) or {@link #onEvents(Player, Events)}, which is called after one
* or more events occurred together.
*/
interface EventListener {
......@@ -621,12 +627,41 @@ public interface Player {
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
default void onExperimentalOffloadSchedulingEnabledChanged(boolean offloadSchedulingEnabled) {}
/**
* Called when the player has started or finished sleeping for offload.
*
* <p>This method is experimental, and will be renamed or removed in a future release.
*/
default void onExperimentalSleepingForOffloadChanged(boolean sleepingForOffload) {}
/**
* Called when one or more player states changed.
*
* <p>State changes and events that happen within one {@link Looper} message queue iteration are
* reported together and only after all individual callbacks were triggered.
*
* <p>Listeners should prefer this method over individual callbacks in the following cases:
*
* <ul>
* <li>They intend to use multiple state values together (e.g. using {@link
* #getCurrentWindowIndex()} to query in {@link #getCurrentTimeline()}).
* <li>The same logic should be triggered for multiple events (e.g. when updating a UI for
* both {@link #onPlaybackStateChanged(int)} and {@link #onPlayWhenReadyChanged(boolean,
* int)}).
* <li>They need access to the {@link Player} object to trigger further events (e.g. to call
* {@link Player#seekTo(long)} after a {@link #onMediaItemTransition(MediaItem, int)}).
* <li>They are interested in events that logically happened together (e.g {@link
* #onPlaybackStateChanged(int)} to {@link #STATE_BUFFERING} because of {@link
* #onMediaItemTransition(MediaItem, int)}).
* </ul>
*
* @param player The {@link Player} whose state changed. Use the getters to obtain the latest
* states.
* @param events The {@link Events} that happened in this iteration, indicating which player
* states changed.
*/
default void onEvents(Player player, Events events) {}
}
/**
......@@ -663,6 +698,37 @@ public interface Player {
}
}
/** A set of {@link EventFlags}. */
final class Events extends MutableFlags {
/**
* Returns whether the given event occurred.
*
* @param event The {@link EventFlags event}.
* @return Whether the event occurred.
*/
@Override
public boolean contains(@EventFlags int event) {
// Overridden to add IntDef compiler enforcement and new JavaDoc.
return super.contains(event);
}
/**
* Returns the {@link EventFlags event} at the given index.
*
* <p>Although index-based access is possible, it doesn't imply a particular order of these
* events.
*
* @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive).
* @return The {@link EventFlags event} at the given index.
*/
@Override
@EventFlags
public int get(int index) {
// Overridden to add IntDef compiler enforcement and new JavaDoc.
return super.get(index);
}
}
/**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}.
......@@ -802,7 +868,11 @@ public interface Player {
/** Timeline changed as a result of a dynamic update introduced by the played media. */
int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1;
/** Reasons for media item transitions. */
/**
* Reasons for media item transitions. One of {@link #MEDIA_ITEM_TRANSITION_REASON_REPEAT}, {@link
* #MEDIA_ITEM_TRANSITION_REASON_AUTO}, {@link #MEDIA_ITEM_TRANSITION_REASON_SEEK} or {@link
* #MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
......@@ -825,6 +895,59 @@ public interface Player {
*/
int MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED = 3;
/**
* Events that can be reported via {@link EventListener#onEvents(Player, Events)}.
*
* <p>One of the {@link Player}{@code .EVENT_*} flags.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
EVENT_TIMELINE_CHANGED,
EVENT_MEDIA_ITEM_TRANSITION,
EVENT_TRACKS_CHANGED,
EVENT_STATIC_METADATA_CHANGED,
EVENT_IS_LOADING_CHANGED,
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_PLAY_WHEN_READY_CHANGED,
EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
EVENT_IS_PLAYING_CHANGED,
EVENT_REPEAT_MODE_CHANGED,
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
EVENT_PLAYER_ERROR,
EVENT_POSITION_DISCONTINUITY,
EVENT_PLAYBACK_PARAMETERS_CHANGED
})
@interface EventFlags {}
/** {@link #getCurrentTimeline()} changed. */
int EVENT_TIMELINE_CHANGED = 0;
/** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */
int EVENT_MEDIA_ITEM_TRANSITION = 1;
/** {@link #getCurrentTrackGroups()} or {@link #getCurrentTrackSelections()} changed. */
int EVENT_TRACKS_CHANGED = 2;
/** {@link #getCurrentStaticMetadata()} changed. */
int EVENT_STATIC_METADATA_CHANGED = 3;
/** {@link #isLoading()} ()} changed. */
int EVENT_IS_LOADING_CHANGED = 4;
/** {@link #getPlaybackState()} changed. */
int EVENT_PLAYBACK_STATE_CHANGED = 5;
/** {@link #getPlayWhenReady()} changed. */
int EVENT_PLAY_WHEN_READY_CHANGED = 6;
/** {@link #getPlaybackSuppressionReason()} changed. */
int EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED = 7;
/** {@link #isPlaying()} changed. */
int EVENT_IS_PLAYING_CHANGED = 8;
/** {@link #getRepeatMode()} changed. */
int EVENT_REPEAT_MODE_CHANGED = 9;
/** {@link #getShuffleModeEnabled()} changed. */
int EVENT_SHUFFLE_MODE_ENABLED_CHANGED = 10;
/** {@link #getPlayerError()} changed. */
int EVENT_PLAYER_ERROR = 11;
/** A position discontinuity occurred. See {@link EventListener#onPositionDiscontinuity(int)}. */
int EVENT_POSITION_DISCONTINUITY = 12;
/** {@link #getPlaybackParameters()} changed. */
int EVENT_PLAYBACK_PARAMETERS_CHANGED = 13;
/** Returns the component of this player for audio output, or null if audio is not supported. */
@Nullable
AudioComponent getAudioComponent();
......
......@@ -701,7 +701,8 @@ public class SimpleExoPlayer extends BasePlayer
builder.releaseTimeoutMs,
builder.pauseAtEndOfMediaItems,
builder.clock,
builder.looper);
builder.looper,
/* wrappingPlayer= */ this);
player.addListener(componentListener);
videoDebugListeners.add(analyticsCollector);
videoListeners.add(analyticsCollector);
......
......@@ -15,7 +15,13 @@
*/
package com.google.android.exoplayer2.util;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.common.base.Supplier;
import java.util.ArrayDeque;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
......@@ -30,8 +36,9 @@ import javax.annotation.Nonnull;
* was enqueued and haven't been removed since.
*
* @param <T> The listener type.
* @param <E> The {@link MutableFlags} type used to indicate which events occurred.
*/
public final class ListenerSet<T> {
public final class ListenerSet<T, E extends MutableFlags> {
/**
* An event sent to a listener.
......@@ -44,17 +51,84 @@ public final class ListenerSet<T> {
void invoke(T listener);
}
private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
/**
* An event sent to a listener when all other events sent during one {@link Looper} message queue
* iteration were handled by the listener.
*
* @param <T> The listener type.
* @param <E> The {@link MutableFlags} type used to indicate which events occurred.
*/
public interface IterationFinishedEvent<T, E extends MutableFlags> {
/**
* Invokes the iteration finished event.
*
* @param listener The listener to invoke the event on.
* @param eventFlags The combined event flags of all events sent in this iteration.
*/
void invoke(T listener, E eventFlags);
}
private static final int MSG_ITERATION_FINISHED = 0;
private final Handler iterationFinishedHandler;
private final Supplier<E> eventFlagsSupplier;
private final IterationFinishedEvent<T, E> iterationFinishedEvent;
private final CopyOnWriteArraySet<ListenerHolder<T, E>> listeners;
private final ArrayDeque<Runnable> flushingEvents;
private final ArrayDeque<Runnable> queuedEvents;
private boolean released;
/** Creates the listener set. */
public ListenerSet() {
listeners = new CopyOnWriteArraySet<>();
/**
* Creates a new listener set.
*
* @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used
* to call all other methods of this class.
* @param eventFlagsSupplier A {@link Supplier} for new instances of {@link E the event flags
* type}.
* @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
* during one {@link Looper} message queue iteration were handled by the listeners.
*/
public ListenerSet(
Looper looper,
Supplier<E> eventFlagsSupplier,
IterationFinishedEvent<T, E> iterationFinishedEvent) {
this(
/* listeners= */ new CopyOnWriteArraySet<>(),
looper,
eventFlagsSupplier,
iterationFinishedEvent);
}
private ListenerSet(
CopyOnWriteArraySet<ListenerHolder<T, E>> listeners,
Looper looper,
Supplier<E> eventFlagsSupplier,
IterationFinishedEvent<T, E> iterationFinishedEvent) {
this.listeners = listeners;
this.eventFlagsSupplier = eventFlagsSupplier;
this.iterationFinishedEvent = iterationFinishedEvent;
flushingEvents = new ArrayDeque<>();
queuedEvents = new ArrayDeque<>();
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("methodref.receiver.bound.invalid")
Handler handler = Util.createHandler(looper, this::handleIterationFinished);
iterationFinishedHandler = handler;
}
/**
* Copies the listener set.
*
* @param looper The new {@link Looper} for the copied listener set.
* @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
* sent during one {@link Looper} message queue iteration were handled by the listeners.
* @return The copied listener set.
*/
@CheckResult
public ListenerSet<T, E> copy(
Looper looper, IterationFinishedEvent<T, E> iterationFinishedEvent) {
return new ListenerSet<>(listeners, looper, eventFlagsSupplier, iterationFinishedEvent);
}
/**
......@@ -69,7 +143,7 @@ public final class ListenerSet<T> {
return;
}
Assertions.checkNotNull(listener);
listeners.add(new ListenerHolder<T>(listener));
listeners.add(new ListenerHolder<>(listener, eventFlagsSupplier));
}
/**
......@@ -80,7 +154,7 @@ public final class ListenerSet<T> {
* @param listener The listener to be removed.
*/
public void remove(T listener) {
for (ListenerHolder<T> listenerHolder : listeners) {
for (ListenerHolder<T, E> listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release();
listeners.remove(listenerHolder);
......@@ -91,20 +165,29 @@ public final class ListenerSet<T> {
/**
* Adds an event that is sent to the listeners when {@link #flushEvents} is called.
*
* @param eventFlag An integer indicating the type of the event, or {@link C#INDEX_UNSET} to not
* report this event with a flag.
* @param event The event.
*/
public void queueEvent(Event<T> event) {
CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
public void queueEvent(int eventFlag, Event<T> event) {
CopyOnWriteArraySet<ListenerHolder<T, E>> listenerSnapshot =
new CopyOnWriteArraySet<>(listeners);
queuedEvents.add(
() -> {
for (ListenerHolder<T> holder : listenerSnapshot) {
holder.invoke(event);
for (ListenerHolder<T, E> holder : listenerSnapshot) {
holder.invoke(eventFlag, event);
}
});
}
/** Notifies listeners of events previously enqueued with {@link #queueEvent(Event)}. */
/** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */
public void flushEvents() {
if (queuedEvents.isEmpty()) {
return;
}
if (!iterationFinishedHandler.hasMessages(MSG_ITERATION_FINISHED)) {
iterationFinishedHandler.obtainMessage(MSG_ITERATION_FINISHED).sendToTarget();
}
boolean recursiveFlushInProgress = !flushingEvents.isEmpty();
flushingEvents.addAll(queuedEvents);
queuedEvents.clear();
......@@ -119,13 +202,15 @@ public final class ListenerSet<T> {
}
/**
* {@link #queueEvent(Event) Queues} a single event and immediately {@link #flushEvents() flushes}
* the event queue to notify all listeners.
* {@link #queueEvent(int, Event) Queues} a single event and immediately {@link #flushEvents()
* flushes} the event queue to notify all listeners.
*
* @param eventFlag An integer flag indicating the type of the event, or {@link C#INDEX_UNSET} to
* not report this event with a flag.
* @param event The event.
*/
public void sendEvent(Event<T> event) {
queueEvent(event);
public void sendEvent(int eventFlag, Event<T> event) {
queueEvent(eventFlag, event);
flushEvents();
}
......@@ -135,30 +220,59 @@ public final class ListenerSet<T> {
* <p>This will ensure no events are sent to any listener after this method has been called.
*/
public void release() {
for (ListenerHolder<T> listenerHolder : listeners) {
for (ListenerHolder<T, E> listenerHolder : listeners) {
listenerHolder.release();
}
listeners.clear();
released = true;
}
private static final class ListenerHolder<T> {
private boolean handleIterationFinished(Message message) {
for (ListenerHolder<T, E> holder : listeners) {
holder.iterationFinished(eventFlagsSupplier, iterationFinishedEvent);
if (iterationFinishedHandler.hasMessages(MSG_ITERATION_FINISHED)) {
// The invocation above triggered new events (and thus scheduled a new message). We need to
// stop here because this new message will take care of informing every listener about the
// new update (including the ones already called here).
break;
}
}
return true;
}
private static final class ListenerHolder<T, E extends MutableFlags> {
@Nonnull public final T listener;
private E eventsFlags;
private boolean released;
public ListenerHolder(@Nonnull T listener) {
public ListenerHolder(@Nonnull T listener, Supplier<E> eventFlagSupplier) {
this.listener = listener;
this.eventsFlags = eventFlagSupplier.get();
}
public void release() {
released = true;
}
public void invoke(Event<T> event) {
public void invoke(int eventFlag, Event<T> event) {
if (!released) {
event.invoke(listener);
if (eventFlag != C.INDEX_UNSET) {
eventsFlags.add(eventFlag);
}
}
}
public void iterationFinished(
Supplier<E> eventFlagSupplier, IterationFinishedEvent<T, E> event) {
if (!released) {
// Reset flags before invoking the listener to ensure we keep all new flags that are set by
// recursive events triggered from this callback.
E flagToNotify = eventsFlags;
eventsFlags = eventFlagSupplier.get();
event.invoke(listener, flagToNotify);
}
}
......@@ -170,7 +284,7 @@ public final class ListenerSet<T> {
if (other == null || getClass() != other.getClass()) {
return false;
}
return listener.equals(((ListenerHolder<?>) other).listener);
return listener.equals(((ListenerHolder<?, ?>) other).listener);
}
@Override
......
/*
* Copyright (C) 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.util;
import android.util.SparseBooleanArray;
import androidx.annotation.Nullable;
/**
* A set of integer flags.
*
* <p>Intended for usages where the number of flags may exceed 32 and can no longer be represented
* by an IntDef.
*/
public class MutableFlags {
private final SparseBooleanArray flags;
/** Creates the set of flags. */
public MutableFlags() {
flags = new SparseBooleanArray();
}
/** Clears all previously set flags. */
public void clear() {
flags.clear();
}
/**
* Adds a flag to the set.
*
* @param flag The flag to add.
*/
public void add(int flag) {
flags.append(flag, /* value= */ true);
}
/**
* Returns whether the set contains the given flag.
*
* @param flag The flag.
* @return Whether the set contains the flag.
*/
public boolean contains(int flag) {
return flags.get(flag);
}
/** Returns the number of flags in this set. */
public int size() {
return flags.size();
}
/**
* Returns the flag at the given index.
*
* @param index The index. Must be between 0 (inclusive) and {@link #size()} (exclusive).
* @return The flag at the given index.
* @throws IllegalArgumentException If index is outside the allowed range.
*/
public int get(int index) {
Assertions.checkArgument(index >= 0 && index < size());
return flags.keyAt(index);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MutableFlags)) {
return false;
}
MutableFlags that = (MutableFlags) o;
return flags.equals(that.flags);
}
@Override
public int hashCode() {
return flags.hashCode();
}
}
......@@ -28,9 +28,11 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
......@@ -57,6 +59,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.BinaryFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.robolectric.TestPlayerRunHelper;
import com.google.android.exoplayer2.source.ClippingMediaSource;
......@@ -110,6 +113,7 @@ import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import java.io.IOException;
......@@ -132,6 +136,7 @@ import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.robolectric.shadows.ShadowAudioManager;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link ExoPlayer}. */
@RunWith(AndroidJUnit4.class)
......@@ -8843,6 +8848,96 @@ public final class ExoPlayerTest {
assertThat(liveOffsetAtEnd).isIn(Range.closed(11_900L, 12_100L));
}
@Test
public void onStateChangedFlags_correspondToListenerCalls() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
EventListener listener = mock(EventListener.class);
player.addListener(listener);
Format formatWithStaticMetadata =
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_H264)
.setMetadata(new Metadata(new BinaryFrame(/* id= */ "", /* data= */ new byte[0])))
.build();
// Set multiple values together.
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), formatWithStaticMetadata));
player.seekTo(2_000);
player.setPlaybackParameters(new PlaybackParameters(/* speed= */ 2.0f));
ShadowLooper.runMainLooperToNextTask();
verify(listener).onTimelineChanged(any(), anyInt());
verify(listener).onMediaItemTransition(any(), anyInt());
verify(listener).onPositionDiscontinuity(anyInt());
verify(listener).onPlaybackParametersChanged(any());
ArgumentCaptor<Player.Events> eventCaptor = ArgumentCaptor.forClass(Player.Events.class);
verify(listener).onEvents(eq(player), eventCaptor.capture());
Player.Events events = eventCaptor.getValue();
assertThat(events.contains(Player.EVENT_TIMELINE_CHANGED)).isTrue();
assertThat(events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(events.contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue();
assertThat(events.contains(Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue();
// Set values recursively.
player.addListener(
new EventListener() {
@Override
public void onRepeatModeChanged(int repeatMode) {
player.setShuffleModeEnabled(true);
}
});
player.setRepeatMode(Player.REPEAT_MODE_ONE);
ShadowLooper.runMainLooperToNextTask();
verify(listener).onRepeatModeChanged(anyInt());
verify(listener).onShuffleModeEnabledChanged(anyBoolean());
verify(listener, times(2)).onEvents(eq(player), eventCaptor.capture());
events = Iterables.getLast(eventCaptor.getAllValues());
assertThat(events.contains(Player.EVENT_REPEAT_MODE_CHANGED)).isTrue();
assertThat(events.contains(Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue();
// Ensure all other events are called (even though we can't control how exactly they are
// combined together in onStateChanged calls).
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
player.play();
player.setMediaItem(MediaItem.fromUri("http://this-will-throw-an-exception.mp4"));
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_IDLE);
ShadowLooper.runMainLooperToNextTask();
// Verify that all callbacks have been called at least once.
verify(listener, atLeastOnce()).onTimelineChanged(any(), anyInt());
verify(listener, atLeastOnce()).onMediaItemTransition(any(), anyInt());
verify(listener, atLeastOnce()).onPositionDiscontinuity(anyInt());
verify(listener, atLeastOnce()).onPlaybackParametersChanged(any());
verify(listener, atLeastOnce()).onRepeatModeChanged(anyInt());
verify(listener, atLeastOnce()).onShuffleModeEnabledChanged(anyBoolean());
verify(listener, atLeastOnce()).onPlaybackStateChanged(anyInt());
verify(listener, atLeastOnce()).onIsLoadingChanged(anyBoolean());
verify(listener, atLeastOnce()).onTracksChanged(any(), any());
verify(listener, atLeastOnce()).onStaticMetadataChanged(any());
verify(listener, atLeastOnce()).onPlayWhenReadyChanged(anyBoolean(), anyInt());
verify(listener, atLeastOnce()).onIsPlayingChanged(anyBoolean());
verify(listener, atLeastOnce()).onPlayerError(any());
// Verify all the same events have been recorded with onStateChanged.
verify(listener, atLeastOnce()).onEvents(eq(player), eventCaptor.capture());
List<Player.Events> allEvents = eventCaptor.getAllValues();
assertThat(containsEvent(allEvents, Player.EVENT_TIMELINE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_POSITION_DISCONTINUITY)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_REPEAT_MODE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYBACK_STATE_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_IS_LOADING_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_TRACKS_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_STATIC_METADATA_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAY_WHEN_READY_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_IS_PLAYING_CHANGED)).isTrue();
assertThat(containsEvent(allEvents, Player.EVENT_PLAYER_ERROR)).isTrue();
}
// Internal methods.
private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) {
......@@ -8870,6 +8965,16 @@ public final class ExoPlayerTest {
shadowOf(Looper.getMainLooper()).idle();
}
private static boolean containsEvent(
List<Player.Events> eventsList, @Player.EventFlags int event) {
for (Player.Events events : eventsList) {
if (events.contains(event)) {
return true;
}
}
return false;
}
// Internal classes.
/* {@link FakeRenderer} that can sleep and be woken-up. */
......
/*
* Copyright (C) 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.util;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link MutableFlags}. */
@RunWith(AndroidJUnit4.class)
public final class MutableFlagsTest {
@Test
public void contains_withoutAdd_returnsFalseForAllValues() {
MutableFlags flags = new MutableFlags();
assertThat(flags.contains(/* flag= */ -1234)).isFalse();
assertThat(flags.contains(/* flag= */ 0)).isFalse();
assertThat(flags.contains(/* flag= */ 2)).isFalse();
assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isFalse();
}
@Test
public void contains_afterAdd_returnsTrueForAddedValues() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ -1234);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 2);
flags.add(/* flag= */ Integer.MAX_VALUE);
assertThat(flags.contains(/* flag= */ -1235)).isFalse();
assertThat(flags.contains(/* flag= */ -1234)).isTrue();
assertThat(flags.contains(/* flag= */ 0)).isTrue();
assertThat(flags.contains(/* flag= */ 1)).isFalse();
assertThat(flags.contains(/* flag= */ 2)).isTrue();
assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE - 1)).isFalse();
assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isTrue();
}
@Test
public void contains_afterClear_returnsFalseForAllValues() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ -1234);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 2);
flags.add(/* flag= */ Integer.MAX_VALUE);
flags.clear();
assertThat(flags.contains(/* flag= */ -1234)).isFalse();
assertThat(flags.contains(/* flag= */ 0)).isFalse();
assertThat(flags.contains(/* flag= */ 2)).isFalse();
assertThat(flags.contains(/* flag= */ Integer.MAX_VALUE)).isFalse();
}
@Test
public void size_withoutAdd_returnsZero() {
MutableFlags flags = new MutableFlags();
assertThat(flags.size()).isEqualTo(0);
}
@Test
public void size_afterAdd_returnsNumberUniqueOfElements() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 123);
flags.add(/* flag= */ 123);
assertThat(flags.size()).isEqualTo(2);
}
@Test
public void size_afterClear_returnsZero() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 123);
flags.clear();
assertThat(flags.size()).isEqualTo(0);
}
@Test
public void get_withNegativeIndex_throwsIllegalArgumentException() {
MutableFlags flags = new MutableFlags();
assertThrows(IllegalArgumentException.class, () -> flags.get(/* index= */ -1));
}
@Test
public void get_withIndexExceedingSize_throwsIllegalArgumentException() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 123);
assertThrows(IllegalArgumentException.class, () -> flags.get(/* index= */ 2));
}
@Test
public void get_afterAdd_returnsAllUniqueValues() {
MutableFlags flags = new MutableFlags();
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 0);
flags.add(/* flag= */ 123);
flags.add(/* flag= */ 123);
flags.add(/* flag= */ 456);
List<Integer> values = new ArrayList<>();
for (int i = 0; i < flags.size(); i++) {
values.add(flags.get(i));
}
assertThat(values).containsExactly(0, 123, 456);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment