import { IUserInfo, ISelfInfoParams, IInitParams } from '../interface/ICallService'; import { CallMediaType, CameraPosition, AudioPlayBackDevice, LOG_LEVEL, devicePermissions } from '@trtc/call-engine-lite-wx'; import { ICallsParam, IInviteUserParam } from '@trtc/call-engine-lite-wx'; import { StoreName, CallStatus, NAME, CALL_DATA_KEY, CallRole, COMPONENT, FeatureButton, ButtonState } from '../const/index'; import { TUICallEngine } from '@trtc/call-engine-lite-wx'; import { beforeCall, handlePackageError } from './miniProgram'; import { CallTips, t } from '../locales/index'; import { handleModalError } from './UIKitModal'; import { handleRepeatedCallError, performanceNow } from '../utils/common-utils'; import { getRemoteUserProfile, noDevicePermissionToast, setLocalUserInfoAudioVideoAvailable } from './utils'; import { ITUIStore } from '../interface/index'; import TuiStore from '../TUIStore/tuiStore'; import ChatCombine from './chatCombine'; import EngineEventHandler from './engineEventHandler'; const TUIStore: ITUIStore = TuiStore.getInstance(); const version = '<@VERSION@>'; export { TUIStore }; const defaultOfflinePushInfo = { title: '', description: t('you have a new call') }; export default class TUICallService { static instance: TUICallService; public _tuiCallEngine: any; private _tim: any = null; private _timerId: any = -1; private _startTimeStamp: number = performanceNow(); private _isFromChat: boolean = false; private _offlinePushInfo = null; private _permissionCheckTimer: any = null; private _chatCombine: any = null; private _engineEventHandler: any = null; constructor() { console.log(`${NAME.PREFIX}version: ${version}`); this._watchTUIStore(); this._engineEventHandler = EngineEventHandler.getInstance({ callService: this }); this._chatCombine = ChatCombine.getInstance({ callService: this }); } static getInstance() { if (!TUICallService.instance) { TUICallService.instance = new TUICallService(); } return TUICallService.instance; } public async init(params: IInitParams) { try { if (this._tuiCallEngine) return; // @ts-ignore let { userID, tim, userSig, sdkAppID, SDKAppID, isFromChat, component = COMPONENT.TUI_CALL_KIT } = params; this._tim = tim; console.log(`${NAME.PREFIX}init sdkAppId: ${sdkAppID || SDKAppID}, userId: ${userID}`); this._tuiCallEngine = TUICallEngine.createInstance({ tim, sdkAppID: sdkAppID || SDKAppID, // 兼容传入 SDKAppID 的问题 callkitVersion: version, isFromChat: isFromChat || false, component, scene: 'wx-uniapp-vue3-lite', }); 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 const uiConfig = TUIStore.getData(StoreName.CALL, NAME.CUSTOM_UI_CONFIG); this._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.init', data: { uiConfig, } }); } 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; this._unwatchTUIStore(); } } catch (error) { console.error(`${NAME.PREFIX}destroyed failed, error: ${error}.`); throw error; } } // ===============================【通话操作】=============================== public async calls(callsParams: ICallsParam) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; // avoid double click when application stuck try { TUIStore.update(StoreName.CALL, NAME.PUSHER_ID, NAME.NEW_PUSHER); const { userIDList, type, chatGroupID, offlinePushInfo } = callsParams; if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; const remoteUserInfoList = userIDList.map(userId => ({ userId })); await this._updateCallStoreBeforeCall(type, remoteUserInfoList, chatGroupID); callsParams.offlinePushInfo = { ...defaultOfflinePushInfo, ...offlinePushInfo }; const response = await this._tuiCallEngine.calls(callsParams); await this._updateCallStoreAfterCall(userIDList, response); } catch (error: any) { handleModalError(error); this._handleCallError(error, 'calls'); } } // public async inviteUser(params: IInviteUserParam) { // if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck // try { // const { userIDList } = params; // let inviteUserInfoList = await getRemoteUserProfile(userIDList, this.getTim()); // const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // const userIDListNotInRemoteUserInfoList = userIDList.filter(userId => { // return !remoteUserInfoList.some(remoteUserInfo => remoteUserInfo.userId === userId); // }); // if (userIDListNotInRemoteUserInfoList.length === 0) { // return; // } // 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}.`); // } // } // public async join(params) { // if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CONNECTED) return; // avoid double click when application stuck // try { // const response = await this._tuiCallEngine.join(params); // TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); // this.startTimer(); // const updateStoreParams = { // [NAME.CALL_ROLE]: CallRole.CALLEE, // [NAME.IS_GROUP]: true, // [NAME.CALL_STATUS]: CallStatus.CONNECTED, // [NAME.CALL_MEDIA_TYPE]: TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE) || CallMediaType.AUDIO, // default audio callMediaType // }; // TUIStore.updateStore(updateStoreParams, StoreName.CALL); // 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 }); // await this.openMicrophone(); // setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // } catch (error) { // this._handleCallError(error, 'join'); // } // } // ===============================【其它对外接口】=============================== public getTUICallEngineInstance(): any { return this?._tuiCallEngine || null; } public setLogLevel(level: LOG_LEVEL) { this?._tuiCallEngine?.setLogLevel(level); } public enableFloatWindow(enable: boolean) { TUIStore.update(StoreName.CALL, NAME.ENABLE_FLOAT_WINDOW, enable); } 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}.`); } } public async enableMuteMode(enable: boolean) { try { } catch (error) { console.warn(`${NAME.PREFIX}enableMuteMode failed, error: ${error}.`); } } // =============================【实验性接口】============================= public callExperimentalAPI(jsonStr: string) { const jsonObj = JSON.parse(jsonStr); if (jsonObj === jsonStr) return; const { api, params } = jsonObj; if (!api || !params) return; try { switch(api) { case 'forceUseV2API': const { enable } = params; TUIStore.update(StoreName.CALL, NAME.IS_FORCE_USE_V2_API, !!enable); break; default: break; } } catch (error) { this._tuiCallEngine?.reportLog?.({ name: 'TUICallKit.callExperimentalAPI.fail', data: { error } }); } } // =============================【内部按钮操作方法】============================= public async accept() { const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); this._tuiCallEngine?.reportLog?.({ name: 'TUICallKit.accept.start', data: { callStatus }, }); if (callStatus === CallStatus.CONNECTED) return; // avoid double click when application stuck, especially for miniProgram try { const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); const deviceMap = { microphone: true, camera: callMediaType === CallMediaType.VIDEO, }; const currentDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap); if (!currentDevicePermission) { const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); if (typeof devicePermissions === 'function') { await devicePermissions(callMediaType); } } TUIStore.update(StoreName.CALL, NAME.PUSHER_ID, NAME.NEW_PUSHER); const response = await this._tuiCallEngine.accept(); await this._handleAcceptResponse(response); } catch (error) { this._tuiCallEngine?.reportLog?.({ name: 'TUICallKit.accept.fail', level: 'error', error, }); if (handleRepeatedCallError(error)) return; handleModalError(error); noDevicePermissionToast(error, CallMediaType.AUDIO, this._tuiCallEngine); this._resetCallStore(); } } private async _handleAcceptResponse(response) { if (response) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CONNECTED) return; // 小程序接通时会进行授权弹框, 状态需要放在 accept 后, 否则先接通后再拉起权限设置 TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED); TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); this.startTimer(); const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; (callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO); 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 }); setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini default open audio } } public async hangup() { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck await this._tuiCallEngine.hangup(); this._resetCallStore(); } public async reject() { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck await this._tuiCallEngine.reject(); this._resetCallStore(); } public async openCamera(videoViewDomID: string) { try { const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION); const isFrontCamera = currentPosition === CameraPosition.FRONT ? true : false; this._tuiCallEngine.openCamera(videoViewDomID, isFrontCamera); setLocalUserInfoAudioVideoAvailable(true, NAME.VIDEO); } catch (error: any) { noDevicePermissionToast(error, CallMediaType.VIDEO, this._tuiCallEngine); console.error(`${NAME.PREFIX}openCamera error: ${error}.`); } } public async closeCamera() { try { await this._tuiCallEngine.closeCamera(); setLocalUserInfoAudioVideoAvailable(false, NAME.VIDEO); } catch (error: any) { console.error(`${NAME.PREFIX}closeCamera error: ${error}.`); } } public async openMicrophone() { try { await this._tuiCallEngine.openMicrophone(); setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); } catch (error: any) { console.error(`${NAME.PREFIX}openMicrophone failed, error: ${error}.`); } } public async closeMicrophone() { try { await this._tuiCallEngine.closeMicrophone(); setLocalUserInfoAudioVideoAvailable(false, NAME.AUDIO); } catch (error: any) { console.error(`${NAME.PREFIX}closeMicrophone failed, error: ${error}.`); } } public switchScreen(userId: string) { if(!userId) return; TUIStore.update(StoreName.CALL, NAME.BIG_SCREEN_USER_ID, userId); } 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}.`); } } 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}.`); } } // ==========================【TUICallEngine 事件处理】========================== private _addListenTuiCallEngineEvent() { this._engineEventHandler.addListenTuiCallEngineEvent(); } private _removeListenTuiCallEngineEvent() { this._engineEventHandler.removeListenTuiCallEngineEvent(); } // 处理用户异常退出的情况, 小程序 ”右滑“、"左上角退出"; web 页面关闭浏览器或关闭 tab 页面 public async handleExceptionExit(event?: any) { try { const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS); const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE); this._tuiCallEngine?.reportLog?.({ name: 'TUICallkit.handleExceptionExit', data: { callStatus, callRole } }); if (callStatus === CallStatus.IDLE) return; // 在呼叫状态下,被叫调用 reject,主叫调用 hangup if (callStatus === CallStatus.CALLING) { if (callRole === CallRole.CALLER) { await this?.hangup(); } else { await this?.reject(); } } if (callStatus === CallStatus.CONNECTED) { await this?.hangup(); } this?._resetCallStore(); } catch (error) { console.error(`${NAME.PREFIX} handleExceptionExit failed, error: ${error}.`); } if (event) { event.returnValue = ''; } } // 通话时长更新 public startTimer(): void { if (this._timerId === -1) { this._startTimeStamp = performanceNow(); this._timerId = setInterval(() => this._updateCallDuration(), 1000); } } private _stopTimer(): void { if (this._timerId !== -1) { clearInterval(this._timerId); this._timerId = -1; } } // =========================【private methods for service use】========================= // 处理 “呼叫” 抛出的异常 private _handleCallError(error: any, methodName?: string) { this._permissionCheckTimer && clearInterval(this._permissionCheckTimer); if (handleRepeatedCallError(error)) return; noDevicePermissionToast(error, CallMediaType.AUDIO, this._tuiCallEngine); console.error(`${NAME.PREFIX}${methodName} failed, error: ${error}.`); this._resetCallStore(); throw error; } private async _updateCallStoreBeforeCall(type: number, remoteUserInfoList: IUserInfo[], groupID?: string): Promise { let callTips = 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 || remoteUserInfoList.length > 1), [NAME.CALL_TIPS]: callTips, [NAME.GROUP_ID]: groupID }; const callStatus = await beforeCall(type, this); // 如果没有权限, 此时为 false. 因此需要在 call 后设置为 calling. 和 web 存在差异 console.log(`${NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`); TUIStore.updateStore({ ...updateStoreParams, [NAME.CALL_STATUS]: callStatus }, StoreName.CALL); const remoteUserInfoLists = await getRemoteUserProfile(remoteUserInfoList.map(obj => obj.userId), this.getTim()); 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); } const deviceMap = { microphone: true, camera: type === CallMediaType.VIDEO, }; let hasDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap); if (!hasDevicePermission) { this._permissionCheckTimer && clearInterval(this._permissionCheckTimer); this._permissionCheckTimer = setInterval(async () => { hasDevicePermission = await this._tuiCallEngine.deviceCheck(deviceMap); if (hasDevicePermission && this._permissionCheckTimer) { clearInterval(this._permissionCheckTimer); TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CALLING); } }, 500); } } private async _updateCallStoreAfterCall(userIdList: string[], response: any) { const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); let localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); TUIStore.update(StoreName.CALL, NAME.CALL_INFO, { mediaType: callMediaType, inviterId: localUserInfo.userId }); if (response) { TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER); const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; if((callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose) { await this.openCamera(NAME.LOCAL_VIDEO); } 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 }); setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini, default open audio } else { this._permissionCheckTimer && clearInterval(this._permissionCheckTimer); this._permissionCheckTimer = null; this._resetCallStore(); } } private _getFeatureButtonDefaultState(buttonName: FeatureButton) { const { button: buttonConfig } = TUIStore.getData(StoreName.CALL, NAME.CUSTOM_UI_CONFIG); return buttonConfig?.[buttonName]?.state; } private _updateCallDuration(): void { TUIStore.update(StoreName.CALL, NAME.DURATION, Math.round((performanceNow() - this._startTimeStamp) / 1000)); } private _resetCallStore() { 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.ENABLE_FLOAT_WINDOW: case NAME.LOCAL_USER_INFO: case NAME.IS_FORCE_USE_V2_API: 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.reset(StoreName.CALL, [NAME.PUSHER_ID], true); // pusher unload 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, []); TUIStore.update(StoreName.CALL, NAME.CAMERA_POSITION, CameraPosition.FRONT); TUIStore.update(StoreName.CALL, NAME.CALL_INFO, {}); } // =========================【监听 TUIStore 中的状态及处理】========================= private _handleCallStatusChange = async (value: CallStatus) => { try { if (value === CallStatus.CALLING) { } 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); TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, ''); if (!isGroup && callMediaType === CallMediaType.VIDEO) { this.switchScreen(remoteUserInfoList[0].domId); } } } } 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, }); } // =========================【set、get methods】========================= 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 } public setIsFromChat(isFromChat: boolean) { this._isFromChat = isFromChat; } }