@trtc/calls-uikit-vue 4.5.1 → 4.5.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trtc/calls-uikit-vue",
3
- "version": "4.5.1",
3
+ "version": "4.5.4",
4
4
  "main": "./tuicall-uikit-vue.umd.js",
5
5
  "module": "./tuicall-uikit-vue.es.js",
6
6
  "types": "./types/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  "dependencies": {
16
16
  "@tencentcloud/lite-chat": "^1.6.3",
17
17
  "@tencentcloud/tui-core-lite": "~1.0.1",
18
- "@trtc/call-engine-lite-js": "~4.0.1"
18
+ "@trtc/call-engine-lite-js": "~4.0.4"
19
19
  },
20
20
  "bugs": {
21
21
  "url": "https://github.com/tencentyun/TUICallKit/issues"
@@ -4,6 +4,9 @@
4
4
  :class="[bodyStyle, miniMizedDeskStyle, mobileVideoStyle, mobileAudioStyle]">
5
5
  <SingleCall v-if="!callInfoContextValue.isGroupCall" class="singCall"></SingleCall>
6
6
  <GroupCall v-else class="singCall"></GroupCall>
7
+ <!-- @if process.env.BUILD_TARGET!='MINI' -->
8
+ <AutoPlayDialog />
9
+ <!-- @endif -->
7
10
  <AISubtitle />
8
11
  </div>
9
12
  </template>
@@ -15,6 +18,7 @@ import GroupCall from './components/GroupCall/GroupCall.vue';
15
18
  import { PermitTip } from './components/common/PermitTip/PermitTip';
16
19
  import { Toast } from './components/common/Toast';
17
20
  import AISubtitle from './components/common/AIAssistant/AISubtitle.vue';
21
+ import AutoPlayDialog from './components/common/AutoPlayDialog/AutoPlayDialog.vue';
18
22
  import { UIKitModal } from './components/common/UIKitModal';
19
23
  import { TUIGlobal, TUIStore, StoreName, TUICallKitAPI, NAME, CallStatus, CallMediaType, VideoDisplayMode, VideoResolution } from '../TUICallService/index';
20
24
  import {
@@ -1,9 +1,6 @@
1
1
  <template>
2
2
  <div class="groupcall-container">
3
3
  <TopBar />
4
- <!-- @if process.env.BUILD_TARGET!='MINI' -->
5
- <AutoPlayDialog />
6
- <!-- @endif -->
7
4
  <Waiting v-if="callStatus === CallStatus.CALLING && callRole === CallRole.CALLEE && !isFloatWindow" />
8
5
  <MediaContainer />
9
6
  <Tip />
@@ -26,7 +23,6 @@
26
23
  <script lang="ts" setup>
27
24
  import { ref, provide, toRefs, onMounted, onUnmounted } from '../../../adapter-vue';
28
25
  import TopBar from '../common/TopBar/TopBar.vue';
29
- import AutoPlayDialog from '../common/AutoPlayDialog/AutoPlayDialog.vue';
30
26
  import Waiting from '../common/Waiting/Waiting.vue';
31
27
  import MediaContainer from './MediaContainer/MediaContainer.vue';
32
28
  import Tip from '../common/Tip/Tip.vue';
@@ -1,9 +1,6 @@
1
1
  <template>
2
2
  <div class="singlecall-container">
3
3
  <TopBar />
4
- <!-- @if process.env.BUILD_TARGET!='MINI' -->
5
- <AutoPlayDialog />
6
- <!-- @endif -->
7
4
  <Waiting v-if="callStatus === CallStatus.CALLING" />
8
5
  <MediaContainer />
9
6
  <Tip />
@@ -15,7 +12,6 @@
15
12
  import { toRefs } from '../../../adapter-vue';
16
13
  import { CallStatus } from '../../../TUICallService';
17
14
  import TopBar from '../common/TopBar/TopBar.vue';
18
- import AutoPlayDialog from '../common/AutoPlayDialog/AutoPlayDialog.vue';
19
15
  import Waiting from '../common/Waiting/Waiting.vue';
20
16
  import MediaContainer from './MediaContainer/MediaContainer.vue';
21
17
  import Tip from '../common/Tip/Tip.vue';
@@ -15,27 +15,33 @@ export default {
15
15
 
16
16
  <script lang="ts" setup>
17
17
  import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
18
- import { TUICallKitAPI, TUIGlobal } from '../../../../TUICallService/index';
18
+ import { TUIStore, StoreName, NAME, TUIGlobal } from '../../../../TUICallService/index';
19
+ import { TUICallKitAPI } from '../../../../TUICallService/index';
19
20
  import { useTranslate } from '../../../hooks';
20
21
  const t = useTranslate();
21
- const isAutoPlayDialogVisible = ref(false);
22
+ // The onAutoPlayFailed event is listened in CallService at initialization time and its result is
23
+ // stored in TUIStore, so here we only need to sync the visibility from the store.
24
+ const isAutoPlayDialogVisible = ref(TUIStore.getData(StoreName.CALL, NAME.IS_SHOW_AUTO_PLAY_DIALOG));
25
+
26
+ const handleAutoPlayDialogVisibleChange = (value: boolean) => {
27
+ isAutoPlayDialogVisible.value = value;
28
+ };
22
29
 
23
30
  onMounted(() => {
24
- const trtcCloudInstance = TUICallKitAPI.getTUICallEngineInstance().getTRTCCloudInstance();
25
- trtcCloudInstance.on('onAutoPlayFailed', autoPlayFailedHandler);
31
+ TUIStore.watch(StoreName.CALL, {
32
+ [NAME.IS_SHOW_AUTO_PLAY_DIALOG]: handleAutoPlayDialogVisibleChange,
33
+ });
26
34
  });
27
35
 
28
36
  onUnmounted(() => {
29
- const trtcCloudInstance = TUICallKitAPI.getTUICallEngineInstance().getTRTCCloudInstance();
30
- trtcCloudInstance.off('onAutoPlayFailed', autoPlayFailedHandler);
37
+ TUIStore.unwatch(StoreName.CALL, {
38
+ [NAME.IS_SHOW_AUTO_PLAY_DIALOG]: handleAutoPlayDialogVisibleChange,
39
+ });
31
40
  });
32
41
 
33
- const autoPlayFailedHandler = () => {
34
- isAutoPlayDialogVisible.value = true;
35
- };
36
-
37
42
  const handleResumePlaybackClick = () => {
38
- isAutoPlayDialogVisible.value = false;
43
+ TUIStore.update(StoreName.CALL, NAME.IS_SHOW_AUTO_PLAY_DIALOG, false);
44
+ TUICallKitAPI?._autoPlayResume?.();
39
45
  };
40
46
  </script>
41
47
 
@@ -48,6 +48,7 @@ export default class EngineEventHandler {
48
48
  callEngine.on(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this); // 通话存在邀请他人时, 通话里的所有人都会抛出
49
49
  callEngine.on(TUICallEvent.SDK_READY, this._handleSDKReady, this); // SDK Ready 回调
50
50
  callEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件
51
+ // @ts-ignore
51
52
  callEngine.on(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this);
52
53
  // @ts-ignore
53
54
  TUICallEvent.CALL_MESSAGE && callEngine.on(TUICallEvent.CALL_MESSAGE, this._handleCallMessage, this); // call message card display event
@@ -75,6 +76,7 @@ export default class EngineEventHandler {
75
76
  callEngine.off(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this);
76
77
  callEngine.off(TUICallEvent.SDK_READY, this._handleSDKReady, this);
77
78
  callEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut, this);
79
+ // @ts-ignore
78
80
  callEngine.off(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this);
79
81
  // @ts-ignore
80
82
  TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.off(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this);
@@ -27,7 +27,7 @@ const TUIGlobal: ITUIGlobal = TuiGlobal.getInstance();
27
27
  const TUIStore: ITUIStore = TuiStore.getInstance();
28
28
  const uiDesign = UIDesign.getInstance();
29
29
  uiDesign.setTUIStore(TUIStore);
30
- const version = '4.5.1';
30
+ const version = '4.5.4';
31
31
  import AIAssistant from './AIAssistant'; // 仅 web 支持 AI 实时字幕
32
32
  const frameWork = 'vue3';
33
33
  export { TUIGlobal, TUIStore, uiDesign };
@@ -51,6 +51,15 @@ export default class TUICallService {
51
51
  private _wasmReadyPromise;
52
52
  private _wasmReadyResolve;
53
53
  private _isInitialized = false;
54
+ // Use an arrow function to keep the `this` binding when registering/unregistering the listener.
55
+ private _handleAutoPlayFailed = (event: any) => {
56
+ this._tuiCallEngine?.reportLog?.({
57
+ name: 'TUICallkit.onAutoPlayFailed',
58
+ });
59
+ // @ts-ignore
60
+ this._autoPlayResume = event?.resume; // webrtc 自动恢复播放失败需要自己调用
61
+ TUIStore.update(StoreName.CALL, NAME.IS_SHOW_AUTO_PLAY_DIALOG, true);
62
+ };
54
63
 
55
64
  constructor() {
56
65
  console.log(`${NAME.PREFIX}version: ${version}`);
@@ -109,6 +118,7 @@ export default class TUICallService {
109
118
  frameWork,
110
119
  // @ts-ignore
111
120
  language: 6,
121
+ // @ts-ignore
112
122
  callkitVersion: version,
113
123
  isFromChat: isFromChat || false,
114
124
  component,
@@ -132,6 +142,9 @@ export default class TUICallService {
132
142
  trtcCloudInstance?.callExperimentalAPI(JSON.stringify(
133
143
  { "api": "enableAutoPlayDialog","params": { "enable": 0 } }
134
144
  ));
145
+ // Register the auto play failed listener at initialization time, otherwise the event may
146
+ // be fired before the AutoPlayDialog component is mounted and the listener would miss it.
147
+ trtcCloudInstance?.on?.('onAutoPlayFailed', this._handleAutoPlayFailed);
135
148
  this._tuiCallEngine?.reportLog?.({
136
149
  name: 'TUICallkit.init',
137
150
  data: {
@@ -153,6 +166,8 @@ export default class TUICallService {
153
166
  }
154
167
  if (this._tuiCallEngine) {
155
168
  this._removeListenTuiCallEngineEvent();
169
+ const trtcCloudInstance = this._tuiCallEngine?.getTRTCCloudInstance?.();
170
+ trtcCloudInstance?.off?.('onAutoPlayFailed', this._handleAutoPlayFailed);
156
171
  await this._tuiCallEngine.destroyInstance();
157
172
  this._tuiCallEngine = null;
158
173
  }
@@ -1,4 +1,5 @@
1
1
  import { CallMediaType, CallStatus } from '../const/index';
2
+ import { checkDevicePermission } from '../utils/permission';
2
3
 
3
4
  export function initialUI() {
4
5
  // 收起键盘
@@ -34,7 +35,9 @@ export async function beforeCall(type: CallMediaType, that: any) {
34
35
  microphone: true,
35
36
  camera: type === CallMediaType.VIDEO,
36
37
  };
37
- const hasDevicePermission = await that._tuiCallEngine.deviceCheck(deviceMap); // miniProgram 检查设备权限
38
+ // `deviceCheck` is not implemented by @trtc/call-engine-lite-wx, so use the
39
+ // mini-program permission utility to pre-check microphone/camera access.
40
+ const hasDevicePermission = await checkDevicePermission(deviceMap); // miniProgram 检查设备权限
38
41
  return hasDevicePermission ? CallStatus.CALLING : CallStatus.IDLE;
39
42
  } catch (error) {
40
43
  console.debug(error);
@@ -27,6 +27,7 @@ export default class CallStore {
27
27
  showPermissionTip: false,
28
28
  netWorkQualityList: [], // 显示网络状态差的提示
29
29
  isMuteSpeaker: false,
30
+ isShowAutoPlayDialog: false, // 是否显示自动播放失败弹窗, 默认: false
30
31
  callID: '', // callEngine 3.1 support
31
32
  groupID: '',
32
33
  roomID: 0,
@@ -1,12 +1,19 @@
1
1
  // @ts-nocheck
2
2
  import type { UIKitModalOptions, UIKitModalResult } from './type';
3
- import TUICallKitServer from '../CallService';
3
+ // Use the explicit `/index` path so the WeChat mini-program `require`, which
4
+ // only appends `.js` and does not resolve a directory to its `index.js`, can
5
+ // locate the module.
6
+ import TUICallKitServer from '../CallService/index';
7
+
8
+ // Native WeChat mini-program only exposes `wx`, while uniapp exposes `uni`.
9
+ // Fall back to `wx` when `uni` is not defined so the modal works in both.
10
+ const platform: any = typeof uni !== 'undefined' ? uni : wx;
4
11
 
5
12
  const URL_REGEX = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/i;
6
13
  const FULL_URL_REGEX = /(https?:\/\/[^\s]+)/g;
7
14
 
8
15
  function isDevEnvironment(): boolean {
9
- const accountInfo = uni.getAccountInfoSync?.();
16
+ const accountInfo = platform.getAccountInfoSync?.();
10
17
  const envVersion = accountInfo?.miniProgram?.envVersion;
11
18
  return envVersion === 'develop' || envVersion === 'trial';
12
19
  }
@@ -82,7 +89,7 @@ const createUIKitModal = (options: UIKitModalOptions): Promise<UIKitModalResult>
82
89
  const processedContent = processContent(options.content);
83
90
  const hasLink = !!url;
84
91
 
85
- uni.showModal({
92
+ platform.showModal({
86
93
  title: options.title || '',
87
94
  content: processedContent,
88
95
  cancelText: '取消',
@@ -91,10 +98,10 @@ const createUIKitModal = (options: UIKitModalOptions): Promise<UIKitModalResult>
91
98
  const action = res.confirm ? 'confirm' : 'cancel';
92
99
  if (res.confirm) {
93
100
  if (hasLink && url) {
94
- uni.setClipboardData({
101
+ platform.setClipboardData({
95
102
  data: url,
96
103
  success: () => {
97
- uni.showToast({ title: '链接已复制', icon: 'success' });
104
+ platform.showToast({ title: '链接已复制', icon: 'success' });
98
105
  },
99
106
  });
100
107
  }
@@ -26,6 +26,7 @@ export const CALL_DATA_KEY: any = {
26
26
  PLAYER: 'player',
27
27
  IS_EAR_PHONE: 'isEarPhone',
28
28
  IS_MUTE_SPEAKER: 'isMuteSpeaker',
29
+ IS_SHOW_AUTO_PLAY_DIALOG: 'isShowAutoPlayDialog',
29
30
  SHOW_PERMISSION_TIP: 'SHOW_PERMISSION_TIP',
30
31
  NETWORK_STATUS: 'NetWorkStatus',
31
32
  CALL_ID: 'callID',
@@ -34,6 +34,7 @@ export interface ICallStore {
34
34
  roomIdType: number;
35
35
  cameraPosition: CameraPosition; // 前置或后置,值为front, back
36
36
  isMuteSpeaker: boolean;
37
+ isShowAutoPlayDialog: boolean; // 是否显示自动播放失败弹窗, 默认: false
37
38
  groupCallMembers: IUserInfo[]; // chat 群会话中在通话的成员
38
39
  // TUICallKit 组件上的属性
39
40
  displayMode: VideoDisplayMode; // 设置预览远端的画面显示模式, 默认: VideoDisplayMode.COVER
@@ -0,0 +1,294 @@
1
+ /**
2
+ * WeChat mini-program permission utility.
3
+ *
4
+ * NOTE: This module is only referenced inside `// @if process.env.BUILD_TARGET='MINI'`
5
+ * conditional-compilation blocks, so it will be tree-shaken out of the web builds.
6
+ * `wx` global typings are not available in this package, therefore `// @ts-ignore`
7
+ * is used before each `wx.*` call, following the same convention as `miniProgram.ts`.
8
+ *
9
+ * Distinguishes two levels of permission denial:
10
+ * 1. Mini-program level: the user has rejected the scope in this mini-program.
11
+ * Fix: call wx.openSetting to let the user toggle it.
12
+ * 2. System level: WeChat itself lacks the OS-level permission (Settings >
13
+ * Privacy > Microphone / Camera on iOS, App Permissions on Android).
14
+ * Fix: guide the user to the system settings page (cannot be opened
15
+ * programmatically).
16
+ */
17
+
18
+ // ── Permission Scope Constants ──
19
+ export const PermissionScope = {
20
+ RECORD: 'scope.record',
21
+ CAMERA: 'scope.camera',
22
+ } as const;
23
+
24
+ // ── Result Types ──
25
+ export interface PermissionCheckResult {
26
+ /** Whether the permission is granted. */
27
+ granted: boolean;
28
+ /** Human-readable tip shown to the user. */
29
+ tip: string;
30
+ /**
31
+ * Denial level:
32
+ * - 'mini-program': rejected within this mini-program → wx.openSetting works.
33
+ * - 'system': WeChat lacks the OS permission → user must go to system settings.
34
+ * - 'granted': already authorized.
35
+ */
36
+ level: 'granted' | 'mini-program' | 'system';
37
+ }
38
+
39
+ // ── Helpers ──
40
+
41
+ /**
42
+ * Get the raw authSetting snapshot from WeChat.
43
+ */
44
+ function getAuthSetting(): Promise<any> {
45
+ return new Promise((resolve) => {
46
+ // @ts-ignore
47
+ wx.getSetting({
48
+ success(res: any) {
49
+ resolve(res.authSetting);
50
+ },
51
+ fail() {
52
+ resolve({});
53
+ },
54
+ });
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Attempt to authorize a scope via wx.authorize.
60
+ */
61
+ function authorizeScope(
62
+ scope: string,
63
+ ): Promise<{ success: boolean; errMsg: string }> {
64
+ return new Promise((resolve) => {
65
+ // @ts-ignore
66
+ wx.authorize({
67
+ scope,
68
+ success() {
69
+ resolve({ success: true, errMsg: '' });
70
+ },
71
+ fail(err: any) {
72
+ resolve({ success: false, errMsg: err.errMsg || '' });
73
+ },
74
+ });
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Check whether a scope has been explicitly denied by the user at the
80
+ * mini-program level (authSetting returns false for that key).
81
+ */
82
+ function isMiniProgramDenied(authSetting: any, scope: string): boolean {
83
+ return authSetting[scope] === false;
84
+ }
85
+
86
+ // ── Public API ──
87
+
88
+ /**
89
+ * Check and request microphone permission.
90
+ *
91
+ * - No camera permission → calls can still proceed (video call falls back to audio).
92
+ * - No microphone permission → calls MUST be blocked; user is guided to settings.
93
+ *
94
+ * The result's `level` field tells you whether the permission is already
95
+ * granted, denied at the mini-program level (wx.openSetting can fix it),
96
+ * or denied at the system level (user must go to OS settings manually).
97
+ */
98
+ export async function checkMicrophonePermission(): Promise<PermissionCheckResult> {
99
+ const authSetting = await getAuthSetting();
100
+
101
+ // Already granted
102
+ if (authSetting[PermissionScope.RECORD] === true) {
103
+ return {
104
+ granted: true,
105
+ tip: '',
106
+ level: 'granted',
107
+ };
108
+ }
109
+
110
+ // Mini-program level: user has explicitly rejected the scope before.
111
+ if (isMiniProgramDenied(authSetting, PermissionScope.RECORD)) {
112
+ return {
113
+ granted: false,
114
+ tip: '您已拒绝麦克风权限,请前往小程序设置页面开启麦克风权限后重试',
115
+ level: 'mini-program',
116
+ };
117
+ }
118
+
119
+ // Scope is undefined — never requested. Try to authorize.
120
+ const result = await authorizeScope(PermissionScope.RECORD);
121
+
122
+ if (result.success) {
123
+ return { granted: true, tip: '', level: 'granted' };
124
+ }
125
+
126
+ // Authorization failed.
127
+ // If the native permission dialog was never shown (user actively cancelled
128
+ // or system-level block), errMsg typically contains "auth deny".
129
+ // When the OS-level permission is off, wx.authorize fails immediately
130
+ // without showing the dialog — we treat this as a system-level issue.
131
+ const isSystemLevelDenial =
132
+ !result.errMsg.includes('cancel') &&
133
+ !result.errMsg.includes('deny');
134
+
135
+ // Heuristic: if the error message suggests the dialog did NOT appear,
136
+ // it is very likely a system-level block.
137
+ if (isSystemLevelDenial || result.errMsg.includes('system')) {
138
+ return {
139
+ granted: false,
140
+ tip: '您的微信没有麦克风权限,请前往手机系统设置 > 微信,开启麦克风权限后重试',
141
+ level: 'system',
142
+ };
143
+ }
144
+
145
+ // User dismissed the native dialog or denied it → mini-program level.
146
+ return {
147
+ granted: false,
148
+ tip: '您拒绝了麦克风权限,请前往小程序设置页面开启麦克风权限后重试',
149
+ level: 'mini-program',
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Check camera permission.
155
+ *
156
+ * Camera permission is REQUIRED for video calls — a denial MUST block the
157
+ * call (both for initiating and for answering). For audio calls this check
158
+ * is irrelevant and should be skipped by the caller.
159
+ *
160
+ * Tip wording here reflects the blocking policy: it tells the user that
161
+ * video calls cannot proceed without camera permission, and points them to
162
+ * the appropriate settings page (mini-program vs. system level).
163
+ */
164
+ export async function checkCameraPermission(): Promise<PermissionCheckResult> {
165
+ const authSetting = await getAuthSetting();
166
+
167
+ if (authSetting[PermissionScope.CAMERA] === true) {
168
+ return { granted: true, tip: '', level: 'granted' };
169
+ }
170
+
171
+ if (isMiniProgramDenied(authSetting, PermissionScope.CAMERA)) {
172
+ return {
173
+ granted: false,
174
+ tip: '您已拒绝摄像头权限,无法发起或接听视频通话,请前往小程序设置页面开启摄像头权限后重试',
175
+ level: 'mini-program',
176
+ };
177
+ }
178
+
179
+ const result = await authorizeScope(PermissionScope.CAMERA);
180
+
181
+ if (result.success) {
182
+ return { granted: true, tip: '', level: 'granted' };
183
+ }
184
+
185
+ const isSystemLevelDenial =
186
+ !result.errMsg.includes('cancel') &&
187
+ !result.errMsg.includes('deny');
188
+
189
+ if (isSystemLevelDenial || result.errMsg.includes('system')) {
190
+ return {
191
+ granted: false,
192
+ tip: '您的微信没有摄像头权限,无法发起或接听视频通话,请前往手机系统设置 > 微信,开启摄像头权限后重试',
193
+ level: 'system',
194
+ };
195
+ }
196
+
197
+ return {
198
+ granted: false,
199
+ tip: '您拒绝了摄像头权限,无法发起或接听视频通话,请前往小程序设置页面开启摄像头权限后重试',
200
+ level: 'mini-program',
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Handle a failed permission check by showing the appropriate dialog and
206
+ * optionally navigating to the settings page.
207
+ *
208
+ * Resolves with `{ confirm }` describing the user's choice:
209
+ * - confirm === true : the user clicked the primary button (前往设置 for the
210
+ * mini-program level, or 我知道了 for the system level).
211
+ * - confirm === false: the user cancelled the dialog (only possible for the
212
+ * mini-program level, which shows a 取消 button).
213
+ *
214
+ * Callers can use this to decide follow-up behavior, e.g. a callee rejecting
215
+ * the incoming call when the user cancels the mandatory permission dialog.
216
+ */
217
+ export function handlePermissionDenied(result: PermissionCheckResult): Promise<{ confirm: boolean }> {
218
+ return new Promise((resolve) => {
219
+ if (result.level === 'mini-program') {
220
+ // Mini-program level: open the mini-program settings page.
221
+ // @ts-ignore
222
+ wx.showModal({
223
+ title: '权限提示',
224
+ content: result.tip,
225
+ confirmText: '前往设置',
226
+ cancelText: '取消',
227
+ success(modalRes: any) {
228
+ if (modalRes.confirm) {
229
+ // @ts-ignore
230
+ wx.openSetting({
231
+ complete() {
232
+ resolve({ confirm: true });
233
+ },
234
+ });
235
+ } else {
236
+ resolve({ confirm: false });
237
+ }
238
+ },
239
+ fail() {
240
+ resolve({ confirm: false });
241
+ },
242
+ });
243
+ } else {
244
+ // System level: can only guide the user, cannot open system settings.
245
+ // @ts-ignore
246
+ wx.showModal({
247
+ title: '权限提示',
248
+ content: result.tip,
249
+ showCancel: false,
250
+ confirmText: '我知道了',
251
+ success() {
252
+ resolve({ confirm: true });
253
+ },
254
+ fail() {
255
+ resolve({ confirm: false });
256
+ },
257
+ });
258
+ }
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Combined device-permission pre-check used as the mini-program replacement
264
+ * for `tuiCallEngine.deviceCheck(deviceMap)`.
265
+ *
266
+ * - Microphone is mandatory for every call (audio and video).
267
+ * - Camera is mandatory only for video calls (`deviceMap.camera === true`).
268
+ *
269
+ * When a required permission is missing, the corresponding guidance dialog is
270
+ * shown (via `handlePermissionDenied`) and `false` is returned so the caller
271
+ * can abort / postpone the call. Returns `true` only when all required
272
+ * permissions are granted.
273
+ */
274
+ export async function checkDevicePermission(
275
+ deviceMap: { microphone?: boolean; camera?: boolean } = {},
276
+ ): Promise<boolean> {
277
+ if (deviceMap.microphone) {
278
+ const micResult = await checkMicrophonePermission();
279
+ if (!micResult.granted) {
280
+ await handlePermissionDenied(micResult);
281
+ return false;
282
+ }
283
+ }
284
+
285
+ if (deviceMap.camera) {
286
+ const cameraResult = await checkCameraPermission();
287
+ if (!cameraResult.granted) {
288
+ await handlePermissionDenied(cameraResult);
289
+ return false;
290
+ }
291
+ }
292
+
293
+ return true;
294
+ }
@@ -17,6 +17,10 @@ export function avoidRepeatedCall() {
17
17
  if (isCallingSet.has(this)) {
18
18
  console.warn((`${NAME.PREFIX}previous ${name}() is ongoing, please avoid repeated calls`));
19
19
  // throw new Error(`previous ${name}() is ongoing, please avoid repeated calls`);
20
+ // Guard the method call itself: some classes decorated with
21
+ // `avoidRepeatedCall` (e.g. CallManager) do not implement
22
+ // `getTUICallEngineInstance`. Using `?.()` short-circuits when the
23
+ // method is absent instead of throwing "is not a function".
20
24
  this?.getTUICallEngineInstance()?.reportLog?.({
21
25
  name: 'TUICallKit.avoidRepeatedCall.fail',
22
26
  data: { name },
@@ -38,7 +42,7 @@ export function avoidRepeatedCall() {
38
42
  descriptor.value.clearCallState = function(instance: any) {
39
43
  isCallingSet.delete(instance);
40
44
  };
41
-
45
+
42
46
  return descriptor;
43
47
  };
44
48
  }
package/src/index.ts CHANGED
@@ -37,7 +37,7 @@ const TUICallType = {
37
37
  AUDIO_CALL: 1,
38
38
  VIDEO_CALL: 2,
39
39
  };
40
- const Version = '4.5.1'; // basic-demo 原来上报使用
40
+ const Version = '4.5.4'; // basic-demo 原来上报使用
41
41
 
42
42
  // 输出产物
43
43
  export {