adrVideoCalling.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <template>
  2. <page-meta :page-font-size="fontValue+'px'" :root-font-size="fontValue+'px'"></page-meta>
  3. <view>
  4. <web-view v-if="webviewUrl" @message="handleMessage" :src="webviewUrl"></web-view>
  5. </view>
  6. </template>
  7. <script setup lang="ts">
  8. import {ref, watch} from 'vue'
  9. import {useUserStore} from '@/store/userStore'
  10. import {onLoad,onReady,onUnload} from "@dcloudio/uni-app";
  11. import {useWsStore} from "@/store/WsStore";
  12. import {storeToRefs} from "pinia";
  13. import {usePeerStore} from "@/store/peerStore";
  14. import UserApi from "@/api/UserApi";
  15. import type Webrtc from '@/mode/Webrtc';
  16. import Auth from "@/api/Auth";
  17. const fontValue=ref(Auth.getfontSize());
  18. import MessageUtils from "@/utils/MessageUtils";
  19. import ChatType from "@/utils/ChatType";
  20. import type VideoSendInfo from "@/plugins/video/mode/VideoSendInfo";
  21. import type VideoCallMessage from "@/plugins/video/mode/VideoCallMessage";
  22. import SendCode from "@/utils/SendCode";
  23. import type VideoClose from "@/plugins/video/mode/VideoClose";
  24. import MessageVideoViewPlugin from "@/plugins/video/MessageVideoViewPlugin";
  25. const user = useUserStore().getUser()
  26. const peerStore = usePeerStore()
  27. const friendId = ref("")
  28. const friend = ref("")
  29. const showVideo = ref(false)
  30. const isConnect = ref(false)
  31. const startTime = ref(0)
  32. const isCaller = ref(false);
  33. const hadClose = ref(false);
  34. const timeCall=ref(0);
  35. const timer=ref();
  36. let audioObj=null;
  37. const checkPermission = function (title: string) {
  38. // #ifndef H5
  39. if (uni.getSystemInfoSync().platform !== 'android') {
  40. return new Promise((resolve) => {
  41. resolve(true)
  42. })
  43. }else {
  44. return new Promise((resolve) => {
  45. plus.android.requestPermissions(
  46. ["android.permission.RECORD_AUDIO", "android.permission.CAMERA"],
  47. function (resultObj) {
  48. if (resultObj.granted.length < 2) {
  49. uni.showToast({
  50. icon: "none",
  51. title,
  52. });
  53. resolve(false)
  54. const timer1 = setTimeout(() => { //没有开对应的权限,打开app的系统权限管理页
  55. let Intent = plus.android.importClass("android.content.Intent");
  56. let Settings = plus.android.importClass("android.provider.Settings");
  57. let Uri = plus.android.importClass("android.net.Uri");
  58. let mainActivity = plus.android.runtimeMainActivity();
  59. let intent = new Intent();
  60. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  61. let uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
  62. intent.setData(uri);
  63. mainActivity.startActivity(intent);
  64. clearTimeout(timer1)
  65. }, 1000)
  66. } else {
  67. resolve(true)
  68. }
  69. }
  70. );
  71. })
  72. }
  73. // #endif
  74. // #ifdef H5
  75. return new Promise((resolve) => {
  76. resolve(true)
  77. })
  78. // #endif
  79. }
  80. onLoad((opt) => {
  81. audioObj=uni.createInnerAudioContext();
  82. audioObj.loop=true;
  83. audioObj.src='/hybrid/html/images/calling.mp3';
  84. usePeerStore().updateBusyStatus(true)
  85. friendId.value = opt?.friendId
  86. console.log(opt);
  87. if(typeof opt?.showVideo === 'string'){
  88. showVideo.value = opt?.showVideo === 'true'
  89. }
  90. if(typeof opt?.isCaller === 'string'){
  91. isCaller.value = opt?.isCaller === 'true'
  92. }
  93. peerStore.updateBusyStatus(true)
  94. UserApi.getUser(friendId.value).then((res) => {
  95. friend.value = res.data
  96. if (user && friend.value) {
  97. checkPermission("请开启相机和麦克风权限")
  98. .then((res) => {
  99. peerStore.setCallId(friendId.value)
  100. peerStore.updateCloseStatus(false)
  101. const avatar = encodeURI(friend.value.avatar)
  102. if(showVideo.value){
  103. //转义url
  104. const url = `/hybrid/html/voices/${isCaller.value? 'Video' : 'Videoanswer'}.html?friendName=${friend.value.name}&friendAvatar=${avatar}&showVideo=${showVideo.value}`
  105. webviewUrl.value = encodeURI(url)
  106. }
  107. else{
  108. //转义url
  109. const url = `/hybrid/html/voices/${isCaller.value? 'audio' : 'audioanswer'}.html?friendName=${friend.value.name}&friendAvatar=${avatar}&showVideo=${showVideo.value}`
  110. webviewUrl.value = encodeURI(url)
  111. }
  112. })
  113. }
  114. sendcallingMsg(isCaller.value);
  115. })
  116. })
  117. onReady(()=>{
  118. chaoshipanduan();
  119. audioObj.play();
  120. });
  121. onUnload(()=>{
  122. clearInterval(timer.value);//正常接通
  123. audioObj.stop();
  124. if(!hadClose.value){
  125. sendCloseMsg(isCaller.value);
  126. }
  127. peerStore.setCallId(undefined);
  128. peerStore.setOffer(undefined);
  129. peerStore.setAnswer(undefined);
  130. peerStore.setCandidate(undefined);
  131. peerStore.updateBusyStatus(false);
  132. peerStore.updateCloseStatus(false);
  133. peerStore.setIsAccept(false);
  134. });
  135. const wsStore = useWsStore()
  136. const webviewUrl = ref()
  137. const {isClose} = storeToRefs(peerStore)
  138. const {isAccept} = storeToRefs(peerStore)
  139. const {offer} = storeToRefs(peerStore)
  140. const {candidate} = storeToRefs(peerStore)
  141. var wv=null;
  142. watch(isClose, (newVal) => {
  143. if (newVal) {
  144. console.log(newVal)
  145. uni.navigateBack()
  146. }
  147. })
  148. watch(isAccept, (newVal) => {
  149. console.log('isAccept',newVal)
  150. if (newVal) {
  151. if(!wv){
  152. timeCall.value=45;
  153. var pages = getCurrentPages();
  154. var page = pages[pages.length - 1];
  155. var currentWebview = page.$getAppWebview();
  156. wv = currentWebview.children()[0];
  157. }
  158. wv.evalJS('startWebRct()');
  159. }
  160. })
  161. watch(offer, (newVal) => {
  162. if (newVal) {//收到answeroffer
  163. if(!wv){
  164. var pages = getCurrentPages();
  165. var page = pages[pages.length - 1];
  166. var currentWebview = page.$getAppWebview();
  167. wv = currentWebview.children()[0];
  168. }
  169. wv.evalJS('appAct('+newVal+')');
  170. }
  171. })
  172. watch(candidate, (newVal) => {
  173. if (newVal) {//收到candidate
  174. if(!wv){
  175. var pages = getCurrentPages();
  176. var page = pages[pages.length - 1];
  177. var currentWebview = page.$getAppWebview();
  178. wv = currentWebview.children()[0];
  179. }
  180. console.log('candidate',newVal)
  181. wv.evalJS('appActcandidate('+newVal+')');
  182. }
  183. })
  184. /**
  185. * 发送通话请求
  186. * @param calling
  187. */
  188. const sendcallingMsg = (calling: boolean) => {
  189. console.log('sendcallingMsg',calling);
  190. if(calling){
  191. if (user.id) {
  192. let msg: Webrtc = {
  193. chatId:friendId.value,
  194. fromId: user.id,
  195. type: ChatType.FRIEND,
  196. msgtype:'calling',
  197. conetType:showVideo.value,
  198. timestamp: new Date().getTime(),
  199. payload:''
  200. }
  201. let data={
  202. code:SendCode.WEBRTC_CALL,
  203. message: msg
  204. }
  205. useWsStore().send(JSON.stringify(data))
  206. }
  207. }
  208. }
  209. /**
  210. * 发送关闭
  211. * @param calling
  212. */
  213. const sendCloseMsg = (calling: boolean) => {
  214. if (user.id) {
  215. const closeMessage= {
  216. code:SendCode.WEBRTC_CLOSE,//9
  217. message: {
  218. chatId:friendId.value,
  219. fromId: user.id,
  220. timestamp: new Date().getTime(),
  221. type: ChatType.FRIEND,
  222. msgtype:'close'
  223. }
  224. }
  225. wsStore.send(JSON.stringify(closeMessage))
  226. console.log('发送关闭消息', closeMessage)
  227. //发起方才有发送视频结果消息的权限
  228. if (isCaller.value) {
  229. //界面展示视频消息情况,例如:对方是否接受了视频通话,通话时长
  230. const message= {
  231. id:null,
  232. localtime:null,
  233. mine:true,
  234. messageType:SendCode.WEBRTC_result,
  235. chatId:friendId.value,
  236. fromId: user.id,
  237. timestamp: new Date().getTime(),
  238. type: ChatType.FRIEND,
  239. result: isConnect.value,
  240. video: showVideo.value,
  241. duration: isConnect.value ? new Date().getTime() - startTime.value : 0
  242. }
  243. console.log(message)
  244. wsStore.sendWEBRTCresult(message);
  245. }
  246. hadClose.value=true;
  247. }
  248. }
  249. /**
  250. * 发送信令交互
  251. * @param offer
  252. */
  253. const sendofferMsg = (offer:any,type:string) => {
  254. if (user.id) {
  255. let msg: Webrtc = {
  256. chatId:friendId.value,
  257. fromId: user.id,
  258. type: ChatType.FRIEND,
  259. msgtype:type,
  260. conetType:showVideo.value,
  261. payload:JSON.stringify(offer.data)
  262. }
  263. wsStore.sendWEBRTC(msg)
  264. console.log('sendofferMsg', msg)
  265. }
  266. }
  267. /**
  268. * 发送信令交互
  269. * @param candidate
  270. */
  271. const sendcandidateMsg = (candidate:any) => {
  272. if (user.id) {
  273. let msg: Webrtc = {
  274. chatId:friendId.value,
  275. fromId: user.id,
  276. type: ChatType.FRIEND,
  277. msgtype:'candidate',
  278. conetType:showVideo.value,
  279. payload:JSON.stringify(candidate.data)
  280. }
  281. wsStore.sendWEBRTC(msg)
  282. console.log('candidate', msg)
  283. //获取Webview
  284. var pages = getCurrentPages();
  285. var page = pages[pages.length - 1];
  286. var currentWebview = page.$getAppWebview();
  287. wv = currentWebview.children()[0];
  288. //wv.evalJS('appAct("11")');
  289. }
  290. }
  291. //接受通话请求
  292. const acceptWEBrtc = () =>{
  293. //console.log('acceptWEBrtc',peerStore.offer);
  294. isConnect.value = true
  295. startTime.value = new Date().getTime()
  296. //给自己发一份忙碌状态,多个客户端同一个人不能同时接受,需要客户端接受后,其他客户端就关闭
  297. useWsStore().send(
  298. JSON.stringify({
  299. code: SendCode.WEBRTC_BUSY,
  300. message: {
  301. chatId:friendId.value,
  302. fromId: user.id,
  303. type: ChatType.FRIEND,
  304. msgtype:'accept',
  305. video: showVideo.value,
  306. }
  307. })
  308. )
  309. }
  310. const chaoshipanduan = () => {
  311. timeCall.value=45;
  312. timer.value = setInterval(() => {
  313. timeCall.value=timeCall.value-1;
  314. if(timeCall.value==0&&!isConnect.value){
  315. MessageUtils.message('暂时无法连接,请稍后重试!')
  316. }
  317. else if(isConnect.value){
  318. clearInterval(timer.value);//正常接通
  319. return;
  320. }
  321. else if(timeCall.value<-2&&!isConnect.value){
  322. uni.navigateBack();
  323. }
  324. }, 1000);
  325. }
  326. /**
  327. * 处理消息 接受来自webview的消息
  328. * @param data
  329. */
  330. const handleMessage = (data: any) => {
  331. const message = data.detail.data[0];
  332. console.log('124444',message);
  333. // 创建音频播放器实例
  334. //var player = plus.audio.createPlayer(stream);
  335. switch (message.type) {
  336. case 'ws'://ws消息
  337. useWsStore().send(JSON.stringify(message.data))
  338. break;
  339. case 'changeSpeaker'://ws消息
  340. break;
  341. case 'connect'://ws消息
  342. isConnect.value = true
  343. startTime.value = new Date().getTime()
  344. audioObj.stop();
  345. break;
  346. case 'accept'://接受视频请求
  347. acceptWEBrtc()
  348. audioObj.stop();
  349. break;
  350. case 'close'://关闭
  351. // audioObj.stop();
  352. // sendCloseMsg(isCaller.value)
  353. // isConnect.value = false
  354. uni.navigateBack();
  355. break;
  356. case 'offer'://创建本地offer
  357. sendofferMsg(message,'offer');
  358. break;
  359. case 'answer'://创建本地offer
  360. sendofferMsg(message,'answer');
  361. break;
  362. case 'candidate'://创建本地offer
  363. sendcandidateMsg(message);
  364. break;
  365. }
  366. }
  367. </script>
  368. <style scoped lang="scss">
  369. .chat-container {
  370. display: block;
  371. flex-direction: column;
  372. align-items: center;
  373. justify-content: center;
  374. height: 100vh;
  375. background-color: #000; /* 微信通常使用深色背景 */
  376. position: relative;
  377. }
  378. .main-video {
  379. width: 100%;
  380. height: 100%;
  381. object-fit: cover; /* 确保视频填充整个屏幕 */
  382. position: absolute;
  383. top: 0;
  384. left: 0;
  385. }
  386. .local-video {
  387. width: 50%;
  388. height: 50%;
  389. position: absolute;
  390. top: 50%;
  391. left: 50%;
  392. }
  393. .controls {
  394. position: absolute;
  395. bottom: 50px; /* 将控制按钮放在屏幕底部 */
  396. display: flex;
  397. justify-content: space-around;
  398. width: 100%;
  399. }
  400. button {
  401. border: none;
  402. border-radius: 50%;
  403. background-color: red;
  404. color: white;
  405. font-size: 32upx;
  406. cursor: pointer;
  407. transition: background-color 0.3s;
  408. z-index: 1000000000000000000000000;
  409. width: 20vw;
  410. height: 20vw;
  411. display: flex;
  412. align-items: center;
  413. justify-content: center;
  414. }
  415. button:hover {
  416. background-color: rgba(0, 0, 0, 0.9); /* 鼠标悬停时颜色加深 */
  417. }
  418. button:active {
  419. background-color: rgba(0, 0, 0, 1); /* 点击时颜色更加深 */
  420. }
  421. /* 特定按钮的图标,例如使用Font Awesome或类似图标库 */
  422. .start-call-icon {
  423. /* 添加开始通话图标样式 */
  424. }
  425. .end-call-icon {
  426. /* 添加结束通话图标样式 */
  427. }
  428. </style>