#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 - (void)setMicrophoneEnabled:(BOOL)value; - (BOOL)isMicrophoneEnabled; - (void)setCameraEnabled:(BOOL)value; - (BOOL)isCameraEnabled; - (BOOL)isPreviewControllerActive; @property (nonatomic, setter = setMicrophoneEnabled:, getter = isMicrophoneEnabled) BOOL microphoneEnabled; @property (nonatomic, setter = setCameraEnabled:, getter = isCameraEnabled) BOOL cameraEnabled; @property (nonatomic, readonly) UIView* cameraPreviewView; @property (nonatomic, getter = isPreviewControllerActive) BOOL previewControllerActive; @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 = isBroadcastingPaused) BOOL paused; @property(nonatomic, readonly) NSDictionary *> *serviceInfo; - (BOOL)isBroadcasting; - (BOOL)isBroadcastingPaused; - (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; bool currentPreviewControllerActive; 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 { NSAssert(_replayKit != nil, @"InitUnityReplayKit should be called before using ReplayKit api."); return _replayKit; } - (BOOL)apiAvailable { return ([RPScreenRecorder class] != nil) && [RPScreenRecorder sharedRecorder].isAvailable; } - (BOOL)recordingPreviewAvailable { return _previewController != nil; } - (BOOL)startRecording { 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 startRecordingWithHandler:^(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; }]; currentPreviewControllerActive = YES; 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; currentPreviewControllerActive = NO; return YES; } - (void)previewControllerDidFinish:(RPPreviewViewController*)previewController { if (previewController != nil) { [previewController dismissViewControllerAnimated: YES completion: nil]; } currentPreviewControllerActive = NO; } - (BOOL)isPreviewControllerActive { return currentPreviewControllerActive; } /**************************************** * 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]; } - (BOOL)isBroadcastingPaused { if (broadcastController == nil) { return NO; } return [broadcastController isBroadcastingPaused]; } - (void)broadcastActivityViewController:(UnityReplayKit_RPBroadcastActivityViewController *)sBroadcastActivityViewController didFinishWithBroadcastController:(id)inRPBroadcastController error:(NSError *)error { dispatch_sync(dispatch_get_main_queue(), ^{ UnityPause(0); broadcastController = inRPBroadcastController; if (broadcastController == nil) // broadcast was canceled { _lastError = [error description]; UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, false, [_lastError UTF8String]); broadcastStartStatusCallback = nil; [UnityGetGLViewController() dismissViewControllerAnimated: YES completion: nil]; } else // start broadcast { [UnityGetGLViewController() dismissViewControllerAnimated: YES completion:^ { [broadcastController startBroadcastWithHandler:^(NSError* error) { if (error != nil) { _lastError = [error description]; UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, false, [_lastError UTF8String]); broadcastStartStatusCallback = nil; broadcastController = nil; } else { UnityReplayKitTriggerBroadcastStatusCallback(broadcastStartStatusCallback, true, ""); broadcastStartStatusCallback = nil; _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; // oh apple, how much do you like confusing docs and contradicting behaviours // on ios13 UIModalPresentationPopover meaning was changed; what's more: it is now broken if portrait is disabled // pre ios13 it was intended to be UIModalPresentationPopover for ipads, UIModalPresentationFullScreen for iphones // yet having fully blackscreen (UIModalPresentationFullScreen) was not looking good for lots of people // so we use popover for both ipad/iphone but ONLY before ios13 // note that tvos is fullscreen always (this is the only option here) #if PLATFORM_TVOS vc.modalPresentationStyle = UIModalPresentationFullScreen; #else if (UnityiOS130orNewer()) { vc.modalPresentationStyle = UIModalPresentationFormSheet; } else { vc.modalPresentationStyle = UIModalPresentationPopover; 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]; }]; } - (void)pauseBroadcasting { if (broadcastController == nil || !broadcastController.broadcasting) { return; } [broadcastController pauseBroadcast]; } - (void)resumeBroadcasting { if (broadcastController == nil || !broadcastController.broadcasting) { return; } [broadcastController resumeBroadcast]; } - (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; } - (void)setMicrophoneEnabled:(BOOL)microphoneEnabled { if (![self apiAvailable]) { return; } id screenRecorder = (id)[RPScreenRecorder sharedRecorder]; if (![screenRecorder respondsToSelector: @selector(setMicrophoneEnabled:)]) { return; } screenRecorder.microphoneEnabled = microphoneEnabled; } - (BOOL)showCameraPreviewAt:(CGPoint)position width:(float)width height:(float)height { 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]; } if (width < 0.0f) width = currentCameraPreviewView.frame.size.width; if (height < 0.0f) 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 static dispatch_once_t onceToken_InitUnityReplayKit; void InitUnityReplayKit() { dispatch_once(&onceToken_InitUnityReplayKit, ^{ _replayKit = [[UnityReplayKit alloc] init]; // that seems to be enough to trigger RPScreenRecorder to become "ready" [RPScreenRecorder sharedRecorder].delegate = _replayKit; }); } #endif // UNITY_REPLAY_KIT_AVAILABLE