This commit is contained in:
haomingming
2023-03-06 17:57:39 +08:00
parent 3794faceae
commit 1066e37c96
4230 changed files with 193453 additions and 0 deletions
+50
View File
@@ -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**,进行技术交流和反馈~
-
+695
View File
@@ -0,0 +1,695 @@
import TUICallEngine, { EVENT, MEDIA_TYPE, AUDIO_PLAYBACK_DEVICE, STATUS } from 'tuicall-engine-wx';
// 组件旨在跨终端维护一个通话状态管理机,以事件发布机制驱动上层进行管理,并通过API调用进行状态变更。
// 组件设计思路将UI和状态管理分离。您可以通过修改`component`文件夹下的文件,适配您的业务场景,
// 在UI展示上,您可以通过属性的方式,将上层的用户头像,名称等数据传入组件内部,`static`下的icon和默认头像图片,
// 只是为了展示基础的效果,您需要根据业务场景进行修改。
Component({
properties: {
config: {
type: Object,
value: {
sdkAppID: 0,
userID: '',
userSig: '',
type: 1,
tim: null,
},
},
backgroundMute: {
type: Boolean,
value: false,
},
},
observers: {
},
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: '',
},
methods: {
resetUI() {
// 收起键盘
wx.hideKeyboard();
},
// 新的邀请回调事件
handleNewInvitationReceived(event) {
this.resetUI();
this.data.config.type = event.data.inviteData.callType;
// 判断是否为多人通话
if (event.data.isFromGroup) {
this.setData({
isGroup: true,
sponsor: event.data.sponsor,
});
// 将主叫方和被叫列表合并在一起 组成全部通话人数
const newList = [...event.data.inviteeList, event.data.sponsor];
// 获取用户信息
this.getUserProfile(newList);
} else {
this.getUserProfile([event.data.sponsor]);
}
this.setData({
config: this.data.config,
callStatus: STATUS.CALLING,
isSponsor: false,
});
},
// 用户接听
handleUserAccept(event) {
// 主叫方则唤起通话页面
if (this.data.isSponsor) {
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: `${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);
}
wx.showToast({
title: `${event.data.invitee}已拒绝`,
});
},
// 用户不在线
handleNoResponse(event) {
if (this.data.isGroup) {
this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
}
wx.showToast({
icon: 'none',
title: `${event.data.timeoutUserList}无应答`,
});
},
// 用户忙线
handleLineBusy(event) {
if (this.data.isGroup) {
this.deleteUsers(this.data.allUsers, event.data.invitee);
}
this.showToast(event.data.invitee);
},
showToast(event) {
this.setData({
showToatTime: this.data.showToatTime + 500,
});
setTimeout(() => {
wx.showToast({
title: `${event}忙线中`,
});
}, this.data.showToatTime);
},
// 用户取消
handleCallingCancel(event) {
if (event.data.invitee !== this.data.config.userID) {
wx.showToast({
title: `${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,
});
}
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,
});
},
// 删除用户列表操作
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.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.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 userID 被邀请方
* @param type 0-为之, 1-语音通话,2-视频通话
*/
async call(params) {
this.resetUI();
if (this.data.callStatus !== STATUS.IDLE) {
return;
}
await wx.$TUICallEngine.call({ userID: params.userID, type: params.type }).then((res) => {
this.data.config.type = params.type;
this.getUserProfile([params.userID]);
this.setData({
pusher: res.pusher,
config: this.data.config,
callStatus: STATUS.CALLING,
isSponsor: true,
});
this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
});
},
/**
* IM群组邀请通话,被邀请方会收到的回调
* 如果当前处于通话中,可以继续调用该函数继续邀请他人进入通话,同时正在通话的用户会收到的回调
*
* @param userIDList 邀请列表
* @param type 1-语音通话,2-视频通话
* @param groupID IM群组ID
*/
async groupCall(params) {
// 判断是否存在groupID
if (!params.groupID) {
wx.showToast({
title: '群ID为空',
});
return;
}
this.resetUI();
if (this.data.callStatus !== STATUS.IDLE) {
return;
}
wx.$TUICallEngine.groupCall({ userIDList: params.userIDList, type: params.type, groupID: params.groupID }).then((res) => {
this.data.config.type = params.type;
this.setData({
pusher: res.pusher,
config: this.data.config,
callStatus: STATUS.CALLING,
isSponsor: true,
isGroup: true,
sponsor: this.data.config.userID,
});
// 将自身的userID插入到邀请列表中,组成完整的用户信息
const list = JSON.parse(JSON.stringify(params.userIDList));
list.unshift(this.data.config.userID);
// 获取用户信息
this.getUserProfile(list);
});
},
/**
* 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数接听来电
*/
async accept() {
wx.$TUICallEngine.accept().then((res) => {
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) => {
wx.showModal({
icon: 'none',
title: 'error',
content: error.message,
showCancel: false,
});
});
},
/**
* 当您作为被邀请方收到的回调时,可以调用该函数拒绝来电
*/
async reject() {
wx.$TUICallEngine.reject().then((res) => {
this.reset();
});
},
messageSentByMe(event) {
const message = event.data.data;
this.triggerEvent('sendMessage', {
message,
});
},
// xml层,是否开启扬声器
setSoundMode(type) {
this.setData({
soundMode: wx.$TUICallEngine.selectAudioPlaybackDevice(type),
});
},
// xml层,挂断
async _hangUp() {
await wx.$TUICallEngine.hangup();
this.reset();
},
// 切换大小屏 (仅支持1v1聊天)
toggleViewSize(event) {
this.setData({
// FIXME _toggleViewSize 不应该为TUICallEngine的方法 后续修改
screen: wx.$TUICallEngine._toggleViewSize(event),
});
},
// 数据重置
reset() {
this.setData({
callStatus: STATUS.IDLE,
isSponsor: false,
isGroup: false,
soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER,
pusher: {}, // TRTC 本地流
playerList: [], // TRTC 远端流
showToatTime: 0,
});
},
// 呼叫中的事件处理
handleCallingEvent(data) {
const { name } = 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':
this._hangUp();
break;
case 'reject':
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;
default:
break;
}
},
// 通话中的事件处理
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':
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;
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();
},
// 初始化TRTCCalling
async init(params) {
// 兼容从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.$TUICallEngine) {
wx.$TUICallEngine = TUICallEngine.createInstance({
tim,
sdkAppID: sdkAppID || SDKAppID,
});
try {
await wx.$TUICallEngine.init({
userID,
userSig,
});
} catch (error) {
console.error(error);
}
}
this._addTSignalingEvent();
},
// 销毁 TUICallEngine
destroyed() {
if (wx.$TUICallEngine) {
this._removeTSignalingEvent();
}
if (!wx.$globalCallSign) {
wx.$TUICallEngine.destroyInstance();
wx.$TUICallEngine = null;
}
},
},
/**
* 生命周期方法
*/
lifetimes: {
created() {
},
attached() {
},
ready() {
this.reset();
if (wx.$globalCallSign) {
wx.$CallManagerInstance.removeEngineInvite();
}
},
detached() {
this.destroyed();
this.reset();
if (wx.$globalCallSign) {
wx.$CallManagerInstance.addEngineInvite();
}
},
error() {
},
},
pageLifetimes: {
show() {
},
hide() {
},
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,148 @@
// 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: {
isClick: true,
},
/**
* 生命周期方法
*/
lifetimes: {
created() {
},
attached() {
},
ready() {
},
detached() {
},
error() {
},
},
/**
* 组件的方法列表
*/
methods: {
async handleCheckAuthor(e) {
const type =this.data.callType;
wx.getSetting({
success: async (res) => {
const isRecord = res.authSetting['scope.record'];
const isCamera = res.authSetting['scope.camera'];
if (!isRecord && type === 1) {
const title = '麦克风权限授权';
const content = '使用语音通话,需要在设置中对麦克风进行授权允许';
try {
await wx.authorize({ scope: 'scope.record' });
this.accept(e);
} catch (e) {
this.handleShowModal(title, content);
}
return;
}
if ((!isRecord || !isCamera) && type === 2) {
const title = '麦克风、摄像头权限授权';
const content = '使用视频通话,需要在设置中对麦克风、摄像头进行授权允许';
try {
await wx.authorize({ scope: 'scope.record' });
await wx.authorize({ scope: 'scope.camera' });
this.accept(e);
} catch (e) {
this.handleShowModal(title, content);
}
return;
}
this.accept(e);
},
});
},
handleShowModal(title, content) {
wx.showModal({
title,
content,
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting();
}
},
});
},
accept(event) {
this.setData({
isClick: false,
});
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);
},
},
});
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,105 @@
<view class="invite-call" wx:if="{{callType === 2}}">
<live-pusher class="local-video" wx:if="{{remoteUsers.length === 1}}" device-position="{{pusher.frontCamera}}" />
<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" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
<image src="../../static/dialing.png" />
</view>
<text>接听</text>
</view>
</view>
</view>
</view>
</view>
<view class="incoming-call audio-call" 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" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
<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,242 @@
.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;
}
.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,147 @@
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);
},
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,153 @@
// 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,
isClick: true,
},
/**
* 生命周期方法
*/
lifetimes: {
created() {
},
attached() {
},
ready() {
},
detached() {
},
error() {
},
},
/**
* 组件的方法列表
*/
methods: {
async handleCheckAuthor(e) {
const type =this.data.callType;
wx.getSetting({
success: async (res) => {
const isRecord = res.authSetting['scope.record'];
const isCamera = res.authSetting['scope.camera'];
if (!isRecord && type === 1) {
const title = '麦克风权限授权';
const content = '使用语音通话,需要在设置中对麦克风进行授权允许';
try {
await wx.authorize({ scope: 'scope.record' });
this.accept(e);
} catch (e) {
this.handleShowModal(title, content);
}
return;
}
if ((!isRecord || !isCamera) && type === 2) {
const title = '麦克风、摄像头权限授权';
const content = '使用视频通话,需要在设置中对麦克风、摄像头进行授权允许';
try {
await wx.authorize({ scope: 'scope.record' });
await wx.authorize({ scope: 'scope.camera' });
this.accept(e);
} catch (e) {
this.handleShowModal(title, content);
}
return;
}
this.accept(e);
},
});
},
handleShowModal(title, content) {
wx.showModal({
title,
content,
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting();
}
},
});
},
accept(event) {
this.setData({
isClick: false,
});
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);
},
},
});
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
@@ -0,0 +1,137 @@
<view class="trtc-calling-index">
<!-- 视频通话 -->
<view class="invite-call" wx:if="{{callType === 2}}">
<live-pusher class="local-video" wx:if="{{isGroup===false}}" device-position="{{pusher.frontCamera}}" />
<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" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
<image src="../../static/dialing.png" />
</view>
<text style="color: #666666">接听</text>
</view>
</view>
</view>
</view>
</view>
<!-- 语音通话 -->
<view class="incoming-call audio-call" 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" catchtap="{{isClick ? 'handleCheckAuthor' :''}}">
<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>
</view>
@@ -0,0 +1,366 @@
.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,173 @@
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);
},
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,34 @@
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.TRTCCallingDelegate.onInvited(event.data);
} 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,25 @@
<template>
<tuicallkit ref="TUICallKit"></tuicallkit>
</template>
<script>
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.TRTCCallingDelegate.onInvited(event.data);
})
},
created() {
},
onUnload() {
this.$refs.TUICallKit.destroyed();
},
}
</script>
<style>
</style>
@@ -0,0 +1,135 @@
import TIM from 'tim-wx-sdk';
import TUICallEngine, { EVENT } from 'tuicall-engine-wx';
/**
* @param sdkAppID 用户的sdkAppID 必传
* @param userID 用户的userID 必传
* @param userSig 用户的userSig 必传
* @param globalCallPagePath 跳转的路径 必传
* @param tim tim实例 非必传
*/
export class CallManager {
sdkAppID = 0
userID = ''
userSig = ''
tim = null
globalCallPagePath = []
constructor() {
}
async init(params) {
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 方法
await wx.$TUICallEngine.init({
userID: this.userID,
userSig: this.userSig,
});
// 监听 TUICallEngine 内部的 TSignaling 事件
this.addEngineInvite();
};
addEngineInvite() {
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() {
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
destroyed() {
this.removeEngineInvite();
this.reset();
wx.$globalCallSign = false;
wx.$TUICallEngine = null;
}
reset() {
this.sdkAppID = 0;
this.userID = '';
this.userSig = '';
this.tim = null;
this.globalCallPagePath = '';
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

+73
View File
@@ -0,0 +1,73 @@
{
"_from": "@tencentcloud/call-uikit-wechat@^1.1.3",
"_id": "@tencentcloud/call-uikit-wechat@1.3.2",
"_inBundle": false,
"_integrity": "sha512-SbqtN89vDzddepNtIymJhzvJy5zqgrEA0yMDCJBupC4ZeiJi2BSwqXa6XAjq0jck7GSjO4g/LriUukZoM2DrOQ==",
"_location": "/@tencentcloud/call-uikit-wechat",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "@tencentcloud/call-uikit-wechat@^1.1.3",
"name": "@tencentcloud/call-uikit-wechat",
"escapedName": "@tencentcloud%2fcall-uikit-wechat",
"scope": "@tencentcloud",
"rawSpec": "^1.1.3",
"saveSpec": null,
"fetchSpec": "^1.1.3"
},
"_requiredBy": [
"/@tencentcloud/chat-uikit-wechat"
],
"_resolved": "https://registry.npmmirror.com/@tencentcloud/call-uikit-wechat/-/call-uikit-wechat-1.3.2.tgz",
"_shasum": "d2c2174bd36345d9f169505b9d0527d4e2dce136",
"_spec": "@tencentcloud/call-uikit-wechat@^1.1.3",
"_where": "E:\\WeChatProjects\\test_miniprogram\\node_modules\\@tencentcloud\\chat-uikit-wechat",
"author": {
"name": "jonyttang",
"email": "jonyttang@tencent.com"
},
"bugs": {
"url": "https://github.com/tencentyun/TUICallKit/issues"
},
"bundleDependencies": false,
"dependencies": {
"tuicall-engine-wx": "^1.4.1"
},
"deprecated": false,
"description": "An Open-source Voice & Video Calling UI Component Based on Tencent Cloud Service.",
"directories": {
"doc": "docs",
"lib": "lib"
},
"homepage": "https://cloud.tencent.com/document/product/647/78733",
"keywords": [
"uikit",
"call",
"Minipragram",
"tencent",
"chat",
"video",
"audio",
"voice",
"语音",
"视频",
"电话",
"通话"
],
"license": "ISC",
"main": "index.js",
"name": "@tencentcloud/call-uikit-wechat",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tencentyun/TUICallKit.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"version": "1.3.2"
}