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