@tencent-rtc/mcp 1.4.1 → 1.4.3

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/resource/doc/callkit/core-sdk/android/answer-call.md +852 -0
  3. package/resource/doc/callkit/core-sdk/android/make-call.md +954 -0
  4. package/resource/doc/callkit/core-sdk/android/offline-push.md +361 -0
  5. package/resource/doc/callkit/core-sdk/flutter/answer-call.md +1160 -0
  6. package/resource/doc/callkit/core-sdk/flutter/make-call.md +1205 -0
  7. package/resource/doc/callkit/core-sdk/flutter/offline-push.md +398 -0
  8. package/resource/doc/callkit/core-sdk/ios/answer-call.md +1090 -0
  9. package/resource/doc/callkit/core-sdk/ios/make-call.md +1248 -0
  10. package/resource/doc/callkit/core-sdk/ios/offline-push.md +485 -0
  11. package/resource/doc/callkit/core-sdk/uni-app/answer-call.md +950 -0
  12. package/resource/doc/callkit/core-sdk/uni-app/make-call.md +1052 -0
  13. package/resource/doc/callkit/preset/android/integration.md +1 -1
  14. package/resource/doc/callkit/preset/flutter/integration.md +1 -1
  15. package/resource/doc/callkit/preset/ios/integration.md +1 -1
  16. package/resource/doc/callkit/preset/react/api.md +1 -1
  17. package/resource/doc/callkit/preset/vue/api.md +1 -1
  18. package/server.js +1 -1
  19. package/tools/call/get_native_callkit_core_integration.js +67 -0
  20. package/tools/call/get_native_callkit_core_integration.js.map +1 -0
  21. package/tools/common/present_framework_choice.js +1 -1
  22. package/tools/common/present_framework_choice.js.map +1 -1
  23. package/tools/common/record_skill_session.js +17 -6
  24. package/tools/common/record_skill_session.js.map +1 -1
  25. package/tools/definitions/get_native_call_uikit_integration_def.js +9 -3
  26. package/tools/definitions/get_native_call_uikit_integration_def.js.map +1 -1
  27. package/tools/definitions/get_native_callkit_core_integration_def.js +63 -0
  28. package/tools/definitions/get_native_callkit_core_integration_def.js.map +1 -0
  29. package/tools/index.js +2 -0
  30. package/tools/index.js.map +1 -1
  31. package/utils/report-cls-client.js +1 -1
  32. package/utils/report-es-client.js +1 -1
  33. package/utils/report-runtime-error-cls-client.js +91 -0
  34. package/utils/report-runtime-error-cls-client.js.map +1 -0
@@ -0,0 +1,1052 @@
1
+ This document helps you build a call-enabled **App** quickly by using the **AtomicXCore SDK** components — **DeviceState**, **CallState**, and the core widget **CallCoreView**.
2
+
3
+ ## Preparation
4
+
5
+ ### Step 1: Activate the Service
6
+
7
+ Follow the **Activate the Service** guide to obtain either a trial or paid version of the SDK.
8
+
9
+ ### Step 2: Import AtomicXCore into the Current Project
10
+
11
+ 1. **Install the component:** Open the [AtomicXCore SDK](https://ext.dcloud.net.cn/plugin?name=tuikit-atomic-x) marketplace page; click **Download Plugin and Import to HBuilderX**, select the target project, and click **Confirm**. Alternatively, download the source from the [Tencent-RTC/TUIKit_uni-app](https://github.com/Tencent-RTC/TUIKit_uni-app) repo and copy the `App/uni_modules/tuikit-atomic-x` directory into your project's `uni_modules/` folder.
12
+
13
+ 2. **Configure project permissions:** Under `app-plus > distribute` in your project's `manifest.json`, make sure the following permissions are declared.
14
+
15
+ **Android (`Android` node):** Add the following entries to the `permissions` array:
16
+
17
+ ```json
18
+ "<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />",
19
+ "<uses-permission android:name=\"android.permission.CAMERA\"/>"
20
+ ```
21
+
22
+ **iOS (`iOS` node):** Add the following entries to the `privacyDescription` object:
23
+
24
+ ```json
25
+ "NSCameraUsageDescription" : "The app needs camera access to make calls",
26
+ "NSMicrophoneUsageDescription" : "The app needs microphone access to make calls"
27
+ ```
28
+
29
+ Add `audio` to the `UIBackgroundModes` array to support background audio playback:
30
+
31
+ ```json
32
+ "UIBackgroundModes" : [ "audio" ]
33
+ ```
34
+
35
+ ### Step 3: Implement the Login Logic
36
+
37
+ In your project, call the `login` method of `LoginState` to complete the login. **This is the prerequisite for using every feature of AtomicXCore.**
38
+
39
+ > **Important:**
40
+ >
41
+ > We recommend calling the `login` method of `LoginState` after your app's own user account has successfully logged in, so that the login flow stays clear and consistent.
42
+
43
+ ```typescript
44
+ import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
45
+
46
+ const { login } = useLoginState();
47
+
48
+ const handleLogin = () => {
49
+ login({
50
+ sdkAppID: 1400000001, // Replace with your SDKAppID
51
+ userID: "test_001", // Replace with your UserID
52
+ userSig: "xxxxxxxxxx" // Use a temporary UserSig during development; when the MCP Server is configured with sdkAppID and sdkSecretKey, the tool can issue it for you
53
+ })
54
+ }
55
+ ```
56
+
57
+ **Login parameter details:**
58
+
59
+ | **Parameter** | **Type** | **Description** |
60
+ | --- | --- | --- |
61
+ | `sdkAppID` | number | Obtain from the console; usually a 10-digit integer starting with `140` or `160`. |
62
+ | `userID` | string | Unique user ID, up to **32 bytes**, with **no special characters**; use letters and digits. Avoid simple IDs like `1` or `123` to reduce multi-device conflicts. |
63
+ | `userSig` | string | Authentication token. For development, generate a temporary `userSig` locally with `genTestUserSig` (see <https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/debug/GenerateTestUserSig.js>). When the **Tencent RTC MCP Server** is configured with `sdkAppID` and `sdkSecretKey`, the tool can auto-issue a `userSig` for you. **In production**, the business server must sign the UserSig with the secret key — never store or distribute the SecretKey on the client. |
64
+
65
+ ## Make Your First Call
66
+
67
+ ### Step 1: Create the Call Page
68
+
69
+ You need a dedicated call page that is launched when a call starts. Implementation:
70
+
71
+ 1. **Create the call page:** Add a new page to host the call UI and handle navigation when an incoming call arrives.
72
+
73
+ 2. **Mount CallCoreView on the call page:** `CallCoreView` is the core call-view component. It automatically observes `CallState` and renders the call UI, and it supports layout switching, avatar and icon customization, and other UI customizations.
74
+
75
+ ```vue
76
+ <call-core-view style="height: 50px; width: 50px;"></call-core-view>
77
+ ```
78
+
79
+ > **Note:**
80
+ >
81
+ > `CallCoreView` is a native component and does not need an explicit `import` in the file. Just place it on the page.
82
+
83
+ **CallCoreView feature overview:**
84
+
85
+ | **Feature** | **Description** | **Reference** |
86
+ | --- | --- | --- |
87
+ | **Set layout mode** | Supports flexible layout switching. If not set, the layout adapts automatically based on the number of participants. | Switch Layout Modes |
88
+ | **Set avatar** | Supports custom avatars for specific users by passing avatar resource paths. | Customize Default Avatar |
89
+ | **Set volume-level icons** | Supports custom volume-level indicator icons for different levels. | Customize Volume Level Icons |
90
+ | **Set network-quality icons** | Supports custom network-quality indicator icons based on real-time network status. | Customize Network Quality Icons |
91
+ | **Set loading animation** | In group calls, supports a GIF animation to indicate users in the waiting state. | Customize Loading Animation |
92
+
93
+ ### Step 2: Add Call Control Buttons
94
+
95
+ You can build your own buttons on top of the APIs provided by **DeviceState** and **CallState**.
96
+
97
+ - **DeviceState:** Microphone (toggle/volume), camera (toggle/switch/quality), screen sharing, and real-time device-state listening. Bind methods to button taps and watch the device state to refresh the button UI in real time.
98
+ - **CallState:** Core call actions such as answer, hang up, and reject. Bind methods to button taps and watch the call state so that the button display stays in sync with the current call stage.
99
+ - **Icon resources:** Button icons are available on GitHub. They are designed by our team for TUICallKit, are copyright-free, and can be used directly.
100
+
101
+ | **Resource** | **Description** | **Download** |
102
+ | --- | --- | --- |
103
+ | `accept.png` | Answer button icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/accept.png) |
104
+ | `hangup.png` | Hang up / reject button icon. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/hangup.png) |
105
+ | `camera-off.png` | Camera off. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/camera-off.png) |
106
+ | `camera-on.png` | Camera on. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/camera-on.png) |
107
+ | `mic-off.png` | Microphone off. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/mic-off.png) |
108
+ | `mic-on.png` | Microphone on. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/mic-on.png) |
109
+ | `handsfree-off.png` | Speaker off. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/handsfree-off.png) |
110
+ | `handsfree-on.png` | Speaker on. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/uni_modules/tuikit-atomic-x/static/icon/handsfree-on.png) |
111
+
112
+ The following example shows how to add hang up, microphone, and camera buttons.
113
+
114
+ 1. **Add the hang up button:** Create a hang up button and call `hangup` in its tap handler to end the call and dismiss the page.
115
+
116
+ ```vue
117
+ <template>
118
+ <view class="btn" @tap="handleHangup">
119
+ <image class="btn-img" :style="[style]" :src="HANGUP_SRC"></image>
120
+ <text class="btn-text" v-if="isShowText">Hang up</text>
121
+ </view>
122
+ </template>
123
+
124
+ <script setup lang="ts">
125
+ import { computed } from "vue";
126
+ import HANGUP_SRC from "../../../static/icon/hangup.png"; // Replace with the icon path used in your project
127
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
128
+
129
+ const { hangup } = useCallState();
130
+
131
+ const props = defineProps({
132
+ size: {
133
+ type: Number,
134
+ default: 60,
135
+ },
136
+ isShowText: {
137
+ type: Boolean,
138
+ default: true,
139
+ },
140
+ });
141
+
142
+ const style = computed(() => ({
143
+ width: props.size + "px",
144
+ height: props.size + "px",
145
+ }));
146
+
147
+ const handleHangup = () => {
148
+ hangup();
149
+ // In the tap handler, call hangup and dismiss the page
150
+ };
151
+ </script>
152
+
153
+ <style scoped>
154
+ .btn {
155
+ margin: 10px 20px;
156
+ }
157
+ .btn-img {
158
+ width: 60px;
159
+ height: 60px;
160
+ border-radius: 140px;
161
+ }
162
+ .btn-text {
163
+ font-size: 12px;
164
+ color: #d5e0f2;
165
+ font-weight: 400;
166
+ text-align: center;
167
+ margin-top: 10px;
168
+ }
169
+ </style>
170
+ ```
171
+
172
+ 2. **Add the microphone toggle button:** Call `openLocalMicrophone` or `closeLocalMicrophone` in the tap handler, and watch the microphone state to update the button text in real time.
173
+
174
+ ```vue
175
+ <template>
176
+ <view class="btn" @tap="handleMic">
177
+ <image class="btn-img" :src="microphoneStatus === DeviceStatus.ON ? MIC_ON_SRC : MIC_OFF_SRC"></image>
178
+ <text class="btn-text">
179
+ {{ microphoneStatus === DeviceStatus.ON ? 'Mic on' : 'Mic off' }}
180
+ </text>
181
+ </view>
182
+ </template>
183
+
184
+ <script setup lang="ts">
185
+ import MIC_ON_SRC from "../../../static/icon/mic-on.png"; // Replace with the icon path used in your project
186
+ import MIC_OFF_SRC from "../../../static/icon/mic-off.png"; // Replace with the icon path used in your project
187
+ import {
188
+ useDeviceState,
189
+ DeviceStatus
190
+ } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
191
+
192
+ const {
193
+ microphoneStatus,
194
+ openLocalMicrophone,
195
+ closeLocalMicrophone,
196
+ } = useDeviceState();
197
+
198
+ // Toggle the microphone in the tap handler
199
+ const handleMic = () => {
200
+ if (microphoneStatus.value === DeviceStatus.ON) {
201
+ closeLocalMicrophone();
202
+ } else {
203
+ openLocalMicrophone();
204
+ }
205
+ };
206
+ </script>
207
+
208
+ <style scoped>
209
+ .btn {
210
+ margin: 10px 20px;
211
+ }
212
+ .btn-img {
213
+ width: 60px;
214
+ height: 60px;
215
+ border-radius: 140px;
216
+ }
217
+ .btn-text {
218
+ font-size: 12px;
219
+ color: #d5e0f2;
220
+ font-weight: 400;
221
+ text-align: center;
222
+ margin-top: 10px;
223
+ }
224
+ </style>
225
+ ```
226
+
227
+ 3. **Add the camera toggle button:** Call `openLocalCamera` or `closeLocalCamera` in the tap handler, and watch the camera state to update the button text in real time.
228
+
229
+ ```vue
230
+ <template>
231
+ <div class="btn" @tap="handleCamera">
232
+ <image class="btn-img" :src="cameraStatus === DeviceStatus.ON ? CAMERA_ON_SRC : CAMERA_OFF_SRC"></image>
233
+ <text class="btn-text">
234
+ {{ cameraStatus === DeviceStatus.ON ? 'Camera on' : 'Camera off' }}
235
+ </text>
236
+ </div>
237
+ </template>
238
+
239
+ <script setup lang="ts">
240
+ import CAMERA_ON_SRC from "../../../static/icon/camera-on.png"; // Replace with the icon path used in your project
241
+ import CAMERA_OFF_SRC from "../../../static/icon/camera-off.png"; // Replace with the icon path used in your project
242
+ import {
243
+ useDeviceState,
244
+ DeviceStatus
245
+ } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
246
+
247
+ const {
248
+ cameraStatus,
249
+ openLocalCamera,
250
+ closeLocalCamera,
251
+ isFrontCamera,
252
+ } = useDeviceState();
253
+
254
+ // Camera button tap handler
255
+ const handleCamera = () => {
256
+ if (cameraStatus.value === DeviceStatus.ON) {
257
+ closeLocalCamera();
258
+ } else {
259
+ openLocalCamera({ isFront: isFrontCamera.value });
260
+ }
261
+ };
262
+ </script>
263
+
264
+ <style>
265
+ .btn {
266
+ margin: 10px 20px;
267
+ }
268
+ .btn-img {
269
+ width: 60px;
270
+ height: 60px;
271
+ border-radius: 140px;
272
+ }
273
+ .btn-text {
274
+ font-size: 12px;
275
+ color: #d5e0f2;
276
+ font-weight: 400;
277
+ text-align: center;
278
+ margin-top: 10px;
279
+ }
280
+ </style>
281
+ ```
282
+
283
+ ### Step 3: Initiate a Call
284
+
285
+ After `calls` returns successfully, navigate to the call page. For a smoother experience, we recommend opening the microphone (and the camera for video calls) automatically based on the media type. Implementation:
286
+
287
+ 1. **Initiate the call:** Call `calls`.
288
+ 2. **Open media devices:** After the call starts, open the microphone; if it is a video call, open the camera as well.
289
+ 3. **Launch the call page:** When the call starts successfully, navigate to the call page.
290
+
291
+ ```vue
292
+ <script setup lang="ts">
293
+ import { ref } from 'vue';
294
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
295
+ import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
296
+
297
+ const { calls } = useCallState();
298
+ const { openLocalCamera, openLocalMicrophone } = useDeviceState();
299
+ const calleeUserID = ref('');
300
+ const mediaType = ref(1); // 0: audio, 1: video
301
+
302
+ const startCall = () => {
303
+ const userID = calleeUserID.value.trim();
304
+ if (!userID) {
305
+ uni.showToast({ icon: 'none', title: 'Please enter the target user ID' });
306
+ return;
307
+ }
308
+
309
+ // 1. Open local devices
310
+ if (mediaType.value === 1) {
311
+ openLocalCamera({ isFront: true });
312
+ }
313
+ openLocalMicrophone();
314
+
315
+ // 2. Initiate the call
316
+ calls({
317
+ participantIds: [userID],
318
+ mediaType: mediaType.value,
319
+ success: () => {
320
+ console.log('Call started successfully');
321
+ uni.navigateTo({
322
+ url: '/uni_modules/tuikit-atomic-x/pages/call?layoutTemplate=Float' // Example path; replace with the actual call page path created in Step 1
323
+ });
324
+ },
325
+ fail: (code: number, message: string) => {
326
+ console.error('Call start failed:', code, message);
327
+ uni.showToast({ icon: 'none', title: `Call failed: ${message}` });
328
+ }
329
+ });
330
+ };
331
+ </script>
332
+ ```
333
+
334
+ ### Step 4: End the Call
335
+
336
+ Whether you call `hangup` or the remote side ends the call, the `onCallEnded` event is fired. Subscribe to this event and dismiss the call page when it fires. Implementation:
337
+
338
+ 1. **Listen for the call-end event:** Subscribe to `onCallEnded`.
339
+ 2. **Dismiss the call page:** When `onCallEnded` fires, close the call UI.
340
+
341
+ ```typescript
342
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
343
+ import { useDeviceState } from '@/uni_modules/tuikit-atomic-x/state/DeviceState';
344
+
345
+ const { addCallListener } = useCallState();
346
+ const { closeLocalCamera, closeLocalMicrophone } = useDeviceState();
347
+
348
+ /**
349
+ * Initialize the call service (minimal example: handle only the call-end event and navigate back).
350
+ * It is recommended to invoke this on app startup.
351
+ */
352
+ export function initCallService() {
353
+ addCallListener('onCallEnded', (event: string) => {
354
+ let res: any;
355
+ try {
356
+ res = JSON.parse(event);
357
+ } catch (error) {
358
+ console.error('[CallService] onCallEnded parse error:', error);
359
+ return;
360
+ }
361
+ console.log('[CallService] onCallEnded, reason:', res.reason);
362
+
363
+ // Close local devices
364
+ closeLocalCamera();
365
+ closeLocalMicrophone();
366
+
367
+ // Navigate back
368
+ const pages = getCurrentPages();
369
+ if (pages.length > 1) {
370
+ uni.navigateBack({ delta: 1 });
371
+ } else {
372
+ uni.redirectTo({ url: '/pages/index/index' }); // Example path; replace with the actual path in your project
373
+ }
374
+ });
375
+ }
376
+ ```
377
+
378
+ **onCallEnded event parameters:**
379
+
380
+ | **Parameter** | **Type** | **Description** |
381
+ | --- | --- | --- |
382
+ | `callId` | String | Unique identifier for this call. |
383
+ | `mediaType` | CallMediaType | Media type of the call. `CallMediaType.Video`: video call. `CallMediaType.Audio`: audio call. |
384
+ | `reason` | CallEndReason | Reason for the call ending. `Unknown`: unknown reason. `Hangup`: hung up by the user. `Reject`: rejected by the callee. `Busy`: callee is busy. `Cancel`: caller canceled before the callee answered. |
385
+ | `userId` | String | User ID that triggered the end event. |
386
+
387
+ ### Demo Result
388
+
389
+ After completing the steps above, the "make a call" feature is now ready to use.
390
+
391
+ ## Customize the UI
392
+
393
+ `CallCoreView` provides extensive UI customization. You can freely replace avatars and indicator icons. For fast integration, download icons directly from **GitHub**. They are designed by our team for TUICallKit and are copyright-free.
394
+
395
+ ### Customize Volume Level Icons
396
+
397
+ Set the `volumeLevelIcons` property of `CallCoreView` to assign icons for each volume level.
398
+
399
+ **Sample code for `volumeLevelIcons`:**
400
+
401
+ ```vue
402
+ <template>
403
+ <view class="call-view-container">
404
+ <view :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }">
405
+ <call-core-view
406
+ :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }"
407
+ :volumeLevelIcons="volumeLevelIcons"
408
+ ></call-core-view>
409
+ </view>
410
+ </view>
411
+ </template>
412
+
413
+ <script setup>
414
+ import { ref, onMounted } from 'vue';
415
+
416
+ const systemInfo = ref({});
417
+
418
+ /**
419
+ * Convert a `/static/` relative path into a local absolute path usable by the native layer.
420
+ */
421
+ function toAbsolutePath(relativePath) {
422
+ if (plus && plus.io && plus.io.convertLocalFileSystemURL) {
423
+ return plus.io.convertLocalFileSystemURL(relativePath);
424
+ }
425
+ return relativePath;
426
+ }
427
+
428
+ /**
429
+ * Batch-convert every value in the object into an absolute path.
430
+ */
431
+ function convertIconPaths(iconMap) {
432
+ const result = {};
433
+ Object.keys(iconMap).forEach(key => {
434
+ result[key] = toAbsolutePath(iconMap[key]);
435
+ });
436
+ return result;
437
+ }
438
+
439
+ // Volume-level icons: key is the level name, value is the local absolute path.
440
+ const volumeLevelIcons = ref(convertIconPaths({
441
+ "Mute": "/static/images/callview-self-mute.png", // Replace with the actual icon path in your project
442
+ "Low": "/static/images/callview-network.png" // Replace with the actual icon path in your project
443
+ }));
444
+
445
+ onMounted(() => {
446
+ uni.getSystemInfo({
447
+ success: (res) => {
448
+ systemInfo.value = res;
449
+ }
450
+ });
451
+ });
452
+ </script>
453
+
454
+ <style>
455
+ .call-view-container {
456
+ background: rgba(15, 16, 20, 0.5);
457
+ overflow: hidden;
458
+ }
459
+ </style>
460
+ ```
461
+
462
+ > **Note:**
463
+ >
464
+ > Parameters that uni-app passes to the native layer only support local paths, so the relative path must be converted to an absolute local path.
465
+
466
+ **Property details:**
467
+
468
+ | **Parameter** | **Type** | **Required** | **Description** |
469
+ | --- | --- | --- | --- |
470
+ | `icons` | \[String: String\] | Yes | Mapping between volume levels and icon resources. Keys (`VolumeLevel`): `Mute` (microphone muted), `Low` (0-25], `Medium` (25-50], `High` (50-75], `Peak` (75-100]. Values (`String`): icon resource paths for each level. |
471
+
472
+ **Volume-level icons:**
473
+
474
+ | **Description** | **Download** |
475
+ | --- | --- |
476
+ | Volume indicator icon. Recommended for `Low` or `Medium` — displayed when the user's volume exceeds the configured level. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/static/images/callview-network.png) |
477
+ | Mute icon. Recommended for `Mute` — displayed when the user is muted. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/static/images/callview-self-mute.png) |
478
+
479
+ ### Customize Network Quality Icons
480
+
481
+ Set the `networkQualityIcons` property of `CallCoreView` to assign icons for each network-quality state.
482
+
483
+ **Sample code for `networkQualityIcons`:**
484
+
485
+ ```vue
486
+ <template>
487
+ <view class="call-view-container">
488
+ <view :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }">
489
+ <call-core-view
490
+ :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }"
491
+ :networkQualityIcons="networkQualityIcons"
492
+ ></call-core-view>
493
+ </view>
494
+ </view>
495
+ </template>
496
+
497
+ <script setup>
498
+ import { ref, onMounted } from 'vue';
499
+
500
+ const systemInfo = ref({});
501
+
502
+ /**
503
+ * Convert a `/static/` relative path into a local absolute path usable by the native layer.
504
+ */
505
+ function toAbsolutePath(relativePath) {
506
+ if (plus && plus.io && plus.io.convertLocalFileSystemURL) {
507
+ return plus.io.convertLocalFileSystemURL(relativePath);
508
+ }
509
+ return relativePath;
510
+ }
511
+
512
+ /**
513
+ * Batch-convert every value in the object into an absolute path.
514
+ */
515
+ function convertIconPaths(iconMap) {
516
+ const result = {};
517
+ Object.keys(iconMap).forEach(key => {
518
+ result[key] = toAbsolutePath(iconMap[key]);
519
+ });
520
+ return result;
521
+ }
522
+
523
+ const networkQualityIcons = ref(convertIconPaths({
524
+ "BAD": "/static/images/callview-network-bad.png", // Replace with the actual icon path in your project
525
+ "VERY_BAD": "/static/images/callview-network-bad.png" // Replace with the actual icon path in your project
526
+ }));
527
+
528
+ onMounted(() => {
529
+ uni.getSystemInfo({
530
+ success: (res) => {
531
+ systemInfo.value = res;
532
+ }
533
+ });
534
+ });
535
+ </script>
536
+
537
+ <style>
538
+ .call-view-container {
539
+ background: rgba(15, 16, 20, 0.5);
540
+ overflow: hidden;
541
+ }
542
+ </style>
543
+ ```
544
+
545
+ **`networkQualityIcons` parameter details:**
546
+
547
+ | **Parameter** | **Type** | **Required** | **Description** |
548
+ | --- | --- | --- | --- |
549
+ | `icons` | \[String: String\] | Yes | Mapping between network-quality levels and icon resources. Keys (`String`): `UNKNOWN`, `EXCELLENT`, `GOOD`, `POOR`, `BAD`, `VERY_BAD`, `DOWN`. Values (`String`): icon resource paths for each state. |
550
+
551
+ **Poor-network indicator icon:**
552
+
553
+ | **Description** | **Download** |
554
+ | --- | --- |
555
+ | Poor-network indicator icon. Recommended for `BAD`, `VERY_BAD`, or `DOWN` — displayed when the network condition is poor. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/static/images/callview-network-bad.png) |
556
+
557
+ ### Customize Default Avatar
558
+
559
+ Set the `participantAvatars` property of `CallCoreView` to assign user avatars. We recommend watching the reactive `allParticipants` data (all participants in the call): set and display each user's avatar when it is available; show a default avatar (placeholder) when the user has no avatar or loading fails.
560
+
561
+ **Sample code for `participantAvatars`:**
562
+
563
+ ```vue
564
+ <template>
565
+ <view class="call-view-container">
566
+ <view :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }">
567
+ <call-core-view
568
+ :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }"
569
+ :participantAvatars="participantAvatars"
570
+ ></call-core-view>
571
+ </view>
572
+ </view>
573
+ </template>
574
+
575
+ <script setup>
576
+ import { ref, watch, onMounted } from 'vue';
577
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
578
+
579
+ const { allParticipants } = useCallState();
580
+ const systemInfo = ref({});
581
+ const participantAvatars = ref({});
582
+ const avatarCache = {};
583
+ const DEFAULT_AVATAR_URL = "https://liteav.sdk.qcloud.com/app/res/picture/voiceroom/avatar/user_avatar1.png";
584
+
585
+ let defaultAvatarLocalPath = "";
586
+ let defaultAvatarReady = false;
587
+
588
+ /**
589
+ * Convert a relative path into a local absolute path usable by the native layer.
590
+ */
591
+ function toAbsolutePath(relativePath) {
592
+ if (plus && plus.io && plus.io.convertLocalFileSystemURL) {
593
+ return plus.io.convertLocalFileSystemURL(relativePath);
594
+ }
595
+ return relativePath;
596
+ }
597
+
598
+ /**
599
+ * Download and cache an avatar; return the local absolute path.
600
+ */
601
+ function downloadAvatar(url) {
602
+ if (avatarCache[url]) {
603
+ return Promise.resolve(avatarCache[url]);
604
+ }
605
+ return new Promise((resolve) => {
606
+ uni.downloadFile({
607
+ url,
608
+ success: (res) => {
609
+ if (res.statusCode === 200) {
610
+ const absolutePath = toAbsolutePath(res.tempFilePath);
611
+ avatarCache[url] = absolutePath;
612
+ resolve(absolutePath);
613
+ } else {
614
+ resolve(defaultAvatarLocalPath || "");
615
+ }
616
+ },
617
+ fail: () => {
618
+ resolve(defaultAvatarLocalPath || "");
619
+ }
620
+ });
621
+ });
622
+ }
623
+
624
+ // Pre-download the default avatar
625
+ const defaultAvatarPromise = downloadAvatar(DEFAULT_AVATAR_URL).then(localPath => {
626
+ defaultAvatarLocalPath = localPath;
627
+ defaultAvatarReady = true;
628
+
629
+ // Once the default avatar is ready, refresh participant avatars once
630
+ if (allParticipants.value && allParticipants.value.length > 0) {
631
+ updateParticipantAvatars(allParticipants.value);
632
+ }
633
+ });
634
+
635
+ /**
636
+ * Download avatars for the given participant list and build a { userId: localPath } map.
637
+ */
638
+ async function updateParticipantAvatars(participants) {
639
+ if (!participants || participants.length === 0) {
640
+ participantAvatars.value = {};
641
+ return;
642
+ }
643
+
644
+ if (!defaultAvatarReady && defaultAvatarPromise) {
645
+ await defaultAvatarPromise;
646
+ }
647
+
648
+ const avatarMap = {};
649
+ const tasks = [];
650
+ participants.forEach(participant => {
651
+ if (participant.id && participant.avatarURL) {
652
+ tasks.push(
653
+ downloadAvatar(participant.avatarURL).then(localPath => {
654
+ avatarMap[participant.id] = localPath;
655
+ })
656
+ );
657
+ } else if (participant.id) {
658
+ avatarMap[participant.id] = defaultAvatarLocalPath || "";
659
+ }
660
+ });
661
+ await Promise.all(tasks);
662
+ participantAvatars.value = avatarMap;
663
+ }
664
+
665
+ // Watch participants and update the avatar map
666
+ watch(() => allParticipants.value, async (newVal) => {
667
+ if (!newVal) return;
668
+ await updateParticipantAvatars(newVal);
669
+ }, { immediate: true, deep: true });
670
+
671
+ onMounted(() => {
672
+ uni.getSystemInfo({
673
+ success: (res) => {
674
+ systemInfo.value = res;
675
+ }
676
+ });
677
+ });
678
+ </script>
679
+
680
+ <style>
681
+ .call-view-container {
682
+ background: rgba(15, 16, 20, 0.5);
683
+ overflow: hidden;
684
+ }
685
+ </style>
686
+ ```
687
+
688
+ **`participantAvatars` parameter details:**
689
+
690
+ | **Parameter** | **Type** | **Required** | **Description** |
691
+ | --- | --- | --- | --- |
692
+ | `avatars` | \[String: String\] | Yes | User-avatar map. Key: the user's `userID`. Value: the absolute local path of that user's avatar resource. |
693
+
694
+ **Default avatar resource:**
695
+
696
+ | **Description** | **Download** |
697
+ | --- | --- |
698
+ | Default avatar. Recommended as a placeholder when the user's avatar fails to load or is not set. | [GitHub](https://github.com/Tencent-RTC/TUIKit_iOS/tree/main/atomic_x/Resources/AtomicX.xcassets/icon/call/default_participant_icon.imageset) |
699
+
700
+ ### Customize Loading Animation
701
+
702
+ Set the `waitingAnimation` property of `CallCoreView` to display a waiting animation for users in the waiting state for a better experience.
703
+
704
+ **Sample code for `waitingAnimation`:**
705
+
706
+ ```vue
707
+ <template>
708
+ <view class="call-view-container">
709
+ <view :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }">
710
+ <call-core-view
711
+ :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }"
712
+ :waitingAnimation="waitingAnimation"
713
+ ></call-core-view>
714
+ </view>
715
+ </view>
716
+ </template>
717
+
718
+ <script setup>
719
+ import { ref, onMounted } from 'vue';
720
+
721
+ const systemInfo = ref({});
722
+
723
+ /**
724
+ * Convert a `/static/` relative path into a local absolute path usable by the native layer.
725
+ */
726
+ function toAbsolutePath(relativePath) {
727
+ if (plus && plus.io && plus.io.convertLocalFileSystemURL) {
728
+ return plus.io.convertLocalFileSystemURL(relativePath);
729
+ }
730
+ return relativePath;
731
+ }
732
+
733
+ // Waiting animation GIF: pass a local absolute path to the native component for rendering
734
+ const waitingAnimation = ref(toAbsolutePath("/static/images/callview-loading.gif")); // Replace with the actual icon path in your project
735
+
736
+ onMounted(() => {
737
+ uni.getSystemInfo({
738
+ success: (res) => {
739
+ systemInfo.value = res;
740
+ }
741
+ });
742
+ });
743
+ </script>
744
+
745
+ <style>
746
+ .call-view-container {
747
+ background: rgba(15, 16, 20, 0.5);
748
+ overflow: hidden;
749
+ }
750
+ </style>
751
+ ```
752
+
753
+ **`waitingAnimation` parameter details:**
754
+
755
+ | **Parameter** | **Type** | **Required** | **Description** |
756
+ | --- | --- | --- | --- |
757
+ | `path` | String | Yes | Absolute path of the GIF image resource. |
758
+
759
+ **Waiting animation:**
760
+
761
+ | **Description** | **Download** |
762
+ | --- | --- |
763
+ | User waiting animation. Recommended for group calls — displayed when the user's status is `waiting`. | [GitHub](https://github.com/Tencent-RTC/TUIKit_uni-app/blob/main/App/static/images/callview-loading.gif) |
764
+
765
+ ### Add Call Duration Indicator
766
+
767
+ The call duration is exposed in real time through the `duration` field of the reactive `activeCall` data. To display it:
768
+
769
+ 1. **Bind the call duration to the UI:** Bind the `activeCall.duration` field to the UI. The field is reactive and drives UI refresh automatically — no manual timer is required.
770
+
771
+ ```vue
772
+ <template>
773
+ <view v-if="isConnected" class="call-timer">
774
+ <text class="timer-text">{{ formattedTime }}</text>
775
+ </view>
776
+ </template>
777
+
778
+ <script setup lang="ts">
779
+ import { computed } from 'vue';
780
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
781
+
782
+ const { selfInfo, activeCall } = useCallState();
783
+
784
+ const isConnected = computed(() => {
785
+ return selfInfo.value?.status === 2;
786
+ });
787
+
788
+ const formattedTime = computed(() => {
789
+ const duration = activeCall.value?.duration ?? 0;
790
+ const hours = Math.floor(duration / 3600);
791
+ const minutes = Math.floor((duration % 3600) / 60);
792
+ const seconds = duration % 60;
793
+ const pad = (n: number) => n.toString().padStart(2, '0');
794
+ if (hours > 0) {
795
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
796
+ }
797
+ return `${pad(minutes)}:${pad(seconds)}`;
798
+ });
799
+ </script>
800
+
801
+ <style scoped>
802
+ .call-timer {
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ }
807
+ .timer-text {
808
+ font-size: 28rpx;
809
+ color: #FFFFFF;
810
+ font-weight: 400;
811
+ }
812
+ </style>
813
+ ```
814
+
815
+ ## More Features
816
+
817
+ ### Customize User Avatar / Nickname
818
+
819
+ Before the call starts, you can use `setSelfInfo` to set your own nickname and avatar.
820
+
821
+ **Sample code for `setSelfInfo`:**
822
+
823
+ ```typescript
824
+ import { useLoginState } from "@/uni_modules/tuikit-atomic-x/state/LoginState";
825
+
826
+ const { setSelfInfo } = useLoginState();
827
+
828
+ /**
829
+ * Set the personal info (nickname, avatar, etc.) of the currently logged-in user.
830
+ */
831
+ export const setSelfUserInfo = (userID: string, nickname: string, avatarURL: string): void => {
832
+ setSelfInfo({
833
+ userProfile: {
834
+ userID,
835
+ nickname,
836
+ avatarURL,
837
+ },
838
+ success: () => {
839
+ console.log('User info set successfully');
840
+ },
841
+ fail: (errCode: number, errMsg: string) => {
842
+ console.error('Failed to set user info:', errCode, errMsg);
843
+ },
844
+ });
845
+ };
846
+ ```
847
+
848
+ **`setSelfInfo` parameter details:**
849
+
850
+ | **Parameter** | **Type** | **Required** | **Description** |
851
+ | --- | --- | --- | --- |
852
+ | `userProfile` | UserProfile | Yes | User info struct: `userID` (the user's ID), `avatarURL` (avatar URL), `nickname` (nickname). See `UserProfile` for more fields. |
853
+
854
+ ### Switch Layout Modes
855
+
856
+ `CallCoreView` ships with three built-in layout modes. Set the `layoutTemplate` property to choose one. If not specified, `CallCoreView` adapts automatically based on the number of participants: 1-on-1 defaults to `Float` mode; multi-party calls switch to `Grid` mode. The layout modes are:
857
+
858
+ | **Mode** | **Layout** | **Interaction** |
859
+ | --- | --- | --- |
860
+ | **Float** | Full-screen self-view while waiting; after connecting, full-screen remote view with a floating self-view. | The floating window can be dragged; tap it to swap with the main view. |
861
+ | **Grid** | All participants are tiled in a grid layout — suitable for 2+ users. Tap to enlarge a single view. | Tap a participant's view to enlarge it. |
862
+ | **PIP** | 1v1 always shows the remote view; multi-party uses the active-speaker strategy and full-screen the current speaker. | While waiting, the self-view is shown; after connecting, the call duration is displayed. |
863
+
864
+ **Sample code for `layoutTemplate`:**
865
+
866
+ ```vue
867
+ <template>
868
+ <view class="call-view-container">
869
+ <view :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }">
870
+ <call-core-view
871
+ :style="{ height: systemInfo?.windowHeight + 'px', width: systemInfo?.safeArea?.width + 'px' }"
872
+ :layoutTemplate="layoutTemplate"
873
+ ></call-core-view>
874
+ </view>
875
+ </view>
876
+ </template>
877
+
878
+ <script setup>
879
+ import { ref, watch, onMounted } from 'vue';
880
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
881
+
882
+ const { activeCall } = useCallState();
883
+ const systemInfo = ref({});
884
+ // Layout template: 'Float' for single floating-window layout, 'Grid' for multi-party tile layout
885
+ const layoutTemplate = ref("Grid");
886
+
887
+ // Switch layout automatically based on call info: Grid for multi-party / group, Float for 1-on-1
888
+ watch(() => activeCall.value, (newValue) => {
889
+ if (!newValue || !newValue.inviteeIds) {
890
+ return;
891
+ }
892
+ const newTemplate = (newValue.inviteeIds.length > 1 || newValue.chatGroupId !== '') ? 'Grid' : 'Float';
893
+ if (layoutTemplate.value !== newTemplate) {
894
+ layoutTemplate.value = newTemplate;
895
+ }
896
+ }, { immediate: true, deep: true });
897
+
898
+ onMounted(() => {
899
+ uni.getSystemInfo({
900
+ success: (res) => {
901
+ systemInfo.value = res;
902
+ }
903
+ });
904
+ });
905
+ </script>
906
+
907
+ <style>
908
+ .call-view-container {
909
+ background: rgba(15, 16, 20, 0.5);
910
+ overflow: hidden;
911
+ }
912
+ </style>
913
+ ```
914
+
915
+ **`layoutTemplate` parameter details:**
916
+
917
+ | **Parameter** | **Type** | **Description** |
918
+ | --- | --- | --- |
919
+ | `layoutTemplate` | String | Layout mode of `CallCoreView`. **Float**: full-screen self-view while waiting; after connecting, full-screen remote view with a floating self-view; floating window supports drag and tap-to-swap. **Grid**: all participants tiled in a grid; suitable for 2+ users; tap to enlarge a single view. **PIP**: 1v1 always shows the remote view; multi-party uses the active-speaker strategy and full-screen the current speaker; while waiting, the self-view is shown; after connecting, the call duration is displayed. |
920
+
921
+ ### Set the Default Call Timeout
922
+
923
+ When initiating a call, specify the wait timeout via the `timeout` field in `CallParams`:
924
+
925
+ ```vue
926
+ <script setup>
927
+ import { ref } from 'vue';
928
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
929
+
930
+ const { calls } = useCallState();
931
+ const calleeUserID = ref('');
932
+ const mediaType = ref(1); // 0: audio, 1: video
933
+
934
+ const startCall = () => {
935
+ const userID = calleeUserID.value.trim();
936
+ if (!userID) {
937
+ uni.showToast({ icon: 'none', title: 'Please enter the target user ID' });
938
+ return;
939
+ }
940
+
941
+ calls({
942
+ participantIds: [userID],
943
+ mediaType: mediaType.value,
944
+ params: {
945
+ timeout: 30
946
+ },
947
+ success: () => {
948
+ console.log('Call started successfully');
949
+ uni.navigateTo({
950
+ url: '/uni_modules/tuikit-atomic-x/pages/call?layoutTemplate=Float' // Example path; replace with the actual call page path created in Step 1
951
+ });
952
+ },
953
+ fail: (code, message) => {
954
+ console.error('Call start failed:', code, message);
955
+ uni.showToast({ icon: 'none', title: `Call failed: ${message}` });
956
+ }
957
+ });
958
+ };
959
+ </script>
960
+ ```
961
+
962
+ **`calls` parameter details:**
963
+
964
+ | **Parameter** | **Type** | **Required** | **Description** |
965
+ | --- | --- | --- | --- |
966
+ | `participantIds` | Array | Yes | List of target user IDs. |
967
+ | `mediaType` | CallMediaType | Yes | Media type. `CallMediaType.Video`: video call. `CallMediaType.Audio`: audio call. |
968
+ | `params` | CallParams | No | Call extension parameters such as room ID, invitation timeout, etc. `roomId` (String): room ID; auto-assigned by the server if omitted. `timeout` (Int): call timeout in seconds. `userData` (String): custom user data. `chatGroupId` (String): Chat group ID for group calls. `isEphemeralCall` (Boolean): whether the call is ephemeral / encrypted (no call record). |
969
+
970
+ ### Play Waiting Ringtone
971
+
972
+ Watch your own call state and play a ringtone while waiting for the callee to answer; stop the ringtone when the call is accepted or ends.
973
+
974
+ ```typescript
975
+ import { watch } from 'vue';
976
+ import { useCallState } from '@/uni_modules/tuikit-atomic-x/state/CallState';
977
+
978
+ const { selfInfo } = useCallState();
979
+
980
+ function ensureAudioContext() {
981
+ if (!uni.$innerAudioContext) {
982
+ const ctx = uni.createInnerAudioContext();
983
+ ctx.onError((err: any) => {
984
+ console.error('[CallService] innerAudioContext onError:', err?.errCode, err?.errMsg || err);
985
+ });
986
+ uni.$innerAudioContext = ctx;
987
+ }
988
+ return uni.$innerAudioContext;
989
+ }
990
+
991
+ function playRingtone(src: string) {
992
+ const ctx = ensureAudioContext();
993
+ try { ctx.stop(); } catch (_e) {}
994
+ ctx.src = src;
995
+ ctx.loop = true;
996
+ ctx.autoplay = true;
997
+ setTimeout(() => {
998
+ try { ctx.play(); } catch (e) {}
999
+ }, 50);
1000
+ }
1001
+
1002
+ function stopRingtone() {
1003
+ if (uni.$innerAudioContext) {
1004
+ try {
1005
+ uni.$innerAudioContext.loop = false;
1006
+ uni.$innerAudioContext.stop();
1007
+ } catch (e) {}
1008
+ }
1009
+ }
1010
+
1011
+ export { playRingtone, stopRingtone };
1012
+
1013
+ // Recommended to invoke on app startup
1014
+ export function initCallService() {
1015
+ ensureAudioContext();
1016
+
1017
+ // When the call is connected (status === 2) or ended (status === 0), stop the ringtone
1018
+ watch(() => selfInfo.value, (newVal) => {
1019
+ if (newVal?.status === 2 || newVal?.status === 0) {
1020
+ stopRingtone();
1021
+ }
1022
+ }, { immediate: true, deep: true });
1023
+ }
1024
+ ```
1025
+
1026
+ ## FAQ
1027
+
1028
+ ### How can I keep the screen always on during a call?
1029
+
1030
+ uni-app provides `uni.setScreenBrightness` for this purpose.
1031
+
1032
+ ### How do I disable the system swipe-back-to-previous-page behavior?
1033
+
1034
+ - **iOS:** Set the `disableSwipeBack` property on the page. See the uni-app official documentation for details.
1035
+ - **Android:** Intercept the system swipe gesture and the physical Back key via `onBackPress`. Sample code:
1036
+
1037
+ > **Note:**
1038
+ >
1039
+ > Intercepting the system swipe gesture and the physical Back key with this approach requires the page to use a custom navigation bar.
1040
+
1041
+ ```vue
1042
+ <script setup>
1043
+ import { onBackPress } from '@dcloudio/uni-app';
1044
+
1045
+ // Intercept the system swipe and physical Back key to avoid accidentally exiting during a call
1046
+ onBackPress((option) => {
1047
+ if (option.from === 'backbutton') {
1048
+ return true; // Returning true blocks the default back behavior
1049
+ }
1050
+ });
1051
+ </script>
1052
+ ```