#if UNITY_REPLAY_KIT_AVAILABLE #import "UnityReplayKit.h" #import "UnityAppController.h" #import "UI/UnityViewControllerBase.h" #import "UnityInterface.h" #import extern "C" void UnityReplayKitTriggerBroadcastStatusCallback(void* callback, bool hasSucceeded, const char* errorMessage); static UnityReplayKit* _replayKit = nil; @protocol UnityReplayKit_RPScreenRecorder - (BOOL)isMicrophoneEnabled; - (void)setCameraEnabled:(BOOL)value; - (BOOL)isCameraEnabled; @property (nonatomic, getter = isMicrophoneEnabled) BOOL microphoneEnabled; @property (nonatomic, setter = setCameraEnabled:, getter = isCameraEnabled) BOOL cameraEnabled; @property (nonatomic, readonly) UIView* cameraPreviewView; @end @protocol UnityReplayKit_RPBroadcastController @property(nonatomic, readonly) NSURL *broadcastURL; @property(nonatomic, readonly, getter = isBroadcasting) BOOL broadcasting; @property(nonatomic, readonly) NSString *broadcastExtensionBundleID; //@property(nonatomic, weak) id delegate; @property(nonatomic, readonly, getter = isPaused) BOOL paused; @property(nonatomic, readonly) NSDictionary *> *serviceInfo; - (BOOL)isBroadcasting; - (void)finishBroadcastWithHandler:(void (^)(NSError *error))handler; - (void)startBroadcastWithHandler:(void (^)(NSError *error))handler; - (void)pauseBroadcast; - (void)resumeBroadcast; @end @interface UnityReplayKit_RPBroadcastActivityViewController : UIViewController @property (nonatomic, weak) id delegate; @end // why do we care about orientation handling: // ReplayKit will disable top-window autorotation // as users keep asking to do autorotation during broadcast/record we create fake empty window with fake view controller // this window will have autorotation disabled instead of unity one // but this is not the end of the story: what fake view controller does is also important // now it is hard to speculate what *actually* happens but with setup like fake view controller takes over control over "supported orientations" // meaning that if we dont do anything suddenly all orientations become enabled. // to avoid that we create this monstrosity that pokes unity for orientation. #if PLATFORM_IOS @interface UnityReplayKitViewController : UnityViewControllerBase { } - (NSUInteger)supportedInterfaceOrientations; @end @implementation UnityReplayKitViewController - (NSUInteger)supportedInterfaceOrientations { NSUInteger ret = 0; if (UnityShouldAutorotate()) { if (UnityIsOrientationEnabled(portrait)) ret |= (1 << UIInterfaceOrientationPortrait); if (UnityIsOrientationEnabled(portraitUpsideDown)) ret |= (1 << UIInterfaceOrientationPortraitUpsideDown); if (UnityIsOrientationEnabled(landscapeLeft)) ret |= (1 << UIInterfaceOrientationLandscapeRight); if (UnityIsOrientationEnabled(landscapeRight)) ret |= (1 << UIInterfaceOrientationLandscapeLeft); } else { switch (UnityRequestedScreenOrientation()) { case portrait: ret = (1 << UIInterfaceOrientationPortrait); break; case portraitUpsideDown: ret = (1 << UIInterfaceOrientationPortraitUpsideDown); break; case landscapeLeft: ret = (1 << UIInterfaceOrientationLandscapeRight); break; case landscapeRight: ret = (1 << UIInterfaceOrientationLandscapeLeft); break; } } return ret; } @end #else #define UnityReplayKitViewController UnityViewControllerBase #endif @implementation UnityReplayKit { id broadcastController; void* broadcastStartStatusCallback; UIView* currentCameraPreviewView; UIWindow* overlayWindow; } - (void)shouldCreateOverlayWindow { UnityShouldCreateReplayKitOverlay(); } - (void)createOverlayWindow { if (self->overlayWindow == nil) { UIWindow* wnd = self->overlayWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds]; wnd.hidden = wnd.userInteractionEnabled = NO; wnd.backgroundColor = nil; wnd.rootViewController = [[UnityReplayKitViewController alloc] init]; } } + (UnityReplayKit*)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _replayKit = [[UnityReplayKit alloc] init]; }); return _replayKit; } - (BOOL)apiAvailable { return ([RPScreenRecorder class] != nil) && [RPScreenRecorder sharedRecorder].isAvailable; } - (BOOL)recordingPreviewAvailable { return _previewController != nil; } - (BOOL)startRecording:(BOOL)enableMicrophone { RPScreenRecorder* recorder = [RPScreenRecorder sharedRecorder]; if (recorder == nil) { _lastError = [NSString stringWithUTF8String: "Failed to get Screen Recorder"]; return NO; } recorder.delegate = self; __block BOOL success = YES; [recorder startRecordingWithMicrophoneEnabled: enableMicrophone handler:^(NSError* error) { if (error != nil) { _lastError = [error description]; success = NO; } else { [self shouldCreateOverlayWindow]; } }]; return success; } - (BOOL)isRecording { RPScreenRecorder* recorder = [RPScreenRecorder sharedRecorder]; if (recorder == nil) { _lastError = [NSString stringWithUTF8String: "Failed to get Screen Recorder"]; return NO; } return recorder.isRecording; } - (BOOL)stopRecording { RPScreenRecorder* recorder = [RPScreenRecorder sharedRecorder]; if (recorder == nil) { _lastError = [NSString stringWithUTF8String: "Failed to get Screen Recorder"]; return NO; } __block BOOL success = YES; [recorder stopRecordingWithHandler:^(RPPreviewViewController* previewViewController, NSError* error) { self->overlayWindow = nil; if (error != nil) { _lastError = [error description]; success = NO; return; } if (previewViewController != nil) { [previewViewController setPreviewControllerDelegate: self]; _previewController = previewViewController; } }]; return success; } - (void)screenRecorder:(RPScreenRecorder*)screenRecorder didStopRecordingWithError:(NSError*)error previewViewController:(RPPreviewViewController*)previewViewController { if (error != nil) { _lastError = [error description]; } self->overlayWindow = nil; _previewController = previewViewController; } - (BOOL)showPreview { if (_previewController == nil) { _lastError = [NSString stringWithUTF8String: "No recording available"]; return NO; } [_previewController setModalPresentationStyle: UIModalPresentationFullScreen]; [GetAppController().rootViewController presentViewController: _previewController animated: YES completion:^() { _previewController = nil; }]; return YES; } - (BOOL)discardPreview { if (_previewController == nil) { return YES; } RPScreenRecorder* recorder = [RPScreenRecorder sharedRecorder]; if (recorder == nil) { _lastError = [NSString stringWithUTF8String: "Failed to get Screen Recorder"]; return NO; } [recorder discardRecordingWithHandler:^() { _previewController = nil; }]; // TODO - the above callback doesn't seem to be working at the moment. _previewController = nil; return YES; } - (void)previewControllerDidFinish:(RPPreviewViewController*)previewController { if (previewController != nil) { [previewController dismissViewControllerAnimated: YES completion: nil]; } } /**************************************** * ReplayKit Broadcasting API * ****************************************/ - (BOOL)broadcastingApiAvailable { return nil != NSClassFromString(@"RPBroadcastController") && nil != NSClassFromString(@"RPBroadcastActivityViewController"); } - (NSURL*)broadcastURL { if (broadcastController == nil) { return nil; } return [broadcastController broadcastURL]; } - (BOOL)isBroadcasting { if (broadcastController == nil) { return NO; } return [broadcastController isBroadcasting]; } - (void)broadcastActivityViewController:(UnityReplayKit_RPBroadcastActivityViewController *)sBroadcastActivityViewController didFinishWithBroadcastController:(id)sBroadcastController error:(NSError *)error { dispatch_sync(dispatch_get_main_queue(), ^{ UnityPause(0); }); if (sBroadcastController == nil) { _lastError = [error description]; UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, false, [_lastError UTF8String]); broadcastStartStatusCallback = nullptr; [UnityGetGLViewController() dismissViewControllerAnimated: YES completion: nil]; return; } broadcastController = sBroadcastController; [UnityGetGLViewController() dismissViewControllerAnimated: YES completion:^ { [broadcastController startBroadcastWithHandler:^(NSError* error) { if (error != nil) { _lastError = [error description]; UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, false, [_lastError UTF8String]); broadcastStartStatusCallback = nullptr; broadcastController = nil; return; } UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, true, ""); broadcastStartStatusCallback = nullptr; _lastError = nil; }]; }]; } - (void)startBroadcastingWithCallback:(void *)callback { Class class_BroadcastActivityViewController = NSClassFromString(@"RPBroadcastActivityViewController"); if (class_BroadcastActivityViewController == nil) { return; } if (broadcastController != nil && broadcastController.broadcasting) { _lastError = @"Broadcast already in progress"; UnityReplayKitTriggerBroadcastStatusCallback(callback, false, [_lastError UTF8String]); return; } if (broadcastStartStatusCallback != nullptr) { _lastError = @"The last attempt to start a broadcast didn\'t finish yet."; UnityReplayKitTriggerBroadcastStatusCallback(callback, false, [_lastError UTF8String]); return; } [class_BroadcastActivityViewController performSelector: @selector(loadBroadcastActivityViewControllerWithHandler:) withObject:^(UnityReplayKit_RPBroadcastActivityViewController* vc, NSError* error) { if (vc == nil || error != nil) { _lastError = [error description]; UnityReplayKitTriggerBroadcastStatusCallback(callback, false, [_lastError UTF8String]); return; } [self shouldCreateOverlayWindow]; UnityPause(1); vc.delegate = self; broadcastStartStatusCallback = callback; #if PLATFORM_TVOS vc.modalPresentationStyle = UIModalPresentationFullScreen; #else vc.modalPresentationStyle = UIModalPresentationPopover; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { vc.popoverPresentationController.sourceRect = CGRectMake(GetAppController().rootView.bounds.size.width / 2, 0, 0, 0); vc.popoverPresentationController.sourceView = GetAppController().rootView; } #endif [UnityGetGLViewController() presentViewController: vc animated: YES completion: nil]; }]; return; } - (void)stopBroadcasting { self->overlayWindow = nil; if (broadcastController == nil || !broadcastController.broadcasting) { broadcastController = nil; return; } [broadcastController finishBroadcastWithHandler:^(NSError* error) { broadcastController = nil; if (error == nil) return; _lastError = [error description]; }]; } - (BOOL)isCameraEnabled { if (![self apiAvailable]) { return NO; } id screenRecorder = (id)[RPScreenRecorder sharedRecorder]; if (![screenRecorder respondsToSelector: @selector(isCameraEnabled)]) { return NO; } return screenRecorder.cameraEnabled; } - (void)setCameraEnabled:(BOOL)cameraEnabled { if (![self apiAvailable]) { return; } id screenRecorder = (id)[RPScreenRecorder sharedRecorder]; if (![screenRecorder respondsToSelector: @selector(setCameraEnabled:)]) { return; } screenRecorder.cameraEnabled = cameraEnabled; } - (BOOL)isMicrophoneEnabled { if (![self apiAvailable]) { return NO; } id screenRecorder = (id)[RPScreenRecorder sharedRecorder]; if (![screenRecorder respondsToSelector: @selector(isMicrophoneEnabled)]) { return NO; } return screenRecorder.microphoneEnabled; } - (BOOL)showCameraPreviewAt:(CGPoint)position { if (currentCameraPreviewView == nil) { if (![self apiAvailable]) { return NO; } id screenRecorder = (id)[RPScreenRecorder sharedRecorder]; UIView* cameraPreviewView = screenRecorder.cameraPreviewView; if (cameraPreviewView == nil) { return NO; } [[UnityGetGLViewController() view] addSubview: cameraPreviewView]; currentCameraPreviewView = cameraPreviewView; [cameraPreviewView setUserInteractionEnabled: NO]; } int width = currentCameraPreviewView.frame.size.width; int height = currentCameraPreviewView.frame.size.height; [currentCameraPreviewView setFrame: CGRectMake(position.x, position.y, width, height)]; return YES; } - (void)hideCameraPreview { if (currentCameraPreviewView != nil) { [currentCameraPreviewView removeFromSuperview]; currentCameraPreviewView = nil; } } @end #endif // UNITY_REPLAY_KIT_AVAILABLE