CallSDK IOS 接⼊⽂档

场景说明

使用 CallSDK 对接,可以将电话功能(外呼、挂断等)集成到您自己的业务系统中,在您自己的业务系统使用此功能。

集成方式灵活

  • 使用自带 UI:Demo 提供了完整的通话界面(拨号、通话中、通话记录等),可以直接使用,快速集成
  • 完全自定义 UI:只使用底层的 Service 层能力,完全按照自己的 UI 设计实现界面

您可以根据项目需求选择合适的集成方式。


0、环境版本等限制

0.1 系统版本要求

项目 最低版本要求 说明
iOS 系统版本 iOS 12.4+ 支持 iPhone
Xcode 版本 Xcode 15.0+ 建议使用最新稳定版本
Swift 版本 Swift 5.0+ 推荐使用 Swift 5.9+
macOS 开发环境 macOS 12.0+ 用于 Xcode 开发

0.2 Framework 依赖

依赖项 说明
JJCallKit.framework 核心 VoIP 框架,基于 PJSIP 实现
架构支持 arm64(真机)
Bitcode 不支持 Bitcode

0.3 权限要求

权限 Info.plist 键值 说明
麦克风权限 NSMicrophoneUsageDescription 必需,用于语音通话
网络权限 系统默认允许 用于 SIP 连接和 WebSocket 通信
后台音频 可选配置 如需后台通话功能,需配置 UIBackgroundModes 中的 audio

0.4 已知限制

  1. 后台保活:如需后台通话,需要配置相应的后台模式,并且 iOS 系统可能会限制后台运行时间。

1、如何将 JJCallKit.framework 集成到新项目中

1.1 准备工作

在开始集成之前,请确保您已准备好以下文件:

  • JJCallKit.framework - 核心 VoIP 框架文件
  • JJPhoneDemo 工程源码(如需要集成 Demo UI,可选)

1.2 添加 Framework 到项目

  1. JJCallKit.framework 拖拽到 Xcode 项目导航器中
  2. 在 Target 的 "General" -> "Frameworks, Libraries, and Embedded Content" 中:
    • 添加 JJCallKit.framework,设置为 "Embed & Sign"
    • 添加系统框架:CoreAudio.frameworkAudioToolbox.frameworkAVFoundation.framework(设置为 "Do Not Embed"

1.3 配置 Build Settings

在 Target 的 "Build Settings" 中配置以下项:

Framework Search Paths

$(PROJECT_DIR)

(或 Framework 所在的具体路径)

Header Search Paths

$(PROJECT_DIR)/JJCallKit.framework/Headers

Preprocessor Macros(添加以下定义)

PJ_IS_LITTLE_ENDIAN=1
PJ_IS_BIG_ENDIAN=0

1.4 配置 Bridging Header(Swift 项目必需)

  1. 创建头文件:YourProjectName-Bridging-Header.h
  2. 添加内容:
    #import <JJCallKit/VoIPManager.h>
    #import <JJCallKit/pj/log.h>
    
  3. "Build Settings" -> "Objective-C Bridging Header" 中设置路径:
    YourProjectName/YourProjectName-Bridging-Header.h
    

1.5 配置权限(必需)

  1. 麦克风权限(必需)

Info.plist 中添加:

<key>NSMicrophoneUsageDescription</key>
<string>用于语音通话</string>
  1. 后台音频(可选,如需后台通话)

Info.plist 中添加:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

2、Demo 部分如何集成到新项目中

根据需求选择合适的方案,将对应文件拖入项目中:

2.1 方案一:只带 Service 层

Service/
  └── PhoneService.swift
Config/
  └── PhoneConfig.swift

2.2 方案二:带完整 UI

Service/
  └── PhoneService.swift
Config/
  └── PhoneConfig.swift
Model/
  └── Call.swift
UI/
  ├── DialViewController.swift
  ├── ActiveCallViewController.swift
  ├── CallSettingsViewController.swift
  └── Components/
      ├── DialKeyboardView.swift
      └── ActiveCallDialKeyboard.swift
Extension/
  └── UIColor+Extension.swift
JJPhoneSDK.swift

3、JJCallKit.framework 各个接口用法说明

3.1 获取单例

let manager = VoIPManager.sharedManager()

3.2 初始化和登录

初始化 SDK

let result = manager.initial()
// result == 0 表示成功,负数表示失败

登录

manager.loginWithAccount(
    account: "6000@useasy",          // 账号(格式:用户名@域名)
    password: "your_password",     // 密码
    environment: .production,      // 使用生产环境
    success: { result in
        // result: NSDictionary,包含登录结果信息
        print("登录成功")
    },
    failure: { code, message in
        // code: 错误码
        // message: 错误描述
        print("登录失败: \(code), \(message ?? "")")
    }
)

登出

manager.logout()

3.3 通话控制

发起呼叫

manager.makeCall("13800138000")  // 被叫号码

挂断通话

manager.hangupCall()

检查是否可以拨打

if manager.canMakeCall() {
    // 可以拨打电话
}

3.4 通话中控制

静音控制

manager.openMute(true)   // true=开启静音,false=关闭静音

免提控制

manager.openLoudSpeaker(true)  // true=开启免提,false=关闭免提

发送 DTMF 信号

manager.sendDTMF("1")  // 参数:0-9, *, #

3.5 外显号码相关(可选)

获取外显策略配置

manager.getCallerStrategyWithSuccess({ config in
    // config: 包含 mobile, agentNumber, numbers, numberSelect 等字段
    print("外显策略: \(config)")
}, failure: { code, message in
    print("获取失败: \(code), \(message ?? "")")
})

获取外显号码列表

manager.getDisplayNumberListWithSuccess({ numberList in
    // numberList: 数组,每个元素包含 id, status, number, province, city
    for number in numberList {
        print("号码: \(number)")
    }
}, failure: { code, message in
    print("获取失败: \(code), \(message ?? "")")
})

设置外显号码

// 方式1:指定单个号码
manager.updateAgentDisplayNumberWithSelectNumber("02112345678", 
                                                 numberGroup: nil,
                                                 success: {
    print("设置成功")
}, failure: { code, message in
    print("设置失败: \(code), \(message ?? "")")
})

// 方式2:指定号码组
manager.updateAgentDisplayNumberWithSelectNumber(nil,
                                                 numberGroup: "group_id",
                                                 success: {
    print("设置成功")
}, failure: { code, message in
    print("设置失败: \(code), \(message ?? "")")
})

3.6 状态通知

Framework 通过 NSNotificationCenter 发送状态通知,需监听这些通知来更新 UI。

通知常量列表

通知名称 说明 UserInfo
kSIPConnectedNotification SIP 连接成功 -
kSIPConnectFailedNotification SIP 连接失败 kSIPReasonDescriptionKey
kSIPDisconnectedNotification SIP 断开连接 -
kSIPKickedOutNotification 被踢下线(453) -
kSIPCallCallingNotification 正在呼叫 -
kSIPCallConnectingNotification 振铃中 -
kSIPCallConfirmNotification 呼叫接通 -
kSIPCallDisconnectNotification 呼叫挂断 -
kSIPCallRetryNotification 呼叫重试 -

3.7 错误码说明

错误码分类

  • 0:成功
  • 1:已登录
  • -1 ~ -99:初始化错误
  • -100 ~ -199:参数验证错误
  • -200 ~ -299:登录流程错误
  • -300 ~ -399:通话相关错误
  • -400 ~ -499:网络错误
  • -9999:未知错误

获取错误描述

let description = VoIPManager.errorDescriptionForCode(errorCode)

获取 SIP 状态码描述

let description = VoIPManager.sipStatusDescription(403)  // 返回 "403 Forbidden - 禁止访问"

3.8 其他方法

刷新音频设备

manager.refreshAudioDevice()  // 通常在通话建立后调用

重置重试次数

manager.resetRetryNumber()

清理状态(高级)

// 清理通话状态
manager.cleanupCompleteCallState()

// 清理 SDK(需重新初始化)
manager.cleanupSDK()

3.9 属性说明

日志消息回调

manager.messageHandler = { message in
    print("日志: \(message)")
}

初始化状态

if manager.isSDKInitialized {
    print("SDK 已初始化")
}

3.10 日志配置(可选)

// 设置日志级别(0=错误,1=一般,2=全量)
VoIPManager.setLogLevel(2)

// 设置日志文件路径(nil 使用默认路径:Documents/VoIPLogs/voip.log)
VoIPManager.setLogFilePath(nil)

// 启用文件日志
VoIPManager.setFileLoggingEnabled(true)

4、注意事项

4.1 重要提示

初始化顺序

  1. 必须先初始化 SDK 再登录

    let result = VoIPManager.sharedManager().initial()
    if result != 0 {
        // 初始化失败,不能继续
        return
    }
    // 初始化成功后再登录
    
  2. 初始化是线程安全的,多次调用不会重复初始化

  3. 音频会话配置:Framework 内部已处理 AVAudioSession,应用层无需额外配置,但需确保不会冲突

登录流程

  1. 登录是异步过程loginWithAccount 返回成功仅表示请求已发出,真实连接结果需监听通知
    // 正确示例:通过通知获取真实连接状态
    NotificationCenter.default.addObserver(
        forName: NSNotification.Name(kSIPConnectedNotification),
        object: nil,
        queue: .main
    ) { _ in
        print("真正连接成功")
    }
    

通话相关

  1. 拨打电话前检查状态

    if manager.canMakeCall() {
        manager.makeCall("13800138000")
    }
    
  2. 通话接通后确保音频会话激活

    NotificationCenter.default.addObserver(
        forName: NSNotification.Name(kSIPCallConfirmNotification),
        object: nil,
        queue: .main
    ) { _ in
        // 通话接通后,确保音频会话正确配置
        PhoneService.shared.ensureAudioSessionForCall()
    }
    
  3. 通话中操作需在接通后进行:静音、免提、DTMF 等操作应在通话接通后使用

4.2 常见问题排查

问题 1:编译错误 "Framework not found"

解决方案

  • 检查 Framework Search Paths 配置是否正确
  • 确认 Framework 文件确实存在于指定路径
  • 尝试 Clean Build Folder(Cmd + Shift + K),然后重新编译

问题 2:运行时错误 "dyld: Library not loaded"

解决方案

  • 确认 Framework 的 Embed 设置已设置为 "Embed & Sign"
  • 检查 Framework 的签名是否有效

问题 3:找不到头文件

解决方案

  • 检查 Header Search Paths 配置是否正确
  • 确认 Bridging Header 路径配置正确
  • 检查 Bridging Header 文件中的 import 语句是否正确

问题 4:Swift 中无法使用 Framework 类

解决方案

  • 确认 Bridging Header 已正确配置
  • 检查 Bridging Header 文件是否已添加到 Target 的编译源中
  • 确认 Framework 的 Headers 文件夹存在且包含 VoIPManager.h

问题 5:通话无声音

现象:能拨通电话但听不到声音

排查步骤

  1. 检查麦克风权限是否已授予
    • 设置 → 隐私 → 麦克风 → 检查应用权限
  2. 检查系统音量是否已调高
  3. 检查是否开启了静音模式(iPhone 侧边开关)
  4. 通话接通后确保音频会话激活:
    PhoneService.shared.ensureAudioSessionForCall()
    
  5. 检查音频路由是否正确(听筒/免提)
  6. 查看日志文件,检查音频设备初始化是否成功

问题 6:呼叫失败

现象:呼叫立即失败或无法接通

排查步骤

  1. 检查 SIP 连接状态:监听 kSIPConnectedNotification 确认已连接
  2. 检查号码格式是否正确
  3. 查看通知中的错误信息:
    NotificationCenter.default.addObserver(
        forName: NSNotification.Name(kSIPCallDisconnectNotification),
        object: nil,
        queue: .main
    ) { notification in
        if let code = notification.userInfo?[kSIPReasonKey] as? Int {
            print("呼叫失败,错误码: \(code)")
        }
    }
    

常见 SIP 错误码

  • 403 Forbidden:呼叫被拒绝
  • 404 Not Found:号码不存在
  • 486 Busy Here:用户忙线
  • 487 Request Terminated:请求已取消

JJPhoneDemo.zip
2.8 MB
2025-12-05