index.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. <html lang="zh">
  2. <head>
  3. <meta charset="utf-8" />
  4. <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
  5. <script type="text/javascript" src="./rtc/adapter-latest.js"></script>
  6. <script type="text/javascript" src='./js/uni.webview.js'></script>
  7. <script type="text/javascript" src='./js/utils.js'></script>
  8. <script type="text/javascript" src='./js/jsonly.js'></script>
  9. <style>
  10. body{
  11. padding:0;
  12. margin:0;
  13. background-image: url('image/wallpaper.png');
  14. background-size: contain;
  15. }
  16. .webrtc-box{
  17. background: #666;
  18. border-radius: 6px;
  19. width:100%;
  20. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  21. }
  22. .localvideo{
  23. width:100vw;
  24. height:100vh;
  25. object-fit: cover;
  26. }
  27. .remotevideo{
  28. min-height: 160px;
  29. width: 100px;
  30. position: fixed;
  31. top: 40px;
  32. right: 15px;
  33. z-index:10;
  34. object-fit: cover;
  35. }
  36. .call-user-box{
  37. position:fixed;
  38. bottom: 20px;
  39. width:100%;
  40. }
  41. .call-user{
  42. display: flex;
  43. justify-content: center;
  44. align-items: center;
  45. flex-direction: column;
  46. margin-bottom:50px;
  47. }
  48. .call-user .avatar{
  49. width:60px;
  50. height:60px;
  51. object-fit: contain;
  52. border-radius: 50%;
  53. overflow: hidden;
  54. }
  55. .call-user .text{
  56. font-size:16px;
  57. margin-top:15px;
  58. color:#f6f6f6
  59. }
  60. .call-time{
  61. color:#f6f6f6;
  62. font-size: 24px;
  63. text-align: center;
  64. }
  65. .calling-button{
  66. display: flex;
  67. justify-content: space-around;
  68. padding: 20px;
  69. }
  70. .calling-button .button{
  71. display: flex;
  72. flex-direction: column;
  73. align-items: center;
  74. justify-content: center;
  75. }
  76. .calling-button .button .image{
  77. width:60px;
  78. height:60px;
  79. margin-bottom: 10px;
  80. }
  81. .calling-button .button .text{
  82. color:#f6f6f6;
  83. }
  84. .calling-button .switch-btn .text{
  85. font-size:12px !important;
  86. }
  87. .calling-button .button .image-icon{
  88. width:40px;
  89. height:40px;
  90. margin-bottom: 10px;
  91. }
  92. </style>
  93. </head>
  94. <body>
  95. <div id="app">
  96. <div class="webrtc-box">
  97. <audio id="music1">
  98. <source src="https://im.file.raingad.com/static/voice/calling.mp3">
  99. </audio>
  100. <video v-show="localStream && is_video" class="localvideo" ref="localvideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="displayBtn = !displayBtn" poster="./image/wallpaper.png"></video>
  101. <video v-show="remoteStream && is_video" class="remotevideo" ref="remotevideo" x5-video-player-fullscreen="true" autoplay x5-playsinline playsinline webkit-playsinline @click="changeVideo()" poster="./image/wallpaper.png"></video>
  102. <div class="call-user-box" v-if="displayBtn">
  103. <div class="call-user" v-if="contact">
  104. <img class="avatar" v-if="status!=2 || !is_video" :src="contact.avatar" alt="">
  105. <div class="text">
  106. <b v-if="!is_video && status==2">{{contact.displayName}}</b>
  107. <span v-if="status!=2">
  108. <span v-if="status==3"> {{contact.displayName}} 正在请求与您{{is_video ? '视频' : '语音'}}通话</span>
  109. <span v-else>您正对 <b>{{contact.displayName}}</b> 发起{{is_video ? '视频' : '语音'}}通话</span>
  110. </span>
  111. </div>
  112. </div>
  113. <div class="call-time" v-if="callTime && status==2">
  114. {{setCallTime()}}
  115. </div>
  116. <div class="calling-button">
  117. <div class="button switch-btn" v-if="status<3" >
  118. <img class="image-icon" :src="'./image/voice'+(voiceStatus ? '' : '-off')+'.png'" @click="switchVoice()"/>
  119. <div class="text">{{ voiceStatus ? '关闭' : '开启'}}麦克风</div>
  120. </div>
  121. <div class="button" v-if="status!=0" >
  122. <img class="image" src="./image/guaduan.png" @click="hangup(true)"/>
  123. <div class="text">挂断</div>
  124. </div>
  125. <div class="button" v-if="status==3" >
  126. <img class="image" src="./image/jieting.png" @click="answer()"/>
  127. <div class="text">接听</div>
  128. </div>
  129. <div class="button switch-btn" v-if="status<3" >
  130. <template v-if="is_video">
  131. <img class="image-icon" :src="'./image/video.png'" @click="exchangeVideo()"/>
  132. <div class="text">切换摄像头</div>
  133. </template>
  134. <template v-else>
  135. <img class="image-icon" :src="'./image/speaker'+(speaker ? '' : '-off')+'.png'" @click="speakBtn()"/>
  136. <div class="text">{{ speaker ? '关闭' : '开启'}}扬声器</div>
  137. </template>
  138. </div>
  139. </div>
  140. </div>
  141. </div>
  142. </div>
  143. </body>
  144. <script type="text/javascript" src='./js/vue.js'></script>
  145. <script>
  146. const params=parseUrl(window.location.href);
  147. const opt=JSON.parse(decodeURIComponent(params.stun));
  148. const config = {
  149. 'iceServers': [{
  150. 'urls': ['stun:stun.xten.com', 'stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302',
  151. 'stun:stun2.l.google.com:19302', 'stun:stun3.l.google.com:19302', 'stun:stun4.l.google.com:19302'
  152. ]
  153. },{
  154. 'urls': opt.stun ? opt.stun : ['stun:stun.callwithus.com'], // 自己搭建服务器地址
  155. "username":opt.stunUser ? opt.stunUser : '',
  156. "credential":opt.stunPass ? opt.stunPass : ''
  157. }
  158. ],
  159. };
  160. const Counter = {
  161. data() {
  162. return {
  163. displayBtn:true,
  164. platform:params.platform,
  165. status: 0, //状态0,默认,1:拨号中,2通话中,3来电中,4忙线
  166. pc: null, //pc实力化
  167. localVideo: "", //本地视频的DOM
  168. remoteVideo: "", //远程视频的DOM
  169. remoteStream: null, // 远端视频流
  170. localStream: null, // 本地视频流
  171. is_video: 0, //是否为视频通话
  172. videoStatus: true, //视频开启状态
  173. voiceStatus: true, //语音开启状态
  174. cutdown: 40, //拨号超时
  175. timer: null, //计时器
  176. offerParams:{},
  177. plus:null,
  178. streamType:1, //视频通话展示方式
  179. facingMode:'user',//前置摄像头还是后置摄像头 user-前置 environment-后置
  180. headset : true, //麦克风 打开true 关闭false,
  181. senders: null, // 数据流
  182. speaker:true, // 听筒 false 扬声器true
  183. callTime:0, //通话时间
  184. callTimeDis:'', //通话时间展示
  185. timerIntervalId:null, //通话计时器
  186. contact:{
  187. id:params.target_id,
  188. displayName:params.name,
  189. avatar:params.avatar
  190. }
  191. };
  192. },
  193. mounted() {
  194. this.pc = new RTCPeerConnection(config);
  195. this.pc.ontrack = (event) => {
  196. console.log(event,'接收视频流');
  197. if(this.localVideo){
  198. this.remoteStream = event.streams[0];
  199. setTimeout(()=>{
  200. this.streamType=2;
  201. },50)
  202. }
  203. };
  204. if (this.platform === 'app') {
  205. document.addEventListener('plusready', () => {
  206. console.log('设置扬声器')
  207. this.plus = plus.audio.createPlayer();
  208. this.plus.setRoute(plus.audio.ROUTE_SPEAKER);
  209. });
  210. }
  211. this.localVideo = this.$refs.localvideo;
  212. this.remoteVideo = this.$refs.remotevideo;
  213. window.addEventListener('message', (e) => {
  214. this.callMessagecallback(e)
  215. }, false);
  216. window.getUniAppMessage = (arg) => {
  217. const data = {
  218. data: jsonly(arg)
  219. }
  220. this.callMessagecallback(data)
  221. }
  222. this.is_video = params.type==1 ? true : false;
  223. this.offerParams = this.is_video ? {
  224. offerToRecieveAudio: 1,
  225. offerToRecieveVideo: 1
  226. } : {
  227. offerToRecieveAudio: 1,
  228. offerToRecieveVideo: 0
  229. }
  230. this.status=params.status
  231. // 如果状态为1,表示拨打电话,并且calling状态为1的时候才是直接拨打;
  232. if(this.status==1){
  233. if(params.calling==1){
  234. this.called(this.is_video)
  235. }
  236. }else{
  237. this.playMusicCall('state');
  238. }
  239. },
  240. watch:{
  241. streamType(val){
  242. // 切换镜头位置
  243. if(val==1){
  244. this.localVideo.srcObject = this.localStream;
  245. this.remoteVideo.srcObject = this.remoteStream;
  246. this.localVideo.muted=true;
  247. this.remoteVideo.muted=false;
  248. }else{
  249. this.localVideo.srcObject = this.remoteStream;
  250. this.remoteVideo.srcObject = this.localStream;
  251. this.localVideo.muted=false;
  252. this.remoteVideo.muted=true;
  253. }
  254. }
  255. },
  256. methods: {
  257. // 开始通话计时
  258. startTime() {
  259. this.timerIntervalId=setInterval(()=>{
  260. this.callTime++
  261. },1000)
  262. },
  263. // 设置通话时间
  264. setCallTime(){
  265. let time=this.callTime;
  266. const hours = Math.floor(time / 3600);
  267. const minutes = Math.floor((time - (hours * 3600)) / 60);
  268. const seconds = time - (hours * 3600) - (minutes * 60);
  269. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  270. },
  271. // 视频电话初始化本地视频
  272. initLocalStream(call_id, is_video) {
  273. let video=is_video;
  274. if(is_video){
  275. video = {
  276. width: window.screen.height,
  277. height: window.screen.width
  278. }
  279. }
  280. navigator.mediaDevices.getUserMedia({
  281. video: video,
  282. audio: {echoCancellation: true}
  283. }).then((stream) => {
  284. this.localStream = stream;
  285. // 同步音频
  286. stream.getTracks().forEach((track) => {
  287. this.pc.addTrack(track, stream);
  288. });
  289. this.localVideo.srcObject = this.localStream;
  290. // 把自己的视频静音
  291. this.localVideo.muted = true;
  292. if(call_id){
  293. this.postMsg({
  294. event:'calling',
  295. status:3,
  296. code:901
  297. });
  298. // 计时器,如果一段时间没有接听则自动挂断
  299. this.timer = setInterval(() => {
  300. this.cutdown--;
  301. if (this.cutdown == 0) {
  302. this.hangup(true);
  303. }
  304. }, 1000)
  305. }else{
  306. // 告诉对方已经接听电话
  307. this.postMsg({ event: 'acceptRtc',code:904});
  308. }
  309. // 监听远程媒体流
  310. }).catch((e) => {
  311. this.postMsg({
  312. event: 'mediaDevices',
  313. })
  314. });
  315. },
  316. // 拨打电话
  317. called(is_video) {
  318. this.is_video = is_video;
  319. this.initLocalStream(true, is_video);
  320. this.playMusicCall('state');
  321. },
  322. // 接听电话
  323. answer() {
  324. this.status = 2;
  325. this.initLocalStream(false, this.is_video);
  326. this.playMusicCall('close');
  327. this.startTime();
  328. },
  329. // 挂断电话
  330. hangup(btn) {
  331. clearInterval(this.timer);
  332. clearInterval(this.timerIntervalId);
  333. if(this.status!=2){
  334. this.playMusicCall('close');
  335. }
  336. if (this.status) {
  337. this.closeLocalMedia(); //关闭本地媒体
  338. this.remoteStream=null; //关闭远程媒体
  339. }
  340. // 通话取消
  341. let code=902;
  342. // 通话中挂断
  343. if(this.status==2 ){
  344. code=906
  345. // 拒绝挂断
  346. }else if(this.status==3 ){
  347. code=903
  348. //对方忙线中
  349. }else if(this.status==4 ){
  350. code=907
  351. }
  352. this.postMsg({
  353. event:'hangup',
  354. isbtn:btn,
  355. callTime:this.callTime,
  356. code:code
  357. })
  358. },
  359. // 关闭本地媒体
  360. closeLocalMedia() {
  361. if (this.localStream && this.localStream.getTracks()) {
  362. this.localStream.getTracks().forEach((track) => {
  363. track.stop();
  364. });
  365. }
  366. this.localStream = null;
  367. },
  368. // 打开或关闭声音
  369. switchVoice() {
  370. if (this.localStream == null) {
  371. alert('请打开音视频');
  372. return false;
  373. }
  374. const tracks = this.localStream.getTracks();
  375. if (this.voiceStatus) {
  376. tracks.forEach(track => {
  377. if (track.kind === 'audio') {
  378. track.enabled = false
  379. }
  380. });
  381. this.voiceStatus = false;
  382. } else {
  383. tracks.forEach(track => {
  384. if (track.kind === 'audio') {
  385. track.enabled = true
  386. }
  387. });
  388. this.voiceStatus = true;
  389. }
  390. },
  391. // 临时开、关视频
  392. switchVideo() {
  393. if (this.localStream == null) {
  394. alert('请打开音视频');
  395. return false;
  396. }
  397. const tracks = this.localStream.getTracks();
  398. if (this.videoStatus) {
  399. tracks.forEach(track => {
  400. if (track.kind === 'video') {
  401. track.enabled = false
  402. }
  403. });
  404. this.videoStatus = false;
  405. } else {
  406. tracks.forEach(track => {
  407. if (track.kind === 'video') {
  408. track.enabled = true
  409. }
  410. });
  411. this.videoStatus = true;
  412. }
  413. },
  414. // 切换前后摄像头
  415. exchangeVideo() {
  416. this.localStream.getTracks().forEach(track => track.stop());
  417. if (this.facingMode == 'user') this.facingMode = 'environment'
  418. else this.facingMode = 'user'
  419. navigator.mediaDevices.getUserMedia({
  420. video: {
  421. width: window.screen.height,
  422. height: window.screen.width,
  423. facingMode: {
  424. exact: this.facingMode
  425. }
  426. },
  427. audio: {
  428. echoCancellation: true,
  429. }
  430. }).then((mediastream) => {
  431. this.senders = this.pc.getSenders()
  432. let videoTrack = mediastream.getVideoTracks()[0];
  433. let audioTrack = mediastream.getAudioTracks()[0];
  434. var sender = this.senders.find((s) => {
  435. return s.track.kind == 'video';
  436. });
  437. var sender2 = this.senders.find((s) => {
  438. return s.track.kind == 'audio';
  439. });
  440. sender.replaceTrack(videoTrack);
  441. sender2.replaceTrack(audioTrack);
  442. if (this.streamType === 2) this.remoteVideo.srcObject = mediastream;
  443. else this.localVideo.srcObject = mediastream
  444. this.localStream = mediastream
  445. if(this.voiceStatus==false){
  446. this.voiceStatus=true;
  447. this.switchVoice();
  448. }
  449. if(this.speaker){
  450. this.speaker = !this.speaker
  451. }else{
  452. this.speaker = !this.speaker
  453. }
  454. })
  455. },
  456. // 播放响铃
  457. playMusicCall(type) {
  458. var audio = document.getElementById("music1");
  459. if(type=='close' && !audio.paused){
  460. audio.pause(); // 暂停
  461. return;
  462. }
  463. if (type === "state") {
  464. audio.loop = true;
  465. } else {
  466. audio.loop = false;
  467. }
  468. if (audio.paused) {
  469. audio.play(); // 播放
  470. } else {
  471. audio.pause(); // 暂停
  472. }
  473. },
  474. // 向uniapp发送消息,页面通讯
  475. postMsg(data) {
  476. if (this.platform === 'app') {
  477. uni.postMessage({
  478. data: data
  479. })
  480. } else {
  481. window.parent.postMessage(data)
  482. }
  483. },
  484. // 接收websocket发送过来的消息,由uniapp接收后传输到当前页面
  485. callMessagecallback(msg){
  486. let e=msg.data;
  487. switch (e.event) {
  488. case "calling":
  489. console.log('发起通话...');
  490. this.called(this.is_video);
  491. break;
  492. case "hangup":
  493. this.hangup(false);
  494. break;
  495. case "busy":
  496. this.status=4;
  497. this.hangup(false);
  498. break;
  499. case "acceptRtc": //已经接听,创建offer并发送
  500. this.status = 2;
  501. clearInterval(this.timer);
  502. this.startTime();
  503. this.playMusicCall();
  504. this.createOffer()
  505. break;
  506. case "turndown":
  507. break;
  508. case "answer":
  509. //同步answer信息...
  510. this.pc.setRemoteDescription(new RTCSessionDescription({
  511. type: 'answer',
  512. sdp: e.sdp
  513. }));
  514. break;
  515. case "iceCandidate":
  516. setTimeout(()=>{
  517. // 添加ice完成通话连接
  518. if (typeof(e.iceCandidate) === 'object') {
  519. this.pc.addIceCandidate(new RTCIceCandidate(e.iceCandidate));
  520. } else {
  521. this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(e.iceCandidate)));
  522. }
  523. },100)
  524. break;
  525. case "offer":
  526. this.pc.setRemoteDescription(new RTCSessionDescription({
  527. type: 'offer',
  528. sdp: e.sdp
  529. }));
  530. this.createAnswer();
  531. break;
  532. }
  533. },
  534. // 创建offer-sdp
  535. createOffer() {
  536. this.pc.createOffer(this.offerParams).then((offer) => {
  537. this.pc.setLocalDescription(offer);
  538. this.postMsg({
  539. event: 'offer',
  540. sdp: offer.sdp
  541. }, '*');
  542. });
  543. // 创建offer需要监听ice流
  544. this.onicecandidate();
  545. },
  546. // 创建应答sdp
  547. createAnswer() {
  548. this.pc.createAnswer(this.offerParams).then((answer) => {
  549. this.pc.setLocalDescription(answer);
  550. this.postMsg({
  551. event: 'answer',
  552. sdp: answer.sdp
  553. }, '*');
  554. this.onicecandidate();
  555. });
  556. },
  557. onicecandidate(){
  558. this.pc.onicecandidate = (event) => {
  559. var iceCandidate = event.candidate;
  560. if (iceCandidate) {
  561. this.postMsg({
  562. event: 'iceCandidate',
  563. iceCandidate: JSON.parse(JSON.stringify(iceCandidate))
  564. }, '*');
  565. }
  566. };
  567. },
  568. //切换视频显示位置
  569. changeVideo(){
  570. this.streamType==1 ? this.streamType=2 : this.streamType=1;
  571. },
  572. //打开关闭扬声器 h5端就是静音 ROUTE_EARPIECE 听筒 ROUTE_SPEAKER 扬声器
  573. speakBtn() {
  574. if (this.speaker) { //扬声器 => 听筒
  575. this.speaker = !this.speaker
  576. if (this.platform === 'h5') {
  577. this.localVideo.muted = true
  578. }
  579. if (this.platform === 'app') {
  580. this.plus.setRoute(plus.audio.ROUTE_EARPIECE);
  581. }
  582. } else { //听筒 => 扬声器
  583. this.speaker = !this.speaker
  584. if (this.platform === 'h5') {
  585. this.localVideo.muted = false
  586. }
  587. if (this.platform === 'app') {
  588. this.plus.setRoute(plus.audio.ROUTE_SPEAKER);
  589. }
  590. }
  591. }
  592. }
  593. }
  594. const app = Vue.createApp(Counter);
  595. app.mount('#app');
  596. </script>
  597. </html>