@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/webrtc.js CHANGED
@@ -1,5 +1,4 @@
1
- var flow = require('@subiz/flow')
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
- if (!collect) collect = function () {}
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
- realtime.subscribe([`webrtc.account.${accid}.agent.${agid}`])
290
+ env.Promise.resolve()
291
+ .then(() => realtime.subscribe([`webrtc.account.${accid}.agent.${agid}`]))
292
+ .catch(reportCallbackError)
274
293
 
275
- let initSession = (callid, cb) => {
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
- // topic agent đã subscribe lúc khởi tạo store, ở đây chỉ cần topic connection.
318
- // .catch bắt buộc: subscribe reject mà không catch thì cb không bao giờ được
319
- // gọi -> promise của makeCall/answerCall treo vĩnh viễn
320
- realtime
321
- .subscribe([`webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`])
322
- .then((out) => {
323
- if (out && out.error) return cb(out.error, connId)
324
- return cb(undefined, connId)
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
- .catch((err) => cb(err || 'subscribe_failed', connId))
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 checkIceConnected = (peer, cb, shouldStop) =>
330
- flow.loop(
331
- () =>
332
- new env.Promise((rs) => {
333
- if (shouldStop && shouldStop()) {
334
- cb && cb('cancelled')
335
- return rs(false)
336
- }
337
- if (!peer || peer.signalingState == 'closed' || peer.iceConnectionState == 'closed') {
338
- cb && cb('closed')
339
- return rs(false)
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
- if (peer.connectionState == 'failed' || peer.iceConnectionState == 'failed') {
342
- cb && cb('failed')
343
- return rs(false)
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
- let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
346
- if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping util connected
347
- cb && cb()
348
- rs(false)
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
- let session = getSessionByCall(callid) || getSessionByConn(connId)
423
- if (session && session.callid == callid && session.connId) connId = session.connId
424
- if (session && !connId) connId = session.connId
425
- if (!connId) return
426
- let mappedCallId = session && session.callid
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 arr = endcallCb[callid]
559
- if (arr && arr.length > 0 && arr.map) arr.map((cb) => cb())
560
- delete endcallCb[callid]
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
- Object.keys(sessionsByConn).map((connId) => cleanupConnection(connId))
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
- streamPm.then((stream) => stopStream(stream, callid)).catch(() => {})
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
- // thiếu stream promise thì trả lỗi có cấu trúc thay vì throw giữa executor
836
- if (!streamPm || !streamPm.then) return env.Promise.resolve({error: 'no_stream'})
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}}}) // fake dialing
853
- return new env.Promise((rs) => {
854
- streamPm
855
- .then((stream) => {
856
- call.mic = 'ready'
857
- if (isCallEndedLocally(callid)) {
858
- delete dialingRequest[callid]
859
- stopStream(stream, callid)
860
- return rs(getLocalEndedCall(callid))
861
- }
862
- publish({created: now, type: 'call_ringing', data: {call_info: {call_id: callid}}}) // fake dialing
863
- this.joinCall(callid, stream, (err, connId) => {
864
- if (isCallEndedLocally(callid)) {
865
- delete dialingRequest[callid]
866
- cleanupCallConnection(callid, connId)
867
- return rs(getLocalEndedCall(callid))
868
- }
869
- if (err) {
870
- delete dialingRequest[callid]
871
- endRequest[callid] = env.Date.now()
872
- publish({created: env.Date.now(), type: 'call_ended', data: {call_info: {call_id: callid}}})
873
- return rs({error: err})
874
- }
875
-
876
- // encodeURIComponent bắt buộc: số dạng "+84..." không encode thì dấu +
877
- // bị server decode thành khoảng trắng
878
- let path = `call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${encodeURIComponent(
879
- number,
880
- )}&from_number=${encodeURIComponent(fromnumber || '')}&account_id=${accid}`
881
- if (channel) path += `&channel=${encodeURIComponent(channel)}`
882
- if (outboundcallentryid) path += `&outbound_call_entry_id=${encodeURIComponent(outboundcallentryid)}`
883
- if (campaignid) path += `&campaign_id=${encodeURIComponent(campaignid)}`
884
- if (isCallEndedLocally(callid)) {
885
- delete dialingRequest[callid]
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
- postPerCall(callid, path, undefined, (body, code) => {
891
- if (isCallEndedLocally(callid)) {
892
- delete dialingRequest[callid]
893
- cleanupCallConnection(callid, connId)
894
- return rs(getLocalEndedCall(callid))
895
- }
896
- if (code != 200) {
897
- logError(accid, agid, body, 'makeCall:365')
898
- delete dialingRequest[callid]
899
- endRequest[callid] = env.Date.now()
900
- publish({created: env.Date.now(), type: 'call_ended', data: {call_info: {call_id: callid}}})
901
- return rs({error: body})
902
- }
903
- body = parseJSON(body)
904
- pinSessionApiBase(connId, body) // node neo PC xác nhận lại host trong response /call
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
- let call = serverCalls[callid]
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
- created: env.Date.now(),
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
- this.listenCall = (callid, cb) => {
952
- let start = env.Date.now()
953
- initSession(callid, (err, connId) => {
954
- if (err) {
955
- cleanupConnection(connId)
956
- return cb(err)
957
- }
958
- if (isCallEndedLocally(callid)) {
959
- cleanupCallConnection(callid, connId)
960
- return cb('call_ended')
961
- }
962
- // /listen luôn đi qua gateway mặc định (chưa biết node neo); response
963
- // mang api_url của node neo PC -> pin cho các request per-call sau
964
- let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&account_id=${accid}`
965
- callAPI('post', url, undefined, (body, code) => {
966
- if (code != 200) logError(accid, agid, body, 'listenCall:399')
967
- if (code == 200) pinSessionApiBase(connId, body)
968
- collect('listen_call', callid, env.Date.now() - start)
969
- if (isCallEndedLocally(callid)) {
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.joinCall = (callid, stream, cb) => {
979
- let start = env.Date.now()
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
- let call = serverCalls[callid]
1034
- if (call && call.status == 'ended') return env.Promise.resolve({error: 'call_ended'})
1035
- let now = env.Date.now()
1036
- // call có thể chưa có trong serverCalls -> trả object tối thiểu thay vì body: undefined
1037
- if (answerRequest[callid]) return env.Promise.resolve({body: call || this.matchCall(callid) || {call_id: callid}})
1038
- answerRequest[callid] = now
1039
- publish({created: env.Date.now(), type: 'call_ringing', data: {call_info: {call_id: callid}}}) // fake dialing
1040
- return new env.Promise((rs) => {
1041
- if (isCallEndedLocally(callid)) {
1042
- stopStream(stream, callid)
1043
- return rs({body: getLocalEndedCall(callid)})
1044
- }
1045
- this.joinCall(callid, stream, (err, connId) => {
1046
- if (isCallEndedLocally(callid)) {
1047
- delete answerRequest[callid]
1048
- cleanupCallConnection(callid, connId)
1049
- return rs({body: getLocalEndedCall(callid)})
1050
- }
1051
- if (err) {
1052
- delete answerRequest[callid]
1053
- endRequest[callid] = now
1054
- publish({created: env.Date.now(), type: 'call_ended', data: {call_info: {call_id: callid}}})
1055
- return rs({error: err})
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