[video_player] Android: video_player_android parts of rotationCorrection fix (#5158)
diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md
index 28d0188..d5acda4 100644
--- a/packages/video_player/video_player_android/CHANGELOG.md
+++ b/packages/video_player/video_player_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.3.5
+
+* Sets rotationCorrection for videos recorded in landscapeRight (https://github.com/flutter/flutter/issues/60327).
+
## 2.3.4
* Updates ExoPlayer to 2.17.1.
diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle
index 32cc203..f9ad4e6 100644
--- a/packages/video_player/video_player_android/android/build.gradle
+++ b/packages/video_player/video_player_android/android/build.gradle
@@ -48,7 +48,9 @@
implementation 'com.google.android.exoplayer:exoplayer-dash:2.17.1'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.17.1'
testImplementation 'junit:junit:4.12'
+ testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.mockito:mockito-inline:3.9.0'
+ testImplementation 'org.robolectric:robolectric:4.5'
}
@@ -63,4 +65,4 @@
}
}
}
-}
+}
\ No newline at end of file
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 dc7c881..f215354 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
@@ -11,6 +11,7 @@
import android.net.Uri;
import android.view.Surface;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
@@ -51,11 +52,11 @@
private final TextureRegistry.SurfaceTextureEntry textureEntry;
- private QueuingEventSink eventSink = new QueuingEventSink();
+ private QueuingEventSink eventSink;
private final EventChannel eventChannel;
- private boolean isInitialized = false;
+ @VisibleForTesting boolean isInitialized = false;
private final VideoPlayerOptions options;
@@ -71,10 +72,11 @@
this.textureEntry = textureEntry;
this.options = options;
- exoPlayer = new ExoPlayer.Builder(context).build();
+ ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
Uri uri = Uri.parse(dataSource);
DataSource.Factory dataSourceFactory;
+
if (isHTTP(uri)) {
DefaultHttpDataSource.Factory httpDataSourceFactory =
new DefaultHttpDataSource.Factory()
@@ -90,10 +92,26 @@
}
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
+
exoPlayer.setMediaSource(mediaSource);
exoPlayer.prepare();
- setupVideoPlayer(eventChannel, textureEntry);
+ setUpVideoPlayer(exoPlayer, new QueuingEventSink());
+ }
+
+ // Constructor used to directly test members of this class.
+ @VisibleForTesting
+ VideoPlayer(
+ ExoPlayer exoPlayer,
+ EventChannel eventChannel,
+ TextureRegistry.SurfaceTextureEntry textureEntry,
+ VideoPlayerOptions options,
+ QueuingEventSink eventSink) {
+ this.eventChannel = eventChannel;
+ this.textureEntry = textureEntry;
+ this.options = options;
+
+ setUpVideoPlayer(exoPlayer, eventSink);
}
private static boolean isHTTP(Uri uri) {
@@ -106,7 +124,6 @@
private MediaSource buildMediaSource(
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
-
int type;
if (formatHint == null) {
type = Util.inferContentType(uri.getLastPathSegment());
@@ -153,8 +170,10 @@
}
}
- private void setupVideoPlayer(
- EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) {
+ private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
+ this.exoPlayer = exoPlayer;
+ this.eventSink = eventSink;
+
eventChannel.setStreamHandler(
new EventChannel.StreamHandler() {
@Override
@@ -264,7 +283,8 @@
}
@SuppressWarnings("SuspiciousNameCombination")
- private void sendInitialized() {
+ @VisibleForTesting
+ void sendInitialized() {
if (isInitialized) {
Map<String, Object> event = new HashMap<>();
event.put("event", "initialized");
@@ -282,7 +302,16 @@
}
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);
+ }
}
+
eventSink.success(event);
}
}
diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java
new file mode 100644
index 0000000..2ed1165
--- /dev/null
+++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java
@@ -0,0 +1,15 @@
+// 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 org.junit.Test;
+
+public class VideoPlayerPluginTest {
+ // This is only a placeholder test and doesn't actually initialize the plugin.
+ @Test
+ public void initPluginDoesNotThrow() {
+ final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
+ }
+}
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 ec960b7..194f790 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
@@ -4,12 +4,154 @@
package io.flutter.plugins.videoplayer;
-import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.view.TextureRegistry;
+import java.util.HashMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
public class VideoPlayerTest {
- // This is only a placeholder test and doesn't actually initialize the plugin.
+ private ExoPlayer fakeExoPlayer;
+ private EventChannel fakeEventChannel;
+ private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
+ private VideoPlayerOptions fakeVideoPlayerOptions;
+ private QueuingEventSink fakeEventSink;
+
+ @Captor private ArgumentCaptor<HashMap<String, Object>> eventCaptor;
+
+ @Before
+ public void before() {
+ MockitoAnnotations.openMocks(this);
+
+ fakeExoPlayer = mock(ExoPlayer.class);
+ fakeEventChannel = mock(EventChannel.class);
+ fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
+ fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
+ fakeEventSink = mock(QueuingEventSink.class);
+ }
+
@Test
- public void initPluginDoesNotThrow() {
- final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
+ public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
+ VideoPlayer videoPlayer =
+ new VideoPlayer(
+ fakeExoPlayer,
+ fakeEventChannel,
+ fakeSurfaceTextureEntry,
+ fakeVideoPlayerOptions,
+ fakeEventSink);
+ Format testFormat =
+ new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
+
+ when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+ videoPlayer.isInitialized = true;
+ videoPlayer.sendInitialized();
+
+ verify(fakeEventSink).success(eventCaptor.capture());
+ HashMap<String, Object> event = 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);
+ }
+
+ @Test
+ public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
+ VideoPlayer videoPlayer =
+ new VideoPlayer(
+ fakeExoPlayer,
+ fakeEventChannel,
+ fakeSurfaceTextureEntry,
+ fakeVideoPlayerOptions,
+ fakeEventSink);
+ Format testFormat =
+ new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
+
+ when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+ videoPlayer.isInitialized = true;
+ videoPlayer.sendInitialized();
+
+ verify(fakeEventSink).success(eventCaptor.capture());
+ HashMap<String, Object> event = 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);
+ }
+
+ @Test
+ public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
+ VideoPlayer videoPlayer =
+ new VideoPlayer(
+ fakeExoPlayer,
+ fakeEventChannel,
+ fakeSurfaceTextureEntry,
+ fakeVideoPlayerOptions,
+ fakeEventSink);
+ Format testFormat =
+ new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
+
+ when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+ videoPlayer.isInitialized = true;
+ videoPlayer.sendInitialized();
+
+ verify(fakeEventSink).success(eventCaptor.capture());
+ HashMap<String, Object> event = 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);
+ }
+
+ @Test
+ public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
+ VideoPlayer videoPlayer =
+ new VideoPlayer(
+ fakeExoPlayer,
+ fakeEventChannel,
+ fakeSurfaceTextureEntry,
+ fakeVideoPlayerOptions,
+ fakeEventSink);
+ Format testFormat =
+ new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
+
+ when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
+ when(fakeExoPlayer.getDuration()).thenReturn(10L);
+
+ videoPlayer.isInitialized = true;
+ videoPlayer.sendInitialized();
+
+ verify(fakeEventSink).success(eventCaptor.capture());
+ HashMap<String, Object> event = 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"), 180);
}
}
diff --git a/packages/video_player/video_player_android/lib/src/android_video_player.dart b/packages/video_player/video_player_android/lib/src/android_video_player.dart
index 5c5fd80..cee6d7d 100644
--- a/packages/video_player/video_player_android/lib/src/android_video_player.dart
+++ b/packages/video_player/video_player_android/lib/src/android_video_player.dart
@@ -130,6 +130,7 @@
duration: Duration(milliseconds: map['duration'] as int),
size: Size((map['width'] as num?)?.toDouble() ?? 0.0,
(map['height'] as num?)?.toDouble() ?? 0.0),
+ rotationCorrection: map['rotationCorrection'] as int? ?? 0,
);
case 'completed':
return VideoEvent(
diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml
index 7e23b32..367b95e 100644
--- a/packages/video_player/video_player_android/pubspec.yaml
+++ b/packages/video_player/video_player_android/pubspec.yaml
@@ -2,7 +2,7 @@
description: Android implementation of the video_player plugin.
repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.3.4
+version: 2.3.5
environment:
sdk: ">=2.14.0 <3.0.0"
@@ -20,7 +20,7 @@
dependencies:
flutter:
sdk: flutter
- video_player_platform_interface: ">=4.2.0 <6.0.0"
+ video_player_platform_interface: ^5.1.1
dev_dependencies:
flutter_test:
diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart
index b7cf763..aad31e4 100644
--- a/packages/video_player/video_player_android/test/android_video_player_test.dart
+++ b/packages/video_player/video_player_android/test/android_video_player_test.dart
@@ -259,6 +259,20 @@
'flutter.io/videoPlayer/videoEvents123',
const StandardMethodCodec()
.encodeSuccessEnvelope(<String, dynamic>{
+ 'event': 'initialized',
+ 'duration': 98765,
+ 'width': 1920,
+ 'height': 1080,
+ 'rotationCorrection': 180,
+ }),
+ (ByteData? data) {});
+
+ await _ambiguate(ServicesBinding.instance)
+ ?.defaultBinaryMessenger
+ .handlePlatformMessage(
+ 'flutter.io/videoPlayer/videoEvents123',
+ const StandardMethodCodec()
+ .encodeSuccessEnvelope(<String, dynamic>{
'event': 'completed',
}),
(ByteData? data) {});
@@ -312,6 +326,13 @@
eventType: VideoEventType.initialized,
duration: const Duration(milliseconds: 98765),
size: const Size(1920, 1080),
+ rotationCorrection: 0,
+ ),
+ VideoEvent(
+ eventType: VideoEventType.initialized,
+ duration: const Duration(milliseconds: 98765),
+ size: const Size(1920, 1080),
+ rotationCorrection: 180,
),
VideoEvent(eventType: VideoEventType.completed),
VideoEvent(