Merge pull request #1325 from Khan/pip

Implement picture in picture for iOS
This commit is contained in:
Hampton Maxwell 2019-02-18 22:13:02 -08:00 committed by GitHub
commit d5fe47f238
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 1 deletions

View File

@ -272,6 +272,7 @@ var styles = StyleSheet.create({
* [minLoadRetryCount](#minLoadRetryCount) * [minLoadRetryCount](#minLoadRetryCount)
* [muted](#muted) * [muted](#muted)
* [paused](#paused) * [paused](#paused)
* [pictureInPicture](#pictureinpicture)
* [playInBackground](#playinbackground) * [playInBackground](#playinbackground)
* [playWhenInactive](#playwheninactive) * [playWhenInactive](#playwheninactive)
* [poster](#poster) * [poster](#poster)
@ -301,14 +302,17 @@ var styles = StyleSheet.create({
* [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)
* [onLoad](#onload) * [onLoad](#onload)
* [onLoadStart](#onloadstart) * [onLoadStart](#onloadstart)
* [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged)
* [onProgress](#onprogress) * [onProgress](#onprogress)
* [onSeek](#onseek) * [onSeek](#onseek)
* [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop)
* [onTimedMetadata](#ontimedmetadata) * [onTimedMetadata](#ontimedmetadata)
### Methods ### Methods
* [dismissFullscreenPlayer](#dismissfullscreenplayer) * [dismissFullscreenPlayer](#dismissfullscreenplayer)
* [presentFullscreenPlayer](#presentfullscreenplayer) * [presentFullscreenPlayer](#presentfullscreenplayer)
* [save](#save) * [save](#save)
* [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)
* [seek](#seek) * [seek](#seek)
### Configurable props ### Configurable props
@ -502,6 +506,13 @@ Controls whether the media is paused
Platforms: all 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 #### playInBackground
Determine whether the media should continue playing while the app is in the background. This allows customers to continue listening to the audio. 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 * **false (default)** - Don't continue playing the media
@ -942,6 +953,22 @@ Example:
Platforms: all Platforms: all
#### onPictureInPictureStatusChanged
Callback function that is called when picture in picture becomes active or inactive.
Property | Type | Description
--- | --- | ---
isActive | boolean | Boolean indicating whether picture in picture is active
Example:
```
{
isActive: true
}
```
Platforms: iOS
#### onProgress #### onProgress
Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing. Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing.
@ -985,6 +1012,13 @@ Both the currentTime & seekTime are reported because the video player may not se
Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Windows UWP
#### onRestoreUserInterfaceForPictureInPictureStop
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
Payload: none
Platforms: iOS
#### onTimedMetadata #### onTimedMetadata
Callback function that is called when timed metadata becomes available Callback function that is called when timed metadata becomes available
@ -1073,6 +1107,18 @@ Future:
Platforms: iOS Platforms: iOS
#### restoreUserInterfaceForPictureInPictureStopCompleted
`restoreUserInterfaceForPictureInPictureStopCompleted(restored)`
This function corresponds to the completion handler in Apple's [restoreUserInterfaceForPictureInPictureStop](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). IMPORTANT: This function must be called after `onRestoreUserInterfaceForPictureInPictureStop` is called.
Example:
```
this.player.restoreUserInterfaceForPictureInPictureStopCompleted(true);
```
Platforms: iOS
#### seek() #### seek()
`seek(seconds)` `seek(seconds)`

View File

@ -78,6 +78,10 @@ export default class Video extends Component {
return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
} }
restoreUserInterfaceForPictureInPictureStopCompleted = (restored) => {
this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restored });
};
_assignRoot = (component) => { _assignRoot = (component) => {
this._root = component; this._root = component;
}; };
@ -198,6 +202,18 @@ export default class Video extends Component {
} }
}; };
_onPictureInPictureStatusChanged = (event) => {
if (this.props.onPictureInPictureStatusChanged) {
this.props.onPictureInPictureStatusChanged(event.nativeEvent);
}
};
_onRestoreUserInterfaceForPictureInPictureStop = (event) => {
if (this.props.onRestoreUserInterfaceForPictureInPictureStop) {
this.props.onRestoreUserInterfaceForPictureInPictureStop();
}
};
_onAudioFocusChanged = (event) => { _onAudioFocusChanged = (event) => {
if (this.props.onAudioFocusChanged) { if (this.props.onAudioFocusChanged) {
this.props.onAudioFocusChanged(event.nativeEvent); this.props.onAudioFocusChanged(event.nativeEvent);
@ -282,6 +298,8 @@ export default class Video extends Component {
onPlaybackRateChange: this._onPlaybackRateChange, onPlaybackRateChange: this._onPlaybackRateChange,
onAudioFocusChanged: this._onAudioFocusChanged, onAudioFocusChanged: this._onAudioFocusChanged,
onAudioBecomingNoisy: this._onAudioBecomingNoisy, onAudioBecomingNoisy: this._onAudioBecomingNoisy,
onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged,
onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop,
}); });
const posterStyle = { const posterStyle = {
@ -405,6 +423,7 @@ Video.propTypes = {
}), }),
stereoPan: PropTypes.number, stereoPan: PropTypes.number,
rate: PropTypes.number, rate: PropTypes.number,
pictureInPicture: PropTypes.bool,
playInBackground: PropTypes.bool, playInBackground: PropTypes.bool,
playWhenInactive: PropTypes.bool, playWhenInactive: PropTypes.bool,
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']), ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
@ -436,6 +455,8 @@ Video.propTypes = {
onPlaybackRateChange: PropTypes.func, onPlaybackRateChange: PropTypes.func,
onAudioFocusChanged: PropTypes.func, onAudioFocusChanged: PropTypes.func,
onAudioBecomingNoisy: PropTypes.func, onAudioBecomingNoisy: PropTypes.func,
onPictureInPictureStatusChanged: PropTypes.func,
needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func,
onExternalPlaybackChange: PropTypes.func, onExternalPlaybackChange: PropTypes.func,
/* Required by react-native */ /* Required by react-native */

View File

@ -16,7 +16,7 @@
#if __has_include(<react-native-video/RCTVideoCache.h>) #if __has_include(<react-native-video/RCTVideoCache.h>)
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, DVAssetLoaderDelegatesDelegate> @interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, DVAssetLoaderDelegatesDelegate>
#else #else
@interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate> @interface RCTVideo : UIView <RCTVideoPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate>
#endif #endif
@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; @property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart;
@ -38,6 +38,8 @@
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume;
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; @property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; @property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPictureInPictureStatusChanged;
@property (nonatomic, copy) RCTBubblingEventBlock onRestoreUserInterfaceForPictureInPictureStop;
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;

View File

@ -27,6 +27,8 @@ static int const RCTVideoUnset = -1;
AVPlayer *_player; AVPlayer *_player;
AVPlayerItem *_playerItem; AVPlayerItem *_playerItem;
NSDictionary *_source; NSDictionary *_source;
AVPictureInPictureController *_pipController;
void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL);
BOOL _playerItemObserversSet; BOOL _playerItemObserversSet;
BOOL _playerBufferEmpty; BOOL _playerBufferEmpty;
AVPlayerLayer *_playerLayer; AVPlayerLayer *_playerLayer;
@ -64,6 +66,7 @@ static int const RCTVideoUnset = -1;
BOOL _playbackStalled; BOOL _playbackStalled;
BOOL _playInBackground; BOOL _playInBackground;
BOOL _playWhenInactive; BOOL _playWhenInactive;
BOOL _pictureInPicture;
NSString * _ignoreSilentSwitch; NSString * _ignoreSilentSwitch;
NSString * _resizeMode; NSString * _resizeMode;
BOOL _fullscreen; BOOL _fullscreen;
@ -100,7 +103,9 @@ static int const RCTVideoUnset = -1;
_playInBackground = false; _playInBackground = false;
_allowsExternalPlayback = YES; _allowsExternalPlayback = YES;
_playWhenInactive = false; _playWhenInactive = false;
_pictureInPicture = false;
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey _ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
#if __has_include(<react-native-video/RCTVideoCache.h>) #if __has_include(<react-native-video/RCTVideoCache.h>)
_videoCache = [RCTVideoCache sharedInstance]; _videoCache = [RCTVideoCache sharedInstance];
#endif #endif
@ -786,6 +791,40 @@ static int const RCTVideoUnset = -1;
_playWhenInactive = playWhenInactive; _playWhenInactive = playWhenInactive;
} }
- (void)setPictureInPicture:(BOOL)pictureInPicture
{
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]) {
dispatch_async(dispatch_get_main_queue(), ^{
[_pipController stopPictureInPicture];
});
}
}
- (void)setRestoreUserInterfaceForPIPStopCompletionHandler:(BOOL)restore
{
if (_restoreUserInterfaceForPIPStopCompletionHandler != NULL) {
_restoreUserInterfaceForPIPStopCompletionHandler(restore);
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
}
}
- (void)setupPipController {
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 - (void)setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch
{ {
_ignoreSilentSwitch = ignoreSilentSwitch; _ignoreSilentSwitch = ignoreSilentSwitch;
@ -1240,6 +1279,8 @@ static int const RCTVideoUnset = -1;
[self.layer addSublayer:_playerLayer]; [self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES; self.layer.needsDisplayOnBoundsChange = YES;
[self setupPipController];
} }
} }
@ -1496,4 +1537,42 @@ static int const RCTVideoUnset = -1;
return array[0]; return array[0];
} }
#pragma mark - Picture in Picture
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
if (self.onPictureInPictureStatusChanged) {
self.onPictureInPictureStatusChanged(@{
@"isActive": [NSNumber numberWithBool:false]
});
}
}
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
if (self.onPictureInPictureStatusChanged) {
self.onPictureInPictureStatusChanged(@{
@"isActive": [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.");
if (self.onRestoreUserInterfaceForPictureInPictureStop) {
self.onRestoreUserInterfaceForPictureInPictureStop(@{});
}
_restoreUserInterfaceForPIPStopCompletionHandler = completionHandler;
}
@end @end

View File

@ -32,6 +32,7 @@ RCT_EXPORT_VIEW_PROPERTY(controls, BOOL);
RCT_EXPORT_VIEW_PROPERTY(volume, float); RCT_EXPORT_VIEW_PROPERTY(volume, float);
RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL); RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL); RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString); RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(rate, float);
RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary);
@ -42,6 +43,7 @@ RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString);
RCT_EXPORT_VIEW_PROPERTY(filter, NSString); RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);
@ -77,6 +79,8 @@ RCT_REMAP_METHOD(save,
} }
}]; }];
} }
RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock);
- (NSDictionary *)constantsToExport - (NSDictionary *)constantsToExport
{ {