-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Refactor VideoPlayer to be less exposed to EventChannel & related
#6908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
2627784
ee4f836
0334e53
4303968
e27eace
f1f7581
d97a4ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<String, String> 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,37 +110,31 @@ public void configureHttpDataSourceFactory(@NonNull Map<String, String> httpHead | |
| httpDataSourceFactory, httpHeaders, userAgent, httpHeadersNotEmpty); | ||
| } | ||
|
|
||
| private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) { | ||
| 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); | ||
| } | ||
| }); | ||
| private void setUpVideoPlayer(ExoPlayer exoPlayer) { | ||
| this.exoPlayer = exoPlayer; | ||
|
|
||
| surface = new Surface(textureEntry.surfaceTexture()); | ||
| exoPlayer.setVideoSurface(surface); | ||
| setAudioAttributes(exoPlayer, options.mixWithOthers); | ||
|
|
||
| // Avoids synthetic accessor. | ||
| VideoPlayerCallbacks events = this.events; | ||
|
|
||
| exoPlayer.addListener( | ||
| new Listener() { | ||
| private boolean isBuffering = false; | ||
|
|
||
| public void setBuffering(boolean buffering) { | ||
| if (isBuffering != buffering) { | ||
| isBuffering = buffering; | ||
| Map<String, Object> 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 +149,8 @@ public void onPlaybackStateChanged(final int playbackState) { | |
| sendInitialized(); | ||
| } | ||
| } else if (playbackState == Player.STATE_ENDED) { | ||
| Map<String, Object> event = new HashMap<>(); | ||
| event.put("event", "completed"); | ||
| eventSink.success(event); | ||
| events.onCompleted(); | ||
| } | ||
|
|
||
| if (playbackState != Player.STATE_BUFFERING) { | ||
| setBuffering(false); | ||
| } | ||
|
|
@@ -180,30 +163,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<String, Object> event = new HashMap<>(); | ||
| event.put("event", "isPlayingStateUpdate"); | ||
| event.put("isPlaying", isPlaying); | ||
| eventSink.success(event); | ||
| } | ||
| events.onIsPlayingStateUpdate(isPlaying); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| void sendBufferingUpdate() { | ||
| Map<String, Object> event = new HashMap<>(); | ||
| event.put("event", "bufferingUpdate"); | ||
| List<? extends Number> 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,43 +221,36 @@ long getPosition() { | |
| @SuppressWarnings("SuspiciousNameCombination") | ||
| @VisibleForTesting | ||
| void sendInitialized() { | ||
| if (isInitialized) { | ||
| Map<String, Object> 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() { | ||
| if (isInitialized) { | ||
| exoPlayer.stop(); | ||
| } | ||
| textureEntry.release(); | ||
| eventChannel.setStreamHandler(null); | ||
| if (surface != null) { | ||
| surface.release(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // 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; | ||
| import androidx.annotation.Nullable; | ||
|
|
||
| /** | ||
| * Callbacks representing events invoked by {@link VideoPlayer}. | ||
| * | ||
| * <p>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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe link the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
| 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); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // 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; | ||
| import androidx.annotation.Nullable; | ||
| import io.flutter.plugin.common.EventChannel; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done! |
||
| 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<String, Object> 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<String, Object> 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<String, Object> event = new HashMap<>(); | ||
| event.put("values", Collections.singletonList(bufferedPosition)); | ||
| eventSink.success(event); | ||
| } | ||
|
|
||
| @Override | ||
| public void onBufferingEnd() { | ||
| Map<String, Object> event = new HashMap<>(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: All of these events are allocating new HashMap with the same value. They could be private constants to avoid generating garbage. None of them seem that chatty though.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to leave it as-is for now, hopefully we can use |
||
| event.put("event", "bufferingEnd"); | ||
| eventSink.success(event); | ||
| } | ||
|
|
||
| @Override | ||
| public void onCompleted() { | ||
| Map<String, Object> 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<String, Object> event = new HashMap<>(); | ||
| event.put("isPlaying", isPlaying); | ||
| eventSink.success(event); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extreme nit: can this be something like
videoPlayerEventsor something to indicate the type of events?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!