index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. <template>
  2. <view class="web_view_wrap">
  3. <web-view
  4. allow="microphone;camera;midi;encrypted-media;"
  5. :src="hybridUrl"
  6. @message="handleMessage"
  7. >
  8. </web-view>
  9. </view>
  10. </template>
  11. <script>
  12. import IMSDK, { IMMethods, SessionType } from "openim-uniapp-polyfill";
  13. import {
  14. CustomMessageStatus,
  15. CustomType,
  16. GroupMemberListTypes,
  17. PageEvents,
  18. } from "../../../constant";
  19. import {
  20. gotoAppPermissionSetting,
  21. judgeIosPermission,
  22. requestAndroidPermission,
  23. } from "../../../util/permission";
  24. import beCalledRing from "../../../static/audio/beCalled.mp3";
  25. import callingRing from "../../../static/audio/calling.wav";
  26. let currentWebview;
  27. let innerAudioContext;
  28. const RtcMessageTypes = {
  29. Accept: "accept",
  30. Refuse: "refuse",
  31. Cancel: "cancel",
  32. Hung: "hung",
  33. Disconnect: "disconnect",
  34. ConnectFailed: "connectFailed",
  35. Invite: "invite",
  36. };
  37. export default {
  38. data() {
  39. return {
  40. callState: {
  41. isGroup: true,
  42. isVideo: true,
  43. isCalled: true,
  44. },
  45. invitationData: {},
  46. connectData: {},
  47. hybridUrl: "/hybrid/html/h5/index.html",
  48. };
  49. },
  50. onLoad(options) {
  51. const data = JSON.parse(options.invitationData);
  52. this.invitationData = data.invitation;
  53. this.callState.isGroup =
  54. this.invitationData.sessionType !== SessionType.Single;
  55. this.callState.isVideo = this.invitationData.mediaType === "video";
  56. this.callState.isCalled = JSON.parse(options.isCalled);
  57. if (options.connectData) {
  58. this.connectData = JSON.parse(options.connectData);
  59. }
  60. const connectDataStr =
  61. this.callState.isGroup && options.connectData
  62. ? `&connectData=${options.connectData}`
  63. : "";
  64. const participantStr = data.participant
  65. ? `&participantInfo=${JSON.stringify(data.participant)}`
  66. : "";
  67. const invitedMemberStr = options.memberList
  68. ? `&invitedMemberInfo=${options.memberList}`
  69. : "";
  70. this.hybridUrl =
  71. this.hybridUrl +
  72. `?isCalled=${options.isCalled}&isGroup=${
  73. this.callState.isGroup
  74. }&isVideo=${this.callState.isVideo}&totalMember=${
  75. data.invitation.inviteeUserIDList.length + 1
  76. }&statusBarHeight=${uni.getWindowInfo().statusBarHeight}` +
  77. connectDataStr +
  78. participantStr +
  79. invitedMemberStr;
  80. console.log(this.hybridUrl);
  81. this.setRTCListener();
  82. this.backPlayRing(this.callState.isCalled ? beCalledRing : callingRing);
  83. },
  84. onReady() {
  85. this.checkRecordPermission();
  86. setTimeout(() => {
  87. currentWebview = this.$scope.$getAppWebview().children()[0];
  88. currentWebview.setStyle({
  89. top: uni.getWindowInfo().statusBarHeight,
  90. });
  91. });
  92. },
  93. onUnload() {
  94. this.disposeRTCListener();
  95. innerAudioContext?.destroy();
  96. console.log("destroy");
  97. },
  98. methods: {
  99. backPlayRing(audioSrc) {
  100. if (this.callState.isGroup && !this.callState.isCalled) {
  101. return;
  102. }
  103. innerAudioContext = uni.createInnerAudioContext();
  104. innerAudioContext.autoplay = true;
  105. innerAudioContext.src = audioSrc;
  106. innerAudioContext.loop = true;
  107. },
  108. handleMessage({ detail: { data } }) {
  109. const type = data[0].type;
  110. switch (type) {
  111. case RtcMessageTypes.Accept:
  112. this.acceptInvite();
  113. innerAudioContext.stop();
  114. break;
  115. case RtcMessageTypes.Refuse:
  116. this.refuseInvite();
  117. innerAudioContext.stop();
  118. break;
  119. case RtcMessageTypes.Cancel:
  120. this.cancelInvite();
  121. innerAudioContext.stop();
  122. break;
  123. case RtcMessageTypes.Hung:
  124. this.insertRtcMessage(CustomMessageStatus.Success, data[0].duration);
  125. uni.navigateBack();
  126. break;
  127. case RtcMessageTypes.Disconnect:
  128. this.insertRtcMessage(CustomMessageStatus.Success, data[0].duration);
  129. uni.navigateBack();
  130. break;
  131. case RtcMessageTypes.ConnectFailed:
  132. uni.$u.toast("连接失败!");
  133. uni.navigateBack();
  134. break;
  135. case RtcMessageTypes.Invite:
  136. uni.$u.route("/pages_im/pages/conversation/groupMemberList/index", {
  137. type: GroupMemberListTypes.CallInvite,
  138. groupID: this.invitationData.groupID,
  139. callMediaType: this.invitationData.mediaType,
  140. });
  141. break;
  142. default:
  143. break;
  144. }
  145. },
  146. sendRtcInvite(mediaType, userIDList) {
  147. const invitation = {
  148. ...this.invitationData,
  149. inviteeUserIDList: userIDList,
  150. };
  151. IMSDK.asyncApi(IMSDK.IMMethods.SignalingInviteInGroup, IMSDK.uuid(), {
  152. invitation,
  153. })
  154. .then(({ data }) => {
  155. console.log(data);
  156. uni.$u.toast("邀请成功!");
  157. })
  158. .catch((err) => {
  159. console.log(err);
  160. uni.$u.toast("邀请失败!");
  161. });
  162. },
  163. async checkRecordPermission() {
  164. if (uni.$u.os() === "ios") {
  165. const iosFlag = judgeIosPermission("record");
  166. const iosFlag2 = judgeIosPermission("camera");
  167. if (iosFlag === 0 || !iosFlag2) {
  168. uni.$u.toast("您已禁止访问麦克风或摄像头,请前往开启");
  169. setTimeout(() => gotoAppPermissionSetting(), 250);
  170. return false;
  171. }
  172. } else {
  173. const androidFlag = await requestAndroidPermission(
  174. "android.permission.RECORD_AUDIO",
  175. );
  176. const androidFlag2 = await requestAndroidPermission(
  177. "android.permission.CAMERA",
  178. );
  179. if (androidFlag === -1 || androidFlag2 === -1) {
  180. uni.$u.toast("您已禁止访问麦克风或摄像头,请前往开启");
  181. setTimeout(() => gotoAppPermissionSetting(), 250);
  182. return false;
  183. }
  184. }
  185. return true;
  186. },
  187. async acceptInvite() {
  188. const options = {
  189. opUserID: this.$store.getters.storeCurrentUserID,
  190. invitation: this.invitationData,
  191. };
  192. IMSDK.asyncApi(
  193. IMSDK.IMMethods.SignalingAccept,
  194. IMSDK.uuid(),
  195. options,
  196. ).then(({ data }) => {
  197. console.log(data);
  198. console.log(currentWebview);
  199. currentWebview.evalJS(`acceptCallback(${JSON.stringify(data)})`);
  200. });
  201. },
  202. refuseInvite() {
  203. const options = {
  204. opUserID: this.$store.getters.storeCurrentUserID,
  205. invitation: this.invitationData,
  206. };
  207. IMSDK.asyncApi(
  208. IMSDK.IMMethods.SignalingReject,
  209. IMSDK.uuid(),
  210. options,
  211. ).finally(() => {
  212. this.insertRtcMessage(CustomMessageStatus.Refuse);
  213. uni.navigateBack();
  214. });
  215. },
  216. cancelInvite() {
  217. const options = {
  218. opUserID: this.$store.getters.storeCurrentUserID,
  219. invitation: this.invitationData,
  220. };
  221. IMSDK.asyncApi(
  222. IMSDK.IMMethods.SignalingCancel,
  223. IMSDK.uuid(),
  224. options,
  225. ).finally(() => {
  226. this.insertRtcMessage(CustomMessageStatus.Cancel);
  227. uni.navigateBack();
  228. });
  229. },
  230. async insertRtcMessage(status, timeStr) {
  231. const customData = {
  232. customType: CustomType.Call,
  233. data: {
  234. duration: timeStr ?? "",
  235. type: this.callState.isVideo
  236. ? CustomType.VideoCall
  237. : CustomType.VoiceCall,
  238. status,
  239. },
  240. };
  241. const message = await IMSDK.asyncApi(
  242. IMMethods.CreateCustomMessage,
  243. IMSDK.uuid(),
  244. {
  245. data: JSON.stringify(customData),
  246. extension: "",
  247. description: "RTC",
  248. },
  249. );
  250. const sourceID =
  251. this.invitationData.groupID || this.invitationData.inviteeUserIDList[0];
  252. const funcName = this.callState.isGroup
  253. ? IMSDK.IMMethods.InsertGroupMessageToLocalStorage
  254. : IMSDK.IMMethods.InsertSingleMessageToLocalStorage;
  255. IMSDK.asyncApi(funcName, IMSDK.uuid(), {
  256. message,
  257. groupID: sourceID,
  258. sendID: this.invitationData.inviterUserID,
  259. })
  260. .then(({ data }) => {
  261. if (this.inCurrentConversation(data)) {
  262. data.isAppend = true;
  263. this.$store.dispatch("message/pushNewMessage", data);
  264. setTimeout(() => uni.$emit(PageEvents.ScrollToBottom, true));
  265. }
  266. })
  267. .catch((err) => {
  268. console.log(err);
  269. });
  270. },
  271. inCurrentConversation(newServerMsg) {
  272. if (this.callState.isGroup) {
  273. return (
  274. newServerMsg.groupID ===
  275. this.$store.getters.storeCurrentConversation.groupID
  276. );
  277. }
  278. return (
  279. newServerMsg.sendID ===
  280. this.$store.getters.storeCurrentConversation.userID ||
  281. newServerMsg.recvID ===
  282. this.$store.getters.storeCurrentConversation.userID
  283. );
  284. },
  285. acceptHandler() {
  286. console.log("acceptHandler");
  287. innerAudioContext.stop();
  288. currentWebview.evalJS(
  289. `acceptCallback(${JSON.stringify(this.connectData)})`,
  290. );
  291. },
  292. refusedHandler({ data }) {
  293. console.log(data);
  294. uni.$u.toast("对方已拒绝!");
  295. this.insertRtcMessage(CustomMessageStatus.Refused);
  296. uni.navigateBack();
  297. },
  298. accessByOtherDeviceHandler({ data }) {
  299. console.log(data);
  300. uni.$u.toast("已在其他设备上处理!");
  301. this.insertRtcMessage(CustomMessageStatus.AccessByOther);
  302. uni.navigateBack();
  303. },
  304. canceledHandler({ data }) {
  305. console.log(data);
  306. const canceledData = JSON.parse(data);
  307. if (canceledData.opUserID === this.invitationData.inviterUserID) {
  308. uni.$u.toast("对方已取消!");
  309. this.insertRtcMessage(CustomMessageStatus.Canceled);
  310. uni.navigateBack();
  311. }
  312. },
  313. timeOutHandler({ data }) {
  314. console.log(data);
  315. uni.$u.toast("对方未接听!");
  316. this.insertRtcMessage(CustomMessageStatus.Timeout);
  317. uni.navigateBack();
  318. },
  319. setRTCListener() {
  320. IMSDK.subscribe(IMSDK.IMEvents.OnInviteeAccepted, this.acceptHandler);
  321. IMSDK.subscribe(IMSDK.IMEvents.OnInviteeRejected, this.refusedHandler);
  322. IMSDK.subscribe(
  323. IMSDK.IMEvents.OnInviteeAcceptedByOtherDevice,
  324. this.accessByOtherDeviceHandler,
  325. );
  326. IMSDK.subscribe(
  327. IMSDK.IMEvents.OnInviteeRejectedByOtherDevice,
  328. this.accessByOtherDeviceHandler,
  329. );
  330. IMSDK.subscribe(
  331. IMSDK.IMEvents.OnInvitationCancelled,
  332. this.canceledHandler,
  333. );
  334. IMSDK.subscribe(IMSDK.IMEvents.OnInvitationTimeout, this.timeOutHandler);
  335. },
  336. disposeRTCListener() {
  337. IMSDK.unsubscribe(IMSDK.IMEvents.OnInviteeAccepted, this.acceptHandler);
  338. IMSDK.unsubscribe(IMSDK.IMEvents.OnInviteeRejected, this.refusedHandler);
  339. IMSDK.unsubscribe(
  340. IMSDK.IMEvents.OnInviteeAcceptedByOtherDevice,
  341. this.accessByOtherDeviceHandler,
  342. );
  343. IMSDK.unsubscribe(
  344. IMSDK.IMEvents.OnInviteeRejectedByOtherDevice,
  345. this.accessByOtherDeviceHandler,
  346. );
  347. IMSDK.unsubscribe(
  348. IMSDK.IMEvents.OnInvitationCancelled,
  349. this.canceledHandler,
  350. );
  351. IMSDK.unsubscribe(
  352. IMSDK.IMEvents.OnInvitationTimeout,
  353. this.timeOutHandler,
  354. );
  355. },
  356. },
  357. onBackPress(options) {
  358. if (options.from === "navigateBack" || !currentWebview) {
  359. if (currentWebview) {
  360. currentWebview.evalJS(`tryDisconnectAgin()`);
  361. }
  362. return false;
  363. }
  364. currentWebview.evalJS(`backConfirm()`);
  365. return true;
  366. },
  367. };
  368. </script>
  369. <style lang="scss" scoped>
  370. .web_view_wrap {
  371. margin-top: var(--status-bar-height);
  372. }
  373. </style>