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_callon_call_media_stateon_call_stateon_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工作后我们需要监听来电,实质是来电后会走来电回调,所以监听来电回调发送的广播即可。方法如下:


好了,今天就到这里吧,内容虽然不多,却花了近两个小时😀😁,明天有时间就继续探究通话的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,809评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,099评论 25 707
  • 爱情海的浪漫与我无关, 迪拜的七星级酒店只听说过。 荷兰的郁金香看过图片, 普罗旺斯的薰衣草梦里见过。 欲望与现实...
    一直想飞的基阅读 222评论 0 1
  • 到这个新的城市近一个星期了,还没有安顿下来,我知道我的时间并不多了。 最近总在反反复复的飘泊中放宽心态。这是我们到...
    羊毛与花少年阅读 314评论 2 2