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__)