JJCallKit 对接文档
场景说明
使用 JJCallKit 对接,可以将电话功能(外呼、挂断等)集成到您自己的业务系统中,在您自己的业务系统使用此功能。
集成方式灵活:
- 使用自带 UI:Demo 提供了完整的通话界面(拨号、通话中、通话记录等),可以直接使用,快速集成
- 完全自定义 UI:只使用底层的 Service 层能力,完全按照自己的 UI 设计实现界面
您可以根据项目需求选择合适的集成方式。
环境版本等限制
系统版本要求
| 项目 | 最低版本要求 | 说明 |
|---|---|---|
| iOS 系统版本 | iOS 12.4+ | 支持 iPhone |
| Xcode 版本 | Xcode 15.0+ | 建议使用最新稳定版本 |
| Swift 版本 | Swift 5.0+ | 推荐使用 Swift 5.9+ |
| macOS 开发环境 | macOS 12.0+ | 用于 Xcode 开发 |
Framework 依赖
| 依赖项 | 说明 |
|---|---|
| JJCallKit.framework | 核心 VoIP 框架,基于 PJSIP 实现 |
| 架构支持 | arm64(真机)、arm64(模拟器)、x86_64(模拟器) |
| Bitcode | 不支持 Bitcode |
权限要求
| 权限 | Info.plist 键值 | 说明 |
|---|---|---|
| 麦克风权限 | NSMicrophoneUsageDescription |
必需,用于语音通话 |
| 网络权限 | 系统默认允许 | 用于 SIP 连接和 WebSocket 通信 |
| 后台音频 | 可选配置 | 如需后台通话功能,需配置 UIBackgroundModes 中的 audio |
已知限制
- 后台保活:如需后台通话,需要配置相应的后台模式,并且 iOS 系统可能会限制后台运行时间。
1、如何将 JJCallKit.framework 集成到新项目中
1.1 准备工作
在开始集成之前,请确保您已准备好以下文件:
JJCallKit.framework- 核心 VoIP 框架文件JJPhoneDemo工程源码(如需要集成 Demo UI,可选)
1.2 添加 Framework 到项目
- 将
JJCallKit.framework拖拽到 Xcode 项目导航器中 - 在 Target 的 "General" -> "Frameworks, Libraries, and Embedded Content" 中:
- 添加
JJCallKit.framework,设置为 "Embed & Sign" - 添加系统框架:
CoreAudio.framework、AudioToolbox.framework、AVFoundation.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 项目必需)
- 创建头文件:
YourProjectName-Bridging-Header.h - 添加内容:
#import <JJCallKit/VoIPManager.h> - 在 "Build Settings" -> "Objective-C Bridging Header" 中设置路径:
YourProjectName/YourProjectName-Bridging-Header.h
1.5 配置权限(必需)
- 麦克风权限(必需)
在 Info.plist 中添加:
<key>NSMicrophoneUsageDescription</key>
<string>用于语音通话</string>
- 后台音频(可选,如需后台通话)
在 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, // 使用生产环境
passwordPk: "secret_password", // 已加密密码
success: { result in
// result: NSDictionary,包含登录结果信息
print("登录成功")
},
failure: { code, message in
// code: 错误码
// message: 错误描述
print("登录失败: \(code), \(message ?? "")")
}
)
参数说明:
account: 登录账号(格式:用户名@域名,如:6000@useasy)password: 登录密码(可选,如果提供了passwordPk则可以不填)passwordPk: 可选的已加密密码,传 nil 表示使用 passwordsuccess: 成功回调,返回登录结果字典(NSDictionary)failure: 失败回调,返回错误码和错误信息
登出
manager.logout()
3.3 通话控制
发起呼叫
manager.makeCall("13800138000",
success: { callId in
print("呼叫成功,callId: \(callId)")
}, failure: { code, message in
print("呼叫失败: \(code), \(message ?? "")")
})
参数说明:
callNumber: 被叫号码success: 成功回调,返回callIdfailure: 失败回调,返回错误码和错误信息
发起呼叫(带自定义参数)
//
manager.makeCall("13800138000",
userData: ["key1": "value1"],
success: { callId in
print("呼叫成功,callId: \(callId)")
}, failure: { code, message in
print("呼叫失败: \(code), \(message ?? "")")
})
参数说明:
callNumber: 被叫号码userData: 自定义数据字典(可选),将作为JSON字符串,编码后长度需小于255字节。如果为nil,则行为与普通makeCall方法相同success: 成功回调,返回callIdfailure: 失败回调,返回错误码和错误信息
挂断通话
manager.hangupCall()
检查是否可以拨打
if manager.canMakeCall() {
// 可以拨打电话
}
3.4 通话中控制
静音控制
manager.openMute(true) // true=开启静音,false=关闭静音
参数说明:
open: YES/true 开启静音,NO/false 关闭静音
免提控制
manager.openLoudSpeaker(true) // true=开启免提,false=关闭免提
参数说明:
open: YES/true 开启扬声器,NO/false 关闭扬声器
查询静音状态
if manager.isMuted {
// 当前已静音
print("当前已静音")
} else {
// 当前未静音
print("当前未静音")
}
说明:查询当前通话的静音状态。如果没有正在进行的通话,返回 false。
查询扬声器状态
if manager.isLoudSpeakerOn {
// 当前使用扬声器
print("当前使用扬声器")
} else {
// 当前使用听筒或其他音频输出设备
print("当前使用听筒")
}
说明:查询当前音频输出设备状态。通过查询 AVAudioSession 的实际输出端口来判断。
发送 DTMF 信号
manager.sendDTMF("1") // 参数:0-9, *, #
参数说明:
dtmfNumber: DTMF号码(0-9, *, #)
3.5 外显号码相关(可选)
获取外显策略配置
manager.getCallerStrategyWithSuccess({ config in
// config: 包含 mobile, agentNumber, numbers, numberSelect 等字段
print("外显策略: \(config)")
}, failure: { code, message in
print("获取失败: \(code), \(message ?? "")")
})
参数说明:
success: 成功回调,返回包含以下字段的字典:mobile, agentNumber, numbers, numberSelect, selectNumber, numberGroup, sipNumber, callerStrategyfailure: 失败回调,返回错误码和错误信息
获取外显号码列表
manager.getDisplayNumberListWithSuccess({ numberList in
// numberList: 数组,每个元素包含 id, status, number, province, city
for number in numberList {
print("号码: \(number)")
}
}, failure: { code, message in
print("获取失败: \(code), \(message ?? "")")
})
参数说明:
success: 成功回调,返回外显号码数组(每个字典包含 id、status、number、province、city)failure: 失败回调,返回错误码和错误信息
设置外显号码
// 方式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 ?? "")")
})
参数说明:
selectNumber: 指定的外显号码(可选,如果指定了 numberGroup 则此参数可为 nil)numberGroup: 指定的外显号码组ID(可选,如果指定了 selectNumber 则此参数可为 nil)success: 成功回调failure: 失败回调,返回错误码和错误信息
注意:必须指定 selectNumber 或 numberGroup 之一。
3.6 状态通知
Framework 通过 NSNotificationCenter 发送状态通知,需监听这些通知来更新 UI。
通知常量列表
连接相关通知:
| 通知名称 | 说明 | UserInfo |
|---|---|---|
kSIPConnectedNotification |
SIP 连接成功 | - |
kSIPConnectFailedNotification |
SIP 连接失败 | kSIPReasonDescriptionKey |
kSIPKickedOutNotification |
被踢下线(453) | - |
kSIPDisconnectedNotification |
SIP 断开连接 | kSIPReasonKey, kSIPReasonDescriptionKey, kSIPIsPassiveKey, kSIPTimestampKey |
呼叫相关通知:
| 通知名称 | 说明 | UserInfo |
|---|---|---|
kSIPCallCallingNotification |
正在呼叫 | - |
kSIPCallConnectingNotification |
振铃中 | - |
kSIPCallConfirmNotification |
呼叫接通 | - |
kSIPCallDisconnectNotification |
呼叫挂断 | kSIPReasonKey, kSIPReasonDescriptionKey, kSIPHangupTypeKey, kSIPTimestampKey, kSIPVoIPStatusCodeKey |
kSIPCallRetryNotification |
呼叫重试 | - |
UserInfo Keys 说明:
kSIPReasonKey: SIP 错误码kSIPReasonDescriptionKey: SIP 错误描述kSIPIsPassiveKey: 是否被动断开kSIPTimestampKey: 时间戳kSIPHangupTypeKey: 挂断类型(主叫挂断/被叫挂断)kSIPVoIPStatusCodeKey: VoIPStatusCode 错误码(当 SIP 状态码映射到 VoIPStatusCode 时使用)
3.7 错误码说明
错误码分类
| 错误码范围 | 分类 | 说明 |
|---|---|---|
| 0 | 成功 | 操作成功 |
| -1 ~ -99 | 初始化错误 | SDK 初始化相关错误 |
| -100 ~ -199 | 参数验证错误 | 输入参数验证失败 |
| -200 ~ -299 | 登录流程错误 | 登录、认证、SIP 注册相关错误 |
| -300 ~ -399 | 通话相关错误 | 呼叫、通话控制相关错误 |
| -400 ~ -499 | 网络错误 | 网络连接、超时相关错误 |
| -2000 ~ -2099 | HTTP 请求错误 | HTTP 接口调用错误 |
| -2100 ~ -2199 | WebSocket 错误 | WebSocket 连接相关错误 |
| -4000 ~ -4099 | 外显号码错误 | 外显号码配置相关错误 |
| -9999 | 未知错误 | 未定义的错误 |
常见错误码列表
初始化错误(-1 ~ -99)
-1:SDK 初始化失败-2:SDK 未初始化-3:PJSIP 创建失败-4:PJSIP 初始化失败-5:传输层创建失败(UDP/TCP)-6:PJSIP 启动失败
参数验证错误(-100 ~ -199)
-100:账号无效-101:密码无效-104:账号格式错误-105:账号为空-106:密码为空-107:服务器 URL 为空-108:Token 为空
登录流程错误(-200 ~ -299)
-200:登录请求失败-201:登录认证失败-202:获取登录配置失败-203:Socket 连接失败-205:获取 SIP 配置失败-206:SIP 配置信息不完整-207:SIP 注册失败(通用,详细 SIP 状态码在错误消息中)-209:SIP 注册被拒绝 (403)-210:账号已被冻结-211:坐席已停用-212:账户未找到-213:坐席未找到-215:SIP 注册未找到 (404)-216:SIP 注册服务器错误 (5xx)-217:获取公钥失败
通话相关错误(-300 ~ -399)
-300:呼叫失败(通用,详细 SIP 状态码在错误消息中)-301:通话权限被拒绝-302:未登录无法呼叫-303:呼叫号码为空-304:呼叫被拒绝 (403)-305:号码不存在 (404)-306:用户忙线 (486)-307:请求已终止 (487)-308:呼叫超时 (408)-309:暂时不可用 (480)
网络错误(-400 ~ -499)
-401:网络超时
HTTP 请求错误(-2000 ~ -2099)
-2001:无效 URL-2007:HTTP 错误 (4xx, 5xx,通用)-2008:空响应数据-2009:JSON 解析失败
WebSocket 错误(-2100 ~ -2199)
-2100:无效的 WebSocket URL-2101:WebSocket 连接失败
外显号码错误(-4000 ~ -4099)
-4001:坐席 ID 为空-4002:获取外显号码配置失败
获取错误描述
let description = VoIPManager.errorDescriptionForCode(errorCode)
print("错误描述: \(description)")
使用示例:
manager.loginWithAccount(
account: "6000@useasy",
password: "password",
environment: .production,
passwordPk: nil,
success: { result in
print("登录成功")
},
failure: { code, message in
let description = VoIPManager.errorDescriptionForCode(code)
print("登录失败: \(code), 描述: \(description), 详情: \(message ?? "")")
}
)
获取 SIP 状态码描述
let description = VoIPManager.sipStatusDescription(403) // 返回 "403 Forbidden - 禁止访问"
常见 SIP 状态码:
100:Trying - 正在尝试180:Ringing - 振铃中200:OK - 成功403:Forbidden - 禁止访问404:Not Found - 未找到408:Request Timeout - 请求超时480:Temporarily Unavailable - 暂时不可用486:Busy Here - 用户忙线487:Request Terminated - 请求已终止500:Internal Server Error - 服务器内部错误503:Service Unavailable - 服务不可用
错误处理建议
- 初始化错误:检查 SDK 是否正确集成,确保在调用其他方法前先初始化
- 参数验证错误:检查输入参数是否为空、格式是否正确
- 登录流程错误:检查账号密码、网络连接、服务器配置
- 通话相关错误:确保已登录且 SIP 连接成功,检查被叫号码格式
- 网络错误:检查网络连接状态,可能需要重试
- 未知错误:查看详细错误消息,联系技术支持
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 重要提示
初始化顺序
-
必须先初始化 SDK 再登录
let result = VoIPManager.sharedManager().initial() if result != 0 { // 初始化失败,不能继续 return } // 初始化成功后再登录 -
初始化是线程安全的,多次调用不会重复初始化
-
音频会话配置:Framework 内部已处理 AVAudioSession,应用层无需额外配置,但需确保不会冲突
登录流程
- 登录是异步过程:
loginWithAccount返回成功仅表示请求已发出,真实连接结果需监听通知// 正确示例:通过通知获取真实连接状态 NotificationCenter.default.addObserver( forName: NSNotification.Name(kSIPConnectedNotification), object: nil, queue: .main ) { _ in print("真正连接成功") }
通话相关
-
拨打电话前检查状态
if manager.canMakeCall() { manager.makeCall("13800138000") } -
通话接通后确保音频会话激活
NotificationCenter.default.addObserver( forName: NSNotification.Name(kSIPCallConfirmNotification), object: nil, queue: .main ) { _ in // 通话接通后,确保音频会话正确配置 PhoneService.shared.ensureAudioSessionForCall() } -
通话中操作需在接通后进行:静音、免提、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:通话无声音
现象:能拨通电话但听不到声音
排查步骤:
- 检查麦克风权限是否已授予
- 设置 → 隐私 → 麦克风 → 检查应用权限
- 检查系统音量是否已调高
- 检查是否开启了静音模式(iPhone 侧边开关)
- 通话接通后确保音频会话激活:
PhoneService.shared.ensureAudioSessionForCall() - 检查音频路由是否正确(听筒/免提)
- 查看日志文件,检查音频设备初始化是否成功
问题 6:呼叫失败
现象:呼叫立即失败或无法接通
排查步骤:
- 检查 SIP 连接状态:监听
kSIPConnectedNotification确认已连接 - 检查号码格式是否正确
- 查看通知中的错误信息:
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:请求已取消
| 版本号 | 更新时间 | 核心更新 |
| 1.0.2 | 12.29 | 支持加密密码登录 |
| 1.0.1 | 12.23 | 错误码问题新增静音和扬声器查询方法外呼支持返回callid外呼支持传自定义参数处理Headers头文件过多问题外呼挂断,标明主叫挂断还是被叫挂断 |
| 1.0.0 | 12.15 |