@tencentcloud/sdk-mcp 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/resource/doc/callkit/core-sdk/android/answer-call.md +942 -0
- package/resource/doc/callkit/core-sdk/android/make-call.md +876 -0
- package/resource/doc/callkit/core-sdk/android/offline-push.md +368 -0
- package/resource/doc/callkit/core-sdk/flutter/answer-call.md +1132 -0
- package/resource/doc/callkit/core-sdk/flutter/make-call.md +1206 -0
- package/resource/doc/callkit/core-sdk/flutter/offline-push.md +357 -0
- package/resource/doc/callkit/core-sdk/ios/answer-call.md +1009 -0
- package/resource/doc/callkit/core-sdk/ios/make-call.md +1154 -0
- package/resource/doc/callkit/core-sdk/ios/offline-push.md +470 -0
- package/resource/doc/callkit/core-sdk/uni-app/answer-call.md +1146 -0
- package/resource/doc/callkit/core-sdk/uni-app/make-call.md +1267 -0
- package/resource/doc/callkit/preset/android/integration.md +1 -1
- package/resource/doc/callkit/preset/flutter/integration.md +1 -1
- package/resource/doc/callkit/preset/ios/integration.md +1 -1
- package/resource/doc/callkit/preset/react/api.md +1 -1
- package/resource/doc/callkit/preset/vue/api.md +1 -1
- package/server.js +1 -1
- package/tools/call/get_native_callkit_core_integration.js +67 -0
- package/tools/call/get_native_callkit_core_integration.js.map +1 -0
- package/tools/common/present_framework_choice.js +1 -1
- package/tools/common/present_framework_choice.js.map +1 -1
- package/tools/common/record_skill_session.js +58 -0
- package/tools/common/record_skill_session.js.map +1 -0
- package/tools/definitions/get_native_call_uikit_integration_def.js +9 -3
- package/tools/definitions/get_native_call_uikit_integration_def.js.map +1 -1
- package/tools/definitions/get_native_callkit_core_integration_def.js +63 -0
- package/tools/definitions/get_native_callkit_core_integration_def.js.map +1 -0
- package/tools/definitions/record_skill_session_def.js +31 -0
- package/tools/definitions/record_skill_session_def.js.map +1 -0
- package/tools/index.js +4 -0
- package/tools/index.js.map +1 -1
- package/utils/report-cls-client.js +10 -1
- package/utils/report-cls-client.js.map +1 -1
- package/utils/report-es-client.js +1 -1
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
本文档将帮助您使用 **AtomicXCore SDK** 的 **DeviceStore**、**CallStore** 以及核心组件 **CallCoreView**,快速完成拨打电话功能。
|
|
2
|
+
|
|
3
|
+
## 核心功能
|
|
4
|
+
|
|
5
|
+
**AtomicXCore** 中用于搭建多人音视频通话场景所需要使用到的核心模块包含以下三个:
|
|
6
|
+
|
|
7
|
+
| 模块 | 功能描述 |
|
|
8
|
+
|------|----------|
|
|
9
|
+
| [CallCoreView](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview) | 通话视图核心组件。自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。 |
|
|
10
|
+
| [CallStore](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callstore) | 通话生命周期管理:拨打电话、接通电话、拒接电话、挂断电话。实时获取参与通话人员音视频状态,通话计时、通话记录等数据。 |
|
|
11
|
+
| [DeviceStore](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/devicestore) | 音视频设备控制:麦克风(开关/音量)、摄像头(开关/切换/画质)、屏幕共享,设备状态实时监听。 |
|
|
12
|
+
|
|
13
|
+
## 准备工作
|
|
14
|
+
|
|
15
|
+
### 步骤1:开通服务
|
|
16
|
+
|
|
17
|
+
请参见 开通服务,获取体验版或付费版 SDK。
|
|
18
|
+
|
|
19
|
+
### 步骤2:集成 SDK
|
|
20
|
+
|
|
21
|
+
**1. 添加 Pod 依赖:** 在您项目的 `Podfile` 文件中添加 `pod 'AtomicXCore'` 依赖。
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
target 'YourProjectTarget' do
|
|
25
|
+
pod 'AtomicXCore'
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **提示:** 若项目没有 Podfile 文件,在终端中进入到您的 `.xcodeproj` 目录下,然后执行 `pod init` 命令创建 `Podfile` 文件。
|
|
30
|
+
|
|
31
|
+
**2. 安装组件:** 在终端进入到 `Podfile` 文件所在的目录,然后执行以下命令安装组件。
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pod install --repo-update
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **提示:** 安装完成后,使用 `YourProjectName.xcworkspace` 文件打开项目。
|
|
38
|
+
|
|
39
|
+
### 步骤3:初始化与登录流程
|
|
40
|
+
|
|
41
|
+
启动通话服务需依次完成以下三步:
|
|
42
|
+
|
|
43
|
+
1. **实例化 CallStore**:`let _ = CallStore.shared` 初始化通话数据层。
|
|
44
|
+
2. **用户登录**:调用 `LoginStore.shared.login(...)` 完成 IM 登录。
|
|
45
|
+
3. **初始化通话引擎**:登录成功后调用 `TUICallEngine.createInstance().init(...)` 初始化 TRTC 通话信令引擎(**必须调用,否则无法正常收发通话**)。
|
|
46
|
+
|
|
47
|
+
示例代码如下:
|
|
48
|
+
|
|
49
|
+
```swift
|
|
50
|
+
import UIKit
|
|
51
|
+
import AtomicXCore
|
|
52
|
+
import RTCRoomEngine
|
|
53
|
+
import Combine
|
|
54
|
+
|
|
55
|
+
class ViewController: UIViewController {
|
|
56
|
+
var cancellables = Set<AnyCancellable>()
|
|
57
|
+
|
|
58
|
+
override func viewDidLoad() {
|
|
59
|
+
super.viewDidLoad()
|
|
60
|
+
|
|
61
|
+
// CallStore 初始化
|
|
62
|
+
let _ = CallStore.shared
|
|
63
|
+
|
|
64
|
+
// 配置您的信息
|
|
65
|
+
let userID = "test_001" // 请替换为您的 UserID
|
|
66
|
+
let sdkAppID: Int = 1400000001 // 请替换为在控制台得到的 SDKAppID
|
|
67
|
+
let secretKey = "**************" // 联调用 SDKSecretKey;勿打入正式包。MCP 可与 sdkAppID 一并配置
|
|
68
|
+
|
|
69
|
+
// 生成 UserSig:联调可用 GenerateTestUserSig;正式须服务端签发。MCP 已配置 sdkAppId、sdkSecretKey 时可由工具自动生成
|
|
70
|
+
let userSig = GenerateTestUserSig.genTestUserSig(
|
|
71
|
+
userID: userID,
|
|
72
|
+
sdkAppID: sdkAppID,
|
|
73
|
+
secretKey: secretKey
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// 登录
|
|
77
|
+
LoginStore.shared.login(
|
|
78
|
+
sdkAppID: Int32(sdkAppID),
|
|
79
|
+
userID: userID,
|
|
80
|
+
userSig: userSig
|
|
81
|
+
) { result in
|
|
82
|
+
switch result {
|
|
83
|
+
case .success:
|
|
84
|
+
Log.info("login success")
|
|
85
|
+
// 完成 TUICallEngine 的初始化
|
|
86
|
+
TUICallEngine.createInstance().`init`(Int32(sdkAppID), userId: userID, userSig: userSig) {
|
|
87
|
+
Log.info("TUICallEngine init success")
|
|
88
|
+
} fail: { code, message in
|
|
89
|
+
Log.error("TUICallEngine init failed, code: \(code), message: \(message ?? "")")
|
|
90
|
+
}
|
|
91
|
+
case .failure(let error):
|
|
92
|
+
Log.error("login failed, code: \(error.code), error: \(error.message)")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**登录接口参数说明:**
|
|
100
|
+
|
|
101
|
+
| 参数 | 类型 | 说明 |
|
|
102
|
+
|------|------|------|
|
|
103
|
+
| `userID` | String | 当前用户的唯一 ID,长度不超过32字节,不支持使用特殊字符,建议使用英文字母或数字。为避免多端登录冲突,请勿使用 1、123 等简单 ID。 |
|
|
104
|
+
| `sdkAppID` | int | 从控制台获取,通常是以 140 或 160 开头的 10 位整数 |
|
|
105
|
+
| `secretKey` | String | 与 `sdkAppID` 配套的 SDKSecretKey,仅用于联调时在本地计算 UserSig;勿随正式客户端分发。**Tencent RTC MCP Server** 可与 `sdkAppID` 一并配置后由工具生成 UserSig。 |
|
|
106
|
+
| `userSig` | String | IM 登录鉴权票据。联调可本地生成临时值(如 [`GenerateTestUserSig.genTestUserSig`](https://github.com/Tencent-RTC/TUIKit_iOS/blob/main/application/Debug/GenerateTestUserSig.swift));**Tencent RTC MCP Server** 已配置 `sdkAppId` 与 `sdkSecretKey` 时可由工具自动生成。**生产环境**须由业务服务端使用密钥签发 UserSig,勿在客户端存储或下发 SecretKey。 |
|
|
107
|
+
|
|
108
|
+
## 实现步骤
|
|
109
|
+
|
|
110
|
+
> **发起通话前,请确保完成登录**,这是服务可用的必要前提。以下将分 5 步为您讲解如何"拨打一通电话"。
|
|
111
|
+
|
|
112
|
+
### 步骤1:创建通话界面
|
|
113
|
+
|
|
114
|
+
您需要创建一个通话页面,当发起通话时唤起通话页面,实现方式如下:
|
|
115
|
+
|
|
116
|
+
1. **创建通话页面**:新建一个 UIViewController 作为通话宿主页面。
|
|
117
|
+
2. **通话页面绑定 CallCoreView**:通话视图核心组件,自动监听 CallStore 数据并完成画面渲染。
|
|
118
|
+
|
|
119
|
+
```swift
|
|
120
|
+
import UIKit
|
|
121
|
+
import AtomicXCore
|
|
122
|
+
|
|
123
|
+
class CallViewController: UIViewController {
|
|
124
|
+
private var callCoreView: CallCoreView?
|
|
125
|
+
|
|
126
|
+
// 1.创建通话页面
|
|
127
|
+
override func viewDidLoad() {
|
|
128
|
+
super.viewDidLoad()
|
|
129
|
+
view.backgroundColor = .black
|
|
130
|
+
|
|
131
|
+
// 2.通话页面绑定 CallCoreView
|
|
132
|
+
callCoreView = CallCoreView(frame: view.bounds)
|
|
133
|
+
callCoreView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
134
|
+
if let callCoreView = callCoreView {
|
|
135
|
+
view.addSubview(callCoreView)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**CallCoreView 视图组件功能说明:**
|
|
142
|
+
|
|
143
|
+
| 功能 | 说明 | 参考文档 |
|
|
144
|
+
|------|------|----------|
|
|
145
|
+
| **设置布局模式** | 支持自由切换布局模式。若未设置,将根据通话人数自动适配布局。 | 切换布局模式 |
|
|
146
|
+
| **设置头像** | 支持通过传入头像资源路径,为特定用户自定义头像。 | 自定义默认头像 |
|
|
147
|
+
| **设置音量提示图标** | 支持根据不同音量等级,配置个性化的音量指示图标。 | 自定义音量提示的图标 |
|
|
148
|
+
| **设置网络提示图标** | 支持根据实时网络质量,配置对应的网络状态提示图标。 | 自定义网络提示的图标 |
|
|
149
|
+
| **设置等待接听用户的动画** | 在多人通话场景下,支持传入 GIF 图像路径,为待接听状态的用户展示动画。 | 自定义 loading 动画 |
|
|
150
|
+
|
|
151
|
+
### 步骤2:添加通话控制按钮
|
|
152
|
+
|
|
153
|
+
您可以参考 [DeviceStore](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/devicestore)、[CallStore](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callstore) 提供的 API,自定义添加您的按钮。
|
|
154
|
+
|
|
155
|
+
- **DeviceStore 功能说明:** 麦克风(开关/音量)、摄像头(开关/切换/画质)、屏幕共享,设备状态实时监听。建议将对应方法绑定至按钮点击事件,并通过监听设备状态变更来实时刷新按钮的 UI 状态。
|
|
156
|
+
- **CallStore 功能说明:** 接听、挂断、拒接等核心通话控制能力。建议将对应方法绑定至按钮点击事件,并监听通话状态的变化,以确保按钮显示与当前通话阶段保持同步。
|
|
157
|
+
|
|
158
|
+
**图标资源下载:** 按钮图标可以直接从 [GitHub](https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call) 下载。这些图标由我们的设计师专为 TUICallKit 打造,无版权风险,可放心使用。
|
|
159
|
+
|
|
160
|
+
| 图标 | 下载地址 |
|
|
161
|
+
|------|----------|
|
|
162
|
+
| 拨号图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_dialing.imageset |
|
|
163
|
+
| 挂断图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_hangup.imageset |
|
|
164
|
+
| 摄像头关闭图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_camera_off.imageset |
|
|
165
|
+
| 摄像头开启图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_camera_on.imageset |
|
|
166
|
+
| 静音开启图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_mute_on.imageset |
|
|
167
|
+
| 静音图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_mute.imageset |
|
|
168
|
+
| 免提图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_handsfree.imageset |
|
|
169
|
+
| 免提开启图标 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_handsfree_on.imageset |
|
|
170
|
+
|
|
171
|
+
**以添加挂断、麦克风、摄像头按钮为例,实现方式如下:**
|
|
172
|
+
|
|
173
|
+
#### 1.1 添加挂断按钮
|
|
174
|
+
|
|
175
|
+
创建并添加挂断按钮,在点击事件中调用 hangup 接口并销毁界面。
|
|
176
|
+
|
|
177
|
+
```swift
|
|
178
|
+
import UIKit
|
|
179
|
+
import AtomicXCore
|
|
180
|
+
import Combine
|
|
181
|
+
|
|
182
|
+
class CallViewController: UIViewController {
|
|
183
|
+
private lazy var buttonHangup: UIButton = {
|
|
184
|
+
let buttonWidth: CGFloat = 80
|
|
185
|
+
let buttonHeight: CGFloat = 80
|
|
186
|
+
let spacing: CGFloat = 30
|
|
187
|
+
let bottomMargin: CGFloat = 80
|
|
188
|
+
let totalWidth = buttonWidth * 3 + spacing * 2
|
|
189
|
+
let startX = (view.bounds.width - totalWidth) / 2
|
|
190
|
+
let buttonY = view.bounds.height - bottomMargin - buttonHeight
|
|
191
|
+
|
|
192
|
+
let button = createButton(
|
|
193
|
+
frame: CGRect(x: startX + (buttonWidth + spacing) * 2, y: buttonY, width: buttonWidth, height: buttonHeight),
|
|
194
|
+
title: "挂断"
|
|
195
|
+
)
|
|
196
|
+
button.backgroundColor = .systemRed
|
|
197
|
+
button.addTarget(self, action: #selector(touchHangupButton), for: .touchUpInside)
|
|
198
|
+
return button
|
|
199
|
+
}()
|
|
200
|
+
|
|
201
|
+
override func viewDidLoad() {
|
|
202
|
+
super.viewDidLoad()
|
|
203
|
+
// 1.添加挂断按钮
|
|
204
|
+
view.addSubview(buttonHangup)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 2.在点击事件中调用 hangup 接口并销毁页面
|
|
208
|
+
@objc private func touchHangupButton() {
|
|
209
|
+
CallStore.shared.hangup(completion: nil)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private func createButton(frame: CGRect, title: String) -> UIButton {
|
|
213
|
+
let button = UIButton(type: .system)
|
|
214
|
+
button.frame = frame
|
|
215
|
+
button.setTitle(title, for: .normal)
|
|
216
|
+
button.setTitleColor(.white, for: .normal)
|
|
217
|
+
button.backgroundColor = UIColor(white: 0.3, alpha: 0.8)
|
|
218
|
+
button.layer.cornerRadius = frame.width / 2
|
|
219
|
+
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
|
|
220
|
+
return button
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### 1.2 添加麦克风开关按钮
|
|
226
|
+
|
|
227
|
+
创建并添加麦克风开关按钮,并在点击事件中调用 openLocalMicrophone 或 closeLocalMicrophone 接口。
|
|
228
|
+
|
|
229
|
+
```swift
|
|
230
|
+
import UIKit
|
|
231
|
+
import AtomicXCore
|
|
232
|
+
import Combine
|
|
233
|
+
|
|
234
|
+
class CallViewController: UIViewController {
|
|
235
|
+
private lazy var buttonMicrophone: UIButton = {
|
|
236
|
+
let buttonWidth: CGFloat = 80
|
|
237
|
+
let buttonHeight: CGFloat = 80
|
|
238
|
+
let spacing: CGFloat = 30
|
|
239
|
+
let bottomMargin: CGFloat = 80
|
|
240
|
+
let totalWidth = buttonWidth * 3 + spacing * 2
|
|
241
|
+
let startX = (view.bounds.width - totalWidth) / 2
|
|
242
|
+
let buttonY = view.bounds.height - bottomMargin - buttonHeight
|
|
243
|
+
|
|
244
|
+
let button = createButton(
|
|
245
|
+
frame: CGRect(x: startX + buttonWidth + spacing, y: buttonY, width: buttonWidth, height: buttonHeight),
|
|
246
|
+
title: "麦克风"
|
|
247
|
+
)
|
|
248
|
+
button.addTarget(self, action: #selector(touchMicrophoneButton), for: .touchUpInside)
|
|
249
|
+
return button
|
|
250
|
+
}()
|
|
251
|
+
|
|
252
|
+
override func viewDidLoad() {
|
|
253
|
+
super.viewDidLoad()
|
|
254
|
+
// 1.添加麦克风开关按钮
|
|
255
|
+
view.addSubview(buttonMicrophone)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 2.点击事件调用开启或关闭麦克风
|
|
259
|
+
@objc private func touchMicrophoneButton() {
|
|
260
|
+
let microphoneStatus = DeviceStore.shared.state.value.microphoneStatus
|
|
261
|
+
if microphoneStatus == .on {
|
|
262
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
263
|
+
} else {
|
|
264
|
+
DeviceStore.shared.openLocalMicrophone(completion: nil)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### 1.3 添加摄像头开关按钮
|
|
271
|
+
|
|
272
|
+
创建并添加摄像头开关按钮,并在点击事件中调用 openLocalCamera 或 closeLocalCamera 接口。
|
|
273
|
+
|
|
274
|
+
```swift
|
|
275
|
+
import UIKit
|
|
276
|
+
import AtomicXCore
|
|
277
|
+
import Combine
|
|
278
|
+
|
|
279
|
+
class CallViewController: UIViewController {
|
|
280
|
+
private lazy var buttonCamera: UIButton = {
|
|
281
|
+
let buttonWidth: CGFloat = 80
|
|
282
|
+
let buttonHeight: CGFloat = 80
|
|
283
|
+
let spacing: CGFloat = 30
|
|
284
|
+
let bottomMargin: CGFloat = 80
|
|
285
|
+
let totalWidth = buttonWidth * 3 + spacing * 2
|
|
286
|
+
let startX = (view.bounds.width - totalWidth) / 2
|
|
287
|
+
let buttonY = view.bounds.height - bottomMargin - buttonHeight
|
|
288
|
+
|
|
289
|
+
let button = createButton(
|
|
290
|
+
frame: CGRect(x: startX, y: buttonY, width: buttonWidth, height: buttonHeight),
|
|
291
|
+
title: "摄像头"
|
|
292
|
+
)
|
|
293
|
+
button.addTarget(self, action: #selector(touchCameraButton), for: .touchUpInside)
|
|
294
|
+
return button
|
|
295
|
+
}()
|
|
296
|
+
|
|
297
|
+
override func viewDidLoad() {
|
|
298
|
+
super.viewDidLoad()
|
|
299
|
+
// 1.添加摄像头开关按钮
|
|
300
|
+
view.addSubview(buttonCamera)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 2.摄像头按钮点击事件
|
|
304
|
+
@objc private func touchCameraButton() {
|
|
305
|
+
let cameraStatus = DeviceStore.shared.state.value.cameraStatus
|
|
306
|
+
if cameraStatus == .on {
|
|
307
|
+
DeviceStore.shared.closeLocalCamera()
|
|
308
|
+
} else {
|
|
309
|
+
let isFront = DeviceStore.shared.state.value.isFrontCamera
|
|
310
|
+
DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### 1.4 实时更新媒体设备按钮文本
|
|
317
|
+
|
|
318
|
+
监听麦克风和摄像头的状态,实时更新按钮文本。
|
|
319
|
+
|
|
320
|
+
```swift
|
|
321
|
+
import UIKit
|
|
322
|
+
import AtomicXCore
|
|
323
|
+
import Combine
|
|
324
|
+
|
|
325
|
+
class CallViewController: UIViewController {
|
|
326
|
+
private var cancellables = Set<AnyCancellable>()
|
|
327
|
+
private var buttonCamera: UIButton?
|
|
328
|
+
private var buttonMicrophone: UIButton?
|
|
329
|
+
|
|
330
|
+
override func viewDidLoad() {
|
|
331
|
+
super.viewDidLoad()
|
|
332
|
+
// 1.监听麦克风和摄像头的状态
|
|
333
|
+
observeDeviceState()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func observeDeviceState() {
|
|
337
|
+
DeviceStore.shared.state.subscribe()
|
|
338
|
+
.map { $0.cameraStatus }
|
|
339
|
+
.removeDuplicates()
|
|
340
|
+
.receive(on: DispatchQueue.main)
|
|
341
|
+
.sink { [weak self] cameraStatus in
|
|
342
|
+
// 2.更新摄像头按钮文本
|
|
343
|
+
let title = cameraStatus == .on ? "关闭摄像头" : "开启摄像头"
|
|
344
|
+
self?.buttonCamera?.setTitle(title, for: .normal)
|
|
345
|
+
}
|
|
346
|
+
.store(in: &cancellables)
|
|
347
|
+
|
|
348
|
+
DeviceStore.shared.state.subscribe()
|
|
349
|
+
.map { $0.microphoneStatus }
|
|
350
|
+
.removeDuplicates()
|
|
351
|
+
.receive(on: DispatchQueue.main)
|
|
352
|
+
.sink { [weak self] microphoneStatus in
|
|
353
|
+
// 2.更新麦克风按钮文本
|
|
354
|
+
let title = microphoneStatus == .on ? "关闭麦克风" : "开启麦克风"
|
|
355
|
+
self?.buttonMicrophone?.setTitle(title, for: .normal)
|
|
356
|
+
}
|
|
357
|
+
.store(in: &cancellables)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 步骤3:申请麦克风/摄像头权限
|
|
363
|
+
|
|
364
|
+
建议在发起通话前,先行检测音视频权限。若权限缺失,请引导用户动态申请。实现方法如下:
|
|
365
|
+
|
|
366
|
+
**1. 声明权限:** 请在应用的 `Info.plist` 文件中添加以下两项,并填写对应的使用说明,这些说明将在系统请求权限时向用户显示:
|
|
367
|
+
|
|
368
|
+
```xml
|
|
369
|
+
<key>NSCameraUsageDescription</key>
|
|
370
|
+
<string>需要访问您的摄像头,并可用于视频通话、群组视频通话等功能</string>
|
|
371
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
372
|
+
<string>需要访问您的麦克风,并可用于音频通话、群组音频通话、视频通话、群组视频通话等功能</string>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**2. 动态申请权限:** 我们推荐您在发起通话时,根据通话媒体类型动态申请音视频权限。以下是权限处理的示例代码:
|
|
376
|
+
|
|
377
|
+
```swift
|
|
378
|
+
import AVFoundation
|
|
379
|
+
import UIKit
|
|
380
|
+
|
|
381
|
+
extension UIViewController {
|
|
382
|
+
func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
|
|
383
|
+
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
384
|
+
switch status {
|
|
385
|
+
case .authorized:
|
|
386
|
+
completion(true)
|
|
387
|
+
case .notDetermined:
|
|
388
|
+
AVCaptureDevice.requestAccess(for: .audio) { granted in
|
|
389
|
+
DispatchQueue.main.async { completion(granted) }
|
|
390
|
+
}
|
|
391
|
+
case .denied, .restricted:
|
|
392
|
+
completion(false)
|
|
393
|
+
@unknown default:
|
|
394
|
+
completion(false)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func checkCameraPermission(completion: @escaping (Bool) -> Void) {
|
|
399
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
400
|
+
switch status {
|
|
401
|
+
case .authorized:
|
|
402
|
+
completion(true)
|
|
403
|
+
case .notDetermined:
|
|
404
|
+
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
405
|
+
DispatchQueue.main.async { completion(granted) }
|
|
406
|
+
}
|
|
407
|
+
case .denied, .restricted:
|
|
408
|
+
completion(false)
|
|
409
|
+
@unknown default:
|
|
410
|
+
completion(false)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
func showPermissionAlert(message: String) {
|
|
415
|
+
let alert = UIAlertController(title: "权限请求", message: message, preferredStyle: .alert)
|
|
416
|
+
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
|
|
417
|
+
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
418
|
+
UIApplication.shared.open(url)
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
422
|
+
present(alert, animated: true)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 步骤4:发起通话
|
|
428
|
+
|
|
429
|
+
您可以在 calls 调用成功后跳转通话界面,我们建议您根据媒体类型自动开启麦克风或摄像头获得更好的通话体验,实现方式如下:
|
|
430
|
+
|
|
431
|
+
1. **发起通话:** 调用 `calls` 发起通话。
|
|
432
|
+
2. **开启媒体设备:** 发起通话成功后开启麦克风,如果是视频通话同时开启摄像头。
|
|
433
|
+
3. **唤起通话页面:** 发起通话成功,唤起通话页面。
|
|
434
|
+
|
|
435
|
+
```swift
|
|
436
|
+
import UIKit
|
|
437
|
+
import AtomicXCore
|
|
438
|
+
import Combine
|
|
439
|
+
|
|
440
|
+
class MainViewController: UIViewController {
|
|
441
|
+
// 1.发起通话
|
|
442
|
+
private func startCall(userIdList: [String], mediaType: CallMediaType) {
|
|
443
|
+
var params = CallParams()
|
|
444
|
+
params.timeout = 30
|
|
445
|
+
|
|
446
|
+
CallStore.shared.calls(
|
|
447
|
+
participantIds: userIdList,
|
|
448
|
+
mediaType: mediaType,
|
|
449
|
+
params: params
|
|
450
|
+
) { [weak self] result in
|
|
451
|
+
switch result {
|
|
452
|
+
case .success:
|
|
453
|
+
// 2.开启媒体设备
|
|
454
|
+
self?.openDevices(for: mediaType)
|
|
455
|
+
// 3.唤起通话页面
|
|
456
|
+
DispatchQueue.main.async {
|
|
457
|
+
let callVC = CallViewController()
|
|
458
|
+
callVC.modalPresentationStyle = .fullScreen
|
|
459
|
+
self?.present(callVC, animated: true)
|
|
460
|
+
}
|
|
461
|
+
case .failure(let error):
|
|
462
|
+
Log.error("发起通话失败: \(error)")
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private func openDevices(for mediaType: CallMediaType) {
|
|
468
|
+
DeviceStore.shared.openLocalMicrophone(completion: nil)
|
|
469
|
+
if mediaType == .video {
|
|
470
|
+
let isFront = DeviceStore.shared.state.value.isFrontCamera
|
|
471
|
+
DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**calls 接口参数详细说明:**
|
|
478
|
+
|
|
479
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
480
|
+
|------|------|----------|------|
|
|
481
|
+
| participantIds | [String] | 是 | 目标用户的 userId 列表 |
|
|
482
|
+
| mediaType | CallMediaType | 是 | 通话媒体类型。`CallMediaType.video`:视频通话;`CallMediaType.audio`:语音通话 |
|
|
483
|
+
| params | CallParams | 否 | 通话扩展参数:`roomId`(String)房间ID;`timeout`(Int)呼叫超时时间(秒);`userData`(String)用户自定义数据;`chatGroupId`(String)Chat群组ID;`isEphemeralCall`(Bool)是否为加密通话 |
|
|
484
|
+
|
|
485
|
+
### 步骤5:结束通话
|
|
486
|
+
|
|
487
|
+
无论您调用 `hangup` 挂断还是对方主动结束通话,均会触发 `onCallEnded` 事件。建议监听该事件,当该事件被触发(即通话结束)时,执行关闭当前界面的操作。实现方式如下:
|
|
488
|
+
|
|
489
|
+
1. **监听通话结束事件:** 监听 `onCallEnded` 事件。
|
|
490
|
+
2. **销毁通话页面:** `onCallEnded` 触发后,销毁通话页面。
|
|
491
|
+
|
|
492
|
+
```swift
|
|
493
|
+
import UIKit
|
|
494
|
+
import AtomicXCore
|
|
495
|
+
import Combine
|
|
496
|
+
|
|
497
|
+
class CallViewController: UIViewController {
|
|
498
|
+
private var cancellables = Set<AnyCancellable>()
|
|
499
|
+
|
|
500
|
+
override func viewDidLoad() {
|
|
501
|
+
super.viewDidLoad()
|
|
502
|
+
// 1.添加通话事件监听
|
|
503
|
+
addListener()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private func addListener() {
|
|
507
|
+
CallStore.shared.callEventPublisher
|
|
508
|
+
.receive(on: DispatchQueue.main)
|
|
509
|
+
.sink { [weak self] event in
|
|
510
|
+
if case .onCallEnded = event {
|
|
511
|
+
// 2.销毁通话页面
|
|
512
|
+
self?.dismiss(animated: true)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
.store(in: &cancellables)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**onCallEnded 事件参数详细说明:**
|
|
521
|
+
|
|
522
|
+
| 参数 | 类型 | 说明 |
|
|
523
|
+
|------|------|------|
|
|
524
|
+
| `callId` | String | 此次通话的唯一标识 |
|
|
525
|
+
| `mediaType` | CallMediaType | 通话媒体类型。`CallMediaType.video`:视频通话;`CallMediaType.audio`:语音通话 |
|
|
526
|
+
| `reason` | CallEndReason | 通话结束原因:`CallEndReason.unknown`未知;`CallEndReason.hangup`正常挂断;`CallEndReason.reject`拒绝接听;`CallEndReason.noResponse`无响应;`CallEndReason.offline`对方离线;`CallEndReason.lineBusy`对方忙线;`CallEndReason.canceled`通话取消;`CallEndReason.otherDeviceAccepted`其他设备已接听;`CallEndReason.otherDeviceReject`其他设备已拒绝;`CallEndReason.endByServer`服务器结束 |
|
|
527
|
+
| `userId` | String | 触发结束的用户 ID |
|
|
528
|
+
|
|
529
|
+
### 运行效果
|
|
530
|
+
|
|
531
|
+
当您完成以上 5 步后,"拨打一通电话"运行效果如下:
|
|
532
|
+
|
|
533
|
+
## 定制页面
|
|
534
|
+
|
|
535
|
+
**CallCoreView** 提供了完善的 UI 定制能力,支持头像及音量提示等图标的自由替换。为助力快速集成,您可以直接从 [GitHub](https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call) 下载。无版权风险,可放心使用。
|
|
536
|
+
|
|
537
|
+
### 自定义音量提示的图标
|
|
538
|
+
|
|
539
|
+
您可以调用 **CallCoreView** 组件的 [setVolumeLevelIcons](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview/setvolumelevelicons(icons:)) 设置音量大小等级不同的提示图标。
|
|
540
|
+
|
|
541
|
+
**setVolumeLevelIcons 示例代码:**
|
|
542
|
+
|
|
543
|
+
```swift
|
|
544
|
+
let volumeLevelIcons: [VolumeLevel: String] = [
|
|
545
|
+
.mute: "对应图标资源的路径"
|
|
546
|
+
]
|
|
547
|
+
callCoreView.setVolumeLevelIcons(icons: volumeLevelIcons)
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**setVolumeLevelIcons 接口参数详细说明:**
|
|
551
|
+
|
|
552
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
553
|
+
|------|------|----------|------|
|
|
554
|
+
| icons | [VolumeLevel: String] | 是 | 音量等级与图标资源的映射表。Key为VolumeLevel:`.mute`麦克风关闭/静音;`.low`音量(0-25];`.medium`音量(25-50];`.high`音量(50-75];`.peak`音量(75-100]。Value为对应图标资源路径 |
|
|
555
|
+
|
|
556
|
+
**音量提示图标:**
|
|
557
|
+
|
|
558
|
+
| 文件名 | 说明 | 下载地址 |
|
|
559
|
+
|--------|------|----------|
|
|
560
|
+
| icon_volume | 【图标含义】音量提示图标。<br>【推荐用法】您可以将该图标等级设置为 `.low` 或 `.medium`,当用户音量大于对应等级时显示。 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_volume.imageset |
|
|
561
|
+
| icon_mic_off | 【图标含义】静音图标。<br>【推荐用法】您可以将该图标等级设置为 `.mute`,当该用户静音时显示。 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/icon_mic_off.imageset |
|
|
562
|
+
|
|
563
|
+
### 自定义网络提示的图标
|
|
564
|
+
|
|
565
|
+
您可以调用 **CallCoreView** 组件的 [setNetworkQualityIcons](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview/setnetworkqualityicons(icons:)) 设置不同网络状态的提示图标。
|
|
566
|
+
|
|
567
|
+
**setNetworkQualityIcons 示例代码:**
|
|
568
|
+
|
|
569
|
+
```swift
|
|
570
|
+
let networkQualityIcons: [NetworkQuality: String] = [
|
|
571
|
+
.bad: "对应图标的路径"
|
|
572
|
+
]
|
|
573
|
+
callCoreView.setNetworkQualityIcons(icons: networkQualityIcons)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**setNetworkQualityIcons 接口参数详细说明:**
|
|
577
|
+
|
|
578
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
579
|
+
|------|------|----------|------|
|
|
580
|
+
| icons | [NetworkQuality: String] | 是 | 网络质量与图标资源的映射表。Key为NetworkQuality:`.unknown`未知;`.excellent`极佳;`.good`较好;`.poor`较差;`.bad`差;`.veryBad`极差;`.down`网络断开。Value为对应图标资源路径 |
|
|
581
|
+
|
|
582
|
+
**网络较差的提示图标:**
|
|
583
|
+
|
|
584
|
+
| 文件名 | 说明 | 下载地址 |
|
|
585
|
+
|--------|------|----------|
|
|
586
|
+
| group_network_low_quality | 【图标含义】网络较差的提示图标。<br>【推荐用法】您可以将该图标等级设置为 `.bad`、`.veryBad` 或 `.down`,当网络较差时显示该图标。 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/group_network_low_quality.imageset |
|
|
587
|
+
|
|
588
|
+
### 自定义默认头像
|
|
589
|
+
|
|
590
|
+
您可以调用 **CallCoreView** 的 [setParticipantAvatars](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview/setparticipantavatars(avatars:)) 接口设置用户头像。建议监听响应式数据 allParticipants。
|
|
591
|
+
|
|
592
|
+
**setParticipantAvatars 示例代码:**
|
|
593
|
+
|
|
594
|
+
```swift
|
|
595
|
+
var avatars: [String: String] = [:]
|
|
596
|
+
let userId = "" // 用户 ID
|
|
597
|
+
let avatarPath = "" // 用户默认头像资源的路径
|
|
598
|
+
avatars[userId] = avatarPath
|
|
599
|
+
callCoreView.setParticipantAvatars(avatars: avatars)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
**setParticipantAvatars 接口参数详细说明:**
|
|
603
|
+
|
|
604
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
605
|
+
|------|------|----------|------|
|
|
606
|
+
| avatars | [String: String] | 是 | 用户头像映射表。Key:用户的 userID;Value:该用户的头像资源绝对路径 |
|
|
607
|
+
|
|
608
|
+
**默认头像资源:**
|
|
609
|
+
|
|
610
|
+
| 文件名 | 说明 | 下载地址 |
|
|
611
|
+
|--------|------|----------|
|
|
612
|
+
| default_participant_icon | 【图标含义】默认头像。<br>【推荐用法】当用户头像加载失败或无头像时,您可以给该用户设置此默认头像。 | https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/default_participant_icon.imageset |
|
|
613
|
+
|
|
614
|
+
### 自定义 loading 动画
|
|
615
|
+
|
|
616
|
+
您可以调用 **CallCoreView** 的 [setWaitingAnimation](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview/setwaitinganimation(path:)) 接口,为等待中用户设置等待动画。
|
|
617
|
+
|
|
618
|
+
**setWaitingAnimation 示例代码:**
|
|
619
|
+
|
|
620
|
+
```swift
|
|
621
|
+
let waitingAnimationPath = "" // 等待动画 GIF 图像资源的路径
|
|
622
|
+
callCoreView.setWaitingAnimation(path: waitingAnimationPath)
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**setWaitingAnimation 接口参数详细说明:**
|
|
626
|
+
|
|
627
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
628
|
+
|------|------|----------|------|
|
|
629
|
+
| path | String | 是 | GIF 格式图像资源的绝对路径 |
|
|
630
|
+
|
|
631
|
+
**等待接听的动画:**
|
|
632
|
+
|
|
633
|
+
| 文件名 | 说明 | 下载地址 |
|
|
634
|
+
|--------|------|----------|
|
|
635
|
+
| loading.gif | 【图标含义】用户等待接听动画。<br>【推荐用法】群组通话时设置的动画。设置后,当用户的状态为等待接听时,显示该动画。 | https://github.com/Tencent-RTC/TUIKit_iOS/blob/main/atomic_x/Resources/CallResource/loading.gif |
|
|
636
|
+
|
|
637
|
+
### 添加通话计时提示
|
|
638
|
+
|
|
639
|
+
通话计时通过响应式数据 [activeCall](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callstate) 的 `duration` 字段实时获得。
|
|
640
|
+
|
|
641
|
+
1. **数据层订阅:** 订阅 `CallStore.shared.state`,建立当前活跃通话的响应式监听。
|
|
642
|
+
2. **绑定通话计时数据:** 将 `activeCall.duration` 字段绑定至 UI 控件。该字段为响应式数据,会自动驱动 UI 实时刷新,无需手动维护定时器。
|
|
643
|
+
|
|
644
|
+
```swift
|
|
645
|
+
import UIKit
|
|
646
|
+
import AtomicXCore
|
|
647
|
+
import Combine
|
|
648
|
+
|
|
649
|
+
class TimerView: UILabel {
|
|
650
|
+
private var cancellables = Set<AnyCancellable>()
|
|
651
|
+
|
|
652
|
+
override init(frame: CGRect) {
|
|
653
|
+
super.init(frame: frame)
|
|
654
|
+
setupView()
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
required init?(coder: NSCoder) {
|
|
658
|
+
super.init(coder: coder)
|
|
659
|
+
setupView()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private func setupView() {
|
|
663
|
+
textColor = .white
|
|
664
|
+
textAlignment = .center
|
|
665
|
+
font = .systemFont(ofSize: 16)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
override func didMoveToWindow() {
|
|
669
|
+
super.didMoveToWindow()
|
|
670
|
+
if window != nil {
|
|
671
|
+
// 1.数据层订阅
|
|
672
|
+
registerActiveCallObserver()
|
|
673
|
+
} else {
|
|
674
|
+
cancellables.removeAll()
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private func registerActiveCallObserver() {
|
|
679
|
+
CallStore.shared.state.subscribe()
|
|
680
|
+
.map { $0.activeCall }
|
|
681
|
+
.removeDuplicates { $0.duration == $1.duration }
|
|
682
|
+
.receive(on: DispatchQueue.main)
|
|
683
|
+
.sink { [weak self] activeCall in
|
|
684
|
+
// 2.绑定通话计时数据
|
|
685
|
+
self?.updateDurationView(activeCall: activeCall)
|
|
686
|
+
}
|
|
687
|
+
.store(in: &cancellables)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private func updateDurationView(activeCall: CallInfo) {
|
|
691
|
+
let currentDuration = Int(activeCall.duration)
|
|
692
|
+
let minutes = currentDuration / 60
|
|
693
|
+
let seconds = currentDuration % 60
|
|
694
|
+
text = String(format: "%02d:%02d", minutes, seconds)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
## 更多功能
|
|
700
|
+
|
|
701
|
+
### 自定义用户头像/昵称
|
|
702
|
+
|
|
703
|
+
通话开始前,您可以通过 `setSelfInfo` 方法,设置自己的昵称和头像。
|
|
704
|
+
|
|
705
|
+
**setSelfInfo 示例代码:**
|
|
706
|
+
|
|
707
|
+
```swift
|
|
708
|
+
import AtomicXCore
|
|
709
|
+
|
|
710
|
+
var userProfile = UserProfile()
|
|
711
|
+
userProfile.userID = "" // 您的 userID
|
|
712
|
+
userProfile.avatarURL = "" // 头像的 url
|
|
713
|
+
userProfile.nickname = "" // 需要设置的昵称
|
|
714
|
+
|
|
715
|
+
LoginStore.shared.setSelfInfo(userProfile: userProfile) { result in
|
|
716
|
+
switch result {
|
|
717
|
+
case .success:
|
|
718
|
+
// 设置成功回调
|
|
719
|
+
case .failure(let error):
|
|
720
|
+
// 设置失败回调
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**setSelfInfo 接口参数详细说明:**
|
|
726
|
+
|
|
727
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
728
|
+
|------|------|----------|------|
|
|
729
|
+
| `userProfile` | [UserProfile](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/userprofile) | 是 | 用户信息结构体:<br><br>- `userID`:用户的 ID。<br>- `avatarURL`:用户头像的 URL。<br>- `nickname`:用户的昵称。<br><br>更多字段详情可参考 [UserProfile](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/userprofile)。 |
|
|
730
|
+
| `completion` | CompletionHandler | 否 | 【字段含义】操作完成回调,用于返回接通电话的结果。 |
|
|
731
|
+
|
|
732
|
+
### 切换布局模式
|
|
733
|
+
|
|
734
|
+
您可以通过 [setLayoutTemplate](https://tencent-rtc.github.io/TUIKit_iOS/documentation/atomicxcore/callcoreview/setlayouttemplate(_:)) 接口灵活切换布局模式。若未主动配置,**CallCoreView** 将根据通话人数自动适配:1v1 默认 `Float` 模式,多人通话自动切换为 `Grid` 模式。
|
|
735
|
+
|
|
736
|
+
**三种布局模式对比:**
|
|
737
|
+
|
|
738
|
+
| 模式 | 布局逻辑 | 交互特性 |
|
|
739
|
+
|------|----------|----------|
|
|
740
|
+
| **Float** | 呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。 | 支持小窗拖拽移动,点击小窗可实现大小画面互换。 |
|
|
741
|
+
| **Grid** | 所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话。 | 支持点击特定成员画面放大查看。 |
|
|
742
|
+
| **Pip** | 1v1 场景固定显示对方画面;多人场景采用 Active Speaker 策略,自动全屏展示正在说话的用户。 | 等待时显示自己的画面,接通后还会显示通话计时。 |
|
|
743
|
+
|
|
744
|
+
**setLayoutTemplate 示例代码:**
|
|
745
|
+
|
|
746
|
+
```swift
|
|
747
|
+
let callCoreView = CallCoreView(frame: view.bounds)
|
|
748
|
+
callCoreView.setLayoutTemplate(.grid)
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
**setLayoutTemplate 接口参数详细说明:**
|
|
752
|
+
|
|
753
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
754
|
+
|------|------|----------|------|
|
|
755
|
+
| template | CallLayoutTemplate | 是 | 布局模式:`.float`(浮窗)、`.grid`(宫格)、`.pip`(画中画) |
|
|
756
|
+
|
|
757
|
+
### 设置通话的默认超时时间
|
|
758
|
+
|
|
759
|
+
在发起通话时,通过 `CallParams` 中的 `timeout` 字段指定等待超时时间。
|
|
760
|
+
|
|
761
|
+
```swift
|
|
762
|
+
var callParams = CallParams()
|
|
763
|
+
callParams.timeout = 30 // 设置通话超时为30秒
|
|
764
|
+
|
|
765
|
+
CallStore.shared.calls(
|
|
766
|
+
participantIds: userIdList,
|
|
767
|
+
mediaType: .video,
|
|
768
|
+
params: callParams,
|
|
769
|
+
completion: nil
|
|
770
|
+
)
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
| 参数 | 类型 | 是否必填 | 说明 |
|
|
774
|
+
|------|------|----------|------|
|
|
775
|
+
| userIdList | [String] | 是 | 目标用户的 userId 列表 |
|
|
776
|
+
| mediaType | CallMediaType | 是 | 通话媒体类型。`CallMediaType.video`:视频通话;`CallMediaType.audio`:语音通话 |
|
|
777
|
+
| params | CallParams | 否 | 通话扩展参数:`roomId`(String)房间ID,可选参数,未指定时由服务端自动分配;`timeout`(Int)呼叫超时时间(秒);`userData`(String)用户自定义数据;`chatGroupId`(String)Chat群组ID,用于群组通话场景;`isEphemeralCall`(Boolean)是否为加密通话(不产生通话记录) |
|
|
778
|
+
|
|
779
|
+
### 实现应用内悬浮窗
|
|
780
|
+
|
|
781
|
+
AtomicXCore SDK 提供了 CallPipView 组件,可以在应用内实现悬浮窗效果。当您的通话界面被其他页面覆盖时(例如用户点击了返回键但通话未结束),可以显示一个悬浮的小窗口,让用户随时查看通话状态并快速返回通话界面。
|
|
782
|
+
|
|
783
|
+
**第一步:** 创建悬浮窗控制器。
|
|
784
|
+
|
|
785
|
+
```swift
|
|
786
|
+
import UIKit
|
|
787
|
+
import AtomicXCore
|
|
788
|
+
import Combine
|
|
789
|
+
|
|
790
|
+
class FloatWindowViewController: UIViewController {
|
|
791
|
+
var tapGestureAction: (() -> Void)?
|
|
792
|
+
private var cancellables = Set<AnyCancellable>()
|
|
793
|
+
|
|
794
|
+
private lazy var callCoreView: CallCoreView = {
|
|
795
|
+
let view = CallCoreView(frame: self.view.bounds)
|
|
796
|
+
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
797
|
+
view.setLayoutTemplate(.pip)
|
|
798
|
+
view.isUserInteractionEnabled = false
|
|
799
|
+
return view
|
|
800
|
+
}()
|
|
801
|
+
|
|
802
|
+
override func viewDidLoad() {
|
|
803
|
+
super.viewDidLoad()
|
|
804
|
+
view.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
|
|
805
|
+
view.layer.cornerRadius = 10
|
|
806
|
+
view.layer.masksToBounds = true
|
|
807
|
+
|
|
808
|
+
view.addSubview(callCoreView)
|
|
809
|
+
|
|
810
|
+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
|
811
|
+
view.addGestureRecognizer(tapGesture)
|
|
812
|
+
|
|
813
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
|
814
|
+
self?.observeCallStatus()
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@objc private func handleTap() {
|
|
819
|
+
tapGestureAction?()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private func observeCallStatus() {
|
|
823
|
+
CallStore.shared.state
|
|
824
|
+
.subscribe(StatePublisherSelector<CallState, CallParticipantStatus>(keyPath: \.selfInfo.status))
|
|
825
|
+
.removeDuplicates()
|
|
826
|
+
.receive(on: DispatchQueue.main)
|
|
827
|
+
.sink { [weak self] status in
|
|
828
|
+
if status == .none {
|
|
829
|
+
NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil)
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
.store(in: &cancellables)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
deinit {
|
|
836
|
+
cancellables.removeAll()
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
**第二步:** 在主界面实现悬浮窗管理逻辑。
|
|
842
|
+
|
|
843
|
+
```swift
|
|
844
|
+
import UIKit
|
|
845
|
+
import AtomicXCore
|
|
846
|
+
|
|
847
|
+
class MainViewController: UIViewController {
|
|
848
|
+
private var floatWindow: UIWindow?
|
|
849
|
+
|
|
850
|
+
override func viewDidLoad() {
|
|
851
|
+
super.viewDidLoad()
|
|
852
|
+
|
|
853
|
+
NotificationCenter.default.addObserver(
|
|
854
|
+
self,
|
|
855
|
+
selector: #selector(showFloatingWindow),
|
|
856
|
+
name: NSNotification.Name("ShowFloatingWindow"),
|
|
857
|
+
object: nil
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
NotificationCenter.default.addObserver(
|
|
861
|
+
self,
|
|
862
|
+
selector: #selector(hideFloatingWindow),
|
|
863
|
+
name: NSNotification.Name("HideFloatingWindow"),
|
|
864
|
+
object: nil
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@objc private func showFloatingWindow() {
|
|
869
|
+
let selfStatus = CallStore.shared.state.value.selfInfo.status
|
|
870
|
+
guard selfStatus == .accept else { return }
|
|
871
|
+
guard floatWindow == nil else { return }
|
|
872
|
+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
|
|
873
|
+
|
|
874
|
+
let pipWidth: CGFloat = 100
|
|
875
|
+
let pipHeight: CGFloat = pipWidth * 16 / 9
|
|
876
|
+
let pipX = UIScreen.main.bounds.width - pipWidth - 20
|
|
877
|
+
let pipY: CGFloat = 100
|
|
878
|
+
|
|
879
|
+
let window = UIWindow(windowScene: windowScene)
|
|
880
|
+
window.windowLevel = .alert + 1
|
|
881
|
+
window.backgroundColor = .clear
|
|
882
|
+
window.frame = CGRect(x: pipX, y: pipY, width: pipWidth, height: pipHeight)
|
|
883
|
+
|
|
884
|
+
let floatVC = FloatWindowViewController()
|
|
885
|
+
floatVC.tapGestureAction = { [weak self] in
|
|
886
|
+
self?.openCallViewController()
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
window.rootViewController = floatVC
|
|
890
|
+
self.floatWindow = window
|
|
891
|
+
|
|
892
|
+
window.isHidden = false
|
|
893
|
+
window.makeKeyAndVisible()
|
|
894
|
+
|
|
895
|
+
if let mainWindow = windowScene.windows.first(where: { $0 != window }) {
|
|
896
|
+
mainWindow.makeKey()
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
@objc private func hideFloatingWindow() {
|
|
901
|
+
floatWindow?.isHidden = true
|
|
902
|
+
floatWindow = nil
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private func openCallViewController() {
|
|
906
|
+
hideFloatingWindow()
|
|
907
|
+
guard let topVC = getTopViewController() else { return }
|
|
908
|
+
let callVC = CallViewController()
|
|
909
|
+
callVC.modalPresentationStyle = .fullScreen
|
|
910
|
+
topVC.present(callVC, animated: true)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
private func getTopViewController() -> UIViewController? {
|
|
914
|
+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
|
915
|
+
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
|
|
916
|
+
let rootVC = keyWindow.rootViewController else {
|
|
917
|
+
return nil
|
|
918
|
+
}
|
|
919
|
+
var topVC = rootVC
|
|
920
|
+
while let presentedVC = topVC.presentedViewController {
|
|
921
|
+
topVC = presentedVC
|
|
922
|
+
}
|
|
923
|
+
return topVC
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
deinit {
|
|
927
|
+
NotificationCenter.default.removeObserver(self)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
**第三步:** 在通话界面添加悬浮窗触发逻辑。
|
|
933
|
+
|
|
934
|
+
```swift
|
|
935
|
+
import UIKit
|
|
936
|
+
import AtomicXCore
|
|
937
|
+
|
|
938
|
+
class CallViewController: UIViewController {
|
|
939
|
+
override func viewWillAppear(_ animated: Bool) {
|
|
940
|
+
super.viewWillAppear(animated)
|
|
941
|
+
NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
945
|
+
super.viewWillDisappear(animated)
|
|
946
|
+
let selfStatus = CallStore.shared.state.value.selfInfo.status
|
|
947
|
+
if selfStatus == .accept {
|
|
948
|
+
NotificationCenter.default.post(name: NSNotification.Name("ShowFloatingWindow"), object: nil)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### 实现应用外画中画
|
|
955
|
+
|
|
956
|
+
AtomicXCore SDK 支持通过底层 TRTC 引擎实现应用外画中画功能。当应用进入后台时,通话画面可以以系统画中画形式悬浮在其他应用之上,用户可以边使用其他应用边进行视频通话。实现方式如下:
|
|
957
|
+
|
|
958
|
+
> **说明:**
|
|
959
|
+
>
|
|
960
|
+
> 1. 需要在 Xcode 的 `Signing & Capabilities` 中添加 `Background Modes` 能力,并勾选 `Audio, AirPlay, and Picture in Picture`。
|
|
961
|
+
> 2. 需要 iOS 15.0 及以上版本支持。
|
|
962
|
+
|
|
963
|
+
1. **配置画中画参数:** 您需要设置画中画窗口的填充模式、用户视频区域、画布参数等参数。
|
|
964
|
+
|
|
965
|
+
```swift
|
|
966
|
+
import Foundation
|
|
967
|
+
import AtomicXCore
|
|
968
|
+
|
|
969
|
+
enum PictureInPictureFillMode: Int, Codable {
|
|
970
|
+
case fill = 0 // 填充模式
|
|
971
|
+
case fit = 1 // 适应模式
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// 用户视频区域
|
|
975
|
+
struct PictureInPictureRegion: Codable {
|
|
976
|
+
let userId: String // 用户 ID
|
|
977
|
+
let width: Double // 宽度(0.0-1.0,相对于画布)
|
|
978
|
+
let height: Double // 高度(0.0-1.0,相对于画布)
|
|
979
|
+
let x: Double // X 坐标(0.0-1.0,相对于画布左上角)
|
|
980
|
+
let y: Double // Y 坐标(0.0-1.0,相对于画布左上角)
|
|
981
|
+
let fillMode: PictureInPictureFillMode // 填充模式
|
|
982
|
+
let streamType: String // 流类型("high" 或 "low")
|
|
983
|
+
let backgroundColor: String // 背景颜色
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// 画布配置
|
|
987
|
+
struct PictureInPictureCanvas: Codable {
|
|
988
|
+
let width: Int // 画布宽度
|
|
989
|
+
let height: Int // 画布高度
|
|
990
|
+
let backgroundColor: String // 背景颜色
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// 画中画参数
|
|
994
|
+
struct PictureInPictureParams: Codable {
|
|
995
|
+
let enable: Bool // 是否启用画中画
|
|
996
|
+
let cameraBackgroundCapture: Bool? // 是否在后台采集摄像头
|
|
997
|
+
let canvas: PictureInPictureCanvas? // 画布配置(可选)
|
|
998
|
+
let regions: [PictureInPictureRegion]? // 用户视频区域列表(可选)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// 画中画请求
|
|
1002
|
+
struct PictureInPictureRequest: Codable {
|
|
1003
|
+
let api: String // API 名称
|
|
1004
|
+
let params: PictureInPictureParams // 参数
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
**2. 启用画中画:** 您可以通过 `configPictureInPicture` 方法启用/关闭画中画功能。
|
|
1009
|
+
|
|
1010
|
+
```swift
|
|
1011
|
+
let params = PictureInPictureParams(
|
|
1012
|
+
enable: true,
|
|
1013
|
+
cameraBackgroundCapture: true,
|
|
1014
|
+
canvas: nil,
|
|
1015
|
+
regions: nil
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
let request = PictureInPictureRequest(
|
|
1019
|
+
api: "configPictureInPicture",
|
|
1020
|
+
params: params
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
// 编码为 JSON 字符串
|
|
1024
|
+
let encoder = JSONEncoder()
|
|
1025
|
+
if let data = try? encoder.encode(request),
|
|
1026
|
+
let jsonString = String(data: data, encoding: .utf8) {
|
|
1027
|
+
TUICallEngine.createInstance().callExperimentalAPI(jsonObject: jsonString)
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### 设置通话过程中屏幕常亮
|
|
1032
|
+
|
|
1033
|
+
通话过程中保持屏幕常亮是通话应用的基本需求。iOS 提供了 `UIApplication.shared.isIdleTimerDisabled` 属性来实现此功能。
|
|
1034
|
+
|
|
1035
|
+
```swift
|
|
1036
|
+
class CallViewController: UIViewController {
|
|
1037
|
+
override func viewDidLoad() {
|
|
1038
|
+
super.viewDidLoad()
|
|
1039
|
+
// 禁用自动锁屏,保持屏幕常亮
|
|
1040
|
+
UIApplication.shared.isIdleTimerDisabled = true
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
1044
|
+
super.viewWillDisappear(animated)
|
|
1045
|
+
// 恢复自动锁屏
|
|
1046
|
+
UIApplication.shared.isIdleTimerDisabled = false
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### 播放等待接听的提示音
|
|
1052
|
+
|
|
1053
|
+
您可以监听自己的通话状态,在等待接听时播放铃声,在接听通话或通话结束时停止播放铃声。
|
|
1054
|
+
|
|
1055
|
+
```swift
|
|
1056
|
+
import Combine
|
|
1057
|
+
|
|
1058
|
+
private var cancellables = Set<AnyCancellable>()
|
|
1059
|
+
|
|
1060
|
+
private func observeSelfCallStatus() {
|
|
1061
|
+
CallStore.shared.state.subscribe()
|
|
1062
|
+
.map { $0.selfInfo.status }
|
|
1063
|
+
.removeDuplicates()
|
|
1064
|
+
.receive(on: DispatchQueue.main)
|
|
1065
|
+
.sink { [weak self] status in
|
|
1066
|
+
if status == .accept || status == .none {
|
|
1067
|
+
// 停止播放铃声
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
if status == .waiting {
|
|
1071
|
+
// 播放铃声
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
.store(in: &cancellables)
|
|
1075
|
+
}
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### 开启后台采集音频/视频
|
|
1079
|
+
|
|
1080
|
+
为了确保应用在进入后台时仍能正常采集音频和视频(例如用户锁屏或切换到其他应用),您需要配置 iOS 的后台模式权限并设置音频会话。
|
|
1081
|
+
|
|
1082
|
+
**配置步骤:** 在 Xcode 中打开您的项目,按照以下步骤操作:
|
|
1083
|
+
|
|
1084
|
+
1. 选择项目的 `Target` → `Signing & Capabilities`。
|
|
1085
|
+
2. 点击 `+ Capability`。
|
|
1086
|
+
3. 搜索并添加 `Background Modes`。
|
|
1087
|
+
4. 勾选以下三个选项:
|
|
1088
|
+
- `Audio, AirPlay, and Picture in Picture`(保持音频采集和画中画功能)。
|
|
1089
|
+
- `Voice over IP`(支持 VoIP 通话)。
|
|
1090
|
+
- `Remote notifications`(可选,用于接收离线推送)。
|
|
1091
|
+
|
|
1092
|
+
配置完成后,您的 `Info.plist` 会自动添加以下内容:
|
|
1093
|
+
|
|
1094
|
+
```xml
|
|
1095
|
+
<key>UIBackgroundModes</key>
|
|
1096
|
+
<array>
|
|
1097
|
+
<string>audio</string>
|
|
1098
|
+
<string>voip</string>
|
|
1099
|
+
<string>remote-notification</string>
|
|
1100
|
+
</array>
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
**配置音频会话(AVAudioSession):**
|
|
1104
|
+
|
|
1105
|
+
为了确保通话音频在后台正常工作,您需要在通话开始前配置音频会话。建议在通话界面的 `viewDidLoad` 或发起通话前设置:
|
|
1106
|
+
|
|
1107
|
+
```swift
|
|
1108
|
+
import AVFoundation
|
|
1109
|
+
|
|
1110
|
+
private func setupAudioSession() {
|
|
1111
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
1112
|
+
do {
|
|
1113
|
+
try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
|
|
1114
|
+
try audioSession.setActive(true)
|
|
1115
|
+
} catch {
|
|
1116
|
+
// 音频会话配置失败
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**播放铃声时的特殊处理(可选):**
|
|
1122
|
+
|
|
1123
|
+
如果您需要在等待接听时播放铃声,并且希望铃声通过扬声器播放,可以临时切换音频会话为 `.playback` 模式:
|
|
1124
|
+
|
|
1125
|
+
```swift
|
|
1126
|
+
private func setAudioSessionForRingtone() {
|
|
1127
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
1128
|
+
do {
|
|
1129
|
+
try audioSession.setCategory(.playback, options: [.allowBluetooth, .allowBluetoothA2DP])
|
|
1130
|
+
try audioSession.overrideOutputAudioPort(.speaker)
|
|
1131
|
+
try audioSession.setActive(true)
|
|
1132
|
+
} catch {
|
|
1133
|
+
// 铃声音频会话配置失败
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private func restoreAudioSessionForCall() {
|
|
1138
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
1139
|
+
do {
|
|
1140
|
+
try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
|
|
1141
|
+
try audioSession.setActive(true)
|
|
1142
|
+
} catch {
|
|
1143
|
+
// 恢复音频会话失败
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
### 设置离线推送参数
|
|
1149
|
+
|
|
1150
|
+
## 常见问题
|
|
1151
|
+
|
|
1152
|
+
### 在通话邀请超时时间内,被邀请者如果离线再上线,能否收到来电事件?
|
|
1153
|
+
|
|
1154
|
+
单人通话时,如果在超时时间内上线,会触发来电邀请;群组通话,如果在超时时间内上线,会拉起未处理的 20 条群消息,如果存在通话邀请,则触发来电邀请事件。
|