[iOS] Add initial CallKit support
This commit is contained in:
parent
8d11b3024e
commit
3b5ee2d4c6
|
@ -174,13 +174,13 @@ null and the Welcome page is enabled, the Welcome page is displayed instead.
|
|||
Example:
|
||||
|
||||
```java
|
||||
Bundle configOverwrite = new Bundle();
|
||||
configOverwrite.putBoolean("startWithAudioMuted", true);
|
||||
configOverwrite.putBoolean("startWithVideoMuted", false);
|
||||
Bundle urlBundle = new Bundle();
|
||||
urlBundle.putBundle("configOverwrite", configOverwrite);
|
||||
urlBundle.putString("url", "https://meet.jit.si/Test123");
|
||||
view.loadURLObject(urlBundle);
|
||||
Bundle config = new Bundle();
|
||||
config.putBoolean("startWithAudioMuted", true);
|
||||
config.putBoolean("startWithVideoMuted", false);
|
||||
Bundle urlObject = new Bundle();
|
||||
urlObject.putBundle("config", config);
|
||||
urlObject.putString("url", "https://meet.jit.si/Test123");
|
||||
view.loadURLObject(urlObject);
|
||||
```
|
||||
|
||||
#### setDefaultURL(URL)
|
||||
|
|
|
@ -76,11 +76,11 @@ instead.
|
|||
|
||||
```objc
|
||||
[jitsiMeetView loadURLObject:@{
|
||||
@"url": @"https://meet.jit.si/test123",
|
||||
@"configOverwrite": @{
|
||||
@"config": @{
|
||||
@"startWithAudioMuted": @YES,
|
||||
@"startWithVideoMuted": @NO
|
||||
}
|
||||
},
|
||||
@"url": @"https://meet.jit.si/test123"
|
||||
}];
|
||||
```
|
||||
|
||||
|
|
|
@ -22,36 +22,49 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTEventDispatcher.h>
|
||||
#import <React/RCTUtils.h>
|
||||
|
||||
// Weakly load CallKit, because it's not available on iOS 9.
|
||||
@import CallKit;
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTConvert.h>
|
||||
#import <React/RCTEventDispatcher.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTUtils.h>
|
||||
|
||||
// Events we will emit.
|
||||
static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
|
||||
static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
|
||||
static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
|
||||
static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
|
||||
|
||||
// The events emitted/supported by RNCallKit:
|
||||
static NSString * const RNCallKitPerformAnswerCallAction
|
||||
= @"performAnswerCallAction";
|
||||
static NSString * const RNCallKitPerformEndCallAction
|
||||
= @"performEndCallAction";
|
||||
static NSString * const RNCallKitPerformSetMutedCallAction
|
||||
= @"performSetMutedCallAction";
|
||||
static NSString * const RNCallKitProviderDidReset
|
||||
= @"providerDidReset";
|
||||
|
||||
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
|
||||
@end
|
||||
|
||||
@implementation RNCallKit
|
||||
{
|
||||
CXCallController *callKitCallController;
|
||||
CXProvider *callKitProvider;
|
||||
CXCallController *_callController;
|
||||
CXProvider *_provider;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
RCT_EXTERN void RCTRegisterModule(Class);
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents
|
||||
{
|
||||
+ (void)load {
|
||||
// Make the react-native module RNCallKit available (to JS) only if CallKit
|
||||
// is available on the executing operating sytem. For example, CallKit is
|
||||
// not available on iOS 9.
|
||||
if ([CXCallController class]) {
|
||||
RCTRegisterModule(self);
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString *)moduleName {
|
||||
return @"RNCallKit";
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[
|
||||
RNCallKitPerformAnswerCallAction,
|
||||
RNCallKitPerformEndCallAction,
|
||||
|
@ -60,33 +73,17 @@ RCT_EXPORT_MODULE()
|
|||
];
|
||||
}
|
||||
|
||||
// Configure CallKit
|
||||
RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][setup] options = %@", options);
|
||||
#endif
|
||||
callKitCallController = [[CXCallController alloc] init];
|
||||
if (callKitProvider) {
|
||||
[callKitProvider invalidate];
|
||||
}
|
||||
callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
|
||||
[callKitProvider setDelegate:self queue:nil];
|
||||
}
|
||||
|
||||
#pragma mark - CXCallController call actions
|
||||
|
||||
// Display the incoming call to the user
|
||||
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
||||
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID
|
||||
handle:(NSString *)handle
|
||||
hasVideo:(BOOL)hasVideo
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
|
||||
NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
|
||||
#endif
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
callUpdate.remoteHandle
|
||||
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
|
||||
|
@ -95,149 +92,208 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|||
callUpdate.supportsGrouping = NO;
|
||||
callUpdate.supportsUngrouping = NO;
|
||||
callUpdate.hasVideo = hasVideo;
|
||||
|
||||
[callKitProvider reportNewIncomingCallWithUUID:uuid
|
||||
update:callUpdate
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error == nil) {
|
||||
resolve(nil);
|
||||
} else {
|
||||
|
||||
[self.provider reportNewIncomingCallWithUUID:callUUID_
|
||||
update:callUpdate
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
reject(nil, @"Error reporting new incoming call", error);
|
||||
} else {
|
||||
resolve(nil);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// End call
|
||||
RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
|
||||
RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
|
||||
NSLog(@"[RNCallKit][endCall] callUUID = %@", callUUID);
|
||||
#endif
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
|
||||
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
|
||||
[self requestTransaction:transaction resolve:resolve reject:reject];
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXEndCallAction *action
|
||||
= [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
|
||||
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
|
||||
resolve:resolve
|
||||
reject:reject];
|
||||
}
|
||||
|
||||
// Mute / unmute (audio)
|
||||
RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
|
||||
muted:(BOOL) muted
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
|
||||
muted:(BOOL)muted
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
|
||||
NSLog(@"[RNCallKit][setMuted] callUUID = %@", callUUID);
|
||||
#endif
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXSetMutedCallAction *action
|
||||
= [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
|
||||
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
|
||||
[self requestTransaction:transaction resolve:resolve reject:reject];
|
||||
= [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
|
||||
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
|
||||
resolve:resolve
|
||||
reject:reject];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
|
||||
#ifdef DEBUG
|
||||
NSLog(
|
||||
@"[RNCallKit][setProviderConfiguration:] dictionary = %@",
|
||||
dictionary);
|
||||
#endif
|
||||
|
||||
CXProviderConfiguration *configuration
|
||||
= [self providerConfigurationFromDictionary:dictionary];
|
||||
if (_provider) {
|
||||
_provider.configuration = configuration;
|
||||
} else {
|
||||
_provider = [[CXProvider alloc] initWithConfiguration:configuration];
|
||||
[_provider setDelegate:self queue:nil];
|
||||
}
|
||||
}
|
||||
|
||||
// Start outgoing call
|
||||
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
|
||||
RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
|
||||
handle:(NSString *)handle
|
||||
video:(BOOL)video
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
|
||||
NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
|
||||
#endif
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
CXHandle *callHandle
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXHandle *handle_
|
||||
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
|
||||
CXStartCallAction *action
|
||||
= [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
|
||||
= [[CXStartCallAction alloc] initWithCallUUID:callUUID_
|
||||
handle:handle_];
|
||||
action.video = video;
|
||||
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
|
||||
[self requestTransaction:transaction resolve:resolve reject:reject];
|
||||
}
|
||||
|
||||
// Indicate call failed
|
||||
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
[callKitProvider reportCallWithUUID:uuid
|
||||
endedAtDate:[NSDate date]
|
||||
reason:CXCallEndedReasonFailed];
|
||||
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]
|
||||
reason:CXCallEndedReasonFailed];
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
// Indicate outgoing call connected
|
||||
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
[callKitProvider reportOutgoingCallWithUUID:uuid
|
||||
connectedAtDate:[NSDate date]];
|
||||
// Indicate outgoing call connected.
|
||||
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]];
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
// Update call in case we have a display name or video capability changes
|
||||
RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
|
||||
// Update call in case we have a display name or video capability changes.
|
||||
RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
||||
options:(NSDictionary *)options
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
|
||||
NSLog(
|
||||
@"[RNCallKit][updateCall] callUUID = %@ options = %@",
|
||||
callUUID,
|
||||
options);
|
||||
#endif
|
||||
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
||||
CXCallUpdate *update = [[CXCallUpdate alloc] init];
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
if (options[@"displayName"]) {
|
||||
update.localizedCallerName = options[@"displayName"];
|
||||
callUpdate.localizedCallerName = options[@"displayName"];
|
||||
}
|
||||
if (options[@"hasVideo"]) {
|
||||
update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
|
||||
callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
|
||||
}
|
||||
[callKitProvider reportCallWithUUID:uuid updated:update];
|
||||
[self.provider reportCallWithUUID:callUUID_ updated:callUpdate];
|
||||
resolve(nil);
|
||||
}
|
||||
|
||||
#pragma mark - Helper methods
|
||||
|
||||
- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
|
||||
{
|
||||
- (CXCallController *)callController {
|
||||
if (!_callController) {
|
||||
_callController = [[CXCallController alloc] init];
|
||||
}
|
||||
|
||||
return _callController;
|
||||
}
|
||||
|
||||
- (CXProvider *)provider {
|
||||
if (!_provider) {
|
||||
[self setProviderConfiguration:nil];
|
||||
}
|
||||
|
||||
return _provider;
|
||||
}
|
||||
|
||||
- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][getProviderConfiguration]");
|
||||
NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
|
||||
#endif
|
||||
|
||||
if (!dictionary) {
|
||||
dictionary = @{};
|
||||
}
|
||||
|
||||
// localizedName
|
||||
NSString *localizedName = dictionary[@"localizedName"];
|
||||
if (!localizedName) {
|
||||
localizedName
|
||||
= [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
|
||||
}
|
||||
|
||||
CXProviderConfiguration *providerConfiguration
|
||||
= [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
|
||||
providerConfiguration.supportsVideo = YES;
|
||||
= [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
|
||||
|
||||
// iconTemplateImageData
|
||||
NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
|
||||
if (iconTemplateImageName) {
|
||||
UIImage *iconTemplateImage = [UIImage imageNamed:iconTemplateImageName];
|
||||
if (iconTemplateImage) {
|
||||
providerConfiguration.iconTemplateImageData
|
||||
= UIImagePNGRepresentation(iconTemplateImage);
|
||||
}
|
||||
}
|
||||
|
||||
providerConfiguration.maximumCallGroups = 1;
|
||||
providerConfiguration.maximumCallsPerCallGroup = 1;
|
||||
providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"];
|
||||
providerConfiguration.supportedHandleTypes
|
||||
= [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
|
||||
if (settings[@"imageName"]) {
|
||||
providerConfiguration.iconTemplateImageData
|
||||
= UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
|
||||
}
|
||||
if (settings[@"ringtoneSound"]) {
|
||||
providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
|
||||
}
|
||||
= [NSSet setWithObjects:@(CXHandleTypeGeneric), nil];
|
||||
providerConfiguration.supportsVideo = YES;
|
||||
|
||||
return providerConfiguration;
|
||||
}
|
||||
|
||||
- (void)requestTransaction:(CXTransaction *)transaction
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject
|
||||
{
|
||||
reject:(RCTPromiseRejectBlock)reject {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
|
||||
#endif
|
||||
[callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
|
||||
if (error == nil) {
|
||||
resolve(nil);
|
||||
} else {
|
||||
NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
|
||||
|
||||
[self.callController requestTransaction:transaction
|
||||
completion:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(
|
||||
@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)",
|
||||
transaction.actions,
|
||||
error);
|
||||
reject(nil, @"Error processing CallKit transaction", error);
|
||||
} else {
|
||||
resolve(nil);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
@ -247,53 +303,62 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
|
|||
// Called when the provider has been reset. We should terminate all calls.
|
||||
- (void)providerDidReset:(CXProvider *)provider {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitProviderDidReset body:nil];
|
||||
}
|
||||
|
||||
// Answering incoming call
|
||||
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
|
||||
{
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performAnswerCallAction:(CXAnswerCallAction *)action {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformAnswerCallAction
|
||||
body:@{ @"callUUID": action.callUUID.UUIDString }];
|
||||
[action fulfill];
|
||||
}
|
||||
|
||||
// Call ended, user request
|
||||
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
|
||||
{
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performEndCallAction:(CXEndCallAction *)action {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformEndCallAction
|
||||
body:@{ @"callUUID": action.callUUID.UUIDString }];
|
||||
[action fulfill];
|
||||
}
|
||||
|
||||
// Handle audio mute from CallKit view
|
||||
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performSetMutedCallAction:(CXSetMutedCallAction *)action {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
|
||||
#endif
|
||||
|
||||
[self sendEventWithName:RNCallKitPerformSetMutedCallAction
|
||||
body:@{ @"callUUID": action.callUUID.UUIDString,
|
||||
@"muted": [NSNumber numberWithBool:action.muted]}];
|
||||
body:@{
|
||||
@"callUUID": action.callUUID.UUIDString,
|
||||
@"muted": @(action.muted)
|
||||
}];
|
||||
[action fulfill];
|
||||
}
|
||||
|
||||
// Starting outgoing call
|
||||
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
|
||||
{
|
||||
- (void) provider:(CXProvider *)provider
|
||||
performStartCallAction:(CXStartCallAction *)action {
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
|
||||
#endif
|
||||
|
||||
[action fulfill];
|
||||
|
||||
// Update call info
|
||||
|
||||
// Update call info.
|
||||
NSUUID *callUUID = action.callUUID;
|
||||
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
|
||||
callUpdate.remoteHandle = action.handle;
|
||||
callUpdate.supportsDTMF = NO;
|
||||
|
@ -301,34 +366,31 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
|
|||
callUpdate.supportsGrouping = NO;
|
||||
callUpdate.supportsUngrouping = NO;
|
||||
callUpdate.hasVideo = action.isVideo;
|
||||
[callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
|
||||
|
||||
// Notify the system about the outgoing call
|
||||
[callKitProvider reportOutgoingCallWithUUID:action.callUUID
|
||||
startedConnectingAtDate:[NSDate date]];
|
||||
[provider reportCallWithUUID:callUUID updated:callUpdate];
|
||||
|
||||
// Notify the system about the outgoing call.
|
||||
[provider reportOutgoingCallWithUUID:callUUID
|
||||
startedConnectingAtDate:[NSDate date]];
|
||||
}
|
||||
|
||||
// These just help with debugging
|
||||
|
||||
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
|
||||
{
|
||||
// The following just help with debugging:
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
|
||||
#endif
|
||||
|
||||
- (void) provider:(CXProvider *)provider
|
||||
didActivateAudioSession:(AVAudioSession *)audioSession {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
|
||||
}
|
||||
|
||||
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
|
||||
#endif
|
||||
- (void) provider:(CXProvider *)provider
|
||||
didDeactivateAudioSession:(AVAudioSession *)audioSession {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
|
||||
}
|
||||
|
||||
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
|
||||
{
|
||||
#ifdef DEBUG
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
|
||||
#endif
|
||||
- (void) provider:(CXProvider *)provider
|
||||
timedOutPerformingAction:(CXAction *)action {
|
||||
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
|
||||
|
||||
@property (copy, nonatomic) NSURL *defaultURL;
|
||||
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
||||
|
||||
@property (nonatomic) BOOL welcomePageEnabled;
|
||||
|
||||
|
|
|
@ -17,26 +17,15 @@
|
|||
#import <CoreText/CoreText.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
@import Intents;
|
||||
|
||||
#import <React/RCTAssert.h>
|
||||
#import <React/RCTLinkingManager.h>
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#include <Availability.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "JitsiMeetView+Private.h"
|
||||
#import "RCTBridgeWrapper.h"
|
||||
|
||||
// Weakly load the Intents framework since it's not available on iOS 9.
|
||||
@import Intents;
|
||||
|
||||
// Constant describing iOS 10.0.0
|
||||
static const NSOperatingSystemVersion ios10 = {
|
||||
.majorVersion = 10,
|
||||
.minorVersion = 0,
|
||||
.patchVersion = 0
|
||||
};
|
||||
|
||||
/**
|
||||
* A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
|
||||
* In the Release configuration, React Native will (intentionally) raise an
|
||||
|
@ -153,50 +142,45 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
|||
continueUserActivity:(NSUserActivity *)userActivity
|
||||
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
|
||||
{
|
||||
NSString *activityType = userActivity.activityType;
|
||||
|
||||
// XXX At least twice we received bug reports about malfunctioning loadURL
|
||||
// in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
|
||||
// expected in our testing. But that was to be expected because the app does
|
||||
// not exercise loadURL. In order to increase the test coverage of loadURL,
|
||||
// channel Universal linking through loadURL.
|
||||
if ([userActivity.activityType
|
||||
isEqualToString:NSUserActivityTypeBrowsingWeb]
|
||||
&& [JitsiMeetView loadURLInViews:userActivity.webpageURL]) {
|
||||
if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
|
||||
&& [self loadURLInViews:userActivity.webpageURL]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Check for CallKit intents only on iOS >= 10
|
||||
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
|
||||
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
|
||||
|| [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
|
||||
INInteraction *interaction = [userActivity interaction];
|
||||
INIntent *intent = interaction.intent;
|
||||
NSString *handle;
|
||||
BOOL isAudio = NO;
|
||||
// Check for a CallKit intent.
|
||||
if ([activityType isEqualToString:@"INStartAudioCallIntent"]
|
||||
|| [activityType isEqualToString:@"INStartVideoCallIntent"]) {
|
||||
INIntent *intent = userActivity.interaction.intent;
|
||||
NSArray<INPerson *> *contacts;
|
||||
NSString *url;
|
||||
BOOL startAudioOnly = NO;
|
||||
|
||||
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
|
||||
INStartAudioCallIntent *startCallIntent
|
||||
= (INStartAudioCallIntent *)intent;
|
||||
handle = startCallIntent.contacts.firstObject.personHandle.value;
|
||||
isAudio = YES;
|
||||
} else {
|
||||
INStartVideoCallIntent *startCallIntent
|
||||
= (INStartVideoCallIntent *)intent;
|
||||
handle = startCallIntent.contacts.firstObject.personHandle.value;
|
||||
}
|
||||
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
|
||||
contacts = ((INStartAudioCallIntent *) intent).contacts;
|
||||
startAudioOnly = YES;
|
||||
} else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
|
||||
contacts = ((INStartVideoCallIntent *) intent).contacts;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
// Load the URL contained in the handle
|
||||
[view loadURLObject:@{
|
||||
@"url": handle,
|
||||
@"configOverwrite": @{
|
||||
@"startAudioOnly": @(isAudio)
|
||||
}
|
||||
}];
|
||||
if (contacts && (url = contacts.firstObject.personHandle.value)) {
|
||||
// Load the URL contained in the handle.
|
||||
[self loadURLObjectInViews:@{
|
||||
@"config": @{
|
||||
@"startAudioOnly": @(startAudioOnly)
|
||||
},
|
||||
@"url": url
|
||||
}];
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return [RCTLinkingManager application:application
|
||||
continueUserActivity:userActivity
|
||||
|
@ -212,7 +196,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
|||
// expected in our testing. But that was to be expected because the app does
|
||||
// not exercise loadURL. In order to increase the test coverage of loadURL,
|
||||
// channel Universal linking through loadURL.
|
||||
if ([JitsiMeetView loadURLInViews:url]) {
|
||||
if ([self loadURLInViews:url]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -341,15 +325,20 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
|||
* at least one {@code JitsiMeetView}; otherwise, {@code NO}.
|
||||
*/
|
||||
+ (BOOL)loadURLInViews:(NSURL *)url {
|
||||
return
|
||||
[self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil];
|
||||
}
|
||||
|
||||
+ (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject {
|
||||
BOOL handled = NO;
|
||||
|
||||
if (views) {
|
||||
for (NSString *externalAPIScope in views) {
|
||||
JitsiMeetView *view
|
||||
= [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
|
||||
= [self viewForExternalAPIScope:externalAPIScope];
|
||||
|
||||
if (view) {
|
||||
[view loadURL:url];
|
||||
[view loadURLObject:urlObject];
|
||||
handled = YES;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { toState } from '../redux';
|
||||
|
||||
/**
|
||||
* Attach a set of local tracks to a conference.
|
||||
*
|
||||
* NOTE The function is internal to this feature.
|
||||
*
|
||||
* @param {JitsiConference} conference - Conference instance.
|
||||
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
|
||||
* @protected
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function _addLocalTracksToConference(conference, localTracks) {
|
||||
|
@ -29,14 +29,33 @@ export function _addLocalTracksToConference(conference, localTracks) {
|
|||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@code JitsiConference} which is joining or joined and is
|
||||
* not leaving. Please note the contrast with merely reading the
|
||||
* {@code conference} state of the feature base/conference which is not joining
|
||||
* but may be leaving already.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {JitsiConference|undefined}
|
||||
*/
|
||||
export function getCurrentConference(stateful) {
|
||||
const { conference, joining, leaving }
|
||||
= toState(stateful)['features/base/conference'];
|
||||
|
||||
return (
|
||||
conference
|
||||
? conference === leaving ? undefined : conference
|
||||
: joining);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
|
||||
* manipulating a conference participant (e.g. pin or select participant).
|
||||
*
|
||||
* NOTE The function is internal to this feature.
|
||||
*
|
||||
* @param {Error} err - The Error which was thrown by the backend while
|
||||
* manipulating a conference participant and which is to be handled.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
export function _handleParticipantError(err) {
|
||||
|
@ -65,10 +84,9 @@ export function isRoomValid(room) {
|
|||
/**
|
||||
* Remove a set of local tracks from a conference.
|
||||
*
|
||||
* NOTE The function is internal to this feature.
|
||||
*
|
||||
* @param {JitsiConference} conference - Conference instance.
|
||||
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
|
||||
* @protected
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function _removeLocalTracksFromConference(conference, localTracks) {
|
||||
|
@ -93,8 +111,6 @@ export function _removeLocalTracksFromConference(conference, localTracks) {
|
|||
* time of this writing, the intention of the function is to abstract the
|
||||
* reporting of errors and facilitate elaborating on it in the future.
|
||||
*
|
||||
* NOTE The function is internal to this feature.
|
||||
*
|
||||
* @param {string} msg - The error message to report.
|
||||
* @param {Error} err - The Error to report.
|
||||
* @private
|
||||
|
|
|
@ -16,9 +16,7 @@ import {
|
|||
SET_RECEIVE_VIDEO_QUALITY,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import {
|
||||
VIDEO_QUALITY_LEVELS
|
||||
} from './constants';
|
||||
import { VIDEO_QUALITY_LEVELS } from './constants';
|
||||
import { isRoomValid } from './functions';
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,44 +1,50 @@
|
|||
/* @flow */
|
||||
|
||||
import { toState } from '../redux';
|
||||
|
||||
import { VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
|
||||
/**
|
||||
* Determines whether video is currently muted by the audio-only authority.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Function|Object} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoMutedByAudioOnly(store: { getState: Function }) {
|
||||
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY);
|
||||
export function isVideoMutedByAudioOnly(stateful: Function | Object) {
|
||||
return (
|
||||
_isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether video is currently muted by a specific
|
||||
* <tt>VIDEO_MUTISM_AUTHORITY</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Function|Object} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @param {number} videoMutismAuthority - The <tt>VIDEO_MUTISM_AUTHORITY</tt>
|
||||
* which is to be checked whether it has muted video.
|
||||
* @returns {boolean} If video is currently muted by the specified
|
||||
* <tt>videoMutismAuthority</tt>, then <tt>true</tt>; otherwise, <tt>false</tt>.
|
||||
*/
|
||||
function _isVideoMutedByAuthority(
|
||||
{ getState }: { getState: Function },
|
||||
stateful: Function | Object,
|
||||
videoMutismAuthority: number) {
|
||||
return Boolean(
|
||||
const { muted } = toState(stateful)['features/base/media'].video;
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
getState()['features/base/media'].video.muted & videoMutismAuthority);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return Boolean(muted & videoMutismAuthority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether video is currently muted by the user authority.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Function|Object} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVideoMutedByUser(store: { getState: Function }) {
|
||||
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.USER);
|
||||
export function isVideoMutedByUser(stateful: Function | Object) {
|
||||
return _isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.USER);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
NativeModules,
|
||||
NativeEventEmitter,
|
||||
Platform
|
||||
} from 'react-native';
|
||||
|
||||
const RNCallKit = NativeModules.RNCallKit;
|
||||
import { NativeModules, NativeEventEmitter } from 'react-native';
|
||||
|
||||
/**
|
||||
* Thin wrapper around Apple's CallKit functionality.
|
||||
|
@ -18,218 +12,28 @@ const RNCallKit = NativeModules.RNCallKit;
|
|||
* the "endCall" method in this class, for example.
|
||||
*
|
||||
* Emitted events:
|
||||
* - performAnswerCallAction: The user pressed the answer button.
|
||||
* - performEndCallAction: The call should be ended.
|
||||
* - performSetMutedCallAction: The call muted state should change. The
|
||||
* ancillary `data` object contains a `muted` attribute.
|
||||
* - providerDidReset: The system has reset, all calls should be terminated.
|
||||
* This event gets no associated data.
|
||||
* - performAnswerCallAction: The user pressed the answer button.
|
||||
* - performEndCallAction: The call should be ended.
|
||||
* - performSetMutedCallAction: The call muted state should change. The
|
||||
* ancillary `data` object contains a `muted` attribute.
|
||||
* - providerDidReset: The system has reset, all calls should be terminated.
|
||||
* This event gets no associated data.
|
||||
*
|
||||
* All events get a `data` object with a `callUUID` property, unless stated
|
||||
* otherwise.
|
||||
*/
|
||||
class CallKit extends NativeEventEmitter {
|
||||
/**
|
||||
* Initializes a new {@code CallKit} instance.
|
||||
*/
|
||||
constructor() {
|
||||
super(RNCallKit);
|
||||
this._setup = false;
|
||||
}
|
||||
let CallKit = NativeModules.RNCallKit;
|
||||
|
||||
/**
|
||||
* Returns True if the current platform is supported, false otherwise. The
|
||||
* supported platforms are: iOS >= 10.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isSupported() {
|
||||
return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
|
||||
}
|
||||
// XXX Rather than wrapping RNCallKit in a new class and forwarding the many
|
||||
// methods of the latter to the former, add the one additional method that we
|
||||
// need to RNCallKit.
|
||||
if (CallKit) {
|
||||
const eventEmitter = new NativeEventEmitter(CallKit);
|
||||
|
||||
/**
|
||||
* Checks if CallKit was setup, and throws an exception in that case.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_checkSetup() {
|
||||
if (!this._setup) {
|
||||
throw new Error('CallKit not initialized, call setup() first.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the given event.
|
||||
*
|
||||
* @param {string} event - Name of the event we are interested in.
|
||||
* @param {Function} listener - Function which will be called when the
|
||||
* desired event is emitted.
|
||||
* @returns {void}
|
||||
*/
|
||||
addEventListener(event, listener) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addListener(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies CallKit about an incoming call. This will display the system
|
||||
* incoming call view.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @param {string} handle - Call handle in CallKit's terms. The room URL.
|
||||
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
displayIncomingCall(uuid, handle, hasVideo = true) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request CallKit to end the call.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
endCall(uuid) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.endCall(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener for the given event.
|
||||
*
|
||||
* @param {string} event - Name of the event we are no longer interested in.
|
||||
* @param {Function} listener - Function which used to be called when the
|
||||
* desired event was emitted.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeEventListener(event, listener) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeListener(event, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate CallKit that the outgoing call with the given UUID is now
|
||||
* connected.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
reportConnectedOutgoingCall(uuid) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.reportConnectedOutgoingCall(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate CallKit that the call with the given UUID has failed.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
reportCallFailed(uuid) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.reportCallFailed(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell CallKit about the audio muted state.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @param {boolean} muted - True if audio is muted, false otherwise.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setMuted(uuid, muted) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.setMuted(uuid, muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare / initialize CallKit. This method must be called before any
|
||||
* other.
|
||||
*
|
||||
* @param {Object} options - Initialization options.
|
||||
* @param {string} options.imageName - Image to be used in CallKit's
|
||||
* application button..
|
||||
* @param {string} options.ringtoneSound - Ringtone to be used for incoming
|
||||
* calls.
|
||||
* @returns {void}
|
||||
*/
|
||||
setup(options = {}) {
|
||||
if (CallKit.isSupported()) {
|
||||
options.appName = NativeModules.AppInfo.name;
|
||||
RNCallKit.setup(options);
|
||||
}
|
||||
|
||||
this._setup = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate CallKit about a new outgoing call.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @param {string} handle - Call handle in CallKit's terms. The room URL in
|
||||
* our case.
|
||||
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
startCall(uuid, handle, hasVideo = true) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.startCall(uuid, handle, hasVideo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an ongoing call's parameters.
|
||||
*
|
||||
* @param {string} uuid - Unique identifier for the call.
|
||||
* @param {Object} options - Object with properties which should be updated.
|
||||
* @param {string} options.displayName - Display name for the caller.
|
||||
* @param {boolean} options.hasVideo - True if the call has video, false
|
||||
* otherwise.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateCall(uuid, options) {
|
||||
this._checkSetup();
|
||||
if (!CallKit.isSupported()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return RNCallKit.updateCall(uuid, options);
|
||||
}
|
||||
CallKit = {
|
||||
...CallKit,
|
||||
addListener: eventEmitter.addListener.bind(eventEmitter)
|
||||
};
|
||||
}
|
||||
|
||||
export default new CallKit();
|
||||
export default CallKit;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* The type of redux action to set the CallKit event listeners.
|
||||
* The type of redux action to set CallKit's event subscriptions.
|
||||
*
|
||||
* {
|
||||
* type: _SET_CALLKIT_LISTENERS,
|
||||
* listeners: Map|null
|
||||
* type: _SET_CALLKIT_SUBSCRIPTIONS,
|
||||
* subscriptions: Array|undefined
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_CALLKIT_LISTENERS = Symbol('_SET_CALLKIT_LISTENERS');
|
||||
export const _SET_CALLKIT_SUBSCRIPTIONS = Symbol('_SET_CALLKIT_SUBSCRIPTIONS');
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
/* @flow */
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import {
|
||||
APP_WILL_MOUNT,
|
||||
APP_WILL_UNMOUNT,
|
||||
appNavigate
|
||||
} from '../../app';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_JOINED
|
||||
CONFERENCE_JOINED,
|
||||
getCurrentConference
|
||||
} from '../../base/conference';
|
||||
import { getInviteURL } from '../../base/connection';
|
||||
import {
|
||||
isVideoMutedByAudioOnly,
|
||||
SET_AUDIO_MUTED,
|
||||
SET_VIDEO_MUTED,
|
||||
isVideoMutedByAudioOnly,
|
||||
setAudioMuted
|
||||
} from '../../base/media';
|
||||
import { MiddlewareRegistry, toState } from '../../base/redux';
|
||||
import { _SET_CALLKIT_LISTENERS } from './actionTypes';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
|
||||
import CallKit from './CallKit';
|
||||
|
||||
/**
|
||||
|
@ -30,165 +29,319 @@ import CallKit from './CallKit';
|
|||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
CallKit && MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case _SET_CALLKIT_LISTENERS: {
|
||||
const { listeners } = getState()['features/callkit'];
|
||||
case _SET_CALLKIT_SUBSCRIPTIONS:
|
||||
return _setCallKitSubscriptions(store, next, action);
|
||||
|
||||
if (listeners) {
|
||||
for (const [ event, listener ] of listeners) {
|
||||
CallKit.removeEventListener(event, listener);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.listeners) {
|
||||
for (const [ event, listener ] of action.listeners) {
|
||||
CallKit.addEventListener(event, listener);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case APP_WILL_MOUNT: {
|
||||
CallKit.setup(); // TODO: set app icon.
|
||||
const listeners = new Map();
|
||||
const callEndListener = data => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID === data.callUUID) {
|
||||
// We arrive here when a call is ended by the system, for
|
||||
// for example when another incoming call is received and the
|
||||
// user selects "End & Accept".
|
||||
delete conference.callUUID;
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
};
|
||||
|
||||
listeners.set('performEndCallAction', callEndListener);
|
||||
|
||||
// Set the same listener for providerDidReset. According to the docs,
|
||||
// when the system resets we should terminate all calls.
|
||||
listeners.set('providerDidReset', callEndListener);
|
||||
|
||||
const setMutedListener = data => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID === data.callUUID) {
|
||||
// Break the loop. Audio can be muted both from the CallKit
|
||||
// interface and from the Jitsi Meet interface. We must keep
|
||||
// them in sync, but at some point the loop needs to be broken.
|
||||
// We are doing it here, on the CallKit handler.
|
||||
const { muted } = getState()['features/base/media'].audio;
|
||||
|
||||
if (muted !== data.muted) {
|
||||
dispatch(setAudioMuted(Boolean(data.muted)));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
listeners.set('performSetMutedCallAction', setMutedListener);
|
||||
|
||||
dispatch({
|
||||
type: _SET_CALLKIT_LISTENERS,
|
||||
listeners
|
||||
});
|
||||
break;
|
||||
}
|
||||
case APP_WILL_MOUNT:
|
||||
return _appWillMount(store, next, action);
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
dispatch({
|
||||
type: _SET_CALLKIT_LISTENERS,
|
||||
listeners: null
|
||||
store.dispatch({
|
||||
type: _SET_CALLKIT_SUBSCRIPTIONS,
|
||||
subscriptions: undefined
|
||||
});
|
||||
break;
|
||||
|
||||
case CONFERENCE_FAILED: {
|
||||
const { callUUID } = action.conference;
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
|
||||
break;
|
||||
case CONFERENCE_LEFT:
|
||||
return _conferenceLeft(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
return _conferenceWillJoin(store, next, action);
|
||||
|
||||
case SET_AUDIO_MUTED:
|
||||
return _setAudioMuted(store, next, action);
|
||||
|
||||
case SET_VIDEO_MUTED:
|
||||
return _setVideoMuted(store, next, action);
|
||||
}
|
||||
|
||||
case CONFERENCE_LEFT: {
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.endCall(callUUID);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportConnectedOutgoingCall(callUUID);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_WILL_JOIN: {
|
||||
const conference = action.conference;
|
||||
const url = getInviteURL(getState);
|
||||
const hasVideo = !isVideoMutedByAudioOnly({ getState });
|
||||
|
||||
// When assigning the call UUID, do so in upper case, since iOS will
|
||||
// return it upper cased.
|
||||
conference.callUUID = uuid.v4().toUpperCase();
|
||||
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
|
||||
.then(() => {
|
||||
const { room } = getState()['features/base/conference'];
|
||||
|
||||
CallKit.updateCall(conference.callUUID, { displayName: room });
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_MUTED: {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
CallKit.setMuted(conference.callUUID, action.muted);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED: {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
const hasVideo = !isVideoMutedByAudioOnly({ getState });
|
||||
|
||||
CallKit.updateCall(conference.callUUID, { hasVideo });
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the currently active conference.
|
||||
* Notifies the feature jwt that the action {@link APP_WILL_MOUNT} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Function|Object} stateOrGetState - The redux state or redux's
|
||||
* {@code getState} function.
|
||||
* @returns {Conference|undefined}
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>APP_WILL_MOUNT</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function getCurrentConference(stateOrGetState: Function | Object): ?Object {
|
||||
const state = toState(stateOrGetState);
|
||||
const { conference, joining } = state['features/base/conference'];
|
||||
function _appWillMount({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
return conference || joining;
|
||||
CallKit.setProviderConfiguration({
|
||||
iconTemplateImageName: 'AppIcon40x40',
|
||||
localizedName: NativeModules.AppInfo.name
|
||||
});
|
||||
|
||||
const context = {
|
||||
dispatch,
|
||||
getState
|
||||
};
|
||||
const subscriptions = [
|
||||
CallKit.addListener(
|
||||
'performEndCallAction',
|
||||
_onPerformEndCallAction,
|
||||
context),
|
||||
CallKit.addListener(
|
||||
'performSetMutedCallAction',
|
||||
_onPerformSetMutedCallAction,
|
||||
context),
|
||||
|
||||
// According to CallKit's documentation, when the system resets we
|
||||
// should terminate all calls. Hence, providerDidReset is the same
|
||||
// to us as performEndCallAction.
|
||||
CallKit.addListener(
|
||||
'providerDidReset',
|
||||
_onPerformEndCallAction,
|
||||
context)
|
||||
];
|
||||
|
||||
dispatch({
|
||||
type: _SET_CALLKIT_SUBSCRIPTIONS,
|
||||
subscriptions
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link CONFERENCE_FAILED} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>CONFERENCE_FAILED</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _conferenceFailed(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportCallFailed(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link CONFERENCE_JOINED} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>CONFERENCE_JOINED</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _conferenceJoined(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.reportConnectedOutgoingCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link CONFERENCE_LEFT} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>CONFERENCE_LEFT</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _conferenceLeft(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { callUUID } = action.conference;
|
||||
|
||||
if (callUUID) {
|
||||
CallKit.endCall(callUUID);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link CONFERENCE_WILL_JOIN} is
|
||||
* being dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>CONFERENCE_WILL_JOIN</tt> which
|
||||
* is being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _conferenceWillJoin({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const conference = action.conference;
|
||||
const url = getInviteURL(getState);
|
||||
const hasVideo = !isVideoMutedByAudioOnly(getState);
|
||||
|
||||
// When assigning the call UUID, do so in upper case, since iOS will
|
||||
// return it upper cased.
|
||||
conference.callUUID = uuid.v4().toUpperCase();
|
||||
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
|
||||
.then(() => {
|
||||
const { room } = getState()['features/base/conference'];
|
||||
|
||||
CallKit.updateCall(conference.callUUID, { displayName: room });
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CallKit's event <tt>performEndCallAction</tt>.
|
||||
*
|
||||
* @param {Object} event - The details of the CallKit event
|
||||
* <tt>performEndCallAction</tt>.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformEndCallAction({ callUUID }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
// We arrive here when a call is ended by the system, for
|
||||
// example when another incoming call is received and the user
|
||||
// selects "End & Accept".
|
||||
delete conference.callUUID;
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CallKit's event <tt>performSetMutedCallAction</tt>.
|
||||
*
|
||||
* @param {Object} event - The details of the CallKit event
|
||||
* <tt>performSetMutedCallAction</tt>.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onPerformSetMutedCallAction({ callUUID, muted: newValue }) {
|
||||
const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID === callUUID) {
|
||||
// Break the loop. Audio can be muted from both CallKit and Jitsi Meet.
|
||||
// We must keep them in sync, but at some point the loop needs to be
|
||||
// broken. We are doing it here, on the CallKit handler.
|
||||
const { muted: oldValue } = getState()['features/base/media'].audio;
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
dispatch(setAudioMuted(Boolean(newValue)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link SET_AUDIO_MUTED} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>SET_AUDIO_MUTED</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _setAudioMuted({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
CallKit.setMuted(conference.callUUID, action.muted);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link _SET_CALLKIT_SUBSCRIPTIONS}
|
||||
* is being dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>_SET_CALLKIT_SUBSCRIPTIONS</tt>
|
||||
* which is being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _setCallKitSubscriptions({ getState }, next, action) {
|
||||
const { subscriptions } = getState()['features/callkit'];
|
||||
|
||||
if (subscriptions) {
|
||||
for (const subscription of subscriptions) {
|
||||
subscription.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature jwt that the action {@link SET_VIDEO_MUTED} is being
|
||||
* dispatched within a specific redux <tt>store</tt>.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified <tt>action</tt>
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified <tt>action</tt> to the specified <tt>store</tt>.
|
||||
* @param {Action} action - The redux action <tt>SET_VIDEO_MUTED</tt> which is
|
||||
* being dispatched in the specified <tt>store</tt>.
|
||||
* @private
|
||||
* @returns {*}
|
||||
*/
|
||||
function _setVideoMuted({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
if (conference && conference.callUUID) {
|
||||
CallKit.updateCall(
|
||||
conference.callUUID,
|
||||
{ hasVideo: !isVideoMutedByAudioOnly(getState) });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import { ReducerRegistry } from '../../base/redux';
|
||||
import { assign, ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import {
|
||||
_SET_CALLKIT_LISTENERS
|
||||
} from './actionTypes';
|
||||
import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
|
||||
import CallKit from './CallKit';
|
||||
|
||||
ReducerRegistry.register('features/callkit', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case _SET_CALLKIT_LISTENERS:
|
||||
return {
|
||||
...state,
|
||||
listeners: action.listeners
|
||||
};
|
||||
}
|
||||
CallKit && ReducerRegistry.register(
|
||||
'features/callkit',
|
||||
(state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case _SET_CALLKIT_SUBSCRIPTIONS:
|
||||
return assign(state, 'subscriptions', action.subscriptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
return state;
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue