import { ITUIStore } from '../interface/ITUIStore'; import { TUICallEvent } from '@trtc/call-engine-lite-wx'; import { IUserInfo } from '../interface/ICallService'; import { CallMediaType } from '@trtc/call-engine-lite-wx'; import { StoreName, CallStatus, NAME, CallRole, ErrorCode, ErrorMessage, NETWORK_QUALITY_THRESHOLD } from '../const/index'; import { CallTips, t } from '../locales/index'; import { initAndCheckRunEnv } from './miniProgram'; import { getRemoteUserProfile, analyzeEventData, deleteRemoteUser } from './utils'; import TuiStore from '../TUIStore/tuiStore'; const TUIStore: ITUIStore = TuiStore.getInstance(); export default class EngineEventHandler { static instance: EngineEventHandler; private _callService: any; constructor(options) { this._callService = options.callService; } static getInstance(options) { if (!EngineEventHandler.instance) { EngineEventHandler.instance = new EngineEventHandler(options); } return EngineEventHandler.instance; } public addListenTuiCallEngineEvent() { const callEngine = this._callService?.getTUICallEngineInstance(); if (!callEngine) { console.warn(`${NAME.PREFIX}add engine event listener failed, engine is empty.`); return; } callEngine.on(TUICallEvent.ERROR, this._handleError, this); callEngine.on(TUICallEvent.ON_CALL_RECEIVED, this._handleNewInvitationReceived, this); // 收到邀请事件 TUICallEvent?.ON_CALL_BEGIN && callEngine.on(TUICallEvent.ON_CALL_BEGIN, this._handleOnCallBegin, this); // 主叫收到被叫接通事件 callEngine.on(TUICallEvent.USER_ENTER, this._handleUserEnter, this); // 有用户进房事件 callEngine.on(TUICallEvent.USER_LEAVE, this._handleUserLeave, this); // 有用户离开通话事件 callEngine.on(TUICallEvent.REJECT, this._handleInviteeReject, this); // 主叫收到被叫的拒绝通话事件 callEngine.on(TUICallEvent.NO_RESP, this._handleNoResponse, this); // 主叫收到被叫的无应答事件 callEngine.on(TUICallEvent.LINE_BUSY, this._handleLineBusy, this); // 主叫收到被叫的忙线事件 callEngine.on(TUICallEvent.ON_CALL_NOT_CONNECTED, this._handleCallNotConnected, this); // 主被叫在通话未建立时, 收到的取消事件 callEngine.on(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this); // 通话存在邀请他人时, 通话里的所有人都会抛出 callEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件 // @ts-ignore // TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.on(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this); // 用户网络质量 callEngine.on(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this); callEngine.on(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, this); callEngine.on(TUICallEvent.CALL_END, this._handleCallingEnd, this); // 主被叫在通话结束时, 收到的通话结束事件 } public removeListenTuiCallEngineEvent() { const callEngine = this._callService?.getTUICallEngineInstance(); callEngine.off(TUICallEvent.ERROR, this._handleError, this); callEngine.off(TUICallEvent.ON_CALL_RECEIVED, this._handleNewInvitationReceived, this); TUICallEvent?.ON_CALL_BEGIN && callEngine.off(TUICallEvent.ON_CALL_BEGIN, this._handleOnCallBegin, this); callEngine.off(TUICallEvent.USER_ENTER, this._handleUserEnter, this); callEngine.off(TUICallEvent.USER_LEAVE, this._handleUserLeave, this); callEngine.off(TUICallEvent.REJECT, this._handleInviteeReject, this); callEngine.off(TUICallEvent.NO_RESP, this._handleNoResponse, this); callEngine.off(TUICallEvent.LINE_BUSY, this._handleLineBusy, this); callEngine.off(TUICallEvent.ON_CALL_NOT_CONNECTED, this._handleCallNotConnected, this); callEngine.off(TUICallEvent.ON_USER_INVITING, this._handleOnUserInviting, this); callEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // @ts-ignore // TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.off(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this); callEngine.off(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this); callEngine.off(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, this); callEngine.off(TUICallEvent.CALL_END, this._handleCallingEnd, this); } 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._callService?.startTimer(); } } 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 } = analyzeEventData(event); let callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.OTHER_SIDE_REJECT_CALL : CallTips.OTHER_SIDE_LINE_BUSY; let userListNeedToShow = ''; if (isGroup) { userListNeedToShow = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId; callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.REJECT_CALL : CallTips.IN_BUSY; } TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: callTipsKey, options: { userList: userListNeedToShow } } }); userId && deleteRemoteUser([userId]); if (TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST).length === 0) { this._callService?._resetCallStore(); } break; } case TUICallEvent.NO_RESP: { const { userIDList = [] } = 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; }); TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: callTipsKey, options: { userList: userInfoList.join() } } }); userIDList.length > 0 && deleteRemoteUser(userIDList); break; } case TUICallEvent.ON_CALL_NOT_CONNECTED: { this._callService?._resetCallStore(); break; } } } 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, { content: ErrorMessage[key], type: NAME.ERROR }); } 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 { callerId = '', callMediaType, inviteData = {}, calleeIdList = [], chatGroupID: groupID = '', roomID, strRoomID } = analyzeEventData(event); const currentUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); const remoteUserIdList: string[] = [callerId, ...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]: (!!groupID || calleeIdList.length > 1), [NAME.CALL_STATUS]: CallStatus.CALLING, [NAME.CALL_MEDIA_TYPE]: type, [NAME.CALL_TIPS]: callTipsKey, [NAME.CALLER_USER_INFO]: { userId: callerId }, [NAME.GROUP_ID]: groupID, }; initAndCheckRunEnv(); this._callService.openCamera(type === CallMediaType.VIDEO); this._callService.openMicrophone() const deviceMap = { microphone: true, camera: type === CallMediaType.VIDEO, }; this._callService._preDevicePermission = await this._callService._tuiCallEngine.deviceCheck(deviceMap); TUIStore.updateStore(updateStoreParams, StoreName.CALL); const remoteUserInfoList = await getRemoteUserProfile(remoteUserIdList, this._callService?.getTim()); const [userInfo] = remoteUserInfoList.filter((userInfo: IUserInfo) => userInfo.userId === callerId); remoteUserInfoList.length > 0 && TUIStore.updateStore({ [NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, [NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList, [NAME.CALLER_USER_INFO]: { userId: callerId, nick: userInfo?.nick || '', avatar: userInfo?.avatar || '', displayUserInfo: userInfo?.remark || userInfo?.nick || callerId, }, }, StoreName.CALL); } private async _handleOnCallBegin(event: any): Promise { this._callerChangeToConnected(); TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, { text: 'answered', duration: 2000 }); await this._callService.openMicrophone(); console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`); // 通知聊天页:视频/语音通话已接通,可关闭等待计时器 try { uni.$emit('TUICall:callStart', event); } catch (_) {} } private async _handleUserEnter(event: any): Promise { const { userID: userId, data } = analyzeEventData(event); // 对方进入房间,通话已接通,通知聊天页关闭等待计时器 try { uni.$emit('TUICall:callStart', event); } catch (_) {} if (userId && TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE === CallRole.CALLEE)) { if (TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS) === CallStatus.CALLING) { this._callService._handleAcceptResponse({}); } } this._callerChangeToConnected(); await this._addUserToRemoteUserInfoList(userId); let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); 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 _handleUserLeave(event: any): void { console.log(`${NAME.PREFIX}userLeave event data: ${JSON.stringify(event)}.`); const { data, userID: userId } = analyzeEventData(event); if (TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)) { const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); const userListNeedToShow: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId; TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: { key: CallTips.END_CALL, options: { userList: userListNeedToShow } } }); } userId && deleteRemoteUser([userId]); } 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 _handleCallNotConnected(event: any): void { this._unNormalEventsManager(event, TUICallEvent.ON_CALL_NOT_CONNECTED); } private async _handleOnUserInviting(event: any) { const { userID: userId } = analyzeEventData(event); if (!userId) return; if (userId !== TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO).userId) { await this._addUserToRemoteUserInfoList(userId); } } private _handleCallingEnd(event: any): void { console.log(`${NAME.PREFIX}callEnd event data: ${JSON.stringify(event)}.`); this._callService?._resetCallStore(); // 通知聊天页等:视频通话已结束,可关闭等待计时器等 try { uni.$emit('TUICall:callEnd', event); } catch (_) {} } private _handleKickedOut(event: any): void { console.log(`${NAME.PREFIX}kickOut event data: ${JSON.stringify(event)}.`); this._callService?.kickedOut && this._callService?.kickedOut(event); TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.KICK_OUT); this._callService?._resetCallStore(); } // private _handleNetworkQuality(event) { // const { networkQualityList = [] } = analyzeEventData(event); // TUIStore.update(StoreName.CALL, NAME.NETWORK_STATUS, networkQualityList); // const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP); // const localUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); // const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // if(!isGroup) { // const isRemoteNetworkPoor = networkQualityList.find(user => remoteUserInfoList[0]?.userId === user?.userId && user?.quality >= NETWORK_QUALITY_THRESHOLD); // if(isRemoteNetworkPoor) { // TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.REMOTE_NETWORK_IS_POOR); // return; // }; // const isLocalNetworkPoor = networkQualityList.find(user => localUserInfo?.userId === user?.userId && user?.quality >= NETWORK_QUALITY_THRESHOLD); // if(isLocalNetworkPoor) { // TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.LOCAL_NETWORK_IS_POOR); // return; // } // } // } private async _startRemoteView(userId: string) { if (!userId) { console.warn(`${NAME.PREFIX}_startRemoteView userID is empty`); return; } try { const displayMode = TUIStore.getData(StoreName.CALL, NAME.DISPLAY_MODE); await this._callService?.getTUICallEngineInstance().startRemoteView({ userID: userId, videoViewDomID: `${userId}_0`, options: { objectFit: displayMode } }); } catch (error: any) { console.error(`${NAME.PREFIX}_startRemoteView error: ${error}.`); return Promise.reject(error); } } private _setRemoteUserInfoAudioVideoAvailable(isAvailable: boolean, type: string, userId: string) { let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); remoteUserInfoList = remoteUserInfoList.map((obj: IUserInfo) => { if (obj.userId === userId) { if (type === NAME.AUDIO) { return { ...obj, isAudioAvailable: isAvailable }; } if (type === NAME.VIDEO) { return { ...obj, isVideoAvailable: isAvailable }; } } 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); } } private async _handleUserVideoAvailable(event: any): Promise { const { userID: userId, isVideoAvailable } = analyzeEventData(event); console.log(`${NAME.PREFIX}_handleUserVideoAvailable event data: ${JSON.stringify(event)}.`); this._setRemoteUserInfoAudioVideoAvailable(isVideoAvailable, NAME.VIDEO, userId); try { isVideoAvailable && await this._startRemoteView(userId); } catch (error) { console.error(`${NAME.PREFIX}_startRemoteView failed, error: ${error}.`); } } private _handleUserAudioAvailable(event: any): void { const { userID: userId, isAudioAvailable } = analyzeEventData(event); console.log(`${NAME.PREFIX}_handleUserAudioAvailable event data: ${JSON.stringify(event)}.`); this._setRemoteUserInfoAudioVideoAvailable(isAudioAvailable, NAME.AUDIO, userId); } private async _addUserToRemoteUserInfoList(userId: string) { let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); const isInRemoteUserList = remoteUserInfoList.find(item => item?.userId === userId); if (!isInRemoteUserList) { remoteUserInfoList.push({ userId }); 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._callService?.getTim()); remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); remoteUserInfoList.forEach((obj) => { if (obj?.userId === userId) { obj = Object.assign(obj, userInfo); } }); TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList); TUIStore.update(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList); } } }