# JJCallKit 对接文档

## 场景说明

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

**集成方式灵活**：
- **使用自带 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（真机）、arm64（模拟器）、x86_64（模拟器） |
| **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.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 项目必需）

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

### 1.5 配置权限（必需）

1. **麦克风权限**（必需）

在 `Info.plist` 中添加：
```xml
<key>NSMicrophoneUsageDescription</key>
<string>用于语音通话</string>
```

2. **后台音频**（可选，如需后台通话）

在 `Info.plist` 中添加：
```xml
<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 获取单例

```swift
let manager = VoIPManager.sharedManager()
```

### 3.2 初始化和登录

#### 初始化 SDK

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

#### 登录

```swift
// 密码和已加密密码二选一，都填写的话，使用已加密密码
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 表示使用 password
- `success`: 成功回调，返回登录结果字典（NSDictionary）
- `failure`: 失败回调，返回错误码和错误信息

#### 登出

```swift
manager.logout()
```

### 3.3 通话控制

#### 发起呼叫

```swift
manager.makeCall("13800138000",
                 success: { callId in
    print("呼叫成功，callId: \(callId)")
}, failure: { code, message in
    print("呼叫失败: \(code), \(message ?? "")")
})
```

**参数说明**：
- `callNumber`: 被叫号码
- `success`: 成功回调，返回callId
- `failure`: 失败回调，返回错误码和错误信息

#### 发起呼叫（带自定义参数）

```swift
// 
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`: 成功回调，返回callId
- `failure`: 失败回调，返回错误码和错误信息

#### 挂断通话

```swift
manager.hangupCall()
```

#### 检查是否可以拨打

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

### 3.4 通话中控制

#### 静音控制

```swift
manager.openMute(true)   // true=开启静音，false=关闭静音
```

**参数说明**：
- `open`: YES/true 开启静音，NO/false 关闭静音

#### 免提控制

```swift
manager.openLoudSpeaker(true)  // true=开启免提，false=关闭免提
```

**参数说明**：
- `open`: YES/true 开启扬声器，NO/false 关闭扬声器

#### 查询静音状态

```swift
if manager.isMuted {
    // 当前已静音
    print("当前已静音")
} else {
    // 当前未静音
    print("当前未静音")
}
```

**说明**：查询当前通话的静音状态。如果没有正在进行的通话，返回 `false`。

#### 查询扬声器状态

```swift
if manager.isLoudSpeakerOn {
    // 当前使用扬声器
    print("当前使用扬声器")
} else {
    // 当前使用听筒或其他音频输出设备
    print("当前使用听筒")
}
```

**说明**：查询当前音频输出设备状态。通过查询 AVAudioSession 的实际输出端口来判断。

#### 发送 DTMF 信号

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

**参数说明**：
- `dtmfNumber`: DTMF号码（0-9, *, #）

### 3.5 外显号码相关（可选）

#### 获取外显策略配置

```swift
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, callerStrategy
- `failure`: 失败回调，返回错误码和错误信息

#### 获取外显号码列表

```swift
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`: 失败回调，返回错误码和错误信息

#### 设置外显号码

```swift
// 方式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`: 挂断类型，值为 `SIPHangupType` 枚举类型
- `kSIPVoIPStatusCodeKey`: VoIPStatusCode 错误码（当 SIP 状态码映射到 VoIPStatusCode 时使用）

**挂断类型枚举定义**：
```objc
// MARK: - 挂断类型定义
typedef NS_ENUM(NSInteger, SIPHangupType) {
    SIPHangupTypeCaller = 0,    // 主叫挂断
    SIPHangupTypeCallee = 1     // 被叫挂断
};
```

### 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`：获取外显号码配置失败

#### 获取错误描述

```swift
let description = VoIPManager.errorDescriptionForCode(errorCode)
print("错误描述: \(description)")
```

**使用示例**：
```swift
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 状态码描述

```swift
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 - 服务不可用

#### 错误处理建议

1. **初始化错误**：检查 SDK 是否正确集成，确保在调用其他方法前先初始化
2. **参数验证错误**：检查输入参数是否为空、格式是否正确
3. **登录流程错误**：检查账号密码、网络连接、服务器配置
4. **通话相关错误**：确保已登录且 SIP 连接成功，检查被叫号码格式
5. **网络错误**：检查网络连接状态，可能需要重试
6. **未知错误**：查看详细错误消息，联系技术支持

### 3.8 其他方法

#### 刷新音频设备

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

#### 重置重试次数

```swift
manager.resetRetryNumber()
```

#### 清理状态（高级）

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

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

### 3.9 属性说明

#### 日志消息回调

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

#### 初始化状态

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

### 3.10 日志配置（可选）

```swift
// 设置日志级别（0=错误，1=一般，2=全量）
VoIPManager.setLogLevel(2)

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

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

---

## 4、注意事项

### 4.1 重要提示

#### 初始化顺序

1. **必须先初始化 SDK 再登录**
   ```swift
   let result = VoIPManager.sharedManager().initial()
   if result != 0 {
       // 初始化失败，不能继续
       return
   }
   // 初始化成功后再登录
   ```

2. **初始化是线程安全的**，多次调用不会重复初始化

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

#### 登录流程

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

#### 通话相关

1. **拨打电话前检查状态**
   ```swift
   if manager.canMakeCall() {
       manager.makeCall("13800138000")
   }
   ```

2. **通话接通后确保音频会话激活**
   ```swift
   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. 通话接通后确保音频会话激活：
   ```swift
   PhoneService.shared.ensureAudioSessionForCall()
   ```
5. 检查音频路由是否正确（听筒/免提）
6. 查看日志文件，检查音频设备初始化是否成功

#### 问题 6：呼叫失败

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

**排查步骤**：
1. 检查 SIP 连接状态：监听 `kSIPConnectedNotification` 确认已连接
2. 检查号码格式是否正确
3. 查看通知中的错误信息：
   ```swift
   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`：请求已取消

---