Implement picture in picture for iOS
Test Plan: - Run on ipad - test out onIsPictureInPictureSupported, onIsPictureInPictureActive, restoreUserInterfaceForPictureInPictureStop, startPictureInPicture, stopPictureInPicture
This commit is contained in:
parent
35e26427ea
commit
617b046789
73
README.md
73
README.md
@ -298,6 +298,8 @@ var styles = StyleSheet.create({
|
||||
* [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent)
|
||||
* [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss)
|
||||
* [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)
|
||||
* [onIsPictureInPictureActive](#onispictureinpictureactive)
|
||||
* [onIsPictureInPictureSupported](#onispictureinpicturesupported)
|
||||
* [onLoad](#onload)
|
||||
* [onLoadStart](#onloadstart)
|
||||
* [onProgress](#onprogress)
|
||||
@ -308,7 +310,10 @@ var styles = StyleSheet.create({
|
||||
* [dismissFullscreenPlayer](#dismissfullscreenplayer)
|
||||
* [presentFullscreenPlayer](#presentfullscreenplayer)
|
||||
* [save](#save)
|
||||
* [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)
|
||||
* [startPictureInPicture](#startpictureinpicture)
|
||||
* [seek](#seek)
|
||||
* [stopPictureInPicture](#stoppictureinpicture)
|
||||
|
||||
### Configurable props
|
||||
|
||||
@ -861,6 +866,38 @@ Payload: none
|
||||
|
||||
Platforms: Android ExoPlayer, Android MediaPlayer, iOS
|
||||
|
||||
#### onIsPictureInPictureActive
|
||||
Callback function that is called when picture in picture becames active on inactive.
|
||||
|
||||
Property | Type | Description
|
||||
--- | --- | ---
|
||||
active | boolean | Boolean indicating whether picture in picture is active
|
||||
|
||||
Example:
|
||||
```
|
||||
{
|
||||
active: true
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### onIsPictureInPictureSupported
|
||||
Callback function that is called initially to determine whether or not picture in picture is supported.
|
||||
|
||||
Property | Type | Description
|
||||
--- | --- | ---
|
||||
supported | boolean | Boolean indicating whether picture in picture is supported
|
||||
|
||||
Example:
|
||||
```
|
||||
{
|
||||
supported: true
|
||||
}
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### onLoad
|
||||
Callback function that is called when the media is loaded and ready to play.
|
||||
|
||||
@ -1057,6 +1094,30 @@ Future:
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### restoreUserInterfaceForPictureInPictureStop
|
||||
`restoreUserInterfaceForPictureInPictureStop(restore)`
|
||||
|
||||
This function corresponds to Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: After picture in picture stops, this function must be called.
|
||||
|
||||
Example:
|
||||
```
|
||||
this.player.restoreUserInterfaceForPictureInPictureStop(true);
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### startPictureInPicture
|
||||
`startPictureInPicture()`
|
||||
|
||||
Calling this function will start picture in picture if it is supported.
|
||||
|
||||
Example:
|
||||
```
|
||||
this.player.startPictureInPicture();
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### seek()
|
||||
`seek(seconds)`
|
||||
|
||||
@ -1086,6 +1147,18 @@ this.player.seek(120, 50); // Seek to 2 minutes with +/- 50 milliseconds accurac
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
#### stopPictureInPicture
|
||||
`stopPictureInPicture()`
|
||||
|
||||
Calling this function will stop picture in picture if it is currently active.
|
||||
|
||||
Example:
|
||||
```
|
||||
this.player.stopPictureInPicture();
|
||||
```
|
||||
|
||||
Platforms: iOS
|
||||
|
||||
|
||||
|
||||
|
||||
|
28
Video.js
28
Video.js
@ -78,6 +78,18 @@ export default class Video extends Component {
|
||||
return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
|
||||
}
|
||||
|
||||
startPictureInPicture = () => {
|
||||
this.setNativeProps({ pictureInPicture: true });
|
||||
};
|
||||
|
||||
stopPictureInPicture = () => {
|
||||
this.setNativeProps({ pictureInPicture: false });
|
||||
};
|
||||
|
||||
restoreUserInterfaceForPictureInPictureStop = (restore) => {
|
||||
this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restore });
|
||||
};
|
||||
|
||||
_assignRoot = (component) => {
|
||||
this._root = component;
|
||||
};
|
||||
@ -198,6 +210,18 @@ export default class Video extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
_onIsPictureInPictureSupported = (event) => {
|
||||
if (this.props.onIsPictureInPictureSupported) {
|
||||
this.props.onIsPictureInPictureSupported(event.nativeEvent);
|
||||
}
|
||||
};
|
||||
|
||||
_onIsPictureInPictureActive = (event) => {
|
||||
if (this.props.onIsPictureInPictureActive) {
|
||||
this.props.onIsPictureInPictureActive(event.nativeEvent);
|
||||
}
|
||||
};
|
||||
|
||||
_onAudioFocusChanged = (event) => {
|
||||
if (this.props.onAudioFocusChanged) {
|
||||
this.props.onAudioFocusChanged(event.nativeEvent);
|
||||
@ -267,6 +291,8 @@ export default class Video extends Component {
|
||||
onPlaybackRateChange: this._onPlaybackRateChange,
|
||||
onAudioFocusChanged: this._onAudioFocusChanged,
|
||||
onAudioBecomingNoisy: this._onAudioBecomingNoisy,
|
||||
onIsPictureInPictureSupported: this._onIsPictureInPictureSupported,
|
||||
onIsPictureInPictureActive: this._onIsPictureInPictureActive,
|
||||
});
|
||||
|
||||
const posterStyle = {
|
||||
@ -420,6 +446,8 @@ Video.propTypes = {
|
||||
onPlaybackRateChange: PropTypes.func,
|
||||
onAudioFocusChanged: PropTypes.func,
|
||||
onAudioBecomingNoisy: PropTypes.func,
|
||||
onIsPictureInPictureSupported: PropTypes.func,
|
||||
onIsPictureInPictureActive: PropTypes.func,
|
||||
onExternalPlaybackChange: PropTypes.func,
|
||||
|
||||
/* Required by react-native */
|
||||
|
@ -16,7 +16,7 @@
|
||||
#if __has_include(<react-native-video/RCTVideoCache.h>)
|
||||
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, DVAssetLoaderDelegatesDelegate>
|
||||
#else
|
||||
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate>
|
||||
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate>
|
||||
#endif
|
||||
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart;
|
||||
@ -38,6 +38,8 @@
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume;
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange;
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange;
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureSupported;
|
||||
@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureActive;
|
||||
|
||||
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
|
@ -27,6 +27,8 @@ static int const RCTVideoUnset = -1;
|
||||
AVPlayer *_player;
|
||||
AVPlayerItem *_playerItem;
|
||||
NSDictionary *_source;
|
||||
AVPictureInPictureController *_pipController;
|
||||
void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL);
|
||||
BOOL _playerItemObserversSet;
|
||||
BOOL _playerBufferEmpty;
|
||||
AVPlayerLayer *_playerLayer;
|
||||
@ -101,6 +103,7 @@ static int const RCTVideoUnset = -1;
|
||||
_allowsExternalPlayback = YES;
|
||||
_playWhenInactive = false;
|
||||
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
|
||||
#if __has_include(<react-native-video/RCTVideoCache.h>)
|
||||
_videoCache = [RCTVideoCache sharedInstance];
|
||||
#endif
|
||||
@ -379,6 +382,12 @@ static int const RCTVideoUnset = -1;
|
||||
@"target": self.reactTag
|
||||
});
|
||||
}
|
||||
|
||||
if (@available(iOS 9, *)) {
|
||||
if (self.onIsPictureInPictureSupported) {
|
||||
self.onIsPictureInPictureSupported(@{@"supported": [NSNumber numberWithBool:(bool)[AVPictureInPictureController isPictureInPictureSupported]]});
|
||||
}
|
||||
}
|
||||
}];
|
||||
});
|
||||
_videoLoadStarted = YES;
|
||||
@ -780,6 +789,37 @@ static int const RCTVideoUnset = -1;
|
||||
_playWhenInactive = playWhenInactive;
|
||||
}
|
||||
|
||||
- (void)setPictureInPicture:(BOOL)pictureInPicture
|
||||
{
|
||||
if (_pipController && pictureInPicture && ![_pipController isPictureInPictureActive]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_pipController startPictureInPicture];
|
||||
});
|
||||
} else if (_pipController && !pictureInPicture && [_pipController isPictureInPictureActive]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_pipController stopPictureInPicture];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore
|
||||
{
|
||||
if (_restoreUserInterfaceForPIPStopCompletionHandler != NULL) {
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler(restore);
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupPipController {
|
||||
if (@available(iOS 9, *)) {
|
||||
if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) {
|
||||
// Create new controller passing reference to the AVPlayerLayer
|
||||
_pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer];
|
||||
_pipController.delegate = self;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch
|
||||
{
|
||||
_ignoreSilentSwitch = ignoreSilentSwitch;
|
||||
@ -1234,6 +1274,8 @@ static int const RCTVideoUnset = -1;
|
||||
|
||||
[self.layer addSublayer:_playerLayer];
|
||||
self.layer.needsDisplayOnBoundsChange = YES;
|
||||
|
||||
[self setupPipController];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1490,4 +1532,35 @@ static int const RCTVideoUnset = -1;
|
||||
return array[0];
|
||||
}
|
||||
|
||||
#pragma mark - Picture in Picture
|
||||
|
||||
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
if (self.onIsPictureInPictureActive && _pipController) {
|
||||
self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:false]});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
if (self.onIsPictureInPictureActive && _pipController) {
|
||||
self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:true]});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
|
||||
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler {
|
||||
NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited.");
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler = completionHandler;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -42,6 +42,8 @@ RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
|
||||
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);
|
||||
@ -77,6 +79,8 @@ RCT_REMAP_METHOD(save,
|
||||
}
|
||||
}];
|
||||
}
|
||||
RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureSupported, RCTBubblingEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureActive, RCTBubblingEventBlock);
|
||||
|
||||
- (NSDictionary *)constantsToExport
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user