Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[video_player] Android: video_player_android parts of rotationCorrection fix #5158

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.3.4

* Updates ExoPlayer to 2.17.1.
* Sets rotationCorrection for videos recorded in landscapeRight (https://github.com/flutter/flutter/issues/60327).

## 2.3.3

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ android {
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'
}


Expand All @@ -63,4 +65,4 @@ android {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,11 +52,11 @@ final class VideoPlayer {

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;

Expand All @@ -71,10 +72,11 @@ final class VideoPlayer {
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()
Expand All @@ -90,10 +92,26 @@ final class VideoPlayer {
}

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) {
Expand All @@ -106,7 +124,6 @@ private static boolean isHTTP(Uri uri) {

private MediaSource buildMediaSource(
Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) {

int type;
if (formatHint == null) {
type = Util.inferContentType(uri.getLastPathSegment());
Expand Down Expand Up @@ -153,8 +170,10 @@ private MediaSource buildMediaSource(
}
}

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
Expand Down Expand Up @@ -264,7 +283,8 @@ long getPosition() {
}

@SuppressWarnings("SuspiciousNameCombination")
private void sendInitialized() {
@VisibleForTesting
void sendInitialized() {
if (isInitialized) {
Map<String, Object> event = new HashMap<>();
event.put("event", "initialized");
Expand All @@ -282,7 +302,16 @@ private void sendInitialized() {
}
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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any other use case where rotationDegrees is useful? I was wondering if sending the value as rotationDegrees could be beneficial. Other than that, this change LGTM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rotationCorrection is the name it has in the (platform-interface-level) VideoEvent that this populates, so for now it's the clearest name for the event.

Now that channels are per-platform-implementation package, if we have a second use for this within Android in the future we can trivially change it at that point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also see #3820 (comment) where we discussed this previously.

I think it would be nice to eventually pass along the rotationDegrees info, but we'd probably want to provide that on all platforms if possible (not just Android).

event.put("rotationCorrection", rotationDegrees);
}
}

eventSink.success(event);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,154 @@

package io.flutter.plugins.videoplayer;

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 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 initPluginDoesNotThrow() {
final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
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(
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ flutter:
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,20 @@ void main() {
}),
(ByteData? data) {});

await _ambiguate(ServicesBinding.instance)
?.defaultBinaryMessenger
.handlePlatformMessage(
'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(
Expand Down Expand Up @@ -312,6 +326,13 @@ void main() {
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(
Expand Down