@94ai/softphone 4.0.1

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,1986 @@
1
+ import {
2
+ Inviter,
3
+ Registerer,
4
+ SessionState,
5
+ URI,
6
+ UserAgent
7
+ } from 'sip.js'
8
+ import { SessionDescriptionHandler } from 'sip.js/lib/api/session-description-handler'
9
+ import { UserAgentDelegate } from 'sip.js/lib/api/user-agent-delegate'
10
+ import { InvitationRejectOptions } from 'sip.js/lib/api/invitation-reject-options'
11
+ import { IncomingResponse } from 'sip.js/lib/core/messages/incoming-response'
12
+ import { RegistererUnregisterOptions } from 'sip.js/lib/api/registerer-unregister-options'
13
+ import { Invitation } from 'sip.js/lib/api/invitation'
14
+ import { OutgoingRequest } from 'sip.js/lib/core/messages/outgoing-request'
15
+ import {
16
+ ajaxOption,
17
+ HandUpInviteOption,
18
+ InvitationAcceptOptionsExtend as InvitationAcceptOptions,
19
+ InviterInviteOptionsExtend as InviterInviteOptions,
20
+ PrepareUserAgentOptions,
21
+ PushData,
22
+ RegisterOptions,
23
+ SessionInfoOptionsExtend as SessionInfoOptions,
24
+ UserAgentDelegateKey,
25
+ UserAgentOptions,
26
+ UserAgentStatusKey
27
+ } from '../types'
28
+ import {
29
+ AfterTransferLabour,
30
+ AiTaskVoiceType,
31
+ AutoAnswer,
32
+ userAgentDefault,
33
+ userAgentStatus
34
+ } from '../const'
35
+ import {
36
+ getAjax,
37
+ isDomainOrIP
38
+ } from '../utils'
39
+ import CryptoJS from 'crypto-js'
40
+ import onChange from 'on-change'
41
+ import FingerprintJS from '@fingerprintjs/fingerprintjs'
42
+
43
+ /**
44
+ * 代理实例类
45
+ */
46
+ export class UserAgentManager {
47
+ #extraHeaders: Array<string> | undefined
48
+ #userAgent: InstanceType<typeof UserAgent> | undefined = undefined
49
+ #registerer: Registerer | undefined = undefined
50
+ #sessionDescriptionHandler: SessionDescriptionHandler | undefined = undefined
51
+ #peerConnection: RTCPeerConnection | undefined = undefined
52
+ #receivers: RTCRtpReceiver[] | [] = []
53
+ #senders: RTCRtpSender[] | [] = []
54
+ #userAgentOption: Partial<UserAgentOptions> = {}
55
+ #currentInvitation: Invitation | undefined = undefined
56
+ #currentInviter: InstanceType<typeof Inviter> | undefined = undefined
57
+ #stream: MediaStream | undefined = new MediaStream()
58
+ #userAgentStatus: typeof userAgentStatus = JSON.parse(JSON.stringify(userAgentStatus))
59
+ #volumeValue = 0.00
60
+ #sign: string
61
+ #timestamp: string | number
62
+ #token: string
63
+ token: string
64
+ #tokenTimestamp: string | number
65
+ #signExpirationTime = 8 * 60 * 1000
66
+ #tokenExpirationTime = 200 * 60 * 1000
67
+ #pushData: Record<string, PushData> = {}
68
+ /**
69
+ * 坐席讲话音量
70
+ */
71
+ public get volumeValue () {
72
+ return this.#volumeValue
73
+ }
74
+ #mediaStreamAudioSourceNode: MediaStreamAudioSourceNode
75
+ #scriptProcessorNode :ScriptProcessorNode
76
+ #mediaStream: MediaStream
77
+ #refreshSpeekVolumn: Function = (param: number) => {}
78
+ #refreshChat: Function = (pushData: PushData) => {}
79
+ #openXhrIntercept: Function = (xhr: any) => {}
80
+ #gatewayXhrIntercept: Function = (xhr: any) => {}
81
+ #signCheck: Function = async (expirationTime: number) => {
82
+ if (this.#sign && this.#timestamp) {
83
+ if (Date.now() - (this.#timestamp as number) > expirationTime) {
84
+ const { sign, timestamp } = await this.#signOverdued()
85
+ this.#sign = sign ?? this.#sign
86
+ this.#timestamp = timestamp ?? this.#timestamp
87
+ }
88
+ }
89
+ }
90
+ #tokenCheck: Function = async (expirationTime: number) => {
91
+ if (this.#tokenTimestamp) {
92
+ if (Date.now() - (this.#tokenTimestamp as number) > expirationTime) {
93
+ if (this.#token) {
94
+ const { token, tokenTimestamp } = await this.#tokenOverdued()
95
+ this.#token = token ?? this.#token
96
+ this.#tokenTimestamp = tokenTimestamp ?? Date.now()
97
+ } else if (this.token) {
98
+ const {
99
+ token
100
+ } = await this.getGatewayAccessToken({
101
+ corpId: this.#appKey,
102
+ sid: this.#agentId,
103
+ secret: this.#appSecret,
104
+ seatOnline: false
105
+ })
106
+ this.token = token
107
+ this.#tokenTimestamp = Date.now()
108
+ }
109
+ }
110
+ }
111
+ }
112
+ #signOverdued: Function = () => {}
113
+ #tokenOverdued: Function = () => {}
114
+ #refreshChatErrorCallback: Function = (pushData: PushData) => {}
115
+ #refreshRequirementCheck: Function = (param: {
116
+ networkSpeed: string
117
+ networkDelay: string
118
+ authorizedMicrophonePermissions: boolean
119
+ authorizedNotificationPermissions: boolean
120
+ }) => {}
121
+
122
+ #testingTimer: ReturnType<typeof setTimeout> | undefined // 测速定时器
123
+ #oneRoundOfTestingTimer: ReturnType<typeof setTimeout> | undefined // 单轮检测定时器
124
+ public networkSpeed = '0.00Mb/s'// 带宽速度
125
+ public networkDelay = '0ms' // 网络延迟
126
+ public onLine = navigator.onLine // 是否在线
127
+ public testspeeding = false // 测速中
128
+ public goodNetworkSpeed = (): boolean => { // 网络状态是否良好
129
+ return Number(this.networkSpeed.replace('Mb/s', '')) * 1000 >= 30
130
+ }
131
+ public goodNetworkDelay = (): boolean => { // 网络状态是否良好
132
+ return Number(this.networkDelay.replace('ms', '')) <= 500 && this.networkDelay !== '0ms'
133
+ }
134
+ public authorizedMicrophonePermissions = false
135
+ public authorizedNotificationPermissions = false
136
+
137
+ /**
138
+ * 检测端服务端宕机,重启,网络抖动出现服务不可达时配置重连
139
+ * */
140
+ /** 重连最大次数 */
141
+ #reconnectionAttempts = 100
142
+ /** 重连间隔 */
143
+ #reconnectionInterval = 3
144
+
145
+ /**
146
+ * 检测服务端保活,默认多久发一次sip options信号,检查服务端状态是否可达
147
+ * */
148
+ #optionsPingInterval = 8
149
+ /** 可能由于网络抖动导致ping失败,(这个时候用户可能在接听电话,不要重连影响通话)这个时候服务器是正常,增加尝试次数确保服务器是真的在重启 */
150
+ #optionsPingAttempts = 3
151
+ #optionsPingAttempt = 0
152
+ #optionsPingFailure = false
153
+ #optionsPingRequest: OutgoingRequest
154
+ /** 重新注册间隔 */
155
+ #registerInterval = 3
156
+ #registerAttempts = 3
157
+ #registerAttempt = 0
158
+
159
+ /**
160
+ * 客户端保活,默认多久发一次"\r\n\r\n"socket信号,告诉服务端客户端还在,不要发送close信号
161
+ * */
162
+ #keepAliveInterval = 0
163
+ #keepAliveDebounce = 0
164
+
165
+ /**
166
+ * 节流用户可能手动触发
167
+ * */
168
+ #reconnectionAttempting = false
169
+ #optionsPingRunning = false
170
+ #registerAttempting = false
171
+
172
+ /**
173
+ * 终止用户可能手动结束
174
+ * */
175
+ #reconnectionTimer: ReturnType<typeof setTimeout> | undefined
176
+ #optionsPingTimer: ReturnType<typeof setTimeout> | undefined
177
+ #registerTimer: ReturnType<typeof setTimeout> | undefined
178
+
179
+ #appKey: string
180
+ #appSecret: string
181
+ #agentTag: string
182
+ #agentId: number
183
+ #openBaseUrl?: string
184
+ #sgOpen?: '0' | '1'
185
+ #sg?: '0' | '1'
186
+ #visitorId?: string
187
+
188
+ /** 断网重连 */
189
+ #attemptReconnectionHandler = (e: Event) => {
190
+ this.#attemptReconnection()
191
+ }
192
+ #empty = () => {
193
+ }
194
+ #businessAttribute: Record<string, any> = {}
195
+
196
+ public get businessAttribute () {
197
+ return this.#businessAttribute
198
+ }
199
+
200
+ #enableChatInfoPush = false
201
+ #enableVolumnTrack = true
202
+
203
+ /**
204
+ * 每次来电是否自动接听
205
+ */
206
+ public ifAutoAnswer = () => {
207
+ return String(this.#businessAttribute.autoAnswer) === AutoAnswer.YES
208
+ }
209
+
210
+ /**
211
+ * 每次来电是否需要转人工
212
+ */
213
+ public notNeedSendStarDtmf = () => {
214
+ return String(this.#businessAttribute.afterTransferLabour) === AfterTransferLabour.DIRECT_ANSWER
215
+ }
216
+
217
+ /**
218
+ * 是否快速外呼
219
+ */
220
+ getFastOutboundCall() {
221
+ return this.#businessAttribute.voiceType === AiTaskVoiceType.LABOUR
222
+ }
223
+
224
+
225
+ getSitOutboundCall() {
226
+ return this.#businessAttribute.voiceType === AiTaskVoiceType.AI
227
+ }
228
+
229
+ constructor (userAgentOption: Partial<UserAgentOptions> = {}) {
230
+ if (typeof userAgentOption !== 'object') {
231
+ throw new Error('userAgentOption must be plain object')
232
+ }
233
+ this.#initOpenApiOption(userAgentOption)
234
+ this.#initGatewayApiOption(userAgentOption)
235
+ this.#initExtraHeaders(userAgentOption)
236
+ this.#initKeepAliveOptions(userAgentOption)
237
+ this.#initRefreshCallback(userAgentOption)
238
+ this.#initIntercept(userAgentOption)
239
+ this.#userAgentOption = this.#getUserAgentOption(userAgentOption)
240
+ }
241
+
242
+
243
+ #trackVolume = () => {
244
+ if (this.#enableVolumnTrack) {
245
+ this.#stopTrackVolume()
246
+ // @ts-ignore
247
+ const AudioContext = window.AudioContext || window.webkitAudioContext
248
+ const audioContext = new AudioContext()
249
+ this.#scriptProcessorNode = audioContext.createScriptProcessor(2048, 1, 1)
250
+ this.#scriptProcessorNode.onaudioprocess = (event: AudioProcessingEvent) => {
251
+ const input = event.inputBuffer.getChannelData(0) // 得到一个长度为2048的数组
252
+ let sum = 0.00
253
+ for (let i = 0; i < input.length; ++i) {
254
+ sum += input[i] * input[i]
255
+ }
256
+ this.#volumeValue = Number(Math.sqrt(sum / input.length).toFixed(2))
257
+ this.#refreshSpeekVolumn(this.#volumeValue)
258
+ }
259
+ this.#mediaStream = new MediaStream()
260
+ this.getSenders().forEach((sender) => {
261
+ if (sender.track) {
262
+ this.#mediaStream.addTrack(sender.track)
263
+ }
264
+ })
265
+ this.#mediaStreamAudioSourceNode = audioContext.createMediaStreamSource(this.#mediaStream)
266
+ this.#mediaStreamAudioSourceNode.connect(this.#scriptProcessorNode)
267
+ this.#scriptProcessorNode.connect(audioContext.destination)
268
+ }
269
+ }
270
+
271
+ #stopTrackVolume = () => {
272
+ if (this.#enableVolumnTrack) {
273
+ if (this.#mediaStreamAudioSourceNode) {
274
+ try {
275
+ this.#mediaStreamAudioSourceNode.disconnect()
276
+ } catch (e) {
277
+ console.log(e)
278
+ }
279
+ }
280
+ if (this.#scriptProcessorNode) {
281
+ try {
282
+ this.#scriptProcessorNode.disconnect()
283
+ } catch (e) {
284
+ console.log(e)
285
+ }
286
+ }
287
+ if (this.#mediaStream) {
288
+ try {
289
+ this.#mediaStream.getTracks().forEach(track => {
290
+ track.stop()
291
+ })
292
+ } catch (e) {
293
+ console.log(e)
294
+ }
295
+ }
296
+ }
297
+ this.#mediaStreamAudioSourceNode = undefined
298
+ this.#scriptProcessorNode = undefined
299
+ this.#mediaStream = undefined
300
+ this.#volumeValue = 0.00
301
+ this.#refreshSpeekVolumn(this.#volumeValue)
302
+ }
303
+
304
+ public disposeSoftphoneEnvRequirementCheck = () => {
305
+ this.#clearSoftphoneEnvRequirementTimer()
306
+ // this.networkSpeed = '0.00Mb/s'
307
+ // this.networkDelay = '0ms'
308
+ window.removeEventListener('offline', this.#offLineAction)
309
+ window.removeEventListener('online', this.#onlineAction)
310
+ window.removeEventListener('unload', this.disposeSoftphoneEnvRequirementCheck)
311
+ }
312
+ #clearSoftphoneEnvRequirementTimer = () => {
313
+ clearInterval(this.#testingTimer)
314
+ this.#testingTimer = undefined
315
+ clearTimeout(this.#oneRoundOfTestingTimer)
316
+ this.#oneRoundOfTestingTimer = undefined
317
+ this.testspeeding = false
318
+ }
319
+
320
+ /**
321
+ * 软电话环境检测
322
+ */
323
+ public softphoneEnvCheck = async () => {
324
+ if (!this.testspeeding) {
325
+ this.#realTimeSyncStatus()
326
+ this.oneRoundOfTesting()
327
+ }
328
+ }
329
+ #realTimeSyncStatus = () => {
330
+ window.addEventListener('offline', this.#offLineAction)
331
+ window.addEventListener('online', this.#onlineAction)
332
+ window.addEventListener('unload', this.disposeSoftphoneEnvRequirementCheck)
333
+ }
334
+ public oneRoundOfTesting = async () => {
335
+ this.testspeeding = true
336
+ this.detectNavigatorPermissions()
337
+ if (this.onLine) {
338
+ this.#detectNetwork(this)
339
+ this.#oneRoundOfTestingTimer = setTimeout(() => {
340
+ this.testspeeding = false
341
+ this.disposeSoftphoneEnvRequirementCheck()
342
+ }, 4000)
343
+ } else {
344
+ this.testspeeding = false
345
+ }
346
+ }
347
+
348
+ #onlineAction = () => {
349
+ this.onLine = true
350
+ this.oneRoundOfTesting()
351
+ }
352
+ #offLineAction = () => {
353
+ this.onLine = false
354
+ this.networkSpeed = '0.00Mb/s'
355
+ this.networkDelay = '0ms'
356
+ this.#refreshRequirementCheck({
357
+ networkSpeed: this.networkSpeed,
358
+ networkDelay: this.networkDelay,
359
+ authorizedMicrophonePermissions: this.authorizedMicrophonePermissions,
360
+ authorizedNotificationPermissions: this.authorizedNotificationPermissions
361
+ })
362
+ this.#clearSoftphoneEnvRequirementTimer()
363
+ }
364
+
365
+ /**
366
+ * 检测麦克风权限
367
+ */
368
+ public detectNavigatorPermissions () {
369
+ if (navigator.userAgent.indexOf('Firefox') === -1) {
370
+ // @ts-ignore
371
+ navigator.permissions.query({ name: 'microphone' })
372
+ .then((permissionObj) => {
373
+ if (permissionObj.state === 'granted') {
374
+ this.authorizedMicrophonePermissions = true
375
+ } else {
376
+ this.authorizedMicrophonePermissions = false
377
+ }
378
+ })
379
+ .catch((error) => {
380
+ console.log('Got error :', error)
381
+ this.authorizedMicrophonePermissions = false
382
+ })
383
+ } else {
384
+ navigator.mediaDevices.enumerateDevices().then(info => {
385
+ if (info.filter(({ kind }) => kind === 'audioinput')[0].label !== '') {
386
+ this.authorizedMicrophonePermissions = true
387
+ } else {
388
+ this.authorizedMicrophonePermissions = false
389
+ }
390
+ })
391
+ }
392
+ navigator.permissions.query({ name: 'notifications' })
393
+ .then((permissionObj) => {
394
+ if (permissionObj.state === 'granted') {
395
+ this.authorizedNotificationPermissions = true
396
+ } else {
397
+ this.authorizedNotificationPermissions = false
398
+ }
399
+ })
400
+ .catch((error) => {
401
+ console.log('Got error :', error)
402
+ this.authorizedNotificationPermissions = false
403
+ })
404
+ }
405
+
406
+ #detectNetwork = async (instance?: InstanceType<typeof UserAgentManager>) => {
407
+ const that = instance || this
408
+ clearInterval(that.#testingTimer)
409
+ that.#testingTimer = undefined
410
+ const testNetWork = async () => {
411
+ if (that.onLine) {
412
+ try {
413
+ const result = await that.#getNetSpeed()
414
+ that.networkSpeed = result.networkSpeed
415
+ that.networkDelay = result.networkDelay
416
+ } catch (err) {
417
+ that.networkSpeed = '0.00Mb/s'
418
+ that.networkDelay = '0ms'
419
+ }
420
+ } else {
421
+ that.networkSpeed = '0.00Mb/s'
422
+ that.networkDelay = '0ms'
423
+ }
424
+ this.#refreshRequirementCheck({
425
+ networkSpeed: this.networkSpeed,
426
+ networkDelay: this.networkDelay,
427
+ authorizedMicrophonePermissions: this.authorizedMicrophonePermissions,
428
+ authorizedNotificationPermissions: this.authorizedNotificationPermissions
429
+ })
430
+ if (!this.#oneRoundOfTestingTimer) {
431
+ this.testspeeding = false
432
+ this.disposeSoftphoneEnvRequirementCheck()
433
+ }
434
+ }
435
+ await testNetWork()
436
+ if (this.#oneRoundOfTestingTimer) {
437
+ that.#testingTimer = setInterval(async function () {
438
+ await testNetWork()
439
+ }, 1000)
440
+ }
441
+ }
442
+
443
+ #getNetSpeed = async (imgUrl: string = ('https://seat.94ai.com/favicon.png'), fileSize: number = 63491, times = 1) => {
444
+ // @ts-ignore
445
+ // const connection = window.navigator.connection // 该api火狐和苹果浏览器不支持,而且不准
446
+ // if (connection && connection.networkSpeed) {
447
+ // return connection.networkSpeed + 'Mb/s'
448
+ // }
449
+ // 多次测速求平均值
450
+ const arr = []
451
+ for (let i = 0; i < times; i++) {
452
+ arr.push(this.#getSpeedWithImg(imgUrl + '?d=' + Date.now() + i, fileSize))
453
+ }
454
+ return Promise.all(arr).then(speeds => {
455
+ let sum = 0
456
+ let sumSpeed = 0
457
+ speeds.forEach((speed:any) => {
458
+ sum += Number(speed[0])
459
+ sumSpeed += Number(speed[1])
460
+ })
461
+ return {
462
+ networkDelay: (sumSpeed / times).toFixed(2) + 'ms',
463
+ networkSpeed: (sum / times).toFixed(2) + 'Mb/s'
464
+ }
465
+ })
466
+ }
467
+
468
+ #getSpeedWithImg = (imgUrl: string = ('https://seat.94ai.com/favicon.png?d=' + Date.now()), fileSize: number = 63491) => {
469
+ return new Promise((resolve, reject) => {
470
+ const img: any = document.createElement('img')
471
+ img.start = window.performance.now()
472
+ img.onload = function () {
473
+ const end = window.performance.now()
474
+ const delay = end - img.start
475
+ const speed = (fileSize * 1000) / (1024 * 1024 * delay)
476
+ resolve([speed.toFixed(2), delay])
477
+ }
478
+ img.onerror = function (e: Error) {
479
+ reject(e)
480
+ }
481
+ img.src = imgUrl
482
+ }).catch(err => {
483
+ throw err
484
+ })
485
+ }
486
+
487
+ public openApiAjax = (param: Partial<ajaxOption>): Promise<any>=>{
488
+ return new Promise(async (resolve, reject) => {
489
+ const { xhr, ajaxData, contentType } = getAjax(resolve, reject, param)
490
+ const { timestamp, sign } = await this.getOpenApiSign()
491
+ xhr.setRequestHeader('appKey', this.#appKey)
492
+ xhr.setRequestHeader('timestamp', String(timestamp))
493
+ xhr.setRequestHeader('sign', sign)
494
+ xhr.setRequestHeader("Content-type",contentType || "application/json")
495
+ await this.#openXhrIntercept(xhr)
496
+ xhr.send(ajaxData)
497
+ })
498
+ }
499
+
500
+ public getGateway() {
501
+ if (this.#sg === '1') {
502
+ return 'https://gateway.sg.94ai.com'
503
+ }
504
+ // return 'http://gateway.test.k8s.com:31962'
505
+ return 'https://gateway.94ai.com'
506
+ }
507
+
508
+ public requestGateway = (param: Partial<ajaxOption>, needToken = true) => {
509
+ return this.gatewayApiAjax({
510
+ ...param,
511
+ url: this.getGateway() + param.url,
512
+ }, needToken)
513
+ }
514
+
515
+ public gatewayApiAjax = (param: Partial<ajaxOption>, needToken = true): Promise<any>=>{
516
+ return new Promise(async (resolve, reject) => {
517
+ const { xhr, ajaxData, contentType } = getAjax(resolve, reject, param)
518
+ if (needToken) {
519
+ const token = await this.getGatewayToken()
520
+ xhr.setRequestHeader('x-token', token)
521
+ xhr.setRequestHeader('seatsToken', token)
522
+ xhr.setRequestHeader('x-authority-token', token)
523
+ }
524
+ xhr.setRequestHeader('x-app-code', 3)
525
+ xhr.setRequestHeader("content-type",contentType || "application/json")
526
+ await this.#gatewayXhrIntercept(xhr)
527
+ xhr.send(ajaxData)
528
+ })
529
+ }
530
+
531
+ public async getGatewayToken() {
532
+ await this.#tokenCheck(this.#tokenExpirationTime)
533
+ if (this.#token) {
534
+ return this.#token
535
+ }
536
+ if (!this.token) {
537
+ await this.#openApiRequirementAssign()
538
+ const {
539
+ token
540
+ } = await this.getGatewayAccessToken({
541
+ corpId: this.#appKey,
542
+ sid: this.#agentId,
543
+ secret: this.#appSecret,
544
+ seatOnline: false
545
+ })
546
+ this.token = token
547
+ this.#tokenTimestamp = Date.now()
548
+ }
549
+ return this.token
550
+ }
551
+
552
+ public getGatewayAccessToken(params: {
553
+ corpId: string,
554
+ secret: string,
555
+ sid: number,
556
+ seatOnline: boolean
557
+ }){
558
+ return this.requestGateway({
559
+ url: '/authority/accessToken/openApi',
560
+ data: params,
561
+ }, false)
562
+ }
563
+
564
+ public getCallNumberDetail(params: {
565
+ id: string,
566
+ taskId: string
567
+ }){
568
+ return this.requestGateway({
569
+ url: '/task-aggre/callCenterNumber/get',
570
+ data: params,
571
+ })
572
+ }
573
+
574
+ public getCallNumberChats(params: {
575
+ callId: string,
576
+ taskId: string
577
+ }){
578
+ return this.requestGateway({
579
+ url: '/dialogue-aggre/call-center-number/chat/list',
580
+ data: params,
581
+ })
582
+ }
583
+
584
+ async getCallInfo() {
585
+ const callIdPushData: any = {
586
+ ...this.#businessAttribute
587
+ }
588
+ const polling = async () => {
589
+ if (callIdPushData.numberId && callIdPushData.taskId) {
590
+ let result: Record<string, any> = {}
591
+ try {
592
+ result = (await this.getCallNumberDetail({ id: callIdPushData.numberId, taskId: callIdPushData.taskId }))
593
+ if (result.code !== 200) {
594
+ console.error(result.message)
595
+ callIdPushData.stop = true
596
+ this.#refreshChatErrorCallback(new Error(result.message))
597
+ return
598
+ }
599
+ } catch (e) {
600
+ const message = `当前会话callid:${callIdPushData.callId},根据numberId:${callIdPushData.numberId},taskId:${callIdPushData.taskId}:查询坐席接听状态异常`
601
+ console.error(message)
602
+ callIdPushData.stop = true
603
+ this.#refreshChatErrorCallback(new Error(result.message))
604
+ return
605
+ }
606
+ const {
607
+ callType,
608
+ newIntentTag,
609
+ intentTag,
610
+ number,
611
+ numberMd5,
612
+ sid,
613
+ config,
614
+ tag,
615
+ state
616
+ } = result.data;
617
+ if (state === 10) { // 用户已挂机
618
+ console.log(`当前会话callid:${callIdPushData.callId}已结束`)
619
+ callIdPushData.stop = true
620
+ // this.#sessionEndCallback(pushData.callid!)
621
+ return
622
+ }
623
+
624
+ callIdPushData.callType ||= callType
625
+ callIdPushData.intentTag = newIntentTag || intentTag
626
+ callIdPushData.number ||= number
627
+ callIdPushData.numberMD5 ||= numberMd5
628
+ callIdPushData.agentId ||= sid
629
+ callIdPushData.tag ||= tag
630
+
631
+ if (!callIdPushData.templateId) {
632
+ if (config && typeof config === 'string') {
633
+ const configP = JSON.parse(config)
634
+ if (configP && configP.templateId) {
635
+ callIdPushData.templateId = configP.templateId
636
+ }
637
+ }
638
+ }
639
+ if (callIdPushData.callId) {
640
+ let res: Record<string, any> = {}
641
+ try {
642
+ res = await this.getCallNumberChats({
643
+ callId: callIdPushData.callId,
644
+ taskId: callIdPushData.taskId
645
+ })
646
+ if (res.code !== 200) {
647
+ console.error(res.message)
648
+ callIdPushData.stop = true
649
+ this.#refreshChatErrorCallback(new Error(res.message))
650
+ return
651
+ }
652
+ } catch (e) {
653
+ const message = `当前会话根据callId:${callIdPushData.callId},taskId:${callIdPushData.taskId}:查询会话对话记录异常`
654
+ console.error(message)
655
+ callIdPushData.stop = true
656
+ this.#refreshChatErrorCallback(new Error(message))
657
+ return
658
+ }
659
+ callIdPushData.chats = res.data.map((v: Record<string, any>) => {
660
+ if (v.matchinfo) {
661
+ v.matchinfo = JSON.parse(v.matchinfo)
662
+ }
663
+ return v
664
+ })
665
+ this.#pushData[callIdPushData.callId] = callIdPushData
666
+ }
667
+ try {
668
+ this.#refreshChat(this.#pushData[callIdPushData.callId])
669
+ } catch (e) {
670
+ console.log(e)
671
+ }
672
+ } else {
673
+ callIdPushData.stop = true
674
+ if (!callIdPushData.taskId) {
675
+ const message = `当前会话callid:${callIdPushData.callId}的taskId不存在,无法查询通话记录`
676
+ console.error(message)
677
+ this.#refreshChatErrorCallback(new Error(message))
678
+ }
679
+ if (!callIdPushData.numberId) {
680
+ const message = `当前会话callid:${callIdPushData.callId}的numberId不存在,无法查询通话记录`
681
+ console.error(message)
682
+ this.#refreshChatErrorCallback(new Error(message))
683
+ }
684
+ }
685
+ }
686
+ try {
687
+ while (true) {
688
+ if (callIdPushData.stop) break
689
+ await polling()
690
+ if (callIdPushData.stop) break
691
+ await new Promise((resolve) => {
692
+ let refreshMessageTimer: ReturnType<typeof setTimeout> | undefined = setTimeout(async () => {
693
+ clearTimeout(refreshMessageTimer)
694
+ refreshMessageTimer = undefined
695
+ resolve(true)
696
+ }, 3000)
697
+ })
698
+ }
699
+ } catch (e) {
700
+ this.#refreshChatErrorCallback(e as Error)
701
+ }
702
+ }
703
+
704
+ async refreshCallChatInfo() {
705
+ if (this.#businessAttribute.numberId && this.#businessAttribute.callId && this.#businessAttribute.taskId && this.#enableChatInfoPush) {
706
+ await this.getCallInfo()
707
+ }
708
+ }
709
+
710
+ public requestOpenApi = (param: Partial<ajaxOption>) => {
711
+ return this.openApiAjax({
712
+ ...param,
713
+ url: this.getOpenApi() + param.url,
714
+ })
715
+ }
716
+
717
+ public getOpenApi () {
718
+ if (this.#openBaseUrl) {
719
+ if (isDomainOrIP(this.#openBaseUrl)) {
720
+ return this.#openBaseUrl
721
+ }
722
+ if (this.#sg === '1') {
723
+ if (this.#sgOpen === '1') {
724
+ return 'https://seatsg.94ai.com/sgopenapi'
725
+ } else {
726
+ return 'https://seatsg.94ai.com/openapi'
727
+ }
728
+ } else {
729
+ if (this.#sgOpen === '1') {
730
+ return 'https://seat.94ai.com/sgopenapi'
731
+ } else {
732
+ return 'https://seat.94ai.com/openapi'
733
+ }
734
+ }
735
+ }
736
+ return 'https://seat.94ai.com/openapi'
737
+ }
738
+
739
+ public getAgentInfo () {
740
+ return this.requestOpenApi({
741
+ url: '/v1/agent/getAgent',
742
+ data: {
743
+ agentTag: this.#agentTag,
744
+ agentId: this.#agentId
745
+ }
746
+ })
747
+ }
748
+
749
+ public async getOpenApiSign(timestamp = Date.now(), appKey?: string, appSecret?: string) {
750
+ await this.#signCheck(this.#signExpirationTime)
751
+ return {
752
+ sign: this.#sign ?? CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA1(appKey || this.#appKey + timestamp, appSecret || this.#appSecret)),
753
+ timestamp: this.#timestamp ?? timestamp
754
+ }
755
+ }
756
+
757
+ public updateOpenApiAgentStatus (agentStatus: 1 | 2 | 3 | 4) {
758
+ return this.requestOpenApi({
759
+ url: '/v1/agent/updateAgentStatus',
760
+ data: {
761
+ agentTag: this.#agentTag,
762
+ agentId: this.#agentId,
763
+ agentStatus
764
+ }
765
+ })
766
+ }
767
+
768
+ public importOpenApiAgentCustomer (number: string) {
769
+ return this.requestOpenApi({
770
+ url: '/v1/task/importAgentCustomer',
771
+ data: {
772
+ agentTag: this.#agentTag,
773
+ agentId: this.#agentId,
774
+ callType: 1001,
775
+ customers: [{ number }]
776
+ }
777
+ })
778
+ }
779
+
780
+ /**
781
+ * 呼叫
782
+ */
783
+ public callNumber(number: string) {
784
+ return this.importOpenApiAgentCustomer(number)
785
+ }
786
+
787
+ /**
788
+ * 小休
789
+ */
790
+ public toggleNap(nap: boolean) {
791
+ if (nap) {
792
+ return this.updateOpenApiAgentStatus(3)
793
+ }
794
+ return this.updateOpenApiAgentStatus(1)
795
+ }
796
+ #initIntercept(userAgentOption: Partial<UserAgentOptions> = {}) {
797
+ this.#openXhrIntercept = userAgentOption.openXhrIntercept ?? this.#openXhrIntercept
798
+ this.#gatewayXhrIntercept = userAgentOption.gatewayXhrIntercept ?? this.#gatewayXhrIntercept
799
+ }
800
+
801
+ #initRefreshCallback(userAgentOption: Partial<UserAgentOptions> = {}) {
802
+ this.#enableChatInfoPush = userAgentOption.enableChatInfoPush ?? this.#enableChatInfoPush
803
+ this.#enableVolumnTrack = userAgentOption.enableVolumnTrack ?? this.#enableVolumnTrack
804
+ this.#refreshSpeekVolumn = userAgentOption.refreshSpeekVolumn ?? this.#refreshSpeekVolumn
805
+ this.#refreshRequirementCheck = userAgentOption.refreshRequirementCheck ?? this.#refreshRequirementCheck
806
+ this.#refreshChat = userAgentOption.refreshChat ?? this.#refreshChat
807
+ this.#refreshChatErrorCallback = userAgentOption.refreshChatErrorCallback ?? this.#refreshChatErrorCallback
808
+ }
809
+
810
+ #initGatewayApiOption(userAgentOption: Partial<UserAgentOptions> = {}) {
811
+ this.#token = userAgentOption.token ?? this.#token
812
+ this.#tokenTimestamp = userAgentOption.tokenTimestamp ?? this.#tokenTimestamp
813
+ this.#tokenExpirationTime = userAgentOption.tokenExpirationTime ?? this.#tokenExpirationTime
814
+ if (this.#token && !this.#tokenTimestamp) {
815
+ this.#tokenTimestamp = Date.now()
816
+ }
817
+ this.#tokenCheck = userAgentOption.tokenCheck ?? this.#tokenCheck
818
+ this.#tokenOverdued = userAgentOption.tokenOverdued ?? this.#tokenOverdued
819
+ }
820
+
821
+ #initOpenApiOption(userAgentOption: Partial<UserAgentOptions> = {}) {
822
+ const {
823
+ agentId,
824
+ agentTag,
825
+ appKey,
826
+ appSecret,
827
+ openBaseUrl,
828
+ sg,
829
+ sgOpen,
830
+ sign,
831
+ timestamp
832
+ } = userAgentOption
833
+ this.#agentId = agentId ?? this.#agentId
834
+ this.#agentTag = agentTag ?? this.#agentTag
835
+ this.#appKey = appKey ?? this.#appKey
836
+ this.#appSecret = appSecret ?? this.#appSecret
837
+ this.#openBaseUrl = openBaseUrl ?? this.#openBaseUrl
838
+ this.#sg = sg ?? this.#sg
839
+ this.#sgOpen = sgOpen ?? this.#sgOpen
840
+ this.#sign= sign ?? this.#sign
841
+ this.#timestamp = timestamp ?? this.#timestamp
842
+ this.#signExpirationTime = userAgentOption.signExpirationTime ?? this.#signExpirationTime
843
+ this.#signCheck = userAgentOption.signCheck ?? this.#signCheck
844
+ this.#signOverdued = userAgentOption.signOverdued ?? this.#signOverdued
845
+ }
846
+
847
+ /**
848
+ * 初始化扩展header配置
849
+ * @param { UserAgentOptions } userAgentOption
850
+ * @private
851
+ */
852
+ #initExtraHeaders (userAgentOption: Partial<UserAgentOptions>) {
853
+ if (userAgentOption.sipHeaders) {
854
+ this.#extraHeaders = userAgentOption.sipHeaders
855
+ delete userAgentOption.sipHeaders
856
+ }
857
+ }
858
+
859
+ public getDeviceId = async () => {
860
+ if (this.#visitorId) return this.#visitorId
861
+ const fp = await FingerprintJS.load()
862
+ const result = await fp.get()
863
+ this.#visitorId = result.visitorId
864
+ return this.#visitorId
865
+ }
866
+
867
+ #meetTheOpenApiRequirement() {
868
+ if ((this.#agentId || this.#agentTag) && ((this.#appKey && this.#appSecret) || (this.#sign && this.#timestamp))) {
869
+ return true
870
+ }
871
+ return false
872
+ }
873
+
874
+ refreshSign(sign: string) {
875
+ this.#sign = sign
876
+ }
877
+
878
+ #openApiRequirementAssign = async () => {
879
+ if (this.#meetTheOpenApiRequirement() &&
880
+ (!this.#userAgentOption.authorizationUsername ||
881
+ !this.#userAgentOption.uri ||
882
+ !this.#userAgentOption.authorizationPassword ||
883
+ !this.#userAgentOption.transportOptions.server
884
+ )
885
+ ) {
886
+ const angentData = await this.getAgentInfo()
887
+ if (angentData.code == 200) {
888
+ const angentInfo = angentData.data
889
+ this.#userAgentOption.authorizationUsername = angentInfo.agentExtension
890
+ this.#userAgentOption.authorizationPassword = angentInfo.extensionPwd
891
+ // @ts-ignore
892
+ this.#userAgentOption.uri = UserAgent.makeURI(`sip:${angentInfo.agentExtension}@${angentInfo.wsRegisterAddress}`)
893
+ this.#userAgentOption.transportOptions.server = `${angentInfo.wsProtocol}://${angentInfo.wsRegisterAddress}`
894
+ this.#agentId = angentInfo.agentId
895
+ this.#agentTag = angentInfo.agentTag
896
+ } else {
897
+ throw new Error(angentData)
898
+ }
899
+ }
900
+ }
901
+
902
+ #viaHostAssign = async () => {
903
+ if (!this.#userAgentOption.viaHost) {
904
+ this.#userAgentOption.viaHost = `${await this.getDeviceId()}.sip`
905
+ }
906
+ }
907
+
908
+ /**
909
+ * 签入
910
+ * @param config 配置
911
+ * @param { (path: keyof UserAgentStatusKey, value: boolean) => void } config.refresh 配置
912
+ * @param { import('sip.js/lib/api/registerer-options').RegistererOptions } config.registererOptions new Registerer Options
913
+ * @param { import('sip.js/lib/api/registerer-register-options').RegistererRegisterOptions } config.registererRegisterOptions Registerer Instance register Options
914
+ * @param { string[] } options.extraHeaders sip custom header
915
+ * @param { import('sip.js/lib/api/user-agent-delegate').UserAgentDelegate } event 事件
916
+ */
917
+ public async prepareUserAgent (config?: PrepareUserAgentOptions, event?: UserAgentDelegate) {
918
+ const {
919
+ refresh,
920
+ registererOptions,
921
+ registererRegisterOptions,
922
+ extraHeaders,
923
+ authorizationUsername,
924
+ authorizationPassword,
925
+ uri,
926
+ contactName,
927
+ transportOptions,
928
+ } = (config || {})
929
+ await this.dispose()
930
+ // @ts-ignore
931
+ this.#userAgentOption.uri = uri ?? this.#userAgentOption.uri
932
+ this.#userAgentOption.authorizationUsername = authorizationUsername ?? this.#userAgentOption.authorizationUsername
933
+ this.#userAgentOption.authorizationPassword = authorizationPassword ?? this.#userAgentOption.authorizationPassword
934
+ this.#userAgentOption.contactName = contactName ?? authorizationUsername ?? this.#userAgentOption.contactName
935
+ if (transportOptions) {
936
+ this.#userAgentOption.transportOptions = {
937
+ ...this.#userAgentOption.transportOptions,
938
+ ...transportOptions
939
+ }
940
+ }
941
+ try {
942
+ // @ts-ignore
943
+ this.#initOpenApiOption(config)
944
+ await this.#openApiRequirementAssign()
945
+ await this.#viaHostAssign()
946
+ await this.#connectUserAgent({ refresh })
947
+ if (event) {
948
+ this.#bindUserAgent(event)
949
+ }
950
+ const userAgent: InstanceType<typeof UserAgent> = await this.#registerUserAgent({
951
+ registererOptions,
952
+ registererRegisterOptions,
953
+ extraHeaders
954
+ })
955
+ return userAgent
956
+ } catch (e) {
957
+ await this.dispose()
958
+ throw e
959
+ }
960
+ }
961
+
962
+ /**
963
+ * 签出
964
+ */
965
+ public async dispose () {
966
+ try {
967
+ this.#clearStream()
968
+ this.#clearSenders()
969
+ this.#clearReceivers()
970
+ this.#clearPeerConnection()
971
+ this.#clearSessionDescriptionHandler()
972
+ this.#clearAllTimer()
973
+ await this.#clearUserAgent()
974
+ this.#clearUserAgentStatus()
975
+ this.#stopTrackVolume()
976
+ this.#businessAttribute = {}
977
+ this.#pushData = {}
978
+ } catch (e) {
979
+ this.#clearUserAgentStatus()
980
+ console.log('sip dispose with error', e)
981
+ }
982
+ }
983
+
984
+ /**
985
+ * sip协议通讯
986
+ * @private
987
+ * @param headers
988
+ */
989
+ #syncSipMessage = (headers: Record<string, any>) => {
990
+ this.#businessAttribute.popWindow = headers['X-Popwindow']?.[0].raw
991
+ this.#businessAttribute.afterTransferLabour = headers['X-Aftertransferlabour']?.[0].raw
992
+ this.#businessAttribute.callId = headers['X-94callid']?.[0].raw
993
+ this.#businessAttribute.cid = headers['X-Cid']?.[0].raw
994
+ this.#businessAttribute.numberId = headers['X-Numberid']?.[0].raw
995
+ this.#businessAttribute.taskId = headers['X-Taskid']?.[0].raw
996
+ this.#businessAttribute.voiceType = headers['X-Voicetype']?.[0].raw
997
+ this.#businessAttribute.processTime = headers['X-Processtime']?.[0].raw
998
+ this.#businessAttribute.userPhone = headers['X-Userphone']?.[0].raw
999
+ this.#businessAttribute.autoAnswer = headers['X-94autoanswer']?.[0].raw
1000
+ this.#businessAttribute.nodeTitle = headers['X-Nodetitle']?.[0].raw.split('-').map((item: number) => String.fromCharCode(item)).join('')
1001
+ this.#businessAttribute.taskName = headers['X-Taskname']?.[0].raw.split('-').map((item: number) => String.fromCharCode(item)).join('')
1002
+ this.#businessAttribute.templateTitle = headers['X-Templatetitle']?.[0].raw.split('-').map((item: number) => String.fromCharCode(item)).join('')
1003
+ this.#businessAttribute.phoneNumber = headers['X-Phonenumber']?.[0].raw || headers.From?.[0].parsed.uri.normal.user
1004
+ }
1005
+
1006
+ /**
1007
+ * 忽略
1008
+ * @param { import('sip.js/lib/api/invitation-reject-options').InvitationRejectOptions } } options 配置
1009
+ */
1010
+ public async ignoreInvite (options: InvitationRejectOptions = {}) {
1011
+ this.#userAgentStatus.incomingStatus = false
1012
+ await this.getCurrentInvitation()!.reject(options)
1013
+ }
1014
+
1015
+ /**
1016
+ * 接听
1017
+ * @param { InvitationAcceptOptions } options 配置
1018
+ */
1019
+ public async acceptInvite (options: InvitationAcceptOptions = {}) {
1020
+ try {
1021
+ // await requestMicroPhonePermission()
1022
+ await new Promise((resolve, reject) => {
1023
+ const fn = async () => {
1024
+ try {
1025
+ const onAck = options?.onAck
1026
+ const onAckTimeout = options?.onAckTimeout
1027
+ delete options?.onAck
1028
+ delete options?.onAckTimeout
1029
+ await this.getCurrentInvitation()!.accept({
1030
+ sessionDescriptionHandlerOptions: {
1031
+ constraints: {
1032
+ audio: true,
1033
+ video: false
1034
+ }
1035
+ },
1036
+ onAck (ackRequest: { message: string }) {
1037
+ resolve(ackRequest)
1038
+ onAck?.()
1039
+ },
1040
+ onAckTimeout () {
1041
+ onAckTimeout?.()
1042
+ reject(new Error('接听响应超时'))
1043
+ },
1044
+ ...(options || {})
1045
+ })
1046
+ } catch (e) {
1047
+ reject(e)
1048
+ }
1049
+ }
1050
+ fn()
1051
+ })
1052
+ if (this.#userAgentStatus.incomingStatus) { // 有可能在接听中服务端下发了cancel,然后又发了200给服务端,服务端会成功响应ack,导致这时候会挂断后又唤起接听成功
1053
+ this.#userAgentStatus.incomingStatus = false
1054
+ this.#userAgentStatus.answerStatus = true
1055
+ }
1056
+ } catch (e) {
1057
+ this.#userAgentStatus.incomingStatus = false
1058
+ throw e
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * 挂断
1064
+ * @param options 配置
1065
+ * @param { import('sip.js/lib/api/invitation-reject-options').InvitationRejectOptions } options.rejectOptions 当会话尚在建立中,配置挂断请求
1066
+ * @param { import('sip.js/lib/api/session-bye-options').SessionByeOptions } options.byeOptions 当会话已建立,配置挂断请求
1067
+ * @param { string[] } options.extraHeaders sip自定义头
1068
+ */
1069
+ public async hangUpInvite (options?: HandUpInviteOption): Promise<boolean | IncomingResponse> {
1070
+ const {
1071
+ rejectOptions,
1072
+ byeOptions,
1073
+ extraHeaders,
1074
+ scoutResponse
1075
+ } = (options || {})
1076
+ return new Promise(async (resolve, reject) => {
1077
+ try {
1078
+ const byeOption: Record<string, any> = byeOptions || {}
1079
+ const onAccept = byeOption.requestDelegate?.onAccept
1080
+ const onReject = byeOption.requestDelegate?.onReject
1081
+ delete byeOption.requestDelegate?.onAccept
1082
+ delete byeOption.requestDelegate?.onReject
1083
+ const requestDelegate = this.#extendFreeDelegate({
1084
+ onAccept (response: IncomingResponse) {
1085
+ console.log('sip bye success')
1086
+ resolve(response)
1087
+ onAccept?.(response)
1088
+ },
1089
+ onReject (response: IncomingResponse) {
1090
+ onReject?.(response)
1091
+ reject(new Error('sip bye fail with code ' + response.message.statusCode))
1092
+ }
1093
+ }, byeOption.requestDelegate as Record<string, Function>)
1094
+ delete byeOption.requestDelegate
1095
+ switch (this.#currentInvitation?.state) {
1096
+ case SessionState.Initial:
1097
+ case SessionState.Establishing:
1098
+ // if (this.#currentInvitation instanceof Inviter) {
1099
+ // // An unestablished outgoing session
1100
+ // this.#currentInvitation!.cancel()
1101
+ // } else {
1102
+ // An unestablished incoming session
1103
+ await this.#currentInvitation!.reject({
1104
+ extraHeaders,
1105
+ ...(rejectOptions || {})
1106
+ })
1107
+ resolve(true)
1108
+ // }
1109
+ break
1110
+ case SessionState.Established:
1111
+ // An established session
1112
+ await this.#currentInvitation!.bye({
1113
+ requestDelegate,
1114
+ requestOptions: {
1115
+ extraHeaders
1116
+ },
1117
+ ...byeOption
1118
+ })
1119
+ if (!scoutResponse) {
1120
+ resolve(true)
1121
+ }
1122
+ break
1123
+ case SessionState.Terminating:
1124
+ case SessionState.Terminated:
1125
+ resolve(true)
1126
+ // Cannot terminate a session that is already terminated
1127
+ break
1128
+ }
1129
+ } catch (e) {
1130
+ reject(e)
1131
+ } finally {
1132
+ this.#userAgentStatus.incomingStatus = false // 可能用户主动挂断,非坐席手动挂断,这个时候自己被动挂断也要把来电状态重置下
1133
+ this.#userAgentStatus.answerStatus = false
1134
+ }
1135
+ })
1136
+ }
1137
+
1138
+ /**
1139
+ * 按*转人工
1140
+ * @param { import('sip.js/lib/api/session-info-options').SessionInfoOptions & {extraHeaders: string[]} } options 配置
1141
+ */
1142
+ public async sendStarDtmf (options: SessionInfoOptions = {}): Promise<IncomingResponse> {
1143
+ return new Promise(async (resolve, reject) => {
1144
+ try {
1145
+ const option = (options || {})
1146
+ const { extraHeaders, requestDelegate } = option
1147
+ const delegate = this.#extendFreeDelegate({
1148
+ onAccept (response: IncomingResponse) {
1149
+ console.log('sip send dtmf Signal=* success')
1150
+ resolve(response)
1151
+ },
1152
+ onReject (response: IncomingResponse) {
1153
+ reject(new Error('sip send dtmf Signal=* fail with code ' + response.message.statusCode))
1154
+ }
1155
+ }, requestDelegate as Record<string, Function>)
1156
+ delete option.requestDelegate
1157
+ await this.getCurrentInvitation().info({
1158
+ requestOptions: {
1159
+ body: {
1160
+ contentDisposition: 'render',
1161
+ contentType: 'application/dtmf-relay',
1162
+ content: 'Signal=*\r\nDuration=100'
1163
+ },
1164
+ extraHeaders
1165
+ },
1166
+ requestDelegate: delegate,
1167
+ ...option
1168
+ })
1169
+ this.#trackVolume()
1170
+ } catch (e) {
1171
+ reject(e)
1172
+ }
1173
+ })
1174
+ }
1175
+
1176
+ /**
1177
+ * 拨出
1178
+ * @param { import('sip.js/lib/api/inviter-invite-options').InviterInviteOptions & {extraHeaders?: string[]} } options 配置
1179
+ */
1180
+ public async sendCurrentInviter (options: InviterInviteOptions = {}) {
1181
+ const { extraHeaders } = (options || {})
1182
+ // @ts-ignore
1183
+ await this.getCurrentInviter().invite({
1184
+ requestOptions: {
1185
+ extraHeaders
1186
+ },
1187
+ ...options
1188
+ })
1189
+ }
1190
+
1191
+ /**
1192
+ * 不接听远端声音
1193
+ */
1194
+ public muteRemoteAudio () {
1195
+ this.#peerConnection!.getReceivers().forEach(item => {
1196
+ item.track.enabled = false
1197
+ })
1198
+ }
1199
+
1200
+ /**
1201
+ * 接听远端声音
1202
+ */
1203
+ public unMuteRemoteAudio () {
1204
+ this.#peerConnection!.getReceivers().forEach(item => {
1205
+ item.track.enabled = true
1206
+ })
1207
+ }
1208
+
1209
+ /**
1210
+ * 不传输本地声音到远端
1211
+ */
1212
+ public muteLocalAudio () {
1213
+ this.#peerConnection!.getSenders().forEach(item => {
1214
+ item.track!.enabled = false
1215
+ })
1216
+ }
1217
+
1218
+ /**
1219
+ * 传输本地声音到远端
1220
+ */
1221
+ public unMuteLocalAudio () {
1222
+ this.#peerConnection!.getSenders().forEach(item => {
1223
+ item.track!.enabled = true
1224
+ })
1225
+ }
1226
+
1227
+ /**
1228
+ * 获取代理实例状态
1229
+ */
1230
+ public getUserAgentStatue () {
1231
+ return this.#userAgentStatus
1232
+ }
1233
+
1234
+ /**
1235
+ * 获取代理实例
1236
+ */
1237
+ public getUserAgent () {
1238
+ if (!this.#userAgent) {
1239
+ // @ts-ignore
1240
+ this.#userAgent = new UserAgent(this.#userAgentOption)
1241
+ }
1242
+ return this.#userAgent
1243
+ }
1244
+
1245
+ /**
1246
+ * 获取会话实例
1247
+ */
1248
+ public getSessionDescriptionHandler () {
1249
+ return this.#sessionDescriptionHandler
1250
+ }
1251
+
1252
+ /**
1253
+ * 获取peerConnection
1254
+ */
1255
+ public getPeerConnection () {
1256
+ return this.#peerConnection
1257
+ }
1258
+
1259
+ /**
1260
+ * 获取senders
1261
+ */
1262
+ public getSenders () {
1263
+ return this.#senders
1264
+ }
1265
+
1266
+ /**
1267
+ * 获取receivers
1268
+ */
1269
+ public getReceivers () {
1270
+ return this.#receivers
1271
+ }
1272
+
1273
+ /**
1274
+ * 获取流对象
1275
+ */
1276
+ public getStream () {
1277
+ if (!this.#stream) {
1278
+ this.#stream = new MediaStream()
1279
+ }
1280
+ this.#receivers.forEach((receiver) => {
1281
+ if (receiver.track) {
1282
+ this.#stream!.addTrack(receiver.track)
1283
+ }
1284
+ })
1285
+ return this.#stream
1286
+ }
1287
+
1288
+ /**
1289
+ * 拨出实例
1290
+ */
1291
+ public getCurrentInviter () {
1292
+ if (!this.#currentInviter) {
1293
+ throw new Error('No currentInviter, please call createCurrentInviter')
1294
+ }
1295
+ return this.#currentInviter
1296
+ }
1297
+
1298
+ /**
1299
+ * 接听实例
1300
+ */
1301
+
1302
+ public getCurrentInvitation () {
1303
+ return this.#currentInvitation
1304
+ }
1305
+
1306
+ /**
1307
+ * 增加状态响应式
1308
+ * @param {{ refresh: (path: keyof UserAgentStatusKey, value: boolean) => void }} config
1309
+ * @private
1310
+ */
1311
+ #initStatus (config: { refresh: (path: UserAgentStatusKey, value: boolean) => void }) {
1312
+ // @ts-ignore
1313
+ this.#userAgentStatus = onChange(JSON.parse(JSON.stringify(userAgentStatus)), typeof config.refresh === 'function' ? config.refresh : this.#empty)
1314
+ }
1315
+
1316
+ /**
1317
+ * 初始换KeepAlive配置
1318
+ * @param { UserAgentOptions } userAgentOption
1319
+ * @private
1320
+ */
1321
+ #initKeepAliveOptions (userAgentOption: Partial<UserAgentOptions>) {
1322
+ this.#reconnectionAttempts = userAgentOption.reconnectionAttempts ?? userAgentOption?.transportOptions?.maxReconnectionAttempts ?? this.#reconnectionAttempts
1323
+ this.#reconnectionInterval = userAgentOption.reconnectionInterval ?? userAgentOption.reconnectionDelay ?? userAgentOption?.transportOptions?.reconnectionTimeout ?? this.#reconnectionInterval
1324
+ this.#keepAliveInterval = userAgentOption.transportOptions?.keepAliveInterval ?? this.#keepAliveInterval
1325
+ this.#keepAliveDebounce = userAgentOption.transportOptions?.keepAliveDebounce ?? this.#keepAliveDebounce
1326
+ this.#registerInterval = userAgentOption.registerInterval ?? this.#registerInterval
1327
+ this.#optionsPingInterval = userAgentOption.optionsPingInterval ?? this.#optionsPingInterval
1328
+ this.#optionsPingAttempts = userAgentOption.optionsPingAttempts ?? this.#optionsPingAttempts
1329
+ this.#registerAttempts = userAgentOption.registerAttempts ?? this.#registerAttempts
1330
+ // window.addEventListener("online", this.#attemptReconnectionHandler);
1331
+ }
1332
+
1333
+ #extendFreeDelegate (initDelegate: Record<string, Function>, extendDelegate?: Record<string, Function>) {
1334
+ if (!extendDelegate) return initDelegate
1335
+ Object.keys(extendDelegate).forEach((extendDelegateKey) => {
1336
+ if (extendDelegate[extendDelegateKey] && typeof extendDelegate[extendDelegateKey] === 'function') {
1337
+ if (initDelegate[extendDelegateKey]) {
1338
+ if (typeof initDelegate[extendDelegateKey] === 'function') {
1339
+ const tmp = initDelegate[extendDelegateKey]
1340
+ initDelegate[extendDelegateKey] = async (param: any) => {
1341
+ await extendDelegate[extendDelegateKey](param)
1342
+ await tmp(param)
1343
+ }
1344
+ }
1345
+ } else {
1346
+ initDelegate[extendDelegateKey] = extendDelegate[extendDelegateKey]
1347
+ }
1348
+ }
1349
+ })
1350
+ return initDelegate
1351
+ }
1352
+
1353
+ /**
1354
+ * 初始化sip配置
1355
+ * @param { UserAgentOptions } userAgentOption
1356
+ * @private
1357
+ */
1358
+ #getUserAgentOption (userAgentOption: Partial<UserAgentOptions>) {
1359
+ const initUserAgentOption: Partial<UserAgentOptions> = {
1360
+ delegate: {
1361
+ onConnect: async () => {
1362
+ console.log('sip connected')
1363
+ this.#optionsPingStart()
1364
+ },
1365
+ onInvite: (invitation: Invitation) => {
1366
+ // @ts-ignore
1367
+ this.#syncSipMessage(invitation.incomingInviteRequest.message.headers)
1368
+ this.refreshCallChatInfo()
1369
+ this.#clearCurrentInvitation()
1370
+ this.#currentInvitation = invitation
1371
+ this.#userAgentStatus.incomingStatus = true
1372
+ invitation.stateChange.addListener(this.#receivedCall)
1373
+ },
1374
+ onDisconnect: async (error?: Error) => {
1375
+ console.log('sip disconnected')
1376
+ let optionsPingFailure = false
1377
+ if (this.#optionsPingInterval > 0) {
1378
+ optionsPingFailure = this.#optionsPingFailure
1379
+ this.#optionsPingStop()
1380
+ }
1381
+ await this.#unRegisterer()
1382
+ // Only attempt to reconnect if network/server dropped the connection (if there is an error)
1383
+ if (error || optionsPingFailure) {
1384
+ this.#attemptReconnection()
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ if (userAgentOption.delegate) {
1390
+ Object.keys(userAgentOption.delegate).forEach((userAgentDelegateKey) => {
1391
+ const key = userAgentDelegateKey as UserAgentDelegateKey
1392
+ if (userAgentOption.delegate![key] && typeof userAgentOption.delegate![key] === 'function') {
1393
+ const tmp = userAgentOption.delegate![key] as Function
1394
+ // @ts-ignore
1395
+ userAgentOption.delegate[key] = async (invitation: Invitation) => {
1396
+ // @ts-ignore
1397
+ initUserAgentOption.delegate![key]?.(invitation)
1398
+ await tmp!(invitation)
1399
+ }
1400
+ }
1401
+ })
1402
+ } else {
1403
+ userAgentOption.delegate = initUserAgentOption.delegate
1404
+ }
1405
+ /** 关闭transport层的重连 */
1406
+ userAgentOption.reconnectionAttempts = 0
1407
+ userAgentOption.transportOptions = {
1408
+ ...(userAgentOption.transportOptions || {}),
1409
+ keepAliveInterval: this.#keepAliveInterval,
1410
+ keepAliveDebounce: this.#keepAliveDebounce
1411
+ }
1412
+ if (typeof userAgentOption.sessionDescriptionHandlerFactoryOptions === 'undefined') {
1413
+ userAgentOption.sessionDescriptionHandlerFactoryOptions = {
1414
+ iceGatheringTimeout: 2000,
1415
+ peerConnectionConfiguration: {
1416
+ iceServers: []
1417
+ }
1418
+ }
1419
+ }
1420
+ return {
1421
+ ...userAgentDefault,
1422
+ ...userAgentOption
1423
+ } as UserAgentOptions
1424
+ }
1425
+
1426
+ /**
1427
+ * 清除接听实例
1428
+ * @private
1429
+ */
1430
+
1431
+ #clearCurrentInvitation () {
1432
+ if (this.#currentInvitation) {
1433
+ this.#currentInvitation.dispose()
1434
+ this.#currentInvitation = undefined
1435
+ }
1436
+ }
1437
+
1438
+ /**
1439
+ * 清除拨出实例
1440
+ * @private
1441
+ */
1442
+ #clearCurrentInviter () {
1443
+ if (this.#currentInviter) {
1444
+ this.#currentInviter.dispose()
1445
+ this.#currentInviter = undefined
1446
+ }
1447
+ }
1448
+
1449
+ /**
1450
+ * 清除用户代理实例
1451
+ * @private
1452
+ */
1453
+ async #clearUserAgent () {
1454
+ if (this.#userAgent) {
1455
+ await this.#userAgent.stop()
1456
+ this.#userAgent = undefined
1457
+ }
1458
+ }
1459
+
1460
+ /**
1461
+ * 清除流对象
1462
+ * @private
1463
+ */
1464
+ #clearStream () {
1465
+ if (this.#stream) {
1466
+ this.#stream.getTracks().forEach(track => {
1467
+ track.stop()
1468
+ })
1469
+ this.#stream = undefined
1470
+ }
1471
+ }
1472
+
1473
+ /**
1474
+ * 重置用户代理状态
1475
+ * @private
1476
+ */
1477
+ #clearUserAgentStatus () {
1478
+ Object.keys(userAgentStatus).forEach(userAgentStatusKey => {
1479
+ const key = userAgentStatusKey as UserAgentStatusKey
1480
+ this.#userAgentStatus[key] = userAgentStatus[key]
1481
+ })
1482
+ }
1483
+
1484
+ /**
1485
+ * 清除会话实例对象
1486
+ * @private
1487
+ */
1488
+ #clearSessionDescriptionHandler () {
1489
+ if (this.#sessionDescriptionHandler) {
1490
+ this.#sessionDescriptionHandler.close()
1491
+ this.#sessionDescriptionHandler = undefined
1492
+ }
1493
+ }
1494
+
1495
+ /**
1496
+ * 清除peerConnection
1497
+ * @private
1498
+ */
1499
+ #clearPeerConnection () {
1500
+ if (this.#peerConnection) {
1501
+ this.#peerConnection.close()
1502
+ this.#peerConnection = undefined
1503
+ }
1504
+ }
1505
+
1506
+ /**
1507
+ * 清除senders
1508
+ * @private
1509
+ */
1510
+ #clearSenders () {
1511
+ this.#senders = []
1512
+ }
1513
+
1514
+ /**
1515
+ * 清除receivers
1516
+ * @private
1517
+ */
1518
+ #clearReceivers () {
1519
+ this.#receivers = []
1520
+ }
1521
+
1522
+ /**
1523
+ * 收集webrtc对象实例
1524
+ * @private
1525
+ */
1526
+ #setupSession () {
1527
+ const invitation = this.getCurrentInvitation()
1528
+ const {
1529
+ sessionDescriptionHandler
1530
+ } = invitation!
1531
+ this.#clearPeerConnection()
1532
+ this.#clearSessionDescriptionHandler()
1533
+ this.#clearSenders()
1534
+ this.#clearReceivers()
1535
+ this.#sessionDescriptionHandler = sessionDescriptionHandler
1536
+ const sessionDescriptionHandlerType = sessionDescriptionHandler as Record<string, any>
1537
+ this.#peerConnection = sessionDescriptionHandlerType.peerConnection as RTCPeerConnection
1538
+ this.#senders = this.#peerConnection.getSenders()
1539
+ this.#receivers = this.#peerConnection.getReceivers()
1540
+ if (this.getFastOutboundCall() || (!this.getSitOutboundCall() && this.notNeedSendStarDtmf())) {
1541
+ this.#trackVolume()
1542
+ }
1543
+ // this.muteLocalAudio()
1544
+ }
1545
+
1546
+ /**
1547
+ * 跟踪会话建立
1548
+ * @param { SessionState } state
1549
+ */
1550
+ #receivedCall = (state: SessionState) => {
1551
+ switch (state) {
1552
+ case SessionState.Initial:
1553
+ break
1554
+ case SessionState.Establishing:
1555
+ break
1556
+ case SessionState.Established:
1557
+ this.#setupSession()
1558
+ break
1559
+ case SessionState.Terminating:
1560
+ // fall through
1561
+ case SessionState.Terminated:
1562
+ let timer: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
1563
+ clearTimeout(timer)
1564
+ timer = undefined
1565
+ this.#userAgentStatus.incomingStatus = false
1566
+ }, 10)
1567
+ this.#stopTrackVolume()
1568
+ break
1569
+ default:
1570
+ throw new Error('Unknown session state.')
1571
+ }
1572
+ }
1573
+
1574
+ /**
1575
+ * 生成拨出实例
1576
+ * @param { string } uri 拨出地址
1577
+ * @private
1578
+ */
1579
+ async #createCurrentInviter (uri: string) {
1580
+ const target = UserAgent.makeURI(uri)
1581
+ if (!target) {
1582
+ throw new Error('Failed to create target URI.')
1583
+ }
1584
+ this.#currentInviter = new Inviter(this.getUserAgent(), target, {
1585
+ sessionDescriptionHandlerOptions: {
1586
+ constraints: {
1587
+ audio: true,
1588
+ video: false
1589
+ }
1590
+ },
1591
+ })
1592
+ return this.#currentInviter
1593
+ }
1594
+
1595
+ /**
1596
+ * 取消注册
1597
+ * @private
1598
+ * @param { import('sip.js/lib/api/registerer-unregister-options').RegistererUnregisterOptions } options 配置
1599
+ */
1600
+ async #unRegisterer (options: RegistererUnregisterOptions = {}) {
1601
+ if (this.#registerer) {
1602
+ console.log('sip due to disconnection, unregistered')
1603
+ return new Promise(async (resolve, reject) => {
1604
+ try {
1605
+ const option = options || {}
1606
+ const requestDelegate = this.#extendFreeDelegate({
1607
+ onAccept: (response: IncomingResponse) => {
1608
+ console.log('sip unregister successed')
1609
+ resolve(response)
1610
+ // 由于业务对接过程涉及多系注册信息同步,状态同步有一定延迟,为了不让用户乱操作,对外保留注册状态
1611
+ // this.#userAgentStatus.registerStatus = false
1612
+ },
1613
+ onReject (response: IncomingResponse) {
1614
+ reject(new Error('sip unregister fail with code ' + response.message.statusCode))
1615
+ }
1616
+ }, option.requestDelegate as Record<string, Function>)
1617
+ delete options.requestDelegate
1618
+ await this.#registerer.unregister({
1619
+ // @ts-ignore
1620
+ requestDelegate,
1621
+ ...options
1622
+ })
1623
+ } catch (error) {
1624
+ reject(error)
1625
+ }
1626
+ })
1627
+ }
1628
+ }
1629
+
1630
+ /**
1631
+ * 监听ping成功
1632
+ * @param requestURI
1633
+ * @param fromURI
1634
+ * @param toURI
1635
+ * @private
1636
+ */
1637
+ #onPingSuccess = (requestURI: URI, fromURI: URI, toURI: URI) => {
1638
+ if (this.#optionsPingInterval) {
1639
+ this.#optionsPingFailure = false
1640
+ this.#userAgentStatus.reconnectStatus = false
1641
+ if (this.#optionsPingRunning) {
1642
+ this.#optionsPingRunning = false
1643
+ this.#optionsPingRun(requestURI, fromURI, toURI)
1644
+ }
1645
+ }
1646
+ }
1647
+ /**
1648
+ * 监听ping失败
1649
+ * @private
1650
+ */
1651
+ #onPingFailure = async () => {
1652
+ if (this.#optionsPingInterval) {
1653
+ this.#optionsPingFailure = true
1654
+ this.#optionsPingRunning = false
1655
+ await this.getUserAgent().transport.disconnect().catch((error) => console.error(error))
1656
+ console.log(`sip transport disconnect`)
1657
+ try {
1658
+ await this.#unRegisterer()
1659
+ } catch {}
1660
+ this.#optionsPingStop()
1661
+ this.#attemptReconnection()
1662
+ }
1663
+ }
1664
+ /**
1665
+ * 启动ping keeyalive
1666
+ * @private
1667
+ */
1668
+ #optionsPingStart = () => {
1669
+ if (this.#optionsPingInterval) {
1670
+ // @ts-ignore
1671
+ const fromURI = this.#userAgentOption.uri.clone()
1672
+ // @ts-ignore
1673
+ const toURI = this.#userAgentOption.uri.clone()
1674
+ // @ts-ignore
1675
+ const requestURI = this.#userAgentOption.uri.clone()
1676
+ requestURI.user = undefined
1677
+ this.#optionsPingRun(requestURI, fromURI, toURI)
1678
+ }
1679
+ }
1680
+ /**
1681
+ * 发起ping
1682
+ * @param { URI } requestURI
1683
+ * @param { URI } fromURI
1684
+ * @param { URI } toURI
1685
+ * @private
1686
+ */
1687
+ #optionsPingRun = (requestURI: URI, fromURI: URI, toURI: URI) => {
1688
+ if (this.#optionsPingInterval) {
1689
+ if (this.#optionsPingRunning) {
1690
+ return
1691
+ }
1692
+ this.#optionsPingRunning = true
1693
+ this.#optionsPingTimer = setTimeout(() => {
1694
+ this.#clearOptionsPingTimer()
1695
+ const core = this.getUserAgent().userAgentCore
1696
+ const message = core.makeOutgoingRequestMessage('OPTIONS', requestURI, fromURI, toURI, {})
1697
+ // @ts-ignore
1698
+ this.#optionsPingRequest = core.request(message, {
1699
+ onAccept: () => {
1700
+ console.log('sip ping ok')
1701
+ this.#optionsPingAttempt = 0
1702
+ this.#optionsPingRequest = undefined
1703
+ this.#onPingSuccess(requestURI, fromURI, toURI)
1704
+ },
1705
+ onReject: (response) => {
1706
+ this.#optionsPingRequest = undefined
1707
+ // - 408 Request Timeout (no response was received)
1708
+ // - 503 Service Unavailable (a transport layer error occured)
1709
+ // - 410 Unauthorized
1710
+ // - 484 Address incomplete
1711
+ // - 482 Loop detected
1712
+ if (response.message.statusCode === 408 || response.message.statusCode === 503) {
1713
+ console.log('sip ping error with code ' + response.message.statusCode)
1714
+ if (this.#optionsPingAttempts > 0 && this.#optionsPingAttempt >= this.#optionsPingAttempts) {
1715
+ console.log(`sip maximum ping attempts reached`)
1716
+ this.#onPingFailure()
1717
+ } else {
1718
+ if (this.#optionsPingAttempt > 0) {
1719
+ console.log(`sip ping retry ${this.#optionsPingAttempt} of ${this.#optionsPingAttempts} fail`)
1720
+ }
1721
+ this.#optionsPingAttempt++
1722
+ console.log(`sip ping retry ${this.#optionsPingAttempt} of ${this.#optionsPingAttempts}`)
1723
+ this.#onPingSuccess(requestURI, fromURI, toURI)
1724
+ }
1725
+ } else {
1726
+ this.#optionsPingAttempt = 0
1727
+ this.#onPingSuccess(requestURI, fromURI, toURI)
1728
+ }
1729
+ }
1730
+ })
1731
+ }, this.#optionsPingInterval * 1000)
1732
+ }
1733
+ }
1734
+ /**
1735
+ * 停止ping
1736
+ * @private
1737
+ */
1738
+ #optionsPingStop = () => {
1739
+ if (this.#optionsPingInterval) {
1740
+ this.#optionsPingRunning = false
1741
+ this.#optionsPingFailure = false
1742
+ if (this.#optionsPingRequest) {
1743
+ try {
1744
+ this.#optionsPingRequest.dispose()
1745
+ } catch {
1746
+ }
1747
+ this.#optionsPingRequest = undefined
1748
+ }
1749
+ if (this.#optionsPingTimer) {
1750
+ this.#clearOptionsPingTimer()
1751
+ }
1752
+ }
1753
+ }
1754
+
1755
+ /**
1756
+ * 清除keepalive机制定时
1757
+ * @private
1758
+ */
1759
+ #clearAllTimer () {
1760
+ this.#clearRegisterTimer()
1761
+ this.#clearReconnectTimer()
1762
+ this.#clearOptionsPingTimer()
1763
+ this.#registerAttempting = false
1764
+ this.#reconnectionAttempting = false
1765
+ this.#optionsPingRunning = false
1766
+ }
1767
+
1768
+ /**
1769
+ * 清除注册定制器
1770
+ * @private
1771
+ */
1772
+ #clearRegisterTimer () {
1773
+ clearTimeout(this.#registerTimer)
1774
+ this.#registerTimer = undefined
1775
+ }
1776
+
1777
+ /**
1778
+ * 清除reconnect定制器
1779
+ * @private
1780
+ */
1781
+ #clearReconnectTimer () {
1782
+ clearTimeout(this.#reconnectionTimer)
1783
+ this.#reconnectionTimer = undefined
1784
+ }
1785
+
1786
+ /**
1787
+ * 清除ping定制器
1788
+ * @private
1789
+ */
1790
+ #clearOptionsPingTimer () {
1791
+ clearTimeout(this.#optionsPingTimer)
1792
+ this.#optionsPingTimer = undefined
1793
+ }
1794
+
1795
+ /**
1796
+ * 重新注册
1797
+ * @private
1798
+ */
1799
+ #attemptRegister = () => {
1800
+ if (this.#registerInterval) {
1801
+ if (this.#registerAttempting) {
1802
+ return
1803
+ }
1804
+ this.#registerAttempting = true
1805
+ this.#userAgentStatus.reconnectStatus = true
1806
+ this.#registerTimer = setTimeout(async () => {
1807
+ try {
1808
+ this.#clearRegisterTimer()
1809
+ if (this.#registerAttempt === 0) {
1810
+ console.log('sip reconnect success then register start')
1811
+ }
1812
+ await this.#registerer.register({
1813
+ requestDelegate: {
1814
+ // @ts-ignore
1815
+ onAccept: (response: IncomingResponse) => {
1816
+ console.log('sip reconnect success and register ok')
1817
+ this.#registerAttempt = 0
1818
+ this.#registerAttempting = false
1819
+ this.#userAgentStatus.registerStatus = true
1820
+ this.#userAgentStatus.reconnectStatus = false
1821
+ this.#optionsPingStart()
1822
+ },
1823
+ // @ts-ignore
1824
+ onReject: async (response: IncomingResponse) => {
1825
+ console.log('sip reconnect success but register fail with code ' + response.message.statusCode)
1826
+ this.#registerAttempting = false
1827
+ // if (response.message.statusCode === 408 || response.message.statusCode === 503) {
1828
+ if (this.#registerAttempts <= this.#registerAttempt) {
1829
+ console.log(`sip maximum register attempts reached`)
1830
+ this.#registerAttempt = 0
1831
+ await this.getUserAgent().transport.disconnect().catch((error) => console.error(error))
1832
+ this.#attemptReconnection()
1833
+ } else {
1834
+ this.#registerAttempt++
1835
+ console.log(`sip reconnect success and register retry ${this.#registerAttempt} of ${this.#registerAttempts}`)
1836
+ this.#attemptRegister()
1837
+ }
1838
+ // }
1839
+ }
1840
+ },
1841
+ requestOptions: {
1842
+ extraHeaders: this.#extraHeaders
1843
+ }
1844
+ })
1845
+ } catch (e) {
1846
+ console.log('sip reconnect then registerer register error', e)
1847
+ this.#registerAttempt = 0
1848
+ this.#registerAttempting = false
1849
+ }
1850
+ }, this.#registerInterval * 1000)
1851
+ }
1852
+ }
1853
+
1854
+ /**
1855
+ * 重连
1856
+ * @param { number } reconnectionAttempt 第几次重连
1857
+ * @private
1858
+ */
1859
+ async #attemptReconnection (reconnectionAttempt = 1): Promise<void> {
1860
+ if (this.#reconnectionInterval) {
1861
+ this.#userAgentStatus.reconnectStatus = true
1862
+ if (this.#reconnectionAttempting) {
1863
+ return
1864
+ }
1865
+ if (reconnectionAttempt > this.#reconnectionAttempts) {
1866
+ console.log(`sip maximum reconnection attempts reached`)
1867
+ await this.dispose()
1868
+ return
1869
+ }
1870
+ console.log('sip reconnection attempt...')
1871
+ this.#reconnectionAttempting = true
1872
+ this.#reconnectionTimer = setTimeout(() => {
1873
+ this.#clearReconnectTimer()
1874
+ this.getUserAgent().reconnect()
1875
+ .then(async () => {
1876
+ console.log(`sip reconnection attempt ${reconnectionAttempt} of ${this.#reconnectionAttempts} - succeeded`)
1877
+ this.#reconnectionAttempting = false
1878
+ this.#attemptRegister()
1879
+ })
1880
+ .catch((error: Error) => {
1881
+ console.error(error)
1882
+ console.log(`sip reconnection attempt ${reconnectionAttempt} of ${this.#reconnectionAttempts} - failed`)
1883
+ this.#reconnectionAttempting = false
1884
+ this.#attemptReconnection(++reconnectionAttempt)
1885
+ })
1886
+ }, reconnectionAttempt === 1 ? 0 : this.#reconnectionInterval * 1000)
1887
+ }
1888
+ }
1889
+
1890
+ /**
1891
+ * 实例代理事件混入
1892
+ */
1893
+ #extendDelegate (delegate: UserAgentDelegate) {
1894
+ Object.keys(delegate).forEach(userAgentDelegateKey => {
1895
+ const key = userAgentDelegateKey as UserAgentDelegateKey
1896
+ if (typeof delegate?.[key] === 'function') {
1897
+ const tmp = delegate[key] as Function
1898
+ // @ts-ignore
1899
+ delegate[key] = async (invitation: Invitation) => {
1900
+ // @ts-ignore
1901
+ this.#userAgentOption.delegate![key]?.(invitation)
1902
+ await tmp(invitation)
1903
+ }
1904
+ }
1905
+ })
1906
+ return delegate
1907
+ }
1908
+
1909
+ /**
1910
+ * 代理事件
1911
+ * @param delegate
1912
+ * @private
1913
+ */
1914
+ #bindUserAgent (delegate: UserAgentDelegate) {
1915
+ const userAgent = this.getUserAgent()
1916
+ // @ts-ignore
1917
+ userAgent.delegate = this.#extendDelegate(delegate)
1918
+ return userAgent
1919
+ }
1920
+
1921
+ /**
1922
+ * 连接
1923
+ * @param {{ refresh: (path: keyof UserAgentStatusKey, value: boolean) => void }} config
1924
+ * @private
1925
+ */
1926
+ async #connectUserAgent (config: { refresh: (path: UserAgentStatusKey, value: boolean) => void }) {
1927
+ this.#initStatus(config)
1928
+ const userAgent = this.getUserAgent()
1929
+ await userAgent.start()
1930
+ if (!userAgent.isConnected()) { // 二次重连
1931
+ await userAgent.reconnect()
1932
+ }
1933
+ if (!userAgent.isConnected()) {
1934
+ throw new Error('链接失败,请稍后再试')
1935
+ }
1936
+ this.#userAgentStatus.connectStatus = true
1937
+ return userAgent
1938
+ }
1939
+
1940
+ /**
1941
+ * 注册
1942
+ * @private
1943
+ * @param options 配置
1944
+ * @param { import('sip.js/lib/api/registerer-options').RegistererOptions } options.registererOptions new Registerer Options
1945
+ * @param { import('sip.js/lib/api/registerer-register-options').RegistererRegisterOptions } options.registererRegisterOptions Registerer Instance register Options
1946
+ * @param { string[] } options.extraHeaders sip custom header
1947
+ */
1948
+ #registerUserAgent (options?: RegisterOptions) {
1949
+ const {
1950
+ registererOptions,
1951
+ registererRegisterOptions,
1952
+ extraHeaders
1953
+ } = (options || {})
1954
+ return new Promise<UserAgent>(async (resolve, reject) => {
1955
+ const userAgent = this.getUserAgent()
1956
+ // @ts-ignore
1957
+ this.#registerer = new Registerer(userAgent, {
1958
+ extraHeaders: extraHeaders || this.#extraHeaders,
1959
+ ...(registererOptions || {})
1960
+ })
1961
+ try {
1962
+ const option = (registererRegisterOptions || {})
1963
+ const requestDelegate = this.#extendFreeDelegate({
1964
+ onAccept: (response: IncomingResponse) => {
1965
+ this.#userAgentStatus.registerStatus = true
1966
+ resolve(userAgent)
1967
+ },
1968
+ onReject: (response: IncomingResponse) => {
1969
+ reject(new Error('sip register fail with code ' + response.message.statusCode))
1970
+ }
1971
+ }, option.requestDelegate as Record<string, Function>)
1972
+ delete option.requestDelegate
1973
+ await this.#registerer.register({
1974
+ // @ts-ignore
1975
+ requestDelegate,
1976
+ requestOptions: {
1977
+ extraHeaders: extraHeaders || this.#extraHeaders
1978
+ },
1979
+ ...option
1980
+ })
1981
+ } catch (e) {
1982
+ reject(e)
1983
+ }
1984
+ })
1985
+ }
1986
+ }