iOSVideoCalling.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. <template>
  2. <page-meta :page-font-size="fontValue+'px'" :root-font-size="fontValue+'px'"></page-meta>
  3. <view>
  4. <cu-custom :isBack='true' >
  5. <template v-slot:content>
  6. <text class="text-jiachu text-black">在线通话</text>
  7. </template>
  8. </cu-custom>
  9. <view class="IncolumnC">
  10. <image class="AvatarImg" :src="friendAvatar" mode="scaleToFill"></image>
  11. <text v-if="friend.value" class="text-black text-lg"></text>
  12. <text class="text-black text-lg" style="margin-top: 10rpx;">{{friendName}}</text>
  13. <view class="InrowC" style="margin-top: 80rpx;">
  14. <text class="text-black text-lg">{{stateNote}}</text>
  15. <image style="width: 52rpx;height: 52rpx;margin-left: 6rpx;" src="/static/mine/loading2.gif" mode=""></image>
  16. </view>
  17. <view class="InrowC" style="margin-top: 300rpx;">
  18. <image v-if="!canClose" style="width:110rpx ;height: 110rpx;" src="/static/mine/icon_on.png" mode="scaleToFill" @click="acceptWebrtc"></image>
  19. <image v-if="canClose" style="width:110rpx ;height: 110rpx;" src="/static/mine/icon_off.png" mode="scaleToFill" @click="closeWebrtcBT"></image>
  20. <image v-if="speacker" style="margin-left: 50rpx;width:110rpx ;height: 110rpx;" src="/static/mine/ysq_on.png" mode="scaleToFill" @click="changeSpeack"></image>
  21. <image v-if="!speacker" style="margin-left: 50rpx;width:110rpx ;height: 110rpx;" src="/static/mine/ysq_off.png" mode="scaleToFill" @click="changeSpeack"></image>
  22. </view>
  23. </view>
  24. </view>
  25. </template>
  26. <script setup lang="ts">
  27. import {onLoad,onReady,onShow,onUnload} from '@dcloudio/uni-app';
  28. import {ref,watch} from 'vue'
  29. // import * as ASCIIUtils from '@/uni_modules/wrs-js-modbusCRCHex/js_sdk/wrs-ASCIIUtils.js';
  30. // import * as HexUtils from '@/uni_modules/wrs-js-modbusCRCHex/js_sdk/wrs-HexUtils.js';
  31. import {
  32. UTSWebRTC
  33. } from "@/uni_modules/wrs-uts-webrtc";
  34. import {
  35. UTSVoip,
  36. UTSLocalNotification,
  37. UTSVoipMgr
  38. } from "@/uni_modules/wrs-uts-voip"
  39. import MessageUtils from "@/utils/MessageUtils";
  40. import CuCustom from '@/colorui/components/cu-custom.vue'
  41. import UserApi from "@/api/UserApi";
  42. import {useUserStore} from '@/store/userStore'
  43. import {useWsStore} from "@/store/WsStore";
  44. import {storeToRefs} from "pinia";
  45. import type Webrtc from '@/mode/Webrtc';
  46. import ChatType from "@/utils/ChatType";
  47. import {usePeerStore} from "@/store/peerStore";
  48. import SendCode from '@/utils/SendCode'
  49. import Auth from "@/api/Auth";
  50. const fontValue=ref(Auth.getfontSize());
  51. let webRTC = new UTSWebRTC();
  52. let candidateList=[];
  53. let friendAvatar='/static/logo512.png';
  54. let friendName='';
  55. let stateNote =ref("正在呼叫");
  56. let audioObj=null;
  57. const timeCall=ref(0);
  58. const timer=ref();
  59. const wsStore = useWsStore();
  60. const peerStore = usePeerStore();
  61. const friendId = ref("");
  62. const friend = ref("");
  63. const startTime = ref(0)
  64. const isCaller = ref(false);
  65. const showVideo = ref(false);
  66. const speacker = ref(false);
  67. const canClose = ref(true);
  68. const user = useUserStore().getUser()
  69. const isConnect = ref(false)
  70. const videoIsOpen = ref(false)
  71. const hadClose = ref(false);
  72. const {isClose} = storeToRefs(peerStore)
  73. const {offer} = storeToRefs(peerStore)
  74. const {answer} = storeToRefs(peerStore)
  75. const {candidate} = storeToRefs(peerStore)
  76. const {isAccept} = storeToRefs(peerStore)
  77. watch(isAccept, (newVal) => {
  78. if (newVal) {
  79. timeCall.value=45;
  80. getoffer();
  81. }
  82. })
  83. watch(isClose, (newVal) => {
  84. console.log('isCloseisClose',newVal)
  85. if (newVal) {
  86. uni.navigateBack()
  87. }
  88. })
  89. watch(offer, (newVal) => {
  90. console.log('offer',isCaller);
  91. if (newVal) {//收到answeroffer
  92. recOffer();
  93. }
  94. })
  95. watch(answer, (newVal) => {
  96. if (newVal) {//收到answer
  97. recAnswer();
  98. }
  99. })
  100. watch(candidate, (newVal) => {
  101. if (newVal) {//收到candidate
  102. console.log('candidate');
  103. var list = JSON.parse(newVal);
  104. list.forEach((item) => {
  105. webRTC.addIceCandidate({
  106. sdp: item.candidate,
  107. sdpMLineIndex: item.sdpMLineIndex,
  108. sdpMid: item.sdpMid
  109. }, (resp) => {
  110. console.log("addIceCandidate result:" + JSON.stringify(resp))
  111. })
  112. })
  113. }
  114. sendCandidate();
  115. })
  116. const chaoshipanduan = () => {
  117. timeCall.value=45;
  118. timer.value = setInterval(() => {
  119. timeCall.value=timeCall.value-1;
  120. if(timeCall.value==0&&!isConnect.value){
  121. MessageUtils.message('暂时无法连接,请稍后重试!')
  122. }
  123. else if(isConnect.value){
  124. clearInterval(timer.value);//正常接通
  125. return;
  126. }
  127. else if(timeCall.value<-2&&!isConnect.value){
  128. uni.navigateBack();
  129. }
  130. }, 1000);
  131. }
  132. const audioObjstart = () => {
  133. audioObj=uni.createInnerAudioContext();
  134. audioObj.loop=true;
  135. audioObj.volume=0.2;
  136. audioObj.src='/hybrid/html/images/calling.mp3';
  137. audioObj.play();
  138. setTimeout(function() {
  139. // 这里写要延时执行的代码
  140. if(showVideo.value){
  141. console.log('onReady1');
  142. webRTC.setSpeakerEnable(true)
  143. speacker.value=true;
  144. }
  145. else{
  146. console.log('onReady2');
  147. webRTC.setSpeakerEnable(false)
  148. }
  149. }, 300);
  150. }
  151. const audioObjstop = () => {
  152. if(audioObj){
  153. audioObj.stop();
  154. }
  155. }
  156. onShow(()=>{
  157. // console.log('onShow',videoIsOpen.value)
  158. if(videoIsOpen.value){
  159. //closeWebrtc();
  160. uni.navigateBack();
  161. return;
  162. }
  163. let state = UTSVoipMgr.getApplicationState()
  164. // console.log('getApplicationstate2-------',state);
  165. if(state!=2){
  166. if(uni.getSystemInfoSync().platform == "ios"&&!isCaller.value){
  167. //let provider = new UTSCXProvider()
  168. UTSVoipMgr.endCall({
  169. uuid: uni.getStorageSync("voip_UUID")
  170. }, (resp: any)=>{
  171. console.log(JSON.stringify(resp))
  172. })
  173. }
  174. initData();
  175. getfriendMsg();
  176. audioObjstart();
  177. setTimeout(function() {
  178. // 这里写要延时执行
  179. if(isCaller.value){//主动发起通话
  180. }
  181. else{
  182. acceptWebrtc();//自动接听
  183. }
  184. }, 1500);
  185. }
  186. })
  187. onReady(()=>{
  188. console.log('onReady');
  189. chaoshipanduan();
  190. });
  191. onLoad((opt) => {
  192. console.log('onLoad')
  193. usePeerStore().updateBusyStatus(true)
  194. usePeerStore().updateCloseStatus(false)
  195. if(typeof opt?.isCaller === 'string'){
  196. isCaller.value = opt?.isCaller === 'true'
  197. if(isCaller.value){
  198. canClose.value = true;
  199. }
  200. console.log('isCaller',canClose);
  201. }
  202. if(typeof opt?.showVideo === 'string'){
  203. showVideo.value = opt?.showVideo === 'true'
  204. }
  205. candidateList=[];
  206. friendId.value = opt?.friendId;
  207. });
  208. const initData = () =>{
  209. webRTC.onCallback((resp) => {
  210. let opt = resp.opt
  211. //console.log(opt);
  212. switch (opt) {
  213. case "didChangeIceConnectionState": {
  214. let state = resp.state
  215. console.log(state);
  216. if(state==4||state==5||state==6){
  217. stateNote.value ='连接已断开';
  218. }
  219. if(state==2){
  220. console.log('已连接')
  221. audioObjstop();
  222. isConnect.value = true
  223. startTime.value = new Date().getTime()
  224. stateNote.value ='已连接';
  225. if(showVideo.value&&!videoIsOpen.value){//打开视频
  226. videoIsOpen.value=true;
  227. webRTC.setSpeakerEnable(true)
  228. // uni.navigateTo({
  229. // url:'/imcall/iOSVideoView'
  230. // })
  231. }
  232. }
  233. // 0:RTCIceConnectionStateNew,
  234. // 1: RTCIceConnectionStateChecking,
  235. // 2:RTCIceConnectionStateConnected,
  236. // 3:RTCIceConnectionStateCompleted,
  237. // 4:RTCIceConnectionStateFailed,
  238. // 5:RTCIceConnectionStateDisconnected,
  239. // 6:RTCIceConnectionStateClosed,
  240. // 7: RTCIceConnectionStateCount,
  241. }
  242. break;
  243. case "didChangeIceGatheringState": {
  244. let state = resp.state
  245. // 0: RTCIceGatheringStateNew
  246. // 1: RTCIceGatheringStateGathering
  247. // 2: RTCIceGatheringStateComplete
  248. }
  249. break;
  250. case "didChangeSignalingState": {
  251. let state = resp.state
  252. // 0: RTCSignalingStateStable,
  253. // 1:RTCSignalingStateHaveLocalOffer,
  254. // 2:RTCSignalingStateHaveLocalPrAnswer,
  255. // 3:RTCSignalingStateHaveRemoteOffer,
  256. // 4:RTCSignalingStateHaveRemotePrAnswer,
  257. // 5: RTCSignalingStateClosed,
  258. }
  259. break;
  260. // 收集本机生成的Candidate
  261. case "didGenerateCandidate":
  262. //this.localCandidate = true
  263. let candidate = resp.candidate
  264. //console.log('iosCandidate',candidate)
  265. let data = {
  266. candidate: candidate.sdp,
  267. sdpMLineIndex: candidate.sdpMLineIndex,
  268. sdpMid: candidate.sdpMid,
  269. }
  270. candidateList.push(data);
  271. break;
  272. default:
  273. break;
  274. }
  275. })
  276. let params = {
  277. iceServers:[],
  278. sdpSemantics:1,
  279. continualGatheringPolicy:1,
  280. constraints:{}
  281. }
  282. // iceServer目前支持2种格式,更多格式请联系作者
  283. // 1.
  284. // {
  285. // urls: ["xxx"]
  286. // }
  287. // 2.
  288. // {
  289. // urls: ["xxx"],
  290. // username: "xxx",
  291. // credential: "xx"
  292. // }
  293. params.iceServers = [{
  294. urls: ["stun:203.175.169.52:3478",
  295. "turn:203.175.169.52:3478"
  296. ],
  297. username: 'aaaaa',
  298. credential: 'bbbbb'
  299. }
  300. ]
  301. params.sdpSemantics = 1 // 0: RTCSdpSemanticsPlanB 1:RTCSdpSemanticsUnifiedPlan
  302. params.continualGatheringPolicy =
  303. 1 // 0: RTCContinualGatheringPolicyGatherOnce 1: RTCContinualGatheringPolicyGatherContinually
  304. params.constraints = {
  305. mandatory: {},
  306. optional: {
  307. DtlsSrtpKeyAgreement: "true"
  308. }
  309. }
  310. console.log(params);
  311. let suc = webRTC.peerConnection(params)
  312. console.log(suc);
  313. if (suc) {
  314. // 添加音频
  315. webRTC.addAudioTrack({
  316. trackId: "audio0",
  317. streamIds: ["stream"]
  318. })
  319. // 添加视频
  320. webRTC.addVideoTrack({
  321. trackId: "video0",
  322. streamIds: ["stream"]
  323. })
  324. // 添加数据channel
  325. webRTC.createDataChannel({
  326. label: "WebRTCData"
  327. })
  328. // 配置音频
  329. webRTC.configureAudioSession({
  330. category: "playAndRecord",
  331. mode: "voiceChat"
  332. })
  333. } else {
  334. console.log("peerConnection fail")
  335. }
  336. };
  337. const getfriendMsg = () => {
  338. UserApi.getUser(friendId.value).then((res) => {
  339. friend.value = res.data
  340. if (friend.value) {
  341. friendName=friend.value.name;
  342. friendAvatar=encodeURI(friend.value.avatar);
  343. if(isCaller.value){//主动发起通话
  344. sendcallingMsg();
  345. }
  346. else{
  347. //acceptWebrtc();//自动接听
  348. }
  349. }
  350. })
  351. }
  352. const changeSpeack = () => {
  353. if(speacker.value){
  354. speacker.value=false;
  355. }
  356. else{
  357. speacker.value=true;
  358. }
  359. console.log('setAudioEnabled',speacker.value)
  360. webRTC.setSpeakerEnable(speacker.value)
  361. }
  362. const acceptWebrtc = () => {
  363. console.log('acceptWebrtc');
  364. startTime.value = new Date().getTime()
  365. //给自己发一份忙碌状态,多个客户端同一个人不能同时接受,需要客户端接受后,其他客户端就关闭
  366. let data={
  367. code: SendCode.WEBRTC_BUSY,
  368. message: {
  369. chatId:friendId.value,
  370. fromId: user.id,
  371. type: ChatType.FRIEND,
  372. msgtype:'accept',
  373. video: showVideo.value,
  374. }
  375. }
  376. console.log(data);
  377. useWsStore().send(JSON.stringify(data));
  378. canClose.value = true;
  379. }
  380. const closeWebrtcBT = () => {
  381. uni.navigateBack();
  382. }
  383. const closeWebrtc = () => {
  384. if (user.id) {
  385. const closeMessage = {
  386. code: SendCode.WEBRTC_CLOSE,
  387. message: {
  388. chatId:friendId.value,
  389. fromId: user.id,
  390. timestamp: new Date().getTime(),
  391. type: ChatType.FRIEND,
  392. msgtype:'close'
  393. }
  394. }
  395. wsStore.send(JSON.stringify(closeMessage))
  396. hadClose.value=true;
  397. console.log('发送关闭消息', closeMessage)
  398. //发起方才有发送视频结果消息的权限
  399. if (isCaller.value) {
  400. //界面展示视频消息情况,例如:对方是否接受了视频通话,通话时长
  401. const message= {
  402. id:null,
  403. localtime:null,
  404. mine:true,
  405. messageType:SendCode.WEBRTC_result,
  406. chatId:friendId.value,
  407. fromId: user.id,
  408. timestamp: new Date().getTime(),
  409. type: ChatType.FRIEND,
  410. result: isConnect.value,
  411. video: showVideo.value,
  412. duration: isConnect.value ? new Date().getTime() - startTime.value : 0
  413. }
  414. console.log(message)
  415. wsStore.sendWEBRTCresult(message);
  416. }
  417. }
  418. }
  419. const sendSocketData = (data:any) => {
  420. wsStore.sendWEBRTC(data)
  421. console.log('发送消息', data);
  422. }
  423. const sendCandidate = () =>{
  424. let msg: Webrtc = {
  425. chatId: friendId.value,
  426. fromId: user.id,
  427. type: ChatType.FRIEND,
  428. msgtype:'candidate',
  429. conetType:showVideo.value,
  430. payload:JSON.stringify(candidateList)
  431. }
  432. console.log("将本机candidate发送给对方")
  433. sendSocketData(msg);
  434. }
  435. //主动发起通话操作,isCaller为true-----------------------------
  436. const sendcallingMsg = () => {
  437. console.log('sendcallingMsg');
  438. let msg: Webrtc = {
  439. chatId:friendId.value,
  440. fromId: user.id,
  441. type: ChatType.FRIEND,
  442. msgtype:'calling',
  443. conetType:showVideo.value,
  444. timestamp: new Date().getTime(),
  445. payload:''
  446. }
  447. let data={
  448. code:SendCode.WEBRTC_CALL,
  449. message: msg
  450. }
  451. console.log('sendcallingMsg',data);
  452. useWsStore().send(JSON.stringify(data))
  453. }
  454. const getoffer = () => {
  455. var videoV="false";
  456. if(showVideo.value){
  457. videoV="true";
  458. }
  459. webRTC.offer({
  460. OfferToReceiveAudio: "true",
  461. OfferToReceiveVideo:videoV
  462. }, (resp:any) => {
  463. let flag = resp.flag
  464. if (flag) {
  465. let sdp = resp.sdp
  466. console.log("发送offer sdp",resp)
  467. let type = ""
  468. switch (sdp.type) {
  469. case 0:
  470. type = "offer"
  471. break;
  472. case 1:
  473. type = "prAnswer"
  474. break;
  475. case 2:
  476. type = "answer"
  477. break;
  478. case 3:
  479. type = "rollback"
  480. break;
  481. default:
  482. break;
  483. }
  484. let data = {
  485. type: "SessionDescription",
  486. payload: {
  487. sdp: sdp.sdp,
  488. type: type
  489. }
  490. }
  491. let msg: Webrtc = {
  492. chatId: friendId.value,
  493. fromId: user.id,
  494. type: ChatType.FRIEND,
  495. msgtype:'offer',
  496. conetType:showVideo.value,
  497. payload:JSON.stringify(data.payload)
  498. }
  499. // 发送给接听方
  500. sendSocketData(msg)
  501. } else {
  502. console.log(resp)
  503. }
  504. })
  505. }
  506. onUnload(()=>{
  507. clearInterval(timer.value);//正常接通
  508. audioObjstop();
  509. if(!hadClose.value){
  510. closeWebrtc();
  511. }
  512. webRTC.close();
  513. peerStore.setCallId(undefined);
  514. peerStore.setOffer(undefined);
  515. peerStore.setAnswer(undefined);
  516. peerStore.setCandidate(undefined);
  517. peerStore.updateBusyStatus(false);
  518. peerStore.updateCloseStatus(false);
  519. peerStore.setIsAccept(false);
  520. });
  521. //收到answer
  522. const recAnswer = () => {
  523. let answer=JSON.parse(peerStore.offer)
  524. console.log('answeroffer',answer);
  525. webRTC.setRemoteDescription({
  526. type: 2,
  527. sdp:answer.sdp
  528. }, (resp:any) => {
  529. if(resp.flag){
  530. sendCandidate();
  531. }
  532. console.log("setRemoteDescription result:" + JSON.stringify(resp))
  533. })
  534. }
  535. //被动接听通话操作,isCaller为false-----------------------------
  536. //收到recOffer
  537. const recOffer = () => {
  538. let payload=JSON.parse(peerStore.offer)
  539. console.log('recOffer1',payload);
  540. // let payload2=JSON.parse(payload.payload)
  541. // console.log('recOffer2',payload2);
  542. webRTC.setRemoteDescription({
  543. type: 0,
  544. sdp: payload.sdp
  545. }, (resp:any) => {
  546. console.log("setRemoteDescription result:" + JSON.stringify(resp))
  547. cranswer();
  548. })
  549. }
  550. const cranswer = () => {
  551. var videoV="false";
  552. if(showVideo.value){
  553. videoV="true";
  554. }
  555. webRTC.answer({
  556. OfferToReceiveAudio: "true",
  557. OfferToReceiveVideo:videoV
  558. }, (resp) => {
  559. let flag = resp.flag
  560. if (flag) {
  561. let sdp = resp.sdp
  562. let type = ""
  563. switch (sdp.type) {
  564. case 0:
  565. type = "offer"
  566. break;
  567. case 1:
  568. type = "prAnswer"
  569. break;
  570. case 2:
  571. type = "answer"
  572. break;
  573. case 3:
  574. type = "rollback"
  575. break;
  576. default:
  577. break;
  578. }
  579. let data = {
  580. type: "SessionDescription",
  581. payload: {
  582. sdp: sdp.sdp,
  583. type: type
  584. }
  585. }
  586. // 发送给呼叫方
  587. let msg: Webrtc = {
  588. chatId:friendId.value,
  589. fromId:user.id,
  590. type: ChatType.FRIEND,
  591. msgtype:'answer',
  592. conetType:showVideo.value,
  593. payload:JSON.stringify(data.payload)
  594. }
  595. console.log('answer',msg)
  596. sendSocketData(msg)
  597. } else {
  598. console.log(resp)
  599. }
  600. })
  601. }
  602. </script>
  603. <style>
  604. .AvatarImg{
  605. margin-top: 40rpx;
  606. width: 160rpx;
  607. height: 160rpx;
  608. border-radius: 10rpx;
  609. }
  610. .IncolumnC{
  611. display: flex;
  612. flex-direction: column;
  613. align-items: center;
  614. justify-content:center;
  615. }
  616. .InrowC{
  617. display: flex;
  618. flex-direction:row;
  619. align-items: center;
  620. justify-content:center;
  621. }
  622. </style>