Implement picture in picture for iOS

Test Plan:
 - Run on ipad
 - test out onIsPictureInPictureSupported, onIsPictureInPictureActive, restoreUserInterfaceForPictureInPictureStop, startPictureInPicture, stopPictureInPicture
This commit is contained in:
Abdulrahman Alzenki 2018-10-26 13:33:03 -07:00 committed by Abdulrahman Alzenki
parent 35e26427ea
commit 617b046789
5 changed files with 181 additions and 1 deletions

View File

@ -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

View File

@ -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 */

View File

@ -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;

View File

@ -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

View File

@ -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
{