TUICallKit.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. import TUICallEngine, { EVENT, MEDIA_TYPE, AUDIO_PLAYBACK_DEVICE, STATUS } from 'tuicall-engine-wx';
  2. import { BellContext } from './serve/bellContext';
  3. import { throttle, isTabBarPage, tabBarConfig } from './utils/commom';
  4. import { THROTTLE_TIME } from './utils/constants';
  5. // 组件旨在跨终端维护一个通话状态管理机,以事件发布机制驱动上层进行管理,并通过API调用进行状态变更。
  6. // 组件设计思路将UI和状态管理分离。您可以通过修改`component`文件夹下的文件,适配您的业务场景,
  7. // 在UI展示上,您可以通过属性的方式,将上层的用户头像,名称等数据传入组件内部,`static`下的icon和默认头像图片,
  8. // 只是为了展示基础的效果,您需要根据业务场景进行修改。
  9. export const PREFIX = 'TUICallKit';
  10. const version = '1.4.4';
  11. let INVITER_BELL_FILEPATH = 'TUICallKit/TUICallKit/static/phone_dialing.mp3';
  12. let INVITEE_BELL_FILEPATH = 'TUICallKit/TUICallKit/static/phone_ringing.mp3';
  13. // 记录组件是否初始化完成
  14. let tuiCallInitReady = false;
  15. // 是否需要隐藏 tabBar 页面
  16. let shouldHideTabBar = false;
  17. Component({
  18. properties: {
  19. config: {
  20. type: Object,
  21. value: {
  22. sdkAppID: 0,
  23. userID: '',
  24. userSig: '',
  25. type: 1,
  26. tim: null,
  27. },
  28. },
  29. backgroundMute: {
  30. type: Boolean,
  31. value: false,
  32. },
  33. },
  34. data: {
  35. callStatus: STATUS.IDLE, // idle、calling、connection
  36. isSponsor: false, // 呼叫者身份 true为呼叫者 false为被叫者
  37. pusher: {}, // TRTC 本地流
  38. playerList: [], // TRTC 远端流
  39. playerProcess: {}, // 经过处理的的远端流(多人通话)
  40. isGroup: false, // 是否为多人通话
  41. remoteUsers: [], // 未排序的通话列表
  42. allUsers: [], // 排序后的通话列表
  43. sponsor: '', // 主叫方
  44. screen: 'pusher', // 视屏通话中,显示大屏幕的流(只限1v1聊天
  45. soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER, // 声音模式 听筒/扬声器
  46. showToatTime: 0, // 弹窗时长
  47. ownUserId: '',
  48. callingBell: null,
  49. bellFilePath: '', // 被叫方铃声地址
  50. },
  51. methods: {
  52. initialUI() {
  53. // 收起键盘
  54. wx.hideKeyboard();
  55. // 隐藏 tabBar
  56. if (shouldHideTabBar) {
  57. wx.hideTabBar();
  58. }
  59. },
  60. // 新的邀请回调事件
  61. async handleNewInvitationReceived(event) {
  62. this.initialUI();
  63. this.checkRunPlatform();
  64. this.data.config.type = event.data.inviteData.callType;
  65. this.data.callingBell.setBellSrc(this.data.bellFilePath || INVITEE_BELL_FILEPATH);
  66. this.data.callingBell.play();
  67. // 判断是否为多人通话
  68. if (event.data.isFromGroup) {
  69. this.setData({
  70. isGroup: true,
  71. sponsor: event.data.sponsor,
  72. });
  73. // 将主叫方和被叫列表合并在一起 组成全部通话人数
  74. const newList = [...event.data.inviteeList, event.data.sponsor];
  75. // 获取用户信息
  76. await this.getUserProfile(newList);
  77. } else {
  78. await this.getUserProfile([event.data.sponsor]);
  79. }
  80. this.setData({
  81. config: this.data.config,
  82. callStatus: STATUS.CALLING,
  83. isSponsor: false,
  84. });
  85. },
  86. // 用户接听
  87. handleUserAccept(event) {
  88. // 主叫方则唤起通话页面
  89. if (this.isSponsor()) {
  90. this.data.callingBell && this.data.callingBell.stop();
  91. this.setData({
  92. callStatus: STATUS.CONNECTED,
  93. });
  94. }
  95. },
  96. // 远端用户进入通话
  97. handleUserEnter(res) {
  98. const newList = this.data.allUsers;
  99. // 多人通话
  100. if (this.data.isGroup) {
  101. // 改变远端用户信息中的isEnter属性
  102. for (let i = 0;i < newList.length;i++) {
  103. if (newList[i].userID === res.data.userID) {
  104. newList[i].isEnter = true;
  105. }
  106. }
  107. }
  108. this.setData({
  109. playerList: res.playerList,
  110. allUsers: newList,
  111. });
  112. },
  113. // 远端用户离开通话
  114. handleUserLeave(res) {
  115. // 多人通话
  116. if (this.data.isGroup) {
  117. wx.showToast({
  118. title: `${this.getUserNickName(res.data.userID)}离开通话`,
  119. });
  120. this.deleteUsers(this.data.allUsers, res.data.userID);
  121. }
  122. this.setData({
  123. playerList: res.data.playerList,
  124. });
  125. },
  126. // 用户数据更新
  127. handleUserUpdate(res) {
  128. this.handleNetStatus(res.data);
  129. const newplayer = {};
  130. const newres = res.data.playerList;
  131. // 多人通话
  132. if (this.data.isGroup) {
  133. // 处理远端流
  134. for (let i = 0;i < newres.length;i++) {
  135. const { userID } = newres[i];
  136. newplayer[userID] = newres[i];
  137. }
  138. };
  139. this.setData({
  140. pusher: res.data.pusher,
  141. playerList: res.data.playerList,
  142. playerProcess: newplayer,
  143. });
  144. },
  145. // 判断网络状态
  146. handleNetStatus(data) {
  147. if (data.pusher.netQualityLevel > 4) {
  148. wx.showToast({
  149. icon: 'none',
  150. title: '您当前网络不佳',
  151. });
  152. }
  153. data.playerList.map((item) => {
  154. if (item.netStatus.netQualityLevel > 4) {
  155. const name = data.playerList.length > 1 ? item.nick : '对方';
  156. wx.showToast({
  157. icon: 'none',
  158. title: `${name}当前网络不佳`,
  159. });
  160. }
  161. return item;
  162. });
  163. },
  164. // 用户拒绝
  165. handleInviteeReject(event) {
  166. if (this.data.isGroup) {
  167. this.deleteUsers(this.data.allUsers, event.data.invitee);
  168. } else {
  169. this.data.callingBell && this.data.callingBell.stop();
  170. }
  171. wx.showToast({
  172. title: `${this.getUserNickName(event.data.invitee)}已拒绝`,
  173. });
  174. },
  175. // 用户不在线
  176. handleNoResponse(event) {
  177. if (this.data.isGroup) {
  178. this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
  179. } else {
  180. this.data.callingBell && this.data.callingBell.stop();
  181. }
  182. wx.showToast({
  183. icon: 'none',
  184. title: `${event.data.timeoutUserList}无应答`,
  185. });
  186. },
  187. // 获取用户昵称
  188. getUserNickName(userId) {
  189. const { remoteUsers } = this.data;
  190. const userObj = remoteUsers.find(item => item.userID === userId);
  191. return userObj.nick ? userObj.nick : userObj.userID;
  192. },
  193. // 用户忙线
  194. handleLineBusy(event) {
  195. if (this.data.isGroup) {
  196. this.deleteUsers(this.data.allUsers, event.data.invitee);
  197. } else {
  198. this.data.callingBell && this.data.callingBell.stop();
  199. }
  200. this.showToast(this.getUserNickName(event.data.invitee));
  201. },
  202. showToast(event) {
  203. this.setData({
  204. showToatTime: this.data.showToatTime + THROTTLE_TIME,
  205. });
  206. setTimeout(() => {
  207. wx.showToast({
  208. title: `${event}忙线中`,
  209. });
  210. }, this.data.showToatTime);
  211. },
  212. // 用户取消
  213. handleCallingCancel(event) {
  214. this.data.callingBell && this.data.callingBell.stop();
  215. if (event.data.invitee !== this.data.config.userID) {
  216. wx.showToast({
  217. title: `${this.getUserNickName(event.data.invitee)}取消通话`,
  218. });
  219. }
  220. this.reset();
  221. },
  222. // 通话超时未应答
  223. handleCallingTimeout(event) {
  224. if (this.data.isGroup) {
  225. // 若是自身未应答 则不弹窗
  226. if (this.data.config.userID === event.data.timeoutUserList[0]) {
  227. this.reset();
  228. return;
  229. }
  230. const newList = this.deleteUsers(this.data.allUsers, event.data.timeoutUserList);
  231. this.setData({
  232. allUsers: newList,
  233. });
  234. } else {
  235. this.data.callingBell && this.data.callingBell.stop();
  236. }
  237. if (this.data.playerList.length === 0) {
  238. this.reset();
  239. }
  240. wx.showToast({
  241. title: `${event.data.timeoutUserList[0]}超时无应答`,
  242. });
  243. },
  244. handleCallingUser(userIDList) {
  245. const remoteUsers = [...this.data.remoteUsers];
  246. const userProfile = remoteUsers.filter(item => userIDList.some(userItem => `${userItem}` === item.userID));
  247. this.setData({
  248. remoteUsers: remoteUsers.filter(item => userIDList.some(userItem => userItem !== item.userID)),
  249. });
  250. let nick = '';
  251. for (let i = 0; i < userProfile.length; i++) {
  252. nick += `${userProfile[i].nick}、`;
  253. }
  254. return nick.slice(0, -1);
  255. },
  256. // 通话结束
  257. handleCallingEnd(event) {
  258. wx.showToast({
  259. title: '通话结束',
  260. duration: 800,
  261. });
  262. this.reset();
  263. },
  264. // SDK Ready 回调
  265. handleSDKReady() {
  266. // 呼叫需在sdk ready之后
  267. },
  268. // 被踢下线
  269. handleKickedOut() {
  270. wx.showToast({
  271. title: '您已被踢下线',
  272. });
  273. },
  274. // 切换通话模式
  275. handleCallMode(event) {
  276. this.data.config.type = event.data.type;
  277. this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
  278. this.setData({
  279. config: this.data.config,
  280. });
  281. },
  282. handleError(event) {
  283. const { code, message } = event.data || {};
  284. console.warn(`${PREFIX} errorCode: ${code}, errorMessage: ${message}`);
  285. },
  286. // 删除用户列表操作
  287. deleteUsers(usersList, userID) {
  288. // 若userID不是数组,则将其转换为数组
  289. if (!Array.isArray(userID)) {
  290. userID = [userID];
  291. }
  292. const list = usersList.filter(item => !userID.includes(item.userID));
  293. this.setData({
  294. allUsers: list,
  295. });
  296. },
  297. // 增加用户列表操作
  298. addUsers(usersList, userID) {
  299. // 若userID不是数组,则将其转换为数组
  300. if (!Array.isArray(userID)) {
  301. userID = [userID];
  302. }
  303. const newList = [...usersList, ...userID];
  304. return newList;
  305. },
  306. // 增加 tsignaling 事件监听
  307. _addTSignalingEvent() {
  308. wx.$TUICallEngine.on(EVENT.ERROR, this.handleError, this);
  309. // 被邀请通话
  310. wx.$TUICallEngine.on(EVENT.INVITED, this.handleNewInvitationReceived, this);
  311. // 用户接听
  312. wx.$TUICallEngine.on(EVENT.USER_ACCEPT, this.handleUserAccept, this);
  313. // 用户进入通话
  314. wx.$TUICallEngine.on(EVENT.USER_ENTER, this.handleUserEnter, this);
  315. // 用户离开通话
  316. wx.$TUICallEngine.on(EVENT.USER_LEAVE, this.handleUserLeave, this);
  317. // 用户更新数据
  318. wx.$TUICallEngine.on(EVENT.USER_UPDATE, this.handleUserUpdate, this);
  319. // 用户拒绝通话
  320. wx.$TUICallEngine.on(EVENT.REJECT, this.handleInviteeReject, this);
  321. // 用户无响应
  322. wx.$TUICallEngine.on(EVENT.NO_RESP, this.handleNoResponse, this);
  323. // 用户忙线
  324. wx.$TUICallEngine.on(EVENT.LINE_BUSY, this.handleLineBusy, this);
  325. // 通话被取消
  326. wx.$TUICallEngine.on(EVENT.CALLING_CANCEL, this.handleCallingCancel, this);
  327. // 通话超时未应答
  328. wx.$TUICallEngine.on(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout, this);
  329. // 通话结束
  330. wx.$TUICallEngine.on(EVENT.CALL_END, this.handleCallingEnd, this);
  331. // SDK Ready 回调
  332. wx.$TUICallEngine.on(EVENT.SDK_READY, this.handleSDKReady, this);
  333. // 被踢下线
  334. wx.$TUICallEngine.on(EVENT.KICKED_OUT, this.handleKickedOut, this);
  335. // 切换通话模式
  336. wx.$TUICallEngine.on(EVENT.CALL_MODE, this.handleCallMode, this);
  337. // 自己发送消息
  338. wx.$TUICallEngine.on(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe, this);
  339. },
  340. // 取消 tsignaling 事件监听
  341. _removeTSignalingEvent() {
  342. wx.$TUICallEngine.off(EVENT.ERROR, this.handleError);
  343. // 被邀请通话
  344. wx.$TUICallEngine.off(EVENT.INVITED, this.handleNewInvitationReceived);
  345. // 用户接听
  346. wx.$TUICallEngine.off(EVENT.USER_ACCEPT, this.handleUserAccept);
  347. // 用户进入通话
  348. wx.$TUICallEngine.off(EVENT.USER_ENTER, this.handleUserEnter);
  349. // 用户离开通话
  350. wx.$TUICallEngine.off(EVENT.USER_LEAVE, this.handleUserLeave);
  351. // 用户更新数据
  352. wx.$TUICallEngine.off(EVENT.USER_UPDATE, this.handleUserUpdate);
  353. // 用户拒绝通话
  354. wx.$TUICallEngine.off(EVENT.REJECT, this.handleInviteeReject);
  355. // 用户无响应
  356. wx.$TUICallEngine.off(EVENT.NO_RESP, this.handleNoResponse);
  357. // 用户忙线
  358. wx.$TUICallEngine.off(EVENT.LINE_BUSY, this.handleLineBusy);
  359. // 通话被取消
  360. wx.$TUICallEngine.off(EVENT.CALLING_CANCEL, this.handleCallingCancel);
  361. // 通话超时未应答
  362. wx.$TUICallEngine.off(EVENT.CALLING_TIMEOUT, this.handleCallingTimeout);
  363. // 通话结束
  364. wx.$TUICallEngine.off(EVENT.CALL_END, this.handleCallingEnd);
  365. // SDK Ready 回调
  366. wx.$TUICallEngine.off(EVENT.SDK_READY, this.handleSDKReady);
  367. // 被踢下线
  368. wx.$TUICallEngine.off(EVENT.KICKED_OUT, this.handleKickedOut);
  369. // 切换通话模式
  370. wx.$TUICallEngine.off(EVENT.CALL_MODE, this.handleCallMode);
  371. // 自己发送消息
  372. wx.$TUICallEngine.off(EVENT.MESSAGE_SENT_BY_ME, this.messageSentByMe);
  373. },
  374. /**
  375. * C2C邀请通话,被邀请方会收到的回调
  376. * 如果当前处于通话中,可以调用该函数以邀请第三方进入通话
  377. * @param {Object} params 呼叫参数
  378. * @param {String} params.userID 被邀请方
  379. * @param {Number} params.type 0-未知,1-语音通话,2-视频通话
  380. * @param {Number} params.roomID 数字房间号, 范围 1~2147483647
  381. * @param {String=} params.userData 扩展字段: 用于在邀请信令中增加扩展信息
  382. */
  383. call: throttle(async function (params) {
  384. this.initialUI();
  385. if (this.data.callStatus !== STATUS.IDLE) {
  386. return;
  387. }
  388. this.checkRunPlatform();
  389. this.data.config.type = params.type;
  390. // 检查设备权限
  391. const deviceMap = {
  392. microphone: true,
  393. camera: params.type === MEDIA_TYPE.VIDEO,
  394. };
  395. const hasDevicePermission = await wx.$TUICallEngine.deviceCheck(deviceMap);
  396. const callStatus = hasDevicePermission ? STATUS.CALLING : STATUS.IDLE;
  397. this.setData({
  398. callStatus,
  399. isSponsor: true,
  400. config: this.data.config,
  401. });
  402. try {
  403. await this.getUserProfile([params.userID]);
  404. } catch (error) {
  405. console.warn(error);
  406. }
  407. wx.$TUICallEngine.call({ userID: params.userID, type: params.type, roomID: params.roomID, userData: params.userData }).then(async (res) => {
  408. if (res) {
  409. this.setData({
  410. callStatus: STATUS.CALLING,
  411. pusher: res.pusher,
  412. });
  413. this.data.callingBell.setBellSrc(INVITER_BELL_FILEPATH);
  414. this.data.callingBell.play();
  415. this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
  416. }
  417. })
  418. .catch((error) => {
  419. if (error.code === -1002) {
  420. wx.showModal({
  421. icon: 'none',
  422. title: 'error',
  423. content: error.message,
  424. showCancel: false,
  425. });
  426. }
  427. this.reset();
  428. throw new Error(error);
  429. });
  430. }, THROTTLE_TIME),
  431. /**
  432. * IM群组邀请通话,被邀请方会收到的回调
  433. * 如果当前处于通话中,可以继续调用该函数继续邀请他人进入通话,同时正在通话的用户会收到的回调
  434. *
  435. * @param {Object} params 呼叫参数
  436. * @param {Array<String>} params.userIDList 邀请列表
  437. * @param {Number} params.type 1-语音通话,2-视频通话
  438. * @param {String} params.groupID IM群组ID
  439. * @param {Number} params.roomID 数字房间号, 范围 1~2147483647
  440. * @param {String=} params.userData 扩展字段: 用于在邀请信令中增加扩展信息
  441. */
  442. groupCall: throttle(async function (params) {
  443. // 判断是否存在groupID
  444. if (!params.groupID) {
  445. wx.showToast({
  446. title: '群ID为空',
  447. });
  448. return;
  449. }
  450. this.initialUI();
  451. if (this.data.callStatus !== STATUS.IDLE) {
  452. return;
  453. }
  454. this.checkRunPlatform();
  455. this.data.config.type = params.type;
  456. // 检查设备权限
  457. const deviceMap = {
  458. microphone: true,
  459. camera: params.type === MEDIA_TYPE.VIDEO,
  460. };
  461. const hasDevicePermission = await wx.$TUICallEngine.deviceCheck(deviceMap);
  462. const callStatus = hasDevicePermission ? STATUS.CALLING : STATUS.IDLE;
  463. this.setData({
  464. callStatus,
  465. isSponsor: true,
  466. isGroup: true,
  467. sponsor: this.data.config.userID,
  468. config: this.data.config,
  469. });
  470. // 将自身的userID插入到邀请列表中,组成完整的用户信息
  471. const list = JSON.parse(JSON.stringify(params.userIDList));
  472. list.unshift(this.data.config.userID);
  473. // 获取用户信息
  474. try {
  475. await this.getUserProfile(list);
  476. } catch (error) {
  477. console.warn(error);
  478. }
  479. wx.$TUICallEngine.groupCall({
  480. userIDList: params.userIDList,
  481. type: params.type,
  482. groupID: params.groupID,
  483. roomID: params.roomID,
  484. userData: params.userData,
  485. }).then(async (res) => {
  486. if (res) {
  487. this.data.config.type = params.type;
  488. this.setData({
  489. pusher: res.pusher,
  490. callStatus: STATUS.CALLING,
  491. });
  492. this.data.callingBell.setBellSrc(INVITER_BELL_FILEPATH);
  493. this.data.callingBell.play();
  494. }
  495. })
  496. .catch((error) => {
  497. if (error.code === -1002) {
  498. wx.showModal({
  499. icon: 'none',
  500. title: 'error',
  501. content: error.message,
  502. showCancel: false,
  503. });
  504. }
  505. this.reset();
  506. throw new Error(error);
  507. });
  508. }, THROTTLE_TIME),
  509. /**
  510. * 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数接听来电
  511. */
  512. accept: throttle(async function () {
  513. wx.$TUICallEngine.accept().then((res) => {
  514. if (res) {
  515. this.data.callingBell && this.data.callingBell.stop();
  516. this.setData({
  517. pusher: res.pusher,
  518. callStatus: STATUS.CONNECTED,
  519. });
  520. // 多人通话需要对自身位置进行修正,将其放到首位
  521. if (this.data.isGroup) {
  522. const newList = this.data.allUsers;
  523. for (let i = 0;i < newList.length;i++) {
  524. if (newList[i].userID === this.data.config.userID) {
  525. newList[i].isEnter = true;
  526. [newList[i], newList[0]] = [newList[0], newList[i]];
  527. }
  528. };
  529. this.setData({
  530. allUsers: newList,
  531. });
  532. }
  533. }
  534. })
  535. .catch((error) => {
  536. console.error(error);
  537. this.reset();
  538. });
  539. }, THROTTLE_TIME),
  540. /**
  541. * 当您作为被邀请方收到的回调时,可以调用该函数拒绝来电
  542. */
  543. reject: throttle(async function () {
  544. wx.$TUICallEngine.reject().then((res) => {
  545. this.data.callingBell && this.data.callingBell.stop();
  546. this.reset();
  547. })
  548. .catch((error) => {
  549. console.error(error);
  550. this.reset();
  551. });
  552. }, THROTTLE_TIME),
  553. messageSentByMe(event) {
  554. const message = event.data.data;
  555. this.triggerEvent('sendMessage', {
  556. message,
  557. });
  558. },
  559. // xml层,是否开启扬声器
  560. setSoundMode(type) {
  561. this.setData({
  562. soundMode: wx.$TUICallEngine.selectAudioPlaybackDevice(type),
  563. });
  564. },
  565. // xml层,挂断
  566. hangup: throttle(async function () {
  567. try {
  568. await wx.$TUICallEngine.hangup();
  569. this.reset();
  570. } catch (error) {
  571. console.error(error);
  572. this.reset();
  573. }
  574. }, THROTTLE_TIME),
  575. // 切换大小屏 (仅支持1v1聊天)
  576. toggleViewSize(event) {
  577. this.setData({
  578. // FIXME _toggleViewSize 不应该为TUICallEngine的方法 后续修改
  579. screen: wx.$TUICallEngine._toggleViewSize(event),
  580. });
  581. },
  582. // 数据重置
  583. reset() {
  584. if (shouldHideTabBar) {
  585. wx.showTabBar();
  586. }
  587. this.setData({
  588. callStatus: STATUS.IDLE,
  589. isSponsor: false,
  590. isGroup: false,
  591. soundMode: AUDIO_PLAYBACK_DEVICE.SPEAKER,
  592. pusher: {}, // TRTC 本地流
  593. playerList: [], // TRTC 远端流
  594. showToatTime: 0,
  595. });
  596. },
  597. // 呼叫中的事件处理
  598. async handleCallingEvent(data) {
  599. const { name, event } = data.detail;
  600. switch (name) {
  601. case 'accept':
  602. this.setSoundMode(this.data.config.type === MEDIA_TYPE.AUDIO ? AUDIO_PLAYBACK_DEVICE.EAR : AUDIO_PLAYBACK_DEVICE.SPEAKER);
  603. this.accept();
  604. break;
  605. case 'hangup':
  606. await this.hangup();
  607. break;
  608. case 'reject':
  609. await this.reject();
  610. break;
  611. case 'toggleSwitchCamera':
  612. wx.$TUICallEngine.switchCamera();
  613. break;
  614. case 'switchAudioCall':
  615. wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
  616. this.data.config.type = MEDIA_TYPE.AUDIO;
  617. this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
  618. this.setData({
  619. config: this.data.config,
  620. });
  621. });
  622. break;
  623. case 'pusherErrorHandler':
  624. this.pusherErrorHandler(event.errMsg);
  625. break;
  626. default:
  627. break;
  628. }
  629. },
  630. // 通话中的事件处理
  631. async handleConnectedEvent(data) {
  632. const { name, event } = data.detail;
  633. switch (name) {
  634. case 'toggleViewSize':
  635. this.toggleViewSize(event);
  636. break;
  637. case 'pusherNetStatus':
  638. wx.$TUICallEngine._pusherNetStatus(event);
  639. break;
  640. case 'playNetStatus':
  641. wx.$TUICallEngine._playNetStatus(event);
  642. break;
  643. case 'pusherStateChangeHandler':
  644. wx.$TUICallEngine._pusherStateChangeHandler(event);
  645. break;
  646. case 'pusherAudioVolumeNotify':
  647. wx.$TUICallEngine._pusherAudioVolumeNotify(event);
  648. break;
  649. case 'playerStateChange':
  650. wx.$TUICallEngine._playerStateChange(event);
  651. break;
  652. case 'playerAudioVolumeNotify':
  653. wx.$TUICallEngine._playerAudioVolumeNotify(event);
  654. break;
  655. case 'pusherAudioHandler':
  656. wx.$TUICallEngine._pusherAudioHandler(event);
  657. break;
  658. case 'hangup':
  659. await this.hangup();
  660. break;
  661. case 'toggleSoundMode':
  662. this.setSoundMode(this.data.soundMode === AUDIO_PLAYBACK_DEVICE.EAR ? AUDIO_PLAYBACK_DEVICE.SPEAKER : AUDIO_PLAYBACK_DEVICE.EAR);
  663. break;
  664. case 'pusherVideoHandler':
  665. wx.$TUICallEngine._pusherVideoHandler(event);
  666. break;
  667. case 'toggleSwitchCamera':
  668. wx.$TUICallEngine.switchCamera(event);
  669. break;
  670. case 'switchAudioCall':
  671. wx.$TUICallEngine.switchCallMediaType(MEDIA_TYPE.AUDIO).then((res) => {
  672. this.data.config.type = MEDIA_TYPE.AUDIO;
  673. this.setData({
  674. config: this.data.config,
  675. });
  676. this.setSoundMode(AUDIO_PLAYBACK_DEVICE.EAR);
  677. });
  678. break;
  679. case 'pusherErrorHandler':
  680. this.pusherErrorHandler(event.errMsg);
  681. break;
  682. default:
  683. break;
  684. }
  685. },
  686. // 处理 pusher 中的异常问题
  687. pusherErrorHandler(code) {
  688. switch (code) {
  689. case 'fail:access denied':
  690. wx.showModal({
  691. icon: 'none',
  692. title: '权限提示',
  693. content: '当前小程序 appid 不具备 <live-pusher> 和 <live-player> 的使用权限,您将无法正常使用实时通话能力,请使用企业小程序账号申请权限后再进行开发体验',
  694. showCancel: false,
  695. });
  696. break;
  697. default:
  698. break;
  699. }
  700. },
  701. // 设置用户的头像、昵称
  702. setSelfInfo(nickName, avatar) {
  703. return wx.$TUICallEngine.setSelfInfo(nickName, avatar);
  704. },
  705. // 获取用户资料
  706. async getUserProfile(userList) {
  707. const imResponse = await this.getTim().getUserProfile({ userIDList: userList });
  708. // 修正用户资料
  709. this.modifyUser(imResponse.data);
  710. },
  711. // 修正用户资料
  712. modifyUser(userIDList) {
  713. const { sponsor } = this.data;
  714. if (this.data.isGroup) {
  715. // 多人通话需要将呼叫者放到第一位 isEnter的作用是区分用户是否进入房间
  716. for (let i = 0;i < userIDList.length;i++) {
  717. // 主叫方的标志位设置成true
  718. if (userIDList[i].userID === sponsor) {
  719. userIDList[i].isEnter = true;
  720. // 对主叫方位置进行修正 将其放到首位
  721. [userIDList[i], userIDList[0]] = [userIDList[0], userIDList[i]];
  722. } else {
  723. // 其他用户默认未进入房间 设置为false
  724. userIDList[i].isEnter = false;
  725. }
  726. }
  727. this.setData({
  728. allUsers: userIDList,
  729. });
  730. }
  731. this.setData({
  732. remoteUsers: userIDList,
  733. });
  734. },
  735. // 获取 tim 实例
  736. getTim() {
  737. return wx.$TUICallEngine.getTim();
  738. },
  739. // 设置日志级别,低于 level 的日志将不会输出
  740. setLogLevel(leave) {
  741. wx.$TUICallEngine.setLogLevel(leave);
  742. },
  743. // 初始化TUICallKit
  744. async init(params) {
  745. if (tuiCallInitReady) return;
  746. tuiCallInitReady = true;
  747. // 关于 tabBar 页面的特化逻辑
  748. this.handleTabBarLogic();
  749. // 兼容从config和init中传值
  750. const { sdkAppID, tim, userID, userSig, SDKAppID } = { ...this.data.config, ...params };
  751. this.setData({
  752. ownUserId: userID,
  753. config: {
  754. ...this.data.config,
  755. sdkAppID: sdkAppID || SDKAppID,
  756. userID,
  757. userSig,
  758. },
  759. });
  760. // 取消全局监听
  761. if (wx.$globalCallSign) {
  762. wx.$CallManagerInstance.removeEngineInvite();
  763. }
  764. this.setUniBellFilePath();
  765. if (!this.data.callingBell) { // 创建铃声实例
  766. const bellContext = new BellContext();
  767. this.setData({
  768. callingBell: bellContext,
  769. });
  770. }
  771. if (!wx.$TUICallEngine) {
  772. wx.$TUICallEngine = TUICallEngine.createInstance({
  773. tim,
  774. sdkAppID: sdkAppID || SDKAppID,
  775. });
  776. /*
  777. _addTSignalingEvent 需要在 init 之前,回调才能正常触发
  778. tap:https://tapd.woa.com/20396022/prong/stories/view/1020396022885781069
  779. */
  780. this._addTSignalingEvent();
  781. try {
  782. await wx.$TUICallEngine.init({
  783. userID,
  784. userSig,
  785. });
  786. } catch (error) {
  787. tuiCallInitReady = false;
  788. throw new Error(error);
  789. }
  790. // 用于全局监听进入 TUICallKit 页面中绑定上当前页面的监听。
  791. } else {
  792. this._addTSignalingEvent();
  793. }
  794. console.log(`${PREFIX} init Ready!`);
  795. },
  796. // 销毁 TUICallEngine
  797. async destroyed() {
  798. if (!wx.$TUICallEngine || !tuiCallInitReady) return;
  799. tuiCallInitReady = false;
  800. this._removeTSignalingEvent();
  801. // 全局监听模式下,离开页面需要重新绑定全局监听,但不卸载 TUICallEngine
  802. if (wx.$globalCallSign) {
  803. // 处理异常退出
  804. if (this.getCallStatus() !== STATUS.IDLE) {
  805. await this.handleExceptionExit();
  806. }
  807. wx.$CallManagerInstance.addEngineInvite();
  808. } else {
  809. // 非全局监听模式下,离开页面需要卸载 TUICallEngine
  810. wx.$TUICallEngine.destroyInstance();
  811. wx.$TUICallEngine = null;
  812. }
  813. if (this.data.callingBell) {
  814. this.data.callingBell.destroyInstance();
  815. this.setData({
  816. callingBell: null,
  817. });
  818. }
  819. this.reset();
  820. console.log(`${PREFIX} destroyed OK!`);
  821. },
  822. /*
  823. 以下两种情况需要全部符合,则需要隐藏 tabBar
  824. 1.用户当前处于 tabBar 页面, isTabBar 为 true
  825. 2.用户没有使用了自定义 tabBar, isCustomTabBar 为 fales
  826. */
  827. handleTabBarLogic() {
  828. const isTabBar = isTabBarPage();
  829. const isCustomTabBar = tabBarConfig().custom || false;
  830. shouldHideTabBar = isTabBar && !isCustomTabBar;
  831. },
  832. // 检测运行时环境
  833. checkRunPlatform() {
  834. const systemInfo = wx.getSystemInfoSync();
  835. if (systemInfo.platform === 'devtools') {
  836. // 当前运行在微信开发者工具里
  837. wx.showModal({
  838. icon: 'none',
  839. title: '运行环境提醒',
  840. content: '微信开发者工具不支持原生推拉流组件(即 <live-pusher> 和 <live-player> 标签),请使用真机调试或者扫码预览。',
  841. showCancel: false,
  842. });
  843. }
  844. },
  845. // 运行在 uniapp 小程序需要修改铃声地址
  846. setUniBellFilePath() {
  847. // 防止用户退出再进入,在地址前重复拼接
  848. if (INVITER_BELL_FILEPATH.indexOf('wxcomponents') !== -1) {
  849. return;
  850. }
  851. // comType 用于 Vue2 的判断,wxcomponents 用于 Vue3 的判断
  852. if (this.dataset.comType === 'wx' || this.is.indexOf('wxcomponents') !== -1) {
  853. INVITER_BELL_FILEPATH = `wxcomponents/${INVITER_BELL_FILEPATH}`;
  854. INVITEE_BELL_FILEPATH = `wxcomponents/${INVITEE_BELL_FILEPATH}`;
  855. }
  856. },
  857. // 自定义铃声
  858. setCallingBell(filePath) {
  859. // 如需恢复默认铃声,filePath 传空即可
  860. this.setData({
  861. bellFilePath: filePath,
  862. });
  863. },
  864. // 处理用户异常退出的情况,处理了右滑退出,以及返回退出的情况。
  865. async handleExceptionExit() {
  866. // 在呼叫状态下,被叫调用 reject,主叫调用 hangup
  867. if (this.getCallStatus() === STATUS.CALLING) {
  868. if (this.isSponsor()) {
  869. await this.hangup();
  870. } else {
  871. await this.reject();
  872. }
  873. }
  874. // 在通话状态下,统一调用 hangup 接口
  875. if (this.getCallStatus() === STATUS.CONNECTED) {
  876. await this.hangup();
  877. }
  878. },
  879. getCallStatus() {
  880. return this.data.callStatus;
  881. },
  882. isSponsor() {
  883. return this.data.isSponsor;
  884. },
  885. },
  886. /**
  887. * 生命周期方法
  888. */
  889. lifetimes: {
  890. created() {
  891. this.reset();
  892. },
  893. attached() {
  894. },
  895. ready() {
  896. console.log(`${PREFIX} SDK Version: ${version}`);
  897. },
  898. async detached() {
  899. await this.destroyed();
  900. },
  901. error() {
  902. },
  903. },
  904. pageLifetimes: {
  905. async show() {
  906. if (this.data.config.sdkAppID) {
  907. await this.init(this.data.config);
  908. }
  909. },
  910. async hide() {
  911. if (this.getCallStatus() === STATUS.IDLE) {
  912. await this.destroyed();
  913. }
  914. },
  915. resize() {
  916. },
  917. },
  918. });