UNPKG

@tencentcloud/call-uikit-vue2

Version:

An Open-source Voice & Video Calling UI Component Based on Tencent Cloud Service.

842 lines (829 loc) 38 kB
import { ITUICallService, ICallParams, IGroupCallParams, IUserInfo, ICallbackParam, ISelfInfoParams, IBellParams, IInviteUserParams, IJoinInGroupCallParams, IInitParams, ICallsParams, } from '../interface/ICallService'; import { StoreName, CallStatus, CallMediaType, NAME, CALL_DATA_KEY, LanguageType, CallRole, LOG_LEVEL, VideoDisplayMode, VideoResolution, StatusChange, AudioPlayBackDevice, CameraPosition, COMPONENT, FeatureButton, ButtonState, LayoutMode, DEFAULT_BLUR_LEVEL, } from '../const/index'; // @ts-ignore import { TUICallEngine } from '@tencentcloud/call-engine-js'; import { checkLocalMP3FileExists } from '../utils/index'; import { CallTips, t } from '../locales/index'; import { BellContext } from './bellContext'; import { VALIDATE_PARAMS, avoidRepeatedCall, paramValidate, statusValidate } from '../utils/validate/index'; import { handleRepeatedCallError, formatTime, performanceNow } from '../utils/common-utils'; import { getRemoteUserProfile, generateStatusChangeText, noDevicePermissionToast, setLocalUserInfoAudioVideoAvailable, getGroupMemberList, getGroupProfile, updateRoomIdAndRoomIdType, updateDeviceList } from './utils'; import timer from '../utils/timer'; import { ITUIGlobal, ITUIStore } from '../interface/index'; import TuiGlobal from '../TUIGlobal/tuiGlobal'; import TuiStore from '../TUIStore/tuiStore'; import { UIDesign } from './UIDesign'; import ChatCombine from './chatCombine'; import EngineEventHandler from './engineEventHandler'; const TUIGlobal: ITUIGlobal = TuiGlobal.getInstance(); const TUIStore: ITUIStore = TuiStore.getInstance(); const uiDesign = UIDesign.getInstance(); uiDesign.setTUIStore(TUIStore); const version = '4.0.4'; const frameWork = 'vue2.7'; export { TUIGlobal, TUIStore, uiDesign }; 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 _isFromChat: boolean = false; private _currentGroupId: string = ''; // The currentGroupId of the group chat that the user is currently in 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; } @avoidRepeatedCall() @paramValidate(VALIDATE_PARAMS.init) 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; 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, SDKAppID: sdkAppID || SDKAppID, // 兼容传入 SDKAppID 的问题 frameWork, // @ts-ignore language: 5, callkitVersion: version, isFromChat: isFromChat || false, component, }); uiDesign.setEngineInstance(this._tuiCallEngine); this._addListenTuiCallEngineEvent(); this._bellContext = new BellContext(); TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO, { userId: userID }); TUIStore.update(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { userId: userID }); uiDesign.updateViewBackgroundUserId('local'); 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._bellContext?.destroy(); this._bellContext = null; } catch (error) { console.error(`${NAME.PREFIX}destroyed failed, error: ${error}.`); throw error; } } // ===============================【通话操作】=============================== @avoidRepeatedCall() @paramValidate(VALIDATE_PARAMS.call) @statusValidate({ engineInstance: true }) public async call(callParams: ICallParams) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; // avoid double click when application stuck 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.getDefaultOfflinePushInfo(), ...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) @statusValidate({ engineInstance: true }) public async groupCall(groupCallParams: IGroupCallParams) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; // avoid double click when application stuck 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.getDefaultOfflinePushInfo(), ...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) @statusValidate({ engineInstance: true }) public async inviteUser(params: IInviteUserParams) { 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}.`); } } @avoidRepeatedCall() @paramValidate(VALIDATE_PARAMS.joinInGroupCall) @statusValidate({ engineInstance: true }) public async joinInGroupCall(params: IJoinInGroupCallParams) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CONNECTED) return; // avoid double click when application stuck 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); const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; (params.type === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO); TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); this.startTimer(); updateDeviceList(this._tuiCallEngine);; await this._tuiCallEngine.setVideoQuality(TUIStore.getData(StoreName.CALL, NAME.VIDEO_RESOLUTION)); 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); } catch (error) { this._handleCallError(error, 'joinInGroupCall'); } } @avoidRepeatedCall() @statusValidate({ engineInstance: true }) public async calls(callsParams: ICallsParams) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) !== CallStatus.IDLE) return; // avoid double click when application stuck try { 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); this.executeExternalBeforeCalling(); callsParams.offlinePushInfo = { ...this.getDefaultOfflinePushInfo(), ...offlinePushInfo }; const response = await this._tuiCallEngine.calls(callsParams); await this._updateCallStoreAfterCall(userIDList, response); } catch (error: any) { this._handleCallError(error, 'calls'); } } @avoidRepeatedCall() @statusValidate({ engineInstance: true }) 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); const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; (params.type === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO); 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]: params.type, [NAME.GROUP_ID]: params.groupID, [NAME.ROOM_ID]: params.roomID, }; TUIStore.updateStore(updateStoreParams, StoreName.CALL); updateDeviceList(this._tuiCallEngine);; await this._tuiCallEngine.setVideoQuality(TUIStore.getData(StoreName.CALL, NAME.VIDEO_RESOLUTION)); 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); } catch (error) { this._handleCallError(error, 'join'); } } // ===============================【其它对外接口】=============================== public getTUICallEngineInstance(): any { return this?._tuiCallEngine || null; } public setLogLevel(level: LOG_LEVEL) { this?._tuiCallEngine?.setLogLevel(level); } @paramValidate(VALIDATE_PARAMS.setLanguage) public setLanguage(language: string) { if (language && Object.values(LanguageType).includes(language as LanguageType)) { TUIStore.update(StoreName.CALL, NAME.LANGUAGE, language); TUIStore.update(StoreName.CALL, NAME.TRANSLATE, t.bind(null)); } } @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}.`); } } public async enableVirtualBackground(enable: boolean) { TUIStore.update(StoreName.CALL, NAME.IS_SHOW_ENABLE_VIRTUAL_BACKGROUND, enable); } @paramValidate(VALIDATE_PARAMS.enableAIVoice) public async enableAIVoice(enable: boolean) { try { await this._tuiCallEngine.enableAIVoice(enable); console.log(`${NAME.PREFIX}enableAIVoice: ${enable}.`); } catch (error: any) { console.error(`${NAME.PREFIX}enableAIVoice failed, error: ${error}.`); throw error; } } // 修改默认铃声:只支持本地铃声文件,不支持在线铃声文件;修改铃声修改的是被叫的铃声 @paramValidate(VALIDATE_PARAMS.setCallingBell) public async setCallingBell(filePath?: string) { let isCheckFileExist: boolean = true; isCheckFileExist = await checkLocalMP3FileExists(filePath); 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}.`); } } public hideFeatureButton(buttonName: FeatureButton) { uiDesign.hideFeatureButton(buttonName); } public setLocalViewBackgroundImage(url: string) { uiDesign.setLocalViewBackgroundImage(url); } public setRemoteViewBackgroundImage(userId: string, url: string) { uiDesign.setRemoteViewBackgroundImage(userId, url); } public setLayoutMode(layoutMode: LayoutMode) { uiDesign.setLayoutMode(layoutMode); } public setCameraDefaultState(isOpen: boolean) { uiDesign.setCameraDefaultState(isOpen); } // =============================【实验性接口】============================= 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 } }); } } // =============================【内部按钮操作方法】============================= @avoidRepeatedCall() 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 { TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED); updateDeviceList(this._tuiCallEngine);; const response = await this._tuiCallEngine.accept(); if (response) { this._chatCombine?.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); const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; (callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO); await this._tuiCallEngine.setVideoQuality(TUIStore.getData(StoreName.CALL, NAME.VIDEO_RESOLUTION)); 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 } } catch (error) { this._tuiCallEngine?.reportLog?.({ name: 'TUICallKit.accept.fail', level: 'error', error, }); if (handleRepeatedCallError(error)) return; noDevicePermissionToast(error, CallMediaType.AUDIO, this._tuiCallEngine); this._resetCallStore(); } } @avoidRepeatedCall() public async hangup() { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck try { const response = await this._tuiCallEngine.hangup(); response?.forEach((item) => { if (item?.code === 0) { this._chatCombine?.callTUIService({ message: item?.data?.message }); } }); } catch (error) { console.debug(error); } this._resetCallStore(); } @avoidRepeatedCall() public async reject() { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.IDLE) return; // avoid double click when application stuck try { const response = await this._tuiCallEngine.reject(); if (response?.code === 0) { this._chatCombine?.callTUIService({ message: response?.data?.message }); } } catch (error) { console.debug(error); } this._resetCallStore(); } @avoidRepeatedCall() public async openCamera(videoViewDomID: string) { try { if (TUIGlobal.isH5 || TUIGlobal.isWeChat) { const currentPosition = TUIStore.getData(StoreName.CALL, NAME.CAMERA_POSITION); const isFrontCamera = currentPosition === CameraPosition.FRONT ? true : false; this._tuiCallEngine.openCamera(videoViewDomID, isFrontCamera); } else { await this._tuiCallEngine.openCamera(videoViewDomID); } setLocalUserInfoAudioVideoAvailable(true, NAME.VIDEO); } catch (error: any) { noDevicePermissionToast(error, CallMediaType.VIDEO, this._tuiCallEngine); console.error(`${NAME.PREFIX}openCamera error: ${error}.`); } } @avoidRepeatedCall() public async closeCamera() { try { await this._tuiCallEngine.closeCamera(); setLocalUserInfoAudioVideoAvailable(false, NAME.VIDEO); } catch (error: any) { console.error(`${NAME.PREFIX}closeCamera error: ${error}.`); } } @avoidRepeatedCall() public async openMicrophone() { try { await this._tuiCallEngine.openMicrophone(); setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); } catch (error: any) { console.error(`${NAME.PREFIX}openMicrophone failed, error: ${error}.`); } } @avoidRepeatedCall() public async closeMicrophone() { try { await this._tuiCallEngine.closeMicrophone(); setLocalUserInfoAudioVideoAvailable(false, NAME.AUDIO); } catch (error: any) { console.error(`${NAME.PREFIX}closeMicrophone failed, error: ${error}.`); } } @avoidRepeatedCall() public unMuteSpeaker() { try { const trtcCloudInstance = this._tuiCallEngine?.getTRTCCloudInstance?.(); if (trtcCloudInstance) { trtcCloudInstance.muteAllRemoteAudio(false); TUIStore.update(StoreName.CALL, NAME.IS_MUTE_SPEAKER, false); } } catch (error: any) { console.error(`${NAME.PREFIX}unMuteSpeaker failed, error: ${error}.`); } } @avoidRepeatedCall() public muteSpeaker() { try { const trtcCloudInstance = this._tuiCallEngine?.getTRTCCloudInstance?.(); if (trtcCloudInstance) { trtcCloudInstance.muteAllRemoteAudio(true); TUIStore.update(StoreName.CALL, NAME.IS_MUTE_SPEAKER, true); } } catch (error: any) { console.error(`${NAME.PREFIX}muteSpeaker 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._chatCombine?.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(); this.statusChanged && this.statusChanged({ oldStatus, newStatus }); } 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 async setBlurBackground(enable: boolean) { try { await this._tuiCallEngine.setBlurBackground(enable ? DEFAULT_BLUR_LEVEL : 0); // 0 indicate close blurBackground TUIStore.update(StoreName.CALL, NAME.ENABLE_VIRTUAL_BACKGROUND, enable); } catch (error) { console.error(`${NAME.PREFIX}_setBlurBackground failed, error: ${error}.`); } } @avoidRepeatedCall() public async switchDevice(params) { try { await this._tuiCallEngine.switchDevice(params); } catch (error) { console.error(`${NAME.PREFIX}_switchDevice failed, error: ${error}.`); } } public async getDeviceList(deviceType: string) { try { const response = await this._tuiCallEngine.getDeviceList(deviceType); return response; } catch (error: any) { this._handleCallError(error, 'call'); } }; // ==========================【TUICallEngine 事件处理】========================== private _addListenTuiCallEngineEvent() { this._engineEventHandler.addListenTuiCallEngineEvent(); } private _removeListenTuiCallEngineEvent() { this._engineEventHandler.removeListenTuiCallEngineEvent(); } // ========================【原 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); } public executeExternalBeforeCalling(): void { this.beforeCalling && this.beforeCalling(); } public 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}.`); } } // 通话时长更新 public startTimer(): void { if (this._timerId === -1) { this._startTimeStamp = performanceNow(); this._timerId = timer.run(NAME.TIMEOUT, this._updateCallDuration.bind(this), { delay: 1000 }); } } // =========================【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<void> { let callTips = CallTips.CALLER_CALLING_MSG; if (groupID || TUIStore.getData(StoreName.CALL, NAME.IS_MINIMIZED) || remoteUserInfoList.length > 1) { callTips = CallTips.CALLER_GROUP_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 }; TUIStore.updateStore({ ...updateStoreParams, [NAME.CALL_STATUS]: CallStatus.CALLING }, StoreName.CALL); this.statusChanged && this.statusChanged({ oldStatus: StatusChange.IDLE, newStatus: (groupID || remoteUserInfoList.length > 1) ? StatusChange.DIALING_GROUP : StatusChange.DIALING_C2C, }); updateDeviceList(this._tuiCallEngine);; 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); } } private async _updateCallStoreAfterCall(userIdList: string[], response: any) { if (response) { TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true); updateRoomIdAndRoomIdType(response?.roomID, response?.strRoomID); const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE); if (response.code === 0) { this._chatCombine?.callTUIService({ message: response?.data?.message }); try { await this._tuiCallEngine.setVideoQuality(TUIStore.getData(StoreName.CALL, NAME.VIDEO_RESOLUTION)); } catch (error) { console.warn(`${NAME.PREFIX}setVideoQuality failed, error: ${error}.`); } } else { this._resetCallStore(); return; } const isCameraDefaultStateClose = this._getFeatureButtonDefaultState(FeatureButton.Camera) === ButtonState.Close; (callMediaType === CallMediaType.VIDEO) && !isCameraDefaultStateClose && await this.openCamera(NAME.LOCAL_VIDEO); 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 } 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 { 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 _resetCallStore() { const oldStatusStr = generateStatusChangeText(); 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.IS_SHOW_ENABLE_VIRTUAL_BACKGROUND: 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.ENABLE_VIRTUAL_BACKGROUND], true); // ENABLE_VIRTUAL_BACKGROUND reset need notify TUIStore.reset(StoreName.CALL, [NAME.IS_MUTE_SPEAKER], true); // IS_MUTE_SPEAKER 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); const newStatusStr = generateStatusChangeText(); if (oldStatusStr !== newStatusStr) { this.statusChanged && this.statusChanged({ oldStatus: oldStatusStr, newStatus: newStatusStr }); } } // =========================【Calling the Chat SDK 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() }); if (!isGroup && callMediaType === CallMediaType.VIDEO) { this.switchScreen(remoteUserInfoList[0].domId); } } if (value === CallStatus.IDLE) { if (this._isFromChat) { const groupAttributes = this._currentGroupId ? await this._chatCombine?.getGroupAttributes(this._tim, this._currentGroupId) : {}; await this._chatCombine?.updateStoreBasedOnGroupAttributes(groupAttributes, TUIStore, this); } } 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, }); } // =========================【融合 chat 】========================= public bindTUICore(TUICore: any) { this._TUICore = TUICore; } // =========================【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; } public setCurrentGroupId(groupId: string) { this._currentGroupId = groupId; } public getCurrentGroupId() { return this._currentGroupId; } public setDefaultOfflinePushInfo(offlinePushInfo) { this._offlinePushInfo = offlinePushInfo; } public getDefaultOfflinePushInfo() { const localUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); if (this._offlinePushInfo) { return this._offlinePushInfo; } return { title: localUserInfo?.displayUserInfo || '', description: t('you have a new call'), }; } public async getCallMessage(message) { return await this._chatCombine.getCallKitMessage(message, this.getTim()); } }