UNPKG

@tencentcloud/call-uikit-vue

Version:

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

405 lines (372 loc) 20.9 kB
import { ITUIStore } from '../interface/ITUIStore'; // @ts-ignore import { TUICallEvent } from '@tencentcloud/call-engine-js'; import { IUserInfo } from '../interface/ICallService'; import { StoreName, CallStatus, CallMediaType, NAME, CallRole, StatusChange, ErrorCode, ErrorMessage, AudioPlayBackDevice, NETWORK_QUALITY_THRESHOLD } from '../const/index'; import { CallTips, t } from '../locales/index'; import { initAndCheckRunEnv } from './miniProgram'; import { getMyProfile, getRemoteUserProfile, updateRoomIdAndRoomIdType, analyzeEventData, deleteRemoteUser } from './utils'; import promiseRetryDecorator from '../utils/decorators/promise-retry'; import { UIDesign } from './UIDesign'; import TuiStore from '../TUIStore/tuiStore'; const TUIStore: ITUIStore = TuiStore.getInstance(); const uiDesign = UIDesign.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); // 收到邀请事件 callEngine.on(TUICallEvent.USER_ACCEPT, this._handleUserAccept, 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.SDK_READY, this._handleSDKReady, this); // SDK Ready 回调 callEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件 callEngine.on(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this); // @ts-ignore TUICallEvent.CALL_MESSAGE && callEngine.on(TUICallEvent.CALL_MESSAGE, this._handleCallMessage, this); // call message card display event // @ts-ignore TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.on(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this); // 用户网络质量 callEngine.on(TUICallEvent.CALLING_END, this._handleCallingEnd, this); // 主被叫在通话结束时, 收到的通话结束事件 callEngine.on(TUICallEvent.CALL_TYPE_CHANGED, this._handleCallTypeChange, this); callEngine.on(TUICallEvent.USER_VOICE_VOLUME, this._handleUserVoiceVolume, this); callEngine.on(TUICallEvent.DEVICED_UPDATED, this._handleDeviceUpdate, this); callEngine.on(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this); callEngine.on(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, this); } public removeListenTuiCallEngineEvent() { const callEngine = this._callService?.getTUICallEngineInstance(); callEngine.off(TUICallEvent.ERROR, this._handleError, this); callEngine.off(TUICallEvent.ON_CALL_RECEIVED, this._handleNewInvitationReceived, this); callEngine.off(TUICallEvent.USER_ACCEPT, this._handleUserAccept, 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.SDK_READY, this._handleSDKReady, this); callEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); callEngine.off(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this); // @ts-ignore TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED && callEngine.off(TUICallEvent.ON_USER_NETWORK_QUALITY_CHANGED, this._handleNetworkQuality, this); callEngine.off(TUICallEvent.CALLING_END, this._handleCallingEnd, this); callEngine.off(TUICallEvent.CALL_TYPE_CHANGED, this._handleCallTypeChange, this); // TODO: web 是 CALL_TYPE_CHANGED 事件, miniProgram 为 CALL_MODE callEngine.off(TUICallEvent.USER_VOICE_VOLUME, this._handleUserVoiceVolume, this); // web callEngine.off(TUICallEvent.DEVICED_UPDATED, this._handleDeviceUpdate, this); callEngine.off(TUICallEvent.USER_VIDEO_AVAILABLE, this._handleUserVideoAvailable, this); callEngine.off(TUICallEvent.USER_AUDIO_AVAILABLE, this._handleUserAudioAvailable, 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 }); } this._callService?.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 { 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, }; updateRoomIdAndRoomIdType(roomID, strRoomID); TUIStore.updateStore(updateStoreParams, StoreName.CALL); this._callService?.executeExternalBeforeCalling(); this._callService?.statusChanged && this._callService?.statusChanged({ oldStatus: StatusChange.IDLE, newStatus: StatusChange.BE_INVITED }); 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 _handleUserAccept(event: any): void { this._callerChangeToConnected(); TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, { text: 'answered', duration: 2000 }); console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`); } private async _handleUserEnter(event: any): Promise<void> { this._callerChangeToConnected(); const { userID: userId, data } = analyzeEventData(event); 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); uiDesign.updateViewBackgroundUserId('remote'); } 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._callService?.executeExternalAfterCalling(); this._unNormalEventsManager(event, TUICallEvent.ON_CALL_NOT_CONNECTED); } private async _handleOnUserInviting(event: any) { const { userID: userId } = analyzeEventData(event); if (!userId) return; TUIStore.update(StoreName.CALL, NAME.IS_GROUP, true); 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?.executeExternalAfterCalling(); this._callService?._resetCallStore(); } // SDK_READY 后才能调用 tim 接口, 否则登录后立刻获取导致调用接口失败. v2.27.4+、v3 接口 login 后会抛出 SDK_READY private async _handleSDKReady(event: any): Promise<void> { let localUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); localUserInfo = await getMyProfile(localUserInfo.userId, this._callService?.getTim()); 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._callService?.kickedOut && this._callService?.kickedOut(event); TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, CallTips.KICK_OUT); this._callService?._resetCallStore(); } private _messageSentByMe(event: any): void { const message = event?.data; this._callService?.onMessageSentByMe && this._callService?.onMessageSentByMe(message); } private _handleCallMessage(event: any) { const message = analyzeEventData(event); this._callService._chatCombine.callTUIService({ message }); } private _handleCallTypeChange(event: any): void { const { newCallType, type } = analyzeEventData(event); TUIStore.update(StoreName.CALL, NAME.CALL_MEDIA_TYPE, newCallType || type); } 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 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; } } } // =============================【 WEB 私有事件】============================== @promiseRetryDecorator({ retries: 5, timeout: 200, onRetrying(retryCount) { console.warn(`${NAME.PREFIX}_startRemoteView, retrying [${retryCount}]`); }, }) private async _startRemoteView(userId: string) { if (!userId) { console.warn(`${NAME.PREFIX}_startRemoteView userID is empty`); return; } if (!document.getElementById(userId)) { console.warn(`${NAME.PREFIX}_startRemoteView can't find HTMLElement sid: ${userId}`); return Promise.reject(); } try { const displayMode = TUIStore.getData(StoreName.CALL, NAME.DISPLAY_MODE); await this._callService?.getTUICallEngineInstance().startRemoteView({ userID: userId, videoViewDomID: userId, 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<any> { const { userID: userId, isVideoAvailable } = analyzeEventData(event); console.log(`${NAME.PREFIX}_handleUserVideoAvailable event data: ${JSON.stringify(event)}.`); try { isVideoAvailable && await this._startRemoteView(userId); } catch (error) { console.error(`${NAME.PREFIX}_startRemoteView failed, error: ${error}.`); } this._setRemoteUserInfoAudioVideoAvailable(isVideoAvailable, NAME.VIDEO, userId); } 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 _handleUserVoiceVolume(event: any): void { try { const { volumeMap: volumeList } = analyzeEventData(event); if ((volumeList || []).length === 0) return; // 减少不必要的更新 const localUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO); let remoteUserInfoList: IUserInfo[] = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); const [localUserVolumeObj] = volumeList.filter((obj: any) => obj.userId === localUserInfo.userId); const remoteUserVolumeObj = volumeList.reduce((acc: any, obj: any) => { if (obj.userId !== localUserInfo.userId) { return { ...acc, [obj.userId]: obj.audioVolume }; } return acc; }, {}); localUserInfo.volume = localUserVolumeObj.audioVolume; remoteUserInfoList = remoteUserInfoList.map((obj: any) => ({ ...obj, volume: remoteUserVolumeObj[obj.userId] })); const updateStoreParams = { [NAME.LOCAL_USER_INFO]: localUserInfo, [NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList, }; TUIStore.updateStore(updateStoreParams, StoreName.CALL); } catch (error) { console.debug(error); } } private _handleDeviceUpdate(event: any): void { const { cameraList, microphoneList, speakerList, currentCamera, currentMicrophone, currentSpeaker } = event; TUIStore.update(StoreName.CALL, NAME.DEVICE_LIST, { cameraList, microphoneList, speakerList, currentCamera, currentMicrophone, currentSpeaker }); } 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); } } }