Flatten listener using existing listeners

Adds a new Listener that extends all other listener.
This is part of the component flattening goal.

After components have been flattened in Player,
and clients transitioned, existing listeners will be deprecated.

PiperOrigin-RevId: 362287507
This commit is contained in:
krocard 2021-03-11 15:31:39 +00:00 committed by Ian Baker
parent a4ad351fb1
commit baf1516ae4
5 changed files with 173 additions and 17 deletions

View File

@ -32,6 +32,8 @@
* Fix playback position issue when re-preparing playback after a * Fix playback position issue when re-preparing playback after a
BehindLiveWindowException BehindLiveWindowException
([#8675](https://github.com/google/ExoPlayer/issues/8675)). ([#8675](https://github.com/google/ExoPlayer/issues/8675)).
* Add a `Listener` interface to receive all player events in a single
object.
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `Player.DefaultEventListener`. Use `Player.EventListener` * Remove `Player.DefaultEventListener`. Use `Player.EventListener`
instead. instead.

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -29,6 +30,60 @@ public abstract class BasePlayer implements Player {
window = new Timeline.Window(); window = new Timeline.Window();
} }
@Override
public final void addListener(Listener listener) {
Assertions.checkNotNull(listener);
@Nullable AudioComponent audioComponent = getAudioComponent();
if (audioComponent != null) {
audioComponent.addAudioListener(listener);
}
@Nullable VideoComponent videoComponent = getVideoComponent();
if (videoComponent != null) {
videoComponent.addVideoListener(listener);
}
@Nullable TextComponent textComponent = getTextComponent();
if (textComponent != null) {
textComponent.addTextOutput(listener);
}
@Nullable MetadataComponent metadataComponent = getMetadataComponent();
if (metadataComponent != null) {
metadataComponent.addMetadataOutput(listener);
}
@Nullable DeviceComponent deviceComponent = getDeviceComponent();
if (deviceComponent != null) {
deviceComponent.addDeviceListener(listener);
}
EventListener eventListener = listener;
addListener(eventListener);
}
@Override
public final void removeListener(Listener listener) {
Assertions.checkNotNull(listener);
@Nullable AudioComponent audioComponent = getAudioComponent();
if (audioComponent != null) {
audioComponent.removeAudioListener(listener);
}
@Nullable VideoComponent videoComponent = getVideoComponent();
if (videoComponent != null) {
videoComponent.removeVideoListener(listener);
}
@Nullable TextComponent textComponent = getTextComponent();
if (textComponent != null) {
textComponent.removeTextOutput(listener);
}
@Nullable MetadataComponent metadataComponent = getMetadataComponent();
if (metadataComponent != null) {
metadataComponent.removeMetadataOutput(listener);
}
@Nullable DeviceComponent deviceComponent = getDeviceComponent();
if (deviceComponent != null) {
deviceComponent.removeDeviceListener(listener);
}
EventListener eventListener = listener;
removeListener(eventListener);
}
@Override @Override
public final void setMediaItem(MediaItem mediaItem) { public final void setMediaItem(MediaItem mediaItem) {
setMediaItems(Collections.singletonList(mediaItem)); setMediaItems(Collections.singletonList(mediaItem));

View File

@ -799,6 +799,19 @@ public interface Player {
} }
} }
/**
* Listener of all changes in the Player.
*
* <p>All methods have no-op default implementations to allow selective overrides.
*/
interface Listener
extends VideoListener,
AudioListener,
TextOutput,
MetadataOutput,
DeviceListener,
EventListener {}
/** /**
* Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or
* {@link #STATE_ENDED}. * {@link #STATE_ENDED}.
@ -1065,21 +1078,42 @@ public interface Player {
Looper getApplicationLooper(); Looper getApplicationLooper();
/** /**
* Register a listener to receive events from the player. The listener's methods will be called on * Registers a listener to receive events from the player. The listener's methods will be called
* the thread that was used to construct the player. However, if the thread used to construct the * on the thread that was used to construct the player. However, if the thread used to construct
* player does not have a {@link Looper}, then the listener will be called on the main thread. * the player does not have a {@link Looper}, then the listener will be called on the main thread.
* *
* @param listener The listener to register. * @param listener The listener to register.
*/ */
void addListener(EventListener listener); void addListener(EventListener listener);
/** /**
* Unregister a listener. The listener will no longer receive events from the player. * Registers a listener to receive all events from the player.
*
* <p>Do not register the listener additionally in individual `Player` components (such as {@link
* Player.AudioComponent#addAudioListener(AudioListener)}, {@link
* Player.VideoComponent#addVideoListener(VideoListener)}) as it will already receive all their
* events.
*
* @param listener The listener to register.
*/
void addListener(Listener listener);
/**
* Unregister a listener registered through {@link #addListener(EventListener)}. The listener will
* no longer receive events from the player.
* *
* @param listener The listener to unregister. * @param listener The listener to unregister.
*/ */
void removeListener(EventListener listener); void removeListener(EventListener listener);
/**
* Unregister a listener registered through {@link #addListener(Listener)}. The listener will no
* longer receive events.
*
* @param listener The listener to unregister.
*/
void removeListener(Listener listener);
/** /**
* Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to * Clears the playlist, adds the specified {@link MediaItem MediaItems} and resets the position to
* the default position. * the default position.

View File

@ -55,6 +55,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Player.EventListener;
import com.google.android.exoplayer2.Player.Listener;
import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
@ -172,25 +173,25 @@ public final class ExoPlayerTest {
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_UNKNOWN); FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_UNKNOWN);
SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build(); SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
EventListener mockEventListener = mock(EventListener.class); Listener mockListener = mock(Listener.class);
player.addListener(mockEventListener); player.addListener(mockListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare(); player.prepare();
player.play(); player.play();
runUntilPlaybackState(player, Player.STATE_ENDED); runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = inOrder(mockEventListener); InOrder inOrder = inOrder(mockListener);
inOrder inOrder
.verify(mockEventListener) .verify(mockListener)
.onTimelineChanged( .onTimelineChanged(
argThat(noUid(expectedMaskingTimeline)), argThat(noUid(expectedMaskingTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder inOrder
.verify(mockEventListener) .verify(mockListener)
.onTimelineChanged( .onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder.verify(mockEventListener, never()).onPositionDiscontinuity(anyInt()); inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
assertThat(renderer.getFormatsRead()).isEmpty(); assertThat(renderer.getFormatsRead()).isEmpty();
assertThat(renderer.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.sampleBufferReadCount).isEqualTo(0);
assertThat(renderer.isEnded).isFalse(); assertThat(renderer.isEnded).isFalse();
@ -202,29 +203,29 @@ public final class ExoPlayerTest {
Timeline timeline = new FakeTimeline(); Timeline timeline = new FakeTimeline();
FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build(); SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build();
EventListener mockEventListener = mock(EventListener.class); Listener mockListener = mock(Listener.class);
player.addListener(mockEventListener); player.addListener(mockListener);
player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare(); player.prepare();
player.play(); player.play();
runUntilPlaybackState(player, Player.STATE_ENDED); runUntilPlaybackState(player, Player.STATE_ENDED);
InOrder inOrder = Mockito.inOrder(mockEventListener); InOrder inOrder = Mockito.inOrder(mockListener);
inOrder inOrder
.verify(mockEventListener) .verify(mockListener)
.onTimelineChanged( .onTimelineChanged(
argThat(noUid(placeholderTimeline)), argThat(noUid(placeholderTimeline)),
eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
inOrder inOrder
.verify(mockEventListener) .verify(mockListener)
.onTimelineChanged( .onTimelineChanged(
argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
inOrder inOrder
.verify(mockEventListener) .verify(mockListener)
.onTracksChanged( .onTracksChanged(
eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any()); eq(new TrackGroupArray(new TrackGroup(ExoPlayerTestRunner.VIDEO_FORMAT))), any());
inOrder.verify(mockEventListener, never()).onPositionDiscontinuity(anyInt()); inOrder.verify(mockListener, never()).onPositionDiscontinuity(anyInt());
assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT); assertThat(renderer.getFormatsRead()).containsExactly(ExoPlayerTestRunner.VIDEO_FORMAT);
assertThat(renderer.sampleBufferReadCount).isEqualTo(1); assertThat(renderer.sampleBufferReadCount).isEqualTo(1);
assertThat(renderer.isEnded).isTrue(); assertThat(renderer.isEnded).isTrue();

View File

@ -18,6 +18,11 @@ package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -79,4 +84,63 @@ public class SimpleExoPlayerTest {
verify(listener).onVideoDisabled(any(), any()); verify(listener).onVideoDisabled(any(), any());
verify(listener).onPlayerReleased(any()); verify(listener).onPlayerReleased(any());
} }
@Test
public void releaseAfterRendererEvents_triggersPendingVideoEventsInListener() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(
ApplicationProvider.getApplicationContext(),
(handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)})
.setClock(new AutoAdvancingFakeClock())
.build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setMediaSource(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_READY);
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener, atLeastOnce()).onEvents(any(), any()); // EventListener
verify(listener).onRenderedFirstFrame(); // VideoListener
}
@Test
public void releaseAfterVolumeChanges_triggerPendingVolumeEventInListener() throws Exception {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.setVolume(0F);
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener).onVolumeChanged(anyFloat());
}
@Test
public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener() {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
int deviceVolume = player.getDeviceVolume();
try {
player.setDeviceVolume(deviceVolume + 1); // No-op if at max volume.
player.setDeviceVolume(deviceVolume - 1); // No-op if at min volume.
} finally {
player.setDeviceVolume(deviceVolume); // Restore original volume.
}
player.release();
ShadowLooper.runMainLooperToNextTask();
verify(listener, atLeast(2)).onDeviceVolumeChanged(anyInt(), anyBoolean());
}
} }