PJSIP开发VoIP记录3-通话的实现
开发工具:Xcode9.2
开发语言:swift 4.0
时隔两月,今天继续探究PISIP进行通话的实现
1.准备工作
1.1通话服务器
要进行通话,首先需要一个通话服务器,有很多关于通话服务器搭建的教程可以研究一下,我这里推荐一种简单的方式,直接使用linphone的服务器sip.linphone.org(还可以和linphone上的用户通话哟)。如果你的服务器连接了运营商的PSTN,那么就可以拨打任意电话了喔,跟普通电话没有任何区别。
1.2测试账号
需要准备两个账号,一个拨打,一个接听。
进入linphone官网滑动到下边点击注册
填写相关信息就ok了
当然也可以直接在App Store上下载linphone
APP进行注册。
1.3两台通话设备
如果你有两台iOS设备(iPhone或者iPad),那么可以直接在两台设备上运行自己写的代码进行通话测试,如果只有一台,那么可以在App Store上下载linphone
APP,在linphone上登录一个账号,用自己写的代码拨打这个号码测试,不过接通会存在问题,麦克风资源无法分配。当然Android也是有linphone的,所以另一台设备是Android也可以下载linphone
进行测试。
2.实现通话
2.1登录通话服务器
登录服务器界面用xib简单画一下就好:
TextField:第一个是服务器地址,第二个是用户名,第三个是用户密码。
登录服务器:
@IBAction func loginAction(_ sender: UIButton) {
guard let userName = userNameTF.text,
let passWord = passWordTF.text,
let serveAddress = hostTF.text
else { return }
var acc_id = pjsua_acc_id()
var cfg = pjsua_acc_config()
pjsua_acc_config_default(&cfg)
let userId = strdup("sip:\(userName)@\(serveAddress)")
cfg.id = pj_str(userId)
cfg.reg_uri = pj_str(strdup("sip:\(serveAddress)"))
cfg.reg_retry_interval = 0
cfg.cred_count = 1
cfg.cred_info.0.realm = pj_str(strdup("*"))
cfg.cred_info.0.username = pj_str(strdup("\(userName)"))
cfg.cred_info.0.data_type = Int32(PJSIP_CRED_DATA_PLAIN_PASSWD.rawValue)
cfg.cred_info.0.data = pj_str(strdup("\(passWord)"))
let status = pjsua_acc_add(&cfg, pj_bool_t(PJ_TRUE.rawValue), &acc_id)
if status != PJ_SUCCESS.rawValue{
print("register error: 登录失败,返回错误号:\(status)")
}
}
在上一篇中有讲到获取到登录结果后我们会发一个名叫n_on_reg_state
的通知,在登录界面中我们先对这个通知进行监听,当监听到登录成功后保存一下当前登录用户的id和服务器地址,在拨打电话的时候我们需要用到这些信息,我这里选择UserDefaults存储,当然也可以用一个全局变量存储,保存完后就可以跳转到拨号界面进行拨号测试:
NotificationCenter.default.addObserver(self, selector: #selector(handleRegisterStatus(notification:)), name: n_on_reg_state, object: nil)
@objc func handleRegisterStatus(notification: Notification){
guard let userInfo = notification.userInfo else { return }
let acc_id = userInfo["acc_id"] as! pjsua_acc_id
let status = userInfo["status"] as! pjsip_status_code
let statusText = userInfo["status_text"] as! String
if status != PJSIP_SC_OK {
print("登录失败,错误信息:\(status),\(statusText)")
return
}
UserDefaults.standard.set(acc_id, forKey: login_account_id)
UserDefaults.standard.set(hostTF.text, forKey: server_uri)
UserDefaults.standard.synchronize()
//跳转拨号页面
present(CallViewController(), animated: true, completion: nil)
}
2.2拨号与挂断
拨号界面也用xib画一下:
TextField中输入需要拨打的号码,这里的号码就是刚刚注册的另一个账号(非现在登录账号)的用户名,点击拨打按钮即可进行拨打。
拨号:
@IBAction func makeCallAction(_ sender: UIButton) {
let acc_id = UserDefaults.standard.object(forKey: login_account_id) as! pjsua_acc_id
let server = UserDefaults.standard.object(forKey: server_uri) as! String
let targetUri = "sip:\(phoneNumTF.text!)@\(server)"
var status = pj_status_t()
var dest_uri = pj_str(strdup(targetUri))
status = pjsua_call_make_call(acc_id, &dest_uri, nil, nil, nil, &call_id)
if status != PJ_SUCCESS.rawValue {
print("外拨错误, 错误信息: \(status):\(pjsip_get_status_text(status))")
}else{
print("拨打成功")
}
}
挂断:
@IBAction func hangUp(){
let status = pjsua_call_hangup(call_id, 0, nil, nil)
if status != PJ_SUCCESS.rawValue {
print("挂断错误, 错误信息: \(status):\(pjsip_get_status_text(status))")
}
}
想要知道当前通话状态怎么办?比如实际开发中需要知道正在连线中,对方接听后需要开始显示计时,对方挂断等,很简单,在上一篇中,通话状态改变我们会发一个叫做n_on_call_state
的通知,监听一下这个通知即可,state
是一个枚举,其他类型可以点进去细看,这里我只罗列几种:
NotificationCenter.default.addObserver(self, selector: #selector(handleCallStatusChanged(notification:)), name: n_on_call_state, object: nil)
@objc func handleCallStatusChanged(notification: Notification){
guard let userInfo = notification.userInfo else { return }
let call_id = userInfo["call_id"] as! pjsua_call_id
let state = userInfo["state"] as! pjsip_inv_state
guard call_id == self.call_id else { return }
switch state {
case PJSIP_INV_STATE_CONNECTING:
print("状态:--- 会话连接中")
case PJSIP_INV_STATE_CONFIRMED:
print("状态:--- 对方接听")
case PJSIP_INV_STATE_DISCONNECTED:
print("状态:--- 会话终止")
default:
print("状态:--- 其它:\(state)")
}
}
2.3接听
在上一篇中我们对来电进行了监听,当有来电的时候就会present这个界面,可以选择接听或者挂断,挂断方法和上边是同一个方法。接听需要指定需要接听的来电id,也就是
callId
,这个id在监听来电信息时获取接听:
@IBAction func answerAction(_ sender: UIButton) {
pjsua_call_answer(callId, 200, nil, nil)
}
在这个界面我们也可以接收n_on_call_state
通知来判断当前通话状态。
3.总结
以上代码就可以实现一个简单的VoIP通话APP了,可以实现最基本的拨号、接听与挂断,但是实际开发需要学习的远不止这些,比如来系统电冲突,电话多通等等都需要开发者处理。如果你连接了PSTN,需要拨打如 10086 、银行等这类电话,需要你根据语音进行按键选择服务,这就需要使用DTMF,或者通话过程中突然要和身边的人说话却又不想让电话对方的人听见,这就需要进行静音操作等。这里顺便提供一下发送DTMF的方法和静音方法
DTMF
这是我实际开发中的通话界面
发送DTMF:
func sendDTMF(sender: TBDialpadButton) {
guard let number = sender.numLabel.text else { return }
dtmfLabel.text! += number
var dtmf = pj_str(strdup(number))
pjsua_call_dial_dtmf(pjsip_call_id, &dtmf)
}
静音
上一篇文章中说过,在获取通话信息时我们拿到了pjsipConfAudioId,通过pjsipConfAudioId我们可以将当前通话设置静音,设置完成后,我们说话对方听不见,但是对方说话我们依然能听见。
@objc func muteBtnClicked(sender: TBTopImageButton){
sender.isSelected = !sender.isSelected
sender.topImageView.image = sender.isSelected ? #imageLiteral(resourceName: "icon_mute_fill") : #imageLiteral(resourceName: "icon_mute")
if pjsipConfAudioId != nil{
let _ = sender.isSelected ? pjsua_conf_disconnect(0, pjsipConfAudioId!) : pjsua_conf_connect(0, pjsipConfAudioId!)
}
}
在实际开发中涉及到用户账号切换,在看了PJSIP源码后并未找到退出登录的方法,但是找到这个API,于是有了下面这个方法:
func logOut(){
if TBUserInfo.shared.PJSIPInfo.acc_id != nil{
pjsua_acc_del(TBUserInfo.shared.PJSIPInfo.acc_id!)
TBUserInfo.shared.resetPJSIP()
}
}
调用pjsua_acc_del
将当前登录的id删除,实现退出登录。