视频
@@ -1,5 +1,5 @@
|
||||
// TUIKitWChat/Chat/index.js
|
||||
import Aegis from './lib/aegis';
|
||||
import Aegis from 'aegis-mp-sdk';
|
||||
import constant from './utils/constant';
|
||||
const app = getApp();
|
||||
Component({
|
||||
@@ -62,7 +62,7 @@ Component({
|
||||
});
|
||||
const TUIConversation = this.selectComponent('#TUIConversation');
|
||||
TUIConversation.init();
|
||||
if (app?.globalData?.reportType !== constant.OPERATING_ENVIRONMENT) {
|
||||
if (!app.globalData || app.globalData.reportType !== constant.OPERATING_ENVIRONMENT) {
|
||||
this.aegisInit();
|
||||
}
|
||||
wx.$chat_reportType = 'chat-uikit-wechat';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# TUICallkit 组件接入说明
|
||||
TUICallkit 是小程序音视频通话 UI 组件,通过编写几行代码,就可以为您的小程序应用添加音视频通话功能。
|
||||
|
||||
## 环境准备
|
||||
|
||||
- 微信 App iOS 最低版本要求:7.0.9
|
||||
- 微信 App Android 最低版本要求:7.0.8
|
||||
- 小程序基础库最低版本要求:2.10.0
|
||||
- 由于小程序测试号不具备 live-pusher 和 live-player 的使用权限,请使用企业小程序账号申请相关权限进行开发
|
||||
- 由于微信开发者工具不支持原生组件(即 live-pusher 和 live-player 标签),需要在真机上进行运行体验
|
||||
|
||||
## 特性
|
||||
- ⚡️ 功能全面 —— 支持单人/多人/音频/视频通话、支持视频转音频通话、支持自由选择通话设备
|
||||
- 🎨 灵活样式 —— 组件开源,可复用逻辑,自定义 UI 样式
|
||||
- 🛠 优秀生态 —— 与 [TUIKit](https://cloud.tencent.com/document/product/269/79737) 协同使用,可以在 [TIM](https://cloud.tencent.com/document/product/269) 会话中直接发起音视频通话
|
||||
- 🌍 跨平台 —— 支持 Android、iOS、Web、小程序、Flutter、UniApp 等 [多个开发平台](https://cloud.tencent.com/document/product/647/78742)
|
||||
- ☁️ 低延迟 —— 腾讯云全球链路资源储备,保证国际链路端到端平均时延 < 300ms
|
||||
- 🤙🏻 低卡顿 —— 抗丢包率超过 80%、抗网络抖动超过 1000ms,弱网环境仍顺畅稳定
|
||||
- 🌈 高品质 —— 支持 720P、1080P 高清画质,70% 丢包率仍可正常视频
|
||||
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
TUICallkit
|
||||
├─ component // UI 组件
|
||||
├─ calling // 呼叫中 UI 组件(1v1通话)
|
||||
└─ connected // 通话中 UI 组件(1v1通话)
|
||||
├─ groupCalling // 呼叫中 UI 组件(群组通话)
|
||||
└─ groupConnected // 通话中 UI 组件(群组通话)
|
||||
├─ pages
|
||||
├─ globalCall // 全局监听页面
|
||||
├─ serve
|
||||
├─ callManager // 全局监听类
|
||||
├─ static // UI icon 图片
|
||||
```
|
||||
|
||||
|
||||
## 使用指引
|
||||
为方便您的使用,本组件配套多篇使用指引:
|
||||
- 如果您想要了解 TUICallKit,请阅读 [组件介绍 TUICallKit](https://cloud.tencent.com/document/product/647/78742)。
|
||||
- 如果您想把我们的功能直接嵌入到您的项目中,请阅读 [快速接入 TUICallKit](https://cloud.tencent.com/document/product/647/78733)。
|
||||
- 如果您想要了解详细 API,请阅读 [API 概览](https://cloud.tencent.com/document/product/647/78759)。
|
||||
|
||||
## 附录
|
||||
|
||||
- 如果你遇到了困难,可以先参阅 [常见问题](https://cloud.tencent.com/document/product/647/78733)。
|
||||
- 如果发现了示例代码的 bug,欢迎提交 issue。
|
||||
- 欢迎加入 QQ 群:**646165204**,进行技术交流和反馈~
|
||||
-
|
||||
@@ -0,0 +1,927 @@
|
||||
import TUICallEngine, { EVENT, MEDIA_TYPE, AUDIO_PLAYBACK_DEVICE, STATUS } from 'tuicall-engine-wx';
|
||||
import { BellContext } from './serve/bellContext';
|
||||
import { throttle, isTabBarPage, tabBarConfig } from './utils/commom';
|
||||
import { THROTTLE_TIME } from './utils/constants';
|
||||
// 组件旨在跨终端维护一个通话状态管理机,以事件发布机制驱动上层进行管理,并通过API调用进行状态变更。
|
||||
// 组件设计思路将UI和状态管理分离。您可以通过修改`component`文件夹下的文件,适配您的业务场景,
|
||||
// 在UI展示上,您可以通过属性的方式,将上层的用户头像,名称等数据传入组件内部,`static`下的icon和默认头像图片,
|
||||
// 只是为了展示基础的效果,您需要根据业务场景进行修改。
|
||||
|
||||
export const PREFIX = 'TUICallKit';
|
||||
const version = '1.4.4';
|
||||
let INVITER_BELL_FILEPATH = 'TUICallKit/TUICallKit/static/phone_dialing.mp3';
|
||||
let INVITEE_BELL_FILEPATH = 'TUICallKit/TUICallKit/static/phone_ringing.mp3';
|
||||
// 记录组件是否初始化完成
|
||||
let tuiCallInitReady = false;
|
||||
// 是否需要隐藏 tabBar 页面
|
||||
let shouldHideTabBar = false;
|
||||
Component({
|
||||
properties: {
|
||||
config: {
|
||||
type: Object,
|
||||
value: {
|
||||
sdkAppID: 0,
|
||||
userID: '',
|
||||
userSig: '',
|
||||
type: 1,
|
||||
tim: null,
|
||||
},
|
||||
},
|
||||
backgroundMute: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
callStatus: STATUS.IDLE, // idle、calling、connection
|
||||
isSponsor: false, // 呼叫者身份 true为呼叫者 false为被叫者
|
||||
pusher: {}, // TRTC 本地流
|
||||
playerList: [], // TRTC 远端流
|
||||
playerProcess: {}, // 经过处理的的远端流(多人通话)
|
||||
isGroup: false, // 是否为多人通话
|
||||
remoteUsers: [], // 未排序的通话列表
|
||||
allUsers: [], // 排序后的通话列表
|
||||
sponsor: '', // 主叫方
|
||||
screen: 'pusher', // 视屏通话中,显示大屏幕的流(只限1v1聊天
|
||||
soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER, // 声音模式 听筒/扬声器
|
||||
showToatTime: 0, // 弹窗时长
|
||||
ownUserId: '',
|
||||
callingBell: null,
|
||||
bellFilePath: '', // 被叫方铃声地址
|
||||
},
|
||||
methods: {
|
||||
initialUI() {
|
||||
// 收起键盘
|
||||
wx.hideKeyboard();
|
||||
// 隐藏 tabBar
|
||||
if (shouldHideTabBar) {
|
||||
wx.hideTabBar();
|
||||
}
|
||||
},
|
||||
// 新的邀请回调事件
|
||||
async handleNewInvitationReceived(event) {
|
||||
this.initialUI();
|
||||
this.checkRunPlatform();
|
||||
this.data.config.type = event.data.inviteData.callType;
|
||||
this.data.callingBell.setBellSrc(this.data.bellFilePath || INVITEE_BELL_FILEPATH);
|
||||
this.data.callingBell.play();
|
||||
// 判断是否为多人通话
|
||||
if (event.data.isFromGroup) {
|
||||
this.setData({
|
||||
isGroup: true,
|
||||
sponsor: event.data.sponsor,
|
||||
});
|
||||
// 将主叫方和被叫列表合并在一起 组成全部通话人数
|
||||
const newList = [...event.data.inviteeList, event.data.sponsor];
|
||||
// 获取用户信息
|
||||
await this.getUserProfile(newList);
|
||||
} else {
|
||||
await this.getUserProfile([event.data.sponsor]);
|
||||
}
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
callStatus: STATUS.CALLING,
|
||||
isSponsor: false,
|
||||
});
|
||||
},
|
||||
// 用户接听
|
||||
handleUserAccept(event) {
|
||||
// 主叫方则唤起通话页面
|
||||
if (this.isSponsor()) {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
this.setData({
|
||||
callStatus: STATUS.CONNECTED,
|
||||
});
|
||||
}
|
||||
},
|
||||
// 远端用户进入通话
|
||||
handleUserEnter(res) {
|
||||
const newList = this.data.allUsers;
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
// 改变远端用户信息中的isEnter属性
|
||||
for (let i = 0;i < newList.length;i++) {
|
||||
if (newList[i].userID === res.data.userID) {
|
||||
newList[i].isEnter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
playerList: res.playerList,
|
||||
allUsers: newList,
|
||||
});
|
||||
},
|
||||
// 远端用户离开通话
|
||||
handleUserLeave(res) {
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
wx.showToast({
|
||||
title: `${this.getUserNickName(res.data.userID)}离开通话`,
|
||||
});
|
||||
this.deleteUsers(this.data.allUsers, res.data.userID);
|
||||
}
|
||||
this.setData({
|
||||
playerList: res.data.playerList,
|
||||
});
|
||||
},
|
||||
// 用户数据更新
|
||||
handleUserUpdate(res) {
|
||||
this.handleNetStatus(res.data);
|
||||
const newplayer = {};
|
||||
const newres = res.data.playerList;
|
||||
// 多人通话
|
||||
if (this.data.isGroup) {
|
||||
// 处理远端流
|
||||
for (let i = 0;i < newres.length;i++) {
|
||||
const { userID } = newres[i];
|
||||
newplayer[userID] = newres[i];
|
||||
}
|
||||
};
|
||||
this.setData({
|
||||
pusher: res.data.pusher,
|
||||
playerList: res.data.playerList,
|
||||
playerProcess: newplayer,
|
||||
});
|
||||
},
|
||||
// 判断网络状态
|
||||
handleNetStatus(data) {
|
||||
if (data.pusher.netQualityLevel > 4) {
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: '您当前网络不佳',
|
||||
});
|
||||
}
|
||||
data.playerList.map((item) => {
|
||||
if (item.netStatus.netQualityLevel > 4) {
|
||||
const name = data.playerList.length > 1 ? item.nick : '对方';
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: `${name}当前网络不佳`,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
},
|
||||
// 用户拒绝
|
||||
handleInviteeReject(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.invitee);
|
||||
} else {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
}
|
||||
wx.showToast({
|
||||
title: `${this.getUserNickName(event.data.invitee)}已拒绝`,
|
||||
});
|
||||
},
|
||||
// 用户不在线
|
||||
handleNoResponse(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
|
||||
} else {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
}
|
||||
wx.showToast({
|
||||
icon: 'none',
|
||||
title: `${event.data.timeoutUserList}无应答`,
|
||||
});
|
||||
},
|
||||
// 获取用户昵称
|
||||
getUserNickName(userId) {
|
||||
const { remoteUsers } = this.data;
|
||||
const userObj = remoteUsers.find(item => item.userID === userId);
|
||||
return userObj.nick ? userObj.nick : userObj.userID;
|
||||
},
|
||||
// 用户忙线
|
||||
handleLineBusy(event) {
|
||||
if (this.data.isGroup) {
|
||||
this.deleteUsers(this.data.allUsers, event.data.invitee);
|
||||
} else {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
}
|
||||
this.showToast(this.getUserNickName(event.data.invitee));
|
||||
},
|
||||
showToast(event) {
|
||||
this.setData({
|
||||
showToatTime: this.data.showToatTime + THROTTLE_TIME,
|
||||
});
|
||||
setTimeout(() => {
|
||||
wx.showToast({
|
||||
title: `${event}忙线中`,
|
||||
});
|
||||
}, this.data.showToatTime);
|
||||
},
|
||||
// 用户取消
|
||||
handleCallingCancel(event) {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
if (event.data.invitee !== this.data.config.userID) {
|
||||
wx.showToast({
|
||||
title: `${this.getUserNickName(event.data.invitee)}取消通话`,
|
||||
});
|
||||
}
|
||||
this.reset();
|
||||
},
|
||||
// 通话超时未应答
|
||||
handleCallingTimeout(event) {
|
||||
if (this.data.isGroup) {
|
||||
// 若是自身未应答 则不弹窗
|
||||
if (this.data.config.userID === event.data.timeoutUserList[0]) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
const newList = this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
|
||||
this.setData({
|
||||
allUsers: newList,
|
||||
});
|
||||
} else {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
}
|
||||
if (this.data.playerList.length === 0) {
|
||||
this.reset();
|
||||
}
|
||||
wx.showToast({
|
||||
title: `${event.data.timeoutUserList[0]}超时无应答`,
|
||||
});
|
||||
},
|
||||
handleCallingUser(userIDList) {
|
||||
const remoteUsers = [...this.data.remoteUsers];
|
||||
const userProfile = remoteUsers.filter(item => userIDList.some(userItem => `${userItem}` === item.userID));
|
||||
this.setData({
|
||||
remoteUsers: remoteUsers.filter(item => userIDList.some(userItem => userItem !== item.userID)),
|
||||
});
|
||||
let nick = '';
|
||||
for (let i = 0; i < userProfile.length; i++) {
|
||||
nick += `${userProfile[i].nick}、`;
|
||||
}
|
||||
return nick.slice(0, -1);
|
||||
},
|
||||
// 通话结束
|
||||
handleCallingEnd(event) {
|
||||
wx.showToast({
|
||||
title: '通话结束',
|
||||
duration: 800,
|
||||
});
|
||||
this.reset();
|
||||
},
|
||||
// SDK Ready 回调
|
||||
handleSDKReady() {
|
||||
// 呼叫需在sdk ready之后
|
||||
},
|
||||
// 被踢下线
|
||||
handleKickedOut() {
|
||||
wx.showToast({
|
||||
title: '您已被踢下线',
|
||||
});
|
||||
},
|
||||
// 切换通话模式
|
||||
handleCallMode(event) {
|
||||
this.data.config.type = event.data.type;
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
},
|
||||
handleError(event) {
|
||||
const { code, message } = event.data || {};
|
||||
console.warn(`${PREFIX} errorCode: ${code}, errorMessage: ${message}`);
|
||||
},
|
||||
// 删除用户列表操作
|
||||
deleteUsers(usersList, userID) {
|
||||
// 若userID不是数组,则将其转换为数组
|
||||
if (!Array.isArray(userID)) {
|
||||
userID = [userID];
|
||||
}
|
||||
const list = usersList.filter(item => !userID.includes(item.userID));
|
||||
this.setData({
|
||||
allUsers: list,
|
||||
});
|
||||
},
|
||||
// 增加用户列表操作
|
||||
addUsers(usersList, userID) {
|
||||
// 若userID不是数组,则将其转换为数组
|
||||
if (!Array.isArray(userID)) {
|
||||
userID = [userID];
|
||||
}
|
||||
const newList = [...usersList, ...userID];
|
||||
return newList;
|
||||
},
|
||||
// 增加 tsignaling 事件监听
|
||||
_addTSignalingEvent() {
|
||||
wx.$TUICallEngine.on(EVENT.ERROR, this.handleError, this);
|
||||
// 被邀请通话
|
||||
wx.$TUICallEngine.on(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
// 用户接听
|
||||
wx.$TUICallEngine.on(EVENT.USER_ACCEPT, this.handleUserAccept, this);
|
||||
// 用户进入通话
|
||||
wx.$TUICallEngine.on(EVENT.USER_ENTER, this.handleUserEnter, this);
|
||||
// 用户离开通话
|
||||
wx.$TUICallEngine.on(EVENT.USER_LEAVE, this.handleUserLeave, this);
|
||||
// 用户更新数据
|
||||
wx.$TUICallEngine.on(EVENT.USER_UPDATE, this.handleUserUpdate, this);
|
||||
// 用户拒绝通话
|
||||
wx.$TUICallEngine.on(EVENT.REJECT, this.handleInviteeReject, this);
|
||||
// 用户无响应
|
||||
wx.$TUICallEngine.on(EVENT.NO_RESP, this.handleNoResponse, this);
|
||||
// 用户忙线
|
||||
wx.$TUICallEngine.on(EVENT.LINE_BUSY, this.handleLineBusy, this);
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_CANCEL, this.handleCallingCancel, this);
|
||||
// 通话超时未应答
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.on(EVENT.CALL_END, this.handleCallingEnd, this);
|
||||
// SDK Ready 回调
|
||||
wx.$TUICallEngine.on(EVENT.SDK_READY, this.handleSDKReady, this);
|
||||
// 被踢下线
|
||||
wx.$TUICallEngine.on(EVENT.KICKED_OUT, this.handleKickedOut, this);
|
||||
// 切换通话模式
|
||||
wx.$TUICallEngine.on(EVENT.CALL_MODE, this.handleCallMode, this);
|
||||
// 自己发送消息
|
||||
wx.$TUICallEngine.on(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe, this);
|
||||
},
|
||||
// 取消 tsignaling 事件监听
|
||||
_removeTSignalingEvent() {
|
||||
wx.$TUICallEngine.off(EVENT.ERROR, this.handleError);
|
||||
// 被邀请通话
|
||||
wx.$TUICallEngine.off(EVENT.INVITED, this.handleNewInvitationReceived);
|
||||
// 用户接听
|
||||
wx.$TUICallEngine.off(EVENT.USER_ACCEPT, this.handleUserAccept);
|
||||
// 用户进入通话
|
||||
wx.$TUICallEngine.off(EVENT.USER_ENTER, this.handleUserEnter);
|
||||
// 用户离开通话
|
||||
wx.$TUICallEngine.off(EVENT.USER_LEAVE, this.handleUserLeave);
|
||||
// 用户更新数据
|
||||
wx.$TUICallEngine.off(EVENT.USER_UPDATE, this.handleUserUpdate);
|
||||
// 用户拒绝通话
|
||||
wx.$TUICallEngine.off(EVENT.REJECT, this.handleInviteeReject);
|
||||
// 用户无响应
|
||||
wx.$TUICallEngine.off(EVENT.NO_RESP, this.handleNoResponse);
|
||||
// 用户忙线
|
||||
wx.$TUICallEngine.off(EVENT.LINE_BUSY, this.handleLineBusy);
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallingCancel);
|
||||
// 通话超时未应答
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallingEnd);
|
||||
// SDK Ready 回调
|
||||
wx.$TUICallEngine.off(EVENT.SDK_READY, this.handleSDKReady);
|
||||
// 被踢下线
|
||||
wx.$TUICallEngine.off(EVENT.KICKED_OUT, this.handleKickedOut);
|
||||
// 切换通话模式
|
||||
wx.$TUICallEngine.off(EVENT.CALL_MODE, this.handleCallMode);
|
||||
// 自己发送消息
|
||||
wx.$TUICallEngine.off(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe);
|
||||
},
|
||||
/**
|
||||
* C2C邀请通话,被邀请方会收到的回调
|
||||
* 如果当前处于通话中,可以调用该函数以邀请第三方进入通话
|
||||
* @param {Object} params 呼叫参数
|
||||
* @param {String} params.userID 被邀请方
|
||||
* @param {Number} params.type 0-未知,1-语音通话,2-视频通话
|
||||
* @param {Number} params.roomID 数字房间号, 范围 1~2147483647
|
||||
* @param {String=} params.userData 扩展字段: 用于在邀请信令中增加扩展信息
|
||||
*/
|
||||
call: throttle(async function (params) {
|
||||
this.initialUI();
|
||||
if (this.data.callStatus !== STATUS.IDLE) {
|
||||
return;
|
||||
}
|
||||
this.checkRunPlatform();
|
||||
this.data.config.type = params.type;
|
||||
// 检查设备权限
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: params.type === MEDIA_TYPE.VIDEO,
|
||||
};
|
||||
const hasDevicePermission = await wx.$TUICallEngine.deviceCheck(deviceMap);
|
||||
const callStatus = hasDevicePermission ? STATUS.CALLING : STATUS.IDLE;
|
||||
this.setData({
|
||||
callStatus,
|
||||
isSponsor: true,
|
||||
config: this.data.config,
|
||||
});
|
||||
try {
|
||||
await this.getUserProfile([params.userID]);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
wx.$TUICallEngine.call({ userID: params.userID, type: params.type, roomID: params.roomID, userData: params.userData }).then(async (res) => {
|
||||
if (res) {
|
||||
this.setData({
|
||||
callStatus: STATUS.CALLING,
|
||||
pusher: res.pusher,
|
||||
});
|
||||
this.data.callingBell.setBellSrc(INVITER_BELL_FILEPATH);
|
||||
this.data.callingBell.play();
|
||||
this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code === -1002) {
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: 'error',
|
||||
content: error.message,
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
this.reset();
|
||||
throw new Error(error);
|
||||
});
|
||||
}, THROTTLE_TIME),
|
||||
/**
|
||||
* IM群组邀请通话,被邀请方会收到的回调
|
||||
* 如果当前处于通话中,可以继续调用该函数继续邀请他人进入通话,同时正在通话的用户会收到的回调
|
||||
*
|
||||
* @param {Object} params 呼叫参数
|
||||
* @param {Array<String>} params.userIDList 邀请列表
|
||||
* @param {Number} params.type 1-语音通话,2-视频通话
|
||||
* @param {String} params.groupID IM群组ID
|
||||
* @param {Number} params.roomID 数字房间号, 范围 1~2147483647
|
||||
* @param {String=} params.userData 扩展字段: 用于在邀请信令中增加扩展信息
|
||||
*/
|
||||
groupCall: throttle(async function (params) {
|
||||
// 判断是否存在groupID
|
||||
if (!params.groupID) {
|
||||
wx.showToast({
|
||||
title: '群ID为空',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.initialUI();
|
||||
if (this.data.callStatus !== STATUS.IDLE) {
|
||||
return;
|
||||
}
|
||||
this.checkRunPlatform();
|
||||
this.data.config.type = params.type;
|
||||
// 检查设备权限
|
||||
const deviceMap = {
|
||||
microphone: true,
|
||||
camera: params.type === MEDIA_TYPE.VIDEO,
|
||||
};
|
||||
const hasDevicePermission = await wx.$TUICallEngine.deviceCheck(deviceMap);
|
||||
const callStatus = hasDevicePermission ? STATUS.CALLING : STATUS.IDLE;
|
||||
this.setData({
|
||||
callStatus,
|
||||
isSponsor: true,
|
||||
isGroup: true,
|
||||
sponsor: this.data.config.userID,
|
||||
config: this.data.config,
|
||||
});
|
||||
// 将自身的userID插入到邀请列表中,组成完整的用户信息
|
||||
const list = JSON.parse(JSON.stringify(params.userIDList));
|
||||
list.unshift(this.data.config.userID);
|
||||
// 获取用户信息
|
||||
try {
|
||||
await this.getUserProfile(list);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
wx.$TUICallEngine.groupCall({
|
||||
userIDList: params.userIDList,
|
||||
type: params.type,
|
||||
groupID: params.groupID,
|
||||
roomID: params.roomID,
|
||||
userData: params.userData,
|
||||
}).then(async (res) => {
|
||||
if (res) {
|
||||
this.data.config.type = params.type;
|
||||
this.setData({
|
||||
pusher: res.pusher,
|
||||
callStatus: STATUS.CALLING,
|
||||
});
|
||||
this.data.callingBell.setBellSrc(INVITER_BELL_FILEPATH);
|
||||
this.data.callingBell.play();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code === -1002) {
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: 'error',
|
||||
content: error.message,
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
this.reset();
|
||||
throw new Error(error);
|
||||
});
|
||||
}, THROTTLE_TIME),
|
||||
/**
|
||||
* 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数接听来电
|
||||
*/
|
||||
accept: throttle(async function () {
|
||||
wx.$TUICallEngine.accept().then((res) => {
|
||||
if (res) {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
this.setData({
|
||||
pusher: res.pusher,
|
||||
callStatus: STATUS.CONNECTED,
|
||||
});
|
||||
// 多人通话需要对自身位置进行修正,将其放到首位
|
||||
if (this.data.isGroup) {
|
||||
const newList = this.data.allUsers;
|
||||
for (let i = 0;i < newList.length;i++) {
|
||||
if (newList[i].userID === this.data.config.userID) {
|
||||
newList[i].isEnter = true;
|
||||
[newList[i], newList[0]] = [newList[0], newList[i]];
|
||||
}
|
||||
};
|
||||
this.setData({
|
||||
allUsers: newList,
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.reset();
|
||||
});
|
||||
}, THROTTLE_TIME),
|
||||
/**
|
||||
* 当您作为被邀请方收到的回调时,可以调用该函数拒绝来电
|
||||
*/
|
||||
reject: throttle(async function () {
|
||||
wx.$TUICallEngine.reject().then((res) => {
|
||||
this.data.callingBell && this.data.callingBell.stop();
|
||||
this.reset();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.reset();
|
||||
});
|
||||
}, THROTTLE_TIME),
|
||||
messageSentByMe(event) {
|
||||
const message = event.data.data;
|
||||
this.triggerEvent('sendMessage', {
|
||||
message,
|
||||
});
|
||||
},
|
||||
// xml层,是否开启扬声器
|
||||
setSoundMode(type) {
|
||||
this.setData({
|
||||
soundMode: wx.$TUICallEngine.selectAudioPlaybackDevice(type),
|
||||
});
|
||||
},
|
||||
// xml层,挂断
|
||||
hangup: throttle(async function () {
|
||||
try {
|
||||
await wx.$TUICallEngine.hangup();
|
||||
this.reset();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.reset();
|
||||
}
|
||||
}, THROTTLE_TIME),
|
||||
// 切换大小屏 (仅支持1v1聊天)
|
||||
toggleViewSize(event) {
|
||||
this.setData({
|
||||
// FIXME _toggleViewSize 不应该为TUICallEngine的方法 后续修改
|
||||
screen: wx.$TUICallEngine._toggleViewSize(event),
|
||||
});
|
||||
},
|
||||
// 数据重置
|
||||
reset() {
|
||||
if (shouldHideTabBar) {
|
||||
wx.showTabBar();
|
||||
}
|
||||
this.setData({
|
||||
callStatus: STATUS.IDLE,
|
||||
isSponsor: false,
|
||||
isGroup: false,
|
||||
soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER,
|
||||
pusher: {}, // TRTC 本地流
|
||||
playerList: [], // TRTC 远端流
|
||||
showToatTime: 0,
|
||||
});
|
||||
},
|
||||
// 呼叫中的事件处理
|
||||
async handleCallingEvent(data) {
|
||||
const { name, event } = data.detail;
|
||||
switch (name) {
|
||||
case 'accept':
|
||||
this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
|
||||
this.accept();
|
||||
break;
|
||||
case 'hangup':
|
||||
await this.hangup();
|
||||
break;
|
||||
case 'reject':
|
||||
await this.reject();
|
||||
break;
|
||||
case 'toggleSwitchCamera':
|
||||
wx.$TUICallEngine.switchCamera();
|
||||
break;
|
||||
case 'switchAudioCall':
|
||||
wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
|
||||
this.data.config.type = MEDIA_TYPE.AUDIO;
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'pusherErrorHandler':
|
||||
this.pusherErrorHandler(event.errMsg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 通话中的事件处理
|
||||
async handleConnectedEvent(data) {
|
||||
const { name, event } = data.detail;
|
||||
switch (name) {
|
||||
case 'toggleViewSize':
|
||||
this.toggleViewSize(event);
|
||||
break;
|
||||
case 'pusherNetStatus':
|
||||
wx.$TUICallEngine._pusherNetStatus(event);
|
||||
break;
|
||||
case 'playNetStatus':
|
||||
wx.$TUICallEngine._playNetStatus(event);
|
||||
break;
|
||||
case 'pusherStateChangeHandler':
|
||||
wx.$TUICallEngine._pusherStateChangeHandler(event);
|
||||
break;
|
||||
case 'pusherAudioVolumeNotify':
|
||||
wx.$TUICallEngine._pusherAudioVolumeNotify(event);
|
||||
break;
|
||||
case 'playerStateChange':
|
||||
wx.$TUICallEngine._playerStateChange(event);
|
||||
break;
|
||||
case 'playerAudioVolumeNotify':
|
||||
wx.$TUICallEngine._playerAudioVolumeNotify(event);
|
||||
break;
|
||||
case 'pusherAudioHandler':
|
||||
wx.$TUICallEngine._pusherAudioHandler(event);
|
||||
break;
|
||||
case 'hangup':
|
||||
await this.hangup();
|
||||
break;
|
||||
case 'toggleSoundMode':
|
||||
this.setSoundMode(this.data.soundMode === AUDIO_PLAYBACK_DEVICE.EAR ? AUDIO_PLAYBACK_DEVICE.SPEAKER : AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
break;
|
||||
case 'pusherVideoHandler':
|
||||
wx.$TUICallEngine._pusherVideoHandler(event);
|
||||
break;
|
||||
case 'toggleSwitchCamera':
|
||||
wx.$TUICallEngine.switchCamera(event);
|
||||
break;
|
||||
case 'switchAudioCall':
|
||||
wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
|
||||
this.data.config.type = MEDIA_TYPE.AUDIO;
|
||||
this.setData({
|
||||
config: this.data.config,
|
||||
});
|
||||
this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
|
||||
});
|
||||
break;
|
||||
case 'pusherErrorHandler':
|
||||
this.pusherErrorHandler(event.errMsg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 处理 pusher 中的异常问题
|
||||
pusherErrorHandler(code) {
|
||||
switch (code) {
|
||||
case 'fail:access denied':
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: '权限提示',
|
||||
content: '当前小程序 appid 不具备 <live-pusher> 和 <live-player> 的使用权限,您将无法正常使用实时通话能力,请使用企业小程序账号申请权限后再进行开发体验',
|
||||
showCancel: false,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 设置用户的头像、昵称
|
||||
setSelfInfo(nickName, avatar) {
|
||||
return wx.$TUICallEngine.setSelfInfo(nickName, avatar);
|
||||
},
|
||||
// 获取用户资料
|
||||
async getUserProfile(userList) {
|
||||
const imResponse = await this.getTim().getUserProfile({ userIDList: userList });
|
||||
// 修正用户资料
|
||||
this.modifyUser(imResponse.data);
|
||||
},
|
||||
// 修正用户资料
|
||||
modifyUser(userIDList) {
|
||||
const { sponsor } = this.data;
|
||||
if (this.data.isGroup) {
|
||||
// 多人通话需要将呼叫者放到第一位 isEnter的作用是区分用户是否进入房间
|
||||
for (let i = 0;i < userIDList.length;i++) {
|
||||
// 主叫方的标志位设置成true
|
||||
if (userIDList[i].userID === sponsor) {
|
||||
userIDList[i].isEnter = true;
|
||||
// 对主叫方位置进行修正 将其放到首位
|
||||
[userIDList[i], userIDList[0]] = [userIDList[0], userIDList[i]];
|
||||
} else {
|
||||
// 其他用户默认未进入房间 设置为false
|
||||
userIDList[i].isEnter = false;
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
allUsers: userIDList,
|
||||
});
|
||||
}
|
||||
this.setData({
|
||||
remoteUsers: userIDList,
|
||||
});
|
||||
},
|
||||
// 获取 tim 实例
|
||||
getTim() {
|
||||
return wx.$TUICallEngine.getTim();
|
||||
},
|
||||
|
||||
// 设置日志级别,低于 level 的日志将不会输出
|
||||
setLogLevel(leave) {
|
||||
wx.$TUICallEngine.setLogLevel(leave);
|
||||
},
|
||||
|
||||
// 初始化TUICallKit
|
||||
async init(params) {
|
||||
if (tuiCallInitReady) return;
|
||||
tuiCallInitReady = true;
|
||||
// 关于 tabBar 页面的特化逻辑
|
||||
this.handleTabBarLogic();
|
||||
// 兼容从config和init中传值
|
||||
const { sdkAppID, tim, userID, userSig, SDKAppID } = { ...this.data.config, ...params };
|
||||
this.setData({
|
||||
ownUserId: userID,
|
||||
config: {
|
||||
...this.data.config,
|
||||
sdkAppID: sdkAppID || SDKAppID,
|
||||
userID,
|
||||
userSig,
|
||||
},
|
||||
});
|
||||
// 取消全局监听
|
||||
if (wx.$globalCallSign) {
|
||||
wx.$CallManagerInstance.removeEngineInvite();
|
||||
}
|
||||
this.setUniBellFilePath();
|
||||
if (!this.data.callingBell) { // 创建铃声实例
|
||||
const bellContext = new BellContext();
|
||||
this.setData({
|
||||
callingBell: bellContext,
|
||||
});
|
||||
}
|
||||
if (!wx.$TUICallEngine) {
|
||||
wx.$TUICallEngine = TUICallEngine.createInstance({
|
||||
tim,
|
||||
sdkAppID: sdkAppID || SDKAppID,
|
||||
});
|
||||
/*
|
||||
_addTSignalingEvent 需要在 init 之前,回调才能正常触发
|
||||
tap:https://tapd.woa.com/20396022/prong/stories/view/1020396022885781069
|
||||
*/
|
||||
this._addTSignalingEvent();
|
||||
try {
|
||||
await wx.$TUICallEngine.init({
|
||||
userID,
|
||||
userSig,
|
||||
});
|
||||
} catch (error) {
|
||||
tuiCallInitReady = false;
|
||||
throw new Error(error);
|
||||
}
|
||||
// 用于全局监听进入 TUICallKit 页面中绑定上当前页面的监听。
|
||||
} else {
|
||||
this._addTSignalingEvent();
|
||||
}
|
||||
console.log(`${PREFIX} init Ready!`);
|
||||
},
|
||||
// 销毁 TUICallEngine
|
||||
async destroyed() {
|
||||
if (!wx.$TUICallEngine || !tuiCallInitReady) return;
|
||||
tuiCallInitReady = false;
|
||||
this._removeTSignalingEvent();
|
||||
// 全局监听模式下,离开页面需要重新绑定全局监听,但不卸载 TUICallEngine
|
||||
if (wx.$globalCallSign) {
|
||||
// 处理异常退出
|
||||
if (this.getCallStatus() !== STATUS.IDLE) {
|
||||
await this.handleExceptionExit();
|
||||
}
|
||||
wx.$CallManagerInstance.addEngineInvite();
|
||||
} else {
|
||||
// 非全局监听模式下,离开页面需要卸载 TUICallEngine
|
||||
wx.$TUICallEngine.destroyInstance();
|
||||
wx.$TUICallEngine = null;
|
||||
}
|
||||
if (this.data.callingBell) {
|
||||
this.data.callingBell.destroyInstance();
|
||||
this.setData({
|
||||
callingBell: null,
|
||||
});
|
||||
}
|
||||
this.reset();
|
||||
console.log(`${PREFIX} destroyed OK!`);
|
||||
},
|
||||
|
||||
/*
|
||||
以下两种情况需要全部符合,则需要隐藏 tabBar
|
||||
1.用户当前处于 tabBar 页面, isTabBar 为 true
|
||||
2.用户没有使用了自定义 tabBar, isCustomTabBar 为 fales
|
||||
*/
|
||||
handleTabBarLogic() {
|
||||
const isTabBar = isTabBarPage();
|
||||
const isCustomTabBar = tabBarConfig().custom || false;
|
||||
shouldHideTabBar = isTabBar && !isCustomTabBar;
|
||||
},
|
||||
|
||||
// 检测运行时环境
|
||||
checkRunPlatform() {
|
||||
const systemInfo = wx.getSystemInfoSync();
|
||||
if (systemInfo.platform === 'devtools') {
|
||||
// 当前运行在微信开发者工具里
|
||||
wx.showModal({
|
||||
icon: 'none',
|
||||
title: '运行环境提醒',
|
||||
content: '微信开发者工具不支持原生推拉流组件(即 <live-pusher> 和 <live-player> 标签),请使用真机调试或者扫码预览。',
|
||||
showCancel: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
// 运行在 uniapp 小程序需要修改铃声地址
|
||||
setUniBellFilePath() {
|
||||
// 防止用户退出再进入,在地址前重复拼接
|
||||
if (INVITER_BELL_FILEPATH.indexOf('wxcomponents') !== -1) {
|
||||
return;
|
||||
}
|
||||
// comType 用于 Vue2 的判断,wxcomponents 用于 Vue3 的判断
|
||||
if (this.dataset.comType === 'wx' || this.is.indexOf('wxcomponents') !== -1) {
|
||||
INVITER_BELL_FILEPATH = `wxcomponents/${INVITER_BELL_FILEPATH}`;
|
||||
INVITEE_BELL_FILEPATH = `wxcomponents/${INVITEE_BELL_FILEPATH}`;
|
||||
}
|
||||
},
|
||||
// 自定义铃声
|
||||
setCallingBell(filePath) {
|
||||
// 如需恢复默认铃声,filePath 传空即可
|
||||
this.setData({
|
||||
bellFilePath: filePath,
|
||||
});
|
||||
},
|
||||
// 处理用户异常退出的情况,处理了右滑退出,以及返回退出的情况。
|
||||
async handleExceptionExit() {
|
||||
// 在呼叫状态下,被叫调用 reject,主叫调用 hangup
|
||||
if (this.getCallStatus() === STATUS.CALLING) {
|
||||
if (this.isSponsor()) {
|
||||
await this.hangup();
|
||||
} else {
|
||||
await this.reject();
|
||||
}
|
||||
}
|
||||
// 在通话状态下,统一调用 hangup 接口
|
||||
if (this.getCallStatus() === STATUS.CONNECTED) {
|
||||
await this.hangup();
|
||||
}
|
||||
},
|
||||
|
||||
getCallStatus() {
|
||||
return this.data.callStatus;
|
||||
},
|
||||
|
||||
isSponsor() {
|
||||
return this.data.isSponsor;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
this.reset();
|
||||
},
|
||||
attached() {
|
||||
|
||||
},
|
||||
ready() {
|
||||
console.log(`${PREFIX} SDK Version: ${version}`);
|
||||
},
|
||||
async detached() {
|
||||
await this.destroyed();
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
pageLifetimes: {
|
||||
async show() {
|
||||
if (this.data.config.sdkAppID) {
|
||||
await this.init(this.data.config);
|
||||
}
|
||||
},
|
||||
async hide() {
|
||||
if (this.getCallStatus() === STATUS.IDLE) {
|
||||
await this.destroyed();
|
||||
}
|
||||
},
|
||||
resize() {
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"TUI-Calling": "./component/calling/calling",
|
||||
"TUI-Connected": "./component/connected/connected",
|
||||
"TUI-groupCalling": "./component/groupCalling/groupCalling",
|
||||
"TUI-groupConnected": "./component/groupConnected/groupConnected"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<view class="TUICalling {{callStatus === 'idle' ? 'hidden': 'show'}}">
|
||||
<view class="TRTCCaling-container {{isGroup && config.type === 2 ?'groupConnected':''}}">
|
||||
<TUI-Calling
|
||||
wx:if="{{callStatus === 'calling' && !isGroup}}"
|
||||
isSponsor="{{isSponsor}}"
|
||||
pusher="{{pusher}}"
|
||||
isGroup="{{isGroup}}"
|
||||
callType="{{config.type}}"
|
||||
remoteUsers="{{remoteUsers}}"
|
||||
bind:callingEvent="handleCallingEvent"
|
||||
></TUI-Calling>
|
||||
|
||||
<TUI-groupCalling
|
||||
wx:if="{{callStatus === 'calling' && isGroup}}"
|
||||
isSponsor="{{isSponsor}}"
|
||||
pusher="{{pusher}}"
|
||||
isGroup="{{isGroup}}"
|
||||
callType="{{config.type}}"
|
||||
allUsers="{{allUsers}}"
|
||||
ownUserId="{{ownUserId}}"
|
||||
bind:callingEvent="handleCallingEvent"
|
||||
></TUI-groupCalling>
|
||||
|
||||
|
||||
<TUI-Connected
|
||||
wx:if="{{callStatus === 'connected' && !isGroup}}"
|
||||
playerList="{{playerList}}"
|
||||
isGroup="{{isGroup}}"
|
||||
userList="{{userList}}"
|
||||
pusher="{{pusher}}"
|
||||
callType="{{config.type}}"
|
||||
soundMode="{{soundMode}}"
|
||||
screen="{{screen}}"
|
||||
bind:connectedEvent="handleConnectedEvent"
|
||||
></TUI-Connected>
|
||||
|
||||
<TUI-groupConnected
|
||||
wx:if="{{callStatus === 'connected' && isGroup}}"
|
||||
allUsers="{{allUsers}}"
|
||||
playerList="{{playerList}}"
|
||||
userList="{{userList}}"
|
||||
isGroup="{{isGroup}}"
|
||||
pusher="{{pusher}}"
|
||||
callType="{{config.type}}"
|
||||
soundMode="{{soundMode}}"
|
||||
screen="{{screen}}"
|
||||
ownUserId="{{ownUserId}}"
|
||||
playerProcess="{{ playerProcess}}"
|
||||
bind:connectedEvent="handleConnectedEvent"
|
||||
></TUI-groupConnected>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,25 @@
|
||||
.TRTCCaling-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/* background-image: url(https://mc.qcloudimg.com/static/img/7da57e0050d308e2e1b1e31afbc42929/bg.png); */
|
||||
margin: 0;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.TUICalling {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 10;
|
||||
background: #ffffff;
|
||||
}
|
||||
.show {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.groupConnected{
|
||||
background-color: #2c292923;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// components/tui-calling/TUICalling/component/calling.js
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
isSponsor: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
remoteUsers: {
|
||||
type: Array,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
},
|
||||
detached() {
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
accept(event) {
|
||||
const data = {
|
||||
name: 'accept',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
reject(event) {
|
||||
const data = {
|
||||
name: 'reject',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
handleErrorImage(e) {
|
||||
const { id } = e.target;
|
||||
const remoteUsers = this.data.remoteUsers.map((item) => {
|
||||
if (item.userID === id) {
|
||||
item.avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({
|
||||
remoteUsers,
|
||||
});
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
pusherErrorHandler(event) {
|
||||
const { detail = '' } = event;
|
||||
const data = {
|
||||
name: 'pusherErrorHandler',
|
||||
event: detail,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<view class="invite-call transition-animation" wx:if="{{callType === 2}}">
|
||||
<!-- wx:if="{{pusher.frontCamera}}" 为了处理下滑动画卡顿的问题 -->
|
||||
<live-pusher class="local-video" wx:if="{{pusher.frontCamera}}" device-position="{{pusher.frontCamera}}" binderror="pusherErrorHandler"/>
|
||||
<view class="invite-calling">
|
||||
<view class="invite-calling-header" wx:if="{{remoteUsers.length === 1}}">
|
||||
<!-- <view class="invite-calling-header-left">
|
||||
<image src="../../static/swtich-camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view> -->
|
||||
<view class="invite-calling-header-right">
|
||||
<view class="invite-calling-header-message">
|
||||
<label class="tips">{{remoteUsers[0].nick || remoteUsers[0].userID}}</label>
|
||||
<text class="tips-subtitle" wx:if="{{!isSponsor}}">邀请你视频通话</text>
|
||||
<text class="tips-subtitle" wx:else>等待对方接受</text>
|
||||
</view>
|
||||
<image class="avatar" src="{{remoteUsers[0].avatar || '../../static/default_avatar.png'}}"
|
||||
id="{{remoteUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="invite-calling-header invite-calling-list" wx:else>
|
||||
<view class="invite-calling-item" wx:for="{{remoteUsers}}" wx:key="userID">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" id="{{item.userID}}"
|
||||
binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-message">
|
||||
<label class="tips">{{item.nick || item.userID}}</label>
|
||||
<text class="tips-subtitle" wx:if="{{!isSponsor}}">邀请你视频通话</text>
|
||||
<text class="tips-subtitle" wx:else>等待对方接受</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="btn-container">
|
||||
<view class="call-operate" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view>
|
||||
<text>挂断</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{!isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<text>挂断</text>
|
||||
</view>
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" catch:tap="accept">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<text>接听</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="incoming-call audio-call transition-animation" wx:if="{{callType === 1}}">
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{remoteUsers[0].avatar || '../../static/default_avatar.png'}}"
|
||||
id="{{remoteUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{remoteUsers[0].nick || remoteUsers[0].userID}}</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="tips-subtitle">{{'等待对方接受'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view wx:if="{{!isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="call-operate" catch:tap="accept">
|
||||
<image src="../../static/dialing.png"/>
|
||||
</view>
|
||||
<view style="margin-top:10px">接听</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px">挂断</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,258 @@
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-operate {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* flex-direction: column;
|
||||
text-align: center; */
|
||||
|
||||
}
|
||||
.btn-operate-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-operate-item text {
|
||||
font-size: 14px;
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.call-switch text{
|
||||
padding: 5px;
|
||||
color: #f0e9e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.call-operate {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
border-radius: 8vh;
|
||||
margin: 0 15vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.40); */
|
||||
font-weight: 600;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tips-subtitle {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.30); */
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.invite-call {
|
||||
/* background: #ffffff; */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.transition-animation {
|
||||
transform: translateY(-100%);
|
||||
animation: slideInDown 0.5s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-call .local-video {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.invite-call .invite-calling {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
.invite-calling-header {
|
||||
margin-top:107px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px;
|
||||
|
||||
}
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.invite-calling-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.invite-calling-header-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.invite-calling-header-right image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.invite-calling .footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
}
|
||||
.invite-calling .btn-operate{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.trtc-calling {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.audio-call {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.audio-call > .btn-operate{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.audio-call > image {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
display: block;
|
||||
margin: 20vw 30vw;
|
||||
margin-top: 40vw;
|
||||
}
|
||||
|
||||
.invite-calling-single > image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
margin: 140px auto 15px;
|
||||
/* margin: 20vw 30vw; */
|
||||
}
|
||||
|
||||
.invite-calling-single .tips {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.invite-calling-single .tips-subtitle {
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #97989C;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
position: relative;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.invite-calling-item image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
playerList: {
|
||||
type: Array,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
soundMode: {
|
||||
type: String,
|
||||
},
|
||||
screen: {
|
||||
type: String,
|
||||
},
|
||||
userList: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
toggleViewSize(event) {
|
||||
const data = {
|
||||
name: 'toggleViewSize',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherNetStatus(event) {
|
||||
const data = {
|
||||
name: 'pusherNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playNetStatus(event) {
|
||||
const data = {
|
||||
name: 'playNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherStateChangeHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherStateChangeHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherErrorHandler(event) {
|
||||
const { detail = '' } = event;
|
||||
const data = {
|
||||
name: 'pusherErrorHandler',
|
||||
event: detail,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerStateChange(event) {
|
||||
const data = {
|
||||
name: 'playerStateChange',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'playerAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSoundMode(event) {
|
||||
const data = {
|
||||
name: 'toggleSoundMode',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherVideoHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherVideoHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
handleConnectErrorImage(e) {
|
||||
const { flag, key, index } = e.target.dataset;
|
||||
if (flag === 'pusher') {
|
||||
this.data.pusher.avatar = '../../static/default_avatar.png';
|
||||
this.setData({
|
||||
pusher: this.data.pusher,
|
||||
});
|
||||
} else {
|
||||
this.data[key][index].avatar = '../../static/default_avatar.png';
|
||||
if (this.data.playerList[index]) {
|
||||
this.data.playerList[index].avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
this.setData({
|
||||
playerList: this.data.playerList,
|
||||
[key]: this.data[key],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<view class="TUICalling-connected-layout {{callType === 1 ? 'audio' : 'video'}}">
|
||||
<view
|
||||
class="{{callType === 1 ? 'pusher-audio' : playerList.length > 1 ? 'stream-box' : (screen === 'pusher' ? 'pusher-video' : 'player')}}"
|
||||
data-screen="pusher" catch:tap="toggleViewSize">
|
||||
<live-pusher class="{{callType === 1 ? 'pusher-audio' : 'live'}}" url="{{pusher.url}}" mode="{{pusher.mode}}"
|
||||
autopush="{{true}}" enable-camera="{{pusher.enableCamera}}" enable-mic="{{true}}"
|
||||
muted="{{!pusher.enableMic}}" enable-agc="{{true}}" enable-ans="{{true}}"
|
||||
enable-ear-monitor="{{pusher.enableEarMonitor}}" auto-focus="{{pusher.enableAutoFocus}}"
|
||||
zoom="{{pusher.enableZoom}}" min-bitrate="{{pusher.minBitrate}}" max-bitrate="{{pusher.maxBitrate}}"
|
||||
video-width="{{pusher.videoWidth}}" video-height="{{pusher.videoHeight}}" beauty="{{pusher.beautyLevel}}"
|
||||
whiteness="{{pusher.whitenessLevel}}" orientation="{{pusher.videoOrientation}}"
|
||||
aspect="{{pusher.videoAspect}}" device-position="{{pusher.frontCamera}}"
|
||||
remote-mirror="{{pusher.enableRemoteMirror}}" local-mirror="{{pusher.localMirror}}"
|
||||
background-mute="{{pusher.enableBackgroundMute}}" audio-quality="{{pusher.audioQuality}}"
|
||||
audio-volume-type="{{pusher.audioVolumeType}}" audio-reverb-type="{{pusher.audioReverbType}}"
|
||||
waiting-image="{{pusher.waitingImage}}" beauty-style="{{pusher.beautyStyle}}" filter="{{pusher.filter}}"
|
||||
bindstatechange="pusherStateChangeHandler" bindnetstatus="pusherNetStatus" binderror="pusherErrorHandler"
|
||||
bindaudiovolumenotify="pusherAudioVolumeNotify" />
|
||||
</view>
|
||||
<view wx:if="{{callType === 1}}" class="TRTCCalling-call-audio-box {{playerList.length > 1 && 'mutil-img'}}">
|
||||
<view class="TRTCCalling-call-audio-img" wx:if="{{playerList.length > 1}}">
|
||||
<image src="{{pusher.avatar || '../../static/default_avatar.png'}}" class="img-place-holder avatar"
|
||||
data-value="{{pusher.userID}}" data-flag="pusher" binderror="handleConnectErrorImage" />
|
||||
<text class="audio-name">{{pusher.nick || pusher.userID}}(自己)</text>
|
||||
</view>
|
||||
<view class="TRTCCalling-call-audio-img" wx:for="{{userList || playerList}}" wx:key="userID">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}" class="img-place-holder avatar"
|
||||
data-value="{{item}}" data-flag="player" data-key="userList" data-index="{{index}}" binderror="handleConnectErrorImage" />
|
||||
<text class="audio-name">{{item.nick || item.userID}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:for="{{playerList}}" wx:key="streamID"
|
||||
class="view-container player-container {{callType === 1 ? 'player-audio' : ''}}">
|
||||
<view
|
||||
class="{{callType === 1 ? 'player-audio' : playerList.length > 1 ? 'stream-box' : (screen === 'player' ? 'pusher-video' : 'player')}}"
|
||||
data-screen="player" catch:tap="toggleViewSize">
|
||||
<live-player class="live" wx:if="{{ item.hasAudio || item.hasVideo }}" id="{{item.id}}" data-userid="{{item.userID}}" data-streamid="{{item.streamID}}"
|
||||
data-streamtype="{{item.streamType}}" src="{{item.src}}" mode="RTC" autoplay="{{item.autoplay}}"
|
||||
mute-audio="{{item.muteAudio}}" mute-video="{{item.muteVideo}}" orientation="{{item.orientation}}"
|
||||
object-fit="{{item.objectFit}}" background-mute="{{item.enableBackgroundMute}}"
|
||||
min-cache="{{item.minCache}}" max-cache="{{item.maxCache}}" sound-mode="{{soundMode}}"
|
||||
enable-recv-message="{{item.enableRecvMessage}}" auto-pause-if-navigate="{{item.autoPauseIfNavigate}}"
|
||||
auto-pause-if-open-native="{{item.autoPauseIfOpenNative}}" bindstatechange="playerStateChange"
|
||||
bindfullscreenchange="playerFullscreenChange" bindnetstatus="playNetStatus"
|
||||
bindaudiovolumenotify="playerAudioVolumeNotify" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="handle-btns">
|
||||
<view class="other-view {{callType === 1 ? 'black' : 'white'}}">
|
||||
<text>{{pusher.chatTime}}</text>
|
||||
</view>
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
<view class="btn-list">
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="pusherAudioHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableMic? '../../static/audio-true.png': '../../static/audio-false.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">麦克风</view>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 1}}">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="toggleSoundMode">
|
||||
<image class="btn-image"
|
||||
src="{{soundMode === 'ear' ? '../../static/speaker-false.png': '../../static/speaker-true.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<text class="{{callType === 2 ? 'white' : ''}}">扬声器</text>
|
||||
</view>
|
||||
|
||||
<view class="button-container" wx:if="{{callType === 2}}">
|
||||
<view class="btn-normal" bindtap="pusherVideoHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableCamera ? '../../static/camera-true.png': '../../static/camera-false.png'}} ">
|
||||
</image>
|
||||
</view>
|
||||
<text class="white">摄像头</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-list" wx:if="{{callType===2}}">
|
||||
<!-- <view class="btn-list-item">
|
||||
<view wx:if="{{playerList.length === 1}}" class="btn-normal" bindtap="switchAudioCall">
|
||||
<image class="btn-image btn-image-small" src="{{ '../../static/trans.png'}} "></image>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="btn-list-item other-view">
|
||||
<view class="btn-container">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view wx:if="{{pusher.enableCamera}}" class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="white">挂断</text>
|
||||
</view>
|
||||
<!-- <view class="btn-list-item btn-footer">
|
||||
<view wx:if="{{pusher.enableCamera}}" class="{{playerList.length > 1 ? 'multi-camera' : 'camera'}}">
|
||||
<image class="camera-image" src="../../static/swtich-camera.png"
|
||||
data-device="{{pusher.frontCamera}}" catch:tap="toggleSwitchCamera" />
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,253 @@
|
||||
.player {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 107px;
|
||||
width: 100px;
|
||||
height: 178px;
|
||||
padding: 16px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.pusher-video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background-color: #f75c45; */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stream-box {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 50vw;
|
||||
height: 260px;
|
||||
/* background-color: #f75c45; */
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.handle-btns {
|
||||
position: absolute;
|
||||
bottom: 44px;
|
||||
width: 100vw;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handle-btns .btn-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-normal {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background: white; */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-hangup {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
/*background: #f75c45;*/
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-hangup > .btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-footer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-footer .multi-camera {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-footer .camera {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 107px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-footer .camera .camera-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.TUICalling-connected-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.audio {
|
||||
padding-top: 15vh;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pusher-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.player-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.live {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.other-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000000;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mutil-img {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img > image {
|
||||
width: 25vw;
|
||||
height: 25vw;
|
||||
margin: 0 4vw;
|
||||
border-radius: 4vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img text {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.btn-image-small {
|
||||
transform: scale(.7);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: -88px;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
padding: 8px 0;
|
||||
font-size: 18px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// components/tui-calling/TUICalling/component/calling.js
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
isSponsor: {
|
||||
type: Boolean,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
allUsers: {
|
||||
type: Array,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
ownUserId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
userID: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
|
||||
},
|
||||
detached() {
|
||||
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
accept(event) {
|
||||
const data = {
|
||||
name: 'accept',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
reject(event) {
|
||||
const data = {
|
||||
name: 'reject',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
handleErrorImage(e) {
|
||||
const { id } = e.target;
|
||||
const allUsers = this.data.allUsers.map((item) => {
|
||||
if (item.userID === id) {
|
||||
item.avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({
|
||||
allUsers,
|
||||
});
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
pusherErrorHandler(event) {
|
||||
const { detail = '' } = event;
|
||||
const data = {
|
||||
name: 'pusherErrorHandler',
|
||||
event: detail,
|
||||
};
|
||||
this.triggerEvent('callingEvent', data);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<!-- 视频通话 -->
|
||||
<view class="invite-call transition-animation" wx:if="{{callType === 2}}">
|
||||
<live-pusher class="local-video" wx:if="{{isGroup===false}}" device-position="{{pusher.frontCamera}}" binderror="pusherErrorHandler"/>
|
||||
<view class="invite-calling">
|
||||
<!-- 主叫方 -->
|
||||
<swiper class="swiper" wx:if="{{isSponsor}}" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white" indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item" wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{item.userID !== ownUserId}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
<!-- 被叫方 -->
|
||||
<view wx:else>
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{allUsers[0].avatar || '../../static/default_avatar.png'}}" id="{{allUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{allUsers[0].nick || allUsers[0].userID }}</view>
|
||||
<view class="invite-txt">邀请你参加多人通话</view>
|
||||
</view>
|
||||
<view class="invite-other-txt">参与通话的还有:</view>
|
||||
<view class="invite-other-list">
|
||||
<view class="invite-other-item" wx:if="{{index>0}}" wx:for="{{allUsers}}" wx:key="item">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-other-item-name">{{ item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer">
|
||||
<!-- <view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item call-switch" catch:tap="switchAudioCall">
|
||||
<view class="call-operate">
|
||||
<image src="../../static/trans.png" />
|
||||
</view>
|
||||
<text>切到语音通话</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="btn-operate" wx:if="{{isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="btn-container">
|
||||
<view class="call-operate" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<!-- <view class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}" catch:tap="toggleSwitchCamera" />
|
||||
</view> -->
|
||||
</view>
|
||||
<text style="color: #666666">挂断</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-operate" wx:if="{{!isSponsor}}">
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<text style="color: #666666">挂断</text>
|
||||
</view>
|
||||
<view class="btn-operate-item">
|
||||
<view class="call-operate" catch:tap="accept">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<text style="color: #666666">接听</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 语音通话 -->
|
||||
<view class="incoming-call audio-call transition-animation" wx:if="{{callType === 1}}">
|
||||
<!-- 主叫方 -->
|
||||
<swiper class="swiper" wx:if="{{isSponsor}}" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white" indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item" wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{item.userID !== ownUserId}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
<!-- 被叫方 -->
|
||||
<view wx:else>
|
||||
<view class="invite-calling-single">
|
||||
<image class="avatar" src="{{allUsers[0].avatar || '../../static/default_avatar.png'}}" id="{{allUsers[0].userID}}" binderror="handleErrorImage" />
|
||||
<view class="tips">{{allUsers[0].nick || allUsers[0].userID }}</view>
|
||||
<view class="invite-txt">邀请你参加多人通话</view>
|
||||
</view>
|
||||
<view class="invite-other-txt">参与通话的还有:</view>
|
||||
<view class="invite-other-list">
|
||||
<view class="invite-other-item" wx:if="{{index>0}}" wx:for="{{allUsers}}" wx:key="item">
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-other-item-name">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 菜单 -->
|
||||
<view class="footer">
|
||||
<view wx:if="{{!isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="reject">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="call-operate" catch:tap="accept">
|
||||
<image src="../../static/dialing.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">接听</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isSponsor && callType === 1}}" class="btn-operate">
|
||||
<view class="button-container">
|
||||
<view class="call-operate" style="background-color: red" catch:tap="hangup">
|
||||
<image src="../../static/hangup.png" />
|
||||
</view>
|
||||
<view style="margin-top:10px;color: #666666">挂断</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,380 @@
|
||||
.transition-animation {
|
||||
transform: translateY(-100%);
|
||||
animation: slideInDown 0.5s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-operate {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
/* flex-direction: column;
|
||||
text-align: center; */
|
||||
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
font-size: 14px;
|
||||
color: #f0e9e9;
|
||||
padding: 5px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 5px;
|
||||
color: #f0e9e9;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.call-operate {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
border-radius: 8vh;
|
||||
margin: 0 15vw;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.40); */
|
||||
font-weight: 600;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tips-subtitle {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0;
|
||||
text-align: right;
|
||||
/* text-shadow: 0 1px 2px rgba(0,0,0,0.30); */
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.invite-call {
|
||||
/* background: #ffffff; */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100px;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.invite-call .local-video {
|
||||
width: 100px;
|
||||
height: 187px;
|
||||
}
|
||||
|
||||
.invite-call .invite-calling {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.invite-calling-header {
|
||||
margin-top: 107px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.invite-calling-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invite-calling-header-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.invite-calling-header-right image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.invite-calling .footer {
|
||||
position: absolute;
|
||||
bottom: 5vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invite-calling .btn-operate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.trtc-calling {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.audio-call {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.audio-call>.btn-operate {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.audio-call>image {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
display: block;
|
||||
margin: 20vw 30vw;
|
||||
margin-top: 40vw;
|
||||
}
|
||||
|
||||
.invite-calling-single>image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
margin: 120px auto 15px;
|
||||
/* margin: 20vw 30vw; */
|
||||
}
|
||||
|
||||
.invite-calling-single .tips {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invite-calling-single .tips-subtitle {
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-size: 14px;
|
||||
color: #97989C;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
margin-top: 107px;
|
||||
min-height: 374px;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
float: left;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg {
|
||||
position: absolute;
|
||||
left: calc(50% - 20px);
|
||||
top: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-transform: rotate(360deg);
|
||||
animation: rotation 2s linear infinite;
|
||||
-moz-animation: rotation 2s linear infinite;
|
||||
-webkit-animation: rotation 2s linear infinite;
|
||||
-o-animation: rotation 2s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
|
||||
/* 被叫者 */
|
||||
.invite-txt {
|
||||
width: 126px;
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
margin: 16px auto 60px auto;
|
||||
}
|
||||
|
||||
.invite-other-txt {
|
||||
width: 112px;
|
||||
height: 20px;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
margin: 0 auto 24px auto;
|
||||
}
|
||||
|
||||
.invite-other-list {
|
||||
position: absolute;
|
||||
left: 14vw;
|
||||
margin-top: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 272px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.invite-other-item {
|
||||
flex: 0.25;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
text-align: center;
|
||||
max-width: 64px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.invite-other-item image {
|
||||
border-radius: 10%;
|
||||
max-width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.invite-other-item-name {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
Component({
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
playerList: {
|
||||
type: Array,
|
||||
},
|
||||
pusher: {
|
||||
type: Object,
|
||||
},
|
||||
callType: {
|
||||
type: Number,
|
||||
},
|
||||
soundMode: {
|
||||
type: String,
|
||||
},
|
||||
screen: {
|
||||
type: String,
|
||||
},
|
||||
userList: {
|
||||
type: Object,
|
||||
},
|
||||
isGroup: {
|
||||
type: Boolean,
|
||||
},
|
||||
allUsers: {
|
||||
type: Array,
|
||||
},
|
||||
playerProcess: {
|
||||
type: Object,
|
||||
},
|
||||
ownUserId: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
userID: null,
|
||||
},
|
||||
/**
|
||||
* 生命周期方法
|
||||
*/
|
||||
lifetimes: {
|
||||
created() {
|
||||
},
|
||||
attached() {
|
||||
},
|
||||
ready() {
|
||||
},
|
||||
detached() {
|
||||
},
|
||||
error() {
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
toggleViewSize(event) {
|
||||
const data = {
|
||||
name: 'toggleViewSize',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherNetStatus(event) {
|
||||
const data = {
|
||||
name: 'pusherNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playNetStatus(event) {
|
||||
const data = {
|
||||
name: 'playNetStatus',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherStateChangeHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherStateChangeHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerStateChange(event) {
|
||||
const data = {
|
||||
name: 'playerStateChange',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherErrorHandler(event) {
|
||||
const { detail = '' } = event;
|
||||
const data = {
|
||||
name: 'pusherErrorHandler',
|
||||
event: detail,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
playerAudioVolumeNotify(event) {
|
||||
const data = {
|
||||
name: 'playerAudioVolumeNotify',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherAudioHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherAudioHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
hangup(event) {
|
||||
const data = {
|
||||
name: 'hangup',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSoundMode(event) {
|
||||
const data = {
|
||||
name: 'toggleSoundMode',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
pusherVideoHandler(event) {
|
||||
const data = {
|
||||
name: 'pusherVideoHandler',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
toggleSwitchCamera(event) {
|
||||
const data = {
|
||||
name: 'toggleSwitchCamera',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
switchAudioCall(event) {
|
||||
const data = {
|
||||
name: 'switchAudioCall',
|
||||
event,
|
||||
};
|
||||
this.triggerEvent('connectedEvent', data);
|
||||
},
|
||||
handleConnectErrorImage(e) {
|
||||
const { flag, key, index } = e.target.dataset;
|
||||
if (flag === 'pusher') {
|
||||
this.data.pusher.avatar = '../../static/default_avatar.png';
|
||||
this.setData({
|
||||
pusher: this.data.pusher,
|
||||
});
|
||||
} else {
|
||||
this.data[key][index].avatar = '../../static/default_avatar.png';
|
||||
if (this.data.playerList[index]) {
|
||||
this.data.playerList[index].avatar = '../../static/default_avatar.png';
|
||||
}
|
||||
this.setData({
|
||||
playerList: this.data.playerList,
|
||||
[key]: this.data[key],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {},
|
||||
"window": {
|
||||
"backgroundColor": "#2c292923;"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<view class="{{callType === 2?'TUICalling-connected-video':'TUICalling-connected-layout'}}">
|
||||
<!-- 语音通话 -->
|
||||
<view wx:if="{{callType === 1}}">
|
||||
<swiper class="swiper" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white"
|
||||
indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item"
|
||||
wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{!item.isEnter}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}"
|
||||
binderror="handleErrorImage"/>
|
||||
<!-- 音量图标 -->
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">{{item.userID===ownUserId?'我':item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
<view class="{{callType === 1 ? 'pusher-audio' : ''}}">
|
||||
<swiper class="swiper" indicator-dots="{{allUsers.length/4 > 1}}" indicator-color="white"
|
||||
indicator-active-color="black">
|
||||
<block wx:for="{{(allUsers.length)/4}}" wx:key="*this" wx:for-index="pos">
|
||||
<swiper-item class="invite-calling-list">
|
||||
<view wx:for="{{allUsers}}" wx:key="userID" class="invite-calling-item"
|
||||
wx:if="{{index >= pos*4 && index < pos*4+4}}">
|
||||
<view id="{{item.userID}}" class="invite-calling-item-message" wx:if="{{!item.isEnter}}">
|
||||
<view class="invite-calling-item-loadimg">
|
||||
<image src="../../static/loading.png"></image>
|
||||
</view>
|
||||
<image class="avatar" src="{{item.avatar || '../../static/default_avatar.png'}}" binderror="handleErrorImage" />
|
||||
<view class="invite-calling-item-id">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
<view wx:else>
|
||||
<!-- 本地流 -->
|
||||
<view wx:if="{{item.userID===ownUserId}}"
|
||||
class="{{callType === 1 ? 'pusher-audio' : 'play-item'}}" data-screen="pusher"
|
||||
catch:tap="toggleViewSize">
|
||||
<live-pusher class="{{callType === 1 ? 'pusher-audio' : 'pusher-ownvideo'}}"
|
||||
url="{{pusher.url}}" mode="{{pusher.mode}}" autopush="{{true}}"
|
||||
enable-camera="{{pusher.enableCamera}}" enable-mic="{{true}}"
|
||||
muted="{{!pusher.enableMic}}" enable-agc="{{true}}" enable-ans="{{true}}"
|
||||
enable-ear-monitor="{{pusher.enableEarMonitor}}"
|
||||
auto-focus="{{pusher.enableAutoFocus}}" zoom="{{pusher.enableZoom}}"
|
||||
min-bitrate="{{pusher.minBitrate}}" max-bitrate="{{pusher.maxBitrate}}"
|
||||
video-width="{{pusher.videoWidth}}" video-height="{{pusher.videoHeight}}"
|
||||
beauty="{{pusher.beautyLevel}}" whiteness="{{pusher.whitenessLevel}}"
|
||||
orientation="{{pusher.videoOrientation}}" aspect="{{pusher.videoAspect}}"
|
||||
device-position="{{pusher.frontCamera}}"
|
||||
remote-mirror="{{pusher.enableRemoteMirror}}"
|
||||
local-mirror="{{pusher.localMirror}}"
|
||||
background-mute="{{pusher.enableBackgroundMute}}"
|
||||
audio-quality="{{pusher.audioQuality}}"
|
||||
audio-volume-type="{{pusher.audioVolumeType}}"
|
||||
audio-reverb-type="{{pusher.audioReverbType}}"
|
||||
waiting-image="{{pusher.waitingImage}}"
|
||||
beauty-style="{{pusher.beautyStyle}}" filter="{{pusher.filter}}"
|
||||
bindstatechange="pusherStateChangeHandler" bindnetstatus="pusherNetStatus"
|
||||
binderror="pusherErrorHandler"
|
||||
bindaudiovolumenotify="pusherAudioVolumeNotify"/>
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">我</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 远端流 -->
|
||||
<view catch:tap="toggleViewSize" class="{{callType === 1 ? 'pusher-audio' : 'play-item'}}"
|
||||
wx:else>
|
||||
<live-player
|
||||
wx:if="{{playerProcess[item.userID]}}"
|
||||
wx:if="{{ playerProcess[item.userID].hasAudio || playerProcess[item.userID].hasVideo }}"
|
||||
class="{{callType === 1 ? 'pusher-audio' : 'pusher-ownvideo'}}"
|
||||
id="{{playerProcess[item.userID].id}}"
|
||||
data-userid="{{playerProcess[item.userID].userID}}"
|
||||
data-streamid="{{playerProcess[item.userID].streamID}}"
|
||||
data-streamtype="{{playerProcess[item.userID].streamType}}"
|
||||
src="{{playerProcess[item.userID].src}}" mode="RTC"
|
||||
autoplay="{{playerProcess[item.userID].autoplay}}"
|
||||
mute-audio="{{playerProcess[item.userID].muteAudio}}"
|
||||
mute-video="{{playerProcess[item.userID].muteVideo}}"
|
||||
orientation="{{playerProcess[item.userID].orientation}}"
|
||||
object-fit="{{playerProcess[item.userID].objectFit}}"
|
||||
background-mute="{{playerProcess[item.userID].enableBackgroundMute}}"
|
||||
min-cache="{{playerProcess[item.userID].minCache}}"
|
||||
max-cache="{{playerProcess[item.userID].maxCache}}"
|
||||
sound-mode="{{soundMode}}"
|
||||
enable-recv-message="{{playerProcess[item.userID].enableRecvMessage}}"
|
||||
auto-pause-if-navigate="{{playerProcess[item.userID].autoPauseIfNavigate}}"
|
||||
auto-pause-if-open-native="{{playerProcess[item.userID].autoPauseIfOpenNative}}"
|
||||
bindstatechange="playerStateChange"
|
||||
bindfullscreenchange="playerFullscreenChange" bindnetstatus="playNetStatus"
|
||||
bindaudiovolumenotify="playerAudioVolumeNotify"/>
|
||||
<!-- 音量图标 -->
|
||||
<view class="player-control">
|
||||
<image src="{{item.avatar || '../../static/default_avatar.png'}}"></image>
|
||||
<view class="name">{{item.nick || item.userID}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
<!-- 菜单 -->
|
||||
<view class="handle-btns">
|
||||
<view class="other-view {{callType === 1 ? 'black' : 'white'}}">
|
||||
<text>{{pusher.chatTime}}</text>
|
||||
</view>
|
||||
<view class="btn-list">
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="pusherAudioHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableMic? '../../static/audio-true.png': '../../static/audio-false.png'}} "></image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">麦克风</view>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 1}}">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view class="{{callType === 2 ? 'white' : ''}}">挂断</view>
|
||||
</view>
|
||||
<view class="button-container">
|
||||
<view class="btn-normal" bindtap="toggleSoundMode">
|
||||
<image class="btn-image"
|
||||
src="{{soundMode === 'ear' ? '../../static/speaker-false.png': '../../static/speaker-true.png'}} "></image>
|
||||
</view>
|
||||
<text class="{{callType === 2 ? 'white' : ''}}">扬声器</text>
|
||||
</view>
|
||||
<view class="button-container" wx:if="{{callType === 2}}">
|
||||
<view class="btn-normal" bindtap="pusherVideoHandler">
|
||||
<image class="btn-image"
|
||||
src="{{pusher.enableCamera ? '../../static/camera-true.png': '../../static/camera-false.png'}} "></image>
|
||||
</view>
|
||||
<text class="white">摄像头</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-list" wx:if="{{callType===2}}">
|
||||
<view class="btn-list-item other-view">
|
||||
<view class="btn-container">
|
||||
<view class="btn-hangup" bindtap="hangup">
|
||||
<image class="btn-image" src="../../static/hangup.png"></image>
|
||||
</view>
|
||||
<view wx:if="{{pusher.enableCamera}}" class="invite-calling-header-left">
|
||||
<image src="../../static/switch_camera.png" data-device="{{pusher.frontCamera}}"
|
||||
catch:tap="toggleSwitchCamera"/>
|
||||
</view>
|
||||
</view>
|
||||
<text class="white">挂断</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,414 @@
|
||||
/* 全屏设置 */
|
||||
.TUICalling-connected-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.TUICalling-connected-video {
|
||||
width: 100%;
|
||||
height: 180%;
|
||||
}
|
||||
|
||||
/* 本地音频 */
|
||||
.stream-box {
|
||||
float: left;
|
||||
width: 187px;
|
||||
height: 187px;
|
||||
position: absolute;
|
||||
top: 13vh;
|
||||
}
|
||||
|
||||
/* 远端音频列表 */
|
||||
.swiper {
|
||||
margin-top: 107px;
|
||||
min-height: 374px;
|
||||
}
|
||||
|
||||
.invite-calling-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.invite-calling-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-item image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 本地视频 */
|
||||
.play-item {
|
||||
width: 100%;
|
||||
height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pusher-ownvideo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* 远端视频列表 */
|
||||
.swiper-list {
|
||||
min-height: 189px;
|
||||
}
|
||||
|
||||
.player-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.player-item {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
min-height: 187px;
|
||||
}
|
||||
|
||||
/* 音量图标 */
|
||||
.player-control {
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
border-radius: 0 6px 6px 0;
|
||||
color: #fff;
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
max-width: 50%;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.player-control image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.player-control .name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
flex: 1;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* 菜单 */
|
||||
.handle-btns {
|
||||
position: absolute;
|
||||
bottom: 44px;
|
||||
width: 100vw;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.handle-btns .btn-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-normal {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background: white; */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.btn-hangup {
|
||||
width: 8vh;
|
||||
height: 8vh;
|
||||
/*background: #f75c45;*/
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.btn-hangup>.btn-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn-footer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-footer .multi-camera {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-footer .camera {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 107px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-footer .camera .camera-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.audio {
|
||||
padding-top: 15vh;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.pusher-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.player-audio {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.other-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.white {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000000;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-box {
|
||||
margin-top: 100px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
/*
|
||||
.mutil-img {
|
||||
justify-content: flex-start !important;
|
||||
} */
|
||||
|
||||
/* .TRTCCalling-call-audio-img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img > image {
|
||||
width: 25vw;
|
||||
height: 25vw;
|
||||
margin: 0 4vw;
|
||||
border-radius: 4vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-call-audio-img text {
|
||||
font-size: 20px;
|
||||
color: #333333;
|
||||
letter-spacing: 0;
|
||||
font-weight: 500;
|
||||
} */
|
||||
|
||||
.TRTCCalling-calling-list {
|
||||
flex: 0.5;
|
||||
/*设置最小宽度,才会让元素排不下,导致换行排列*/
|
||||
min-width: 50%;
|
||||
min-height: 187px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.TRTCCalling-calling-list image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.TRTCCalling-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-list-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.btn-image-small {
|
||||
transform: scale(.7);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #dddddd;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.invite-calling-header-left {
|
||||
position: absolute;
|
||||
right: -88px;
|
||||
}
|
||||
|
||||
.invite-calling-header-left image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.call-switch .call-operate {
|
||||
width: 4vh;
|
||||
height: 3vh;
|
||||
}
|
||||
|
||||
.call-operate image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.call-switch text {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-operate-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-operate-item text {
|
||||
padding: 8px 0;
|
||||
font-size: 18px;
|
||||
color: #666666;
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.invite-calling-item-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
float: left;
|
||||
background: rgba(0, 0, 0, 0.60);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg {
|
||||
position: absolute;
|
||||
left: calc(50% - 20px);
|
||||
top: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
-webkit-transform: rotate(360deg);
|
||||
animation: rotation 2s linear infinite;
|
||||
-moz-animation: rotation 2s linear infinite;
|
||||
-webkit-animation: rotation 2s linear infinite;
|
||||
-o-animation: rotation 2s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-calling-item-loadimg image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invite-calling-item-id {
|
||||
position: absolute;
|
||||
left: 2%;
|
||||
bottom: 2%;
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MEDIA_TYPE } from '../../utils/constants';
|
||||
Page({
|
||||
TUICallKit: null,
|
||||
data: {
|
||||
config: {
|
||||
},
|
||||
},
|
||||
async onLoad(option) {
|
||||
const config = JSON.parse(option.configData);
|
||||
this.setData(
|
||||
{
|
||||
config: { ...this.data.config, ...config },
|
||||
},
|
||||
async () => {
|
||||
this.TUICallKit = this.selectComponent('#TUICallKit-component');
|
||||
try {
|
||||
await this.TUICallKit.init(config);
|
||||
const event = JSON.parse(option.data);
|
||||
wx.$TUICallEngine.setPusherAttributesHandler({
|
||||
enableCamera: event.data.inviteData.callType === MEDIA_TYPE.VIDEO,
|
||||
});
|
||||
wx.$TUICallEngine.TRTCCallingDelegate.onInvited(event.data);
|
||||
wx.createLivePusherContext().startPreview();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
this.TUICallKit.destroyed();
|
||||
},
|
||||
|
||||
onShow() {},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"TUICallKit": "../../TUICallKit"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<view class="">
|
||||
<TUICallKit
|
||||
id="TUICallKit-component"
|
||||
></TUICallKit>
|
||||
</view>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<tuicallkit ref="TUICallKit"></tuicallkit>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { MEDIA_TYPE } from '../../utils/constants';
|
||||
export default {
|
||||
onLoad(option) {
|
||||
const config = JSON.parse(option.configData);
|
||||
this.$nextTick(async () => {
|
||||
await this.$refs.TUICallKit.init(config);
|
||||
const event = JSON.parse(option.data);
|
||||
wx.$TUICallEngine.setPusherAttributesHandler({
|
||||
enableCamera: event.data.inviteData.callType === MEDIA_TYPE.VIDEO,
|
||||
});
|
||||
wx.$TUICallEngine.TRTCCallingDelegate.onInvited(event.data);
|
||||
wx.createLivePusherContext().startPreview();
|
||||
})
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
onUnload() {
|
||||
this.$refs.TUICallKit.destroyed();
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { PREFIX } from '../TUICallKit';
|
||||
export class BellContext {
|
||||
constructor() {
|
||||
this.context = wx.createInnerAudioContext();
|
||||
this.context.loop = true;
|
||||
this.context.onError((res) => {
|
||||
this.handleError(res.errCode);
|
||||
});
|
||||
}
|
||||
setBellSrc(filePath) {
|
||||
const fs = wx.getFileSystemManager();
|
||||
try {
|
||||
fs.readFileSync(filePath, 'utf8', 0);
|
||||
} catch (e) {
|
||||
console.warn(`${PREFIX}-setCallingBell: 媒体铃声路径错误`, filePath);
|
||||
}
|
||||
this.context.src = filePath;
|
||||
}
|
||||
play() {
|
||||
this.context.play();
|
||||
}
|
||||
stop() {
|
||||
this.context.stop();
|
||||
}
|
||||
handleError(code) {
|
||||
let errMsg = '';
|
||||
switch (code) {
|
||||
case 10001:
|
||||
errMsg = '系统错误';
|
||||
break;
|
||||
case 10002:
|
||||
errMsg = '网络错误';
|
||||
break;
|
||||
case 10003:
|
||||
errMsg = '文件错误';
|
||||
break;
|
||||
case 10004:
|
||||
errMsg = '格式错误';
|
||||
break;
|
||||
default:
|
||||
errMsg = '未知错误';
|
||||
break;
|
||||
}
|
||||
console.warn(`${PREFIX}-setCallingBell: 媒体铃声${errMsg}`);
|
||||
}
|
||||
destroyInstance() {
|
||||
this.context.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import TIM from 'tim-wx-sdk';
|
||||
import TUICallEngine, { EVENT } from 'tuicall-engine-wx';
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
sdkAppID = 0;
|
||||
userID = '';
|
||||
userSig = '';
|
||||
tim = null;
|
||||
globalCallPagePath = '';
|
||||
currentListenStatus = false;
|
||||
async init(params) {
|
||||
if (wx.$globalCallSign) return;
|
||||
const { sdkAppID, SDKAppID, userID, tim, globalCallPagePath, userSig } = params;
|
||||
this.sdkAppID = sdkAppID || SDKAppID;
|
||||
this.userID = userID;
|
||||
this.userSig = userSig;
|
||||
this.globalCallPagePath = globalCallPagePath;
|
||||
this.tim = tim;
|
||||
// 挂载全局变量
|
||||
wx.$globalCallSign = true;
|
||||
// 设置标志位 用于移除监听
|
||||
wx.$CallManagerInstance = this;
|
||||
if (!this.tim) {
|
||||
this.tim = TIM.create({
|
||||
SDKAppID: this.sdkAppID,
|
||||
});
|
||||
}
|
||||
// 创建 TUICallEngine 实例
|
||||
wx.$TUICallEngine = TUICallEngine.createInstance({
|
||||
sdkAppID: this.sdkAppID,
|
||||
tim: this.tim,
|
||||
});
|
||||
// 调用 init 方法
|
||||
try {
|
||||
await wx.$TUICallEngine.init({
|
||||
userID: this.userID,
|
||||
userSig: this.userSig,
|
||||
});
|
||||
// 监听 TUICallEngine 内部的 TSignaling 事件
|
||||
this.addEngineInvite();
|
||||
} catch (error) {
|
||||
wx.$globalCallSign = false;
|
||||
wx.$CallManagerInstance = null;
|
||||
}
|
||||
console.log(`${PREFIX} init Ready!`);
|
||||
};
|
||||
|
||||
addEngineInvite() {
|
||||
if (this.currentListenStatus) return;
|
||||
this.currentListenStatus = true;
|
||||
wx.$TUICallEngine.on(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
};
|
||||
|
||||
addEngineCallEnd() {
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.on(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.on(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
}
|
||||
|
||||
removeEngineInvite() {
|
||||
this.currentListenStatus = false;
|
||||
wx.$TUICallEngine.off(EVENT.INVITED, this.handleNewInvitationReceived, this);
|
||||
this.removeEngineCallEnd();
|
||||
}
|
||||
|
||||
removeEngineCallEnd() {
|
||||
// 若当前已在globalCall页面 则无需处理
|
||||
if (this.getRoute().route === this.globalCallPagePath) {
|
||||
return;
|
||||
}
|
||||
// 通话被取消
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
// 通话结束
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
}
|
||||
|
||||
|
||||
handleNewInvitationReceived(event) {
|
||||
// 若当前已在globalCall页面 则无需处理
|
||||
if (this.getRoute().route === this.globalCallPagePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听 TUICallEngine 自身的通话结束事件
|
||||
this.addEngineCallEnd();
|
||||
const configData = {
|
||||
sdkAppID: this.sdkAppID,
|
||||
userID: this.userID,
|
||||
userSig: this.userSig,
|
||||
};
|
||||
wx.navigateTo({
|
||||
url: `/${this.globalCallPagePath}?data=${JSON.stringify(event)}&configData=${JSON.stringify(configData)}`,
|
||||
});
|
||||
};
|
||||
|
||||
handleCallEnd() {
|
||||
wx.$TUICallEngine._resetTUICallEngine();
|
||||
wx.navigateBack({
|
||||
success: () => {
|
||||
},
|
||||
fail: () => {
|
||||
},
|
||||
complete: () => {
|
||||
wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallEnd, this);
|
||||
wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallEnd, this);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 获取当前的页面地址
|
||||
getRoute() {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
// 卸载 callManger
|
||||
async destroyed() {
|
||||
this.removeEngineInvite();
|
||||
this.reset();
|
||||
await wx.$TUICallEngine.destroyInstance();
|
||||
wx.$globalCallSign = false;
|
||||
wx.$TUICallEngine = null;
|
||||
}
|
||||
reset() {
|
||||
this.sdkAppID = 0;
|
||||
this.userID = '';
|
||||
this.userSig = '';
|
||||
this.tim = null;
|
||||
this.globalCallPagePath = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1017 B |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 799 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 714 B |
@@ -0,0 +1,35 @@
|
||||
// 节流函数
|
||||
export const throttle = function (func, wait) {
|
||||
let previousTime = 0;
|
||||
return function () {
|
||||
const now = Date.now();
|
||||
const context = this;
|
||||
const args = [...arguments];
|
||||
if (now - previousTime > wait) {
|
||||
func.apply(context, args);
|
||||
previousTime = now;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 获取小程序配置
|
||||
export const tabBarConfig = function () {
|
||||
// eslint-disable-next-line no-undef
|
||||
return __wxConfig;
|
||||
};
|
||||
|
||||
// 当前是否是 tabBar 页面
|
||||
export const isTabBarPage = function () {
|
||||
if (!tabBarConfig().tabBar) return false;
|
||||
const tabBar = tabBarConfig().tabBar.list || [];
|
||||
const tabBarList = tabBar.map(obj => obj.pagePath);
|
||||
const currentPage = `${getRoute()}.html`;
|
||||
return tabBarList.indexOf(currentPage) !== -1;
|
||||
};
|
||||
|
||||
// 获取当前的页面地址
|
||||
export const getRoute = function () {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
return currentPage.route;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const THROTTLE_TIME = 500;
|
||||
|
||||
export const MEDIA_TYPE = {
|
||||
AUDIO: 1,
|
||||
VIDEO: 2,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@tencentcloud/call-uikit-wechat",
|
||||
"version": "1.4.4",
|
||||
"description": "TUICallKit miniProgram",
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
"通话",
|
||||
"Call",
|
||||
"视频通话",
|
||||
"音频通话",
|
||||
"音视频"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git"
|
||||
},
|
||||
"author": "Tencent Cloud Client R&D Center",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"tuicall-engine-wx": "^1.4.8"
|
||||
}
|
||||
}
|
||||