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()); + } }