[iOS] Add initial CallKit support

This commit is contained in:
Lyubo Marinov 2017-09-28 16:25:04 -05:00
parent 8d11b3024e
commit 3b5ee2d4c6
12 changed files with 647 additions and 621 deletions

View File

@ -174,13 +174,13 @@ null and the Welcome page is enabled, the Welcome page is displayed instead.
Example: Example:
```java ```java
Bundle configOverwrite = new Bundle(); Bundle config = new Bundle();
configOverwrite.putBoolean("startWithAudioMuted", true); config.putBoolean("startWithAudioMuted", true);
configOverwrite.putBoolean("startWithVideoMuted", false); config.putBoolean("startWithVideoMuted", false);
Bundle urlBundle = new Bundle(); Bundle urlObject = new Bundle();
urlBundle.putBundle("configOverwrite", configOverwrite); urlObject.putBundle("config", config);
urlBundle.putString("url", "https://meet.jit.si/Test123"); urlObject.putString("url", "https://meet.jit.si/Test123");
view.loadURLObject(urlBundle); view.loadURLObject(urlObject);
``` ```
#### setDefaultURL(URL) #### setDefaultURL(URL)

View File

@ -76,11 +76,11 @@ instead.
```objc ```objc
[jitsiMeetView loadURLObject:@{ [jitsiMeetView loadURLObject:@{
@"url": @"https://meet.jit.si/test123", @"config": @{
@"configOverwrite": @{
@"startWithAudioMuted": @YES, @"startWithAudioMuted": @YES,
@"startWithVideoMuted": @NO @"startWithVideoMuted": @NO
} },
@"url": @"https://meet.jit.si/test123"
}]; }];
``` ```

View File

@ -22,36 +22,49 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <UIKit/UIKit.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 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. // The events emitted/supported by RNCallKit:
static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction"; static NSString * const RNCallKitPerformAnswerCallAction
static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction"; = @"performAnswerCallAction";
static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction"; static NSString * const RNCallKitPerformEndCallAction
static NSString *const RNCallKitProviderDidReset = @"providerDidReset"; = @"performEndCallAction";
static NSString * const RNCallKitPerformSetMutedCallAction
= @"performSetMutedCallAction";
static NSString * const RNCallKitProviderDidReset
= @"providerDidReset";
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate> @interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
@end @end
@implementation RNCallKit @implementation RNCallKit
{ {
CXCallController *callKitCallController; CXCallController *_callController;
CXProvider *callKitProvider; 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 @[ return @[
RNCallKitPerformAnswerCallAction, RNCallKitPerformAnswerCallAction,
RNCallKitPerformEndCallAction, 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 // Display the incoming call to the user
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID
handle:(NSString *)handle handle:(NSString *)handle
hasVideo:(BOOL)hasVideo hasVideo:(BOOL)hasVideo
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString); NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
#endif #endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle callUpdate.remoteHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
@ -95,149 +92,208 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
callUpdate.supportsGrouping = NO; callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO; callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = hasVideo; callUpdate.hasVideo = hasVideo;
[callKitProvider reportNewIncomingCallWithUUID:uuid [self.provider reportNewIncomingCallWithUUID:callUUID_
update:callUpdate update:callUpdate
completion:^(NSError * _Nullable error) { completion:^(NSError * _Nullable error) {
if (error == nil) { if (error) {
resolve(nil);
} else {
reject(nil, @"Error reporting new incoming call", error); reject(nil, @"Error reporting new incoming call", error);
} else {
resolve(nil);
} }
}]; }];
} }
// End call // End call
RCT_EXPORT_METHOD(endCall:(NSString *)uuidString RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString); NSLog(@"[RNCallKit][endCall] callUUID = %@", callUUID);
#endif #endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid]; NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action]; CXEndCallAction *action
[self requestTransaction:transaction resolve:resolve reject:reject]; = [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
resolve:resolve
reject:reject];
} }
// Mute / unmute (audio) // Mute / unmute (audio)
RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
muted:(BOOL) muted muted:(BOOL)muted
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString); NSLog(@"[RNCallKit][setMuted] callUUID = %@", callUUID);
#endif #endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXSetMutedCallAction *action CXSetMutedCallAction *action
= [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted]; = [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action]; [self requestTransaction:[[CXTransaction alloc] initWithAction:action]
[self requestTransaction:transaction resolve:resolve reject:reject]; 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 // Start outgoing call
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
handle:(NSString *)handle handle:(NSString *)handle
video:(BOOL)video video:(BOOL)video
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString); NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
#endif #endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXHandle *callHandle NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXHandle *handle_
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
CXStartCallAction *action CXStartCallAction *action
= [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; = [[CXStartCallAction alloc] initWithCallUUID:callUUID_
handle:handle_];
action.video = video; action.video = video;
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject]; [self requestTransaction:transaction resolve:resolve reject:reject];
} }
// Indicate call failed // Indicate call failed
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{ NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; [self.provider reportCallWithUUID:callUUID_
[callKitProvider reportCallWithUUID:uuid endedAtDate:[NSDate date]
endedAtDate:[NSDate date] reason:CXCallEndedReasonFailed];
reason:CXCallEndedReasonFailed];
resolve(nil); resolve(nil);
} }
// Indicate outgoing call connected // Indicate outgoing call connected.
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{ NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; [self.provider reportOutgoingCallWithUUID:callUUID_
[callKitProvider reportOutgoingCallWithUUID:uuid connectedAtDate:[NSDate date]];
connectedAtDate:[NSDate date]];
resolve(nil); resolve(nil);
} }
// Update call in case we have a display name or video capability changes // Update call in case we have a display name or video capability changes.
RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
options:(NSDictionary *)options options:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) reject:(RCTPromiseRejectBlock)reject) {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options); NSLog(
@"[RNCallKit][updateCall] callUUID = %@ options = %@",
callUUID,
options);
#endif #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"]) { if (options[@"displayName"]) {
update.localizedCallerName = options[@"displayName"]; callUpdate.localizedCallerName = options[@"displayName"];
} }
if (options[@"hasVideo"]) { 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); resolve(nil);
} }
#pragma mark - Helper methods #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 #ifdef DEBUG
NSLog(@"[RNCallKit][getProviderConfiguration]"); NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
#endif #endif
if (!dictionary) {
dictionary = @{};
}
// localizedName
NSString *localizedName = dictionary[@"localizedName"];
if (!localizedName) {
localizedName
= [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
}
CXProviderConfiguration *providerConfiguration CXProviderConfiguration *providerConfiguration
= [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]]; = [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
providerConfiguration.supportsVideo = YES;
// iconTemplateImageData
NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
if (iconTemplateImageName) {
UIImage *iconTemplateImage = [UIImage imageNamed:iconTemplateImageName];
if (iconTemplateImage) {
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation(iconTemplateImage);
}
}
providerConfiguration.maximumCallGroups = 1; providerConfiguration.maximumCallGroups = 1;
providerConfiguration.maximumCallsPerCallGroup = 1; providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"];
providerConfiguration.supportedHandleTypes providerConfiguration.supportedHandleTypes
= [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil]; = [NSSet setWithObjects:@(CXHandleTypeGeneric), nil];
if (settings[@"imageName"]) { providerConfiguration.supportsVideo = YES;
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
}
if (settings[@"ringtoneSound"]) {
providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
}
return providerConfiguration; return providerConfiguration;
} }
- (void)requestTransaction:(CXTransaction *)transaction - (void)requestTransaction:(CXTransaction *)transaction
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject reject:(RCTPromiseRejectBlock)reject {
{
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction); NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
#endif #endif
[callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
if (error == nil) { [self.callController requestTransaction:transaction
resolve(nil); completion:^(NSError * _Nullable error) {
} else { if (error) {
NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error); NSLog(
@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)",
transaction.actions,
error);
reject(nil, @"Error processing CallKit transaction", 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. // Called when the provider has been reset. We should terminate all calls.
- (void)providerDidReset:(CXProvider *)provider { - (void)providerDidReset:(CXProvider *)provider {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]"); NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
#endif #endif
[self sendEventWithName:RNCallKitProviderDidReset body:nil]; [self sendEventWithName:RNCallKitProviderDidReset body:nil];
} }
// Answering incoming call // Answering incoming call
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action - (void) provider:(CXProvider *)provider
{ performAnswerCallAction:(CXAnswerCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformAnswerCallAction [self sendEventWithName:RNCallKitPerformAnswerCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }]; body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill]; [action fulfill];
} }
// Call ended, user request // Call ended, user request
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action - (void) provider:(CXProvider *)provider
{ performEndCallAction:(CXEndCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformEndCallAction [self sendEventWithName:RNCallKitPerformEndCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }]; body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill]; [action fulfill];
} }
// Handle audio mute from CallKit view // Handle audio mute from CallKit view
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { - (void) provider:(CXProvider *)provider
performSetMutedCallAction:(CXSetMutedCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformSetMutedCallAction [self sendEventWithName:RNCallKitPerformSetMutedCallAction
body:@{ @"callUUID": action.callUUID.UUIDString, body:@{
@"muted": [NSNumber numberWithBool:action.muted]}]; @"callUUID": action.callUUID.UUIDString,
@"muted": @(action.muted)
}];
[action fulfill]; [action fulfill];
} }
// Starting outgoing call // Starting outgoing call
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action - (void) provider:(CXProvider *)provider
{ performStartCallAction:(CXStartCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
#endif #endif
[action fulfill]; [action fulfill];
// Update call info // Update call info.
NSUUID *callUUID = action.callUUID;
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle = action.handle; callUpdate.remoteHandle = action.handle;
callUpdate.supportsDTMF = NO; callUpdate.supportsDTMF = NO;
@ -301,34 +366,31 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
callUpdate.supportsGrouping = NO; callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO; callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = action.isVideo; callUpdate.hasVideo = action.isVideo;
[callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate]; [provider reportCallWithUUID:callUUID updated:callUpdate];
// Notify the system about the outgoing call // Notify the system about the outgoing call.
[callKitProvider reportOutgoingCallWithUUID:action.callUUID [provider reportOutgoingCallWithUUID:callUUID
startedConnectingAtDate:[NSDate date]]; startedConnectingAtDate:[NSDate date]];
} }
// These just help with debugging // The following just help with debugging:
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG #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 - (void) provider:(CXProvider *)provider
{ didDeactivateAudioSession:(AVAudioSession *)audioSession {
#ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
#endif
} }
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action - (void) provider:(CXProvider *)provider
{ timedOutPerformingAction:(CXAction *)action {
#ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
#endif
} }
#endif
@end @end

View File

@ -23,7 +23,7 @@
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate; @property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (copy, nonatomic) NSURL *defaultURL; @property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic) BOOL welcomePageEnabled; @property (nonatomic) BOOL welcomePageEnabled;

View File

@ -17,26 +17,15 @@
#import <CoreText/CoreText.h> #import <CoreText/CoreText.h>
#include <mach/mach_time.h> #include <mach/mach_time.h>
@import Intents;
#import <React/RCTAssert.h> #import <React/RCTAssert.h>
#import <React/RCTLinkingManager.h> #import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h> #import <React/RCTRootView.h>
#include <Availability.h>
#import <Foundation/Foundation.h>
#import "JitsiMeetView+Private.h" #import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.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. * A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
* In the Release configuration, React Native will (intentionally) raise an * In the Release configuration, React Native will (intentionally) raise an
@ -153,50 +142,45 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
continueUserActivity:(NSUserActivity *)userActivity continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{ {
NSString *activityType = userActivity.activityType;
// XXX At least twice we received bug reports about malfunctioning loadURL // 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 // 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 // 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, // not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL. // channel Universal linking through loadURL.
if ([userActivity.activityType if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]
isEqualToString:NSUserActivityTypeBrowsingWeb] && [self loadURLInViews:userActivity.webpageURL]) {
&& [JitsiMeetView loadURLInViews:userActivity.webpageURL]) {
return YES; return YES;
} }
// Check for CallKit intents only on iOS >= 10 // Check for a CallKit intent.
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) { if ([activityType isEqualToString:@"INStartAudioCallIntent"]
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"] || [activityType isEqualToString:@"INStartVideoCallIntent"]) {
|| [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) { INIntent *intent = userActivity.interaction.intent;
INInteraction *interaction = [userActivity interaction]; NSArray<INPerson *> *contacts;
INIntent *intent = interaction.intent; NSString *url;
NSString *handle; BOOL startAudioOnly = NO;
BOOL isAudio = NO;
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) { if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
INStartAudioCallIntent *startCallIntent contacts = ((INStartAudioCallIntent *) intent).contacts;
= (INStartAudioCallIntent *)intent; startAudioOnly = YES;
handle = startCallIntent.contacts.firstObject.personHandle.value; } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
isAudio = YES; contacts = ((INStartVideoCallIntent *) intent).contacts;
} else { }
INStartVideoCallIntent *startCallIntent
= (INStartVideoCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
}
if (handle) { if (contacts && (url = contacts.firstObject.personHandle.value)) {
// Load the URL contained in the handle // Load the URL contained in the handle.
[view loadURLObject:@{ [self loadURLObjectInViews:@{
@"url": handle, @"config": @{
@"configOverwrite": @{ @"startAudioOnly": @(startAudioOnly)
@"startAudioOnly": @(isAudio) },
} @"url": url
}]; }];
return YES; return YES;
} }
} }
}
return [RCTLinkingManager application:application return [RCTLinkingManager application:application
continueUserActivity:userActivity 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 // 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, // not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL. // channel Universal linking through loadURL.
if ([JitsiMeetView loadURLInViews:url]) { if ([self loadURLInViews:url]) {
return YES; return YES;
} }
@ -341,15 +325,20 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
* at least one {@code JitsiMeetView}; otherwise, {@code NO}. * at least one {@code JitsiMeetView}; otherwise, {@code NO}.
*/ */
+ (BOOL)loadURLInViews:(NSURL *)url { + (BOOL)loadURLInViews:(NSURL *)url {
return
[self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil];
}
+ (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject {
BOOL handled = NO; BOOL handled = NO;
if (views) { if (views) {
for (NSString *externalAPIScope in views) { for (NSString *externalAPIScope in views) {
JitsiMeetView *view JitsiMeetView *view
= [JitsiMeetView viewForExternalAPIScope:externalAPIScope]; = [self viewForExternalAPIScope:externalAPIScope];
if (view) { if (view) {
[view loadURL:url]; [view loadURLObject:urlObject];
handled = YES; handled = YES;
} }
} }

View File

@ -1,12 +1,12 @@
import { JitsiTrackErrors } from '../lib-jitsi-meet'; import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { toState } from '../redux';
/** /**
* Attach a set of local tracks to a conference. * Attach a set of local tracks to a conference.
* *
* NOTE The function is internal to this feature.
*
* @param {JitsiConference} conference - Conference instance. * @param {JitsiConference} conference - Conference instance.
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks. * @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
* @protected
* @returns {Promise} * @returns {Promise}
*/ */
export function _addLocalTracksToConference(conference, localTracks) { export function _addLocalTracksToConference(conference, localTracks) {
@ -29,14 +29,33 @@ export function _addLocalTracksToConference(conference, localTracks) {
return Promise.all(promises); 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 * Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
* manipulating a conference participant (e.g. pin or select participant). * 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 * @param {Error} err - The Error which was thrown by the backend while
* manipulating a conference participant and which is to be handled. * manipulating a conference participant and which is to be handled.
* @protected
* @returns {void} * @returns {void}
*/ */
export function _handleParticipantError(err) { export function _handleParticipantError(err) {
@ -65,10 +84,9 @@ export function isRoomValid(room) {
/** /**
* Remove a set of local tracks from a conference. * Remove a set of local tracks from a conference.
* *
* NOTE The function is internal to this feature.
*
* @param {JitsiConference} conference - Conference instance. * @param {JitsiConference} conference - Conference instance.
* @param {JitsiLocalTrack[]} localTracks - List of local media tracks. * @param {JitsiLocalTrack[]} localTracks - List of local media tracks.
* @protected
* @returns {Promise} * @returns {Promise}
*/ */
export function _removeLocalTracksFromConference(conference, localTracks) { 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 * time of this writing, the intention of the function is to abstract the
* reporting of errors and facilitate elaborating on it in the future. * 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 {string} msg - The error message to report.
* @param {Error} err - The Error to report. * @param {Error} err - The Error to report.
* @private * @private

View File

@ -16,9 +16,7 @@ import {
SET_RECEIVE_VIDEO_QUALITY, SET_RECEIVE_VIDEO_QUALITY,
SET_ROOM SET_ROOM
} from './actionTypes'; } from './actionTypes';
import { import { VIDEO_QUALITY_LEVELS } from './constants';
VIDEO_QUALITY_LEVELS
} from './constants';
import { isRoomValid } from './functions'; import { isRoomValid } from './functions';
/** /**

View File

@ -1,44 +1,50 @@
/* @flow */ /* @flow */
import { toState } from '../redux';
import { VIDEO_MUTISM_AUTHORITY } from './constants'; import { VIDEO_MUTISM_AUTHORITY } from './constants';
/** /**
* Determines whether video is currently muted by the audio-only authority. * 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} * @returns {boolean}
*/ */
export function isVideoMutedByAudioOnly(store: { getState: Function }) { export function isVideoMutedByAudioOnly(stateful: Function | Object) {
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY); return (
_isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
} }
/** /**
* Determines whether video is currently muted by a specific * Determines whether video is currently muted by a specific
* <tt>VIDEO_MUTISM_AUTHORITY</tt>. * <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> * @param {number} videoMutismAuthority - The <tt>VIDEO_MUTISM_AUTHORITY</tt>
* which is to be checked whether it has muted video. * which is to be checked whether it has muted video.
* @returns {boolean} If video is currently muted by the specified * @returns {boolean} If video is currently muted by the specified
* <tt>videoMutismAuthority</tt>, then <tt>true</tt>; otherwise, <tt>false</tt>. * <tt>videoMutismAuthority</tt>, then <tt>true</tt>; otherwise, <tt>false</tt>.
*/ */
function _isVideoMutedByAuthority( function _isVideoMutedByAuthority(
{ getState }: { getState: Function }, stateful: Function | Object,
videoMutismAuthority: number) { videoMutismAuthority: number) {
return Boolean( const { muted } = toState(stateful)['features/base/media'].video;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
getState()['features/base/media'].video.muted & videoMutismAuthority); return Boolean(muted & videoMutismAuthority);
} }
/** /**
* Determines whether video is currently muted by the user authority. * 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} * @returns {boolean}
*/ */
export function isVideoMutedByUser(store: { getState: Function }) { export function isVideoMutedByUser(stateful: Function | Object) {
return _isVideoMutedByAuthority(store, VIDEO_MUTISM_AUTHORITY.USER); return _isVideoMutedByAuthority(stateful, VIDEO_MUTISM_AUTHORITY.USER);
} }
/** /**

View File

@ -1,10 +1,4 @@
import { import { NativeModules, NativeEventEmitter } from 'react-native';
NativeModules,
NativeEventEmitter,
Platform
} from 'react-native';
const RNCallKit = NativeModules.RNCallKit;
/** /**
* Thin wrapper around Apple's CallKit functionality. * Thin wrapper around Apple's CallKit functionality.
@ -18,218 +12,28 @@ const RNCallKit = NativeModules.RNCallKit;
* the "endCall" method in this class, for example. * the "endCall" method in this class, for example.
* *
* Emitted events: * Emitted events:
* - performAnswerCallAction: The user pressed the answer button. * - performAnswerCallAction: The user pressed the answer button.
* - performEndCallAction: The call should be ended. * - performEndCallAction: The call should be ended.
* - performSetMutedCallAction: The call muted state should change. The * - performSetMutedCallAction: The call muted state should change. The
* ancillary `data` object contains a `muted` attribute. * ancillary `data` object contains a `muted` attribute.
* - providerDidReset: The system has reset, all calls should be terminated. * - providerDidReset: The system has reset, all calls should be terminated.
* This event gets no associated data. * This event gets no associated data.
* *
* All events get a `data` object with a `callUUID` property, unless stated * All events get a `data` object with a `callUUID` property, unless stated
* otherwise. * otherwise.
*/ */
class CallKit extends NativeEventEmitter { let CallKit = NativeModules.RNCallKit;
/**
* Initializes a new {@code CallKit} instance.
*/
constructor() {
super(RNCallKit);
this._setup = false;
}
/** // XXX Rather than wrapping RNCallKit in a new class and forwarding the many
* Returns True if the current platform is supported, false otherwise. The // methods of the latter to the former, add the one additional method that we
* supported platforms are: iOS >= 10. // need to RNCallKit.
* if (CallKit) {
* @private const eventEmitter = new NativeEventEmitter(CallKit);
* @returns {boolean}
*/
static isSupported() {
return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
}
/** CallKit = {
* Checks if CallKit was setup, and throws an exception in that case. ...CallKit,
* addListener: eventEmitter.addListener.bind(eventEmitter)
* @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);
}
} }
export default new CallKit(); export default CallKit;

View File

@ -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, * type: _SET_CALLKIT_SUBSCRIPTIONS,
* listeners: Map|null * subscriptions: Array|undefined
* } * }
* *
* @protected * @protected
*/ */
export const _SET_CALLKIT_LISTENERS = Symbol('_SET_CALLKIT_LISTENERS'); export const _SET_CALLKIT_SUBSCRIPTIONS = Symbol('_SET_CALLKIT_SUBSCRIPTIONS');

View File

@ -1,27 +1,26 @@
/* @flow */ /* @flow */
import { NativeModules } from 'react-native';
import uuid from 'uuid'; import uuid from 'uuid';
import { import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
APP_WILL_MOUNT,
APP_WILL_UNMOUNT,
appNavigate
} from '../../app';
import { import {
CONFERENCE_FAILED, CONFERENCE_FAILED,
CONFERENCE_LEFT, CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN, CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED CONFERENCE_JOINED,
getCurrentConference
} from '../../base/conference'; } from '../../base/conference';
import { getInviteURL } from '../../base/connection'; import { getInviteURL } from '../../base/connection';
import { import {
isVideoMutedByAudioOnly,
SET_AUDIO_MUTED, SET_AUDIO_MUTED,
SET_VIDEO_MUTED, SET_VIDEO_MUTED,
isVideoMutedByAudioOnly,
setAudioMuted setAudioMuted
} from '../../base/media'; } from '../../base/media';
import { MiddlewareRegistry, toState } from '../../base/redux'; import { MiddlewareRegistry } from '../../base/redux';
import { _SET_CALLKIT_LISTENERS } from './actionTypes';
import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
import CallKit from './CallKit'; import CallKit from './CallKit';
/** /**
@ -30,165 +29,319 @@ import CallKit from './CallKit';
* @param {Store} store - The redux store. * @param {Store} store - The redux store.
* @returns {Function} * @returns {Function}
*/ */
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { CallKit && MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) { switch (action.type) {
case _SET_CALLKIT_LISTENERS: { case _SET_CALLKIT_SUBSCRIPTIONS:
const { listeners } = getState()['features/callkit']; return _setCallKitSubscriptions(store, next, action);
if (listeners) { case APP_WILL_MOUNT:
for (const [ event, listener ] of listeners) { return _appWillMount(store, next, action);
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_UNMOUNT: case APP_WILL_UNMOUNT:
dispatch({ store.dispatch({
type: _SET_CALLKIT_LISTENERS, type: _SET_CALLKIT_SUBSCRIPTIONS,
listeners: null subscriptions: undefined
}); });
break; break;
case CONFERENCE_FAILED: { case CONFERENCE_FAILED:
const { callUUID } = action.conference; return _conferenceFailed(store, next, action);
if (callUUID) { case CONFERENCE_JOINED:
CallKit.reportCallFailed(callUUID); 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: { return next(action);
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;
}); });
/** /**
* 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 * @param {Store} store - The redux store in which the specified <tt>action</tt>
* {@code getState} function. * is being dispatched.
* @returns {Conference|undefined} * @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 { function _appWillMount({ dispatch, getState }, next, action) {
const state = toState(stateOrGetState); const result = next(action);
const { conference, joining } = state['features/base/conference'];
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;
} }

View File

@ -1,17 +1,15 @@
import { ReducerRegistry } from '../../base/redux'; import { assign, ReducerRegistry } from '../../base/redux';
import { import { _SET_CALLKIT_SUBSCRIPTIONS } from './actionTypes';
_SET_CALLKIT_LISTENERS import CallKit from './CallKit';
} from './actionTypes';
ReducerRegistry.register('features/callkit', (state = {}, action) => { CallKit && ReducerRegistry.register(
switch (action.type) { 'features/callkit',
case _SET_CALLKIT_LISTENERS: (state = {}, action) => {
return { switch (action.type) {
...state, case _SET_CALLKIT_SUBSCRIPTIONS:
listeners: action.listeners return assign(state, 'subscriptions', action.subscriptions);
}; }
}
return state; return state;
}); });