@sdk185/sip-phone-sdk26 0.2.11 → 0.2.13

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,1289 @@
1
+ <!-- 主模板:视频通话界面 -->
2
+ <template>
3
+ <!-- 主容器:使用flex垂直布局,内容居中对齐 -->
4
+ <div class="phone-nmain 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="chatURL+'/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 " 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="chatURL+'/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 btn-box"
47
+ >
48
+ <img class="img-open-close" v-if="!isAudioMute" :src="chatURL+'/static/adudio-open.png'" >
49
+ <img class="img-open-close" v-else :src="chatURL+'/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="chatURL+'/static/camera-open.png'" alt="mic-on">
59
+ <img class="img-open-close" v-else :src="chatURL+'/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;z-index: 9999;"
69
+ v-show="isCalling && showHangup"
70
+ @click="onHangupBtnClick"
71
+ >
72
+ <img class="hangup-img" :src="chatURL+'/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;pointer-events: none !important;"
118
+ controls
119
+ controlsList="nodownload nofullscreen noremoteplayback"
120
+ disablepictureinpicture
121
+ ></video>
122
+ </vue-draggable-resizable>
123
+ <!-- 本地视频窗口 -->
124
+ <vue-draggable-resizable
125
+ v-show="localVideo.show"
126
+ :w="localVideo.w"
127
+ :h="localVideo.h"
128
+ :x="localVideo.x"
129
+ :y="localVideo.y"
130
+ :z="localVideo.z"
131
+ :draggable="localVideo.draggable"
132
+ :handles="[]"
133
+ :parent="true"
134
+ :active="false"
135
+ class-name="elevation-6 video-panel"
136
+ ref="localVideoWrapper"
137
+ @mousedown.native="onLocalVideoDragCallback($event, 'localVideo')"
138
+ >
139
+ <video
140
+ v-show="localVideo.show"
141
+ autoplay="autoplay"
142
+ muted
143
+ webkit-playsinline="true"
144
+ playsinline="true"
145
+ x-webkit-airplay="true"
146
+ x5-video-player-type="h5"
147
+ x5-video-orientation="h5"
148
+ ref="localVideo"
149
+ style="pointer-events: none !important;"
150
+ class="sip-call-video"
151
+
152
+ ></video>
153
+ </vue-draggable-resizable>
154
+ </div>
155
+ </v-expand-transition>
156
+ <!-- 拨号盘组件 -->
157
+ <v-expand-transition>
158
+ <dial-panel-mini
159
+ :isCalling="isCalling"
160
+ v-show="showDialPad"
161
+ @makeCall="onDialPadMakeCallBtnClick"
162
+ @close="showDialPad=false"
163
+ @dtmf="onDialPadDtmfBtnClick"
164
+ @hangup="onHangupBtnClick"
165
+ ref="dialPanelMini"
166
+ />
167
+ </v-expand-transition>
168
+ <!-- 音频播放器(用于播放提示音) -->
169
+ <v-bottom-sheet v-model="showAudioPlayer" hide-overlay>
170
+ <audio :src="require('@/assets/outgoing-call2.mp3')" loop ref="audioPlayer"></audio>
171
+ </v-bottom-sheet>
172
+ <!-- 提示信息弹窗 -->
173
+ <v-snackbar v-model="showSnackbar" :timeout="2000">
174
+ {{snackbarText}}
175
+ </v-snackbar>
176
+ </div>
177
+ </template>
178
+
179
+ <script>
180
+ import 'webrtc-adapter'
181
+ import SipMix from '@/libs/SIP_MIX_WEB'
182
+ import DialPanelMini from '@/components/DialPanelMini'
183
+ import VueDraggableResizable from 'vue-draggable-resizable'
184
+ import { closeStream } from '@/libs/tool'
185
+ import jsNative from 'js-native-n22'
186
+
187
+ export default {
188
+ name: 'MobilePhone',
189
+ api: null,
190
+ locateHandler: -1,
191
+ sendPositionHandler: -1,
192
+ clientHeight: 0,
193
+ clientWidth: 0,
194
+
195
+ components: {
196
+ VueDraggableResizable,
197
+ DialPanelMini
198
+ },
199
+
200
+ props: {
201
+ calledNum: { // 被叫
202
+ type: String,
203
+ default: ''
204
+ },
205
+
206
+ callingNum: { // 主叫
207
+ type: String,
208
+ default: 'H5User',
209
+ validator: (v) => !!v
210
+ },
211
+
212
+ callType: {
213
+ type: String,
214
+ default: 'videoOnly',
215
+ validator: v => ['audio', 'video', 'videoOnly'].indexOf(v) > -1
216
+ },
217
+
218
+ debug: {
219
+ type: Boolean,
220
+ default: false
221
+ },
222
+
223
+ needDial: {
224
+ type: Boolean,
225
+ default: false
226
+ },
227
+
228
+ autoHideLocalVideo: {
229
+ type: Boolean,
230
+ default: false
231
+ },
232
+
233
+ showLocation: {
234
+ type: Boolean,
235
+ default: true
236
+ },
237
+
238
+ callData: {
239
+ type: String,
240
+ default: ''
241
+ }
242
+ },
243
+
244
+ data: function () {
245
+ return {
246
+ prompt: `${this.$t('serverConnecting')}...`,
247
+ callPrompt: this.$t('pleaseBePatient'),
248
+ chatURL:"https://kefu-gm-jc-dat.boc-samsunglife.cn",// 环境切换
249
+ isCalling: false,
250
+ showHangup: false,
251
+ showAudioPlayer: true,
252
+ showVideoCt: false,
253
+ showDialPad: false,
254
+ showSnackbar: false,
255
+
256
+ miniMode: false,
257
+ dialNum: '',
258
+
259
+ //controls: '',
260
+
261
+ isAudioMute: false,
262
+ isVideoMute: false,
263
+ cameraType: 'front',
264
+ videoInputList: [],
265
+ videoInputIndex: 0,
266
+
267
+ localVideo: {
268
+ w: 1,
269
+ h: 1,
270
+ x: 0,
271
+ y: 0,
272
+ z: 1000,
273
+ show: false,
274
+ draggable: true
275
+ },
276
+
277
+ remoteVideo: {
278
+ w: 1,
279
+ h: 1,
280
+ x: 0,
281
+ y: 0,
282
+ z: 1000,
283
+ show: false,
284
+ draggable: true
285
+ },
286
+
287
+ connected: false,
288
+ callDuration: 0,
289
+
290
+ durationHandler: -1,
291
+ snackbarText: '',
292
+
293
+ callId: '',
294
+ lastCallId: '',
295
+ queueCount: 0,
296
+
297
+ latitude: 0,
298
+ longitude: 0,
299
+ altitude: 0
300
+ }
301
+ },
302
+
303
+ watch: {
304
+ // isCalling (is) {
305
+ // if (is) {
306
+ // this.startDurationCount()
307
+ // } else {
308
+ // this.stopDurationCount()
309
+ // }
310
+ // },
311
+ callId (id) {
312
+ this.lastCallId = id || this.lastCallId
313
+ },
314
+ prompt (v) {
315
+ console.info('Prompt', v)
316
+ }
317
+ },
318
+
319
+ computed: {
320
+ duration () {
321
+ let s = this.callDuration % 60
322
+ s = s < 10 ? `0${s}` : `${s}`
323
+ let m = parseInt(this.callDuration / 60)
324
+ m = m < 10 ? `0${m}` : `${m}`
325
+ return `${m}:${s}`
326
+ }
327
+ },
328
+
329
+ created() {
330
+ setTimeout(() => {
331
+ // this.onMakeCallBtnClick();
332
+ // 处理微信公众号传递的参数
333
+ // this.handleWechatParams();
334
+ }, 2000);
335
+ },
336
+
337
+ methods: {
338
+ handleAndroidParams(val){
339
+ console.log("handleAndroidParamsvalvalval", val)
340
+ jsNative.bridge.callhandler('webRTC', '', (result) => {
341
+ console.log("%c调用原生返回成功", "color: green", result)
342
+ console.log("c调用原生返回成功valval", val)
343
+ if (!result.error) {
344
+ // success && success(result.content)
345
+ console.log('webRTC调用成功', result.content)
346
+ console.log('webRTC调用成功valval', val)
347
+ this.handleWechatParams(val);
348
+ } else {
349
+ // fail && fail(result.content)
350
+ console.log('webRTC调用失败', result.content)
351
+ console.log('webRTC调用失败valval', val)
352
+ }
353
+ })
354
+ },
355
+
356
+
357
+ // 处理微信公众号参数
358
+ handleWechatParams(val) {
359
+ // ===== 测试代码开始 =====
360
+ // 测试用的URL参数字符串 - 模拟微信公众号传递的参数
361
+ // const testParams = 'debug=true&calledNum=0000021000021001&av=video&callData={"calledNum":"0000021000021001","videoRecordId":"ae756872ff3a122817848719605aa5cb"}';
362
+ // // 解析测试参数
363
+ // const urlParams = new URLSearchParams(testParams);
364
+ // // 获取参数值
365
+ // const calledNum = urlParams.get('calledNum');
366
+ // const callDataStr = urlParams.get('callData');
367
+ console.log('e行销传过来的参数',val);
368
+
369
+ // ===== 原始代码(已注释) ===== // 用于微信公众号 此方式是获取url参数;父子组件传值可以使用props
370
+ // const urlParams = new URLSearchParams(window.location.search);
371
+ // const calledNum = urlParams.get('calledNum');
372
+ // const callDataStr = urlParams.get('callData');
373
+ // const urlParams = val;
374
+ const calledNum = val.calledNum;
375
+ const callDataStr = val.callData;
376
+
377
+ // console.log('测试模式:使用模拟参数');
378
+
379
+ if (calledNum) {
380
+ // 存储被叫号码
381
+ this.setDialNum(calledNum);
382
+
383
+ // 如果有callData,解析并存储
384
+ let callData = null;
385
+ try {
386
+ callData = callDataStr ? JSON.parse(decodeURIComponent(callDataStr)) : null;
387
+ // 这里可以将callData存储到Vuex或组件的data中
388
+ if (callData) {
389
+ // 例如:this.$store.commit('setCallData', callData);
390
+ console.log('Call data received:', callData);
391
+ }
392
+ } catch (e) {
393
+ console.error('Failed to parse callData:', e);
394
+ }
395
+
396
+ // 自动发起呼叫
397
+ this.$nextTick(() => {
398
+ this.onMakeCallBtnClick();
399
+ });
400
+ }
401
+ },
402
+
403
+ _resetCmpWHLRTB ({rl =0, rt = 0, ll = 0, lt = 0} = {}) {
404
+ this.$refs.remoteVideoWrapper.left = rl
405
+ this.$refs.remoteVideoWrapper.top = rt
406
+
407
+ this.$refs.localVideoWrapper.left = ll
408
+ this.$refs.localVideoWrapper.top = lt
409
+
410
+ this.$refs.localVideoWrapper.parentWidth = this.clientWidth
411
+ this.$refs.localVideoWrapper.parentHeight = this.clientHeight
412
+ this.$refs.localVideoWrapper.right = this.clientWidth - this.localVideo.w
413
+ this.$refs.localVideoWrapper.bottom = this.clientHeight - this.localVideo.h
414
+
415
+ this.$refs.remoteVideoWrapper.parentWidth = this.clientWidth
416
+ this.$refs.remoteVideoWrapper.parentHeight = this.clientHeight
417
+ this.$refs.remoteVideoWrapper.right = this.clientWidth - this.remoteVideo.w
418
+ this.$refs.remoteVideoWrapper.bottom = this.clientHeight - this.remoteVideo.h
419
+ },
420
+
421
+ toMiniMode () {
422
+ this._resetCmpWHLRTB()
423
+ Object.assign(this.localVideo, {
424
+ show: false,
425
+ draggable: false
426
+ })
427
+ this.miniMode = true
428
+
429
+ setTimeout(() => {
430
+ Object.assign(this.remoteVideo, {
431
+ show: true,
432
+ w: parseInt(this.clientWidth / 3),
433
+ h: parseInt(this.clientHeight / 3),
434
+ x: 0,
435
+ y: 0,
436
+ z: 1100,
437
+ draggable: true
438
+ })
439
+ }, 500)
440
+ },
441
+
442
+ toNormalMode () {
443
+ this._resetCmpWHLRTB()
444
+ Object.assign(this.localVideo, {
445
+ show: true,
446
+ w: parseInt(this.clientWidth / 3),
447
+ h: parseInt(this.clientHeight / 3),
448
+ x: 0,
449
+ y: 0,
450
+ z: 1100,
451
+ draggable: false
452
+ })
453
+ this.miniMode = false
454
+
455
+ setTimeout(() => {
456
+ // this._resetCmpWHLRTB()
457
+ Object.assign(this.remoteVideo, {
458
+ show: true,
459
+ w: this.clientWidth,
460
+ h: this.clientHeight,
461
+ x: 0,
462
+ y: 0,
463
+ z: 1000,
464
+ draggable: true
465
+ })
466
+ }, 500)
467
+ },
468
+
469
+ onDialPadBtnClick () {
470
+ this.showDialPad = true
471
+ },
472
+
473
+ onDialPadMakeCallBtnClick ({dialNum}) {
474
+ this.dialNum = dialNum
475
+ this.onMakeCallBtnClick()
476
+ },
477
+
478
+ onDialPadDtmfBtnClick (v) {
479
+ this.sipMix.sendDTMF({tones: v})
480
+ },
481
+
482
+ onChangeVideoMiniModeBtnClick () {
483
+ this.toMiniMode()
484
+ },
485
+
486
+ onChangeVideoNormalModeBtnClick () {
487
+ this.toNormalMode()
488
+ },
489
+
490
+ onChangeVideoFullScreenBtnClick () {
491
+ if (this.remoteVideo.show) {
492
+ this.$refs.remoteVideoWrapper.left = 0
493
+ this.$refs.remoteVideoWrapper.top = 0
494
+
495
+ this.$refs.localVideoWrapper.left = 0
496
+ this.$refs.localVideoWrapper.top = 0
497
+
498
+ this.$nextTick(() => {
499
+ if (this.localVideo.w === this.clientWidth) {
500
+ Object.assign(this.localVideo, {
501
+ w: parseInt(this.clientWidth / 3),
502
+ h: parseInt(this.clientHeight / 3),
503
+ z: 1100,
504
+ draggable: true
505
+ })
506
+ Object.assign(this.remoteVideo, {
507
+ w: this.clientWidth,
508
+ h: this.clientHeight,
509
+ z: 1000,
510
+ draggable: false
511
+ })
512
+ } else {
513
+ Object.assign(this.localVideo, {
514
+ w: this.clientWidth,
515
+ h: this.clientHeight,
516
+ z: 1000,
517
+ draggable: false
518
+ })
519
+ Object.assign(this.remoteVideo, {
520
+ w: parseInt(this.clientWidth / 3),
521
+ h: parseInt(this.clientHeight / 3),
522
+ z: 1100,
523
+ draggable: true
524
+ })
525
+ }
526
+ })
527
+ }
528
+ },
529
+
530
+ onLocalVideoDragCallback (e, target) {
531
+ if (target === 'remoteVideo' && this.remoteVideo.w !== this.clientWidth) {
532
+ e.stopPropagation()
533
+ } else if (target === 'localVideo' && this.localVideo.w !== this.clientWidth) {
534
+ e.stopPropagation()
535
+ }
536
+ },
537
+
538
+ onSipCallClick (){
539
+ this.autoShowHangupBtn()
540
+ },
541
+
542
+ async onMakeCallBtnClick() {
543
+ if (!this.needDial) {
544
+ this.dialNum = this.calledNum
545
+ }
546
+
547
+ const params = { target: this.dialNum }
548
+ console.log('paramsparams Data', params)
549
+ console.log('this.callData Data', this.callData)
550
+ console.log('window.config.videoParams', window.config.videoParams)
551
+ if (this.callType === 'videoOnly') {
552
+ console.log('videoOnly')
553
+ params.video = Object.assign({
554
+ facingMode: 'user'
555
+ }, window.config.videoParams)
556
+ params.audio = false
557
+ } else if (this.callType === 'video') {
558
+ console.log('video')
559
+ params.video = Object.assign({
560
+ facingMode: 'user'
561
+ }, window.config.videoParams)
562
+ params.audio = true
563
+ } else {
564
+ params.video = false
565
+ params.audio = true
566
+ }
567
+
568
+ if (this.callData) {
569
+ params.callData = this.callData
570
+ console.log('Call Da1ta', this.callData)
571
+ }
572
+
573
+ // console.info('Call Params', params)
574
+ try {
575
+ await this.sipMix.call(params)
576
+ } catch (e) {
577
+ this.snackbarText = this.$t('gotCameraStreamFailed')
578
+ this.showSnackbar = true
579
+ }
580
+
581
+ this.isCalling = true
582
+ this.showHangup = true
583
+ if (this.needDial) {
584
+ this.callPrompt = `${this.$t('calling')} ${this.dialNum}`
585
+ this.showDialPad = false
586
+ } else {
587
+ // this.callPrompt = `${this.$t('calling')}...${this.$t('pleaseBePatient')}`
588
+ this.callPrompt = `${this.$t('pleaseBePatient')}`
589
+ }
590
+
591
+ },
592
+ onHangupBtnClick () {
593
+ this.sipMix.hangup();
594
+ // this.$emit('hangup-event', { imType: 0 });// 通知父组件
595
+ // 触发 hangup 事件,并传递参数给父组件
596
+ this.$emit('hangup', {
597
+ // callId: this.callId,
598
+ // duration: this.duration,
599
+ // timestamp: new Date().getTime(),
600
+ imType: 0,
601
+ });
602
+ },
603
+
604
+ onMuteBtnClick (type) {
605
+ if (type === 'audio') {
606
+ if (this.isAudioMute) {
607
+ this.sipMix.unmute({audio: true})
608
+ } else {
609
+ this.sipMix.mute({audio: true})
610
+ }
611
+ this.isAudioMute = !this.isAudioMute
612
+ } else {
613
+ if (this.isVideoMute) {
614
+ this.sipMix.unmute({video: true})
615
+ } else {
616
+ this.sipMix.mute({video: true})
617
+ }
618
+ this.isVideoMute = !this.isVideoMute
619
+ }
620
+ },
621
+
622
+
623
+ onShowLocalVideoBtnClick () {
624
+ // this.localVideo.show = !this.localVideo.show
625
+ this.localVideo.z = (this.localVideo.z === 1 ? 1100 : 1)
626
+ },
627
+
628
+ dealCallEndOrFail () {
629
+ // this.$refs.audioPlayer.currentTime = 0
630
+ this.$refs.audioPlayer.pause()
631
+ this.showVideoCt = false
632
+ this.remoteVideo.show = false
633
+ this.localVideo.show = false
634
+ this.localVideo.w = this.clientWidth
635
+ this.localVideo.h = this.clientHeight
636
+ this.isCalling = false
637
+ this.callId = ''
638
+ this.prompt = this.$t('serverConnected')
639
+
640
+ if (this.needDial) {
641
+ this.showDialPad = true
642
+ }
643
+
644
+ this.stopDurationCount()
645
+ this.toNormalMode()
646
+ this.stopSendPosition()
647
+ },
648
+
649
+ autoShowHangupBtn () {
650
+ this.showHangup = true
651
+ /* setTimeout(() => {
652
+ this.showHangup = false
653
+ }, 3000) */
654
+ },
655
+
656
+ stopDurationCount () {
657
+ this.callDuration = 0
658
+ clearInterval(this.durationHandler)
659
+ },
660
+
661
+ startDurationCount () {
662
+ this.stopDurationCount()
663
+ this.durationHandler = setInterval(() => {
664
+ this.callDuration++
665
+ }, 1000)
666
+ },
667
+
668
+ // async onSwitchCameraBtnClick() {
669
+ // const list = await navigator.mediaDevices.enumerateDevices()
670
+ // console.warn(list)
671
+ // let videoOption = {
672
+ // facingMode: 'user',
673
+ // }
674
+ // if (this.cameraType === 'front') {
675
+ // videoOption = {
676
+ // facingMode: { exact: 'environment' }
677
+ // }
678
+ // }
679
+ // Object.assign(videoOption, window.config.videoParams)
680
+
681
+ // const stream = await navigator.mediaDevices.getUserMedia({
682
+ // video: videoOption,
683
+ // audio: false
684
+ // })
685
+ // if (stream) {
686
+ // this.cameraType = this.cameraType === 'front' ? 'back' : 'front'
687
+ // this.$refs.localVideo.srcObject = stream
688
+ // this.sipMix.changeLocalVideoTrack({stream})
689
+ // }
690
+ // },
691
+ async onSwitchCameraBtnClick() {
692
+ // 1. 获取当前设备列表(包含音频/视频输入设备)
693
+ const list = await navigator.mediaDevices.enumerateDevices()
694
+ console.warn(list) // 调试用:打印设备列表,可看到摄像头设备信息
695
+
696
+ // 2. 初始化摄像头配置(默认前置摄像头)
697
+ let videoOption = {
698
+ facingMode: 'user', // user = 前置摄像头(面向用户)
699
+ }
700
+
701
+ // 3. 切换摄像头配置:如果当前是前置,切换为后置
702
+ if (this.cameraType === 'front') {
703
+ videoOption = {
704
+ facingMode: { exact: 'environment' } // environment = 后置摄像头(面向环境)
705
+ }
706
+ }
707
+
708
+ // 4. 合并全局视频参数(比如分辨率、帧率等配置)
709
+ Object.assign(videoOption, window.config.videoParams)
710
+
711
+ // 5. 重新请求摄像头媒体流(只请求视频,不请求音频)
712
+ const stream = await navigator.mediaDevices.getUserMedia({
713
+ video: videoOption,
714
+ audio: false
715
+ })
716
+
717
+ // 6. 流获取成功后执行核心操作
718
+ if (stream) {
719
+ // ① 切换摄像头状态标记(front ↔ back)
720
+ this.cameraType = this.cameraType === 'front' ? 'back' : 'front'
721
+
722
+ // ② 更新本地video标签的流(这是视频画面显示的核心)
723
+ this.$refs.localVideo.srcObject = stream
724
+
725
+ // 本次新增修改点:强制显示本地视频
726
+ this.localVideo.show = true
727
+
728
+ // ③ 同步新的视频流到通话链路(让对方也看到切换后的画面)
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.stopDurationCount() // 确保停止计时器 0105
802
+ this.dealCallEndOrFail() // 清理通话相关状态
803
+ this.callPrompt = this.$t('cancelled') // 显示通话已取消提示
804
+
805
+ // 触发挂断事件,通知父组件
806
+ this.$emit('hangup', {
807
+ imType: 0, // 即时通讯类型
808
+ reason: 'call_failed', // 失败原因
809
+ error: e // 错误信息
810
+ });
811
+
812
+ // 2秒后恢复默认提示
813
+ setTimeout(() => {
814
+ this.callPrompt = this.$t('pleaseBePatient')
815
+ }, 2000)
816
+ })
817
+
818
+ // 处理通话结束事件(对方挂断或通话正常结束)
819
+ this.sipMix.on('ended', () => {
820
+ console.warn('通话结束')
821
+ this.stopDurationCount() // 确保停止计时器 0105
822
+ this.dealCallEndOrFail() // 清理通话相关状态
823
+ this.callPrompt = this.$t('hangedUp') // 显示通话已挂断提示
824
+
825
+ // 触发挂断事件,通知父组件
826
+ this.$emit('hangup', {
827
+ imType: 0, // 即时通讯类型
828
+ reason: 'call_ended' // 通话结束原因
829
+ });
830
+
831
+ // 2秒后恢复默认提示
832
+ setTimeout(() => {
833
+ this.callPrompt = this.$t('pleaseBePatient')
834
+ }, 2000)
835
+ })
836
+
837
+ this.sipMix.on('localStream', async (stream, info) => {
838
+ console.info('localStream', stream, info)
839
+ this.$refs.localVideo.srcObject = stream
840
+ if(this.localVideo.show) {
841
+ return
842
+ }
843
+ this.localVideo.w = this.clientWidth
844
+ this.localVideo.h = this.clientHeight
845
+ this.localVideo.draggable = false
846
+ this.showVideoCt = true
847
+ this.localVideo.show = true
848
+
849
+ // await this.$nextTick()
850
+ // this.$refs.localVideo.srcObject = stream
851
+ })
852
+
853
+ this.sipMix.on('remoteStream', (stream, info) => {
854
+ console.info('remoteStream', stream, info)
855
+ this._resetCmpWHLRTB()
856
+
857
+ Object.assign(this.localVideo, {
858
+ w: parseInt(this.clientWidth / 3),
859
+ h: parseInt(this.clientHeight / 3),
860
+ x: 0,
861
+ y: 80,
862
+ z: 1100,
863
+ draggable: true
864
+ })
865
+ this.$nextTick(() => {
866
+ this.$refs.localVideoWrapper.moveVertically(80)
867
+ })
868
+
869
+
870
+ setTimeout(async () => {
871
+ Object.assign(this.remoteVideo, {
872
+ show: true,
873
+ w: this.clientWidth,
874
+ h: this.clientHeight,
875
+ x: 0,
876
+ y: 0,
877
+ z: 1000,
878
+ draggable: false
879
+ })
880
+
881
+ await this.$nextTick()
882
+ if (this.autoHideLocalVideo) {
883
+ this.localVideo.show = false
884
+ }
885
+ this.$refs.remoteVideo.srcObject = stream
886
+ }, 500)
887
+
888
+ })
889
+
890
+ this.sipMix.on('remoteRing', () => {
891
+ this.callPrompt = this.$t('remoteRinging')
892
+ // this.$refs.audioPlayer.currentTime = 0
893
+ const promise = this.$refs.audioPlayer.play()
894
+ if (promise !== undefined) {
895
+ promise.catch(error => {
896
+ console.info(error)
897
+ if (error.name === 'NotAllowedError') {
898
+ this.showAudioPlayer = true
899
+ }
900
+ })
901
+ }
902
+ })
903
+
904
+ this.sipMix.on('accepted', (e) => {
905
+ this.callPrompt = this.$t('remoteAccept')
906
+ const {callId} = e
907
+ this.callId = callId
908
+ this.$refs.audioPlayer.pause()
909
+ this.autoShowHangupBtn()
910
+ })
911
+
912
+ this.sipMix.on('confirmed', () => {
913
+ this.startDurationCount(); // 通话完全建立后开始计时 0105
914
+ this.startSendPosition()
915
+ })
916
+
917
+ this.sipMix.on('newInfo', ({content, contentType}) => {
918
+ console.info('New Info', contentType, content)
919
+ })
920
+
921
+ this.sipMix.start({
922
+ deviceId: this.callingNum,
923
+ password,
924
+ register,
925
+ debug
926
+ })
927
+ },
928
+
929
+ startLocate () {
930
+ if (navigator.geolocation) {
931
+ navigator.geolocation.getCurrentPosition(position => {
932
+ console.info('Position', position)
933
+ const {latitude, longitude, altitude } = position?.coords || {}
934
+ console.info(`获取地里位置信息: ${latitude}, ${longitude}, ${altitude}`)
935
+ /*this.sendCustomEvent({
936
+ act: 'position',
937
+ params: {latitude, longitude, accuracy}
938
+ })*/
939
+ this.latitude = latitude
940
+ this.longitude = longitude
941
+ this.altitude = altitude
942
+ }, () => {
943
+ console.info('获取地里位置信息失败')
944
+ })
945
+
946
+ this.locateHandler = setTimeout (() => {
947
+ this.startLocate()
948
+ }, 15000)
949
+ }
950
+ },
951
+
952
+ stopLocate () {
953
+ clearTimeout(this.locateHandler)
954
+ },
955
+
956
+ startSendPosition () {
957
+ if (this.showLocation && this.locateHandler) {
958
+ this.sipMix.sendInfo({
959
+ encode: false,
960
+ content: {
961
+ type: 'Position',
962
+ content: {
963
+ latitude: this.latitude,
964
+ longitude: this.longitude,
965
+ altitude: this.altitude
966
+ }
967
+ }
968
+ })
969
+ /*this.sipMix.sendMessage({
970
+ target: this.calledNum,
971
+ content: {
972
+ type: 'Position',
973
+ content: {
974
+ latitude: this.latitude,
975
+ longitude: this.longitude,
976
+ altitude: this.altitude
977
+ }
978
+ }
979
+ })*/
980
+ }
981
+ this.sendPositionHandler = setTimeout(() => {
982
+ this.startSendPosition()
983
+ }, 15000)
984
+ },
985
+
986
+ stopSendPosition () {
987
+ clearTimeout(this.sendPositionHandler)
988
+ }
989
+ },
990
+
991
+ async mounted() {
992
+ console.info('MobilePhone mounted')
993
+ await this.checkBrowser().catch(e => {
994
+ this.showSnackbar = true
995
+ this.snackbarText = e
996
+ })
997
+
998
+ setTimeout(() => {
999
+ // this.miniMode = false;// 调试用
1000
+ // this.showDialPad = false;// 调试用
1001
+ // this.isCalling = true;// 调试用
1002
+ // this.needDial = false;// 调试用
1003
+ // this.showHangup = true;// 调试用
1004
+ const {clientHeight, clientWidth} = window.document.body
1005
+ this.clientHeight = clientHeight
1006
+ this.clientWidth = clientWidth
1007
+ this.localVideo.w = clientWidth
1008
+ this.localVideo.h = clientHeight
1009
+
1010
+ if (this.showLocation) {
1011
+ this.startLocate()
1012
+ }
1013
+ }, 200)
1014
+
1015
+ this.$nextTick(() => {
1016
+ this.showAudioPlayer = false
1017
+ })
1018
+ },
1019
+
1020
+ beforeDestroy(){
1021
+ this.stopDurationCount();
1022
+ this.sipMix = null;
1023
+ SipMix.destroyInstance()
1024
+ }
1025
+ }
1026
+ </script>
1027
+ <!-- <style>
1028
+ @import './vuetify.css';
1029
+ </style> -->
1030
+ <style lang="scss">
1031
+ .phone-nmain{
1032
+ background-color: #737778;
1033
+ padding: 25% 10px 20% !important;
1034
+ width: 100vw;
1035
+ height: 100vh;
1036
+ overflow: hidden;
1037
+
1038
+
1039
+ .sip-call-wrapper {
1040
+ position: absolute;
1041
+ left: 0;
1042
+ top: 0;
1043
+ width: 100vw;
1044
+ height: 100vh;
1045
+ z-index: 100;
1046
+ }
1047
+
1048
+ .sip-btn-group {
1049
+ width: 100%;
1050
+ position: absolute;
1051
+ //bottom: 250px;
1052
+ bottom: 25%;
1053
+ z-index: 4000;
1054
+ }
1055
+
1056
+ @media screen and (min-width: 1024px) {
1057
+ .sip-btn-group {
1058
+ position: absolute;
1059
+ bottom: 40px;
1060
+ width: 320px;
1061
+ z-index: 4000;
1062
+ }
1063
+ }
1064
+
1065
+ .sip-call-video {
1066
+ width: 100%;
1067
+ height: 100%;
1068
+ object-fit: cover;
1069
+ background-color: #000000;
1070
+ }
1071
+
1072
+ .video-panel {
1073
+ position: absolute;
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ justify-content: space-around;
1077
+ align-items: center;
1078
+ }
1079
+
1080
+ .video-timer {
1081
+ background-color: rgba(255,255,255,0.80);
1082
+ position: absolute;
1083
+ top: 40px;
1084
+ left: 16px;
1085
+ font-size: 14px;
1086
+ font-weight: bold;
1087
+ padding: 2px 8px 2px 6px;
1088
+ border-radius: 12px;
1089
+ z-index: 2000;
1090
+ display: flex;
1091
+ justify-content: space-around;
1092
+ align-items: center;
1093
+ }
1094
+
1095
+ .video-switch-btn {
1096
+ position: absolute !important;
1097
+ top: 40px;
1098
+ right: 80px;
1099
+ z-index: 2000;
1100
+ img {
1101
+ background-color: #000000;
1102
+ border-radius: 6px;
1103
+ border: 1px solid #000000;
1104
+ }
1105
+ }
1106
+
1107
+ .video-mini-mode {
1108
+ position: absolute !important;
1109
+ top: 40px;
1110
+ right: 48px;
1111
+ z-index: 2000;
1112
+ }
1113
+
1114
+ .camera-switch-btn {
1115
+ position: absolute !important;
1116
+ top: 40px;
1117
+ right: 16px;
1118
+ z-index: 2000;
1119
+ }
1120
+
1121
+ .video-normal-mode {
1122
+ position: absolute !important;
1123
+ top: 16px;
1124
+ right: 16px;
1125
+ z-index: 2000;
1126
+ }
1127
+
1128
+ .location-wrapper {
1129
+ position: absolute;
1130
+ top: 0;
1131
+ right: 0;
1132
+ width: 150px;
1133
+ background-color: #00000055;
1134
+ color: #FFFFFF;
1135
+ padding: 6px;
1136
+ p {
1137
+ font-size: 12px;
1138
+ margin: 0;
1139
+ padding: 0;
1140
+ width: 100%;
1141
+ white-space: nowrap;
1142
+ text-overflow: ellipsis;
1143
+ }
1144
+ }
1145
+ }
1146
+ </style>
1147
+ <style scoped lang="scss">
1148
+ .phone-nmain{
1149
+ .custom-mute-btn {
1150
+ background: transparent !important;
1151
+ box-shadow: none !important;
1152
+ z-index: 9999 !important;
1153
+ min-width: 0 !important;
1154
+ width: auto !important;
1155
+ height: auto !important;
1156
+ padding: 0 !important;
1157
+ margin: 0 !important;
1158
+ }
1159
+
1160
+ /* 移除按钮悬停和激活状态的背景色 */
1161
+ .custom-mute-btn::before {
1162
+ background-color: transparent !important;
1163
+ opacity: 0 !important;
1164
+ }
1165
+
1166
+ .btn-box{
1167
+ padding: 0 90px !important; // 0113
1168
+ margin-left: -100px !important; // 0113
1169
+ }
1170
+ /* 移除按钮的点击效果 */
1171
+ .custom-mute-btn:hover::before,
1172
+ .custom-mute-btn:focus::before,
1173
+ .custom-mute-btn:active::before {
1174
+ opacity: 0 !important;
1175
+ }
1176
+
1177
+ .transparent-btn {
1178
+ background: transparent !important;
1179
+ box-shadow: none !important;
1180
+ min-width: 0 !important;
1181
+ width: auto !important;
1182
+ height: auto !important;
1183
+ padding: 0 !important;
1184
+ margin: 0 !important;
1185
+ /* opacity: .6 !important; */
1186
+ position: absolute !important;
1187
+ left: calc(50% + 80px) !important;
1188
+ color: #FFFFFF !important;
1189
+ }
1190
+ .transparent-btn::before {
1191
+ background-color: transparent !important;
1192
+ opacity: 0 !important;
1193
+ }
1194
+
1195
+ .call-btn{
1196
+ /* margin-top:160px; */
1197
+ position: relative;
1198
+ top: 0%;
1199
+ }
1200
+ .call-img{
1201
+ width:100px;
1202
+ height:100px;
1203
+ border-radius: 50%;
1204
+ }
1205
+
1206
+
1207
+ .call-label{
1208
+ font-size: 16px;
1209
+ color: #FFFFFF
1210
+ }
1211
+
1212
+ .cancel-btn{
1213
+ opacity: 1;
1214
+ background: transparent !important;
1215
+ box-shadow: none !important;
1216
+ position: relative !important;
1217
+ margin-top: -50px !important;
1218
+ }
1219
+ .cancel-img {
1220
+ width: 80px;
1221
+ height: 80px;
1222
+ }
1223
+ .cancel-label{
1224
+ top: 78px;
1225
+ position: absolute; color: #FFFFFF;
1226
+ font-size: 16px;
1227
+ }
1228
+
1229
+
1230
+ .call-duration{
1231
+ font-size: 14px;margin-top: -12.5px; color: #FFFFFF;
1232
+ }
1233
+ .hangup-img{
1234
+ width: 80px;
1235
+ height: 80px;
1236
+ }
1237
+ .hangup-label{
1238
+ top: 73px;
1239
+ position: absolute; color: #FFFFFF;
1240
+ font-size: 14px;
1241
+ }
1242
+
1243
+ .img-open-close{
1244
+ width: 55px;
1245
+ height: 80px;
1246
+ }
1247
+ }
1248
+ /* 隐藏整个控制栏背景 */
1249
+ video::-webkit-media-controls {
1250
+ opacity: 1 !important;
1251
+ height: 20px !important;
1252
+ }
1253
+
1254
+ /* 隐藏进度条(核心) */
1255
+ video::-webkit-media-controls-timeline {
1256
+ display: none !important;
1257
+ }
1258
+
1259
+ /* 隐藏当前时间 */
1260
+ video::-webkit-media-controls-current-time-display {
1261
+ display: none !important;
1262
+ }
1263
+
1264
+ /* 隐藏总时长 */
1265
+ video::-webkit-media-controls-time-remaining-display {
1266
+ display: none !important;
1267
+ }
1268
+
1269
+ /* 完全禁用播放/暂停按钮交互 */
1270
+ video::-webkit-media-controls-play-button {
1271
+ pointer-events: none !important;
1272
+ }
1273
+
1274
+ /* 禁用暂停按钮交互 */
1275
+ video::-webkit-media-controls-pause-button {
1276
+ pointer-events: none !important;
1277
+ }
1278
+
1279
+ /* 禁用整个播放/暂停控制区域交互 */
1280
+ video::-webkit-media-controls-start-playback-button {
1281
+ pointer-events: none !important;
1282
+ }
1283
+
1284
+
1285
+ /* 隐藏音量按钮(可选) */
1286
+ video::-webkit-media-controls-volume-slider {
1287
+ display: none !important;
1288
+ }
1289
+ </style>