diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index de10f1e2f1dd..821918f57dab 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -135,4 +135,29 @@ and so on. To learn about playback speed limitations, see the [`setPlaybackSpeed` method documentation](https://pub.dev/documentation/video_player/latest/video_player/VideoPlayerController/setPlaybackSpeed.html). +### Picture in Picture + +#### iOS +On iOS the picture in picture is linked to the AVPlayerController. +If you want to enable picture in picture make sure to enable the `audio` capability (in Xcode's UI it will say **Audio, AirPlay, and Picture in Picture**). +Not setting this capability but calling `setPictureInPictureOverlayRectMessage` and `setPictureInPicture` will not start the picture in picture. + +```xml + UIBackgroundModes + + audio + +``` + +Example: +![The example app running in iOS with picture in picture enabled](https://github.com/flutter/plugins/blob/main/packages/video_player/video_player/doc/demo_pip_iphone.gif?raw=true) + +#### Android + +On Android the implementation is different. There is no link to the video player. Your complete app will be minimized ([picture in picutre Android documentation](https://developer.android.com/guide/topics/ui/picture-in-picture)) + +You have multiple options on Android: +- [simple_pip_mode](https://pub.dev/packages/simple_pip_mode) +- Create your own plugin that follows the andorid documentation + Furthermore, see the example app for an example playback speed implementation. diff --git a/packages/video_player/video_player/doc/demo_pip_iphone.gif b/packages/video_player/video_player/doc/demo_pip_iphone.gif new file mode 100644 index 000000000000..19aef62d010c Binary files /dev/null and b/packages/video_player/video_player/doc/demo_pip_iphone.gif differ diff --git a/packages/video_player/video_player/example/ios/Runner/Info.plist b/packages/video_player/video_player/example/ios/Runner/Info.plist index ff775ec6e32e..d8f889040ca0 100644 --- a/packages/video_player/video_player/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player/example/ios/Runner/Info.plist @@ -27,6 +27,10 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index 208cd2fc6c39..775b254ed946 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -207,6 +207,11 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { late VideoPlayerController _controller; + final GlobalKey> _playerKey = + GlobalKey>(); + final Key _pictureInPictureKey = UniqueKey(); + bool _enableStartPictureInPictureAutomaticallyFromInline = false; + Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) .loadString('assets/bumble_bee_captions.vtt'); @@ -243,17 +248,95 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { children: [ Container(padding: const EdgeInsets.only(top: 20.0)), const Text('With remote mp4'), + FutureBuilder( + key: _pictureInPictureKey, + future: _controller.isPictureInPictureSupported(), + builder: (BuildContext context, AsyncSnapshot snapshot) => + Text(snapshot.data ?? false + ? 'Picture in picture is supported' + : 'Picture in picture is not supported'), + ), + Row( + children: [ + const SizedBox(width: 16), + const Expanded( + child: Text( + 'Start picture in picture automatically when going to background'), + ), + Switch( + value: _enableStartPictureInPictureAutomaticallyFromInline, + onChanged: (bool newValue) { + setState(() { + _enableStartPictureInPictureAutomaticallyFromInline = + newValue; + }); + _controller.setAutomaticallyStartPictureInPicture( + enableStartPictureInPictureAutomaticallyFromInline: + _enableStartPictureInPictureAutomaticallyFromInline); + }, + ), + const SizedBox(width: 16), + ], + ), + MaterialButton( + color: Colors.blue, + onPressed: () { + final RenderBox? box = + _playerKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) { + return; + } + final Offset offset = box.localToGlobal(Offset.zero); + _controller.setPictureInPictureOverlayRect( + rect: Rect.fromLTWH( + offset.dx, + offset.dy, + box.size.width, + box.size.height, + ), + ); + }, + child: const Text('Set picture in picture overlay rect'), + ), + MaterialButton( + color: Colors.blue, + onPressed: () { + if (_controller.value.isPictureInPictureActive) { + _controller.stopPictureInPicture(); + } else { + _controller.startPictureInPicture(); + } + }, + child: Text(_controller.value.isPictureInPictureActive + ? 'Stop picture in picture' + : 'Start picture in picture'), + ), Container( padding: const EdgeInsets.all(20), child: AspectRatio( aspectRatio: _controller.value.aspectRatio, child: Stack( + key: _playerKey, alignment: Alignment.bottomCenter, children: [ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), - _ControlsOverlay(controller: _controller), - VideoProgressIndicator(_controller, allowScrubbing: true), + if (_controller.value.isPictureInPictureActive) ...[ + Container(color: Colors.white), + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.picture_in_picture), + SizedBox(height: 8), + Text('This video is playing in picture in picture.'), + ], + ), + ] else ...[ + VideoProgressIndicator(_controller, allowScrubbing: true), + _ControlsOverlay(controller: _controller), + ], ], ), ), diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 0b30e9fb01e7..dcbbd3f32c91 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -37,3 +37,11 @@ flutter: - assets/bumble_bee_captions.srt - assets/bumble_bee_captions.vtt - assets/Audio.mp3 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player: + path: ../../../video_player/video_player + video_player_platform_interface: + path: ../../../video_player/video_player_platform_interface diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 5720e2d9d136..84a39dec1a52 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -47,6 +47,7 @@ class VideoPlayerValue { this.isPlaying = false, this.isLooping = false, this.isBuffering = false, + this.isPictureInPictureActive = false, this.volume = 1.0, this.playbackSpeed = 1.0, this.rotationCorrection = 0, @@ -105,6 +106,9 @@ class VideoPlayerValue { /// The current speed of the playback. final double playbackSpeed; + /// True if picture in picture is currently active. + final bool isPictureInPictureActive; + /// A description of the error if present. /// /// If [hasError] is false this is `null`. @@ -153,6 +157,7 @@ class VideoPlayerValue { bool? isPlaying, bool? isLooping, bool? isBuffering, + bool? isPictureInPictureActive, double? volume, double? playbackSpeed, int? rotationCorrection, @@ -169,6 +174,8 @@ class VideoPlayerValue { isPlaying: isPlaying ?? this.isPlaying, isLooping: isLooping ?? this.isLooping, isBuffering: isBuffering ?? this.isBuffering, + isPictureInPictureActive: + isPictureInPictureActive ?? this.isPictureInPictureActive, volume: volume ?? this.volume, playbackSpeed: playbackSpeed ?? this.playbackSpeed, rotationCorrection: rotationCorrection ?? this.rotationCorrection, @@ -191,6 +198,7 @@ class VideoPlayerValue { 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' 'isBuffering: $isBuffering, ' + 'isPictureInPictureActive: $isPictureInPictureActive, ' 'volume: $volume, ' 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription)'; @@ -399,6 +407,12 @@ class VideoPlayerController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.startingPictureInPicture: + value = value.copyWith(isPictureInPictureActive: true); + break; + case VideoEventType.stoppedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: false); + break; case VideoEventType.unknown: break; } @@ -518,6 +532,53 @@ class VideoPlayerController extends ValueNotifier { await _videoPlayerPlatform.setVolume(_textureId, value.volume); } + /// Returns true if picture in picture is supported on the device. + Future isPictureInPictureSupported() => + _videoPlayerPlatform.isPictureInPictureSupported(); + + /// Enable/disable to start picture in picture automatically when the app goes to the background. + Future setAutomaticallyStartPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setAutomaticallyStartPictureInPicture( + textureId: _textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ); + } + + /// Set the location of the video player view. So picture in picture can use it for animating + Future setPictureInPictureOverlayRect({ + required Rect rect, + }) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setPictureInPictureOverlayRect( + textureId: _textureId, + rect: rect, + ); + } + + /// Start picture in picture mode + Future startPictureInPicture() async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.startPictureInPicture(_textureId); + } + + /// Stop picture in picture mode + Future stopPictureInPicture() async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.stopPictureInPicture(_textureId); + } + Future _applyPlaybackSpeed() async { if (_isDisposedOrNotInitialized) { return; @@ -689,6 +750,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { _VideoAppLifeCycleObserver(this._controller); bool _wasPlayingBeforePause = false; + bool _isPictureInPictureActive = false; final VideoPlayerController _controller; void initialize() { @@ -699,6 +761,10 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { _wasPlayingBeforePause = _controller.value.isPlaying; + _isPictureInPictureActive = _controller.value.isPictureInPictureActive; + if (!_isPictureInPictureActive) { + _controller.pause(); + } _controller.pause(); } else if (state == AppLifecycleState.resumed) { if (_wasPlayingBeforePause) { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index d75456ace469..f57749cc15fa 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.5.1 +version: 2.6.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -31,3 +31,15 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player_android: + path: ../../video_player/video_player_android + video_player_avfoundation: + path: ../../video_player/video_player_avfoundation + video_player_platform_interface: + path: ../../video_player/video_player_platform_interface + video_player_web: + path: ../../video_player/video_player_web diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 663fc9f8e897..c6416adc9422 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -80,6 +80,25 @@ class FakeController extends ValueNotifier Future setClosedCaptionFile( Future? closedCaptionFile, ) async {} + + @override + Future isPictureInPictureSupported() async => true; + + @override + Future setAutomaticallyStartPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) async {} + + @override + Future setPictureInPictureOverlayRect({ + required Rect rect, + }) async {} + + @override + Future startPictureInPicture() async {} + + @override + Future stopPictureInPicture() async {} } Future _loadClosedCaption() async => @@ -933,6 +952,7 @@ void main() { 'isPlaying: true, ' 'isLooping: true, ' 'isBuffering: true, ' + 'isPictureInPictureActive: false, ' 'volume: 0.5, ' 'playbackSpeed: 1.5, ' 'errorDescription: null)'); diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index fb79a77fb2cb..11f4acd3f063 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -243,6 +243,8 @@ class MiniController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.startingPictureInPicture: + case VideoEventType.stoppedPictureInPicture: case VideoEventType.unknown: break; } diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 16ffe17e7ba3..706e26948f99 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -33,3 +33,11 @@ flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player_android: + path: ../../../video_player/video_player_android + video_player_platform_interface: + path: ../../../video_player/video_player_platform_interface diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 3f46ec8a4d79..09774350b5a2 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -26,3 +26,9 @@ dev_dependencies: flutter_test: sdk: flutter pigeon: ^2.0.1 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player_platform_interface: + path: ../../video_player/video_player_platform_interface diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile index fe37427f8a74..9e843931a57d 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Podfile +++ b/packages/video_player/video_player_avfoundation/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 6069bf313e8e..20a56d0e9d15 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1320; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -516,7 +516,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -566,7 +566,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c5858c80e959..0632b6533bc8 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -50,5 +54,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m index f9f66e04bcb3..7548e74b3454 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerTests/VideoPlayerTests.m @@ -3,6 +3,7 @@ // found in the LICENSE file. @import AVFoundation; +@import AVKit; @import video_player_avfoundation; @import XCTest; @@ -233,7 +234,6 @@ - (void)testTransformFix { } }]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; - // Starts paused. AVPlayer *avPlayer = player.player; XCTAssertEqual(avPlayer.rate, 0); @@ -254,6 +254,28 @@ - (void)testTransformFix { XCTAssertNil(error); XCTAssertEqual(avPlayer.volume, 0.1f); + // Set Picture In Picture + NSNumber *isPictureInPictureSupported = [videoPlayerPlugin isPictureInPictureSupported:&error]; + XCTAssertNil(error); + XCTAssertNotNil(isPictureInPictureSupported); + XCTAssertEqual(isPictureInPictureSupported.boolValue, + [AVPictureInPictureController isPictureInPictureSupported]); + if (isPictureInPictureSupported.boolValue) { + FLTStartPictureInPictureMessage *startPictureInPicture = + [FLTStartPictureInPictureMessage makeWithTextureId:textureId]; + XCTestExpectation *startingPiPExpectation = + [self expectationWithDescription:@"startingPictureInPicture"]; + [player onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"startingPictureInPicture"]) { + [startingPiPExpectation fulfill]; + } + }]; + [videoPlayerPlugin startPictureInPicture:startPictureInPicture error:&error]; + XCTAssertNil(error); + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + } + [player onCancelWithArguments:nil]; return initializationEvent; diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m index 54c97030c3ae..5ad801f52825 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -5,6 +5,7 @@ @import os.log; @import XCTest; @import CoreGraphics; +@import AVKit; @interface VideoPlayerUITests : XCTestCase @property(nonatomic, strong) XCUIApplication *app; @@ -31,6 +32,34 @@ - (void)testPlayVideo { XCTAssertTrue([playButton waitForExistenceWithTimeout:30.0]); [playButton tap]; + if ([AVPictureInPictureController isPictureInPictureSupported]) { + XCUIElement *pipSupportedText = app.staticTexts[@"Picture in picture is supported"]; + XCTAssertTrue([pipSupportedText waitForExistenceWithTimeout:30.0]); + + XCUIElement *pipPrepareButton = app.buttons[@"Set picture in picture overlay rect"]; + XCTAssertTrue([pipPrepareButton waitForExistenceWithTimeout:30.0]); + [pipPrepareButton tap]; + + XCUIElement *pipStartButton = app.buttons[@"Start picture in picture"]; + XCTAssertTrue([pipStartButton waitForExistenceWithTimeout:30.0]); + [pipStartButton tap]; + + XCUIElement *pipUIView = app.otherElements[@"PIPUIView"]; + XCTAssertTrue([pipUIView waitForExistenceWithTimeout:30.0]); + + XCUIElement *pipStopButton = app.buttons[@"Stop picture in picture"]; + XCTAssertTrue([pipStopButton waitForExistenceWithTimeout:30.0]); + [pipStopButton tap]; + + XCTAssertTrue([pipStartButton waitForExistenceWithTimeout:30.0]); + + XCTAssertFalse([pipStopButton exists]); + XCTAssertFalse([pipUIView exists]); + } else { + XCTAssertTrue( + [app.staticTexts[@"Picture in picture is not supported"] waitForExistenceWithTimeout:30.0]); + } + NSPredicate *find1xButton = [NSPredicate predicateWithFormat:@"label CONTAINS '1.0x'"]; XCUIElement *playbackSpeed1x = [app.staticTexts elementMatchingPredicate:find1xButton]; BOOL foundPlaybackSpeed1x = [playbackSpeed1x waitForExistenceWithTimeout:30.0]; diff --git a/packages/video_player/video_player_avfoundation/example/lib/main.dart b/packages/video_player/video_player_avfoundation/example/lib/main.dart index d385fd0ee66a..6c72a021a443 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -36,7 +36,10 @@ class _App extends StatelessWidget { icon: Icon(Icons.favorite), text: 'Remote enc m3u8', ), - Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset mp4'), + Tab( + icon: Icon(Icons.insert_drive_file), + text: 'Asset mp4', + ), ], ), ), @@ -115,6 +118,11 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { late MiniController _controller; + final GlobalKey> _playerKey = + GlobalKey>(); + final Key _pictureInPictureKey = UniqueKey(); + bool _enableStartPictureInPictureAutomaticallyFromInline = false; + @override void initState() { super.initState(); @@ -141,9 +149,73 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { children: [ Container(padding: const EdgeInsets.only(top: 20.0)), const Text('With remote mp4'), + FutureBuilder( + key: _pictureInPictureKey, + future: _controller.isPictureInPictureSupported(), + builder: (BuildContext context, AsyncSnapshot snapshot) => + Text(snapshot.data ?? false + ? 'Picture in picture is supported' + : 'Picture in picture is not supported'), + ), + Row( + children: [ + const SizedBox(width: 16), + const Expanded( + child: Text( + 'Start picture in picture automatically when going to background'), + ), + Switch( + value: _enableStartPictureInPictureAutomaticallyFromInline, + onChanged: (bool newValue) { + setState(() { + _enableStartPictureInPictureAutomaticallyFromInline = + newValue; + }); + _controller.setAutomaticallyStartPictureInPicture( + enableStartPictureInPictureAutomaticallyFromInline: + _enableStartPictureInPictureAutomaticallyFromInline); + }, + ), + const SizedBox(width: 16), + ], + ), + MaterialButton( + color: Colors.blue, + onPressed: () { + final RenderBox? box = + _playerKey.currentContext?.findRenderObject() as RenderBox?; + if (box == null) { + return; + } + final Offset offset = box.localToGlobal(Offset.zero); + _controller.setPictureInPictureOverlayRect( + rect: Rect.fromLTWH( + offset.dx, + offset.dy, + box.size.width, + box.size.height, + ), + ); + }, + child: const Text('Set picture in picture overlay rect'), + ), + MaterialButton( + color: Colors.blue, + onPressed: () { + if (_controller.value.isPictureInPictureActive) { + _controller.stopPictureInPicture(); + } else { + _controller.startPictureInPicture(); + } + }, + child: Text(_controller.value.isPictureInPictureActive + ? 'Stop picture in picture' + : 'Start picture in picture'), + ), Container( padding: const EdgeInsets.all(20), child: AspectRatio( + key: _playerKey, aspectRatio: _controller.value.aspectRatio, child: Stack( alignment: Alignment.bottomCenter, diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index fb79a77fb2cb..692abe863126 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -35,6 +35,7 @@ class VideoPlayerValue { this.isInitialized = false, this.isPlaying = false, this.isBuffering = false, + this.isPictureInPictureActive = false, this.playbackSpeed = 1.0, this.errorDescription, }); @@ -70,6 +71,9 @@ class VideoPlayerValue { /// The current speed of the playback. final double playbackSpeed; + /// True if picture in picture is currently active. + final bool isPictureInPictureActive; + /// A description of the error if present. /// /// If [hasError] is false this is `null`. @@ -112,6 +116,7 @@ class VideoPlayerValue { bool? isInitialized, bool? isPlaying, bool? isBuffering, + bool? isPictureInPictureActive, double? playbackSpeed, String? errorDescription, }) { @@ -123,6 +128,8 @@ class VideoPlayerValue { isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, isBuffering: isBuffering ?? this.isBuffering, + isPictureInPictureActive: + isPictureInPictureActive ?? this.isPictureInPictureActive, playbackSpeed: playbackSpeed ?? this.playbackSpeed, errorDescription: errorDescription ?? this.errorDescription, ); @@ -243,6 +250,12 @@ class MiniController extends ValueNotifier { case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); break; + case VideoEventType.startingPictureInPicture: + value = value.copyWith(isPictureInPictureActive: true); + break; + case VideoEventType.stoppedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: false); + break; case VideoEventType.unknown: break; } @@ -338,6 +351,42 @@ class MiniController extends ValueNotifier { await _applyPlaybackSpeed(); } + /// Returns true if picture in picture is supported on the device. + Future isPictureInPictureSupported() { + return _platform.isPictureInPictureSupported(); + } + + /// Enable/disable to start picture in picture automatically when the app goes to the background. + Future setAutomaticallyStartPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + return _platform.setAutomaticallyStartPictureInPicture( + textureId: textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ); + } + + /// Set the location of the video player view. So picture in picture can use it for animating + Future setPictureInPictureOverlayRect({ + required Rect rect, + }) { + return _platform.setPictureInPictureOverlayRect( + textureId: textureId, + rect: rect, + ); + } + + /// Start picture in picture mode + Future startPictureInPicture() { + return _platform.startPictureInPicture(textureId); + } + + /// Stop picture in picture mode + Future stopPictureInPicture() { + return _platform.stopPictureInPicture(textureId); + } + void _updatePosition(Duration position) { value = value.copyWith(position: position); } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 422fb91e35e5..5df066ca9574 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -33,3 +33,11 @@ flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player_avfoundation: + path: ../../../video_player/video_player_avfoundation + video_player_platform_interface: + path: ../../../video_player/video_player_platform_interface diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m index 3b066769621c..9c29d755430d 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/FLTVideoPlayerPlugin.m @@ -3,10 +3,9 @@ // found in the LICENSE file. #import "FLTVideoPlayerPlugin.h" - #import +#import #import - #import "AVAssetTrackUtils.h" #import "messages.g.h" @@ -33,7 +32,8 @@ - (void)onDisplayLink:(CADisplayLink *)link { } @end -@interface FLTVideoPlayer : NSObject +@interface FLTVideoPlayer + : NSObject @property(readonly, nonatomic) AVPlayer *player; @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 @@ -41,8 +41,10 @@ @interface FLTVideoPlayer : NSObject // streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). // An invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams // for issue #1, and restore the correct width and height for issue #2. +// It is also used to start picture in picture @property(readonly, nonatomic) AVPlayerLayer *playerLayer; @property(readonly, nonatomic) CADisplayLink *displayLink; +@property(nonatomic) AVPictureInPictureController *pictureInPictureController; @property(nonatomic) FlutterEventChannel *eventChannel; @property(nonatomic) FlutterEventSink eventSink; @property(nonatomic) CGAffineTransform preferredTransform; @@ -50,6 +52,7 @@ @interface FLTVideoPlayer : NSObject @property(nonatomic, readonly) BOOL isPlaying; @property(nonatomic) BOOL isLooping; @property(nonatomic, readonly) BOOL isInitialized; +@property(nonatomic) BOOL isPictureInPictureStarted; - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FLTFrameUpdater *)frameUpdater httpHeaders:(nonnull NSDictionary *)headers; @@ -247,9 +250,16 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams // for issue #1, and restore the correct width and height for issue #2. + // It is also used to start picture in picture _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; + // We set the opacity to 0.001 because it is an overlay. + // Picture in picture will show a placeholder over other widgets when video_player is used in a + // ScrollView, PageView or in a widget that changes location. + _playerLayer.opacity = 0.001; [rootViewController().view.layer addSublayer:_playerLayer]; + [self setupPipController]; + [self createVideoOutputAndDisplayLink:frameUpdater]; [self addObservers:item]; @@ -259,6 +269,70 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item return self; } +- (void)setupPipController { + if ([AVPictureInPictureController isPictureInPictureSupported]) { + self.pictureInPictureController = + [[AVPictureInPictureController alloc] initWithPlayerLayer:self.playerLayer]; + [self setAutomaticallyStartPictureInPicture:NO]; + self.pictureInPictureController.delegate = self; + } +} + +- (void)setAutomaticallyStartPictureInPicture: + (BOOL)canStartPictureInPictureAutomaticallyFromInline { + if (!self.pictureInPictureController) return; + if (@available(iOS 14.2, *)) { + self.pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = + canStartPictureInPictureAutomaticallyFromInline; + } +} + +- (void)setPictureInPictureOverlayRect:(CGRect)frame { + if (_player) { + self.playerLayer.frame = frame; + } +} + +- (void)startOrStopPictureInPicture:(BOOL)shouldPictureInPictureStart { + if (![AVPictureInPictureController isPictureInPictureSupported] || + self.isPictureInPictureStarted == shouldPictureInPictureStart) { + return; + } + + self.isPictureInPictureStarted = shouldPictureInPictureStart; + if (self.pictureInPictureController && self.isPictureInPictureStarted && + ![self.pictureInPictureController isPictureInPictureActive]) { + if (_eventSink != nil) { + // The event is already send here to make sure that Flutter UI can be updates as soon as + // possible + _eventSink(@{@"event" : @"startingPictureInPicture"}); + } + [self.pictureInPictureController startPictureInPicture]; + } else if (self.pictureInPictureController && !self.isPictureInPictureStarted && + [self.pictureInPictureController isPictureInPictureActive]) { + [self.pictureInPictureController stopPictureInPicture]; + } +} + +#pragma mark - AVPictureInPictureControllerDelegate + +- (void)pictureInPictureControllerDidStopPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { + self.isPictureInPictureStarted = NO; + if (_eventSink != nil) { + _eventSink(@{@"event" : @"stoppedPictureInPicture"}); + } +} + +- (void)pictureInPictureControllerDidStartPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController { + self.isPictureInPictureStarted = YES; + if (_eventSink != nil) { + _eventSink(@{@"event" : @"startingPictureInPicture"}); + } + [self updatePlayingState]; +} + - (void)observeValueForKeyPath:(NSString *)path ofObject:(id)object change:(NSDictionary *)change @@ -658,4 +732,55 @@ - (void)setMixWithOthers:(FLTMixWithOthersMessage *)input } } +- (nullable NSNumber *)isPictureInPictureSupported: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + return @([AVPictureInPictureController isPictureInPictureSupported]); +} + +- (void)setAutomaticallyStartPictureInPicture:(FLTAutomaticallyStartPictureInPictureMessage *)input + error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player + setAutomaticallyStartPictureInPicture:input.enableStartPictureInPictureAutomaticallyFromInline + .boolValue]; +} + +- (void)setPictureInPictureOverlayRect:(FLTSetPictureInPictureOverlayRectMessage *)input + error:(FlutterError **)error { + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player setPictureInPictureOverlayRect:CGRectMake(input.rect.left.floatValue, + input.rect.top.floatValue, + input.rect.width.floatValue, + input.rect.height.floatValue)]; +} + +- (BOOL)doesInfoPlistSupportPictureInPicture:(FlutterError **)error { + NSArray *backgroundModes = [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIBackgroundModes"]; + if (![backgroundModes isKindOfClass:[NSArray class]] || + ![backgroundModes containsObject:@"audio"]) { + *error = [FlutterError errorWithCode:@"video_player" + message:@"missing audio UIBackgroundModes audio in Info.plist" + details:nil]; + return NO; + } + return YES; +} + +- (void)startPictureInPicture:(FLTStartPictureInPictureMessage *)input + error:(FlutterError **)error { + if (![self doesInfoPlistSupportPictureInPicture:error]) { + return; + } + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player startOrStopPictureInPicture:YES]; +} + +- (void)stopPictureInPicture:(FLTStopPictureInPictureMessage *)input error:(FlutterError **)error { + if (![self doesInfoPlistSupportPictureInPicture:error]) { + return; + } + FLTVideoPlayer *player = self.playersByTextureId[input.textureId]; + [player startOrStopPictureInPicture:NO]; +} + @end diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h index 130d4849f372..6a97e700a3ad 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v2.0.1), do not edit directly. +// Autogenerated from Pigeon (v2.0.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -18,6 +18,11 @@ NS_ASSUME_NONNULL_BEGIN @class FLTPositionMessage; @class FLTCreateMessage; @class FLTMixWithOthersMessage; +@class FLTAutomaticallyStartPictureInPictureMessage; +@class FLTSetPictureInPictureOverlayRectMessage; +@class FLTPictureInPictureOverlayRect; +@class FLTStartPictureInPictureMessage; +@class FLTStopPictureInPictureMessage; @interface FLTTextureMessage : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. @@ -80,6 +85,52 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong) NSNumber *mixWithOthers; @end +@interface FLTAutomaticallyStartPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId + enableStartPictureInPictureAutomaticallyFromInline: + (NSNumber *)enableStartPictureInPictureAutomaticallyFromInline; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong) NSNumber *enableStartPictureInPictureAutomaticallyFromInline; +@end + +@interface FLTSetPictureInPictureOverlayRectMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId + rect:(nullable FLTPictureInPictureOverlayRect *)rect; +@property(nonatomic, strong) NSNumber *textureId; +@property(nonatomic, strong, nullable) FLTPictureInPictureOverlayRect *rect; +@end + +@interface FLTPictureInPictureOverlayRect : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTop:(NSNumber *)top + left:(NSNumber *)left + width:(NSNumber *)width + height:(NSNumber *)height; +@property(nonatomic, strong) NSNumber *top; +@property(nonatomic, strong) NSNumber *left; +@property(nonatomic, strong) NSNumber *width; +@property(nonatomic, strong) NSNumber *height; +@end + +@interface FLTStartPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId; +@property(nonatomic, strong) NSNumber *textureId; +@end + +@interface FLTStopPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSNumber *)textureId; +@property(nonatomic, strong) NSNumber *textureId; +@end + /// The codec used by FLTAVFoundationVideoPlayerApi. NSObject *FLTAVFoundationVideoPlayerApiGetCodec(void); @@ -101,6 +152,16 @@ NSObject *FLTAVFoundationVideoPlayerApiGetCodec(void); - (void)pause:(FLTTextureMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(FLTMixWithOthersMessage *)msg error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isPictureInPictureSupported:(FlutterError *_Nullable *_Nonnull)error; +- (void)setPictureInPictureOverlayRect:(FLTSetPictureInPictureOverlayRectMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAutomaticallyStartPictureInPicture:(FLTAutomaticallyStartPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)startPictureInPicture:(FLTStartPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)stopPictureInPicture:(FLTStopPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; @end extern void FLTAVFoundationVideoPlayerApiSetup( diff --git a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m index d82dc386878d..6fd82bfde0d1 100644 --- a/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m +++ b/packages/video_player/video_player_avfoundation/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v2.0.1), do not edit directly. +// Autogenerated from Pigeon (v2.0.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" #import @@ -61,6 +61,26 @@ @interface FLTMixWithOthersMessage () + (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end +@interface FLTAutomaticallyStartPictureInPictureMessage () ++ (FLTAutomaticallyStartPictureInPictureMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTSetPictureInPictureOverlayRectMessage () ++ (FLTSetPictureInPictureOverlayRectMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTPictureInPictureOverlayRect () ++ (FLTPictureInPictureOverlayRect *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTStartPictureInPictureMessage () ++ (FLTStartPictureInPictureMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTStopPictureInPictureMessage () ++ (FLTStopPictureInPictureMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end @implementation FLTTextureMessage + (instancetype)makeWithTextureId:(NSNumber *)textureId { @@ -227,30 +247,171 @@ - (NSDictionary *)toMap { } @end +@implementation FLTAutomaticallyStartPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId + enableStartPictureInPictureAutomaticallyFromInline: + (NSNumber *)enableStartPictureInPictureAutomaticallyFromInline { + FLTAutomaticallyStartPictureInPictureMessage *pigeonResult = + [[FLTAutomaticallyStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.enableStartPictureInPictureAutomaticallyFromInline = + enableStartPictureInPictureAutomaticallyFromInline; + return pigeonResult; +} ++ (FLTAutomaticallyStartPictureInPictureMessage *)fromMap:(NSDictionary *)dict { + FLTAutomaticallyStartPictureInPictureMessage *pigeonResult = + [[FLTAutomaticallyStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.enableStartPictureInPictureAutomaticallyFromInline = + GetNullableObject(dict, @"enableStartPictureInPictureAutomaticallyFromInline"); + NSAssert(pigeonResult.enableStartPictureInPictureAutomaticallyFromInline != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.enableStartPictureInPictureAutomaticallyFromInline + ? self.enableStartPictureInPictureAutomaticallyFromInline + : [NSNull null]), + @"enableStartPictureInPictureAutomaticallyFromInline", nil]; +} +@end + +@implementation FLTSetPictureInPictureOverlayRectMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId + rect:(nullable FLTPictureInPictureOverlayRect *)rect { + FLTSetPictureInPictureOverlayRectMessage *pigeonResult = + [[FLTSetPictureInPictureOverlayRectMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.rect = rect; + return pigeonResult; +} ++ (FLTSetPictureInPictureOverlayRectMessage *)fromMap:(NSDictionary *)dict { + FLTSetPictureInPictureOverlayRectMessage *pigeonResult = + [[FLTSetPictureInPictureOverlayRectMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + pigeonResult.rect = [FLTPictureInPictureOverlayRect fromMap:GetNullableObject(dict, @"rect")]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.rect ? [self.rect toMap] : [NSNull null]), @"rect", nil]; +} +@end + +@implementation FLTPictureInPictureOverlayRect ++ (instancetype)makeWithTop:(NSNumber *)top + left:(NSNumber *)left + width:(NSNumber *)width + height:(NSNumber *)height { + FLTPictureInPictureOverlayRect *pigeonResult = [[FLTPictureInPictureOverlayRect alloc] init]; + pigeonResult.top = top; + pigeonResult.left = left; + pigeonResult.width = width; + pigeonResult.height = height; + return pigeonResult; +} ++ (FLTPictureInPictureOverlayRect *)fromMap:(NSDictionary *)dict { + FLTPictureInPictureOverlayRect *pigeonResult = [[FLTPictureInPictureOverlayRect alloc] init]; + pigeonResult.top = GetNullableObject(dict, @"top"); + NSAssert(pigeonResult.top != nil, @""); + pigeonResult.left = GetNullableObject(dict, @"left"); + NSAssert(pigeonResult.left != nil, @""); + pigeonResult.width = GetNullableObject(dict, @"width"); + NSAssert(pigeonResult.width != nil, @""); + pigeonResult.height = GetNullableObject(dict, @"height"); + NSAssert(pigeonResult.height != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.top ? self.top : [NSNull null]), @"top", + (self.left ? self.left : [NSNull null]), @"left", + (self.width ? self.width : [NSNull null]), @"width", + (self.height ? self.height : [NSNull null]), @"height", nil]; +} +@end + +@implementation FLTStartPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId { + FLTStartPictureInPictureMessage *pigeonResult = [[FLTStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + return pigeonResult; +} ++ (FLTStartPictureInPictureMessage *)fromMap:(NSDictionary *)dict { + FLTStartPictureInPictureMessage *pigeonResult = [[FLTStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return + [NSDictionary dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), + @"textureId", nil]; +} +@end + +@implementation FLTStopPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSNumber *)textureId { + FLTStopPictureInPictureMessage *pigeonResult = [[FLTStopPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + return pigeonResult; +} ++ (FLTStopPictureInPictureMessage *)fromMap:(NSDictionary *)dict { + FLTStopPictureInPictureMessage *pigeonResult = [[FLTStopPictureInPictureMessage alloc] init]; + pigeonResult.textureId = GetNullableObject(dict, @"textureId"); + NSAssert(pigeonResult.textureId != nil, @""); + return pigeonResult; +} +- (NSDictionary *)toMap { + return + [NSDictionary dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), + @"textureId", nil]; +} +@end + @interface FLTAVFoundationVideoPlayerApiCodecReader : FlutterStandardReader @end @implementation FLTAVFoundationVideoPlayerApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FLTCreateMessage fromMap:[self readValue]]; + return [FLTAutomaticallyStartPictureInPictureMessage fromMap:[self readValue]]; case 129: - return [FLTLoopingMessage fromMap:[self readValue]]; + return [FLTCreateMessage fromMap:[self readValue]]; case 130: - return [FLTMixWithOthersMessage fromMap:[self readValue]]; + return [FLTLoopingMessage fromMap:[self readValue]]; case 131: - return [FLTPlaybackSpeedMessage fromMap:[self readValue]]; + return [FLTMixWithOthersMessage fromMap:[self readValue]]; case 132: - return [FLTPositionMessage fromMap:[self readValue]]; + return [FLTPictureInPictureOverlayRect fromMap:[self readValue]]; case 133: - return [FLTTextureMessage fromMap:[self readValue]]; + return [FLTPlaybackSpeedMessage fromMap:[self readValue]]; case 134: + return [FLTPositionMessage fromMap:[self readValue]]; + + case 135: + return [FLTSetPictureInPictureOverlayRectMessage fromMap:[self readValue]]; + + case 136: + return [FLTStartPictureInPictureMessage fromMap:[self readValue]]; + + case 137: + return [FLTStopPictureInPictureMessage fromMap:[self readValue]]; + + case 138: + return [FLTTextureMessage fromMap:[self readValue]]; + + case 139: return [FLTVolumeMessage fromMap:[self readValue]]; default: @@ -263,27 +424,42 @@ @interface FLTAVFoundationVideoPlayerApiCodecWriter : FlutterStandardWriter @end @implementation FLTAVFoundationVideoPlayerApiCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[FLTCreateMessage class]]) { + if ([value isKindOfClass:[FLTAutomaticallyStartPictureInPictureMessage class]]) { [self writeByte:128]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTLoopingMessage class]]) { + } else if ([value isKindOfClass:[FLTCreateMessage class]]) { [self writeByte:129]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTMixWithOthersMessage class]]) { + } else if ([value isKindOfClass:[FLTLoopingMessage class]]) { [self writeByte:130]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTPlaybackSpeedMessage class]]) { + } else if ([value isKindOfClass:[FLTMixWithOthersMessage class]]) { [self writeByte:131]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTPositionMessage class]]) { + } else if ([value isKindOfClass:[FLTPictureInPictureOverlayRect class]]) { [self writeByte:132]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTTextureMessage class]]) { + } else if ([value isKindOfClass:[FLTPlaybackSpeedMessage class]]) { [self writeByte:133]; [self writeValue:[value toMap]]; - } else if ([value isKindOfClass:[FLTVolumeMessage class]]) { + } else if ([value isKindOfClass:[FLTPositionMessage class]]) { [self writeByte:134]; [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTSetPictureInPictureOverlayRectMessage class]]) { + [self writeByte:135]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTStartPictureInPictureMessage class]]) { + [self writeByte:136]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTStopPictureInPictureMessage class]]) { + [self writeByte:137]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTTextureMessage class]]) { + [self writeByte:138]; + [self writeValue:[value toMap]]; + } else if ([value isKindOfClass:[FLTVolumeMessage class]]) { + [self writeByte:139]; + [self writeValue:[value toMap]]; } else { [super writeValue:value]; } @@ -541,4 +717,109 @@ void FLTAVFoundationVideoPlayerApiSetup(id binaryMesseng [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.isPictureInPictureSupported" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isPictureInPictureSupported:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(isPictureInPictureSupported:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSNumber *output = [api isPictureInPictureSupported:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + @"dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPictureInPictureOverlayRect" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setPictureInPictureOverlayRect:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setPictureInPictureOverlayRect:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSetPictureInPictureOverlayRectMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setPictureInPictureOverlayRect:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi." + @"setAutomaticallyStartPictureInPicture" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setAutomaticallyStartPictureInPicture:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setAutomaticallyStartPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTAutomaticallyStartPictureInPictureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setAutomaticallyStartPictureInPicture:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.startPictureInPicture" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(startPictureInPicture:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(startPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTStartPictureInPictureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api startPictureInPicture:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AVFoundationVideoPlayerApi.stopPictureInPicture" + binaryMessenger:binaryMessenger + codec:FLTAVFoundationVideoPlayerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(stopPictureInPicture:error:)], + @"FLTAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(stopPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTStopPictureInPictureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api stopPictureInPicture:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index b5ebedda41e1..4dab81d660d0 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -146,6 +146,10 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return VideoEvent(eventType: VideoEventType.bufferingStart); case 'bufferingEnd': return VideoEvent(eventType: VideoEventType.bufferingEnd); + case 'stoppedPictureInPicture': + return VideoEvent(eventType: VideoEventType.stoppedPictureInPicture); + case 'startingPictureInPicture': + return VideoEvent(eventType: VideoEventType.startingPictureInPicture); default: return VideoEvent(eventType: VideoEventType.unknown); } @@ -163,6 +167,57 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { .setMixWithOthers(MixWithOthersMessage(mixWithOthers: mixWithOthers)); } + @override + Future isPictureInPictureSupported() { + return _api.isPictureInPictureSupported(); + } + + @override + Future setAutomaticallyStartPictureInPicture({ + required int textureId, + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + return _api.setAutomaticallyStartPictureInPicture( + AutomaticallyStartPictureInPictureMessage( + textureId: textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ), + ); + } + + @override + Future setPictureInPictureOverlayRect({ + required int textureId, + required Rect rect, + }) { + return _api.setPictureInPictureOverlayRect( + SetPictureInPictureOverlayRectMessage( + textureId: textureId, + rect: PictureInPictureOverlayRect( + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + ), + ), + ); + } + + @override + Future startPictureInPicture(int textureId) { + return _api.startPictureInPicture(StartPictureInPictureMessage( + textureId: textureId, + )); + } + + @override + Future stopPictureInPicture(int textureId) { + return _api.stopPictureInPicture(StopPictureInPictureMessage( + textureId: textureId, + )); + } + EventChannel _eventChannelFor(int textureId) { return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); } diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index a745c66322d4..5ca8890fac39 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v2.0.1), do not edit directly. +// Autogenerated from Pigeon (v2.0.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // @dart = 2.12 @@ -191,31 +191,176 @@ class MixWithOthersMessage { } } +class AutomaticallyStartPictureInPictureMessage { + AutomaticallyStartPictureInPictureMessage({ + required this.textureId, + required this.enableStartPictureInPictureAutomaticallyFromInline, + }); + + int textureId; + bool enableStartPictureInPictureAutomaticallyFromInline; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + pigeonMap['enableStartPictureInPictureAutomaticallyFromInline'] = + enableStartPictureInPictureAutomaticallyFromInline; + return pigeonMap; + } + + static AutomaticallyStartPictureInPictureMessage decode(Object message) { + final Map pigeonMap = message as Map; + return AutomaticallyStartPictureInPictureMessage( + textureId: pigeonMap['textureId']! as int, + enableStartPictureInPictureAutomaticallyFromInline: + pigeonMap['enableStartPictureInPictureAutomaticallyFromInline']! + as bool, + ); + } +} + +class SetPictureInPictureOverlayRectMessage { + SetPictureInPictureOverlayRectMessage({ + required this.textureId, + this.rect, + }); + + int textureId; + PictureInPictureOverlayRect? rect; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + pigeonMap['rect'] = rect == null ? null : rect!.encode(); + return pigeonMap; + } + + static SetPictureInPictureOverlayRectMessage decode(Object message) { + final Map pigeonMap = message as Map; + return SetPictureInPictureOverlayRectMessage( + textureId: pigeonMap['textureId']! as int, + rect: pigeonMap['rect'] != null + ? PictureInPictureOverlayRect.decode(pigeonMap['rect']!) + : null, + ); + } +} + +class PictureInPictureOverlayRect { + PictureInPictureOverlayRect({ + required this.top, + required this.left, + required this.width, + required this.height, + }); + + double top; + double left; + double width; + double height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['top'] = top; + pigeonMap['left'] = left; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static PictureInPictureOverlayRect decode(Object message) { + final Map pigeonMap = message as Map; + return PictureInPictureOverlayRect( + top: pigeonMap['top']! as double, + left: pigeonMap['left']! as double, + width: pigeonMap['width']! as double, + height: pigeonMap['height']! as double, + ); + } +} + +class StartPictureInPictureMessage { + StartPictureInPictureMessage({ + required this.textureId, + }); + + int textureId; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + return pigeonMap; + } + + static StartPictureInPictureMessage decode(Object message) { + final Map pigeonMap = message as Map; + return StartPictureInPictureMessage( + textureId: pigeonMap['textureId']! as int, + ); + } +} + +class StopPictureInPictureMessage { + StopPictureInPictureMessage({ + required this.textureId, + }); + + int textureId; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + return pigeonMap; + } + + static StopPictureInPictureMessage decode(Object message) { + final Map pigeonMap = message as Map; + return StopPictureInPictureMessage( + textureId: pigeonMap['textureId']! as int, + ); + } +} + class _AVFoundationVideoPlayerApiCodec extends StandardMessageCodec { const _AVFoundationVideoPlayerApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is CreateMessage) { + if (value is AutomaticallyStartPictureInPictureMessage) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is LoopingMessage) { + } else if (value is CreateMessage) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is MixWithOthersMessage) { + } else if (value is LoopingMessage) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is PlaybackSpeedMessage) { + } else if (value is MixWithOthersMessage) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PositionMessage) { + } else if (value is PictureInPictureOverlayRect) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is TextureMessage) { + } else if (value is PlaybackSpeedMessage) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is VolumeMessage) { + } else if (value is PositionMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); + } else if (value is SetPictureInPictureOverlayRectMessage) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is StartPictureInPictureMessage) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is StopPictureInPictureMessage) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -225,24 +370,40 @@ class _AVFoundationVideoPlayerApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return CreateMessage.decode(readValue(buffer)!); + return AutomaticallyStartPictureInPictureMessage.decode( + readValue(buffer)!); case 129: - return LoopingMessage.decode(readValue(buffer)!); + return CreateMessage.decode(readValue(buffer)!); case 130: - return MixWithOthersMessage.decode(readValue(buffer)!); + return LoopingMessage.decode(readValue(buffer)!); case 131: - return PlaybackSpeedMessage.decode(readValue(buffer)!); + return MixWithOthersMessage.decode(readValue(buffer)!); case 132: - return PositionMessage.decode(readValue(buffer)!); + return PictureInPictureOverlayRect.decode(readValue(buffer)!); case 133: - return TextureMessage.decode(readValue(buffer)!); + return PlaybackSpeedMessage.decode(readValue(buffer)!); case 134: + return PositionMessage.decode(readValue(buffer)!); + + case 135: + return SetPictureInPictureOverlayRectMessage.decode(readValue(buffer)!); + + case 136: + return StartPictureInPictureMessage.decode(readValue(buffer)!); + + case 137: + return StopPictureInPictureMessage.decode(readValue(buffer)!); + + case 138: + return TextureMessage.decode(readValue(buffer)!); + + case 139: return VolumeMessage.decode(readValue(buffer)!); default: @@ -535,4 +696,137 @@ class AVFoundationVideoPlayerApi { return; } } + + Future isPictureInPictureSupported() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.isPictureInPictureSupported', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future setPictureInPictureOverlayRect( + SetPictureInPictureOverlayRectMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPictureInPictureOverlayRect', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setAutomaticallyStartPictureInPicture( + AutomaticallyStartPictureInPictureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setAutomaticallyStartPictureInPicture', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future startPictureInPicture( + StartPictureInPictureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.startPictureInPicture', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future stopPictureInPicture(StopPictureInPictureMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.stopPictureInPicture', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_msg]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } } diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 695ff34e3ebd..fdf7bb193bd0 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -16,35 +16,41 @@ import 'package:pigeon/pigeon.dart'; )) class TextureMessage { TextureMessage(this.textureId); + int textureId; } class LoopingMessage { LoopingMessage(this.textureId, this.isLooping); + int textureId; bool isLooping; } class VolumeMessage { VolumeMessage(this.textureId, this.volume); + int textureId; double volume; } class PlaybackSpeedMessage { PlaybackSpeedMessage(this.textureId, this.speed); + int textureId; double speed; } class PositionMessage { PositionMessage(this.textureId, this.position); + int textureId; int position; } class CreateMessage { CreateMessage({required this.httpHeaders}); + String? asset; String? uri; String? packageName; @@ -54,31 +60,105 @@ class CreateMessage { class MixWithOthersMessage { MixWithOthersMessage(this.mixWithOthers); + bool mixWithOthers; } +class AutomaticallyStartPictureInPictureMessage { + AutomaticallyStartPictureInPictureMessage( + this.textureId, + this.enableStartPictureInPictureAutomaticallyFromInline, + ); + + int textureId; + bool enableStartPictureInPictureAutomaticallyFromInline; +} + +class SetPictureInPictureOverlayRectMessage { + SetPictureInPictureOverlayRectMessage( + this.textureId, + this.rect, + ); + + int textureId; + PictureInPictureOverlayRect? rect; +} + +class PictureInPictureOverlayRect { + PictureInPictureOverlayRect({ + required this.top, + required this.left, + required this.width, + required this.height, + }); + + double top; + double left; + double width; + double height; +} + +class StartPictureInPictureMessage { + StartPictureInPictureMessage(this.textureId); + + int textureId; +} + +class StopPictureInPictureMessage { + StopPictureInPictureMessage(this.textureId); + + int textureId; +} + @HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') abstract class AVFoundationVideoPlayerApi { @ObjCSelector('initialize') void initialize(); + @ObjCSelector('create:') TextureMessage create(CreateMessage msg); + @ObjCSelector('dispose:') void dispose(TextureMessage msg); + @ObjCSelector('setLooping:') void setLooping(LoopingMessage msg); + @ObjCSelector('setVolume:') void setVolume(VolumeMessage msg); + @ObjCSelector('setPlaybackSpeed:') void setPlaybackSpeed(PlaybackSpeedMessage msg); + @ObjCSelector('play:') void play(TextureMessage msg); + @ObjCSelector('position:') PositionMessage position(TextureMessage msg); + @ObjCSelector('seekTo:') void seekTo(PositionMessage msg); + @ObjCSelector('pause:') void pause(TextureMessage msg); + @ObjCSelector('setMixWithOthers:') void setMixWithOthers(MixWithOthersMessage msg); + + @ObjCSelector('isPictureInPictureSupported') + bool isPictureInPictureSupported(); + + @ObjCSelector('setPictureInPictureOverlayRect:') + void setPictureInPictureOverlayRect( + SetPictureInPictureOverlayRectMessage msg); + + @ObjCSelector('setAutomaticallyStartPictureInPicture:') + void setAutomaticallyStartPictureInPicture( + AutomaticallyStartPictureInPictureMessage msg); + + @ObjCSelector('startPictureInPicture:') + void startPictureInPicture(StartPictureInPictureMessage msg); + + @ObjCSelector('stopPictureInPicture:') + void stopPictureInPicture(StopPictureInPictureMessage msg); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index a5204137af20..a54ea685e7bd 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.8 +version: 2.4.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -25,3 +25,9 @@ dev_dependencies: flutter_test: sdk: flutter pigeon: ^2.0.1 + + +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + video_player_platform_interface: + path: ../../video_player/video_player_platform_interface diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index e7c3b5ba4ff3..35af9b5a2cbf 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -23,6 +23,11 @@ class _ApiLogger implements TestHostVideoPlayerApi { VolumeMessage? volumeMessage; PlaybackSpeedMessage? playbackSpeedMessage; MixWithOthersMessage? mixWithOthersMessage; + SetPictureInPictureOverlayRectMessage? setPictureInPictureOverlayRectMessage; + AutomaticallyStartPictureInPictureMessage? + automaticallyStartPictureInPictureMessage; + StartPictureInPictureMessage? startPictureInPictureMessage; + StopPictureInPictureMessage? stopPictureInPictureMessage; @override TextureMessage create(CreateMessage arg) { @@ -90,6 +95,38 @@ class _ApiLogger implements TestHostVideoPlayerApi { log.add('setPlaybackSpeed'); playbackSpeedMessage = arg; } + + @override + bool isPictureInPictureSupported() { + log.add('isPictureInPictureSupported'); + return true; + } + + @override + void setAutomaticallyStartPictureInPicture( + AutomaticallyStartPictureInPictureMessage msg) { + log.add('setAutomaticallyStartPictureInPicture'); + automaticallyStartPictureInPictureMessage = msg; + } + + @override + void setPictureInPictureOverlayRect( + SetPictureInPictureOverlayRectMessage msg) { + log.add('setPictureInPictureOverlayRect'); + setPictureInPictureOverlayRectMessage = msg; + } + + @override + void startPictureInPicture(StartPictureInPictureMessage msg) { + log.add('startPictureInPicture'); + startPictureInPictureMessage = msg; + } + + @override + void stopPictureInPicture(StopPictureInPictureMessage msg) { + log.add('stopPictureInPicture'); + stopPictureInPictureMessage = msg; + } } void main() { @@ -233,6 +270,59 @@ void main() { expect(position, const Duration(milliseconds: 234)); }); + test('isPictureInPictureSupported', () async { + final bool isSupported = await player.isPictureInPictureSupported(); + expect(log.log.last, 'isPictureInPictureSupported'); + expect(isSupported, true); + }); + + test('setAutomaticallyStartPictureInPicture true', () async { + await player.setAutomaticallyStartPictureInPicture( + textureId: 1, + enableStartPictureInPictureAutomaticallyFromInline: true); + expect(log.log.last, 'setAutomaticallyStartPictureInPicture'); + expect(log.automaticallyStartPictureInPictureMessage?.textureId, 1); + expect( + log.automaticallyStartPictureInPictureMessage + ?.enableStartPictureInPictureAutomaticallyFromInline, + true); + }); + + test('setAutomaticallyStartPictureInPicture false', () async { + await player.setAutomaticallyStartPictureInPicture( + textureId: 1, + enableStartPictureInPictureAutomaticallyFromInline: false); + expect(log.log.last, 'setAutomaticallyStartPictureInPicture'); + expect(log.automaticallyStartPictureInPictureMessage?.textureId, 1); + expect( + log.automaticallyStartPictureInPictureMessage + ?.enableStartPictureInPictureAutomaticallyFromInline, + false); + }); + + test('setPictureInPictureOverlayRect', () async { + await player.setPictureInPictureOverlayRect( + textureId: 1, rect: const Rect.fromLTWH(0, 1, 2, 3)); + expect(log.log.last, 'setPictureInPictureOverlayRect'); + expect(log.setPictureInPictureOverlayRectMessage?.textureId, 1); + expect(log.setPictureInPictureOverlayRectMessage?.rect?.left, 0); + expect(log.setPictureInPictureOverlayRectMessage?.rect?.top, 1); + expect(log.setPictureInPictureOverlayRectMessage?.rect?.width, 2); + expect(log.setPictureInPictureOverlayRectMessage?.rect?.height, 3); + }); + + test('startPictureInPicture', () async { + await player.startPictureInPicture(1); + expect(log.log.last, 'startPictureInPicture'); + expect(log.startPictureInPictureMessage?.textureId, 1); + }); + + test('stopPictureInPicture', () async { + await player.stopPictureInPicture(1); + expect(log.log.last, 'stopPictureInPicture'); + expect(log.stopPictureInPictureMessage?.textureId, 1); + }); + test('videoEventsFor', () async { _ambiguate(ServicesBinding.instance) ?.defaultBinaryMessenger diff --git a/packages/video_player/video_player_avfoundation/test/test_api.g.dart b/packages/video_player/video_player_avfoundation/test/test_api.g.dart index c8f7bbd026a5..88d521a3a057 100644 --- a/packages/video_player/video_player_avfoundation/test/test_api.g.dart +++ b/packages/video_player/video_player_avfoundation/test/test_api.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v2.0.1), do not edit directly. +// Autogenerated from Pigeon (v2.0.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis // ignore_for_file: avoid_relative_lib_imports @@ -13,7 +13,6 @@ import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - // TODO(gaaclarke): The following output had to be tweaked from a relative path to a uri. import 'package:video_player_avfoundation/src/messages.g.dart'; @@ -21,27 +20,42 @@ class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { const _TestHostVideoPlayerApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is CreateMessage) { + if (value is AutomaticallyStartPictureInPictureMessage) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is LoopingMessage) { + } else if (value is CreateMessage) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is MixWithOthersMessage) { + } else if (value is LoopingMessage) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is PlaybackSpeedMessage) { + } else if (value is MixWithOthersMessage) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is PositionMessage) { + } else if (value is PictureInPictureOverlayRect) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is TextureMessage) { + } else if (value is PlaybackSpeedMessage) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is VolumeMessage) { + } else if (value is PositionMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); + } else if (value is SetPictureInPictureOverlayRectMessage) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is StartPictureInPictureMessage) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is StopPictureInPictureMessage) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is TextureMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -51,24 +65,40 @@ class _TestHostVideoPlayerApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return CreateMessage.decode(readValue(buffer)!); + return AutomaticallyStartPictureInPictureMessage.decode( + readValue(buffer)!); case 129: - return LoopingMessage.decode(readValue(buffer)!); + return CreateMessage.decode(readValue(buffer)!); case 130: - return MixWithOthersMessage.decode(readValue(buffer)!); + return LoopingMessage.decode(readValue(buffer)!); case 131: - return PlaybackSpeedMessage.decode(readValue(buffer)!); + return MixWithOthersMessage.decode(readValue(buffer)!); case 132: - return PositionMessage.decode(readValue(buffer)!); + return PictureInPictureOverlayRect.decode(readValue(buffer)!); case 133: - return TextureMessage.decode(readValue(buffer)!); + return PlaybackSpeedMessage.decode(readValue(buffer)!); case 134: + return PositionMessage.decode(readValue(buffer)!); + + case 135: + return SetPictureInPictureOverlayRectMessage.decode(readValue(buffer)!); + + case 136: + return StartPictureInPictureMessage.decode(readValue(buffer)!); + + case 137: + return StopPictureInPictureMessage.decode(readValue(buffer)!); + + case 138: + return TextureMessage.decode(readValue(buffer)!); + + case 139: return VolumeMessage.decode(readValue(buffer)!); default: @@ -91,6 +121,13 @@ abstract class TestHostVideoPlayerApi { void seekTo(PositionMessage msg); void pause(TextureMessage msg); void setMixWithOthers(MixWithOthersMessage msg); + bool isPictureInPictureSupported(); + void setPictureInPictureOverlayRect( + SetPictureInPictureOverlayRectMessage msg); + void setAutomaticallyStartPictureInPicture( + AutomaticallyStartPictureInPictureMessage msg); + void startPictureInPicture(StartPictureInPictureMessage msg); + void stopPictureInPicture(StopPictureInPictureMessage msg); static void setup(TestHostVideoPlayerApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -301,5 +338,104 @@ abstract class TestHostVideoPlayerApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.isPictureInPictureSupported', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final bool output = api.isPictureInPictureSupported(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPictureInPictureOverlayRect', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPictureInPictureOverlayRect was null.'); + final List args = (message as List?)!; + final SetPictureInPictureOverlayRectMessage? arg_msg = + (args[0] as SetPictureInPictureOverlayRectMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setPictureInPictureOverlayRect was null, expected non-null SetPictureInPictureOverlayRectMessage.'); + api.setPictureInPictureOverlayRect(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.setAutomaticallyStartPictureInPicture', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setAutomaticallyStartPictureInPicture was null.'); + final List args = (message as List?)!; + final AutomaticallyStartPictureInPictureMessage? arg_msg = + (args[0] as AutomaticallyStartPictureInPictureMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.setAutomaticallyStartPictureInPicture was null, expected non-null AutomaticallyStartPictureInPictureMessage.'); + api.setAutomaticallyStartPictureInPicture(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.startPictureInPicture', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.startPictureInPicture was null.'); + final List args = (message as List?)!; + final StartPictureInPictureMessage? arg_msg = + (args[0] as StartPictureInPictureMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.startPictureInPicture was null, expected non-null StartPictureInPictureMessage.'); + api.startPictureInPicture(arg_msg!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AVFoundationVideoPlayerApi.stopPictureInPicture', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.stopPictureInPicture was null.'); + final List args = (message as List?)!; + final StopPictureInPictureMessage? arg_msg = + (args[0] as StopPictureInPictureMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.AVFoundationVideoPlayerApi.stopPictureInPicture was null, expected non-null StopPictureInPictureMessage.'); + api.stopPictureInPicture(arg_msg!); + return {}; + }); + } + } } } diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index e1acbf578027..d6620d5ca56f 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -2,6 +2,10 @@ * Updates minimum Flutter version to 3.0. +## 6.1.0 + +* Added support for picture in picture on iOS + ## 6.0.1 * Fixes comment describing file URI construction. diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index d3df9b25df53..81972602ed2c 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -102,6 +102,39 @@ abstract class VideoPlayerPlatform extends PlatformInterface { Future setMixWithOthers(bool mixWithOthers) { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } + + /// Returns true if picture in picture is supported on the device. + Future isPictureInPictureSupported() async => false; + + /// Enable/disable to start picture in picture automatically when the app goes to the background. + Future setAutomaticallyStartPictureInPicture({ + required int textureId, + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + throw UnimplementedError( + 'setAutomaticallyStartPictureInPicture() has not been implemented.'); + } + + /// Set the location of the video player view. So picture in picture can use it for animating. + Future setPictureInPictureOverlayRect({ + required int textureId, + required Rect rect, + }) { + throw UnimplementedError( + 'setPictureInPictureOverlayRect() has not been implemented.'); + } + + /// Start picture in picture mode + Future startPictureInPicture(int textureId) { + throw UnimplementedError( + 'startPictureInPicture() has not been implemented.'); + } + + /// Stop picture in picture mode + Future stopPictureInPicture(int textureId) { + throw UnimplementedError( + 'stopPictureInPicture() has not been implemented.'); + } } class _PlaceholderImplementation extends VideoPlayerPlatform {} @@ -279,6 +312,12 @@ enum VideoEventType { /// The video stopped to buffer. bufferingEnd, + /// The video starting to picture in picture. + startingPictureInPicture, + + /// The video stopped to picture in picture. + stoppedPictureInPicture, + /// An unknown event has been received. unknown, } diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 8c6a8f400bb2..3a21afb25d13 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/main/packages/video_player/v issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 6.0.1 +version: 6.1.0 environment: sdk: ">=2.12.0 <3.0.0"