From c290a5fbaacf7f506f7c81d0918b9ccc79945b86 Mon Sep 17 00:00:00 2001 From: David Chopin Date: Tue, 21 Feb 2023 09:12:09 -0600 Subject: [PATCH 1/2] Made it so that a BetterPlayerEvent with a betterPlayerEventType of pipStart is posted whenever we enter pip mode on iOS devices - DC --- ios/Classes/BetterPlayerPlugin.m | 1 + lib/src/core/better_player_controller.dart | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ios/Classes/BetterPlayerPlugin.m b/ios/Classes/BetterPlayerPlugin.m index ed50403fa..63d93499d 100644 --- a/ios/Classes/BetterPlayerPlugin.m +++ b/ios/Classes/BetterPlayerPlugin.m @@ -404,6 +404,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { double width = [argsMap[@"width"] doubleValue]; double height = [argsMap[@"height"] doubleValue]; [player enablePictureInPicture:CGRectMake(left, top, width, height)]; + result(nil); } else if ([@"isPictureInPictureSupported" isEqualToString:call.method]){ if (@available(iOS 9.0, *)){ if ([AVPictureInPictureController isPictureInPictureSupported]){ diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index d1a53bc31..8bd613758 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -1085,12 +1085,14 @@ class BetterPlayerController { return; } final Offset position = renderBox.localToGlobal(Offset.zero); - return videoPlayerController?.enablePictureInPicture( + await videoPlayerController?.enablePictureInPicture( left: position.dx, top: position.dy, width: renderBox.size.width, height: renderBox.size.height, ); + _postEvent(BetterPlayerEvent(BetterPlayerEventType.pipStart)); + return; } else { BetterPlayerUtils.log("Unsupported PiP in current platform."); } From 4c9e0ccce3a83dedf05313336a26e74a0accc917 Mon Sep 17 00:00:00 2001 From: David Chopin Date: Tue, 21 Feb 2023 10:13:58 -0600 Subject: [PATCH 2/2] Added tests for issue #1168 - DC --- lib/src/core/better_player_controller.dart | 5 +- test/better_player_controller_test.dart | 45 ++++++++++ test/mock_build_context.dart | 96 ++++++++++++++++++++++ test/mock_global_key.dart | 9 ++ test/mock_render_box.dart | 11 +++ test/mock_video_player_controller.dart | 6 ++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 test/mock_build_context.dart create mode 100644 test/mock_global_key.dart create mode 100644 test/mock_render_box.dart diff --git a/lib/src/core/better_player_controller.dart b/lib/src/core/better_player_controller.dart index 8bd613758..c85e76305 100644 --- a/lib/src/core/better_player_controller.dart +++ b/lib/src/core/better_player_controller.dart @@ -8,6 +8,7 @@ import 'package:better_player/src/subtitles/better_player_subtitles_factory.dart import 'package:better_player/src/video_player/video_player.dart'; import 'package:better_player/src/video_player/video_player_platform_interface.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; @@ -1067,7 +1068,7 @@ class BetterPlayerController { _wasInFullScreenBeforePiP = _isFullScreen; _wasControlsEnabledBeforePiP = _controlsEnabled; setControlsEnabled(false); - if (Platform.isAndroid) { + if (defaultTargetPlatform == TargetPlatform.android) { _wasInFullScreenBeforePiP = _isFullScreen; await videoPlayerController?.enablePictureInPicture( left: 0, top: 0, width: 0, height: 0); @@ -1075,7 +1076,7 @@ class BetterPlayerController { _postEvent(BetterPlayerEvent(BetterPlayerEventType.pipStart)); return; } - if (Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.iOS) { final RenderBox? renderBox = betterPlayerGlobalKey.currentContext! .findRenderObject() as RenderBox?; if (renderBox == null) { diff --git a/test/better_player_controller_test.dart b/test/better_player_controller_test.dart index e6117df33..8073f81bd 100644 --- a/test/better_player_controller_test.dart +++ b/test/better_player_controller_test.dart @@ -1,7 +1,10 @@ import 'package:better_player/better_player.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; + import 'better_player_mock_controller.dart'; import 'better_player_test_utils.dart'; +import 'mock_global_key.dart'; import 'mock_method_channel.dart'; import 'mock_video_player_controller.dart'; @@ -431,6 +434,48 @@ void main() { await Future.delayed(const Duration(milliseconds: 3000), () {}); expect(eventCount, 3); }); + + test("enablePictureInPicture sends event on iOS", () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final BetterPlayerController betterPlayerMockController = + BetterPlayerTestUtils.setupBetterPlayerMockController(); + final mockVideoPlayerController = + BetterPlayerTestUtils.setupMockVideoPlayerControler(); + mockVideoPlayerController.isPipSupported = true; + betterPlayerMockController.videoPlayerController = + mockVideoPlayerController; + final mockGlobalKey = MockGlobalKey(); + int pipStartCalls = 0; + betterPlayerMockController.addEventsListener((event) { + if (event.betterPlayerEventType == BetterPlayerEventType.pipStart) { + pipStartCalls += 1; + } + }); + betterPlayerMockController.enablePictureInPicture(mockGlobalKey); + await Future.delayed(const Duration(milliseconds: 3000), () {}); + expect(pipStartCalls, 1); + }); + + test("enablePictureInPicture sends event on Android", () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + final BetterPlayerController betterPlayerMockController = + BetterPlayerTestUtils.setupBetterPlayerMockController(); + final mockVideoPlayerController = + BetterPlayerTestUtils.setupMockVideoPlayerControler(); + mockVideoPlayerController.isPipSupported = true; + betterPlayerMockController.videoPlayerController = + mockVideoPlayerController; + final mockGlobalKey = MockGlobalKey(); + int pipStartCalls = 0; + betterPlayerMockController.addEventsListener((event) { + if (event.betterPlayerEventType == BetterPlayerEventType.pipStart) { + pipStartCalls += 1; + } + }); + betterPlayerMockController.enablePictureInPicture(mockGlobalKey); + await Future.delayed(const Duration(milliseconds: 3000), () {}); + expect(pipStartCalls, 1); + }); }, ); } diff --git a/test/mock_build_context.dart b/test/mock_build_context.dart new file mode 100644 index 000000000..ddd964f23 --- /dev/null +++ b/test/mock_build_context.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'mock_render_box.dart'; + +class MockBuildContext extends BuildContext { + @override + bool get debugDoingBuild => throw UnimplementedError(); + + @override + InheritedWidget dependOnInheritedElement(InheritedElement ancestor, + {Object? aspect}) { + throw UnimplementedError(); + } + + @override + T? dependOnInheritedWidgetOfExactType( + {Object? aspect}) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeElement(String name, + {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) { + throw UnimplementedError(); + } + + @override + List describeMissingAncestor( + {required Type expectedAncestorType}) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeOwnershipChain(String name) { + throw UnimplementedError(); + } + + @override + DiagnosticsNode describeWidget(String name, + {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}) { + throw UnimplementedError(); + } + + @override + void dispatchNotification(Notification notification) {} + + @override + T? findAncestorRenderObjectOfType() { + throw UnimplementedError(); + } + + @override + T? findAncestorStateOfType>() { + throw UnimplementedError(); + } + + @override + T? findAncestorWidgetOfExactType() { + throw UnimplementedError(); + } + + @override + RenderObject? findRenderObject() { + return MockRenderBox(); + } + + @override + T? findRootAncestorStateOfType>() { + throw UnimplementedError(); + } + + @override + InheritedElement? + getElementForInheritedWidgetOfExactType() { + throw UnimplementedError(); + } + + @override + bool get mounted => throw UnimplementedError(); + + @override + BuildOwner? get owner => throw UnimplementedError(); + + @override + Size? get size => throw UnimplementedError(); + + @override + void visitAncestorElements(bool Function(Element element) visitor) {} + + @override + void visitChildElements(ElementVisitor visitor) {} + + @override + Widget get widget => throw UnimplementedError(); +} diff --git a/test/mock_global_key.dart b/test/mock_global_key.dart new file mode 100644 index 000000000..a6d5ca9d6 --- /dev/null +++ b/test/mock_global_key.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'mock_build_context.dart'; + +class MockGlobalKey extends GlobalKey { + MockGlobalKey() : super.constructor(); + + @override + BuildContext? get currentContext => MockBuildContext(); +} diff --git a/test/mock_render_box.dart b/test/mock_render_box.dart new file mode 100644 index 000000000..077215fce --- /dev/null +++ b/test/mock_render_box.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class MockRenderBox extends RenderBox { + @override + Offset localToGlobal(Offset point, {RenderObject? ancestor}) { + return Offset(0, 0); + } + + @override + Size get size => Size.zero; +} diff --git a/test/mock_video_player_controller.dart b/test/mock_video_player_controller.dart index b9bceebab..46f6e59b9 100644 --- a/test/mock_video_player_controller.dart +++ b/test/mock_video_player_controller.dart @@ -9,6 +9,7 @@ class MockVideoPlayerController extends VideoPlayerController { bool isLoopingState = false; double volume = 0.0; double speed = 1.0; + bool isPipSupported = false; @override Future play() async { @@ -75,4 +76,9 @@ class MockVideoPlayerController extends VideoPlayerController { String? clearKey, String? videoExtension, }) async {} + + @override + Future isPictureInPictureSupported() { + return Future.value(isPipSupported); + } }