@zhangxuejing123./sip-phone-sdk 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +24 -0
  2. package/babel.config.js +5 -0
  3. package/dist/demo.html +21 -0
  4. package/dist/media/outgoing-call2.b8558579.mp3 +0 -0
  5. package/dist/sip-phone-sdk.common.js +42439 -0
  6. package/dist/sip-phone-sdk.css +5 -0
  7. package/dist/sip-phone-sdk.umd.js +42449 -0
  8. package/dist/sip-phone-sdk.umd.min.js +14 -0
  9. package/jsconfig.json +19 -0
  10. package/package-lock.json +15671 -0
  11. package/package.json +99 -0
  12. package/public/SIP_MIX_WEB.js +50 -0
  13. package/public/en.js +24 -0
  14. package/public/favicon.ico +0 -0
  15. package/public/index.html +32 -0
  16. package/public/index.test.html +31 -0
  17. package/public/zh.js +24 -0
  18. package/src/.DS_Store +0 -0
  19. package/src/App.vue +136 -0
  20. package/src/assets/.DS_Store +0 -0
  21. package/src/assets/adudio-open.png +0 -0
  22. package/src/assets/audio-close.png +0 -0
  23. package/src/assets/call-down.png +0 -0
  24. package/src/assets/camera-close.png +0 -0
  25. package/src/assets/camera-open.png +0 -0
  26. package/src/assets/icon_Recording_Fill_Red_Active.svg +15 -0
  27. package/src/assets/icon_Recording_Fill_Red_Inactive.svg +15 -0
  28. package/src/assets/img_Avatar_User.png +0 -0
  29. package/src/assets/normal-logo.png +0 -0
  30. package/src/assets/outgoing-call2.mp3 +0 -0
  31. package/src/components/DialPanelMini.vue +179 -0
  32. package/src/components/MobilePhone copy 2.vue +1173 -0
  33. package/src/components/MobilePhone copy.vue +1046 -0
  34. package/src/components/MobilePhone.vue +1157 -0
  35. package/src/components/index.js +36 -0
  36. package/src/components/vuetify.css +29663 -0
  37. package/src/index.js +100 -0
  38. package/src/lang/en.js +24 -0
  39. package/src/lang/zh.js +24 -0
  40. package/src/libs/SIP_MIX_WEB.js +50 -0
  41. package/src/libs/en.js +24 -0
  42. package/src/libs/tool.js +312 -0
  43. package/src/libs/zh.js +24 -0
  44. package/src/main.js +24 -0
  45. package/src/plugins/vuetify.js +11 -0
  46. package/src/utils/rem.js +22 -0
  47. package/vue.config copy 2.js +39 -0
  48. package/vue.config copy.js +11 -0
  49. package/vue.config.js +39 -0
@@ -0,0 +1,1157 @@
1
+ <!-- 主模板:视频通话界面 -->
2
+ <template>
3
+ <!-- 主容器:使用flex垂直布局,内容居中对齐 -->
4
+ <div class="d-flex flex-column align-center justify-space-between sip-call-main">
5
+ <!-- 位置信息显示区域 -->
6
+ <div class="location-wrapper" v-if="showLocation">
7
+ <!-- 显示经纬度和海拔信息 -->
8
+ <p>LAT: {{latitude}}</p>
9
+ <p>LNG: {{longitude}}</p>
10
+ <p>ALT: {{altitude}}</p>
11
+ </div>
12
+ <!-- 通话信息显示区域 -->
13
+ <div class="d-flex flex-column align-center call-btn" v-if="!miniMode && !showDialPad">
14
+ <!-- 应用logo -->
15
+ <img class="elevation-4 call-img" alt="logo" src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/normal-logo.png"/>
16
+ <!-- 通话状态提示 -->
17
+ <p class="mt-6 mb-0 call-label" >{{callPrompt}}</p>
18
+ </div>
19
+ <!-- 功能按钮区域 -->
20
+ <div class="pr-10 pl-10 btn-group" v-if="!miniMode && !showDialPad">
21
+ <v-row>
22
+ <v-col cols="12" class="text-center">
23
+ <!-- 通话计时器 -->
24
+ <div
25
+ v-show="isCalling && showHangup" class="call-duration">
26
+ 视频时长 {{duration}}
27
+ </div>
28
+ </v-col>
29
+ </v-row>
30
+ <!-- 第一行按钮组 -->
31
+ <v-row class="d-flex justify-space-around mb-10">
32
+ <!-- 取消按钮 - 用于微信公众号调用的场景 -->
33
+ <v-btn
34
+ class="cancel-btn"
35
+ style=" "
36
+ v-show="!isCalling && !needDial"
37
+ @click="onHangupBtnClick"
38
+ >
39
+ <img class="cancel-img" src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/call-down.png" alt="hangup" >
40
+ <span class="cancel-label">取消</span>
41
+ </v-btn>
42
+ <v-btn
43
+ large
44
+ v-show="isCalling && showHangup"
45
+ @click="onMuteBtnClick('audio')"
46
+ class="custom-mute-btn"
47
+ >
48
+ <img class="img-open-close" v-if="!isAudioMute" src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/adudio-open.png" >
49
+ <img class="img-open-close" v-else src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/adudio-close.png" >
50
+ </v-btn>
51
+ <!-- 摄像头开关按钮 -->
52
+ <v-btn
53
+ large
54
+ v-show="isCalling && showHangup"
55
+ @click="onMuteBtnClick('video')"
56
+ class="custom-mute-btn"
57
+ >
58
+ <img class="img-open-close" v-if="!isVideoMute" src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/camera-open.png" alt="mic-on">
59
+ <img class="img-open-close" v-else src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/camera-close.png" alt="mic-off">
60
+ </v-btn>
61
+
62
+ </v-row>
63
+
64
+ <!-- 第二行按钮组 -->
65
+ <v-row class="d-flex justify-center mt-10" style="position: relative; width: 100%;">
66
+ <!-- 挂断按钮 -->
67
+ <v-btn
68
+ style="opacity: 1; background: transparent; box-shadow: none; position: relative;"
69
+ v-show="isCalling && showHangup"
70
+ @click="onHangupBtnClick"
71
+ >
72
+ <img class="hangup-img" src="https://kefu-gm-jc-dat.boc-samsunglife.cn/static/call-down.png" alt="hangup">
73
+ <span class="hangup-label">挂断</span>
74
+ </v-btn>
75
+ <!-- 切换摄像头按钮 -->
76
+ <v-btn
77
+ v-show="isCalling && showHangup"
78
+ @click="onSwitchCameraBtnClick"
79
+ class="transparent-btn"
80
+ >
81
+ <v-icon size="30">mdi-camera-flip</v-icon>
82
+ </v-btn>
83
+ </v-row>
84
+ </div>
85
+ <!-- 视频容器区域 -->
86
+ <v-expand-transition>
87
+ <div class="sip-call-wrapper" v-show="showVideoCt" @click="onSipCallClick">
88
+ <!-- 远程视频窗口 -->
89
+ <vue-draggable-resizable
90
+ v-show="remoteVideo.show"
91
+ :w="remoteVideo.w"
92
+ :h="remoteVideo.h"
93
+ :x="remoteVideo.x"
94
+ :y="remoteVideo.y"
95
+ :z="remoteVideo.z"
96
+ :draggable="remoteVideo.draggable"
97
+ :handles="[]"
98
+ :parent="true"
99
+ :active="false"
100
+ class-name="elevation-6 video-panel"
101
+ ref="remoteVideoWrapper"
102
+ @mousedown.native="onLocalVideoDragCallback($event, 'remoteVideo')"
103
+ >
104
+ <v-btn v-if="miniMode" class="video-normal-mode" dark text x-small width="24" height="24" min-width="24" @click="onChangeVideoNormalModeBtnClick">
105
+ <v-icon>mdi-arrow-bottom-right-bold-box</v-icon>
106
+ </v-btn>
107
+ <video
108
+ v-show="remoteVideo.show"
109
+ autoplay="autoplay"
110
+ webkit-playsinline="true"
111
+ playsinline="true"
112
+ x-webkit-airplay="true"
113
+ x5-video-player-type="h5"
114
+ x5-video-orientation="h5"
115
+ ref="remoteVideo"
116
+ class="sip-call-video"
117
+ style="object-fit: contain;"
118
+ controls
119
+ ></video>
120
+ </vue-draggable-resizable>
121
+ <!-- 本地视频窗口 -->
122
+ <vue-draggable-resizable
123
+ v-show="localVideo.show"
124
+ :w="localVideo.w"
125
+ :h="localVideo.h"
126
+ :x="localVideo.x"
127
+ :y="localVideo.y"
128
+ :z="localVideo.z"
129
+ :draggable="localVideo.draggable"
130
+ :handles="[]"
131
+ :parent="true"
132
+ :active="false"
133
+ class-name="elevation-6 video-panel"
134
+ ref="localVideoWrapper"
135
+ @mousedown.native="onLocalVideoDragCallback($event, 'localVideo')"
136
+ >
137
+ <video
138
+ v-show="localVideo.show"
139
+ autoplay="autoplay"
140
+ muted
141
+ webkit-playsinline="true"
142
+ playsinline="true"
143
+ x-webkit-airplay="true"
144
+ x5-video-player-type="h5"
145
+ x5-video-orientation="h5"
146
+ ref="localVideo"
147
+ class="sip-call-video"
148
+ controls
149
+ ></video>
150
+ </vue-draggable-resizable>
151
+ </div>
152
+ </v-expand-transition>
153
+ <!-- 拨号盘组件 -->
154
+ <v-expand-transition>
155
+ <dial-panel-mini
156
+ :isCalling="isCalling"
157
+ v-show="showDialPad"
158
+ @makeCall="onDialPadMakeCallBtnClick"
159
+ @close="showDialPad=false"
160
+ @dtmf="onDialPadDtmfBtnClick"
161
+ @hangup="onHangupBtnClick"
162
+ ref="dialPanelMini"
163
+ />
164
+ </v-expand-transition>
165
+ <!-- 音频播放器(用于播放提示音) -->
166
+ <v-bottom-sheet v-model="showAudioPlayer" hide-overlay>
167
+ <audio :src="require('@/assets/outgoing-call2.mp3')" loop ref="audioPlayer"></audio>
168
+ </v-bottom-sheet>
169
+ <!-- 提示信息弹窗 -->
170
+ <v-snackbar v-model="showSnackbar" :timeout="2000">
171
+ {{snackbarText}}
172
+ </v-snackbar>
173
+ </div>
174
+ </template>
175
+
176
+ <script>
177
+ import 'webrtc-adapter'
178
+ import SipMix from '@/libs/SIP_MIX_WEB'
179
+ import DialPanelMini from '@/components/DialPanelMini'
180
+ import VueDraggableResizable from 'vue-draggable-resizable'
181
+ import { closeStream } from '@/libs/tool'
182
+
183
+
184
+ export default {
185
+ name: 'MobilePhone',
186
+ api: null,
187
+ locateHandler: -1,
188
+ sendPositionHandler: -1,
189
+ clientHeight: 0,
190
+ clientWidth: 0,
191
+
192
+ components: {
193
+ VueDraggableResizable,
194
+ DialPanelMini
195
+ },
196
+
197
+ props: {
198
+ calledNum: { // 被叫
199
+ type: String,
200
+ default: ''
201
+ },
202
+
203
+ callingNum: { // 主叫
204
+ type: String,
205
+ default: 'H5User',
206
+ validator: (v) => !!v
207
+ },
208
+
209
+ callType: {
210
+ type: String,
211
+ default: 'videoOnly',
212
+ validator: v => ['audio', 'video', 'videoOnly'].indexOf(v) > -1
213
+ },
214
+
215
+ debug: {
216
+ type: Boolean,
217
+ default: false
218
+ },
219
+
220
+ needDial: {
221
+ type: Boolean,
222
+ default: false
223
+ },
224
+
225
+ autoHideLocalVideo: {
226
+ type: Boolean,
227
+ default: false
228
+ },
229
+
230
+ showLocation: {
231
+ type: Boolean,
232
+ default: true
233
+ },
234
+
235
+ callData: {
236
+ type: String,
237
+ default: ''
238
+ }
239
+ },
240
+
241
+ data: function () {
242
+ return {
243
+ prompt: `${this.$t('serverConnecting')}...`,
244
+ callPrompt: this.$t('pleaseBePatient'),
245
+
246
+ isCalling: false,
247
+ showHangup: false,
248
+ showAudioPlayer: true,
249
+ showVideoCt: false,
250
+ showDialPad: false,
251
+ showSnackbar: false,
252
+
253
+ miniMode: false,
254
+ dialNum: '',
255
+
256
+ controls: '',
257
+
258
+ isAudioMute: false,
259
+ isVideoMute: false,
260
+ cameraType: 'front',
261
+ videoInputList: [],
262
+ videoInputIndex: 0,
263
+
264
+ localVideo: {
265
+ w: 1,
266
+ h: 1,
267
+ x: 0,
268
+ y: 0,
269
+ z: 1000,
270
+ show: false,
271
+ draggable: true
272
+ },
273
+
274
+ remoteVideo: {
275
+ w: 1,
276
+ h: 1,
277
+ x: 0,
278
+ y: 0,
279
+ z: 1000,
280
+ show: false,
281
+ draggable: true
282
+ },
283
+
284
+ connected: false,
285
+ callDuration: 0,
286
+
287
+ durationHandler: -1,
288
+ snackbarText: '',
289
+
290
+ callId: '',
291
+ lastCallId: '',
292
+ queueCount: 0,
293
+
294
+ latitude: 0,
295
+ longitude: 0,
296
+ altitude: 0
297
+ }
298
+ },
299
+
300
+ watch: {
301
+ isCalling (is) {
302
+ if (is) {
303
+ this.startDurationCount()
304
+ } else {
305
+ this.stopDurationCount()
306
+ }
307
+ },
308
+ callId (id) {
309
+ this.lastCallId = id || this.lastCallId
310
+ },
311
+ prompt (v) {
312
+ console.info('Prompt', v)
313
+ }
314
+ },
315
+
316
+ computed: {
317
+ duration () {
318
+ let s = this.callDuration % 60
319
+ s = s < 10 ? `0${s}` : `${s}`
320
+ let m = parseInt(this.callDuration / 60)
321
+ m = m < 10 ? `0${m}` : `${m}`
322
+ return `${m}:${s}`
323
+ }
324
+ },
325
+
326
+ created() {
327
+ setTimeout(() => {
328
+ // this.onMakeCallBtnClick();
329
+ // 处理微信公众号传递的参数
330
+ // this.handleWechatParams();
331
+ }, 2000);
332
+ },
333
+
334
+ methods: {
335
+ // 处理微信公众号参数
336
+ handleWechatParams(val) {
337
+ // ===== 测试代码开始 =====
338
+ // 测试用的URL参数字符串 - 模拟微信公众号传递的参数
339
+ // const testParams = 'debug=true&calledNum=0000021000021001&av=video&callData={"calledNum":"0000021000021001","videoRecordId":"ae756872ff3a122817848719605aa5cb"}';
340
+ // // 解析测试参数
341
+ // const urlParams = new URLSearchParams(testParams);
342
+ // // 获取参数值
343
+ // const calledNum = urlParams.get('calledNum');
344
+ // const callDataStr = urlParams.get('callData');
345
+ console.log('测试模式:使用模拟参数');
346
+
347
+ // ===== 原始代码(已注释) ===== // 用于微信公众号 此方式是获取url参数;父子组件传值可以使用props
348
+ // const urlParams = new URLSearchParams(window.location.search);
349
+ // const calledNum = urlParams.get('calledNum');
350
+ // const callDataStr = urlParams.get('callData');
351
+ // const urlParams = val;
352
+ const calledNum = val.calledNum;
353
+ const callDataStr = val.callData;
354
+
355
+ console.log('测试模式:使用模拟参数');
356
+
357
+ if (calledNum) {
358
+ // 存储被叫号码
359
+ this.setDialNum(calledNum);
360
+
361
+ // 如果有callData,解析并存储
362
+ let callData = null;
363
+ try {
364
+ callData = callDataStr ? JSON.parse(decodeURIComponent(callDataStr)) : null;
365
+ // 这里可以将callData存储到Vuex或组件的data中
366
+ if (callData) {
367
+ // 例如:this.$store.commit('setCallData', callData);
368
+ console.log('Call data received:', callData);
369
+ }
370
+ } catch (e) {
371
+ console.error('Failed to parse callData:', e);
372
+ }
373
+
374
+ // 自动发起呼叫
375
+ this.$nextTick(() => {
376
+ this.onMakeCallBtnClick();
377
+ });
378
+ }
379
+ },
380
+
381
+ _resetCmpWHLRTB ({rl =0, rt = 0, ll = 0, lt = 0} = {}) {
382
+ this.$refs.remoteVideoWrapper.left = rl
383
+ this.$refs.remoteVideoWrapper.top = rt
384
+
385
+ this.$refs.localVideoWrapper.left = ll
386
+ this.$refs.localVideoWrapper.top = lt
387
+
388
+ this.$refs.localVideoWrapper.parentWidth = this.clientWidth
389
+ this.$refs.localVideoWrapper.parentHeight = this.clientHeight
390
+ this.$refs.localVideoWrapper.right = this.clientWidth - this.localVideo.w
391
+ this.$refs.localVideoWrapper.bottom = this.clientHeight - this.localVideo.h
392
+
393
+ this.$refs.remoteVideoWrapper.parentWidth = this.clientWidth
394
+ this.$refs.remoteVideoWrapper.parentHeight = this.clientHeight
395
+ this.$refs.remoteVideoWrapper.right = this.clientWidth - this.remoteVideo.w
396
+ this.$refs.remoteVideoWrapper.bottom = this.clientHeight - this.remoteVideo.h
397
+ },
398
+
399
+ toMiniMode () {
400
+ this._resetCmpWHLRTB()
401
+ Object.assign(this.localVideo, {
402
+ show: false,
403
+ draggable: false
404
+ })
405
+ this.miniMode = true
406
+
407
+ setTimeout(() => {
408
+ Object.assign(this.remoteVideo, {
409
+ show: true,
410
+ w: parseInt(this.clientWidth / 3),
411
+ h: parseInt(this.clientHeight / 3),
412
+ x: 0,
413
+ y: 0,
414
+ z: 1100,
415
+ draggable: true
416
+ })
417
+ }, 500)
418
+ },
419
+
420
+ toNormalMode () {
421
+ this._resetCmpWHLRTB()
422
+ Object.assign(this.localVideo, {
423
+ show: true,
424
+ w: parseInt(this.clientWidth / 3),
425
+ h: parseInt(this.clientHeight / 3),
426
+ x: 0,
427
+ y: 0,
428
+ z: 1100,
429
+ draggable: false
430
+ })
431
+ this.miniMode = false
432
+
433
+ setTimeout(() => {
434
+ // this._resetCmpWHLRTB()
435
+ Object.assign(this.remoteVideo, {
436
+ show: true,
437
+ w: this.clientWidth,
438
+ h: this.clientHeight,
439
+ x: 0,
440
+ y: 0,
441
+ z: 1000,
442
+ draggable: true
443
+ })
444
+ }, 500)
445
+ },
446
+
447
+ onDialPadBtnClick () {
448
+ this.showDialPad = true
449
+ },
450
+
451
+ onDialPadMakeCallBtnClick ({dialNum}) {
452
+ this.dialNum = dialNum
453
+ this.onMakeCallBtnClick()
454
+ },
455
+
456
+ onDialPadDtmfBtnClick (v) {
457
+ this.sipMix.sendDTMF({tones: v})
458
+ },
459
+
460
+ onChangeVideoMiniModeBtnClick () {
461
+ this.toMiniMode()
462
+ },
463
+
464
+ onChangeVideoNormalModeBtnClick () {
465
+ this.toNormalMode()
466
+ },
467
+
468
+ onChangeVideoFullScreenBtnClick () {
469
+ if (this.remoteVideo.show) {
470
+ this.$refs.remoteVideoWrapper.left = 0
471
+ this.$refs.remoteVideoWrapper.top = 0
472
+
473
+ this.$refs.localVideoWrapper.left = 0
474
+ this.$refs.localVideoWrapper.top = 0
475
+
476
+ this.$nextTick(() => {
477
+ if (this.localVideo.w === this.clientWidth) {
478
+ Object.assign(this.localVideo, {
479
+ w: parseInt(this.clientWidth / 3),
480
+ h: parseInt(this.clientHeight / 3),
481
+ z: 1100,
482
+ draggable: true
483
+ })
484
+ Object.assign(this.remoteVideo, {
485
+ w: this.clientWidth,
486
+ h: this.clientHeight,
487
+ z: 1000,
488
+ draggable: false
489
+ })
490
+ } else {
491
+ Object.assign(this.localVideo, {
492
+ w: this.clientWidth,
493
+ h: this.clientHeight,
494
+ z: 1000,
495
+ draggable: false
496
+ })
497
+ Object.assign(this.remoteVideo, {
498
+ w: parseInt(this.clientWidth / 3),
499
+ h: parseInt(this.clientHeight / 3),
500
+ z: 1100,
501
+ draggable: true
502
+ })
503
+ }
504
+ })
505
+ }
506
+ },
507
+
508
+ onLocalVideoDragCallback (e, target) {
509
+ if (target === 'remoteVideo' && this.remoteVideo.w !== this.clientWidth) {
510
+ e.stopPropagation()
511
+ } else if (target === 'localVideo' && this.localVideo.w !== this.clientWidth) {
512
+ e.stopPropagation()
513
+ }
514
+ },
515
+
516
+ onSipCallClick (){
517
+ this.autoShowHangupBtn()
518
+ },
519
+
520
+ async onMakeCallBtnClick() {
521
+ if (!this.needDial) {
522
+ this.dialNum = this.calledNum
523
+ }
524
+
525
+ const params = { target: this.dialNum }
526
+ console.log('paramsparams Data', params)
527
+ console.log('this.callData Data', this.callData)
528
+ console.log('window.config.videoParams', window.config.videoParams)
529
+ if (this.callType === 'videoOnly') {
530
+ console.log('videoOnly')
531
+ params.video = Object.assign({
532
+ facingMode: 'user'
533
+ }, window.config.videoParams)
534
+ params.audio = false
535
+ } else if (this.callType === 'video') {
536
+ console.log('video')
537
+ params.video = Object.assign({
538
+ facingMode: 'user'
539
+ }, window.config.videoParams)
540
+ params.audio = true
541
+ } else {
542
+ params.video = false
543
+ params.audio = true
544
+ }
545
+
546
+ if (this.callData) {
547
+ params.callData = this.callData
548
+ console.log('Call Da1ta', this.callData)
549
+ }
550
+
551
+ // console.info('Call Params', params)
552
+ try {
553
+ await this.sipMix.call(params)
554
+ } catch (e) {
555
+ this.snackbarText = this.$t('gotCameraStreamFailed')
556
+ this.showSnackbar = true
557
+ }
558
+
559
+ this.isCalling = true
560
+ this.showHangup = true
561
+ if (this.needDial) {
562
+ this.callPrompt = `${this.$t('calling')} ${this.dialNum}`
563
+ this.showDialPad = false
564
+ } else {
565
+ // this.callPrompt = `${this.$t('calling')}...${this.$t('pleaseBePatient')}`
566
+ this.callPrompt = `${this.$t('pleaseBePatient')}`
567
+ }
568
+
569
+ },
570
+ onHangupBtnClick () {
571
+ this.sipMix.hangup();
572
+ // this.$emit('hangup-event', { imType: 0 });// 通知父组件
573
+ // 触发 hangup 事件,并传递参数给父组件
574
+ this.$emit('hangup', {
575
+ // callId: this.callId,
576
+ // duration: this.duration,
577
+ // timestamp: new Date().getTime(),
578
+ imType: 0,
579
+ });
580
+ },
581
+
582
+ onMuteBtnClick (type) {
583
+ if (type === 'audio') {
584
+ if (this.isAudioMute) {
585
+ this.sipMix.unmute({audio: true})
586
+ } else {
587
+ this.sipMix.mute({audio: true})
588
+ }
589
+ this.isAudioMute = !this.isAudioMute
590
+ } else {
591
+ if (this.isVideoMute) {
592
+ this.sipMix.unmute({video: true})
593
+ } else {
594
+ this.sipMix.mute({video: true})
595
+ }
596
+ this.isVideoMute = !this.isVideoMute
597
+ }
598
+ },
599
+
600
+
601
+ onShowLocalVideoBtnClick () {
602
+ // this.localVideo.show = !this.localVideo.show
603
+ this.localVideo.z = (this.localVideo.z === 1 ? 1100 : 1)
604
+ },
605
+
606
+ dealCallEndOrFail () {
607
+ // this.$refs.audioPlayer.currentTime = 0
608
+ this.$refs.audioPlayer.pause()
609
+ this.showVideoCt = false
610
+ this.remoteVideo.show = false
611
+ this.localVideo.show = false
612
+ this.localVideo.w = this.clientWidth
613
+ this.localVideo.h = this.clientHeight
614
+ this.isCalling = false
615
+ this.callId = ''
616
+ this.prompt = this.$t('serverConnected')
617
+
618
+ if (this.needDial) {
619
+ this.showDialPad = true
620
+ }
621
+
622
+ this.stopDurationCount()
623
+ this.toNormalMode()
624
+ this.stopSendPosition()
625
+ },
626
+
627
+ autoShowHangupBtn () {
628
+ this.showHangup = true
629
+ /* setTimeout(() => {
630
+ this.showHangup = false
631
+ }, 3000) */
632
+ },
633
+
634
+ stopDurationCount () {
635
+ this.callDuration = 0
636
+ clearInterval(this.durationHandler)
637
+ },
638
+
639
+ startDurationCount () {
640
+ this.stopDurationCount()
641
+ this.durationHandler = setInterval(() => {
642
+ this.callDuration++
643
+ }, 1000)
644
+ },
645
+
646
+ async onSwitchCameraBtnClick() {
647
+ const list = await navigator.mediaDevices.enumerateDevices()
648
+ console.warn(list)
649
+ let videoOption = {
650
+ facingMode: 'user',
651
+ }
652
+ if (this.cameraType === 'front') {
653
+ videoOption = {
654
+ facingMode: { exact: 'environment' }
655
+ }
656
+ }
657
+ Object.assign(videoOption, window.config.videoParams)
658
+
659
+ const stream = await navigator.mediaDevices.getUserMedia({
660
+ video: videoOption,
661
+ audio: false
662
+ })
663
+ if (stream) {
664
+ this.cameraType = this.cameraType === 'front' ? 'back' : 'front'
665
+ this.$refs.localVideo.srcObject = stream
666
+ this.sipMix.changeLocalVideoTrack({stream})
667
+ }
668
+ },
669
+
670
+ setDialNum (dialNum) {
671
+ this.$refs.dialPanelMini.setDialNum(dialNum)
672
+ },
673
+
674
+ async checkBrowser() {
675
+ const stream = await navigator.mediaDevices.getUserMedia({video: true, audio: true})
676
+ if (stream) {
677
+ closeStream(stream)
678
+ } else {
679
+ return Promise.reject(this.$t('cameraOrMicUnsupported'))
680
+ }
681
+
682
+
683
+ if (!window.RTCPeerConnection) {
684
+ return Promise.reject(this.$t('WebRTCUnsupported'))
685
+ }
686
+
687
+ const list = await navigator.mediaDevices.enumerateDevices()
688
+ const vList = list.filter(e => e.kind === 'videoinput')
689
+ vList.forEach(device => {
690
+ console.warn({
691
+ label: device.label,
692
+ deviceId: device.deviceId,
693
+ groupId: device.groupId
694
+ })
695
+ })
696
+
697
+ this.videoInputList = vList
698
+
699
+ return Promise.resolve(true)
700
+ },
701
+
702
+ startSip ({debug, password, register, autoDial = false}) {
703
+ const {domain, port, path} = window.config
704
+
705
+ this.sipMix = SipMix.getInstance({
706
+ domain, port,
707
+ path
708
+ })
709
+ this.sipMix.on('socketError', () => this.prompt = this.$t('connectionFailedPleaseCheckYourNetwork'))
710
+
711
+ this.sipMix.on('connecting', () => this.prompt = this.$t('serverConnected') + '...')
712
+ this.sipMix.on('connected', () => {
713
+ this.prompt = this.$t('serverConnected')
714
+ if (!register && autoDial) {
715
+ this.onMakeCallBtnClick()
716
+ }
717
+ })
718
+ this.sipMix.on('disconnected', () => this.prompt = this.$t('connectionClosed'))
719
+
720
+ // 非注册模式不会触发以下3个事件
721
+ this.sipMix.on('registered', () => {
722
+ this.prompt = this.$t('registered')
723
+ if (register && autoDial) {
724
+ this.onMakeCallBtnClick()
725
+ }
726
+ })
727
+ this.sipMix.on('unregistered', () => this.prompt = this.$t('unregistered'))
728
+ this.sipMix.on('registrationFailed', () => this.prompt = this.$t('registerFailed'))
729
+
730
+
731
+ this.sipMix.on('newSession', ({sessionId}) => {
732
+ console.info('New session, session id: ', sessionId)
733
+ })
734
+
735
+ // 处理通话失败事件
736
+ this.sipMix.on('failed', (e) => {
737
+ console.warn('通话失败', e)
738
+ this.dealCallEndOrFail() // 清理通话相关状态
739
+ this.callPrompt = this.$t('cancelled') // 显示通话已取消提示
740
+
741
+ // 触发挂断事件,通知父组件
742
+ this.$emit('hangup', {
743
+ imType: 0, // 即时通讯类型
744
+ reason: 'call_failed', // 失败原因
745
+ error: e // 错误信息
746
+ });
747
+
748
+ // 2秒后恢复默认提示
749
+ setTimeout(() => {
750
+ this.callPrompt = this.$t('pleaseBePatient')
751
+ }, 2000)
752
+ })
753
+
754
+ // 处理通话结束事件(对方挂断或通话正常结束)
755
+ this.sipMix.on('ended', () => {
756
+ console.warn('通话结束')
757
+ this.dealCallEndOrFail() // 清理通话相关状态
758
+ this.callPrompt = this.$t('hangedUp') // 显示通话已挂断提示
759
+
760
+ // 触发挂断事件,通知父组件
761
+ this.$emit('hangup', {
762
+ imType: 0, // 即时通讯类型
763
+ reason: 'call_ended' // 通话结束原因
764
+ });
765
+
766
+ // 2秒后恢复默认提示
767
+ setTimeout(() => {
768
+ this.callPrompt = this.$t('pleaseBePatient')
769
+ }, 2000)
770
+ })
771
+
772
+ this.sipMix.on('localStream', async (stream, info) => {
773
+ console.info('localStream', stream, info)
774
+ this.localVideo.w = this.clientWidth
775
+ this.localVideo.h = this.clientHeight
776
+ this.localVideo.draggable = false
777
+ this.showVideoCt = true
778
+ this.localVideo.show = true
779
+
780
+ await this.$nextTick()
781
+ this.$refs.localVideo.srcObject = stream
782
+ })
783
+
784
+ this.sipMix.on('remoteStream', (stream, info) => {
785
+ console.info('remoteStream', stream, info)
786
+ this._resetCmpWHLRTB()
787
+
788
+ Object.assign(this.localVideo, {
789
+ w: parseInt(this.clientWidth / 3),
790
+ h: parseInt(this.clientHeight / 3),
791
+ x: 0,
792
+ y: 80,
793
+ z: 1100,
794
+ draggable: true
795
+ })
796
+ this.$nextTick(() => {
797
+ this.$refs.localVideoWrapper.moveVertically(80)
798
+ })
799
+
800
+
801
+ setTimeout(async () => {
802
+ Object.assign(this.remoteVideo, {
803
+ show: true,
804
+ w: this.clientWidth,
805
+ h: this.clientHeight,
806
+ x: 0,
807
+ y: 0,
808
+ z: 1000,
809
+ draggable: false
810
+ })
811
+
812
+ await this.$nextTick()
813
+ if (this.autoHideLocalVideo) {
814
+ this.localVideo.show = false
815
+ }
816
+ this.$refs.remoteVideo.srcObject = stream
817
+ }, 500)
818
+
819
+ })
820
+
821
+ this.sipMix.on('remoteRing', () => {
822
+ this.callPrompt = this.$t('remoteRinging')
823
+ // this.$refs.audioPlayer.currentTime = 0
824
+ const promise = this.$refs.audioPlayer.play()
825
+ if (promise !== undefined) {
826
+ promise.catch(error => {
827
+ console.info(error)
828
+ if (error.name === 'NotAllowedError') {
829
+ this.showAudioPlayer = true
830
+ }
831
+ })
832
+ }
833
+ })
834
+
835
+ this.sipMix.on('accepted', (e) => {
836
+ this.callPrompt = this.$t('remoteAccept')
837
+ const {callId} = e
838
+ this.callId = callId
839
+ this.$refs.audioPlayer.pause()
840
+ this.autoShowHangupBtn()
841
+ })
842
+
843
+ this.sipMix.on('confirmed', () => {
844
+ this.startSendPosition()
845
+ })
846
+
847
+ this.sipMix.on('newInfo', ({content, contentType}) => {
848
+ console.info('New Info', contentType, content)
849
+ })
850
+
851
+ this.sipMix.start({
852
+ deviceId: this.callingNum,
853
+ password,
854
+ register,
855
+ debug
856
+ })
857
+ },
858
+
859
+ startLocate () {
860
+ if (navigator.geolocation) {
861
+ navigator.geolocation.getCurrentPosition(position => {
862
+ console.info('Position', position)
863
+ const {latitude, longitude, altitude } = position?.coords || {}
864
+ console.info(`获取地里位置信息: ${latitude}, ${longitude}, ${altitude}`)
865
+ /*this.sendCustomEvent({
866
+ act: 'position',
867
+ params: {latitude, longitude, accuracy}
868
+ })*/
869
+ this.latitude = latitude
870
+ this.longitude = longitude
871
+ this.altitude = altitude
872
+ }, () => {
873
+ console.info('获取地里位置信息失败')
874
+ })
875
+
876
+ this.locateHandler = setTimeout (() => {
877
+ this.startLocate()
878
+ }, 15000)
879
+ }
880
+ },
881
+
882
+ stopLocate () {
883
+ clearTimeout(this.locateHandler)
884
+ },
885
+
886
+ startSendPosition () {
887
+ if (this.showLocation && this.locateHandler) {
888
+ this.sipMix.sendInfo({
889
+ encode: false,
890
+ content: {
891
+ type: 'Position',
892
+ content: {
893
+ latitude: this.latitude,
894
+ longitude: this.longitude,
895
+ altitude: this.altitude
896
+ }
897
+ }
898
+ })
899
+ /*this.sipMix.sendMessage({
900
+ target: this.calledNum,
901
+ content: {
902
+ type: 'Position',
903
+ content: {
904
+ latitude: this.latitude,
905
+ longitude: this.longitude,
906
+ altitude: this.altitude
907
+ }
908
+ }
909
+ })*/
910
+ }
911
+ this.sendPositionHandler = setTimeout(() => {
912
+ this.startSendPosition()
913
+ }, 15000)
914
+ },
915
+
916
+ stopSendPosition () {
917
+ clearTimeout(this.sendPositionHandler)
918
+ }
919
+ },
920
+
921
+ async mounted() {
922
+ console.info('MobilePhone mounted')
923
+ await this.checkBrowser().catch(e => {
924
+ this.showSnackbar = true
925
+ this.snackbarText = e
926
+ })
927
+
928
+ setTimeout(() => {
929
+ const {clientHeight, clientWidth} = window.document.body
930
+ this.clientHeight = clientHeight
931
+ this.clientWidth = clientWidth
932
+ this.localVideo.w = clientWidth
933
+ this.localVideo.h = clientHeight
934
+
935
+ if (this.showLocation) {
936
+ this.startLocate()
937
+ }
938
+ }, 200)
939
+
940
+ this.$nextTick(() => {
941
+ this.showAudioPlayer = false
942
+ })
943
+ }
944
+ }
945
+ </script>
946
+ <!-- <style>
947
+ @import './vuetify.css';
948
+ </style> -->
949
+ <style lang="scss">
950
+ .sip-call-main {
951
+ background-color: #737778;
952
+ padding: 25% 20px 20% !important;
953
+ width: 100vw;
954
+ height: 100vh;
955
+ overflow: hidden;
956
+ }
957
+
958
+ .sip-call-wrapper {
959
+ position: absolute;
960
+ left: 0;
961
+ top: 0;
962
+ width: 100vw;
963
+ height: 100vh;
964
+ z-index: 100;
965
+ }
966
+
967
+ .btn-group {
968
+ width: 100%;
969
+ position: absolute;
970
+ bottom: 500px;
971
+ z-index: 4000;
972
+ }
973
+
974
+ @media screen and (min-width: 1024px) {
975
+ .btn-group {
976
+ position: absolute;
977
+ bottom: 80px;
978
+ width: 640px;
979
+ z-index: 4000;
980
+ }
981
+ }
982
+
983
+ .sip-call-video {
984
+ width: 100%;
985
+ height: 100%;
986
+ object-fit: cover;
987
+ background-color: #000000;
988
+ }
989
+
990
+ .video-panel {
991
+ position: absolute;
992
+ display: flex;
993
+ flex-direction: column;
994
+ justify-content: space-around;
995
+ align-items: center;
996
+ }
997
+
998
+ .video-timer {
999
+ background-color: rgba(255,255,255,0.80);
1000
+ position: absolute;
1001
+ top: 80px;
1002
+ left: 32px;
1003
+ font-size: 28px;
1004
+ font-weight: bold;
1005
+ padding: 4px 16px 4px 12px;
1006
+ border-radius: 24px;
1007
+ z-index: 2000;
1008
+ display: flex;
1009
+ justify-content: space-around;
1010
+ align-items: center;
1011
+ }
1012
+
1013
+ .video-switch-btn {
1014
+ position: absolute !important;
1015
+ top: 80px;
1016
+ right: 160px;
1017
+ z-index: 2000;
1018
+ img {
1019
+ background-color: #000000;
1020
+ border-radius: 12px;
1021
+ border: 2px solid #000000;
1022
+ }
1023
+ }
1024
+
1025
+ .video-mini-mode {
1026
+ position: absolute !important;
1027
+ top: 80px;
1028
+ right: 96px;
1029
+ z-index: 2000;
1030
+ }
1031
+
1032
+ .camera-switch-btn {
1033
+ position: absolute !important;
1034
+ top: 80px;
1035
+ right: 32px;
1036
+ z-index: 2000;
1037
+ }
1038
+
1039
+ .video-normal-mode {
1040
+ position: absolute !important;
1041
+ top: 32px;
1042
+ right: 32px;
1043
+ z-index: 2000;
1044
+ }
1045
+
1046
+ .location-wrapper {
1047
+ position: absolute;
1048
+ top: 0;
1049
+ right: 0;
1050
+ width: 300px;
1051
+ background-color: #00000055;
1052
+ color: #FFFFFF;
1053
+ padding: 12px;
1054
+ p {
1055
+ font-size: 24px;
1056
+ margin: 0;
1057
+ padding: 0;
1058
+ width: 100%;
1059
+ white-space: nowrap;
1060
+ text-overflow: ellipsis;
1061
+ }
1062
+ }
1063
+ </style>
1064
+ <style scoped>
1065
+ .custom-mute-btn {
1066
+ background: transparent !important;
1067
+ box-shadow: none !important;
1068
+ z-index: 9999 !important;
1069
+ min-width: 0 !important;
1070
+ width: auto !important;
1071
+ height: auto !important;
1072
+ padding: 0 !important;
1073
+ margin: 0 !important;
1074
+ }
1075
+
1076
+ /* 移除按钮悬停和激活状态的背景色 */
1077
+ .custom-mute-btn::before {
1078
+ background-color: transparent !important;
1079
+ opacity: 0 !important;
1080
+ }
1081
+
1082
+ /* 移除按钮的点击效果 */
1083
+ .custom-mute-btn:hover::before,
1084
+ .custom-mute-btn:focus::before,
1085
+ .custom-mute-btn:active::before {
1086
+ opacity: 0 !important;
1087
+ }
1088
+
1089
+ .transparent-btn {
1090
+ background: transparent !important;
1091
+ box-shadow: none !important;
1092
+ min-width: 0 !important;
1093
+ width: auto !important;
1094
+ height: auto !important;
1095
+ padding: 0 !important;
1096
+ margin: 0 !important;
1097
+ opacity: .6 !important;
1098
+ position: absolute !important;
1099
+ left: calc(50% + 160px) !important;
1100
+ }
1101
+ .transparent-btn::before {
1102
+ background-color: transparent !important;
1103
+ opacity: 0 !important;
1104
+ }
1105
+
1106
+ .call-btn{
1107
+ margin-top:320px;
1108
+ }
1109
+ .call-img{
1110
+ width:200px;
1111
+ height:200px;
1112
+ border-radius: 50%;
1113
+ }
1114
+
1115
+
1116
+ .call-label{
1117
+ font-size: 32px;
1118
+ color: #FFFFFF
1119
+ }
1120
+
1121
+ .cancel-btn{
1122
+ opacity: 1;
1123
+ background: transparent !important;
1124
+ box-shadow: none !important;
1125
+ position: relative !important;
1126
+ margin-top: -100px !important;
1127
+ }
1128
+ .cancel-img {
1129
+ width: 160px;
1130
+ height: 160px;
1131
+ }
1132
+ .cancel-label{
1133
+ top: 156px;
1134
+ position: absolute; color: #FFFFFF;
1135
+ font-size: 32px;
1136
+ }
1137
+
1138
+
1139
+ .call-duration{
1140
+ font-size: 28px;margin-top: -25px; color: #FFFFFF;
1141
+ }
1142
+ .hangup-img{
1143
+ width: 160px;
1144
+ height: 160px;
1145
+ }
1146
+ .hangup-label{
1147
+ top: 146px;
1148
+ position: absolute; color: #FFFFFF;
1149
+ font-size: 28px;
1150
+ }
1151
+
1152
+ .img-open-close{
1153
+ width: 110px;
1154
+ height: 160px;
1155
+ }
1156
+
1157
+ </style>