-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[video_player] #60048 ios picture in picture #3500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
595d3df
4831f57
976b630
a7d5dd5
876dfe8
a4ca9e7
49f769a
5477f0b
0764e27
65fdf65
d4d9ff9
ff6d02f
ae7500a
4eeb6e1
9110d0f
211c6b3
5a8735b
4583229
70284aa
7289998
ee96f77
da13c09
4d83a52
ac920fe
8394840
19b1440
1963d27
f71a27a
b3e05a0
1325254
72ba79a
8c67ab8
f935a16
42a7815
fa93319
b0103d0
236ab51
1cf961d
5c18256
ddc7139
45494b9
0597f32
5c1f759
e94ccaa
9dc85a0
2b72605
601c506
4574fa9
1135695
cd7fede
e9aba08
30c0b7a
c0ccbfc
8ee1dcb
58db59a
16ae8cb
a15c3b7
87b92c2
5183028
002faa0
148cd55
d88cf33
45f96f5
9459bd5
d9f883b
c9993dc
3e401c1
b250e9c
01aaf8d
ee6db47
2e3196b
29c1d3b
6d8fb1e
114f5b8
5861e1d
114f9ac
3c1b4e6
3985a77
1649be4
bc56139
e1348c8
7e70da0
4abe6f1
2006494
58dabf4
881e80e
917bdd4
fba55da
b0f42d8
e0231d8
c58b722
376bcf5
5dd6e13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this addition necessary? |
||
|
||
dev_dependencies: | ||
build_runner: ^2.1.10 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 `<project root>/ios/Runner/Info.plist`: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second sentence here is a fragment. It should probably say something like "Adding this mode will result in ..." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Everything from "which is located in" on can be removed; documenting basic iOS project structure isn't necessary here. |
||
|
||
```xml | ||
stuartmorgan-g marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<key>UIBackgroundModes</key> | ||
<array> | ||
<string>audio</string> | ||
</array> | ||
``` | ||
|
||
> [!IMPORTANT] | ||
> Failing to add the `audio` **Background Modes** capability will result in a silent failure to start picture-in-picture playback. | ||
|
||
Example: | ||
 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. READMEs should never have references to non-pinned URLs, as it means old versions' READMEs can change or break at any time. E.g., renaming the plugin in the future would break all previous READMEs. This will need to be added in a follow-up PR, using a hash instead of |
||
|
||
#### 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. |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file should be listed in a |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -213,6 +213,11 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { | |
class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { | ||
late VideoPlayerController _controller; | ||
|
||
final GlobalKey<State<StatefulWidget>> _playerKey = | ||
GlobalKey<State<StatefulWidget>>(); | ||
final Key _pictureInPictureKey = UniqueKey(); | ||
bool _enableStartPictureInPictureAutomaticallyFromInline = false; | ||
|
||
Future<ClosedCaptionFile> _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: <Widget>[ | ||
Container(padding: const EdgeInsets.only(top: 20.0)), | ||
const Text('With remote mp4'), | ||
FutureBuilder<bool>( | ||
key: _pictureInPictureKey, | ||
future: _controller.isPictureInPictureSupported(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't most of the UI need to be conditional on this? It looks like the button below to trigger PiP is unconditionally enabled and will call a method that isn't implemented on most platforms, for instance. |
||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) => | ||
Text(snapshot.data ?? false | ||
? 'Picture-in-picture is supported' | ||
: 'Picture-in-picture is not supported'), | ||
), | ||
Row( | ||
children: <Widget>[ | ||
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'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this something that clients have to call manually? Shouldn't the controller be determining this information from the widget as needed? |
||
), | ||
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: <Widget>[ | ||
VideoPlayer(_controller), | ||
ClosedCaption(text: _controller.value.caption.text), | ||
_ControlsOverlay(controller: _controller), | ||
VideoProgressIndicator(_controller, allowScrubbing: true), | ||
if (_controller.value.isPictureInPictureActive) ...<Widget>[ | ||
Container(color: Colors.white), | ||
const Column( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: <Widget>[ | ||
Icon(Icons.picture_in_picture), | ||
SizedBox(height: 8), | ||
Text('This video is playing in picture-in-picture.'), | ||
], | ||
), | ||
] else ...<Widget>[ | ||
VideoProgressIndicator(_controller, allowScrubbing: true), | ||
_ControlsOverlay(controller: _controller), | ||
], | ||
], | ||
), | ||
), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
stuartmorgan-g marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// 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<VideoPlayerValue> { | |
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<VideoPlayerValue> { | |
await _videoPlayerPlatform.setVolume(_textureId, value.volume); | ||
} | ||
|
||
/// Returns true if picture-in-picture is supported on the device. | ||
Future<bool> isPictureInPictureSupported() => | ||
_videoPlayerPlatform.isPictureInPictureSupported(); | ||
|
||
/// Enables/disables starting picture-in-picture automatically when the app goes to the background. | ||
Future<void> 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<void> setPictureInPictureOverlaySettings({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per my comment in the example code, it's not clear to me why this needs to be public API. |
||
required PictureInPictureOverlaySettings settings, | ||
}) async { | ||
if (!value.isInitialized || _isDisposed) { | ||
return; | ||
} | ||
await _videoPlayerPlatform.setPictureInPictureOverlaySettings( | ||
textureId: _textureId, | ||
settings: settings, | ||
); | ||
} | ||
|
||
/// Starts picture-in-picture mode. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The methods that require |
||
Future<void> startPictureInPicture() async { | ||
if (!value.isInitialized || _isDisposed) { | ||
return; | ||
} | ||
await _videoPlayerPlatform.startPictureInPicture(_textureId); | ||
} | ||
|
||
/// Stops picture-in-picture mode. | ||
Future<void> stopPictureInPicture() async { | ||
if (!value.isInitialized || _isDisposed) { | ||
return; | ||
} | ||
await _videoPlayerPlatform.stopPictureInPicture(_textureId); | ||
} | ||
|
||
Future<void> _applyPlaybackSpeed() async { | ||
if (_isDisposedOrNotInitialized) { | ||
return; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the Dart SDK version being changed in all the pubspecs? What new Dart feature does this PR rely on?