diff --git a/README.md b/README.md index dd1aa6f9c4..a3325247c4 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ var styles = StyleSheet.create({ * [ignoreSilentSwitch](#ignoresilentswitch) * [muted](#muted) * [paused](#paused) +* [pictureInPicture](#pictureinpicture) * [playInBackground](#playinbackground) * [playWhenInactive](#playwheninactive) * [poster](#poster) @@ -283,6 +284,7 @@ var styles = StyleSheet.create({ * [volume](#volume) ### Event props +* [needsToRestoreUserInterfaceForPictureInPictureStop](#needstorestoreuserinterfaceforpictureinpicturestop) * [onAudioBecomingNoisy](#onaudiobecomingnoisy) * [onEnd](#onend) * [onExternalPlaybackChange](#onexternalplaybackchange) @@ -290,8 +292,7 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent) * [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss) * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) -* [onIsPictureInPictureActive](#onispictureinpictureactive) -* [onIsPictureInPictureSupported](#onispictureinpicturesupported) +* [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) * [onLoad](#onload) * [onLoadStart](#onloadstart) * [onProgress](#onprogress) @@ -301,9 +302,7 @@ var styles = StyleSheet.create({ * [dismissFullscreenPlayer](#dismissfullscreenplayer) * [presentFullscreenPlayer](#presentfullscreenplayer) * [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop) -* [startPictureInPicture](#startpictureinpicture) * [seek](#seek) -* [stopPictureInPicture](#stoppictureinpicture) ### Configurable props @@ -418,6 +417,13 @@ Controls whether the media is paused Platforms: all +#### pictureInPicture +Determine whether the media should played as picture in picture. +* **false (default)** - Don't not play as picture in picture +* **true** - Play the media as picture in picture + +Platforms: iOS + #### playInBackground Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio. * **false (default)** - Don't continue playing the media @@ -672,6 +678,13 @@ Platforms: all ### Event props +#### needsToRestoreUserInterfaceForPictureInPictureStop +Callback function that is called when picture in picture is stopped and requires restoring the user interface. Call `restoreUserInterfaceForPictureInPictureStop` when this method is called. + +Payload: none + +Platforms: iOS + #### onAudioBecomingNoisy Callback function that is called when the audio is about to become 'noisy' due to a change in audio outputs. Typically this is called when audio output is being switched from an external source like headphones back to the internal speaker. It's a good idea to pause the media when this happens so the speaker doesn't start blasting sound. @@ -732,33 +745,17 @@ Payload: none Platforms: Android ExoPlayer, Android MediaPlayer, iOS -#### onIsPictureInPictureActive +#### onPictureInPictureStatusChanged Callback function that is called when picture in picture becomes active or 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 +isActive | boolean | Boolean indicating whether picture in picture is active Example: ``` { - supported: true + isActive: true } ``` @@ -913,7 +910,7 @@ Platforms: Android ExoPlayer, Android MediaPlayer, 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. +This function corresponds to Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after needsToRestoreUserInterfaceForPictureInPictureStop is called. Example: ``` @@ -922,18 +919,6 @@ 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)` @@ -963,18 +948,6 @@ 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 - ### iOS App Transport Security diff --git a/Video.js b/Video.js index 77ce849057..8153b490fd 100644 --- a/Video.js +++ b/Video.js @@ -71,14 +71,6 @@ export default class Video extends Component { this.setNativeProps({ fullscreen: false }); }; - startPictureInPicture = () => { - this.setNativeProps({ pictureInPicture: true }); - }; - - stopPictureInPicture = () => { - this.setNativeProps({ pictureInPicture: false }); - }; - restoreUserInterfaceForPictureInPictureStop = (restore) => { this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restore }); }; @@ -197,15 +189,15 @@ export default class Video extends Component { } }; - _onIsPictureInPictureSupported = (event) => { - if (this.props.onIsPictureInPictureSupported) { - this.props.onIsPictureInPictureSupported(event.nativeEvent); + _onPictureInPictureStatusChanged = (event) => { + if (this.props.onPictureInPictureStatusChanged) { + this.props.onPictureInPictureStatusChanged(event.nativeEvent); } }; - _onIsPictureInPictureActive = (event) => { - if (this.props.onIsPictureInPictureActive) { - this.props.onIsPictureInPictureActive(event.nativeEvent); + onRestoreUserInterfaceForPictureInPictureStop = (event) => { + if (this.props.onRestoreUserInterfaceForPictureInPictureStop) { + this.props.onRestoreUserInterfaceForPictureInPictureStop(); } }; @@ -277,8 +269,8 @@ export default class Video extends Component { onPlaybackRateChange: this._onPlaybackRateChange, onAudioFocusChanged: this._onAudioFocusChanged, onAudioBecomingNoisy: this._onAudioBecomingNoisy, - onIsPictureInPictureSupported: this._onIsPictureInPictureSupported, - onIsPictureInPictureActive: this._onIsPictureInPictureActive, + onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged, + onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop, }); const posterStyle = { @@ -373,6 +365,7 @@ Video.propTypes = { }), stereoPan: PropTypes.number, rate: PropTypes.number, + pictureInPicture: PropTypes.bool, playInBackground: PropTypes.bool, playWhenInactive: PropTypes.bool, ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), @@ -400,8 +393,8 @@ Video.propTypes = { onPlaybackRateChange: PropTypes.func, onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, - onIsPictureInPictureSupported: PropTypes.func, - onIsPictureInPictureActive: PropTypes.func, + onPictureInPictureStatusChanged: PropTypes.func, + needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func, onExternalPlaybackChange: PropTypes.func, /* Required by react-native */ diff --git a/ios/Video/RCTVideo.h b/ios/Video/RCTVideo.h index 9ff1064268..f9bc7b6960 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -37,7 +37,8 @@ @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; @property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureSupported; -@property (nonatomic, copy) RCTBubblingEventBlock onIsPictureInPictureActive; +@property (nonatomic, copy) RCTBubblingEventBlock onPictureInPictureStatusChanged; +@property (nonatomic, copy) RCTBubblingEventBlock onRestoreUserInterfaceForPictureInPictureStop; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index e35ad9d728..f4e0695dd9 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -63,6 +63,7 @@ @implementation RCTVideo BOOL _playbackStalled; BOOL _playInBackground; BOOL _playWhenInactive; + BOOL _pictureInPicture; NSString * _ignoreSilentSwitch; NSString * _resizeMode; BOOL _fullscreen; @@ -95,6 +96,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _playInBackground = false; _allowsExternalPlayback = YES; _playWhenInactive = false; + _pictureInPicture = false; _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey _restoreUserInterfaceForPIPStopCompletionHandler = NULL; #if __has_include() @@ -372,12 +374,6 @@ - (void)setSrc:(NSDictionary *)source @"target": self.reactTag }); } - - if (@available(iOS 9, *)) { - if (self.onIsPictureInPictureSupported) { - self.onIsPictureInPictureSupported(@{@"supported": [NSNumber numberWithBool:(bool)[AVPictureInPictureController isPictureInPictureSupported]]}); - } - } }]; }); _videoLoadStarted = YES; @@ -758,11 +754,16 @@ - (void)setPlayWhenInactive:(BOOL)playWhenInactive - (void)setPictureInPicture:(BOOL)pictureInPicture { - if (_pipController && pictureInPicture && ![_pipController isPictureInPictureActive]) { + if (_pictureInPicture == pictureInPicture) { + return; + } + + _pictureInPicture = pictureInPicture; + if (_pipController && _pictureInPicture && ![_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController startPictureInPicture]; }); - } else if (_pipController && !pictureInPicture && [_pipController isPictureInPictureActive]) { + } else if (_pipController && !_pictureInPicture && [_pipController isPictureInPictureActive]) { dispatch_async(dispatch_get_main_queue(), ^{ [_pipController stopPictureInPicture]; }); @@ -778,12 +779,10 @@ - (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore } - (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; - } + if (!_pipController && _playerLayer && [AVPictureInPictureController isPictureInPictureSupported]) { + // Create new controller passing reference to the AVPlayerLayer + _pipController = [[AVPictureInPictureController alloc] initWithPlayerLayer:_playerLayer]; + _pipController.delegate = self; } } @@ -1383,14 +1382,18 @@ - (void)removeFromSuperview #pragma mark - Picture in Picture - (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onIsPictureInPictureActive && _pipController) { - self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:false]}); + if (self.onPictureInPictureStatusChanged) { + self.onPictureInPictureStatusChanged(@{ + @"isActive": [NSNumber numberWithBool:false] + }); } } - (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { - if (self.onIsPictureInPictureActive && _pipController) { - self.onIsPictureInPictureActive(@{@"active": [NSNumber numberWithBool:true]}); + if (self.onPictureInPictureStatusChanged) { + self.onPictureInPictureStatusChanged(@{ + @"isActive": [NSNumber numberWithBool:true] + }); } } @@ -1408,6 +1411,9 @@ - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPict - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL))completionHandler { NSAssert(_restoreUserInterfaceForPIPStopCompletionHandler == NULL, @"restoreUserInterfaceForPIPStopCompletionHandler was not called after picture in picture was exited."); + if (self.onRestoreUserInterfaceForPictureInPictureStop) { + self.onRestoreUserInterfaceForPictureInPictureStop(@{}); + } _restoreUserInterfaceForPIPStopCompletionHandler = completionHandler; } diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index bf152eedb6..cf88ade524 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -32,6 +32,7 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(volume, float); RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL); RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL); +RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL); RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); @@ -39,7 +40,6 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString); 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); @@ -60,8 +60,8 @@ - (dispatch_queue_t)methodQueue RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureSupported, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onIsPictureInPictureActive, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); - (NSDictionary *)constantsToExport {