Created
October 14, 2017 15:55
-
-
Save raresloth/806bc45b6f4f4bb6668648a6f740bb56 to your computer and use it in GitHub Desktop.
ReplayKit recorder and broadcaster classes used in King Rabbit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FURBroadcaster.h | |
// furdemption | |
// | |
// Created by Austin Borden on 10/8/16. | |
// Copyright © 2016 RareSloth LLC. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
#import <ReplayKit/ReplayKit.h> | |
extern NSString *const kFURBroadcasterDidStartNotification; | |
extern NSString *const kFURBroadcasterDidEndNotification; | |
@interface FURBroadcaster : NSObject | |
+ (instancetype)sharedInstance; | |
+ (BOOL)isAvailable; | |
- (void)toggleBroadcast:(BOOL)shouldBroadcast; | |
- (BOOL)isBroadcasting; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FURBroadcaster.m | |
// furdemption | |
// | |
// Created by Austin Borden on 10/8/16. | |
// Copyright © 2016 RareSloth LLC. All rights reserved. | |
// | |
#import "FURBroadcaster.h" | |
#import "UIDevice+FURExtension.h" | |
NSString *const kFURBroadcasterDidStartNotification = @"kFURBroadcastDidStartNotification"; | |
NSString *const kFURBroadcasterDidEndNotification = @"kFURBroadcasterDidEndNotification"; | |
@interface FURBroadcaster()<RPBroadcastActivityViewControllerDelegate, RPBroadcastControllerDelegate> | |
@property (nonatomic, strong) RPBroadcastController *broadcastController; | |
@end | |
@implementation FURBroadcaster | |
static FURBroadcaster *_sharedInstance = nil; | |
+ (FURBroadcaster *)sharedInstance | |
{ | |
if (!_sharedInstance) | |
{ | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
_sharedInstance = [[FURBroadcaster alloc] init]; | |
}); | |
} | |
return _sharedInstance; | |
} | |
+ (BOOL)isAvailable | |
{ | |
// ReplayKit broadcasting is only available on iOS 10+ | |
return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10") && | |
[UIDevice isDeviceCapableOf:FURAppDeviceCapability60FPS]; | |
} | |
- (instancetype)init | |
{ | |
if (!(self = [super init])) | |
{ | |
return nil; | |
} | |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; | |
return self; | |
} | |
- (void)applicationDidBecomeActive | |
{ | |
if ([self isBroadcasting]) | |
{ | |
[_broadcastController resumeBroadcast]; | |
} | |
} | |
- (void)toggleBroadcast:(BOOL)shouldBroadcast | |
{ | |
if (shouldBroadcast) | |
{ | |
[self beginBroadcasting]; | |
} | |
else | |
{ | |
[self endBroadcasting]; | |
} | |
} | |
- (void)endBroadcasting | |
{ | |
if (!_broadcastController) | |
{ | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURBroadcasterDidEndNotification object:nil]; | |
return; | |
} | |
CLS_LOG(@"[BROADCASTER] Ending broadcast"); | |
DISPLAY_ACTIVITY; | |
__weak __typeof(self)weakSelf = self; | |
[_broadcastController finishBroadcastWithHandler:^(NSError * _Nullable error) { | |
CLS_LOG(@"[BROADCASTER] Ended broadcast"); | |
HIDE_ACTIVITY; | |
if (error) | |
{ | |
RECORD_ERROR(@"BROADCAST_STOP_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
else | |
{ | |
weakSelf.broadcastController = nil; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURBroadcasterDidEndNotification object:nil]; | |
} | |
}]; | |
} | |
- (void)beginBroadcasting | |
{ | |
CLS_LOG(@"[BROADCASTER] Loading broadcast"); | |
DISPLAY_ACTIVITY; | |
__weak __typeof(self)weakSelf = self; | |
[RPBroadcastActivityViewController loadBroadcastActivityViewControllerWithHandler:^(RPBroadcastActivityViewController * _Nullable broadcastActivityViewController, NSError * _Nullable error) { | |
CLS_LOG(@"[BROADCASTER] Presenting broadcast activity view"); | |
if (error) | |
{ | |
RECORD_ERROR(@"BROADCAST_START_ERROR", error); | |
HIDE_ACTIVITY; | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
else | |
{ | |
broadcastActivityViewController.modalPresentationStyle = UIModalPresentationPopover; | |
broadcastActivityViewController.popoverPresentationController.sourceView = mainView(); | |
broadcastActivityViewController.popoverPresentationController.sourceRect = CGRectMake(SCREEN_WIDTH - 26, 60, 2, 2); | |
broadcastActivityViewController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionUp; | |
broadcastActivityViewController.delegate = weakSelf; | |
[mainViewController() presentViewController:broadcastActivityViewController animated:YES completion:nil]; | |
} | |
}]; | |
} | |
- (BOOL)isBroadcasting | |
{ | |
return _broadcastController.isBroadcasting; | |
} | |
#pragma mark - <RPBroadcastActivityViewControllerDelegate> | |
- (void)broadcastActivityViewController:(RPBroadcastActivityViewController *)broadcastActivityViewController | |
didFinishWithBroadcastController:(RPBroadcastController *)broadcastController | |
error:(NSError *)error | |
{ | |
if (error) | |
{ | |
[broadcastActivityViewController dismissViewControllerAnimated:YES completion:^{ | |
HIDE_ACTIVITY; | |
if (error.code != RPRecordingErrorUserDeclined) | |
{ | |
RECORD_ERROR(@"BROADCAST_ACTIVITY_VIEW_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
}]; | |
return; | |
} | |
[broadcastActivityViewController dismissViewControllerAnimated:YES completion:nil]; | |
_broadcastController = broadcastController; | |
_broadcastController.delegate = self; | |
CLS_LOG(@"[BROADCASTER] Starting broadcast"); | |
__weak __typeof(self)weakSelf = self; | |
[_broadcastController startBroadcastWithHandler:^(NSError * _Nullable error) { | |
HIDE_ACTIVITY; | |
if (error) | |
{ | |
RECORD_ERROR(@"BROADCAST_START_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
else | |
{ | |
CLS_LOG(@"[BROADCASTER] Started broadcast"); | |
if (weakSelf.broadcastController.broadcastExtensionBundleID) | |
{ | |
RECORD_EVENT(@"BROADCAST_STARTED", (@{ @"broadcastExtension": weakSelf.broadcastController.broadcastExtensionBundleID})); | |
} | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURBroadcasterDidStartNotification object:nil]; | |
} | |
}]; | |
} | |
#pragma mark - <RPBroadcastControllerDelegate> | |
- (void)broadcastController:(RPBroadcastController *)broadcastController didFinishWithError:(NSError *)error | |
{ | |
if (error) | |
{ | |
RECORD_ERROR(@"BROADCAST_STOP_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
CLS_LOG(@"[BROADCASTER] Finished broadcast"); | |
self.broadcastController = nil; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURBroadcasterDidEndNotification object:nil]; | |
} | |
- (void)broadcastController:(RPBroadcastController *)broadcastController | |
didUpdateServiceInfo:(NSDictionary<NSString *,NSObject<NSCoding> *> *)serviceInfo | |
{ | |
} | |
- (void)dealloc | |
{ | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FURRecorder.h | |
// furdemption | |
// | |
// Created by Austin Borden on 10/11/16. | |
// Copyright © 2016 RareSloth LLC. All rights reserved. | |
// | |
#import <ReplayKit/ReplayKit.h> | |
extern NSString *const kFURRecorderDidStartNotification; | |
extern NSString *const kFURRecorderDidEndNotification; | |
@interface FURRecorder : NSObject | |
+ (RPScreenRecorder *)recorder; | |
+ (BOOL)isAvailable; | |
+ (BOOL)isRecording; // Need to keep track of this since broadcasting makes the recorder say it's recording | |
+ (BOOL)isCameraAvailable; | |
+ (BOOL)isMicrophoneAvailable; | |
+ (void)requestCameraPermissionsWithCompletion:(void(^)(BOOL granted))completionBlock; | |
+ (void)start; | |
+ (void)stop; | |
// Use when recording seems to be stuck | |
+ (void)forceStop; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FURRecorder.m | |
// furdemption | |
// | |
// Created by Austin Borden on 10/11/16. | |
// Copyright © 2016 RareSloth LLC. All rights reserved. | |
// | |
#import "FURRecorder.h" | |
#import "UIDevice+FURExtension.h" | |
#import "FURLocalGameSaveHandler.h" | |
NSString *const kFURRecorderDidStartNotification = @"kFURRecorderDidStartNotification"; | |
NSString *const kFURRecorderDidEndNotification = @"kFURRecorderDidEndNotification"; | |
@interface FURRecorder()<RPScreenRecorderDelegate, RPPreviewViewControllerDelegate> | |
@property (nonatomic, strong) RPScreenRecorder *recorder; | |
@property (nonatomic) BOOL hasMicrophoneBeenEnabled; | |
@property (nonatomic) BOOL isRecording; | |
@end | |
@implementation FURRecorder | |
static FURRecorder *_sharedInstance = nil; | |
+ (FURRecorder *)sharedInstance | |
{ | |
if (!_sharedInstance) | |
{ | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
_sharedInstance = [[FURRecorder alloc] init]; | |
}); | |
} | |
return _sharedInstance; | |
} | |
+ (RPScreenRecorder *)recorder | |
{ | |
[self sharedInstance].recorder = [RPScreenRecorder sharedRecorder]; | |
return [self sharedInstance].recorder; | |
} | |
+ (BOOL)isAvailable | |
{ | |
// ReplayKit recording is only available on iOS 9+ | |
return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9") && | |
[self recorder].isAvailable; | |
} | |
+ (BOOL)isRecording | |
{ | |
return [self sharedInstance].isRecording; | |
} | |
+ (BOOL)isCameraAvailable | |
{ | |
return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10") && | |
[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized; | |
} | |
+ (BOOL)isMicrophoneAvailable | |
{ | |
return [[self recorder] isMicrophoneEnabled] || | |
[self sharedInstance].hasMicrophoneBeenEnabled; // Querying AVCaptureDevice doens't work for mic permissions through ReplayKit | |
} | |
+ (void)requestCameraPermissionsWithCompletion:(void(^)(BOOL granted))completionBlock | |
{ | |
CLS_LOG(@"[RECORDER] Requesting camera permissions"); | |
DISPLAY_ACTIVITY; | |
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
CLS_LOG(@"[RECORDER] Camera permissions completed: %@", granted ? @"GRANTED" : @"DENIED"); | |
HIDE_ACTIVITY; | |
if (completionBlock) completionBlock(granted); | |
}); | |
}]; | |
} | |
+ (void)start | |
{ | |
DISPLAY_ACTIVITY; | |
if ([self isCameraAvailable]) | |
{ | |
CLS_LOG(@"[RECORDER] Beginning recording with camera"); | |
[[self recorder] setCameraEnabled:YES]; | |
[[self recorder] setMicrophoneEnabled:YES]; | |
[[self recorder] startRecordingWithHandler:[self startRecordingCompletionBlock]]; | |
} | |
else | |
{ | |
CLS_LOG(@"[RECORDER] Beginning recording without camera"); | |
[[self recorder] startRecordingWithMicrophoneEnabled:YES handler:[self startRecordingCompletionBlock]]; | |
} | |
} | |
+ (void(^)(NSError *error))startRecordingCompletionBlock | |
{ | |
__weak __typeof(self)weakSelf = self; | |
return ^(NSError * _Nullable error) { | |
HIDE_ACTIVITY; | |
if (error) | |
{ | |
if (error.code != RPRecordingErrorUserDeclined) | |
{ | |
RECORD_ERROR(@"RECORD_START_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
} | |
else | |
{ | |
CLS_LOG(@"[RECORDER] Recording started"); | |
if ([weakSelf.recorder isMicrophoneEnabled]) | |
{ | |
[weakSelf sharedInstance].hasMicrophoneBeenEnabled = YES; | |
} | |
[weakSelf sharedInstance].isRecording = YES; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURRecorderDidStartNotification object:nil]; | |
} | |
}; | |
} | |
+ (void)stop | |
{ | |
CLS_LOG(@"[RECORDER] Stopping recording"); | |
DISPLAY_ACTIVITY; | |
__weak __typeof(self)weakSelf = self; | |
[[self recorder] stopRecordingWithHandler:^(RPPreviewViewController * _Nullable previewViewController, NSError * _Nullable error) { | |
CLS_LOG(@"[RECORDER] Stopped recording"); | |
if (error || !previewViewController) | |
{ | |
HIDE_ACTIVITY; | |
if (error) | |
{ | |
RECORD_ERROR(@"RECORD_STOP_ERROR", error); | |
[[CCDirector sharedDirector].runningScene displayErrorAlert:error]; | |
} | |
} | |
else | |
{ | |
CLS_LOG(@"[RECORDER] Presenting preview controller"); | |
[weakSelf sharedInstance].isRecording = NO; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURRecorderDidEndNotification object:nil]; | |
[FURLoggingAudio setBackgroundMuted:YES]; | |
previewViewController.previewControllerDelegate = [weakSelf sharedInstance]; | |
previewViewController.modalPresentationStyle = UIModalPresentationFormSheet; | |
[mainViewController() presentViewController:previewViewController animated:YES completion:nil]; | |
} | |
}]; | |
} | |
+ (void)forceStop | |
{ | |
CLS_LOG(@"[RECORDER] Force stopping recording by discarding"); | |
DISPLAY_ACTIVITY; | |
__weak __typeof(self)weakSelf = self; | |
[[FURRecorder recorder] discardRecordingWithHandler:^{ | |
CLS_LOG(@"[RECORDER] Discarded recording"); | |
[FURLoggingAudio setBackgroundMuted:NO]; | |
[weakSelf sharedInstance].isRecording = NO; | |
[[NSNotificationCenter defaultCenter] postNotificationName:kFURRecorderDidEndNotification object:nil]; | |
HIDE_ACTIVITY; | |
}]; | |
} | |
- (instancetype)init | |
{ | |
if (!(self = [super init])) | |
{ | |
return nil; | |
} | |
_recorder = [RPScreenRecorder sharedRecorder]; | |
_recorder.delegate = self; | |
return self; | |
} | |
#pragma mark - <RPScreenRecorderDelegate> | |
- (void)screenRecorder:(RPScreenRecorder *)screenRecorder didStopRecordingWithError:(NSError *)error previewViewController:(nullable RPPreviewViewController *)previewViewController; | |
{ | |
} | |
- (void)screenRecorderDidChangeAvailability:(RPScreenRecorder *)screenRecorder | |
{ | |
} | |
#pragma mark - <RPPreviewViewControllerDelegate> | |
- (void)previewControllerDidFinish:(RPPreviewViewController *)previewController | |
{ | |
} | |
- (void)previewController:(RPPreviewViewController *)previewController didFinishWithActivityTypes:(NSSet<NSString *> *)activityTypes | |
{ | |
NSString *activityTypesString = [[[activityTypes allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@", "]; | |
if (activityTypesString.length) | |
{ | |
RECORD_EVENT(@"RECORDING_COMPLETE", (@{ @"activityTypes": activityTypesString })); | |
} | |
CLS_LOG(@"[RECORDER] Dismissing preview controller"); | |
[previewController dismissViewControllerAnimated:YES completion:^{ | |
CLS_LOG(@"[RECORDER] Discarding recording"); | |
[[FURRecorder recorder] discardRecordingWithHandler:^{ | |
CLS_LOG(@"[RECORDER] Discarded recording"); | |
[FURLoggingAudio setBackgroundMuted:NO]; | |
HIDE_ACTIVITY; | |
}]; | |
}]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FURReplayKitNavBar is the UI element that exposes access to starting/stopping recording/broadcasting