3.18 患者端

This commit is contained in:
zoujiandong
2024-03-18 16:43:38 +08:00
parent faa8070ec3
commit 43153c15a7
102 changed files with 4477 additions and 357 deletions
+2
View File
@@ -43,6 +43,7 @@ Component({
callStatus: value,
});
if(value=="calling"){
TUICallKitServer.setLogLevel(0);
if(this.data.callerUserInfo.userId){
let chat=TUICallKitServer.getTim();
let promise=chat.getUserProfile({
@@ -210,6 +211,7 @@ Component({
[PUSHER]: this.handlePusherChange.bind(that),
[PLAYER]: this.handlePlayerListChange.bind(that),
});
},
},
});
@@ -8,7 +8,7 @@ Component({
callStatus: {
type: String,
observer(callStatus) {
console.log("改编状态qq"+callStatus);
console.log("改编状态:"+callStatus);
}
},
callMediaType: {
@@ -44,13 +44,22 @@ Component({
type: Boolean
}
},
pageLifetimes: {
show: function() {
// 页面被展示
lifetimes: {
moved(){
//console.log('moved')
},
hide: function() {
// 页面被隐藏
this.hangup()
detached(){
console.log('detached')
this.reject()
}
},
pageLifetimes: {
show() {
},
hide() {
},
resize: function(size) {
// 页面尺寸变化
@@ -71,8 +80,12 @@ Component({
doctorInfo:{}
},
methods: {
async accept() {
await TUICallKitServer.accept();
accept() {
//延迟接听;
setTimeout(async()=>{
await TUICallKitServer.enableMuteMode(false);
await TUICallKitServer.accept();
},800)
},
async hangup() {
await TUICallKitServer.hangup();
@@ -0,0 +1,86 @@
import { CallStatus, NAME, CallRole } from '../const/index';
import { IBellParams } from '../interface/index';
import { isUndefined } from '../utils/common-utils';
const DEFAULT_CALLER_BELL_FILEPATH = '/TUICallKit/static/phone_dialing.mp3';
const DEFAULT_CALLEE_BELL_FILEPATH = '/TUICallKit/static/phone_ringing.mp3';
export class BellContext {
private _bellContext: any = null;
private _isMuteBell: boolean = false;
private _calleeBellFilePath: string = DEFAULT_CALLEE_BELL_FILEPATH;
private _callRole: string = CallRole.UNKNOWN;
private _callStatus: string = CallStatus.IDLE;
constructor() {
// @ts-ignore
this._bellContext = wx.createInnerAudioContext();
this._bellContext.loop = true;
}
setBellSrc() {
// @ts-ignore
const fs = wx.getFileSystemManager();
try {
let playBellFilePath = DEFAULT_CALLER_BELL_FILEPATH;
if (this._callRole === CallRole.CALLEE) {
playBellFilePath = this._calleeBellFilePath || DEFAULT_CALLEE_BELL_FILEPATH;
}
fs.readFileSync(playBellFilePath, 'utf8', 0);
this._bellContext.src = playBellFilePath;
} catch (error) {
console.warn(`${NAME.PREFIX}Failed to setBellSrc, ${error}`);
}
}
setBellProperties(bellParams: IBellParams) {
this._callRole = bellParams.callRole || this._callRole;
this._callStatus = bellParams.callStatus || this._callStatus;
this._calleeBellFilePath = bellParams.calleeBellFilePath || this._calleeBellFilePath;
// undefined/false || isMuteBell => isMuteBell (不符合预期)
this._isMuteBell = isUndefined(bellParams.isMuteBell) ? this._isMuteBell : bellParams.isMuteBell;
}
async play() {
try {
if (this._callStatus !== CallStatus.CALLING) {
return ;
}
this.setBellSrc();
if (this._callRole === CallRole.CALLEE && !this._isMuteBell) {
await this._bellContext.play();
}
if (this._callRole === CallRole.CALLER) {
await this._bellContext.play();
}
} catch (error) {
console.warn(`${NAME.PREFIX}Failed to play audio file, ${error}`);
}
}
async stop() {
try {
this._bellContext.stop();
} catch (error) {
console.warn(`${NAME.PREFIX}Failed to stop audio file, ${error}`);
}
}
async setBellMute(enable: boolean) {
if (this._callStatus !== CallStatus.CALLING && this._callRole !== CallRole.CALLEE) {
return;
}
if (enable) {
await this.stop();
} else {
await this.play();
}
}
destroy() {
try {
this._isMuteBell = false;
this._calleeBellFilePath = '';
this._callRole = CallRole.UNKNOWN;
this._callStatus = CallStatus.IDLE;
this._bellContext.destroy();
this._bellContext = null;
} catch (error) {
console.warn(`${NAME.PREFIX}Failed to destroy, ${error}`);
}
}
}
+2
View File
@@ -11,7 +11,9 @@ export default class TUICallService implements ITUICallService {
private _tim;
private _TUICore;
private _timerId;
private _startTimeStamp;
private _bellContext;
private _defaultOfflinePushInfo;
constructor();
static getInstance(): TUICallService;
init(params: IInitParams): Promise<void>;
+57 -15
View File
@@ -56,7 +56,12 @@ class TUICallService {
this._tim = null;
this._TUICore = null;
this._timerId = -1;
this._startTimeStamp = (0, common_utils_1.performanceNow)();
this._bellContext = null;
this._defaultOfflinePushInfo = {
title: '',
description: (0, index_2.t)('you have a new call'),
};
// =========================【监听 TUIStore 中的状态】=========================
this._handleCallStatusChange = (value) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
@@ -76,6 +81,7 @@ class TUICallService {
const callMediaType = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_MEDIA_TYPE);
const remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST);
const oldStatus = isGroup ? index_1.StatusChange.DIALING_GROUP : index_1.StatusChange.DIALING_C2C;
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_TIPS, '');
this.statusChanged && this.statusChanged({ oldStatus, newStatus: (0, utils_1.generateStatusChangeText)(TUIStore) });
if (!isGroup && callMediaType === index_1.CallMediaType.VIDEO) {
this.switchScreen(remoteUserInfoList[0].domId);
@@ -124,6 +130,7 @@ class TUICallService {
});
this._addListenTuiCallEngineEvent();
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, { userId: userID });
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, { userId: userID });
yield this._tuiCallEngine.login({ userID, userSig, assetsPath: '' }); // web && mini
}
catch (error) {
@@ -156,11 +163,12 @@ class TUICallService {
call(callParams) {
return __awaiter(this, void 0, void 0, function* () {
try {
const { type, userID } = callParams;
const { type, userID, offlinePushInfo } = callParams;
if (TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS) !== index_1.CallStatus.IDLE)
return;
yield this._updateCallStoreBeforeCall(type, [{ userId: userID }]);
this._executeExternalBeforeCalling(); // 执行外部传入的 beforeCall 方法
callParams.offlinePushInfo = Object.assign(Object.assign({}, this._defaultOfflinePushInfo), offlinePushInfo);
const response = yield this._tuiCallEngine.call(callParams);
yield this._updateCallStoreAfterCall([userID], response);
}
@@ -173,12 +181,13 @@ class TUICallService {
groupCall(groupCallParams) {
return __awaiter(this, void 0, void 0, function* () {
try {
const { userIDList, type, groupID } = groupCallParams;
const { userIDList, type, groupID, offlinePushInfo } = groupCallParams;
if (TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS) !== index_1.CallStatus.IDLE)
return;
const remoteUserInfoList = userIDList.map(userId => ({ userId }));
yield this._updateCallStoreBeforeCall(type, remoteUserInfoList, groupID);
this._executeExternalBeforeCalling();
groupCallParams.offlinePushInfo = Object.assign(Object.assign({}, this._defaultOfflinePushInfo), offlinePushInfo);
const response = yield this._tuiCallEngine.groupCall(groupCallParams);
yield this._updateCallStoreAfterCall(userIDList, response);
}
@@ -194,6 +203,7 @@ class TUICallService {
let inviteUserInfoList = yield (0, utils_1.getRemoteUserProfile)(userIDList, this.getTim(), TUIStore);
const remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, [...remoteUserInfoList, ...inviteUserInfoList]);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, [...remoteUserInfoList, ...inviteUserInfoList]);
this._tuiCallEngine && (yield this._tuiCallEngine.inviteUser(params));
}
catch (error) {
@@ -223,6 +233,7 @@ class TUICallService {
this.setSoundMode(params.type === index_1.CallMediaType.AUDIO ? index_1.AudioPlayBackDevice.EAR : index_1.AudioPlayBackDevice.SPEAKER);
const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO);
}
catch (error) {
@@ -301,6 +312,7 @@ class TUICallService {
this.setSoundMode(callMediaType === index_1.CallMediaType.AUDIO ? index_1.AudioPlayBackDevice.EAR : index_1.AudioPlayBackDevice.SPEAKER);
const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO); // web && mini default open audio
}
}
@@ -422,8 +434,11 @@ class TUICallService {
}
switchCamera() {
return __awaiter(this, void 0, void 0, function* () {
const currentPosition = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CAMERA_POSITION);
const targetPosition = currentPosition === index_1.CameraPosition.BACK ? index_1.CameraPosition.FRONT : index_1.CameraPosition.BACK;
try {
yield this._tuiCallEngine.switchCamera();
yield this._tuiCallEngine.switchCamera(targetPosition);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CAMERA_POSITION, targetPosition);
}
catch (error) {
console.error(`${index_1.NAME.PREFIX}_switchCamera failed, error: ${error}.`);
@@ -541,16 +556,19 @@ class TUICallService {
const [userInfo] = remoteUserInfoList.filter((userInfo) => userInfo.userId === sponsor);
remoteUserInfoList.length > 0 && TUIStore.updateStore({
[index_1.NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList,
[index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList,
[index_1.NAME.CALLER_USER_INFO]: {
userId: sponsor,
nick: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.nick) || '',
avatar: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.avatar) || '',
displayUserInfo: (userInfo === null || userInfo === void 0 ? void 0 : userInfo.remark) || (userInfo === null || userInfo === void 0 ? void 0 : userInfo.nick) || sponsor,
},
}, index_1.StoreName.CALL);
});
}
_handleUserAccept(event) {
this._callerChangeToConnected();
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.TOAST_INFO, (0, index_2.t)('answered'));
console.log(`${index_1.NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`);
}
_handleUserEnter(event) {
@@ -562,7 +580,10 @@ class TUICallService {
const isInRemoteUserList = remoteUserInfoList.find(item => (item === null || item === void 0 ? void 0 : item.userId) === userId);
if (!isInRemoteUserList) {
remoteUserInfoList.push({ userId });
remoteUserInfoList.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
if (remoteUserInfoList.length > 0) {
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
}
const [userInfo] = yield (0, utils_1.getRemoteUserProfile)([userId], this.getTim(), TUIStore);
remoteUserInfoList = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST);
remoteUserInfoList.forEach((obj) => {
@@ -576,7 +597,10 @@ class TUICallService {
obj.isEnter = true;
return obj;
});
remoteUserInfoList.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
if (remoteUserInfoList.length > 0) {
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
}
console.log(`${index_1.NAME.PREFIX}userEnter event data: ${JSON.stringify(event)}.`);
});
}
@@ -660,8 +684,10 @@ class TUICallService {
_handleSDKReady(event) {
return __awaiter(this, void 0, void 0, function* () {
let localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO);
localUserInfo = yield (0, utils_1.getMyProfile)(localUserInfo.userId, this.getTim(), TUIStore); // 方法里已经 try...catch 了
localUserInfo = yield (0, utils_1.getMyProfile)(localUserInfo.userId, this.getTim(), TUIStore);
this._defaultOfflinePushInfo.title = localUserInfo === null || localUserInfo === void 0 ? void 0 : localUserInfo.displayUserInfo;
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, localUserInfo);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo);
});
}
_handleKickedOut(event) {
@@ -757,15 +783,18 @@ class TUICallService {
localUserInfo = Object.assign(Object.assign({}, localUserInfo), { isVideoAvailable: isAvailable });
}
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, localUserInfo);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, localUserInfo);
}
_updateCallStoreBeforeCall(type, remoteUserInfoList, groupID) {
return __awaiter(this, void 0, void 0, function* () {
const callTips = groupID || TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.IS_MINIMIZED) ? index_2.CallTips.CALLER_GROUP_CALLING_MSG : index_2.CallTips.CALLER_CALLING_MSG;
let updateStoreParams = {
[index_1.NAME.CALL_MEDIA_TYPE]: type,
[index_1.NAME.CALL_ROLE]: index_1.CallRole.CALLER,
[index_1.NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList,
[index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList,
[index_1.NAME.IS_GROUP]: !!groupID,
[index_1.NAME.CALL_TIPS]: (0, index_2.t)(index_2.CallTips.CALLER_CALLING_MSG),
[index_1.NAME.CALL_TIPS]: (0, index_2.t)(callTips),
[index_1.NAME.GROUP_ID]: groupID
};
const pusher = { enableCamera: type === index_1.CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风
@@ -775,7 +804,10 @@ class TUICallService {
console.log(`${index_1.NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS, callStatus);
const remoteUserInfoLists = yield (0, utils_1.getRemoteUserProfile)(remoteUserInfoList.map(obj => obj.userId), this.getTim(), TUIStore);
remoteUserInfoLists.length > 0 && TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists);
if (remoteUserInfoLists.length > 0) {
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoLists);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoLists);
}
});
}
_updateCallStoreAfterCall(userIdList, response) {
@@ -792,6 +824,7 @@ class TUICallService {
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_STATUS, index_1.CallStatus.CALLING); // 小程序未授权时, 此时状态为 idle; web 直接设置为 calling
const localUserInfo = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, localUserInfo), { isEnter: true }));
this._setLocalUserInfoAudioVideoAvailable(true, index_1.NAME.AUDIO); // web && mini, default open audio
}
else {
@@ -800,9 +833,13 @@ class TUICallService {
});
}
_resetCurrentDevice() {
// 挂断后,重置当前摄像头麦克风为默认设备
const { cameraList, microphoneList } = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST, { microphoneList, cameraList, currentCamera: (cameraList === null || cameraList === void 0 ? void 0 : cameraList[0]) || {}, currentMicrophone: (microphoneList === null || microphoneList === void 0 ? void 0 : microphoneList[0]) || {} });
// 挂断后,重置当前摄像头麦克风和扬声器为默认设备
const { cameraList, microphoneList, speakerList } = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.DEVICE_LIST, { microphoneList, cameraList, speakerList,
currentCamera: (cameraList === null || cameraList === void 0 ? void 0 : cameraList[0]) || {},
currentMicrophone: (microphoneList === null || microphoneList === void 0 ? void 0 : microphoneList[0]) || {},
currentSpeaker: (speakerList === null || speakerList === void 0 ? void 0 : speakerList[0]) || {}
});
}
_resetCallStore() {
const oldStatusStr = (0, utils_1.generateStatusChangeText)(TUIStore);
@@ -820,7 +857,8 @@ class TUICallService {
case index_1.NAME.DISPLAY_MODE:
case index_1.NAME.VIDEO_RESOLUTION:
case index_1.NAME.ENABLE_FLOAT_WINDOW:
case index_1.NAME.LOCAL_USER_INFO: {
case index_1.NAME.LOCAL_USER_INFO:
case index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN: {
return false;
}
default: {
@@ -835,6 +873,9 @@ class TUICallService {
TUIStore.reset(index_1.StoreName.CALL, [index_1.NAME.IS_MINIMIZED], true); // isMinimized reset need notify
TUIStore.reset(index_1.StoreName.CALL, [index_1.NAME.IS_EAR_PHONE], true); // isEarPhone reset need notify
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO, Object.assign(Object.assign({}, TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO)), { isVideoAvailable: false, isAudioAvailable: false }));
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN, Object.assign(Object.assign({}, TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN)), { isVideoAvailable: false, isAudioAvailable: false }));
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, []);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, []);
this._resetCurrentDevice();
const newStatusStr = (0, utils_1.generateStatusChangeText)(TUIStore);
if (oldStatusStr !== newStatusStr) {
@@ -857,13 +898,13 @@ class TUICallService {
// 通话时长更新
_startTimer() {
if (this._timerId === -1) {
this._startTimeStamp = (0, common_utils_1.performanceNow)();
this._timerId = timer_1.default.run(index_1.NAME.TIMEOUT, this._updateCallDuration.bind(this), { delay: 1000 });
}
}
_updateCallDuration() {
let callDurationStr = TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.CALL_DURATION);
const callDurationNum = (0, common_utils_1.formatTimeInverse)(callDurationStr);
callDurationStr = (0, common_utils_1.formatTime)(callDurationNum + 1);
const callDurationNum = Math.round(((0, common_utils_1.performanceNow)() - this._startTimeStamp) / 1000); // miniProgram stop timer when background
const callDurationStr = (0, common_utils_1.formatTime)(callDurationNum);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.CALL_DURATION, callDurationStr);
}
_stopTimer() {
@@ -880,6 +921,7 @@ class TUICallService {
remoteUserInfoList = remoteUserInfoList.filter((obj) => obj.userId !== userId);
});
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_LIST, remoteUserInfoList);
TUIStore.update(index_1.StoreName.CALL, index_1.NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST, remoteUserInfoList);
}
_analyzeEventData(event) {
return (event === null || event === void 0 ? void 0 : event.data) || {}; // mini INVITED
@@ -0,0 +1,991 @@
import { TUICore, TUILogin, TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core';
import {
ITUICallService,
ICallParams,
IGroupCallParams,
IUserInfo,
ICallbackParam,
ISelfInfoParams,
IBellParams,
IInviteUserParams,
IJoinInGroupCallParams,
IInitParams,
} from '../interface/ICallService';
import {
StoreName,
CallStatus,
CallMediaType,
NAME,
CALL_DATA_KEY,
LanguageType,
CallRole,
LOG_LEVEL,
VideoDisplayMode,
VideoResolution,
StatusChange,
AudioCallIcon,
VideoCallIcon,
ErrorCode,
ErrorMessage,
AudioPlayBackDevice,
CameraPosition,
} from '../const/index';
// @ts-ignore
import { TUICallEngine, EVENT as TUICallEvent } from 'tuicall-engine-wx';
import { CallTips, t } from '../locales/index';
import { initAndCheckRunEnv, beforeCall, handlePackageError } from './miniProgram';
import { BellContext } from './bellContext';
import { VALIDATE_PARAMS, avoidRepeatedCall, paramValidate } from '../utils/validate/index';
import { handleRepeatedCallError, handleNoDevicePermissionError, formatTime, performanceNow } from '../utils/common-utils';
import { getMyProfile, getRemoteUserProfile, generateText, generateStatusChangeText, getGroupMemberList, getGroupProfile } from './utils';
import timer from '../utils/timer';
import { ITUIGlobal } from '../interface/ITUIGlobal';
import { ITUIStore } from '../interface/ITUIStore';
import TuiGlobal from '../TUIGlobal/tuiGlobal';
import TuiStore from '../TUIStore/tuiStore';
const TUIGlobal: ITUIGlobal = TuiGlobal.getInstance();
const TUIStore: ITUIStore = TuiStore.getInstance();
const version = '2.1.1';
export { TUIGlobal, TUIStore };
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 _defaultOfflinePushInfo = {
title: '',
description: t('you have a new call'),
};
constructor() {
console.log(`${NAME.PREFIX}version: ${version}`);
this._watchTUIStore();
this._bellContext = new BellContext();
// 下面:TUICore注册事件,注册组件服务,注册界面拓展
TUICore.registerEvent(TUIConstants.TUILogin.EVENT.LOGIN_STATE_CHANGED, TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGIN_SUCCESS, this);
TUICore.registerService(TUIConstants.TUICalling.SERVICE.NAME, this);
TUICore.registerExtension(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, 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 } = 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,
// @ts-ignore
sdkAppID: sdkAppID || SDKAppID, // 兼容传入 SDKAppID 的问题
callkitVersion: version,
chat: isFromChat || false,
});
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
} 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;
}
} catch (error) {
console.error(`${NAME.PREFIX}destroyed failed, error: ${error}.`);
throw error;
}
}
// ===============================【通话操作】===============================
@avoidRepeatedCall()
@paramValidate(VALIDATE_PARAMS.call)
public async call(callParams: ICallParams) {
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._defaultOfflinePushInfo, ...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)
public async groupCall(groupCallParams: IGroupCallParams) {
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._defaultOfflinePushInfo, ...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)
public async inviteUser(params: IInviteUserParams) {
try {
const { userIDList } = params;
let inviteUserInfoList = await getRemoteUserProfile(userIDList, this.getTim(), TUIStore);
const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
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}.`);
this._resetCallStore();
throw error;
}
}
@avoidRepeatedCall()
@paramValidate(VALIDATE_PARAMS.joinInGroupCall)
public async joinInGroupCall(params: IJoinInGroupCallParams) {
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);
(params.type === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO);
TUIStore.update(StoreName.CALL, NAME.IS_CLICKABLE, true);
this._startTimer();
TUIStore.update(StoreName.CALL, NAME.PUSHER, response);
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 });
this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO);
} catch (error) {
console.error(`${NAME.PREFIX}joinInGroupCall failed, error: ${error}.`);
this._resetCallStore();
throw error;
}
}
// ===============================【其它对外接口】===============================
public getTUICallEngineInstance(): any {
return this?._tuiCallEngine || null;
}
public setLogLevel(level: LOG_LEVEL) {
this?._tuiCallEngine?.setLogLevel(level);
}
@paramValidate(VALIDATE_PARAMS.setLanguage)
public setLanguage(language: LanguageType) {
if (language && Object.values(LanguageType).includes(language)) {
TUIStore.update(StoreName.CALL, NAME.LANGUAGE, language);
}
}
@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}.`);
}
}
// 修改默认铃声:只支持本地铃声文件,不支持在线铃声文件;修改铃声修改的是被叫的铃声
@paramValidate(VALIDATE_PARAMS.setCallingBell)
public async setCallingBell(filePath?: string) {
let isCheckFileExist: boolean = true;
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}.`);
}
}
// =============================【内部按钮操作方法】=============================
@avoidRepeatedCall()
public async accept() {
try {
const response = await this._tuiCallEngine.accept();
if (response) {
// 小程序接通时会进行授权弹框, 状态需要放在 accept 后, 否则先接通后再拉起权限设置
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CONNECTED);
this._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);
(callMediaType === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO);
response.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, response.pusher);
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 });
this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini default open audio
}
} catch (error) {
if (handleRepeatedCallError(error)) return;
this._noDevicePermissionToast(error, CallMediaType.AUDIO);
this._resetCallStore();
}
}
@avoidRepeatedCall()
public async hangup() {
try {
const response = await this._tuiCallEngine.hangup();
response?.forEach((item) => {
if (item?.code === 0) {
this._callTUIService({ message: item?.data?.message });
}
});
} catch (error) {
console.debug(error);
}
this._resetCallStore();
}
@avoidRepeatedCall()
public async reject() {
try {
const response = await this._tuiCallEngine.reject();
if (response?.code === 0) {
this._callTUIService({ message: response?.data?.message });
}
} catch (error) {
console.debug(error);
}
this._resetCallStore();
}
@avoidRepeatedCall()
public async openCamera(videoViewDomID: string) {
try {
await this._tuiCallEngine.openCamera();
this._setLocalUserInfoAudioVideoAvailable(true, NAME.VIDEO);
} catch (error: any) {
this._noDevicePermissionToast(error, CallMediaType.VIDEO);
console.error(`${NAME.PREFIX}openCamera error: ${error}.`);
}
}
@avoidRepeatedCall()
public async closeCamera() {
try {
await this._tuiCallEngine.closeCamera();
this._setLocalUserInfoAudioVideoAvailable(false, NAME.VIDEO);
} catch (error: any) {
console.error(`${NAME.PREFIX}closeCamera error: ${error}.`);
}
}
@avoidRepeatedCall()
public async openMicrophone() {
try {
await this._tuiCallEngine.openMicrophone();
this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO);
} catch (error: any) {
console.error(`${NAME.PREFIX}openMicrophone failed, error: ${error}.`);
}
}
@avoidRepeatedCall()
public async closeMicrophone() {
try {
await this._tuiCallEngine.closeMicrophone();
this._setLocalUserInfoAudioVideoAvailable(false, NAME.AUDIO);
} catch (error: any) {
console.error(`${NAME.PREFIX}closeMicrophone 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._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(TUIStore);
this.statusChanged && this.statusChanged({ oldStatus, newStatus });
this.setSoundMode(AudioPlayBackDevice.EAR);
} 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 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}.`);
}
}
// 切前后置 miniProgram, 切扬声器
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
}
// ==========================【TUICallEngine 事件处理】==========================
private _addListenTuiCallEngineEvent() {
if (!this._tuiCallEngine) {
console.warn(`${NAME.PREFIX}add engine event listener failed, engine is empty.`);
return;
}
this._tuiCallEngine.on(TUICallEvent.ERROR, this._handleError, this);
this._tuiCallEngine.on(TUICallEvent.INVITED, this._handleNewInvitationReceived, this); // 收到邀请事件
this._tuiCallEngine.on(TUICallEvent.USER_ACCEPT, this._handleUserAccept, this); // 主叫收到被叫接通事件
this._tuiCallEngine.on(TUICallEvent.USER_ENTER, this._handleUserEnter, this); // 有用户进房事件
this._tuiCallEngine.on(TUICallEvent.USER_LEAVE, this._handleUserLeave, this); // 有用户离开通话事件
this._tuiCallEngine.on(TUICallEvent.REJECT, this._handleInviteeReject, this); // 主叫收到被叫的拒绝通话事件
this._tuiCallEngine.on(TUICallEvent.NO_RESP, this._handleNoResponse, this); // 主叫收到被叫的无应答事件
this._tuiCallEngine.on(TUICallEvent.LINE_BUSY, this._handleLineBusy, this); // 主叫收到被叫的忙线事件
this._tuiCallEngine.on(TUICallEvent.CALLING_CANCEL, this._handleCallingCancel, this); // 主被叫在通话未建立时, 收到的取消事件
this._tuiCallEngine.on(TUICallEvent.SDK_READY, this._handleSDKReady, this); // SDK Ready 回调
this._tuiCallEngine.on(TUICallEvent.KICKED_OUT, this._handleKickedOut, this); // 未开启多端登录时, 多端登录收到的被踢事件
this._tuiCallEngine.on(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe, this);
this._tuiCallEngine.on(TUICallEvent.CALL_END, this._handleCallingEnd, this); // 主被叫在通话结束时, 收到的通话结束事件
// @ts-ignore
this._tuiCallEngine.on(TUICallEvent.CALL_MODE, this._handleCallTypeChange, this);
// @ts-ignore
this._tuiCallEngine.on(TUICallEvent.USER_UPDATE, this._handleUserUpdate, this); // mini: user data update
}
private _removeListenTuiCallEngineEvent() {
this._tuiCallEngine.off(TUICallEvent.ERROR, this._handleError);
this._tuiCallEngine.off(TUICallEvent.INVITED, this._handleNewInvitationReceived);
this._tuiCallEngine.off(TUICallEvent.USER_ACCEPT, this._handleUserAccept);
this._tuiCallEngine.off(TUICallEvent.USER_ENTER, this._handleUserEnter);
this._tuiCallEngine.off(TUICallEvent.USER_LEAVE, this._handleUserLeave);
this._tuiCallEngine.off(TUICallEvent.REJECT, this._handleInviteeReject);
this._tuiCallEngine.off(TUICallEvent.NO_RESP, this._handleNoResponse);
this._tuiCallEngine.off(TUICallEvent.LINE_BUSY, this._handleLineBusy);
this._tuiCallEngine.off(TUICallEvent.CALLING_CANCEL, this._handleCallingCancel);
this._tuiCallEngine.off(TUICallEvent.SDK_READY, this._handleSDKReady);
this._tuiCallEngine.off(TUICallEvent.KICKED_OUT, this._handleKickedOut);
this._tuiCallEngine.off(TUICallEvent.MESSAGE_SENT_BY_ME, this._messageSentByMe);
this._tuiCallEngine.off(TUICallEvent.CALL_END, this._handleCallingEnd);
// @ts-ignore
this._tuiCallEngine.off(TUICallEvent.CALL_MODE, this._handleCallTypeChange); // 切换通话事件 miniProgram CALL_MODE
// @ts-ignore
this._tuiCallEngine.off(TUICallEvent.USER_UPDATE, this._handleUserUpdate); // mini: user data update
}
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, { text: callTips, type: NAME.ERROR });
}
this._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 { sponsor = '', isFromGroup, callMediaType, inviteData = {}, calleeIdList = [], groupID = '' } = this._analyzeEventData(event);
const currentUserInfo: IUserInfo = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO);
const remoteUserIdList: string[] = [sponsor, ...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]: isFromGroup,
[NAME.CALL_STATUS]: CallStatus.CALLING,
[NAME.CALL_MEDIA_TYPE]: type,
[NAME.CALL_TIPS]: t(callTipsKey),
[NAME.CALLER_USER_INFO]: { userId: sponsor },
[NAME.GROUP_ID]: groupID,
};
initAndCheckRunEnv();
const pusher = { enableCamera: type === CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风
updateStoreParams = { ...updateStoreParams, [NAME.PUSHER]: pusher };
TUIStore.updateStore(updateStoreParams, StoreName.CALL);
this._executeExternalBeforeCalling();
this.statusChanged && this.statusChanged({ oldStatus: StatusChange.IDLE, newStatus: StatusChange.BE_INVITED });
const remoteUserInfoList = await getRemoteUserProfile(remoteUserIdList, this.getTim(), TUIStore);
const [userInfo] = remoteUserInfoList.filter((userInfo: IUserInfo) => userInfo.userId === sponsor);
remoteUserInfoList.length > 0 && TUIStore.updateStore({
[NAME.REMOTE_USER_INFO_LIST]: remoteUserInfoList,
[NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST]: remoteUserInfoList,
[NAME.CALLER_USER_INFO]: {
userId: sponsor,
nick: userInfo?.nick || '',
avatar: userInfo?.avatar || '',
displayUserInfo: userInfo?.remark || userInfo?.nick || sponsor,
},
}, StoreName.CALL);
}
private _handleUserAccept(event: any): void {
this._callerChangeToConnected();
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, t('answered'));
console.log(`${NAME.PREFIX}accept event data: ${JSON.stringify(event)}.`);
}
private async _handleUserEnter(event: any): Promise<void> {
this._callerChangeToConnected();
const { userID: userId, data } = this._analyzeEventData(event);
data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList);
let remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
const isInRemoteUserList = remoteUserInfoList.find(item => item?.userId === userId);
if (!isInRemoteUserList) {
remoteUserInfoList.push({ userId });
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);
}
const [userInfo] = await getRemoteUserProfile([userId], this.getTim(), TUIStore);
remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
remoteUserInfoList.forEach((obj) => {
if (obj?.userId === userId) {
obj = Object.assign(obj, userInfo);
}
});
}
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 _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._startTimer();
}
}
private _handleUserLeave(event: any): void {
console.log(`${NAME.PREFIX}userLeave event data: ${JSON.stringify(event)}.`);
const { data, userID: userId } = this._analyzeEventData(event);
data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList);
if (TUIStore.getData(StoreName.CALL, NAME.IS_GROUP)) {
const remoteUserInfoList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST);
const prefix: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId;
const text = generateText(TUIStore, CallTips.END_CALL, prefix);
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text });
}
userId && this._deleteRemoteUser([userId]);
}
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 } = this._analyzeEventData(event);
let callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.OTHER_SIDE_REJECT_CALL : CallTips.OTHER_SIDE_LINE_BUSY;
let text = generateText(TUIStore, callTipsKey);
if (isGroup) {
const prefix: string = (remoteUserInfoList.find(obj => obj.userId === userId) || {}).displayUserInfo || userId;
callTipsKey = eventName === TUICallEvent.REJECT ? CallTips.REJECT_CALL : CallTips.IN_BUSY;
text = generateText(TUIStore, callTipsKey, prefix);
}
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text });
userId && this._deleteRemoteUser([userId]);
break;
}
case TUICallEvent.NO_RESP: {
const { userIDList = [] } = this._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;
});
const text = isGroup ? generateText(TUIStore, callTipsKey, userInfoList.join()) : generateText(TUIStore, callTipsKey);
TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text });
userIDList.length > 0 && this._deleteRemoteUser(userIDList);
break;
}
case TUICallEvent.CALLING_CANCEL: {
// TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text: generateText(TUIStore, CallTips.CANCEL) });
this._resetCallStore();
break;
}
}
}
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 _handleCallingCancel(event: any): void {
this._executeExternalAfterCalling();
this._unNormalEventsManager(event, TUICallEvent.CALLING_CANCEL);
}
private _handleCallingEnd(event: any): void {
console.log(`${NAME.PREFIX}callEnd event data: ${JSON.stringify(event)}.`);
this._executeExternalAfterCalling();
this._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.getTim(), TUIStore);
this._defaultOfflinePushInfo.title = localUserInfo?.displayUserInfo;
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.kickedOut && this.kickedOut(event);
TUIStore.update(StoreName.CALL, NAME.CALL_TIPS, generateText(TUIStore, CallTips.KICK_OUT));
this._resetCallStore();
}
private _handleCallTypeChange(event: any): void {
const { newCallType, type } = this._analyzeEventData(event);
TUIStore.update(StoreName.CALL, NAME.CALL_MEDIA_TYPE, newCallType || type);
this.setSoundMode(AudioPlayBackDevice.EAR);
}
private _messageSentByMe(event: any): void {
const message = event?.data;
this.onMessageSentByMe && this.onMessageSentByMe(message);
}
// ==========================【 miniProgram 私有事件】==========================
private _handleUserUpdate(event: any): void {
const data = this._analyzeEventData(event);
data?.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, data.pusher);
data?.playerList && TUIStore.update(StoreName.CALL, NAME.PLAYER, data.playerList);
}
// 处理 “呼叫” 抛出的异常
private _handleCallError(error: any, methodName?: string) {
if (handleRepeatedCallError(error)) return;
handlePackageError(error); // 无套餐提示, 小程序 engine 不抛出 onError
this._noDevicePermissionToast(error, CallMediaType.AUDIO);
console.error(`${NAME.PREFIX}${methodName} failed, error: ${error}.`);
this._resetCallStore();
throw error;
}
// ========================【原 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);
}
private _executeExternalBeforeCalling(): void {
this.beforeCalling && this.beforeCalling();
}
private _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}.`);
}
}
// =========================【 miniProgram 私有公共方法】=========================
// 处理用户异常退出的情况,处理了右滑退出,以及返回退出的情况。
private async _handleExceptionExit() {
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
const callRole = TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE);
// 在呼叫状态下, 被叫调用 reject,主叫调用 hangup
if (callStatus === CallStatus.CALLING) {
callRole === CallRole.CALLER && await this.hangup();
callRole === CallRole.CALLEE && await this.reject();
}
// 在通话状态下, 统一调用 hangup 接口
callStatus === CallStatus.CONNECTED && await this.hangup();
}
private _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);
}
private async _updateCallStoreBeforeCall(type: number, remoteUserInfoList: IUserInfo[], groupID?: string): Promise<void> {
const callTips = groupID || TUIStore.getData(StoreName.CALL, NAME.IS_MINIMIZED) ? CallTips.CALLER_GROUP_CALLING_MSG : 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,
[NAME.CALL_TIPS]: t(callTips),
[NAME.GROUP_ID]: groupID
};
const pusher = { enableCamera: type === CallMediaType.VIDEO, enableMic: true }; // mini 默认打开麦克风
updateStoreParams = { ...updateStoreParams, [NAME.PUSHER]: pusher };
TUIStore.updateStore(updateStoreParams, StoreName.CALL);
const callStatus = await beforeCall(type, this); // 如果没有权限, 此时为 false. 因此需要在 call 后设置为 calling. 和 web 存在差异
console.log(`${NAME.PREFIX}mini beforeCall return callStatus: ${callStatus}.`);
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, callStatus);
const remoteUserInfoLists = await getRemoteUserProfile(remoteUserInfoList.map(obj => obj.userId), this.getTim(), TUIStore);
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);
TUIStore.update(StoreName.CALL, NAME.ROOM_ID, response.roomID);
this._callTUIService({ message: response?.data?.message });
response.pusher && TUIStore.update(StoreName.CALL, NAME.PUSHER, response.pusher);
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
(callMediaType === CallMediaType.VIDEO) && await this.openCamera(NAME.LOCAL_VIDEO);
this.setSoundMode(callMediaType === CallMediaType.AUDIO ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
TUIStore.update(StoreName.CALL, NAME.CALL_STATUS, CallStatus.CALLING); // 小程序未授权时, 此时状态为 idle; web 直接设置为 calling
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 });
this._setLocalUserInfoAudioVideoAvailable(true, NAME.AUDIO); // web && mini, default open audio
} else {
this._resetCallStore();
}
}
private _resetCurrentDevice() {
// 挂断后,重置当前摄像头,麦克风和扬声器为默认设备
const { cameraList, microphoneList, speakerList } = TUIStore.getData(StoreName.CALL, NAME.DEVICE_LIST);
TUIStore.update(
StoreName.CALL,
NAME.DEVICE_LIST,
{ microphoneList, cameraList, speakerList,
currentCamera: cameraList?.[0] || {},
currentMicrophone: microphoneList?.[0] || {},
currentSpeaker: speakerList?.[0] || {}
},
);
}
private _resetCallStore() {
const oldStatusStr = generateStatusChangeText(TUIStore);
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.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.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, []);
this._resetCurrentDevice();
const newStatusStr = generateStatusChangeText(TUIStore);
if (oldStatusStr !== newStatusStr) {
this.statusChanged && this.statusChanged({ oldStatus: oldStatusStr, newStatus: newStatusStr });
}
}
private _noDevicePermissionToast(error: any, type: CallMediaType): void {
if (handleNoDevicePermissionError(error)) {
let text = '';
if (type === CallMediaType.AUDIO) {
text = generateText(TUIStore, CallTips.NO_MICROPHONE_DEVICE_PERMISSION);
}
if (type === CallMediaType.VIDEO) {
text = generateText(TUIStore, CallTips.NO_CAMERA_DEVICE_PERMISSION);
}
text && TUIStore.update(StoreName.CALL, NAME.TOAST_INFO, { text, type: NAME.ERROR });
console.error(`${NAME.PREFIX}call failed, error: ${error.message}.`);
}
}
// 通话时长更新
private _startTimer(): void {
if (this._timerId === -1) {
this._startTimeStamp = performanceNow();
this._timerId = timer.run(NAME.TIMEOUT, this._updateCallDuration.bind(this), { delay: 1000 });
}
}
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 _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);
}
private _analyzeEventData(event: any): any {
return event?.data || {}; // mini INVITED
}
// =========================【调用 chat 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(TUIStore) });
if (!isGroup && callMediaType === CallMediaType.VIDEO) {
this.switchScreen(remoteUserInfoList[0].domId);
}
}
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,
});
}
// =========================【web 融合 chat 提供的方法】=========================
public bindTUICore(TUICore: any) {
this._TUICore = TUICore;
}
// 下面方法用于接入 TUICore
private _callTUIService(params) {
const { message } = params || {};
TUICore.callService({
serviceName: TUIConstants.TUIChat.SERVICE.NAME,
method: TUIConstants.TUIChat.SERVICE.METHOD.UPDATE_MESSAGE_LIST,
params: { message },
});
}
public async onNotifyEvent(eventName: string, subKey: string) {
try {
if (eventName === TUIConstants.TUILogin.EVENT.LOGIN_STATE_CHANGED) {
if (subKey === TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGIN_SUCCESS) {
// TUICallkit 收到登录成功时执行自己的业务逻辑处理
// @ts-ignore
const { chat, userID, userSig, SDKAppID } = TUILogin.getContext();
await this.init({ tim: chat, userID, userSig, sdkAppID: SDKAppID, isFromChat: true });
} else if (subKey === TUIConstants.TUILogin.EVENT_SUB_KEY.USER_LOGOUT_SUCCESS) {
await this.destroyed();
}
}
} catch (error) {
console.error(`${NAME.PREFIX}TUICore onNotifyEvent failed, error: ${error}.`);
}
}
public async onCall(method: String, params: any) {
if (method === TUIConstants.TUICalling.SERVICE.METHOD.START_CALL) {
await this._handleTUICoreOnClick(params, params.type);
}
}
private async _handleTUICoreOnClick(options, type: CallMediaType) {
try {
const { groupID, userIDList = [], ...rest } = options;
if (groupID) {
await this.groupCall({ groupID, userIDList, type, ...rest });
} else if (userIDList.length === 1) {
await this.call({ userID: userIDList[0], type, ...rest });
}
} catch (error: any) {
console.debug(error);
}
}
public onGetExtension(extensionID: string, params: any) {
if (extensionID === TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID) {
const list = [];
const audioCallExtension: ExtensionInfo = {
weight: 1000,
text: '语音通话',
icon: AudioCallIcon,
data: {
name: 'voiceCall',
},
listener: {
onClicked: async options => await this._handleTUICoreOnClick(options, options.type || CallMediaType.AUDIO), // 点击时发起通话
},
};
const videoCallExtension: ExtensionInfo = {
weight: 900,
text: '视频通话',
icon: VideoCallIcon,
data: {
name: 'videoCall',
},
listener: {
onClicked: async options => await this._handleTUICoreOnClick(options, options.type || CallMediaType.VIDEO), // 点击时发起通话
},
};
if (!params?.filterVoice) {
list.push(audioCallExtension);
}
if (!params?.filterVideo) {
list.push(videoCallExtension);
}
return list;
}
}
}
@@ -0,0 +1,55 @@
import { CallMediaType, CallStatus } from '../const/index';
export function initialUI() {
// 收起键盘
// @ts-ignore
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,
});
}
}
@@ -30,7 +30,7 @@ function setDefaultUserInfo(userId, domId) {
exports.setDefaultUserInfo = setDefaultUserInfo;
// 获取个人用户信息
function getMyProfile(myselfUserId, tim, TUIStore) {
var _a, _b, _c;
var _a, _b, _c, _d, _e;
return __awaiter(this, void 0, void 0, function* () {
let localUserInfo = setDefaultUserInfo(myselfUserId, index_1.NAME.LOCAL_VIDEO);
try {
@@ -39,7 +39,7 @@ function getMyProfile(myselfUserId, tim, TUIStore) {
const res = yield tim.getMyProfile();
const currentLocalUserInfo = TUIStore === null || TUIStore === void 0 ? void 0 : TUIStore.getData(index_1.StoreName.CALL, index_1.NAME.LOCAL_USER_INFO); // localUserInfo may have been updated
if ((res === null || res === void 0 ? void 0 : res.code) === 0) {
localUserInfo = Object.assign(Object.assign(Object.assign({}, localUserInfo), currentLocalUserInfo), { userId: (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.userID, nick: (_b = res === null || res === void 0 ? void 0 : res.data) === null || _b === void 0 ? void 0 : _b.nick, avatar: (_c = res === null || res === void 0 ? void 0 : res.data) === null || _c === void 0 ? void 0 : _c.avatar });
localUserInfo = Object.assign(Object.assign(Object.assign({}, localUserInfo), currentLocalUserInfo), { userId: (_a = res === null || res === void 0 ? void 0 : res.data) === null || _a === void 0 ? void 0 : _a.userID, nick: (_b = res === null || res === void 0 ? void 0 : res.data) === null || _b === void 0 ? void 0 : _b.nick, avatar: (_c = res === null || res === void 0 ? void 0 : res.data) === null || _c === void 0 ? void 0 : _c.avatar, displayUserInfo: ((_d = res === null || res === void 0 ? void 0 : res.data) === null || _d === void 0 ? void 0 : _d.nick) || ((_e = res === null || res === void 0 ? void 0 : res.data) === null || _e === void 0 ? void 0 : _e.userID) });
}
return localUserInfo;
}
@@ -0,0 +1,142 @@
import { NAME, StoreName, CallStatus, StatusChange, CallMediaType } from '../const/index';
import { IUserInfo } from '../interface/ICallService';
import { ITUIStore } from '../interface/ITUIStore';
import { t } from '../locales/index';
// 设置默认的 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, TUIStore: 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, TUIStore: any): Promise<any> {
let remoteUserInfoList: IUserInfo[] = userIdList.map((userId: string) => setDefaultUserInfo(userId));
try {
if (!tim) return remoteUserInfoList;
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;
} catch (error) {
console.error(`${NAME.PREFIX}getRemoteUserProfile failed, error: ${error}.`);
return remoteUserInfoList;
}
}
// 生成弹框提示文案
export function generateText(TUIStore: ITUIStore, key: string, prefix?: string, suffix?: string): string {
const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
let callTips = `${t(key)}`;
if (isGroup) {
callTips = prefix ? `${prefix} ${callTips}` : callTips;
callTips = suffix ? `${callTips} ${suffix}` : callTips;
}
return callTips;
}
// 生成 statusChange 抛出的字符串
export function generateStatusChangeText(TUIStore: ITUIStore): string {
const callStatus = TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS);
if (callStatus === CallStatus.IDLE) {
return StatusChange.IDLE;
}
const isGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
if (callStatus === CallStatus.CALLING) {
return isGroup ? StatusChange.DIALING_GROUP : StatusChange.DIALING_C2C;
}
const callMediaType = TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE);
if (isGroup) {
return callMediaType === CallMediaType.AUDIO ? StatusChange.CALLING_GROUP_AUDIO : StatusChange.CALLING_GROUP_VIDEO;
}
return callMediaType === CallMediaType.AUDIO ? StatusChange.CALLING_C2C_AUDIO : StatusChange.CALLING_C2C_VIDEO;
}
// 获取群组[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 });
if (res.code === 0) {
return res.data.group || groupProfile;
}
} catch(error) {
console.error(`${NAME.PREFIX}getGroupProfile failed, error: ${error}.`);
return groupProfile;
}
}
@@ -0,0 +1,43 @@
import { APP_NAMESPACE, IS_PC, IS_H5, IN_WX_MINI_APP, IN_UNI_NATIVE_APP, IN_UNI_APP, IS_MAC, IS_WIN } from '../utils/env';
import { ITUIGlobal } from '../interface/ITUIGlobal';
export default class TUIGlobal implements ITUIGlobal {
static instance: TUIGlobal;
public global: any = APP_NAMESPACE;
public isPC: boolean = false;
public isH5: boolean = false;
public isWeChat: boolean = false;
public isApp: boolean = false;
public isUniPlatform: boolean = false;
public isOfficial: boolean = false;
public isWIN: boolean = false;
public isMAC: boolean = false;
constructor() {
this.initEnv();
}
/**
* 获取 TUIGlobal 实例
* @returns {TUIGlobal}
*/
static getInstance() {
if (!TUIGlobal.instance) {
TUIGlobal.instance = new TUIGlobal();
}
return TUIGlobal.instance;
}
initEnv() {
this.isPC = IS_PC;
this.isH5 = IS_H5;
this.isWeChat = IN_WX_MINI_APP;
this.isApp = IN_UNI_NATIVE_APP && !IN_WX_MINI_APP; // uniApp 打包小程序时 IN_UNI_NATIVE_APP 为 true,所以此处需要增加条件
this.isUniPlatform = IN_UNI_APP;
this.isWIN = IS_WIN;
this.isMAC = IS_MAC;
}
initOfficial(SDKAppID: number) {
this.isOfficial = (SDKAppID === 1400187352 || SDKAppID === 1400188366);
}
}
@@ -9,7 +9,9 @@ class CallStore {
callRole: index_1.CallRole.UNKNOWN,
callMediaType: index_1.CallMediaType.UNKNOWN,
localUserInfo: { userId: '' },
localUserInfoExcludeVolume: { userId: '' },
remoteUserInfoList: [],
remoteUserInfoExcludeVolumeList: [],
callerUserInfo: { userId: '' },
isGroup: false,
callDuration: '00:00:00',
@@ -24,6 +26,7 @@ class CallStore {
showPermissionTip: false,
groupID: '',
roomID: 0,
cameraPosition: index_1.CameraPosition.FRONT,
// TUICallKit 组件上的属性
displayMode: index_1.VideoDisplayMode.COVER,
videoResolution: index_1.VideoResolution.RESOLUTION_480P,
@@ -0,0 +1,66 @@
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, CameraPosition } from '../const/index';
import { ICallStore } from '../interface/ICallStore';
import { getLanguage } from '../utils/common-utils';
export default class CallStore {
public defaultStore: ICallStore = {
callStatus: CallStatus.IDLE,
callRole: CallRole.UNKNOWN,
callMediaType: CallMediaType.UNKNOWN,
localUserInfo: { userId: '' },
localUserInfoExcludeVolume: { userId: '' },
remoteUserInfoList: [],
remoteUserInfoExcludeVolumeList: [],
callerUserInfo: { userId: '' },
isGroup: false,
callDuration: '00:00:00', // 通话时长
callTips: '', // 通话提示的信息. 例如: '等待谁接听', 'xxx 拒绝通话', 'xxx 挂断通话'
toastInfo: { text: '' }, // 远端用户挂断、拒绝、超时、忙线等的 toast 提示信息
isMinimized: false, // 用来记录当前是否悬浮窗模式
enableFloatWindow: false, // 开启/关闭悬浮窗功能,设置为false,通话界面左上角的悬浮窗按钮会隐藏
bigScreenUserId: '', // 当前大屏幕显示的 userID 用户
language: getLanguage(), // en, zh-cn
isClickable: false, // 是否可点击, 用于按钮增加 loading 效果,不可点击
deviceList: { cameraList: [], microphoneList: [], currentCamera: {}, currentMicrophone: {} },
showPermissionTip: false,
groupID: '',
roomID: 0,
cameraPosition: CameraPosition.FRONT, // 前置或后置,值为front, back
// TUICallKit 组件上的属性
displayMode: VideoDisplayMode.COVER, // 设置预览远端的画面显示模式
videoResolution: VideoResolution.RESOLUTION_480P,
showSelectUser: false,
// 小程序相关属性
pusher: {},
player: [],
isEarPhone: false, // 是否是听筒, 默认: false
};
public store: ICallStore = Object.assign({}, this.defaultStore);;
public update(key: keyof ICallStore, data: any): void {
switch (key) {
// case 'callTips':
// break;
default:
// resolve "Type 'any' is not assignable to type 'never'.ts", ref: https://github.com/microsoft/TypeScript/issues/31663
(this.store[key] as any) = data as any;
}
}
public getData(key: string | undefined): any {
if (!key) return this.store;
return this.store[key as keyof ICallStore];
}
// reset call store
public reset(keyList: Array<string> = []) {
if (keyList.length === 0) {
keyList = Object.keys(this.store);
}
const resetToDefault = keyList.reduce((acc, key) => ({ ...acc, [key]: this.defaultStore[key as keyof ICallStore] }), {});
this.store = {
...this.defaultStore,
...this.store,
...resetToDefault,
};
}
}
@@ -0,0 +1,157 @@
import { ITUIStore, IOptions, Task } from '../interface/ITUIStore';
import { StoreName, NAME } from '../const/index';
import CallStore from './callStore';
import { isString, isNumber, isBoolean } from '../utils/common-utils';
export default class TUIStore implements ITUIStore {
static instance: TUIStore;
public task: Task;
private storeMap: Partial<Record<StoreName, any>>;
private timerId: number = -1;
constructor() {
this.storeMap = {
[StoreName.CALL]: new CallStore(),
};
// todo 此处后续优化结构后调整
this.task = {} as Task; // 保存监听回调列表
}
/**
* 获取 TUIStore 实例
* @returns {TUIStore}
*/
static getInstance() {
if (!TUIStore.instance) {
TUIStore.instance = new TUIStore();
}
return TUIStore.instance;
}
/**
* UI 组件注册监听回调
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息
* @param {Object} params 扩展参数
* @param {String} params.notifyRangeWhenWatch 注册时监听时的通知范围, 'all' - 通知所有注册该 key 的监听; 'myself' - 通知本次注册该 key 的监听; 默认不通知
*/
watch(storeName: StoreName, options: IOptions, params?: any) {
if (!this.task[storeName]) {
this.task[storeName] = {};
}
const watcher = this.task[storeName];
Object.keys(options).forEach((key) => {
const callback = options[key];
if (!watcher[key]) {
watcher[key] = new Map();
}
watcher[key].set(callback, 1);
const { notifyRangeWhenWatch } = params || {};
// 注册监听后, 通知所有注册该 key 的监听,使用 'all' 时要特别注意是否对其他地方的监听产生影响
if (notifyRangeWhenWatch === NAME.ALL) {
this.notify(storeName, key);
}
// 注册监听后, 仅通知自己本次监听该 key 的数据
if (notifyRangeWhenWatch === NAME.MYSELF) {
const data = this.getData(storeName, key);
callback.call(this, data);
}
});
}
/**
* UI 取消组件监听回调
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息,包含需要取消的回掉等
*/
unwatch(storeName: StoreName, options: IOptions) {
// todo 该接口暂未支持,unwatch掉同一类,如仅传入store注销掉该store下的所有callback,同样options仅传入key注销掉该key下的所有callback
// options的callback function为必填参数,后续修改
if (!this.task[storeName]) {
return;
};
// if (isString(options)) {
// // 移除所有的监听
// if (options === '*') {
// const watcher = this.task[storeName];
// Object.keys(watcher).forEach(key => {
// watcher[key].clear();
// });
// } else {
// console.warn(`${NAME.PREFIX}unwatch warning: options is ${options}`);
// }
// return;
// }
const watcher = this.task[storeName];
Object.keys(options).forEach((key: string) => {
watcher[key].delete(options[key]);
});
}
/**
* 通用 store 数据更新,messageList 的变更需要单独处理
* @param {StoreName} storeName store 名称
* @param {string} key 变更的 key
* @param {unknown} data 变更的数据
*/
update(storeName: StoreName, key: string, data: unknown) {
// 基本数据类型时, 如果相等, 就不进行更新, 减少不必要的 notify
if (isString(data) || isNumber(data) || isBoolean(data)) {
const currentData = this.storeMap[storeName]['store'][key]; // eslint-disable-line
if (currentData === data) return;
}
this.storeMap[storeName]?.update(key, data);
this.notify(storeName, key);
}
/**
* 获取 Store 数据
* @param {StoreName} storeName store 名称
* @param {string} key 待获取的 key
* @returns {Any}
*/
getData(storeName: StoreName, key: string) {
return this.storeMap[storeName]?.getData(key);
}
/**
* UI 组件注册监听回调
* @param {StoreName} storeName store 名称
* @param {string} key 变更的 key
*/
private notify(storeName: StoreName, key: string) {
if (!this.task[storeName]) {
return;
}
const watcher = this.task[storeName];
if (watcher[key]) {
const callbackMap = watcher[key];
const data = this.getData(storeName, key);
for (const [callback] of callbackMap.entries()) {
callback.call(this, data);
}
}
}
public reset(storeName: StoreName, keyList: Array<string> = [], isNotificationNeeded = false) {
if (storeName in this.storeMap) {
const store = this.storeMap[storeName];
// reset all
if (keyList.length === 0) {
keyList = Object.keys(store?.store);
}
store.reset(keyList);
if (isNotificationNeeded) {
keyList.forEach((key) => {
this.notify(storeName, key);
});
}
}
}
// 批量修改多个 key-value
public updateStore(params: any, name?: StoreName): void {
const storeName = name ? name : StoreName.CALL;
Object.keys(params).forEach((key) => {
this.update(storeName, key, params[key]);
});
}
}
Binary file not shown.
Binary file not shown.
+6 -1
View File
@@ -89,5 +89,10 @@ export declare enum AudioPlayBackDevice {
}
export declare enum DeviceType {
MICROPHONE = "microphone",
CAMERA = "camera"
CAMERA = "camera",
SPEAKER = "speaker"
}
export declare enum CameraPosition {
FRONT = 0,
BACK = 1
}
+7 -1
View File
@@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DeviceType = exports.AudioPlayBackDevice = exports.StatusChange = exports.LanguageType = exports.VideoResolution = exports.VideoDisplayMode = exports.CallStatus = exports.CallRole = exports.CallMediaType = exports.StoreName = void 0;
exports.CameraPosition = exports.DeviceType = exports.AudioPlayBackDevice = exports.StatusChange = exports.LanguageType = exports.VideoResolution = exports.VideoDisplayMode = exports.CallStatus = exports.CallRole = exports.CallMediaType = exports.StoreName = void 0;
/**
* @property {String} call 1v1 通话 + 群组通话
* @property {String} CUSTOM 自定义 Store
@@ -101,4 +101,10 @@ var DeviceType;
(function (DeviceType) {
DeviceType["MICROPHONE"] = "microphone";
DeviceType["CAMERA"] = "camera";
DeviceType["SPEAKER"] = "speaker";
})(DeviceType = exports.DeviceType || (exports.DeviceType = {}));
var CameraPosition;
(function (CameraPosition) {
CameraPosition[CameraPosition["FRONT"] = 0] = "FRONT";
CameraPosition[CameraPosition["BACK"] = 1] = "BACK";
})(CameraPosition = exports.CameraPosition || (exports.CameraPosition = {}));
+107
View File
@@ -0,0 +1,107 @@
/**
* @property {String} call 1v1 通话 + 群组通话
* @property {String} CUSTOM 自定义 Store
*/
export enum StoreName {
CALL = 'call',
CUSTOM = 'custom'
}
/**
* @property {String} idle 空闲
* @property {String} connecting 呼叫等待中
* @property {String} connected 通话中
*/
export enum CallMediaType {
UNKNOWN,
AUDIO,
VIDEO,
}
/**
* @property {String} caller 主叫
* @property {String} callee 被叫
*/
export enum CallRole {
UNKNOWN = 'unknown',
CALLEE = 'callee',
CALLER = 'caller',
}
/**
* @property {String} idle 空闲
* @property {String} calling 呼叫等待中
* @property {String} connected 通话中
*/
export enum CallStatus {
IDLE = 'idle',
CALLING = 'calling',
CONNECTED = 'connected',
}
/**
* 视频画面显示模式
* 播放视频流默认使用 cover 模式; 播放屏幕共享流默认使用 contain 模式。
* @property {String} contain 优先保证视频内容全部显示。视频尺寸等比缩放,直至视频窗口的一边与视窗边框对齐。如果视频尺寸与显示视窗尺寸不一致,在保持长宽比的前提下,将视频进行缩放后填满视窗,缩放后的视频四周会有一圈黑边。
* @property {String} cover 优先保证视窗被填满。视频尺寸等比缩放,直至整个视窗被视频填满。如果视频长宽与显示窗口不同,则视频流会按照显示视窗的比例进行周边裁剪或图像拉伸后填满视窗
* @property {String} fill 保证视窗被填满的同时保证视频内容全部显示,但是不保证视频尺寸比例不变。视频的宽高会被拉伸至和视窗尺寸一致。(该选项值自 v4.12.1 开始支持)
*/
export enum VideoDisplayMode {
CONTAIN = 'contain',
COVER = 'cover',
FILL = 'fill',
}
/**
* 视频分辨率
* @property {String} 480p
* @property {String} 720p
* @property {String} 1080p
*/
export enum VideoResolution {
RESOLUTION_480P = '480p',
RESOLUTION_720P = '720p',
RESOLUTION_1080P = '1080p',
}
// 支持的语言
export enum LanguageType {
EN = 'en',
'ZH-CN' = 'zh-cn',
JA_JP = 'ja_JP',
}
export type TDeviceList = {
cameraList: any[],
microphoneList: any[],
currentCamera: any,
currentMicrophone: any,
};
/* === 【原来 TUICallKit 对外暴露】=== */
// 原来 web callKit 定义通知外部状态变更的变量, 对外暴露
export const StatusChange = {
IDLE: "idle",
BE_INVITED: "be-invited",
DIALING_C2C: "dialing-c2c",
DIALING_GROUP: "dialing-group",
CALLING_C2C_AUDIO: "calling-c2c-audio",
CALLING_C2C_VIDEO: "calling-c2c-video",
CALLING_GROUP_AUDIO: "calling-group-audio",
CALLING_GROUP_VIDEO: "calling-group-video",
} as const;
/* === 【小程序使用】=== */
/**
* @property {String} ear 听筒
* @property {String} speaker 免提
*/
export enum AudioPlayBackDevice {
EAR = 'ear',
SPEAKER = 'speaker',
};
export enum DeviceType {
MICROPHONE = 'microphone',
CAMERA = 'camera',
SPEAKER = 'speaker',
}
export enum CameraPosition {
FRONT = 0,
BACK = 1,
}
+20
View File
@@ -0,0 +1,20 @@
// 错误码
export const ErrorCode: any = {
SWITCH_TO_AUDIO_CALL_FAILED: 60001,
SWITCH_TO_VIDEO_CALL_FAILED: 60002,
MICROPHONE_UNAVAILABLE: 60003,
CAMERA_UNAVAILABLE: 60004,
BAN_DEVICE: 60005,
NOT_SUPPORTED_WEBRTC: 60006,
ERROR_BLACKLIST: 20007,
} as const;
export const ErrorMessage: any = {
SWITCH_TO_AUDIO_CALL_FAILED: 'switchToAudioCall-call-failed',
SWITCH_TO_VIDEO_CALL_FAILED: 'switchToVideoCall-call-failed',
MICROPHONE_UNAVAILABLE: 'microphone-unavailable',
CAMERA_UNAVAILABLE: 'camera-unavailable',
BAN_DEVICE: 'ban-device',
NOT_SUPPORTED_WEBRTC: 'not-supported-webrtc',
ERROR_BLACKLIST: 'blacklist-user-tips'
} as const;
+3 -1
View File
@@ -26,7 +26,9 @@ exports.CALL_DATA_KEY = {
CALL_ROLE: 'callRole',
CALL_MEDIA_TYPE: 'callMediaType',
LOCAL_USER_INFO: 'localUserInfo',
LOCAL_USER_INFO_EXCLUDE_VOLUMN: 'localUserInfoExcludeVolume',
REMOTE_USER_INFO_LIST: 'remoteUserInfoList',
REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST: 'remoteUserInfoExcludeVolumeList',
CALLER_USER_INFO: 'callerUserInfo',
IS_GROUP: 'isGroup',
CALL_DURATION: 'callDuration',
@@ -47,7 +49,7 @@ exports.CALL_DATA_KEY = {
ROOM_ID: 'roomID',
SHOW_SELECT_USER: 'showSelectUser',
};
exports.NAME = Object.assign({ PREFIX: '【CallService】', AUDIO: 'audio', VIDEO: 'video', LOCAL_VIDEO: 'localVideo', ERROR: 'error', TIMEOUT: 'timeout', RAF: 'raf', INTERVAL: 'interval', DEFAULT: 'default', BOOLEAN: 'boolean', STRING: 'string', NUMBER: 'number', OBJECT: 'object', ARRAY: 'array', FUNCTION: 'function', UNDEFINED: "undefined", ALL: 'all', MYSELF: 'myself', DEVICE_LIST: 'deviceList' }, exports.CALL_DATA_KEY);
exports.NAME = Object.assign({ PREFIX: '【CallService】', AUDIO: 'audio', VIDEO: 'video', LOCAL_VIDEO: 'localVideo', ERROR: 'error', TIMEOUT: 'timeout', RAF: 'raf', INTERVAL: 'interval', DEFAULT: 'default', BOOLEAN: 'boolean', STRING: 'string', NUMBER: 'number', OBJECT: 'object', ARRAY: 'array', FUNCTION: 'function', UNDEFINED: "undefined", ALL: 'all', MYSELF: 'myself', DEVICE_LIST: 'deviceList', CAMERA_POSITION: 'cameraPosition' }, exports.CALL_DATA_KEY);
exports.AudioCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call.png';
exports.VideoCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call-video-reverse.svg';
exports.MAX_NUMBER_ROOM_ID = 2147483647;
+70
View File
@@ -0,0 +1,70 @@
export * from './call';
export * from './error';
export * from './log';
// import { keys } from 'ts-transformer-keys';
// import { ICallStore } from '../interface/store';
// console.warn('--> ', keys<ICallStore>())
export const CALL_DATA_KEY: any = {
CALL_STATUS: 'callStatus',
CALL_ROLE: 'callRole',
CALL_MEDIA_TYPE: 'callMediaType',
LOCAL_USER_INFO: 'localUserInfo',
LOCAL_USER_INFO_EXCLUDE_VOLUMN: 'localUserInfoExcludeVolume',
REMOTE_USER_INFO_LIST: 'remoteUserInfoList',
REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST: 'remoteUserInfoExcludeVolumeList',
CALLER_USER_INFO: 'callerUserInfo',
IS_GROUP: 'isGroup',
CALL_DURATION: 'callDuration',
CALL_TIPS: 'callTips',
TOAST_INFO: 'toastInfo',
IS_MINIMIZED: 'isMinimized',
ENABLE_FLOAT_WINDOW: 'enableFloatWindow',
BIG_SCREEN_USER_ID: 'bigScreenUserId',
LANGUAGE: 'language',
IS_CLICKABLE: 'isClickable',
DISPLAY_MODE: 'displayMode',
VIDEO_RESOLUTION: 'videoResolution',
PUSHER: 'pusher',
PLAYER: 'player',
IS_EAR_PHONE: 'isEarPhone',
SHOW_PERMISSION_TIP: 'SHOW_PERMISSION_TIP',
GROUP_ID: 'groupID',
ROOM_ID: 'roomID',
SHOW_SELECT_USER: 'showSelectUser',
};
export const NAME = {
PREFIX: '【CallService】',
AUDIO: 'audio',
VIDEO: 'video',
LOCAL_VIDEO: 'localVideo',
ERROR: 'error',
TIMEOUT: 'timeout',
RAF: 'raf',
INTERVAL: 'interval',
DEFAULT: 'default',
BOOLEAN: 'boolean',
STRING: 'string',
NUMBER: 'number',
OBJECT: 'object',
ARRAY: 'array',
FUNCTION: 'function',
UNDEFINED: "undefined",
ALL: 'all',
MYSELF: 'myself',
DEVICE_LIST: 'deviceList',
CAMERA_POSITION: 'cameraPosition',
...CALL_DATA_KEY,
};
export const AudioCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call.png';
export const VideoCallIcon = 'https://web.sdk.qcloud.com/component/TUIKit/assets/call-video-reverse.svg';
export const MAX_NUMBER_ROOM_ID = 2147483647;
export enum PLATFORM {
// eslint-disable-next-line no-unused-vars
MAC = 'mac',
// eslint-disable-next-line no-unused-vars
WIN = 'win',
}
+9
View File
@@ -0,0 +1,9 @@
/* eslint-disable */
// 唯一一个变量格式有问题的, 但是为了和原来 TUICallKit 对外暴露的保持一致
export enum LOG_LEVEL {
NORMAL = 0, // 普通级别,日志量较多,接入时建议使用
RELEASE = 1, // release级别,SDK 输出关键信息,生产环境时建议使用
WARNING = 2, // 告警级别,SDK 只输出告警和错误级别的日志
ERROR = 3, // 错误级别,SDK 只输出错误级别的日志
NONE = 4, // 无日志级别,SDK 将不打印任何日志
}
+23
View File
@@ -0,0 +1,23 @@
import TUICallService, { TUIGlobal, TUIStore } from './CallService/index';
import { StoreName, NAME, CallRole, CallMediaType, CallStatus, StatusChange, VideoResolution, VideoDisplayMode, AudioPlayBackDevice } from './const/index';
import { t } from './locales/index';
// 实例化
const TUICallKitServer = TUICallService.getInstance();
// 输出产物
export {
TUIGlobal,
TUIStore,
StoreName,
TUICallKitServer,
NAME,
CallStatus,
CallRole,
CallMediaType,
StatusChange,
VideoResolution,
VideoDisplayMode,
AudioPlayBackDevice,
t,
};
@@ -0,0 +1,97 @@
import { CallStatus, CallRole } from '../const/index';
/**
* @interface ITUICallService
*/
export interface ITUICallService {
/**
* 初始化 Service
* @function
* @private
*/
init(params: any): void;
/**
* 1v1 通话
* @function
* @param {SwitchUserStatusParams} options 用户状态控制参数
*/
call(callParams: ICallParams): void;
}
type SDKAppID = { SDKAppID: number; } | { sdkAppID: number; };
export interface IInitParamsBase {
userID: string;
userSig: string;
tim?: any;
isFromChat?: boolean;
}
export type IInitParams = IInitParamsBase & SDKAppID;
// call params interface
export interface ICallParams {
userID: string;
type: number;
roomID?: number;
userData?: string;
timeout?: number;
offlinePushInfo?: IOfflinePushInfo;
}
// groupCall params interface
export interface IGroupCallParams {
userIDList: Array<string>;
type: number;
groupID: string;
roomID?: number;
userData?: string;
timeout?: number;
offlinePushInfo?: IOfflinePushInfo;
}
// userInfo interface
export interface IUserInfo {
userId: string;
nick?: string;
avatar?: string;
remark?: string;
displayUserInfo?: string; // 远端用户信息展示: remark -> nick -> userId, 简化 UI 组件; 本地用户信息展示: nick -> userId
isAudioAvailable?: boolean; // 用来设置: 麦克风是否打开
isVideoAvailable?: boolean; // 用来设置: 摄像头是否打开
volume?: number;
isEnter?: boolean; // 远端用户, 用来控制预览远端是否显示 loading 效果; 本地用户, 用来控制 "呼叫"、"接通" 接通后显示的 loading 效果
domId?: string; // 播放流 dom 节点, localUserInfo 的 domId = 'localVideo'; remoteUserInfo 的 domId 就是 userId
}
export interface IOfflinePushInfo {
title?: string,
description?: string,
androidOPPOChannelID?: string,
extension: String
}
export interface ICallbackParam {
beforeCalling?: (...args: any[]) => void;
afterCalling?: (...args: any[]) => void;
onMinimized?: (...args: any[]) => void;
onMessageSentByMe?: (...args: any[]) => void;
kickedOut?: (...args: any[]) => void;
statusChanged?: (...args: any[]) => void;
}
export interface ISelfInfoParams {
nickName: string;
avatar: string;
}
export interface IBellParams {
callRole?: CallRole;
callStatus?: CallStatus;
isMuteBell?: boolean;
calleeBellFilePath?: string;
}
export interface IInviteUserParams {
userIDList: string[];
offlinePushInfo?: IOfflinePushInfo;
}
export interface IJoinInGroupCallParams {
type: number;
groupID: string;
roomID: number;
}
+4 -1
View File
@@ -1,4 +1,4 @@
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList } from '../const/index';
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition } from '../const/index';
import { IUserInfo } from './index';
export interface IToastInfo {
text: string;
@@ -9,7 +9,9 @@ export interface ICallStore {
callRole: CallRole;
callMediaType: CallMediaType;
localUserInfo: IUserInfo;
localUserInfoExcludeVolume: IUserInfo;
remoteUserInfoList: Array<IUserInfo>;
remoteUserInfoExcludeVolumeList: Array<IUserInfo>;
callerUserInfo: IUserInfo;
isGroup: boolean;
callDuration: string;
@@ -24,6 +26,7 @@ export interface ICallStore {
deviceList: TDeviceList;
groupID: string;
roomID: number;
cameraPosition: CameraPosition;
displayMode: VideoDisplayMode;
videoResolution: VideoResolution;
pusher: any;
@@ -0,0 +1,41 @@
import { CallStatus, CallRole, CallMediaType, VideoDisplayMode, VideoResolution, TDeviceList, CameraPosition } from '../const/index';
import { IUserInfo } from './index';
export interface IToastInfo {
text: string;
type?: string; // 默认 info 通知, 取值: 'info' | 'warn' | 'success' | 'error'
}
export interface ICallStore {
callStatus: CallStatus; // 当前的通话状态, 默认: 'idle'
callRole: CallRole; // 通话角色, 默认: 'callee'
callMediaType: CallMediaType; // 通话类型
localUserInfo: IUserInfo; // 自己的信息, 默认: { userId: '' }
localUserInfoExcludeVolume: IUserInfo; // 不包含音量的当前用户信息
remoteUserInfoList: Array<IUserInfo>; // 远端用户信息列表, 默认: []
remoteUserInfoExcludeVolumeList: Array<IUserInfo>; // 不包含音量的远端用户信息列表
// 被叫在未接通时,展示主叫的 userId、头像。但是如果主叫进入通话后再挂断,此时被叫无法知道主叫的信息了。
// 因为目前 store 中仅提供了 remoteUserInfoList 数据,主叫离开后,被叫就没有主叫的信息了。因此考虑在 store 中增加 callerUserInfo 字段。
callerUserInfo: IUserInfo;
isGroup: boolean; // 是否是群组通话, 默认: false
callDuration: string; // 通话时长, 默认: '00:00:00'
callTips: string; // 通话提示的信息. 例如: '等待谁接听', 'xxx 拒绝通话', 'xxx 挂断通话'
toastInfo: IToastInfo; // 远端用户挂断、拒绝、超时、忙线等的 toast 提示信息
isMinimized: boolean; // 当前是否悬浮窗模式, 默认: false
enableFloatWindow: boolean, // 开启/关闭悬浮窗功能,默认: false
bigScreenUserId: string, // 当前大屏幕显示的 userID 用户
language: string; // 当前语言
isClickable: boolean; // 按钮是否可点击(呼叫后, '挂断' 按钮不可点击, 发送信令后才可以点击)
showPermissionTip: boolean; // 设备权限弹窗是否展示(如果有麦克风权限为 false,如果没有麦克风也没有摄像头权限为 true)
deviceList: TDeviceList;
groupID: string;
roomID: number;
cameraPosition: CameraPosition; // 前置或后置,值为front, back
// TUICallKit 组件上的属性
displayMode: VideoDisplayMode; // 设置预览远端的画面显示模式, 默认: VideoDisplayMode.COVER
videoResolution: VideoResolution; // 设置视频分辨率, 默认: VideoResolution.RESOLUTION_480P
// 小程序相关属性
pusher: any;
player: any[];
isEarPhone: boolean; // 是否是听筒, 默认: false
showSelectUser: boolean;
}
@@ -0,0 +1,38 @@
/**
* @interface TUIGlobal
* @property {Object} global 根据运行环境代理 wx、uni、window
* @property {Boolean} isPC true 标识是 pc 网页
* @property {Boolean} isH5 true 标识是 手机 H5
* @property {Boolean} isWeChat true 标识是 微信小程序
* @property {Boolean} isApp true 标识是 uniapp 打包的 native app
* @property {Boolean} isUniPlatform true 标识当前应用是通过 uniapp 平台打包的产物
* @property {Boolean} isOfficial true 标识是腾讯云官网 Demo 应用
* @property {Boolean} isWIN true 标识是window系统pc
* @property {Boolean} isMAC true 标识是mac os系统pc
*/
export interface ITUIGlobal {
global: any; // 挂在 wx、uni、window 对象
isPC: boolean;
isH5: boolean;
isWeChat: boolean;
isApp: boolean;
isUniPlatform: boolean;
isOfficial: boolean;
isWIN: boolean;
isMAC: boolean;
/**
* 初始化 TUIGlobal 环境变量
* @function
* @private
*/
initEnv(): void;
/**
* 初始化 isOfficial
* @function
* @param {number} SDKAppID 当前实例的应用 SDKAppID
* @private
*/
initOfficial(SDKAppID: number): void;
}
@@ -0,0 +1,85 @@
import { StoreName } from '../const/index';
// 此处 Map 的 value = 1 为占位作用
export type Task = Record<StoreName, Record<string, Map<(data?: unknown) => void, 1>>>
export interface IOptions {
[key: string]: (newData?: any) => void;
}
/**
* @class TUIStore
* @property {ICustomStore} customStore 自定义 store,可根据业务需要通过以下 API 进行数据操作。
*/
export interface ITUIStore {
task: Task;
/**
* UI 组件注册监听
* @function
* @param {StoreName} storeName store 名称
* @param {IOptions} options UI 组件注册的监听信息
* @param {Object} params 扩展参数
* @param {String} params.notifyRangeWhenWatch 注册时监听时的通知范围
* @example
* // UI 层监听会话列表更新通知
* let onConversationListUpdated = function(conversationList) {
* console.warn(conversationList);
* }
* TUIStore.watch(StoreName.CONV, {
* conversationList: onConversationListUpdated,
* })
*/
watch(storeName: StoreName, options: IOptions, params?: any): void;
/**
* UI 组件取消监听回调
* @function
* @param {StoreName} storeName store 名称
* @param {IOptions} options 监听信息,包含需要取消的回调等
* @example
* // UI 层取消监听会话列表更新通知
* TUIStore.unwatch(StoreName.CONV, {
* conversationList: onConversationListUpdated,
* })
*/
unwatch(storeName: StoreName, options: IOptions | string): void;
/**
* 获取 store 中的数据
* @function
* @param {StoreName} storeName store 名称
* @param {String} key 需要获取的 key
* @private
*/
getData(storeName: StoreName, key: string): any;
/**
* 更新 store
* - 需要使用自定义 store 时可以用此 API 更新自定义数据
* @function
* @param {StoreName} storeName store 名称
* @param {String} key 需要更新的 key
* @example
* // UI 层更新自定义 Store 数据
* TUIStore.update(StoreName.CUSTOM, 'customKey', 'customData')
*/
update(storeName: StoreName, key: string, data: unknown): void;
/**
* 重置 store 内数据
* @function
* @param {StoreName} storeName store 名称
* @param {Array<string>} keyList 需要 reset 的 keyList
* @param {boolean} isNotificationNeeded 是否需要触发更新
* @private
*/
reset: (storeName: StoreName, keyList?: Array<string>, isNotificationNeeded?: boolean) => void;
/**
* 修改多个 key-value
* @param {Object} params 多个 key-value 组成的 object
* @param {StoreName} storeName store 名称
*/
updateStore: (params: any, name?: StoreName) => void;
}
@@ -0,0 +1,4 @@
export * from './ICallService';
export * from './ICallStore';
export * from './ITUIGlobal';
export * from './ITUIStore';
+35 -24
View File
@@ -1,25 +1,54 @@
export declare const en: {
hangup: string;
reject: string;
accept: string;
camera: string;
microphone: string;
speaker: string;
'open camera': string;
'close camera': string;
'open microphone': string;
'close microphone': string;
'video-to-audio': string;
'other side reject call': string;
'reject call': string;
accept: string;
cancel: string;
'other side line busy': string;
'in busy': string;
'call timeout': string;
'no response from the other side': string;
'end call': string;
timeout: string;
'kick out': string;
'caller calling message': string;
'callee calling video message': string;
'callee calling audio message': string;
'no microphone access': string;
'no camera access': string;
'invite member': string;
speaker: string;
'Invited group call': string;
waiting: string;
me: string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
'camera enabled': string;
'camera disabled': string;
'microphone enabled': string;
'microphone disabled': string;
'speaker phone': string;
'ear piece': string;
'wait to be called': string;
answered: string;
'people in the call': string;
'failed to obtain speakers': string;
'you have a new call': string;
'Those involved': string;
call: string;
'video-call': string;
@@ -35,18 +64,15 @@ export declare const en: {
'not-support-multi-call': string;
userID: string;
'already-enter': string;
waiting: string;
'camera-opened': string;
'camera-closed': string;
'microphone-opened': string;
'microphone-closed': string;
camera: string;
microphone: string;
timeout: string;
'kick out': string;
'image-resolution': string;
'default-image-resolution': string;
'invited-person': string;
'video-to-audio': string;
me: string;
'be-rejected': string;
'be-no-response': string;
'be-line-busy': string;
@@ -72,19 +98,4 @@ export declare const en: {
'accept-error': string;
'accept-device-error': string;
'call-error': string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'open camera': string;
'close camera': string;
'open microphone': string;
'close microphone': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
};
+45 -28
View File
@@ -2,27 +2,62 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.en = void 0;
exports.en = {
// 按钮文案
'hangup': 'Hang up',
'reject': 'Decline',
'accept': 'Accept',
'camera': 'Camera',
'microphone': 'Microphone',
'speaker': 'speaker',
'open camera': 'Open Camera',
'close camera': 'Close Camera',
'open microphone': 'Open Microphone',
'close microphone': 'Close Microphone',
'video-to-audio': 'Switch to audio',
// 通话结果
'other side reject call': 'other side reject call',
'reject call': 'Reject Call',
'accept': 'Accept',
'cancel': 'Cancel Call',
'other side line busy': 'other side line busy',
'in busy': 'in busy',
'call timeout': 'call timeout',
'no response from the other side': 'no response from the other side',
'end call': 'end call',
'timeout': 'timeout',
'kick out': 'kick out',
'caller calling message': 'Waiting for the callee to accept the invitation...',
'callee calling video message': 'You are invited to a video call...',
'callee calling audio message': 'You are invited to a audio call...',
// 通话提示语
'caller calling message': 'Awaiting response',
'callee calling video message': 'invites you to a video call',
'callee calling audio message': 'invites you to a voice call',
'no microphone access': 'no microphone access',
'no camera access': 'no camera access',
'invite member': 'Invite Member',
'speaker': 'speaker',
'Invited group call': 'Invited you to a group call',
'Invited group call': 'invites you to a group call',
'waiting': 'Calling...',
'me': '(me)',
// 弹出层文案
'browser-authorization': 'Browser authorization',
'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy',
'win-privacy': 'Setting -> Privacy and Security -> App permissions',
'mac-preferences': 'Open System Preferences',
'win-preferences': 'Open Setting',
'Please enter userID': 'Please enter userID',
'View more': 'View more',
'people selected': 'people selected',
'Select all': 'Select all',
'Cancel': 'Cancel',
'Done': 'Done',
// UI3.0 新增
'camera enabled': 'Camera On',
'camera disabled': 'Camera Off',
'microphone enabled': 'Unmuted',
'microphone disabled': 'Muted',
'speaker phone': 'Speaker',
'ear piece': 'Earpiece',
'wait to be called': 'Waiting',
'answered': 'Connected',
'people in the call': ' other(s) in the call',
'failed to obtain speakers': 'failed to obtain speakers',
'you have a new call': 'You have a new call',
// 待废弃文案
'Those involved': 'Those involved in the call are',
'call': 'call',
'video-call': 'video call',
@@ -38,18 +73,15 @@ exports.en = {
'not-support-multi-call': 'multi-person call interface is not open',
'userID': 'userID',
'already-enter': 'entered the call',
'waiting': 'Calling...',
'camera-opened': 'Camera on',
'camera-closed': 'Camera off',
'microphone-opened': 'Mic on',
'microphone-closed': 'Mic off',
'camera': 'Camera',
'microphone': 'Microphone',
'timeout': 'timeout',
'kick out': 'kick out',
'image-resolution': 'Resolution',
'default-image-resolution': 'Default',
'invited-person': 'Invite',
'video-to-audio': 'Switch to audio',
'me': '(me)',
'be-rejected': 'Call declined, ',
'be-no-response': 'No response, ',
'be-line-busy': 'Line busy, ',
@@ -75,19 +107,4 @@ exports.en = {
'accept-error': 'Accept failed',
'accept-device-error': 'Accept failed, unable to auth calling device',
'call-error': 'Start call failed',
'browser-authorization': 'Browser authorization',
'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy',
'win-privacy': 'Setting -> Privacy and Security -> App permissions',
'mac-preferences': 'Open System Preferences',
'win-preferences': 'Open Setting',
'open camera': 'Open Camera',
'close camera': 'Close Camera',
'open microphone': 'Open Microphone',
'close microphone': 'Close Microphone',
'Please enter userID': 'Please enter userID',
'View more': 'View more',
'people selected': 'people selected',
'Select all': 'Select all',
'Cancel': 'Cancel',
'Done': 'Done',
};
+107
View File
@@ -0,0 +1,107 @@
export const en = {
// 按钮文案
'hangup': 'Hang up',
'reject': 'Decline',
'accept': 'Accept',
'camera': 'Camera',
'microphone': 'Microphone',
'speaker': 'speaker',
'open camera': 'Open Camera',
'close camera': 'Close Camera',
'open microphone': 'Open Microphone',
'close microphone': 'Close Microphone',
'video-to-audio': 'Switch to audio',
// 通话结果
'other side reject call': 'other side reject call',
'reject call': 'Reject Call',
'cancel': 'Cancel Call',
'other side line busy': 'other side line busy',
'in busy': 'in busy',
'call timeout': 'call timeout',
'no response from the other side': 'no response from the other side',
'end call': 'end call',
// 通话提示语
'caller calling message': 'Awaiting response',
'callee calling video message': 'invites you to a video call',
'callee calling audio message': 'invites you to a voice call',
'no microphone access': 'no microphone access',
'no camera access': 'no camera access',
'invite member': 'Invite Member',
'Invited group call': 'invites you to a group call',
'waiting': 'Calling...',
'me': '(me)',
// 弹出层文案
'browser-authorization': 'Browser authorization',
'mac-privacy': 'System Preferences -> Security and Privacy -> Privacy',
'win-privacy': 'Setting -> Privacy and Security -> App permissions',
'mac-preferences': 'Open System Preferences',
'win-preferences': 'Open Setting',
'Please enter userID': 'Please enter userID',
'View more': 'View more',
'people selected': 'people selected',
'Select all': 'Select all',
'Cancel': 'Cancel',
'Done': 'Done',
// UI3.0 新增
'camera enabled': 'Camera On',
'camera disabled': 'Camera Off',
'microphone enabled': 'Unmuted',
'microphone disabled': 'Muted',
'speaker phone': 'Speaker',
'ear piece': 'Earpiece',
'wait to be called': 'Waiting',
'answered': 'Connected',
'people in the call': ' other(s) in the call',
'failed to obtain speakers': 'failed to obtain speakers',
'you have a new call': 'You have a new call',
// 待废弃文案
'Those involved': 'Those involved in the call are',
'call': 'call',
'video-call': 'video call',
'audio-call': 'audio call',
'search': 'search',
'search-result': 'search result',
'no-user': 'user not found',
'member-not-added': 'member not added',
'input-phone-userID': 'phone number or userID',
'not-login': 'not logged in',
'login-status-expire': 'login status is invalid, please refresh the page and try again',
'experience-multi-call': 'experience multi-person calls, please download the full-featured demo: ',
'not-support-multi-call': 'multi-person call interface is not open',
'userID': 'userID',
'already-enter': 'entered the call',
'camera-opened': 'Camera on',
'camera-closed': 'Camera off',
'microphone-opened': 'Mic on',
'microphone-closed': 'Mic off',
'timeout': 'timeout',
'kick out': 'kick out',
'image-resolution': 'Resolution',
'default-image-resolution': 'Default',
'invited-person': 'Invite',
'be-rejected': 'Call declined, ',
'be-no-response': 'No response, ',
'be-line-busy': 'Line busy, ',
'be-canceled': 'The call is canceled, ',
'voice-call-end': 'Voice call ended',
'video-call-end': 'Video call ended',
'method-call-failed': 'Failed to sync the operation',
'failed-to-obtain-permission': 'Failed to obtain permissions',
'environment-detection-failed': 'Failed to check the environment',
'switchToAudioCall-call-failed': 'switch to audio call method failed',
'switchToVideoCall-call-failed': 'switch to video call method failed',
'microphone-unavailable': 'No mic found',
'camera-unavailable': 'No camera found',
'ban-device': 'Device access denied',
'not-supported-webrtc': 'Your current environment does not support WebRTC',
'blacklist-user-tips': 'The identifier is in blacklist. Failed to send this message!',
'is-already-calling': 'TUICallKit is already on a call',
'need-init': 'Before initiating a call with TUICallKit, ensure that the TUICallKitServer.init() method has executed successfully. ',
"can't call yourself": "Can't call yourself", // eslint-disable-line
'Use-phone-and-computer': 'Use your mobile phone and computer to experience video calls',
'Wechat scan right QR code': 'Wechat scan right QR code',
'Scan the QR code above': 'Scan the QR code above',
'accept-error': 'Accept failed',
'accept-device-error': 'Accept failed, unable to auth calling device',
'call-error': 'Start call failed',
};
@@ -18,6 +18,7 @@ exports.CallTips = {
TIMEOUT: 'timeout',
KICK_OUT: 'kick out',
CALLER_CALLING_MSG: 'caller calling message',
CALLER_GROUP_CALLING_MSG: 'wait to be called',
CALLEE_CALLING_VIDEO_MSG: 'callee calling video message',
CALLEE_CALLING_AUDIO_MSG: 'callee calling audio message',
NO_MICROPHONE_DEVICE_PERMISSION: 'no microphone access',
@@ -0,0 +1,57 @@
import { TUIStore } from '../CallService/index';
import { NAME, StoreName } from '../const/index';
import { en } from './en';
import { zh } from './zh-cn';
import { ja_JP } from './ja_JP';
export const CallTips: any = {
OTHER_SIDE: 'other side',
CANCEL: 'cancel',
OTHER_SIDE_REJECT_CALL: 'other side reject call',
REJECT_CALL: 'reject call',
OTHER_SIDE_LINE_BUSY: 'other side line busy',
IN_BUSY: 'in busy',
CALL_TIMEOUT: 'call timeout',
END_CALL: 'end call',
TIMEOUT: 'timeout',
KICK_OUT: 'kick out',
CALLER_CALLING_MSG: 'caller calling message',
CALLER_GROUP_CALLING_MSG: 'wait to be called',
CALLEE_CALLING_VIDEO_MSG: 'callee calling video message',
CALLEE_CALLING_AUDIO_MSG: 'callee calling audio message',
NO_MICROPHONE_DEVICE_PERMISSION: 'no microphone access',
NO_CAMERA_DEVICE_PERMISSION: 'no camera access',
};
export const languageData: languageDataType = {
en,
'zh-cn': zh,
ja_JP,
};
// language translate
export function t(key: any): string {
const language = TUIStore.getData(StoreName.CALL, NAME.LANGUAGE);
// eslint-disable-next-line
for (const langKey in languageData) {
if (langKey === language) {
const currentLanguage = languageData[langKey];
// eslint-disable-next-line
for (const sentenceKey in currentLanguage) {
if (sentenceKey === key) {
return currentLanguage[sentenceKey];
}
}
}
}
const enString = key['en']?.key; // eslint-disable-line
console.error(`${NAME.PREFIX}translation is not found: ${key}.`);
return enString;
}
interface languageItemType {
[key: string]: string;
}
interface languageDataType {
[key: string]: languageItemType;
}
+31 -17
View File
@@ -1,23 +1,50 @@
export declare const ja_JP: {
hangup: string;
reject: string;
accept: string;
camera: string;
microphone: string;
speaker: string;
'other side reject call': string;
'reject call': string;
accept: string;
cancel: string;
'other side line busy': string;
'in busy': string;
'call timeout': string;
'end call': string;
timeout: string;
'kick out': string;
'caller calling message': string;
'callee calling video message': string;
'callee calling audio message': string;
'no microphone access': string;
'no camera access': string;
'invite member': string;
speaker: string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
'open camera': string;
'close camera': string;
'open microphone': string;
'close microphone': string;
'camera enabled': string;
'camera disabled': string;
'microphone enabled': string;
'microphone disabled': string;
'speaker phone': string;
'wait to be called': string;
answered: string;
'people in the call': string;
'failed to obtain speakers': string;
'you have a new call': string;
timeout: string;
'kick out': string;
'Invited group call': string;
'Those involved': string;
call: string;
@@ -42,8 +69,6 @@ export declare const ja_JP: {
'camera-closed': string;
'microphone-opened': string;
'microphone-closed': string;
camera: string;
microphone: string;
'image-resolution': string;
'default-image-resolution': string;
'invited-person': string;
@@ -71,15 +96,4 @@ export declare const ja_JP: {
'accept-error': string;
'accept-device-error': string;
'call-error': string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
};
+41 -21
View File
@@ -2,25 +2,58 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.ja_JP = void 0;
exports.ja_JP = {
// 按钮文案
'hangup': '通話終了',
'reject': '拒否',
'accept': '応答',
'camera': 'カメラ',
'microphone': 'マイク',
'speaker': 'スピーカー',
// 通话结果
'other side reject call': '通話が拒否されました',
'reject call': '通話拒否',
'accept': '応答',
'cancel': '通話をキャンセル',
'other side line busy': '相手が通話中です',
'in busy': '通話中',
'call timeout': '呼び出しタイムアウト',
'end call': '通話終了',
'timeout': 'タイムアウト',
'kick out': 'キックアウトされました',
'caller calling message': '相手が招待を承諾するのを待っています。',
'callee calling video message': 'ビデオ通話に招待されました',
'callee calling audio message': '音声通話に招待されました。',
// 通话提示语
'caller calling message': '応答を待っています',
'callee calling video message': 'ビデオ通話に招待されました',
'callee calling audio message': '音声通話に招待されました',
'no microphone access': 'マイクにアクセスできません',
'no camera access': 'カメラにアクセスできません',
'invite member': 'メンバーを招待する',
'speaker': 'スピーカー',
// 弹出层文案
'browser-authorization': 'ブラウザ認証',
'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー',
'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可',
'mac-preferences': 'システム環境設定を開く',
'win-preferences': 'システム設定を開く',
'Please enter userID': 'ユーザーIDを入力してください',
'View more': 'もっと見る',
'people selected': '人が選択されました',
'Select all': 'すべて選択',
'Cancel': 'キャンセル',
'Done': '完了',
// UI3.0文案
'open camera': 'オープンカメラ',
'close camera': 'カメラを閉じる',
'open microphone': 'オープンマイク',
'close microphone': 'マイクを閉じる',
'camera enabled': 'カメラオン',
'camera disabled': 'カメラオフ',
'microphone enabled': 'マイクオン',
'microphone disabled': 'マイクオフ',
'speaker phone': 'スピーカーオン',
'wait to be called': '待機中',
'answered': '接続済み',
'people in the call': '通話に参加している人たち',
'failed to obtain speakers': 'スピーカーが見つかりません',
'you have a new call': '新しい通話があります',
// 待废弃文案
'timeout': 'タイムアウト',
'kick out': 'キックアウトされました',
'Invited group call': 'グループ通話に招待されました。',
'Those involved': '参加者:',
'call': '通話',
@@ -45,13 +78,11 @@ exports.ja_JP = {
'camera-closed': 'カメラがオフになっています',
'microphone-opened': 'マイクがオンになっています',
'microphone-closed': 'マイクがオフになっています',
'camera': 'カメラ',
'microphone': 'マイク',
'image-resolution': '解像度',
'default-image-resolution': 'デフォルト解像度',
'invited-person': 'メンバーを招待',
'video-to-audio': '音声通話に切り替えます',
'me': '自分',
'me': '(自分)',
'be-rejected': '通話が拒否されました, ',
'be-no-response': '応答なし, ',
'be-line-busy': '相手が通話中です, ',
@@ -74,15 +105,4 @@ exports.ja_JP = {
'accept-error': '接続できませんでした',
'accept-device-error': '接続できませんでした。発信側デバイスを認証できません',
'call-error': '通話が開始できませんでした',
'browser-authorization': 'ブラウザ認証',
'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー',
'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可',
'mac-preferences': 'システム環境設定を開く',
'win-preferences': 'システム設定を開く',
'Please enter userID': 'ユーザーIDを入力してください',
'View more': 'もっと見る',
'people selected': '人が選択されました',
'Select all': 'すべて選択',
'Cancel': 'キャンセル',
'Done': '完了',
};
+105
View File
@@ -0,0 +1,105 @@
export const ja_JP = {
// 按钮文案
'hangup': '通話終了',
'reject': '拒否',
'accept': '応答',
'camera': 'カメラ',
'microphone': 'マイク',
'speaker': 'スピーカー',
// 通话结果
'other side reject call': '通話が拒否されました',
'reject call': '通話拒否',
'cancel': '通話をキャンセル',
'other side line busy': '相手が通話中です',
'in busy': '通話中',
'call timeout': '呼び出しタイムアウト',
'end call': '通話終了',
// 通话提示语
'caller calling message': '応答を待っています',
'callee calling video message': 'ビデオ通話に招待されました',
'callee calling audio message': '音声通話に招待されました',
'no microphone access': 'マイクにアクセスできません',
'no camera access': 'カメラにアクセスできません',
'invite member': 'メンバーを招待する',
// 弹出层文案
'browser-authorization': 'ブラウザ認証',
'mac-privacy': 'システム環境設定 -> セキュリティとプライバシー ->プライバシー',
'win-privacy': '設定 -> セキュリティとプライバシー ->アプリのアクセス許可',
'mac-preferences': 'システム環境設定を開く',
'win-preferences': 'システム設定を開く',
'Please enter userID': 'ユーザーIDを入力してください',
'View more': 'もっと見る',
'people selected': '人が選択されました',
'Select all': 'すべて選択',
'Cancel': 'キャンセル',
'Done': '完了',
// UI3.0文案
'open camera': 'オープンカメラ',
'close camera': 'カメラを閉じる',
'open microphone': 'オープンマイク',
'close microphone': 'マイクを閉じる',
'camera enabled': 'カメラオン',
'camera disabled': 'カメラオフ',
'microphone enabled': 'マイクオン',
'microphone disabled': 'マイクオフ',
'speaker phone': 'スピーカーオン',
'wait to be called': '待機中',
'answered': '接続済み',
'people in the call': '通話に参加している人たち',
'failed to obtain speakers': 'スピーカーが見つかりません',
'you have a new call': '新しい通話があります',
// 待废弃文案
'timeout': 'タイムアウト',
'kick out': 'キックアウトされました',
'Invited group call': 'グループ通話に招待されました。',
'Those involved': '参加者:',
'call': '通話',
'video-call': 'ビデオ通話',
'audio-call': '音声通話',
'search': '検索',
'search-result': '検索結果',
'Wechat scan right QR code': 'WeChatで右側にあるQRコードを読み取ります。',
'Use-phone-and-computer': '携帯電話とコンピュータを使用してビデオ通話を体験してください',
'Scan the QR code above': '上のQRコードを読み取ります。',
'no-user': 'ユーザーが見つかりませんでした',
'member-not-added': 'メンバーが追加されていません',
'not-login': 'ログインしていません',
'login-status-expire': 'ログインの有効期限が過ぎています。ページを更新してもう一度お試しください',
'experience-multi-call': '複数人で同時に音声通話できるグループ通話機能を体験するには、全機能のデモをダウンロードしてください',
'not-support-multi-call': 'グループ通話インターフェイスが開いていません',
'input-phone-userID': '携帯電話番号/ユーザーIDを入力してください',
'userID': 'ユーザーID',
'already-enter': 'すでに通話に参加しています',
'waiting': '応答を待っています...',
'camera-opened': 'カメラがオンになっています',
'camera-closed': 'カメラがオフになっています',
'microphone-opened': 'マイクがオンになっています',
'microphone-closed': 'マイクがオフになっています',
'image-resolution': '解像度',
'default-image-resolution': 'デフォルト解像度',
'invited-person': 'メンバーを招待',
'video-to-audio': '音声通話に切り替えます',
'me': '(自分)',
'be-rejected': '通話が拒否されました, ',
'be-no-response': '応答なし, ',
'be-line-busy': '相手が通話中です, ',
'be-canceled': '相手が通話をキャンセルしました',
'voice-call-end': '音声通話が終了しました',
'video-call-end': 'ビデオ通話が終了しました',
'method-call-failed': '操作の同期に失敗しました',
'failed-to-obtain-permission': '権限の取得に失敗しました',
'environment-detection-failed': '環境の検出に失敗しました',
'switchToAudioCall-call-failed': '音声通話に切り替えることはできません',
'switchToVideoCall-call-failed': 'ビデオ通話に切り替えることはできません',
'microphone-unavailable': '使用できるマイクがありません',
'camera-unavailable': '使用できるカメラがありません',
'ban-device': 'デバイスへのアクセスが拒否されました',
'not-supported-webrtc': '現在の環境はWebRTCをサポートしていません',
'blacklist-user-tips': 'ユーザーはブラックリストに登録され、通話が開始できませんでした',
'is-already-calling': 'TUICallKit はすでに通話中です',
'need-init': 'TUICallKitで通話を開始する前に、TUICallKitServer.init() メソッドが正常に実行されたことを確認してください。',
"can't call yourself": '自分に電話をかけることができません',
'accept-error': '接続できませんでした',
'accept-device-error': '接続できませんでした。発信側デバイスを認証できません',
'call-error': '通話が開始できませんでした',
};
+35 -28
View File
@@ -1,25 +1,56 @@
export declare const zh: {
hangup: string;
reject: string;
accept: string;
camera: string;
microphone: string;
speaker: string;
'open camera': string;
'close camera': string;
'open microphone': string;
'close microphone': string;
'video-to-audio': string;
'other side reject call': string;
'reject call': string;
accept: string;
cancel: string;
'other side line busy': string;
'in busy': string;
'call timeout': string;
'end call': string;
timeout: string;
'kick out': string;
'caller calling message': string;
'callee calling video message': string;
'callee calling audio message': string;
'no microphone access': string;
'no camera access': string;
'invite member': string;
speaker: string;
'Invited group call': string;
'Those involved': string;
waiting: string;
me: string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
'camera enabled': string;
'camera disabled': string;
'microphone enabled': string;
'microphone disabled': string;
'speaker phone': string;
'ear piece': string;
'wait to be called': string;
answered: string;
'people in the call': string;
'failed to obtain speakers': string;
'you have a new call': string;
timeout: string;
'kick out': string;
call: string;
'video-call': string;
'audio-call': string;
@@ -37,18 +68,9 @@ export declare const zh: {
'input-phone-userID': string;
userID: string;
'already-enter': string;
waiting: string;
'camera-opened': string;
'camera-closed': string;
'microphone-opened': string;
'microphone-closed': string;
camera: string;
microphone: string;
'image-resolution': string;
'default-image-resolution': string;
'invited-person': string;
'video-to-audio': string;
me: string;
'be-rejected': string;
'be-no-response': string;
'be-line-busy': string;
@@ -71,19 +93,4 @@ export declare const zh: {
'accept-error': string;
'accept-device-error': string;
'call-error': string;
'browser-authorization': string;
'mac-privacy': string;
'win-privacy': string;
'mac-preferences': string;
'win-preferences': string;
'open camera': string;
'close camera': string;
'open microphone': string;
'close microphone': string;
'Please enter userID': string;
'View more': string;
'people selected': string;
'Select all': string;
Cancel: string;
Done: string;
};
+45 -32
View File
@@ -2,27 +2,64 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.zh = void 0;
exports.zh = {
// 按钮文案
'hangup': '挂断',
'reject': '拒绝',
'accept': '接受',
'camera': '摄像头',
'microphone': '麦克风',
'speaker': '扬声器',
'open camera': '打开摄像头',
'close camera': '关闭摄像头',
'open microphone': '打开麦克风',
'close microphone': '关闭麦克风',
'video-to-audio': '转语音通话',
// 通话结果
'other side reject call': '对方已拒绝',
'reject call': '拒绝通话',
'accept': '接受',
'cancel': '取消通话',
'other side line busy': '对方忙线',
'in busy': '正在忙',
'call timeout': '呼叫超时',
'end call': '结束通话',
'timeout': '超时',
'kick out': '被踢',
'caller calling message': '正在等待对方接受邀请…',
'callee calling video message': '邀请您进行视频通话',
'callee calling audio message': '邀请您进行语音通话…',
// 通话提示语
'caller calling message': '等待对方接受邀请',
'callee calling video message': '邀请你视频通话',
'callee calling audio message': '邀请你语音通话',
'no microphone access': '没有麦克风权限',
'no camera access': '没有摄像头权限',
'invite member': '邀请成员',
'speaker': '扬声器',
'Invited group call': '邀请你参加多人通话',
'Invited group call': '邀请你加入多人通话',
'Those involved': '参与通话的有:',
'waiting': '等待接听...',
'me': '(我)',
// 弹出层文案
'browser-authorization': '浏览器授权',
'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私',
'win-privacy': '设置 -> 隐私和安全性 -> 应用权限',
'mac-preferences': '打开系统偏好设置',
'win-preferences': '打开系统设置',
'Please enter userID': '请输入 userID',
'View more': '查看更多',
'people selected': '人已选中',
'Select all': '全选',
'Cancel': '取消',
'Done': '完成',
// UI3.0 新增
'camera enabled': '摄像头已开',
'camera disabled': '摄像头已关',
'microphone enabled': '麦克风已开',
'microphone disabled': '麦克风已关',
'speaker phone': '扬声器已开',
'ear piece': '扬声器已关',
'wait to be called': '等待接听',
'answered': '已接通',
'people in the call': '人参与通话',
'failed to obtain speakers': '无法获取扬声器',
'you have a new call': '您有一个新的通话',
// 待废弃文案
'timeout': '超时',
'kick out': '被踢',
'call': '通话',
'video-call': '视频通话',
'audio-call': '音频通话',
@@ -40,18 +77,9 @@ exports.zh = {
'input-phone-userID': '请输入手机号/用户ID',
'userID': '用户ID',
'already-enter': '已经进入当前通话',
'waiting': '等待接听...',
'camera-opened': '摄像头已开',
'camera-closed': '摄像头已关',
'microphone-opened': '麦克风已开',
'microphone-closed': '麦克风已关',
'camera': '摄像头',
'microphone': '麦克风',
'image-resolution': '分辨率',
'default-image-resolution': '默认分辨率',
'invited-person': '添加成员',
'video-to-audio': '切到语音通话',
'me': '(我)',
'be-rejected': '对方已拒绝,',
'be-no-response': '对方无应答,',
'be-line-busy': '对方忙线中,',
@@ -74,19 +102,4 @@ exports.zh = {
'accept-error': '接通失败',
'accept-device-error': '接通失败,通话设备获取失败',
'call-error': '发起通话失败',
'browser-authorization': '浏览器授权',
'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私',
'win-privacy': '设置 -> 隐私和安全性 -> 应用权限',
'mac-preferences': '打开系统偏好设置',
'win-preferences': '打开系统设置',
'open camera': '打开摄像头',
'close camera': '关闭摄像头',
'open microphone': '打开麦克风',
'close microphone': '关闭麦克风',
'Please enter userID': '请输入 userID',
'View more': '查看更多',
'people selected': '人已选中',
'Select all': '全选',
'Cancel': '取消',
'Done': '完成',
};
+102
View File
@@ -0,0 +1,102 @@
export const zh = {
// 按钮文案
'hangup': '挂断',
'reject': '拒绝',
'accept': '接受',
'camera': '摄像头',
'microphone': '麦克风',
'speaker': '扬声器',
'open camera': '打开摄像头',
'close camera': '关闭摄像头',
'open microphone': '打开麦克风',
'close microphone': '关闭麦克风',
'video-to-audio': '转语音通话',
// 通话结果
'other side reject call': '对方已拒绝',
'reject call': '拒绝通话',
'cancel': '取消通话',
'other side line busy': '对方忙线',
'in busy': '正在忙',
'call timeout': '呼叫超时',
'end call': '结束通话',
// 通话提示语
'caller calling message': '等待对方接受邀请',
'callee calling video message': '邀请你视频通话',
'callee calling audio message': '邀请你语音通话',
'no microphone access': '没有麦克风权限',
'no camera access': '没有摄像头权限',
'invite member': '邀请成员',
'Invited group call': '邀请你加入多人通话',
'Those involved': '参与通话的有:',
'waiting': '等待接听...',
'me': '(我)',
// 弹出层文案
'browser-authorization': '浏览器授权',
'mac-privacy': '系统偏好设置 -> 安全与隐私 -> 隐私',
'win-privacy': '设置 -> 隐私和安全性 -> 应用权限',
'mac-preferences': '打开系统偏好设置',
'win-preferences': '打开系统设置',
'Please enter userID': '请输入 userID',
'View more': '查看更多',
'people selected': '人已选中',
'Select all': '全选',
'Cancel': '取消',
'Done': '完成',
// UI3.0 新增
'camera enabled': '摄像头已开',
'camera disabled': '摄像头已关',
'microphone enabled': '麦克风已开',
'microphone disabled': '麦克风已关',
'speaker phone': '扬声器已开',
'ear piece': '扬声器已关',
'wait to be called': '等待接听',
'answered': '已接通',
'people in the call': '人参与通话',
'failed to obtain speakers': '无法获取扬声器',
'you have a new call': '您有一个新的通话',
// 待废弃文案
'timeout': '超时',
'kick out': '被踢',
'call': '通话',
'video-call': '视频通话',
'audio-call': '音频通话',
'search': '搜索',
'search-result': '搜索结果',
'Wechat scan right QR code': '微信扫右二维码',
'Use-phone-and-computer': '用手机与电脑互打体验视频通话',
'Scan the QR code above': '扫描上方二维码',
'no-user': '未搜索到用户',
'member-not-added': '未添加成员',
'not-login': '未登录',
'login-status-expire': '登录状态已失效,请刷新网页重试',
'experience-multi-call': '体验多人通话请下载全功能demo:',
'not-support-multi-call': '多人通话接口未开放',
'input-phone-userID': '请输入手机号/用户ID',
'userID': '用户ID',
'already-enter': '已经进入当前通话',
'image-resolution': '分辨率',
'default-image-resolution': '默认分辨率',
'invited-person': '添加成员',
'be-rejected': '对方已拒绝,',
'be-no-response': '对方无应答,',
'be-line-busy': '对方忙线中,',
'be-canceled': '对方已取消',
'voice-call-end': '语音通话结束',
'video-call-end': '视频通话结束',
'method-call-failed': '同步操作失败',
'failed-to-obtain-permission': '权限获取失败',
'environment-detection-failed': '环境检测失败',
'switchToAudioCall-call-failed': '切语音调用失败',
'switchToVideoCall-call-failed': '切视频调用失败',
'microphone-unavailable': '没有可用的麦克风设备',
'camera-unavailable': '没有可用的摄像头设备',
'ban-device': '用户禁止使用设备',
'not-supported-webrtc': '当前环境不支持 WebRTC',
'blacklist-user-tips': '发起通话失败,被对方拉入黑名单,禁止发起!',
'is-already-calling': 'TUICallKit 已在通话状态',
'need-init': 'TUICallKit 发起通话前需保证 TUICallKitServer.init() 方法执行成功',
"can't call yourself": '不能呼叫自己', // eslint-disable-line
'accept-error': '接通失败',
'accept-device-error': '接通失败,通话设备获取失败',
'call-error': '发起通话失败',
};
+2
View File
@@ -1,11 +1,13 @@
export declare class CallManager {
private _globalCallPagePath;
private _isPageRedirected;
private _targetPagePath;
init(params: any): Promise<void>;
private _watchTUIStore;
private _unwatchTUIStore;
private _handleCallStatusChange;
private _handleCallStatusToCalling;
private _handleCallStatusToIdle;
getRoute(): any;
destroyed(): Promise<void>;
}
@@ -34,6 +34,7 @@ class CallManager {
constructor() {
this._globalCallPagePath = '';
this._isPageRedirected = false;
this._targetPagePath = '';
this._handleCallStatusChange = (value) => __awaiter(this, void 0, void 0, function* () {
switch (value) {
case index_2.CallStatus.CALLING:
@@ -88,6 +89,7 @@ class CallManager {
_handleCallStatusToCalling() {
if (this._isPageRedirected)
return;
this._targetPagePath = this.getRoute().route;
// @ts-ignore
wx.navigateTo({
url: `/${this._globalCallPagePath}`,
@@ -103,6 +105,10 @@ class CallManager {
_handleCallStatusToIdle() {
if (!this._isPageRedirected)
return;
if (this._targetPagePath === this.getRoute().route) {
this._isPageRedirected = false;
return;
}
// @ts-ignore
wx.navigateBack({
success: () => {
@@ -114,6 +120,13 @@ class CallManager {
complete: () => { },
});
}
// 获取当前的页面地址
getRoute() {
// @ts-ignore
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage;
}
// 卸载 callManger
destroyed() {
return __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,117 @@
import { TUICallKitServer, NAME, TUIStore, StoreName } from '../../index';
import { CallStatus } from '../const/index';
import { avoidRepeatedCall } from '../utils/validate/index';
/**
* @param {Number} sdkAppID 用户的sdkAppID 必传
* @param {String} userID 用户的userID 必传
* @param {String} userSig 用户的userSig 必传
* @param {String} globalCallPagePath 跳转的路径 必传
* @param {ChatSDK} tim tim实例 非必传
*/
const PREFIX = 'callManager';
export class CallManager {
private _globalCallPagePath:string = '';
private _isPageRedirected:boolean = false;
private _targetPagePath:string = '';
@avoidRepeatedCall()
public async init(params) {
const { sdkAppID, userID, userSig, globalCallPagePath, tim } = params;
if (!globalCallPagePath) {
console.error(`${PREFIX} globalCallPagePath Can not be empty!`);
return;
};
this._globalCallPagePath = globalCallPagePath;
try {
await TUICallKitServer.init({
sdkAppID,
userID,
userSig,
tim,
});
this._watchTUIStore();
// 全局监听下,关闭悬浮窗
TUICallKitServer.enableFloatWindow(false);
console.log(`${PREFIX} init Ready!`);
} catch (error) {
console.error(`${PREFIX} init fail!`);
}
}
// =========================【监听 TUIStore 中的状态】=========================
private _watchTUIStore() {
TUIStore?.watch(StoreName.CALL, {
[NAME.CALL_STATUS]: this._handleCallStatusChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
}
private _unwatchTUIStore() {
TUIStore?.unwatch(StoreName.CALL, {
[NAME.CALL_STATUS]: this._handleCallStatusChange,
});
}
private _handleCallStatusChange = async (value: CallStatus) => {
switch (value) {
case CallStatus.CALLING:
case CallStatus.CONNECTED:
this._handleCallStatusToCalling();
break;
case CallStatus.IDLE:
this._handleCallStatusToIdle();
break;
}
};
private _handleCallStatusToCalling() {
if (this._isPageRedirected) return;
this._targetPagePath = this.getRoute().route;
// @ts-ignore
wx.navigateTo({
url: `/${this._globalCallPagePath}`,
success: () => {
this._isPageRedirected = true;
},
fail: () => {
console.error(`${PREFIX} navigateTo fail!`);
},
complete: () => {},
});
}
private _handleCallStatusToIdle() {
if (!this._isPageRedirected) return;
if(this._targetPagePath === this.getRoute().route) {
this._isPageRedirected = false;
return;
}
// @ts-ignore
wx.navigateBack({
success: () => {
this._isPageRedirected = false;
},
fail: () => {
console.error(`${PREFIX} navigateBack fail!`);
},
complete: () => {},
});
}
// 获取当前的页面地址
getRoute() {
// @ts-ignore
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
return currentPage;
}
// 卸载 callManger
public async destroyed() {
this._globalCallPagePath = '';
this._isPageRedirected = false;
this._unwatchTUIStore();
await TUICallKitServer.destroyed();
}
}
@@ -170,16 +170,16 @@ function handleNoDevicePermissionError(error) {
exports.handleNoDevicePermissionError = handleNoDevicePermissionError;
/*
* 获取向下取整的 performance.now() 值
* 在不支持 performance.now 的浏览器中,使用 Date.now(). 例如 ie 9ie 10,避免加载 sdk 时报错
* @export
* @return {Number}
*/
function performanceNow() {
// 在不支持 performance.now 的浏览器中,使用 Date.now()
// 例如 ie 9,ie 10,避免加载 sdk 时报错
if (!performance || !performance.now) {
return Date.now();
}
return Math.floor(performance.now());
// if (!performance || !performance.now) {
// return Date.now();
// }
// return Math.floor(performance.now()); // uni-app 打包小程序没有 performance, 报错
return Date.now();
}
exports.performanceNow = performanceNow;
/**
@@ -0,0 +1,239 @@
import { NAME } from '../const/index';
import TUIGlobal from '../TUIGlobal/tuiGlobal';
export const isUndefined = function (input: any) {
return typeof input === NAME.UNDEFINED;
};
export const isPlainObject = function (input: any) {
// 注意不能使用以下方式判断,因为IE9/IE10下,对象的__proto__是 undefined
// return isObject(input) && input.__proto__ === Object.prototype;
if (typeof input !== NAME.OBJECT || input === null) {
return false;
}
const proto = Object.getPrototypeOf(input);
if (proto === null) { // edge case Object.create(null)
return true;
}
let baseProto = proto;
while (Object.getPrototypeOf(baseProto) !== null) {
baseProto = Object.getPrototypeOf(baseProto);
}
// 原型链第一个和最后一个比较
return proto === baseProto;
};
export const isArray = function (input: any) {
if (typeof Array.isArray === NAME.FUNCTION) {
return Array.isArray(input);
}
return (Object as any).prototype.toString.call(input).match(/^\[object (.*)\]$/)[1].toLowerCase() === NAME.ARRAY;
};
export const isPrivateKey = function (key: string) {
return key.startsWith('_');
};
export const isUrl = function (url: string) {
return /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+[a-zA-Z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url);
};
/**
* 检测input类型是否为string
* @param {*} input 任意类型的输入
* @returns {Boolean} true->string / false->not a string
*/
export const isString = function (input: any) {
return typeof input === NAME.STRING;
};
export const isBoolean = function (input: any) {
return typeof input === NAME.BOOLEAN;
};
export const isNumber = function (input: any) {
return (
// eslint-disable-next-line
input !== null &&
((typeof input === NAME.NUMBER && !isNaN(input - 0)) || (typeof input === NAME.OBJECT && input.constructor === Number))
);
};
export function formatTime(secondTime: number): string {
const hours: number = Math.floor(secondTime / 3600);
const minutes: number = Math.floor((secondTime % 3600) / 60);
const seconds: number = Math.floor(secondTime % 60);
let callDurationStr: string = hours > 9 ? `${hours}` : `0${hours}`;
callDurationStr += minutes > 9 ? `:${minutes}` : `:0${minutes}`;
callDurationStr += seconds > 9 ? `:${seconds}` : `:0${seconds}`;
return callDurationStr;
}
export function formatTimeInverse(stringTime: string): number {
const list = stringTime.split(':');
return parseInt(list[0]) * 3600 + parseInt(list[1]) * 60 + parseInt(list[2]); // eslint-disable-line
}
// Determine if it is a JSON string
export function isJSON(str: string) {
if (typeof str === NAME.STRING) {
try {
const data = JSON.parse(str);
if (data) {
return true;
}
return false;
} catch (error) {
console.debug(error);
return false;
}
}
return false;
}
// Determine if it is a JSON string
export const JSONToObject = function (str: string) {
if (!str || !isJSON(str)) {
return str;
}
return JSON.parse(str);
};
/**
* 重试函数, catch 时,重试
* @param {Promise} promise 需重试的函数
* @param {number} num 需要重试的次数
* @param {number} time 间隔时间(s
* @returns {Promise<any>} im 接口的 response 原样返回
*/
export const retryPromise = (promise: Promise<any>, num: number = 6, time: number = 0.5) => {
let n = num;
const func = () => promise;
return func()
.catch((error: any) => {
if (n === 0) {
throw error;
}
const timer = setTimeout(() => {
func();
clearTimeout(timer);
n = n - 1;
}, time * 1000);
});
};
// /**
// * 节流函数(目前 TUICallKit 增加防重调用装饰器,该方法可删除)
// * @param {Function} func 传入的函数
// * @param {wait} time 间隔时间(ms
// */
// export const throttle = (func: Function, wait: number) => {
// let previousTime = 0;
// return function () {
// const now = Date.now();
// const args = [...arguments];
// if (now - previousTime > wait) {
// func.apply(this, args);
// previousTime = now;
// }
// };
// }
/**
* web call engine 重复调用时的错误, 这种错误在 TUICallKit 应该忽略
* @param {any} error 错误信息
* @returns {Boolean}
*/
export function handleRepeatedCallError(error: any) {
if (error?.message.indexOf('is ongoing, please avoid repeated calls') !== -1) {
return true;
}
return false;
}
/**
* 设备无权限时的错误处理
* @param {any} error 错误信息
* @returns {Boolean}
*/
export function handleNoDevicePermissionError(error: any) {
const { message } = error;
if (message.indexOf('NotAllowedError: Permission denied') !== -1) {
return true;
}
return false;
}
/*
* 获取向下取整的 performance.now() 值
* 在不支持 performance.now 的浏览器中,使用 Date.now(). 例如 ie 9ie 10,避免加载 sdk 时报错
* @export
* @return {Number}
*/
export function performanceNow() {
// if (!performance || !performance.now) {
// return Date.now();
// }
// return Math.floor(performance.now()); // uni-app 打包小程序没有 performance, 报错
return Date.now();
}
/**
* 检测input类型是否为function
* @param {*} input 任意类型的输入
* @returns {Boolean} true->input is a function
*/
export const isFunction = function (input: any) {
return typeof input === NAME.FUNCTION;
};
/*
* 获取浏览器语言
* @export
* @return {zh-cn | en}
*/
export const getLanguage = () => {
if (TUIGlobal.getInstance().isWeChat) {
return 'zh-cn';
}
// @ts-ignore
const lang = (navigator?.language || navigator?.userLanguage || '').substr(0, 2);
let language = 'en';
switch (lang) {
case 'zh':
language = 'zh-cn';
break;
case 'ja':
language = 'ja_JP';
break;
default:
language = 'en';
}
return language;
};
export function noop(e: any) {}
/**
* Get the object type string
* @param {*} input 任意类型的输入
* @returns {String} the object type string
*/
export const getType = function(input) {
return Object.prototype.toString
.call(input)
.match(/^\[object (.*)\]$/)[1]
.toLowerCase();
};
// 修改对象键名
export function modifyObjectKey(obj, oldKey, newKey) {
if (!obj.hasOwnProperty(oldKey)) {
return obj;
}
const newObj = {};
Object.keys(obj).forEach(key => {
if (key === oldKey) {
newObj[newKey] = obj[key];
} else {
newObj[key] = obj[key];
}
});
return newObj;
}
@@ -0,0 +1,32 @@
/**
* 异步函数重试 Interface
* @param {number} [retries = 5] 重试次数
* @param {number} [timeout = 2000] 重试间隔时间
* @param {Function=} onError 重试错误回调
* @param {Function=} onRetrying 重试回调
* @param {Function=} onRetryFailed 重试失败回调
*/
interface IPromiseRetryDecoratorSettings {
retries?: number;
timeout?: number;
onError?: Function;
onRetrying?: Function;
onRetryFailed?: Function;
}
/**
* 装饰器函数:给异步函数增加重试
* @param {Object} settings 入参
* @returns {Function}
* @example
* class LocalStream {
* @promiseRetryDecorator({
* retries: 10,
* timeout: 3000,
* onRetryFailed: function(error) {
* }
* })
* async recoverCapture(options) {}
* }
*/
export default function promiseRetryDecorator(settings: IPromiseRetryDecoratorSettings): (target: any, name: any, descriptor: any) => any;
export {};
@@ -0,0 +1,40 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const retry_1 = __importDefault(require("../retry"));
;
/**
* 装饰器函数:给异步函数增加重试
* @param {Object} settings 入参
* @returns {Function}
* @example
* class LocalStream {
* @promiseRetryDecorator({
* retries: 10,
* timeout: 3000,
* onRetryFailed: function(error) {
* }
* })
* async recoverCapture(options) {}
* }
*/
function promiseRetryDecorator(settings) {
return function (target, name, descriptor) {
const { retries = 5, timeout = 2000, onError, onRetrying, onRetryFailed } = settings;
const oldFn = (0, retry_1.default)({
retryFunction: descriptor.value,
settings: { retries, timeout },
onError,
onRetrying,
onRetryFailed,
context: null,
});
descriptor.value = function (...args) {
return oldFn.apply(this, args);
};
return descriptor;
};
}
exports.default = promiseRetryDecorator;
@@ -0,0 +1,51 @@
import promiseRetry from '../retry';
/**
* 异步函数重试 Interface
* @param {number} [retries = 5] 重试次数
* @param {number} [timeout = 2000] 重试间隔时间
* @param {Function=} onError 重试错误回调
* @param {Function=} onRetrying 重试回调
* @param {Function=} onRetryFailed 重试失败回调
*/
interface IPromiseRetryDecoratorSettings {
retries?: number;
timeout?: number;
onError?: Function;
onRetrying?: Function;
onRetryFailed?: Function;
};
/**
* 装饰器函数:给异步函数增加重试
* @param {Object} settings 入参
* @returns {Function}
* @example
* class LocalStream {
* @promiseRetryDecorator({
* retries: 10,
* timeout: 3000,
* onRetryFailed: function(error) {
* }
* })
* async recoverCapture(options) {}
* }
*/
export default function promiseRetryDecorator(settings: IPromiseRetryDecoratorSettings) {
return function(target, name, descriptor) {
const { retries = 5, timeout = 2000, onError, onRetrying, onRetryFailed } = settings;
const oldFn = promiseRetry({
retryFunction: descriptor.value,
settings: { retries, timeout },
onError,
onRetrying,
onRetryFailed,
context: null,
});
descriptor.value = function(...args) {
return oldFn.apply(this, args);
};
return descriptor;
};
}
+51
View File
@@ -0,0 +1,51 @@
// eslint-disable-next-line
declare var wx: any;
// eslint-disable-next-line
declare var uni: any;
// eslint-disable-next-line
declare var window: any;
// 在 uniApp 框架下,打包 H5、ios app、android app 时存在 wx/qq/tt/swan/my 等变量会导致引入 web sdk 环境判断失效
// 小程序 getSystemInfoSync 返回的 fontSizeSetting 在 H5 和 app 中为 undefined,所以通过 fontSizeSetting 增强小程序环境判断
// wx 小程序
export const IN_WX_MINI_APP = (typeof wx !== 'undefined' && typeof wx.getSystemInfoSync === 'function' && Boolean(wx.getSystemInfoSync().fontSizeSetting));
// 用 uni-app 打包 native app,此时运行于 js core,无 window 等对象,此时调用 api 都得 uni.xxx,由于风格跟小程序类似,就归为 IN_MINI_APP 的一种
export const IN_UNI_NATIVE_APP = (typeof uni !== 'undefined' && typeof uni === 'undefined');
export const IN_MINI_APP = IN_WX_MINI_APP || IN_UNI_NATIVE_APP;
export const IN_UNI_APP = (typeof uni !== 'undefined');
// 在 uniApp 框架下,由于客户打包 ios app、android app 时 window 不一定存在,所以通过 !IN_MINI_APP 进行判断
// 非 uniApp 框架下,仍然通过 window 结合 IN_MINI_APP 进行判断,可兼容 Taro3.0+ 暴露 window 对象引起的 IN_BROWSER 判断失效问题
export const IN_BROWSER = (function () {
if (typeof uni !== 'undefined') {
return !IN_MINI_APP;
}
return (typeof window !== 'undefined') && !IN_MINI_APP;
}());
// 命名空间
export const APP_NAMESPACE = (function () {
if (IN_WX_MINI_APP) {
return wx;
}
if (IN_UNI_APP) {
return uni;
}
return window;
}());
// eslint-disable-next-line no-mixed-operators
const USER_AGENT = IN_BROWSER && window && window.navigator && window.navigator.userAgent || '';
const IS_ANDROID = /Android/i.test(USER_AGENT);
const IS_WIN_PHONE = /(?:Windows Phone)/.test(USER_AGENT);
const IS_SYMBIAN = /(?:SymbianOS)/.test(USER_AGENT);
const IS_IOS = /iPad/i.test(USER_AGENT) || /iPhone/i.test(USER_AGENT) || /iPod/i.test(USER_AGENT);
export const IS_H5 = IS_ANDROID || IS_WIN_PHONE || IS_SYMBIAN || IS_IOS;
export const IS_PC = IN_BROWSER && !IS_H5;
export const IS_WIN = IS_PC && USER_AGENT.includes('Windows NT');
export const IS_MAC = IS_PC && USER_AGENT.includes('Mac');
+16
View File
@@ -0,0 +1,16 @@
export async function checkLocalMP3FileExists(src: string) {
if (!src) return false;
try {
const response = await new Promise<XMLHttpRequest>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('HEAD', src, true);
xhr.onload = () => resolve(xhr);
xhr.onerror = () => reject(xhr);
xhr.send();
});
return response.status === 200 && response.getResponseHeader('Content-Type') === 'audio/mpeg';
} catch (error) {
console.warn(error);
return false;
}
}
@@ -0,0 +1,31 @@
import { isPlainObject } from './common-utils';
const isEmpty = function (input: any) {
// Null and Undefined...
if (input === null || typeof (input) === 'undefined') return true;
// Booleans...
if (typeof input === 'boolean') return false;
// Numbers...
if (typeof input === 'number') return input === 0;
// Strings...
if (typeof input === 'string') return input.length === 0;
// Functions...
if (typeof input === 'function') return input.length === 0;
// Arrays...
if (Array.isArray(input)) return input.length === 0;
// Errors...
if (input instanceof Error) return input.message === '';
// plain object
if (isPlainObject(input)) {
// eslint-disable-next-line
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key)) {
return false;
}
}
return true;
}
return false;
};
export default isEmpty;
+36
View File
@@ -0,0 +1,36 @@
/**
* 给异步函数封装重试逻辑
* @param {Object} options 重试逻辑入参
* @param {Object} options.retryFunction 需要封装的异步函数
* @param {Object} options.settings 重试属性
* @param {Number} [options.settings.retries = 5] 重试次数
* @param {Number} [options.settings.timeout = 1000] 重试间隔
* @param {onErrorCallback} options.onError 重试错误回调
* @param {onRetryingCallback} [options.onRetrying] 重试后的回调
* @param {Object} options.context 上下文,可选
* @returns {Function} 封装后的函数
* @example
* const getUserMedia = promiseRetry({
* retryFunction: getUserMedia_,
* settings: { retries: 5, timeout: 2000 },
* onError: (error, retry, reject) => {
* if (error.name === 'NotReadableError') {
* retry();
* } else {
* reject(error);
* }
* },
* onRetrying: retryCount => {
* console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`);
* }
* });
*/
declare function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }: {
retryFunction: any;
settings: any;
onError: any;
onRetrying: any;
onRetryFailed: any;
context: any;
}): (...args: any[]) => Promise<unknown>;
export default promiseRetry;
+95
View File
@@ -0,0 +1,95 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const common_utils_1 = require("./common-utils");
const RETRY_STATE_NOT_START = 0;
const RETRY_STATE_STARTED = 1;
const RETRY_STATE_STOPPED = 2;
/**
* 给异步函数封装重试逻辑
* @param {Object} options 重试逻辑入参
* @param {Object} options.retryFunction 需要封装的异步函数
* @param {Object} options.settings 重试属性
* @param {Number} [options.settings.retries = 5] 重试次数
* @param {Number} [options.settings.timeout = 1000] 重试间隔
* @param {onErrorCallback} options.onError 重试错误回调
* @param {onRetryingCallback} [options.onRetrying] 重试后的回调
* @param {Object} options.context 上下文,可选
* @returns {Function} 封装后的函数
* @example
* const getUserMedia = promiseRetry({
* retryFunction: getUserMedia_,
* settings: { retries: 5, timeout: 2000 },
* onError: (error, retry, reject) => {
* if (error.name === 'NotReadableError') {
* retry();
* } else {
* reject(error);
* }
* },
* onRetrying: retryCount => {
* console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`);
* }
* });
*/
function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }) {
return function (...args) {
const retries = settings.retries || 5;
let retryCount = 0;
let timer = -1;
let retryState = RETRY_STATE_NOT_START;
const run = (resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const ctx = context || this;
try {
const result = yield retryFunction.apply(ctx, args);
// 执行成功,正常返回
retryCount = 0;
resolve(result);
}
catch (error) {
// 用于停止重试
const stopRetry = () => {
clearTimeout(timer);
retryCount = 0;
retryState = RETRY_STATE_STOPPED;
reject(error);
};
const retry = () => {
if (retryState !== RETRY_STATE_STOPPED && retryCount < retries) {
retryCount++;
retryState = RETRY_STATE_STARTED;
if ((0, common_utils_1.isFunction)(onRetrying)) {
onRetrying.call(ctx, retryCount, stopRetry);
}
timer = setTimeout(() => {
timer = -1;
run(resolve, reject);
}, (0, common_utils_1.isUndefined)(settings.timeout) ? 1000 : settings.timeout);
}
else {
stopRetry();
if ((0, common_utils_1.isFunction)(onRetryFailed)) {
onRetryFailed.call(ctx, error);
}
}
};
if ((0, common_utils_1.isFunction)(onError)) {
onError.call(ctx, error, retry, reject, args);
}
else {
retry();
}
}
});
return new Promise(run);
};
}
exports.default = promiseRetry;
+88
View File
@@ -0,0 +1,88 @@
import { isFunction, isUndefined } from './common-utils';
const RETRY_STATE_NOT_START = 0;
const RETRY_STATE_STARTED = 1;
const RETRY_STATE_STOPPED = 2;
/**
* 给异步函数封装重试逻辑
* @param {Object} options 重试逻辑入参
* @param {Object} options.retryFunction 需要封装的异步函数
* @param {Object} options.settings 重试属性
* @param {Number} [options.settings.retries = 5] 重试次数
* @param {Number} [options.settings.timeout = 1000] 重试间隔
* @param {onErrorCallback} options.onError 重试错误回调
* @param {onRetryingCallback} [options.onRetrying] 重试后的回调
* @param {Object} options.context 上下文,可选
* @returns {Function} 封装后的函数
* @example
* const getUserMedia = promiseRetry({
* retryFunction: getUserMedia_,
* settings: { retries: 5, timeout: 2000 },
* onError: (error, retry, reject) => {
* if (error.name === 'NotReadableError') {
* retry();
* } else {
* reject(error);
* }
* },
* onRetrying: retryCount => {
* console.warn(`getUserMedia NotReadableError observed, retrying [${retryCount}/5]`);
* }
* });
*/
function promiseRetry({ retryFunction, settings, onError, onRetrying, onRetryFailed, context }) {
return function(...args) {
const retries = settings.retries || 5;
let retryCount = 0;
let timer: any = -1;
let retryState = RETRY_STATE_NOT_START;
const run = async (resolve, reject) => {
const ctx = context || this;
try {
const result = await retryFunction.apply(ctx, args);
// 执行成功,正常返回
retryCount = 0;
resolve(result);
} catch (error) {
// 用于停止重试
const stopRetry = () => {
clearTimeout(timer);
retryCount = 0;
retryState = RETRY_STATE_STOPPED;
reject(error);
};
const retry = () => {
if (retryState !== RETRY_STATE_STOPPED && retryCount < retries) {
retryCount++;
retryState = RETRY_STATE_STARTED;
if (isFunction(onRetrying)) {
onRetrying.call(ctx, retryCount, stopRetry);
}
timer = setTimeout(
() => {
timer = -1;
run(resolve, reject);
},
isUndefined(settings.timeout) ? 1000 : settings.timeout
);
} else {
stopRetry();
if (isFunction(onRetryFailed)) {
onRetryFailed.call(ctx, error);
}
}
};
if (isFunction(onError)) {
onError.call(ctx, error, retry, reject, args);
} else {
retry();
}
}
};
return new Promise(run);
};
}
export default promiseRetry;
+1 -1
View File
@@ -42,7 +42,7 @@ declare class Timer {
* @param {*} count
* @returns ID
*/
static interval(taskItem: any): NodeJS.Timer;
static interval(taskItem: any): NodeJS.Timeout;
/**
* 延迟执行回调
* count = 0,循环
+162
View File
@@ -0,0 +1,162 @@
/* eslint-disable */
import { isPlainObject, performanceNow, isFunction } from './common-utils';
import { NAME } from '../const/index';
/**
* 定时器,功能:
* 1. 支持定时执行回调 [1,n] 次,用于常规延迟、定时操作
* @example
* // 默认嵌套执行,count=0 无限次
* timer.run(callback, {delay: 2000});
* // count=1 等同于 原始setTimeout
* timer.run(callback, {delay: 2000, count:0});
* 2. 支持 RAF 执行回调,用于小流渲染,audio音量获取等任务,需要渲染频率稳定,支持页面退后台后,用 setTimeout 接管,最短 1s 执行一次
* @example
* // 默认60fps,可以根据单位时长换算,默认开启后台执行
* timer.run('raf', callback, {fps: 60});
* // 设置执行次数
* timer.run('raf', callback, {fps: 60, count: 300, backgroundTask: false});
* 3. 支持空闲任务执行回调, requestIdleCallback 在帧渲染的空闲时间执行任务,可以用于 storage 写入等低优先级的任务
* @example
* // 支持原生setInterval 但不推荐使用,定时任务推荐用 timeout
* timer.run('interval', callback, {delay:2000, count:10})
*/
class Timer {
static taskMap = new Map();
static currentTaskID = 1;
static generateTaskID() {
return this.currentTaskID++;
}
/**
*
* @param {string} taskName 'interval' 'timeout'
* @param {function} callback
* @param {object} options include:
* @param {number} options.delay millisecond
* @param {number} options.count 定时器回调执行次数,0 无限次 or n次
* @param {boolean} options.backgroundTask 在页面静默后是否继续执行定时器
*/
static run(taskName = NAME.TIMEOUT, callback: any, options: any) {
// default options
if (taskName === NAME.INTERVAL) {
options = { ...{ delay: 2000, count: 0, backgroundTask: true }, ...options };
} else {
options = { ...{ delay: 2000, count: 0, backgroundTask: true }, ...options };
}
// call run(function, {...})
if (isPlainObject(callback)) {
options = { ...options, ...callback };
}
if (isFunction(taskName)) {
callback = taskName;
taskName = NAME.TIMEOUT;
}
// 1. 创建 taskID,作为 timer task 的唯一标识,在本函数执行完后返回,用于在调用的地方实现互斥逻辑
// 2. 根据 taskName 执行相应的函数
const taskItem = {
taskID: this.generateTaskID(),
loopCount: 0,
intervalID: null,
timeoutID: null,
taskName,
callback,
...options
};
this.taskMap.set(taskItem.taskID, taskItem);
// console.log(`timer run task:${taskItem.taskName}, task queue size: ${this.taskMap.size}`);
if (taskName === NAME.INTERNAL) {
this.interval(taskItem);
} else {
this.timeout(taskItem);
}
return taskItem.taskID;
}
/**
* 定时循环执行回调函数
* 可以指定循环的时间间隔
* 可以指定循环次数
* @param {object} taskItem
* @param {function} callback
* @param {*} delay
* @param {*} count
* @returns ID
*/
static interval(taskItem: any) {
// setInterval 缺点,浏览器退后台会降频,循环执行间隔时间不可靠
// 创建进入定时器循环的任务函数,函数内:1. 判断是否满足执行条件,2.执行 callback
// 通过 setInterval 执行任务函数
// 将 intervalID 记录到 taskMap 对应的 item
const task = () => {
taskItem.callback();
taskItem.loopCount += 1;
if (this.isBreakLoop(taskItem)) {
return;
}
};
return taskItem.intervalID = setInterval(task, taskItem.delay);
}
/**
* 延迟执行回调
* count = 0,循环
* count = n, 执行n次
* @param {object} taskItem
*
*/
static timeout(taskItem: any) {
// setTimeout 浏览器退后台,延迟变为至少1s
const task: any = () => {
// 执行回调
taskItem.callback();
taskItem.loopCount += 1;
if (this.isBreakLoop(taskItem)) {
return;
}
// 不修正延迟,每次callback间隔平均
return taskItem.timeoutID = setTimeout(task, taskItem.delay);
};
return taskItem.timeoutID = setTimeout(task, taskItem.delay);
}
static hasTask(taskID: any) {
return this.taskMap.has(taskID);
}
static clearTask(taskID: any) {
// console.log('timer clearTask start', `| taskID:${taskID} | size:${this.taskMap.size}`);
if (!this.taskMap.has(taskID)) {
return true;
}
const { intervalID, timeoutID, onVisibilitychange } = this.taskMap.get(taskID);
if (intervalID) {
clearInterval(intervalID);
}
if (timeoutID) {
clearTimeout(timeoutID);
}
if (onVisibilitychange) {
document.removeEventListener('visibilitychange', onVisibilitychange);
}
this.taskMap.delete(taskID);
// console.log('timer clearTask end ', `| size:${this.taskMap.size}`);
return true;
}
/**
* 1. 如果已移除出定时队列,退出当前任务
* 2. 如果当前任务已满足次数限制,则退出当前任务
* @param {object} taskItem
* @returns
*/
static isBreakLoop(taskItem: any) {
if (!this.taskMap.has(taskItem.taskID)) {
return true;
}
if (taskItem.count !== 0 && taskItem.loopCount >= taskItem.count) {
this.clearTask(taskItem.taskID);
return true;
}
return false;
}
}
export default Timer;
@@ -0,0 +1,34 @@
import { NAME } from '../../const/index';
/**
* 装饰器:阻止函数重复调用
* @export
* @param {Object} options 入参
* @param {Function} options.fn 函数
* @param {Object} options.context 上下文对象
* @param {String} options.name 函数名
* @returns {Function} 封装后的函数
*/
export function avoidRepeatedCall() {
return function (target: any, name: string, descriptor: any) {
const oldFn = descriptor.value;
const isCallingSet = new Set();
descriptor.value = async function (...args: any[]) {
if (isCallingSet.has(this)) {
console.warn((`${NAME.PREFIX}previous ${name}() is ongoing, please avoid repeated calls`));
// throw new Error(`previous ${name}() is ongoing, please avoid repeated calls`);
return;
}
try {
isCallingSet.add(this);
const result = await oldFn.apply(this, args);
isCallingSet.delete(this);
return result;
} catch (error) {
isCallingSet.delete(this);
throw error;
}
};
return descriptor;
};
}
@@ -0,0 +1,11 @@
import { avoidRepeatedCall } from './avoidRepeatedCall';
import { paramValidate } from './validateParams';
import { VALIDATE_PARAMS } from './validateConfig';
// import { apiCallQueue } from "./apiCallQueue";
export {
VALIDATE_PARAMS,
paramValidate,
avoidRepeatedCall,
// apiCallQueue,
};
@@ -0,0 +1,188 @@
import { NAME, MAX_NUMBER_ROOM_ID, VideoResolution, VideoDisplayMode } from "../../const/index";
export const VALIDATE_PARAMS = {
init: {
SDKAppID: {
required: true,
rules: [NAME.NUMBER],
allowEmpty: false,
},
userID: {
required: true,
rules: [NAME.STRING],
allowEmpty: false,
},
userSig: {
required: true,
rules: [NAME.STRING],
allowEmpty: false,
},
tim: {
required: false,
rules: [NAME.OBJECT],
},
},
call: {
userID: {
required: true,
rules: [NAME.STRING],
allowEmpty: false
},
type: {
required: true,
rules: [NAME.NUMBER],
range: [1, 2],
allowEmpty: false
},
roomID: {
required: false,
rules: [NAME.NUMBER], // 仅支持数字房间号, 后续会支持字符串房间号
range: `1~${MAX_NUMBER_ROOM_ID}`,
allowEmpty: false,
},
userData: {
required: false,
rules: [NAME.STRING],
allowEmpty: false,
},
timeout: {
required: false,
rules: [NAME.NUMBER],
allowEmpty: false
}
},
groupCall: {
userIDList: {
required: true,
rules: [NAME.ARRAY],
allowEmpty: false
},
type: {
required: true,
rules: [NAME.NUMBER],
range: [1, 2],
allowEmpty: false
},
groupID: {
required: true,
rules: [NAME.STRING],
allowEmpty: false
},
roomID: {
required: false,
rules: [NAME.NUMBER], // 仅支持数字房间号, 后续会支持字符串房间号
range: `1~${MAX_NUMBER_ROOM_ID}`,
allowEmpty: false
},
timeout: {
required: false,
rules: [NAME.NUMBER],
allowEmpty: false
},
userData: {
required: false,
rules: [NAME.STRING],
allowEmpty: false,
},
offlinePushInfo: {
required: false,
rules: [NAME.OBJECT],
allowEmpty: false,
},
},
joinInGroupCall: {
type: {
required: true,
rules: [NAME.NUMBER],
range: [1, 2],
allowEmpty: false
},
groupID: {
required: true,
rules: [NAME.STRING],
allowEmpty: false
},
roomID: {
required: true,
rules: [NAME.NUMBER],
allowEmpty: false,
},
},
inviteUser: {
userIDList: {
required: true,
rules: [NAME.ARRAY],
allowEmpty: false
},
},
setSelfInfo: {
nickName: {
required: false,
rules: [NAME.STRING],
allowEmpty: false,
},
avatar: {
required: false,
rules: [NAME.STRING],
allowEmpty: false,
}
},
enableFloatWindow: [
{
key: "enable",
required: false,
rules: [NAME.BOOLEAN],
allowEmpty: false,
}
],
enableAIVoice: [
{
key: "enable",
required: true,
rules: [NAME.BOOLEAN],
allowEmpty: false,
}
],
enableMuteMode: [
{
key: "enable",
required: true,
rules: [NAME.BOOLEAN],
allowEmpty: false,
}
],
setCallingBell: [
{
key: "filePath",
required: false,
rules: [NAME.STRING],
allowEmpty: true,
}
],
setLanguage: [
{
key: "language",
required: true,
rules: [NAME.STRING],
allowEmpty: false
}
],
setVideoDisplayMode: [
{
key: "displayMode",
required: true,
rules: [NAME.STRING],
range: [VideoDisplayMode.CONTAIN, VideoDisplayMode.COVER, VideoDisplayMode.FILL],
allowEmpty: false
}
],
setVideoResolution: [
{
key: "resolution",
required: true,
rules: [NAME.STRING],
range: [VideoResolution.RESOLUTION_1080P, VideoResolution.RESOLUTION_480P, VideoResolution.RESOLUTION_720P],
allowEmpty: false
}
]
};
@@ -0,0 +1,88 @@
import { getType, isArray, isString, isUndefined, isNumber, modifyObjectKey } from "../common-utils";
import { NAME } from "../../const/index";
const PREFIX = NAME.PREFIX + "API";
export function paramValidate (config: any) {
return function (target, propertyName: string, descriptor: PropertyDescriptor) {
let method = descriptor.value;
descriptor.value = function (...args) {
doValidate.call(this, config, args, propertyName);
return method.apply(this, args);
};
return descriptor;
};
}
function doValidate(config, args, name) {
try {
// 兼容 init 方法中: SDKAppID sdkAppID 两种写法的参数校验判断
if (!args[0].SDKAppID) {
config = modifyObjectKey(config, "SDKAppID", "sdkAppID");
}
if (isArray(config)) {
for (let i = 0; i < config.length; i++) {
check.call(this, {
...config[i],
value: args[i],
name,
});
}
} else {
for (const key in config) {
if (config.hasOwnProperty(key)) {
check.call(this, {
...config[key],
value: args[0][key],
name,
key,
});
}
}
}
} catch (error) {
console.error(error);
throw error;
}
}
function check({ required, rules, range, value, allowEmpty, name, key }) {
// 用户没传指定参数
if (isUndefined(value)) {
// 检查必填参数, 若配置是必填则报错
if (required) {
throw new Error(`${PREFIX}<${name}>: ${key} is required.`);
} else {
return;
}
}
// 判断参数类型是否正确
const result = rules.some((item)=>item === getType(value));
let type = '';
if (!result) {
for (let i = 0; i < rules.length; i++) {
let str = rules[i];
str = str.replace(str[0], str[0].toUpperCase());
type += `${str}/`;
}
type = type.substring(0, type.length - 1);
throw new Error(`${PREFIX}<${name}>: ${key} must be ${type}, current ${key} is ${typeof value}.`);
}
// 不允许传空值, 例如: '', ' '
if (allowEmpty === false) {
const isEmptyString = isString(value) && value.trim() === '';
if (isEmptyString) {
throw new Error(`${PREFIX}<${name}>: ${key} is blank.`);
}
}
// 判断是否符合限制条件
if (isArray(range)) {
if (range && range.indexOf(value) === -1) {
throw new Error(`${PREFIX}<${name}>: ${key} error, only be ${range}, current ${key} is ${value}.`);
}
}
// 取值范围, 前闭后闭
if (isString(range) && range.indexOf('~') !== -1) {
const valueList = range.split('~');
if (value < +valueList[0] || value > +valueList[1] || (isNumber(value) && Number.isNaN(value))) {
throw new Error(`${PREFIX}<${name}>: ${key} error, only be ${range}, current ${key} is ${value}.`);
}
}
}