Skip to content

Instantly share code, notes, and snippets.

@raresloth
Created October 14, 2017 15:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save raresloth/806bc45b6f4f4bb6668648a6f740bb56 to your computer and use it in GitHub Desktop.
Save raresloth/806bc45b6f4f4bb6668648a6f740bb56 to your computer and use it in GitHub Desktop.
ReplayKit recorder and broadcaster classes used in King Rabbit
//
// 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
//
// 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
//
// 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
//
// 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
//
// FURReplayKitNavBar.m
// furdemption
//
// Created by Austin Borden on 10/7/16.
// Copyright © 2016 RareSloth LLC. All rights reserved.
//
#import "FURReplayKitNavBar.h"
#import "FURBroadcaster.h"
#import "UIDevice+FURExtension.h"
#import "FURRecorder.h"
static BOOL _hasUsedBroadcast = NO;
static BOOL _hasUsedRecord = NO;
@interface FURReplayKitNavBar()
@property (nonatomic, strong) NSDate *recordRequestFailureThresholdDate;
@end
@implementation FURReplayKitNavBar
- (instancetype)init
{
if (!(self = [super init]))
{
return nil;
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(broadcastDidStart) name:kFURBroadcasterDidStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(broadcastDidEnd) name:kFURBroadcasterDidEndNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recordingDidStart) name:kFURRecorderDidStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(recordingDidEnd) name:kFURRecorderDidEndNotification object:nil];
return self;
}
- (void)applicationDidBecomeActive
{
[self removeAllReplayKitButtons];
[self dismissCameraPreview];
if ([FURRecorder isRecording])
{
[self displayCameraPreview];
[self displayRecordButtons];
}
else if ([FURBroadcaster sharedInstance].isBroadcasting)
{
[self displayBroadcastButton];
}
else
{
[self redisplayRecordAndBroadcastButtons];
}
}
- (void)broadcastDidStart
{
_hasUsedBroadcast = YES;
[self removeAllReplayKitButtons];
[self displayBroadcastButton];
}
- (void)broadcastDidEnd
{
[self redisplayRecordAndBroadcastButtons];
}
- (void)recordingDidStart
{
_hasUsedRecord = YES;
_recordRequestFailureThresholdDate = nil;
[self displayCameraPreview];
[self removeAllReplayKitButtons];
[self displayRecordButtons];
}
- (void)recordingDidEnd
{
_recordRequestFailureThresholdDate = nil;
[self dismissCameraPreview];
[self redisplayRecordAndBroadcastButtons];
}
- (void)onEnter
{
[super onEnter];
[self redisplayRecordAndBroadcastButtons];
}
- (void)redisplayRecordAndBroadcastButtons
{
[self removeAllReplayKitButtons];
[self displayBroadcastButton];
[self displayRecordButtons];
}
- (void)displayBroadcastButton
{
if (_hasUsedRecord ||
![FURBroadcaster isAvailable] ||
[FURRecorder isRecording])
{
return;
}
BOOL isBroadcasting = [FURBroadcaster sharedInstance].isBroadcasting;
NSString *assetName = isBroadcasting ? @"ui_stop_recording" : @"ui_broadcast_off";
FURMenuButton *button = [FURMenuButton pressableButtonWithAssetName:assetName block:^(FURMenuButton *sender) {
[[FURBroadcaster sharedInstance] toggleBroadcast:![FURBroadcaster sharedInstance].isBroadcasting];
}];
[self addRightButtonItem:button];
}
- (void)displayRecordButtons
{
if (_hasUsedBroadcast ||
![FURRecorder isAvailable] ||
[FURBroadcaster sharedInstance].isBroadcasting)
{
return;
}
BOOL isRecording = [FURRecorder isRecording];
NSString *assetName = isRecording ? @"ui_stop_recording" : @"ui_record_off";
__weak __typeof(self)weakSelf = self;
FURMenuButton *button = [FURMenuButton pressableButtonWithAssetName:assetName block:^(FURMenuButton *sender) {
if (![FURRecorder isRecording])
{
[weakSelf requestCameraPermissionsAndStartRecording];
}
else
{
[FURRecorder stop];
}
}];
[self addRightButtonItem:button];
if (isRecording)
{
if ([FURRecorder isMicrophoneAvailable])
{
assetName = [FURRecorder recorder].isMicrophoneEnabled ? @"ui_mic_on" : @"ui_mic_off";
button = [FURMenuButton pressableButtonWithAssetName:assetName block:^(FURMenuButton *sender) {
[[FURRecorder recorder] setMicrophoneEnabled:![FURRecorder recorder].isMicrophoneEnabled];
[weakSelf recordingFeaturesDidChange];
}];
[self addRightButtonItem:button];
}
if ([FURRecorder isCameraAvailable])
{
assetName = [FURRecorder recorder].isCameraEnabled ? @"ui_camera_on" : @"ui_camera_off";
button = [FURMenuButton pressableButtonWithAssetName:assetName block:^(FURMenuButton *sender) {
[[FURRecorder recorder] setCameraEnabled:![FURRecorder recorder].isCameraEnabled];
if ([FURRecorder recorder].isCameraEnabled)
{
[weakSelf displayCameraPreview];
}
else
{
[weakSelf dismissCameraPreview];
}
[weakSelf recordingFeaturesDidChange];
}];
[self addRightButtonItem:button];
}
}
}
- (void)requestCameraPermissionsAndStartRecording
{
__weak __typeof(self)weakSelf = self;
[FURRecorder requestCameraPermissionsWithCompletion:^(BOOL granted) {
[weakSelf rescheduleCheckForRecordFailure];
[FURRecorder start];
}];
}
- (void)rescheduleCheckForRecordFailure
{
[self unschedule:@selector(checkForRecordFailure)];
_recordRequestFailureThresholdDate = [NSDate dateWithTimeIntervalSinceNow:10];
[self schedule:@selector(checkForRecordFailure) interval:1];
}
- (void)checkForRecordFailure
{
if (!_recordRequestFailureThresholdDate)
{
return;
}
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive)
{
[self rescheduleCheckForRecordFailure];
}
else if ([[NSDate date] timeIntervalSinceDate:_recordRequestFailureThresholdDate] > 0)
{
_recordRequestFailureThresholdDate = nil;
[FURRecorder forceStop];
}
}
- (void)recordingFeaturesDidChange
{
[self removeAllReplayKitButtons];
[self displayRecordButtons];
}
- (void)displayCameraPreview
{
if (![FURRecorder isCameraAvailable] ||
![FURRecorder recorder].isCameraEnabled)
{
return;
}
UIView *previewView = [FURRecorder recorder].cameraPreviewView;
if (previewView)
{
CGRect bottomLeftFrame = previewView.frame;
bottomLeftFrame.origin = ccp(5.f, SCREEN_HEIGHT - bottomLeftFrame.size.height - 5.f);
previewView.frame = bottomLeftFrame;
[mainView() addSubview:previewView];
// Some tricky stuff to get the video to re-orient
@try
{
NSArray *layers = [[previewView layer] sublayers];
AVCaptureVideoPreviewLayer *previewLayer = [layers firstObject];
if ([previewLayer.connection isVideoOrientationSupported])
{
[previewLayer.connection setVideoOrientation:(AVCaptureVideoOrientation)[[UIApplication sharedApplication] statusBarOrientation]];
}
}
@catch (NSException *exception)
{
RECORD_ERROR_STRING(@"RECORDER_ROTATE_FAILED", [exception description]);
}
}
}
- (void)dismissCameraPreview
{
if (![FURRecorder isCameraAvailable])
{
return;
}
[[FURRecorder recorder].cameraPreviewView removeFromSuperview];
}
- (void)removeAllReplayKitButtons
{
[self removeAllRightButtons];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
@raresloth
Copy link
Author

FURReplayKitNavBar is the UI element that exposes access to starting/stopping recording/broadcasting

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment