From 262778463259530264dc0fdb983d06850e5e9795 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Tue, 11 Jun 2024 11:49:38 -0700 Subject: [PATCH 1/7] Refactor VideoPlayer to be less reliant on channels/sinks deep within. --- .../plugins/videoplayer/VideoPlayer.java | 123 ++++++------------ .../videoplayer/VideoPlayerCallbacks.java | 21 +++ .../VideoPlayerEventCallbacks.java | 93 +++++++++++++ .../videoplayer/VideoPlayerPlugin.java | 4 +- .../plugins/videoplayer/VideoPlayerTest.java | 104 ++++++++------- 5 files changed, 213 insertions(+), 132 deletions(-) create mode 100644 packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java create mode 100644 packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 4529db72d86..e0810a4a021 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -28,12 +28,7 @@ import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import io.flutter.plugin.common.EventChannel; import io.flutter.view.TextureRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Map; final class VideoPlayer { @@ -48,9 +43,7 @@ final class VideoPlayer { private final TextureRegistry.SurfaceTextureEntry textureEntry; - private QueuingEventSink eventSink; - - private final EventChannel eventChannel; + private final VideoPlayerCallbacks events; private static final String USER_AGENT = "User-Agent"; @@ -62,13 +55,13 @@ final class VideoPlayer { VideoPlayer( Context context, - EventChannel eventChannel, + VideoPlayerCallbacks events, TextureRegistry.SurfaceTextureEntry textureEntry, String dataSource, String formatHint, @NonNull Map httpHeaders, VideoPlayerOptions options) { - this.eventChannel = eventChannel; + this.events = events; this.textureEntry = textureEntry; this.options = options; @@ -86,24 +79,23 @@ final class VideoPlayer { exoPlayer.setMediaItem(mediaItem); exoPlayer.prepare(); - setUpVideoPlayer(exoPlayer, new QueuingEventSink()); + setUpVideoPlayer(exoPlayer); } // Constructor used to directly test members of this class. @VisibleForTesting VideoPlayer( ExoPlayer exoPlayer, - EventChannel eventChannel, + VideoPlayerCallbacks events, TextureRegistry.SurfaceTextureEntry textureEntry, VideoPlayerOptions options, - QueuingEventSink eventSink, DefaultHttpDataSource.Factory httpDataSourceFactory) { - this.eventChannel = eventChannel; + this.events = events; this.textureEntry = textureEntry; this.options = options; this.httpDataSourceFactory = httpDataSourceFactory; - setUpVideoPlayer(exoPlayer, eventSink); + setUpVideoPlayer(exoPlayer); } @VisibleForTesting @@ -118,22 +110,8 @@ public void configureHttpDataSourceFactory(@NonNull Map httpHead httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty); } - private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) { + private void setUpVideoPlayer(ExoPlayer exoPlayer) { this.exoPlayer = exoPlayer; - this.eventSink = eventSink; - - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink.setDelegate(sink); - } - - @Override - public void onCancel(Object o) { - eventSink.setDelegate(null); - } - }); surface = new Surface(textureEntry.surfaceTexture()); exoPlayer.setVideoSurface(surface); @@ -144,11 +122,14 @@ public void onCancel(Object o) { private boolean isBuffering = false; public void setBuffering(boolean buffering) { - if (isBuffering != buffering) { - isBuffering = buffering; - Map event = new HashMap<>(); - event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd"); - eventSink.success(event); + if (isBuffering == buffering) { + return; + } + isBuffering = buffering; + if (buffering) { + events.onBufferingStart(); + } else { + events.onBufferingEnd(); } } @@ -163,11 +144,8 @@ public void onPlaybackStateChanged(final int playbackState) { sendInitialized(); } } else if (playbackState == Player.STATE_ENDED) { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); + events.onCompleted(); } - if (playbackState != Player.STATE_BUFFERING) { setBuffering(false); } @@ -180,30 +158,20 @@ public void onPlayerError(@NonNull final PlaybackException error) { // See https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window exoPlayer.seekToDefaultPosition(); exoPlayer.prepare(); - } else if (eventSink != null) { - eventSink.error("VideoError", "Video player had error " + error, null); + } else { + events.onError("VideoPlayer", "Video player had error " + error, null); } } @Override public void onIsPlayingChanged(boolean isPlaying) { - if (eventSink != null) { - Map event = new HashMap<>(); - event.put("event", "isPlayingStateUpdate"); - event.put("isPlaying", isPlaying); - eventSink.success(event); - } + events.onIsPlayingStateUpdate(isPlaying); } }); } void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); + events.onBufferingUpdate(exoPlayer.getBufferedPosition()); } private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { @@ -248,35 +216,29 @@ long getPosition() { @SuppressWarnings("SuspiciousNameCombination") @VisibleForTesting void sendInitialized() { - if (isInitialized) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("duration", exoPlayer.getDuration()); - - VideoSize videoSize = exoPlayer.getVideoSize(); - int width = videoSize.width; - int height = videoSize.height; - if (width != 0 && height != 0) { - int rotationDegrees = videoSize.unappliedRotationDegrees; - // Switch the width/height if video was taken in portrait mode - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = videoSize.height; - height = videoSize.width; - } - event.put("width", width); - event.put("height", height); - - // Rotating the video with ExoPlayer does not seem to be possible with a Surface, - // so inform the Flutter code that the widget needs to be rotated to prevent - // upside-down playback for videos with rotationDegrees of 180 (other orientations work - // correctly without correction). - if (rotationDegrees == 180) { - event.put("rotationCorrection", rotationDegrees); - } + if (!isInitialized) { + return; + } + VideoSize videoSize = exoPlayer.getVideoSize(); + @Nullable Integer rotationCorrection = null; + int width = videoSize.width; + int height = videoSize.height; + if (width != 0 && height != 0) { + int rotationDegrees = videoSize.unappliedRotationDegrees; + // Switch the width/height if video was taken in portrait mode + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = videoSize.height; + height = videoSize.width; + } + // Rotating the video with ExoPlayer does not seem to be possible with a Surface, + // so inform the Flutter code that the widget needs to be rotated to prevent + // upside-down playback for videos with rotationDegrees of 180 (other orientations work + // correctly without correction). + if (rotationDegrees == 180) { + rotationCorrection = rotationDegrees; } - - eventSink.success(event); } + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); } void dispose() { @@ -284,7 +246,6 @@ void dispose() { exoPlayer.stop(); } textureEntry.release(); - eventChannel.setStreamHandler(null); if (surface != null) { surface.release(); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java new file mode 100644 index 00000000000..c80b27a5be0 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -0,0 +1,21 @@ +package io.flutter.plugins.videoplayer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Callbacks representing events invoked by {@link VideoPlayer}. + * + *

In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the + * expected events to send back through the plugin channel. In tests methods can be overridden in + * order to assert results. + */ +interface VideoPlayerCallbacks { + void onInitialized(int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees); + void onBufferingStart(); + void onBufferingUpdate(long bufferedPosition); + void onBufferingEnd(); + void onCompleted(); + void onError(@NonNull String code, @Nullable String message, @Nullable Object details); + void onIsPlayingStateUpdate(boolean isPlaying); +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java new file mode 100644 index 00000000000..b79030d93c2 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -0,0 +1,93 @@ +package io.flutter.plugins.videoplayer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.EventChannel; + +final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks { + private final EventChannel.EventSink eventSink; + + static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) { + QueuingEventSink eventSink = new QueuingEventSink(); + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + eventSink.setDelegate(events); + } + + @Override + public void onCancel(Object arguments) { + eventSink.setDelegate(null); + } + }); + return VideoPlayerEventCallbacks.withSink(eventSink); + } + + static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { + return new VideoPlayerEventCallbacks(eventSink); + } + + private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + } + + @Override + public void onInitialized(int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("width", width); + event.put("height", height); + event.put("duration", durationInMs); + if (rotationCorrectionInDegrees != null) { + event.put("rotationCorrection", rotationCorrectionInDegrees); + } + eventSink.success(event); + } + + @Override + public void onBufferingStart() { + Map event = new HashMap<>(); + event.put("event", "bufferingStart"); + eventSink.success(event); + } + + @Override + public void onBufferingUpdate(long bufferedPosition) { + // iOS supports a list of buffered ranges, so we send as a list with a single range. + Map event = new HashMap<>(); + event.put("values", Collections.singletonList(bufferedPosition)); + eventSink.success(event); + } + + @Override + public void onBufferingEnd() { + Map event = new HashMap<>(); + event.put("event", "bufferingEnd"); + eventSink.success(event); + } + + @Override + public void onCompleted() { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } + + @Override + public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) { + eventSink.error(code, message, details); + } + + @Override + public void onIsPlayingStateUpdate(boolean isPlaying) { + Map event = new HashMap<>(); + event.put("isPlaying", isPlaying); + eventSink.success(event); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 5259e1ad3fe..62dca403ac4 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -125,7 +125,7 @@ public void initialize() { player = new VideoPlayer( flutterState.applicationContext, - eventChannel, + VideoPlayerEventCallbacks.bindTo(eventChannel), handle, "asset:///" + assetLookupKey, null, @@ -136,7 +136,7 @@ public void initialize() { player = new VideoPlayer( flutterState.applicationContext, - eventChannel, + VideoPlayerEventCallbacks.bindTo(eventChannel), handle, arg.getUri(), arg.getFormatHint(), diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index a7a03e9e1ed..eb1a9db2e26 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -19,12 +19,13 @@ import androidx.media3.common.VideoSize; import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.exoplayer.ExoPlayer; -import io.flutter.plugin.common.EventChannel; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,38 +38,41 @@ @RunWith(RobolectricTestRunner.class) public class VideoPlayerTest { private ExoPlayer fakeExoPlayer; - private EventChannel fakeEventChannel; private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry; - private SurfaceTexture fakeSurfaceTexture; private VideoPlayerOptions fakeVideoPlayerOptions; private QueuingEventSink fakeEventSink; private DefaultHttpDataSource.Factory httpDataSourceFactorySpy; @Captor private ArgumentCaptor> eventCaptor; + private AutoCloseable mocks; + @Before public void before() { - MockitoAnnotations.openMocks(this); + mocks = MockitoAnnotations.openMocks(this); fakeExoPlayer = mock(ExoPlayer.class); - fakeEventChannel = mock(EventChannel.class); fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class); - fakeSurfaceTexture = mock(SurfaceTexture.class); + SurfaceTexture fakeSurfaceTexture = mock(SurfaceTexture.class); when(fakeSurfaceTextureEntry.surfaceTexture()).thenReturn(fakeSurfaceTexture); fakeVideoPlayerOptions = mock(VideoPlayerOptions.class); fakeEventSink = mock(QueuingEventSink.class); httpDataSourceFactorySpy = spy(new DefaultHttpDataSource.Factory()); } + @After + public void after() throws Exception { + mocks.close(); + } + @Test public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); videoPlayer.configureHttpDataSourceFactory(new HashMap<>()); @@ -84,10 +88,9 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); Map httpHeaders = new HashMap() { @@ -110,10 +113,9 @@ public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); Map httpHeaders = new HashMap() { @@ -134,10 +136,9 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 90, 1f); @@ -148,13 +149,15 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 200); - assertEquals(event.get("height"), 100); - assertEquals(event.get("rotationCorrection"), null); + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); + + assertEquals(expected, actual); } @Test @@ -162,10 +165,9 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 270, 1f); @@ -176,13 +178,15 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); + + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 200); - assertEquals(event.get("height"), 100); - assertEquals(event.get("rotationCorrection"), null); + assertEquals(expected, actual); } @Test @@ -190,10 +194,9 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 0, 1f); @@ -204,13 +207,15 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 100); - assertEquals(event.get("height"), 200); - assertEquals(event.get("rotationCorrection"), null); + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); + + assertEquals(expected, actual); } @Test @@ -218,10 +223,9 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); VideoSize testVideoSize = new VideoSize(100, 200, 180, 1f); @@ -232,13 +236,16 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { videoPlayer.sendInitialized(); verify(fakeEventSink).success(eventCaptor.capture()); - HashMap event = eventCaptor.getValue(); + HashMap actual = eventCaptor.getValue(); + + Map expected = new HashMap<>(); + expected.put("event", "initialized"); + expected.put("duration", 10L); + expected.put("width", 200); + expected.put("height", 100); + expected.put("rotationCorrection", 180); - assertEquals(event.get("event"), "initialized"); - assertEquals(event.get("duration"), 10L); - assertEquals(event.get("width"), 100); - assertEquals(event.get("height"), 200); - assertEquals(event.get("rotationCorrection"), 180); + assertEquals(expected, actual); } @Test @@ -246,10 +253,9 @@ public void onIsPlayingChangedSendsExpectedEvent() { VideoPlayer videoPlayer = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); doAnswer( @@ -257,7 +263,7 @@ public void onIsPlayingChangedSendsExpectedEvent() { invocation -> { Map event = new HashMap<>(); event.put("event", "isPlayingStateUpdate"); - event.put("isPlaying", (Boolean) invocation.getArguments()[0]); + event.put("isPlaying", invocation.getArguments()[0]); fakeEventSink.success(event); return null; }) @@ -288,13 +294,13 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() { .when(fakeExoPlayer) .addListener(any()); + @SuppressWarnings("unused") VideoPlayer unused = new VideoPlayer( fakeExoPlayer, - fakeEventChannel, + VideoPlayerEventCallbacks.withSink(fakeEventSink), fakeSurfaceTextureEntry, fakeVideoPlayerOptions, - fakeEventSink, httpDataSourceFactorySpy); PlaybackException exception = From ee4f836915fbed29333dc9ad5c03f6c751ac6a8e Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Tue, 11 Jun 2024 13:18:11 -0700 Subject: [PATCH 2/7] Format. --- .../videoplayer/VideoPlayerCallbacks.java | 21 ++- .../VideoPlayerEventCallbacks.java | 141 +++++++++--------- .../plugins/videoplayer/VideoPlayerTest.java | 1 - 3 files changed, 84 insertions(+), 79 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index c80b27a5be0..0c93d731c5e 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -11,11 +11,18 @@ * order to assert results. */ interface VideoPlayerCallbacks { - void onInitialized(int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees); - void onBufferingStart(); - void onBufferingUpdate(long bufferedPosition); - void onBufferingEnd(); - void onCompleted(); - void onError(@NonNull String code, @Nullable String message, @Nullable Object details); - void onIsPlayingStateUpdate(boolean isPlaying); + void onInitialized( + int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees); + + void onBufferingStart(); + + void onBufferingUpdate(long bufferedPosition); + + void onBufferingEnd(); + + void onCompleted(); + + void onError(@NonNull String code, @Nullable String message, @Nullable Object details); + + void onIsPlayingStateUpdate(boolean isPlaying); } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index b79030d93c2..615097c054b 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -2,92 +2,91 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import io.flutter.plugin.common.EventChannel; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import io.flutter.plugin.common.EventChannel; - final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks { - private final EventChannel.EventSink eventSink; + private final EventChannel.EventSink eventSink; - static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) { - QueuingEventSink eventSink = new QueuingEventSink(); - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink events) { - eventSink.setDelegate(events); - } + static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) { + QueuingEventSink eventSink = new QueuingEventSink(); + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + eventSink.setDelegate(events); + } - @Override - public void onCancel(Object arguments) { - eventSink.setDelegate(null); - } - }); - return VideoPlayerEventCallbacks.withSink(eventSink); - } + @Override + public void onCancel(Object arguments) { + eventSink.setDelegate(null); + } + }); + return VideoPlayerEventCallbacks.withSink(eventSink); + } - static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { - return new VideoPlayerEventCallbacks(eventSink); - } + static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { + return new VideoPlayerEventCallbacks(eventSink); + } - private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { - this.eventSink = eventSink; - } + private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + } - @Override - public void onInitialized(int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("width", width); - event.put("height", height); - event.put("duration", durationInMs); - if (rotationCorrectionInDegrees != null) { - event.put("rotationCorrection", rotationCorrectionInDegrees); - } - eventSink.success(event); + @Override + public void onInitialized( + int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("width", width); + event.put("height", height); + event.put("duration", durationInMs); + if (rotationCorrectionInDegrees != null) { + event.put("rotationCorrection", rotationCorrectionInDegrees); } + eventSink.success(event); + } - @Override - public void onBufferingStart() { - Map event = new HashMap<>(); - event.put("event", "bufferingStart"); - eventSink.success(event); - } + @Override + public void onBufferingStart() { + Map event = new HashMap<>(); + event.put("event", "bufferingStart"); + eventSink.success(event); + } - @Override - public void onBufferingUpdate(long bufferedPosition) { - // iOS supports a list of buffered ranges, so we send as a list with a single range. - Map event = new HashMap<>(); - event.put("values", Collections.singletonList(bufferedPosition)); - eventSink.success(event); - } + @Override + public void onBufferingUpdate(long bufferedPosition) { + // iOS supports a list of buffered ranges, so we send as a list with a single range. + Map event = new HashMap<>(); + event.put("values", Collections.singletonList(bufferedPosition)); + eventSink.success(event); + } - @Override - public void onBufferingEnd() { - Map event = new HashMap<>(); - event.put("event", "bufferingEnd"); - eventSink.success(event); - } + @Override + public void onBufferingEnd() { + Map event = new HashMap<>(); + event.put("event", "bufferingEnd"); + eventSink.success(event); + } - @Override - public void onCompleted() { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); - } + @Override + public void onCompleted() { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } - @Override - public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) { - eventSink.error(code, message, details); - } + @Override + public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) { + eventSink.error(code, message, details); + } - @Override - public void onIsPlayingStateUpdate(boolean isPlaying) { - Map event = new HashMap<>(); - event.put("isPlaying", isPlaying); - eventSink.success(event); - } + @Override + public void onIsPlayingStateUpdate(boolean isPlaying) { + Map event = new HashMap<>(); + event.put("isPlaying", isPlaying); + eventSink.success(event); + } } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index eb1a9db2e26..b6440a4a6e0 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -24,7 +24,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; - import org.junit.After; import org.junit.Before; import org.junit.Test; From 0334e5341d3a545b3bdab2ee906e44566917f69d Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 12 Jun 2024 09:24:26 -0700 Subject: [PATCH 3/7] ++ --- .../java/io/flutter/plugins/videoplayer/VideoPlayer.java | 5 +++++ .../io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java | 4 ++++ .../plugins/videoplayer/VideoPlayerEventCallbacks.java | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index e0810a4a021..6ed7eacbe41 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -110,6 +110,8 @@ public void configureHttpDataSourceFactory(@NonNull Map httpHead httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty); } + + private void setUpVideoPlayer(ExoPlayer exoPlayer) { this.exoPlayer = exoPlayer; @@ -117,6 +119,9 @@ private void setUpVideoPlayer(ExoPlayer exoPlayer) { exoPlayer.setVideoSurface(surface); setAudioAttributes(exoPlayer, options.mixWithOthers); + // Avoids synthetic accessor. + VideoPlayerCallbacks events = this.events; + exoPlayer.addListener( new Listener() { private boolean isBuffering = false; diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index 0c93d731c5e..a5f356bffcf 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.videoplayer; import androidx.annotation.NonNull; diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index 615097c054b..964e763786f 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.videoplayer; import androidx.annotation.NonNull; From 4303968739dfc250371e6f543ad6f0ca4aeab4d7 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 12 Jun 2024 14:12:57 -0700 Subject: [PATCH 4/7] ++ --- .../io/flutter/plugins/videoplayer/VideoPlayerTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index b6440a4a6e0..3164bc2c74e 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -211,8 +211,8 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() { Map expected = new HashMap<>(); expected.put("event", "initialized"); expected.put("duration", 10L); - expected.put("width", 200); - expected.put("height", 100); + expected.put("width", 100); + expected.put("height", 200); assertEquals(expected, actual); } @@ -240,8 +240,8 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() { Map expected = new HashMap<>(); expected.put("event", "initialized"); expected.put("duration", 10L); - expected.put("width", 200); - expected.put("height", 100); + expected.put("width", 100); + expected.put("height", 200); expected.put("rotationCorrection", 180); assertEquals(expected, actual); From e27eaced2f0669f3a01038645cb219a09ae6347a Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 12 Jun 2024 14:44:39 -0700 Subject: [PATCH 5/7] ++ --- .../java/io/flutter/plugins/videoplayer/VideoPlayer.java | 4 +--- .../flutter/plugins/videoplayer/VideoPlayerCallbacks.java | 3 +-- .../plugins/videoplayer/VideoPlayerEventCallbacks.java | 6 ++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 6ed7eacbe41..049ac0283e7 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -110,8 +110,6 @@ public void configureHttpDataSourceFactory(@NonNull Map httpHead httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty); } - - private void setUpVideoPlayer(ExoPlayer exoPlayer) { this.exoPlayer = exoPlayer; @@ -225,7 +223,7 @@ void sendInitialized() { return; } VideoSize videoSize = exoPlayer.getVideoSize(); - @Nullable Integer rotationCorrection = null; + int rotationCorrection = 0; int width = videoSize.width; int height = videoSize.height; if (width != 0 && height != 0) { diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index a5f356bffcf..52801a4ac9a 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -15,8 +15,7 @@ * order to assert results. */ interface VideoPlayerCallbacks { - void onInitialized( - int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees); + void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees); void onBufferingStart(); diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java index 964e763786f..bc9041eedd1 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.EventChannel; import java.util.Collections; import java.util.HashMap; @@ -31,6 +32,7 @@ public void onCancel(Object arguments) { return VideoPlayerEventCallbacks.withSink(eventSink); } + @VisibleForTesting static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) { return new VideoPlayerEventCallbacks(eventSink); } @@ -41,13 +43,13 @@ private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) { @Override public void onInitialized( - int width, int height, long durationInMs, @Nullable Integer rotationCorrectionInDegrees) { + int width, int height, long durationInMs, int rotationCorrectionInDegrees) { Map event = new HashMap<>(); event.put("event", "initialized"); event.put("width", width); event.put("height", height); event.put("duration", durationInMs); - if (rotationCorrectionInDegrees != null) { + if (rotationCorrectionInDegrees != 0) { event.put("rotationCorrection", rotationCorrectionInDegrees); } eventSink.success(event); From f1f7581a464fe1312e8cb2e678e972fec316c6d5 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 09:58:53 -0700 Subject: [PATCH 6/7] Address feedback. --- .../plugins/videoplayer/VideoPlayer.java | 14 ++++++------ .../videoplayer/VideoPlayerCallbacks.java | 2 ++ .../plugins/videoplayer/VideoPlayerTest.java | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 049ac0283e7..2d56b234806 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -43,7 +43,7 @@ final class VideoPlayer { private final TextureRegistry.SurfaceTextureEntry textureEntry; - private final VideoPlayerCallbacks events; + private final VideoPlayerCallbacks videoPlayerEvents; private static final String USER_AGENT = "User-Agent"; @@ -61,7 +61,7 @@ final class VideoPlayer { String formatHint, @NonNull Map httpHeaders, VideoPlayerOptions options) { - this.events = events; + this.videoPlayerEvents = events; this.textureEntry = textureEntry; this.options = options; @@ -90,7 +90,7 @@ final class VideoPlayer { TextureRegistry.SurfaceTextureEntry textureEntry, VideoPlayerOptions options, DefaultHttpDataSource.Factory httpDataSourceFactory) { - this.events = events; + this.videoPlayerEvents = events; this.textureEntry = textureEntry; this.options = options; this.httpDataSourceFactory = httpDataSourceFactory; @@ -118,7 +118,7 @@ private void setUpVideoPlayer(ExoPlayer exoPlayer) { setAudioAttributes(exoPlayer, options.mixWithOthers); // Avoids synthetic accessor. - VideoPlayerCallbacks events = this.events; + VideoPlayerCallbacks events = this.videoPlayerEvents; exoPlayer.addListener( new Listener() { @@ -162,7 +162,7 @@ public void onPlayerError(@NonNull final PlaybackException error) { exoPlayer.seekToDefaultPosition(); exoPlayer.prepare(); } else { - events.onError("VideoPlayer", "Video player had error " + error, null); + events.onError("VideoError", "Video player had error " + error, null); } } @@ -174,7 +174,7 @@ public void onIsPlayingChanged(boolean isPlaying) { } void sendBufferingUpdate() { - events.onBufferingUpdate(exoPlayer.getBufferedPosition()); + videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition()); } private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) { @@ -241,7 +241,7 @@ void sendInitialized() { rotationCorrection = rotationDegrees; } } - events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); + videoPlayerEvents.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection); } void dispose() { diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java index 52801a4ac9a..b3a1a3967d8 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java @@ -13,6 +13,8 @@ *

In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the * expected events to send back through the plugin channel. In tests methods can be overridden in * order to assert results. + * + *

See {@link androidx.media3.common.Player.Listener} for details. */ interface VideoPlayerCallbacks { void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 3164bc2c74e..04d358af7cb 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -309,4 +309,26 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() { verify(fakeExoPlayer).seekToDefaultPosition(); verify(fakeExoPlayer).prepare(); } + + @Test + public void otherErrorsReportVideoErrorWithErrorString() { + List listeners = new LinkedList<>(); + doAnswer(invocation -> listeners.add(invocation.getArgument(0))) + .when(fakeExoPlayer) + .addListener(any()); + + @SuppressWarnings("unused") + VideoPlayer unused = + new VideoPlayer( + fakeExoPlayer, + VideoPlayerEventCallbacks.withSink(fakeEventSink), + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + httpDataSourceFactorySpy); + + PlaybackException exception = new PlaybackException("You did bad kid", null, PlaybackException.ERROR_CODE_DECODING_FAILED); + listeners.forEach(listener -> listener.onPlayerError(exception)); + + verify(fakeEventSink).error(eq("VideoError"), contains("You did bad kid"), any()); + } } From d97a4ada59fdd1a44ee01c6d1372ff3437b839d1 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 13 Jun 2024 10:12:53 -0700 Subject: [PATCH 7/7] ++ --- .../plugins/videoplayer/VideoPlayerTest.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java index 04d358af7cb..72def743d51 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -314,19 +314,21 @@ public void behindLiveWindowErrorResetsPlayerToDefaultPosition() { public void otherErrorsReportVideoErrorWithErrorString() { List listeners = new LinkedList<>(); doAnswer(invocation -> listeners.add(invocation.getArgument(0))) - .when(fakeExoPlayer) - .addListener(any()); + .when(fakeExoPlayer) + .addListener(any()); @SuppressWarnings("unused") VideoPlayer unused = - new VideoPlayer( - fakeExoPlayer, - VideoPlayerEventCallbacks.withSink(fakeEventSink), - fakeSurfaceTextureEntry, - fakeVideoPlayerOptions, - httpDataSourceFactorySpy); - - PlaybackException exception = new PlaybackException("You did bad kid", null, PlaybackException.ERROR_CODE_DECODING_FAILED); + new VideoPlayer( + fakeExoPlayer, + VideoPlayerEventCallbacks.withSink(fakeEventSink), + fakeSurfaceTextureEntry, + fakeVideoPlayerOptions, + httpDataSourceFactorySpy); + + PlaybackException exception = + new PlaybackException( + "You did bad kid", null, PlaybackException.ERROR_CODE_DECODING_FAILED); listeners.forEach(listener -> listener.onPlayerError(exception)); verify(fakeEventSink).error(eq("VideoError"), contains("You did bad kid"), any());