@subiz/wsclient 4.0.36 → 4.0.38
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.
- package/README.md +365 -92
- package/common.js +54 -53
- package/package.json +11 -10
- package/realtime.js +256 -140
- package/webphone.js +31 -20
- package/webrtc.js +340 -277
- package/.claude/settings.local.json +0 -12
- package/.prettierrc.js +0 -19
- package/realtime.test.js +0 -338
- package/vite.config.js +0 -16
package/webrtc.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
var
|
|
2
|
-
var {logError, xhrsend, randomString} = require('./common.js')
|
|
1
|
+
var {xhrsend, randomString, reportCallbackError} = require('./common.js')
|
|
3
2
|
const servers = {
|
|
4
3
|
iceServers: [{urls: ['stun:stun.l.google.com:19302']}],
|
|
5
4
|
}
|
|
@@ -7,7 +6,20 @@ const servers = {
|
|
|
7
6
|
// env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, clearTimeout, setInterval, clearInterval, jsonify, parseJSON}
|
|
8
7
|
function WebRTCConn(options) {
|
|
9
8
|
let {apiUrl, access_token, accid, agid, realtime, callAPI, env, onTrack, onEvent, collect} = options
|
|
10
|
-
|
|
9
|
+
// Application callbacks must never interrupt signaling or resource cleanup.
|
|
10
|
+
let protect =
|
|
11
|
+
(cb) =>
|
|
12
|
+
(...args) => {
|
|
13
|
+
try {
|
|
14
|
+
let result = cb && cb(...args)
|
|
15
|
+
if (result && result.then) env.Promise.resolve(result).catch(reportCallbackError)
|
|
16
|
+
} catch (err) {
|
|
17
|
+
reportCallbackError(err)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
onEvent = protect(onEvent)
|
|
21
|
+
onTrack = protect(onTrack)
|
|
22
|
+
collect = protect(collect)
|
|
11
23
|
if (!apiUrl) apiUrl = 'https://callcenter.subiz.com.vn/wc/'
|
|
12
24
|
|
|
13
25
|
let dead = false
|
|
@@ -36,6 +48,8 @@ function WebRTCConn(options) {
|
|
|
36
48
|
const disconnectGraceMs = 30000
|
|
37
49
|
const rtpStaleMs = 90000
|
|
38
50
|
const inviteStaleMs = 120000
|
|
51
|
+
const setupTimeoutMs = options.setupTimeoutMs > 0 ? options.setupTimeoutMs : 60000
|
|
52
|
+
let operations = new Set()
|
|
39
53
|
|
|
40
54
|
let sessionsByCall = {}
|
|
41
55
|
let sessionsByConn = {}
|
|
@@ -97,8 +111,11 @@ function WebRTCConn(options) {
|
|
|
97
111
|
// thì bỏ pin và thử lại một lần qua gateway mặc định — gateway vẫn proxy về
|
|
98
112
|
// home được nên không bao giờ tệ hơn trước khi pin
|
|
99
113
|
let postPerCall = (callid, path, body, cb) => {
|
|
114
|
+
if (dead) return cb && cb(jsonify({error: 'shutdown'}), -1)
|
|
115
|
+
let owner = getSessionByCall(callid)
|
|
100
116
|
let base = apiBaseFor(callid)
|
|
101
117
|
callAPI('post', base + path, body, (respBody, code) => {
|
|
118
|
+
if (dead || (owner && getSessionByCall(callid) !== owner)) return
|
|
102
119
|
if ((!code || code < 0) && base != apiUrl) {
|
|
103
120
|
console.warn('[webrtc]', 'PINNED HOST UNREACHABLE -> FALLBACK GATEWAY', 'call_id', callid, base)
|
|
104
121
|
let session = getSessionByCall(callid)
|
|
@@ -270,9 +287,12 @@ function WebRTCConn(options) {
|
|
|
270
287
|
}
|
|
271
288
|
|
|
272
289
|
// to receive incoming call
|
|
273
|
-
|
|
290
|
+
env.Promise.resolve()
|
|
291
|
+
.then(() => realtime.subscribe([`webrtc.account.${accid}.agent.${agid}`]))
|
|
292
|
+
.catch(reportCallbackError)
|
|
274
293
|
|
|
275
|
-
let initSession = (callid,
|
|
294
|
+
let initSession = (callid, op) => {
|
|
295
|
+
op.check()
|
|
276
296
|
let connId = 'webrtc' + randomString(20)
|
|
277
297
|
let peer = new env.RTCPeerConnection(servers)
|
|
278
298
|
// call này đã có session gắn connection khác (listenCall rồi mới answerCall,
|
|
@@ -281,8 +301,9 @@ function WebRTCConn(options) {
|
|
|
281
301
|
let prev = getSessionByCall(callid)
|
|
282
302
|
let prevMuted = prev && prev.muted
|
|
283
303
|
if (prev && prev.peer && prev.connId && prev.connId != connId) {
|
|
284
|
-
cleanupConnection(prev.connId)
|
|
304
|
+
cleanupConnection(prev.connId, 'replaced')
|
|
285
305
|
}
|
|
306
|
+
op.connId = connId
|
|
286
307
|
let session = bindSessionConnection(callid, connId, peer)
|
|
287
308
|
if (session) {
|
|
288
309
|
session.status = 'connecting'
|
|
@@ -306,7 +327,7 @@ function WebRTCConn(options) {
|
|
|
306
327
|
|
|
307
328
|
peer.onicecandidate = (event) => {
|
|
308
329
|
if (!event.candidate) return
|
|
309
|
-
if (dead) return
|
|
330
|
+
if (dead || getSessionByConn(connId) !== session || isCallEndedLocally(callid)) return
|
|
310
331
|
postPerCall(
|
|
311
332
|
callid,
|
|
312
333
|
`ice-candidates?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&account_id=${accid}`,
|
|
@@ -314,40 +335,191 @@ function WebRTCConn(options) {
|
|
|
314
335
|
)
|
|
315
336
|
}
|
|
316
337
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
338
|
+
return session
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let operationError = (code) => Object.assign(new Error(String(code)), {code})
|
|
342
|
+
|
|
343
|
+
// One operation spans microphone acquisition, subscription, SDP/ICE and the
|
|
344
|
+
// final signaling response. Its identity survives session replacement/cleanup.
|
|
345
|
+
let startOperation = (callid) => {
|
|
346
|
+
for (let previous of operations) {
|
|
347
|
+
if (previous.callid === callid) previous.cancel('replaced')
|
|
348
|
+
}
|
|
349
|
+
let op = {callid, waits: new Set(), aborters: new Set()}
|
|
350
|
+
op.check = () => {
|
|
351
|
+
if (dead || op.reason) throw operationError(op.reason || 'shutdown')
|
|
352
|
+
if (isCallEndedLocally(callid)) throw operationError('call_ended')
|
|
353
|
+
if (op.connId && getConnIdByCall(callid) !== op.connId) throw operationError('replaced')
|
|
354
|
+
}
|
|
355
|
+
op.clearDeadline = () => {
|
|
356
|
+
env.clearTimeout(op.timer)
|
|
357
|
+
op.timer = undefined
|
|
358
|
+
}
|
|
359
|
+
op.cancel = (reason) => {
|
|
360
|
+
if (op.reason) return
|
|
361
|
+
op.reason = reason
|
|
362
|
+
op.clearDeadline()
|
|
363
|
+
for (let abort of op.aborters) abort()
|
|
364
|
+
for (let reject of op.waits) reject(operationError(reason))
|
|
365
|
+
op.waits.clear()
|
|
366
|
+
}
|
|
367
|
+
op.wait = (promise) => {
|
|
368
|
+
return new env.Promise((resolve, reject) => {
|
|
369
|
+
op.waits.add(reject)
|
|
370
|
+
env.Promise.resolve(promise).then(
|
|
371
|
+
(value) => {
|
|
372
|
+
op.waits.delete(reject)
|
|
373
|
+
try {
|
|
374
|
+
op.check()
|
|
375
|
+
resolve(value)
|
|
376
|
+
} catch (err) {
|
|
377
|
+
reject(err)
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
(err) => {
|
|
381
|
+
op.waits.delete(reject)
|
|
382
|
+
reject(err)
|
|
383
|
+
},
|
|
384
|
+
)
|
|
385
|
+
try {
|
|
386
|
+
op.check()
|
|
387
|
+
} catch (err) {
|
|
388
|
+
op.waits.delete(reject)
|
|
389
|
+
reject(err)
|
|
390
|
+
}
|
|
325
391
|
})
|
|
326
|
-
|
|
392
|
+
}
|
|
393
|
+
op.dispose = () => {
|
|
394
|
+
op.clearDeadline()
|
|
395
|
+
for (let abort of op.aborters) abort()
|
|
396
|
+
operations.delete(op)
|
|
397
|
+
}
|
|
398
|
+
op.timer = env.setTimeout(() => op.cancel('setup_timeout'), setupTimeoutMs)
|
|
399
|
+
operations.add(op)
|
|
400
|
+
return op
|
|
327
401
|
}
|
|
328
402
|
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
() =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
403
|
+
let acquireStream = (op, stream) =>
|
|
404
|
+
op.wait(
|
|
405
|
+
env.Promise.resolve(stream).then((stream) => {
|
|
406
|
+
// Permission prompts cannot be aborted. Dispose of their late streams.
|
|
407
|
+
if (dead || op.reason || isCallEndedLocally(op.callid)) {
|
|
408
|
+
stopStream(stream)
|
|
409
|
+
throw operationError(op.reason || (dead ? 'shutdown' : 'call_ended'))
|
|
410
|
+
}
|
|
411
|
+
op.stream = stream
|
|
412
|
+
if (!stream || !stream.getAudioTracks().some((track) => track.readyState !== 'ended'))
|
|
413
|
+
throw operationError('no_stream')
|
|
414
|
+
return stream
|
|
415
|
+
}),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
let requestForOperation = (op, url, body) => {
|
|
419
|
+
op.check()
|
|
420
|
+
return op.wait(
|
|
421
|
+
new env.Promise((resolve, reject) => {
|
|
422
|
+
let settled = false
|
|
423
|
+
let transport
|
|
424
|
+
let abort = () => {
|
|
425
|
+
if (settled) return
|
|
426
|
+
settled = true
|
|
427
|
+
op.aborters.delete(abort)
|
|
428
|
+
try {
|
|
429
|
+
if (typeof transport === 'function') transport()
|
|
430
|
+
else if (transport && transport.abort) transport.abort()
|
|
431
|
+
} catch (err) {
|
|
432
|
+
reportCallbackError(err)
|
|
340
433
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
434
|
+
}
|
|
435
|
+
op.aborters.add(abort)
|
|
436
|
+
try {
|
|
437
|
+
transport = callAPI('post', url, body, (body, code) => {
|
|
438
|
+
if (settled) return
|
|
439
|
+
settled = true
|
|
440
|
+
op.aborters.delete(abort)
|
|
441
|
+
resolve({body, code})
|
|
442
|
+
})
|
|
443
|
+
// A custom transport may synchronously trigger shutdown before returning.
|
|
444
|
+
if (op.reason) {
|
|
445
|
+
if (typeof transport === 'function') transport()
|
|
446
|
+
else if (transport && transport.abort) transport.abort()
|
|
344
447
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
448
|
+
} catch (err) {
|
|
449
|
+
abort()
|
|
450
|
+
reject(err)
|
|
451
|
+
}
|
|
452
|
+
}),
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let postForOperation = async (op, path, body) => {
|
|
457
|
+
let base = apiBaseFor(op.callid)
|
|
458
|
+
let out = await requestForOperation(op, base + path, body)
|
|
459
|
+
op.check()
|
|
460
|
+
if ((!out.code || out.code < 0) && base !== apiUrl) {
|
|
461
|
+
let session = getSessionByConn(op.connId)
|
|
462
|
+
if (session) session.apiBase = undefined
|
|
463
|
+
out = await requestForOperation(op, apiUrl + path, body)
|
|
464
|
+
}
|
|
465
|
+
return out
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let connectOperation = async (op, stream, talk) => {
|
|
469
|
+
op.check()
|
|
470
|
+
op.stream = stream
|
|
471
|
+
let session = initSession(op.callid, op)
|
|
472
|
+
session.localStream = stream
|
|
473
|
+
applySessionMuted(session)
|
|
474
|
+
let subscribed = await op.wait(
|
|
475
|
+
realtime.subscribe([`webrtc.account.${accid}.agent.${agid}.webrtc_connection.${session.connId}`]),
|
|
350
476
|
)
|
|
477
|
+
op.check()
|
|
478
|
+
if (subscribed && subscribed.error) throw operationError(subscribed.error)
|
|
479
|
+
session.status = 'joining'
|
|
480
|
+
let started = env.Date.now()
|
|
481
|
+
let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${session.connId}&call_id=${op.callid}&account_id=${accid}`
|
|
482
|
+
if (talk) url += '&talk=true'
|
|
483
|
+
let out = await requestForOperation(op, url)
|
|
484
|
+
op.check()
|
|
485
|
+
collect(talk ? 'join_call' : 'listen_call', op.callid, env.Date.now() - started)
|
|
486
|
+
if (out.code !== 200) throw operationError(out.body || 'listen_failed')
|
|
487
|
+
pinSessionApiBase(session.connId, out.body)
|
|
488
|
+
while (true) {
|
|
489
|
+
op.check()
|
|
490
|
+
let peer = session.peer
|
|
491
|
+
if (!peer || peer.signalingState === 'closed') throw operationError('closed')
|
|
492
|
+
if (peer.connectionState === 'failed' || peer.iceConnectionState === 'failed') throw operationError('failed')
|
|
493
|
+
if (peer.iceConnectionState === 'connected' || peer.iceConnectionState === 'completed') break
|
|
494
|
+
let timer
|
|
495
|
+
try {
|
|
496
|
+
await op.wait(
|
|
497
|
+
new env.Promise((resolve) => {
|
|
498
|
+
timer = env.setTimeout(resolve, 100)
|
|
499
|
+
}),
|
|
500
|
+
)
|
|
501
|
+
} finally {
|
|
502
|
+
env.clearTimeout(timer)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
op.check()
|
|
506
|
+
session.status = 'connected'
|
|
507
|
+
return session
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
let failOperation = (op, err) => {
|
|
511
|
+
let reason = (err && (err.code || err.message)) || err || 'setup_failed'
|
|
512
|
+
let current = getSessionByCall(op.callid)
|
|
513
|
+
let replaced = reason === 'replaced' || (current && current.connId !== op.connId)
|
|
514
|
+
let base = apiBaseFor(op.callid)
|
|
515
|
+
if (op.connId) cleanupCallConnection(op.callid, op.connId)
|
|
516
|
+
stopStream(op.stream)
|
|
517
|
+
if (!dead && !replaced && reason !== 'call_ended' && !isCallEndedLocally(op.callid)) {
|
|
518
|
+
expireCall({call_id: op.callid, hangup_code: String(reason)})
|
|
519
|
+
if (reason === 'setup_timeout' && op.connId) signalHangup(op.callid, base)
|
|
520
|
+
}
|
|
521
|
+
return reason
|
|
522
|
+
}
|
|
351
523
|
|
|
352
524
|
let readyToSendCandidateResolve = {}
|
|
353
525
|
let waitForRemoteDescription = {}
|
|
@@ -362,7 +534,7 @@ function WebRTCConn(options) {
|
|
|
362
534
|
})
|
|
363
535
|
}
|
|
364
536
|
|
|
365
|
-
let cleanupConnection = (connId) => {
|
|
537
|
+
let cleanupConnection = (connId, reason = 'call_ended') => {
|
|
366
538
|
if (!connId) return
|
|
367
539
|
let session = getSessionByConn(connId)
|
|
368
540
|
let peer = session && session.peer
|
|
@@ -416,17 +588,17 @@ function WebRTCConn(options) {
|
|
|
416
588
|
delete waitForRemoteDescription[connId]
|
|
417
589
|
delete readyToSendCandidateResolve[connId]
|
|
418
590
|
delete negotiationQueue[connId]
|
|
591
|
+
for (let op of operations) {
|
|
592
|
+
if (op.connId === connId) op.cancel(reason)
|
|
593
|
+
}
|
|
419
594
|
}
|
|
420
595
|
|
|
421
596
|
let cleanupCallConnection = (callid, connId) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (!
|
|
426
|
-
|
|
427
|
-
if (mappedCallId && mappedCallId != callid) return
|
|
428
|
-
markSessionEnded(callid)
|
|
429
|
-
cleanupConnection(connId)
|
|
597
|
+
// An explicit connection ID is authoritative. Never replace it with a newer
|
|
598
|
+
// session's ID when a stale request or ICE waiter finishes.
|
|
599
|
+
let session = connId ? getSessionByConn(connId) : getSessionByCall(callid)
|
|
600
|
+
if (!session || session.callid !== callid) return
|
|
601
|
+
cleanupConnection(session.connId)
|
|
430
602
|
}
|
|
431
603
|
|
|
432
604
|
let cleanEndedCall = () => {
|
|
@@ -543,7 +715,6 @@ function WebRTCConn(options) {
|
|
|
543
715
|
}
|
|
544
716
|
let _livenessInterval = env.setInterval(checkCallLiveness, 5000)
|
|
545
717
|
|
|
546
|
-
let endcallCb = {}
|
|
547
718
|
publish = (ev) => {
|
|
548
719
|
if (ev.type == 'call_ended') {
|
|
549
720
|
let callid = ev.data && ev.data.call_info && ev.data.call_info.call_id
|
|
@@ -555,9 +726,9 @@ function WebRTCConn(options) {
|
|
|
555
726
|
// do not clean up this now or some functions cannot work after the call ended
|
|
556
727
|
// DONOT: delete call2Connection[callid]
|
|
557
728
|
|
|
558
|
-
let
|
|
559
|
-
|
|
560
|
-
|
|
729
|
+
for (let op of operations) {
|
|
730
|
+
if (op.callid === callid) op.cancel('call_ended')
|
|
731
|
+
}
|
|
561
732
|
}
|
|
562
733
|
|
|
563
734
|
let direction = ev.data && ev.data.call_info && ev.data.call_info.direction
|
|
@@ -569,9 +740,15 @@ function WebRTCConn(options) {
|
|
|
569
740
|
this.shutdown = () => {
|
|
570
741
|
if (dead) return
|
|
571
742
|
dead = true
|
|
743
|
+
let callids = new Set(Object.keys(sessionsByCall))
|
|
572
744
|
env.clearInterval(_livenessInterval)
|
|
573
745
|
env.clearInterval(_endcalInterval)
|
|
574
|
-
|
|
746
|
+
for (let op of operations) {
|
|
747
|
+
callids.add(op.callid)
|
|
748
|
+
op.cancel('shutdown')
|
|
749
|
+
}
|
|
750
|
+
Object.keys(sessionsByConn).map((connId) => cleanupConnection(connId, 'shutdown'))
|
|
751
|
+
for (let callid of callids) expireCall({call_id: callid, hangup_code: 'shutdown'})
|
|
575
752
|
}
|
|
576
753
|
|
|
577
754
|
this.matchCall = (callid) => {
|
|
@@ -714,7 +891,6 @@ function WebRTCConn(options) {
|
|
|
714
891
|
.catch((err) => {
|
|
715
892
|
// không catch thì thành unhandled rejection; candidate hỏng chỉ cần log
|
|
716
893
|
console.error('[webrtc]', 'ADD CANDIDATE FAILED', connId, err)
|
|
717
|
-
logError(accid, agid, (err && err.message) || err, 'addIceCandidate')
|
|
718
894
|
})
|
|
719
895
|
}
|
|
720
896
|
|
|
@@ -767,11 +943,17 @@ function WebRTCConn(options) {
|
|
|
767
943
|
peer = session && session.peer
|
|
768
944
|
if (!peer || isCallEndedLocally(session && session.callid)) return
|
|
769
945
|
return peer.setLocalDescription(answer).then(() => {
|
|
946
|
+
if (
|
|
947
|
+
dead ||
|
|
948
|
+
getSessionByConn(connId) !== session ||
|
|
949
|
+
session.peer !== peer ||
|
|
950
|
+
isCallEndedLocally(session.callid)
|
|
951
|
+
)
|
|
952
|
+
return
|
|
770
953
|
// call_id: server dùng để log theo cuộc gọi
|
|
771
954
|
let path = `finish?x-access-token=${access_token}&connection_id=${connId}&call_id=${session.callid}&version=2&negotiation_id=${negid}&account_id=${accid}`
|
|
772
955
|
postPerCall(session.callid, path, jsonify(answer), function (body, code) {
|
|
773
956
|
if (code !== 200) {
|
|
774
|
-
logError(accid, agid, body, 'finish:295')
|
|
775
957
|
return console.error('CANNOT FINISH', body)
|
|
776
958
|
}
|
|
777
959
|
return
|
|
@@ -781,7 +963,6 @@ function WebRTCConn(options) {
|
|
|
781
963
|
}
|
|
782
964
|
negotiationQueue[connId] = (negotiationQueue[connId] || env.Promise.resolve()).then(handleOffer).catch((err) => {
|
|
783
965
|
console.error('[webrtc]', 'NEGOTIATION FAILED', connId, err)
|
|
784
|
-
logError(accid, agid, (err && err.message) || err, 'negotiation_offered')
|
|
785
966
|
})
|
|
786
967
|
return
|
|
787
968
|
}
|
|
@@ -824,16 +1005,14 @@ function WebRTCConn(options) {
|
|
|
824
1005
|
}
|
|
825
1006
|
})
|
|
826
1007
|
|
|
827
|
-
this.makeCall = (number, fromnumber, streamPm, callid, campaignid, outboundcallentryid, channel) => {
|
|
1008
|
+
this.makeCall = async (number, fromnumber, streamPm, callid, campaignid, outboundcallentryid, channel) => {
|
|
828
1009
|
if (!callid) callid = 'webcall-' + randomString(30)
|
|
829
|
-
if (isCallEndedLocally(callid)) {
|
|
830
|
-
if (streamPm && streamPm.then)
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
return new env.Promise((rs) => rs(getLocalEndedCall(callid)))
|
|
1010
|
+
if (dead || isCallEndedLocally(callid)) {
|
|
1011
|
+
if (streamPm && streamPm.then) env.Promise.resolve(streamPm).then(stopStream, reportCallbackError)
|
|
1012
|
+
return dead ? {error: 'shutdown'} : getLocalEndedCall(callid)
|
|
834
1013
|
}
|
|
835
|
-
|
|
836
|
-
|
|
1014
|
+
if (!streamPm || !streamPm.then) return {error: 'no_stream'}
|
|
1015
|
+
let op = startOperation(callid)
|
|
837
1016
|
let now = env.Date.now()
|
|
838
1017
|
rememberCall(callid)
|
|
839
1018
|
let call = {
|
|
@@ -849,235 +1028,120 @@ function WebRTCConn(options) {
|
|
|
849
1028
|
mic: 'waiting',
|
|
850
1029
|
}
|
|
851
1030
|
dialingRequest[callid] = call
|
|
852
|
-
publish({created: now, type: 'call_ringing', data: {call_info: {call_id: callid}}})
|
|
853
|
-
|
|
854
|
-
streamPm
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
cleanupCallConnection(callid, connId)
|
|
887
|
-
return rs(getLocalEndedCall(callid))
|
|
888
|
-
}
|
|
1031
|
+
publish({created: now, type: 'call_ringing', data: {call_info: {call_id: callid}}})
|
|
1032
|
+
try {
|
|
1033
|
+
let stream = await acquireStream(op, streamPm)
|
|
1034
|
+
op.check()
|
|
1035
|
+
call.mic = 'ready'
|
|
1036
|
+
publish({created: now, type: 'call_ringing', data: {call_info: {call_id: callid}}})
|
|
1037
|
+
let session = await connectOperation(op, stream, true)
|
|
1038
|
+
op.check()
|
|
1039
|
+
let path = `call?x-access-token=${access_token}&connection_id=${session.connId}&call_id=${callid}&number=${encodeURIComponent(number)}&from_number=${encodeURIComponent(fromnumber || '')}&account_id=${accid}`
|
|
1040
|
+
if (channel) path += `&channel=${encodeURIComponent(channel)}`
|
|
1041
|
+
if (outboundcallentryid) path += `&outbound_call_entry_id=${encodeURIComponent(outboundcallentryid)}`
|
|
1042
|
+
if (campaignid) path += `&campaign_id=${encodeURIComponent(campaignid)}`
|
|
1043
|
+
let out = await postForOperation(op, path)
|
|
1044
|
+
op.check()
|
|
1045
|
+
if (out.code !== 200) throw operationError(out.body || 'call_failed')
|
|
1046
|
+
let body = parseJSON(out.body)
|
|
1047
|
+
pinSessionApiBase(session.connId, body)
|
|
1048
|
+
if (body && body.call_id) {
|
|
1049
|
+
serverCalls[callid] = body
|
|
1050
|
+
delete dialingRequest[callid]
|
|
1051
|
+
}
|
|
1052
|
+
op.clearDeadline() // The call can now last indefinitely, until an end event.
|
|
1053
|
+
if (body && body.status === 'ended') {
|
|
1054
|
+
publish({created: env.Date.now(), type: 'call_ended', data: {call_info: body}})
|
|
1055
|
+
return this.matchCall(callid)
|
|
1056
|
+
}
|
|
1057
|
+
await op.wait(new env.Promise(() => {})) // publish(call_ended) settles this wait
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
let reason = failOperation(op, err)
|
|
1060
|
+
return reason === 'call_ended' ? getLocalEndedCall(callid) : {error: reason}
|
|
1061
|
+
} finally {
|
|
1062
|
+
op.dispose()
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
889
1065
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
// backend có thể chỉ trả {"ok":true} chứ không kèm call info. Trong trường hợp đó phải
|
|
906
|
-
// giữ nguyên dialing request, nếu không cuộc gọi sẽ biến mất khỏi store cho tới khi kết thúc
|
|
907
|
-
if (body && body.call_id) {
|
|
908
|
-
serverCalls[callid] = body
|
|
909
|
-
delete dialingRequest[callid]
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
let call = this.matchCall(callid)
|
|
913
|
-
if (call && call.status == 'ended') return rs(call)
|
|
914
|
-
if (!endcallCb[callid]) endcallCb[callid] = []
|
|
915
|
-
endcallCb[callid].push(() => rs(this.matchCall(callid)))
|
|
916
|
-
})
|
|
917
|
-
})
|
|
918
|
-
})
|
|
919
|
-
.catch((err) => rs({error: err}))
|
|
920
|
-
})
|
|
1066
|
+
let signalHangup = (callid, base) => {
|
|
1067
|
+
let path = `hangup?x-access-token=${access_token}&call_id=${callid}&account_id=${accid}`
|
|
1068
|
+
try {
|
|
1069
|
+
callAPI('post', base + path, undefined, (body, code) => {
|
|
1070
|
+
if (!dead && (!code || code < 0) && base !== apiUrl) {
|
|
1071
|
+
try {
|
|
1072
|
+
callAPI('post', apiUrl + path, undefined, () => {})
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
reportCallbackError(err)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
1078
|
+
} catch (err) {
|
|
1079
|
+
reportCallbackError(err)
|
|
1080
|
+
}
|
|
921
1081
|
}
|
|
922
1082
|
|
|
923
1083
|
this.hangupCall = (callid) => {
|
|
924
|
-
if (!callid) return
|
|
925
|
-
if (isCallEndedLocally(callid)) return
|
|
1084
|
+
if (dead || !callid || isCallEndedLocally(callid)) return
|
|
926
1085
|
endRequest[callid] = env.Date.now()
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
if (!call) {
|
|
930
|
-
call = {call_id: callid}
|
|
931
|
-
serverCalls[callid] = call
|
|
932
|
-
}
|
|
1086
|
+
let call = serverCalls[callid] || {call_id: callid}
|
|
1087
|
+
serverCalls[callid] = call
|
|
933
1088
|
call.status = 'ended'
|
|
934
1089
|
call.hangup_code = 'cancel'
|
|
935
1090
|
call.ended = env.Date.now()
|
|
936
|
-
markSessionEnded(callid)
|
|
937
|
-
// chốt host pin TRƯỚC publish: publish(call_ended) sẽ dọn session (mất pin)
|
|
938
1091
|
let hangupBase = apiBaseFor(callid)
|
|
939
|
-
publish({
|
|
940
|
-
|
|
941
|
-
type: 'call_ended',
|
|
942
|
-
data: {call_info: Object.assign({}, call, {call_id: callid, status: 'ended'})},
|
|
943
|
-
})
|
|
944
|
-
let path = `hangup?x-access-token=${access_token}&call_id=${callid}&account_id=${accid}`
|
|
945
|
-
callAPI('post', hangupBase + path, undefined, (body, code) => {
|
|
946
|
-
// lỗi mạng tới host pin -> thử lại qua gateway (gateway locate theo call_id)
|
|
947
|
-
if ((!code || code < 0) && hangupBase != apiUrl) callAPI('post', apiUrl + path, undefined, function () {})
|
|
948
|
-
})
|
|
1092
|
+
publish({created: env.Date.now(), type: 'call_ended', data: {call_info: Object.assign({}, call)}})
|
|
1093
|
+
signalHangup(callid, hangupBase)
|
|
949
1094
|
}
|
|
950
1095
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
cleanupCallConnection(callid, connId)
|
|
971
|
-
return cb('call_ended')
|
|
972
|
-
}
|
|
973
|
-
return code != 200 ? cb(body) : cb()
|
|
974
|
-
})
|
|
975
|
-
})
|
|
1096
|
+
let join = async (callid, stream, cb, talk) => {
|
|
1097
|
+
if (dead || isCallEndedLocally(callid)) {
|
|
1098
|
+
stopStream(stream)
|
|
1099
|
+
protect(cb)(dead ? 'shutdown' : 'call_ended')
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
let op = startOperation(callid)
|
|
1103
|
+
op.stream = stream
|
|
1104
|
+
try {
|
|
1105
|
+
let session = await connectOperation(op, stream, talk)
|
|
1106
|
+
op.check()
|
|
1107
|
+
op.dispose()
|
|
1108
|
+
protect(cb)(undefined, session.connId)
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
let reason = failOperation(op, err)
|
|
1111
|
+
protect(cb)(reason, op.connId)
|
|
1112
|
+
} finally {
|
|
1113
|
+
op.dispose()
|
|
1114
|
+
}
|
|
976
1115
|
}
|
|
977
1116
|
|
|
978
|
-
this.
|
|
979
|
-
|
|
980
|
-
initSession(callid, (err, connId) => {
|
|
981
|
-
if (err) {
|
|
982
|
-
stopStream(stream, connId)
|
|
983
|
-
cleanupConnection(connId)
|
|
984
|
-
return cb(err, connId)
|
|
985
|
-
}
|
|
986
|
-
let session = getSessionByConn(connId)
|
|
987
|
-
if (session) {
|
|
988
|
-
session.localStream = stream
|
|
989
|
-
session.status = 'joining'
|
|
990
|
-
applySessionMuted(session)
|
|
991
|
-
}
|
|
992
|
-
if (isCallEndedLocally(callid)) {
|
|
993
|
-
cleanupCallConnection(callid, connId)
|
|
994
|
-
return cb('call_ended', connId)
|
|
995
|
-
}
|
|
996
|
-
// /listen luôn đi qua gateway mặc định (chưa biết node neo); response
|
|
997
|
-
// mang api_url của node neo PC -> pin cho các request per-call sau
|
|
998
|
-
let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&talk=true&account_id=${accid}`
|
|
999
|
-
callAPI('post', url, undefined, (body, code) => {
|
|
1000
|
-
if (code != 200) logError(accid, agid, body, 'joinCall:413')
|
|
1001
|
-
if (code == 200) pinSessionApiBase(connId, body)
|
|
1002
|
-
collect('join_call', callid, env.Date.now() - start)
|
|
1003
|
-
if (isCallEndedLocally(callid)) {
|
|
1004
|
-
cleanupCallConnection(callid, connId)
|
|
1005
|
-
return cb('call_ended', connId)
|
|
1006
|
-
}
|
|
1007
|
-
if (code != 200) {
|
|
1008
|
-
return cb(body, connId)
|
|
1009
|
-
}
|
|
1010
|
-
let session = getSessionByConn(connId)
|
|
1011
|
-
checkIceConnected(
|
|
1012
|
-
session && session.peer,
|
|
1013
|
-
(err) => {
|
|
1014
|
-
if (err) {
|
|
1015
|
-
cleanupCallConnection(callid, connId)
|
|
1016
|
-
return cb(err, connId)
|
|
1017
|
-
}
|
|
1018
|
-
if (isCallEndedLocally(callid)) {
|
|
1019
|
-
cleanupCallConnection(callid, connId)
|
|
1020
|
-
return cb('call_ended', connId)
|
|
1021
|
-
}
|
|
1022
|
-
let session = getSessionByConn(connId)
|
|
1023
|
-
if (session) session.status = 'connected'
|
|
1024
|
-
cb(undefined, connId)
|
|
1025
|
-
},
|
|
1026
|
-
() => isCallEndedLocally(callid) || !getSessionByConn(connId),
|
|
1027
|
-
)
|
|
1028
|
-
})
|
|
1029
|
-
})
|
|
1030
|
-
}
|
|
1117
|
+
this.listenCall = (callid, cb) => join(callid, undefined, cb, false)
|
|
1118
|
+
this.joinCall = (callid, stream, cb) => join(callid, stream, cb, true)
|
|
1031
1119
|
|
|
1032
|
-
this.answerCall = (callid, stream) => {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (answerRequest[callid]) return
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
let path = `answer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&account_id=${accid}`
|
|
1058
|
-
if (isCallEndedLocally(callid)) {
|
|
1059
|
-
delete answerRequest[callid]
|
|
1060
|
-
cleanupCallConnection(callid, connId)
|
|
1061
|
-
return rs({body: getLocalEndedCall(callid)})
|
|
1062
|
-
}
|
|
1063
|
-
postPerCall(callid, path, undefined, (body, code) => {
|
|
1064
|
-
if (isCallEndedLocally(callid)) {
|
|
1065
|
-
delete answerRequest[callid]
|
|
1066
|
-
cleanupCallConnection(callid, connId)
|
|
1067
|
-
return rs({body: getLocalEndedCall(callid)})
|
|
1068
|
-
}
|
|
1069
|
-
body = parseJSON(body)
|
|
1070
|
-
if (code != 200) {
|
|
1071
|
-
logError(accid, agid, body, 'answerCall:442')
|
|
1072
|
-
delete answerRequest[callid]
|
|
1073
|
-
endRequest[callid] = now
|
|
1074
|
-
publish({created: env.Date.now(), type: 'call_ended', data: {call_info: {call_id: callid}}})
|
|
1075
|
-
return rs({error: body})
|
|
1076
|
-
}
|
|
1077
|
-
return rs({body})
|
|
1078
|
-
})
|
|
1079
|
-
})
|
|
1080
|
-
})
|
|
1120
|
+
this.answerCall = async (callid, stream) => {
|
|
1121
|
+
if (dead || isCallEndedLocally(callid)) {
|
|
1122
|
+
env.Promise.resolve(stream).then(stopStream, reportCallbackError)
|
|
1123
|
+
return {error: dead ? 'shutdown' : 'call_ended'}
|
|
1124
|
+
}
|
|
1125
|
+
if (answerRequest[callid]) return {body: this.matchCall(callid) || {call_id: callid}}
|
|
1126
|
+
let op = startOperation(callid)
|
|
1127
|
+
answerRequest[callid] = env.Date.now()
|
|
1128
|
+
publish({created: env.Date.now(), type: 'call_ringing', data: {call_info: {call_id: callid}}})
|
|
1129
|
+
try {
|
|
1130
|
+
stream = await acquireStream(op, stream)
|
|
1131
|
+
let session = await connectOperation(op, stream, true)
|
|
1132
|
+
op.check()
|
|
1133
|
+
let path = `answer?x-access-token=${access_token}&connection_id=${session.connId}&call_id=${callid}&account_id=${accid}`
|
|
1134
|
+
let out = await postForOperation(op, path)
|
|
1135
|
+
op.check()
|
|
1136
|
+
if (out.code !== 200) throw operationError(out.body || 'answer_failed')
|
|
1137
|
+
return {body: parseJSON(out.body)}
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
delete answerRequest[callid]
|
|
1140
|
+
let reason = failOperation(op, err)
|
|
1141
|
+
return {error: reason}
|
|
1142
|
+
} finally {
|
|
1143
|
+
op.dispose()
|
|
1144
|
+
}
|
|
1081
1145
|
}
|
|
1082
1146
|
|
|
1083
1147
|
this.sendDtmf = (key, callid) =>
|
|
@@ -1089,7 +1153,6 @@ function WebRTCConn(options) {
|
|
|
1089
1153
|
key,
|
|
1090
1154
|
)}&account_id=${accid}`
|
|
1091
1155
|
postPerCall(callid, path, undefined, (body, code) => {
|
|
1092
|
-
if (code != 200) logError(accid, agid, body, 'sendDtmf:463')
|
|
1093
1156
|
body = parseJSON(body)
|
|
1094
1157
|
return code != 200 ? rs({error: body}) : rs({body})
|
|
1095
1158
|
})
|
|
@@ -1105,7 +1168,6 @@ function WebRTCConn(options) {
|
|
|
1105
1168
|
tonumber,
|
|
1106
1169
|
)}&account_id=${accid}`
|
|
1107
1170
|
postPerCall(callid, path, undefined, (body, code) => {
|
|
1108
|
-
if (code != 200) logError(accid, agid, body, 'transferCall:478')
|
|
1109
1171
|
collect('transfer_call', callid, env.Date.now() - start)
|
|
1110
1172
|
body = parseJSON(body)
|
|
1111
1173
|
return code != 200 ? rs({error: body}) : rs({body})
|
|
@@ -1113,6 +1175,7 @@ function WebRTCConn(options) {
|
|
|
1113
1175
|
})
|
|
1114
1176
|
|
|
1115
1177
|
this.setCallMuted = (callid, muted) => {
|
|
1178
|
+
if (dead) return {error: 'shutdown'}
|
|
1116
1179
|
let session = ensureSession(callid)
|
|
1117
1180
|
if (!session) return {error: 'call_not_found'}
|
|
1118
1181
|
session.muted = !!muted
|