Skip to content

Commit

Permalink
Merge pull request TheWidlarzGroup#1325 from Khan/pip
Browse files Browse the repository at this point in the history
Implement picture in picture for iOS
  • Loading branch information
cobarx authored Feb 19, 2019
2 parents 967dc3f + 62dc913 commit d5fe47f
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 1 deletion.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ var styles = StyleSheet.create({
* [minLoadRetryCount](#minLoadRetryCount)
* [muted](#muted)
* [paused](#paused)
* [pictureInPicture](#pictureinpicture)
* [playInBackground](#playinbackground)
* [playWhenInactive](#playwheninactive)
* [poster](#poster)
Expand Down Expand Up @@ -301,14 +302,17 @@ var styles = StyleSheet.create({
* [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)
* [onLoad](#onload)
* [onLoadStart](#onloadstart)
* [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged)
* [onProgress](#onprogress)
* [onSeek](#onseek)
* [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop)
* [onTimedMetadata](#ontimedmetadata)

### Methods
* [dismissFullscreenPlayer](#dismissfullscreenplayer)
* [presentFullscreenPlayer](#presentfullscreenplayer)
* [save](#save)
* [restoreUserInterfaceForPictureInPictureStop](#restoreuserinterfaceforpictureinpicturestop)
* [seek](#seek)

### Configurable props
Expand Down Expand Up @@ -502,6 +506,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
Expand Down Expand Up @@ -942,6 +953,22 @@ Example:

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
Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing.

Expand Down Expand Up @@ -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

#### 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
Callback function that is called when timed metadata becomes available

Expand Down Expand Up @@ -1073,6 +1107,18 @@ Future:

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(seconds)`

Expand Down
21 changes: 21 additions & 0 deletions Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export default class Video extends Component {
return await NativeModules.VideoManager.save(options, findNodeHandle(this._root));
}

restoreUserInterfaceForPictureInPictureStopCompleted = (restored) => {
this.setNativeProps({ restoreUserInterfaceForPIPStopCompletionHandler: restored });
};

_assignRoot = (component) => {
this._root = component;
};
Expand Down Expand Up @@ -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) => {
if (this.props.onAudioFocusChanged) {
this.props.onAudioFocusChanged(event.nativeEvent);
Expand Down Expand Up @@ -282,6 +298,8 @@ export default class Video extends Component {
onPlaybackRateChange: this._onPlaybackRateChange,
onAudioFocusChanged: this._onAudioFocusChanged,
onAudioBecomingNoisy: this._onAudioBecomingNoisy,
onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged,
onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop,
});

const posterStyle = {
Expand Down Expand Up @@ -405,6 +423,7 @@ Video.propTypes = {
}),
stereoPan: PropTypes.number,
rate: PropTypes.number,
pictureInPicture: PropTypes.bool,
playInBackground: PropTypes.bool,
playWhenInactive: PropTypes.bool,
ignoreSilentSwitch: PropTypes.oneOf(['ignore', 'obey']),
Expand Down Expand Up @@ -436,6 +455,8 @@ Video.propTypes = {
onPlaybackRateChange: PropTypes.func,
onAudioFocusChanged: PropTypes.func,
onAudioBecomingNoisy: PropTypes.func,
onPictureInPictureStatusChanged: PropTypes.func,
needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func,
onExternalPlaybackChange: PropTypes.func,

/* Required by react-native */
Expand Down
4 changes: 3 additions & 1 deletion ios/Video/RCTVideo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +38,8 @@
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume;
@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange;
@property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPictureInPictureStatusChanged;
@property (nonatomic, copy) RCTBubblingEventBlock onRestoreUserInterfaceForPictureInPictureStop;

- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;

Expand Down
79 changes: 79 additions & 0 deletions ios/Video/RCTVideo.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ @implementation RCTVideo
AVPlayer *_player;
AVPlayerItem *_playerItem;
NSDictionary *_source;
AVPictureInPictureController *_pipController;
void (^__strong _Nonnull _restoreUserInterfaceForPIPStopCompletionHandler)(BOOL);
BOOL _playerItemObserversSet;
BOOL _playerBufferEmpty;
AVPlayerLayer *_playerLayer;
Expand Down Expand Up @@ -64,6 +66,7 @@ @implementation RCTVideo
BOOL _playbackStalled;
BOOL _playInBackground;
BOOL _playWhenInactive;
BOOL _pictureInPicture;
NSString * _ignoreSilentSwitch;
NSString * _resizeMode;
BOOL _fullscreen;
Expand Down Expand Up @@ -100,7 +103,9 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
_playInBackground = false;
_allowsExternalPlayback = YES;
_playWhenInactive = false;
_pictureInPicture = false;
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
_restoreUserInterfaceForPIPStopCompletionHandler = NULL;
#if __has_include(<react-native-video/RCTVideoCache.h>)
_videoCache = [RCTVideoCache sharedInstance];
#endif
Expand Down Expand Up @@ -786,6 +791,40 @@ - (void)setPlayWhenInactive:(BOOL)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
{
_ignoreSilentSwitch = ignoreSilentSwitch;
Expand Down Expand Up @@ -1240,6 +1279,8 @@ - (void)usePlayerLayer

[self.layer addSublayer:_playerLayer];
self.layer.needsDisplayOnBoundsChange = YES;

[self setupPipController];
}
}

Expand Down Expand Up @@ -1496,4 +1537,42 @@ - (NSString *)cacheDirectoryPath {
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
4 changes: 4 additions & 0 deletions ios/Video/RCTVideoManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -42,6 +43,7 @@ - (dispatch_queue_t)methodQueue
RCT_EXPORT_VIEW_PROPERTY(filter, NSString);
RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
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);
Expand Down Expand Up @@ -77,6 +79,8 @@ - (dispatch_queue_t)methodQueue
}
}];
}
RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock);

- (NSDictionary *)constantsToExport
{
Expand Down

0 comments on commit d5fe47f

Please sign in to comment.