[iOS] Proxy CallKit API to be a feature of the SDK
With this the RN component and the consumer app can share same CallKit provider, configuration, and enable to be part of multiple listeners of the CallKit flow events. The main driver of this is to enable the consumer app to be able to report an incoming call to the OS before loading the JitsiMeetView. Once the user answers the call, the app can instantiate a JitsiMeetView, pass the CallKit call UUIID, and the Jitsi Meet components will handle the connection and report back to CallKit that the call has been established.
This commit is contained in:
parent
520bb8bd22
commit
e5309a6482
|
@ -38,6 +38,9 @@
|
|||
B386B85D20981A75000DEF7A /* Invite.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85620981A75000DEF7A /* Invite.m */; };
|
||||
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
|
||||
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
|
||||
C69EFA0C209A0F660027712B /* JMCallKitNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */; };
|
||||
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */; };
|
||||
C69EFA0E209A0F660027712B /* JMCallKitEventListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */; };
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
|
||||
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
|
||||
|
@ -83,6 +86,9 @@
|
|||
B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = "<group>"; };
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
|
||||
C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitNotifier.swift; sourceTree = "<group>"; };
|
||||
C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitProxy.swift; sourceTree = "<group>"; };
|
||||
C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitEventListener.swift; sourceTree = "<group>"; };
|
||||
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
|
||||
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
|
||||
|
@ -138,6 +144,7 @@
|
|||
0BD906E71EC0C00300C8C18E /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C69EFA02209A0EFD0027712B /* callkit */,
|
||||
B386B84F20981A11000DEF7A /* invite */,
|
||||
C6A3426B204F127900E062DD /* picture-in-picture */,
|
||||
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
|
||||
|
@ -198,6 +205,16 @@
|
|||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C69EFA02209A0EFD0027712B /* callkit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */,
|
||||
C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */,
|
||||
C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */,
|
||||
);
|
||||
path = callkit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C6A3426B204F127900E062DD /* picture-in-picture */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -390,7 +407,10 @@
|
|||
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
|
||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
||||
C69EFA0C209A0F660027712B /* JMCallKitNotifier.swift in Sources */,
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
|
||||
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */,
|
||||
C69EFA0E209A0F660027712B /* JMCallKitEventListener.swift in Sources */,
|
||||
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTUtils.h>
|
||||
|
||||
#import <JitsiMeet/JitsiMeet-Swift.h>
|
||||
|
||||
// The events emitted/supported by RNCallKit:
|
||||
static NSString * const RNCallKitPerformAnswerCallAction
|
||||
= @"performAnswerCallAction";
|
||||
|
@ -37,14 +39,10 @@ static NSString * const RNCallKitPerformSetMutedCallAction
|
|||
static NSString * const RNCallKitProviderDidReset
|
||||
= @"providerDidReset";
|
||||
|
||||
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
|
||||
@interface RNCallKit : RCTEventEmitter <JMCallKitEventListener>
|
||||
@end
|
||||
|
||||
@implementation RNCallKit
|
||||
{
|
||||
CXCallController *_callController;
|
||||
CXProvider *_provider;
|
||||
}
|
||||
|
||||
RCT_EXTERN void RCTRegisterModule(Class);
|
||||
|
||||
|
@ -70,35 +68,8 @@ RCT_EXTERN void RCTRegisterModule(Class);
|
|||
];
|
||||
}
|
||||
|
||||
// Display the incoming call to the user
|
||||
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID
|
||||
handle:(NSString *)handle
|
||||
hasVideo:(BOOL)hasVideo
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
|
||||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
callUpdate.remoteHandle
|
||||
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
|
||||
callUpdate.supportsDTMF = NO;
|
||||
callUpdate.supportsHolding = NO;
|
||||
callUpdate.supportsGrouping = NO;
|
||||
callUpdate.supportsUngrouping = NO;
|
||||
callUpdate.hasVideo = hasVideo;
|
||||
|
||||
[self.provider reportNewIncomingCallWithUUID:callUUID_
|
||||
update:callUpdate
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
reject(nil, @"Error reporting new incoming call", error);
|
||||
} else {
|
||||
resolve(nil);
|
||||
}
|
||||
}];
|
||||
- (void)dealloc {
|
||||
[JMCallKitProxy removeListener:self];
|
||||
}
|
||||
|
||||
// End call
|
||||
|
@ -141,14 +112,12 @@ RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
|
|||
dictionary);
|
||||
#endif
|
||||
|
||||
CXProviderConfiguration *configuration
|
||||
= [self providerConfigurationFromDictionary:dictionary];
|
||||
if (_provider) {
|
||||
_provider.configuration = configuration;
|
||||
} else {
|
||||
_provider = [[CXProvider alloc] initWithConfiguration:configuration];
|
||||
[_provider setDelegate:self queue:nil];
|
||||
if (![JMCallKitProxy hasProviderBeenConfigurated]) {
|
||||
[self configureProviderFromDictionary:dictionary];
|
||||
}
|
||||
|
||||
// register to receive CallKit proxy events
|
||||
[JMCallKitProxy addListener: self];
|
||||
}
|
||||
|
||||
// Start outgoing call
|
||||
|
@ -162,6 +131,15 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
|
|||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
// Don't start a call action if there's
|
||||
// an active call for this UUID
|
||||
// (i.e. JitsiMeetView was configured from an incoming call
|
||||
if ([JMCallKitProxy hasActiveCallForUUID:callUUID]) {
|
||||
resolve(nil);
|
||||
return;
|
||||
}
|
||||
|
||||
CXHandle *handle_
|
||||
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
|
||||
CXStartCallAction *action
|
||||
|
@ -177,8 +155,8 @@ RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
|
|||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
[self.provider reportCallWithUUID:callUUID_
|
||||
endedAtDate:[NSDate date]
|
||||
[JMCallKitProxy reportCallWith:callUUID_
|
||||
endedAt:nil
|
||||
reason:CXCallEndedReasonFailed];
|
||||
resolve(nil);
|
||||
}
|
||||
|
@ -188,8 +166,8 @@ RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
|
|||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
[self.provider reportOutgoingCallWithUUID:callUUID_
|
||||
connectedAtDate:[NSDate date]];
|
||||
[JMCallKitProxy reportOutgoingCallWith:callUUID_
|
||||
connectedAt:nil];
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
|
@ -206,36 +184,20 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
|||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
if (options[@"displayName"]) {
|
||||
callUpdate.localizedCallerName = options[@"displayName"];
|
||||
}
|
||||
if (options[@"hasVideo"]) {
|
||||
callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
|
||||
}
|
||||
[self.provider reportCallWithUUID:callUUID_ updated:callUpdate];
|
||||
NSString *displayName = options[@"displayName"];
|
||||
BOOL hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
|
||||
|
||||
[JMCallKitProxy reportCallUpdateWith:callUUID_
|
||||
handle:nil
|
||||
displayName:displayName
|
||||
hasVideo:hasVideo];
|
||||
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
#pragma mark - Helper methods
|
||||
|
||||
- (CXCallController *)callController {
|
||||
if (!_callController) {
|
||||
_callController = [[CXCallController alloc] init];
|
||||
}
|
||||
|
||||
return _callController;
|
||||
}
|
||||
|
||||
- (CXProvider *)provider {
|
||||
if (!_provider) {
|
||||
[self setProviderConfiguration:nil];
|
||||
}
|
||||
|
||||
return _provider;
|
||||
}
|
||||
|
||||
- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary {
|
||||
- (void)configureProviderFromDictionary:(NSDictionary* )dictionary {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
|
||||
#endif
|
||||
|
@ -251,30 +213,25 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
|||
= [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
|
||||
}
|
||||
|
||||
CXProviderConfiguration *providerConfiguration
|
||||
= [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
|
||||
|
||||
// iconTemplateImageData
|
||||
NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
|
||||
NSData *iconTemplateImageData;
|
||||
if (iconTemplateImageName) {
|
||||
UIImage *iconTemplateImage
|
||||
= [UIImage imageNamed:iconTemplateImageName
|
||||
inBundle:[NSBundle bundleForClass:self.class]
|
||||
compatibleWithTraitCollection:nil];
|
||||
if (iconTemplateImage) {
|
||||
providerConfiguration.iconTemplateImageData
|
||||
iconTemplateImageData
|
||||
= UIImagePNGRepresentation(iconTemplateImage);
|
||||
}
|
||||
}
|
||||
|
||||
providerConfiguration.maximumCallGroups = 1;
|
||||
providerConfiguration.maximumCallsPerCallGroup = 1;
|
||||
providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"];
|
||||
providerConfiguration.supportedHandleTypes
|
||||
= [NSSet setWithObjects:@(CXHandleTypeGeneric), nil];
|
||||
providerConfiguration.supportsVideo = YES;
|
||||
NSString *ringtoneSound = dictionary[@"ringtoneSound"];
|
||||
|
||||
return providerConfiguration;
|
||||
[JMCallKitProxy configureCallKitProviderWithLocalizedName:localizedName
|
||||
ringtoneSound:ringtoneSound
|
||||
iconTemplateImageData:iconTemplateImageData];
|
||||
}
|
||||
|
||||
- (void)requestTransaction:(CXTransaction *)transaction
|
||||
|
@ -284,7 +241,7 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
|||
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
|
||||
#endif
|
||||
|
||||
[self.callController requestTransaction:transaction
|
||||
[JMCallKitProxy request:transaction
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(
|
||||
|
@ -298,10 +255,10 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
|||
}];
|
||||
}
|
||||
|
||||
#pragma mark - CXProviderDelegate
|
||||
#pragma mark - JitsiMeetCallKitListener
|
||||
|
||||
// Called when the provider has been reset. We should terminate all calls.
|
||||
- (void)providerDidReset:(CXProvider *)provider {
|
||||
- (void)providerDidReset {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
|
||||
#endif
|
||||
|
@ -310,84 +267,61 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
|||
}
|
||||
|
||||
// Answering incoming call
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performAnswerCallAction:(CXAnswerCallAction *)action {
|
||||
- (void) performAnswerCallWithUUID:(NSUUID *)UUID {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformAnswerCallAction
|
||||
body:@{ @"callUUID": action.callUUID.UUIDString }];
|
||||
[action fulfill];
|
||||
body:@{ @"callUUID": UUID.UUIDString }];
|
||||
}
|
||||
|
||||
// Call ended, user request
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performEndCallAction:(CXEndCallAction *)action {
|
||||
- (void) performEndCallWithUUID:(NSUUID *)UUID {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformEndCallAction
|
||||
body:@{ @"callUUID": action.callUUID.UUIDString }];
|
||||
[action fulfill];
|
||||
body:@{ @"callUUID": UUID.UUIDString }];
|
||||
}
|
||||
|
||||
// Handle audio mute from CallKit view
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performSetMutedCallAction:(CXSetMutedCallAction *)action {
|
||||
- (void) performSetMutedCallWithUUID:(NSUUID *)UUID
|
||||
isMuted:(BOOL)isMuted {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformSetMutedCallAction
|
||||
body:@{
|
||||
@"callUUID": action.callUUID.UUIDString,
|
||||
@"muted": @(action.muted)
|
||||
@"callUUID": UUID.UUIDString,
|
||||
@"muted": @(isMuted)
|
||||
}];
|
||||
[action fulfill];
|
||||
}
|
||||
|
||||
// Starting outgoing call
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performStartCallAction:(CXStartCallAction *)action {
|
||||
- (void) performStartCallWithUUID:(NSUUID *)UUID
|
||||
isVideo:(BOOL)isVideo {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
|
||||
#endif
|
||||
|
||||
[action fulfill];
|
||||
|
||||
// Update call info.
|
||||
NSUUID *callUUID = action.callUUID;
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
callUpdate.remoteHandle = action.handle;
|
||||
callUpdate.supportsDTMF = NO;
|
||||
callUpdate.supportsHolding = NO;
|
||||
callUpdate.supportsGrouping = NO;
|
||||
callUpdate.supportsUngrouping = NO;
|
||||
callUpdate.hasVideo = action.isVideo;
|
||||
[provider reportCallWithUUID:callUUID updated:callUpdate];
|
||||
|
||||
// Notify the system about the outgoing call.
|
||||
[provider reportOutgoingCallWithUUID:callUUID
|
||||
startedConnectingAtDate:[NSDate date]];
|
||||
[JMCallKitProxy reportOutgoingCallWith:UUID
|
||||
startedConnectingAt:nil];
|
||||
}
|
||||
|
||||
// The following just help with debugging:
|
||||
#ifdef DEBUG
|
||||
|
||||
- (void) provider:(CXProvider *)provider
|
||||
didActivateAudioSession:(AVAudioSession *)audioSession {
|
||||
- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
|
||||
}
|
||||
|
||||
- (void) provider:(CXProvider *)provider
|
||||
didDeactivateAudioSession:(AVAudioSession *)audioSession {
|
||||
- (void) providerDidDeactivateAudioSessionWithSession:(AVAudioSession *)session {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
|
||||
}
|
||||
|
||||
- (void) provider:(CXProvider *)provider
|
||||
timedOutPerformingAction:(CXAction *)action {
|
||||
- (void) providerTimedOutPerformingActionWithAction:(CXAction *)action {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import AVKit
|
||||
import CallKit
|
||||
import Foundation
|
||||
|
||||
@objc public protocol JMCallKitEventListener: NSObjectProtocol {
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func providerDidReset()
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func performAnswerCall(UUID: UUID)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func performEndCall(UUID: UUID)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func performSetMutedCall(UUID: UUID, isMuted: Bool)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func performStartCall(UUID: UUID, isVideo: Bool)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func providerDidActivateAudioSession(session: AVAudioSession)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func providerDidDeactivateAudioSession(session: AVAudioSession)
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
@objc optional func providerTimedOutPerformingAction(action: CXAction)
|
||||
}
|
||||
|
||||
internal struct JMCallKitEventListenerWrapper: Hashable {
|
||||
|
||||
public var hashValue: Int
|
||||
|
||||
internal weak var listener: JMCallKitEventListener?
|
||||
|
||||
public init(listener: JMCallKitEventListener) {
|
||||
self.listener = listener
|
||||
self.hashValue = listener.hash
|
||||
}
|
||||
|
||||
public static func ==(lhs: JMCallKitEventListenerWrapper,
|
||||
rhs: JMCallKitEventListenerWrapper) -> Bool {
|
||||
return lhs.hashValue == rhs.hashValue
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import AVKit
|
||||
import CallKit
|
||||
import Foundation
|
||||
|
||||
internal final class JMCallKitNotifier: NSObject, CXProviderDelegate {
|
||||
|
||||
private var listeners = Set<JMCallKitEventListenerWrapper>()
|
||||
|
||||
internal override init() {}
|
||||
|
||||
// MARK: - Add/remove listeners
|
||||
|
||||
func addListener(_ listener: JMCallKitEventListener) {
|
||||
let wrapper = JMCallKitEventListenerWrapper(listener: listener)
|
||||
objc_sync_enter(listeners)
|
||||
listeners.insert(wrapper)
|
||||
objc_sync_exit(listeners)
|
||||
}
|
||||
|
||||
func removeListener(_ listener: JMCallKitEventListener) {
|
||||
let wrapper = JMCallKitEventListenerWrapper(listener: listener)
|
||||
objc_sync_enter(listeners)
|
||||
listeners.remove(wrapper)
|
||||
objc_sync_exit(listeners)
|
||||
}
|
||||
|
||||
// MARK: - CXProviderDelegate
|
||||
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach { $0.listener?.providerDidReset?() }
|
||||
objc_sync_exit(listeners)
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach { $0.listener?.performAnswerCall?(UUID: action.callUUID) }
|
||||
objc_sync_exit(listeners)
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach { $0.listener?.performEndCall?(UUID: action.callUUID) }
|
||||
objc_sync_exit(listeners)
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach {
|
||||
$0.listener?.performSetMutedCall?(UUID: action.callUUID,
|
||||
isMuted: action.isMuted)
|
||||
}
|
||||
objc_sync_exit(listeners)
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach {
|
||||
$0.listener?.performStartCall?(UUID: action.callUUID,
|
||||
isVideo: action.isVideo)
|
||||
}
|
||||
objc_sync_exit(listeners)
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach {
|
||||
$0.listener?.providerDidActivateAudioSession?(session: audioSession)
|
||||
}
|
||||
objc_sync_exit(listeners)
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
objc_sync_enter(listeners)
|
||||
listeners.forEach {
|
||||
$0.listener?.providerDidDeactivateAudioSession?(session: audioSession)
|
||||
}
|
||||
objc_sync_exit(listeners)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CallKit
|
||||
import Foundation
|
||||
|
||||
/// JitsiMeet CallKit proxy
|
||||
@available(iOS 10.0, *)
|
||||
@objc public final class JMCallKitProxy: NSObject {
|
||||
|
||||
override private init() {}
|
||||
|
||||
// MARK: - CallKit proxy
|
||||
|
||||
internal static let cxProvider: CXProvider = {
|
||||
let config = CXProviderConfiguration(localizedName: "")
|
||||
let provider = CXProvider(configuration: config)
|
||||
return provider
|
||||
}()
|
||||
|
||||
internal static let cxCallController: CXCallController = {
|
||||
return CXCallController()
|
||||
}()
|
||||
|
||||
internal static let callKitNotifier: JMCallKitNotifier = {
|
||||
return JMCallKitNotifier()
|
||||
}()
|
||||
|
||||
internal static var cxProviderConfiguration: CXProviderConfiguration? {
|
||||
didSet {
|
||||
guard let providerConfiguration = cxProviderConfiguration else { return }
|
||||
cxProvider.configuration = providerConfiguration
|
||||
cxProvider.setDelegate(callKitNotifier, queue: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables the proxy in between callkit and the consumers of the SDK
|
||||
/// Default to enabled, set to false when you don't want to use callkit
|
||||
@objc public static var enabled: Bool = true {
|
||||
didSet {
|
||||
if enabled == false {
|
||||
cxProvider.setDelegate(nil, queue: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func hasProviderBeenConfigurated() -> Bool {
|
||||
return cxProviderConfiguration != nil
|
||||
}
|
||||
|
||||
@objc public static func configureCallKitProvider(localizedName: String,
|
||||
ringtoneSound: String?,
|
||||
iconTemplateImageData: Data?) {
|
||||
let configuration = CXProviderConfiguration(localizedName: localizedName)
|
||||
configuration.ringtoneSound = ringtoneSound
|
||||
configuration.iconTemplateImageData = iconTemplateImageData
|
||||
|
||||
configuration.maximumCallGroups = 1
|
||||
configuration.maximumCallsPerCallGroup = 1
|
||||
configuration.supportedHandleTypes = [CXHandle.HandleType.generic]
|
||||
configuration.supportsVideo = true
|
||||
cxProviderConfiguration = configuration
|
||||
}
|
||||
|
||||
@objc public static func addListener(_ listener: JMCallKitEventListener) {
|
||||
callKitNotifier.addListener(listener)
|
||||
}
|
||||
|
||||
@objc public static func removeListener(_ listener: JMCallKitEventListener) {
|
||||
callKitNotifier.removeListener(listener)
|
||||
}
|
||||
|
||||
@objc public static func hasActiveCallForUUID(_ callUUID: String) -> Bool {
|
||||
let activeCallForUUID = cxCallController.callObserver.calls.first {
|
||||
$0.uuid == UUID(uuidString: callUUID)
|
||||
}
|
||||
guard activeCallForUUID != nil else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@objc public static func reportNewIncomingCall(UUID: UUID,
|
||||
handle: String?,
|
||||
displayName: String?,
|
||||
hasVideo: Bool,
|
||||
completion: @escaping (Error?) -> Void) {
|
||||
guard enabled else { return }
|
||||
|
||||
let callUpdate = makeCXUpdate(handle: handle,
|
||||
displayName: displayName,
|
||||
hasVideo: hasVideo)
|
||||
cxProvider.reportNewIncomingCall(with: UUID,
|
||||
update: callUpdate,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@objc public static func reportCallUpdate(with UUID: UUID,
|
||||
handle: String?,
|
||||
displayName: String?,
|
||||
hasVideo: Bool) {
|
||||
guard enabled else { return }
|
||||
|
||||
let callUpdate = makeCXUpdate(handle: handle,
|
||||
displayName: displayName,
|
||||
hasVideo: hasVideo)
|
||||
cxProvider.reportCall(with: UUID, updated: callUpdate)
|
||||
}
|
||||
|
||||
@objc public static func reportCall(with UUID: UUID,
|
||||
endedAt dateEnded: Date?,
|
||||
reason endedReason: CXCallEndedReason) {
|
||||
guard enabled else { return }
|
||||
|
||||
cxProvider.reportCall(with: UUID,
|
||||
endedAt: dateEnded,
|
||||
reason: endedReason)
|
||||
}
|
||||
|
||||
@objc public static func reportOutgoingCall(with UUID: UUID,
|
||||
startedConnectingAt dateStartedConnecting: Date?) {
|
||||
guard enabled else { return }
|
||||
|
||||
cxProvider.reportOutgoingCall(with: UUID,
|
||||
startedConnectingAt: dateStartedConnecting)
|
||||
}
|
||||
|
||||
@objc public static func reportOutgoingCall(with UUID: UUID,
|
||||
connectedAt dateConnected: Date?) {
|
||||
guard enabled else { return }
|
||||
|
||||
cxProvider.reportOutgoingCall(with: UUID, connectedAt: dateConnected)
|
||||
}
|
||||
|
||||
@objc public static func request(_ transaction: CXTransaction,
|
||||
completion: @escaping (Error?) -> Swift.Void) {
|
||||
guard enabled else { return }
|
||||
|
||||
cxCallController.request(transaction, completion: completion)
|
||||
}
|
||||
|
||||
// MARK: - Callkit Proxy helpers
|
||||
|
||||
private static func makeCXUpdate(handle: String?,
|
||||
displayName: String?,
|
||||
hasVideo: Bool) -> CXCallUpdate {
|
||||
let update = CXCallUpdate()
|
||||
update.supportsDTMF = false
|
||||
update.supportsHolding = false
|
||||
update.supportsGrouping = false
|
||||
update.supportsUngrouping = false
|
||||
update.hasVideo = hasVideo
|
||||
|
||||
if let handle = handle {
|
||||
update.remoteHandle = CXHandle(type: .generic,
|
||||
value: handle)
|
||||
}
|
||||
|
||||
if let displayName = displayName {
|
||||
update.localizedCallerName = displayName
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,7 @@ const WHITELISTED_KEYS = [
|
|||
'callStatsConfIDNamespace',
|
||||
'callStatsID',
|
||||
'callStatsSecret',
|
||||
'callUUID',
|
||||
'channelLastN',
|
||||
'constraints',
|
||||
'debug',
|
||||
|
|
|
@ -235,10 +235,12 @@ function _conferenceWillJoin({ getState }, next, action) {
|
|||
const state = getState();
|
||||
const url = getInviteURL(state);
|
||||
const hasVideo = !isVideoMutedByAudioOnly(state);
|
||||
const { callUUID } = state['features/base/config'];
|
||||
|
||||
// When assigning the call UUID, do so in upper case, since iOS will
|
||||
// return it upper cased.
|
||||
conference.callUUID = uuid.v4().toUpperCase();
|
||||
conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
|
||||
|
||||
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
|
||||
.then(() => {
|
||||
const { room } = state['features/base/conference'];
|
||||
|
|
Loading…
Reference in New Issue