Skip to content

Commit 9381a8b

Browse files
KyleFinmvanbeusekom
authored andcommitted
[video_player] Android: video_player_android parts of rotationCorrection fix (flutter#5158)
1 parent d5c0769 commit 9381a8b

File tree

8 files changed

+228
-14
lines changed

8 files changed

+228
-14
lines changed

packages/video_player/video_player_android/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.5
2+
3+
* Sets rotationCorrection for videos recorded in landscapeRight (https://github.com/flutter/flutter/issues/60327).
4+
15
## 2.3.4
26

37
* Updates ExoPlayer to 2.17.1.

packages/video_player/video_player_android/android/build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ android {
4848
implementation 'com.google.android.exoplayer:exoplayer-dash:2.17.1'
4949
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.17.1'
5050
testImplementation 'junit:junit:4.12'
51+
testImplementation 'androidx.test:core:1.3.0'
5152
testImplementation 'org.mockito:mockito-inline:3.9.0'
53+
testImplementation 'org.robolectric:robolectric:4.5'
5254
}
5355

5456

@@ -63,4 +65,4 @@ android {
6365
}
6466
}
6567
}
66-
}
68+
}

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java

+37-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.net.Uri;
1212
import android.view.Surface;
1313
import androidx.annotation.NonNull;
14+
import androidx.annotation.VisibleForTesting;
1415
import com.google.android.exoplayer2.C;
1516
import com.google.android.exoplayer2.ExoPlayer;
1617
import com.google.android.exoplayer2.Format;
@@ -51,11 +52,11 @@ final class VideoPlayer {
5152

5253
private final TextureRegistry.SurfaceTextureEntry textureEntry;
5354

54-
private QueuingEventSink eventSink = new QueuingEventSink();
55+
private QueuingEventSink eventSink;
5556

5657
private final EventChannel eventChannel;
5758

58-
private boolean isInitialized = false;
59+
@VisibleForTesting boolean isInitialized = false;
5960

6061
private final VideoPlayerOptions options;
6162

@@ -71,10 +72,11 @@ final class VideoPlayer {
7172
this.textureEntry = textureEntry;
7273
this.options = options;
7374

74-
exoPlayer = new ExoPlayer.Builder(context).build();
75+
ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
7576

7677
Uri uri = Uri.parse(dataSource);
7778
DataSource.Factory dataSourceFactory;
79+
7880
if (isHTTP(uri)) {
7981
DefaultHttpDataSource.Factory httpDataSourceFactory =
8082
new DefaultHttpDataSource.Factory()
@@ -90,10 +92,26 @@ final class VideoPlayer {
9092
}
9193

9294
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
95+
9396
exoPlayer.setMediaSource(mediaSource);
9497
exoPlayer.prepare();
9598

96-
setupVideoPlayer(eventChannel, textureEntry);
99+
setUpVideoPlayer(exoPlayer, new QueuingEventSink());
100+
}
101+
102+
// Constructor used to directly test members of this class.
103+
@VisibleForTesting
104+
VideoPlayer(
105+
ExoPlayer exoPlayer,
106+
EventChannel eventChannel,
107+
TextureRegistry.SurfaceTextureEntry textureEntry,
108+
VideoPlayerOptions options,
109+
QueuingEventSink eventSink) {
110+
this.eventChannel = eventChannel;
111+
this.textureEntry = textureEntry;
112+
this.options = options;
113+
114+
setUpVideoPlayer(exoPlayer, eventSink);
97115
}
98116

99117
private static boolean isHTTP(Uri uri) {
@@ -106,7 +124,6 @@ private static boolean isHTTP(Uri uri) {
106124

107125
private MediaSource buildMediaSource(
108126
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {
109-
110127
int type;
111128
if (formatHint == null) {
112129
type = Util.inferContentType(uri.getLastPathSegment());
@@ -153,8 +170,10 @@ private MediaSource buildMediaSource(
153170
}
154171
}
155172

156-
private void setupVideoPlayer(
157-
EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) {
173+
private void setUpVideoPlayer(ExoPlayer exoPlayer, QueuingEventSink eventSink) {
174+
this.exoPlayer = exoPlayer;
175+
this.eventSink = eventSink;
176+
158177
eventChannel.setStreamHandler(
159178
new EventChannel.StreamHandler() {
160179
@Override
@@ -264,7 +283,8 @@ long getPosition() {
264283
}
265284

266285
@SuppressWarnings("SuspiciousNameCombination")
267-
private void sendInitialized() {
286+
@VisibleForTesting
287+
void sendInitialized() {
268288
if (isInitialized) {
269289
Map<String, Object> event = new HashMap<>();
270290
event.put("event", "initialized");
@@ -282,7 +302,16 @@ private void sendInitialized() {
282302
}
283303
event.put("width", width);
284304
event.put("height", height);
305+
306+
// Rotating the video with ExoPlayer does not seem to be possible with a Surface,
307+
// so inform the Flutter code that the widget needs to be rotated to prevent
308+
// upside-down playback for videos with rotationDegrees of 180 (other orientations work
309+
// correctly without correction).
310+
if (rotationDegrees == 180) {
311+
event.put("rotationCorrection", rotationDegrees);
312+
}
285313
}
314+
286315
eventSink.success(event);
287316
}
288317
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.videoplayer;
6+
7+
import org.junit.Test;
8+
9+
public class VideoPlayerPluginTest {
10+
// This is only a placeholder test and doesn't actually initialize the plugin.
11+
@Test
12+
public void initPluginDoesNotThrow() {
13+
final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
14+
}
15+
}

packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java

+145-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,154 @@
44

55
package io.flutter.plugins.videoplayer;
66

7+
import static org.junit.Assert.assertEquals;
8+
import static org.mockito.Mockito.mock;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
11+
12+
import com.google.android.exoplayer2.ExoPlayer;
13+
import com.google.android.exoplayer2.Format;
14+
import io.flutter.plugin.common.EventChannel;
15+
import io.flutter.view.TextureRegistry;
16+
import java.util.HashMap;
17+
import org.junit.Before;
718
import org.junit.Test;
19+
import org.junit.runner.RunWith;
20+
import org.mockito.ArgumentCaptor;
21+
import org.mockito.Captor;
22+
import org.mockito.MockitoAnnotations;
23+
import org.robolectric.RobolectricTestRunner;
824

25+
@RunWith(RobolectricTestRunner.class)
926
public class VideoPlayerTest {
10-
// This is only a placeholder test and doesn't actually initialize the plugin.
27+
private ExoPlayer fakeExoPlayer;
28+
private EventChannel fakeEventChannel;
29+
private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
30+
private VideoPlayerOptions fakeVideoPlayerOptions;
31+
private QueuingEventSink fakeEventSink;
32+
33+
@Captor private ArgumentCaptor<HashMap<String, Object>> eventCaptor;
34+
35+
@Before
36+
public void before() {
37+
MockitoAnnotations.openMocks(this);
38+
39+
fakeExoPlayer = mock(ExoPlayer.class);
40+
fakeEventChannel = mock(EventChannel.class);
41+
fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
42+
fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
43+
fakeEventSink = mock(QueuingEventSink.class);
44+
}
45+
46+
@Test
47+
public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
48+
VideoPlayer videoPlayer =
49+
new VideoPlayer(
50+
fakeExoPlayer,
51+
fakeEventChannel,
52+
fakeSurfaceTextureEntry,
53+
fakeVideoPlayerOptions,
54+
fakeEventSink);
55+
Format testFormat =
56+
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
57+
58+
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
59+
when(fakeExoPlayer.getDuration()).thenReturn(10L);
60+
61+
videoPlayer.isInitialized = true;
62+
videoPlayer.sendInitialized();
63+
64+
verify(fakeEventSink).success(eventCaptor.capture());
65+
HashMap<String, Object> event = eventCaptor.getValue();
66+
67+
assertEquals(event.get("event"), "initialized");
68+
assertEquals(event.get("duration"), 10L);
69+
assertEquals(event.get("width"), 200);
70+
assertEquals(event.get("height"), 100);
71+
assertEquals(event.get("rotationCorrection"), null);
72+
}
73+
1174
@Test
12-
public void initPluginDoesNotThrow() {
13-
final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
75+
public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
76+
VideoPlayer videoPlayer =
77+
new VideoPlayer(
78+
fakeExoPlayer,
79+
fakeEventChannel,
80+
fakeSurfaceTextureEntry,
81+
fakeVideoPlayerOptions,
82+
fakeEventSink);
83+
Format testFormat =
84+
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
85+
86+
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
87+
when(fakeExoPlayer.getDuration()).thenReturn(10L);
88+
89+
videoPlayer.isInitialized = true;
90+
videoPlayer.sendInitialized();
91+
92+
verify(fakeEventSink).success(eventCaptor.capture());
93+
HashMap<String, Object> event = eventCaptor.getValue();
94+
95+
assertEquals(event.get("event"), "initialized");
96+
assertEquals(event.get("duration"), 10L);
97+
assertEquals(event.get("width"), 200);
98+
assertEquals(event.get("height"), 100);
99+
assertEquals(event.get("rotationCorrection"), null);
100+
}
101+
102+
@Test
103+
public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
104+
VideoPlayer videoPlayer =
105+
new VideoPlayer(
106+
fakeExoPlayer,
107+
fakeEventChannel,
108+
fakeSurfaceTextureEntry,
109+
fakeVideoPlayerOptions,
110+
fakeEventSink);
111+
Format testFormat =
112+
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
113+
114+
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
115+
when(fakeExoPlayer.getDuration()).thenReturn(10L);
116+
117+
videoPlayer.isInitialized = true;
118+
videoPlayer.sendInitialized();
119+
120+
verify(fakeEventSink).success(eventCaptor.capture());
121+
HashMap<String, Object> event = eventCaptor.getValue();
122+
123+
assertEquals(event.get("event"), "initialized");
124+
assertEquals(event.get("duration"), 10L);
125+
assertEquals(event.get("width"), 100);
126+
assertEquals(event.get("height"), 200);
127+
assertEquals(event.get("rotationCorrection"), null);
128+
}
129+
130+
@Test
131+
public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
132+
VideoPlayer videoPlayer =
133+
new VideoPlayer(
134+
fakeExoPlayer,
135+
fakeEventChannel,
136+
fakeSurfaceTextureEntry,
137+
fakeVideoPlayerOptions,
138+
fakeEventSink);
139+
Format testFormat =
140+
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
141+
142+
when(fakeExoPlayer.getVideoFormat()).thenReturn(testFormat);
143+
when(fakeExoPlayer.getDuration()).thenReturn(10L);
144+
145+
videoPlayer.isInitialized = true;
146+
videoPlayer.sendInitialized();
147+
148+
verify(fakeEventSink).success(eventCaptor.capture());
149+
HashMap<String, Object> event = eventCaptor.getValue();
150+
151+
assertEquals(event.get("event"), "initialized");
152+
assertEquals(event.get("duration"), 10L);
153+
assertEquals(event.get("width"), 100);
154+
assertEquals(event.get("height"), 200);
155+
assertEquals(event.get("rotationCorrection"), 180);
14156
}
15157
}

packages/video_player/video_player_android/lib/src/android_video_player.dart

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
130130
duration: Duration(milliseconds: map['duration'] as int),
131131
size: Size((map['width'] as num?)?.toDouble() ?? 0.0,
132132
(map['height'] as num?)?.toDouble() ?? 0.0),
133+
rotationCorrection: map['rotationCorrection'] as int? ?? 0,
133134
);
134135
case 'completed':
135136
return VideoEvent(

packages/video_player/video_player_android/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_android
22
description: Android implementation of the video_player plugin.
33
repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.3.4
5+
version: 2.3.5
66

77
environment:
88
sdk: ">=2.14.0 <3.0.0"
@@ -20,7 +20,7 @@ flutter:
2020
dependencies:
2121
flutter:
2222
sdk: flutter
23-
video_player_platform_interface: ">=4.2.0 <6.0.0"
23+
video_player_platform_interface: ^5.1.1
2424

2525
dev_dependencies:
2626
flutter_test:

packages/video_player/video_player_android/test/android_video_player_test.dart

+21
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,20 @@ void main() {
253253
}),
254254
(ByteData? data) {});
255255

256+
await _ambiguate(ServicesBinding.instance)
257+
?.defaultBinaryMessenger
258+
.handlePlatformMessage(
259+
'flutter.io/videoPlayer/videoEvents123',
260+
const StandardMethodCodec()
261+
.encodeSuccessEnvelope(<String, dynamic>{
262+
'event': 'initialized',
263+
'duration': 98765,
264+
'width': 1920,
265+
'height': 1080,
266+
'rotationCorrection': 180,
267+
}),
268+
(ByteData? data) {});
269+
256270
await _ambiguate(ServicesBinding.instance)
257271
?.defaultBinaryMessenger
258272
.handlePlatformMessage(
@@ -312,6 +326,13 @@ void main() {
312326
eventType: VideoEventType.initialized,
313327
duration: const Duration(milliseconds: 98765),
314328
size: const Size(1920, 1080),
329+
rotationCorrection: 0,
330+
),
331+
VideoEvent(
332+
eventType: VideoEventType.initialized,
333+
duration: const Duration(milliseconds: 98765),
334+
size: const Size(1920, 1080),
335+
rotationCorrection: 180,
315336
),
316337
VideoEvent(eventType: VideoEventType.completed),
317338
VideoEvent(

0 commit comments

Comments
 (0)