From baf1516ae4b7a5fc84c7dd6b8b8b55e4c47a0fd1 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 11 Mar 2021 15:31:39 +0000 Subject: [PATCH] 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 --- RELEASENOTES.md | 2 + .../google/android/exoplayer2/BasePlayer.java | 55 ++++++++++++++++ .../com/google/android/exoplayer2/Player.java | 42 ++++++++++-- .../android/exoplayer2/ExoPlayerTest.java | 27 ++++---- .../exoplayer2/SimpleExoPlayerTest.java | 64 +++++++++++++++++++ 5 files changed, 173 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ec55fa058b..e445f75466 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,8 @@ * Fix playback position issue when re-preparing playback after a BehindLiveWindowException ([#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 `Player.DefaultEventListener`. Use `Player.EventListener` instead. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 3fedba8ef4..baf0623036 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.Collections; import java.util.List; @@ -29,6 +30,60 @@ public abstract class BasePlayer implements Player { 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 public final void setMediaItem(MediaItem mediaItem) { setMediaItems(Collections.singletonList(mediaItem)); diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Player.java b/library/common/src/main/java/com/google/android/exoplayer2/Player.java index df7c83da0b..40fb87de0e 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/Player.java @@ -799,6 +799,19 @@ public interface Player { } } + /** + * Listener of all changes in the Player. + * + *

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 * {@link #STATE_ENDED}. @@ -1065,21 +1078,42 @@ public interface Player { Looper getApplicationLooper(); /** - * Register a listener to receive events from the player. The listener's methods will be called on - * the thread that was used to construct the player. However, if the thread used to construct the - * player does not have a {@link Looper}, then the listener will be called on the main thread. + * Registers a listener to receive events from the player. The listener's methods will be called + * on the thread that was used to construct the player. However, if the thread used to construct + * the player does not have a {@link Looper}, then the listener will be called on the main thread. * * @param listener The listener to register. */ 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. + * + *

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. */ 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 * the default position. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index a4946fa0f6..ba29b5fdbd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -55,6 +55,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.Player.DiscontinuityReason; 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.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -172,25 +173,25 @@ public final class ExoPlayerTest { FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_UNKNOWN); SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build(); - EventListener mockEventListener = mock(EventListener.class); - player.addListener(mockEventListener); + Listener mockListener = mock(Listener.class); + player.addListener(mockListener); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); player.prepare(); player.play(); runUntilPlaybackState(player, Player.STATE_ENDED); - InOrder inOrder = inOrder(mockEventListener); + InOrder inOrder = inOrder(mockListener); inOrder - .verify(mockEventListener) + .verify(mockListener) .onTimelineChanged( argThat(noUid(expectedMaskingTimeline)), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); inOrder - .verify(mockEventListener) + .verify(mockListener) .onTimelineChanged( 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.sampleBufferReadCount).isEqualTo(0); assertThat(renderer.isEnded).isFalse(); @@ -202,29 +203,29 @@ public final class ExoPlayerTest { Timeline timeline = new FakeTimeline(); FakeRenderer renderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); SimpleExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(renderer).build(); - EventListener mockEventListener = mock(EventListener.class); - player.addListener(mockEventListener); + Listener mockListener = mock(Listener.class); + player.addListener(mockListener); player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT)); player.prepare(); player.play(); runUntilPlaybackState(player, Player.STATE_ENDED); - InOrder inOrder = Mockito.inOrder(mockEventListener); + InOrder inOrder = Mockito.inOrder(mockListener); inOrder - .verify(mockEventListener) + .verify(mockListener) .onTimelineChanged( argThat(noUid(placeholderTimeline)), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)); inOrder - .verify(mockEventListener) + .verify(mockListener) .onTimelineChanged( argThat(noUid(timeline)), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE)); inOrder - .verify(mockEventListener) + .verify(mockListener) .onTracksChanged( 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.sampleBufferReadCount).isEqualTo(1); assertThat(renderer.isEnded).isTrue(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/SimpleExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/SimpleExoPlayerTest.java index a11961c301..6636134f04 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/SimpleExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/SimpleExoPlayerTest.java @@ -18,6 +18,11 @@ package com.google.android.exoplayer2; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.common.truth.Truth.assertThat; 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.verify; @@ -79,4 +84,63 @@ public class SimpleExoPlayerTest { verify(listener).onVideoDisabled(any(), 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()); + } }