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 all 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
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
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
4 changes: 2 additions & 2 deletions packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_android
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"
Expand All @@ -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