diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index aff0afef21f..97f35ac2699 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the camera plugin. publish_to: none environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" dependencies: @@ -18,6 +18,7 @@ dependencies: sdk: flutter path_provider: ^2.0.0 video_player: ^2.7.0 + video_player_platform_interface: ^6.2.3 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 22a4bc0ed80..a3cd59102cd 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.10.0 -* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Updates minimum supported SDK version to Flutter 3.22/Dart 3.6. +* Adds support for picture-in-picture on iOS. ## 2.9.2 @@ -73,6 +74,7 @@ receives an`Uri` instead of a `String` url. * Deprecates `VideoPlayerController.network` factory method. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. + ## 2.6.1 * Synchronizes `VideoPlayerValue.isPlaying` with underlying video player. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 094bdd66699..41e407b782b 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -140,3 +140,25 @@ 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). Furthermore, see the example app for an example playback speed implementation. + +### Picture-in-Picture + +#### iOS +To enable picture-in-picture functionality, you need to add the **Background Modes** capabilities for **Audio, AirPlay, and Picture in Picture** as described in [Configuring your app for media playback > Configure the background modes](https://developer.apple.com/documentation/AVFoundation/configuring-your-app-for-media-playback#Configure-the-background-modes). Resulting in a new string entry `audio` in the array value of the entry `UIBackgroundModes` in your `Info.plist` file, which is located in `/ios/Runner/Info.plist`: + +```xml + UIBackgroundModes + + audio + +``` + +> [!IMPORTANT] +> Failing to add the `audio` **Background Modes** capability will result in a silent failure to start picture-in-picture playback. + +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, picture-in-picture mode is implemented at the application level rather than the video element level, so this plugin does not implement picture-in-picture mode on Android. 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 00000000000..19aef62d010 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 4e29652e6d2..1f3167d5a5f 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 0ce77d603b5..8537742f264 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -213,6 +213,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'); @@ -250,17 +255,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.setAutomaticallyStartsPictureInPicture( + 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.setPictureInPictureOverlaySettings( + settings: PictureInPictureOverlaySettings( + 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), + const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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 5755db029dd..5f455b87038 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the video_player plugin. publish_to: none environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" dependencies: @@ -35,3 +35,12 @@ 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_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/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index b7ba8340fa6..5f5b38da7a3 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -17,6 +17,7 @@ export 'package:video_player_platform_interface/video_player_platform_interface. show DataSourceType, DurationRange, + PictureInPictureOverlaySettings, VideoFormat, VideoPlayerOptions, VideoPlayerWebOptions, @@ -54,6 +55,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, @@ -113,6 +115,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`. @@ -167,6 +172,7 @@ class VideoPlayerValue { bool? isPlaying, bool? isLooping, bool? isBuffering, + bool? isPictureInPictureActive, double? volume, double? playbackSpeed, int? rotationCorrection, @@ -184,6 +190,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, @@ -207,6 +215,7 @@ class VideoPlayerValue { 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' 'isBuffering: $isBuffering, ' + 'isPictureInPictureActive: $isPictureInPictureActive, ' 'volume: $volume, ' 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription, ' @@ -226,6 +235,7 @@ class VideoPlayerValue { isPlaying == other.isPlaying && isLooping == other.isLooping && isBuffering == other.isBuffering && + isPictureInPictureActive == other.isPictureInPictureActive && volume == other.volume && playbackSpeed == other.playbackSpeed && errorDescription == other.errorDescription && @@ -492,6 +502,10 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith(isBuffering: true); case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); + case VideoEventType.startedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: true); + case VideoEventType.stoppedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: false); case VideoEventType.isPlayingStateUpdate: if (event.isPlaying ?? false) { value = @@ -618,6 +632,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(); + + /// Enables/disables starting picture-in-picture automatically when the app goes to the background. + Future setAutomaticallyStartsPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setAutomaticallyStartsPictureInPicture( + textureId: _textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ); + } + + /// Sets the location of the video player view in order to animate the picture-in-picture view. + Future setPictureInPictureOverlaySettings({ + required PictureInPictureOverlaySettings settings, + }) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setPictureInPictureOverlaySettings( + textureId: _textureId, + settings: settings, + ); + } + + /// Starts picture-in-picture mode. + Future startPictureInPicture() async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.startPictureInPicture(_textureId); + } + + /// Stops picture-in-picture mode. + Future stopPictureInPicture() async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.stopPictureInPicture(_textureId); + } + Future _applyPlaybackSpeed() async { if (_isDisposedOrNotInitialized) { return; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c19f6c6f77c..409faad9ea2 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,10 +3,10 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/packages/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.9.2 +version: 2.10.0 environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" flutter: @@ -25,10 +25,10 @@ dependencies: flutter: sdk: flutter html: ^0.15.0 - video_player_android: ^2.3.5 - video_player_avfoundation: ^2.5.6 - video_player_platform_interface: ^6.2.0 - video_player_web: ^2.1.0 + video_player_android: ^2.7.17 + video_player_avfoundation: ^2.6.5 + video_player_platform_interface: ^6.2.3 + video_player_web: ^2.3.3 dev_dependencies: flutter_test: @@ -37,3 +37,12 @@ dev_dependencies: topics: - video - video-player + +# 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 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 f6eef244811..2bb2105f5b5 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -83,6 +83,25 @@ class FakeController extends ValueNotifier Future setClosedCaptionFile( Future? closedCaptionFile, ) async {} + + @override + Future isPictureInPictureSupported() async => true; + + @override + Future setAutomaticallyStartsPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) async {} + + @override + Future setPictureInPictureOverlaySettings({ + required PictureInPictureOverlaySettings settings, + }) async {} + + @override + Future startPictureInPicture() async {} + + @override + Future stopPictureInPicture() async {} } Future _loadClosedCaption() async => @@ -1067,12 +1086,36 @@ void main() { 'isPlaying: true, ' 'isLooping: true, ' 'isBuffering: true, ' + 'isPictureInPictureActive: false, ' 'volume: 0.5, ' 'playbackSpeed: 1.5, ' 'errorDescription: null, ' 'isCompleted: false),'); }); + group('equals', () { + test('identical objects are equal', () { + const VideoPlayerValue a = VideoPlayerValue(duration: Duration.zero); + const VideoPlayerValue b = VideoPlayerValue(duration: Duration.zero); + expect(a, equals(b)); + }); + + test('objects differing in isPictureInPictureActive should not be equal', + () { + const VideoPlayerValue a = VideoPlayerValue( + duration: Duration.zero, + // Ignore the default value of isPictureInPictureActive, to ensure test stability in the future. + // ignore: avoid_redundant_argument_values + isPictureInPictureActive: false, + ); + const VideoPlayerValue b = VideoPlayerValue( + duration: Duration.zero, + isPictureInPictureActive: true, + ); + expect(a, isNot(equals(b))); + }); + }); + group('copyWith()', () { test('exact copy', () { const VideoPlayerValue original = VideoPlayerValue.uninitialized(); 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 a38013533d6..f8f93263c4b 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 @@ -266,6 +266,10 @@ class MiniController extends ValueNotifier { value = value.copyWith(isBuffering: false); case VideoEventType.isPlayingStateUpdate: value = value.copyWith(isPlaying: event.isPlaying); + case VideoEventType.startedPictureInPicture: + throw UnimplementedError(); + case VideoEventType.stoppedPictureInPicture: + throw UnimplementedError(); 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 01ce43d2c53..4505fa503df 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -31,3 +31,8 @@ flutter: assets: - assets/flutter-mark-square-64.png - assets/Butterfly-209.mp4 + +# 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/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 0f0b4dabac2..701275b6f31 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.7.0 -* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Updates minimum supported SDK version to Flutter 3.22/Dart 3.6. +* Adds support for picture-in-picture. ## 2.6.5 diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index f35d38bd989..bf158a6a8af 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -3,6 +3,7 @@ // found in the LICENSE file. @import AVFoundation; +@import AVKit; @import video_player_avfoundation; @import XCTest; @@ -662,7 +663,6 @@ - (void)testSeekToleranceWhenSeekingToEnd { } }]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; - // Starts paused. AVPlayer *avPlayer = player.player; XCTAssertEqual(avPlayer.rate, 0); @@ -680,6 +680,30 @@ - (void)testSeekToleranceWhenSeekingToEnd { XCTAssertNil(error); XCTAssertEqual(avPlayer.volume, 0.1f); + // Set picture-in-picture + NSNumber *isPictureInPictureSupported = [videoPlayerPlugin isPictureInPictureSupported:&error]; + XCTAssertNil(error); + XCTAssertNotNil(isPictureInPictureSupported); + if (@available(macOS 10.15, *)) { + XCTAssertEqual(isPictureInPictureSupported.boolValue, + [AVPictureInPictureController isPictureInPictureSupported]); + } + if (isPictureInPictureSupported.boolValue) { + FVPStartPictureInPictureMessage *startPictureInPicture = + [FVPStartPictureInPictureMessage makeWithTextureId:textureId.integerValue]; + XCTestExpectation *startedPiPExpectation = + [self expectationWithDescription:@"startedPictureInPicture"]; + [player onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"startedPictureInPicture"]) { + [startedPiPExpectation 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/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 5892274a37d..fbc9801c25a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -2,12 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "./include/video_player_avfoundation/FVPVideoPlayer.h" -#import "./include/video_player_avfoundation/FVPVideoPlayer_Test.h" +#import "video_player_avfoundation/FVPVideoPlayer.h" +#import "video_player_avfoundation/FVPVideoPlayer_Test.h" +#import +#import #import -#import "./include/video_player_avfoundation/AVAssetTrackUtils.h" +#import "video_player_avfoundation/AVAssetTrackUtils.h" +#import "video_player_avfoundation/FVPDisplayLink.h" static void *timeRangeContext = &timeRangeContext; static void *statusContext = &statusContext; @@ -44,6 +47,12 @@ @interface FVPVideoPlayer () - (void)updatePlayingState; @end +API_AVAILABLE(macos(10.15)) +@interface FVPVideoPlayer () +@property(nonatomic) AVPictureInPictureController *pictureInPictureController; +@property(nonatomic) BOOL pictureInPictureStarted; +@end + @implementation FVPVideoPlayer - (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FVPFrameUpdater *)frameUpdater @@ -136,6 +145,9 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.flutterViewLayer addSublayer:_playerLayer]; + // Configure Picture in Picture controller + [self setUpPictureInPictureController]; + // Configure output. _displayLink = displayLink; NSDictionary *pixBuffAttributes = @{ @@ -605,4 +617,76 @@ - (void)removeKeyValueObservers { [_player removeObserver:self forKeyPath:@"rate"]; } +/// Sets up the picture in picture controller and assigns the AVPictureInPictureControllerDelegate +/// to the controller. +- (void)setUpPictureInPictureController { + if (@available(macOS 10.15, *)) { + if (AVPictureInPictureController.isPictureInPictureSupported) { + self.pictureInPictureController = + [[AVPictureInPictureController alloc] initWithPlayerLayer:self.playerLayer]; + [self setAutomaticallyStartPictureInPicture:NO]; + _pictureInPictureController.delegate = self; + } + } else { + // We don't do anything here because there is no setup required below macOS 10.15. + } +} + +- (void)setAutomaticallyStartPictureInPicture: + (BOOL)canStartPictureInPictureAutomaticallyFromInline { + if (!self.pictureInPictureController) return; +#if TARGET_OS_IOS + if (@available(iOS 14.2, *)) { + self.pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = + canStartPictureInPictureAutomaticallyFromInline; + } +#endif +} + +- (void)setPictureInPictureOverlayFrame:(CGRect)frame { + self.playerLayer.frame = frame; +} + +- (void)setPictureInPictureStarted:(BOOL)startPictureInPicture { + if (@available(macOS 10.15, *)) { + if (!AVPictureInPictureController.isPictureInPictureSupported || + _pictureInPictureStarted == startPictureInPicture) { + return; + } + } else { + return; + } + + _pictureInPictureStarted = startPictureInPicture; + if (_pictureInPictureStarted && ![self.pictureInPictureController isPictureInPictureActive]) { + if (_eventSink != nil) { + // The event is sent here to make sure that the Flutter UI can be updated as soon as possible. + _eventSink(@{@"event" : @"startedPictureInPicture"}); + } + [self.pictureInPictureController startPictureInPicture]; + } else if (!_pictureInPictureStarted && + [self.pictureInPictureController isPictureInPictureActive]) { + [self.pictureInPictureController stopPictureInPicture]; + } +} + +#pragma mark - AVPictureInPictureControllerDelegate + +- (void)pictureInPictureControllerDidStopPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController API_AVAILABLE(macos(10.15)) { + _pictureInPictureStarted = NO; + if (_eventSink != nil) { + _eventSink(@{@"event" : @"stoppedPictureInPicture"}); + } +} + +- (void)pictureInPictureControllerDidStartPictureInPicture: + (AVPictureInPictureController *)pictureInPictureController API_AVAILABLE(macos(10.15)) { + _pictureInPictureStarted = YES; + if (_eventSink != nil) { + _eventSink(@{@"event" : @"startingPictureInPicture"}); + } + [self updatePlayingState]; +} + @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index 7c70da03d66..bda4d196b7b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -213,4 +213,68 @@ - (void)setMixWithOthers:(BOOL)mixWithOthers #endif } +- (nullable NSNumber *)isPictureInPictureSupported: + (FlutterError *_Nullable __autoreleasing *_Nonnull)error { + if (@available(macOS 10.15, *)) { +#if TARGET_OS_OSX + return @(AVPictureInPictureController.isPictureInPictureSupported); +#else + return @((BOOL) (AVPictureInPictureController.isPictureInPictureSupported && + [self configuredPictureInPictureBackgroundMode])); +#endif + } else { + return @NO; + } +} + +- (void)setAutomaticallyStartsPictureInPicture: + (FVPAutomaticallyStartsPictureInPictureMessage *)input + error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByTextureId[@(input.textureId)]; + [player setAutomaticallyStartPictureInPicture:input.enableStartPictureInPictureAutomaticallyFromInline]; +} + +- (void)setPictureInPictureOverlaySettings:(FVPSetPictureInPictureOverlaySettingsMessage *)input + error:(FlutterError **)error { + FVPVideoPlayer *player = self.playersByTextureId[@(input.textureId)]; + [player setPictureInPictureOverlayFrame:CGRectMake(input.settings.left, input.settings.top, + input.settings.width, input.settings.height)]; +} + +- (BOOL)configuredPictureInPictureBackgroundMode { + id backgroundModes = [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIBackgroundModes"]; + return + [backgroundModes isKindOfClass:[NSArray class]] && [backgroundModes containsObject:@"audio"]; +} + +- (void)startPictureInPicture:(FVPStartPictureInPictureMessage *)input + error:(FlutterError **)error { +#if TARGET_OS_IOS + if (![self configuredPictureInPictureBackgroundMode]) { + *error = [FlutterError + errorWithCode:@"video_player" + message:@"Failed to start picture-in-picture because UIBackgroundModes: audio " + @"is not enabled in Info.plist" + details:nil]; + return; + } +#endif + + FVPVideoPlayer *player = self.playersByTextureId[@(input.textureId)]; + player.pictureInPictureStarted = YES; +} + +- (void)stopPictureInPicture:(FVPStopPictureInPictureMessage *)input error:(FlutterError **)error { + if (![self configuredPictureInPictureBackgroundMode]) { + *error = [FlutterError + errorWithCode:@"video_player" + message:@"Failed to stop picture-in-picture because UIBackgroundModes: audio " + @"is not enabled in Info.plist" + details:nil]; + return; + } + FVPVideoPlayer *player = self.playersByTextureId[@(input.textureId)]; + player.pictureInPictureStarted = NO; +} + @end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h index 27f95e7692d..e718b562bd8 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h @@ -9,6 +9,7 @@ #endif #import +#import #import "FVPAVFactory.h" #import "FVPDisplayLink.h" @@ -81,6 +82,10 @@ NS_ASSUME_NONNULL_BEGIN /// Tells the player to run its frame updater until it receives a frame, regardless of the /// play/pause state. - (void)expectFrame; + +- (void)setAutomaticallyStartPictureInPicture:(BOOL)automaticallyStartPictureInPicture; +- (void)setPictureInPictureOverlayFrame:(CGRect)frame; +- (void)setPictureInPictureStarted:(BOOL)startPictureInPicture; @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index afb118f14cd..8043a809e92 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/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 (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -14,6 +14,11 @@ NS_ASSUME_NONNULL_BEGIN @class FVPCreationOptions; +@class FVPAutomaticallyStartsPictureInPictureMessage; +@class FVPSetPictureInPictureOverlaySettingsMessage; +@class FVPPictureInPictureOverlaySettingsMessage; +@class FVPStartPictureInPictureMessage; +@class FVPStopPictureInPictureMessage; @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. @@ -30,6 +35,49 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy) NSDictionary *httpHeaders; @end +@interface FVPAutomaticallyStartsPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSInteger)textureId + enableStartPictureInPictureAutomaticallyFromInline: + (BOOL)enableStartPictureInPictureAutomaticallyFromInline; +@property(nonatomic, assign) NSInteger textureId; +@property(nonatomic, assign) BOOL enableStartPictureInPictureAutomaticallyFromInline; +@end + +@interface FVPSetPictureInPictureOverlaySettingsMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSInteger)textureId + settings:(nullable FVPPictureInPictureOverlaySettingsMessage *)settings; +@property(nonatomic, assign) NSInteger textureId; +@property(nonatomic, strong, nullable) FVPPictureInPictureOverlaySettingsMessage *settings; +@end + +@interface FVPPictureInPictureOverlaySettingsMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTop:(double)top left:(double)left width:(double)width height:(double)height; +@property(nonatomic, assign) double top; +@property(nonatomic, assign) double left; +@property(nonatomic, assign) double width; +@property(nonatomic, assign) double height; +@end + +@interface FVPStartPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSInteger)textureId; +@property(nonatomic, assign) NSInteger textureId; +@end + +@interface FVPStopPictureInPictureMessage : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTextureId:(NSInteger)textureId; +@property(nonatomic, assign) NSInteger textureId; +@end + /// The codec used by all APIs. NSObject *FVPGetMessagesCodec(void); @@ -57,6 +105,16 @@ NSObject *FVPGetMessagesCodec(void); completion:(void (^)(FlutterError *_Nullable))completion; - (void)pausePlayer:(NSInteger)textureId error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSNumber *)isPictureInPictureSupported:(FlutterError *_Nullable *_Nonnull)error; +- (void)setPictureInPictureOverlaySettings:(FVPSetPictureInPictureOverlaySettingsMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setAutomaticallyStartsPictureInPicture:(FVPAutomaticallyStartsPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)startPictureInPicture:(FVPStartPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; +- (void)stopPictureInPicture:(FVPStopPictureInPictureMessage *)msg + error:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFVPAVFoundationVideoPlayerApi( diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 9749b125a9e..eeec379306e 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/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 (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/video_player_avfoundation/messages.g.h" @@ -36,6 +36,36 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FVPAutomaticallyStartsPictureInPictureMessage () ++ (FVPAutomaticallyStartsPictureInPictureMessage *)fromList:(NSArray *)list; ++ (nullable FVPAutomaticallyStartsPictureInPictureMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPSetPictureInPictureOverlaySettingsMessage () ++ (FVPSetPictureInPictureOverlaySettingsMessage *)fromList:(NSArray *)list; ++ (nullable FVPSetPictureInPictureOverlaySettingsMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPPictureInPictureOverlaySettingsMessage () ++ (FVPPictureInPictureOverlaySettingsMessage *)fromList:(NSArray *)list; ++ (nullable FVPPictureInPictureOverlaySettingsMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPStartPictureInPictureMessage () ++ (FVPStartPictureInPictureMessage *)fromList:(NSArray *)list; ++ (nullable FVPStartPictureInPictureMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPStopPictureInPictureMessage () ++ (FVPStopPictureInPictureMessage *)fromList:(NSArray *)list; ++ (nullable FVPStopPictureInPictureMessage *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPCreationOptions + (instancetype)makeWithAsset:(nullable NSString *)asset uri:(nullable NSString *)uri @@ -73,6 +103,140 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { } @end +@implementation FVPAutomaticallyStartsPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSInteger)textureId + enableStartPictureInPictureAutomaticallyFromInline: + (BOOL)enableStartPictureInPictureAutomaticallyFromInline { + FVPAutomaticallyStartsPictureInPictureMessage *pigeonResult = + [[FVPAutomaticallyStartsPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.enableStartPictureInPictureAutomaticallyFromInline = + enableStartPictureInPictureAutomaticallyFromInline; + return pigeonResult; +} ++ (FVPAutomaticallyStartsPictureInPictureMessage *)fromList:(NSArray *)list { + FVPAutomaticallyStartsPictureInPictureMessage *pigeonResult = + [[FVPAutomaticallyStartsPictureInPictureMessage alloc] init]; + pigeonResult.textureId = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.enableStartPictureInPictureAutomaticallyFromInline = + [GetNullableObjectAtIndex(list, 1) boolValue]; + return pigeonResult; +} ++ (nullable FVPAutomaticallyStartsPictureInPictureMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPAutomaticallyStartsPictureInPictureMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.textureId), + @(self.enableStartPictureInPictureAutomaticallyFromInline), + ]; +} +@end + +@implementation FVPSetPictureInPictureOverlaySettingsMessage ++ (instancetype)makeWithTextureId:(NSInteger)textureId + settings:(nullable FVPPictureInPictureOverlaySettingsMessage *)settings { + FVPSetPictureInPictureOverlaySettingsMessage *pigeonResult = + [[FVPSetPictureInPictureOverlaySettingsMessage alloc] init]; + pigeonResult.textureId = textureId; + pigeonResult.settings = settings; + return pigeonResult; +} ++ (FVPSetPictureInPictureOverlaySettingsMessage *)fromList:(NSArray *)list { + FVPSetPictureInPictureOverlaySettingsMessage *pigeonResult = + [[FVPSetPictureInPictureOverlaySettingsMessage alloc] init]; + pigeonResult.textureId = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.settings = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FVPSetPictureInPictureOverlaySettingsMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPSetPictureInPictureOverlaySettingsMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.textureId), + self.settings ?: [NSNull null], + ]; +} +@end + +@implementation FVPPictureInPictureOverlaySettingsMessage ++ (instancetype)makeWithTop:(double)top + left:(double)left + width:(double)width + height:(double)height { + FVPPictureInPictureOverlaySettingsMessage *pigeonResult = + [[FVPPictureInPictureOverlaySettingsMessage alloc] init]; + pigeonResult.top = top; + pigeonResult.left = left; + pigeonResult.width = width; + pigeonResult.height = height; + return pigeonResult; +} ++ (FVPPictureInPictureOverlaySettingsMessage *)fromList:(NSArray *)list { + FVPPictureInPictureOverlaySettingsMessage *pigeonResult = + [[FVPPictureInPictureOverlaySettingsMessage alloc] init]; + pigeonResult.top = [GetNullableObjectAtIndex(list, 0) doubleValue]; + pigeonResult.left = [GetNullableObjectAtIndex(list, 1) doubleValue]; + pigeonResult.width = [GetNullableObjectAtIndex(list, 2) doubleValue]; + pigeonResult.height = [GetNullableObjectAtIndex(list, 3) doubleValue]; + return pigeonResult; +} ++ (nullable FVPPictureInPictureOverlaySettingsMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPPictureInPictureOverlaySettingsMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.top), + @(self.left), + @(self.width), + @(self.height), + ]; +} +@end + +@implementation FVPStartPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSInteger)textureId { + FVPStartPictureInPictureMessage *pigeonResult = [[FVPStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + return pigeonResult; +} ++ (FVPStartPictureInPictureMessage *)fromList:(NSArray *)list { + FVPStartPictureInPictureMessage *pigeonResult = [[FVPStartPictureInPictureMessage alloc] init]; + pigeonResult.textureId = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FVPStartPictureInPictureMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPStartPictureInPictureMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.textureId), + ]; +} +@end + +@implementation FVPStopPictureInPictureMessage ++ (instancetype)makeWithTextureId:(NSInteger)textureId { + FVPStopPictureInPictureMessage *pigeonResult = [[FVPStopPictureInPictureMessage alloc] init]; + pigeonResult.textureId = textureId; + return pigeonResult; +} ++ (FVPStopPictureInPictureMessage *)fromList:(NSArray *)list { + FVPStopPictureInPictureMessage *pigeonResult = [[FVPStopPictureInPictureMessage alloc] init]; + pigeonResult.textureId = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FVPStopPictureInPictureMessage *)nullableFromList:(NSArray *)list { + return (list) ? [FVPStopPictureInPictureMessage fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.textureId), + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader @@ -80,6 +244,16 @@ - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 129: return [FVPCreationOptions fromList:[self readValue]]; + case 130: + return [FVPAutomaticallyStartsPictureInPictureMessage fromList:[self readValue]]; + case 131: + return [FVPSetPictureInPictureOverlaySettingsMessage fromList:[self readValue]]; + case 132: + return [FVPPictureInPictureOverlaySettingsMessage fromList:[self readValue]]; + case 133: + return [FVPStartPictureInPictureMessage fromList:[self readValue]]; + case 134: + return [FVPStopPictureInPictureMessage fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -93,6 +267,21 @@ - (void)writeValue:(id)value { if ([value isKindOfClass:[FVPCreationOptions class]]) { [self writeByte:129]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPAutomaticallyStartsPictureInPictureMessage class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPSetPictureInPictureOverlaySettingsMessage class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPPictureInPictureOverlaySettingsMessage class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPStartPictureInPictureMessage class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPStopPictureInPictureMessage class]]) { + [self writeByte:134]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -398,4 +587,128 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName: + [NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.isPictureInPictureSupported", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isPictureInPictureSupported:)], + @"FVPAVFoundationVideoPlayerApi 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:[NSString + stringWithFormat: + @"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.setPictureInPictureOverlaySettings", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setPictureInPictureOverlaySettings:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setPictureInPictureOverlaySettings:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPSetPictureInPictureOverlaySettingsMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setPictureInPictureOverlaySettings:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi." + @"setAutomaticallyStartsPictureInPicture", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setAutomaticallyStartsPictureInPicture:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(setAutomaticallyStartsPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPAutomaticallyStartsPictureInPictureMessage *arg_msg = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api setAutomaticallyStartsPictureInPicture:arg_msg error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.startPictureInPicture", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(startPictureInPicture:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(startPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPStartPictureInPictureMessage *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:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"AVFoundationVideoPlayerApi.stopPictureInPicture", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(stopPictureInPicture:error:)], + @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " + @"@selector(stopPictureInPicture:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FVPStopPictureInPictureMessage *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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 94ce699b626..d04983b2d7c 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 @@ -366,6 +366,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 953072E2E07B12E1C01BDB6F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/video_player_avfoundation/video_player_avfoundation_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/video_player_avfoundation_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist index 4e29652e6d2..1f3167d5a5f 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist @@ -27,6 +27,10 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile 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 54c97030c3a..2fdc13a1140 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 313ae7f50dc..4dc93e1f0ec 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/main.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/main.dart @@ -5,6 +5,7 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'mini_controller.dart'; @@ -36,7 +37,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 +119,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 +150,75 @@ 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.setAutomaticallyStartsPictureInPicture( + 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.setPictureInPictureOverlaySettings( + settings: PictureInPictureOverlaySettings( + 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 a38013533d6..4543221e5a2 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 @@ -37,6 +37,7 @@ class VideoPlayerValue { this.isInitialized = false, this.isPlaying = false, this.isBuffering = false, + this.isPictureInPictureActive = false, this.playbackSpeed = 1.0, this.errorDescription, }); @@ -72,6 +73,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`. @@ -114,6 +118,7 @@ class VideoPlayerValue { bool? isInitialized, bool? isPlaying, bool? isBuffering, + bool? isPictureInPictureActive, double? playbackSpeed, String? errorDescription, }) { @@ -125,6 +130,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, ); @@ -140,6 +147,7 @@ class VideoPlayerValue { listEquals(buffered, other.buffered) && isPlaying == other.isPlaying && isBuffering == other.isBuffering && + isPictureInPictureActive == other.isPictureInPictureActive && playbackSpeed == other.playbackSpeed && errorDescription == other.errorDescription && size == other.size && @@ -152,6 +160,7 @@ class VideoPlayerValue { buffered, isPlaying, isBuffering, + isPictureInPictureActive, playbackSpeed, errorDescription, size, @@ -264,6 +273,10 @@ class MiniController extends ValueNotifier { value = value.copyWith(isBuffering: true); case VideoEventType.bufferingEnd: value = value.copyWith(isBuffering: false); + case VideoEventType.startedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: true); + case VideoEventType.stoppedPictureInPicture: + value = value.copyWith(isPictureInPictureActive: false); case VideoEventType.isPlayingStateUpdate: value = value.copyWith(isPlaying: event.isPlaying); case VideoEventType.unknown: @@ -361,6 +374,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 starting picture-in-picture automatically when the app goes to the background. + Future setAutomaticallyStartsPictureInPicture({ + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + return _platform.setAutomaticallyStartsPictureInPicture( + textureId: textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ); + } + + /// Set the location of the video player view, so that picture-in-picture can use it for animating + Future setPictureInPictureOverlaySettings({ + required PictureInPictureOverlaySettings settings, + }) { + return _platform.setPictureInPictureOverlaySettings( + textureId: textureId, + settings: settings, + ); + } + + /// Starts picture-in-picture mode. + Future startPictureInPicture() { + return _platform.startPictureInPicture(textureId); + } + + /// Stops 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 ab457872a0d..70dae094304 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the video_player plugin. publish_to: none environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" dependencies: @@ -31,3 +31,10 @@ 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/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index ace8f749a6b..1124871797e 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 @@ -128,6 +128,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 'startedPictureInPicture': + return VideoEvent(eventType: VideoEventType.startedPictureInPicture); case 'isPlayingStateUpdate': return VideoEvent( eventType: VideoEventType.isPlayingStateUpdate, @@ -149,6 +153,57 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return _api.setMixWithOthers(mixWithOthers); } + @override + Future isPictureInPictureSupported() { + return _api.isPictureInPictureSupported(); + } + + @override + Future setAutomaticallyStartsPictureInPicture({ + required int textureId, + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + return _api.setAutomaticallyStartsPictureInPicture( + AutomaticallyStartsPictureInPictureMessage( + textureId: textureId, + enableStartPictureInPictureAutomaticallyFromInline: + enableStartPictureInPictureAutomaticallyFromInline, + ), + ); + } + + @override + Future setPictureInPictureOverlaySettings({ + required int textureId, + required PictureInPictureOverlaySettings settings, + }) { + return _api.setPictureInPictureOverlaySettings( + SetPictureInPictureOverlaySettingsMessage( + textureId: textureId, + settings: PictureInPictureOverlaySettingsMessage( + top: settings.rect.top, + left: settings.rect.left, + width: settings.rect.width, + height: settings.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 ea4a93fdaba..aa25b369b0a 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 (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), 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, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -71,6 +71,136 @@ class CreationOptions { } } +class AutomaticallyStartsPictureInPictureMessage { + AutomaticallyStartsPictureInPictureMessage({ + required this.textureId, + required this.enableStartPictureInPictureAutomaticallyFromInline, + }); + + int textureId; + + bool enableStartPictureInPictureAutomaticallyFromInline; + + Object encode() { + return [ + textureId, + enableStartPictureInPictureAutomaticallyFromInline, + ]; + } + + static AutomaticallyStartsPictureInPictureMessage decode(Object result) { + result as List; + return AutomaticallyStartsPictureInPictureMessage( + textureId: result[0]! as int, + enableStartPictureInPictureAutomaticallyFromInline: result[1]! as bool, + ); + } +} + +class SetPictureInPictureOverlaySettingsMessage { + SetPictureInPictureOverlaySettingsMessage({ + required this.textureId, + this.settings, + }); + + int textureId; + + PictureInPictureOverlaySettingsMessage? settings; + + Object encode() { + return [ + textureId, + settings, + ]; + } + + static SetPictureInPictureOverlaySettingsMessage decode(Object result) { + result as List; + return SetPictureInPictureOverlaySettingsMessage( + textureId: result[0]! as int, + settings: result[1] as PictureInPictureOverlaySettingsMessage?, + ); + } +} + +class PictureInPictureOverlaySettingsMessage { + PictureInPictureOverlaySettingsMessage({ + required this.top, + required this.left, + required this.width, + required this.height, + }); + + double top; + + double left; + + double width; + + double height; + + Object encode() { + return [ + top, + left, + width, + height, + ]; + } + + static PictureInPictureOverlaySettingsMessage decode(Object result) { + result as List; + return PictureInPictureOverlaySettingsMessage( + top: result[0]! as double, + left: result[1]! as double, + width: result[2]! as double, + height: result[3]! as double, + ); + } +} + +class StartPictureInPictureMessage { + StartPictureInPictureMessage({ + required this.textureId, + }); + + int textureId; + + Object encode() { + return [ + textureId, + ]; + } + + static StartPictureInPictureMessage decode(Object result) { + result as List; + return StartPictureInPictureMessage( + textureId: result[0]! as int, + ); + } +} + +class StopPictureInPictureMessage { + StopPictureInPictureMessage({ + required this.textureId, + }); + + int textureId; + + Object encode() { + return [ + textureId, + ]; + } + + static StopPictureInPictureMessage decode(Object result) { + result as List; + return StopPictureInPictureMessage( + textureId: result[0]! as int, + ); + } +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -81,6 +211,21 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is CreationOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is AutomaticallyStartsPictureInPictureMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SetPictureInPictureOverlaySettingsMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PictureInPictureOverlaySettingsMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is StartPictureInPictureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is StopPictureInPictureMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -91,6 +236,19 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: return CreationOptions.decode(readValue(buffer)!); + case 130: + return AutomaticallyStartsPictureInPictureMessage.decode( + readValue(buffer)!); + case 131: + return SetPictureInPictureOverlaySettingsMessage.decode( + readValue(buffer)!); + case 132: + return PictureInPictureOverlaySettingsMessage.decode( + readValue(buffer)!); + case 133: + return StartPictureInPictureMessage.decode(readValue(buffer)!); + case 134: + return StopPictureInPictureMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -385,4 +543,131 @@ class AVFoundationVideoPlayerApi { return; } } + + Future isPictureInPictureSupported() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPictureInPictureSupported$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future setPictureInPictureOverlaySettings( + SetPictureInPictureOverlaySettingsMessage msg) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPictureInPictureOverlaySettings$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([msg]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future setAutomaticallyStartsPictureInPicture( + AutomaticallyStartsPictureInPictureMessage msg) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutomaticallyStartsPictureInPicture$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([msg]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future startPictureInPicture(StartPictureInPictureMessage msg) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.startPictureInPicture$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([msg]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future stopPictureInPicture(StopPictureInPictureMessage msg) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.stopPictureInPicture$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([msg]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } 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 7a2d2957be1..db43d2af0ba 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -26,6 +26,48 @@ class CreationOptions { Map httpHeaders; } +class AutomaticallyStartsPictureInPictureMessage { + AutomaticallyStartsPictureInPictureMessage( + this.textureId, + this.enableStartPictureInPictureAutomaticallyFromInline, + ); + int textureId; + bool enableStartPictureInPictureAutomaticallyFromInline; +} + +class SetPictureInPictureOverlaySettingsMessage { + SetPictureInPictureOverlaySettingsMessage( + this.textureId, + this.settings, + ); + int textureId; + PictureInPictureOverlaySettingsMessage? settings; +} + +class PictureInPictureOverlaySettingsMessage { + PictureInPictureOverlaySettingsMessage({ + 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') @@ -52,4 +94,16 @@ abstract class AVFoundationVideoPlayerApi { void pause(int textureId); @ObjCSelector('setMixWithOthers:') void setMixWithOthers(bool mixWithOthers); + @ObjCSelector('isPictureInPictureSupported') + bool isPictureInPictureSupported(); + @ObjCSelector('setPictureInPictureOverlaySettings:') + void setPictureInPictureOverlaySettings( + SetPictureInPictureOverlaySettingsMessage msg); + @ObjCSelector('setAutomaticallyStartsPictureInPicture:') + void setAutomaticallyStartsPictureInPicture( + AutomaticallyStartsPictureInPictureMessage 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 e6e72fefab2..592a0c5a3d0 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,10 +2,10 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/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.6.5 +version: 2.7.0 environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" flutter: @@ -34,3 +34,8 @@ dev_dependencies: topics: - video - video-player + +# 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 dac0553fbed..1b7fc4998d1 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 @@ -19,6 +19,12 @@ class _ApiLogger implements TestHostVideoPlayerApi { double? volume; double? playbackSpeed; bool? mixWithOthers; + SetPictureInPictureOverlaySettingsMessage? + setPictureInPictureOverlaySettingMessage; + AutomaticallyStartsPictureInPictureMessage? + automaticallyStartsPictureInPictureMessage; + StartPictureInPictureMessage? startPictureInPictureMessage; + StopPictureInPictureMessage? stopPictureInPictureMessage; @override int create(CreationOptions options) { @@ -90,6 +96,38 @@ class _ApiLogger implements TestHostVideoPlayerApi { playbackSpeed = speed; this.textureId = textureId; } + + @override + bool isPictureInPictureSupported() { + log.add('isPictureInPictureSupported'); + return true; + } + + @override + void setAutomaticallyStartsPictureInPicture( + AutomaticallyStartsPictureInPictureMessage msg) { + log.add('setAutomaticallyStartsPictureInPicture'); + automaticallyStartsPictureInPictureMessage = msg; + } + + @override + void setPictureInPictureOverlaySettings( + SetPictureInPictureOverlaySettingsMessage msg) { + log.add('setPictureInPictureOverlaySettings'); + setPictureInPictureOverlaySettingMessage = msg; + } + + @override + void startPictureInPicture(StartPictureInPictureMessage msg) { + log.add('startPictureInPicture'); + startPictureInPictureMessage = msg; + } + + @override + void stopPictureInPicture(StopPictureInPictureMessage msg) { + log.add('stopPictureInPicture'); + stopPictureInPictureMessage = msg; + } } void main() { @@ -245,6 +283,63 @@ 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('setAutomaticallyStartsPictureInPicture true', () async { + await player.setAutomaticallyStartsPictureInPicture( + textureId: 1, + enableStartPictureInPictureAutomaticallyFromInline: true); + expect(log.log.last, 'setAutomaticallyStartsPictureInPicture'); + expect(log.automaticallyStartsPictureInPictureMessage?.textureId, 1); + expect( + log.automaticallyStartsPictureInPictureMessage + ?.enableStartPictureInPictureAutomaticallyFromInline, + true); + }); + + test('setAutomaticallyStartsPictureInPicture false', () async { + await player.setAutomaticallyStartsPictureInPicture( + textureId: 1, + enableStartPictureInPictureAutomaticallyFromInline: false); + expect(log.log.last, 'setAutomaticallyStartsPictureInPicture'); + expect(log.automaticallyStartsPictureInPictureMessage?.textureId, 1); + expect( + log.automaticallyStartsPictureInPictureMessage + ?.enableStartPictureInPictureAutomaticallyFromInline, + false); + }); + + test('setPictureInPictureOverlaySettings', () async { + await player.setPictureInPictureOverlaySettings( + textureId: 1, + settings: const PictureInPictureOverlaySettings( + rect: Rect.fromLTWH(0, 1, 2, 3), + ), + ); + expect(log.log.last, 'setPictureInPictureOverlaySettings'); + expect(log.setPictureInPictureOverlaySettingMessage?.textureId, 1); + expect(log.setPictureInPictureOverlaySettingMessage?.settings?.left, 0); + expect(log.setPictureInPictureOverlaySettingMessage?.settings?.top, 1); + expect(log.setPictureInPictureOverlaySettingMessage?.settings?.width, 2); + expect(log.setPictureInPictureOverlaySettingMessage?.settings?.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 { const String mockChannel = 'flutter.io/videoPlayer/videoEvents123'; TestDefaultBinaryMessengerBinding.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 6dbbb53d233..55e3df6735d 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 (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), 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, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports @@ -23,6 +23,21 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is CreationOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is AutomaticallyStartsPictureInPictureMessage) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SetPictureInPictureOverlaySettingsMessage) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is PictureInPictureOverlaySettingsMessage) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is StartPictureInPictureMessage) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is StopPictureInPictureMessage) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -33,6 +48,19 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: return CreationOptions.decode(readValue(buffer)!); + case 130: + return AutomaticallyStartsPictureInPictureMessage.decode( + readValue(buffer)!); + case 131: + return SetPictureInPictureOverlaySettingsMessage.decode( + readValue(buffer)!); + case 132: + return PictureInPictureOverlaySettingsMessage.decode( + readValue(buffer)!); + case 133: + return StartPictureInPictureMessage.decode(readValue(buffer)!); + case 134: + return StopPictureInPictureMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -66,6 +94,18 @@ abstract class TestHostVideoPlayerApi { void setMixWithOthers(bool mixWithOthers); + bool isPictureInPictureSupported(); + + void setPictureInPictureOverlaySettings( + SetPictureInPictureOverlaySettingsMessage msg); + + void setAutomaticallyStartsPictureInPicture( + AutomaticallyStartsPictureInPictureMessage msg); + + void startPictureInPicture(StartPictureInPictureMessage msg); + + void stopPictureInPicture(StopPictureInPictureMessage msg); + static void setUp( TestHostVideoPlayerApi? api, { BinaryMessenger? binaryMessenger, @@ -432,5 +472,163 @@ abstract class TestHostVideoPlayerApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.isPictureInPictureSupported$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + try { + final bool output = api.isPictureInPictureSupported(); + return [output]; + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPictureInPictureOverlaySettings$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPictureInPictureOverlaySettings was null.'); + final List args = (message as List?)!; + final SetPictureInPictureOverlaySettingsMessage? arg_msg = + (args[0] as SetPictureInPictureOverlaySettingsMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setPictureInPictureOverlaySettings was null, expected non-null SetPictureInPictureOverlaySettingsMessage.'); + try { + api.setPictureInPictureOverlaySettings(arg_msg!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutomaticallyStartsPictureInPicture$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutomaticallyStartsPictureInPicture was null.'); + final List args = (message as List?)!; + final AutomaticallyStartsPictureInPictureMessage? arg_msg = + (args[0] as AutomaticallyStartsPictureInPictureMessage?); + assert(arg_msg != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setAutomaticallyStartsPictureInPicture was null, expected non-null AutomaticallyStartsPictureInPictureMessage.'); + try { + api.setAutomaticallyStartsPictureInPicture(arg_msg!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.startPictureInPicture$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.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.video_player_avfoundation.AVFoundationVideoPlayerApi.startPictureInPicture was null, expected non-null StartPictureInPictureMessage.'); + try { + api.startPictureInPicture(arg_msg!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.stopPictureInPicture$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(pigeonVar_channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.video_player_avfoundation.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.video_player_avfoundation.AVFoundationVideoPlayerApi.stopPictureInPicture was null, expected non-null StopPictureInPictureMessage.'); + try { + api.stopPictureInPicture(arg_msg!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 3c1e70fec40..cb652ad87de 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 6.3.0 -* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Updates minimum supported SDK version to Flutter 3.22/Dart 3.6. +* Adds support for picture-in-picture. ## 6.2.3 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 d169c5f16d4..a00d3ced0e8 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 @@ -103,6 +103,39 @@ abstract class VideoPlayerPlatform extends PlatformInterface { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } + /// Returns true if picture-in-picture is supported on the device. + Future isPictureInPictureSupported() async => false; + + /// Enable/disable starting picture-in-picture automatically when the app goes to the background. + Future setAutomaticallyStartsPictureInPicture({ + required int textureId, + required bool enableStartPictureInPictureAutomaticallyFromInline, + }) { + throw UnimplementedError( + 'setAutomaticallyStartsPictureInPicture() has not been implemented.'); + } + + /// Set the location of the video player view, so that picture-in-picture can use it for animating. + Future setPictureInPictureOverlaySettings({ + required int textureId, + required PictureInPictureOverlaySettings settings, + }) { + throw UnimplementedError( + 'setPictureInPictureOverlayRect() has not been implemented.'); + } + + /// Starts picture-in-picture mode. + Future startPictureInPicture(int textureId) { + throw UnimplementedError( + 'startPictureInPicture() has not been implemented.'); + } + + /// Stops picture-in-picture mode. + Future stopPictureInPicture(int textureId) { + throw UnimplementedError( + 'stopPictureInPicture() has not been implemented.'); + } + /// Sets additional options on web Future setWebOptions(int textureId, VideoPlayerWebOptions options) { throw UnimplementedError('setWebOptions() has not been implemented.'); @@ -294,6 +327,12 @@ enum VideoEventType { /// The video stopped to buffer. bufferingEnd, + /// The video is started picture-in-picture mode. + startedPictureInPicture, + + /// The video is exited picture-in-picture mode. + stoppedPictureInPicture, + /// The playback state of the video has changed. /// /// This event is fired when the video starts or pauses due to user actions or @@ -476,3 +515,15 @@ class VideoPlayerWebOptionsControls { return controlsList.join(' '); } } + +/// [PictureInPictureOverlaySettings] can be optionally used to set the position and size of the picture-in-picture overlay. +@immutable +class PictureInPictureOverlaySettings { + /// Set the position and size of the picture-in-picture overlay using [rect]. + const PictureInPictureOverlaySettings({ + required this.rect, + }); + + /// The rect represents the global Flutter coordinates using logic pixels of the picture-in-picture overlay. + final Rect rect; +} diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 6a0f1e65c21..48e332e64fb 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,10 +4,10 @@ repository: https://github.com/flutter/packages/tree/main/packages/video_player/ 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.2.3 +version: 6.3.0 environment: - sdk: ^3.4.0 + sdk: ^3.6.0 flutter: ">=3.22.0" dependencies: