From 43153c15a7f2fe28f56a527a2e3bd3e4105e4a6d Mon Sep 17 00:00:00 2001 From: zoujiandong <10130823232@qq.com> Date: Mon, 18 Mar 2024 16:43:38 +0800 Subject: [PATCH] =?UTF-8?q?3.18=20=E6=82=A3=E8=80=85=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TUICallKit/TUICallKit/TUICallKit.js | 2 + .../component/SingleCall/SingleCall.js | 31 +- .../TUICallService/CallService/bellContext.ts | 86 ++ .../TUICallService/CallService/index.d.ts | 2 + .../TUICallService/CallService/index.js | 72 +- .../TUICallService/CallService/index.ts | 991 ++++++++++++++++++ .../TUICallService/CallService/miniProgram.ts | 55 + .../TUICallService/CallService/utils.js | 4 +- .../TUICallService/CallService/utils.ts | 142 +++ .../TUICallService/TUIGlobal/tuiGlobal.ts | 43 + .../TUICallService/TUIStore/callStore.js | 3 + .../TUICallService/TUIStore/callStore.ts | 66 ++ .../TUICallService/TUIStore/tuiStore.ts | 157 +++ .../TUICallService/assets/phone_dialing.mp3 | Bin 0 -> 9178 bytes .../TUICallService/assets/phone_ringing.mp3 | Bin 0 -> 8752 bytes TUICallKit/TUICallService/const/call.d.ts | 7 +- TUICallKit/TUICallService/const/call.js | 8 +- TUICallKit/TUICallService/const/call.ts | 107 ++ TUICallKit/TUICallService/const/error.ts | 20 + TUICallKit/TUICallService/const/index.js | 4 +- TUICallKit/TUICallService/const/index.ts | 70 ++ TUICallKit/TUICallService/const/log.ts | 9 + TUICallKit/TUICallService/index.ts | 23 + .../TUICallService/interface/ICallService.ts | 97 ++ .../TUICallService/interface/ICallStore.d.ts | 5 +- .../TUICallService/interface/ICallStore.ts | 41 + .../TUICallService/interface/ITUIGlobal.ts | 38 + .../TUICallService/interface/ITUIStore.ts | 85 ++ TUICallKit/TUICallService/interface/index.ts | 4 + TUICallKit/TUICallService/locales/en.d.ts | 59 +- TUICallKit/TUICallService/locales/en.js | 73 +- TUICallKit/TUICallService/locales/en.ts | 107 ++ TUICallKit/TUICallService/locales/index.js | 1 + TUICallKit/TUICallService/locales/index.ts | 57 + TUICallKit/TUICallService/locales/ja_JP.d.ts | 48 +- TUICallKit/TUICallService/locales/ja_JP.js | 62 +- TUICallKit/TUICallService/locales/ja_JP.ts | 105 ++ TUICallKit/TUICallService/locales/zh-cn.d.ts | 63 +- TUICallKit/TUICallService/locales/zh-cn.js | 77 +- TUICallKit/TUICallService/locales/zh-cn.ts | 102 ++ .../TUICallService/serve/callManager.d.ts | 2 + .../TUICallService/serve/callManager.js | 13 + .../TUICallService/serve/callManager.ts | 117 +++ .../TUICallService/utils/common-utils.js | 12 +- .../TUICallService/utils/common-utils.ts | 239 +++++ .../utils/decorators/promise-retry.d.ts | 32 + .../utils/decorators/promise-retry.js | 40 + .../utils/decorators/promise-retry.ts | 51 + TUICallKit/TUICallService/utils/env.ts | 51 + TUICallKit/TUICallService/utils/index.ts | 16 + TUICallKit/TUICallService/utils/is-empty.ts | 31 + TUICallKit/TUICallService/utils/retry.d.ts | 36 + TUICallKit/TUICallService/utils/retry.js | 95 ++ TUICallKit/TUICallService/utils/retry.ts | 88 ++ TUICallKit/TUICallService/utils/timer.d.ts | 2 +- TUICallKit/TUICallService/utils/timer.ts | 162 +++ .../utils/validate/avoidRepeatedCall.ts | 34 + .../TUICallService/utils/validate/index.ts | 11 + .../utils/validate/validateConfig.ts | 188 ++++ .../utils/validate/validateParams.ts | 88 ++ .../MessageElements/CustomMessage/index.js | 6 +- .../MessageElements/CustomMessage/index.wxml | 6 +- .../MessageElements/CustomMessage/index.wxss | 23 + .../TUIChat/components/MessageInput/index.js | 14 +- .../components/MessageInput/index.wxml | 2 +- .../TUIChat/components/MessageList/index.js | 3 +- .../TUIChat/components/MessageList/index.wxml | 4 +- TUIService/TUIKit/components/TUIChat/index.js | 6 +- .../TUIKit/components/TUIChat/index.wxml | 3 +- .../TUIKit/components/TUIChat/index.wxss | 2 +- .../TUIKit/static/images/video-left.png | Bin 0 -> 1068 bytes .../TUIKit/static/images/video-right.png | Bin 0 -> 855 bytes app.js | 14 +- filters/filter.wxs | 15 + miniprogram_npm/tim-wx-sdk/index.js | 4 +- miniprogram_npm/trtc-wx-sdk/index.js | 6 +- miniprogram_npm/trtc-wx-sdk/index.js.map | 2 +- miniprogram_npm/tsignaling-wx/index.js | 4 +- miniprogram_npm/tuicall-engine-wx/index.js | 8 +- .../tuicall-engine-wx/index.js.map | 2 +- pages/index/index.js | 7 + pages/index/index.wxml | 10 +- pages/index/index.wxss | 11 + pages/message/message.wxml | 5 +- patient/pages/comment/comment.wxml | 54 +- patient/pages/comment/comment.wxss | 57 + .../pages/expertConsult/expertConsult.wxml | 10 +- .../pages/expertConsult/expertConsult.wxss | 26 +- patient/pages/expertDetail/expertDetail.js | 95 +- patient/pages/expertDetail/expertDetail.wxml | 43 +- patient/pages/expertDetail/expertDetail.wxss | 30 +- patient/pages/orderDetail/orderDetail.wxml | 2 +- patient/pages/search/search.wxml | 8 +- patient/pages/search/search.wxss | 11 + patient/pages/videoList/videoList.wxml | 8 +- patient/pages/videoList/videoList.wxss | 14 +- patient/pages/writeSick/writeSick.js | 10 +- sugarCheck/pages/globalCall/globalCall.js | 66 -- sugarCheck/pages/globalCall/globalCall.json | 3 - sugarCheck/pages/globalCall/globalCall.wxml | 2 - sugarCheck/pages/globalCall/globalCall.wxss | 1 - utils/request.js | 3 +- 102 files changed, 4477 insertions(+), 357 deletions(-) create mode 100644 TUICallKit/TUICallService/CallService/bellContext.ts create mode 100644 TUICallKit/TUICallService/CallService/index.ts create mode 100644 TUICallKit/TUICallService/CallService/miniProgram.ts create mode 100644 TUICallKit/TUICallService/CallService/utils.ts create mode 100644 TUICallKit/TUICallService/TUIGlobal/tuiGlobal.ts create mode 100644 TUICallKit/TUICallService/TUIStore/callStore.ts create mode 100644 TUICallKit/TUICallService/TUIStore/tuiStore.ts create mode 100644 TUICallKit/TUICallService/assets/phone_dialing.mp3 create mode 100644 TUICallKit/TUICallService/assets/phone_ringing.mp3 create mode 100644 TUICallKit/TUICallService/const/call.ts create mode 100644 TUICallKit/TUICallService/const/error.ts create mode 100644 TUICallKit/TUICallService/const/index.ts create mode 100644 TUICallKit/TUICallService/const/log.ts create mode 100644 TUICallKit/TUICallService/index.ts create mode 100644 TUICallKit/TUICallService/interface/ICallService.ts create mode 100644 TUICallKit/TUICallService/interface/ICallStore.ts create mode 100644 TUICallKit/TUICallService/interface/ITUIGlobal.ts create mode 100644 TUICallKit/TUICallService/interface/ITUIStore.ts create mode 100644 TUICallKit/TUICallService/interface/index.ts create mode 100644 TUICallKit/TUICallService/locales/en.ts create mode 100644 TUICallKit/TUICallService/locales/index.ts create mode 100644 TUICallKit/TUICallService/locales/ja_JP.ts create mode 100644 TUICallKit/TUICallService/locales/zh-cn.ts create mode 100644 TUICallKit/TUICallService/serve/callManager.ts create mode 100644 TUICallKit/TUICallService/utils/common-utils.ts create mode 100644 TUICallKit/TUICallService/utils/decorators/promise-retry.d.ts create mode 100644 TUICallKit/TUICallService/utils/decorators/promise-retry.js create mode 100644 TUICallKit/TUICallService/utils/decorators/promise-retry.ts create mode 100644 TUICallKit/TUICallService/utils/env.ts create mode 100644 TUICallKit/TUICallService/utils/index.ts create mode 100644 TUICallKit/TUICallService/utils/is-empty.ts create mode 100644 TUICallKit/TUICallService/utils/retry.d.ts create mode 100644 TUICallKit/TUICallService/utils/retry.js create mode 100644 TUICallKit/TUICallService/utils/retry.ts create mode 100644 TUICallKit/TUICallService/utils/timer.ts create mode 100644 TUICallKit/TUICallService/utils/validate/avoidRepeatedCall.ts create mode 100644 TUICallKit/TUICallService/utils/validate/index.ts create mode 100644 TUICallKit/TUICallService/utils/validate/validateConfig.ts create mode 100644 TUICallKit/TUICallService/utils/validate/validateParams.ts create mode 100644 TUIService/TUIKit/static/images/video-left.png create mode 100644 TUIService/TUIKit/static/images/video-right.png delete mode 100644 sugarCheck/pages/globalCall/globalCall.js delete mode 100644 sugarCheck/pages/globalCall/globalCall.json delete mode 100644 sugarCheck/pages/globalCall/globalCall.wxml delete mode 100644 sugarCheck/pages/globalCall/globalCall.wxss diff --git a/TUICallKit/TUICallKit/TUICallKit.js b/TUICallKit/TUICallKit/TUICallKit.js index 3e15b1c..e814148 100644 --- a/TUICallKit/TUICallKit/TUICallKit.js +++ b/TUICallKit/TUICallKit/TUICallKit.js @@ -43,6 +43,7 @@ Component({ callStatus: value, }); if(value=="calling"){ + TUICallKitServer.setLogLevel(0); if(this.data.callerUserInfo.userId){ let chat=TUICallKitServer.getTim(); let promise=chat.getUserProfile({ @@ -210,6 +211,7 @@ Component({ [PUSHER]: this.handlePusherChange.bind(that), [PLAYER]: this.handlePlayerListChange.bind(that), }); + }, }, }); diff --git a/TUICallKit/TUICallKit/component/SingleCall/SingleCall.js b/TUICallKit/TUICallKit/component/SingleCall/SingleCall.js index aa46d9a..a200ebd 100644 --- a/TUICallKit/TUICallKit/component/SingleCall/SingleCall.js +++ b/TUICallKit/TUICallKit/component/SingleCall/SingleCall.js @@ -8,7 +8,7 @@ Component({ callStatus: { type: String, observer(callStatus) { - console.log("改编状态qq:"+callStatus); + console.log("改编状态:"+callStatus); } }, callMediaType: { @@ -44,13 +44,22 @@ Component({ type: Boolean } }, - pageLifetimes: { - show: function() { - // 页面被展示 + lifetimes: { + moved(){ + //console.log('moved') }, - hide: function() { - // 页面被隐藏 - this.hangup() + detached(){ + console.log('detached') + this.reject() + } + }, + pageLifetimes: { + show() { + + }, + hide() { + + }, resize: function(size) { // 页面尺寸变化 @@ -71,8 +80,12 @@ Component({ doctorInfo:{} }, methods: { - async accept() { - await TUICallKitServer.accept(); + accept() { + //延迟接听; + setTimeout(async()=>{ + await TUICallKitServer.enableMuteMode(false); + await TUICallKitServer.accept(); + },800) }, async hangup() { await TUICallKitServer.hangup(); diff --git a/TUICallKit/TUICallService/CallService/bellContext.ts b/TUICallKit/TUICallService/CallService/bellContext.ts new file mode 100644 index 0000000..39534c7 --- /dev/null +++ b/TUICallKit/TUICallService/CallService/bellContext.ts @@ -0,0 +1,86 @@ +import { CallStatus, NAME, CallRole } from '../const/index'; +import { IBellParams } from '../interface/index'; +import { isUndefined } from '../utils/common-utils'; +const DEFAULT_CALLER_BELL_FILEPATH = '/TUICallKit/static/phone_dialing.mp3'; +const DEFAULT_CALLEE_BELL_FILEPATH = '/TUICallKit/static/phone_ringing.mp3'; + +export class BellContext { + private _bellContext: any = null; + private _isMuteBell: boolean = false; + private _calleeBellFilePath: string = DEFAULT_CALLEE_BELL_FILEPATH; + private _callRole: string = CallRole.UNKNOWN; + private _callStatus: string = CallStatus.IDLE; + + constructor() { + // @ts-ignore + this._bellContext = wx.createInnerAudioContext(); + this._bellContext.loop = true; + } + + setBellSrc() { + // @ts-ignore + const fs = wx.getFileSystemManager(); + try { + let playBellFilePath = DEFAULT_CALLER_BELL_FILEPATH; + if (this._callRole === CallRole.CALLEE) { + playBellFilePath = this._calleeBellFilePath || DEFAULT_CALLEE_BELL_FILEPATH; + } + fs.readFileSync(playBellFilePath, 'utf8', 0); + this._bellContext.src = playBellFilePath; + } catch (error) { + console.warn(`${NAME.PREFIX}Failed to setBellSrc, ${error}`); + } + } + setBellProperties(bellParams: IBellParams) { + this._callRole = bellParams.callRole || this._callRole; + this._callStatus = bellParams.callStatus || this._callStatus; + this._calleeBellFilePath = bellParams.calleeBellFilePath || this._calleeBellFilePath; + // undefined/false || isMuteBell => isMuteBell (不符合预期) + this._isMuteBell = isUndefined(bellParams.isMuteBell) ? this._isMuteBell : bellParams.isMuteBell; + } + async play() { + try { + if (this._callStatus !== CallStatus.CALLING) { + return ; + } + this.setBellSrc(); + if (this._callRole === CallRole.CALLEE && !this._isMuteBell) { + await this._bellContext.play(); + } + if (this._callRole === CallRole.CALLER) { + await this._bellContext.play(); + } + } catch (error) { + console.warn(`${NAME.PREFIX}Failed to play audio file, ${error}`); + } + } + async stop() { + try { + this._bellContext.stop(); + } catch (error) { + console.warn(`${NAME.PREFIX}Failed to stop audio file, ${error}`); + } + } + async setBellMute(enable: boolean) { + if (this._callStatus !== CallStatus.CALLING && this._callRole !== CallRole.CALLEE) { + return; + } + if (enable) { + await this.stop(); + } else { + await this.play(); + } + } + destroy() { + try { + this._isMuteBell = false; + this._calleeBellFilePath = ''; + this._callRole = CallRole.UNKNOWN; + this._callStatus = CallStatus.IDLE; + this._bellContext.destroy(); + this._bellContext = null; + } catch (error) { + console.warn(`${NAME.PREFIX}Failed to destroy, ${error}`); + } + } +} diff --git a/TUICallKit/TUICallService/CallService/index.d.ts b/TUICallKit/TUICallService/CallService/index.d.ts index c5872c6..1d378ff 100644 --- a/TUICallKit/TUICallService/CallService/index.d.ts +++ b/TUICallKit/TUICallService/CallService/index.d.ts @@ -11,7 +11,9 @@ export default class TUICallService implements ITUICallService { private _tim; private _TUICore; private _timerId; + private _startTimeStamp; private _bellContext; + private _defaultOfflinePushInfo; constructor(); static getInstance(): TUICallService; init(params: IInitParams): Promise; diff --git a/TUICallKit/TUICallService/CallService/index.js b/TUICallKit/TUICallService/CallService/index.js index 02072c9..c27cd02 100644 --- a/TUICallKit/TUICallService/CallService/index.js +++ b/TUICallKit/TUICallService/CallService/index.js @@ -56,7 +56,12 @@ class TUICallService { this._tim = null; this._TUICore = null; this._timerId = -1; + this._startTimeStamp = (0, common_utils_1.performanceNow)(); this._bellContext = null; + this._defaultOfflinePushInfo = { + title: '', + description: (0, index_2.t)('you have a new call'), + }; // =========================【监听 TUIStore 中的状态】========================= this._handleCallStatusChange = (value) => __awaiter(this, void 0, void 0, function* () { var _a, _b; @@ -76,6 +81,7 @@ class TUICallService { const callMediaType = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_MEDIA_TYPE); const remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST); const oldStatus = isGroup ? index_1.StatusChange.DIALING_GROUP : index_1.StatusChange.DIALING_C2C; + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_TIPS, ''); this.statusChanged && this.statusChanged({ oldStatus, newStatus: (0, utils_1.generateStatusChangeText)(TUIStore) }); if (!isGroup && callMediaType === index_1.CallMediaType.VIDEO) { this.switchScreen(remoteUserInfoList[0].domId); @@ -124,6 +130,7 @@ class TUICallService { }); this._addListenTuiCallEngineEvent(); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, { userId: userID }); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { userId: userID }); yield this._tuiCallEngine.login({ userID, userSig, assetsPath: '' }); // web && mini } catch (error) { @@ -156,11 +163,12 @@ class TUICallService { call(callParams) { return __awaiter(this, void 0, void 0, function* () { try { - const { type, userID } = callParams; + const { type, userID, offlinePushInfo } = callParams; if (TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS) !== index_1.CallStatus.IDLE) return; yield this._updateCallStoreBeforeCall(type, [{ userId: userID }]); this._executeExternalBeforeCalling(); // 执行外部传入的 beforeCall 方法 + callParams.offlinePushInfo = Object.assign(Object.assign({}, this._defaultOfflinePushInfo), offlinePushInfo); const response = yield this._tuiCallEngine.call(callParams); yield this._updateCallStoreAfterCall([userID], response); } @@ -173,12 +181,13 @@ class TUICallService { groupCall(groupCallParams) { return __awaiter(this, void 0, void 0, function* () { try { - const { userIDList, type, groupID } = groupCallParams; + const { userIDList, type, groupID, offlinePushInfo } = groupCallParams; if (TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS) !== index_1.CallStatus.IDLE) return; const remoteUserInfoList = userIDList.map(userId => ({ userId })); yield this._updateCallStoreBeforeCall(type, remoteUserInfoList, groupID); this._executeExternalBeforeCalling(); + groupCallParams.offlinePushInfo = Object.assign(Object.assign({}, this._defaultOfflinePushInfo), offlinePushInfo); const response = yield this._tuiCallEngine.groupCall(groupCallParams); yield this._updateCallStoreAfterCall(userIDList, response); } @@ -194,6 +203,7 @@ class TUICallService { let inviteUserInfoList = yield (0, utils_1.getRemoteUserProfile)(userIDList, this.getTim(), TUIStore); const remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, [...remoteUserInfoList, ...inviteUserInfoList]); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, [...remoteUserInfoList, ...inviteUserInfoList]); this._tuiCallEngine && (yield this._tuiCallEngine.inviteUser(params)); } catch (error) { @@ -223,6 +233,7 @@ class TUICallService { this.setSoundMode(params.type === index_1.CallMediaType.AUDIO ? index_1.AudioPlayBackDevice.EAR : index_1.AudioPlayBackDevice.SPEAKER); const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO); } catch (error) { @@ -301,6 +312,7 @@ class TUICallService { this.setSoundMode(callMediaType === index_1.CallMediaType.AUDIO ? index_1.AudioPlayBackDevice.EAR : index_1.AudioPlayBackDevice.SPEAKER); const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO); // web && mini default open audio } } @@ -422,8 +434,11 @@ class TUICallService { } switchCamera() { return __awaiter(this, void 0, void 0, function* () { + const currentPosition = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CAMERA_POSITION); + const targetPosition = currentPosition === index_1.CameraPosition.BACK ? index_1.CameraPosition.FRONT : index_1.CameraPosition.BACK; try { - yield this._tuiCallEngine.switchCamera(); + yield this._tuiCallEngine.switchCamera(targetPosition); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CAMERA_POSITION, targetPosition); } catch (error) { console.error(`${index_1.NAME.PREFIX}_switchCamera failed, error: ${error}.`); @@ -541,16 +556,19 @@ class TUICallService { const [userInfo] = remoteUserInfoList.filter((userInfo) => userInfo.userId === sponsor); remoteUserInfoList.length > 0 && TUIStore.updateStore({ [index_1.NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, + [index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList, [index_1.NAME.CALLER_USER_INFO]: { userId: sponsor, nick: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.nick) || '', avatar: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.avatar) || '', + displayUserInfo: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.remark) || (userInfo === null || userInfo === void 0 ? void 0 : userInfo.nick) || sponsor, }, }, index_1.StoreName.CALL); }); } _handleUserAccept(event) { this._callerChangeToConnected(); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.TOAST_INFO, (0, index_2.t)('answered')); console.log(`${index_1.NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`); } _handleUserEnter(event) { @@ -562,7 +580,10 @@ class TUICallService { const isInRemoteUserList = remoteUserInfoList.find(item => (item === null || item === void 0 ? void 0 : item.userId) === userId); if (!isInRemoteUserList) { remoteUserInfoList.push({ userId }); - remoteUserInfoList.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + if (remoteUserInfoList.length > 0) { + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); + } const [userInfo] = yield (0, utils_1.getRemoteUserProfile)([userId], this.getTim(), TUIStore); remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST); remoteUserInfoList.forEach((obj) => { @@ -576,7 +597,10 @@ class TUICallService { obj.isEnter = true; return obj; }); - remoteUserInfoList.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + if (remoteUserInfoList.length > 0) { + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); + } console.log(`${index_1.NAME.PREFIX}userEnter event data: ${JSON.stringify(event)}.`); }); } @@ -660,8 +684,10 @@ class TUICallService { _handleSDKReady(event) { return __awaiter(this, void 0, void 0, function* () { let localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); - localUserInfo = yield (0, utils_1.getMyProfile)(localUserInfo.userId, this.getTim(), TUIStore); // 方法里已经 try...catch 了 + localUserInfo = yield (0, utils_1.getMyProfile)(localUserInfo.userId, this.getTim(), TUIStore); + this._defaultOfflinePushInfo.title = localUserInfo === null || localUserInfo === void 0 ? void 0 : localUserInfo.displayUserInfo; TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, localUserInfo); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo); }); } _handleKickedOut(event) { @@ -757,15 +783,18 @@ class TUICallService { localUserInfo = Object.assign(Object.assign({}, localUserInfo), { isVideoAvailable: isAvailable }); } TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, localUserInfo); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo); } _updateCallStoreBeforeCall(type, remoteUserInfoList, groupID) { return __awaiter(this, void 0, void 0, function* () { + const callTips = groupID || TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.IS_MINIMIZED) ? index_2.CallTips.CALLER_GROUP_CALLING_MSG : index_2.CallTips.CALLER_CALLING_MSG; let updateStoreParams = { [index_1.NAME.CALL_MEDIA_TYPE]: type, [index_1.NAME.CALL_ROLE]: index_1.CallRole.CALLER, [index_1.NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, + [index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList, [index_1.NAME.IS_GROUP]: !!groupID, - [index_1.NAME.CALL_TIPS]: (0, index_2.t)(index_2.CallTips.CALLER_CALLING_MSG), + [index_1.NAME.CALL_TIPS]: (0, index_2.t)(callTips), [index_1.NAME.GROUP_ID]: groupID }; const pusher = { enableCamera: type === index_1.CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风 @@ -775,7 +804,10 @@ class TUICallService { console.log(`${index_1.NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS, callStatus); const remoteUserInfoLists = yield (0, utils_1.getRemoteUserProfile)(remoteUserInfoList.map(obj => obj.userId), this.getTim(), TUIStore); - remoteUserInfoLists.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists); + if (remoteUserInfoLists.length > 0) { + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoLists); + } }); } _updateCallStoreAfterCall(userIdList, response) { @@ -792,6 +824,7 @@ class TUICallService { TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS, index_1.CallStatus.CALLING); // 小程序未授权时, 此时状态为 idle; web 直接设置为 calling const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true })); this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO); // web && mini, default open audio } else { @@ -800,9 +833,13 @@ class TUICallService { }); } _resetCurrentDevice() { - // 挂断后,重置当前摄像头和麦克风为默认设备 - const { cameraList, microphoneList } = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST); - TUIStore.update(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST, { microphoneList, cameraList, currentCamera: (cameraList === null || cameraList === void 0 ? void 0 : cameraList[0]) || {}, currentMicrophone: (microphoneList === null || microphoneList === void 0 ? void 0 : microphoneList[0]) || {} }); + // 挂断后,重置当前摄像头,麦克风和扬声器为默认设备 + const { cameraList, microphoneList, speakerList } = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST, { microphoneList, cameraList, speakerList, + currentCamera: (cameraList === null || cameraList === void 0 ? void 0 : cameraList[0]) || {}, + currentMicrophone: (microphoneList === null || microphoneList === void 0 ? void 0 : microphoneList[0]) || {}, + currentSpeaker: (speakerList === null || speakerList === void 0 ? void 0 : speakerList[0]) || {} + }); } _resetCallStore() { const oldStatusStr = (0, utils_1.generateStatusChangeText)(TUIStore); @@ -820,7 +857,8 @@ class TUICallService { case index_1.NAME.DISPLAY_MODE: case index_1.NAME.VIDEO_RESOLUTION: case index_1.NAME.ENABLE_FLOAT_WINDOW: - case index_1.NAME.LOCAL_USER_INFO: { + case index_1.NAME.LOCAL_USER_INFO: + case index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN: { return false; } default: { @@ -835,6 +873,9 @@ class TUICallService { TUIStore.reset(index_1.StoreName.CALL, [index_1.NAME.IS_MINIMIZED], true); // isMinimized reset need notify TUIStore.reset(index_1.StoreName.CALL, [index_1.NAME.IS_EAR_PHONE], true); // isEarPhone reset need notify TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO)), { isVideoAvailable: false, isAudioAvailable: false })); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN)), { isVideoAvailable: false, isAudioAvailable: false })); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, []); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, []); this._resetCurrentDevice(); const newStatusStr = (0, utils_1.generateStatusChangeText)(TUIStore); if (oldStatusStr !== newStatusStr) { @@ -857,13 +898,13 @@ class TUICallService { // 通话时长更新 _startTimer() { if (this._timerId === -1) { + this._startTimeStamp = (0, common_utils_1.performanceNow)(); this._timerId = timer_1.default.run(index_1.NAME.TIMEOUT, this._updateCallDuration.bind(this), { delay: 1000 }); } } _updateCallDuration() { - let callDurationStr = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_DURATION); - const callDurationNum = (0, common_utils_1.formatTimeInverse)(callDurationStr); - callDurationStr = (0, common_utils_1.formatTime)(callDurationNum + 1); + const callDurationNum = Math.round(((0, common_utils_1.performanceNow)() - this._startTimeStamp) / 1000); // miniProgram stop timer when background + const callDurationStr = (0, common_utils_1.formatTime)(callDurationNum); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_DURATION, callDurationStr); } _stopTimer() { @@ -880,6 +921,7 @@ class TUICallService { remoteUserInfoList = remoteUserInfoList.filter((obj) => obj.userId !== userId); }); TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); } _analyzeEventData(event) { return (event === null || event === void 0 ? void 0 : event.data) || {}; // mini INVITED diff --git a/TUICallKit/TUICallService/CallService/index.ts b/TUICallKit/TUICallService/CallService/index.ts new file mode 100644 index 0000000..cbaa8cd --- /dev/null +++ b/TUICallKit/TUICallService/CallService/index.ts @@ -0,0 +1,991 @@ +import { TUICore, TUILogin, TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core'; +import { + ITUICallService, + ICallParams, + IGroupCallParams, + IUserInfo, + ICallbackParam, + ISelfInfoParams, + IBellParams, + IInviteUserParams, + IJoinInGroupCallParams, + IInitParams, +} from '../interface/ICallService'; +import { + StoreName, + CallStatus, + CallMediaType, + NAME, + CALL_DATA_KEY, + LanguageType, + CallRole, + LOG_LEVEL, + VideoDisplayMode, + VideoResolution, + StatusChange, + AudioCallIcon, + VideoCallIcon, + ErrorCode, + ErrorMessage, + AudioPlayBackDevice, + CameraPosition, +} from '../const/index'; +// @ts-ignore +import { TUICallEngine, EVENT as TUICallEvent } from 'tuicall-engine-wx'; +import { CallTips, t } from '../locales/index'; +import { initAndCheckRunEnv, beforeCall, handlePackageError } from './miniProgram'; +import { BellContext } from './bellContext'; +import { VALIDATE_PARAMS, avoidRepeatedCall, paramValidate } from '../utils/validate/index'; +import { handleRepeatedCallError, handleNoDevicePermissionError, formatTime, performanceNow } from '../utils/common-utils'; +import { getMyProfile, getRemoteUserProfile, generateText, generateStatusChangeText, getGroupMemberList, getGroupProfile } from './utils'; +import timer from '../utils/timer'; +import { ITUIGlobal } from '../interface/ITUIGlobal'; +import { ITUIStore } from '../interface/ITUIStore'; +import TuiGlobal from '../TUIGlobal/tuiGlobal'; +import TuiStore from '../TUIStore/tuiStore'; +const TUIGlobal: ITUIGlobal = TuiGlobal.getInstance(); +const TUIStore: ITUIStore = TuiStore.getInstance(); +const version = '2.1.1'; +export { TUIGlobal, TUIStore }; + +export default class TUICallService implements ITUICallService { + static instance: TUICallService; + public _tuiCallEngine: any; + private _tim: any = null; + private _TUICore: any = null; + private _timerId: number = -1; + private _startTimeStamp: number = performanceNow(); + private _bellContext: any = null; + private _defaultOfflinePushInfo = { + title: '', + description: t('you have a new call'), + }; + constructor() { + console.log(`${NAME.PREFIX}version: ${version}`); + this._watchTUIStore(); + this._bellContext = new BellContext(); + // 下面:TUICore注册事件,注册组件服务,注册界面拓展 + TUICore.registerEvent(TUIConstants.TUILogin.EVENT.LOGIN_STATE_CHANGED, TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGIN_SUCCESS, this); + TUICore.registerService(TUIConstants.TUICalling.SERVICE.NAME, this); + TUICore.registerExtension(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, this); + } + static getInstance() { + if (!TUICallService.instance) { + TUICallService.instance = new TUICallService(); + } + return TUICallService.instance; + } + @avoidRepeatedCall() + @paramValidate(VALIDATE_PARAMS.init) + public async init(params: IInitParams) { + try { + if (this._tuiCallEngine) return; + // @ts-ignore + let { userID, tim, userSig, sdkAppID, SDKAppID, isFromChat } = params; + if (this._TUICore) { + sdkAppID = this._TUICore.SDKAppID; + tim = this._TUICore.tim; + } + this._tim = tim; + console.log(`${NAME.PREFIX}init sdkAppId: ${sdkAppID || SDKAppID}, userId: ${userID}`); + this._tuiCallEngine = TUICallEngine.createInstance({ + tim, + // @ts-ignore + sdkAppID: sdkAppID || SDKAppID, // 兼容传入 SDKAppID 的问题 + callkitVersion: version, + chat: isFromChat || false, + }); + this._addListenTuiCallEngineEvent(); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { userId: userID }); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { userId: userID }); + await this._tuiCallEngine.login({ userID, userSig, assetsPath: '' }); // web && mini + } catch (error) { + console.error(`${NAME.PREFIX}init failed, error: ${error}.`); + throw error; + } + } + // component destroy + public async destroyed() { + try { + const currentCallStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); + if (currentCallStatus !== CallStatus.IDLE) { + throw new Error(`please destroyed when status is idle, current status: ${currentCallStatus}`); + } + if (this._tuiCallEngine) { + this._removeListenTuiCallEngineEvent(); + await this._tuiCallEngine.destroyInstance(); + this._tuiCallEngine = null; + } + } catch (error) { + console.error(`${NAME.PREFIX}destroyed failed, error: ${error}.`); + throw error; + } + } + // ===============================【通话操作】=============================== + @avoidRepeatedCall() + @paramValidate(VALIDATE_PARAMS.call) + public async call(callParams: ICallParams) { + try { + const { type, userID, offlinePushInfo } = callParams; + if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; + await this._updateCallStoreBeforeCall(type, [{ userId: userID }]); + this._executeExternalBeforeCalling(); // 执行外部传入的 beforeCall 方法 + callParams.offlinePushInfo = { ...this._defaultOfflinePushInfo, ...offlinePushInfo }; + const response = await this._tuiCallEngine.call(callParams); + await this._updateCallStoreAfterCall([userID], response); + } catch (error: any) { + this._handleCallError(error, 'call'); + } + }; + @avoidRepeatedCall() + @paramValidate(VALIDATE_PARAMS.groupCall) + public async groupCall(groupCallParams: IGroupCallParams) { + try { + const { userIDList, type, groupID, offlinePushInfo } = groupCallParams; + if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; + const remoteUserInfoList = userIDList.map(userId => ({ userId })); + await this._updateCallStoreBeforeCall(type, remoteUserInfoList, groupID); + this._executeExternalBeforeCalling(); + groupCallParams.offlinePushInfo = { ...this._defaultOfflinePushInfo, ...offlinePushInfo }; + const response = await this._tuiCallEngine.groupCall(groupCallParams); + await this._updateCallStoreAfterCall(userIDList, response); + } catch (error: any) { + this._handleCallError(error, 'groupCall'); + } + } + @avoidRepeatedCall() + @paramValidate(VALIDATE_PARAMS.inviteUser) + public async inviteUser(params: IInviteUserParams) { + try { + const { userIDList } = params; + let inviteUserInfoList = await getRemoteUserProfile(userIDList, this.getTim(), TUIStore); + const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, [...remoteUserInfoList, ...inviteUserInfoList]); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, [...remoteUserInfoList, ...inviteUserInfoList]); + this._tuiCallEngine && await this._tuiCallEngine.inviteUser(params); + } catch (error: any) { + console.error(`${NAME.PREFIX}inviteUser failed, error: ${error}.`); + this._resetCallStore(); + throw error; + } + } + @avoidRepeatedCall() + @paramValidate(VALIDATE_PARAMS.joinInGroupCall) + public async joinInGroupCall(params: IJoinInGroupCallParams) { + try { + const updateStoreParams = { + [NAME.CALL_ROLE]: CallRole.CALLEE, + [NAME.IS_GROUP]: true, + [NAME.CALL_STATUS]: CallStatus.CONNECTED, + [NAME.CALL_MEDIA_TYPE]: params.type, + [NAME.GROUP_ID]: params.groupID, + [NAME.ROOM_ID]: params.roomID, + }; + TUIStore.updateStore(updateStoreParams, StoreName.CALL); + const response = await this._tuiCallEngine.joinInGroupCall(params); + (params.type === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO); + TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); + this._startTimer(); + TUIStore.update(StoreName.CALL, NAME.PUSHER, response); + this.setSoundMode(params.type === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER); + const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true }); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true }); + this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); + } catch (error) { + console.error(`${NAME.PREFIX}joinInGroupCall failed, error: ${error}.`); + this._resetCallStore(); + throw error; + } + } + // ===============================【其它对外接口】=============================== + public getTUICallEngineInstance(): any { + return this?._tuiCallEngine || null; + } + public setLogLevel(level: LOG_LEVEL) { + this?._tuiCallEngine?.setLogLevel(level); + } + @paramValidate(VALIDATE_PARAMS.setLanguage) + public setLanguage(language: LanguageType) { + if (language && Object.values(LanguageType).includes(language)) { + TUIStore.update(StoreName.CALL, NAME.LANGUAGE, language); + } + } + @paramValidate(VALIDATE_PARAMS.enableFloatWindow) + public enableFloatWindow(enable: boolean) { + TUIStore.update(StoreName.CALL, NAME.ENABLE_FLOAT_WINDOW, enable); + } + @paramValidate(VALIDATE_PARAMS.setSelfInfo) + public async setSelfInfo(params: ISelfInfoParams) { + const { nickName, avatar } = params; + try { + await this._tuiCallEngine.setSelfInfo(nickName, avatar); + } catch (error) { + console.error(`${NAME.PREFIX}setSelfInfo failed, error: ${error}.`); + } + } + // 修改默认铃声:只支持本地铃声文件,不支持在线铃声文件;修改铃声修改的是被叫的铃声 + @paramValidate(VALIDATE_PARAMS.setCallingBell) + public async setCallingBell(filePath?: string) { + let isCheckFileExist: boolean = true; + if (!isCheckFileExist) { + console.warn(`${NAME.PREFIX}setCallingBell failed, filePath: ${filePath}.`); + return ; + } + const bellParams: IBellParams = { calleeBellFilePath: filePath }; + this._bellContext.setBellProperties(bellParams); + } + @paramValidate(VALIDATE_PARAMS.enableMuteMode) + public async enableMuteMode(enable: boolean) { + try { + const bellParams: IBellParams = { isMuteBell: enable }; + this._bellContext.setBellProperties(bellParams); + await this._bellContext.setBellMute(enable); + } catch (error) { + console.warn(`${NAME.PREFIX}enableMuteMode failed, error: ${error}.`); + } + } + // =============================【内部按钮操作方法】============================= + @avoidRepeatedCall() + public async accept() { + try { + const response = await this._tuiCallEngine.accept(); + if (response) { + // 小程序接通时会进行授权弹框, 状态需要放在 accept 后, 否则先接通后再拉起权限设置 + TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED); + this._callTUIService({ message: response?.data?.message }); + TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); + this._startTimer(); + const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); + (callMediaType === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO); + response.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, response.pusher); + this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER); + const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true }); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true }); + this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini default open audio + } + } catch (error) { + if (handleRepeatedCallError(error)) return; + this._noDevicePermissionToast(error, CallMediaType.AUDIO); + this._resetCallStore(); + } + } + @avoidRepeatedCall() + public async hangup() { + try { + const response = await this._tuiCallEngine.hangup(); + response?.forEach((item) => { + if (item?.code === 0) { + this._callTUIService({ message: item?.data?.message }); + } + }); + } catch (error) { + console.debug(error); + } + this._resetCallStore(); + } + @avoidRepeatedCall() + public async reject() { + try { + const response = await this._tuiCallEngine.reject(); + if (response?.code === 0) { + this._callTUIService({ message: response?.data?.message }); + } + } catch (error) { + console.debug(error); + } + this._resetCallStore(); + } + @avoidRepeatedCall() + public async openCamera(videoViewDomID: string) { + try { + await this._tuiCallEngine.openCamera(); + this._setLocalUserInfoAudioVideoAvailable(true, NAME.VIDEO); + } catch (error: any) { + this._noDevicePermissionToast(error, CallMediaType.VIDEO); + console.error(`${NAME.PREFIX}openCamera error: ${error}.`); + } + } + @avoidRepeatedCall() + public async closeCamera() { + try { + await this._tuiCallEngine.closeCamera(); + this._setLocalUserInfoAudioVideoAvailable(false, NAME.VIDEO); + } catch (error: any) { + console.error(`${NAME.PREFIX}closeCamera error: ${error}.`); + } + } + @avoidRepeatedCall() + public async openMicrophone() { + try { + await this._tuiCallEngine.openMicrophone(); + this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); + } catch (error: any) { + console.error(`${NAME.PREFIX}openMicrophone failed, error: ${error}.`); + } + } + @avoidRepeatedCall() + public async closeMicrophone() { + try { + await this._tuiCallEngine.closeMicrophone(); + this._setLocalUserInfoAudioVideoAvailable(false, NAME.AUDIO); + } catch (error: any) { + console.error(`${NAME.PREFIX}closeMicrophone failed, error: ${error}.`); + } + } + @avoidRepeatedCall() + public switchScreen(userId: string) { + if(!userId) return; + TUIStore.update(StoreName.CALL, NAME.BIG_SCREEN_USER_ID, userId); + } + // support video to audio; not support audio to video + @avoidRepeatedCall() + public async switchCallMediaType() { + try { + const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); + if (callMediaType === CallMediaType.AUDIO) { + console.warn(`${NAME.PREFIX}switchCallMediaType failed, ${callMediaType} not support.`); + return; + } + const response = await this._tuiCallEngine.switchCallMediaType(CallMediaType.AUDIO); + if (response?.code === 0) { + this._callTUIService({ message: response?.data?.message }); + } + TUIStore.update(StoreName.CALL, NAME.CALL_MEDIA_TYPE, CallMediaType.AUDIO); + const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); + const oldStatus = isGroup ? StatusChange.CALLING_GROUP_VIDEO : StatusChange.CALLING_C2C_VIDEO; + const newStatus = generateStatusChangeText(TUIStore); + this.statusChanged && this.statusChanged({ oldStatus, newStatus }); + this.setSoundMode(AudioPlayBackDevice.EAR); + } catch (error: any) { + console.error(`${NAME.PREFIX}switchCallMediaType failed, error: ${error}.`); + } + } + @avoidRepeatedCall() + public async switchCamera() { + const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION); + const targetPosition = currentPosition === CameraPosition.BACK ? CameraPosition.FRONT : CameraPosition.BACK; + try { + await this._tuiCallEngine.switchCamera(targetPosition); + TUIStore.update(StoreName.CALL, NAME.CAMERA_POSITION, targetPosition); + } catch (error) { + console.error(`${NAME.PREFIX}_switchCamera failed, error: ${error}.`); + } + } + @avoidRepeatedCall() + public setSoundMode(type?: string): void { + try { + let isEarPhone = TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE); + const soundMode = type || (isEarPhone ? AudioPlayBackDevice.SPEAKER : AudioPlayBackDevice.EAR); // UI 层切换时传参数 + this._tuiCallEngine?.selectAudioPlaybackDevice(soundMode); + if (type) { + isEarPhone = type === AudioPlayBackDevice.EAR; + } else { + isEarPhone = !isEarPhone; + } + TUIStore.update(StoreName.CALL, NAME.IS_EAR_PHONE, isEarPhone); + } catch (error) { + console.error(`${NAME.PREFIX}setSoundMode failed, error: ${error}.`); + } + } + // 切前后置 miniProgram, 切扬声器 + public getTim() { + if (this._tim) return this._tim; + if (!this._tuiCallEngine) { + console.warn(`${NAME.PREFIX}getTim warning: _tuiCallEngine Instance is not available.`); + return null; + } + return this._tuiCallEngine?.tim || this._tuiCallEngine?.getTim(); // mini support getTim interface + } + // ==========================【TUICallEngine 事件处理】========================== + private _addListenTuiCallEngineEvent() { + if (!this._tuiCallEngine) { + console.warn(`${NAME.PREFIX}add engine event listener failed, engine is empty.`); + return; + } + this._tuiCallEngine.on(TUICallEvent.ERROR, this._handleError, this); + this._tuiCallEngine.on(TUICallEvent.INVITED, this._handleNewInvitationReceived, this); // 收到邀请事件 + this._tuiCallEngine.on(TUICallEvent.USER_ACCEPT, this._handleUserAccept, this); // 主叫收到被叫接通事件 + this._tuiCallEngine.on(TUICallEvent.USER_ENTER, this._handleUserEnter, this); // 有用户进房事件 + this._tuiCallEngine.on(TUICallEvent.USER_LEAVE, this._handleUserLeave, this); // 有用户离开通话事件 + this._tuiCallEngine.on(TUICallEvent.REJECT, this._handleInviteeReject, this); // 主叫收到被叫的拒绝通话事件 + this._tuiCallEngine.on(TUICallEvent.NO_RESP, this._handleNoResponse, this); // 主叫收到被叫的无应答事件 + this._tuiCallEngine.on(TUICallEvent.LINE_BUSY, this._handleLineBusy, this); // 主叫收到被叫的忙线事件 + this._tuiCallEngine.on(TUICallEvent.CALLING_CANCEL, this._handleCallingCancel, this); // 主被叫在通话未建立时, 收到的取消事件 + this._tuiCallEngine.on(TUICallEvent.SDK_READY, this._handleSDKReady, this); // SDK Ready 回调 + this._tuiCallEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件 + this._tuiCallEngine.on(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this); + this._tuiCallEngine.on(TUICallEvent.CALL_END, this._handleCallingEnd, this); // 主被叫在通话结束时, 收到的通话结束事件 + // @ts-ignore + this._tuiCallEngine.on(TUICallEvent.CALL_MODE, this._handleCallTypeChange, this); + // @ts-ignore + this._tuiCallEngine.on(TUICallEvent.USER_UPDATE, this._handleUserUpdate, this); // mini: user data update + } + private _removeListenTuiCallEngineEvent() { + this._tuiCallEngine.off(TUICallEvent.ERROR, this._handleError); + this._tuiCallEngine.off(TUICallEvent.INVITED, this._handleNewInvitationReceived); + this._tuiCallEngine.off(TUICallEvent.USER_ACCEPT, this._handleUserAccept); + this._tuiCallEngine.off(TUICallEvent.USER_ENTER, this._handleUserEnter); + this._tuiCallEngine.off(TUICallEvent.USER_LEAVE, this._handleUserLeave); + this._tuiCallEngine.off(TUICallEvent.REJECT, this._handleInviteeReject); + this._tuiCallEngine.off(TUICallEvent.NO_RESP, this._handleNoResponse); + this._tuiCallEngine.off(TUICallEvent.LINE_BUSY, this._handleLineBusy); + this._tuiCallEngine.off(TUICallEvent.CALLING_CANCEL, this._handleCallingCancel); + this._tuiCallEngine.off(TUICallEvent.SDK_READY, this._handleSDKReady); + this._tuiCallEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut); + this._tuiCallEngine.off(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe); + this._tuiCallEngine.off(TUICallEvent.CALL_END, this._handleCallingEnd); + // @ts-ignore + this._tuiCallEngine.off(TUICallEvent.CALL_MODE, this._handleCallTypeChange); // 切换通话事件 miniProgram CALL_MODE + // @ts-ignore + this._tuiCallEngine.off(TUICallEvent.USER_UPDATE, this._handleUserUpdate); // mini: user data update + } + private _handleError(event: any): void { + const { code, message } = event || {}; + const index = Object.values(ErrorCode).indexOf(code); + let callTips = ''; + if (index !== -1) { + const key = Object.keys(ErrorCode)[index]; + callTips = t(ErrorMessage[key]); + callTips && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text: callTips, type: NAME.ERROR }); + } + this._executeExternalAfterCalling(); + console.error(`${NAME.PREFIX}_handleError, errorCode: ${code}; errorMessage: ${callTips || message}.`); + } + private async _handleNewInvitationReceived(event: any) { + console.log(`${NAME.PREFIX}onCallReceived event data: ${JSON.stringify(event)}.`); + const { sponsor = '', isFromGroup, callMediaType, inviteData = {}, calleeIdList = [], groupID = '' } = this._analyzeEventData(event); + const currentUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + const remoteUserIdList: string[] = [sponsor, ...calleeIdList.filter((userId: string) => userId !== currentUserInfo.userId)]; + const type = callMediaType || inviteData.callType; + const callTipsKey = type === CallMediaType.AUDIO ? CallTips.CALLEE_CALLING_AUDIO_MSG : CallTips.CALLEE_CALLING_VIDEO_MSG; + let updateStoreParams = { + [NAME.CALL_ROLE]: CallRole.CALLEE, + [NAME.IS_GROUP]: isFromGroup, + [NAME.CALL_STATUS]: CallStatus.CALLING, + [NAME.CALL_MEDIA_TYPE]: type, + [NAME.CALL_TIPS]: t(callTipsKey), + [NAME.CALLER_USER_INFO]: { userId: sponsor }, + [NAME.GROUP_ID]: groupID, + }; + initAndCheckRunEnv(); + const pusher = { enableCamera: type === CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风 + updateStoreParams = { ...updateStoreParams, [NAME.PUSHER]: pusher }; + TUIStore.updateStore(updateStoreParams, StoreName.CALL); + this._executeExternalBeforeCalling(); + this.statusChanged && this.statusChanged({ oldStatus: StatusChange.IDLE, newStatus: StatusChange.BE_INVITED }); + + const remoteUserInfoList = await getRemoteUserProfile(remoteUserIdList, this.getTim(), TUIStore); + const [userInfo] = remoteUserInfoList.filter((userInfo: IUserInfo) => userInfo.userId === sponsor); + remoteUserInfoList.length > 0 && TUIStore.updateStore({ + [NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, + [NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList, + [NAME.CALLER_USER_INFO]: { + userId: sponsor, + nick: userInfo?.nick || '', + avatar: userInfo?.avatar || '', + displayUserInfo: userInfo?.remark || userInfo?.nick || sponsor, + }, + }, StoreName.CALL); + } + private _handleUserAccept(event: any): void { + this._callerChangeToConnected(); + TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, t('answered')); + console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`); + } + private async _handleUserEnter(event: any): Promise { + this._callerChangeToConnected(); + const { userID: userId, data } = this._analyzeEventData(event); + data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList); + let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + const isInRemoteUserList = remoteUserInfoList.find(item => item?.userId === userId); + if (!isInRemoteUserList) { + remoteUserInfoList.push({ userId }); + + if (remoteUserInfoList.length > 0) { + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); + } + const [userInfo] = await getRemoteUserProfile([userId], this.getTim(), TUIStore); + remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + remoteUserInfoList.forEach((obj) => { + if (obj?.userId === userId) { + obj = Object.assign(obj, userInfo); + } + }); + } + remoteUserInfoList = remoteUserInfoList.map((obj: IUserInfo) => { + if (obj.userId === userId) obj.isEnter = true; + return obj; + }); + + if (remoteUserInfoList.length > 0) { + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); + } + console.log(`${NAME.PREFIX}userEnter event data: ${JSON.stringify(event)}.`); + } + private _callerChangeToConnected() { + const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE); + const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); + if (callStatus === CallStatus.CALLING && callRole === CallRole.CALLER) { + TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED); + this._startTimer(); + } + } + private _handleUserLeave(event: any): void { + console.log(`${NAME.PREFIX}userLeave event data: ${JSON.stringify(event)}.`); + const { data, userID: userId } = this._analyzeEventData(event); + data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList); + if (TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)) { + const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + const prefix: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId; + const text = generateText(TUIStore, CallTips.END_CALL, prefix); + TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text }); + } + userId && this._deleteRemoteUser([userId]); + } + private _unNormalEventsManager(event: any, eventName: TUICallEvent): void { + console.log(`${NAME.PREFIX}${eventName} event data: ${JSON.stringify(event)}.`); + const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); + const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + switch (eventName) { + case TUICallEvent.REJECT: + case TUICallEvent.LINE_BUSY: { + const { userID: userId } = this._analyzeEventData(event); + let callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.OTHER_SIDE_REJECT_CALL : CallTips.OTHER_SIDE_LINE_BUSY; + let text = generateText(TUIStore, callTipsKey); + if (isGroup) { + const prefix: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId; + callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.REJECT_CALL : CallTips.IN_BUSY; + text = generateText(TUIStore, callTipsKey, prefix); + } + TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text }); + userId && this._deleteRemoteUser([userId]); + break; + } + case TUICallEvent.NO_RESP: { + const { userIDList = [] } = this._analyzeEventData(event); + const callTipsKey = isGroup ? CallTips.TIMEOUT : CallTips.CALL_TIMEOUT; + const userInfoList: string[] = userIDList.map(userId => { + const userInfo: IUserInfo = remoteUserInfoList.find(obj => obj.userId === userId) || {}; + return userInfo.displayUserInfo || userId; + }); + const text = isGroup ? generateText(TUIStore, callTipsKey, userInfoList.join()) : generateText(TUIStore, callTipsKey); + TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text }); + userIDList.length > 0 && this._deleteRemoteUser(userIDList); + break; + } + case TUICallEvent.CALLING_CANCEL: { + // TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text: generateText(TUIStore, CallTips.CANCEL) }); + this._resetCallStore(); + break; + } + } + } + private _handleInviteeReject(event: any): void { + this._unNormalEventsManager(event, TUICallEvent.REJECT); + } + private _handleNoResponse(event: any): void { + this._unNormalEventsManager(event, TUICallEvent.NO_RESP); + } + private _handleLineBusy(event: any): void { + this._unNormalEventsManager(event, TUICallEvent.LINE_BUSY); + } + private _handleCallingCancel(event: any): void { + this._executeExternalAfterCalling(); + this._unNormalEventsManager(event, TUICallEvent.CALLING_CANCEL); + } + private _handleCallingEnd(event: any): void { + console.log(`${NAME.PREFIX}callEnd event data: ${JSON.stringify(event)}.`); + this._executeExternalAfterCalling(); + this._resetCallStore(); + } + // SDK_READY 后才能调用 tim 接口, 否则登录后立刻获取导致调用接口失败. v2.27.4+、v3 接口 login 后会抛出 SDK_READY + private async _handleSDKReady(event: any): Promise { + let localUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + localUserInfo = await getMyProfile(localUserInfo.userId, this.getTim(), TUIStore); + this._defaultOfflinePushInfo.title = localUserInfo?.displayUserInfo; + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, localUserInfo); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo); + } + private _handleKickedOut(event: any): void { + console.log(`${NAME.PREFIX}kickOut event data: ${JSON.stringify(event)}.`); + this.kickedOut && this.kickedOut(event); + TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, generateText(TUIStore, CallTips.KICK_OUT)); + this._resetCallStore(); + } + private _handleCallTypeChange(event: any): void { + const { newCallType, type } = this._analyzeEventData(event); + TUIStore.update(StoreName.CALL, NAME.CALL_MEDIA_TYPE, newCallType || type); + this.setSoundMode(AudioPlayBackDevice.EAR); + } + private _messageSentByMe(event: any): void { + const message = event?.data; + this.onMessageSentByMe && this.onMessageSentByMe(message); + } + // ==========================【 miniProgram 私有事件】========================== + private _handleUserUpdate(event: any): void { + const data = this._analyzeEventData(event); + data?.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, data.pusher); + data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList); + } + // 处理 “呼叫” 抛出的异常 + private _handleCallError(error: any, methodName?: string) { + if (handleRepeatedCallError(error)) return; + handlePackageError(error); // 无套餐提示, 小程序 engine 不抛出 onError + this._noDevicePermissionToast(error, CallMediaType.AUDIO); + console.error(`${NAME.PREFIX}${methodName} failed, error: ${error}.`); + this._resetCallStore(); + throw error; + } + // ========================【原 Web CallKit 提供的方法】======================== + public beforeCalling: ((...args: any[]) => void) | undefined; // 原来 + public afterCalling: ((...args: any[]) => void) | undefined; + public onMinimized: ((...args: any[]) => void) | undefined; + public onMessageSentByMe: ((...args: any[]) => void) | undefined; + public kickedOut: ((...args: any[]) => void) | undefined; + public statusChanged: ((...args: any[]) => void) | undefined; + public setCallback(params: ICallbackParam) { + const { beforeCalling, afterCalling, onMinimized, onMessageSentByMe, kickedOut, statusChanged } = params; + beforeCalling && (this.beforeCalling = beforeCalling); + afterCalling && (this.afterCalling = afterCalling); + onMinimized && (this.onMinimized = onMinimized); + onMessageSentByMe && (this.onMessageSentByMe = onMessageSentByMe); + kickedOut && (this.kickedOut = kickedOut); + statusChanged && (this.statusChanged = statusChanged); + } + + public toggleMinimize() { + const isMinimized = TUIStore.getData(StoreName.CALL, NAME.IS_MINIMIZED); + TUIStore.update(StoreName.CALL, NAME.IS_MINIMIZED, !isMinimized); + console.log(`${NAME.PREFIX}toggleMinimize: ${isMinimized} -> ${!isMinimized}.`); + this.onMinimized && this.onMinimized(isMinimized, !isMinimized); + } + private _executeExternalBeforeCalling(): void { + this.beforeCalling && this.beforeCalling(); + } + private _executeExternalAfterCalling(): void { + this.afterCalling && this.afterCalling(); + } + // ========================【TUICallKit 组件属性设置方法】======================== + @paramValidate(VALIDATE_PARAMS.setVideoDisplayMode) + public setVideoDisplayMode(displayMode: VideoDisplayMode) { + TUIStore.update(StoreName.CALL, NAME.DISPLAY_MODE, displayMode); + } + @paramValidate(VALIDATE_PARAMS.setVideoResolution) + public async setVideoResolution(resolution: VideoResolution) { + try { + if (!resolution) return; + TUIStore.update(StoreName.CALL, NAME.VIDEO_RESOLUTION, resolution); + await this._tuiCallEngine.setVideoQuality(resolution); + } catch (error) { + console.warn(`${NAME.PREFIX}setVideoResolution failed, error: ${error}.`); + } + } + // =========================【 miniProgram 私有公共方法】========================= + // 处理用户异常退出的情况,处理了右滑退出,以及返回退出的情况。 + private async _handleExceptionExit() { + const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); + const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE); + // 在呼叫状态下, 被叫调用 reject,主叫调用 hangup + if (callStatus === CallStatus.CALLING) { + callRole === CallRole.CALLER && await this.hangup(); + callRole === CallRole.CALLEE && await this.reject(); + } + // 在通话状态下, 统一调用 hangup 接口 + callStatus === CallStatus.CONNECTED && await this.hangup(); + } + private _setLocalUserInfoAudioVideoAvailable(isAvailable: boolean, type: string) { + let localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + if (type === NAME.AUDIO) { + localUserInfo = { ...localUserInfo, isAudioAvailable: isAvailable }; + } + if (type === NAME.VIDEO) { + localUserInfo = { ...localUserInfo, isVideoAvailable: isAvailable }; + } + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, localUserInfo); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo); + } + private async _updateCallStoreBeforeCall(type: number, remoteUserInfoList: IUserInfo[], groupID?: string): Promise { + const callTips = groupID || TUIStore.getData(StoreName.CALL, NAME.IS_MINIMIZED) ? CallTips.CALLER_GROUP_CALLING_MSG : CallTips.CALLER_CALLING_MSG; + let updateStoreParams: any = { + [NAME.CALL_MEDIA_TYPE]: type, + [NAME.CALL_ROLE]: CallRole.CALLER, + [NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, + [NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList, + [NAME.IS_GROUP]: !!groupID, + [NAME.CALL_TIPS]: t(callTips), + [NAME.GROUP_ID]: groupID + }; + const pusher = { enableCamera: type === CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风 + updateStoreParams = { ...updateStoreParams, [NAME.PUSHER]: pusher }; + TUIStore.updateStore(updateStoreParams, StoreName.CALL); + const callStatus = await beforeCall(type, this); // 如果没有权限, 此时为 false. 因此需要在 call 后设置为 calling. 和 web 存在差异 + console.log(`${NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`); + TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, callStatus); + const remoteUserInfoLists = await getRemoteUserProfile(remoteUserInfoList.map(obj => obj.userId), this.getTim(), TUIStore); + + if (remoteUserInfoLists.length > 0) { + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoLists); + } + } + private async _updateCallStoreAfterCall(userIdList: string[], response: any) { + if (response) { + TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); + TUIStore.update(StoreName.CALL, NAME.ROOM_ID, response.roomID); + this._callTUIService({ message: response?.data?.message }); + response.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, response.pusher); + const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); + (callMediaType === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO); + this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER); + TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CALLING); // 小程序未授权时, 此时状态为 idle; web 直接设置为 calling + const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { ...localUserInfo, isEnter: true }); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { ...localUserInfo, isEnter: true }); + this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini, default open audio + } else { + this._resetCallStore(); + } + } + private _resetCurrentDevice() { + // 挂断后,重置当前摄像头,麦克风和扬声器为默认设备 + const { cameraList, microphoneList, speakerList } = TUIStore.getData(StoreName.CALL, NAME.DEVICE_LIST); + TUIStore.update( + StoreName.CALL, + NAME.DEVICE_LIST, + { microphoneList, cameraList, speakerList, + currentCamera: cameraList?.[0] || {}, + currentMicrophone: microphoneList?.[0] || {}, + currentSpeaker: speakerList?.[0] || {} + }, + ); + } + private _resetCallStore() { + const oldStatusStr = generateStatusChangeText(TUIStore); + this._stopTimer(); + // localUserInfo, language 在通话结束后不需要清除 + // callStatus 清除需要通知; isMinimized 也需要通知(basic-vue3 中切小窗关闭后, 再呼叫还是小窗, 因此需要通知到组件侧) + // isGroup 也不清除(engine 先抛 cancel 事件, 再抛 reject 事件) + // displayMode、videoResolution 也不能清除, 组件不卸载, 这些属性也需保留, 否则采用默认值. + // enableFloatWindow 不清除:开启/关闭悬浮窗功能。 + let notResetOrNotifyKeys = Object.keys(CALL_DATA_KEY).filter((key) => { + switch (CALL_DATA_KEY[key]) { + case NAME.CALL_STATUS: + case NAME.LANGUAGE: + case NAME.IS_GROUP: + case NAME.DISPLAY_MODE: + case NAME.VIDEO_RESOLUTION: + case NAME.ENABLE_FLOAT_WINDOW: + case NAME.LOCAL_USER_INFO: + case NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN: { + return false; + } + default: { + return true; + } + } + }); + notResetOrNotifyKeys = notResetOrNotifyKeys.map(key => CALL_DATA_KEY[key]); + TUIStore.reset(StoreName.CALL, notResetOrNotifyKeys); + const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); + callStatus !== CallStatus.IDLE && TUIStore.reset(StoreName.CALL, [NAME.CALL_STATUS], true); // callStatus reset need notify + TUIStore.reset(StoreName.CALL, [NAME.IS_MINIMIZED], true); // isMinimized reset need notify + TUIStore.reset(StoreName.CALL, [NAME.IS_EAR_PHONE], true); // isEarPhone reset need notify + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { + ...TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO), + isVideoAvailable: false, + isAudioAvailable: false, + }); + TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { + ...TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN), + isVideoAvailable: false, + isAudioAvailable: false, + }); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, []); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, []); + this._resetCurrentDevice(); + const newStatusStr = generateStatusChangeText(TUIStore); + if (oldStatusStr !== newStatusStr) { + this.statusChanged && this.statusChanged({ oldStatus: oldStatusStr, newStatus: newStatusStr }); + } + } + private _noDevicePermissionToast(error: any, type: CallMediaType): void { + if (handleNoDevicePermissionError(error)) { + let text = ''; + if (type === CallMediaType.AUDIO) { + text = generateText(TUIStore, CallTips.NO_MICROPHONE_DEVICE_PERMISSION); + } + if (type === CallMediaType.VIDEO) { + text = generateText(TUIStore, CallTips.NO_CAMERA_DEVICE_PERMISSION); + } + + + text && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text, type: NAME.ERROR }); + console.error(`${NAME.PREFIX}call failed, error: ${error.message}.`); + } + } + // 通话时长更新 + private _startTimer(): void { + if (this._timerId === -1) { + this._startTimeStamp = performanceNow(); + this._timerId = timer.run(NAME.TIMEOUT, this._updateCallDuration.bind(this), { delay: 1000 }); + } + } + private _updateCallDuration(): void { + const callDurationNum = Math.round((performanceNow() - this._startTimeStamp) / 1000); // miniProgram stop timer when background + const callDurationStr = formatTime(callDurationNum); + TUIStore.update(StoreName.CALL, NAME.CALL_DURATION, callDurationStr); + } + private _stopTimer(): void { + if (this._timerId !== -1) { + timer.clearTask(this._timerId); + this._timerId = -1; + } + } + private _deleteRemoteUser(userIdList: string[]): void { + if (userIdList.length === 0) return; + let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + userIdList.forEach((userId) => { + remoteUserInfoList = remoteUserInfoList.filter((obj: IUserInfo) => obj.userId !== userId); + }); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); + TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); + } + private _analyzeEventData(event: any): any { + return event?.data || {}; // mini INVITED + } + // =========================【调用 chat api】========================= + // 获取群成员 + public async getGroupMemberList(count: number, offset: number) { + const groupID = TUIStore.getData(StoreName.CALL, NAME.GROUP_ID); + let groupMemberList = await getGroupMemberList(groupID, this.getTim(), count, offset); + return groupMemberList; + } + // 获取群信息 + public async getGroupProfile() { + const groupID: string = TUIStore.getData(StoreName.CALL, NAME.GROUP_ID); + return await getGroupProfile(groupID, this.getTim()); + } + // =========================【监听 TUIStore 中的状态】========================= + private _handleCallStatusChange = async (value: CallStatus) => { + try { + const bellParams: IBellParams = { + callRole: TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE), + callStatus: TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS), + }; + this._bellContext.setBellProperties(bellParams); + if (value === CallStatus.CALLING) { + await this?._bellContext?.play(); + } else { + // 状态变更通知 + if (value === CallStatus.CONNECTED) { + const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); + const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); + const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); + const oldStatus = isGroup ? StatusChange.DIALING_GROUP : StatusChange.DIALING_C2C; + TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, ''); + this.statusChanged && this.statusChanged({ oldStatus, newStatus: generateStatusChangeText(TUIStore) }); + if (!isGroup && callMediaType === CallMediaType.VIDEO) { + this.switchScreen(remoteUserInfoList[0].domId); + } + } + await this?._bellContext?.stop(); + } + } catch (error) { + console.warn(`${NAME.PREFIX}handleCallStatusChange, ${error}.`); + } + }; + private _watchTUIStore() { + TUIStore?.watch(StoreName.CALL, { + [NAME.CALL_STATUS]: this._handleCallStatusChange, + }); + } + private _unwatchTUIStore() { + TUIStore?.unwatch(StoreName.CALL, { + [NAME.CALL_STATUS]: this._handleCallStatusChange, + }); + } + // =========================【web 融合 chat 提供的方法】========================= + public bindTUICore(TUICore: any) { + this._TUICore = TUICore; + } + // 下面方法用于接入 TUICore + private _callTUIService(params) { + const { message } = params || {}; + TUICore.callService({ + serviceName: TUIConstants.TUIChat.SERVICE.NAME, + method: TUIConstants.TUIChat.SERVICE.METHOD.UPDATE_MESSAGE_LIST, + params: { message }, + }); + } + public async onNotifyEvent(eventName: string, subKey: string) { + try { + if (eventName === TUIConstants.TUILogin.EVENT.LOGIN_STATE_CHANGED) { + if (subKey === TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGIN_SUCCESS) { + // TUICallkit 收到登录成功时执行自己的业务逻辑处理 + // @ts-ignore + const { chat, userID, userSig, SDKAppID } = TUILogin.getContext(); + await this.init({ tim: chat, userID, userSig, sdkAppID: SDKAppID, isFromChat: true }); + } else if (subKey === TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGOUT_SUCCESS) { + await this.destroyed(); + } + } + } catch (error) { + console.error(`${NAME.PREFIX}TUICore onNotifyEvent failed, error: ${error}.`); + } + } + public async onCall(method: String, params: any) { + if (method === TUIConstants.TUICalling.SERVICE.METHOD.START_CALL) { + await this._handleTUICoreOnClick(params, params.type); + } + } + private async _handleTUICoreOnClick(options, type: CallMediaType) { + try { + const { groupID, userIDList = [], ...rest } = options; + if (groupID) { + await this.groupCall({ groupID, userIDList, type, ...rest }); + } else if (userIDList.length === 1) { + await this.call({ userID: userIDList[0], type, ...rest }); + } + } catch (error: any) { + console.debug(error); + } + } + public onGetExtension(extensionID: string, params: any) { + if (extensionID === TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID) { + const list = []; + const audioCallExtension: ExtensionInfo = { + weight: 1000, + text: '语音通话', + icon: AudioCallIcon, + data: { + name: 'voiceCall', + }, + listener: { + onClicked: async options => await this._handleTUICoreOnClick(options, options.type || CallMediaType.AUDIO), // 点击时发起通话 + }, + }; + const videoCallExtension: ExtensionInfo = { + weight: 900, + text: '视频通话', + icon: VideoCallIcon, + data: { + name: 'videoCall', + }, + listener: { + onClicked: async options => await this._handleTUICoreOnClick(options, options.type || CallMediaType.VIDEO), // 点击时发起通话 + }, + }; + if (!params?.filterVoice) { + list.push(audioCallExtension); + } + if (!params?.filterVideo) { + list.push(videoCallExtension); + } + return list; + } + } +} \ No newline at end of file diff --git a/TUICallKit/TUICallService/CallService/miniProgram.ts b/TUICallKit/TUICallService/CallService/miniProgram.ts new file mode 100644 index 0000000..76466f5 --- /dev/null +++ b/TUICallKit/TUICallService/CallService/miniProgram.ts @@ -0,0 +1,55 @@ +import { CallMediaType, CallStatus } from '../const/index'; + +export function initialUI() { + // 收起键盘 + // @ts-ignore + wx.hideKeyboard({ + complete: () => {}, + }); +}; +// 检测运行时环境, 当是微信开发者工具时, 提示用户需要手机调试 +export function checkRunPlatform() { + // @ts-ignore + const systemInfo = wx.getSystemInfoSync(); + if (systemInfo.platform === 'devtools') { + // 当前运行在微信开发者工具里 + // @ts-ignore + wx.showModal({ + icon: 'none', + title: '运行环境提醒', + content: '微信开发者工具不支持原生推拉流组件(即 标签),请使用真机调试或者扫码预览。', + showCancel: false, + }); + } +}; +export function initAndCheckRunEnv() { + initialUI(); // miniProgram 收起键盘, 隐藏 tabBar + checkRunPlatform(); // miniProgram 检测运行时环境 +} +export async function beforeCall(type: CallMediaType, that: any) { + try { + initAndCheckRunEnv(); + // 检查设备权限 + const deviceMap = { + microphone: true, + camera: type === CallMediaType.VIDEO, + }; + const hasDevicePermission = await that._tuiCallEngine.deviceCheck(deviceMap); // miniProgram 检查设备权限 + return hasDevicePermission ? CallStatus.CALLING : CallStatus.IDLE; + } catch (error) { + console.debug(error); + return CallStatus.IDLE; + } +} +// 套餐问题提示, 小程序最低需要群组通话版, 1v1 通话版本使用 TRTC 就会报错 +export function handlePackageError(error) { + if (error?.code === -1002) { + // @ts-ignore + wx.showModal({ + icon: 'none', + title: 'error', + content: error?.message || '', + showCancel: false, + }); + } +} \ No newline at end of file diff --git a/TUICallKit/TUICallService/CallService/utils.js b/TUICallKit/TUICallService/CallService/utils.js index 0eaed04..d599734 100644 --- a/TUICallKit/TUICallService/CallService/utils.js +++ b/TUICallKit/TUICallService/CallService/utils.js @@ -30,7 +30,7 @@ function setDefaultUserInfo(userId, domId) { exports.setDefaultUserInfo = setDefaultUserInfo; // 获取个人用户信息 function getMyProfile(myselfUserId, tim, TUIStore) { - var _a, _b, _c; + var _a, _b, _c, _d, _e; return __awaiter(this, void 0, void 0, function* () { let localUserInfo = setDefaultUserInfo(myselfUserId, index_1.NAME.LOCAL_VIDEO); try { @@ -39,7 +39,7 @@ function getMyProfile(myselfUserId, tim, TUIStore) { const res = yield tim.getMyProfile(); const currentLocalUserInfo = TUIStore === null || TUIStore === void 0 ? void 0 : TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); // localUserInfo may have been updated if ((res === null || res === void 0 ? void 0 : res.code) === 0) { - localUserInfo = Object.assign(Object.assign(Object.assign({}, localUserInfo), currentLocalUserInfo), { userId: (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.userID, nick: (_b = res === null || res === void 0 ? void 0 : res.data) === null || _b === void 0 ? void 0 : _b.nick, avatar: (_c = res === null || res === void 0 ? void 0 : res.data) === null || _c === void 0 ? void 0 : _c.avatar }); + localUserInfo = Object.assign(Object.assign(Object.assign({}, localUserInfo), currentLocalUserInfo), { userId: (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.userID, nick: (_b = res === null || res === void 0 ? void 0 : res.data) === null || _b === void 0 ? void 0 : _b.nick, avatar: (_c = res === null || res === void 0 ? void 0 : res.data) === null || _c === void 0 ? void 0 : _c.avatar, displayUserInfo: ((_d = res === null || res === void 0 ? void 0 : res.data) === null || _d === void 0 ? void 0 : _d.nick) || ((_e = res === null || res === void 0 ? void 0 : res.data) === null || _e === void 0 ? void 0 : _e.userID) }); } return localUserInfo; } diff --git a/TUICallKit/TUICallService/CallService/utils.ts b/TUICallKit/TUICallService/CallService/utils.ts new file mode 100644 index 0000000..53183cc --- /dev/null +++ b/TUICallKit/TUICallService/CallService/utils.ts @@ -0,0 +1,142 @@ +import { NAME, StoreName, CallStatus, StatusChange, CallMediaType } from '../const/index'; +import { IUserInfo } from '../interface/ICallService'; +import { ITUIStore } from '../interface/ITUIStore'; +import { t } from '../locales/index'; + +// 设置默认的 UserInfo 信息 +export function setDefaultUserInfo(userId: string, domId?: string): IUserInfo { + const userInfo: IUserInfo = { + userId, + nick: '', + avatar: '', + remark: '', + displayUserInfo: '', + isAudioAvailable: false, + isVideoAvailable: false, + isEnter: false, + domId: domId || userId, + }; + return domId ? userInfo : { ...userInfo, isEnter: false }; // localUserInfo 没有 isEnter, remoteUserInfoList 有 isEnter +} +// 获取个人用户信息 +export async function getMyProfile(myselfUserId: string, tim: any, TUIStore: any): Promise { + let localUserInfo: IUserInfo = setDefaultUserInfo(myselfUserId, NAME.LOCAL_VIDEO); + try { + if (!tim) return localUserInfo; + const res = await tim.getMyProfile(); + const currentLocalUserInfo = TUIStore?.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); // localUserInfo may have been updated + if (res?.code === 0) { + localUserInfo = { + ...localUserInfo, + ...currentLocalUserInfo, + userId: res?.data?.userID, + nick: res?.data?.nick, + avatar: res?.data?.avatar, + displayUserInfo: res?.data?.nick || res?.data?.userID, + }; + } + return localUserInfo; + } catch (error) { + console.error(`${NAME.PREFIX}getMyProfile failed, error: ${error}.`); + return localUserInfo; + } +} +// 获取远端用户列表信息 +export async function getRemoteUserProfile(userIdList: Array, tim: any, TUIStore: any): Promise { + let remoteUserInfoList: IUserInfo[] = userIdList.map((userId: string) => setDefaultUserInfo(userId)); + try { + if (!tim) return remoteUserInfoList; + const res = await tim.getFriendProfile({ userIDList: userIdList }); + if (res.code === 0) { + const { friendList = [], failureUserIDList = [] } = res.data; + let unFriendList: IUserInfo[] = failureUserIDList.map((obj: any) => obj.userID); + if (failureUserIDList.length > 0) { + const res = await tim.getUserProfile({ userIDList: failureUserIDList.map((obj: any) => obj.userID) }); + if (res?.code === 0) { + unFriendList = res?.data || []; + } + } + const currentRemoteUserInfoList = TUIStore?.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // remoteUserInfoList may have been updated + const tempFriendIdList: string[] = friendList.map((obj: any) => obj.userID); + const tempUnFriendIdList: string[] = unFriendList.map((obj: any) => obj.userID); + remoteUserInfoList = userIdList.map((userId: string) => { + const defaultUserInfo: IUserInfo = setDefaultUserInfo(userId); + const friendListIndex: number = tempFriendIdList.indexOf(userId); + const unFriendListIndex: number = tempUnFriendIdList.indexOf(userId); + let remark = ''; + let nick = ''; + let displayUserInfo = '' ; + let avatar = ''; + if (friendListIndex !== -1) { + remark = friendList[friendListIndex]?.remark || ''; + nick = friendList[friendListIndex]?.profile?.nick || ''; + displayUserInfo = remark || nick || defaultUserInfo.userId || ''; + avatar = friendList[friendListIndex]?.profile?.avatar || ''; + } + if (unFriendListIndex !== -1) { + nick = unFriendList[unFriendListIndex]?.nick || ''; + displayUserInfo = nick || defaultUserInfo.userId || ''; + avatar = unFriendList[unFriendListIndex]?.avatar || ''; + } + const userInfo = currentRemoteUserInfoList.find(subObj => subObj.userId === userId) || {}; + return { ...defaultUserInfo, ...userInfo, remark, nick, displayUserInfo, avatar }; + }); + } + return remoteUserInfoList; + } catch (error) { + console.error(`${NAME.PREFIX}getRemoteUserProfile failed, error: ${error}.`); + return remoteUserInfoList; + } +} +// 生成弹框提示文案 +export function generateText(TUIStore: ITUIStore, key: string, prefix?: string, suffix?: string): string { + const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); + let callTips = `${t(key)}`; + if (isGroup) { + callTips = prefix ? `${prefix} ${callTips}` : callTips; + callTips = suffix ? `${callTips} ${suffix}` : callTips; + } + return callTips; +} +// 生成 statusChange 抛出的字符串 +export function generateStatusChangeText(TUIStore: ITUIStore): string { + const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); + if (callStatus === CallStatus.IDLE) { + return StatusChange.IDLE; + } + const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); + if (callStatus === CallStatus.CALLING) { + return isGroup ? StatusChange.DIALING_GROUP : StatusChange.DIALING_C2C; + } + const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); + if (isGroup) { + return callMediaType === CallMediaType.AUDIO ? StatusChange.CALLING_GROUP_AUDIO : StatusChange.CALLING_GROUP_VIDEO; + } + return callMediaType === CallMediaType.AUDIO ? StatusChange.CALLING_C2C_AUDIO : StatusChange.CALLING_C2C_VIDEO; +} +// 获取群组[offset, count + offset]区间成员 +export async function getGroupMemberList(groupID: string, tim: any, count, offset) { + let groupMemberList = []; + try { + const res = await tim.getGroupMemberList({ groupID, count, offset }); + if (res.code === 0) { + return res.data.memberList || groupMemberList; + } + } catch(error) { + console.error(`${NAME.PREFIX}getGroupMember failed, error: ${error}.`); + return groupMemberList; + } +} +// 获取 IM 群信息 +export async function getGroupProfile(groupID: string, tim: any): Promise { + let groupProfile = {}; + try { + const res = await tim.getGroupProfile({ groupID }); + if (res.code === 0) { + return res.data.group || groupProfile; + } + } catch(error) { + console.error(`${NAME.PREFIX}getGroupProfile failed, error: ${error}.`); + return groupProfile; + } +} \ No newline at end of file diff --git a/TUICallKit/TUICallService/TUIGlobal/tuiGlobal.ts b/TUICallKit/TUICallService/TUIGlobal/tuiGlobal.ts new file mode 100644 index 0000000..843166c --- /dev/null +++ b/TUICallKit/TUICallService/TUIGlobal/tuiGlobal.ts @@ -0,0 +1,43 @@ +import { APP_NAMESPACE, IS_PC, IS_H5, IN_WX_MINI_APP, IN_UNI_NATIVE_APP, IN_UNI_APP, IS_MAC, IS_WIN } from '../utils/env'; +import { ITUIGlobal } from '../interface/ITUIGlobal'; + +export default class TUIGlobal implements ITUIGlobal { + static instance: TUIGlobal; + public global: any = APP_NAMESPACE; + public isPC: boolean = false; + public isH5: boolean = false; + public isWeChat: boolean = false; + public isApp: boolean = false; + public isUniPlatform: boolean = false; + public isOfficial: boolean = false; + public isWIN: boolean = false; + public isMAC: boolean = false; + constructor() { + this.initEnv(); + } + + /** + * 获取 TUIGlobal 实例 + * @returns {TUIGlobal} + */ + static getInstance() { + if (!TUIGlobal.instance) { + TUIGlobal.instance = new TUIGlobal(); + } + return TUIGlobal.instance; + } + + initEnv() { + this.isPC = IS_PC; + this.isH5 = IS_H5; + this.isWeChat = IN_WX_MINI_APP; + this.isApp = IN_UNI_NATIVE_APP && !IN_WX_MINI_APP; // uniApp 打包小程序时 IN_UNI_NATIVE_APP 为 true,所以此处需要增加条件 + this.isUniPlatform = IN_UNI_APP; + this.isWIN = IS_WIN; + this.isMAC = IS_MAC; + } + + initOfficial(SDKAppID: number) { + this.isOfficial = (SDKAppID === 1400187352 || SDKAppID === 1400188366); + } +} diff --git a/TUICallKit/TUICallService/TUIStore/callStore.js b/TUICallKit/TUICallService/TUIStore/callStore.js index 8ac66ef..cf9bd6c 100644 --- a/TUICallKit/TUICallService/TUIStore/callStore.js +++ b/TUICallKit/TUICallService/TUIStore/callStore.js @@ -9,7 +9,9 @@ class CallStore { callRole: index_1.CallRole.UNKNOWN, callMediaType: index_1.CallMediaType.UNKNOWN, localUserInfo: { userId: '' }, + localUserInfoExcludeVolume: { userId: '' }, remoteUserInfoList: [], + remoteUserInfoExcludeVolumeList: [], callerUserInfo: { userId: '' }, isGroup: false, callDuration: '00:00:00', @@ -24,6 +26,7 @@ class CallStore { showPermissionTip: false, groupID: '', roomID: 0, + cameraPosition: index_1.CameraPosition.FRONT, // TUICallKit 组件上的属性 displayMode: index_1.VideoDisplayMode.COVER, videoResolution: index_1.VideoResolution.RESOLUTION_480P, diff --git a/TUICallKit/TUICallService/TUIStore/callStore.ts b/TUICallKit/TUICallService/TUIStore/callStore.ts new file mode 100644 index 0000000..57dd07d --- /dev/null +++ b/TUICallKit/TUICallService/TUIStore/callStore.ts @@ -0,0 +1,66 @@ +import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, CameraPosition } from '../const/index'; +import { ICallStore } from '../interface/ICallStore'; +import { getLanguage } from '../utils/common-utils'; + +export default class CallStore { + public defaultStore: ICallStore = { + callStatus: CallStatus.IDLE, + callRole: CallRole.UNKNOWN, + callMediaType: CallMediaType.UNKNOWN, + localUserInfo: { userId: '' }, + localUserInfoExcludeVolume: { userId: '' }, + remoteUserInfoList: [], + remoteUserInfoExcludeVolumeList: [], + callerUserInfo: { userId: '' }, + isGroup: false, + callDuration: '00:00:00', // 通话时长 + callTips: '', // 通话提示的信息. 例如: '等待谁接听', 'xxx 拒绝通话', 'xxx 挂断通话' + toastInfo: { text: '' }, // 远端用户挂断、拒绝、超时、忙线等的 toast 提示信息 + isMinimized: false, // 用来记录当前是否悬浮窗模式 + enableFloatWindow: false, // 开启/关闭悬浮窗功能,设置为false,通话界面左上角的悬浮窗按钮会隐藏 + bigScreenUserId: '', // 当前大屏幕显示的 userID 用户 + language: getLanguage(), // en, zh-cn + isClickable: false, // 是否可点击, 用于按钮增加 loading 效果,不可点击 + deviceList: { cameraList: [], microphoneList: [], currentCamera: {}, currentMicrophone: {} }, + showPermissionTip: false, + groupID: '', + roomID: 0, + cameraPosition: CameraPosition.FRONT, // 前置或后置,值为front, back + // TUICallKit 组件上的属性 + displayMode: VideoDisplayMode.COVER, // 设置预览远端的画面显示模式 + videoResolution: VideoResolution.RESOLUTION_480P, + showSelectUser: false, + // 小程序相关属性 + pusher: {}, + player: [], + isEarPhone: false, // 是否是听筒, 默认: false + }; + public store: ICallStore = Object.assign({}, this.defaultStore);; + + public update(key: keyof ICallStore, data: any): void { + switch (key) { + // case 'callTips': + // break; + default: + // resolve "Type 'any' is not assignable to type 'never'.ts", ref: https://github.com/microsoft/TypeScript/issues/31663 + (this.store[key] as any) = data as any; + } + } + + public getData(key: string | undefined): any { + if (!key) return this.store; + return this.store[key as keyof ICallStore]; + } + // reset call store + public reset(keyList: Array = []) { + if (keyList.length === 0) { + keyList = Object.keys(this.store); + } + const resetToDefault = keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key as keyof ICallStore] }), {}); + this.store = { + ...this.defaultStore, + ...this.store, + ...resetToDefault, + }; + } +} diff --git a/TUICallKit/TUICallService/TUIStore/tuiStore.ts b/TUICallKit/TUICallService/TUIStore/tuiStore.ts new file mode 100644 index 0000000..749f9cb --- /dev/null +++ b/TUICallKit/TUICallService/TUIStore/tuiStore.ts @@ -0,0 +1,157 @@ +import { ITUIStore, IOptions, Task } from '../interface/ITUIStore'; +import { StoreName, NAME } from '../const/index'; +import CallStore from './callStore'; +import { isString, isNumber, isBoolean } from '../utils/common-utils'; + +export default class TUIStore implements ITUIStore { + static instance: TUIStore; + public task: Task; + private storeMap: Partial>; + private timerId: number = -1; + constructor() { + this.storeMap = { + [StoreName.CALL]: new CallStore(), + }; + // todo 此处后续优化结构后调整 + this.task = {} as Task; // 保存监听回调列表 + } + + /** + * 获取 TUIStore 实例 + * @returns {TUIStore} + */ + static getInstance() { + if (!TUIStore.instance) { + TUIStore.instance = new TUIStore(); + } + return TUIStore.instance; + } + + /** + * UI 组件注册监听回调 + * @param {StoreName} storeName store 名称 + * @param {IOptions} options 监听信息 + * @param {Object} params 扩展参数 + * @param {String} params.notifyRangeWhenWatch 注册时监听时的通知范围, 'all' - 通知所有注册该 key 的监听; 'myself' - 通知本次注册该 key 的监听; 默认不通知 + */ + watch(storeName: StoreName, options: IOptions, params?: any) { + if (!this.task[storeName]) { + this.task[storeName] = {}; + } + const watcher = this.task[storeName]; + Object.keys(options).forEach((key) => { + const callback = options[key]; + if (!watcher[key]) { + watcher[key] = new Map(); + } + watcher[key].set(callback, 1); + const { notifyRangeWhenWatch } = params || {}; + // 注册监听后, 通知所有注册该 key 的监听,使用 'all' 时要特别注意是否对其他地方的监听产生影响 + if (notifyRangeWhenWatch === NAME.ALL) { + this.notify(storeName, key); + } + // 注册监听后, 仅通知自己本次监听该 key 的数据 + if (notifyRangeWhenWatch === NAME.MYSELF) { + const data = this.getData(storeName, key); + callback.call(this, data); + } + }); + } + + /** + * UI 取消组件监听回调 + * @param {StoreName} storeName store 名称 + * @param {IOptions} options 监听信息,包含需要取消的回掉等 + */ + unwatch(storeName: StoreName, options: IOptions) { + // todo 该接口暂未支持,unwatch掉同一类,如仅传入store注销掉该store下的所有callback,同样options仅传入key注销掉该key下的所有callback + // options的callback function为必填参数,后续修改 + if (!this.task[storeName]) { + return; + }; + // if (isString(options)) { + // // 移除所有的监听 + // if (options === '*') { + // const watcher = this.task[storeName]; + // Object.keys(watcher).forEach(key => { + // watcher[key].clear(); + // }); + // } else { + // console.warn(`${NAME.PREFIX}unwatch warning: options is ${options}`); + // } + // return; + // } + const watcher = this.task[storeName]; + Object.keys(options).forEach((key: string) => { + watcher[key].delete(options[key]); + }); + } + + /** + * 通用 store 数据更新,messageList 的变更需要单独处理 + * @param {StoreName} storeName store 名称 + * @param {string} key 变更的 key + * @param {unknown} data 变更的数据 + */ + update(storeName: StoreName, key: string, data: unknown) { + // 基本数据类型时, 如果相等, 就不进行更新, 减少不必要的 notify + if (isString(data) || isNumber(data) || isBoolean(data)) { + const currentData = this.storeMap[storeName]['store'][key]; // eslint-disable-line + if (currentData === data) return; + } + this.storeMap[storeName]?.update(key, data); + this.notify(storeName, key); + } + + /** + * 获取 Store 数据 + * @param {StoreName} storeName store 名称 + * @param {string} key 待获取的 key + * @returns {Any} + */ + getData(storeName: StoreName, key: string) { + return this.storeMap[storeName]?.getData(key); + } + + /** + * UI 组件注册监听回调 + * @param {StoreName} storeName store 名称 + * @param {string} key 变更的 key + */ + private notify(storeName: StoreName, key: string) { + if (!this.task[storeName]) { + return; + } + const watcher = this.task[storeName]; + if (watcher[key]) { + const callbackMap = watcher[key]; + const data = this.getData(storeName, key); + for (const [callback] of callbackMap.entries()) { + callback.call(this, data); + } + } + } + + public reset(storeName: StoreName, keyList: Array = [], isNotificationNeeded = false) { + if (storeName in this.storeMap) { + const store = this.storeMap[storeName]; + // reset all + if (keyList.length === 0) { + keyList = Object.keys(store?.store); + } + store.reset(keyList); + if (isNotificationNeeded) { + keyList.forEach((key) => { + this.notify(storeName, key); + }); + } + } + } + // 批量修改多个 key-value + public updateStore(params: any, name?: StoreName): void { + const storeName = name ? name : StoreName.CALL; + Object.keys(params).forEach((key) => { + this.update(storeName, key, params[key]); + }); + } +} diff --git a/TUICallKit/TUICallService/assets/phone_dialing.mp3 b/TUICallKit/TUICallService/assets/phone_dialing.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f392528b74f642871a5114f38577b39b8547db9d GIT binary patch literal 9178 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 literal 0 HcmV?d00001 diff --git a/TUICallKit/TUICallService/assets/phone_ringing.mp3 b/TUICallKit/TUICallService/assets/phone_ringing.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c3838f3323254eb5543dfe05e57a2c2a4a241ade GIT binary patch literal 8752 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 ', keys()) + +export const CALL_DATA_KEY: any = { + CALL_STATUS: 'callStatus', + CALL_ROLE: 'callRole', + CALL_MEDIA_TYPE: 'callMediaType', + LOCAL_USER_INFO: 'localUserInfo', + LOCAL_USER_INFO_EXCLUDE_VOLUMN: 'localUserInfoExcludeVolume', + REMOTE_USER_INFO_LIST: 'remoteUserInfoList', + REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST: 'remoteUserInfoExcludeVolumeList', + CALLER_USER_INFO: 'callerUserInfo', + IS_GROUP: 'isGroup', + CALL_DURATION: 'callDuration', + CALL_TIPS: 'callTips', + TOAST_INFO: 'toastInfo', + IS_MINIMIZED: 'isMinimized', + ENABLE_FLOAT_WINDOW: 'enableFloatWindow', + BIG_SCREEN_USER_ID: 'bigScreenUserId', + LANGUAGE: 'language', + IS_CLICKABLE: 'isClickable', + DISPLAY_MODE: 'displayMode', + VIDEO_RESOLUTION: 'videoResolution', + PUSHER: 'pusher', + PLAYER: 'player', + IS_EAR_PHONE: 'isEarPhone', + SHOW_PERMISSION_TIP: 'SHOW_PERMISSION_TIP', + GROUP_ID: 'groupID', + ROOM_ID: 'roomID', + SHOW_SELECT_USER: 'showSelectUser', +}; + +export const NAME = { + PREFIX: '【CallService】', + AUDIO: 'audio', + VIDEO: 'video', + LOCAL_VIDEO: 'localVideo', + ERROR: 'error', + TIMEOUT: 'timeout', + RAF: 'raf', + INTERVAL: 'interval', + DEFAULT: 'default', + BOOLEAN: 'boolean', + STRING: 'string', + NUMBER: 'number', + OBJECT: 'object', + ARRAY: 'array', + FUNCTION: 'function', + UNDEFINED: "undefined", + ALL: 'all', + MYSELF: 'myself', + DEVICE_LIST: 'deviceList', + CAMERA_POSITION: 'cameraPosition', + ...CALL_DATA_KEY, +}; + +export const AudioCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call.png'; +export const VideoCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call-video-reverse.svg'; +export const MAX_NUMBER_ROOM_ID = 2147483647; +export enum PLATFORM { + // eslint-disable-next-line no-unused-vars + MAC = 'mac', + // eslint-disable-next-line no-unused-vars + WIN = 'win', +} diff --git a/TUICallKit/TUICallService/const/log.ts b/TUICallKit/TUICallService/const/log.ts new file mode 100644 index 0000000..8c137d5 --- /dev/null +++ b/TUICallKit/TUICallService/const/log.ts @@ -0,0 +1,9 @@ +/* eslint-disable */ +// 唯一一个变量格式有问题的, 但是为了和原来 TUICallKit 对外暴露的保持一致 +export enum LOG_LEVEL { + NORMAL = 0, // 普通级别,日志量较多,接入时建议使用 + RELEASE = 1, // release级别,SDK 输出关键信息,生产环境时建议使用 + WARNING = 2, // 告警级别,SDK 只输出告警和错误级别的日志 + ERROR = 3, // 错误级别,SDK 只输出错误级别的日志 + NONE = 4, // 无日志级别,SDK 将不打印任何日志 +} diff --git a/TUICallKit/TUICallService/index.ts b/TUICallKit/TUICallService/index.ts new file mode 100644 index 0000000..8e76eec --- /dev/null +++ b/TUICallKit/TUICallService/index.ts @@ -0,0 +1,23 @@ +import TUICallService, { TUIGlobal, TUIStore } from './CallService/index'; +import { StoreName, NAME, CallRole, CallMediaType, CallStatus, StatusChange, VideoResolution, VideoDisplayMode, AudioPlayBackDevice } from './const/index'; +import { t } from './locales/index'; + +// 实例化 +const TUICallKitServer = TUICallService.getInstance(); + +// 输出产物 +export { + TUIGlobal, + TUIStore, + StoreName, + TUICallKitServer, + NAME, + CallStatus, + CallRole, + CallMediaType, + StatusChange, + VideoResolution, + VideoDisplayMode, + AudioPlayBackDevice, + t, +}; diff --git a/TUICallKit/TUICallService/interface/ICallService.ts b/TUICallKit/TUICallService/interface/ICallService.ts new file mode 100644 index 0000000..dc68769 --- /dev/null +++ b/TUICallKit/TUICallService/interface/ICallService.ts @@ -0,0 +1,97 @@ +import { CallStatus, CallRole } from '../const/index'; + +/** + * @interface ITUICallService +*/ +export interface ITUICallService { + + /** + * 初始化 Service + * @function + * @private + */ + init(params: any): void; + + /** + * 1v1 通话 + * @function + * @param {SwitchUserStatusParams} options 用户状态控制参数 + */ + call(callParams: ICallParams): void; +} + +type SDKAppID = { SDKAppID: number; } | { sdkAppID: number; }; +export interface IInitParamsBase { + userID: string; + userSig: string; + tim?: any; + isFromChat?: boolean; +} +export type IInitParams = IInitParamsBase & SDKAppID; +// call params interface +export interface ICallParams { + userID: string; + type: number; + roomID?: number; + userData?: string; + timeout?: number; + offlinePushInfo?: IOfflinePushInfo; +} +// groupCall params interface +export interface IGroupCallParams { + userIDList: Array; + type: number; + groupID: string; + roomID?: number; + userData?: string; + timeout?: number; + offlinePushInfo?: IOfflinePushInfo; +} +// userInfo interface +export interface IUserInfo { + userId: string; + nick?: string; + avatar?: string; + remark?: string; + displayUserInfo?: string; // 远端用户信息展示: remark -> nick -> userId, 简化 UI 组件; 本地用户信息展示: nick -> userId + isAudioAvailable?: boolean; // 用来设置: 麦克风是否打开 + isVideoAvailable?: boolean; // 用来设置: 摄像头是否打开 + volume?: number; + isEnter?: boolean; // 远端用户, 用来控制预览远端是否显示 loading 效果; 本地用户, 用来控制 "呼叫"、"接通" 接通后显示的 loading 效果 + domId?: string; // 播放流 dom 节点, localUserInfo 的 domId = 'localVideo'; remoteUserInfo 的 domId 就是 userId +} +export interface IOfflinePushInfo { + title?: string, + description?: string, + androidOPPOChannelID?: string, + extension: String +} +export interface ICallbackParam { + beforeCalling?: (...args: any[]) => void; + afterCalling?: (...args: any[]) => void; + onMinimized?: (...args: any[]) => void; + onMessageSentByMe?: (...args: any[]) => void; + kickedOut?: (...args: any[]) => void; + statusChanged?: (...args: any[]) => void; +} + +export interface ISelfInfoParams { + nickName: string; + avatar: string; +} + +export interface IBellParams { + callRole?: CallRole; + callStatus?: CallStatus; + isMuteBell?: boolean; + calleeBellFilePath?: string; +} +export interface IInviteUserParams { + userIDList: string[]; + offlinePushInfo?: IOfflinePushInfo; +} +export interface IJoinInGroupCallParams { + type: number; + groupID: string; + roomID: number; +} diff --git a/TUICallKit/TUICallService/interface/ICallStore.d.ts b/TUICallKit/TUICallService/interface/ICallStore.d.ts index 9dcd2e2..8eb280f 100644 --- a/TUICallKit/TUICallService/interface/ICallStore.d.ts +++ b/TUICallKit/TUICallService/interface/ICallStore.d.ts @@ -1,4 +1,4 @@ -import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList } from '../const/index'; +import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition } from '../const/index'; import { IUserInfo } from './index'; export interface IToastInfo { text: string; @@ -9,7 +9,9 @@ export interface ICallStore { callRole: CallRole; callMediaType: CallMediaType; localUserInfo: IUserInfo; + localUserInfoExcludeVolume: IUserInfo; remoteUserInfoList: Array; + remoteUserInfoExcludeVolumeList: Array; callerUserInfo: IUserInfo; isGroup: boolean; callDuration: string; @@ -24,6 +26,7 @@ export interface ICallStore { deviceList: TDeviceList; groupID: string; roomID: number; + cameraPosition: CameraPosition; displayMode: VideoDisplayMode; videoResolution: VideoResolution; pusher: any; diff --git a/TUICallKit/TUICallService/interface/ICallStore.ts b/TUICallKit/TUICallService/interface/ICallStore.ts new file mode 100644 index 0000000..9c91259 --- /dev/null +++ b/TUICallKit/TUICallService/interface/ICallStore.ts @@ -0,0 +1,41 @@ +import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition } from '../const/index'; +import { IUserInfo } from './index'; + +export interface IToastInfo { + text: string; + type?: string; // 默认 info 通知, 取值: 'info' | 'warn' | 'success' | 'error' +} +export interface ICallStore { + callStatus: CallStatus; // 当前的通话状态, 默认: 'idle' + callRole: CallRole; // 通话角色, 默认: 'callee' + callMediaType: CallMediaType; // 通话类型 + localUserInfo: IUserInfo; // 自己的信息, 默认: { userId: '' } + localUserInfoExcludeVolume: IUserInfo; // 不包含音量的当前用户信息 + remoteUserInfoList: Array; // 远端用户信息列表, 默认: [] + remoteUserInfoExcludeVolumeList: Array; // 不包含音量的远端用户信息列表 + // 被叫在未接通时,展示主叫的 userId、头像。但是如果主叫进入通话后再挂断,此时被叫无法知道主叫的信息了。 + // 因为目前 store 中仅提供了 remoteUserInfoList 数据,主叫离开后,被叫就没有主叫的信息了。因此考虑在 store 中增加 callerUserInfo 字段。 + callerUserInfo: IUserInfo; + isGroup: boolean; // 是否是群组通话, 默认: false + callDuration: string; // 通话时长, 默认: '00:00:00' + callTips: string; // 通话提示的信息. 例如: '等待谁接听', 'xxx 拒绝通话', 'xxx 挂断通话' + toastInfo: IToastInfo; // 远端用户挂断、拒绝、超时、忙线等的 toast 提示信息 + isMinimized: boolean; // 当前是否悬浮窗模式, 默认: false + enableFloatWindow: boolean, // 开启/关闭悬浮窗功能,默认: false + bigScreenUserId: string, // 当前大屏幕显示的 userID 用户 + language: string; // 当前语言 + isClickable: boolean; // 按钮是否可点击(呼叫后, '挂断' 按钮不可点击, 发送信令后才可以点击) + showPermissionTip: boolean; // 设备权限弹窗是否展示(如果有麦克风权限为 false,如果没有麦克风也没有摄像头权限为 true) + deviceList: TDeviceList; + groupID: string; + roomID: number; + cameraPosition: CameraPosition; // 前置或后置,值为front, back + // TUICallKit 组件上的属性 + displayMode: VideoDisplayMode; // 设置预览远端的画面显示模式, 默认: VideoDisplayMode.COVER + videoResolution: VideoResolution; // 设置视频分辨率, 默认: VideoResolution.RESOLUTION_480P + // 小程序相关属性 + pusher: any; + player: any[]; + isEarPhone: boolean; // 是否是听筒, 默认: false + showSelectUser: boolean; +} diff --git a/TUICallKit/TUICallService/interface/ITUIGlobal.ts b/TUICallKit/TUICallService/interface/ITUIGlobal.ts new file mode 100644 index 0000000..48b43fb --- /dev/null +++ b/TUICallKit/TUICallService/interface/ITUIGlobal.ts @@ -0,0 +1,38 @@ +/** + * @interface TUIGlobal + * @property {Object} global 根据运行环境代理 wx、uni、window + * @property {Boolean} isPC true 标识是 pc 网页 + * @property {Boolean} isH5 true 标识是 手机 H5 + * @property {Boolean} isWeChat true 标识是 微信小程序 + * @property {Boolean} isApp true 标识是 uniapp 打包的 native app + * @property {Boolean} isUniPlatform true 标识当前应用是通过 uniapp 平台打包的产物 + * @property {Boolean} isOfficial true 标识是腾讯云官网 Demo 应用 + * @property {Boolean} isWIN true 标识是window系统pc + * @property {Boolean} isMAC true 标识是mac os系统pc +*/ +export interface ITUIGlobal { + global: any; // 挂在 wx、uni、window 对象 + isPC: boolean; + isH5: boolean; + isWeChat: boolean; + isApp: boolean; + isUniPlatform: boolean; + isOfficial: boolean; + isWIN: boolean; + isMAC: boolean; + + /** + * 初始化 TUIGlobal 环境变量 + * @function + * @private + */ + initEnv(): void; + + /** + * 初始化 isOfficial + * @function + * @param {number} SDKAppID 当前实例的应用 SDKAppID + * @private + */ + initOfficial(SDKAppID: number): void; +} diff --git a/TUICallKit/TUICallService/interface/ITUIStore.ts b/TUICallKit/TUICallService/interface/ITUIStore.ts new file mode 100644 index 0000000..c325355 --- /dev/null +++ b/TUICallKit/TUICallService/interface/ITUIStore.ts @@ -0,0 +1,85 @@ +import { StoreName } from '../const/index'; + +// 此处 Map 的 value = 1 为占位作用 +export type Task = Record void, 1>>> + +export interface IOptions { + [key: string]: (newData?: any) => void; +} + +/** + * @class TUIStore + * @property {ICustomStore} customStore 自定义 store,可根据业务需要通过以下 API 进行数据操作。 +*/ +export interface ITUIStore { + task: Task; + + /** + * UI 组件注册监听 + * @function + * @param {StoreName} storeName store 名称 + * @param {IOptions} options UI 组件注册的监听信息 + * @param {Object} params 扩展参数 + * @param {String} params.notifyRangeWhenWatch 注册时监听时的通知范围 + * @example + * // UI 层监听会话列表更新通知 + * let onConversationListUpdated = function(conversationList) { + * console.warn(conversationList); + * } + * TUIStore.watch(StoreName.CONV, { + * conversationList: onConversationListUpdated, + * }) + */ + watch(storeName: StoreName, options: IOptions, params?: any): void; + + /** + * UI 组件取消监听回调 + * @function + * @param {StoreName} storeName store 名称 + * @param {IOptions} options 监听信息,包含需要取消的回调等 + * @example + * // UI 层取消监听会话列表更新通知 + * TUIStore.unwatch(StoreName.CONV, { + * conversationList: onConversationListUpdated, + * }) + */ + unwatch(storeName: StoreName, options: IOptions | string): void; + + /** + * 获取 store 中的数据 + * @function + * @param {StoreName} storeName store 名称 + * @param {String} key 需要获取的 key + * @private + */ + getData(storeName: StoreName, key: string): any; + + /** + * 更新 store + * - 需要使用自定义 store 时可以用此 API 更新自定义数据 + * @function + * @param {StoreName} storeName store 名称 + * @param {String} key 需要更新的 key + * @example + * // UI 层更新自定义 Store 数据 + * TUIStore.update(StoreName.CUSTOM, 'customKey', 'customData') + */ + update(storeName: StoreName, key: string, data: unknown): void; + + /** + * 重置 store 内数据 + * @function + * @param {StoreName} storeName store 名称 + * @param {Array} keyList 需要 reset 的 keyList + * @param {boolean} isNotificationNeeded 是否需要触发更新 + * @private + */ + reset: (storeName: StoreName, keyList?: Array, isNotificationNeeded?: boolean) => void; + + /** + * 修改多个 key-value + * @param {Object} params 多个 key-value 组成的 object + * @param {StoreName} storeName store 名称 + */ + updateStore: (params: any, name?: StoreName) => void; +} diff --git a/TUICallKit/TUICallService/interface/index.ts b/TUICallKit/TUICallService/interface/index.ts new file mode 100644 index 0000000..5f8f48c --- /dev/null +++ b/TUICallKit/TUICallService/interface/index.ts @@ -0,0 +1,4 @@ +export * from './ICallService'; +export * from './ICallStore'; +export * from './ITUIGlobal'; +export * from './ITUIStore'; diff --git a/TUICallKit/TUICallService/locales/en.d.ts b/TUICallKit/TUICallService/locales/en.d.ts index c7efd74..9976a1a 100644 --- a/TUICallKit/TUICallService/locales/en.d.ts +++ b/TUICallKit/TUICallService/locales/en.d.ts @@ -1,25 +1,54 @@ export declare const en: { hangup: string; reject: string; + accept: string; + camera: string; + microphone: string; + speaker: string; + 'open camera': string; + 'close camera': string; + 'open microphone': string; + 'close microphone': string; + 'video-to-audio': string; 'other side reject call': string; 'reject call': string; - accept: string; cancel: string; 'other side line busy': string; 'in busy': string; 'call timeout': string; 'no response from the other side': string; 'end call': string; - timeout: string; - 'kick out': string; 'caller calling message': string; 'callee calling video message': string; 'callee calling audio message': string; 'no microphone access': string; 'no camera access': string; 'invite member': string; - speaker: string; 'Invited group call': string; + waiting: string; + me: string; + 'browser-authorization': string; + 'mac-privacy': string; + 'win-privacy': string; + 'mac-preferences': string; + 'win-preferences': string; + 'Please enter userID': string; + 'View more': string; + 'people selected': string; + 'Select all': string; + Cancel: string; + Done: string; + 'camera enabled': string; + 'camera disabled': string; + 'microphone enabled': string; + 'microphone disabled': string; + 'speaker phone': string; + 'ear piece': string; + 'wait to be called': string; + answered: string; + 'people in the call': string; + 'failed to obtain speakers': string; + 'you have a new call': string; 'Those involved': string; call: string; 'video-call': string; @@ -35,18 +64,15 @@ export declare const en: { 'not-support-multi-call': string; userID: string; 'already-enter': string; - waiting: string; 'camera-opened': string; 'camera-closed': string; 'microphone-opened': string; 'microphone-closed': string; - camera: string; - microphone: string; + timeout: string; + 'kick out': string; 'image-resolution': string; 'default-image-resolution': string; 'invited-person': string; - 'video-to-audio': string; - me: string; 'be-rejected': string; 'be-no-response': string; 'be-line-busy': string; @@ -72,19 +98,4 @@ export declare const en: { 'accept-error': string; 'accept-device-error': string; 'call-error': string; - 'browser-authorization': string; - 'mac-privacy': string; - 'win-privacy': string; - 'mac-preferences': string; - 'win-preferences': string; - 'open camera': string; - 'close camera': string; - 'open microphone': string; - 'close microphone': string; - 'Please enter userID': string; - 'View more': string; - 'people selected': string; - 'Select all': string; - Cancel: string; - Done: string; }; diff --git a/TUICallKit/TUICallService/locales/en.js b/TUICallKit/TUICallService/locales/en.js index 9188607..5d2d1fb 100644 --- a/TUICallKit/TUICallService/locales/en.js +++ b/TUICallKit/TUICallService/locales/en.js @@ -2,27 +2,62 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.en = void 0; exports.en = { + // 按钮文案 'hangup': 'Hang up', 'reject': 'Decline', + 'accept': 'Accept', + 'camera': 'Camera', + 'microphone': 'Microphone', + 'speaker': 'speaker', + 'open camera': 'Open Camera', + 'close camera': 'Close Camera', + 'open microphone': 'Open Microphone', + 'close microphone': 'Close Microphone', + 'video-to-audio': 'Switch to audio', + // 通话结果 'other side reject call': 'other side reject call', 'reject call': 'Reject Call', - 'accept': 'Accept', 'cancel': 'Cancel Call', 'other side line busy': 'other side line busy', 'in busy': 'in busy', 'call timeout': 'call timeout', 'no response from the other side': 'no response from the other side', 'end call': 'end call', - 'timeout': 'timeout', - 'kick out': 'kick out', - 'caller calling message': 'Waiting for the callee to accept the invitation...', - 'callee calling video message': 'You are invited to a video call...', - 'callee calling audio message': 'You are invited to a audio call...', + // 通话提示语 + 'caller calling message': 'Awaiting response', + 'callee calling video message': 'invites you to a video call', + 'callee calling audio message': 'invites you to a voice call', 'no microphone access': 'no microphone access', 'no camera access': 'no camera access', 'invite member': 'Invite Member', - 'speaker': 'speaker', - 'Invited group call': 'Invited you to a group call', + 'Invited group call': 'invites you to a group call', + 'waiting': 'Calling...', + 'me': '(me)', + // 弹出层文案 + 'browser-authorization': 'Browser authorization', + 'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy', + 'win-privacy': 'Setting -> Privacy and Security -> App permissions', + 'mac-preferences': 'Open System Preferences', + 'win-preferences': 'Open Setting', + 'Please enter userID': 'Please enter userID', + 'View more': 'View more', + 'people selected': 'people selected', + 'Select all': 'Select all', + 'Cancel': 'Cancel', + 'Done': 'Done', + // UI3.0 新增 + 'camera enabled': 'Camera On', + 'camera disabled': 'Camera Off', + 'microphone enabled': 'Unmuted', + 'microphone disabled': 'Muted', + 'speaker phone': 'Speaker', + 'ear piece': 'Earpiece', + 'wait to be called': 'Waiting', + 'answered': 'Connected', + 'people in the call': ' other(s) in the call', + 'failed to obtain speakers': 'failed to obtain speakers', + 'you have a new call': 'You have a new call', + // 待废弃文案 'Those involved': 'Those involved in the call are', 'call': 'call', 'video-call': 'video call', @@ -38,18 +73,15 @@ exports.en = { 'not-support-multi-call': 'multi-person call interface is not open', 'userID': 'userID', 'already-enter': 'entered the call', - 'waiting': 'Calling...', 'camera-opened': 'Camera on', 'camera-closed': 'Camera off', 'microphone-opened': 'Mic on', 'microphone-closed': 'Mic off', - 'camera': 'Camera', - 'microphone': 'Microphone', + 'timeout': 'timeout', + 'kick out': 'kick out', 'image-resolution': 'Resolution', 'default-image-resolution': 'Default', 'invited-person': 'Invite', - 'video-to-audio': 'Switch to audio', - 'me': '(me)', 'be-rejected': 'Call declined, ', 'be-no-response': 'No response, ', 'be-line-busy': 'Line busy, ', @@ -75,19 +107,4 @@ exports.en = { 'accept-error': 'Accept failed', 'accept-device-error': 'Accept failed, unable to auth calling device', 'call-error': 'Start call failed', - 'browser-authorization': 'Browser authorization', - 'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy', - 'win-privacy': 'Setting -> Privacy and Security -> App permissions', - 'mac-preferences': 'Open System Preferences', - 'win-preferences': 'Open Setting', - 'open camera': 'Open Camera', - 'close camera': 'Close Camera', - 'open microphone': 'Open Microphone', - 'close microphone': 'Close Microphone', - 'Please enter userID': 'Please enter userID', - 'View more': 'View more', - 'people selected': 'people selected', - 'Select all': 'Select all', - 'Cancel': 'Cancel', - 'Done': 'Done', }; diff --git a/TUICallKit/TUICallService/locales/en.ts b/TUICallKit/TUICallService/locales/en.ts new file mode 100644 index 0000000..b96d213 --- /dev/null +++ b/TUICallKit/TUICallService/locales/en.ts @@ -0,0 +1,107 @@ +export const en = { + // 按钮文案 + 'hangup': 'Hang up', + 'reject': 'Decline', + 'accept': 'Accept', + 'camera': 'Camera', + 'microphone': 'Microphone', + 'speaker': 'speaker', + 'open camera': 'Open Camera', + 'close camera': 'Close Camera', + 'open microphone': 'Open Microphone', + 'close microphone': 'Close Microphone', + 'video-to-audio': 'Switch to audio', + // 通话结果 + 'other side reject call': 'other side reject call', + 'reject call': 'Reject Call', + 'cancel': 'Cancel Call', + 'other side line busy': 'other side line busy', + 'in busy': 'in busy', + 'call timeout': 'call timeout', + 'no response from the other side': 'no response from the other side', + 'end call': 'end call', + // 通话提示语 + 'caller calling message': 'Awaiting response', + 'callee calling video message': 'invites you to a video call', + 'callee calling audio message': 'invites you to a voice call', + 'no microphone access': 'no microphone access', + 'no camera access': 'no camera access', + 'invite member': 'Invite Member', + 'Invited group call': 'invites you to a group call', + 'waiting': 'Calling...', + 'me': '(me)', + // 弹出层文案 + 'browser-authorization': 'Browser authorization', + 'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy', + 'win-privacy': 'Setting -> Privacy and Security -> App permissions', + 'mac-preferences': 'Open System Preferences', + 'win-preferences': 'Open Setting', + 'Please enter userID': 'Please enter userID', + 'View more': 'View more', + 'people selected': 'people selected', + 'Select all': 'Select all', + 'Cancel': 'Cancel', + 'Done': 'Done', + // UI3.0 新增 + 'camera enabled': 'Camera On', + 'camera disabled': 'Camera Off', + 'microphone enabled': 'Unmuted', + 'microphone disabled': 'Muted', + 'speaker phone': 'Speaker', + 'ear piece': 'Earpiece', + 'wait to be called': 'Waiting', + 'answered': 'Connected', + 'people in the call': ' other(s) in the call', + 'failed to obtain speakers': 'failed to obtain speakers', + 'you have a new call': 'You have a new call', + // 待废弃文案 + 'Those involved': 'Those involved in the call are', + 'call': 'call', + 'video-call': 'video call', + 'audio-call': 'audio call', + 'search': 'search', + 'search-result': 'search result', + 'no-user': 'user not found', + 'member-not-added': 'member not added', + 'input-phone-userID': 'phone number or userID', + 'not-login': 'not logged in', + 'login-status-expire': 'login status is invalid, please refresh the page and try again', + 'experience-multi-call': 'experience multi-person calls, please download the full-featured demo: ', + 'not-support-multi-call': 'multi-person call interface is not open', + 'userID': 'userID', + 'already-enter': 'entered the call', + 'camera-opened': 'Camera on', + 'camera-closed': 'Camera off', + 'microphone-opened': 'Mic on', + 'microphone-closed': 'Mic off', + 'timeout': 'timeout', + 'kick out': 'kick out', + 'image-resolution': 'Resolution', + 'default-image-resolution': 'Default', + 'invited-person': 'Invite', + 'be-rejected': 'Call declined, ', + 'be-no-response': 'No response, ', + 'be-line-busy': 'Line busy, ', + 'be-canceled': 'The call is canceled, ', + 'voice-call-end': 'Voice call ended', + 'video-call-end': 'Video call ended', + 'method-call-failed': 'Failed to sync the operation', + 'failed-to-obtain-permission': 'Failed to obtain permissions', + 'environment-detection-failed': 'Failed to check the environment', + 'switchToAudioCall-call-failed': 'switch to audio call method failed', + 'switchToVideoCall-call-failed': 'switch to video call method failed', + 'microphone-unavailable': 'No mic found', + 'camera-unavailable': 'No camera found', + 'ban-device': 'Device access denied', + 'not-supported-webrtc': 'Your current environment does not support WebRTC', + 'blacklist-user-tips': 'The identifier is in blacklist. Failed to send this message!', + 'is-already-calling': 'TUICallKit is already on a call', + 'need-init': 'Before initiating a call with TUICallKit, ensure that the TUICallKitServer.init() method has executed successfully. ', + "can't call yourself": "Can't call yourself", // eslint-disable-line + 'Use-phone-and-computer': 'Use your mobile phone and computer to experience video calls', + 'Wechat scan right QR code': 'Wechat scan right QR code', + 'Scan the QR code above': 'Scan the QR code above', + 'accept-error': 'Accept failed', + 'accept-device-error': 'Accept failed, unable to auth calling device', + 'call-error': 'Start call failed', +}; diff --git a/TUICallKit/TUICallService/locales/index.js b/TUICallKit/TUICallService/locales/index.js index 12a5316..88f17f2 100644 --- a/TUICallKit/TUICallService/locales/index.js +++ b/TUICallKit/TUICallService/locales/index.js @@ -18,6 +18,7 @@ exports.CallTips = { TIMEOUT: 'timeout', KICK_OUT: 'kick out', CALLER_CALLING_MSG: 'caller calling message', + CALLER_GROUP_CALLING_MSG: 'wait to be called', CALLEE_CALLING_VIDEO_MSG: 'callee calling video message', CALLEE_CALLING_AUDIO_MSG: 'callee calling audio message', NO_MICROPHONE_DEVICE_PERMISSION: 'no microphone access', diff --git a/TUICallKit/TUICallService/locales/index.ts b/TUICallKit/TUICallService/locales/index.ts new file mode 100644 index 0000000..6fc923c --- /dev/null +++ b/TUICallKit/TUICallService/locales/index.ts @@ -0,0 +1,57 @@ +import { TUIStore } from '../CallService/index'; +import { NAME, StoreName } from '../const/index'; +import { en } from './en'; +import { zh } from './zh-cn'; +import { ja_JP } from './ja_JP'; + +export const CallTips: any = { + OTHER_SIDE: 'other side', + CANCEL: 'cancel', + OTHER_SIDE_REJECT_CALL: 'other side reject call', + REJECT_CALL: 'reject call', + OTHER_SIDE_LINE_BUSY: 'other side line busy', + IN_BUSY: 'in busy', + CALL_TIMEOUT: 'call timeout', + END_CALL: 'end call', + TIMEOUT: 'timeout', + KICK_OUT: 'kick out', + CALLER_CALLING_MSG: 'caller calling message', + CALLER_GROUP_CALLING_MSG: 'wait to be called', + CALLEE_CALLING_VIDEO_MSG: 'callee calling video message', + CALLEE_CALLING_AUDIO_MSG: 'callee calling audio message', + NO_MICROPHONE_DEVICE_PERMISSION: 'no microphone access', + NO_CAMERA_DEVICE_PERMISSION: 'no camera access', +}; + +export const languageData: languageDataType = { + en, + 'zh-cn': zh, + ja_JP, +}; + +// language translate +export function t(key: any): string { + const language = TUIStore.getData(StoreName.CALL, NAME.LANGUAGE); + // eslint-disable-next-line + for (const langKey in languageData) { + if (langKey === language) { + const currentLanguage = languageData[langKey]; + // eslint-disable-next-line + for (const sentenceKey in currentLanguage) { + if (sentenceKey === key) { + return currentLanguage[sentenceKey]; + } + } + } + } + const enString = key['en']?.key; // eslint-disable-line + console.error(`${NAME.PREFIX}translation is not found: ${key}.`); + return enString; +} + +interface languageItemType { + [key: string]: string; +} +interface languageDataType { + [key: string]: languageItemType; +} diff --git a/TUICallKit/TUICallService/locales/ja_JP.d.ts b/TUICallKit/TUICallService/locales/ja_JP.d.ts index d03dfc7..2b2af4a 100644 --- a/TUICallKit/TUICallService/locales/ja_JP.d.ts +++ b/TUICallKit/TUICallService/locales/ja_JP.d.ts @@ -1,23 +1,50 @@ export declare const ja_JP: { hangup: string; reject: string; + accept: string; + camera: string; + microphone: string; + speaker: string; 'other side reject call': string; 'reject call': string; - accept: string; cancel: string; 'other side line busy': string; 'in busy': string; 'call timeout': string; 'end call': string; - timeout: string; - 'kick out': string; 'caller calling message': string; 'callee calling video message': string; 'callee calling audio message': string; 'no microphone access': string; 'no camera access': string; 'invite member': string; - speaker: string; + 'browser-authorization': string; + 'mac-privacy': string; + 'win-privacy': string; + 'mac-preferences': string; + 'win-preferences': string; + 'Please enter userID': string; + 'View more': string; + 'people selected': string; + 'Select all': string; + Cancel: string; + Done: string; + 'open camera': string; + 'close camera': string; + 'open microphone': string; + 'close microphone': string; + 'camera enabled': string; + 'camera disabled': string; + 'microphone enabled': string; + 'microphone disabled': string; + 'speaker phone': string; + 'wait to be called': string; + answered: string; + 'people in the call': string; + 'failed to obtain speakers': string; + 'you have a new call': string; + timeout: string; + 'kick out': string; 'Invited group call': string; 'Those involved': string; call: string; @@ -42,8 +69,6 @@ export declare const ja_JP: { 'camera-closed': string; 'microphone-opened': string; 'microphone-closed': string; - camera: string; - microphone: string; 'image-resolution': string; 'default-image-resolution': string; 'invited-person': string; @@ -71,15 +96,4 @@ export declare const ja_JP: { 'accept-error': string; 'accept-device-error': string; 'call-error': string; - 'browser-authorization': string; - 'mac-privacy': string; - 'win-privacy': string; - 'mac-preferences': string; - 'win-preferences': string; - 'Please enter userID': string; - 'View more': string; - 'people selected': string; - 'Select all': string; - Cancel: string; - Done: string; }; diff --git a/TUICallKit/TUICallService/locales/ja_JP.js b/TUICallKit/TUICallService/locales/ja_JP.js index ca52108..ca579b1 100644 --- a/TUICallKit/TUICallService/locales/ja_JP.js +++ b/TUICallKit/TUICallService/locales/ja_JP.js @@ -2,25 +2,58 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.ja_JP = void 0; exports.ja_JP = { + // 按钮文案 'hangup': '通話終了', 'reject': '拒否', + 'accept': '応答', + 'camera': 'カメラ', + 'microphone': 'マイク', + 'speaker': 'スピーカー', + // 通话结果 'other side reject call': '通話が拒否されました', 'reject call': '通話拒否', - 'accept': '応答', 'cancel': '通話をキャンセル', 'other side line busy': '相手が通話中です', 'in busy': '通話中', 'call timeout': '呼び出しタイムアウト', 'end call': '通話終了', - 'timeout': 'タイムアウト', - 'kick out': 'キックアウトされました', - 'caller calling message': '相手が招待を承諾するのを待っています。', - 'callee calling video message': 'ビデオ通話に招待されました。', - 'callee calling audio message': '音声通話に招待されました。', + // 通话提示语 + 'caller calling message': '応答を待っています', + 'callee calling video message': 'ビデオ通話に招待されました', + 'callee calling audio message': '音声通話に招待されました', 'no microphone access': 'マイクにアクセスできません', 'no camera access': 'カメラにアクセスできません', 'invite member': 'メンバーを招待する', - 'speaker': 'スピーカー', + // 弹出层文案 + 'browser-authorization': 'ブラウザ認証', + 'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー', + 'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可', + 'mac-preferences': 'システム環境設定を開く', + 'win-preferences': 'システム設定を開く', + 'Please enter userID': 'ユーザーIDを入力してください', + 'View more': 'もっと見る', + 'people selected': '人が選択されました', + 'Select all': 'すべて選択', + 'Cancel': 'キャンセル', + 'Done': '完了', + // UI3.0文案 + 'open camera': 'オープンカメラ', + 'close camera': 'カメラを閉じる', + 'open microphone': 'オープンマイク', + 'close microphone': 'マイクを閉じる', + 'camera enabled': 'カメラオン', + 'camera disabled': 'カメラオフ', + 'microphone enabled': 'マイクオン', + 'microphone disabled': 'マイクオフ', + 'speaker phone': 'スピーカーオン', + 'wait to be called': '待機中', + 'answered': '接続済み', + 'people in the call': '通話に参加している人たち', + 'failed to obtain speakers': 'スピーカーが見つかりません', + 'you have a new call': '新しい通話があります', + // 待废弃文案 + 'timeout': 'タイムアウト', + 'kick out': 'キックアウトされました', 'Invited group call': 'グループ通話に招待されました。', 'Those involved': '参加者:', 'call': '通話', @@ -45,13 +78,11 @@ exports.ja_JP = { 'camera-closed': 'カメラがオフになっています', 'microphone-opened': 'マイクがオンになっています', 'microphone-closed': 'マイクがオフになっています', - 'camera': 'カメラ', - 'microphone': 'マイク', 'image-resolution': '解像度', 'default-image-resolution': 'デフォルト解像度', 'invited-person': 'メンバーを招待', 'video-to-audio': '音声通話に切り替えます', - 'me': '(自分)', + 'me': '(自分)', 'be-rejected': '通話が拒否されました, ', 'be-no-response': '応答なし, ', 'be-line-busy': '相手が通話中です, ', @@ -74,15 +105,4 @@ exports.ja_JP = { 'accept-error': '接続できませんでした', 'accept-device-error': '接続できませんでした。発信側デバイスを認証できません', 'call-error': '通話が開始できませんでした', - 'browser-authorization': 'ブラウザ認証', - 'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー', - 'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可', - 'mac-preferences': 'システム環境設定を開く', - 'win-preferences': 'システム設定を開く', - 'Please enter userID': 'ユーザーIDを入力してください', - 'View more': 'もっと見る', - 'people selected': '人が選択されました', - 'Select all': 'すべて選択', - 'Cancel': 'キャンセル', - 'Done': '完了', }; diff --git a/TUICallKit/TUICallService/locales/ja_JP.ts b/TUICallKit/TUICallService/locales/ja_JP.ts new file mode 100644 index 0000000..fd4cbe0 --- /dev/null +++ b/TUICallKit/TUICallService/locales/ja_JP.ts @@ -0,0 +1,105 @@ +export const ja_JP = { + // 按钮文案 + 'hangup': '通話終了', + 'reject': '拒否', + 'accept': '応答', + 'camera': 'カメラ', + 'microphone': 'マイク', + 'speaker': 'スピーカー', + // 通话结果 + 'other side reject call': '通話が拒否されました', + 'reject call': '通話拒否', + 'cancel': '通話をキャンセル', + 'other side line busy': '相手が通話中です', + 'in busy': '通話中', + 'call timeout': '呼び出しタイムアウト', + 'end call': '通話終了', + // 通话提示语 + 'caller calling message': '応答を待っています', + 'callee calling video message': 'ビデオ通話に招待されました', + 'callee calling audio message': '音声通話に招待されました', + 'no microphone access': 'マイクにアクセスできません', + 'no camera access': 'カメラにアクセスできません', + 'invite member': 'メンバーを招待する', + // 弹出层文案 + 'browser-authorization': 'ブラウザ認証', + 'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー', + 'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可', + 'mac-preferences': 'システム環境設定を開く', + 'win-preferences': 'システム設定を開く', + 'Please enter userID': 'ユーザーIDを入力してください', + 'View more': 'もっと見る', + 'people selected': '人が選択されました', + 'Select all': 'すべて選択', + 'Cancel': 'キャンセル', + 'Done': '完了', + // UI3.0文案 + 'open camera': 'オープンカメラ', + 'close camera': 'カメラを閉じる', + 'open microphone': 'オープンマイク', + 'close microphone': 'マイクを閉じる', + 'camera enabled': 'カメラオン', + 'camera disabled': 'カメラオフ', + 'microphone enabled': 'マイクオン', + 'microphone disabled': 'マイクオフ', + 'speaker phone': 'スピーカーオン', + 'wait to be called': '待機中', + 'answered': '接続済み', + 'people in the call': '通話に参加している人たち', + 'failed to obtain speakers': 'スピーカーが見つかりません', + 'you have a new call': '新しい通話があります', + // 待废弃文案 + 'timeout': 'タイムアウト', + 'kick out': 'キックアウトされました', + 'Invited group call': 'グループ通話に招待されました。', + 'Those involved': '参加者:', + 'call': '通話', + 'video-call': 'ビデオ通話', + 'audio-call': '音声通話', + 'search': '検索', + 'search-result': '検索結果', + 'Wechat scan right QR code': 'WeChatで右側にあるQRコードを読み取ります。', + 'Use-phone-and-computer': '携帯電話とコンピュータを使用してビデオ通話を体験してください', + 'Scan the QR code above': '上のQRコードを読み取ります。', + 'no-user': 'ユーザーが見つかりませんでした', + 'member-not-added': 'メンバーが追加されていません', + 'not-login': 'ログインしていません', + 'login-status-expire': 'ログインの有効期限が過ぎています。ページを更新してもう一度お試しください', + 'experience-multi-call': '複数人で同時に音声通話できるグループ通話機能を体験するには、全機能のデモをダウンロードしてください', + 'not-support-multi-call': 'グループ通話インターフェイスが開いていません', + 'input-phone-userID': '携帯電話番号/ユーザーIDを入力してください', + 'userID': 'ユーザーID', + 'already-enter': 'すでに通話に参加しています', + 'waiting': '応答を待っています...', + 'camera-opened': 'カメラがオンになっています', + 'camera-closed': 'カメラがオフになっています', + 'microphone-opened': 'マイクがオンになっています', + 'microphone-closed': 'マイクがオフになっています', + 'image-resolution': '解像度', + 'default-image-resolution': 'デフォルト解像度', + 'invited-person': 'メンバーを招待', + 'video-to-audio': '音声通話に切り替えます', + 'me': '(自分)', + 'be-rejected': '通話が拒否されました, ', + 'be-no-response': '応答なし, ', + 'be-line-busy': '相手が通話中です, ', + 'be-canceled': '相手が通話をキャンセルしました', + 'voice-call-end': '音声通話が終了しました', + 'video-call-end': 'ビデオ通話が終了しました', + 'method-call-failed': '操作の同期に失敗しました', + 'failed-to-obtain-permission': '権限の取得に失敗しました', + 'environment-detection-failed': '環境の検出に失敗しました', + 'switchToAudioCall-call-failed': '音声通話に切り替えることはできません', + 'switchToVideoCall-call-failed': 'ビデオ通話に切り替えることはできません', + 'microphone-unavailable': '使用できるマイクがありません', + 'camera-unavailable': '使用できるカメラがありません', + 'ban-device': 'デバイスへのアクセスが拒否されました', + 'not-supported-webrtc': '現在の環境はWebRTCをサポートしていません', + 'blacklist-user-tips': 'ユーザーはブラックリストに登録され、通話が開始できませんでした', + 'is-already-calling': 'TUICallKit はすでに通話中です', + 'need-init': 'TUICallKitで通話を開始する前に、TUICallKitServer.init() メソッドが正常に実行されたことを確認してください。', + "can't call yourself": '自分に電話をかけることができません', + 'accept-error': '接続できませんでした', + 'accept-device-error': '接続できませんでした。発信側デバイスを認証できません', + 'call-error': '通話が開始できませんでした', +}; diff --git a/TUICallKit/TUICallService/locales/zh-cn.d.ts b/TUICallKit/TUICallService/locales/zh-cn.d.ts index 369b1c7..d6dc968 100644 --- a/TUICallKit/TUICallService/locales/zh-cn.d.ts +++ b/TUICallKit/TUICallService/locales/zh-cn.d.ts @@ -1,25 +1,56 @@ export declare const zh: { hangup: string; reject: string; + accept: string; + camera: string; + microphone: string; + speaker: string; + 'open camera': string; + 'close camera': string; + 'open microphone': string; + 'close microphone': string; + 'video-to-audio': string; 'other side reject call': string; 'reject call': string; - accept: string; cancel: string; 'other side line busy': string; 'in busy': string; 'call timeout': string; 'end call': string; - timeout: string; - 'kick out': string; 'caller calling message': string; 'callee calling video message': string; 'callee calling audio message': string; 'no microphone access': string; 'no camera access': string; 'invite member': string; - speaker: string; 'Invited group call': string; 'Those involved': string; + waiting: string; + me: string; + 'browser-authorization': string; + 'mac-privacy': string; + 'win-privacy': string; + 'mac-preferences': string; + 'win-preferences': string; + 'Please enter userID': string; + 'View more': string; + 'people selected': string; + 'Select all': string; + Cancel: string; + Done: string; + 'camera enabled': string; + 'camera disabled': string; + 'microphone enabled': string; + 'microphone disabled': string; + 'speaker phone': string; + 'ear piece': string; + 'wait to be called': string; + answered: string; + 'people in the call': string; + 'failed to obtain speakers': string; + 'you have a new call': string; + timeout: string; + 'kick out': string; call: string; 'video-call': string; 'audio-call': string; @@ -37,18 +68,9 @@ export declare const zh: { 'input-phone-userID': string; userID: string; 'already-enter': string; - waiting: string; - 'camera-opened': string; - 'camera-closed': string; - 'microphone-opened': string; - 'microphone-closed': string; - camera: string; - microphone: string; 'image-resolution': string; 'default-image-resolution': string; 'invited-person': string; - 'video-to-audio': string; - me: string; 'be-rejected': string; 'be-no-response': string; 'be-line-busy': string; @@ -71,19 +93,4 @@ export declare const zh: { 'accept-error': string; 'accept-device-error': string; 'call-error': string; - 'browser-authorization': string; - 'mac-privacy': string; - 'win-privacy': string; - 'mac-preferences': string; - 'win-preferences': string; - 'open camera': string; - 'close camera': string; - 'open microphone': string; - 'close microphone': string; - 'Please enter userID': string; - 'View more': string; - 'people selected': string; - 'Select all': string; - Cancel: string; - Done: string; }; diff --git a/TUICallKit/TUICallService/locales/zh-cn.js b/TUICallKit/TUICallService/locales/zh-cn.js index ac854ae..881df3c 100644 --- a/TUICallKit/TUICallService/locales/zh-cn.js +++ b/TUICallKit/TUICallService/locales/zh-cn.js @@ -2,27 +2,64 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.zh = void 0; exports.zh = { + // 按钮文案 'hangup': '挂断', 'reject': '拒绝', + 'accept': '接受', + 'camera': '摄像头', + 'microphone': '麦克风', + 'speaker': '扬声器', + 'open camera': '打开摄像头', + 'close camera': '关闭摄像头', + 'open microphone': '打开麦克风', + 'close microphone': '关闭麦克风', + 'video-to-audio': '转语音通话', + // 通话结果 'other side reject call': '对方已拒绝', 'reject call': '拒绝通话', - 'accept': '接受', 'cancel': '取消通话', 'other side line busy': '对方忙线', 'in busy': '正在忙', 'call timeout': '呼叫超时', 'end call': '结束通话', - 'timeout': '超时', - 'kick out': '被踢', - 'caller calling message': '正在等待对方接受邀请…', - 'callee calling video message': '邀请您进行视频通话…', - 'callee calling audio message': '邀请您进行语音通话…', + // 通话提示语 + 'caller calling message': '等待对方接受邀请', + 'callee calling video message': '邀请你视频通话', + 'callee calling audio message': '邀请你语音通话', 'no microphone access': '没有麦克风权限', 'no camera access': '没有摄像头权限', 'invite member': '邀请成员', - 'speaker': '扬声器', - 'Invited group call': '邀请你参加多人通话', + 'Invited group call': '邀请你加入多人通话', 'Those involved': '参与通话的有:', + 'waiting': '等待接听...', + 'me': '(我)', + // 弹出层文案 + 'browser-authorization': '浏览器授权', + 'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私', + 'win-privacy': '设置 -> 隐私和安全性 -> 应用权限', + 'mac-preferences': '打开系统偏好设置', + 'win-preferences': '打开系统设置', + 'Please enter userID': '请输入 userID', + 'View more': '查看更多', + 'people selected': '人已选中', + 'Select all': '全选', + 'Cancel': '取消', + 'Done': '完成', + // UI3.0 新增 + 'camera enabled': '摄像头已开', + 'camera disabled': '摄像头已关', + 'microphone enabled': '麦克风已开', + 'microphone disabled': '麦克风已关', + 'speaker phone': '扬声器已开', + 'ear piece': '扬声器已关', + 'wait to be called': '等待接听', + 'answered': '已接通', + 'people in the call': '人参与通话', + 'failed to obtain speakers': '无法获取扬声器', + 'you have a new call': '您有一个新的通话', + // 待废弃文案 + 'timeout': '超时', + 'kick out': '被踢', 'call': '通话', 'video-call': '视频通话', 'audio-call': '音频通话', @@ -40,18 +77,9 @@ exports.zh = { 'input-phone-userID': '请输入手机号/用户ID', 'userID': '用户ID', 'already-enter': '已经进入当前通话', - 'waiting': '等待接听...', - 'camera-opened': '摄像头已开', - 'camera-closed': '摄像头已关', - 'microphone-opened': '麦克风已开', - 'microphone-closed': '麦克风已关', - 'camera': '摄像头', - 'microphone': '麦克风', 'image-resolution': '分辨率', 'default-image-resolution': '默认分辨率', 'invited-person': '添加成员', - 'video-to-audio': '切到语音通话', - 'me': '(我)', 'be-rejected': '对方已拒绝,', 'be-no-response': '对方无应答,', 'be-line-busy': '对方忙线中,', @@ -74,19 +102,4 @@ exports.zh = { 'accept-error': '接通失败', 'accept-device-error': '接通失败,通话设备获取失败', 'call-error': '发起通话失败', - 'browser-authorization': '浏览器授权', - 'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私', - 'win-privacy': '设置 -> 隐私和安全性 -> 应用权限', - 'mac-preferences': '打开系统偏好设置', - 'win-preferences': '打开系统设置', - 'open camera': '打开摄像头', - 'close camera': '关闭摄像头', - 'open microphone': '打开麦克风', - 'close microphone': '关闭麦克风', - 'Please enter userID': '请输入 userID', - 'View more': '查看更多', - 'people selected': '人已选中', - 'Select all': '全选', - 'Cancel': '取消', - 'Done': '完成', }; diff --git a/TUICallKit/TUICallService/locales/zh-cn.ts b/TUICallKit/TUICallService/locales/zh-cn.ts new file mode 100644 index 0000000..a6769f8 --- /dev/null +++ b/TUICallKit/TUICallService/locales/zh-cn.ts @@ -0,0 +1,102 @@ +export const zh = { + // 按钮文案 + 'hangup': '挂断', + 'reject': '拒绝', + 'accept': '接受', + 'camera': '摄像头', + 'microphone': '麦克风', + 'speaker': '扬声器', + 'open camera': '打开摄像头', + 'close camera': '关闭摄像头', + 'open microphone': '打开麦克风', + 'close microphone': '关闭麦克风', + 'video-to-audio': '转语音通话', + // 通话结果 + 'other side reject call': '对方已拒绝', + 'reject call': '拒绝通话', + 'cancel': '取消通话', + 'other side line busy': '对方忙线', + 'in busy': '正在忙', + 'call timeout': '呼叫超时', + 'end call': '结束通话', + // 通话提示语 + 'caller calling message': '等待对方接受邀请', + 'callee calling video message': '邀请你视频通话', + 'callee calling audio message': '邀请你语音通话', + 'no microphone access': '没有麦克风权限', + 'no camera access': '没有摄像头权限', + 'invite member': '邀请成员', + 'Invited group call': '邀请你加入多人通话', + 'Those involved': '参与通话的有:', + 'waiting': '等待接听...', + 'me': '(我)', + // 弹出层文案 + 'browser-authorization': '浏览器授权', + 'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私', + 'win-privacy': '设置 -> 隐私和安全性 -> 应用权限', + 'mac-preferences': '打开系统偏好设置', + 'win-preferences': '打开系统设置', + 'Please enter userID': '请输入 userID', + 'View more': '查看更多', + 'people selected': '人已选中', + 'Select all': '全选', + 'Cancel': '取消', + 'Done': '完成', + // UI3.0 新增 + 'camera enabled': '摄像头已开', + 'camera disabled': '摄像头已关', + 'microphone enabled': '麦克风已开', + 'microphone disabled': '麦克风已关', + 'speaker phone': '扬声器已开', + 'ear piece': '扬声器已关', + 'wait to be called': '等待接听', + 'answered': '已接通', + 'people in the call': '人参与通话', + 'failed to obtain speakers': '无法获取扬声器', + 'you have a new call': '您有一个新的通话', + // 待废弃文案 + 'timeout': '超时', + 'kick out': '被踢', + 'call': '通话', + 'video-call': '视频通话', + 'audio-call': '音频通话', + 'search': '搜索', + 'search-result': '搜索结果', + 'Wechat scan right QR code': '微信扫右二维码', + 'Use-phone-and-computer': '用手机与电脑互打体验视频通话', + 'Scan the QR code above': '扫描上方二维码', + 'no-user': '未搜索到用户', + 'member-not-added': '未添加成员', + 'not-login': '未登录', + 'login-status-expire': '登录状态已失效,请刷新网页重试', + 'experience-multi-call': '体验多人通话请下载全功能demo:', + 'not-support-multi-call': '多人通话接口未开放', + 'input-phone-userID': '请输入手机号/用户ID', + 'userID': '用户ID', + 'already-enter': '已经进入当前通话', + 'image-resolution': '分辨率', + 'default-image-resolution': '默认分辨率', + 'invited-person': '添加成员', + 'be-rejected': '对方已拒绝,', + 'be-no-response': '对方无应答,', + 'be-line-busy': '对方忙线中,', + 'be-canceled': '对方已取消', + 'voice-call-end': '语音通话结束', + 'video-call-end': '视频通话结束', + 'method-call-failed': '同步操作失败', + 'failed-to-obtain-permission': '权限获取失败', + 'environment-detection-failed': '环境检测失败', + 'switchToAudioCall-call-failed': '切语音调用失败', + 'switchToVideoCall-call-failed': '切视频调用失败', + 'microphone-unavailable': '没有可用的麦克风设备', + 'camera-unavailable': '没有可用的摄像头设备', + 'ban-device': '用户禁止使用设备', + 'not-supported-webrtc': '当前环境不支持 WebRTC', + 'blacklist-user-tips': '发起通话失败,被对方拉入黑名单,禁止发起!', + 'is-already-calling': 'TUICallKit 已在通话状态', + 'need-init': 'TUICallKit 发起通话前需保证 TUICallKitServer.init() 方法执行成功', + "can't call yourself": '不能呼叫自己', // eslint-disable-line + 'accept-error': '接通失败', + 'accept-device-error': '接通失败,通话设备获取失败', + 'call-error': '发起通话失败', +}; diff --git a/TUICallKit/TUICallService/serve/callManager.d.ts b/TUICallKit/TUICallService/serve/callManager.d.ts index 3474175..c91458f 100644 --- a/TUICallKit/TUICallService/serve/callManager.d.ts +++ b/TUICallKit/TUICallService/serve/callManager.d.ts @@ -1,11 +1,13 @@ export declare class CallManager { private _globalCallPagePath; private _isPageRedirected; + private _targetPagePath; init(params: any): Promise; private _watchTUIStore; private _unwatchTUIStore; private _handleCallStatusChange; private _handleCallStatusToCalling; private _handleCallStatusToIdle; + getRoute(): any; destroyed(): Promise; } diff --git a/TUICallKit/TUICallService/serve/callManager.js b/TUICallKit/TUICallService/serve/callManager.js index 7f4fbe5..2d2b3ff 100644 --- a/TUICallKit/TUICallService/serve/callManager.js +++ b/TUICallKit/TUICallService/serve/callManager.js @@ -34,6 +34,7 @@ class CallManager { constructor() { this._globalCallPagePath = ''; this._isPageRedirected = false; + this._targetPagePath = ''; this._handleCallStatusChange = (value) => __awaiter(this, void 0, void 0, function* () { switch (value) { case index_2.CallStatus.CALLING: @@ -88,6 +89,7 @@ class CallManager { _handleCallStatusToCalling() { if (this._isPageRedirected) return; + this._targetPagePath = this.getRoute().route; // @ts-ignore wx.navigateTo({ url: `/${this._globalCallPagePath}`, @@ -103,6 +105,10 @@ class CallManager { _handleCallStatusToIdle() { if (!this._isPageRedirected) return; + if (this._targetPagePath === this.getRoute().route) { + this._isPageRedirected = false; + return; + } // @ts-ignore wx.navigateBack({ success: () => { @@ -114,6 +120,13 @@ class CallManager { complete: () => { }, }); } + // 获取当前的页面地址 + getRoute() { + // @ts-ignore + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + return currentPage; + } // 卸载 callManger destroyed() { return __awaiter(this, void 0, void 0, function* () { diff --git a/TUICallKit/TUICallService/serve/callManager.ts b/TUICallKit/TUICallService/serve/callManager.ts new file mode 100644 index 0000000..3b1b361 --- /dev/null +++ b/TUICallKit/TUICallService/serve/callManager.ts @@ -0,0 +1,117 @@ +import { TUICallKitServer, NAME, TUIStore, StoreName } from '../../index'; +import { CallStatus } from '../const/index'; +import { avoidRepeatedCall } from '../utils/validate/index'; +/** + * @param {Number} sdkAppID 用户的sdkAppID 必传 + * @param {String} userID 用户的userID 必传 + * @param {String} userSig 用户的userSig 必传 + * @param {String} globalCallPagePath 跳转的路径 必传 + * @param {ChatSDK} tim tim实例 非必传 + */ +const PREFIX = 'callManager'; +export class CallManager { + private _globalCallPagePath:string = ''; + private _isPageRedirected:boolean = false; + private _targetPagePath:string = ''; + @avoidRepeatedCall() + public async init(params) { + const { sdkAppID, userID, userSig, globalCallPagePath, tim } = params; + if (!globalCallPagePath) { + console.error(`${PREFIX} globalCallPagePath Can not be empty!`); + return; + }; + this._globalCallPagePath = globalCallPagePath; + try { + await TUICallKitServer.init({ + sdkAppID, + userID, + userSig, + tim, + }); + this._watchTUIStore(); + // 全局监听下,关闭悬浮窗 + TUICallKitServer.enableFloatWindow(false); + console.log(`${PREFIX} init Ready!`); + } catch (error) { + console.error(`${PREFIX} init fail!`); + } + } + + // =========================【监听 TUIStore 中的状态】========================= + private _watchTUIStore() { + TUIStore?.watch(StoreName.CALL, { + [NAME.CALL_STATUS]: this._handleCallStatusChange, + }, { + notifyRangeWhenWatch: NAME.MYSELF, + }); + } + + private _unwatchTUIStore() { + TUIStore?.unwatch(StoreName.CALL, { + [NAME.CALL_STATUS]: this._handleCallStatusChange, + }); + } + + private _handleCallStatusChange = async (value: CallStatus) => { + switch (value) { + case CallStatus.CALLING: + case CallStatus.CONNECTED: + this._handleCallStatusToCalling(); + break; + + case CallStatus.IDLE: + this._handleCallStatusToIdle(); + break; + } + }; + + private _handleCallStatusToCalling() { + if (this._isPageRedirected) return; + this._targetPagePath = this.getRoute().route; + // @ts-ignore + wx.navigateTo({ + url: `/${this._globalCallPagePath}`, + success: () => { + this._isPageRedirected = true; + }, + fail: () => { + console.error(`${PREFIX} navigateTo fail!`); + }, + complete: () => {}, + }); + } + + private _handleCallStatusToIdle() { + if (!this._isPageRedirected) return; + if(this._targetPagePath === this.getRoute().route) { + this._isPageRedirected = false; + return; + } + // @ts-ignore + wx.navigateBack({ + success: () => { + this._isPageRedirected = false; + }, + fail: () => { + console.error(`${PREFIX} navigateBack fail!`); + }, + complete: () => {}, + }); + } + + // 获取当前的页面地址 + getRoute() { + // @ts-ignore + const pages = getCurrentPages(); + const currentPage = pages[pages.length - 1]; + return currentPage; + } + + // 卸载 callManger + public async destroyed() { + this._globalCallPagePath = ''; + this._isPageRedirected = false; + this._unwatchTUIStore(); + await TUICallKitServer.destroyed(); + } +} diff --git a/TUICallKit/TUICallService/utils/common-utils.js b/TUICallKit/TUICallService/utils/common-utils.js index 53237d0..57dc19f 100644 --- a/TUICallKit/TUICallService/utils/common-utils.js +++ b/TUICallKit/TUICallService/utils/common-utils.js @@ -170,16 +170,16 @@ function handleNoDevicePermissionError(error) { exports.handleNoDevicePermissionError = handleNoDevicePermissionError; /* * 获取向下取整的 performance.now() 值 + * 在不支持 performance.now 的浏览器中,使用 Date.now(). 例如 ie 9,ie 10,避免加载 sdk 时报错 * @export * @return {Number} */ function performanceNow() { - // 在不支持 performance.now 的浏览器中,使用 Date.now() - // 例如 ie 9,ie 10,避免加载 sdk 时报错 - if (!performance || !performance.now) { - return Date.now(); - } - return Math.floor(performance.now()); + // if (!performance || !performance.now) { + // return Date.now(); + // } + // return Math.floor(performance.now()); // uni-app 打包小程序没有 performance, 报错 + return Date.now(); } exports.performanceNow = performanceNow; /** diff --git a/TUICallKit/TUICallService/utils/common-utils.ts b/TUICallKit/TUICallService/utils/common-utils.ts new file mode 100644 index 0000000..9a9a62f --- /dev/null +++ b/TUICallKit/TUICallService/utils/common-utils.ts @@ -0,0 +1,239 @@ +import { NAME } from '../const/index'; +import TUIGlobal from '../TUIGlobal/tuiGlobal'; + +export const isUndefined = function (input: any) { + return typeof input === NAME.UNDEFINED; +}; + +export const isPlainObject = function (input: any) { + // 注意不能使用以下方式判断,因为IE9/IE10下,对象的__proto__是 undefined + // return isObject(input) && input.__proto__ === Object.prototype; + if (typeof input !== NAME.OBJECT || input === null) { + return false; + } + const proto = Object.getPrototypeOf(input); + if (proto === null) { // edge case Object.create(null) + return true; + } + let baseProto = proto; + while (Object.getPrototypeOf(baseProto) !== null) { + baseProto = Object.getPrototypeOf(baseProto); + } + // 原型链第一个和最后一个比较 + return proto === baseProto; +}; + +export const isArray = function (input: any) { + if (typeof Array.isArray === NAME.FUNCTION) { + return Array.isArray(input); + } + return (Object as any).prototype.toString.call(input).match(/^\[object (.*)\]$/)[1].toLowerCase() === NAME.ARRAY; +}; + +export const isPrivateKey = function (key: string) { + return key.startsWith('_'); +}; + +export const isUrl = function (url: string) { + return /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+[a-zA-Z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url); +}; +/** + * 检测input类型是否为string + * @param {*} input 任意类型的输入 + * @returns {Boolean} true->string / false->not a string + */ +export const isString = function (input: any) { + return typeof input === NAME.STRING; +}; +export const isBoolean = function (input: any) { + return typeof input === NAME.BOOLEAN; +}; +export const isNumber = function (input: any) { + return ( + // eslint-disable-next-line + input !== null && + ((typeof input === NAME.NUMBER && !isNaN(input - 0)) || (typeof input === NAME.OBJECT && input.constructor === Number)) + ); +}; + +export function formatTime(secondTime: number): string { + const hours: number = Math.floor(secondTime / 3600); + const minutes: number = Math.floor((secondTime % 3600) / 60); + const seconds: number = Math.floor(secondTime % 60); + let callDurationStr: string = hours > 9 ? `${hours}` : `0${hours}`; + callDurationStr += minutes > 9 ? `:${minutes}` : `:0${minutes}`; + callDurationStr += seconds > 9 ? `:${seconds}` : `:0${seconds}`; + return callDurationStr; +} +export function formatTimeInverse(stringTime: string): number { + const list = stringTime.split(':'); + return parseInt(list[0]) * 3600 + parseInt(list[1]) * 60 + parseInt(list[2]); // eslint-disable-line +} + + +// Determine if it is a JSON string +export function isJSON(str: string) { + if (typeof str === NAME.STRING) { + try { + const data = JSON.parse(str); + if (data) { + return true; + } + return false; + } catch (error) { + console.debug(error); + return false; + } + } + return false; +} + +// Determine if it is a JSON string +export const JSONToObject = function (str: string) { + if (!str || !isJSON(str)) { + return str; + } + return JSON.parse(str); +}; + + +/** + * 重试函数, catch 时,重试 + * @param {Promise} promise 需重试的函数 + * @param {number} num 需要重试的次数 + * @param {number} time 间隔时间(s) + * @returns {Promise} im 接口的 response 原样返回 + */ +export const retryPromise = (promise: Promise, num: number = 6, time: number = 0.5) => { + let n = num; + const func = () => promise; + return func() + .catch((error: any) => { + if (n === 0) { + throw error; + } + const timer = setTimeout(() => { + func(); + clearTimeout(timer); + n = n - 1; + }, time * 1000); + }); +}; + +// /** +// * 节流函数(目前 TUICallKit 增加防重调用装饰器,该方法可删除) +// * @param {Function} func 传入的函数 +// * @param {wait} time 间隔时间(ms) +// */ +// export const throttle = (func: Function, wait: number) => { +// let previousTime = 0; +// return function () { +// const now = Date.now(); +// const args = [...arguments]; +// if (now - previousTime > wait) { +// func.apply(this, args); +// previousTime = now; +// } +// }; +// } + +/** + * web call engine 重复调用时的错误, 这种错误在 TUICallKit 应该忽略 + * @param {any} error 错误信息 + * @returns {Boolean} + */ +export function handleRepeatedCallError(error: any) { + if (error?.message.indexOf('is ongoing, please avoid repeated calls') !== -1) { + return true; + } + return false; +} +/** + * 设备无权限时的错误处理 + * @param {any} error 错误信息 + * @returns {Boolean} + */ +export function handleNoDevicePermissionError(error: any) { + const { message } = error; + if (message.indexOf('NotAllowedError: Permission denied') !== -1) { + return true; + } + return false; +} + +/* + * 获取向下取整的 performance.now() 值 + * 在不支持 performance.now 的浏览器中,使用 Date.now(). 例如 ie 9,ie 10,避免加载 sdk 时报错 + * @export + * @return {Number} + */ +export function performanceNow() { + // if (!performance || !performance.now) { + // return Date.now(); + // } + // return Math.floor(performance.now()); // uni-app 打包小程序没有 performance, 报错 + return Date.now(); + +} +/** + * 检测input类型是否为function + * @param {*} input 任意类型的输入 + * @returns {Boolean} true->input is a function + */ +export const isFunction = function (input: any) { + return typeof input === NAME.FUNCTION; +}; + +/* + * 获取浏览器语言 + * @export + * @return {zh-cn | en} + */ +export const getLanguage = () => { + if (TUIGlobal.getInstance().isWeChat) { + return 'zh-cn'; + } + // @ts-ignore + const lang = (navigator?.language || navigator?.userLanguage || '').substr(0, 2); + let language = 'en'; + switch (lang) { + case 'zh': + language = 'zh-cn'; + break; + case 'ja': + language = 'ja_JP'; + break; + default: + language = 'en'; + } + return language; +}; + +export function noop(e: any) {} + +/** + * Get the object type string + * @param {*} input 任意类型的输入 + * @returns {String} the object type string + */ +export const getType = function(input) { + return Object.prototype.toString + .call(input) + .match(/^\[object (.*)\]$/)[1] + .toLowerCase(); +}; +// 修改对象键名 +export function modifyObjectKey(obj, oldKey, newKey) { + if (!obj.hasOwnProperty(oldKey)) { + return obj; + } + const newObj = {}; + Object.keys(obj).forEach(key => { + if (key === oldKey) { + newObj[newKey] = obj[key]; + } else { + newObj[key] = obj[key]; + } + }); + return newObj; +} \ No newline at end of file diff --git a/TUICallKit/TUICallService/utils/decorators/promise-retry.d.ts b/TUICallKit/TUICallService/utils/decorators/promise-retry.d.ts new file mode 100644 index 0000000..bb50af1 --- /dev/null +++ b/TUICallKit/TUICallService/utils/decorators/promise-retry.d.ts @@ -0,0 +1,32 @@ +/** + * 异步函数重试 Interface + * @param {number} [retries = 5] 重试次数 + * @param {number} [timeout = 2000] 重试间隔时间 + * @param {Function=} onError 重试错误回调 + * @param {Function=} onRetrying 重试回调 + * @param {Function=} onRetryFailed 重试失败回调 + */ +interface IPromiseRetryDecoratorSettings { + retries?: number; + timeout?: number; + onError?: Function; + onRetrying?: Function; + onRetryFailed?: Function; +} +/** + * 装饰器函数:给异步函数增加重试 + * @param {Object} settings 入参 + * @returns {Function} + * @example + * class LocalStream { + * @promiseRetryDecorator({ + * retries: 10, + * timeout: 3000, + * onRetryFailed: function(error) { + * } + * }) + * async recoverCapture(options) {} + * } + */ +export default function promiseRetryDecorator(settings: IPromiseRetryDecoratorSettings): (target: any, name: any, descriptor: any) => any; +export {}; diff --git a/TUICallKit/TUICallService/utils/decorators/promise-retry.js b/TUICallKit/TUICallService/utils/decorators/promise-retry.js new file mode 100644 index 0000000..dc48ef4 --- /dev/null +++ b/TUICallKit/TUICallService/utils/decorators/promise-retry.js @@ -0,0 +1,40 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const retry_1 = __importDefault(require("../retry")); +; +/** + * 装饰器函数:给异步函数增加重试 + * @param {Object} settings 入参 + * @returns {Function} + * @example + * class LocalStream { + * @promiseRetryDecorator({ + * retries: 10, + * timeout: 3000, + * onRetryFailed: function(error) { + * } + * }) + * async recoverCapture(options) {} + * } + */ +function promiseRetryDecorator(settings) { + return function (target, name, descriptor) { + const { retries = 5, timeout = 2000, onError, onRetrying, onRetryFailed } = settings; + const oldFn = (0, retry_1.default)({ + retryFunction: descriptor.value, + settings: { retries, timeout }, + onError, + onRetrying, + onRetryFailed, + context: null, + }); + descriptor.value = function (...args) { + return oldFn.apply(this, args); + }; + return descriptor; + }; +} +exports.default = promiseRetryDecorator; diff --git a/TUICallKit/TUICallService/utils/decorators/promise-retry.ts b/TUICallKit/TUICallService/utils/decorators/promise-retry.ts new file mode 100644 index 0000000..6c926ce --- /dev/null +++ b/TUICallKit/TUICallService/utils/decorators/promise-retry.ts @@ -0,0 +1,51 @@ +import promiseRetry from '../retry'; + +/** + * 异步函数重试 Interface + * @param {number} [retries = 5] 重试次数 + * @param {number} [timeout = 2000] 重试间隔时间 + * @param {Function=} onError 重试错误回调 + * @param {Function=} onRetrying 重试回调 + * @param {Function=} onRetryFailed 重试失败回调 + */ +interface IPromiseRetryDecoratorSettings { + retries?: number; + timeout?: number; + onError?: Function; + onRetrying?: Function; + onRetryFailed?: Function; +}; + +/** + * 装饰器函数:给异步函数增加重试 + * @param {Object} settings 入参 + * @returns {Function} + * @example + * class LocalStream { + * @promiseRetryDecorator({ + * retries: 10, + * timeout: 3000, + * onRetryFailed: function(error) { + * } + * }) + * async recoverCapture(options) {} + * } + */ +export default function promiseRetryDecorator(settings: IPromiseRetryDecoratorSettings) { + return function(target, name, descriptor) { + const { retries = 5, timeout = 2000, onError, onRetrying, onRetryFailed } = settings; + const oldFn = promiseRetry({ + retryFunction: descriptor.value, + settings: { retries, timeout }, + onError, + onRetrying, + onRetryFailed, + context: null, + }); + + descriptor.value = function(...args) { + return oldFn.apply(this, args); + }; + return descriptor; + }; +} diff --git a/TUICallKit/TUICallService/utils/env.ts b/TUICallKit/TUICallService/utils/env.ts new file mode 100644 index 0000000..267855c --- /dev/null +++ b/TUICallKit/TUICallService/utils/env.ts @@ -0,0 +1,51 @@ +// eslint-disable-next-line +declare var wx: any; +// eslint-disable-next-line +declare var uni: any; +// eslint-disable-next-line +declare var window: any; + +// 在 uniApp 框架下,打包 H5、ios app、android app 时存在 wx/qq/tt/swan/my 等变量会导致引入 web sdk 环境判断失效 +// 小程序 getSystemInfoSync 返回的 fontSizeSetting 在 H5 和 app 中为 undefined,所以通过 fontSizeSetting 增强小程序环境判断 +// wx 小程序 +export const IN_WX_MINI_APP = (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function' && Boolean(wx.getSystemInfoSync().fontSizeSetting)); + +// 用 uni-app 打包 native app,此时运行于 js core,无 window 等对象,此时调用 api 都得 uni.xxx,由于风格跟小程序类似,就归为 IN_MINI_APP 的一种 +export const IN_UNI_NATIVE_APP = (typeof uni !== 'undefined' && typeof uni === 'undefined'); + +export const IN_MINI_APP = IN_WX_MINI_APP || IN_UNI_NATIVE_APP; + +export const IN_UNI_APP = (typeof uni !== 'undefined'); + +// 在 uniApp 框架下,由于客户打包 ios app、android app 时 window 不一定存在,所以通过 !IN_MINI_APP 进行判断 +// 非 uniApp 框架下,仍然通过 window 结合 IN_MINI_APP 进行判断,可兼容 Taro3.0+ 暴露 window 对象引起的 IN_BROWSER 判断失效问题 +export const IN_BROWSER = (function () { + if (typeof uni !== 'undefined') { + return !IN_MINI_APP; + } + return (typeof window !== 'undefined') && !IN_MINI_APP; +}()); + +// 命名空间 +export const APP_NAMESPACE = (function () { + if (IN_WX_MINI_APP) { + return wx; + } + if (IN_UNI_APP) { + return uni; + } + return window; +}()); + +// eslint-disable-next-line no-mixed-operators +const USER_AGENT = IN_BROWSER && window && window.navigator && window.navigator.userAgent || ''; +const IS_ANDROID = /Android/i.test(USER_AGENT); +const IS_WIN_PHONE = /(?:Windows Phone)/.test(USER_AGENT); +const IS_SYMBIAN = /(?:SymbianOS)/.test(USER_AGENT); +const IS_IOS = /iPad/i.test(USER_AGENT) || /iPhone/i.test(USER_AGENT) || /iPod/i.test(USER_AGENT); + +export const IS_H5 = IS_ANDROID || IS_WIN_PHONE || IS_SYMBIAN || IS_IOS; + +export const IS_PC = IN_BROWSER && !IS_H5; +export const IS_WIN = IS_PC && USER_AGENT.includes('Windows NT'); +export const IS_MAC = IS_PC && USER_AGENT.includes('Mac'); diff --git a/TUICallKit/TUICallService/utils/index.ts b/TUICallKit/TUICallService/utils/index.ts new file mode 100644 index 0000000..ed9791c --- /dev/null +++ b/TUICallKit/TUICallService/utils/index.ts @@ -0,0 +1,16 @@ +export async function checkLocalMP3FileExists(src: string) { + if (!src) return false; + try { + const response = await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', src, true); + xhr.onload = () => resolve(xhr); + xhr.onerror = () => reject(xhr); + xhr.send(); + }); + return response.status === 200 && response.getResponseHeader('Content-Type') === 'audio/mpeg'; + } catch (error) { + console.warn(error); + return false; + } +} diff --git a/TUICallKit/TUICallService/utils/is-empty.ts b/TUICallKit/TUICallService/utils/is-empty.ts new file mode 100644 index 0000000..550e02b --- /dev/null +++ b/TUICallKit/TUICallService/utils/is-empty.ts @@ -0,0 +1,31 @@ +import { isPlainObject } from './common-utils'; + +const isEmpty = function (input: any) { + // Null and Undefined... + if (input === null || typeof (input) === 'undefined') return true; + // Booleans... + if (typeof input === 'boolean') return false; + // Numbers... + if (typeof input === 'number') return input === 0; + // Strings... + if (typeof input === 'string') return input.length === 0; + // Functions... + if (typeof input === 'function') return input.length === 0; + // Arrays... + if (Array.isArray(input)) return input.length === 0; + // Errors... + if (input instanceof Error) return input.message === ''; + // plain object + if (isPlainObject(input)) { + // eslint-disable-next-line + for (const key in input) { + if (Object.prototype.hasOwnProperty.call(input, key)) { + return false; + } + } + return true; + } + return false; +}; + +export default isEmpty; diff --git a/TUICallKit/TUICallService/utils/retry.d.ts b/TUICallKit/TUICallService/utils/retry.d.ts new file mode 100644 index 0000000..77ffdb2 --- /dev/null +++ b/TUICallKit/TUICallService/utils/retry.d.ts @@ -0,0 +1,36 @@ +/** + * 给异步函数封装重试逻辑 + * @param {Object} options 重试逻辑入参 + * @param {Object} options.retryFunction 需要封装的异步函数 + * @param {Object} options.settings 重试属性 + * @param {Number} [options.settings.retries = 5] 重试次数 + * @param {Number} [options.settings.timeout = 1000] 重试间隔 + * @param {onErrorCallback} options.onError 重试错误回调 + * @param {onRetryingCallback} [options.onRetrying] 重试后的回调 + * @param {Object} options.context 上下文,可选 + * @returns {Function} 封装后的函数 + * @example + * const getUserMedia = promiseRetry({ + * retryFunction: getUserMedia_, + * settings: { retries: 5, timeout: 2000 }, + * onError: (error, retry, reject) => { + * if (error.name === 'NotReadableError') { + * retry(); + * } else { + * reject(error); + * } + * }, + * onRetrying: retryCount => { + * console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`); + * } + * }); + */ +declare function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }: { + retryFunction: any; + settings: any; + onError: any; + onRetrying: any; + onRetryFailed: any; + context: any; +}): (...args: any[]) => Promise; +export default promiseRetry; diff --git a/TUICallKit/TUICallService/utils/retry.js b/TUICallKit/TUICallService/utils/retry.js new file mode 100644 index 0000000..10cdb19 --- /dev/null +++ b/TUICallKit/TUICallService/utils/retry.js @@ -0,0 +1,95 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const common_utils_1 = require("./common-utils"); +const RETRY_STATE_NOT_START = 0; +const RETRY_STATE_STARTED = 1; +const RETRY_STATE_STOPPED = 2; +/** + * 给异步函数封装重试逻辑 + * @param {Object} options 重试逻辑入参 + * @param {Object} options.retryFunction 需要封装的异步函数 + * @param {Object} options.settings 重试属性 + * @param {Number} [options.settings.retries = 5] 重试次数 + * @param {Number} [options.settings.timeout = 1000] 重试间隔 + * @param {onErrorCallback} options.onError 重试错误回调 + * @param {onRetryingCallback} [options.onRetrying] 重试后的回调 + * @param {Object} options.context 上下文,可选 + * @returns {Function} 封装后的函数 + * @example + * const getUserMedia = promiseRetry({ + * retryFunction: getUserMedia_, + * settings: { retries: 5, timeout: 2000 }, + * onError: (error, retry, reject) => { + * if (error.name === 'NotReadableError') { + * retry(); + * } else { + * reject(error); + * } + * }, + * onRetrying: retryCount => { + * console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`); + * } + * }); + */ +function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }) { + return function (...args) { + const retries = settings.retries || 5; + let retryCount = 0; + let timer = -1; + let retryState = RETRY_STATE_NOT_START; + const run = (resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const ctx = context || this; + try { + const result = yield retryFunction.apply(ctx, args); + // 执行成功,正常返回 + retryCount = 0; + resolve(result); + } + catch (error) { + // 用于停止重试 + const stopRetry = () => { + clearTimeout(timer); + retryCount = 0; + retryState = RETRY_STATE_STOPPED; + reject(error); + }; + const retry = () => { + if (retryState !== RETRY_STATE_STOPPED && retryCount < retries) { + retryCount++; + retryState = RETRY_STATE_STARTED; + if ((0, common_utils_1.isFunction)(onRetrying)) { + onRetrying.call(ctx, retryCount, stopRetry); + } + timer = setTimeout(() => { + timer = -1; + run(resolve, reject); + }, (0, common_utils_1.isUndefined)(settings.timeout) ? 1000 : settings.timeout); + } + else { + stopRetry(); + if ((0, common_utils_1.isFunction)(onRetryFailed)) { + onRetryFailed.call(ctx, error); + } + } + }; + if ((0, common_utils_1.isFunction)(onError)) { + onError.call(ctx, error, retry, reject, args); + } + else { + retry(); + } + } + }); + return new Promise(run); + }; +} +exports.default = promiseRetry; diff --git a/TUICallKit/TUICallService/utils/retry.ts b/TUICallKit/TUICallService/utils/retry.ts new file mode 100644 index 0000000..4748584 --- /dev/null +++ b/TUICallKit/TUICallService/utils/retry.ts @@ -0,0 +1,88 @@ +import { isFunction, isUndefined } from './common-utils'; + +const RETRY_STATE_NOT_START = 0; +const RETRY_STATE_STARTED = 1; +const RETRY_STATE_STOPPED = 2; + +/** + * 给异步函数封装重试逻辑 + * @param {Object} options 重试逻辑入参 + * @param {Object} options.retryFunction 需要封装的异步函数 + * @param {Object} options.settings 重试属性 + * @param {Number} [options.settings.retries = 5] 重试次数 + * @param {Number} [options.settings.timeout = 1000] 重试间隔 + * @param {onErrorCallback} options.onError 重试错误回调 + * @param {onRetryingCallback} [options.onRetrying] 重试后的回调 + * @param {Object} options.context 上下文,可选 + * @returns {Function} 封装后的函数 + * @example + * const getUserMedia = promiseRetry({ + * retryFunction: getUserMedia_, + * settings: { retries: 5, timeout: 2000 }, + * onError: (error, retry, reject) => { + * if (error.name === 'NotReadableError') { + * retry(); + * } else { + * reject(error); + * } + * }, + * onRetrying: retryCount => { + * console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`); + * } + * }); + */ +function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }) { + return function(...args) { + const retries = settings.retries || 5; + let retryCount = 0; + let timer: any = -1; + let retryState = RETRY_STATE_NOT_START; + const run = async (resolve, reject) => { + const ctx = context || this; + try { + const result = await retryFunction.apply(ctx, args); + // 执行成功,正常返回 + retryCount = 0; + resolve(result); + } catch (error) { + // 用于停止重试 + const stopRetry = () => { + clearTimeout(timer); + retryCount = 0; + retryState = RETRY_STATE_STOPPED; + reject(error); + }; + const retry = () => { + if (retryState !== RETRY_STATE_STOPPED && retryCount < retries) { + retryCount++; + retryState = RETRY_STATE_STARTED; + if (isFunction(onRetrying)) { + onRetrying.call(ctx, retryCount, stopRetry); + } + timer = setTimeout( + () => { + timer = -1; + run(resolve, reject); + }, + isUndefined(settings.timeout) ? 1000 : settings.timeout + ); + } else { + stopRetry(); + if (isFunction(onRetryFailed)) { + onRetryFailed.call(ctx, error); + } + } + }; + if (isFunction(onError)) { + onError.call(ctx, error, retry, reject, args); + } else { + retry(); + } + } + }; + + return new Promise(run); + }; +} + +export default promiseRetry; diff --git a/TUICallKit/TUICallService/utils/timer.d.ts b/TUICallKit/TUICallService/utils/timer.d.ts index d88f832..050f260 100644 --- a/TUICallKit/TUICallService/utils/timer.d.ts +++ b/TUICallKit/TUICallService/utils/timer.d.ts @@ -42,7 +42,7 @@ declare class Timer { * @param {*} count * @returns ID */ - static interval(taskItem: any): NodeJS.Timer; + static interval(taskItem: any): NodeJS.Timeout; /** * 延迟执行回调 * count = 0,循环 diff --git a/TUICallKit/TUICallService/utils/timer.ts b/TUICallKit/TUICallService/utils/timer.ts new file mode 100644 index 0000000..6b11e0c --- /dev/null +++ b/TUICallKit/TUICallService/utils/timer.ts @@ -0,0 +1,162 @@ +/* eslint-disable */ +import { isPlainObject, performanceNow, isFunction } from './common-utils'; +import { NAME } from '../const/index'; + +/** + * 定时器,功能: + * 1. 支持定时执行回调 [1,n] 次,用于常规延迟、定时操作 + * @example + * // 默认嵌套执行,count=0 无限次 + * timer.run(callback, {delay: 2000}); + * // count=1 等同于 原始setTimeout + * timer.run(callback, {delay: 2000, count:0}); + * 2. 支持 RAF 执行回调,用于小流渲染,audio音量获取等任务,需要渲染频率稳定,支持页面退后台后,用 setTimeout 接管,最短 1s 执行一次 + * @example + * // 默认60fps,可以根据单位时长换算,默认开启后台执行 + * timer.run('raf', callback, {fps: 60}); + * // 设置执行次数 + * timer.run('raf', callback, {fps: 60, count: 300, backgroundTask: false}); + * 3. 支持空闲任务执行回调, requestIdleCallback 在帧渲染的空闲时间执行任务,可以用于 storage 写入等低优先级的任务 + * @example + * // 支持原生setInterval 但不推荐使用,定时任务推荐用 timeout + * timer.run('interval', callback, {delay:2000, count:10}) + */ + +class Timer { + static taskMap = new Map(); + static currentTaskID = 1; + static generateTaskID() { + return this.currentTaskID++; + } + /** + * + * @param {string} taskName 'interval' 'timeout' + * @param {function} callback + * @param {object} options include: + * @param {number} options.delay millisecond + * @param {number} options.count 定时器回调执行次数,0 无限次 or n次 + * @param {boolean} options.backgroundTask 在页面静默后是否继续执行定时器 + */ + static run(taskName = NAME.TIMEOUT, callback: any, options: any) { + // default options + if (taskName === NAME.INTERVAL) { + options = { ...{ delay: 2000, count: 0, backgroundTask: true }, ...options }; + } else { + options = { ...{ delay: 2000, count: 0, backgroundTask: true }, ...options }; + } + // call run(function, {...}) + if (isPlainObject(callback)) { + options = { ...options, ...callback }; + } + if (isFunction(taskName)) { + callback = taskName; + taskName = NAME.TIMEOUT; + } + // 1. 创建 taskID,作为 timer task 的唯一标识,在本函数执行完后返回,用于在调用的地方实现互斥逻辑 + // 2. 根据 taskName 执行相应的函数 + const taskItem = { + taskID: this.generateTaskID(), + loopCount: 0, + intervalID: null, + timeoutID: null, + taskName, + callback, + ...options + }; + this.taskMap.set(taskItem.taskID, taskItem); + // console.log(`timer run task:${taskItem.taskName}, task queue size: ${this.taskMap.size}`); + if (taskName === NAME.INTERNAL) { + this.interval(taskItem); + } else { + this.timeout(taskItem); + } + return taskItem.taskID; + } + + /** + * 定时循环执行回调函数 + * 可以指定循环的时间间隔 + * 可以指定循环次数 + * @param {object} taskItem + * @param {function} callback + * @param {*} delay + * @param {*} count + * @returns ID + */ + static interval(taskItem: any) { + // setInterval 缺点,浏览器退后台会降频,循环执行间隔时间不可靠 + // 创建进入定时器循环的任务函数,函数内:1. 判断是否满足执行条件,2.执行 callback + // 通过 setInterval 执行任务函数 + // 将 intervalID 记录到 taskMap 对应的 item + const task = () => { + taskItem.callback(); + taskItem.loopCount += 1; + if (this.isBreakLoop(taskItem)) { + return; + } + }; + return taskItem.intervalID = setInterval(task, taskItem.delay); + } + /** + * 延迟执行回调 + * count = 0,循环 + * count = n, 执行n次 + * @param {object} taskItem + * + */ + static timeout(taskItem: any) { + // setTimeout 浏览器退后台,延迟变为至少1s + const task: any = () => { + // 执行回调 + taskItem.callback(); + taskItem.loopCount += 1; + if (this.isBreakLoop(taskItem)) { + return; + } + // 不修正延迟,每次callback间隔平均 + return taskItem.timeoutID = setTimeout(task, taskItem.delay); + }; + return taskItem.timeoutID = setTimeout(task, taskItem.delay); + } + + static hasTask(taskID: any) { + return this.taskMap.has(taskID); + } + + static clearTask(taskID: any) { + // console.log('timer clearTask start', `| taskID:${taskID} | size:${this.taskMap.size}`); + if (!this.taskMap.has(taskID)) { + return true; + } + const { intervalID, timeoutID, onVisibilitychange } = this.taskMap.get(taskID); + if (intervalID) { + clearInterval(intervalID); + } + if (timeoutID) { + clearTimeout(timeoutID); + } + if (onVisibilitychange) { + document.removeEventListener('visibilitychange', onVisibilitychange); + } + this.taskMap.delete(taskID); + // console.log('timer clearTask end ', `| size:${this.taskMap.size}`); + return true; + } + /** + * 1. 如果已移除出定时队列,退出当前任务 + * 2. 如果当前任务已满足次数限制,则退出当前任务 + * @param {object} taskItem + * @returns + */ + static isBreakLoop(taskItem: any) { + if (!this.taskMap.has(taskItem.taskID)) { + return true; + } + if (taskItem.count !== 0 && taskItem.loopCount >= taskItem.count) { + this.clearTask(taskItem.taskID); + return true; + } + return false; + } +} +export default Timer; diff --git a/TUICallKit/TUICallService/utils/validate/avoidRepeatedCall.ts b/TUICallKit/TUICallService/utils/validate/avoidRepeatedCall.ts new file mode 100644 index 0000000..df56eb4 --- /dev/null +++ b/TUICallKit/TUICallService/utils/validate/avoidRepeatedCall.ts @@ -0,0 +1,34 @@ +import { NAME } from '../../const/index'; + +/** + * 装饰器:阻止函数重复调用 + * @export + * @param {Object} options 入参 + * @param {Function} options.fn 函数 + * @param {Object} options.context 上下文对象 + * @param {String} options.name 函数名 + * @returns {Function} 封装后的函数 + */ +export function avoidRepeatedCall() { + return function (target: any, name: string, descriptor: any) { + const oldFn = descriptor.value; + const isCallingSet = new Set(); + descriptor.value = async function (...args: any[]) { + if (isCallingSet.has(this)) { + console.warn((`${NAME.PREFIX}previous ${name}() is ongoing, please avoid repeated calls`)); + // throw new Error(`previous ${name}() is ongoing, please avoid repeated calls`); + return; + } + try { + isCallingSet.add(this); + const result = await oldFn.apply(this, args); + isCallingSet.delete(this); + return result; + } catch (error) { + isCallingSet.delete(this); + throw error; + } + }; + return descriptor; + }; +} diff --git a/TUICallKit/TUICallService/utils/validate/index.ts b/TUICallKit/TUICallService/utils/validate/index.ts new file mode 100644 index 0000000..283fe0e --- /dev/null +++ b/TUICallKit/TUICallService/utils/validate/index.ts @@ -0,0 +1,11 @@ +import { avoidRepeatedCall } from './avoidRepeatedCall'; +import { paramValidate } from './validateParams'; +import { VALIDATE_PARAMS } from './validateConfig'; +// import { apiCallQueue } from "./apiCallQueue"; + +export { + VALIDATE_PARAMS, + paramValidate, + avoidRepeatedCall, + // apiCallQueue, +}; diff --git a/TUICallKit/TUICallService/utils/validate/validateConfig.ts b/TUICallKit/TUICallService/utils/validate/validateConfig.ts new file mode 100644 index 0000000..c31882a --- /dev/null +++ b/TUICallKit/TUICallService/utils/validate/validateConfig.ts @@ -0,0 +1,188 @@ +import { NAME, MAX_NUMBER_ROOM_ID, VideoResolution, VideoDisplayMode } from "../../const/index"; + +export const VALIDATE_PARAMS = { + init: { + SDKAppID: { + required: true, + rules: [NAME.NUMBER], + allowEmpty: false, + }, + userID: { + required: true, + rules: [NAME.STRING], + allowEmpty: false, + }, + userSig: { + required: true, + rules: [NAME.STRING], + allowEmpty: false, + }, + tim: { + required: false, + rules: [NAME.OBJECT], + }, + }, + call: { + userID: { + required: true, + rules: [NAME.STRING], + allowEmpty: false + }, + type: { + required: true, + rules: [NAME.NUMBER], + range: [1, 2], + allowEmpty: false + }, + roomID: { + required: false, + rules: [NAME.NUMBER], // 仅支持数字房间号, 后续会支持字符串房间号 + range: `1~${MAX_NUMBER_ROOM_ID}`, + allowEmpty: false, + }, + userData: { + required: false, + rules: [NAME.STRING], + allowEmpty: false, + }, + timeout: { + required: false, + rules: [NAME.NUMBER], + allowEmpty: false + } + }, + groupCall: { + userIDList: { + required: true, + rules: [NAME.ARRAY], + allowEmpty: false + }, + type: { + required: true, + rules: [NAME.NUMBER], + range: [1, 2], + allowEmpty: false + }, + groupID: { + required: true, + rules: [NAME.STRING], + allowEmpty: false + }, + roomID: { + required: false, + rules: [NAME.NUMBER], // 仅支持数字房间号, 后续会支持字符串房间号 + range: `1~${MAX_NUMBER_ROOM_ID}`, + allowEmpty: false + }, + timeout: { + required: false, + rules: [NAME.NUMBER], + allowEmpty: false + }, + userData: { + required: false, + rules: [NAME.STRING], + allowEmpty: false, + }, + offlinePushInfo: { + required: false, + rules: [NAME.OBJECT], + allowEmpty: false, + }, + }, + joinInGroupCall: { + type: { + required: true, + rules: [NAME.NUMBER], + range: [1, 2], + allowEmpty: false + }, + groupID: { + required: true, + rules: [NAME.STRING], + allowEmpty: false + }, + roomID: { + required: true, + rules: [NAME.NUMBER], + allowEmpty: false, + }, + }, + inviteUser: { + userIDList: { + required: true, + rules: [NAME.ARRAY], + allowEmpty: false + }, + }, + setSelfInfo: { + nickName: { + required: false, + rules: [NAME.STRING], + allowEmpty: false, + }, + avatar: { + required: false, + rules: [NAME.STRING], + allowEmpty: false, + } + }, + enableFloatWindow: [ + { + key: "enable", + required: false, + rules: [NAME.BOOLEAN], + allowEmpty: false, + } + ], + enableAIVoice: [ + { + key: "enable", + required: true, + rules: [NAME.BOOLEAN], + allowEmpty: false, + } + ], + enableMuteMode: [ + { + key: "enable", + required: true, + rules: [NAME.BOOLEAN], + allowEmpty: false, + } + ], + setCallingBell: [ + { + key: "filePath", + required: false, + rules: [NAME.STRING], + allowEmpty: true, + } + ], + setLanguage: [ + { + key: "language", + required: true, + rules: [NAME.STRING], + allowEmpty: false + } + ], + setVideoDisplayMode: [ + { + key: "displayMode", + required: true, + rules: [NAME.STRING], + range: [VideoDisplayMode.CONTAIN, VideoDisplayMode.COVER, VideoDisplayMode.FILL], + allowEmpty: false + } + ], + setVideoResolution: [ + { + key: "resolution", + required: true, + rules: [NAME.STRING], + range: [VideoResolution.RESOLUTION_1080P, VideoResolution.RESOLUTION_480P, VideoResolution.RESOLUTION_720P], + allowEmpty: false + } + ] +}; diff --git a/TUICallKit/TUICallService/utils/validate/validateParams.ts b/TUICallKit/TUICallService/utils/validate/validateParams.ts new file mode 100644 index 0000000..c1a0543 --- /dev/null +++ b/TUICallKit/TUICallService/utils/validate/validateParams.ts @@ -0,0 +1,88 @@ +import { getType, isArray, isString, isUndefined, isNumber, modifyObjectKey } from "../common-utils"; +import { NAME } from "../../const/index"; +const PREFIX = NAME.PREFIX + "API"; + +export function paramValidate (config: any) { + return function (target, propertyName: string, descriptor: PropertyDescriptor) { + let method = descriptor.value; + descriptor.value = function (...args) { + doValidate.call(this, config, args, propertyName); + return method.apply(this, args); + }; + return descriptor; + }; +} +function doValidate(config, args, name) { + try { + // 兼容 init 方法中: SDKAppID sdkAppID 两种写法的参数校验判断 + if (!args[0].SDKAppID) { + config = modifyObjectKey(config, "SDKAppID", "sdkAppID"); + } + if (isArray(config)) { + for (let i = 0; i < config.length; i++) { + check.call(this, { + ...config[i], + value: args[i], + name, + }); + } + } else { + for (const key in config) { + if (config.hasOwnProperty(key)) { + check.call(this, { + ...config[key], + value: args[0][key], + name, + key, + }); + } + } + } + } catch (error) { + console.error(error); + throw error; + } +} +function check({ required, rules, range, value, allowEmpty, name, key }) { + // 用户没传指定参数 + if (isUndefined(value)) { + // 检查必填参数, 若配置是必填则报错 + if (required) { + throw new Error(`${PREFIX}<${name}>: ${key} is required.`); + } else { + return; + } + } + // 判断参数类型是否正确 + const result = rules.some((item)=>item === getType(value)); + let type = ''; + if (!result) { + for (let i = 0; i < rules.length; i++) { + let str = rules[i]; + str = str.replace(str[0], str[0].toUpperCase()); + type += `${str}/`; + } + type = type.substring(0, type.length - 1); + throw new Error(`${PREFIX}<${name}>: ${key} must be ${type}, current ${key} is ${typeof value}.`); + } + // 不允许传空值, 例如: '', ' ' + if (allowEmpty === false) { + const isEmptyString = isString(value) && value.trim() === ''; + if (isEmptyString) { + throw new Error(`${PREFIX}<${name}>: ${key} is blank.`); + } + } + // 判断是否符合限制条件 + if (isArray(range)) { + if (range && range.indexOf(value) === -1) { + throw new Error(`${PREFIX}<${name}>: ${key} error, only be ${range}, current ${key} is ${value}.`); + } + } + // 取值范围, 前闭后闭 + if (isString(range) && range.indexOf('~') !== -1) { + const valueList = range.split('~'); + if (value < +valueList[0] || value > +valueList[1] || (isNumber(value) && Number.isNaN(value))) { + throw new Error(`${PREFIX}<${name}>: ${key} error, only be ${range}, current ${key} is ${value}.`); + } + } +} diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.js b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.js index bef1cf0..e23fbea 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.js +++ b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.js @@ -267,8 +267,8 @@ Component({ }]; return renderDom; } - //13 赠送对话 - if (customMessage.message_type ===14 || customMessage.message_type ===13){ + //13 赠送对话customMessage.message_type ===14 || + if (customMessage.message_type ===13){ let data = customMessage.data; const renderDom = [{ type: 'send_talk', @@ -285,7 +285,7 @@ Component({ const renderDom = [{ type: 'video_time', title: customMessage.title, - desc: customMessage.desc, + desc: customMessage.desc.replace(/text/g,'span'), }]; return renderDom; diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxml b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxml index 84bbfab..2ea5ec6 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxml +++ b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxml @@ -35,7 +35,9 @@ {{renderDom[0].text}} + {{renderDom[0].text}} + {{renderDom[0].text}} @@ -107,9 +109,9 @@ - {{renderDom[0].desc}} + - {{renderDom[0].desc}} + \ No newline at end of file diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxss b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxss index f570e7f..2ccee1f 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxss +++ b/TUIService/TUIKit/components/TUIChat/components/MessageElements/CustomMessage/index.wxss @@ -380,6 +380,7 @@ color: #666666; border-style: none none solid solid } .call-message.my-text { + flex-direction: row-reverse; max-width: 60vw; min-height: 50rpx; padding: 10rpx 24rpx; @@ -407,6 +408,7 @@ color: #666666; padding: 10rpx 24rpx; line-height: 50rpx; position: relative; + border-radius: 10px 10px 10px 10px; background: rgb(255, 255, 255); border: 1rpx solid #D8D8D8; @@ -431,4 +433,25 @@ color: #666666; } .video_time_16{ display: none; + } + .classImgleft{ + width:30rpx; + height:30rpx; + margin-right:10rpx; + } + .classImgright{ + margin-top: 4rpx; + width:30rpx; + height:30rpx; + margin-left:10rpx; + } + .call-message{ + display: flex; + align-items: center; + } + .tipColor{ + color:#3BB5FE; + } + .tipRoundsColor{ + color: #FF4D4F; } \ No newline at end of file diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.js b/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.js index 4ee331a..1b00136 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.js +++ b/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.js @@ -68,6 +68,15 @@ Component({ }); }, }, + rest_rounds:{ + type:Number, + value:0, + observer(newVal){ + this.setData({ + rest_rounds:newVal + }) + } + }, msgData:{ type: Object, value:{}, @@ -146,6 +155,7 @@ Component({ patient_family_data:{}, message: '', message_rounds:0, + rest_rounds:0, times_number:0,//总共回合数 msgData:{}, rest_time:0, @@ -745,7 +755,7 @@ pageLifetimes:{ }, //发送成功之后回合数+1; sendCallback(){ - let {times_number,msgData}=this.data; + let {times_number,msgData,rest_rounds}=this.data; let rounds=msgData.msg_round; if(msgData.msg_type==1){ rounds=rounds+1; @@ -759,7 +769,7 @@ pageLifetimes:{ }); setTimeout(() => { if(times_number>=0){ - if(rounds==times_number){ + if(rounds==times_number || rest_rounds==0){ wx.showToast({ title: '沟通已达到限制的'+times_number+'个回合', icon:"none" diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.wxml b/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.wxml index aef53d9..dcbcd2b 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.wxml +++ b/TUIService/TUIKit/components/TUIChat/components/MessageInput/index.wxml @@ -2,7 +2,7 @@ - 视频电话 + 更多 diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageList/index.js b/TUIService/TUIKit/components/TUIChat/components/MessageList/index.js index 0d75119..99a19ef 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageList/index.js +++ b/TUIService/TUIKit/components/TUIChat/components/MessageList/index.js @@ -253,7 +253,8 @@ Component({ }else{ let jsonData=JSON.parse(messageList[i].payload.data); - if(jsonData.message_type==1){ + //13 赠送回合数 + if(jsonData.message_type==1 || jsonData.message_type==13){ this.setData({ 'msgData.msg_round':0, 'msgData.msg_type':2 diff --git a/TUIService/TUIKit/components/TUIChat/components/MessageList/index.wxml b/TUIService/TUIKit/components/TUIChat/components/MessageList/index.wxml index 7a17240..871dd6a 100644 --- a/TUIService/TUIKit/components/TUIChat/components/MessageList/index.wxml +++ b/TUIService/TUIKit/components/TUIChat/components/MessageList/index.wxml @@ -3,7 +3,7 @@ 没有更多啦 -