PJSIP开发VoIP记录2 - 配置
开发工具:Xcode9.2
开发语言:swift 4.0
自从写了上一篇集成后,一直忙于工作,忙到连说话的都没有多余的精力。趁今天节日放假,继续来探究PJSIP的配置。
为了模块化管理,我新建了一个TBPJSIPManager
单例,将PJSIP的相关方法放到这里统一管理。
如图:
好了,现在正式开始配置PJSIP,配置方法在单例中定义为func configPJSIP()
,在调用PSJIP相关操作基本都会返回一个是否成功的状态,这个状态的类型是pj_status_t
,当这个值是PJ_SUCCESS.rawValue
则表示操作成功
1.创建SUA
var status: pj_status_t = pjsua_create()
if status != PJ_SUCCESS.rawValue{
print("error create pjsua")
}
这里将status
定义为变量是因为后面的配置可以继续使用这个变量,而不再重复创建
2.配置SUA
先声明三个配置变量,分别配置pjsua、pjsua_media、pjsua_logging。
var cfg = pjsua_config()
var media_cfg = pjsua_media_config()
var log_cfg = pjsua_logging_config()
2.1 回调函数配置(pjsua_config())
回调函数配置其实就是将上一步的pjsua_config()
赋值给SUA并绑定相关属性
pjsua_config_default(&cfg)
cfg.cb.on_incoming_call = on_incoming_call
cfg.cb.on_call_media_state = on_call_media_state
cfg.cb.on_call_state = on_call_state
cfg.cb.on_reg_state = on_reg_state
等号后面跟的on_incoming_call
、on_call_media_state
、on_call_state
、on_reg_state
分别是来电、媒体、电话状态、登录状态的回调(闭包),为了方便,我直接将其命名为cb
的对应属性名,如果认为这样的命名方式不好可以另起名字。值得一提的是,PJSIP提供的reg方法实质是登录服务器方法,而不是我们理解的注册方法,需要服务器有该用户才能reg成功。(目前我没发现移动端注册服务器的API,或者是我本身理解出错,如果有人知道,敬请指正)
2.1.1 声明相关回调
上一步中的四个回调函数我们并未声明,会报错,接下来我们声明这四个回调,实质就是swift中的闭包,将他们声明为TBPJSIPManager
的常量
来电回调
let on_incoming_call: (@convention(c) (pjsua_acc_id, pjsua_call_id, UnsafeMutablePointer<pjsip_rx_data>?) -> Void) = { (acc_id, call_id, rdata) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
let remote_info = NSString(utf8String: ci.remote_info.ptr)
guard let startIndex = remote_info?.range(of: "<").location else { return }
guard let endIndex = remote_info?.range(of: ">").location else { return }
var remote_address = remote_info?.substring(with: NSMakeRange(startIndex + 1, endIndex - startIndex - 1))
remote_address = remote_address?.components(separatedBy: ":")[1]
let argument = ["call_id":call_id,"remote_address":remote_address ?? "has no remote_address"] as [String : Any]
DispatchQueue.main.async {
NotificationCenter.default.post(name: n_on_incoming_call, object: nil, userInfo: argument)
}
}
全局常量通知名字
let n_on_reg_state = NSNotification.Name("on_reg_state")
let n_on_call_state = NSNotification.Name("on_call_state")
let n_on_incoming_call = NSNotification.Name("on_incoming_call")
通过remote_info
我们可以拿到很多来电信息,其中remote_address
是截取来电人信息,是拨打电话时进行的配置,一般配置为名字或者电话号码。当获取到需要的信息后,将需要的信息保存到字典,通过通知在主线程进行发送,需要这些信息的地方接收广播就可以进行相应处理。以下配置道理一模一样
媒体状态回调
let on_call_media_state: (@convention(c) (pjsua_call_id) -> Void) = { (call_id) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
if ci.media_status == PJSUA_CALL_MEDIA_ACTIVE {
pjsua_conf_connect(ci.conf_slot, 0)
pjsua_conf_connect(0, ci.conf_slot)
}
}
电话状态回调
let on_call_state: (@convention(c) (pjsua_call_id, UnsafeMutablePointer<pjsip_event>?) -> Void) = { (call_id, event) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
let argument = ["call_id":call_id, "state":ci.state, "pjsipConfAudioId":ci.conf_slot] as [String : Any]
DispatchQueue.main.async {
NotificationCenter.default.post(name: n_on_call_state, object: nil, userInfo: argument)
}
}
提取pjsipConfAudioId
是因为后面配置通话静音需要这个值
注册状态回调(暂且叫注册吧)
let on_reg_state: (@convention(c) (pjsua_acc_id) -> Void) = { (acc_id) in
var status = pj_status_t()
var info = pjsua_acc_info()
status = pjsua_acc_get_info(acc_id, &info)
if status != PJ_SUCCESS.rawValue{
return
}
let argument = ["acc_id":acc_id, "status_text":String(utf8String: info.status_text.ptr) ?? "has no status_text", "status":info.status] as [String : Any]
DispatchQueue.main.async {
NotificationCenter.default.post(name: n_on_reg_state, object: nil, userInfo: argument)
}
}
2.2 媒体相关配置(pjsua_media_config())
pjsua_media_config_default(&media_cfg)
media_cfg.clock_rate = 16000
media_cfg.snd_clock_rate = 16000
media_cfg.ec_tail_len = 0
这三个属性大概是处理时钟频率和声音去噪,可以看看.h文件中的具体介绍
2.3 日志相关配置(pjsua_logging_config())
日志配置分正式环境和开发环境,开发环境需要日志,但是正式环境并不需要调试日志,所以可以关掉,我定义了一个isDebugModel
判断是否是正式环境
pjsua_logging_config_default(&log_cfg)
if isDebugModel{
log_cfg.msg_logging = pj_bool_t(PJ_TRUE.rawValue)
log_cfg.console_level = 4
log_cfg.level = 5
}else{
log_cfg.msg_logging = pj_bool_t(PJ_FALSE.rawValue)
log_cfg.console_level = 0
log_cfg.level = 0
}
3. 初始化PJSUA
完成第二步的相关配置后,我们就可以用这些配置去初始化SUA了,此处用到了之前判断执行状态的变量status
status = pjsua_init(&cfg, &log_cfg, &media_cfg)
if status != PJ_SUCCESS.rawValue{
print("error init pjsua")
}
4. 配置传输类型
传输类型可以支持TCP、UDP,这里用UDP,监听本地默认的一个UDP端口即可
var transport_config = pjsua_transport_config()
pjsua_transport_config_default(&transport_config)
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &transport_config, nil)
if status != PJ_SUCCESS.rawValue{
print("error add transport for pjsua")
}
5. 启动PJSUA
基本工作完成后便可以启动SUA让其工作了
status = pjsua_start()
if status != PJ_SUCCESS.rawValue{
print("error start pjsua")
}
至此TBPJSIPManager
单例算完成了,整体代码如下
import UIKit
let n_on_reg_state = NSNotification.Name("on_reg_state")
let n_on_call_state = NSNotification.Name("on_call_state")
let n_on_incoming_call = NSNotification.Name("on_incoming_call")
class TBPJSIPManager: NSObject {
let isDebugModel = true
public static let shared = TBPJSIPManager()
fileprivate override init() { }
func configPJSIP(){
// 创建SUA
var status: pj_status_t = pjsua_create()
if status != PJ_SUCCESS.rawValue{
print("error create pjsua")
}
// SUA相关配置
var cfg = pjsua_config()
var media_cfg = pjsua_media_config()
var log_cfg = pjsua_logging_config()
// 回调函数配置
pjsua_config_default(&cfg)
cfg.cb.on_incoming_call = on_incoming_call
cfg.cb.on_call_media_state = on_call_media_state
cfg.cb.on_call_state = on_call_state
cfg.cb.on_reg_state = on_reg_state
// 媒体相关配置
pjsua_media_config_default(&media_cfg)
media_cfg.clock_rate = 16000
media_cfg.snd_clock_rate = 16000
media_cfg.ec_tail_len = 0
// 日志相关配置
pjsua_logging_config_default(&log_cfg)
if isDebugModel{
log_cfg.msg_logging = pj_bool_t(PJ_TRUE.rawValue)
log_cfg.console_level = 4
log_cfg.level = 5
}else{
log_cfg.msg_logging = pj_bool_t(PJ_FALSE.rawValue)
log_cfg.console_level = 0
log_cfg.level = 0
}
// 初始化PJSUA
status = pjsua_init(&cfg, &log_cfg, &media_cfg)
if status != PJ_SUCCESS.rawValue{
print("error init pjsua")
}
// 传输类型配置
//pjsua 监听了本地的一个UDP端口,这个端口我们是可以配置死的( pjsua_transport_config ),但如果不配置, pjsua 会随机选择一个未被占用的端口,这样挺好,否则应用可能会Crash掉。
var transport_config = pjsua_transport_config()
pjsua_transport_config_default(&transport_config)
status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &transport_config, nil)
if status != PJ_SUCCESS.rawValue{
print("error add transport for pjsua")
}
// 启动PJSUA
status = pjsua_start()
if status != PJ_SUCCESS.rawValue{
print("error start pjsua")
}
}
/// 来电回调
// 把所有的回调函数都包装成通知对外发布,在这里需要注意,所有的通知都放到主线程
let on_incoming_call: (@convention(c) (pjsua_acc_id, pjsua_call_id, UnsafeMutablePointer<pjsip_rx_data>?) -> Void) = { (acc_id, call_id, rdata) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
let remote_info = NSString(utf8String: ci.remote_info.ptr)
guard let startIndex = remote_info?.range(of: "<").location else { return }
guard let endIndex = remote_info?.range(of: ">").location else { return }
var remote_address = remote_info?.substring(with: NSMakeRange(startIndex + 1, endIndex - startIndex - 1))
remote_address = remote_address?.components(separatedBy: ":")[1]
let argument = ["call_id":call_id,"remote_address":remote_address ?? "has no remote_address"] as [String : Any]
DispatchQueue.main.async {
///本Demo中只有AppDelegate在监听这个通知,实际开发也这样的话没必要发通知,灵活处理
NotificationCenter.default.post(name: n_on_incoming_call, object: nil, userInfo: argument)
}
}
/// 媒体状态回调(通话建立后,要播放RTP流)
let on_call_media_state: (@convention(c) (pjsua_call_id) -> Void) = { (call_id) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
if ci.media_status == PJSUA_CALL_MEDIA_ACTIVE {
pjsua_conf_connect(ci.conf_slot, 0)
pjsua_conf_connect(0, ci.conf_slot)
}
}
/// 电话状态回调
let on_call_state: (@convention(c) (pjsua_call_id, UnsafeMutablePointer<pjsip_event>?) -> Void) = { (call_id, event) in
var ci = pjsua_call_info()
pjsua_call_get_info(call_id, &ci)
let argument = ["call_id":call_id, "state":ci.state, "pjsipConfAudioId":ci.conf_slot] as [String : Any]
DispatchQueue.main.async {
NotificationCenter.default.post(name: n_on_call_state, object: nil, userInfo: argument)
}
}
///注册状态回调
let on_reg_state: (@convention(c) (pjsua_acc_id) -> Void) = { (acc_id) in
var status = pj_status_t()
var info = pjsua_acc_info()
status = pjsua_acc_get_info(acc_id, &info)
if status != PJ_SUCCESS.rawValue{
return
}
let argument = ["acc_id":acc_id, "status_text":String(utf8String: info.status_text.ptr) ?? "has no status_text", "status":info.status] as [String : Any]
DispatchQueue.main.async {
NotificationCenter.default.post(name: n_on_reg_state, object: nil, userInfo: argument)
}
}
}
在启动应用后我们就先配置PJSIP,在application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
方法中调用TBPJSIPManager.shared.configPJSIP()
即可
6. 监听来电
最后一步,当PJSUA工作后我们需要监听来电,实质是来电后会走来电回调,所以监听来电回调发送的广播即可。方法如下:
好了,今天就到这里吧,内容虽然不多,却花了近两个小时😀😁,明天有时间就继续探究通话的实现。