@sdk185/sip-phone-sdk26 0.0.7 → 0.0.9

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