Skip to content

Commit 84c9dbb

Browse files
MagmusacyCopilot
andauthored
Create hook and native methods to handle incoming voip calls (#59)
* Move media projection fgs to WebRTCForegroundService * Remove file uploaded by mistake * Fixes outlined by copilot + format * Fix format * Format * Report incoming call on the JS side * Handle VoIP push token registration * Remove manually calling myself form code and add some logs for dev purposes * Simplify message passing between objects with callbacks instead of notification center * Add PushKit category * Add useVoIPEvents hook * Change queue to a serial one * Synchronously get voip token on mount * format * lint * Fix typo and remove obsolete code * format * Rename * lint * Change naming again lol * Minor renaming * Add some default values and enforce roomName * Fix race condition scenario with sending payload and answering the call * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Always report incoming calls --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 2950203 commit 84c9dbb

14 files changed

Lines changed: 436 additions & 36 deletions

ios/RCTWebRTC/CallKitManager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ typedef void (^CallKitBoolCallback)(BOOL);
88
@interface CallKitManager : NSObject<CXProviderDelegate>
99

1010
@property(nonatomic, copy) CallKitVoidCallback onCallStarted;
11+
@property(nonatomic, copy) CallKitVoidCallback onCallAnswered;
1112
@property(nonatomic, copy) CallKitVoidCallback onCallEnded;
1213
@property(nonatomic, copy) CallKitStringCallback onCallFailed;
1314
@property(nonatomic, copy) CallKitBoolCallback onCallMuted;
1415
@property(nonatomic, copy) CallKitBoolCallback onCallHeld;
1516
@property(nonatomic, readonly) BOOL hasActiveCall;
17+
@property(nonatomic, readonly) BOOL isCallAnswered;
18+
19+
+ (instancetype)shared;
1620

1721
- (void)startCallWithDisplayName:(NSString *)displayName isVideo:(BOOL)isVideo;
22+
- (void)reportIncomingCallWithDisplayName:(NSString *)displayName isVideo:(BOOL)isVideo;
1823
- (void)endCall;
1924

2025
@end

ios/RCTWebRTC/CallKitManager.m

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ @interface CallKitManager ()
77
@property(nonatomic, strong) CXCallController *callController;
88
@property(nonatomic, strong) CXProvider *provider;
99
@property(nonatomic, strong) NSUUID *currentCallUUID;
10+
@property(nonatomic, assign) BOOL isCallAnswered;
1011
@end
1112

1213
@implementation CallKitManager
1314

15+
+ (instancetype)shared {
16+
static CallKitManager *sharedInstance = nil;
17+
static dispatch_once_t onceToken;
18+
dispatch_once(&onceToken, ^{
19+
sharedInstance = [[CallKitManager alloc] init];
20+
});
21+
return sharedInstance;
22+
}
23+
1424
- (instancetype)init {
1525
self = [super init];
1626
if (self) {
@@ -70,6 +80,35 @@ - (void)startCallWithDisplayName:(NSString *)displayName isVideo:(BOOL)isVideo {
7080
}];
7181
}
7282

83+
- (void)reportIncomingCallWithDisplayName:(NSString *)displayName isVideo:(BOOL)isVideo {
84+
NSUUID *uuid = [NSUUID UUID];
85+
self.currentCallUUID = uuid;
86+
self.isCallAnswered = NO;
87+
88+
CXCallUpdate *update = [[CXCallUpdate alloc] init];
89+
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:displayName];
90+
update.localizedCallerName = displayName;
91+
update.hasVideo = isVideo;
92+
update.supportsHolding = NO;
93+
update.supportsGrouping = NO;
94+
update.supportsUngrouping = NO;
95+
update.supportsDTMF = NO;
96+
97+
__weak typeof(self) weakSelf = self;
98+
[self.provider reportNewIncomingCallWithUUID:uuid
99+
update:update
100+
completion:^(NSError *_Nullable error) {
101+
if (error) {
102+
NSLog(@"[CallKitManager] Failed to report incoming call: %@",
103+
error.localizedDescription);
104+
weakSelf.currentCallUUID = nil;
105+
if (weakSelf.onCallFailed) {
106+
weakSelf.onCallFailed(error.localizedDescription);
107+
}
108+
}
109+
}];
110+
}
111+
73112
- (void)endCall {
74113
if (self.currentCallUUID == nil) {
75114
NSLog(@"[CallKitManager] No active call to end");
@@ -95,6 +134,7 @@ - (void)endCall {
95134

96135
- (void)cleanup {
97136
self.currentCallUUID = nil;
137+
self.isCallAnswered = NO;
98138
}
99139

100140
#pragma mark - CXProviderDelegate
@@ -116,6 +156,10 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)
116156
}
117157

118158
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
159+
self.isCallAnswered = YES;
160+
if (self.onCallAnswered) {
161+
self.onCallAnswered();
162+
}
119163
[action fulfill];
120164
}
121165

ios/RCTWebRTC/VoipManager.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface VoipManager : NSObject
4+
@property(nonatomic, copy, readonly, nullable) NSString *token;
5+
@property(nonatomic, copy, readonly, nullable) NSDictionary *pendingIncomingCall;
6+
@property(nonatomic, copy) void (^onTokenUpdated)(NSString *token);
7+
@property(nonatomic, copy) void (^onIncomingPush)(NSDictionary *payload);
8+
+ (instancetype)shared;
9+
+ (void)registerForVoIPPushes;
10+
- (void)clearPendingIncomingCall;
11+
@end

ios/RCTWebRTC/VoipManager.m

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#import "VoipManager.h"
2+
#import <PushKit/PushKit.h>
3+
#import "CallKitManager.h"
4+
5+
@interface VoipManager ()<PKPushRegistryDelegate>
6+
@property(nonatomic, strong) PKPushRegistry *registry;
7+
@property(nonatomic, strong) dispatch_queue_t registryQueue;
8+
@property(copy, readwrite, nullable) NSString *token;
9+
@property(copy, readwrite, nullable) NSDictionary *pendingIncomingCall;
10+
@end
11+
12+
@implementation VoipManager
13+
14+
+ (instancetype)shared {
15+
static VoipManager *sharedInstance = nil;
16+
static dispatch_once_t onceToken;
17+
dispatch_once(&onceToken, ^{
18+
sharedInstance = [[VoipManager alloc] init];
19+
});
20+
21+
return sharedInstance;
22+
}
23+
24+
+ (void)registerForVoIPPushes {
25+
[[self shared] registerForVoIPPushes];
26+
}
27+
28+
- (void)registerForVoIPPushes {
29+
if (self.registry != nil) {
30+
return;
31+
}
32+
33+
self.registryQueue = dispatch_queue_create("io.fishjam.voippush", DISPATCH_QUEUE_SERIAL);
34+
self.registry = [[PKPushRegistry alloc] initWithQueue:self.registryQueue];
35+
self.registry.delegate = self;
36+
self.registry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
37+
}
38+
39+
#pragma mark - PKPushRegistryDelegate
40+
41+
- (void)pushRegistry:(PKPushRegistry *)registry
42+
didUpdatePushCredentials:(PKPushCredentials *)pushCredentials
43+
forType:(PKPushType)type {
44+
const unsigned char *bytes = pushCredentials.token.bytes;
45+
NSMutableString *hex = [NSMutableString stringWithCapacity:pushCredentials.token.length * 2];
46+
for (NSUInteger i = 0; i < pushCredentials.token.length; i++) {
47+
[hex appendFormat:@"%02x", bytes[i]];
48+
}
49+
NSString *tokenString = [hex copy];
50+
51+
if ([tokenString isEqualToString:self.token]) {
52+
return;
53+
}
54+
self.token = tokenString;
55+
56+
if (self.onTokenUpdated) {
57+
self.onTokenUpdated(tokenString);
58+
}
59+
}
60+
61+
- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type {
62+
self.token = nil;
63+
}
64+
65+
- (void)pushRegistry:(PKPushRegistry *)registry
66+
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
67+
forType:(PKPushType)type
68+
withCompletionHandler:(void (^)(void))completion {
69+
NSMutableDictionary *dict = [payload.dictionaryPayload mutableCopy];
70+
NSString *displayName = dict[@"displayName"];
71+
BOOL isVideo = [dict[@"isVideo"] boolValue];
72+
dict[@"isVideo"] = @(isVideo);
73+
74+
if (displayName == nil || displayName.length == 0) {
75+
displayName = @"Incoming call";
76+
dict[@"displayName"] = displayName;
77+
}
78+
79+
[[CallKitManager shared] reportIncomingCallWithDisplayName:displayName isVideo:isVideo];
80+
81+
// Buffer the payload if the app was cold-launched and JS side hasn't yet loaded
82+
self.pendingIncomingCall = dict;
83+
84+
if (self.onIncomingPush) {
85+
self.onIncomingPush(dict ?: @{});
86+
}
87+
88+
completion();
89+
}
90+
91+
- (void)clearPendingIncomingCall {
92+
self.pendingIncomingCall = nil;
93+
}
94+
95+
@end

ios/RCTWebRTC/WebRTCModule+CallKit.m

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
#import "CallKitManager.h"
88

9+
#import "WebRTCModule+PushKit.h"
10+
911
static void *CallKitManagerKey = &CallKitManagerKey;
1012

1113
@implementation WebRTCModule (CallKit)
@@ -16,11 +18,14 @@ - (CallKitManager *)callKitManager {
1618
return manager;
1719
}
1820

19-
manager = [[CallKitManager alloc] init];
21+
manager = [CallKitManager shared];
2022
__weak typeof(self) weakSelf = self;
2123
manager.onCallStarted = ^{
2224
[weakSelf sendEventWithName:kEventCallKitActionPerformed body:@{@"started" : [NSNull null]}];
2325
};
26+
manager.onCallAnswered = ^{
27+
[weakSelf sendEventWithName:kEventCallKitActionPerformed body:@{@"answer" : [NSNull null]}];
28+
};
2429
manager.onCallEnded = ^{
2530
[weakSelf sendEventWithName:kEventCallKitActionPerformed body:@{@"ended" : [NSNull null]}];
2631
};
@@ -38,6 +43,17 @@ - (CallKitManager *)callKitManager {
3843
return manager;
3944
}
4045

46+
- (void)startObserving {
47+
[super startObserving];
48+
[self callKitManager];
49+
[self startObservingPushKit];
50+
}
51+
52+
- (void)stopObserving {
53+
[self stopObservingPushKit];
54+
[super stopObserving];
55+
}
56+
4157
RCT_EXPORT_METHOD(startCallKitSession
4258
: (NSString *)displayName isVideo
4359
: (BOOL)isVideo resolver
@@ -69,4 +85,8 @@ - (CallKitManager *)callKitManager {
6985
return @([self callKitManager].hasActiveCall);
7086
}
7187

88+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isCallAnswered) {
89+
return @([self callKitManager].isCallAnswered);
90+
}
91+
7292
@end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#import "WebRTCModule.h"
2+
3+
@interface WebRTCModule (PushKit)
4+
5+
- (void)startObservingPushKit;
6+
- (void)stopObservingPushKit;
7+
8+
@end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#import "WebRTCModule+PushKit.h"
2+
3+
#import "VoipManager.h"
4+
5+
@implementation WebRTCModule (PushKit)
6+
7+
- (void)startObservingPushKit {
8+
VoipManager *push = [VoipManager shared];
9+
__weak typeof(self) weakSelf = self;
10+
11+
push.onTokenUpdated = ^(NSString *token) {
12+
[weakSelf sendEventWithName:kEventVoipPush body:@{@"registered" : token ?: @""}];
13+
};
14+
15+
push.onIncomingPush = ^(NSDictionary *payload) {
16+
[weakSelf sendEventWithName:kEventVoipPush body:@{@"incoming" : payload ?: @{}}];
17+
};
18+
19+
NSString *token = push.token;
20+
if (token.length > 0) {
21+
[weakSelf sendEventWithName:kEventVoipPush body:@{@"registered" : token}];
22+
}
23+
24+
NSDictionary *pendingCall = push.pendingIncomingCall;
25+
if (pendingCall) {
26+
[weakSelf sendEventWithName:kEventVoipPush body:@{@"incoming" : pendingCall}];
27+
}
28+
}
29+
30+
- (void)stopObservingPushKit {
31+
VoipManager *push = [VoipManager shared];
32+
push.onTokenUpdated = nil;
33+
push.onIncomingPush = nil;
34+
}
35+
36+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getVoipToken) {
37+
return [VoipManager shared].token ?: [NSNull null];
38+
}
39+
40+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getPendingIncomingCall) {
41+
return [VoipManager shared].pendingIncomingCall ?: [NSNull null];
42+
}
43+
44+
RCT_EXPORT_METHOD(clearPendingIncomingCall) {
45+
[[VoipManager shared] clearPendingIncomingCall];
46+
}
47+
48+
@end

ios/RCTWebRTC/WebRTCModule.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ static NSString *const kEventCallKitActionPerformed = @"callKitActionPerformed";
2525
static NSString *const kEventAudioOutputChanged = @"audioOutputChanged";
2626
static NSString *const kEventLivestreamStatusChanged = @"livestreamStatusChanged";
2727
static NSString *const kMediaStreamVideoTracksChangedNotification = @"RTCMediaStreamVideoTracksChangedNotification";
28+
static NSString *const kEventVoipPush = @"voipPushEvent";
2829

2930
@class FJAudioSinkBox;
3031

ios/RCTWebRTC/WebRTCModule.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ - (dispatch_queue_t)methodQueue {
131131
kEventPeerConnectionOnTrack,
132132
kEventCallKitActionPerformed,
133133
kEventAudioOutputChanged,
134-
kEventLivestreamStatusChanged
134+
kEventLivestreamStatusChanged,
135+
kEventVoipPush
135136
];
136137
}
137138

src/CallKit.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type CallKitConfig = {
99

1010
export type CallKitAction = {
1111
started?: undefined;
12+
answer?: undefined;
1213
ended?: undefined;
1314
failed?: string;
1415
muted?: boolean;
@@ -35,5 +36,12 @@ export function hasActiveCallKitSession(): boolean {
3536
if (Platform.OS !== 'ios') {
3637
return false;
3738
}
38-
return WebRTCModule.hasActiveCallKitSession;
39+
return WebRTCModule.hasActiveCallKitSession();
40+
}
41+
42+
export function isCallAnswered(): boolean {
43+
if (Platform.OS !== 'ios') {
44+
return false;
45+
}
46+
return WebRTCModule.isCallAnswered();
3947
}

0 commit comments

Comments
 (0)