diff --git a/RCTVideo.h b/RCTVideo.h index 757ed542..6f8aeab2 100644 --- a/RCTVideo.h +++ b/RCTVideo.h @@ -1,10 +1,13 @@ #import "RCTView.h" #import #import "AVKit/AVKit.h" +#import "UIView+FindUIViewController.h" +#import "RCTVideoPlayerViewController.h" +#import "RCTVideoPlayerViewControllerDelegate.h" @class RCTEventDispatcher; -@interface RCTVideo : UIView +@interface RCTVideo : UIView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/RCTVideo.m b/RCTVideo.m index 5cf83d27..a613ded7 100644 --- a/RCTVideo.m +++ b/RCTVideo.m @@ -37,6 +37,8 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; BOOL _paused; BOOL _repeat; NSString * _resizeMode; + BOOL _fullscreenPlayerPresented; + UIViewController * _presentingViewController; } - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher @@ -69,7 +71,9 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; } - (AVPlayerViewController*)createPlayerViewController:(AVPlayer*)player withPlayerItem:(AVPlayerItem*)playerItem { - AVPlayerViewController* playerLayer= [[AVPlayerViewController alloc] init]; + RCTVideoPlayerViewController* playerLayer= [[RCTVideoPlayerViewController alloc] init]; + playerLayer.showsPlaybackControls = NO; + playerLayer.rctDelegate = self; playerLayer.view.frame = self.bounds; playerLayer.player = _player; playerLayer.view.frame = self.bounds; @@ -436,6 +440,54 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; _repeat = repeat; } +- (BOOL)getFullscreen +{ + return _fullscreenPlayerPresented; +} + +- (void)setFullscreen:(BOOL)fullscreen +{ + if( fullscreen && !_fullscreenPlayerPresented ) + { + // Ensure player view controller is not null + if( !_playerViewController ) + { + [self usePlayerViewController]; + } + // Set presentation style to fullscreen + [_playerViewController setModalPresentationStyle:UIModalPresentationFullScreen]; + + // Find the nearest view controller + UIViewController *viewController = [self firstAvailableUIViewController]; + if( !viewController ) + { + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + viewController = keyWindow.rootViewController; + if( viewController.childViewControllers.count > 0 ) + { + viewController = viewController.childViewControllers.lastObject; + } + } + if( viewController ) + { + _presentingViewController = viewController; + [_eventDispatcher sendInputEventWithName:@"onVideoFullscreenPlayerWillPresent" body:@{@"target": self.reactTag}]; + [viewController presentViewController:_playerViewController animated:true completion:^{ + _playerViewController.showsPlaybackControls = YES; + _fullscreenPlayerPresented = fullscreen; + [_eventDispatcher sendInputEventWithName:@"onVideoFullscreenPlayerDidPresent" body:@{@"target": self.reactTag}]; + }]; + } + } + else if ( !fullscreen && _fullscreenPlayerPresented ) + { + [self videoPlayerViewControllerWillDismiss:_playerViewController]; + [_presentingViewController dismissViewControllerAnimated:true completion:^{ + [self videoPlayerViewControllerDidDismiss:_playerViewController]; + }]; + } +} + - (void)usePlayerViewController { if( _player ) @@ -478,6 +530,27 @@ static NSString *const playbackBufferEmptyKeyPath = @"playbackBufferEmpty"; } } +#pragma mark - RCTVideoPlayerViewControllerDelegate + +- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController +{ + if (_playerViewController == playerViewController && _fullscreenPlayerPresented) + { + [_eventDispatcher sendInputEventWithName:@"onVideoFullscreenPlayerWillDismiss" body:@{@"target": self.reactTag}]; + } +} + +- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController +{ + if (_playerViewController == playerViewController && _fullscreenPlayerPresented) + { + _fullscreenPlayerPresented = false; + _presentingViewController = nil; + [self applyModifiers]; + [_eventDispatcher sendInputEventWithName:@"onVideoFullscreenPlayerDidDismiss" body:@{@"target": self.reactTag}]; + } +} + #pragma mark - React View Management - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex diff --git a/RCTVideo.xcodeproj/project.pbxproj b/RCTVideo.xcodeproj/project.pbxproj index 9a65aa45..502dd2a2 100644 --- a/RCTVideo.xcodeproj/project.pbxproj +++ b/RCTVideo.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 31CAFB211CADA8CD009BCF6F /* UIView+FindUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 31CAFB201CADA8CD009BCF6F /* UIView+FindUIViewController.m */; }; + 31CAFB2F1CADC77F009BCF6F /* RCTVideoPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 31CAFB2E1CADC77F009BCF6F /* RCTVideoPlayerViewController.m */; }; BBD49E3F1AC8DEF000610F8E /* RCTVideo.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD49E3A1AC8DEF000610F8E /* RCTVideo.m */; }; BBD49E401AC8DEF000610F8E /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = BBD49E3C1AC8DEF000610F8E /* RCTVideoManager.m */; }; /* End PBXBuildFile section */ @@ -25,6 +27,11 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 31CAFB1F1CADA8CD009BCF6F /* UIView+FindUIViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+FindUIViewController.h"; sourceTree = ""; }; + 31CAFB201CADA8CD009BCF6F /* UIView+FindUIViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+FindUIViewController.m"; sourceTree = ""; }; + 31CAFB2D1CADC77F009BCF6F /* RCTVideoPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVideoPlayerViewController.h; sourceTree = ""; }; + 31CAFB2E1CADC77F009BCF6F /* RCTVideoPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVideoPlayerViewController.m; sourceTree = ""; }; + 31CAFB301CAE6B5F009BCF6F /* RCTVideoPlayerViewControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVideoPlayerViewControllerDelegate.h; sourceTree = ""; }; BBD49E391AC8DEF000610F8E /* RCTVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVideo.h; sourceTree = ""; }; BBD49E3A1AC8DEF000610F8E /* RCTVideo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVideo.m; sourceTree = ""; }; BBD49E3B1AC8DEF000610F8E /* RCTVideoManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVideoManager.h; sourceTree = ""; }; @@ -55,8 +62,13 @@ children = ( BBD49E391AC8DEF000610F8E /* RCTVideo.h */, BBD49E3A1AC8DEF000610F8E /* RCTVideo.m */, + 31CAFB301CAE6B5F009BCF6F /* RCTVideoPlayerViewControllerDelegate.h */, + 31CAFB2D1CADC77F009BCF6F /* RCTVideoPlayerViewController.h */, + 31CAFB2E1CADC77F009BCF6F /* RCTVideoPlayerViewController.m */, BBD49E3B1AC8DEF000610F8E /* RCTVideoManager.h */, BBD49E3C1AC8DEF000610F8E /* RCTVideoManager.m */, + 31CAFB1F1CADA8CD009BCF6F /* UIView+FindUIViewController.h */, + 31CAFB201CADA8CD009BCF6F /* UIView+FindUIViewController.m */, 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; @@ -117,6 +129,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 31CAFB211CADA8CD009BCF6F /* UIView+FindUIViewController.m in Sources */, + 31CAFB2F1CADC77F009BCF6F /* RCTVideoPlayerViewController.m in Sources */, BBD49E3F1AC8DEF000610F8E /* RCTVideo.m in Sources */, BBD49E401AC8DEF000610F8E /* RCTVideoManager.m in Sources */, ); diff --git a/RCTVideoManager.m b/RCTVideoManager.m index 7c0dfbc2..b75e379c 100644 --- a/RCTVideoManager.m +++ b/RCTVideoManager.m @@ -24,7 +24,11 @@ RCT_EXPORT_MODULE(); @"onVideoError", @"onVideoProgress", @"onVideoSeek", - @"onVideoEnd" + @"onVideoEnd", + @"onVideoFullscreenPlayerWillPresent", + @"onVideoFullscreenPlayerDidPresent", + @"onVideoFullscreenPlayerWillDismiss", + @"onVideoFullscreenPlayerDidDismiss" ]; } @@ -43,6 +47,7 @@ RCT_EXPORT_VIEW_PROPERTY(volume, float); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, float); RCT_EXPORT_VIEW_PROPERTY(currentTime, float); +RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); - (NSDictionary *)constantsToExport { diff --git a/RCTVideoPlayerViewController.h b/RCTVideoPlayerViewController.h new file mode 100644 index 00000000..d427b63d --- /dev/null +++ b/RCTVideoPlayerViewController.h @@ -0,0 +1,15 @@ +// +// RCTVideoPlayerViewController.h +// RCTVideo +// +// Created by Stanisław Chmiela on 31.03.2016. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import "RCTVideo.h" +#import "RCTVideoPlayerViewControllerDelegate.h" + +@interface RCTVideoPlayerViewController : AVPlayerViewController +@property (nonatomic, weak) id rctDelegate; +@end diff --git a/RCTVideoPlayerViewController.m b/RCTVideoPlayerViewController.m new file mode 100644 index 00000000..b1b768d7 --- /dev/null +++ b/RCTVideoPlayerViewController.m @@ -0,0 +1,28 @@ +// +// RCTVideoPlayerViewController.m +// RCTVideo +// +// Created by Stanisław Chmiela on 31.03.2016. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "RCTVideoPlayerViewController.h" + +@interface RCTVideoPlayerViewController () + +@end + +@implementation RCTVideoPlayerViewController + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [_rctDelegate videoPlayerViewControllerDidDismiss:self]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [_rctDelegate videoPlayerViewControllerWillDismiss:self]; + [super viewWillDisappear:animated]; +} + +@end diff --git a/RCTVideoPlayerViewControllerDelegate.h b/RCTVideoPlayerViewControllerDelegate.h new file mode 100644 index 00000000..ec6500af --- /dev/null +++ b/RCTVideoPlayerViewControllerDelegate.h @@ -0,0 +1,15 @@ +// +// RCTVideoPlayerViewControllerDelegate.h +// RCTVideo +// +// Created by Stanisław Chmiela on 01.04.2016. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import +#import "AVKit/AVKit.h" + +@protocol RCTVideoPlayerViewControllerDelegate +- (void)videoPlayerViewControllerWillDismiss:(AVPlayerViewController *)playerViewController; +- (void)videoPlayerViewControllerDidDismiss:(AVPlayerViewController *)playerViewController; +@end diff --git a/UIView+FindUIViewController.h b/UIView+FindUIViewController.h new file mode 100644 index 00000000..09214261 --- /dev/null +++ b/UIView+FindUIViewController.h @@ -0,0 +1,15 @@ +// +// UIView+FindUIViewController.h +// RCTVideo +// +// Created by Stanisław Chmiela on 31.03.2016. +// Copyright © 2016 Facebook. All rights reserved. +// +// Source: http://stackoverflow.com/a/3732812/1123156 + +#import + +@interface UIView (FindUIViewController) +- (UIViewController *) firstAvailableUIViewController; +- (id) traverseResponderChainForUIViewController; +@end diff --git a/UIView+FindUIViewController.m b/UIView+FindUIViewController.m new file mode 100644 index 00000000..bc721b96 --- /dev/null +++ b/UIView+FindUIViewController.m @@ -0,0 +1,28 @@ +// +// UIView+FindUIViewController.m +// RCTVideo +// +// Created by Stanisław Chmiela on 31.03.2016. +// Copyright © 2016 Facebook. All rights reserved. +// +// Source: http://stackoverflow.com/a/3732812/1123156 + +#import "UIView+FindUIViewController.h" + +@implementation UIView (FindUIViewController) +- (UIViewController *) firstAvailableUIViewController { + // convenience function for casting and to "mask" the recursive function + return (UIViewController *)[self traverseResponderChainForUIViewController]; +} + +- (id) traverseResponderChainForUIViewController { + id nextResponder = [self nextResponder]; + if ([nextResponder isKindOfClass:[UIViewController class]]) { + return nextResponder; + } else if ([nextResponder isKindOfClass:[UIView class]]) { + return [nextResponder traverseResponderChainForUIViewController]; + } else { + return nil; + } +} +@end diff --git a/Video.js b/Video.js index cc763c37..60859b17 100644 --- a/Video.js +++ b/Video.js @@ -21,6 +21,8 @@ export default class Video extends Component { constructor(props, context) { super(props, context); this.seek = this.seek.bind(this); + this.presentFullscreenPlayer = this.presentFullscreenPlayer.bind(this); + this.dismissFullscreenPlayer = this.dismissFullscreenPlayer.bind(this); this._assignRoot = this._assignRoot.bind(this); this._onLoadStart = this._onLoadStart.bind(this); this._onLoad = this._onLoad.bind(this); @@ -28,6 +30,10 @@ export default class Video extends Component { this._onProgress = this._onProgress.bind(this); this._onSeek = this._onSeek.bind(this); this._onEnd = this._onEnd.bind(this); + this._onFullscreenPlayerWillPresent = this._onFullscreenPlayerWillPresent.bind(this); + this._onFullscreenPlayerDidPresent = this._onFullscreenPlayerDidPresent.bind(this); + this._onFullscreenPlayerWillDismiss = this._onFullscreenPlayerWillDismiss.bind(this); + this._onFullscreenPlayerDidDismiss = this._onFullscreenPlayerDidDismiss.bind(this); } setNativeProps(nativeProps) { @@ -38,6 +44,14 @@ export default class Video extends Component { this.setNativeProps({ seek: time }); } + presentFullscreenPlayer() { + this.setNativeProps({ fullscreen: true }); + } + + dismissFullscreenPlayer() { + this.setNativeProps({ fullscreen: false }); + } + _assignRoot(component) { this._root = component; } @@ -78,6 +92,30 @@ export default class Video extends Component { } } + _onFullscreenPlayerWillPresent(event) { + if (this.props.onFullscreenPlayerWillPresent) { + this.props.onFullscreenPlayerWillPresent(event.nativeEvent); + } + } + + _onFullscreenPlayerDidPresent(event) { + if (this.props.onFullscreenPlayerDidPresent) { + this.props.onFullscreenPlayerDidPresent(event.nativeEvent); + } + } + + _onFullscreenPlayerWillDismiss(event) { + if (this.props.onFullscreenPlayerWillDismiss) { + this.props.onFullscreenPlayerWillDismiss(event.nativeEvent); + } + } + + _onFullscreenPlayerDidDismiss(event) { + if (this.props.onFullscreenPlayerDidDismiss) { + this.props.onFullscreenPlayerDidDismiss(event.nativeEvent); + } + } + render() { const { source, @@ -119,6 +157,10 @@ export default class Video extends Component { onVideoProgress: this._onProgress, onVideoSeek: this._onSeek, onVideoEnd: this._onEnd, + onVideoFullscreenPlayerWillPresent: this._onFullscreenPlayerWillPresent, + onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent, + onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, + onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, }); return ( @@ -134,6 +176,7 @@ Video.propTypes = { /* Native only */ src: PropTypes.object, seek: PropTypes.number, + fullscreen: PropTypes.bool, /* Wrapper component */ source: PropTypes.object, @@ -151,6 +194,10 @@ Video.propTypes = { onProgress: PropTypes.func, onSeek: PropTypes.func, onEnd: PropTypes.func, + onFullscreenPlayerWillPresent: PropTypes.func, + onFullscreenPlayerDidPresent: PropTypes.func, + onFullscreenPlayerWillDismiss: PropTypes.func, + onFullscreenPlayerDidDismiss: PropTypes.func, /* Required by react-native */ scaleX: React.PropTypes.number, @@ -165,5 +212,6 @@ const RCTVideo = requireNativeComponent('RCTVideo', Video, { nativeOnly: { src: true, seek: true, + fullscreen: true, }, }); diff --git a/package.json b/package.json index 3e15b8b1..c37bbeae 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "RCTVideoManager.h", "RCTVideoManager.m", "README.md", + "UIView+FindUIViewController.h", + "UIView+FindUIViewController.m", + "RCTVideoPlayerViewController.h", + "RCTVideoPlayerViewController.m", + "RCTVideoPlayerViewControllerDelegate.h", "Video.js", "VideoResizeMode.js", "react-native-video.podspec"