This commit is contained in:
Your Name
2026-03-11 09:49:47 +08:00
parent 02ae537b4c
commit 38ad60f4bb
290 changed files with 36917 additions and 123 deletions
@@ -0,0 +1,46 @@
import { UIKitModal } from '../../../components/UIKitModal'
const MINI_MODAL_ERROR_CODES = [
-1001,
-1002,
101002,
];
const MINI_MODAL_ERROR_MAP = {
'-1001': {
id: 10001,
content: '您的应用还未开通音视频通话能力'
},
'-1002': {
id: 10002,
content: '您暂不支持使用该能力,请前往购买页购买开通'
},
'101002': {
id: 10014,
content: '发起通话失败,用户 ID 无效,请确认该用户已注册'
}
}
export function handleModalError(error) {
if (!error || !error?.code) {
return;
}
handleMiniModalError(error);
}
function handleMiniModalError(error) {
if (!MINI_MODAL_ERROR_CODES.includes(error.code)) {
return;
}
const errorInfo = MINI_MODAL_ERROR_MAP[error.code.toString()];
if (errorInfo) {
UIKitModal.openModal({
id: errorInfo.id,
content: errorInfo.content,
title: '错误',
type: 'error'
});
}
}
@@ -0,0 +1,47 @@
import { TUIBridge } from '../../../TUIBridge/index';
import { LOG_LEVEL } from '@trtc/call-engine-lite-wx';
import { COMPONENT, NAME, EVENT } from '../const/index';
export default class ChatCombine {
static instance: ChatCombine;
private _callService: any;
constructor(options) {
this._callService = options.callService;
TUIBridge.registerEvent(EVENT.LOGIN_SUCCESS, this);
TUIBridge.registerEvent(EVENT.LOGOUT_SUCCESS, this);
TUIBridge.registerEvent(EVENT.ON_CALLS, this);
}
static getInstance(options) {
if (!ChatCombine.instance) {
ChatCombine.instance = new ChatCombine(options);
}
return ChatCombine.instance;
}
/**
* tuicore notify event manager
* @param {String} eventName event name
* @param {String} subKey sub key
* @param {Any} options tuicore event parameters
*/
public async onNotifyEvent(options?: any) {
try {
if (options?.eventName === EVENT.LOGIN_SUCCESS) {
const { chat, userID, userSig, SDKAppID } = options?.params || {};
await this._callService?.init({ tim: chat, userID, userSig, sdkAppID: SDKAppID, isFromChat: true, component: COMPONENT.TIM_CALL_KIT });
this._callService?.setIsFromChat(true);
this._callService?.setLogLevel(LOG_LEVEL.NORMAL);
}
if (options?.eventName === EVENT.LOGOUT_SUCCESS) {
await this._callService?.destroyed();
}
if (options?.eventName === EVENT.ON_CALLS) {
options?.params && this._callService.calls(options.params);
}
} catch (error) {
console.error(`${NAME.PREFIX}TUICore onNotifyEvent failed, error: ${error}.`);
}
}
}
@@ -0,0 +1,334 @@
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<void> {
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)}.`);
}
private async _handleUserEnter(event: any): Promise<void> {
const { userID: userId, data } = analyzeEventData(event);
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();
}
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<any> {
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);
}
}
}
@@ -0,0 +1,543 @@
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<void> {
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;
}
}
@@ -0,0 +1,66 @@
import { CallMediaType } from '@trtc/call-engine-lite-wx';
import { CallStatus } from '../const/index';
export function initialUI() {
// 收起键盘
// @ts-ignore
wx.hideKeyboard && wx.hideKeyboard({
complete: () => {},
});
};
// 检测运行时环境, 当是微信开发者工具时, 提示用户需要手机调试
export function checkRunPlatform() {
// @ts-ignore
const systemInfo = wx.getSystemInfoSync();
if (systemInfo.platform === 'devtools') {
// 当前运行在微信开发者工具里
// @ts-ignore
wx.showModal({
icon: 'none',
title: '运行环境提醒',
content: '微信开发者工具不支持原生推拉流组件(即 <live-pusher> 和 <live-player> 标签),请使用真机调试或者扫码预览。',
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,
});
}
}
export function handleNoPusherCapabilityError(){
// @ts-ignore
wx.showModal({
icon: 'none',
title: '权限提示',
content: '当前小程序 appid 不具备 <live-pusher> 和 <live-player> 的使用权限,您将无法正常使用实时通话能力,请使用企业小程序账号申请权限后再进行开发体验',
showCancel: false,
});
}
@@ -0,0 +1,199 @@
import { CallMediaType } from '@trtc/call-engine-lite-wx';
import { NAME, StoreName } from '../const/index';
import { handleNoDevicePermissionError } from '../utils/common-utils';
import { IUserInfo } from '../interface/ICallService';
import { ITUIStore } from '../interface/ITUIStore';
import { CallTips, t } from '../locales/index';
import TuiStore from '../TUIStore/tuiStore';
// @ts-ignore
const TUIStore: ITUIStore = TuiStore.getInstance();
// 设置默认的 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): Promise<IUserInfo> {
// 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<string>, tim: any): Promise<any> {
let remoteUserInfoList: IUserInfo[] = userIdList.map((userId: string) => setDefaultUserInfo(userId));
try {
if (!tim) return remoteUserInfoList;
if (tim?.getFriendProfile) {
// 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;
} else {
const res = await tim.getUserProfile({ userIDList: userIdList });
const currentRemoteUserInfoList = TUIStore?.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST); // remoteUserInfoList may have been updated
remoteUserInfoList = userIdList.map((userId: string) => {
const defaultUserInfo: IUserInfo = setDefaultUserInfo(userId);
const userInfo = currentRemoteUserInfoList.find(subObj => subObj.userId === userId) || {};
const userData = (res.data || []).find(obj => obj.userID === userId) || {};
return {
...defaultUserInfo,
...userInfo,
nick: userData?.nick || '',
displayUserInfo: userData?.nick || userId,
avatar: userData?.avatar || '',
};
});
return remoteUserInfoList;
}
} catch (error) {
console.error(`${NAME.PREFIX}getRemoteUserProfile failed, error: ${error}.`);
return remoteUserInfoList;
}
}
// 获取群组[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<any> {
// let groupProfile = {};
// try {
// const res = await tim.getGroupProfile({ groupID });
// return res.data.group || groupProfile;
// } catch(error) {
// console.warn(`${NAME.PREFIX}getGroupProfile failed, error: ${error}.`);
// return groupProfile;
// }
// }
/**
* web and miniProgram call engine throw event data structure are different
* @param {any} event call engine throw out data
* @returns {any} data
*/
export function analyzeEventData(event: any): any {
return event || {};
}
/**
* delete user from remoteUserInfoList
* @param {string[]} userIdList to be deleted userIdList
* @param {ITUIStore} TUIStore TUIStore instance
*/
export function 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);
}
/**
* update the no device permission toast
* @param {any} error error
* @param {CallMediaType} type call midia type
* @param {any} tuiCallEngine TUICallEngine instance
*/
export function noDevicePermissionToast(error, type: CallMediaType, tuiCallEngine: any) {
let toastInfoKey = '';
if (handleNoDevicePermissionError(error)) {
if (type === CallMediaType.AUDIO) {
toastInfoKey = CallTips.NO_MICROPHONE_DEVICE_PERMISSION;
}
if (type === CallMediaType.VIDEO) {
toastInfoKey = CallTips.NO_CAMERA_DEVICE_PERMISSION;
}
toastInfoKey && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { content: toastInfoKey, type: NAME.ERROR });
console.error(`${NAME.PREFIX}call failed, error: ${error.message}.`);
}
}
/**
* set localUserInfo audio/video available
* @param {boolean} isAvailable is available
* @param {string} type callMediaType 'audio' | 'video'
* @param {ITUIStore} TUIStore TUIStore instance
*/
export function 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);
}