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..e3ddf78064 --- /dev/null +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsua.entitlements @@ -0,0 +1,16 @@ + + + + + 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 7f01d2759a..fbf6c5e873 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.h @@ -18,14 +18,23 @@ * 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; @end diff --git a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m index 1d56c82462..8a6ef9dc32 100644 --- a/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m +++ b/pjsip-apps/src/pjsua/ios/ipjsua/ipjsuaAppDelegate.m @@ -22,6 +22,8 @@ #import #import #import +#import +#import #include "../../pjsua_app.h" #include "../../pjsua_app_config.h" @@ -33,14 +35,172 @@ @implementation ipjsuaAppDelegate #define THIS_FILE "ipjsuaAppDelegate.m" -#define KEEP_ALIVE_INTERVAL 600 +/* 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" ipjsuaAppDelegate *app; static pjsua_app_cfg_t app_cfg; static bool isShuttingDown; static char **restartArgv; static int restartArgc; -Reachability *internetReach; +Reachability *internetReach; + +/* Mapping between CallKit UUID and pjsua_call_id. */ +NSMutableDictionary *call_map; + +enum { + REREGISTER = 1, + ANSWER_CALL, + END_CALL, + ACTIVATE_AUDIO, + DEACTIVATE_AUDIO, + HANDLE_IP_CHANGE, + HANDLE_ORI_CHANGE +}; + +#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); \ +} + +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 & 0xF; + 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; + pjsua_call_id call_id = (pjsua_call_id)((long)user_data & 0xFF0) >> 4; + + status = pjsua_call_answer(call_id, PJSIP_SC_OK, NULL, NULL); + if (status != PJ_SUCCESS) { + 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_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) { + 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 we use CallKit, sound device may not work until audio session + * becomes active, so we need to force reopen sound device here. + */ + 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) { + 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 { @@ -48,18 +208,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,18 +228,34 @@ - (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 && ![curReach connectionRequired]) { - pjsua_ip_change_param param; - pjsua_ip_change_param_default(¶m); - pjsua_handle_ip_change(¶m); + SCHEDULE_TIMER(HANDLE_IP_CHANGE); } } +- (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 +347,57 @@ - (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); + /* 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. + */ + 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 +414,116 @@ - (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 +didDectivateAudioSession:(AVAudioSession *) audioSession +{ + NSLog(@"Did deactivate Audio Session"); +} + +- (void)provider:(CXProvider *)provider + performAnswerCallAction:(CXAnswerCallAction *)action +{ + 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 (call_map[action.callUUID].intValue != PJSUA_INVALID_ID) break; + [NSThread sleepForTimeInterval:0.1]; + } + + [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; + } + + 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 %@", 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 (call_map[action.callUUID].intValue != PJSUA_INVALID_ID) break; + [NSThread sleepForTimeInterval:0.1]; + } + + [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; + } + + long code = (long)END_CALL | (call_id << 4); + pjsua_schedule_timer2(pjsip_funcs, (void *)code, 0); +} + +- (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; + if (![audioSession setCategory:AVAudioSessionCategoryPlayAndRecord + mode:AVAudioSessionModeVoiceChat + options:0 error:&error]) + { + NSLog(@"Error setting up audio session: %@", error.localizedDescription); + } + if (![audioSession setActive:YES error:&error]) { + NSLog(@"Error activating audio session: %@", error.localizedDescription); + } + + /* Report the incoming call to the system using CallKit. */ + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + [self.provider reportNewIncomingCallWithUUID:uuid + update:callUpdate + completion:^(NSError * _Nullable error) + { + if (error) { + NSLog(@"Error reporting incoming call: %@", + error.localizedDescription); + [call_map removeObjectForKey:uuid]; + } + }]; + + /* 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 +535,62 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( } self.window.rootViewController = self.viewController; [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; + 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]; + }); + } + }]; + + /* 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"]; + configuration.maximumCallGroups = 1; + configuration.maximumCallsPerCallGroup = 1; + 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 * notification is posted, the method "reachabilityChanged" will be called. */ @@ -211,9 +603,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [self updateWithReachability: internetReach]; app = self; - - /* Start pjsua app thread */ - [NSThread detachNewThreadSelector:@selector(pjsuaStart) toTarget:self withObject:nil]; return YES; } @@ -226,78 +615,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); - } - - /* 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); - } - } + 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 @@ -316,27 +641,44 @@ - (void)applicationWillTerminate:(UIApplication *)application } -pj_bool_t showNotification(pjsua_call_id call_id) +pj_bool_t reportCallState(pjsua_call_id call_id) { - /* This is deprecated. Use VoIP Push Notifications with PushKit - * framework instead. - */ -#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). + pjsua_call_info info; + + pjsua_call_get_info(call_id, &info); + + 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]; + [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. */ - alert.alertAction = @"Activate app"; - - dispatch_async(dispatch_get_main_queue(), - ^{[[UIApplication sharedApplication] - presentLocalNotificationNow:alert];}); + pjsua_schedule_timer2(pjsip_funcs, (void *)DEACTIVATE_AUDIO, 1500); + } + + return PJ_FALSE; +} + +pj_bool_t showNotification(pjsua_call_id call_id) +{ +#if USE_PUSH_NOTIFICATION + NSLog(@"Receiving incoming call %d", call_id); + + 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 diff --git a/pjsip-apps/src/pjsua/pjsua_app.c b/pjsip-apps/src/pjsua/pjsua_app.c index 3c819b34ec..37260251e7 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) { diff --git a/pjsip/src/pjsua-lib/pjsua_acc.c b/pjsip/src/pjsua-lib/pjsua_acc.c index c113467b42..0326f5480c 100644 --- a/pjsip/src/pjsua-lib/pjsua_acc.c +++ b/pjsip/src/pjsua-lib/pjsua_acc.c @@ -1605,7 +1605,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);