@trtc/calls-uikit-react 4.5.10 → 4.6.0
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 +2 -2
- package/src/Components/TUICallKit/TUICallKit.tsx +75 -3
- package/src/Components/components/common/ButtonPanel/ButtonPanel.tsx +3 -3
- package/src/Components/components/common/ButtonPanel/ButtonPanelH5/ButtonPanelH5.tsx +5 -5
- package/src/Components/components/common/ButtonPanel/button/Camera.tsx +49 -6
- package/src/Components/components/common/ButtonPanel/button/Microphone.tsx +48 -6
- package/src/Components/components/common/FloatingWindow/FloatingWindow.tsx +2 -2
- package/src/Components/components/common/PermissionDialog/DevicePermissionBanner.module.scss +55 -0
- package/src/Components/components/common/PermissionDialog/DevicePermissionBanner.tsx +78 -0
- package/src/Components/components/common/PermissionDialog/index.ts +3 -0
- package/src/Components/components/common/StreamInfo/StreamInfoH5/StreamInfoH5.tsx +4 -4
- package/src/Components/components/common/TopBar/h5/TopBarH5.tsx +2 -2
- package/src/Components/components/common/TopBar/pc/TopBarPC.tsx +2 -2
- package/src/Components/context/CallUserInfoContext.ts +3 -0
- package/src/TUICallService/CallService/index.ts +153 -9
- package/src/TUICallService/CallService/utils.ts +35 -23
- package/src/TUICallService/TUIStore/callStore.ts +1 -0
- package/src/TUICallService/const/index.ts +27 -0
- package/src/TUICallService/index.ts +2 -1
- package/src/TUICallService/interface/ICallStore.ts +2 -1
- package/src/TUICallService/locales/en.ts +26 -0
- package/src/TUICallService/locales/index.ts +66 -0
- package/src/TUICallService/locales/ja_JP.ts +26 -0
- package/src/TUICallService/locales/zh-cn.ts +35 -0
- package/src/index.ts +1 -1
- package/tuicall-uikit-react.es.js +3658 -3285
- package/tuicall-uikit-react.umd.js +4 -4
- package/types/Components/components/common/PermissionDialog/DevicePermissionBanner.d.ts +1 -0
- package/types/Components/components/common/PermissionDialog/index.d.ts +2 -0
- package/types/Components/context/CallUserInfoContext.d.ts +1 -0
- package/types/TUICallService/CallService/index.d.ts +30 -0
- package/types/TUICallService/const/index.d.ts +13 -0
- package/types/TUICallService/index.d.ts +2 -2
- package/types/TUICallService/interface/ICallStore.d.ts +2 -1
- package/types/TUICallService/locales/en.d.ts +25 -0
- package/types/TUICallService/locales/index.d.ts +14 -0
- package/types/TUICallService/locales/ja_JP.d.ts +25 -0
- package/types/TUICallService/locales/zh-cn.d.ts +25 -0
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
StoreName, CallStatus, CallMediaType, NAME, CALL_DATA_KEY, LanguageType, CallRole, LOG_LEVEL, VideoDisplayMode,
|
|
6
6
|
VideoResolution, StatusChange, AudioPlayBackDevice, CameraPosition, COMPONENT, FeatureButton, ButtonState,
|
|
7
|
-
LayoutMode, DEFAULT_BLUR_LEVEL,
|
|
7
|
+
LayoutMode, DEFAULT_BLUR_LEVEL, DEVICE_ERROR_CODE, DeviceErrorReason, DeviceUnavailableReasons,
|
|
8
8
|
} from '../const/index';
|
|
9
9
|
// @ts-ignore
|
|
10
10
|
import { TUICallEngine } from '@trtc/call-engine-lite-js';
|
|
11
11
|
import { checkLocalMP3FileExists } from '../utils/index';
|
|
12
12
|
import { handleModalError } from './UIKitModal';
|
|
13
|
-
import { CallTips, t } from '../locales/index';
|
|
13
|
+
import { CallTips, t, buildDeviceUnavailableText } from '../locales/index';
|
|
14
14
|
import { BellContext } from './bellContext';
|
|
15
15
|
import { avoidRepeatedCall } from '../utils/validate/index';
|
|
16
16
|
import { handleRepeatedCallError, formatTime, performanceNow, initBrowserCloseDetection } from '../utils/common-utils';
|
|
@@ -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.
|
|
30
|
+
const version = '4.6.0';
|
|
31
31
|
import AIAssistant from './AIAssistant'; // 仅 web 支持 AI 实时字幕
|
|
32
32
|
const frameWork = 'react';
|
|
33
33
|
export { TUIGlobal, TUIStore, uiDesign };
|
|
@@ -45,6 +45,11 @@ export default class TUICallService {
|
|
|
45
45
|
private _currentGroupId: string = ''; // The currentGroupId of the group chat that the user is currently in
|
|
46
46
|
private _offlinePushInfo = null;
|
|
47
47
|
private _permissionCheckTimer: any = null;
|
|
48
|
+
// Device permission error codes maintained by the service layer (0 = no error).
|
|
49
|
+
// Populated by catching the error thrown from engine's device permission check,
|
|
50
|
+
// replacing the removed engine-side audioErrorName/videoErrorName fields.
|
|
51
|
+
private _deviceErrorCodes = { audio: DEVICE_ERROR_CODE.NONE, video: DEVICE_ERROR_CODE.NONE };
|
|
52
|
+
|
|
48
53
|
private _chatCombine: any = null;
|
|
49
54
|
private _engineEventHandler: any = null;
|
|
50
55
|
// wasm ready
|
|
@@ -217,6 +222,10 @@ export default class TUICallService {
|
|
|
217
222
|
const { userIDList, type, chatGroupID, offlinePushInfo } = callsParams;
|
|
218
223
|
if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return;
|
|
219
224
|
const remoteUserInfoList = userIDList.map(userId => ({ userId }));
|
|
225
|
+
// Engine no longer probes device permission internally, so the service layer
|
|
226
|
+
// must do it before the call. This keeps updateDeviceList and the post-call
|
|
227
|
+
// permission dialog working against the real device state.
|
|
228
|
+
await this.probeDevicePermission({ audio: true, video: type === CallMediaType.VIDEO });
|
|
220
229
|
await this._updateCallStoreBeforeCall(type, remoteUserInfoList, chatGroupID);
|
|
221
230
|
this.executeExternalBeforeCalling();
|
|
222
231
|
callsParams.offlinePushInfo = { ...this.getDefaultOfflinePushInfo(), ...offlinePushInfo };
|
|
@@ -240,18 +249,25 @@ export default class TUICallService {
|
|
|
240
249
|
};
|
|
241
250
|
TUIStore.updateStore(updateStoreParams, StoreName.CALL);
|
|
242
251
|
|
|
252
|
+
// Engine no longer probes device permission internally; probe here so the
|
|
253
|
+
// following updateDeviceList and audio availability read the real state.
|
|
254
|
+
await this.probeDevicePermission({
|
|
255
|
+
audio: true,
|
|
256
|
+
video: updateStoreParams[NAME.CALL_MEDIA_TYPE] === CallMediaType.VIDEO,
|
|
257
|
+
});
|
|
243
258
|
const response = await this._tuiCallEngine.join(params);
|
|
244
259
|
TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true);
|
|
245
260
|
this.startTimer();
|
|
246
261
|
|
|
247
|
-
updateDeviceList(this._tuiCallEngine)
|
|
262
|
+
updateDeviceList(this._tuiCallEngine);
|
|
248
263
|
await this._tuiCallEngine.setVideoQuality(TUIStore.getData(StoreName.CALL, NAME.VIDEO_RESOLUTION));
|
|
249
264
|
const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
|
250
265
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
|
251
266
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
|
252
267
|
|
|
253
268
|
await this.openMicrophone();
|
|
254
|
-
|
|
269
|
+
const joinPermission = this._tuiCallEngine?.getDevicePermission();
|
|
270
|
+
setLocalUserInfoAudioVideoAvailable(!!joinPermission?.audio, NAME.AUDIO);
|
|
255
271
|
|
|
256
272
|
// // Current policy: By default, the camera remains off when joining GroupCall
|
|
257
273
|
// if (TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE) === CallMediaType.VIDEO) {
|
|
@@ -377,11 +393,29 @@ export default class TUICallService {
|
|
|
377
393
|
data: { callStatus },
|
|
378
394
|
});
|
|
379
395
|
if (callStatus === CallStatus.CONNECTED) return; // avoid double click when application stuck, especially for miniProgram
|
|
396
|
+
|
|
397
|
+
// Probe device permission so the following accept flow (updateDeviceList /
|
|
398
|
+
// openMicrophone / unavailable hints) runs against the real device state.
|
|
399
|
+
// Unavailable devices no longer block accept: we connect directly and surface
|
|
400
|
+
// a toast + banner afterwards instead of popping a confirm dialog.
|
|
401
|
+
const acceptMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
|
402
|
+
await this.probeDevicePermission({
|
|
403
|
+
audio: true,
|
|
404
|
+
video: acceptMediaType === CallMediaType.VIDEO,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await this._doAccept();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Perform the actual accept (called directly or after the accept-permission dialog)
|
|
411
|
+
public async _doAccept() {
|
|
380
412
|
try {
|
|
381
413
|
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED);
|
|
382
|
-
updateDeviceList(this._tuiCallEngine)
|
|
414
|
+
updateDeviceList(this._tuiCallEngine);
|
|
383
415
|
const response = await this._tuiCallEngine.accept();
|
|
384
416
|
await this._handleAcceptResponse(response);
|
|
417
|
+
const acceptCallMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
|
418
|
+
this._updateDeviceUnavailableHints(acceptCallMediaType);
|
|
385
419
|
} catch (error) {
|
|
386
420
|
this._tuiCallEngine?.reportLog?.({
|
|
387
421
|
name: 'TUICallKit.accept.fail',
|
|
@@ -406,7 +440,8 @@ export default class TUICallService {
|
|
|
406
440
|
const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
|
407
441
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
|
408
442
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
|
409
|
-
|
|
443
|
+
const acceptPermission = this._tuiCallEngine?.getDevicePermission();
|
|
444
|
+
setLocalUserInfoAudioVideoAvailable(!!acceptPermission?.audio, NAME.AUDIO);
|
|
410
445
|
}
|
|
411
446
|
}
|
|
412
447
|
@avoidRepeatedCall()
|
|
@@ -441,6 +476,11 @@ export default class TUICallService {
|
|
|
441
476
|
}
|
|
442
477
|
@avoidRepeatedCall()
|
|
443
478
|
public async openCamera(videoViewDomID: string) {
|
|
479
|
+
// Skip opening camera if video permission is already known to be unavailable
|
|
480
|
+
if (this.isDeviceKnownUnavailable(NAME.VIDEO as 'video')) {
|
|
481
|
+
console.warn(`${NAME.PREFIX}openCamera skipped: video device unavailable (code ${this._deviceErrorCodes.video}).`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
444
484
|
try {
|
|
445
485
|
if (TUIGlobal.isH5 || TUIGlobal.isWeChat) {
|
|
446
486
|
const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION);
|
|
@@ -466,6 +506,11 @@ export default class TUICallService {
|
|
|
466
506
|
}
|
|
467
507
|
@avoidRepeatedCall()
|
|
468
508
|
public async openMicrophone() {
|
|
509
|
+
// Skip opening microphone if audio permission is already known to be unavailable
|
|
510
|
+
if (this.isDeviceKnownUnavailable(NAME.AUDIO as 'audio')) {
|
|
511
|
+
console.warn(`${NAME.PREFIX}openMicrophone skipped: audio device unavailable (code ${this._deviceErrorCodes.audio}).`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
469
514
|
try {
|
|
470
515
|
await this._tuiCallEngine.openMicrophone();
|
|
471
516
|
setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO);
|
|
@@ -659,6 +704,102 @@ export default class TUICallService {
|
|
|
659
704
|
}
|
|
660
705
|
}
|
|
661
706
|
// =========================【private methods for service use】=========================
|
|
707
|
+
/**
|
|
708
|
+
* Check device permissions after call/accept succeeds.
|
|
709
|
+
* 1. Show an error toast summarizing unavailable devices.
|
|
710
|
+
* 2. Store the unavailable-device reasons so the banner bar and button badges
|
|
711
|
+
* can render accordingly (aligned with interaction design).
|
|
712
|
+
*/
|
|
713
|
+
private _updateDeviceUnavailableHints(callMediaType: CallMediaType): void {
|
|
714
|
+
const deviceUnavailableReasons = this._resolveUnavailableDevices(callMediaType);
|
|
715
|
+
if (!deviceUnavailableReasons) {
|
|
716
|
+
TUIStore.update(StoreName.CALL, NAME.DEVICE_UNAVAILABLE_REASONS, null);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Build toast message by grouping devices per reason so mixed cases
|
|
721
|
+
// (e.g. mic no-permission + camera no-device) are described accurately.
|
|
722
|
+
const toastText = buildDeviceUnavailableText(deviceUnavailableReasons.microphone, deviceUnavailableReasons.camera, 'toast');
|
|
723
|
+
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, {
|
|
724
|
+
content: { key: 'permission-toast-raw', options: { text: toastText } },
|
|
725
|
+
type: NAME.ERROR,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Store the unavailable-device reasons for banner bar + button badges
|
|
729
|
+
TUIStore.update(StoreName.CALL, NAME.DEVICE_UNAVAILABLE_REASONS, deviceUnavailableReasons);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Resolve the classified reasons for currently unavailable devices from the engine's
|
|
734
|
+
* cached permission. Returns null when all required devices are available; otherwise
|
|
735
|
+
* returns the per-device reason. Derived values (whether it is a permission issue,
|
|
736
|
+
* device-name text) are computed by the UI from these reasons.
|
|
737
|
+
*/
|
|
738
|
+
private _resolveUnavailableDevices(callMediaType: CallMediaType): DeviceUnavailableReasons | null {
|
|
739
|
+
const permission = this._tuiCallEngine?.getDevicePermission();
|
|
740
|
+
if (!permission) return null;
|
|
741
|
+
|
|
742
|
+
const noMicrophone = !permission.audio;
|
|
743
|
+
const noCamera = callMediaType === CallMediaType.VIDEO && !permission.video;
|
|
744
|
+
if (!noMicrophone && !noCamera) return null;
|
|
745
|
+
|
|
746
|
+
// Map the engine's audio/video permission to device-named reasons for the UI.
|
|
747
|
+
return {
|
|
748
|
+
microphone: noMicrophone ? this._classifyDeviceErrorReason(this._deviceErrorCodes.audio) : null,
|
|
749
|
+
camera: noCamera ? this._classifyDeviceErrorReason(this._deviceErrorCodes.video) : null,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private _classifyDeviceErrorReason(errorCode: number): DeviceErrorReason {
|
|
754
|
+
switch (errorCode) {
|
|
755
|
+
case DEVICE_ERROR_CODE.PERMISSION_DENIED: return 'no-permission';
|
|
756
|
+
case DEVICE_ERROR_CODE.NO_MICROPHONE:
|
|
757
|
+
case DEVICE_ERROR_CODE.NO_CAMERA: return 'no-device';
|
|
758
|
+
case DEVICE_ERROR_CODE.MICROPHONE_NOT_READABLE:
|
|
759
|
+
case DEVICE_ERROR_CODE.CAMERA_NOT_READABLE: return 'device-occupied';
|
|
760
|
+
default: return 'unknown';
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Probe device permission by reusing engine's device permission check, which
|
|
766
|
+
* triggers getUserMedia and classifies the failure (incl. iOS repeated-call and
|
|
767
|
+
* http protocol handling). The thrown error's code is cached per device so the
|
|
768
|
+
* service layer can decide device availability without engine-side errorName.
|
|
769
|
+
* Audio and video are probed separately because engine aborts on the first failure.
|
|
770
|
+
*/
|
|
771
|
+
public async probeDevicePermission(opts: { audio?: boolean; video?: boolean }): Promise<void> {
|
|
772
|
+
if (opts.audio) {
|
|
773
|
+
try {
|
|
774
|
+
await this._tuiCallEngine?._checkDevicePermission({ audio: true });
|
|
775
|
+
this._deviceErrorCodes.audio = DEVICE_ERROR_CODE.NONE;
|
|
776
|
+
} catch (error: any) {
|
|
777
|
+
this._deviceErrorCodes.audio = error?.code ?? DEVICE_ERROR_CODE.NONE;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (opts.video) {
|
|
781
|
+
try {
|
|
782
|
+
// isThrowVideoError is required, otherwise engine swallows the video error.
|
|
783
|
+
await this._tuiCallEngine?._checkDevicePermission({ video: true, isThrowVideoError: true });
|
|
784
|
+
this._deviceErrorCodes.video = DEVICE_ERROR_CODE.NONE;
|
|
785
|
+
} catch (error: any) {
|
|
786
|
+
this._deviceErrorCodes.video = error?.code ?? DEVICE_ERROR_CODE.NONE;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Clear the cached device error so the next probe re-triggers the browser prompt.
|
|
792
|
+
public resetDeviceError(type: 'audio' | 'video'): void {
|
|
793
|
+
if (type === NAME.AUDIO) this._deviceErrorCodes.audio = DEVICE_ERROR_CODE.NONE;
|
|
794
|
+
if (type === NAME.VIDEO) this._deviceErrorCodes.video = DEVICE_ERROR_CODE.NONE;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Whether a device is known to be unavailable (permission off / no device / occupied).
|
|
798
|
+
public isDeviceKnownUnavailable(type: 'audio' | 'video'): boolean {
|
|
799
|
+
const permission = this._tuiCallEngine?.getDevicePermission();
|
|
800
|
+
if (type === NAME.AUDIO) return !permission?.audio && this._deviceErrorCodes.audio !== DEVICE_ERROR_CODE.NONE;
|
|
801
|
+
return !permission?.video && this._deviceErrorCodes.video !== DEVICE_ERROR_CODE.NONE;
|
|
802
|
+
}
|
|
662
803
|
// 处理 "呼叫" 抛出的异常
|
|
663
804
|
private _handleCallError(error: any, methodName?: string) {
|
|
664
805
|
this._permissionCheckTimer && clearInterval(this._permissionCheckTimer);
|
|
@@ -691,7 +832,7 @@ export default class TUICallService {
|
|
|
691
832
|
oldStatus: StatusChange.IDLE,
|
|
692
833
|
newStatus: (groupID || remoteUserInfoList.length > 1) ? StatusChange.DIALING_GROUP : StatusChange.DIALING_C2C,
|
|
693
834
|
});
|
|
694
|
-
updateDeviceList(this._tuiCallEngine)
|
|
835
|
+
updateDeviceList(this._tuiCallEngine);
|
|
695
836
|
const remoteUserInfoLists = await getRemoteUserProfile(remoteUserInfoList.map(obj => obj.userId), this.getTim());
|
|
696
837
|
|
|
697
838
|
if (remoteUserInfoLists.length > 0) {
|
|
@@ -721,7 +862,10 @@ export default class TUICallService {
|
|
|
721
862
|
const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
|
|
722
863
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true });
|
|
723
864
|
TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true });
|
|
724
|
-
|
|
865
|
+
const callPermission = this._tuiCallEngine?.getDevicePermission();
|
|
866
|
+
setLocalUserInfoAudioVideoAvailable(!!callPermission?.audio, NAME.AUDIO);
|
|
867
|
+
// Show device-unavailable hints (banner / button badges / toast) once the call is established.
|
|
868
|
+
this._updateDeviceUnavailableHints(callMediaType);
|
|
725
869
|
} else {
|
|
726
870
|
this._permissionCheckTimer && clearInterval(this._permissionCheckTimer);
|
|
727
871
|
this._permissionCheckTimer = null;
|
|
@@ -200,19 +200,29 @@ export function deleteRemoteUser(userIdList: string[]): void {
|
|
|
200
200
|
TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
|
|
201
201
|
}
|
|
202
202
|
export function updateDeviceList(tuiCallEngine) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
// Check current permission state to avoid triggering getUserMedia prompts.
|
|
204
|
+
// getDeviceList internally calls _checkDevicePermission which fires getUserMedia
|
|
205
|
+
// when the permission has not been checked yet (permission.audio/video is false).
|
|
206
|
+
// This races with Engine.calls() and causes duplicate browser prompts.
|
|
207
|
+
const permission = tuiCallEngine?.getDevicePermission?.();
|
|
208
|
+
const hasAudioPermission = permission?.audio === true;
|
|
209
|
+
const hasVideoPermission = permission?.video === true;
|
|
210
|
+
|
|
211
|
+
if (hasAudioPermission) {
|
|
212
|
+
tuiCallEngine?.getDeviceList('speaker').then((result)=>{
|
|
213
|
+
const deviceList = TUIStore.getData(StoreName.CALL, NAME.DEVICE_LIST);
|
|
214
|
+
const currentSpeaker = result?.[0] || {};
|
|
215
|
+
TUIStore.update(
|
|
216
|
+
StoreName.CALL,
|
|
217
|
+
NAME.DEVICE_LIST,
|
|
218
|
+
{ ...deviceList, speakerList: result, currentSpeaker },
|
|
219
|
+
);
|
|
220
|
+
}).catch(error =>{
|
|
221
|
+
console.error(`${NAME.PREFIX}updateSpeakerList failed, error: ${JSON.stringify(error)}.`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
214
224
|
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
|
|
215
|
-
if (callMediaType === CallMediaType.VIDEO) {
|
|
225
|
+
if (callMediaType === CallMediaType.VIDEO && hasVideoPermission) {
|
|
216
226
|
tuiCallEngine?.getDeviceList('camera').then((result) => {
|
|
217
227
|
const deviceList = TUIStore.getData(StoreName.CALL, NAME.DEVICE_LIST);
|
|
218
228
|
const currentCamera = result?.[0] || {};
|
|
@@ -225,17 +235,19 @@ export function updateDeviceList(tuiCallEngine) {
|
|
|
225
235
|
console.error(`${NAME.PREFIX}updateCameraList failed, error: ${error}.`);
|
|
226
236
|
});
|
|
227
237
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
if (hasAudioPermission) {
|
|
239
|
+
tuiCallEngine?.getDeviceList('microphones').then((result) => {
|
|
240
|
+
const deviceList = TUIStore.getData(StoreName.CALL, NAME.DEVICE_LIST);
|
|
241
|
+
const currentMicrophone = result?.[0] || {};
|
|
242
|
+
TUIStore.update(
|
|
243
|
+
StoreName.CALL,
|
|
244
|
+
NAME.DEVICE_LIST,
|
|
245
|
+
{ ...deviceList, microphoneList: result, currentMicrophone },
|
|
246
|
+
);
|
|
247
|
+
}).catch(error => {
|
|
248
|
+
console.error(`${NAME.PREFIX}updateMicrophoneList failed, error: ${error}.`);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
239
251
|
}
|
|
240
252
|
/**
|
|
241
253
|
* update the no device permission toast
|
|
@@ -60,6 +60,7 @@ export default class CallStore {
|
|
|
60
60
|
realtimeMessageList: [],
|
|
61
61
|
isAITranscriberEnabled: false,
|
|
62
62
|
isAITranscriberRunning: false,
|
|
63
|
+
deviceUnavailableReasons: null, // per-device unavailable reasons (null = available)
|
|
63
64
|
};
|
|
64
65
|
public store: ICallStore = deepClone(this.defaultStore);
|
|
65
66
|
public prevStore: ICallStore = deepClone(this.defaultStore);
|
|
@@ -40,6 +40,7 @@ export const CALL_DATA_KEY: any = {
|
|
|
40
40
|
PUSHER_ID: 'pusherId',
|
|
41
41
|
IS_FORCE_USE_V2_API: 'isForceUseV2API',
|
|
42
42
|
MODAL_ERROR: 'modalError',
|
|
43
|
+
DEVICE_UNAVAILABLE_REASONS: 'deviceUnavailableReasons',
|
|
43
44
|
TRANSCRIBER_ROBOT_ID: 'transcriberRobotId',
|
|
44
45
|
REALTIME_MESSAGE_LIST: 'realtimeMessageList',
|
|
45
46
|
IS_AI_TRANSCRIBER_ENABLED: 'isAITranscriberEnabled', // Controls UI visibility
|
|
@@ -84,6 +85,32 @@ export const NAME = {
|
|
|
84
85
|
...CHAT_DATA_KEY,
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
// Stable device error codes exposed by TUICallEngine (kept in sync with the
|
|
89
|
+
// engine's public ERROR_CODE). Used by the service layer to classify device
|
|
90
|
+
// failures caught from engine's device permission check, instead of relying on
|
|
91
|
+
// engine-internal errorName fields.
|
|
92
|
+
// Classified reason why a device is unavailable, shared by the service layer and UI.
|
|
93
|
+
export type DeviceErrorReason = 'no-permission' | 'no-device' | 'device-occupied' | 'unknown' | null;
|
|
94
|
+
|
|
95
|
+
// Per-device reason why a device is currently unavailable. The value is the classified
|
|
96
|
+
// reason (e.g. 'no-permission' / 'no-device' / 'device-occupied'); null means that device
|
|
97
|
+
// is available. The keys follow the engine's audio/video wording:
|
|
98
|
+
// - microphone (audio input)
|
|
99
|
+
// - camera (video input)
|
|
100
|
+
export interface DeviceUnavailableReasons {
|
|
101
|
+
microphone: DeviceErrorReason;
|
|
102
|
+
camera: DeviceErrorReason;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const DEVICE_ERROR_CODE = {
|
|
106
|
+
NONE: 0, // no error
|
|
107
|
+
PERMISSION_DENIED: -1101, // NotAllowedError, permission refused
|
|
108
|
+
NO_MICROPHONE: 60003, // NotFoundError, no microphone device
|
|
109
|
+
NO_CAMERA: 60004, // NotFoundError, no camera device
|
|
110
|
+
MICROPHONE_NOT_READABLE: 60007, // NotReadableError, mic occupied / hardware error
|
|
111
|
+
CAMERA_NOT_READABLE: 60008, // NotReadableError, camera occupied / hardware error
|
|
112
|
+
};
|
|
113
|
+
|
|
87
114
|
export const AudioCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call.png';
|
|
88
115
|
export const VideoCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call-video-reverse.svg';
|
|
89
116
|
export const MAX_NUMBER_ROOM_ID = 2147483647;
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
LayoutMode,
|
|
14
14
|
CloudRecordPolicy,
|
|
15
15
|
} from './const/index';
|
|
16
|
-
import { t } from './locales/index';
|
|
16
|
+
import { t, buildDeviceUnavailableText } from './locales/index';
|
|
17
17
|
|
|
18
18
|
// 实例化
|
|
19
19
|
const TUICallKitAPI = TUICallService.getInstance();
|
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
VideoDisplayMode,
|
|
33
33
|
AudioPlayBackDevice,
|
|
34
34
|
t,
|
|
35
|
+
buildDeviceUnavailableText,
|
|
35
36
|
uiDesign,
|
|
36
37
|
FeatureButton,
|
|
37
38
|
LayoutMode,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition, ICustomUIConfig } from '../const/index';
|
|
1
|
+
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition, ICustomUIConfig, DeviceUnavailableReasons } from '../const/index';
|
|
2
2
|
import { IUserInfo, INetWorkQuality } from './index';
|
|
3
3
|
|
|
4
4
|
export interface IToastInfo {
|
|
@@ -58,4 +58,5 @@ export interface ICallStore {
|
|
|
58
58
|
realtimeMessageList: [], // 实时转写消息列表
|
|
59
59
|
isAITranscriberEnabled: boolean, // AI 转写UI是否显示
|
|
60
60
|
isAITranscriberRunning: boolean, // AI transcriber running state
|
|
61
|
+
deviceUnavailableReasons: DeviceUnavailableReasons | null, // Per-device unavailable reason (null = available); drives banner/button badges & toast
|
|
61
62
|
}
|
|
@@ -49,6 +49,10 @@ export const en = {
|
|
|
49
49
|
'camera disabled': 'Camera Off',
|
|
50
50
|
'microphone enabled': 'Unmuted',
|
|
51
51
|
'microphone disabled': 'Muted',
|
|
52
|
+
'no mic permission': 'No Mic Access',
|
|
53
|
+
'no mic device': 'No Mic Found',
|
|
54
|
+
'no cam permission': 'No Cam Access',
|
|
55
|
+
'no cam device': 'No Cam Found',
|
|
52
56
|
'speaker enabled': 'Speaker On',
|
|
53
57
|
'speaker disabled': 'Speaker Off',
|
|
54
58
|
'open speaker': 'Turn on speaker',
|
|
@@ -152,6 +156,28 @@ export const en = {
|
|
|
152
156
|
'error.10017': "Failed to start the call: the callee userID cannot be the current logged-in user. Please check the callee userID you passed in.",
|
|
153
157
|
'error.10018': "Failed to start the call: the number of callees in a single call must be between 1 and 8. Please check the userIDList you passed in.",
|
|
154
158
|
'error': 'Error',
|
|
159
|
+
// Permission / device unavailability texts
|
|
160
|
+
'permission-device-separator': ' and ',
|
|
161
|
+
'permission-phrase-separator': '; ',
|
|
162
|
+
'permission-sentence-separator': '. ',
|
|
163
|
+
'permission-phrase-perm': '{{ devices }} access denied',
|
|
164
|
+
'permission-phrase-no-device': '{{ devices }} not detected',
|
|
165
|
+
'permission-phrase-occupied': '{{ devices }} is in use by another app',
|
|
166
|
+
'permission-phrase-unknown': '{{ devices }} unavailable',
|
|
167
|
+
'permission-toast-suffix': ', cannot be used during the call.',
|
|
168
|
+
'permission-toast-raw': '{{ text }}',
|
|
169
|
+
'permission-banner-guide-perm': 'Please allow access in the browser address bar.',
|
|
170
|
+
'permission-banner-guide-perm-h5': 'Please enable in system settings.',
|
|
171
|
+
'permission-banner-guide-no-device': 'Please connect device and try again.',
|
|
172
|
+
'permission-banner-guide-no-device-h5': 'Please connect device.',
|
|
173
|
+
'permission-btn-mic-no-perm': 'Microphone access denied. Please click the lock icon in the address bar to allow.',
|
|
174
|
+
'permission-btn-mic-no-device': 'No microphone detected. Please connect a microphone.',
|
|
175
|
+
'permission-btn-mic-occupied': 'Microphone is in use by another app. Please close that app.',
|
|
176
|
+
'permission-btn-cam-no-perm': 'Camera access denied. Please click the lock icon in the address bar to allow.',
|
|
177
|
+
'permission-btn-cam-no-device': 'No camera detected. Please connect a camera.',
|
|
178
|
+
'permission-btn-cam-occupied': 'Camera is in use by another app. Please close that app.',
|
|
179
|
+
'permission-btn-mic-no-perm-h5': 'Microphone access denied. Please allow browser microphone access in system settings.',
|
|
180
|
+
'permission-btn-cam-no-perm-h5': 'Camera access denied. Please allow browser camera access in system settings.',
|
|
155
181
|
// AI Subtitle Settings
|
|
156
182
|
'ai-subtitle': 'AI Subtitle',
|
|
157
183
|
'ai-subtitle-settings': 'AI Subtitle Settings',
|
|
@@ -59,3 +59,69 @@ interface languageItemType {
|
|
|
59
59
|
interface languageDataType {
|
|
60
60
|
[key: string]: languageItemType;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
// Map a classified device error reason to its phrase translation key.
|
|
64
|
+
const DEVICE_REASON_PHRASE_KEY: { [reason: string]: string } = {
|
|
65
|
+
'no-permission': 'permission-phrase-perm',
|
|
66
|
+
'no-device': 'permission-phrase-no-device',
|
|
67
|
+
'device-occupied': 'permission-phrase-occupied',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a device-unavailability message by grouping devices by their error reason,
|
|
72
|
+
* so each reason is described accurately. For example, when the microphone has no
|
|
73
|
+
* permission and the camera has no device, it produces
|
|
74
|
+
* "麦克风权限未开启,未检测到摄像头" instead of wrongly saying both lack permission.
|
|
75
|
+
*
|
|
76
|
+
* @param audioReason classified reason for the microphone, or null if available
|
|
77
|
+
* @param videoReason classified reason for the camera, or null if available
|
|
78
|
+
* @param scene 'plain' returns the grouped description only; 'toast' appends the
|
|
79
|
+
* in-call suffix; 'banner' appends the guidance sentence
|
|
80
|
+
* @param isMobile selects PC vs H5 guidance text (banner only)
|
|
81
|
+
* @returns the composed localized text, or '' when no device is unavailable
|
|
82
|
+
*/
|
|
83
|
+
export function buildDeviceUnavailableText(
|
|
84
|
+
audioReason: string | null,
|
|
85
|
+
videoReason: string | null,
|
|
86
|
+
scene: 'plain' | 'toast' | 'banner',
|
|
87
|
+
isMobile = false,
|
|
88
|
+
): string {
|
|
89
|
+
const reasonToDevices: { [reason: string]: string[] } = {};
|
|
90
|
+
const collect = (reason: string | null, deviceName: string) => {
|
|
91
|
+
if (!reason) return;
|
|
92
|
+
(reasonToDevices[reason] = reasonToDevices[reason] || []).push(deviceName);
|
|
93
|
+
};
|
|
94
|
+
collect(audioReason, t('microphone'));
|
|
95
|
+
collect(videoReason, t('camera'));
|
|
96
|
+
|
|
97
|
+
const reasons = Object.keys(reasonToDevices);
|
|
98
|
+
if (reasons.length === 0) return '';
|
|
99
|
+
|
|
100
|
+
const deviceSep = t('permission-device-separator');
|
|
101
|
+
const phraseSep = t('permission-phrase-separator');
|
|
102
|
+
const phrases = reasons.map((reason) => {
|
|
103
|
+
const deviceNameText = reasonToDevices[reason].join(deviceSep);
|
|
104
|
+
const phraseKey = DEVICE_REASON_PHRASE_KEY[reason] || 'permission-phrase-unknown';
|
|
105
|
+
return t({ key: phraseKey, options: { devices: deviceNameText } });
|
|
106
|
+
});
|
|
107
|
+
const description = phrases.join(phraseSep);
|
|
108
|
+
|
|
109
|
+
if (scene === 'plain') {
|
|
110
|
+
return description;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (scene === 'toast') {
|
|
114
|
+
return `${description}${t('permission-toast-suffix')}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Banner: permission issues take priority since the user must actively grant
|
|
118
|
+
// access; otherwise guide the user to connect a device.
|
|
119
|
+
const hasPermIssue = reasons.includes('no-permission');
|
|
120
|
+
let guideKey: string;
|
|
121
|
+
if (hasPermIssue) {
|
|
122
|
+
guideKey = isMobile ? 'permission-banner-guide-perm-h5' : 'permission-banner-guide-perm';
|
|
123
|
+
} else {
|
|
124
|
+
guideKey = isMobile ? 'permission-banner-guide-no-device-h5' : 'permission-banner-guide-no-device';
|
|
125
|
+
}
|
|
126
|
+
return `${description}${t('permission-sentence-separator')}${t(guideKey)}`;
|
|
127
|
+
}
|
|
@@ -60,6 +60,10 @@ export const ja_JP = {
|
|
|
60
60
|
'camera disabled': 'カメラオフ',
|
|
61
61
|
'microphone enabled': 'マイクオン',
|
|
62
62
|
'microphone disabled': 'マイクオフ',
|
|
63
|
+
'no mic permission': 'マイク権限なし',
|
|
64
|
+
'no mic device': 'マイク未検出',
|
|
65
|
+
'no cam permission': 'カメラ権限なし',
|
|
66
|
+
'no cam device': 'カメラ未検出',
|
|
63
67
|
'speaker enabled': 'スピーカーオン',
|
|
64
68
|
'speaker disabled': 'スピーカーオフ',
|
|
65
69
|
'open speaker': 'スピーカーをオンにする',
|
|
@@ -150,6 +154,28 @@ export const ja_JP = {
|
|
|
150
154
|
'error.10017': "通話の発信に失敗しました:着信側 userID を現在ログイン中のユーザー自身に指定することはできません。渡した着信側 userID を確認してください。",
|
|
151
155
|
'error.10018': "通話の発信に失敗しました:1 回の通話で指定できる着信側ユーザー数は 1〜8 人である必要があります。渡した userIDList を確認してください。",
|
|
152
156
|
'error': 'エラー',
|
|
157
|
+
// Permission / device unavailability texts
|
|
158
|
+
'permission-device-separator': '、',
|
|
159
|
+
'permission-phrase-separator': '、',
|
|
160
|
+
'permission-sentence-separator': '。',
|
|
161
|
+
'permission-phrase-perm': '{{ devices }}のアクセス許可が未設定です',
|
|
162
|
+
'permission-phrase-no-device': '{{ devices }}が検出されません',
|
|
163
|
+
'permission-phrase-occupied': '{{ devices }}が他のアプリで使用中です',
|
|
164
|
+
'permission-phrase-unknown': '{{ devices }}は使用できません',
|
|
165
|
+
'permission-toast-suffix': '。通話中は使用できません。',
|
|
166
|
+
'permission-toast-raw': '{{ text }}',
|
|
167
|
+
'permission-banner-guide-perm': 'ブラウザのアドレスバーでアクセスを許可してください。',
|
|
168
|
+
'permission-banner-guide-perm-h5': 'システム設定で有効にしてください。',
|
|
169
|
+
'permission-banner-guide-no-device': 'デバイスを接続して再試行してください。',
|
|
170
|
+
'permission-banner-guide-no-device-h5': 'デバイスを接続してください。',
|
|
171
|
+
'permission-btn-mic-no-perm': 'マイクのアクセス許可が未設定です。アドレスバーのロックアイコンをクリックして許可してください。',
|
|
172
|
+
'permission-btn-mic-no-device': 'マイクが検出されません。マイクを接続してください。',
|
|
173
|
+
'permission-btn-mic-occupied': 'マイクが他のアプリで使用中です。他のアプリを閉じてください。',
|
|
174
|
+
'permission-btn-cam-no-perm': 'カメラのアクセス許可が未設定です。アドレスバーのロックアイコンをクリックして許可してください。',
|
|
175
|
+
'permission-btn-cam-no-device': 'カメラが検出されません。カメラを接続してください。',
|
|
176
|
+
'permission-btn-cam-occupied': 'カメラが他のアプリで使用中です。他のアプリを閉じてください。',
|
|
177
|
+
'permission-btn-mic-no-perm-h5': 'マイクのアクセス許可が未設定です。システム設定でブラウザのマイクを許可してください。',
|
|
178
|
+
'permission-btn-cam-no-perm-h5': 'カメラのアクセス許可が未設定です。システム設定でブラウザのカメラを許可してください。',
|
|
153
179
|
// AI Subtitle Settings
|
|
154
180
|
'ai-subtitle': 'AI字幕',
|
|
155
181
|
'ai-subtitle-settings': 'AI字幕設定',
|
|
@@ -49,6 +49,10 @@ export const zh = {
|
|
|
49
49
|
'camera disabled': '摄像头已关',
|
|
50
50
|
'microphone enabled': '麦克风已开',
|
|
51
51
|
'microphone disabled': '麦克风已关',
|
|
52
|
+
'no mic permission': '无麦克风权限',
|
|
53
|
+
'no mic device': '无麦克风设备',
|
|
54
|
+
'no cam permission': '无摄像头权限',
|
|
55
|
+
'no cam device': '无摄像头设备',
|
|
52
56
|
'speaker enabled': '扬声器已开',
|
|
53
57
|
'speaker disabled': '扬声器已关',
|
|
54
58
|
'open speaker': '开启扬声器',
|
|
@@ -147,6 +151,37 @@ export const zh = {
|
|
|
147
151
|
'error.10017': "发起通话失败:被叫 userID 不能是当前登录用户自己,请检查传入的被叫 userID。",
|
|
148
152
|
'error.10018': "发起通话失败:单次呼叫的被叫用户数量需在 1 到 8 之间,请检查传入的 userIDList。",
|
|
149
153
|
'error': '错误',
|
|
154
|
+
// Permission / device unavailability texts
|
|
155
|
+
// Separator used when joining multiple device names sharing the same reason (e.g. microphone、camera)
|
|
156
|
+
'permission-device-separator': '、',
|
|
157
|
+
// Separator used when joining phrases of different reasons
|
|
158
|
+
'permission-phrase-separator': ',',
|
|
159
|
+
// Separator between the device description and the guidance sentence (banner)
|
|
160
|
+
'permission-sentence-separator': ',',
|
|
161
|
+
// Device unavailability phrases (grouped by reason, combined dynamically)
|
|
162
|
+
'permission-phrase-perm': '{{ devices }}权限未开启',
|
|
163
|
+
'permission-phrase-no-device': '未检测到{{ devices }}',
|
|
164
|
+
'permission-phrase-occupied': '{{ devices }}被其他应用占用',
|
|
165
|
+
'permission-phrase-unknown': '{{ devices }}不可用',
|
|
166
|
+
// Toast: suffix appended after the combined device phrases
|
|
167
|
+
'permission-toast-suffix': ',通话中将无法使用',
|
|
168
|
+
// Toast: raw passthrough template for pre-composed text
|
|
169
|
+
'permission-toast-raw': '{{ text }}',
|
|
170
|
+
// Banner: guidance appended after the device phrases (PC / H5)
|
|
171
|
+
'permission-banner-guide-perm': '请在浏览器地址栏允许访问',
|
|
172
|
+
'permission-banner-guide-perm-h5': '请前往系统设置开启',
|
|
173
|
+
'permission-banner-guide-no-device': '请连接设备后重试',
|
|
174
|
+
'permission-banner-guide-no-device-h5': '请连接设备',
|
|
175
|
+
// Button click toast (PC)
|
|
176
|
+
'permission-btn-mic-no-perm': '麦克风权限未开启,请在浏览器地址栏点击锁定图标允许使用',
|
|
177
|
+
'permission-btn-mic-no-device': '未检测到麦克风设备,请连接麦克风后重试',
|
|
178
|
+
'permission-btn-mic-occupied': '麦克风被其他应用占用,请关闭占用应用后重试',
|
|
179
|
+
'permission-btn-cam-no-perm': '摄像头权限未开启,请在浏览器地址栏点击锁定图标允许使用',
|
|
180
|
+
'permission-btn-cam-no-device': '未检测到摄像头设备,请连接摄像头后重试',
|
|
181
|
+
'permission-btn-cam-occupied': '摄像头被其他应用占用,请关闭占用应用后重试',
|
|
182
|
+
// Button click toast (H5)
|
|
183
|
+
'permission-btn-mic-no-perm-h5': '麦克风权限未开启,请在手机系统设置中允许浏览器使用麦克风',
|
|
184
|
+
'permission-btn-cam-no-perm-h5': '摄像头权限未开启,请在手机系统设置中允许浏览器使用摄像头',
|
|
150
185
|
// AI Subtitle Settings
|
|
151
186
|
'ai-subtitle': 'AI 字幕',
|
|
152
187
|
'ai-subtitle-settings': 'AI 字幕设置',
|