From 7e60054f9c56eb109fba27f72f81758ab81c77bd Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 3 Sep 2016 23:40:49 -0700 Subject: [PATCH] Partial update to modern Objective-C syntax. Use PJ_LOG instead of NSLog when appropriate. Add support for video. Add support for handling reachability changes. Add support for STUN and TURN servers that can be changed on-demand. Add support for detecting system DNS servers (these are also updated on reachability changes). Add API to verify SIP URI strings. Do not assume sip and support sips addresses. For safety, make sure that stringWithPJString copies the memory. Add support for keeping alive a TCP connection in the background (pre iOS 9). Prioritize Opus and H264. Rename GSLogSipError to GSLogPJSIPError. Support custom headers in SIP INVITES. Add the ability to answer a call with codes. Add support for parsing SIP Reason headers (RFC 3326) on CANCEL messages. Fix wrong if (self = [super init]) initializers. Add support for checking if calls have ended. Add support for checking the remote information in a call. Add support for getting the last SIP status text and code in a call. Make all delegates weak. --- Gossip.xcodeproj/project.pbxproj | 14 + Gossip/GSAccount+Private.h | 5 +- Gossip/GSAccount.h | 23 +- Gossip/GSAccount.m | 367 ++++++++++++++++--- Gossip/GSAccountConfiguration.h | 35 +- Gossip/GSAccountConfiguration.m | 55 ++- Gossip/GSCall+Private.h | 5 +- Gossip/GSCall.h | 91 ++++- Gossip/GSCall.m | 568 ++++++++++++++++++++++++++++-- Gossip/GSCodecInfo+Private.h | 3 +- Gossip/GSCodecInfo.h | 3 +- Gossip/GSCodecInfo.m | 6 +- Gossip/GSConfiguration.h | 33 +- Gossip/GSConfiguration.m | 60 ++-- Gossip/GSDispatch.h | 4 +- Gossip/GSDispatch.m | 178 ++++++++-- Gossip/GSIncomingCall.h | 9 +- Gossip/GSIncomingCall.m | 70 +++- Gossip/GSNotifications.h | 22 +- Gossip/GSNotifications.m | 11 + Gossip/GSOutgoingCall.h | 11 +- Gossip/GSOutgoingCall.m | 77 +++- Gossip/GSPJUtil.h | 10 +- Gossip/GSPJUtil.m | 33 +- Gossip/GSReachability.h | 64 ++++ Gossip/GSReachability.m | 242 +++++++++++++ Gossip/GSRingback.h | 6 +- Gossip/GSRingback.m | 15 +- Gossip/GSUserAgent+Private.h | 1 - Gossip/GSUserAgent.h | 29 +- Gossip/GSUserAgent.m | 580 ++++++++++++++++++++++++++++--- Gossip/Gossip.h | 7 +- Gossip/RFC3326ReasonParser.c | 140 ++++++++ Gossip/RFC3326ReasonParser.h | 20 ++ Gossip/Util.h | 13 +- 35 files changed, 2453 insertions(+), 357 deletions(-) create mode 100644 Gossip/GSReachability.h create mode 100644 Gossip/GSReachability.m create mode 100644 Gossip/RFC3326ReasonParser.c create mode 100644 Gossip/RFC3326ReasonParser.h diff --git a/Gossip.xcodeproj/project.pbxproj b/Gossip.xcodeproj/project.pbxproj index 278afaa6b..91ce73958 100644 --- a/Gossip.xcodeproj/project.pbxproj +++ b/Gossip.xcodeproj/project.pbxproj @@ -88,6 +88,9 @@ 22D43D9115AAC9120099C79F /* GSECallInitController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22D43D8E15AAC90B0099C79F /* GSECallInitController.m */; }; 22D43D9415AACD680099C79F /* GSECallViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 22D43D9315AACD680099C79F /* GSECallViewController.m */; }; 22D43D9615AACFAD0099C79F /* GSECallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22D43D9515AACFAD0099C79F /* GSECallViewController.xib */; }; + 4935915D1D7BF92C0007DE44 /* RFC3326ReasonParser.c in Sources */ = {isa = PBXBuildFile; fileRef = 4935915B1D7BF92C0007DE44 /* RFC3326ReasonParser.c */; }; + 4935915E1D7BF92C0007DE44 /* RFC3326ReasonParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 4935915C1D7BF92C0007DE44 /* RFC3326ReasonParser.h */; }; + 493591601D7BF95A0007DE44 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4935915F1D7BF95A0007DE44 /* libresolv.tbd */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -184,6 +187,9 @@ 22D43D9215AACD680099C79F /* GSECallViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GSECallViewController.h; sourceTree = ""; }; 22D43D9315AACD680099C79F /* GSECallViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GSECallViewController.m; sourceTree = ""; }; 22D43D9515AACFAD0099C79F /* GSECallViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = GSECallViewController.xib; sourceTree = ""; }; + 4935915B1D7BF92C0007DE44 /* RFC3326ReasonParser.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = RFC3326ReasonParser.c; sourceTree = ""; }; + 4935915C1D7BF92C0007DE44 /* RFC3326ReasonParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RFC3326ReasonParser.h; sourceTree = ""; }; + 4935915F1D7BF95A0007DE44 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -208,6 +214,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 493591601D7BF95A0007DE44 /* libresolv.tbd in Frameworks */, 224AFFF715A6E9F600CC5890 /* CoreGraphics.framework in Frameworks */, 22639AE919718AC4004BD40C /* libpjnath-arm-apple-darwin9.a in Frameworks */, 22639AE219718AC4004BD40C /* libilbccodec-arm-apple-darwin9.a in Frameworks */, @@ -325,6 +332,7 @@ 22AD824515A5987C0000D05C /* Frameworks */ = { isa = PBXGroup; children = ( + 4935915F1D7BF95A0007DE44 /* libresolv.tbd */, 2242FB9115A6D8F500B34D4F /* AudioToolbox.framework */, 2242FB9315A6D8FC00B34D4F /* AVFoundation.framework */, 2242FB8C15A6D8B000B34D4F /* CFNetwork.framework */, @@ -340,6 +348,8 @@ 22AD824815A5987C0000D05C /* Gossip */ = { isa = PBXGroup; children = ( + 4935915B1D7BF92C0007DE44 /* RFC3326ReasonParser.c */, + 4935915C1D7BF92C0007DE44 /* RFC3326ReasonParser.h */, 224EFF7815A6BA9000CD541C /* Gossip.h */, 227F443E15E23004007C364C /* GSAccount+Private.h */, 224EFF7F15A6C7D100CD541C /* GSAccount.h */, @@ -407,6 +417,7 @@ 224EFF7515A6B97F00CD541C /* GSConfiguration.h in Headers */, 224EFF8115A6C7D100CD541C /* GSAccount.h in Headers */, 22AD82DD15A5A1550000D05C /* GSUserAgent.h in Headers */, + 4935915E1D7BF92C0007DE44 /* RFC3326ReasonParser.h in Headers */, 221BED8315B0338F00464679 /* GSCodecInfo.h in Headers */, 22D43D8615AAC1C40099C79F /* GSCall.h in Headers */, 225D129C15B6B2780041F416 /* GSNotifications.h in Headers */, @@ -526,6 +537,7 @@ buildActionMask = 2147483647; files = ( 22AD82DE15A5A1550000D05C /* GSUserAgent.m in Sources */, + 4935915D1D7BF92C0007DE44 /* RFC3326ReasonParser.c in Sources */, 224EFF7615A6B97F00CD541C /* GSConfiguration.m in Sources */, 224EFF8215A6C7D100CD541C /* GSAccount.m in Sources */, 224EFF8715A6CC1F00CD541C /* GSAccountConfiguration.m in Sources */, @@ -604,6 +616,7 @@ 22AD824E15A5987C0000D05C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -633,6 +646,7 @@ 22AD824F15A5987C0000D05C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = YES; diff --git a/Gossip/GSAccount+Private.h b/Gossip/GSAccount+Private.h index 1f80eba4b..bc00a7bfd 100644 --- a/Gossip/GSAccount+Private.h +++ b/Gossip/GSAccount+Private.h @@ -7,11 +7,10 @@ // #import "GSAccount.h" -#import "GSAccountConfiguration.h" - @interface GSAccount (Private) -@property (nonatomic, readonly, copy) GSAccountConfiguration *configuration; +- (pj_status_t)networkAddressChanged; +- (pj_status_t)disconnectWithoutReachability; @end diff --git a/Gossip/GSAccount.h b/Gossip/GSAccount.h index 93951af23..bbdfbc765 100644 --- a/Gossip/GSAccount.h +++ b/Gossip/GSAccount.h @@ -5,10 +5,11 @@ // Created by Chakrit Wichian on 7/6/12. // -#import -#import "GSAccountConfiguration.h" -@class GSAccount, GSCall; +#import "PJSIP.h" +@import Foundation; + +@class GSAccount, GSAccountConfiguration, GSCall; /// Account Status enum. typedef enum { @@ -19,12 +20,13 @@ typedef enum { GSAccountStatusDisconnecting, ///< Account is being unregistered from the SIP server. } GSAccountStatus; ///< Account status enum. +NS_ASSUME_NONNULL_BEGIN /// Delegate to receive account activity. @protocol GSAccountDelegate -/// Called when an account recieves an incoming call. -/** Call GSCall::begin to accept incoming call or GSCall::end to deny. +/// Called when an account receives an incoming call. +/** Call -[GSCall begin] to accept incoming call or - [GSCall end] to deny. * This should be done in a timely fashion since we do not support timeouts for incoming call yet. */ - (void)account:(GSAccount *)account didReceiveIncomingCall:(GSCall *)call; @@ -36,8 +38,12 @@ typedef enum { @property (nonatomic, readonly) int accountId; ///< Account Id, automatically assigned by PJSIP. @property (nonatomic, readonly) GSAccountStatus status; ///< Account registration status. Supports KVO notification. +@property (nonatomic, readonly) int lastStatusCode; ///< Account registration error code Last registration error code. When the status field contains a SIP status code that indicates a registration failure, last registration error code contains the error code that causes the failure. In any other case, its value is zero. +@property (nonatomic, nullable, readonly) NSString *lastStatusReason; ///< Account registration error reason. + +@property (nonatomic, readonly, copy) GSAccountConfiguration *configuration; -@property (nonatomic, unsafe_unretained) id delegate; ///< Account activity delegate. +@property (nullable, nonatomic, weak) id delegate; ///< Account activity delegate. /// Configures account with the specified configuration. /** Must be run once and only once before using the GSAccount instance. @@ -47,4 +53,9 @@ typedef enum { - (BOOL)connect; ///< Connects and begin registering with the configured SIP registration server. - (BOOL)disconnect; ///< Unregister from the SIP registration server and disconnects. +- (BOOL)setSTUNEnabled:(BOOL)enabled; +- (BOOL)updateTURNServers; + @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSAccount.m b/Gossip/GSAccount.m index 11796d306..b09362b8c 100644 --- a/Gossip/GSAccount.m +++ b/Gossip/GSAccount.m @@ -6,25 +6,30 @@ // #import "GSAccount.h" -#import "GSAccount+Private.h" +#import "GSAccountConfiguration.h" #import "GSCall.h" #import "GSDispatch.h" #import "GSUserAgent.h" -#import "PJSIP.h" #import "Util.h" +@interface GSAccount () + +@property (nonatomic, readwrite) int lastStatusCode; +@property (nonatomic, nullable, readwrite) NSString *lastStatusReason; + +@end @implementation GSAccount { - GSAccountConfiguration *_config; + pjsip_transport * _Nullable _current_transport; } +@synthesize configuration = _config; + - (id)init { - if (self = [super init]) { + self = [super init]; + if (self) { _accountId = PJSUA_INVALID_ID; _status = GSAccountStatusOffline; - _config = nil; - - _delegate = nil; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self @@ -39,6 +44,10 @@ - (id)init { selector:@selector(registrationStateDidChange:) name:GSSIPRegistrationStateDidChangeNotification object:[GSDispatch class]]; + [center addObserver:self + selector:@selector(transportStateDidChange:) + name:GSSIPTransportStateDidChangeNotification + object:[GSDispatch class]]; } return self; } @@ -46,43 +55,72 @@ - (id)init { - (void)dealloc { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self]; - + GSUserAgent *agent = [GSUserAgent sharedAgent]; if (_accountId != PJSUA_INVALID_ID && [agent status] != GSUserAgentStateDestroyed) { GSLogIfFails(pjsua_acc_del(_accountId)); _accountId = PJSUA_INVALID_ID; } - + _accountId = PJSUA_INVALID_ID; - _config = nil; -} - - -- (GSAccountConfiguration *)configuration { - return _config; } - - (BOOL)configure:(GSAccountConfiguration *)configuration { _config = [configuration copy]; // prepare account config pjsua_acc_config accConfig; pjsua_acc_config_default(&accConfig); - - accConfig.id = [GSPJUtil PJAddressWithString:_config.address]; - accConfig.reg_uri = [GSPJUtil PJAddressWithString:_config.domain]; + + // To avoid issues with multiple invites sent to IP addresses that both route to us +// accConfig.allow_contact_rewrite = PJ_TRUE; +// accConfig.contact_rewrite_method = PJSUA_CONTACT_REWRITE_UNREGISTER; + + // You can use a colorbar for testing by chaning vid_cap_dev to the appropriate device number + // accConfig.vid_cap_dev = 4; + accConfig.reg_retry_interval = 20; + accConfig.id = [GSPJUtil PJStringWithString:_config.address]; + accConfig.reg_uri = [GSPJUtil PJStringWithString:_config.registrar]; accConfig.register_on_acc_add = PJ_FALSE; // connect manually accConfig.publish_enabled = _config.enableStatusPublishing ? PJ_TRUE : PJ_FALSE; + accConfig.vid_in_auto_show = _config.autoShowIncomingVideo ? PJ_TRUE : PJ_FALSE; + accConfig.vid_out_auto_transmit = _config.autoTransmitOutgoingVideo ? PJ_TRUE : PJ_FALSE; + if (!_config.proxyServer) { accConfig.proxy_cnt = 0; } else { accConfig.proxy_cnt = 1; - accConfig.proxy[0] = [GSPJUtil PJAddressWithString:_config.proxyServer]; + accConfig.proxy[0] = [GSPJUtil PJStringWithString:_config.proxyServer]; } - // adds credentials info + if (!_config.proxyServer) { + accConfig.proxy_cnt = 0; + } else { + accConfig.proxy_cnt = 1; + accConfig.proxy[0] = [GSPJUtil PJStringWithString:_config.proxyServer]; + } + + // Enable TURN + if (_config.TURNServer != nil) { + accConfig.turn_cfg_use = PJSUA_TURN_CONFIG_USE_CUSTOM; + + accConfig.turn_cfg.enable_turn = PJ_TRUE; + accConfig.turn_cfg.turn_server = [GSPJUtil PJStringWithString:_config.TURNServer]; + accConfig.turn_cfg.turn_conn_type = _config.TURNTransportType; + + if (_config.TURNUsername != nil && _config.TURNCredential != nil) { + pj_stun_auth_cred cred; + pj_bzero(&cred, sizeof(cred)); + cred.type = PJ_STUN_AUTH_CRED_STATIC; + cred.data.static_cred.username = [GSPJUtil PJStringWithString:_config.TURNUsername]; + cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN; + cred.data.static_cred.data = [GSPJUtil PJStringWithString:_config.TURNCredential]; + accConfig.turn_cfg.turn_auth_cred = cred; + } + } + + // Credentials pjsip_cred_info creds; creds.scheme = [GSPJUtil PJStringWithString:_config.authScheme]; creds.realm = [GSPJUtil PJStringWithString:_config.authRealm]; @@ -92,16 +130,101 @@ - (BOOL)configure:(GSAccountConfiguration *)configuration { accConfig.cred_count = 1; accConfig.cred_info[0] = creds; - + // finish GSReturnNoIfFails(pjsua_acc_add(&accConfig, PJ_TRUE, &_accountId)); return YES; } +- (BOOL)setSTUNEnabled:(BOOL)enabled { + pj_status_t status; + pjsua_acc_config acc_cfg_existing; + + // create a new pj pool used when copying the existing config + pj_pool_t *pool = pjsua_pool_create("GSAccount", 1000, 1000); + if (pool == NULL) { + return NO; + } + + // now copy the existing account config - if you already have one + status = pjsua_acc_get_config(self.accountId, pool, &acc_cfg_existing); + if (status != PJ_SUCCESS) { + return NO; + } + + pjsua_acc_config acc_cfg_new; + + pjsua_acc_config_dup(pool, &acc_cfg_new, &acc_cfg_existing); + + pj_pool_release(pool); + + acc_cfg_new.sip_stun_use = enabled ? PJSUA_STUN_USE_DEFAULT : PJSUA_STUN_USE_DISABLED; + acc_cfg_new.media_stun_use = enabled ? PJSUA_STUN_USE_DEFAULT : PJSUA_STUN_USE_DISABLED; + + // Now apply the new config without STUN to the current config + status = pjsua_acc_modify(self.accountId, &acc_cfg_new); + if (status != PJ_SUCCESS) { + return NO; + } + + return YES; +} + +- (BOOL)updateTURNServers { + pj_status_t status; + pjsua_acc_config acc_cfg_existing; + + // create a new pj pool used when copying the existing config + pj_pool_t *pool = pjsua_pool_create("GSAccount", 1000, 1000); + if (pool == NULL) { + return NO; + } + + // now copy the existing account config - if you already have one + status = pjsua_acc_get_config(self.accountId, pool, &acc_cfg_existing); + if (status != PJ_SUCCESS) { + return NO; + } + + pjsua_acc_config acc_cfg_new; + + pjsua_acc_config_dup(pool, &acc_cfg_new, &acc_cfg_existing); + + pj_pool_release(pool); + + if (_config.TURNServer != nil) { + acc_cfg_new.turn_cfg_use = PJSUA_TURN_CONFIG_USE_CUSTOM; + + acc_cfg_new.turn_cfg.enable_turn = PJ_TRUE; + acc_cfg_new.turn_cfg.turn_server = [GSPJUtil PJStringWithString:_config.TURNServer]; + acc_cfg_new.turn_cfg.turn_conn_type = _config.TURNTransportType; + + pj_stun_auth_cred cred; + pj_bzero(&cred, sizeof(cred)); + if (_config.TURNUsername != nil && _config.TURNCredential != nil) { + cred.type = PJ_STUN_AUTH_CRED_STATIC; + cred.data.static_cred.username = [GSPJUtil PJStringWithString:_config.TURNUsername]; + cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN; + cred.data.static_cred.data = [GSPJUtil PJStringWithString:_config.TURNCredential]; + } + acc_cfg_new.turn_cfg.turn_auth_cred = cred; + } else { + acc_cfg_new.turn_cfg_use = PJSUA_TURN_CONFIG_USE_DEFAULT; + acc_cfg_new.turn_cfg.enable_turn = PJ_FALSE; + } + + // Now apply the new config without STUN to the current config + status = pjsua_acc_modify(self.accountId, &acc_cfg_new); + if (status != PJ_SUCCESS) { + return NO; + } + + return YES; +} - (BOOL)connect { NSAssert(!!_config, @"GSAccount not configured."); - + GSReturnNoIfFails(pjsua_acc_set_registration(_accountId, PJ_TRUE)); GSReturnNoIfFails(pjsua_acc_set_online_status(_accountId, PJ_TRUE)); return YES; @@ -109,12 +232,36 @@ - (BOOL)connect { - (BOOL)disconnect { NSAssert(!!_config, @"GSAccount not configured."); - + GSReturnNoIfFails(pjsua_acc_set_online_status(_accountId, PJ_FALSE)); GSReturnNoIfFails(pjsua_acc_set_registration(_accountId, PJ_FALSE)); + return YES; } +/// This avoids the Disconnecting delay when you don't have reachability +- (pj_status_t)disconnectWithoutReachability { + NSAssert(!!_config, @"GSAccount not configured."); + + pj_status_t status = pjsua_acc_set_online_status(_accountId, PJ_FALSE); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsua_acc_set_online_status(PJ_FALSE) error")); + } + + status = pjsua_acc_set_registration(_accountId, PJ_FALSE); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsua_acc_set_registration(PJ_FALSE) error")); + } + + [self setStatus:GSAccountStatusOffline]; + + status = [self shutdownAndUnsetCurrentTransport]; + if (status != PJ_SUCCESS) { + return status; + } + + return PJ_SUCCESS; +} - (void)setStatus:(GSAccountStatus)newStatus { if (_status == newStatus) // don't send KVO notices unless it really changes. @@ -123,17 +270,20 @@ - (void)setStatus:(GSAccountStatus)newStatus { _status = newStatus; } - - (void)didReceiveIncomingCall:(NSNotification *)notif { pjsua_acc_id accountId = GSNotifGetInt(notif, GSSIPAccountIdKey); pjsua_call_id callId = GSNotifGetInt(notif, GSSIPCallIdKey); if (accountId == PJSUA_INVALID_ID || accountId != _accountId) return; + pjsip_rx_data *invite = GSNotifGetPointer(notif, GSSIPDataKey); + __block GSAccount *self_ = self; __block id delegate_ = _delegate; dispatch_async(dispatch_get_main_queue(), ^{ - GSCall *call = [GSCall incomingCallWithId:callId toAccount:self]; + GSIncomingCall *call = [GSCall incomingCallWithId:callId + invite:invite + toAccount:self]; if (![delegate_ respondsToSelector:@selector(account:didReceiveIncomingCall:)]) return; // call is disposed/hungup on dealloc @@ -145,15 +295,25 @@ - (void)didReceiveIncomingCall:(NSNotification *)notif { - (void)registrationDidStart:(NSNotification *)notif { pjsua_acc_id accountId = GSNotifGetInt(notif, GSSIPAccountIdKey); - pj_bool_t renew = GSNotifGetBool(notif, GSSIPRenewKey); if (accountId == PJSUA_INVALID_ID || accountId != _accountId) return; - GSAccountStatus accStatus = 0; - accStatus = renew ? GSAccountStatusConnecting : GSAccountStatusDisconnecting; - - __block id self_ = self; - dispatch_async(dispatch_get_main_queue(), ^{ [self_ setStatus:accStatus]; }); + pj_bool_t renew = GSNotifGetBool(notif, GSSIPRenewKey); + // Note that the Disconnecting status will be mistakenly set if you use PJSUA_CONTACT_REWRITE_UNREGISTER. This requires a patch to PJSIP to know for what purpose a REGISTER message was sent. + GSAccountStatus accStatus = renew ? GSAccountStatusConnecting : GSAccountStatusDisconnecting; + + [self setStatus:accStatus]; + + // IP Address Change + if (accountId != _accountId) + return; + + pjsua_reg_info *info = GSNotifGetPointer(notif, GSSIPDataKey); + + pjsip_regc_info regc_info; + pjsip_regc_get_info(info->regc, ®c_info); + + [self setCurrentTransport:regc_info.transport]; } - (void)registrationStateDidChange:(NSNotification *)notif { @@ -163,17 +323,21 @@ - (void)registrationStateDidChange:(NSNotification *)notif { GSAccountStatus accStatus; - pjsua_acc_info info; - GSReturnIfFails(pjsua_acc_get_info(accountId, &info)); - - if (info.reg_last_err != PJ_SUCCESS) { + pjsua_acc_info acc_info; + GSReturnIfFails(pjsua_acc_get_info(accountId, &acc_info)); + + if (acc_info.reg_last_err != PJ_SUCCESS) { accStatus = GSAccountStatusInvalid; - } else { - pjsip_status_code code = info.status; - if (code == 0 || (info.online_status == PJ_FALSE)) { - accStatus = GSAccountStatusOffline; - } else if (PJSIP_IS_STATUS_IN_CLASS(code, 100) || PJSIP_IS_STATUS_IN_CLASS(code, 300)) { + pjsip_status_code code = acc_info.status; + if (code == 0 || (acc_info.online_status == PJ_FALSE)) { + if (code == PJSIP_SC_REQUEST_TIMEOUT) { + PJ_LOG(3, (__FILENAME__, "Registration Did Change Timeout!..")); + accStatus = GSAccountStatusInvalid; + } else { + accStatus = GSAccountStatusOffline; + } + } else if (PJSIP_IS_STATUS_IN_CLASS(code, 100) || PJSIP_IS_STATUS_IN_CLASS(code, 300)) {; accStatus = GSAccountStatusConnecting; } else if (PJSIP_IS_STATUS_IN_CLASS(code, 200)) { accStatus = GSAccountStatusConnected; @@ -182,8 +346,125 @@ - (void)registrationStateDidChange:(NSNotification *)notif { } } - __block id self_ = self; - dispatch_async(dispatch_get_main_queue(), ^{ [self_ setStatus:accStatus]; }); + pjsua_reg_info *info = GSNotifGetPointer(notif, GSSIPDataKey); + + pjsip_regc_info regc_info; + pjsip_regc_get_info(info->regc, ®c_info); + [self setCurrentTransport:regc_info.transport]; + + struct pjsip_regc_cbparam *rp = info->cbparam; + + self.lastStatusCode = rp->code; + self.lastStatusReason = [GSPJUtil stringWithPJString:&rp->reason]; + + [self setStatus:accStatus]; + + // IP address change + if (rp->code/100 == 2 && rp->expiration > 0 && rp->contact_cnt > 0) { + /* We already saved the transport instance */ + } else { + [self unsetCurrentTransport]; + } +} + +- (void)transportStateDidChange:(NSNotification *)notification { + pjsip_transport_state state = GSNotifGetInt(notification, GSSIPTransportStateKey); + pjsip_transport *tp = GSNotifGetPointer(notification, GSSIPTransportKey); + + if (state == PJSIP_TP_STATE_DISCONNECTED && _current_transport == tp) { + [self unsetCurrentTransport]; + } +} + +#pragma mark - Transport + +- (pj_status_t)setCurrentTransport:(pjsip_transport * _Nullable)transport { + if (_current_transport != transport) { + pj_status_t status; + if (_current_transport) { + PJ_LOG(3, (__FILENAME__, "Releasing transport..")); + status = pjsip_transport_dec_ref(_current_transport); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsip_transport_dec_ref() error")); + return status; + } + + _current_transport = NULL; + } + /* Save transport instance so that we can close it later when + * new IP address is detected. + */ + PJ_LOG(3, (__FILENAME__, "Saving transport..")); + _current_transport = transport; + if (_current_transport != NULL) { + status = pjsip_transport_add_ref(_current_transport); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsip_transport_add_ref() error")); + return status; + } + } + + return PJ_SUCCESS; + } + + return PJ_FALSE; +} + +- (pj_status_t)shutdownAndUnsetCurrentTransport { + if (_current_transport) { + pj_status_t status = pjsip_transport_shutdown(_current_transport); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsip_transport_shutdown() error")); + return status; + } + + return [self unsetCurrentTransport]; + } + + return PJ_FALSE; +} + +- (pj_status_t)unsetCurrentTransport { + if (_current_transport) { + PJ_LOG(3, (__FILENAME__, "Releasing transport..")); + pj_status_t status = pjsip_transport_dec_ref(_current_transport); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsip_transport_dec_ref() error")); + return status; + } + _current_transport = NULL; + return PJ_SUCCESS; + } + + return PJ_FALSE; +} + +#pragma mark - Network Changes + +- (pj_status_t)networkAddressChanged { + pj_status_t status; + + PJ_LOG(3, (__FILENAME__, "IP address changed... will shutdown transport and reregister")); + + status = [self shutdownAndUnsetCurrentTransport]; + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Could not shut down the current transport, reregistration may not work initially")); + } + + // Go online + status = pjsua_acc_set_registration(_accountId, PJ_TRUE); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsua_acc_set_registration(PJ_TRUE) error")); + return status; + } + + status = pjsua_acc_set_online_status(_accountId, PJ_TRUE); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "pjsua_acc_set_online_status(PJ_TRUE) error")); + return status; + } + + return PJ_SUCCESS; } @end diff --git a/Gossip/GSAccountConfiguration.h b/Gossip/GSAccountConfiguration.h index 245946336..cf3129898 100644 --- a/Gossip/GSAccountConfiguration.h +++ b/Gossip/GSAccountConfiguration.h @@ -5,26 +5,47 @@ // Created by Chakrit Wichian on 7/6/12. // -#import +#import "PJSIP.h" +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN /// Account configuration. Only supports "digest" and plain-text password authentication atm. @interface GSAccountConfiguration : NSObject -@property (nonatomic, copy) NSString *address; ///< SIP address. -@property (nonatomic, copy) NSString *domain; ///< SIP domain. -@property (nonatomic, copy) NSString *proxyServer; ///< SIP outbound proxy server. +@property (nonatomic, copy) NSString *address; ///< SIP address. sip:user@host +@property (nonatomic, copy) NSString *registrar; ///< SIP domain. sip:host +@property (nonatomic, copy, nullable) NSString *proxyServer; ///< SIP outbound proxy server. @property (nonatomic, copy) NSString *authScheme; ///< Authentication scheme. Defaults to "digest". @property (nonatomic, copy) NSString *authRealm; ///< Authentication realm. Defaults to "*". @property (nonatomic, copy) NSString *username; ///< SIP username. @property (nonatomic, copy) NSString *password; ///< SIP password. -@property (nonatomic) BOOL enableStatusPublishing; ///< Enable online/status publishing for services that support them. +@property (nonatomic, copy, nullable) NSString *TURNServer; ///< TURN Server address. + +/** + * Specify the connection type to be used to the TURN server. Valid + * values are PJ_TURN_TP_UDP or PJ_TURN_TP_TCP. + * + * Default: PJ_TURN_TP_UDP + */ +@property (nonatomic) pj_turn_tp_type TURNTransportType; +@property (nonatomic, copy, nullable) NSString *TURNUsername; +@property (nonatomic, copy, nullable) NSString *TURNCredential; + + +@property (nonatomic) BOOL enableStatusPublishing; ///< Enable online/status publishing for services that support them. Can prevent registration for services which don't support it so NO by default. @property (nonatomic) BOOL enableRingback; ///< Enable automatic ringback sounds. @property (nonatomic, copy) NSString *ringbackFilename; ///< Filename to play as ringback sounds. Defaults to "ringtone.wav" so you can just include it in your bundle and Gossip will pick it up. -+ (id)defaultConfiguration; ///< Creates a GSAccountConfiguration instance with default configuration values. -+ (id)configurationWithConfiguration:(GSAccountConfiguration *)configuration; ///< Copy constructor. +@property (nonatomic) BOOL autoShowIncomingVideo; +@property (nonatomic) BOOL autoTransmitOutgoingVideo; + ++ (instancetype)defaultConfiguration; ///< Creates a GSAccountConfiguration instance with default configuration values. ++ (instancetype)configurationWithConfiguration:(GSAccountConfiguration *)configuration; ///< Copy constructor. @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSAccountConfiguration.m b/Gossip/GSAccountConfiguration.m index 6a0894375..325ca238d 100644 --- a/Gossip/GSAccountConfiguration.m +++ b/Gossip/GSAccountConfiguration.m @@ -7,65 +7,54 @@ #import "GSAccountConfiguration.h" - @implementation GSAccountConfiguration -+ (id)defaultConfiguration { ++ (instancetype)defaultConfiguration { return [[self alloc] init]; } -+ (id)configurationWithConfiguration:(GSAccountConfiguration *)configuration { ++ (instancetype)configurationWithConfiguration:(GSAccountConfiguration *)configuration { return [configuration copy]; } - - (id)init { - if (!(self = [super init])) - return nil; // super init failed. + self = [super init]; - _address = nil; - _domain = nil; - _proxyServer = nil; - _authScheme = @"digest"; - _authRealm = @"*"; - _username = nil; - _password = nil; - - _enableRingback = YES; - _ringbackFilename = @"ringtone.wav"; + if (self) { + _authScheme = @"digest"; + _authRealm = @"*"; + + _enableRingback = YES; + _ringbackFilename = @"ringtone.wav"; + _TURNTransportType = PJ_TURN_TP_UDP; + } - // can prevent registration for services which don't support it so NO by default. - _enableStatusPublishing = NO; return self; } -- (void)dealloc { - _address = nil; - _domain = nil; - _proxyServer = nil; - _authScheme = nil; - _authRealm = nil; - _username = nil; - _password = nil; - _ringbackFilename = nil; -} - - - (id)copyWithZone:(NSZone *)zone { GSAccountConfiguration *replica = [GSAccountConfiguration defaultConfiguration]; replica.address = self.address; - replica.domain = self.domain; + replica.registrar = self.registrar; replica.proxyServer = self.proxyServer; replica.authScheme = self.authScheme; replica.authRealm = self.authRealm; replica.username = self.username; replica.password = self.password; - - replica.enableStatusPublishing = self.enableStatusPublishing; + replica.TURNServer = self.TURNServer; + replica.TURNTransportType = self.TURNTransportType; + replica.TURNUsername = self.TURNUsername; + replica.TURNCredential = self.TURNCredential; + replica.enableStatusPublishing = self.enableStatusPublishing; + replica.enableRingback = self.enableRingback; replica.ringbackFilename = self.ringbackFilename; + + replica.autoShowIncomingVideo = self.autoShowIncomingVideo; + replica.autoTransmitOutgoingVideo = self.autoTransmitOutgoingVideo; + return replica; } diff --git a/Gossip/GSCall+Private.h b/Gossip/GSCall+Private.h index d96bad58a..f222ea6ba 100644 --- a/Gossip/GSCall+Private.h +++ b/Gossip/GSCall+Private.h @@ -7,11 +7,12 @@ #import "GSCall.h" - @interface GSCall (Private) +@property (nonatomic, nullable, readwrite) NSDictionary *inviteHeaders; + // private setter for internal use -- (void)setCallId:(int)callId; +- (void)setCallId:(pjsua_call_id)callId; - (void)setStatus:(GSCallStatus)status; @end diff --git a/Gossip/GSCall.h b/Gossip/GSCall.h index 3b5cc292f..067b79755 100644 --- a/Gossip/GSCall.h +++ b/Gossip/GSCall.h @@ -5,54 +5,119 @@ // Created by Chakrit Wichian on 7/9/12. // -#import -#import "GSAccount.h" -#import "GSRingback.h" +@import Foundation; +#import "PJSIP.h" + +@class GSAccount, GSRingback, GSCall, GSIncomingCall, GSOutgoingCall; /// Call session states. -typedef enum { +typedef NS_ENUM(NSUInteger, GSCallStatus) { GSCallStatusReady, ///< Call is ready to be made/pickup. GSCallStatusCalling, ///< Call is ringing. GSCallStatusConnecting, ///< User or other party has pick up the call. GSCallStatusConnected, ///< Call has connected and sound is now coming through. GSCallStatusDisconnected, ///< Call has been disconnected (user hangup/other party hangup.) -} GSCallStatus; +}; + +NS_ASSUME_NONNULL_BEGIN + +#if PJMEDIA_HAS_VIDEO + +@import AVFoundation; +@import UIKit; + +/// Delegate to receive call activity. +@protocol GSCallDelegate + +/// Called on an arbitrary thread when an a PJSIP video view is ready to be displayed. +- (void)call:(GSCall *)account providesView:(UIView *)view isNative:(BOOL)isNative; +@end + +#endif -// TODO: Video call support? /// Represents a single calling session (either incoming or outgoing.) @interface GSCall : NSObject -@property (nonatomic, unsafe_unretained, readonly) GSAccount *account; ///< An account this call is being made from. -@property (nonatomic, readonly) GSRingback *ringback; ///< Returns the current GSRingback instance used to play the call's ringback. +#if PJMEDIA_HAS_VIDEO + +@property (nonatomic, weak) id delegate; ///< Call activity delegate. -@property (nonatomic, readonly) int callId; ///< Call id. Autogenerated from PJSIP. +#endif + +@property (nonatomic, weak, readonly) GSAccount *account; ///< An account this call is being made from. +@property (nullable, nonatomic, readonly) GSRingback *ringback; ///< Returns the current GSRingback instance used to play the call's ringback. + +@property (nonatomic, readonly) pjsua_call_id callId; ///< Call id. Autogenerated from PJSIP. @property (nonatomic, readonly) GSCallStatus status; ///< Call status. Supports KVO notification. +@property (nonatomic, readonly) pjsip_status_code lastStatusCode; ///< Last status code heard. +@property (nullable, nonatomic, readonly) NSString *lastStatusText; ///< Last status text heard. + +@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled; ///< This is only set when you get an incoming cancellation reason right now. +@property (nonatomic, readonly) pjsip_status_code cancellationReasonCode; ///< Last status code heard from the Reason: header of a CANCEL message. +@property (nullable, nonatomic, readonly) NSString *cancellationReasonText; ///< Last status text heard from the Reason: header of a CANCEL message. @property (nonatomic, readonly) float volume; ///< Call volume. Set to 0 to mute. @property (nonatomic, readonly) float micVolume; ///< Call microphone volume. i.e. the volume to transmit sound from the mic. Set to 0 to mute. -/// Creats a new outgoing call to the specified remoteUri. +@property (nullable, nonatomic, copy, readonly) NSString *remoteInfo; + +/// Will return NSNotFound on errors. Dynamically queries call info connect_duration.sec +@property (nonatomic, readonly) long durationConnected; + +#if PJMEDIA_HAS_VIDEO + +/// Checks the call settings +@property (nonatomic, readonly, getter=isVideoEnabled) BOOL videoEnabled; +@property (nonatomic, readonly, getter=isReceivingVideo) BOOL receivingVideo; +@property (nonatomic, readonly, getter=isTransmittingVideo) BOOL transmittingVideo; + +/// Should only be called if tranmsittingVideo is YES. May return AVCaptureDevicePositionUnspecified for any errors +@property (nonatomic, readonly) AVCaptureDevicePosition captureDevicePosition; + +- (BOOL)setVideoTransmissionEnabled:(BOOL)enabled; +- (BOOL)setCaptureDevicePosition:(AVCaptureDevicePosition)position; +- (void)enumerateVideoViews:(nonnull void (^)(UIView *view, BOOL isNative, BOOL *stop))block; + +#endif + ++ (BOOL)verifySIPURIString:(NSString *)URIString; + +/// Creates a new outgoing call to the specified remoteURI. /** Use begin() to begin calling. */ -+ (id)outgoingCallToUri:(NSString *)remoteUri fromAccount:(GSAccount *)account; ++ (GSOutgoingCall *)outgoingCallToURI:(NSString *)remoteURI + fromAccount:(GSAccount *)account + videoEnabled:(BOOL)videoEnabled + customHeaders:(nullable NSDictionary *)customHeaders; /// Creates a new incoming call from the specified PJSIP call id. And associate it with the speicifed account. /** Do not call this method directly, implement GSAccountDelegate and listen to the * GSAccountDelegate::account:didReceiveIncomingCall: message instead. */ -+ (id)incomingCallWithId:(int)callId toAccount:(GSAccount *)account; ++ (GSIncomingCall *)incomingCallWithId:(pjsua_call_id)callId + invite:(pjsip_rx_data *)invite + toAccount:(GSAccount *)account; /// Initialize a new call with the specified account. /** Do not initialize a GSCall instance directly, instead use one of the provided static methods. * This method is only inteded to be used by child classes. */ -- (id)initWithAccount:(GSAccount *)account; +- (instancetype)initWithAccount:(GSAccount *)account; - (BOOL)setVolume:(float)volume; ///< Sets the call volume. Returns YES if successful. - (BOOL)setMicVolume:(float)volume; ///< Sets the microphone volume. Returns YES if successful. - (BOOL)begin; ///< Begins calling for outgoing call or answer incoming call. +- (BOOL)hasEnded; ///< Whether the call has ended. - (BOOL)end; ///< Stop calling and/or hangup call. +- (BOOL)answerWithCode:(unsigned)code; ///< Please use begin and end for traditional answers. This method is to send a response to an incoming INVITE request. Depending on the status code specified as parameter, this function may send provisional response, establish the call, or terminate the call. - (BOOL)sendDTMFDigits:(NSString *)digits; ///< Sends DTMF digits over the call. +/// Custom headers in the INVITE +@property (nonatomic, nullable, readonly) NSDictionary *inviteHeaders; + +//- (nullable NSString *)stringForHeaderKey:(NSString *)headerKey; + @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSCall.m b/Gossip/GSCall.m index e5455daa8..b998b8b7a 100644 --- a/Gossip/GSCall.m +++ b/Gossip/GSCall.m @@ -7,7 +7,9 @@ #import "GSCall.h" #import "GSCall+Private.h" -#import "GSAccount+Private.h" + +#import "GSAccount.h" +#import "GSRingback.h" #import "GSDispatch.h" #import "GSIncomingCall.h" #import "GSOutgoingCall.h" @@ -15,7 +17,16 @@ #import "GSUserAgent+Private.h" #import "PJSIP.h" #import "Util.h" +#import "RFC3326ReasonParser.h" +@interface GSCall() + +@property (nonatomic, nullable, readwrite) NSDictionary *inviteHeaders; +@property (nonatomic, readwrite, getter=isCancelled) BOOL cancelled; +@property (nonatomic, readwrite) pjsip_status_code cancellationReasonCode; +@property (nullable, nonatomic, readwrite) NSString *cancellationReasonText; + +@end @implementation GSCall { pjsua_call_id _callId; @@ -24,42 +35,58 @@ @implementation GSCall { float _volumeScale; } -+ (id)outgoingCallToUri:(NSString *)remoteUri fromAccount:(GSAccount *)account { - GSOutgoingCall *call = [GSOutgoingCall alloc]; - call = [call initWithRemoteUri:remoteUri fromAccount:account]; - - return call; -} +#ifdef PJMEDIA_HAS_VIDEO -+ (id)incomingCallWithId:(int)callId toAccount:(GSAccount *)account { - GSIncomingCall *call = [GSIncomingCall alloc]; - call = [call initWithCallId:callId toAccount:account]; +@dynamic receivingVideo, transmittingVideo, videoEnabled; - return call; +#endif + +@dynamic durationConnected, remoteInfo; + ++ (BOOL)verifySIPURIString:(NSString *)URIString +{ + return [GSPJUtil verifySIPURIString:URIString]; } ++ (GSOutgoingCall *)outgoingCallToURI:(NSString *)outgoingCallToURI + fromAccount:(GSAccount *)account + videoEnabled:(BOOL)videoEnabled + customHeaders:(NSDictionary *)customHeaders { + GSOutgoingCall *call = [GSOutgoingCall alloc]; + call = [call initWithRemoteURI:outgoingCallToURI + fromAccount:account + videoEnabled:videoEnabled + customHeaders:customHeaders]; + + return call; +} -- (id)init { - return [self initWithAccount:nil]; ++ (GSIncomingCall *)incomingCallWithId:(pjsua_call_id)callId + invite:(pjsip_rx_data *)invite + toAccount:(GSAccount *)account { + return [[GSIncomingCall alloc] initWithCallId:callId + invite:invite + toAccount:account]; } -- (id)initWithAccount:(GSAccount *)account { - if (self = [super init]) { +- (instancetype)initWithAccount:(GSAccount *)account { + self = [super init]; + + if (self) { GSAccountConfiguration *config = account.configuration; - + _account = account; _status = GSCallStatusReady; _callId = PJSUA_INVALID_ID; - _ringback = nil; if (config.enableRingback) { _ringback = [GSRingback ringbackWithSoundNamed:config.ringbackFilename]; } - + _volumeScale = [GSUserAgent sharedAgent].configuration.volumeScale; _volume = 1.0 / _volumeScale; _micVolume = 1.0 / _volumeScale; - + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(callStateDidChange:) @@ -69,6 +96,14 @@ - (id)initWithAccount:(GSAccount *)account { selector:@selector(callMediaStateDidChange:) name:GSSIPCallMediaStateDidChangeNotification object:[GSDispatch class]]; + [center addObserver:self + selector:@selector(callMediaEvent:) + name:GSSIPCallMediaEventNotification + object:[GSDispatch class]]; + [center addObserver:self + selector:@selector(callCancelledWithReason:) + name:GSSIPParsedCancelReasonHeaderNotification + object:[GSDispatch class]]; } return self; } @@ -76,27 +111,24 @@ - (id)initWithAccount:(GSAccount *)account { - (void)dealloc { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self]; - + if (_ringback && _ringback.isPlaying) { [_ringback stop]; - _ringback = nil; } - + if (_callId != PJSUA_INVALID_ID && pjsua_call_is_active(_callId)) { GSLogIfFails(pjsua_call_hangup(_callId, 0, NULL, NULL)); } - _account = nil; _callId = PJSUA_INVALID_ID; - _ringback = nil; } - -- (int)callId { +- (pjsua_call_id)callId { return _callId; } -- (void)setCallId:(int)callId { +// TODO: Automatic KVO? +- (void)setCallId:(pjsua_call_id)callId { [self willChangeValueForKey:@"callId"]; _callId = callId; [self didChangeValueForKey:@"callId"]; @@ -108,6 +140,17 @@ - (void)setStatus:(GSCallStatus)status { [self didChangeValueForKey:@"status"]; } +- (void)setLastStatusCode:(pjsip_status_code)lastStatusCode { + [self willChangeValueForKey:@"lastStatusCode"]; + _lastStatusCode = lastStatusCode; + [self didChangeValueForKey:@"lastStatusCode"]; +} + +- (void)setLastStatusText:(NSString *)lastStatusText { + [self willChangeValueForKey:@"lastStatusText"]; + _lastStatusText = lastStatusText; + [self didChangeValueForKey:@"lastStatusText"]; +} - (float)volume { return _volume; @@ -133,6 +176,9 @@ - (BOOL)setMicVolume:(float)micVolume { return result; } +- (BOOL)hasEnded { + return (self.callId == PJSUA_INVALID_ID); +} - (BOOL)begin { // for child overrides only @@ -144,28 +190,39 @@ - (BOOL)end { return NO; } +- (BOOL)answerWithCode:(unsigned)code { + // for child overrides only + return NO; +} + +//- (nullable NSString *)stringForHeaderKey:(NSString *)headerKey { +// // for child overrides only +// return nil; +//} - (BOOL)sendDTMFDigits:(NSString *)digits { pj_str_t pjDigits = [GSPJUtil PJStringWithString:digits]; - pjsua_call_dial_dtmf(_callId, &pjDigits); + if (pjsua_call_dial_dtmf(_callId, &pjDigits) == PJ_SUCCESS) { + return YES; + } + + return NO; } - - (void)startRingback { if (!_ringback || _ringback.isPlaying) return; - + [_ringback play]; } - (void)stopRingback { if (!(_ringback && _ringback.isPlaying)) return; - + [_ringback stop]; } - - (void)callStateDidChange:(NSNotification *)notif { pjsua_call_id callId = GSNotifGetInt(notif, GSSIPCallIdKey); pjsua_acc_id accountId = GSNotifGetInt(notif, GSSIPAccountIdKey); @@ -173,7 +230,13 @@ - (void)callStateDidChange:(NSNotification *)notif { return; pjsua_call_info callInfo; - pjsua_call_get_info(_callId, &callInfo); + GSReturnIfFails(pjsua_call_get_info(_callId, &callInfo)); + + pjsip_status_code lastStatusCode = callInfo.last_status; + NSString *lastStatusText; + if (lastStatusCode != 0) { + lastStatusText = [GSPJUtil stringWithPJString:&callInfo.last_status_text]; + } GSCallStatus callStatus; switch (callInfo.state) { @@ -195,6 +258,18 @@ - (void)callStateDidChange:(NSNotification *)notif { case PJSIP_INV_STATE_CONFIRMED: { [self stopRingback]; callStatus = GSCallStatusConnected; + + // int vid_idx; + // pjsua_vid_win_id wid; + // + // vid_idx = pjsua_call_get_vid_stream_idx(callId); + // if (vid_idx >= 0) { + // pjsua_call_info ci; + // + // pjsua_call_get_info(callId, &ci); + // wid = ci.media[vid_idx].stream.vid.win_in; + // } + } break; case PJSIP_INV_STATE_DISCONNECTED: { @@ -203,15 +278,97 @@ - (void)callStateDidChange:(NSNotification *)notif { } break; } - __block id self_ = self; - dispatch_async(dispatch_get_main_queue(), ^{ [self_ setStatus:callStatus]; }); + [self setLastStatusCode:lastStatusCode]; + [self setLastStatusText:lastStatusText]; + [self setStatus:callStatus]; +} + +#if PJMEDIA_HAS_VIDEO + +- (void)enumerateVideoViews:(nonnull void (^)(UIView *view, BOOL isNative, BOOL *stop))block { + NSParameterAssert(block); + if (!block) { + return; + } + + if (![self isReceivingVideo] && ![self isTransmittingVideo]) { + PJ_LOG(3, (__FILENAME__, "Cannot enumerate video views when not transmitting or receiving video")); + return; + } + + BOOL stop = NO; + + for (pjsua_vid_win_id i = 0; i < PJSUA_MAX_VID_WINS; ++i) { + if (stop == YES) { + break; + } + + pjsua_vid_win_info wi; + + if (pjsua_vid_win_get_info(i, &wi) == PJ_SUCCESS) { + UIView *view = (__bridge UIView *)wi.hwnd.info.ios.window; + if (view) { + block(view, (wi.is_native == 1) ? YES : NO, &stop); + } + } + } +} + +// http://lists.pjsip.org/pipermail/pjsip_lists.pjsip.org/2016-August/019417.html +- (BOOL)sendVideoKeyframe { + if (_callId == PJSUA_INVALID_ID) { + return NO; + } + + int med_idx = pjsua_call_get_vid_stream_idx(_callId); + if (med_idx == -1) { + return NO; + } + + pjsua_call_vid_strm_op_param param; + + pjsua_call_vid_strm_op op = PJSUA_CALL_VID_STRM_SEND_KEYFRAME; + + pjsua_call_vid_strm_op_param_default(¶m); + param.med_idx = med_idx; + + pj_status_t status = pjsua_call_set_vid_strm(_callId, op, ¶m); + + if (status != PJ_SUCCESS) { + return NO; + } + + return YES; +} + +#endif + +// Sending PJSUA_INVALID_ID will display all of the available ones +- (void)displayWindow:(pjsua_vid_win_id)wid { +#if PJMEDIA_HAS_VIDEO + pjsua_vid_win_id i = (wid == PJSUA_INVALID_ID) ? 0 : wid; + pjsua_vid_win_id last = (wid == PJSUA_INVALID_ID) ? PJSUA_MAX_VID_WINS : wid + 1; + + for (;i < last; ++i) { + pjsua_vid_win_info wi; + + if (pjsua_vid_win_get_info(i, &wi) == PJ_SUCCESS) { + UIView *view = (__bridge UIView *)wi.hwnd.info.ios.window; + if (view) { + [self.delegate call:self + providesView:view + isNative:(wi.is_native == 1) ? YES : NO]; + } + } + } +#endif } - (void)callMediaStateDidChange:(NSNotification *)notif { pjsua_call_id callId = GSNotifGetInt(notif, GSSIPCallIdKey); if (callId != _callId) return; - + pjsua_call_info callInfo; GSReturnIfFails(pjsua_call_get_info(_callId, &callInfo)); @@ -222,8 +379,88 @@ - (void)callMediaStateDidChange:(NSNotification *)notif { [self adjustVolume:_volume mic:_micVolume]; } + +#if PJMEDIA_HAS_VIDEO + for (unsigned mi = 0; mi < callInfo.media_cnt; ++mi) { + switch (callInfo.media[mi].type) { + case PJMEDIA_TYPE_VIDEO: + if (callInfo.media_status != PJSUA_CALL_MEDIA_ACTIVE) + return; + // For now show all windows + [self displayWindow:PJSUA_INVALID_ID]; + break; + default: + break; + } + } + + /* Check if remote has just tried to enable video */ + if (callInfo.rem_offerer && callInfo.rem_vid_cnt) + { + /* Check if there is active video */ + int vid_idx = pjsua_call_get_vid_stream_idx(callId); + + if (vid_idx == -1 || callInfo.media[vid_idx].dir == PJMEDIA_DIR_NONE) { + PJ_LOG(3, (__FILENAME__, "Incoming video offer was rejected")); + } + } +#endif } +- (void)callCancelledWithReason:(NSNotification *)notification { + pjsua_call_id callId = GSNotifGetInt(notification, GSSIPCallIdKey); + if (callId != _callId) { + return; + } + + pjsip_reason_hdr *header = GSNotifGetPointer(notification, GSSIPDataKey); + if (header != NULL) { + self.cancelled = YES; + self.cancellationReasonCode = (pjsip_status_code)header->cause; + self.cancellationReasonText = [GSPJUtil stringWithPJString:&header->text]; + } +} + +- (void)callMediaEvent:(NSNotification *)notif { + //#if PJMEDIA_HAS_VIDEO + // pjsua_call_id callId = GSNotifGetInt(notif, GSSIPCallIdKey); + // if (callId != _callId) + // return; + // + // pjmedia_event *event = GSNotifGetPointer(notif, GSSIPDataKey); + // + // char event_name[5]; + // pjmedia_fourcc_name(event->type, event_name); + // + // // PJ_LOG(3, (__FILENAME__, "Media event %s", event_name)); + // + // if (event->type == PJMEDIA_EVENT_FMT_CHANGED) { + // /* Adjust renderer window size to original video size */ + // pjsua_call_info ci; + // + // GSReturnIfFails(pjsua_call_get_info(_callId, &ci)); + // + // unsigned med_idx = GSNotifGetUnsigned(notif, GSSIPMediaIdKey); + // + // if ((ci.media[med_idx].type == PJMEDIA_TYPE_VIDEO) && + // (ci.media[med_idx].dir & PJMEDIA_DIR_DECODING)) + // { + // pjsua_vid_win_id wid; + // pjmedia_rect_size size; + // pjsua_vid_win_info win_info; + // + // wid = ci.media[med_idx].stream.vid.win_in; + // pjsua_vid_win_get_info(wid, &win_info); + // + // size = event->data.fmt_changed.new_fmt.det.vid.size; + // if (size.w != win_info.size.w || size.h != win_info.size.h) { + // pjsua_vid_win_set_size(wid, &size); + // [self displayWindow:wid]; + // } + // } + // } + //#endif +} - (BOOL)adjustVolume:(float)volume mic:(float)micVolume { GSAssert(0.0 <= volume && volume <= 1.0, @"Volume value must be between 0.0 and 1.0"); @@ -260,4 +497,263 @@ - (BOOL)adjustVolume:(float)volume mic:(float)micVolume { return YES; } +#if PJMEDIA_HAS_VIDEO + +// Incoming and Outgoing may override this. +// TODO: Check that the heuristics work +- (BOOL)isVideoEnabled { + if (_callId == PJSUA_INVALID_ID) { + return NO; + } + + pjsua_call_info call_info; + if (pjsua_call_get_info(_callId, &call_info) != PJ_SUCCESS) { + PJ_LOG(3, (__FILENAME__, "Could not get call info")); + return NO; + } + + if (call_info.setting.vid_cnt > 0) { + return YES; + } + + return NO; +} + +- (BOOL)isReceivingVideo { + if (_callId == PJSUA_INVALID_ID) { + return NO; + } + + int med_idx = pjsua_call_get_vid_stream_idx(_callId); + if (med_idx == -1) { + return NO; + } + + if (pjsua_call_vid_stream_is_running(_callId, med_idx, PJMEDIA_DIR_DECODING) != PJ_TRUE) { + return NO; + } + + return YES; +} + +- (BOOL)isTransmittingVideo { + if (_callId == PJSUA_INVALID_ID) { + return NO; + } + + int med_idx = pjsua_call_get_vid_stream_idx(_callId); + if (med_idx == -1) { + return NO; + } + + if (pjsua_call_vid_stream_is_running(_callId, med_idx, PJMEDIA_DIR_ENCODING) != PJ_TRUE) { + return NO; + } + + return YES; +} + +- (BOOL)setVideoTransmissionEnabled:(BOOL)enabled { + if (_callId == PJSUA_INVALID_ID) { + return NO; + } + + int med_idx = pjsua_call_get_vid_stream_idx(_callId); + if (med_idx == -1) { + return NO; + } + + pj_status_t transmissionAlreadyEnabled = pjsua_call_vid_stream_is_running(_callId, med_idx, PJMEDIA_DIR_ENCODING); + + // Check if we actually ahve something to do + if (enabled && transmissionAlreadyEnabled) { + return YES; + } + + if (!enabled && !transmissionAlreadyEnabled) { + return YES; + } + + pjsua_call_vid_strm_op_param param; + + pjsua_call_vid_strm_op op = enabled ? PJSUA_CALL_VID_STRM_START_TRANSMIT : PJSUA_CALL_VID_STRM_STOP_TRANSMIT; + + pjsua_call_vid_strm_op_param_default(¶m); + param.med_idx = med_idx; + + pj_status_t status = pjsua_call_set_vid_strm(_callId, op, ¶m); + + if (status != PJ_SUCCESS) { + return NO; + } + + return YES; +} + +static char *name_camera_front = "Front Camera"; +static char *name_camera_back = "Back Camera"; + +- (AVCaptureDevicePosition)captureDevicePosition { + if (![self isTransmittingVideo]) { + PJ_LOG(3, (__FILENAME__, "Not encoding any video currenty")); + return AVCaptureDevicePositionUnspecified; + } + + pjsua_call_info call_info; + if (pjsua_call_get_info(_callId, &call_info) != PJ_SUCCESS) { + PJ_LOG(3, (__FILENAME__, "Could not get call info")); + return AVCaptureDevicePositionUnspecified; + } + + pjmedia_vid_dev_index *cap_dev_index = NULL; + for (unsigned mi = 0; mi < call_info.media_cnt; ++mi) { + if (call_info.media[mi].type == PJMEDIA_TYPE_VIDEO && + (call_info.media[mi].dir & PJMEDIA_DIR_ENCODING) != 0) { + cap_dev_index = &call_info.media[mi].stream.vid.cap_dev; + } + } + + if (cap_dev_index == NULL) { + PJ_LOG(3, (__FILENAME__, "Could not find capture device for PJMEDIA_DIR_ENCODING")); + return AVCaptureDevicePositionUnspecified; + } + + if (*cap_dev_index == PJMEDIA_VID_INVALID_DEV) { + PJ_LOG(3, (__FILENAME__, "Found PJMEDIA_VID_INVALID_DEV")); + return AVCaptureDevicePositionUnspecified; + } + + pjmedia_vid_dev_info dev_info; + + pj_status_t status = pjsua_vid_dev_get_info(*cap_dev_index, &dev_info); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not get device info")); + return AVCaptureDevicePositionUnspecified; + } + + if (pj_ansi_strcmp(dev_info.name, name_camera_front) == 0) { + return AVCaptureDevicePositionFront; + } else if (pj_ansi_strcmp(dev_info.name, name_camera_back) == 0) { + return AVCaptureDevicePositionBack; + } + + return AVCaptureDevicePositionUnspecified; +} + +- (BOOL)setCaptureDevicePosition:(AVCaptureDevicePosition)position { + if (position != AVCaptureDevicePositionFront && position != AVCaptureDevicePositionBack) { + PJ_LOG(2, (__FILENAME__, "Invalid position value")); + return NO; + } + + if (![self isTransmittingVideo]) { + PJ_LOG(3, (__FILENAME__, "Not encoding any video currenty")); + return NO; + } + + pjsua_call_info call_info; + if (pjsua_call_get_info(_callId, &call_info) != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not get call info")); + return NO; + } + + unsigned *media_idx = NULL; + for (unsigned mi = 0; mi < call_info.media_cnt; ++mi) { + if (call_info.media[mi].type == PJMEDIA_TYPE_VIDEO && + (call_info.media[mi].dir & PJMEDIA_DIR_ENCODING) != 0) { + media_idx = &call_info.media[mi].index; + } + } + + if (media_idx == NULL) { + PJ_LOG(2, (__FILENAME__, "Could not find media stream for PJMEDIA_DIR_ENCODING")); + return NO; + } + +#define MAX_DEV_COUNT 64 + pjmedia_vid_dev_info info[MAX_DEV_COUNT]; + unsigned count = MAX_DEV_COUNT; + pj_status_t status = pjsua_vid_enum_devs(info, &count); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not enumerate devices. Status: %s", [GSPJUtil errorWithSIPStatus:status].description.UTF8String)); + return NO; + } + + pjmedia_vid_dev_index *cap_dev = NULL; + + char *name_match; + if (position == AVCaptureDevicePositionFront) { + name_match = name_camera_front; + } else { + name_match = name_camera_back; + } + + for (unsigned i = 0; i < count; ++i) { + if ((info[i].dir & PJMEDIA_DIR_ENCODING) != 0) { + if (pj_ansi_strcmp(info[i].driver, "AVF") == 0) { + if (pj_ansi_strcmp(info[i].name, name_match) == 0) { + cap_dev = &info[i].id; + break; + } + } + } + } + + if (cap_dev == NULL) { + PJ_LOG(2, (__FILENAME__, "Could not find device for PJMEDIA_DIR_ENCODING")); + return NO; + } + + pjsua_call_vid_strm_op_param param; + pjsua_call_vid_strm_op_param_default(¶m); + param.med_idx = *media_idx; + param.cap_dev = *cap_dev; + + status = pjsua_call_set_vid_strm(_callId, + PJSUA_CALL_VID_STRM_CHANGE_CAP_DEV, + ¶m); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not change video stream. Status: %s", [GSPJUtil errorWithSIPStatus:status].description.UTF8String)); + return NO; + } + + return YES; +} + +#endif + +- (NSString *)remoteInfo { + if (_callId == PJSUA_INVALID_ID) { + return nil; + } + + pjsua_call_info callInfo; + + pj_status_t status = pjsua_call_get_info(_callId, &callInfo); + + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not get call info. Status: %s", [GSPJUtil errorWithSIPStatus:status].description.UTF8String)); + return nil; + } + + return [GSPJUtil stringWithPJString:&callInfo.remote_info]; +} + +- (long)durationConnected { + if (_callId == PJSUA_INVALID_ID) { + return NSNotFound; + } + + pjsua_call_info callInfo; + + pj_status_t status = pjsua_call_get_info(_callId, &callInfo); + + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not get call info. Status: %s", [GSPJUtil errorWithSIPStatus:status].description.UTF8String)); + return NSNotFound; + } + + return callInfo.connect_duration.sec; +} + @end diff --git a/Gossip/GSCodecInfo+Private.h b/Gossip/GSCodecInfo+Private.h index 239034a7e..ff2dd0122 100644 --- a/Gossip/GSCodecInfo+Private.h +++ b/Gossip/GSCodecInfo+Private.h @@ -8,9 +8,8 @@ #import "GSCodecInfo.h" #import "PJSIP.h" - @interface GSCodecInfo (Private) -- (id)initWithCodecInfo:(pjsua_codec_info *)codecInfo; +- (instancetype)initWithCodecInfo:(pjsua_codec_info *)codecInfo; @end diff --git a/Gossip/GSCodecInfo.h b/Gossip/GSCodecInfo.h index 5c63b9985..6179be9f7 100644 --- a/Gossip/GSCodecInfo.h +++ b/Gossip/GSCodecInfo.h @@ -5,8 +5,7 @@ // Created by Chakrit Wichian on 7/13/12. // -#import - +@import Foundation; /// Contains information for a codec. @interface GSCodecInfo : NSObject diff --git a/Gossip/GSCodecInfo.m b/Gossip/GSCodecInfo.m index 184d839a6..c23efca90 100644 --- a/Gossip/GSCodecInfo.m +++ b/Gossip/GSCodecInfo.m @@ -10,19 +10,18 @@ #import "PJSIP.h" #import "Util.h" - @implementation GSCodecInfo { pjsua_codec_info _info; } - (id)initWithCodecInfo:(pjsua_codec_info *)codecInfo { - if (self = [super init]) { + self = [super init]; + if (self) { _info = *codecInfo; } return self; } - - (NSString *)codecId { return [GSPJUtil stringWithPJString:&_info.codec_id]; } @@ -46,7 +45,6 @@ - (BOOL)setMaxPriority { return [self setPriority:254]; } - - (BOOL)disable { return [self setPriority:0]; // 0 disables the codec as said in pjsua online doc } diff --git a/Gossip/GSConfiguration.h b/Gossip/GSConfiguration.h index 3af698d82..621c15740 100644 --- a/Gossip/GSConfiguration.h +++ b/Gossip/GSConfiguration.h @@ -5,34 +5,37 @@ // Created by Chakrit Wichian on 7/6/12. // -#import -#import "GSAccountConfiguration.h" +@import Foundation; +#import "GSAccountConfiguration.h" /// Supported transport types. -typedef enum { - GSUDPTransportType, ///< UDP transport type. - GSUDP6TransportType, ///< UDP on IPv6 transport type. - GSTCPTransportType, ///< TCP transport type. - GSTCP6TransportType, ///< TCP on IPv6 transport type. -} GSTransportType; - +typedef NS_ENUM(NSUInteger, GSTransportType) { + GSTransportTypeUDP, ///< UDP transport type. + GSTransportTypeUDP6, ///< UDP on IPv6 transport type. + GSTransportTypeTCP, ///< TCP transport type. + GSTransportTypeTCP6, ///< TCP on IPv6 transport type. + GSTransportTypeTLS, ///< TLS transport type. + GSTransportTypeTLS6 ///< TLS on IPv6 transport type. +}; /// Main class for configuring a SIP user agent. @interface GSConfiguration : NSObject -@property (nonatomic) unsigned int logLevel; ///< PJSIP log level. -@property (nonatomic) unsigned int consoleLogLevel; ///< PJSIP console output level. +@property (nonatomic) unsigned int logLevel; ///< PJSIP log level. 1 to 6 (verbose). Default 2. +@property (nonatomic) unsigned int consoleLogLevel; ///< PJSIP console output level. 1 to 6 (verbose). Default 2. +@property (nonatomic) BOOL logMessages; ///< PJSIP log SIP messages. Default NO @property (nonatomic) GSTransportType transportType; ///< Transport type to use for connection. @property (nonatomic) unsigned int clockRate; ///< PJSIP clock rate. @property (nonatomic) unsigned int soundClockRate; ///< PJSIP sound clock rate. -@property (nonatomic) float volumeScale; ///< Used for scaling volumes up and down. +@property (nonatomic) float volumeScale; ///< Used for scaling volumes up and down. Default 2.0. -@property (nonatomic, strong) GSAccountConfiguration *account; +@property (nonatomic, copy, nullable) NSOrderedSet *STUNServers; ///< STUN Server addresses. +@property (nonatomic, strong, nullable) GSAccountConfiguration *account; -+ (id)defaultConfiguration; -+ (id)configurationWithConfiguration:(GSConfiguration *)configuration; ++ (nonnull instancetype)defaultConfiguration; ++ (nonnull instancetype)configurationWithConfiguration:(nonnull GSConfiguration *)configuration; @end diff --git a/Gossip/GSConfiguration.m b/Gossip/GSConfiguration.m index d578ad8c3..4ba375e6c 100644 --- a/Gossip/GSConfiguration.m +++ b/Gossip/GSConfiguration.m @@ -7,59 +7,55 @@ #import "GSConfiguration.h" - @implementation GSConfiguration -+ (id)defaultConfiguration { ++ (instancetype)defaultConfiguration { return [[GSConfiguration alloc] init]; } -+ (id)configurationWithConfiguration:(GSConfiguration *)configuration { ++ (instancetype)configurationWithConfiguration:(GSConfiguration *)configuration { return [configuration copy]; } - -- (id)init { - if (!(self = [super init])) - return nil; // init failed. - - // default values - _logLevel = 2; - _consoleLogLevel = 2; - - _transportType = GSUDPTransportType; - - // match clock rate to default number provided by PJSIP. - // http://www.pjsip.org/pjsip/docs/html/structpjsua__media__config.htm#a24792c277d6c6c309eccda9047f641a5 - // setting sound clock rate to zero makes it use the conference bridge rate - // http://www.pjsip.org/pjsip/docs/html/structpjsua__media__config.htm#aeb0fbbdf83b12a29903509adf16ccb3b - _clockRate = 16000; - _soundClockRate = 0; - - // default volume scale to 2.0 so 1.0 is twice as loud as PJSIP would normally emit. - _volumeScale = 2.0; - - _account = [GSAccountConfiguration defaultConfiguration]; +- (instancetype)init +{ + self = [super init]; + if (self) { + // default values + _logLevel = 2; + _consoleLogLevel = 2; + + _transportType = GSTransportTypeUDP; + + // match clock rate to default number provided by PJSIP. + // http://www.pjsip.org/pjsip/docs/html/structpjsua__media__config.htm#a24792c277d6c6c309eccda9047f641a5 + // setting sound clock rate to zero makes it use the conference bridge rate + // http://www.pjsip.org/pjsip/docs/html/structpjsua__media__config.htm#aeb0fbbdf83b12a29903509adf16ccb3b + _clockRate = 16000; + _soundClockRate = 0; + + // default volume scale to 2.0 so 1.0 is twice as loud as PJSIP would normally emit. + _volumeScale = 2.0; + + _account = [GSAccountConfiguration defaultConfiguration]; + } return self; } -- (void)dealloc { - _account = nil; -} - - - (id)copyWithZone:(NSZone *)zone { GSConfiguration *replica = [[[self class] allocWithZone:zone] init]; // TODO: Probably better to do via class_copyPropertyList. replica.logLevel = self.logLevel; replica.consoleLogLevel = self.consoleLogLevel; + replica.logMessages = self.logMessages; replica.transportType = self.transportType; - + replica.STUNServers = self.STUNServers; + replica.clockRate = self.clockRate; replica.soundClockRate = self.soundClockRate; replica.volumeScale = self.volumeScale; - + replica.account = [self.account copy]; return replica; } diff --git a/Gossip/GSDispatch.h b/Gossip/GSDispatch.h index 929df395d..7b8b669a7 100644 --- a/Gossip/GSDispatch.h +++ b/Gossip/GSDispatch.h @@ -5,11 +5,11 @@ // Created by Chakrit Wichian on 7/6/12. // -#import +@import Foundation; + #import "PJSIP.h" #import "GSNotifications.h" // almost always needed by importers - @interface GSDispatch : NSObject + (void)configureCallbacksForAgent:(pjsua_config *)uaConfig; diff --git a/Gossip/GSDispatch.m b/Gossip/GSDispatch.m index 87c374d77..b5cfc7e50 100644 --- a/Gossip/GSDispatch.m +++ b/Gossip/GSDispatch.m @@ -6,70 +6,80 @@ // #import "GSDispatch.h" +#import "GSPJUtil.h" +#import +#import +#import +#import "RFC3326ReasonParser.h" -void onRegistrationStarted(pjsua_acc_id accountId, pj_bool_t renew); -void onRegistrationState(pjsua_acc_id accountId); +void onRegistrationStarted(pjsua_acc_id accountId, pjsua_reg_info *info); +void onRegistrationState(pjsua_acc_id accountId, pjsua_reg_info *info); void onIncomingCall(pjsua_acc_id accountId, pjsua_call_id callId, pjsip_rx_data *rdata); void onCallMediaState(pjsua_call_id callId); void onCallState(pjsua_call_id callId, pjsip_event *e); - +void onCallMediaEvent(pjsua_call_id call_id, unsigned med_idx, pjmedia_event *event); +void onTransportState(pjsip_transport *tp, pjsip_transport_state state, const pjsip_transport_state_info *info); +void onSTUNResolutionComplete(const pj_stun_resolve_result *result); +void onNATDetection(const pj_stun_nat_detect_result *result); +void onCallTSXState(pjsua_call_id call_id, pjsip_transaction *tsx, pjsip_event *e); static dispatch_queue_t _queue = NULL; - @implementation GSDispatch + (void)initialize { - _queue = dispatch_queue_create("GSDispatch", DISPATCH_QUEUE_SERIAL); + _queue = dispatch_queue_create("GSDispatch", DISPATCH_QUEUE_SERIAL); } + (void)configureCallbacksForAgent:(pjsua_config *)uaConfig { - uaConfig->cb.on_reg_started = &onRegistrationStarted; - uaConfig->cb.on_reg_state = &onRegistrationState; + uaConfig->cb.on_reg_started2 = &onRegistrationStarted; + uaConfig->cb.on_reg_state2 = &onRegistrationState; uaConfig->cb.on_incoming_call = &onIncomingCall; uaConfig->cb.on_call_media_state = &onCallMediaState; uaConfig->cb.on_call_state = &onCallState; + uaConfig->cb.on_call_media_event = &onCallMediaEvent; + uaConfig->cb.on_transport_state = &onTransportState; + uaConfig->cb.on_stun_resolution_complete = &onSTUNResolutionComplete; + uaConfig->cb.on_nat_detect = &onNATDetection; + uaConfig->cb.on_call_tsx_state = &onCallTSXState; } - #pragma mark - Dispatch sink // TODO: May need to implement some form of subscriber filtering // orthogonaly/globally if we're to scale. But right now a few // dictionary lookups on the receiver side probably wouldn't hurt much. -+ (void)dispatchRegistrationStarted:(pjsua_acc_id)accountId renew:(pj_bool_t)renew { - NSLog(@"Gossip: dispatchRegistrationStarted(%d, %d)", accountId, renew); ++ (void)dispatchRegistrationStarted:(pjsua_acc_id)accountId info:(pjsua_reg_info *)info { + // PJ_LOG(3, (__FILENAME__, "dispatchRegistrationStarted(%d, %d)", accountId, info->renew)); - NSDictionary *info = nil; - info = [NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithInt:accountId], GSSIPAccountIdKey, - [NSNumber numberWithBool:renew], GSSIPRenewKey, nil]; + NSDictionary *userInfo = @{GSSIPAccountIdKey: @(accountId), + GSSIPRenewKey: @(info->renew), + GSSIPDataKey:[NSValue valueWithPointer:info]}; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center postNotificationName:GSSIPRegistrationDidStartNotification object:self - userInfo:info]; + userInfo:userInfo]; } -+ (void)dispatchRegistrationState:(pjsua_acc_id)accountId { - NSLog(@"Gossip: dispatchRegistrationState(%d)", accountId); ++ (void)dispatchRegistrationState:(pjsua_acc_id)accountId info:(pjsua_reg_info *)info { + // PJ_LOG(3, (__FILENAME__, "dispatchRegistrationState(%d)", accountId)); + + NSDictionary *userInfo = @{GSSIPAccountIdKey: @(accountId), + GSSIPDataKey:[NSValue valueWithPointer:info]}; - NSDictionary *info = nil; - info = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:accountId] - forKey:GSSIPAccountIdKey]; - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center postNotificationName:GSSIPRegistrationStateDidChangeNotification object:self - userInfo:info]; + userInfo:userInfo]; } + (void)dispatchIncomingCall:(pjsua_acc_id)accountId callId:(pjsua_call_id)callId data:(pjsip_rx_data *)data { - NSLog(@"Gossip: dispatchIncomingCall(%d, %d)", accountId, callId); + // PJ_LOG(3, (__FILENAME__, "dispatchIncomingCall(%d, %d)", accountId, callId)); NSDictionary *info = nil; info = [NSDictionary dictionaryWithObjectsAndKeys: @@ -84,7 +94,7 @@ + (void)dispatchIncomingCall:(pjsua_acc_id)accountId } + (void)dispatchCallMediaState:(pjsua_call_id)callId { - NSLog(@"Gossip: dispatchCallMediaState(%d)", callId); + // PJ_LOG(3, (__FILENAME__, "dispatchCallMediaState(%d)", callId)); NSDictionary *info = nil; info = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:callId] @@ -97,8 +107,8 @@ + (void)dispatchCallMediaState:(pjsua_call_id)callId { } + (void)dispatchCallState:(pjsua_call_id)callId event:(pjsip_event *)e { - NSLog(@"Gossip: dispatchCallState(%d)", callId); - + // PJ_LOG(3, (__FILENAME__, "dispatchCallState(%d)", callId)); + NSDictionary *info = nil; info = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:callId], GSSIPCallIdKey, @@ -110,6 +120,64 @@ + (void)dispatchCallState:(pjsua_call_id)callId event:(pjsip_event *)e { userInfo:info]; } ++ (void)dispatchCallMediaEvent:(pjsua_call_id)callId + mediaId:(unsigned)mediaId + event:(pjmedia_event *)e { + // PJ_LOG(3, (__FILENAME__, "dispatchCallMediaEvent(%d)", callId)); + NSDictionary *info = nil; + info = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:callId], GSSIPCallIdKey, + [NSNumber numberWithUnsignedInt:mediaId], GSSIPMediaIdKey, + [NSValue valueWithPointer:e], GSSIPDataKey, nil]; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:GSSIPCallMediaEventNotification + object:self + userInfo:info]; +} + ++ (void)dispatchTransportStateChanged:(pjsip_transport *)tp + state:(pjsip_transport_state)state + info:(const pjsip_transport_state_info *)info { + NSDictionary *userInfo = @{GSSIPTransportStateKey: @(state), + GSSIPTransportStateInfoKey: [NSValue valueWithPointer:info], + GSSIPTransportKey: [NSValue valueWithPointer:tp]}; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:GSSIPTransportStateDidChangeNotification + object:self + userInfo:userInfo]; +} + ++ (void)dispatchSTUNResolutionComplete:(const pj_stun_resolve_result *)result { + NSDictionary *info = @{GSSIPDataKey: [NSValue valueWithPointer:result]}; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:GSSIPSTUNResolutionCompleteNotification + object:self + userInfo:info]; +} + ++ (void)dispatchNATDetected:(const pj_stun_nat_detect_result *)result { + NSDictionary *info = @{GSSIPDataKey: [NSValue valueWithPointer:result]}; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:GSSIPNATDetectedNotification + object:self + userInfo:info]; +} + ++ (void)dispatchParsedCancelReasonHeader:(pjsip_reason_hdr *)header + forCallID:(pjsua_call_id)callID { + NSDictionary *info = @{GSSIPCallIdKey: @(callID), + GSSIPDataKey: [NSValue valueWithPointer:header]}; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:GSSIPParsedCancelReasonHeaderNotification + object:self + userInfo:info]; +} + @end @@ -123,7 +191,7 @@ static inline void dispatch(dispatch_block_t block) { // See the "Implementing tasks using blocks" section for more info // REF: http://developer.apple.com/library/ios/#documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html @autoreleasepool { - + // NOTE: Needs to use dispatch_sync() instead of dispatch_async() because we do not know // the lifetime of the stuff being given to us by PJSIP (e.g. pjsip_rx_data*) so we // must process it completely before the method ends. @@ -131,13 +199,12 @@ static inline void dispatch(dispatch_block_t block) { } } - -void onRegistrationStarted(pjsua_acc_id accountId, pj_bool_t renew) { - dispatch(^{ [GSDispatch dispatchRegistrationStarted:accountId renew:renew]; }); +void onRegistrationStarted(pjsua_acc_id accountId, pjsua_reg_info *info) { + dispatch(^{ [GSDispatch dispatchRegistrationStarted:accountId info:info]; }); } -void onRegistrationState(pjsua_acc_id accountId) { - dispatch(^{ [GSDispatch dispatchRegistrationState:accountId]; }); +void onRegistrationState(pjsua_acc_id accountId, pjsua_reg_info *info) { + dispatch(^{ [GSDispatch dispatchRegistrationState:accountId info:info]; }); } void onIncomingCall(pjsua_acc_id accountId, pjsua_call_id callId, pjsip_rx_data *rdata) { @@ -151,3 +218,50 @@ void onCallMediaState(pjsua_call_id callId) { void onCallState(pjsua_call_id callId, pjsip_event *e) { dispatch(^{ [GSDispatch dispatchCallState:callId event:e]; }); } + +void onCallMediaEvent(pjsua_call_id callId, unsigned med_idx, pjmedia_event *e) { + dispatch(^{ [GSDispatch dispatchCallMediaEvent:callId mediaId:med_idx event:e]; }); +} + +void onTransportState(pjsip_transport *tp, pjsip_transport_state state, const pjsip_transport_state_info *info) { + dispatch(^{ [GSDispatch dispatchTransportStateChanged:tp state:state info:info]; }); +} + +void onSTUNResolutionComplete(const pj_stun_resolve_result *result) { + dispatch(^{ [GSDispatch dispatchSTUNResolutionComplete:result]; }); +} + +void onNATDetection(const pj_stun_nat_detect_result *result) { + dispatch(^{ [GSDispatch dispatchNATDetected:result]; }); +} + +void onCallTSXState(pjsua_call_id call_id, pjsip_transaction *tsx, pjsip_event *e) { + if (tsx->method.id == PJSIP_CANCEL_METHOD) { + char *needle; + if (asprintf(&needle, "%s%s", pjsip_reason_header_name.ptr, ": ") != -1 && needle != NULL) { + char *position = strstr(e->body.rx_msg.rdata->pkt_info.packet, needle); + + if (position != NULL && position != e->body.rx_msg.rdata->pkt_info.packet) { + position += strlen(needle); + + pj_pool_t *pool = pjsua_pool_create("GSDispatch.onCallTSXState", 1000, 1000); + if (pool == NULL) { + PJ_LOG(3, ("GSDispatch.onCallTSXState", "Could not create pool to parse reason header")); + return; + } + + pj_str_t headerReason = pj_str(position); + pjsip_reason_hdr *reasonHeader = pjsip_parse_hdr(pool, &pjsip_reason_header_name, headerReason.ptr, headerReason.slen, NULL); + + if (reasonHeader != NULL) { + dispatch(^{ [GSDispatch dispatchParsedCancelReasonHeader:reasonHeader + forCallID:call_id]; }); + } else { + PJ_LOG(3, ("GSDispatch.onCallTSXState", "Could not parse reason header")); + } + + pj_pool_release(pool); + } + } + } +} diff --git a/Gossip/GSIncomingCall.h b/Gossip/GSIncomingCall.h index 4b0b21e86..be5561d57 100644 --- a/Gossip/GSIncomingCall.h +++ b/Gossip/GSIncomingCall.h @@ -5,11 +5,18 @@ // Created by Chakrit Wichian on 7/12/12. // +@import Foundation; + #import "GSCall.h" +NS_ASSUME_NONNULL_BEGIN @interface GSIncomingCall : GSCall -- (id)initWithCallId:(int)callId toAccount:(GSAccount *)account; +- (instancetype)initWithCallId:(pjsua_call_id)callId + invite:(pjsip_rx_data *)invite + toAccount:(GSAccount *)account; @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSIncomingCall.m b/Gossip/GSIncomingCall.m index d2d2b1d74..2dc80ce78 100644 --- a/Gossip/GSIncomingCall.m +++ b/Gossip/GSIncomingCall.m @@ -10,16 +10,54 @@ #import "PJSIP.h" #import "Util.h" +@implementation GSIncomingCall { +// pjsip_rx_data *_invite; +} -@implementation GSIncomingCall - -- (id)initWithCallId:(int)callId toAccount:(GSAccount *)account { - if (self = [super initWithAccount:account]) { +- (instancetype)initWithCallId:(pjsua_call_id)callId + invite:(pjsip_rx_data *)invite + toAccount:(GSAccount *)account { + self = [super initWithAccount:account]; + if (self) { [self setCallId:callId]; + + // Somehow PJSIP does not store it in the place you think it would store it. I would love to use their APIS rather than do the parsing myself + NSString *inviteString = [[NSString alloc] initWithBytes:invite->pkt_info.packet + length:PJSIP_MAX_PKT_LEN + encoding:NSUTF8StringEncoding]; + NSArray *inviteLines = [inviteString componentsSeparatedByString:@"\r\n"]; + + NSMutableDictionary *headers = [[NSMutableDictionary alloc] initWithCapacity:inviteLines.count]; + + for (NSString *line in inviteLines) { + NSArray *headerValue = [line componentsSeparatedByString:@": "]; + NSUInteger count = headerValue.count; + if (count > 1) { + [headers setObject:headerValue[1] forKey:headerValue.firstObject]; + } + } + + self.inviteHeaders = [headers copy]; } return self; } +//- (nullable NSString *)stringForHeaderKey:(nonnull NSString *)headerKey { +// NSParameterAssert(headerKey); + +// if (!_invite) { +// return nil; +// } +// +// pj_str_t event_hdr_name = [GSPJUtil PJStringWithString:headerKey]; +// pjsip_generic_string_hdr *event_hdr = (pjsip_generic_string_hdr*)pjsip_msg_find_hdr_by_name(_invite->msg_info.msg, &event_hdr_name, NULL); +// if (event_hdr == NULL) { +// return nil; +// } +// +// pj_str_t event_value = event_hdr->hvalue; +// return [GSPJUtil stringWithPJString:&event_value]; +//} - (BOOL)begin { NSAssert(self.callId != PJSUA_INVALID_ID, @"Call has already ended."); @@ -38,4 +76,28 @@ - (BOOL)end { return YES; } +- (BOOL)answerWithCode:(unsigned)code { + NSAssert(self.callId != PJSUA_INVALID_ID, @"Call has not begun yet."); + GSReturnNoIfFails(pjsua_call_answer(self.callId, code, NULL, NULL)); + return YES; +} + +- (BOOL)isVideoEnabled { + if (self.callId == PJSUA_INVALID_ID) { + return NO; + } + + pjsua_call_info call_info; + if (pjsua_call_get_info(self.callId, &call_info) != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not get call info")); + return NO; + } + + if (call_info.rem_vid_cnt > 0) { + return YES; + } + + return NO; +} + @end diff --git a/Gossip/GSNotifications.h b/Gossip/GSNotifications.h index f022be395..def131c2f 100644 --- a/Gossip/GSNotifications.h +++ b/Gossip/GSNotifications.h @@ -5,9 +5,9 @@ // Created by Chakrit Wichian on 7/9/12. // -#import +@import Foundation; -/// Defines notification names +#pragma mark - Defines Notification Names #define GSConstDefine(name_) extern NSString *const name_; GSConstDefine(GSSIPRegistrationStateDidChangeNotification); @@ -15,6 +15,12 @@ GSConstDefine(GSSIPRegistrationDidStartNotification); GSConstDefine(GSSIPCallStateDidChangeNotification); GSConstDefine(GSSIPIncomingCallNotification); GSConstDefine(GSSIPCallMediaStateDidChangeNotification); +GSConstDefine(GSSIPCallMediaEventNotification); +GSConstDefine(GSSIPTransportStateDidChangeNotification); +GSConstDefine(GSSIPSTUNResolutionCompleteNotification); +GSConstDefine(GSSIPNATDetectedNotification); +GSConstDefine(GSSIPParsedCancelReasonHeaderNotification); + GSConstDefine(GSSIPVolumeDidChangeNotification); GSConstDefine(GSVolumeDidChangeNotification); @@ -22,14 +28,24 @@ GSConstDefine(GSVolumeDidChangeNotification); GSConstDefine(GSSIPAccountIdKey); GSConstDefine(GSSIPRenewKey); GSConstDefine(GSSIPCallIdKey); + +GSConstDefine(GSSIPTransportStateKey); // pjsip_transport_state +GSConstDefine(GSSIPTransportKey); // NSValue pjsip_transport * +GSConstDefine(GSSIPTransportStateInfoKey); // NSValue pjsip_transport_state_info * + GSConstDefine(GSSIPDataKey); +GSConstDefine(GSSIPMediaIdKey); + GSConstDefine(GSVolumeKey); GSConstDefine(GSMicVolumeKey); -// helper macros +#pragma mark - Helper macros + #define GSNotifGetInt(notif_, key_) ([[[notif_ userInfo] objectForKey:key_] intValue]) +#define GSNotifGetUnsigned(notif_, key_) ([[[notif_ userInfo] objectForKey:key_] unsignedIntValue]) +#define GSNotifGetPointer(notif_, key_) ([((NSValue *)[[notif_ userInfo] objectForKey:key_]) pointerValue]) #define GSNotifGetBool(notif_, key_) ([[[notif_ userInfo] objectForKey:key_] boolValue]) #define GSNotifGetString(info_, key_) ((NSString *)[[notif_ userInfo] objectForKey:key_]) diff --git a/Gossip/GSNotifications.m b/Gossip/GSNotifications.m index 6546a49a8..f7a469d2d 100644 --- a/Gossip/GSNotifications.m +++ b/Gossip/GSNotifications.m @@ -14,13 +14,24 @@ GSConstSynthesize(GSSIPCallStateDidChangeNotification); GSConstSynthesize(GSSIPIncomingCallNotification); GSConstSynthesize(GSSIPCallMediaStateDidChangeNotification); +GSConstSynthesize(GSSIPCallMediaEventNotification); +GSConstSynthesize(GSSIPTransportStateDidChangeNotification); +GSConstSynthesize(GSSIPSTUNResolutionCompleteNotification); +GSConstSynthesize(GSSIPNATDetectedNotification); +GSConstSynthesize(GSSIPParsedCancelReasonHeaderNotification); GSConstSynthesize(GSVolumeDidChangeNotification); GSConstSynthesize(GSSIPAccountIdKey); GSConstSynthesize(GSSIPRenewKey); GSConstSynthesize(GSSIPCallIdKey); + +GSConstSynthesize(GSSIPTransportStateKey); +GSConstSynthesize(GSSIPTransportKey); +GSConstSynthesize(GSSIPTransportStateInfoKey); + GSConstSynthesize(GSSIPDataKey); +GSConstSynthesize(GSSIPMediaIdKey); GSConstSynthesize(GSVolumeKey); GSConstSynthesize(GSMicVolumeKey); diff --git a/Gossip/GSOutgoingCall.h b/Gossip/GSOutgoingCall.h index d482ce8ab..693c6f69e 100644 --- a/Gossip/GSOutgoingCall.h +++ b/Gossip/GSOutgoingCall.h @@ -7,12 +7,17 @@ #import "GSCall.h" +NS_ASSUME_NONNULL_BEGIN @interface GSOutgoingCall : GSCall -@property (nonatomic, copy, readonly) NSString *remoteUri; +@property (nonatomic, copy, readonly) NSString *remoteURI; -- (id)initWithRemoteUri:(NSString *)remoteUri - fromAccount:(GSAccount *)account; +- (instancetype)initWithRemoteURI:(NSString *)remoteURI + fromAccount:(GSAccount *)account + videoEnabled:(BOOL)videoEnabled + customHeaders:(nullable NSDictionary *)customHeaders; @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSOutgoingCall.m b/Gossip/GSOutgoingCall.m index 58009bbd8..e6df6e191 100644 --- a/Gossip/GSOutgoingCall.m +++ b/Gossip/GSOutgoingCall.m @@ -6,47 +6,88 @@ // #import "GSOutgoingCall.h" + +#import "GSAccount.h" #import "GSCall+Private.h" #import "PJSIP.h" #import "Util.h" +@implementation GSOutgoingCall { + BOOL _enableVideoTransmissionInCallSetting; + NSDictionary *_customHeaders; +} -@implementation GSOutgoingCall - -@synthesize remoteUri = _remoteUri; - -- (id)initWithRemoteUri:(NSString *)remoteUri fromAccount:(GSAccount *)account { - if (self = [super initWithAccount:account]) { - _remoteUri = [remoteUri copy]; +- (instancetype)initWithRemoteURI:(NSString *)remoteURI + fromAccount:(GSAccount *)account + videoEnabled:(BOOL)videoEnabled + customHeaders:(nullable NSDictionary *)customHeaders +{ + self = [super initWithAccount:account]; + if (self) { + _remoteURI = [remoteURI copy]; + _enableVideoTransmissionInCallSetting = videoEnabled; + _customHeaders = customHeaders; } return self; } -- (void)dealloc { - _remoteUri = nil; +- (BOOL)isVideoEnabled { + if (self.callId == PJSUA_INVALID_ID) { + return _enableVideoTransmissionInCallSetting; + } + + return [super isVideoEnabled]; } +- (NSString *)remoteInfo { + if (self.callId == PJSUA_INVALID_ID) { + return _remoteURI; + } + + return [super remoteInfo]; +} - (BOOL)begin { - if (![_remoteUri hasPrefix:@"sip:"]) - _remoteUri = [@"sip:" stringByAppendingString:_remoteUri]; + NSAssert([NSThread isMainThread], @"We must be called on the main thread"); - pj_str_t remoteUri = [GSPJUtil PJStringWithString:_remoteUri]; + pj_str_t remoteUri = [GSPJUtil PJStringWithString:_remoteURI]; pjsua_call_setting callSetting; pjsua_call_setting_default(&callSetting); + callSetting.aud_cnt = 1; - callSetting.vid_cnt = 0; // TODO: Video calling support? + callSetting.vid_cnt = (_enableVideoTransmissionInCallSetting == YES) ? 1 : 0; pjsua_call_id callId; - GSReturnNoIfFails(pjsua_call_make_call(self.account.accountId, &remoteUri, &callSetting, NULL, NULL, &callId)); + + if (_customHeaders.count > 0) { + pj_pool_t *pool; + pjsua_msg_data msg_data; + pjsua_msg_data_init(&msg_data); + pj_caching_pool cp; + + pj_caching_pool_init(&cp, &pj_pool_factory_default_policy, 0); + pool= pj_pool_create(&cp.factory, "header", 1000, 1000, NULL); + + for (NSString *key in [_customHeaders allKeys]) { +// PJ_LOG(3, (__FILENAME__, "Setting custom header in call: '%s: %s'", key.UTF8String, ((NSString *)[_customHeaders objectForKey:key]).UTF8String)); + pj_str_t hname = pj_str((char *)[key UTF8String]); + char *headerValue = (char *)[(NSString *)[_customHeaders objectForKey:key] UTF8String]; + pj_str_t hvalue = pj_str(headerValue); + pjsip_generic_string_hdr *add_hdr = pjsip_generic_string_hdr_create(pool, &hname, &hvalue); + pj_list_push_back(&msg_data.hdr_list, add_hdr); + } + GSReturnNoIfFails(pjsua_call_make_call(self.account.accountId, &remoteUri, &callSetting, NULL, &msg_data, &callId)); + } else { + GSReturnNoIfFails(pjsua_call_make_call(self.account.accountId, &remoteUri, &callSetting, NULL, NULL, &callId)); + } [self setCallId:callId]; return YES; } - (BOOL)end { - NSAssert(self.callId != PJSUA_INVALID_ID, @"Call has not begun yet."); + NSAssert(self.callId != PJSUA_INVALID_ID, @"Call has not begun yet."); GSReturnNoIfFails(pjsua_call_hangup(self.callId, 0, NULL, NULL)); [self setStatus:GSCallStatusDisconnected]; @@ -54,4 +95,10 @@ - (BOOL)end { return YES; } +- (BOOL)answerWithCode:(unsigned)code { + NSAssert(self.callId != PJSUA_INVALID_ID, @"Call has not begun yet."); + GSReturnNoIfFails(pjsua_call_answer(self.callId, code, NULL, NULL)); + return YES; +} + @end diff --git a/Gossip/GSPJUtil.h b/Gossip/GSPJUtil.h index 4627c20a1..3d3f0ab39 100644 --- a/Gossip/GSPJUtil.h +++ b/Gossip/GSPJUtil.h @@ -5,9 +5,11 @@ // Created by Chakrit Wichian on 7/6/12. // -#import +@import Foundation; + #import "PJSIP.h" +NS_ASSUME_NONNULL_BEGIN /// General utilities for working with PJSIP in ObjC land. /** Had to use a static class instead since categories cause compilation problems @@ -23,7 +25,9 @@ /// Creates pj_str_t from NSString. Instance lifetime depends on the NSString instance. + (pj_str_t)PJStringWithString:(NSString *)string; -/// Creates pj_str_t from NSString prefixed with "sip:". Instance lifetime depends on the NSString instance. -+ (pj_str_t)PJAddressWithString:(NSString *)string; +/// Verifies that `URIString` is a valid SIP URI such as `sip:user@host` ++ (BOOL)verifySIPURIString:(NSString *)URIString; @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSPJUtil.m b/Gossip/GSPJUtil.m index 025a824fa..e0e017908 100644 --- a/Gossip/GSPJUtil.m +++ b/Gossip/GSPJUtil.m @@ -7,7 +7,6 @@ #import "GSPJUtil.h" - @implementation GSPJUtil + (NSError *)errorWithSIPStatus:(pj_status_t)status { @@ -33,27 +32,35 @@ + (NSError *)errorWithSIPStatus:(pj_status_t)status { return err; } - + (NSString *)stringWithPJString:(const pj_str_t *)pjString { - NSString *result = [NSString alloc]; - result = [result initWithBytesNoCopy:pjString->ptr - length:pjString->slen - encoding:NSASCIIStringEncoding - freeWhenDone:NO]; + return [[NSString alloc] initWithBytes:pjString->ptr + length:pjString->slen + encoding:NSUTF8StringEncoding]; +} + ++ (BOOL)verifySIPURIString:(nonnull NSString *)URIString { + return [self verifySIPURICString:[URIString cStringUsingEncoding:NSUTF8StringEncoding]]; +} + ++ (BOOL)verifySIPURICString:(const char *)URIString { + NSParameterAssert(URIString); + if (!URIString) { + return NO; + } - return result; + if (pjsua_verify_sip_url(URIString) == PJ_SUCCESS) { + return YES; + } + + return NO; } + (pj_str_t)PJStringWithString:(NSString *)string { const char *cStr = [string cStringUsingEncoding:NSASCIIStringEncoding]; // TODO: UTF8? - + pj_str_t result; pj_cstr(&result, cStr); return result; } -+ (pj_str_t)PJAddressWithString:(NSString *)string { - return [self PJStringWithString:[@"sip:" stringByAppendingString:string]]; -} - @end diff --git a/Gossip/GSReachability.h b/Gossip/GSReachability.h new file mode 100644 index 000000000..0ba10dec4 --- /dev/null +++ b/Gossip/GSReachability.h @@ -0,0 +1,64 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Basic demonstration of how to use the SystemConfiguration Reachablity APIs. + */ + +#import +#import +#import + + +typedef enum : NSInteger { + GSNotReachable = 0, + GSReachableViaWiFi, + GSReachableViaWWAN +} GSNetworkStatus; + +#pragma mark IPv6 Support +//Reachability fully support IPv6. For full details, see ReadMe.md. + + +extern NSString *kGSReachabilityChangedNotification; + + +@interface GSReachability : NSObject + +/*! + * Use to check the reachability of a given host name. + */ ++ (instancetype)reachabilityWithHostName:(NSString *)hostName; + +/*! + * Use to check the reachability of a given IP address. + */ ++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress; + +/*! + * Checks whether the default route is available. Should be used by applications that do not connect to a particular host. + */ ++ (instancetype)reachabilityForInternetConnection; + + +#pragma mark reachabilityForLocalWiFi +//reachabilityForLocalWiFi has been removed from the sample. See ReadMe.md for more information. +//+ (instancetype)reachabilityForLocalWiFi; + +/*! + * Start listening for reachability notifications on the current run loop. + */ +- (BOOL)startNotifier; +- (void)stopNotifier; + +- (GSNetworkStatus)currentReachabilityStatus; + +/*! + * WWAN may be available, but not active until a connection has been established. WiFi may require a connection for VPN on Demand. + */ +- (BOOL)connectionRequired; + +@end + + diff --git a/Gossip/GSReachability.m b/Gossip/GSReachability.m new file mode 100644 index 000000000..929a0a81f --- /dev/null +++ b/Gossip/GSReachability.m @@ -0,0 +1,242 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Basic demonstration of how to use the SystemConfiguration Reachablity APIs. + */ + +#import +#import +#import +#import +#import + +#import + +#import "GSReachability.h" + +#pragma mark IPv6 Support +//Reachability fully support IPv6. For full details, see ReadMe.md. + + +NSString *kGSReachabilityChangedNotification = @"kNetworkGSReachabilityChangedNotification"; + + +#pragma mark - Supporting functions + +#define kShouldPrintReachabilityFlags 0 + +static void PrintReachabilityFlags(SCNetworkReachabilityFlags flags, const char* comment) +{ +#if kShouldPrintReachabilityFlags + + PJ_LOG(3, (__FILENAME__, "GSReachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n", + (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', + (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', + + (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', + (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', + (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-', + comment + )); +#endif +} + + +static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) +{ +#pragma unused (target, flags) + NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback"); + NSCAssert([(__bridge NSObject*) info isKindOfClass: [GSReachability class]], @"info was wrong class in ReachabilityCallback"); + + GSReachability* noteObject = (__bridge GSReachability *)info; + // Post a notification to notify the client that the network reachability changed. + [[NSNotificationCenter defaultCenter] postNotificationName: kGSReachabilityChangedNotification object: noteObject]; +} + + +#pragma mark - Reachability implementation + +@implementation GSReachability +{ + SCNetworkReachabilityRef _reachabilityRef; +} + ++ (instancetype)reachabilityWithHostName:(NSString *)hostName +{ + GSReachability* returnValue = NULL; + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]); + if (reachability != NULL) + { + returnValue= [[self alloc] init]; + if (returnValue != NULL) + { + returnValue->_reachabilityRef = reachability; + } + else { + CFRelease(reachability); + } + } + return returnValue; +} + + ++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress +{ + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, hostAddress); + + GSReachability* returnValue = NULL; + + if (reachability != NULL) + { + returnValue = [[self alloc] init]; + if (returnValue != NULL) + { + returnValue->_reachabilityRef = reachability; + } + else { + CFRelease(reachability); + } + } + return returnValue; +} + + ++ (instancetype)reachabilityForInternetConnection +{ + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress]; +} + +#pragma mark reachabilityForLocalWiFi +//reachabilityForLocalWiFi has been removed from the sample. See ReadMe.md for more information. +//+ (instancetype)reachabilityForLocalWiFi + + + +#pragma mark - Start and stop notifier + +- (BOOL)startNotifier +{ + BOOL returnValue = NO; + SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, NULL, NULL}; + + if (SCNetworkReachabilitySetCallback(_reachabilityRef, ReachabilityCallback, &context)) + { + if (SCNetworkReachabilityScheduleWithRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode)) + { + returnValue = YES; + } + } + + return returnValue; +} + + +- (void)stopNotifier +{ + if (_reachabilityRef != NULL) + { + SCNetworkReachabilityUnscheduleFromRunLoop(_reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + } +} + + +- (void)dealloc +{ + [self stopNotifier]; + if (_reachabilityRef != NULL) + { + CFRelease(_reachabilityRef); + } +} + + +#pragma mark - Network Flag Handling + +- (GSNetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags +{ + PrintReachabilityFlags(flags, "networkStatusForFlags"); + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) + { + // The target host is not reachable. + return GSNotReachable; + } + + GSNetworkStatus returnValue = GSNotReachable; + + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) + { + /* + If the target host is reachable and no connection is required then we'll assume (for now) that you're on Wi-Fi... + */ + returnValue = GSReachableViaWiFi; + } + + if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) + { + /* + ... and the connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs... + */ + + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) + { + /* + ... and no [user] intervention is needed... + */ + returnValue = GSReachableViaWiFi; + } + } + + if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) + { + /* + ... but WWAN connections are OK if the calling application is using the CFNetwork APIs. + */ + returnValue = GSReachableViaWWAN; + } + + return returnValue; +} + + +- (BOOL)connectionRequired +{ + NSAssert(_reachabilityRef != NULL, @"connectionRequired called with NULL reachabilityRef"); + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags)) + { + return (flags & kSCNetworkReachabilityFlagsConnectionRequired); + } + + return NO; +} + + +- (GSNetworkStatus)currentReachabilityStatus +{ + NSAssert(_reachabilityRef != NULL, @"currentNetworkStatus called with NULL SCNetworkReachabilityRef"); + GSNetworkStatus returnValue = GSNotReachable; + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags)) + { + returnValue = [self networkStatusForFlags:flags]; + } + + return returnValue; +} + + +@end diff --git a/Gossip/GSRingback.h b/Gossip/GSRingback.h index dd849e397..7080a41d8 100644 --- a/Gossip/GSRingback.h +++ b/Gossip/GSRingback.h @@ -4,10 +4,8 @@ // // Created by Chakrit Wichian on 8/15/12. // -// - -#import +@import Foundation; /// Ringback sound player. @interface GSRingback : NSObject @@ -16,7 +14,7 @@ @property (nonatomic, readonly) float volume; ///< Returns current ringback volume. /// Creates GSRingback instance with ringback tone from the specified filename. -+ (id)ringbackWithSoundNamed:(NSString *)filename; ++ (instancetype)ringbackWithSoundNamed:(NSString *)filename; - (BOOL)setVolume:(float)volume; ///< Sets ringback volume. This value is subject to GSConfiguration.volumeScale. diff --git a/Gossip/GSRingback.m b/Gossip/GSRingback.m index 0e0a9643f..6f6c35c85 100644 --- a/Gossip/GSRingback.m +++ b/Gossip/GSRingback.m @@ -4,7 +4,6 @@ // // Created by Chakrit Wichian on 8/15/12. // -// #import "GSRingback.h" #import "GSUserAgent.h" @@ -12,7 +11,6 @@ #import "PJSIP.h" #import "Util.h" - @implementation GSRingback { float _volume; float _volumeScale; @@ -21,13 +19,13 @@ @implementation GSRingback { pjsua_player_id _playerId; } -+ (id)ringbackWithSoundNamed:(NSString *)filename { ++ (instancetype)ringbackWithSoundNamed:(NSString *)filename { return [[self alloc] initWithSoundNamed:filename]; } - -- (id)initWithSoundNamed:(NSString *)filename { - if (self = [super init]) { +- (instancetype)initWithSoundNamed:(NSString *)filename { + self = [super init]; + if (self) { NSBundle *bundle = [NSBundle mainBundle]; _isPlaying = NO; @@ -41,7 +39,7 @@ - (id)initWithSoundNamed:(NSString *)filename { filename = [filename lastPathComponent]; filename = [bundle pathForResource:[filename stringByDeletingPathExtension] ofType:[filename pathExtension]]; - NSLog(@"Gossip: ringbackWithSoundNamed: %@", filename); + PJ_LOG(3, (__FILENAME__, "ringbackWithSoundNamed: %@", filename.UTF8String)); // create pjsua media playlist const pj_str_t filenames[] = { [GSPJUtil PJStringWithString:filename] }; @@ -59,7 +57,6 @@ - (void)dealloc { } } - - (BOOL)setVolume:(float)volume { GSAssert(0.0 <= volume && volume <= 1.0, @"Volume value must be between 0.0 and 1.0"); @@ -71,7 +68,6 @@ - (BOOL)setVolume:(float)volume { return YES; } - - (BOOL)play { GSAssert(!_isPlaying, @"Already connected to a call."); @@ -88,5 +84,4 @@ - (BOOL)stop { return YES; } - @end diff --git a/Gossip/GSUserAgent+Private.h b/Gossip/GSUserAgent+Private.h index 0be333f3a..83b148452 100644 --- a/Gossip/GSUserAgent+Private.h +++ b/Gossip/GSUserAgent+Private.h @@ -8,7 +8,6 @@ #import "GSUserAgent.h" #import "GSConfiguration.h" - @interface GSUserAgent (Private) @property (nonatomic, readonly) GSConfiguration *configuration; diff --git a/Gossip/GSUserAgent.h b/Gossip/GSUserAgent.h index 1b7c79d62..aeb59e54a 100644 --- a/Gossip/GSUserAgent.h +++ b/Gossip/GSUserAgent.h @@ -5,19 +5,23 @@ // Created by Chakrit Wichian on 7/5/12. // -#import +@import Foundation; + #import "GSAccount.h" #import "GSConfiguration.h" +#import "GSReachability.h" + +NS_ASSUME_NONNULL_BEGIN +extern NSString *GSUserAgentNetworkReachabilityChangedNotification; -typedef enum { +typedef NS_ENUM(NSInteger, GSUserAgentState) { GSUserAgentStateUninitialized = 0, GSUserAgentStateCreated = 1, GSUserAgentStateConfigured = 2, GSUserAgentStateStarted = 3, - GSUserAgentStateDestroyed = -1, // TODO: Remove? Since it's equivalent to uninitialized. -} GSUserAgentState; - + GSUserAgentStateDestroyed = -1 // TODO: Remove? Since it's equivalent to uninitialized. +}; /// Mains SIP user agent interface. Applications should configure the shared instance on startup. /** Only a single GSUserAgent may be created for each application since PJSIP only supports a single user agent at a time. @@ -30,11 +34,12 @@ typedef enum { */ @interface GSUserAgent : NSObject -@property (nonatomic, strong, readonly) GSAccount *account; ///< Default GSAccount instance with the configured SIP account registration. +@property (nonatomic, strong, readonly, nullable) GSAccount *account; ///< Default GSAccount instance with the configured SIP account registration. @property (nonatomic, readonly) GSUserAgentState status; ///< User agent configuration state. Supports KVO notification. +@property (nonatomic, nullable, readonly) GSConfiguration *configuration; /// Obtains the shared user agent instance. -+ (GSUserAgent *)sharedAgent; ++ (instancetype)sharedAgent; /// Configure the agent for use. /** This method must be called on application startup and before using any SIP functionality. @@ -57,7 +62,15 @@ typedef enum { */ - (BOOL)reset; +- (BOOL)updateSTUNServers; + /// Gets an array of GSCodecInfo for codecs loaded by PJSIP. -- (NSArray *)arrayOfAvailableCodecs; +- (nullable NSArray *)arrayOfAvailableCodecs; + +/// Pre iOS 9 handler to be used with UIApplication APIs. This is the PJSIP keep alive solution for TCP transports. +- (void)backgroundKeepAliveHandler; +- (GSNetworkStatus)currentReachabilityStatus; @end + +NS_ASSUME_NONNULL_END diff --git a/Gossip/GSUserAgent.m b/Gossip/GSUserAgent.m index 64f2abb69..b1ba74934 100644 --- a/Gossip/GSUserAgent.m +++ b/Gossip/GSUserAgent.m @@ -9,32 +9,47 @@ #import "GSUserAgent+Private.h" #import "GSCodecInfo.h" #import "GSCodecInfo+Private.h" +#import "GSAccount+Private.h" #import "GSDispatch.h" #import "PJSIP.h" #import "Util.h" +#import "RFC3326ReasonParser.h" +#include +#include +#include +#include + +NSString *GSUserAgentNetworkReachabilityChangedNotification = @"GSUserAgentNetworkReachabilityChangedNotification"; + +@import UIKit; + +@interface GSUserAgent () + +@property (nonatomic, nullable, readwrite) GSConfiguration *configuration; + +@end @implementation GSUserAgent { - GSConfiguration *_config; pjsua_transport_id _transportId; + GSReachability *_reachability; } @synthesize account = _account; @synthesize status = _status; -+ (GSUserAgent *)sharedAgent { ++ (instancetype)sharedAgent { static dispatch_once_t onceToken; static GSUserAgent *agent = nil; dispatch_once(&onceToken, ^{ agent = [[GSUserAgent alloc] init]; }); - return agent; } - -- (id)init { - if (self = [super init]) { - _account = nil; - _config = nil; +- (instancetype)init +{ + self = [super init]; + if (self) { + NSAssert([NSThread isMainThread], @"We must be called on the main thread"); _transportId = PJSUA_INVALID_ID; _status = GSUserAgentStateUninitialized; @@ -42,7 +57,205 @@ - (id)init { return self; } +/// You must link against libresolv to use this. +- (nullable NSMutableArray *)systemDNSServers { + res_state res = malloc(sizeof(struct __res_state)); + if (res == NULL) { + return nil; + } + + int result = res_ninit(res); + + NSMutableArray *servers; + + if (result == 0) { + int count = res->nscount; + + if (count > 0) { + servers = [[NSMutableArray alloc] initWithCapacity:count]; + + for (int i = 0; i < count; i++) + { + sa_family_t family = res->nsaddr_list[i].sin_family; + + NSString *server; + if (family == AF_INET) { + char address[INET_ADDRSTRLEN]; // String representation of address + inet_ntop(AF_INET, & (res->nsaddr_list[i].sin_addr.s_addr), address, INET_ADDRSTRLEN); + server = [NSString stringWithUTF8String:address]; + if (!server) { + PJ_LOG(2, (__FILENAME__, "Could not create NSString for C String %s", address)); + continue; + } + } else if (family == AF_INET6) { + // TODO: This code is untested + char address[INET6_ADDRSTRLEN]; // String representation of address + inet_ntop(AF_INET6, &(res->nsaddr_list[i].sin_addr.s_addr), address, INET6_ADDRSTRLEN); + server = [NSString stringWithUTF8String:address]; + if (!server) { + PJ_LOG(2, (__FILENAME__, "Could not create NSString for C String %s", address)); + continue; + } + } else { + PJ_LOG(3, (__FILENAME__, "Unknown sin_family")); + continue; + } + + [servers addObject:server]; + } + } + } + + free(res); + return servers; +} + +- (void)setCodecPreferences { + pj_status_t status; + + const pj_str_t codec_id_opus = {"Opus", 4}; + status = pjsua_codec_set_priority(&codec_id_opus, 255); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Coud not set Opus priority")); + } + + const pj_str_t codec_id_h264 = {"H264", 4}; + pjmedia_vid_codec_param param; + status = pjsua_vid_codec_get_param(&codec_id_h264, ¶m); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Coud not get information for H264")); + return; + } + + status = pjsua_vid_codec_set_priority(&codec_id_h264, 255); + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Could not set H264 priority")); + } + + const pjmedia_vid_codec_info *info; + status = pjmedia_vid_codec_mgr_get_codec_info2(NULL, PJMEDIA_FORMAT_H264, &info); + + if (status != PJ_SUCCESS) { + PJ_LOG(2, (__FILENAME__, "Coud not get information for codec")); + return; + } + + /* A base16 (hexadecimal) representation of the following three bytes in the sequence parameter set NAL unit is specified in 1: 1) profile_idc, 2) a byte herein referred to as profile-iop, composed of the values of constraint_set0_flag, constraint_set1_flag, constraint_set2_flag, constraint_set3_flag, constraint_set4_flag, constraint_set5_flag, and reserved_zero_2bits in bit- significance order, starting from the most-significant bit, and 3) level_idc. */ + // https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Profiles + // https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Levels + // http://www.lighterra.com/papers/videoencodingh264/ + // http://stackoverflow.com/questions/22960928/identify-h264-profile-and-level-from-profile-level-id-in-sdp + // http://stackoverflow.com/questions/23494168/h264-profile-iop-explained + // https://supportforums.cisco.com/blog/150641/h264-profiles-cts-174 + + // 42 = Binary 0100 0010 (Baseline) + // 80 = Binary 1000 0000 (Bits 1 to 4 are flag 0 through 3. 0000 reserved 4 zero bits) + // 33 = Binary 001 1110 (Decimal 51 which equals level 5.1) + + param.dec_fmtp.param[0].name = pj_str("profile-level-id"); + param.dec_fmtp.param[0].val = pj_str("428033"); + + param.enc_fmtp.param[0].name = pj_str("profile-level-id"); + param.enc_fmtp.param[0].val = pj_str("428033"); + + pjmedia_rect_size maximumSize = {1024, 768}; + param.dec_fmt.det.vid.size = maximumSize; + param.enc_fmt.det.vid.size = maximumSize; + + param.enc_fmt.det.vid.fps.num = 30; + param.enc_fmt.det.vid.fps.denum = 1; + param.dec_fmt.det.vid.fps.num = 30; + param.dec_fmt.det.vid.fps.denum = 1; + + param.enc_fmt.det.vid.avg_bps = 1500000; + param.enc_fmt.det.vid.max_bps = 2500000; + param.dec_fmt.det.vid.avg_bps = 1500000; + param.dec_fmt.det.vid.max_bps = 2500000; + + // 640 x 480 + // param.enc_fmt.det.vid.avg_bps = 768000; + // param.enc_fmt.det.vid.max_bps = 102400; + // param.dec_fmt.det.vid.avg_bps = 768000; + // param.dec_fmt.det.vid.max_bps = 102400; + status = pjmedia_vid_codec_mgr_set_default_param(NULL, info, ¶m); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Coud not set default parameters for video")); + return; + } +} + +- (void)willEnterForegroundNotification:(NSNotification *)notification +{ + if (_account != nil) { + if (_account.status == GSAccountStatusOffline || + _account.status == GSAccountStatusInvalid || + _account.status == GSAccountStatusDisconnecting) { + PJ_LOG(3, (__FILENAME__, "Entering Foreground: Account status is offline, invalid or disconnecting, so we will try to connect again. Account status is %d", _account.status)); + [_account connect]; + } else { + PJ_LOG(3, (__FILENAME__, "Entering Foreground: Account status is not offline, invalid or disconnecting, so we will not try to connect again. Account status is %d", _account.status)); + } + } else { + PJ_LOG(3, (__FILENAME__, "Entering Foreground: Account is missing")); + } +} + +- (void)deviceOrientationChanged:(NSNotification *)notification +{ +#if PJMEDIA_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; + +// PJ_LOG(3, (__FILENAME__, "Device orientation changed: %ld", (prev_ori = dev_ori))); + + if (dev_ori >= UIDeviceOrientationPortrait && + dev_ori <= UIDeviceOrientationLandscapeRight) + { + if (!pj_thread_is_registered()) { + pj_thread_register("Gossip", 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--) { + pj_status_t status = pjsua_vid_dev_set_setting(i, PJMEDIA_VID_DEV_CAP_ORIENTATION, + &pj_ori[dev_ori-1], PJ_TRUE); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Could not set the video device orientation")); + } + } + } +#endif +} + - (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillEnterForegroundNotification + object:nil]; + +#if PJMEDIA_HAS_VIDEO + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +#endif + if (_transportId != PJSUA_INVALID_ID) { pjsua_transport_close(_transportId, PJ_TRUE); _transportId = PJSUA_INVALID_ID; @@ -52,16 +265,9 @@ - (void)dealloc { pjsua_destroy(); } - _account = nil; - _config = nil; _status = GSUserAgentStateDestroyed; } - -- (GSConfiguration *)configuration { - return _config; -} - - (GSUserAgentState)status { return _status; } @@ -72,10 +278,31 @@ - (void)setStatus:(GSUserAgentState)status { [self didChangeValueForKey:@"status"]; } - - (BOOL)configure:(GSConfiguration *)config { - GSAssert(!_config, @"Gossip: User agent is already configured."); - _config = [config copy]; + NSAssert([NSThread isMainThread], @"We must be called on the main thread"); + + if (self.status != GSUserAgentStateUninitialized && self.status != GSUserAgentStateDestroyed) { + return NO; + } + + static pj_thread_desc a_thread_desc; + static pj_thread_t *a_thread; + + if (!pj_thread_is_registered()) { + pj_thread_register("Gossip", a_thread_desc, &a_thread); + } + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged:) + name:kGSReachabilityChangedNotification + object:nil]; + + _reachability = [GSReachability reachabilityForInternetConnection]; + [_reachability startNotifier]; + [self logReachabilityStatus:_reachability]; + + GSAssert(!_configuration, @"Gossip: User agent is already configured."); + _configuration = [config copy]; // create agent GSReturnNoIfFails(pjsua_create()); @@ -86,74 +313,240 @@ - (BOOL)configure:(GSConfiguration *)config { pjsua_logging_config logConfig; pjsua_media_config mediaConfig; + pjsua_logging_config_default(&logConfig); + pjsua_media_config_default(&mediaConfig); pjsua_config_default(&uaConfig); + + // Enable STUN + if (_configuration.STUNServers != nil) { + // We have TURN, so ignore STUN failures + uaConfig.stun_ignore_failure = PJ_TRUE; + + NSUInteger desiredServerCount = _configuration.STUNServers.count; + size_t stun_srv_limit = sizeof(uaConfig.stun_srv) / sizeof(uaConfig.stun_srv[0]); + for (NSUInteger i = 0; i < desiredServerCount; i++) { + NSString *server = _configuration.STUNServers[i]; + uaConfig.stun_srv[i] = [GSPJUtil PJStringWithString:server]; + if (i == (stun_srv_limit - 1)) { + break; + } + } + + uaConfig.stun_srv_cnt = MIN((unsigned)desiredServerCount, (unsigned)stun_srv_limit); + } + + // Enable ICE for all accounts + mediaConfig.enable_ice = PJ_TRUE; + + uaConfig.use_srtp = PJMEDIA_SRTP_MANDATORY; + uaConfig.srtp_secure_signaling = 1; + + pj_status_t status = pjsip_register_hdr_parser("Reason", NULL, &parse_hdr_reason); + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Could not register RFC 3326 Reason parsing method")); + return NO; + } + [GSDispatch configureCallbacksForAgent:&uaConfig]; - pjsua_logging_config_default(&logConfig); - logConfig.level = _config.logLevel; - logConfig.console_level = _config.consoleLogLevel; + logConfig.level = _configuration.logLevel; + logConfig.console_level = _configuration.consoleLogLevel; + logConfig.msg_logging = _configuration.logMessages == YES ? PJ_TRUE : PJ_FALSE; - pjsua_media_config_default(&mediaConfig); - mediaConfig.clock_rate = _config.clockRate; - mediaConfig.snd_clock_rate = _config.soundClockRate; - mediaConfig.ec_tail_len = 0; // not sure what this does (Siphon use this.) + mediaConfig.clock_rate = _configuration.clockRate; + mediaConfig.snd_clock_rate = _configuration.soundClockRate; + + /* Echo canceller. The software AEC probably is the most CPU intensive module in PJSIP. To reduce the CPU usage, shorten the EC tail length to lower value (the pjsua_media_config.ec_tail_len setting), or even disable it altogether by setting pjsua_media_config.ec_tail_len to zero. */ + // mediaConfig.ec_tail_len = 0; GSReturnNoIfFails(pjsua_init(&uaConfig, &logConfig, &mediaConfig)); - // Configure the DNS resolvers to also handle SRV records - pjsip_endpoint* endpoint = pjsua_get_pjsip_endpt(); - pj_dns_resolver* resolver; - pj_str_t google_dns = [GSPJUtil PJStringWithString:@"8.8.8.8"]; - struct pj_str_t servers[] = { google_dns }; - GSReturnNoIfFails(pjsip_endpt_create_resolver(endpoint, &resolver)); - GSReturnNoIfFails(pj_dns_resolver_set_ns(resolver, 1, servers, nil)); - GSReturnNoIfFails(pjsip_endpt_set_resolver(endpoint, resolver)); - - // create UDP transport - // TODO: Make configurable? (which transport type to use/other transport opts) + GSReturnNoIfFails([self updateDNSServers]); + // TODO: Make separate class? since things like public_addr might be useful to some. pjsua_transport_config transportConfig; pjsua_transport_config_default(&transportConfig); pjsip_transport_type_e transportType = 0; - switch (_config.transportType) { - case GSUDPTransportType: transportType = PJSIP_TRANSPORT_UDP; break; - case GSUDP6TransportType: transportType = PJSIP_TRANSPORT_UDP6; break; - case GSTCPTransportType: transportType = PJSIP_TRANSPORT_TCP; break; - case GSTCP6TransportType: transportType = PJSIP_TRANSPORT_TCP6; break; + switch (_configuration.transportType) { + case GSTransportTypeUDP: transportType = PJSIP_TRANSPORT_UDP; break; + case GSTransportTypeUDP6: transportType = PJSIP_TRANSPORT_UDP6; break; + case GSTransportTypeTCP: transportType = PJSIP_TRANSPORT_TCP; break; + case GSTransportTypeTCP6: transportType = PJSIP_TRANSPORT_TCP6; break; + case GSTransportTypeTLS: transportType = PJSIP_TRANSPORT_TLS; break; + case GSTransportTypeTLS6: transportType = PJSIP_TRANSPORT_TLS6; break; } GSReturnNoIfFails(pjsua_transport_create(transportType, &transportConfig, &_transportId)); [self setStatus:GSUserAgentStateConfigured]; - + + [self setCodecPreferences]; + // configure account _account = [[GSAccount alloc] init]; - return [_account configure:_config.account]; + return [_account configure:_configuration.account]; } +- (pj_status_t)updateDNSServers { + // Configure the DNS resolvers to handle SRV records + NSMutableArray *DNSServers = [self systemDNSServers]; + pjsip_endpoint *endpoint = pjsua_get_pjsip_endpt(); + if (DNSServers) { + PJ_LOG(2, (__FILENAME__, "Current system DNS servers: %s", DNSServers.description.UTF8String)); + pj_dns_resolver *resolver; + + NSUInteger count = DNSServers.count; + + pj_str_t *servers = malloc(sizeof(pj_str_t) * count); + + for (NSUInteger i = 0; i < count; i++) { + NSString *server = DNSServers[i]; + pj_str_t pj_DNS = [GSPJUtil PJStringWithString:server]; + servers[i] = pj_DNS; + } + + pj_status_t status = pjsip_endpt_create_resolver(endpoint, &resolver); + if (status != PJ_SUCCESS) { + GSLogPJSIPError(status); + free(servers); + return PJ_FALSE; + } + + status = pj_dns_resolver_set_ns(resolver, 1, servers, nil); + free(servers); + if (status != PJ_SUCCESS) { + GSLogPJSIPError(status); + return PJ_FALSE; + } + + return pjsip_endpt_set_resolver(endpoint, resolver); + } else { + PJ_LOG(2, (__FILENAME__, "Can not get system DNS Servers. Disabling custom DNS in PJSIP.")); + return pjsip_endpt_set_resolver(endpoint, NULL); + } + + return PJ_FALSE; +} + +- (BOOL)updateSTUNServers { + if (_configuration.STUNServers == nil) { + PJ_LOG(3, (__FILENAME__, "Can not update STUN servers with an empty set, please disable STUN on the account instead")); + return false; + } + + NSUInteger desiredServerCount = _configuration.STUNServers.count; + + pj_pool_t *pool = pjsua_pool_create("GSUserAgent", 1000, 1000); + if (pool == NULL) { + return NO; + } + + // 8 is the max in pjsua_config. TODO: Get this dynamically? + pj_size_t stun_srv_limit = 8; + pj_str_t *stun_srv = pj_pool_calloc(pool, stun_srv_limit, sizeof(pj_str_t)); + + pj_pool_release(pool); + + if (stun_srv == NULL) { + return NO; + } + + for (NSUInteger i = 0; i < desiredServerCount; i++) { + NSString *server = _configuration.STUNServers[i]; + stun_srv[i] = [GSPJUtil PJStringWithString:server]; + if (i == (stun_srv_limit - 1)) { + break; + } + } + + unsigned count = MIN((unsigned)desiredServerCount, (unsigned)stun_srv_limit); + + /* If \a wait parameter is non-zero, this will return + * PJ_SUCCESS if one usable STUN server is found. + * Otherwise it will always return PJ_SUCCESS, and + * application will be notified about the result in + * the callback #on_stun_resolution_complete. */ + pj_status_t status = pjsua_update_stun_servers(count, stun_srv, PJ_FALSE); + if (status != PJ_SUCCESS) { + return NO; + } + + return YES; +} - (BOOL)start { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(willEnterForegroundNotification:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + +#if PJMEDIA_HAS_VIDEO + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deviceOrientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications]; +#endif + + if (self.status == GSUserAgentStateStarted) { + return NO; + } + + static pj_thread_desc a_thread_desc; + static pj_thread_t *a_thread; + + if (!pj_thread_is_registered()) { + pj_thread_register("Gossip", a_thread_desc, &a_thread); + } + GSReturnNoIfFails(pjsua_start()); [self setStatus:GSUserAgentStateStarted]; return YES; } - (BOOL)reset { + if (self.status == GSUserAgentStateDestroyed) { + return NO; + } + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillEnterForegroundNotification + object:nil]; + +#if PJMEDIA_HAS_VIDEO + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +#endif + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:kGSReachabilityChangedNotification + object:nil]; + [_reachability stopNotifier]; + _reachability = nil; + + static pj_thread_desc a_thread_desc; + static pj_thread_t *a_thread; + + if (!pj_thread_is_registered()) { + pj_thread_register("Gossip", a_thread_desc, &a_thread); + } + [_account disconnect]; - + // needs to nil account before pjsua_destroy so pjsua_acc_del succeeds. _transportId = PJSUA_INVALID_ID; _account = nil; - _config = nil; - NSLog(@"Destroying..."); + _configuration = nil; + PJ_LOG(3, (__FILENAME__, "Destroying...")); GSReturnNoIfFails(pjsua_destroy()); [self setStatus:GSUserAgentStateDestroyed]; return YES; } - - (NSArray *)arrayOfAvailableCodecs { - GSAssert(!!_config, @"Gossip: User agent not configured."); + GSAssert(!!_configuration, @"Gossip: User agent not configured."); NSMutableArray *arr = [[NSMutableArray alloc] init]; @@ -172,4 +565,95 @@ - (NSArray *)arrayOfAvailableCodecs { return [NSArray arrayWithArray:arr]; } +- (void)backgroundKeepAliveHandler { + static pj_thread_desc a_thread_desc; + static pj_thread_t *a_thread; + int i; + + if (!pj_thread_is_registered()) { + pj_thread_register("Gossip", 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); + } + } + + // pj_thread_sleep(5000); +} + +#pragma mark - Reachability + +- (void)logReachabilityStatus:(GSReachability *)currentReachability { + GSNetworkStatus netStatus = [currentReachability currentReachabilityStatus]; + BOOL connectionRequired = [currentReachability connectionRequired]; + + switch (netStatus) { + case GSNotReachable: + PJ_LOG(3, (__FILENAME__, "Network Not Available... Disconnecting Account")); + connectionRequired = NO; + break; + case GSReachableViaWiFi: + PJ_LOG(3, (__FILENAME__, "Reachable via Wi-Fi..")); + break; + case GSReachableViaWWAN: + PJ_LOG(3, (__FILENAME__, "Reachable via WWAN..")); + break; + } + + if (connectionRequired) { + PJ_LOG(3, (__FILENAME__, "New SIP REGISTRATION required")); + } +} + +- (void)reachabilityChanged:(NSNotification *)notification +{ + pj_status_t status = [self updateDNSServers]; + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Reachability changed: could not update DNS servers...")); + } else { + PJ_LOG(3, (__FILENAME__, "Reachability changed: updated DNS servers...")); + } + + [[NSNotificationCenter defaultCenter] postNotificationName:GSUserAgentNetworkReachabilityChangedNotification object:notification.object userInfo:notification.userInfo]; + + GSReachability *currentReachability = notification.object; + + NSAssert([currentReachability isKindOfClass:GSReachability.class], @"Wrong reachability class"); + [self logReachabilityStatus:currentReachability]; + + GSNetworkStatus netStatus = [currentReachability currentReachabilityStatus]; + + if (netStatus == GSNotReachable) { + status = [self.account disconnectWithoutReachability]; + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Could not disconnect account")); + } + } else { + if (self.account.status != GSAccountStatusConnected && self.account.status != GSAccountStatusConnecting) { + PJ_LOG(3, (__FILENAME__, "Network Is Available... Will Reconnect Account with Status %d", self.account.status)); + [self.account connect]; + } else { + PJ_LOG(3, (__FILENAME__, "Network Is Available... Will NOT reconnect account with status %d", self.account.status)); + } + } + + if ([currentReachability currentReachabilityStatus] != GSNotReachable && + ![currentReachability connectionRequired]) { + status = [self.account networkAddressChanged]; + if (status != PJ_SUCCESS) { + PJ_PERROR(1, (__FILENAME__, status, "Could not update network address for account")); + } + } +} + +- (GSNetworkStatus)currentReachabilityStatus { + return _reachability.currentReachabilityStatus; +} + @end diff --git a/Gossip/Gossip.h b/Gossip/Gossip.h index 2643b6830..a87ef9292 100644 --- a/Gossip/Gossip.h +++ b/Gossip/Gossip.h @@ -5,14 +5,11 @@ // Created by Chakrit Wichian on 7/6/12. // -/** /file - * /brief Convenience import for application developer. - */ - - #import "GSConfiguration.h" #import "GSAccountConfiguration.h" #import "GSUserAgent.h" #import "GSAccount.h" #import "GSCall.h" +#import "GSOutgoingCall.h" #import "GSCodecInfo.h" +#import "GSReachability.h" diff --git a/Gossip/RFC3326ReasonParser.c b/Gossip/RFC3326ReasonParser.c new file mode 100644 index 000000000..e21b32b85 --- /dev/null +++ b/Gossip/RFC3326ReasonParser.c @@ -0,0 +1,140 @@ +// +// RFC3326ReasonParser.c +// Gossip +// + +#include "RFC3326ReasonParser.h" + +#import + +const pj_str_t pjsip_reason_header_name = {"Reason", 6}; + +static int pjsip_reason_hdr_print(pjsip_reason_hdr *hdr, char *buf, pj_size_t size); +static pjsip_reason_hdr * pjsip_reason_hdr_clone( pj_pool_t *pool, const pjsip_reason_hdr *hdr); +static pjsip_reason_hdr * pjsip_reason_hdr_shallow_clone( pj_pool_t *pool, const pjsip_reason_hdr *hdr); + +static pjsip_hdr_vptr reason_hdr_vptr = +{ + (pjsip_hdr_clone_fptr) &pjsip_reason_hdr_clone, + (pjsip_hdr_clone_fptr) &pjsip_reason_hdr_shallow_clone, + (pjsip_hdr_print_fptr) &pjsip_reason_hdr_print, +}; + +PJ_DEF(pjsip_reason_hdr *) pjsip_reason_hdr_init(pj_pool_t *pool, void *mem) { + pjsip_reason_hdr *hdr = (pjsip_reason_hdr *)mem; + + PJ_UNUSED_ARG(pool); + + pj_bzero(mem, sizeof(pjsip_reason_hdr)); + init_hdr(hdr, PJSIP_H_OTHER, &reason_hdr_vptr); + return hdr; +} + +PJ_DEF(pjsip_reason_hdr *) pjsip_reason_hdr_create(pj_pool_t *pool) { + void *mem = pj_pool_alloc(pool, sizeof(pjsip_cseq_hdr)); + return pjsip_reason_hdr_init(pool, mem); +} + +/// TODO: This implementation is untested +static int pjsip_reason_hdr_print(pjsip_reason_hdr *hdr, char *buf, pj_size_t size) { + int returnVal = snprintf(buf, size, "Reason: %s;cause=%lu;text=\"%s\"", hdr->reason.ptr, hdr->cause, hdr->text.ptr); + + if (returnVal < 0) { + PJ_LOG(3, ("RFC3326ReasonParser", "Error in pjspip_reason_hdr_print")); + } + + return returnVal; +} + +static pjsip_reason_hdr * pjsip_reason_hdr_clone(pj_pool_t *pool, const pjsip_reason_hdr *rhs) { + pjsip_reason_hdr *hdr = pjsip_reason_hdr_create(pool); + + hdr->type = rhs->type; + hdr->name = rhs->name; + hdr->sname = rhs->sname; + + hdr->cause = rhs->cause; + pj_strdup(pool, &hdr->reason, &rhs->reason); + pj_strdup(pool, &hdr->text, &rhs->text); + return hdr; +} + +static pjsip_reason_hdr * pjsip_reason_hdr_shallow_clone(pj_pool_t *pool, const pjsip_reason_hdr *rhs) { + pjsip_reason_hdr *hdr = PJ_POOL_ALLOC_T(pool, pjsip_reason_hdr); + pj_memcpy(hdr, rhs, sizeof(*hdr)); + return hdr; +} + +/* Case insensitive comparison */ +#define parser_stricmp(s1, s2) (s1.slen!=s2.slen || pj_stricmp_alnum(&s1, &s2)) + +/* Parse Reason header. */ +PJ_DEF(pjsip_hdr *) parse_hdr_reason(pjsip_parse_ctx *ctx) { + // "Reason: SIP;cause=200;text="Answered elsewhere"\r\n\r\n" + // Once the header is parsed you get this: "SIP;cause=200;text=\"Answered elsewhere\"\r\n\r\n" + pj_scanner *scanner = ctx->scanner; + pjsip_reason_hdr *hdr = pjsip_reason_hdr_create(ctx->pool); + + // Scan 'Reason: SIP;' + pj_scan_get_until_ch(scanner, ';', &hdr->reason); + pj_strrtrim(&hdr->reason); + + if (*scanner->curptr == ';' && !pj_scan_is_eof(scanner)) { + pj_scan_advance_n(scanner, 1, PJ_TRUE); + } + + // Scan list key=value; fields + // This can be turned into a struct if more headers are needed + static const pj_str_t keyCause = {"cause", 5}; + static const pj_str_t keyText = {"text", 4}; + + pj_str_t key; + pj_str_t value; + while (!pj_scan_is_eof(scanner)) { + pj_scan_get_until_ch(scanner, '=', &key); + pj_strrtrim(&key); + + if (*scanner->curptr == '=' && !pj_scan_is_eof(scanner)) { + pj_scan_advance_n(scanner, 1, PJ_TRUE); + } + + if (!key.slen) { + continue; + } + + if (parser_stricmp(key, keyCause) == 0) { + pj_scan_get_until_ch(scanner, ';', &value); + if (!value.slen) { + continue; + } + + pj_strrtrim(&value); + hdr->cause = pj_strtoul(&value); + + if (*scanner->curptr == ';' && !pj_scan_is_eof(scanner)) { + pj_scan_advance_n(scanner, 1, PJ_TRUE); + } + } else if (parser_stricmp(key, keyText) == 0) { + pj_scan_get_quote(scanner, '"', '"', &value); + + if (!value.slen) { + continue; + } + + /* Remove the quotes */ + value.ptr++; + value.slen -= 2; + + pj_strrtrim(&value); + hdr->text = value; + + if (*scanner->curptr == ';' && !pj_scan_is_eof(scanner)) { + pj_scan_advance_n(scanner, 1, PJ_TRUE); + } + } else { + pj_scan_get_newline(scanner); + } + } + + return (pjsip_hdr *)hdr; +} diff --git a/Gossip/RFC3326ReasonParser.h b/Gossip/RFC3326ReasonParser.h new file mode 100644 index 000000000..54d59a2c3 --- /dev/null +++ b/Gossip/RFC3326ReasonParser.h @@ -0,0 +1,20 @@ +// +// RFC3326ReasonParser.h +// Gossip +// + +#import "PJSIP.h" + +/** + * Reason header. + */ +typedef struct pjsip_reason_hdr +{ + PJSIP_DECL_HDR_MEMBER(struct pjsip_reason_hdr); + pj_str_t reason; /**< Reason text. */ + unsigned long cause; /**< Cause code. */ + pj_str_t text; /**< Text. */ +} pjsip_reason_hdr; + +extern const pj_str_t pjsip_reason_header_name; +extern pjsip_hdr * parse_hdr_reason(pjsip_parse_ctx *ctx); diff --git a/Gossip/Util.h b/Gossip/Util.h index 4750772fa..fec611517 100644 --- a/Gossip/Util.h +++ b/Gossip/Util.h @@ -5,29 +5,26 @@ // Created by Chakrit Wichian on 7/6/12. // - // additional util imports #import "GSPJUtil.h" - // just in case we need to compile w/o assertions #define GSAssert NSAssert - // PJSIP status check macros -#define GSLogSipError(status_) \ - NSLog(@"Gossip: %@", [GSPJUtil errorWithSIPStatus:status_]); +#define GSLogPJSIPError(status_) \ + PJ_LOG(3, (__FILENAME__, "Gossip: %s", [GSPJUtil errorWithSIPStatus:status_].description.UTF8String)); #define GSLogIfFails(aStatement_) do { \ pj_status_t status = (aStatement_); \ if (status != PJ_SUCCESS) \ - GSLogSipError(status); \ + GSLogPJSIPError(status); \ } while (0) #define GSReturnValueIfFails(aStatement_, returnValue_) do { \ pj_status_t status = (aStatement_); \ if (status != PJ_SUCCESS) { \ - GSLogSipError(status); \ + GSLogPJSIPError(status); \ return returnValue_; \ } \ } while(0) @@ -35,3 +32,5 @@ #define GSReturnIfFails(aStatement_) GSReturnValueIfFails(aStatement_, ) #define GSReturnNoIfFails(aStatement_) GSReturnValueIfFails(aStatement_, NO) #define GSReturnNilIfFails(aStatement_) GSReturnValueIfFails(aStatement_, nil) + +#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)