mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +00:00
Override available commands in ForwardingPlayer
This change adds an API in the ForwardingPlayer to disable commands. This is affecting what Player.isCommandAvailable() returns as well as what is being advertised from the EventListener.onAvailableCommandsChanged() callback. For the callback case, the ForwardingPlayer needs to intercept the callback. It does so by wrapping registered EventListener and Listener instances, which resulted in some boiler-plate code. In addition, there is logic on the wrapped listeners to avoid triggering a queued callback if all listeners have been removed in the meantime. This includes the case where new listeners are added while callbacks scheduled for the removed listeners are still pending. PiperOrigin-RevId: 371139703
This commit is contained in:
parent
cdff456621
commit
7d2a2aa283
4 changed files with 927 additions and 5 deletions
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
testImplementation project(modulePrefix + 'library-core')
|
||||
testImplementation project(modulePrefix + 'testutils')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,31 +15,74 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.device.DeviceInfo;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.util.Clock;
|
||||
import com.google.android.exoplayer2.util.ListenerSet;
|
||||
import com.google.android.exoplayer2.video.VideoSize;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link Player} that forwards operations to another {@link Player}. Applications can use this
|
||||
* class to suppress or modify specific operations, by overriding the respective methods.
|
||||
*
|
||||
* <p>An application can {@link #setDisabledCommands disable available commands}. When the wrapped
|
||||
* player advertises available commands, either with {@link Player#isCommandAvailable(int)} or with
|
||||
* {@link Listener#onAvailableCommandsChanged}, the disabled commands will be filtered out.
|
||||
*/
|
||||
public class ForwardingPlayer extends BasePlayer {
|
||||
private final Player player;
|
||||
private final Clock clock;
|
||||
@Nullable private ForwardingListener forwardingListener;
|
||||
|
||||
private Commands disabledCommands;
|
||||
@Nullable private Commands unfilteredCommands;
|
||||
@Nullable private Commands filteredCommands;
|
||||
|
||||
/** Creates a new instance that forwards all operations to {@code player}. */
|
||||
public ForwardingPlayer(Player player) {
|
||||
this(player, Clock.DEFAULT);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ ForwardingPlayer(Player player, Clock clock) {
|
||||
this.player = player;
|
||||
this.clock = clock;
|
||||
this.disabledCommands = Commands.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the disabled {@link Commands}.
|
||||
*
|
||||
* <p>When querying for available commands with {@link #isCommandAvailable(int)}, or when the
|
||||
* wrapped player advertises available commands with {@link Listener#isCommandAvailable}, disabled
|
||||
* commands will be filtered out.
|
||||
*/
|
||||
public void setDisabledCommands(Commands commands) {
|
||||
checkState(player.getApplicationLooper().equals(Looper.myLooper()));
|
||||
disabledCommands = commands;
|
||||
filteredCommands = null;
|
||||
if (forwardingListener != null) {
|
||||
forwardingListener.maybeAdvertiseAvailableCommands();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the disabled commands. */
|
||||
public Commands getDisabledCommands() {
|
||||
return disabledCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -49,22 +92,34 @@ public class ForwardingPlayer extends BasePlayer {
|
|||
|
||||
@Override
|
||||
public void addListener(EventListener listener) {
|
||||
player.addListener(listener);
|
||||
addListener(new EventListenerWrapper(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener listener) {
|
||||
player.addListener(listener);
|
||||
if (forwardingListener == null) {
|
||||
forwardingListener = new ForwardingListener(this);
|
||||
}
|
||||
if (!forwardingListener.isRegistered()) {
|
||||
forwardingListener.registerTo(player);
|
||||
}
|
||||
forwardingListener.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(EventListener listener) {
|
||||
player.removeListener(listener);
|
||||
removeListener(new EventListenerWrapper(listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener listener) {
|
||||
player.removeListener(listener);
|
||||
if (forwardingListener == null) {
|
||||
return;
|
||||
}
|
||||
forwardingListener.removeListener(listener);
|
||||
if (!forwardingListener.hasListeners()) {
|
||||
forwardingListener.unregisterFrom(player);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -95,7 +150,12 @@ public class ForwardingPlayer extends BasePlayer {
|
|||
|
||||
@Override
|
||||
public Commands getAvailableCommands() {
|
||||
return player.getAvailableCommands();
|
||||
Commands commands = player.getAvailableCommands();
|
||||
if (filteredCommands == null || !commands.equals(unfilteredCommands)) {
|
||||
filteredCommands = filterCommands(commands, disabledCommands);
|
||||
unfilteredCommands = commands;
|
||||
}
|
||||
return filteredCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -364,4 +424,474 @@ public class ForwardingPlayer extends BasePlayer {
|
|||
public void setDeviceMuted(boolean muted) {
|
||||
player.setDeviceMuted(muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a {@link Listener} and intercepts {@link EventListener#onAvailableCommandsChanged} in
|
||||
* order to filter disabled commands. All other operations are forwarded to the wrapped {@link
|
||||
* Listener}.
|
||||
*/
|
||||
private static class ForwardingListener implements Listener {
|
||||
private final ForwardingPlayer player;
|
||||
private final ListenerSet<Listener> listeners;
|
||||
private boolean registered;
|
||||
private Commands lastReceivedCommands;
|
||||
private Commands lastAdvertisedCommands;
|
||||
|
||||
public ForwardingListener(ForwardingPlayer forwardingPlayer) {
|
||||
this.player = forwardingPlayer;
|
||||
listeners =
|
||||
new ListenerSet<>(
|
||||
forwardingPlayer.player.getApplicationLooper(),
|
||||
forwardingPlayer.clock,
|
||||
(listener, flags) -> listener.onEvents(forwardingPlayer, new Events(flags)));
|
||||
lastReceivedCommands = Commands.EMPTY;
|
||||
lastAdvertisedCommands = Commands.EMPTY;
|
||||
}
|
||||
|
||||
public void registerTo(Player player) {
|
||||
checkState(!registered);
|
||||
player.addListener(this);
|
||||
lastReceivedCommands = player.getAvailableCommands();
|
||||
lastAdvertisedCommands = lastReceivedCommands;
|
||||
registered = true;
|
||||
}
|
||||
|
||||
public void unregisterFrom(Player player) {
|
||||
checkState(registered);
|
||||
player.removeListener(this);
|
||||
registered = false;
|
||||
}
|
||||
|
||||
public boolean isRegistered() {
|
||||
return registered;
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean hasListeners() {
|
||||
return listeners.size() > 0;
|
||||
}
|
||||
|
||||
// VideoListener callbacks
|
||||
@Override
|
||||
public void onVideoSizeChanged(VideoSize videoSize) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onVideoSizeChanged(videoSize));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onVideoSizeChanged(
|
||||
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
listeners.sendEvent(
|
||||
C.INDEX_UNSET,
|
||||
listener ->
|
||||
listener.onVideoSizeChanged(
|
||||
width, height, unappliedRotationDegrees, pixelWidthHeightRatio));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceSizeChanged(int width, int height) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onSurfaceSizeChanged(width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame() {
|
||||
listeners.sendEvent(C.INDEX_UNSET, Listener::onRenderedFirstFrame);
|
||||
}
|
||||
|
||||
// AudioListener callbacks
|
||||
|
||||
@Override
|
||||
public void onAudioSessionIdChanged(int audioSessionId) {
|
||||
listeners.sendEvent(
|
||||
C.INDEX_UNSET, listener -> listener.onAudioSessionIdChanged(audioSessionId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioAttributesChanged(AudioAttributes audioAttributes) {
|
||||
listeners.sendEvent(
|
||||
C.INDEX_UNSET, listener -> listener.onAudioAttributesChanged(audioAttributes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVolumeChanged(float volume) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onVolumeChanged(volume));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
|
||||
listeners.sendEvent(
|
||||
C.INDEX_UNSET, listener -> listener.onSkipSilenceEnabledChanged(skipSilenceEnabled));
|
||||
}
|
||||
|
||||
// TextOutput callbacks
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onCues(cues));
|
||||
}
|
||||
|
||||
// MetadataOutput callbacks
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onMetadata(metadata));
|
||||
}
|
||||
|
||||
// DeviceListener callbacks
|
||||
|
||||
@Override
|
||||
public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onDeviceInfoChanged(deviceInfo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceVolumeChanged(int volume, boolean muted) {
|
||||
listeners.sendEvent(C.INDEX_UNSET, listener -> listener.onDeviceVolumeChanged(volume, muted));
|
||||
}
|
||||
|
||||
// EventListener callbacks
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_TIMELINE_CHANGED, listener -> listener.onTimelineChanged(timeline, reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_TIMELINE_CHANGED,
|
||||
listener -> listener.onTimelineChanged(timeline, manifest, reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaItemTransition(
|
||||
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_MEDIA_ITEM_TRANSITION,
|
||||
listener -> listener.onMediaItemTransition(mediaItem, reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
listeners.sendEvent(
|
||||
EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(trackGroups, trackSelections));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStaticMetadataChanged(List<Metadata> metadataList) {
|
||||
listeners.sendEvent(
|
||||
EVENT_STATIC_METADATA_CHANGED,
|
||||
listener -> listener.onStaticMetadataChanged(metadataList));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
|
||||
listeners.sendEvent(
|
||||
EVENT_MEDIA_METADATA_CHANGED, listener -> listener.onMediaMetadataChanged(mediaMetadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsLoadingChanged(boolean isLoading) {
|
||||
listeners.sendEvent(
|
||||
EVENT_IS_LOADING_CHANGED, listener -> listener.onIsLoadingChanged(isLoading));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
listeners.sendEvent(
|
||||
EVENT_IS_LOADING_CHANGED, listener -> listener.onLoadingChanged(isLoading));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableCommandsChanged(Commands availableCommands) {
|
||||
lastReceivedCommands = availableCommands;
|
||||
maybeAdvertiseAvailableCommands();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {
|
||||
listeners.sendEvent(
|
||||
C.INDEX_UNSET, listener -> listener.onPlayerStateChanged(playWhenReady, playbackState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(@State int state) {
|
||||
listeners.sendEvent(
|
||||
EVENT_PLAYBACK_STATE_CHANGED, listener -> listener.onPlaybackStateChanged(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, @State int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_PLAY_WHEN_READY_CHANGED,
|
||||
listener -> listener.onPlayWhenReadyChanged(playWhenReady, reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSuppressionReasonChanged(
|
||||
@PlaybackSuppressionReason int playbackSuppressionReason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
|
||||
listener -> listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsPlayingChanged(boolean isPlaying) {
|
||||
listeners.sendEvent(
|
||||
EVENT_IS_PLAYING_CHANGED, listener -> listener.onIsPlayingChanged(isPlaying));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@RepeatMode int repeatMode) {
|
||||
listeners.sendEvent(
|
||||
EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
listeners.sendEvent(
|
||||
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
|
||||
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
listeners.sendEvent(EVENT_PLAYER_ERROR, listener -> listener.onPlayerError(error));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_POSITION_DISCONTINUITY, listener -> listener.onPositionDiscontinuity(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {
|
||||
listeners.sendEvent(
|
||||
EVENT_POSITION_DISCONTINUITY,
|
||||
listener -> listener.onPositionDiscontinuity(oldPosition, newPosition, reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
listeners.sendEvent(
|
||||
EVENT_PLAYBACK_PARAMETERS_CHANGED,
|
||||
listener -> listener.onPlaybackParametersChanged(playbackParameters));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onSeekProcessed() {
|
||||
listeners.sendEvent(C.INDEX_UNSET, EventListener::onSeekProcessed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvents(Player player, Events events) {
|
||||
// Do nothing, individual callbacks will trigger this event on behalf of the forwarding
|
||||
// player.
|
||||
}
|
||||
|
||||
public void maybeAdvertiseAvailableCommands() {
|
||||
Commands commandsToAdvertise = filterCommands(lastReceivedCommands, player.disabledCommands);
|
||||
if (!commandsToAdvertise.equals(lastAdvertisedCommands)) {
|
||||
lastAdvertisedCommands = commandsToAdvertise;
|
||||
listeners.sendEvent(
|
||||
EVENT_AVAILABLE_COMMANDS_CHANGED,
|
||||
listener -> listener.onAvailableCommandsChanged(commandsToAdvertise));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an {@link EventListener} as a {@link Listener} so that it can be used by the {@link
|
||||
* ForwardingListener}.
|
||||
*/
|
||||
private static class EventListenerWrapper implements Listener {
|
||||
private final EventListener listener;
|
||||
|
||||
/** Wraps an {@link EventListener}. */
|
||||
public EventListenerWrapper(EventListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
// EventListener callbacks
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {
|
||||
listener.onTimelineChanged(timeline, manifest, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) {
|
||||
listener.onTimelineChanged(timeline, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaItemTransition(
|
||||
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {
|
||||
listener.onMediaItemTransition(mediaItem, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
listener.onTracksChanged(trackGroups, trackSelections);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStaticMetadataChanged(List<Metadata> metadataList) {
|
||||
listener.onStaticMetadataChanged(metadataList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaMetadataChanged(MediaMetadata mediaMetadata) {
|
||||
listener.onMediaMetadataChanged(mediaMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsLoadingChanged(boolean isLoading) {
|
||||
listener.onIsLoadingChanged(isLoading);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
listener.onLoadingChanged(isLoading);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableCommandsChanged(Commands availableCommands) {
|
||||
listener.onAvailableCommandsChanged(availableCommands);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {
|
||||
listener.onPlayerStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(@State int state) {
|
||||
listener.onPlaybackStateChanged(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(
|
||||
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
|
||||
listener.onPlayWhenReadyChanged(playWhenReady, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSuppressionReasonChanged(
|
||||
@PlaybackSuppressionReason int playbackSuppressionReason) {
|
||||
listener.onPlaybackSuppressionReasonChanged(playbackSuppressionReason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsPlayingChanged(boolean isPlaying) {
|
||||
listener.onIsPlayingChanged(isPlaying);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(@RepeatMode int repeatMode) {
|
||||
listener.onRepeatModeChanged(repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
|
||||
listener.onShuffleModeEnabledChanged(shuffleModeEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
listener.onPlayerError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
|
||||
listener.onPositionDiscontinuity(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(
|
||||
PositionInfo oldPosition, PositionInfo newPosition, @DiscontinuityReason int reason) {
|
||||
listener.onPositionDiscontinuity(oldPosition, newPosition, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
listener.onPlaybackParametersChanged(playbackParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation") // Forwarding to deprecated method.
|
||||
public void onSeekProcessed() {
|
||||
listener.onSeekProcessed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvents(Player player, Events events) {
|
||||
listener.onEvents(player, events);
|
||||
}
|
||||
|
||||
// Other Listener callbacks, they should never be invoked on this wrapper.
|
||||
|
||||
@Override
|
||||
public void onMetadata(Metadata metadata) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof EventListenerWrapper)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EventListenerWrapper that = (EventListenerWrapper) o;
|
||||
return listener.equals(that.listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return listener.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the remaining available commands after removing disabled commands. */
|
||||
private static Commands filterCommands(Commands availableCommands, Commands disabledCommands) {
|
||||
if (disabledCommands.size() == 0) {
|
||||
return availableCommands;
|
||||
}
|
||||
|
||||
Commands.Builder builder = new Commands.Builder();
|
||||
for (int i = 0; i < availableCommands.size(); i++) {
|
||||
int command = availableCommands.get(i);
|
||||
builder.addIf(command, !disabledCommands.contains(command));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@ public final class ListenerSet<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/** Returns the number of added listeners. */
|
||||
public int size() {
|
||||
return listeners.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event that is sent to the listeners when {@link #flushEvents} is called.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,386 @@
|
|||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_PLAY_PAUSE;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_PREPARE_STOP;
|
||||
import static com.google.android.exoplayer2.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
|
||||
import static com.google.android.exoplayer2.Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.EVENT_PLAYBACK_STATE_CHANGED;
|
||||
import static com.google.android.exoplayer2.Player.STATE_READY;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.testutil.StubExoPlayer;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InOrder;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Unit test for {@link ForwardingPlayer}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ForwardingPlayerTest {
|
||||
@Test
|
||||
public void getAvailableCommands_withDisabledCommands_filtersDisabledCommands() {
|
||||
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
|
||||
assertThat(forwardingPlayer.getAvailableCommands())
|
||||
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAvailableCommands_playerAvailableCommandsChanged_returnsFreshCommands() {
|
||||
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
assertThat(forwardingPlayer.getAvailableCommands())
|
||||
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
|
||||
player.setAvailableCommands(
|
||||
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
|
||||
assertThat(forwardingPlayer.getAvailableCommands())
|
||||
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isCommandAvailable_withDisabledCommands_filtersDisabledCommands() {
|
||||
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
|
||||
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PLAY_PAUSE)).isTrue();
|
||||
assertThat(forwardingPlayer.isCommandAvailable(COMMAND_PREPARE_STOP)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setDisabledCommands_triggersOnCommandsAvailableChanged() {
|
||||
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.addListener(listener);
|
||||
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = inOrder(listener);
|
||||
inOrder.verify(listener).onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE));
|
||||
inOrder
|
||||
.verify(listener)
|
||||
.onEvents(
|
||||
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setDisabledCommands_withoutChangingAvailableCommands_noCallbackTriggered() {
|
||||
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.addListener(listener);
|
||||
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setDisabledCommands_multipleTimes_availableCommandsUpdated() {
|
||||
Player player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
assertThat(forwardingPlayer.getAvailableCommands())
|
||||
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP));
|
||||
|
||||
forwardingPlayer.setDisabledCommands(
|
||||
buildCommands(COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
assertThat(forwardingPlayer.getAvailableCommands())
|
||||
.isEqualTo(buildCommands(COMMAND_PLAY_PAUSE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCommandsAvailableChanged_listenerChangesCommandsRecursively_secondCallbackCalled() {
|
||||
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
Player.Listener listener =
|
||||
spy(
|
||||
new Player.Listener() {
|
||||
@Override
|
||||
public void onAvailableCommandsChanged(Player.Commands availableCommands) {
|
||||
// The callback changes the forwarding player's disabled commands triggering
|
||||
// exactly one more callback.
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
}
|
||||
});
|
||||
forwardingPlayer.addListener(listener);
|
||||
|
||||
Player.Commands updatedCommands =
|
||||
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM);
|
||||
player.setAvailableCommands(updatedCommands);
|
||||
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = inOrder(listener);
|
||||
inOrder
|
||||
.verify(listener)
|
||||
.onAvailableCommandsChanged(
|
||||
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
inOrder
|
||||
.verify(listener)
|
||||
.onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE, COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
inOrder
|
||||
.verify(listener)
|
||||
.onEvents(
|
||||
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
interceptingOnAvailableCommandsChanged_withDisabledCommands_filtersDisabledCommands() {
|
||||
FakePlayer player = new FakePlayer();
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.addListener(listener);
|
||||
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_PREPARE_STOP));
|
||||
ShadowLooper.idleMainLooper();
|
||||
// Setting the disabled commands did not affect the available commands, hence no callback was
|
||||
// triggered.
|
||||
verifyNoMoreInteractions(listener);
|
||||
|
||||
// The wrapped player advertises new available commands.
|
||||
Player.Commands updatedCommands = buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
player.setAvailableCommands(updatedCommands);
|
||||
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
|
||||
ShadowLooper.idleMainLooper();
|
||||
verify(listener).onAvailableCommandsChanged(buildCommands(COMMAND_PLAY_PAUSE));
|
||||
verify(listener)
|
||||
.onEvents(
|
||||
same(forwardingPlayer), argThat(new EventsMatcher(EVENT_AVAILABLE_COMMANDS_CHANGED)));
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
interceptingOnAvailableCommandsChanged_withDisabledCommandsButAvailableCommandsNotChanged_doesNotForwardCallback() {
|
||||
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
forwardingPlayer.addListener(listener);
|
||||
|
||||
// Disable commands that do not affect the available commands.
|
||||
forwardingPlayer.setDisabledCommands(buildCommands(COMMAND_SEEK_TO_MEDIA_ITEM));
|
||||
ShadowLooper.idleMainLooper();
|
||||
verifyNoMoreInteractions(listener);
|
||||
|
||||
// The wrapped player advertises new available commands which, after filtering the disabled
|
||||
// commands, do not change the available commands.
|
||||
Player.Commands updatedCommands =
|
||||
buildCommands(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP, COMMAND_SEEK_TO_MEDIA_ITEM);
|
||||
player.setAvailableCommands(updatedCommands);
|
||||
player.forwardingListener.onAvailableCommandsChanged(updatedCommands);
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeListener_removesListenerFromPlayer() {
|
||||
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
|
||||
forwardingPlayer.addListener(listener);
|
||||
assertThat(player.forwardingListener).isNotNull();
|
||||
forwardingPlayer.removeListener(listener);
|
||||
assertThat(player.forwardingListener).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addEventListener_forwardsEventListenerEvents() {
|
||||
FakePlayer player = new FakePlayer(COMMAND_PLAY_PAUSE, COMMAND_PREPARE_STOP);
|
||||
Player.EventListener eventListener = mock(Player.EventListener.class);
|
||||
ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player);
|
||||
|
||||
forwardingPlayer.addListener(eventListener);
|
||||
player.forwardingListener.onPlaybackStateChanged(STATE_READY);
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = inOrder(eventListener);
|
||||
inOrder.verify(eventListener).onPlaybackStateChanged(STATE_READY);
|
||||
inOrder
|
||||
.verify(eventListener)
|
||||
.onEvents(same(forwardingPlayer), argThat(new EventsMatcher(EVENT_PLAYBACK_STATE_CHANGED)));
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forwardingListener_overridesAllListenerMethods() throws Exception {
|
||||
// Check with reflection that ForwardingListener in ForwardingPlayer overrides all Listener
|
||||
// methods.
|
||||
Class<?> forwardingListenerClass = getNestedClass("ForwardingListener");
|
||||
List<Method> publicListenerMethods = getPublicMethods(Player.Listener.class);
|
||||
for (Method method : publicListenerMethods) {
|
||||
assertThat(
|
||||
forwardingListenerClass.getDeclaredMethod(
|
||||
method.getName(), method.getParameterTypes()))
|
||||
.isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eventListenerWrapper_overridesAllEventListenerMethods() throws Exception {
|
||||
// Check with reflection that EventListenerWrapper in ForwardingPlayer overrides all
|
||||
// EventListener methods.
|
||||
Class<?> listenerWrapperClass = getNestedClass("EventListenerWrapper");
|
||||
List<Method> publicListenerMethods = getPublicMethods(Player.EventListener.class);
|
||||
for (Method method : publicListenerMethods) {
|
||||
assertThat(
|
||||
listenerWrapperClass.getDeclaredMethod(method.getName(), method.getParameterTypes()))
|
||||
.isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
private static class FakePlayer extends StubExoPlayer {
|
||||
private Commands availableCommands;
|
||||
/**
|
||||
* Supports up to 1 registered listener, named deliberately forwardingListener to emphasize its
|
||||
* purpose.
|
||||
*/
|
||||
@Nullable private Listener forwardingListener;
|
||||
|
||||
public FakePlayer() {
|
||||
this.availableCommands = Commands.EMPTY;
|
||||
}
|
||||
|
||||
public FakePlayer(@Command int... commands) {
|
||||
this.availableCommands = new Commands.Builder().addAll(commands).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener listener) {
|
||||
checkState(this.forwardingListener == null);
|
||||
this.forwardingListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener listener) {
|
||||
checkState(this.forwardingListener.equals(listener));
|
||||
this.forwardingListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Commands getAvailableCommands() {
|
||||
return availableCommands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Looper getApplicationLooper() {
|
||||
return Looper.getMainLooper();
|
||||
}
|
||||
|
||||
public void setAvailableCommands(Commands availableCommands) {
|
||||
this.availableCommands = availableCommands;
|
||||
}
|
||||
}
|
||||
|
||||
private static Player.Commands buildCommands(@Player.Command int... commands) {
|
||||
return new Player.Commands.Builder().addAll(commands).build();
|
||||
}
|
||||
|
||||
private Class<?> getNestedClass(String className) {
|
||||
for (Class<?> declaredClass : ForwardingPlayer.class.getDeclaredClasses()) {
|
||||
if (declaredClass.getSimpleName().equals(className)) {
|
||||
return declaredClass;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
private static class EventsMatcher implements ArgumentMatcher<Player.Events> {
|
||||
private final int[] events;
|
||||
|
||||
private EventsMatcher(int... events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Player.Events argument) {
|
||||
if (events.length != argument.size()) {
|
||||
return false;
|
||||
}
|
||||
for (int event : events) {
|
||||
if (!argument.contains(event)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all the methods of Java interface. */
|
||||
private static List<Method> getPublicMethods(Class<?> anInterface) {
|
||||
assertThat(anInterface.isInterface()).isTrue();
|
||||
// Run a BFS over all extended interfaces to inspect them all.
|
||||
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
|
||||
interfacesQueue.add(anInterface);
|
||||
Set<Class<?>> interfaces = new HashSet<>();
|
||||
while (!interfacesQueue.isEmpty()) {
|
||||
Class<?> currentInterface = interfacesQueue.remove();
|
||||
if (interfaces.add(currentInterface)) {
|
||||
Collections.addAll(interfacesQueue, currentInterface.getInterfaces());
|
||||
}
|
||||
}
|
||||
|
||||
List<Method> list = new ArrayList<>();
|
||||
for (Class<?> currentInterface : interfaces) {
|
||||
for (Method method : currentInterface.getDeclaredMethods()) {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
list.add(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue