From 609b57c1e49133476f2e40b49488baa43210aba6 Mon Sep 17 00:00:00 2001 From: sauwming Date: Mon, 18 Mar 2024 12:08:11 +0800 Subject: [PATCH 1/3] Add Push Notification in ipjsua sample app --- .../ios/ipjsua.xcodeproj/project.pbxproj | 34 +++- .../src/pjsua/ios/ipjsua/ipjsua-Info.plist | 2 + .../src/pjsua/ios/ipjsua/ipjsua.entitlements | 8 + .../src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h | 7 +- .../src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m | 158 ++++++++++++++++-- pjsip/src/pjsua-lib/pjsua_acc.c | 3 +- 6 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements diff --git a/pjsip-apps/src/pjsua/ios/ipjsua.xcodeproj/project.pbxproj b/pjsip-apps/src/pjsua/ios/ipjsua.xcodeproj/project.pbxproj index 4fe446aef5..9a70996b7f 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua.xcodeproj/project.pbxproj +++ b/pjsip-apps/src/pjsua/ios/ipjsua.xcodeproj/project.pbxproj @@ -42,6 +42,9 @@ 3AF0582816F050780046B835 /* ipjsuaViewController_iPad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3AF0582616F050780046B835 /* ipjsuaViewController_iPad.xib */; }; 3AF253001EFBD15E00213893 /* libyuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF252FF1EFBD15E00213893 /* libyuv.a */; }; 3AF253021EFBD36E00213893 /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF253011EFBD36E00213893 /* VideoToolbox.framework */; }; + 3AF9B5422B8890880043987D /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF9B5412B8890880043987D /* PushKit.framework */; }; + 3AF9B5462B8890F40043987D /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF9B5442B8890F40043987D /* UserNotifications.framework */; }; + 3AF9B5482BA407AD0043987D /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF9B5472BA407AD0043987D /* CallKit.framework */; }; 7485A6AF1F09AAE500122F1A /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 7485A6AE1F09AAE500122F1A /* Reachability.m */; }; 7485A6B11F09B2D500122F1A /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7485A6B01F09B2D500122F1A /* SystemConfiguration.framework */; }; E5E991E61B67A45500017E67 /* libg7221codec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E991D41B67A45500017E67 /* libg7221codec.a */; }; @@ -105,6 +108,11 @@ 3AF0582716F050780046B835 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/ipjsuaViewController_iPad.xib; sourceTree = ""; }; 3AF252FF1EFBD15E00213893 /* libyuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libyuv.a; sourceTree = ""; }; 3AF253011EFBD36E00213893 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; + 3AF9B5402B88896D0043987D /* ipjsua.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ipjsua.entitlements; sourceTree = ""; }; + 3AF9B5412B8890880043987D /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = System/Library/Frameworks/PushKit.framework; sourceTree = SDKROOT; }; + 3AF9B5432B8890F40043987D /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + 3AF9B5442B8890F40043987D /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 3AF9B5472BA407AD0043987D /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; }; 7485A6AD1F09AAE500122F1A /* Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = ""; }; 7485A6AE1F09AAE500122F1A /* Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = ""; }; 7485A6B01F09B2D500122F1A /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; @@ -138,6 +146,7 @@ 3AF253021EFBD36E00213893 /* VideoToolbox.framework in Frameworks */, E5E991EC1B67A45500017E67 /* libpjmedia-codec.a in Frameworks */, 3AA31FF818F3FB4C00112C3D /* CFNetwork.framework in Frameworks */, + 3AF9B5422B8890880043987D /* PushKit.framework in Frameworks */, E5E991E61B67A45500017E67 /* libg7221codec.a in Frameworks */, E5E991EB1B67A45500017E67 /* libpjmedia-audiodev.a in Frameworks */, 3AA31FFB18F3FB4C00112C3D /* CoreImage.framework in Frameworks */, @@ -152,8 +161,10 @@ E5E991EE1B67A45500017E67 /* libpjmedia.a in Frameworks */, E5E991EA1B67A45500017E67 /* libpjlib-util.a in Frameworks */, E5E991ED1B67A45500017E67 /* libpjmedia-videodev.a in Frameworks */, + 3AF9B5462B8890F40043987D /* UserNotifications.framework in Frameworks */, E5E991E81B67A45500017E67 /* libilbccodec.a in Frameworks */, 3A4E3B5B2B6205BA0016735C /* Network.framework in Frameworks */, + 3AF9B5482BA407AD0043987D /* CallKit.framework in Frameworks */, 3AA3200118F3FB4C00112C3D /* CoreGraphics.framework in Frameworks */, 3AA31FF918F3FB4C00112C3D /* CoreAudio.framework in Frameworks */, 3AA31FFD18F3FB4C00112C3D /* CoreVideo.framework in Frameworks */, @@ -211,6 +222,10 @@ 3AF0580716F050770046B835 /* Frameworks */ = { isa = PBXGroup; children = ( + 3AF9B5472BA407AD0043987D /* CallKit.framework */, + 3AF9B5442B8890F40043987D /* UserNotifications.framework */, + 3AF9B5432B8890F40043987D /* UserNotificationsUI.framework */, + 3AF9B5412B8890880043987D /* PushKit.framework */, 3A4E3B5A2B6205B90016735C /* Network.framework */, 3A4E3B522B61FEAB0016735C /* Metal.framework */, 3A4E3B532B61FEAB0016735C /* MetalKit.framework */, @@ -237,6 +252,7 @@ 3AF0580E16F050770046B835 /* ipjsua */ = { isa = PBXGroup; children = ( + 3AF9B5402B88896D0043987D /* ipjsua.entitlements */, 3A92068D16F1D1A100D49F96 /* pjsua */, 3AF0581716F050780046B835 /* ipjsuaAppDelegate.h */, 3AF0581816F050780046B835 /* ipjsuaAppDelegate.m */, @@ -513,7 +529,9 @@ 3AF0582C16F050780046B835 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = ipjsua/ipjsua.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 93NHJQ455P; @@ -545,10 +563,11 @@ "$(PROJECT_DIR)", ); ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.teluu.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_BUNDLE_IDENTIFIER = "com.teluupush.--PRODUCT-NAME-rfc1034identifier-"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.teluupush.ipjsua; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Teluu Profile"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "ipjsua Push"; VALID_ARCHS = arm64; WRAPPER_EXTENSION = app; }; @@ -557,7 +576,9 @@ 3AF0582D16F050780046B835 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = ipjsua/ipjsua.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 93NHJQ455P; @@ -588,10 +609,11 @@ "$(PROJECT_DIR)", ); ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.teluu.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_BUNDLE_IDENTIFIER = "com.teluupush.--PRODUCT-NAME-rfc1034identifier-"; + "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.teluupush.ipjsua; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Teluu Profile"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "ipjsua Push"; VALID_ARCHS = arm64; WRAPPER_EXTENSION = app; }; diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua-Info.plist b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua-Info.plist index e4aa56d774..1b8f68bb45 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua-Info.plist +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua-Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion en + AppIdentifierPrefix + $(AppIdentifierPrefix) CFBundleDisplayName ${PRODUCT_NAME} CFBundleExecutable diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements new file mode 100644 index 0000000000..903def2af5 --- /dev/null +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h index 7f01d2759a..f4c5e63166 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h @@ -18,14 +18,19 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#import #import +#import @class ipjsuaViewController; -@interface ipjsuaAppDelegate : UIResponder +@interface ipjsuaAppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) PKPushRegistry *voipRegistry; +@property (strong, nonatomic) NSMutableString *token; + @property (strong, nonatomic) ipjsuaViewController *viewController; @end diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m index 1d56c82462..dec02b1fee 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m @@ -22,6 +22,7 @@ #import #import #import +#import #include "../../pjsua_app.h" #include "../../pjsua_app_config.h" @@ -33,6 +34,14 @@ @implementation ipjsuaAppDelegate #define THIS_FILE "ipjsuaAppDelegate.m" +/* Specify if we use push notification. */ +#define USE_PUSH_NOTIFICATION 1 + +/* Account details. */ +#define SIP_DOMAIN "sip.pjsip.org" +#define SIP_USER "test" +#define SIP_PASSWD "test" + #define KEEP_ALIVE_INTERVAL 600 ipjsuaAppDelegate *app; @@ -48,18 +57,18 @@ - (void) updateWithReachability: (Reachability *)curReach BOOL connectionRequired = [curReach connectionRequired]; switch (netStatus) { case NotReachable: - PJ_LOG(3,("", "Access Not Available..")); + NSLog(@"Access Not Available.."); connectionRequired= NO; break; case ReachableViaWiFi: - PJ_LOG(3,("", "Reachable WiFi..")); + NSLog(@"Reachable WiFi.."); break; case ReachableViaWWAN: - PJ_LOG(3,("", "Reachable WWAN..")); + NSLog(@"Reachable WWAN.."); break; } if (connectionRequired) { - PJ_LOG(3,("", "Connection Required")); + NSLog(@"Connection Required"); } } @@ -68,7 +77,7 @@ - (void)reachabilityChanged: (NSNotification *)note { Reachability* curReach = [note object]; NSParameterAssert([curReach isKindOfClass: [Reachability class]]); - PJ_LOG(3,("", "reachability changed..")); + NSLog(@"reachability changed.."); [self updateWithReachability: curReach]; if ([curReach currentReachabilityStatus] != NotReachable && @@ -76,10 +85,28 @@ - (void)reachabilityChanged: (NSNotification *)note { pjsua_ip_change_param param; pjsua_ip_change_param_default(¶m); - pjsua_handle_ip_change(¶m); +// pjsua_handle_ip_change(¶m); } } +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type +{ + if ([credentials.token length] == 0) { + NSLog(@"voip token NULL"); + return; + } + + /* Get device token */ + const char *data = [credentials.token bytes]; + self.token = [NSMutableString string]; + for (NSUInteger i = 0; i < [credentials.token length]; i++) { + [self.token appendFormat:@"%02.2hhx", data[i]]; + } + NSLog(@"== VOIP Push Notification Token: %@ ==", self.token); + + /* Now start pjsua */ + [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self withObject:nil]; +} void displayLog(const char *msg, int len) { @@ -171,6 +198,53 @@ - (void)pjsuaStart object:[UIDevice currentDevice]]; }); + static char contact_uri_buf[1024]; + pjsua_acc_config cfg; + + pjsua_acc_config_default(&cfg); + cfg.id = pj_str("sip:" SIP_USER "@" SIP_DOMAIN); + cfg.reg_uri = pj_str("sip:" SIP_DOMAIN ";transport=tcp"); + cfg.cred_count = 1; + cfg.cred_info[0].realm = pj_str(SIP_DOMAIN); + cfg.cred_info[0].scheme = pj_str("digest"); + cfg.cred_info[0].username = pj_str(SIP_USER); + cfg.cred_info[0].data = pj_str(SIP_PASSWD); + + /* If we have Push Notification token, put it in the registration + * Contact URI params. + */ + if ([self.token length]) { + /* According to RFC 8599: + * - pn-provider is the Push Notification Service provider. Here + * we use APNS (Apple Push Notification Service). + * - pn-param is composed of Team ID and Topic separated by + * a period (.). + * The Topic consists of the Bundle ID, the app's unique ID, + * and the app's service value ("voip" for VoIP apps), separated + * by a period (.). + * - pn-prid is the PN token. + */ + NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary]; + NSString *bundleID = infoDictionary[@"CFBundleIdentifier"]; + NSString *teamID = infoDictionary[@"AppIdentifierPrefix"]; + NSLog(@"Team ID from settings: %@", teamID); + + pj_ansi_snprintf(contact_uri_buf, sizeof(contact_uri_buf), + ";pn-provider=apns" + ";pn-param=%s%s.voip" + ";pn-prid=%s", + (teamID? [teamID UTF8String]: "93NHJQ455P."), + [bundleID UTF8String], + [self.token UTF8String]); + cfg.reg_contact_uri_params = pj_str(contact_uri_buf); + } + status = pjsua_acc_add(&cfg, PJ_TRUE, NULL); + if (status != PJ_SUCCESS) { + char errmsg[PJ_ERR_MSG_SIZE]; + pj_strerror(status, errmsg, sizeof(errmsg)); + displayMsg(errmsg); + } + status = pjsua_app_run(PJ_TRUE); if (status != PJ_SUCCESS) { char errmsg[PJ_ERR_MSG_SIZE]; @@ -187,6 +261,42 @@ - (void)pjsuaStart restartArgc = 0; } +- (void)reportIncomingCall { + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + + /* Report the incoming call to the system using CallKit. */ + CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] + initWithLocalizedName:@"ipjsua"]; + CXProvider *provider = [[CXProvider alloc] + initWithConfiguration:configuration]; + + [provider reportNewIncomingCallWithUUID:[NSUUID UUID] update:callUpdate + completion:^(NSError * _Nullable error) + { + if (error) { + NSLog(@"Error reporting incoming call: %@", + error.localizedDescription); + } else { + NSLog(@"Incoming call reported successfully"); + } + }]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry + didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion +{ + /* Handle incoming VoIP push notification. */ + NSLog(@"Receiving push notification"); + /* Re-register. */ + [self performSelectorOnMainThread:@selector(keepAlive) withObject:nil waitUntilDone:YES]; + /* Report the incoming call via CallKit. */ + [self reportIncomingCall]; + + /* Call the completion handler when you have finished processing the incoming call. */ + completion(); +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; @@ -198,7 +308,31 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( } self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; - + +#if USE_PUSH_NOTIFICATION + /* Set up a push notification registry for Voice over IP (VoIP). */ + self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; + self.voipRegistry.delegate = self; + self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; + + /* Request permission for push notifications. */ + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = self; + [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | + UNAuthorizationOptionBadge | + UNAuthorizationOptionSound) + completionHandler:^(BOOL granted, NSError * _Nullable error) + { + NSLog(@"Notification request %sgranted", (granted ? "" : "not")); + + if (granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + [application registerForRemoteNotifications]; + }); + } + }]; +#endif + /* Observe the kNetworkReachabilityChangedNotification. When that * notification is posted, the method "reachabilityChanged" will be called. */ @@ -212,8 +346,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( app = self; - /* Start pjsua app thread */ +#if !USE_PUSH_NOTIFICATION + /* Start pjsua app thread immediately, otherwise we do it after push + * notification setup completes. + */ [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self withObject:nil]; +#endif return YES; } @@ -275,10 +413,6 @@ - (void)keepAlive { pj_thread_register("ipjsua", a_thread_desc, &a_thread); } - /* Since iOS requires that the minimum keep alive interval is 600s, - * application needs to make sure that the account's registration - * timeout is long enough. - */ for (i = 0; i < (int)pjsua_acc_get_count(); ++i) { if (pjsua_acc_is_valid(i)) { pjsua_acc_set_registration(i, PJ_TRUE); diff --git a/pjsip/src/pjsua-lib/pjsua_acc.c b/pjsip/src/pjsua-lib/pjsua_acc.c index 40d10765a1..b9e5c6543f 100644 --- a/pjsip/src/pjsua-lib/pjsua_acc.c +++ b/pjsip/src/pjsua-lib/pjsua_acc.c @@ -1597,7 +1597,8 @@ static void update_regc_contact(pjsua_acc *acc) acc->cfg.reg_contact_params.slen + acc->cfg.reg_contact_uri_params.slen + (need_outbound? - (acc->rfc5626_instprm.slen + acc->rfc5626_regprm.slen): 0); + (acc->rfc5626_instprm.slen + acc->rfc5626_regprm.slen): 0) + + 5; /* allowance */ if (len > acc->contact.slen) { reg_contact.ptr = (char*) pj_pool_alloc(acc->pool, len); From 97b8118cf163f02254861494c13f4a750a5bc2c3 Mon Sep 17 00:00:00 2001 From: sauwming Date: Thu, 4 Apr 2024 14:13:11 +0800 Subject: [PATCH 2/3] Integrate CallKit to handle incoming call --- .../src/pjsua/ios/ipjsua/ipjsua.entitlements | 8 + .../src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h | 6 +- .../src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m | 407 +++++++++++++----- pjsip-apps/src/pjsua/pjsua_app.c | 5 + 4 files changed, 316 insertions(+), 110 deletions(-) diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements index 903def2af5..e3ddf78064 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements @@ -4,5 +4,13 @@ aps-environment development + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.network.client + diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h index f4c5e63166..fbf6c5e873 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h @@ -18,18 +18,22 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ +#import #import #import #import @class ipjsuaViewController; -@interface ipjsuaAppDelegate : UIResponder +@interface ipjsuaAppDelegate : UIResponder + @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) PKPushRegistry *voipRegistry; @property (strong, nonatomic) NSMutableString *token; +@property (strong, nonatomic) CXProvider *provider; @property (strong, nonatomic) ipjsuaViewController *viewController; diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m index dec02b1fee..3550451309 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m @@ -22,6 +22,7 @@ #import #import #import +#import #import #include "../../pjsua_app.h" @@ -37,19 +38,166 @@ @implementation ipjsuaAppDelegate /* Specify if we use push notification. */ #define USE_PUSH_NOTIFICATION 1 +/* Specify the timeout (in sec) to wait for the incoming INVITE + * to come after we receive push notification. + */ +#define MAX_INV_TIMEOUT 10 + /* Account details. */ #define SIP_DOMAIN "sip.pjsip.org" #define SIP_USER "test" #define SIP_PASSWD "test" -#define KEEP_ALIVE_INTERVAL 600 - ipjsuaAppDelegate *app; static pjsua_app_cfg_t app_cfg; static bool isShuttingDown; static char **restartArgv; static int restartArgc; -Reachability *internetReach; +Reachability *internetReach; + +static pjsua_call_id push_call_id; +static NSUUID *push_call_uuid; + +enum { + REREGISTER = 1, + ANSWER_CALL, + END_CALL, + ACTIVATE_AUDIO, + DEACTIVATE_AUDIO, + HANDLE_IP_CHANGE, + HANDLE_ORI_CHANGE +}; + +#define SCHEDULE_TIMER(action) \ +{ \ + static pj_thread_desc a_thread_desc; \ + static pj_thread_t *a_thread; \ + if (!pj_thread_is_registered()) { \ + pj_thread_register("ipjsua", a_thread_desc, &a_thread); \ + } \ + pjsua_schedule_timer2(pjsip_funcs, (void *)action, 0); \ +} + +static void pjsip_funcs(void *user_data) +{ + /* IMPORTANT: + * We need to call PJSIP API from a separate thread since + * PJSIP API can potentially block the main/GUI thread. + * And make sure we don't use Apple's Dispatch / gcd since + * it's incompatible with POSIX threads. + * In this example, we take advantage of PJSUA's timer thread + * to invoke PJSIP APIs. For a more complex application, + * it is recommended to create your own separate thread + * instead for this purpose. + */ + long code = (long)user_data; + if (code == REREGISTER) { + for (unsigned i = 0; i < pjsua_acc_get_count(); ++i) { + if (pjsua_acc_is_valid(i)) { + pjsua_acc_set_registration(i, PJ_TRUE); + } + } + } else if (code == ANSWER_CALL) { + pj_status_t status; + + if (push_call_id == PJSUA_INVALID_ID) return; + + status = pjsua_call_answer(push_call_id, PJSIP_SC_OK, NULL, NULL); + if (status != PJ_SUCCESS) { + [app.provider reportCallWithUUID:push_call_uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; + } + } else if (code == END_CALL) { + pj_status_t status; + pjsua_call_info info; + pjsua_call_id end_call_id; + + if (push_call_id == PJSUA_INVALID_ID) return; + + end_call_id = push_call_id; + push_call_id = PJSUA_INVALID_ID; + push_call_uuid = nil; + pjsua_call_get_info(end_call_id, &info); + if (info.state < PJSIP_INV_STATE_CONFIRMED) { + status = pjsua_call_answer(end_call_id, PJSIP_SC_DECLINE, NULL, NULL); + } else { + status = pjsua_call_hangup(end_call_id, PJSIP_SC_OK, NULL, NULL); + } + if (status != PJ_SUCCESS) { + NSLog(@"Failed ending the call %d", status); + } + } else if (code == ACTIVATE_AUDIO) { + pjsua_call_info call_info; + + if (push_call_id == PJSUA_INVALID_ID) return; + if (pjsua_snd_is_active()) return; + + /* If sound device is not yet active, we open it now and connect it + * to the call media. + */ + pjsua_set_snd_dev(PJSUA_SND_DEFAULT_CAPTURE_DEV, PJSUA_SND_DEFAULT_PLAYBACK_DEV); + + pjsua_call_get_info(push_call_id, &call_info); + + for (unsigned mi = 0; mi < call_info.media_cnt; ++mi) { + if (call_info.media[mi].type == PJMEDIA_TYPE_AUDIO && + (call_info.media[mi].status == PJSUA_CALL_MEDIA_ACTIVE || + call_info.media[mi].status == PJSUA_CALL_MEDIA_REMOTE_HOLD)) + { + pjsua_conf_port_id call_conf_slot; + call_conf_slot = call_info.media[mi].stream.aud.conf_slot; + pjsua_conf_connect(0, call_conf_slot); + pjsua_conf_connect(call_conf_slot, 0); + } + } + } else if (code == DEACTIVATE_AUDIO) { + NSLog(@"Deactivating audio session, is sound active: %d", + pjsua_snd_is_active()); + if (!pjsua_snd_is_active()) { + [[AVAudioSession sharedInstance] setActive:NO + withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation + error:nil]; + } + } else if (code == HANDLE_IP_CHANGE) { + pjsua_ip_change_param param; + pjsua_ip_change_param_default(¶m); + pjsua_handle_ip_change(¶m); + } else if (code == HANDLE_ORI_CHANGE) { +#if PJSUA_HAS_VIDEO + const pjmedia_orient pj_ori[4] = + { + PJMEDIA_ORIENT_ROTATE_90DEG, /* UIDeviceOrientationPortrait */ + PJMEDIA_ORIENT_ROTATE_270DEG, /* UIDeviceOrientationPortraitUpsideDown */ + PJMEDIA_ORIENT_ROTATE_180DEG, /* UIDeviceOrientationLandscapeLeft, + home button on the right side */ + PJMEDIA_ORIENT_NATURAL /* UIDeviceOrientationLandscapeRight, + home button on the left side */ + }; + static UIDeviceOrientation prev_ori = 0; + UIDeviceOrientation dev_ori = [[UIDevice currentDevice] orientation]; + int i; + + if (dev_ori == prev_ori) return; + + NSLog(@"Device orientation changed: %d", (int)(prev_ori = dev_ori)); + + if (dev_ori >= UIDeviceOrientationPortrait && + dev_ori <= UIDeviceOrientationLandscapeRight) + { + /* Here we set the orientation for all video devices. + * This may return failure for renderer devices or for + * capture devices which do not support orientation setting, + * we can simply ignore them. + */ + for (i = pjsua_vid_dev_count()-1; i >= 0; i--) { + pjsua_vid_dev_set_setting(i, PJMEDIA_VID_DEV_CAP_ORIENTATION, + &pj_ori[dev_ori-1], PJ_TRUE); + } + } +#endif + } +} - (void) updateWithReachability: (Reachability *)curReach { @@ -83,9 +231,7 @@ - (void)reachabilityChanged: (NSNotification *)note if ([curReach currentReachabilityStatus] != NotReachable && ![curReach connectionRequired]) { - pjsua_ip_change_param param; - pjsua_ip_change_param_default(¶m); -// pjsua_handle_ip_change(¶m); + SCHEDULE_TIMER(HANDLE_IP_CHANGE); } } @@ -102,7 +248,7 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush for (NSUInteger i = 0; i < [credentials.token length]; i++) { [self.token appendFormat:@"%02.2hhx", data[i]]; } - NSLog(@"== VOIP Push Notification Token: %@ ==", self.token); + NSLog(@"VOIP Push Notification Token: %@", self.token); /* Now start pjsua */ [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self withObject:nil]; @@ -179,6 +325,8 @@ - (void)pjsuaStart app_cfg.on_config_init = &pjsuaOnAppConfigCb; while (!isShuttingDown) { + push_call_id = PJSUA_INVALID_ID; + push_call_uuid = nil; status = pjsua_app_init(&app_cfg); if (status != PJ_SUCCESS) { char errmsg[PJ_ERR_MSG_SIZE]; @@ -209,6 +357,10 @@ - (void)pjsuaStart cfg.cred_info[0].scheme = pj_str("digest"); cfg.cred_info[0].username = pj_str(SIP_USER); cfg.cred_info[0].data = pj_str(SIP_PASSWD); + /* Uncomment this to enable video. Note that video can only + * work in the foreground. + */ + // app_config_init_video(&cfg); /* If we have Push Notification token, put it in the registration * Contact URI params. @@ -261,23 +413,84 @@ - (void)pjsuaStart restartArgc = 0; } +- (void) provider:(CXProvider *) provider +didActivateAudioSession:(AVAudioSession *) audioSession +{ + NSLog(@"Did activate Audio Session"); + + pjsua_schedule_timer2(pjsip_funcs, (void *)ACTIVATE_AUDIO, 0); +} + +- (void)provider:(CXProvider *)provider + performAnswerCallAction:(CXAnswerCallAction *)action +{ + NSLog(@"Perform answer call %d", push_call_id); + + /* User has answered the call, but we may need to wait for + * the incoming INVITE to come. + */ + for (int i = MAX_INV_TIMEOUT * 1000 / 100; i >= 0; i--) { + if (push_call_id != PJSUA_INVALID_ID) break; + [NSThread sleepForTimeInterval:0.1]; + } + if (push_call_id == PJSUA_INVALID_ID) { + [action fail]; + return; + } + + pjsua_schedule_timer2(pjsip_funcs, (void *)ANSWER_CALL, 0); + + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + performEndCallAction:(CXEndCallAction *)action +{ + NSLog(@"Perform end call %d", push_call_id); + + /* User has declined or ended the call. For call decline, we may need + * to wait for the incoming INVITE to come. + */ + for (int i = MAX_INV_TIMEOUT * 1000 / 100; i >= 0; i--) { + if (push_call_id != PJSUA_INVALID_ID) break; + [NSThread sleepForTimeInterval:0.1]; + } + if (push_call_id == PJSUA_INVALID_ID) { + [action fulfill]; + return; + } + + pjsua_schedule_timer2(pjsip_funcs, (void *)END_CALL, 0); + + [action fulfill]; +} + - (void)reportIncomingCall { - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + /* Activate audio session. */ + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + NSError *error = nil; + if (![audioSession setCategory:AVAudioSessionCategoryPlayAndRecord + mode:AVAudioSessionModeVoiceChat + options:0 error:&error]) + { + NSLog(@"Error setting up audio session: %@", error.localizedDescription); + return; + } + if (![audioSession setActive:YES error:&error]) { + NSLog(@"Error activating audio session: %@", error.localizedDescription); + return; + } /* Report the incoming call to the system using CallKit. */ - CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] - initWithLocalizedName:@"ipjsua"]; - CXProvider *provider = [[CXProvider alloc] - initWithConfiguration:configuration]; - - [provider reportNewIncomingCallWithUUID:[NSUUID UUID] update:callUpdate - completion:^(NSError * _Nullable error) + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + push_call_uuid = [NSUUID UUID]; + [self.provider reportNewIncomingCallWithUUID:push_call_uuid + update:callUpdate + completion:^(NSError * _Nullable error) { if (error) { NSLog(@"Error reporting incoming call: %@", error.localizedDescription); - } else { - NSLog(@"Incoming call reported successfully"); } }]; } @@ -288,8 +501,12 @@ - (void)pushRegistry:(PKPushRegistry *)registry { /* Handle incoming VoIP push notification. */ NSLog(@"Receiving push notification"); - /* Re-register. */ - [self performSelectorOnMainThread:@selector(keepAlive) withObject:nil waitUntilDone:YES]; + + /* Re-register, so the server will send us the suspended INVITE. */ + push_call_id = PJSUA_INVALID_ID; + push_call_uuid = nil; + SCHEDULE_TIMER(REREGISTER); + /* Report the incoming call via CallKit. */ [self reportIncomingCall]; @@ -331,6 +548,33 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( }); } }]; + + /* Note that opening audio device when in the background will not trigger + * permission request, so we won't be able to use audio device. + * Thus, we need to request permission now. + */ + [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) + { + NSLog(@"Microphone access %sgranted", (granted ? "" : "not")); + }]; + + /* We need to request local network access permission as well to + * immediately send media traffic when in the background. + * Due to its complexity, the code is not included here in the sample app. + * Please refer to the Apple "Local Network Privacy" FAQ for more details. + */ + + /* Create CallKit provider. */ + CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] + initWithLocalizedName:@"ipjsua"]; + self.provider = [[CXProvider alloc] initWithConfiguration:configuration]; + [self.provider setDelegate:self queue:nil]; +#else + /* Start pjsua app thread immediately, otherwise we do it after push + * notification setup completes. + */ + [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self + withObject:nil]; #endif /* Observe the kNetworkReachabilityChangedNotification. When that @@ -345,13 +589,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [self updateWithReachability: internetReach]; app = self; - -#if !USE_PUSH_NOTIFICATION - /* Start pjsua app thread immediately, otherwise we do it after push - * notification setup completes. - */ - [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self withObject:nil]; -#endif return YES; } @@ -364,74 +601,14 @@ - (void)applicationWillResignActive:(UIApplication *)application - (void)orientationChanged:(NSNotification *)note { -#if PJSUA_HAS_VIDEO - const pjmedia_orient pj_ori[4] = - { - PJMEDIA_ORIENT_ROTATE_90DEG, /* UIDeviceOrientationPortrait */ - PJMEDIA_ORIENT_ROTATE_270DEG, /* UIDeviceOrientationPortraitUpsideDown */ - PJMEDIA_ORIENT_ROTATE_180DEG, /* UIDeviceOrientationLandscapeLeft, - home button on the right side */ - PJMEDIA_ORIENT_NATURAL /* UIDeviceOrientationLandscapeRight, - home button on the left side */ - }; - static pj_thread_desc a_thread_desc; - static pj_thread_t *a_thread; - static UIDeviceOrientation prev_ori = 0; - UIDeviceOrientation dev_ori = [[UIDevice currentDevice] orientation]; - int i; - - if (dev_ori == prev_ori) return; - - NSLog(@"Device orientation changed: %d", (int)(prev_ori = dev_ori)); - - if (dev_ori >= UIDeviceOrientationPortrait && - dev_ori <= UIDeviceOrientationLandscapeRight) - { - if (!pj_thread_is_registered()) { - pj_thread_register("ipjsua", a_thread_desc, &a_thread); - } - - /* Here we set the orientation for all video devices. - * This may return failure for renderer devices or for - * capture devices which do not support orientation setting, - * we can simply ignore them. - */ - for (i = pjsua_vid_dev_count()-1; i >= 0; i--) { - pjsua_vid_dev_set_setting(i, PJMEDIA_VID_DEV_CAP_ORIENTATION, - &pj_ori[dev_ori-1], PJ_TRUE); - } - } -#endif -} - -- (void)keepAlive { - static pj_thread_desc a_thread_desc; - static pj_thread_t *a_thread; - int i; - - if (!pj_thread_is_registered()) { - pj_thread_register("ipjsua", a_thread_desc, &a_thread); - } - - for (i = 0; i < (int)pjsua_acc_get_count(); ++i) { - if (pjsua_acc_is_valid(i)) { - pjsua_acc_set_registration(i, PJ_TRUE); - } - } + SCHEDULE_TIMER(HANDLE_ORI_CHANGE); } - (void)applicationDidEnterBackground:(UIApplication *)application { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - [self performSelectorOnMainThread:@selector(keepAlive) withObject:nil waitUntilDone:YES]; - -#if 0 - /* setKeepAliveTimeout is deprecated. Use PushKit instead. */ - [application setKeepAliveTimeout:KEEP_ALIVE_INTERVAL handler: ^{ - [self performSelectorOnMainThread:@selector(keepAlive) withObject:nil waitUntilDone:YES]; - }]; -#endif + SCHEDULE_TIMER(REREGISTER); + /* Allow the re-registration to complete. */ + [NSThread sleepForTimeInterval:0.3]; } - (void)applicationWillEnterForeground:(UIApplication *)application @@ -450,28 +627,40 @@ - (void)applicationWillTerminate:(UIApplication *)application } +pj_bool_t reportCallState(pjsua_call_id call_id) +{ + pjsua_call_info call_info; + + pjsua_call_get_info(call_id, &call_info); + + if (call_info.state == PJSIP_INV_STATE_DISCONNECTED) { + if (call_id == push_call_id && push_call_uuid) { + [app.provider reportCallWithUUID:push_call_uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonRemoteEnded]; + push_call_id = PJSUA_INVALID_ID; + push_call_uuid = nil; + } + } + + /* Check if we need to deactivate audio session. Note that sound device + * will only be closed after pjsua_media_config.snd_auto_close_time. + */ + pjsua_schedule_timer2(pjsip_funcs, (void *)DEACTIVATE_AUDIO, 1500); + + return PJ_FALSE; +} + pj_bool_t showNotification(pjsua_call_id call_id) { - /* This is deprecated. Use VoIP Push Notifications with PushKit - * framework instead. +#if USE_PUSH_NOTIFICATION + NSLog(@"Receiving incoming call %d", call_id); + push_call_id = call_id; + /* If we report the incoming call using CallKit, we cannot open + * the sound device until the audio session becomes active. */ -#if 0 - // Create a new notification - UILocalNotification* alert = [[UILocalNotification alloc] init]; - if (alert) - { - alert.repeatInterval = 0; - alert.alertBody = @"Incoming call received..."; - /* This action just brings the app to the FG, it doesn't - * automatically answer the call (unless you specify the - * --auto-answer option). - */ - alert.alertAction = @"Activate app"; - - dispatch_async(dispatch_get_main_queue(), - ^{[[UIApplication sharedApplication] - presentLocalNotificationNow:alert];}); - } + if (!pjsua_snd_is_active()) + pjsua_set_no_snd_dev(); #endif return PJ_FALSE; diff --git a/pjsip-apps/src/pjsua/pjsua_app.c b/pjsip-apps/src/pjsua/pjsua_app.c index f072b39083..07976aefe3 100644 --- a/pjsip-apps/src/pjsua/pjsua_app.c +++ b/pjsip-apps/src/pjsua/pjsua_app.c @@ -47,6 +47,7 @@ static void stereo_demo(); #ifdef USE_GUI pj_bool_t showNotification(pjsua_call_id call_id); +pj_bool_t reportCallState(pjsua_call_id call_id); #endif static void ringback_start(pjsua_call_id call_id); @@ -171,6 +172,10 @@ static void on_call_state(pjsua_call_id call_id, pjsip_event *e) PJ_UNUSED_ARG(e); +#ifdef USE_GUI + reportCallState(call_id); +#endif + pjsua_call_get_info(call_id, &call_info); if (call_info.state == PJSIP_INV_STATE_DISCONNECTED) { From 9b8a6e21354567d90c0065d107b3f66f20a8dbf1 Mon Sep 17 00:00:00 2001 From: sauwming Date: Fri, 19 Apr 2024 15:17:19 +0800 Subject: [PATCH 3/3] Handle multiple incoming calls --- .../src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m | 223 ++++++++++-------- 1 file changed, 121 insertions(+), 102 deletions(-) diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m index 3550451309..8a6ef9dc32 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m @@ -55,8 +55,8 @@ @implementation ipjsuaAppDelegate static int restartArgc; Reachability *internetReach; -static pjsua_call_id push_call_id; -static NSUUID *push_call_uuid; +/* Mapping between CallKit UUID and pjsua_call_id. */ +NSMutableDictionary *call_map; enum { REREGISTER = 1, @@ -68,13 +68,16 @@ @implementation ipjsuaAppDelegate HANDLE_ORI_CHANGE }; -#define SCHEDULE_TIMER(action) \ -{ \ +#define REGISTER_THREAD \ static pj_thread_desc a_thread_desc; \ static pj_thread_t *a_thread; \ if (!pj_thread_is_registered()) { \ pj_thread_register("ipjsua", a_thread_desc, &a_thread); \ - } \ + } + +#define SCHEDULE_TIMER(action) \ +{ \ + REGISTER_THREAD \ pjsua_schedule_timer2(pjsip_funcs, (void *)action, 0); \ } @@ -90,7 +93,7 @@ static void pjsip_funcs(void *user_data) * it is recommended to create your own separate thread * instead for this purpose. */ - long code = (long)user_data; + long code = (long)user_data & 0xF; if (code == REREGISTER) { for (unsigned i = 0; i < pjsua_acc_get_count(); ++i) { if (pjsua_acc_is_valid(i)) { @@ -99,56 +102,56 @@ static void pjsip_funcs(void *user_data) } } else if (code == ANSWER_CALL) { pj_status_t status; + pjsua_call_id call_id = (pjsua_call_id)((long)user_data & 0xFF0) >> 4; - if (push_call_id == PJSUA_INVALID_ID) return; - - status = pjsua_call_answer(push_call_id, PJSIP_SC_OK, NULL, NULL); + status = pjsua_call_answer(call_id, PJSIP_SC_OK, NULL, NULL); if (status != PJ_SUCCESS) { - [app.provider reportCallWithUUID:push_call_uuid - endedAtDate:[NSDate date] - reason:CXCallEndedReasonFailed]; + NSUUID *uuid =(__bridge NSUUID *)pjsua_call_get_user_data(call_id); + if (uuid) { + [app.provider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; + } } } else if (code == END_CALL) { pj_status_t status; - pjsua_call_info info; - pjsua_call_id end_call_id; - - if (push_call_id == PJSUA_INVALID_ID) return; - - end_call_id = push_call_id; - push_call_id = PJSUA_INVALID_ID; - push_call_uuid = nil; - pjsua_call_get_info(end_call_id, &info); - if (info.state < PJSIP_INV_STATE_CONFIRMED) { - status = pjsua_call_answer(end_call_id, PJSIP_SC_DECLINE, NULL, NULL); - } else { - status = pjsua_call_hangup(end_call_id, PJSIP_SC_OK, NULL, NULL); - } + pjsua_call_id call_id = (pjsua_call_id)((long)user_data & 0xFF0) >> 4; + + status = pjsua_call_hangup(call_id, PJSIP_SC_OK, NULL, NULL); if (status != PJ_SUCCESS) { - NSLog(@"Failed ending the call %d", status); + NSUUID *uuid =(__bridge NSUUID *)pjsua_call_get_user_data(call_id); + if (uuid) { + [app.provider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; + } } } else if (code == ACTIVATE_AUDIO) { pjsua_call_info call_info; + pjsua_call_id ids[PJSUA_MAX_CALLS]; + unsigned count = PJSUA_MAX_CALLS; - if (push_call_id == PJSUA_INVALID_ID) return; - if (pjsua_snd_is_active()) return; - - /* If sound device is not yet active, we open it now and connect it - * to the call media. + /* If we use CallKit, sound device may not work until audio session + * becomes active, so we need to force reopen sound device here. */ - pjsua_set_snd_dev(PJSUA_SND_DEFAULT_CAPTURE_DEV, PJSUA_SND_DEFAULT_PLAYBACK_DEV); - - pjsua_call_get_info(push_call_id, &call_info); - - for (unsigned mi = 0; mi < call_info.media_cnt; ++mi) { - if (call_info.media[mi].type == PJMEDIA_TYPE_AUDIO && - (call_info.media[mi].status == PJSUA_CALL_MEDIA_ACTIVE || - call_info.media[mi].status == PJSUA_CALL_MEDIA_REMOTE_HOLD)) - { - pjsua_conf_port_id call_conf_slot; - call_conf_slot = call_info.media[mi].stream.aud.conf_slot; - pjsua_conf_connect(0, call_conf_slot); - pjsua_conf_connect(call_conf_slot, 0); + pjsua_set_no_snd_dev(); + pjsua_set_snd_dev(PJSUA_SND_DEFAULT_CAPTURE_DEV, + PJSUA_SND_DEFAULT_PLAYBACK_DEV); + + pjsua_enum_calls(ids, &count); + for (unsigned i = 0; i < count; i++) { + pjsua_call_get_info(i, &call_info); + + for (unsigned mi = 0; mi < call_info.media_cnt; ++mi) { + if (call_info.media[mi].type == PJMEDIA_TYPE_AUDIO && + (call_info.media[mi].status == PJSUA_CALL_MEDIA_ACTIVE || + call_info.media[mi].status == PJSUA_CALL_MEDIA_REMOTE_HOLD)) + { + pjsua_conf_port_id call_conf_slot; + call_conf_slot = call_info.media[mi].stream.aud.conf_slot; + pjsua_conf_connect(0, call_conf_slot); + pjsua_conf_connect(call_conf_slot, 0); + } } } } else if (code == DEACTIVATE_AUDIO) { @@ -325,8 +328,6 @@ - (void)pjsuaStart app_cfg.on_config_init = &pjsuaOnAppConfigCb; while (!isShuttingDown) { - push_call_id = PJSUA_INVALID_ID; - push_call_uuid = nil; status = pjsua_app_init(&app_cfg); if (status != PJ_SUCCESS) { char errmsg[PJ_ERR_MSG_SIZE]; @@ -421,51 +422,78 @@ - (void) provider:(CXProvider *) provider pjsua_schedule_timer2(pjsip_funcs, (void *)ACTIVATE_AUDIO, 0); } +- (void) provider:(CXProvider *) provider +didDectivateAudioSession:(AVAudioSession *) audioSession +{ + NSLog(@"Did deactivate Audio Session"); +} + - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { - NSLog(@"Perform answer call %d", push_call_id); + NSLog(@"Perform answer call %@", action.callUUID.UUIDString); /* User has answered the call, but we may need to wait for * the incoming INVITE to come. */ for (int i = MAX_INV_TIMEOUT * 1000 / 100; i >= 0; i--) { - if (push_call_id != PJSUA_INVALID_ID) break; + if (call_map[action.callUUID].intValue != PJSUA_INVALID_ID) break; [NSThread sleepForTimeInterval:0.1]; } - if (push_call_id == PJSUA_INVALID_ID) { - [action fail]; + + [action fulfill]; + + pjsua_call_id call_id = call_map[action.callUUID].intValue; + if (call_id == PJSUA_INVALID_ID) { + [app.provider reportCallWithUUID:action.callUUID + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; return; } - pjsua_schedule_timer2(pjsip_funcs, (void *)ANSWER_CALL, 0); - - [action fulfill]; + long code = (long)ANSWER_CALL | (call_id << 4); + pjsua_schedule_timer2(pjsip_funcs, (void *)code, 0); } - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { - NSLog(@"Perform end call %d", push_call_id); + NSLog(@"Perform end call %@", action.callUUID.UUIDString); - /* User has declined or ended the call. For call decline, we may need - * to wait for the incoming INVITE to come. + /* User has answered the call, but we may need to wait for + * the incoming INVITE to come. */ for (int i = MAX_INV_TIMEOUT * 1000 / 100; i >= 0; i--) { - if (push_call_id != PJSUA_INVALID_ID) break; + if (call_map[action.callUUID].intValue != PJSUA_INVALID_ID) break; [NSThread sleepForTimeInterval:0.1]; } - if (push_call_id == PJSUA_INVALID_ID) { - [action fulfill]; + + [action fulfill]; + + pjsua_call_id call_id = call_map[action.callUUID].intValue; + if (call_id == PJSUA_INVALID_ID) { + [app.provider reportCallWithUUID:action.callUUID + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; return; } - pjsua_schedule_timer2(pjsip_funcs, (void *)END_CALL, 0); - - [action fulfill]; + long code = (long)END_CALL | (call_id << 4); + pjsua_schedule_timer2(pjsip_funcs, (void *)code, 0); } -- (void)reportIncomingCall { +- (void)pushRegistry:(PKPushRegistry *)registry + didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion +{ + /* Handle incoming VoIP push notification. */ + NSUUID *uuid = [NSUUID UUID]; + call_map[uuid] = @(PJSUA_INVALID_ID); + NSLog(@"Receiving push notification %@", uuid.UUIDString); + + /* Re-register, so the server will send us the suspended INVITE. */ + SCHEDULE_TIMER(REREGISTER); + /* Activate audio session. */ AVAudioSession *audioSession = [AVAudioSession sharedInstance]; NSError *error = nil; @@ -474,41 +502,23 @@ - (void)reportIncomingCall { options:0 error:&error]) { NSLog(@"Error setting up audio session: %@", error.localizedDescription); - return; } if (![audioSession setActive:YES error:&error]) { NSLog(@"Error activating audio session: %@", error.localizedDescription); - return; } /* Report the incoming call to the system using CallKit. */ CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - push_call_uuid = [NSUUID UUID]; - [self.provider reportNewIncomingCallWithUUID:push_call_uuid + [self.provider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError * _Nullable error) { if (error) { NSLog(@"Error reporting incoming call: %@", error.localizedDescription); + [call_map removeObjectForKey:uuid]; } }]; -} - -- (void)pushRegistry:(PKPushRegistry *)registry - didReceiveIncomingPushWithPayload:(PKPushPayload *)payload - forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion -{ - /* Handle incoming VoIP push notification. */ - NSLog(@"Receiving push notification"); - - /* Re-register, so the server will send us the suspended INVITE. */ - push_call_id = PJSUA_INVALID_ID; - push_call_uuid = nil; - SCHEDULE_TIMER(REREGISTER); - - /* Report the incoming call via CallKit. */ - [self reportIncomingCall]; /* Call the completion handler when you have finished processing the incoming call. */ completion(); @@ -527,6 +537,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [self.window makeKeyAndVisible]; #if USE_PUSH_NOTIFICATION + call_map = [NSMutableDictionary dictionary]; + /* Set up a push notification registry for Voice over IP (VoIP). */ self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; self.voipRegistry.delegate = self; @@ -567,6 +579,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( /* Create CallKit provider. */ CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:@"ipjsua"]; + configuration.maximumCallGroups = 1; + configuration.maximumCallsPerCallGroup = 1; self.provider = [[CXProvider alloc] initWithConfiguration:configuration]; [self.provider setDelegate:self queue:nil]; #else @@ -629,24 +643,24 @@ - (void)applicationWillTerminate:(UIApplication *)application pj_bool_t reportCallState(pjsua_call_id call_id) { - pjsua_call_info call_info; + pjsua_call_info info; - pjsua_call_get_info(call_id, &call_info); + pjsua_call_get_info(call_id, &info); - if (call_info.state == PJSIP_INV_STATE_DISCONNECTED) { - if (call_id == push_call_id && push_call_uuid) { - [app.provider reportCallWithUUID:push_call_uuid + if (info.state == PJSIP_INV_STATE_DISCONNECTED) { + NSUUID *uuid = (__bridge NSUUID *)pjsua_call_get_user_data(call_id); + if (uuid && call_map[uuid].intValue == call_id) { + [app.provider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonRemoteEnded]; - push_call_id = PJSUA_INVALID_ID; - push_call_uuid = nil; + [call_map removeObjectForKey:uuid]; } - } - /* Check if we need to deactivate audio session. Note that sound device - * will only be closed after pjsua_media_config.snd_auto_close_time. - */ - pjsua_schedule_timer2(pjsip_funcs, (void *)DEACTIVATE_AUDIO, 1500); + /* Check if we need to deactivate audio session. Note that sound device + * will only be closed after pjsua_media_config.snd_auto_close_time. + */ + pjsua_schedule_timer2(pjsip_funcs, (void *)DEACTIVATE_AUDIO, 1500); + } return PJ_FALSE; } @@ -655,12 +669,17 @@ pj_bool_t showNotification(pjsua_call_id call_id) { #if USE_PUSH_NOTIFICATION NSLog(@"Receiving incoming call %d", call_id); - push_call_id = call_id; - /* If we report the incoming call using CallKit, we cannot open - * the sound device until the audio session becomes active. - */ - if (!pjsua_snd_is_active()) - pjsua_set_no_snd_dev(); + + for (NSUUID *uuid in call_map) { + if (call_map[uuid].intValue == PJSUA_INVALID_ID) { + NSLog(@"Associating incoming call %d with UUID %@", + call_id, uuid.UUIDString); + call_map[uuid] = @(call_id); + pjsua_call_set_user_data(call_id, (__bridge void *)uuid); + + break; + } + } #endif return PJ_FALSE;