@subiz/wsclient 4.0.35 → 4.0.37
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 +398 -15
- package/common.js +54 -53
- package/package.json +11 -10
- package/realtime.js +274 -127
- package/webphone.js +38 -26
- package/webrtc.js +961 -278
- package/.prettierrc.js +0 -19
- package/realtime.test.js +0 -338
- package/vite.config.js +0 -16
package/webrtc.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
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
|
}
|
|
6
5
|
|
|
7
|
-
//
|
|
8
|
-
// env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, setInterval, clearInterval, jsonify, parseJSON}
|
|
9
|
-
// set env._is_webrtc_local to true for local testing
|
|
6
|
+
// env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, clearTimeout, setInterval, clearInterval, jsonify, parseJSON}
|
|
10
7
|
function WebRTCConn(options) {
|
|
11
8
|
let {apiUrl, access_token, accid, agid, realtime, callAPI, env, onTrack, onEvent, collect} = options
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
23
|
+
if (!apiUrl) apiUrl = 'https://callcenter.subiz.com.vn/wc/'
|
|
17
24
|
|
|
18
25
|
let dead = false
|
|
19
26
|
callAPI = callAPI || xhrsend // allow hook
|
|
@@ -27,124 +34,572 @@ function WebRTCConn(options) {
|
|
|
27
34
|
return env.JSON.stringify(str)
|
|
28
35
|
} catch (e) {}
|
|
29
36
|
}
|
|
30
|
-
let streams = {}
|
|
31
37
|
let serverCalls = {}
|
|
32
38
|
|
|
33
39
|
let call2Connection = {}
|
|
34
40
|
let dialingRequest = {}
|
|
35
41
|
let answerRequest = {}
|
|
36
42
|
let endRequest = {}
|
|
43
|
+
let callSeenAt = {}
|
|
44
|
+
let publish
|
|
45
|
+
|
|
46
|
+
// ICE consent is the primary liveness signal. RTP is only a secondary signal because
|
|
47
|
+
// silence is a valid call state and must not cause an immediate local hangup.
|
|
48
|
+
const disconnectGraceMs = 30000
|
|
49
|
+
const rtpStaleMs = 90000
|
|
50
|
+
const inviteStaleMs = 120000
|
|
51
|
+
const setupTimeoutMs = options.setupTimeoutMs > 0 ? options.setupTimeoutMs : 60000
|
|
52
|
+
let operations = new Set()
|
|
53
|
+
|
|
54
|
+
let sessionsByCall = {}
|
|
55
|
+
let sessionsByConn = {}
|
|
56
|
+
|
|
57
|
+
let getSessionByCall = (callid) => {
|
|
58
|
+
if (!callid) return undefined
|
|
59
|
+
return sessionsByCall[callid]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let getSessionByConn = (connId) => {
|
|
63
|
+
if (!connId) return undefined
|
|
64
|
+
return sessionsByConn[connId]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let getConnIdByCall = (callid) => {
|
|
68
|
+
let session = getSessionByCall(callid)
|
|
69
|
+
return (session && session.connId) || call2Connection[callid]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Server trả `api_url` trong response /listen|/call = node đang neo
|
|
73
|
+
// PeerConnection. Client pin session vào đó: các request per-call sau
|
|
74
|
+
// (finish/ice/answer/dtmf/refer/hangup) đi thẳng node neo, khỏi qua hop proxy
|
|
75
|
+
// gateway. Server không trả api_url (single node/dev) -> giữ nguyên apiUrl.
|
|
76
|
+
let apiHostSuffixes = options.apiHostSuffixes || ['.subiz.com.vn']
|
|
77
|
+
let normalizeApiBase = (u) => {
|
|
78
|
+
if (!u || typeof u != 'string') return undefined
|
|
79
|
+
if (u.slice(0, 8) != 'https://') return undefined
|
|
80
|
+
let host = u.slice(8).split('/')[0].split(':')[0].toLowerCase()
|
|
81
|
+
if (!host) return undefined
|
|
82
|
+
let ok = false
|
|
83
|
+
apiHostSuffixes.map((sfx) => {
|
|
84
|
+
if (!sfx) return
|
|
85
|
+
sfx = sfx.toLowerCase()
|
|
86
|
+
if (sfx[0] == '.') {
|
|
87
|
+
if (host.length > sfx.length && host.slice(-sfx.length) == sfx) ok = true
|
|
88
|
+
} else if (host == sfx) ok = true
|
|
89
|
+
})
|
|
90
|
+
if (!ok) return undefined
|
|
91
|
+
return u[u.length - 1] == '/' ? u : u + '/'
|
|
92
|
+
}
|
|
37
93
|
|
|
38
|
-
|
|
94
|
+
// chỉ nhận host thuộc whitelist — token nằm trong query string, nghe theo một
|
|
95
|
+
// api_url giả là dâng token cho host lạ
|
|
96
|
+
let pinSessionApiBase = (connId, body) => {
|
|
97
|
+
let data = typeof body == 'string' ? parseJSON(body) : body
|
|
98
|
+
let base = normalizeApiBase(data && data.api_url)
|
|
99
|
+
let session = getSessionByConn(connId)
|
|
100
|
+
if (!base || !session) return
|
|
101
|
+
if (session.apiBase != base) console.info('[webrtc]', 'PIN API HOST', 'call_id', session.callid, base)
|
|
102
|
+
session.apiBase = base
|
|
103
|
+
}
|
|
39
104
|
|
|
40
|
-
|
|
41
|
-
|
|
105
|
+
let apiBaseFor = (callid) => {
|
|
106
|
+
let session = getSessionByCall(callid)
|
|
107
|
+
return (session && session.apiBase) || apiUrl
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// POST per-call: ưu tiên host đã pin; lỗi MẠNG (status 0/-1, không phải 4xx/5xx)
|
|
111
|
+
// thì bỏ pin và thử lại một lần qua gateway mặc định — gateway vẫn proxy về
|
|
112
|
+
// home được nên không bao giờ tệ hơn trước khi pin
|
|
113
|
+
let postPerCall = (callid, path, body, cb) => {
|
|
114
|
+
if (dead) return cb && cb(jsonify({error: 'shutdown'}), -1)
|
|
115
|
+
let owner = getSessionByCall(callid)
|
|
116
|
+
let base = apiBaseFor(callid)
|
|
117
|
+
callAPI('post', base + path, body, (respBody, code) => {
|
|
118
|
+
if (dead || (owner && getSessionByCall(callid) !== owner)) return
|
|
119
|
+
if ((!code || code < 0) && base != apiUrl) {
|
|
120
|
+
console.warn('[webrtc]', 'PINNED HOST UNREACHABLE -> FALLBACK GATEWAY', 'call_id', callid, base)
|
|
121
|
+
let session = getSessionByCall(callid)
|
|
122
|
+
if (session && session.apiBase == base) session.apiBase = undefined
|
|
123
|
+
return callAPI('post', apiUrl + path, body, cb || function () {})
|
|
124
|
+
}
|
|
125
|
+
cb && cb(respBody, code)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let ensureSession = (callid) => {
|
|
130
|
+
if (!callid) return undefined
|
|
131
|
+
let session = sessionsByCall[callid]
|
|
132
|
+
if (!session) {
|
|
133
|
+
session = {
|
|
134
|
+
callid,
|
|
135
|
+
connId: undefined,
|
|
136
|
+
peer: undefined,
|
|
137
|
+
localStream: undefined,
|
|
138
|
+
remoteStream: undefined,
|
|
139
|
+
status: 'new',
|
|
140
|
+
}
|
|
141
|
+
sessionsByCall[callid] = session
|
|
142
|
+
}
|
|
143
|
+
return session
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let bindSessionConnection = (callid, connId, peer) => {
|
|
147
|
+
let session = ensureSession(callid)
|
|
148
|
+
if (!session) return undefined
|
|
149
|
+
let connOwner = getSessionByConn(connId)
|
|
150
|
+
if (connOwner && connOwner !== session && connOwner.callid && connOwner.callid != callid) {
|
|
151
|
+
console.warn(
|
|
152
|
+
'[webrtc]',
|
|
153
|
+
'SKIP bind connection owned by another call',
|
|
154
|
+
'call_id',
|
|
155
|
+
callid,
|
|
156
|
+
'conn_id',
|
|
157
|
+
connId,
|
|
158
|
+
'owner_call_id',
|
|
159
|
+
connOwner.callid,
|
|
160
|
+
)
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
if (session.connId && session.connId != connId && sessionsByConn[session.connId] === session) {
|
|
164
|
+
delete sessionsByConn[session.connId]
|
|
165
|
+
}
|
|
166
|
+
session.connId = connId
|
|
167
|
+
if (peer) session.peer = peer
|
|
168
|
+
if (connId) {
|
|
169
|
+
sessionsByConn[connId] = session
|
|
170
|
+
call2Connection[callid] = connId
|
|
171
|
+
}
|
|
172
|
+
return session
|
|
173
|
+
}
|
|
42
174
|
|
|
43
|
-
let
|
|
175
|
+
let markSessionEnded = (callid) => {
|
|
176
|
+
let session = getSessionByCall(callid)
|
|
177
|
+
if (!session) return
|
|
178
|
+
session.status = 'ended'
|
|
179
|
+
session.cancelled = true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let applySessionMuted = (session) => {
|
|
183
|
+
if (!session || !session.localStream) return
|
|
184
|
+
session.localStream.getAudioTracks().forEach((track) => {
|
|
185
|
+
try {
|
|
186
|
+
track.enabled = !session.muted
|
|
187
|
+
} catch (e) {}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let isCallEndedLocally = (callid) => {
|
|
192
|
+
if (!callid) return false
|
|
193
|
+
let call = serverCalls[callid]
|
|
194
|
+
let session = getSessionByCall(callid)
|
|
195
|
+
return !!endRequest[callid] || (session && session.cancelled) || (call && call.status == 'ended')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let rememberCall = (callid) => {
|
|
199
|
+
if (callid && callSeenAt[callid] === undefined) callSeenAt[callid] = env.Date.now()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let expireCall = (call_info = {}) => {
|
|
203
|
+
let callid = call_info.call_id
|
|
204
|
+
if (!callid || isCallEndedLocally(callid)) return
|
|
205
|
+
let call = Object.assign({}, dialingRequest[callid], serverCalls[callid], call_info, {
|
|
206
|
+
call_id: callid,
|
|
207
|
+
status: 'ended',
|
|
208
|
+
ended: call_info.ended || env.Date.now(),
|
|
209
|
+
hangup_code: call_info.hangup_code || 'cancel',
|
|
210
|
+
})
|
|
211
|
+
serverCalls[callid] = call
|
|
212
|
+
publish({
|
|
213
|
+
created: env.Date.now(),
|
|
214
|
+
type: 'call_ended',
|
|
215
|
+
data: {call_info: call},
|
|
216
|
+
})
|
|
217
|
+
return call
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let clearDisconnectTimer = (session) => {
|
|
221
|
+
if (!session || !session.disconnectTimer) return
|
|
222
|
+
if (env.clearTimeout) env.clearTimeout(session.disconnectTimer)
|
|
223
|
+
else env.clearInterval(session.disconnectTimer)
|
|
224
|
+
session.disconnectTimer = undefined
|
|
225
|
+
session.disconnectedAt = undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let expireTransportSession = (session, hangupCode) => {
|
|
229
|
+
if (!session || session.transportEnded || isCallEndedLocally(session.callid)) return
|
|
230
|
+
session.transportEnded = true
|
|
231
|
+
clearDisconnectTimer(session)
|
|
232
|
+
expireCall({
|
|
233
|
+
call_id: session.callid,
|
|
234
|
+
device_id: session.connId,
|
|
235
|
+
hangup_code: hangupCode,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let handlePeerStateChange = (callid, connId, peer) => {
|
|
240
|
+
let session = getSessionByConn(connId)
|
|
241
|
+
if (!session || session.callid != callid || session.peer !== peer || session.transportEnded) return
|
|
242
|
+
let state = peer.connectionState
|
|
243
|
+
let iceState = peer.iceConnectionState
|
|
244
|
+
let now = env.Date.now()
|
|
245
|
+
let connected = state == 'connected' || iceState == 'connected' || iceState == 'completed'
|
|
246
|
+
let failed = state == 'failed' || iceState == 'failed'
|
|
247
|
+
let closed = state == 'closed' || iceState == 'closed'
|
|
248
|
+
let disconnected = state == 'disconnected' || iceState == 'disconnected'
|
|
249
|
+
|
|
250
|
+
if (failed || closed) {
|
|
251
|
+
return expireTransportSession(session, failed ? 'webrtc_transport_failed' : 'webrtc_transport_closed')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (disconnected) {
|
|
255
|
+
if (session.disconnectTimer) return
|
|
256
|
+
session.disconnectedAt = now
|
|
257
|
+
session.disconnectTimer = env.setTimeout(() => {
|
|
258
|
+
let current = getSessionByConn(connId)
|
|
259
|
+
if (!current || current.peer !== peer || current.transportEnded) return
|
|
260
|
+
let currentState = peer.connectionState
|
|
261
|
+
let currentIceState = peer.iceConnectionState
|
|
262
|
+
if (currentState != 'disconnected' && currentIceState != 'disconnected') {
|
|
263
|
+
clearDisconnectTimer(current)
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
expireTransportSession(current, 'webrtc_disconnected_timeout')
|
|
267
|
+
}, disconnectGraceMs)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (connected) {
|
|
272
|
+
session.everConnected = true
|
|
273
|
+
session.lastTransportHealthyAt = now
|
|
274
|
+
}
|
|
275
|
+
clearDisconnectTimer(session)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let getLocalEndedCall = (callid) => {
|
|
279
|
+
return (
|
|
280
|
+
this.matchCall(callid) || {
|
|
281
|
+
call_id: callid,
|
|
282
|
+
ended: endRequest[callid] || env.Date.now(),
|
|
283
|
+
status: 'ended',
|
|
284
|
+
hangup_code: 'cancel',
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// to receive incoming call
|
|
290
|
+
env.Promise.resolve()
|
|
291
|
+
.then(() => realtime.subscribe([`webrtc.account.${accid}.agent.${agid}`]))
|
|
292
|
+
.catch(reportCallbackError)
|
|
293
|
+
|
|
294
|
+
let initSession = (callid, op) => {
|
|
295
|
+
op.check()
|
|
44
296
|
let connId = 'webrtc' + randomString(20)
|
|
45
297
|
let peer = new env.RTCPeerConnection(servers)
|
|
46
|
-
|
|
47
|
-
|
|
298
|
+
// call này đã có session gắn connection khác (listenCall rồi mới answerCall,
|
|
299
|
+
// hoặc retry join): phải đóng connection cũ trước khi bind cái mới — peer bị
|
|
300
|
+
// thay không tự chết, ICE/mic của nó tiếp tục chạy (leak)
|
|
301
|
+
let prev = getSessionByCall(callid)
|
|
302
|
+
let prevMuted = prev && prev.muted
|
|
303
|
+
if (prev && prev.peer && prev.connId && prev.connId != connId) {
|
|
304
|
+
cleanupConnection(prev.connId, 'replaced')
|
|
305
|
+
}
|
|
306
|
+
op.connId = connId
|
|
307
|
+
let session = bindSessionConnection(callid, connId, peer)
|
|
308
|
+
if (session) {
|
|
309
|
+
session.status = 'connecting'
|
|
310
|
+
if (prevMuted !== undefined) session.muted = prevMuted
|
|
311
|
+
}
|
|
312
|
+
peer.ontrack = (event) => {
|
|
313
|
+
let session = getSessionByConn(connId)
|
|
314
|
+
// session.peer !== peer: connection này đã bị thay bởi lần bind mới hơn —
|
|
315
|
+
// track của nó là track mồ côi, không được đưa lên UI
|
|
316
|
+
if (!session || session.peer !== peer || isCallEndedLocally(session.callid)) {
|
|
317
|
+
if (event.streams && event.streams[0]) stopStream(event.streams[0], connId)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (event.streams && event.streams[0]) session.remoteStream = event.streams[0]
|
|
321
|
+
onTrack && onTrack(event)
|
|
322
|
+
}
|
|
323
|
+
let onPeerStateChange = () => handlePeerStateChange(callid, connId, peer)
|
|
324
|
+
peer.onconnectionstatechange = onPeerStateChange
|
|
325
|
+
peer.oniceconnectionstatechange = onPeerStateChange
|
|
48
326
|
waitForRemoteDescription[connId] = new env.Promise((rs) => (readyToSendCandidateResolve[connId] = rs))
|
|
49
327
|
|
|
50
328
|
peer.onicecandidate = (event) => {
|
|
51
329
|
if (!event.candidate) return
|
|
52
|
-
if (dead) return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
330
|
+
if (dead || getSessionByConn(connId) !== session || isCallEndedLocally(callid)) return
|
|
331
|
+
postPerCall(
|
|
332
|
+
callid,
|
|
333
|
+
`ice-candidates?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&account_id=${accid}`,
|
|
334
|
+
jsonify(event.candidate),
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
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
|
+
}
|
|
66
391
|
})
|
|
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
|
|
401
|
+
}
|
|
402
|
+
|
|
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)
|
|
433
|
+
}
|
|
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()
|
|
447
|
+
}
|
|
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
|
|
67
466
|
}
|
|
68
467
|
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}),
|
|
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}`]),
|
|
78
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
|
+
}
|
|
79
523
|
|
|
80
524
|
let readyToSendCandidateResolve = {}
|
|
81
525
|
let waitForRemoteDescription = {}
|
|
82
|
-
let
|
|
526
|
+
let negotiationQueue = {} // connId -> promise chain, xử lý tuần tự các renegotiation offer
|
|
527
|
+
let stopStream = (stream, connId) => {
|
|
83
528
|
if (!stream) return
|
|
84
529
|
stream.getTracks().forEach((track) => {
|
|
85
|
-
|
|
86
|
-
|
|
530
|
+
try {
|
|
531
|
+
track.stop()
|
|
532
|
+
track.enabled = false
|
|
533
|
+
} catch (e) {}
|
|
87
534
|
})
|
|
88
535
|
}
|
|
89
536
|
|
|
90
|
-
let
|
|
91
|
-
if (
|
|
92
|
-
let
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
537
|
+
let cleanupConnection = (connId, reason = 'call_ended') => {
|
|
538
|
+
if (!connId) return
|
|
539
|
+
let session = getSessionByConn(connId)
|
|
540
|
+
let peer = session && session.peer
|
|
541
|
+
if (session) {
|
|
542
|
+
session.transportEnded = true
|
|
543
|
+
clearDisconnectTimer(session)
|
|
544
|
+
}
|
|
545
|
+
if (peer) {
|
|
546
|
+
if (peer.getSenders) {
|
|
547
|
+
peer.getSenders().forEach((sender) => {
|
|
548
|
+
try {
|
|
549
|
+
if (sender.track) sender.track.stop()
|
|
550
|
+
} catch (e) {}
|
|
551
|
+
if (sender.replaceTrack) {
|
|
552
|
+
let promise = sender.replaceTrack(null)
|
|
553
|
+
if (promise && promise.catch) promise.catch(() => {})
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
if (peer.getReceivers) {
|
|
558
|
+
peer.getReceivers().forEach((receiver) => {
|
|
559
|
+
try {
|
|
560
|
+
if (receiver.track) receiver.track.stop()
|
|
561
|
+
} catch (e) {}
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
if (peer.getTransceivers) {
|
|
565
|
+
peer.getTransceivers().forEach((transceiver) => {
|
|
566
|
+
try {
|
|
567
|
+
if (transceiver.stop && peer.signalingState !== 'closed') transceiver.stop()
|
|
568
|
+
} catch (e) {}
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
if (peer.signalingState !== 'closed') peer.close()
|
|
573
|
+
} catch (e) {}
|
|
574
|
+
}
|
|
122
575
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
576
|
+
let localStream = session && session.localStream
|
|
577
|
+
let remoteStream = session && session.remoteStream
|
|
578
|
+
stopStream(localStream, connId)
|
|
579
|
+
stopStream(remoteStream, connId)
|
|
580
|
+
if (session) {
|
|
581
|
+
session.peer = undefined
|
|
582
|
+
session.localStream = undefined
|
|
583
|
+
session.remoteStream = undefined
|
|
584
|
+
session.status = 'ended'
|
|
585
|
+
delete sessionsByConn[connId]
|
|
586
|
+
if (session.callid && sessionsByCall[session.callid] === session) delete sessionsByCall[session.callid]
|
|
587
|
+
}
|
|
588
|
+
delete waitForRemoteDescription[connId]
|
|
589
|
+
delete readyToSendCandidateResolve[connId]
|
|
590
|
+
delete negotiationQueue[connId]
|
|
591
|
+
for (let op of operations) {
|
|
592
|
+
if (op.connId === connId) op.cancel(reason)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
136
595
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})
|
|
144
|
-
})
|
|
596
|
+
let cleanupCallConnection = (callid, 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)
|
|
145
602
|
}
|
|
146
|
-
syncCallWithServer()
|
|
147
|
-
let _syncCallInterval = env.setInterval(syncCallWithServer, 10000)
|
|
148
603
|
|
|
149
604
|
let cleanEndedCall = () => {
|
|
150
605
|
if (dead) return env.clearInterval(_endcalInterval)
|
|
@@ -153,46 +608,127 @@ function WebRTCConn(options) {
|
|
|
153
608
|
})
|
|
154
609
|
|
|
155
610
|
Object.keys(answerRequest).map((callid) => {
|
|
156
|
-
if (env.Date.now() -
|
|
611
|
+
if (env.Date.now() - answerRequest[callid] > 60000) delete answerRequest[callid]
|
|
157
612
|
})
|
|
158
613
|
|
|
159
614
|
Object.keys(serverCalls).map((callid) => {
|
|
160
615
|
let call = serverCalls[callid]
|
|
161
616
|
if (!call) return delete serverCalls[callid]
|
|
162
617
|
if (call.status != 'ended') return
|
|
163
|
-
|
|
618
|
+
// call.ended có thể thiếu -> NaN > 60000 luôn false, entry không bao giờ được dọn
|
|
619
|
+
if (env.Date.now() - (call.ended || 0) > 60000) delete serverCalls[callid]
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
Object.keys(dialingRequest).map((callid) => {
|
|
623
|
+
let call = this.matchCall(callid)
|
|
624
|
+
if (!call) return delete dialingRequest[callid]
|
|
625
|
+
if (call.status != 'ended') return
|
|
626
|
+
if (env.Date.now() - (call.ended || 0) > 60000) delete dialingRequest[callid]
|
|
164
627
|
})
|
|
165
628
|
|
|
166
629
|
Object.keys(call2Connection).map((callid) => {
|
|
167
630
|
let call = this.matchCall(callid)
|
|
168
631
|
if (!call) return delete call2Connection[callid]
|
|
169
632
|
if (call.status != 'ended') return
|
|
170
|
-
if (env.Date.now() - call.ended > 60000) delete call2Connection[callid]
|
|
633
|
+
if (env.Date.now() - (call.ended || 0) > 60000) delete call2Connection[callid]
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
Object.keys(callSeenAt).map((callid) => {
|
|
637
|
+
let call = serverCalls[callid] || dialingRequest[callid]
|
|
638
|
+
if (!call || (call.status == 'ended' && env.Date.now() - (call.ended || 0) > 60000)) {
|
|
639
|
+
delete callSeenAt[callid]
|
|
640
|
+
}
|
|
171
641
|
})
|
|
172
642
|
}
|
|
173
643
|
let _endcalInterval = env.setInterval(cleanEndedCall, 120000)
|
|
174
644
|
|
|
175
|
-
let
|
|
176
|
-
|
|
645
|
+
let refreshRtpStats = (session) => {
|
|
646
|
+
let peer = session && session.peer
|
|
647
|
+
if (!peer || !peer.getStats || session.transportEnded) return
|
|
648
|
+
let statsPromise
|
|
649
|
+
try {
|
|
650
|
+
statsPromise = peer.getStats()
|
|
651
|
+
} catch (e) {
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
if (!statsPromise || !statsPromise.then) return
|
|
655
|
+
statsPromise
|
|
656
|
+
.then((reports) => {
|
|
657
|
+
if (!reports || !reports.forEach || session.peer !== peer || session.transportEnded) return
|
|
658
|
+
let packets = 0
|
|
659
|
+
let hasRtpReport = false
|
|
660
|
+
reports.forEach((report) => {
|
|
661
|
+
if (report.type != 'inbound-rtp' && report.type != 'outbound-rtp') return
|
|
662
|
+
let kind = report.kind || report.mediaType
|
|
663
|
+
if (kind && kind != 'audio') return
|
|
664
|
+
hasRtpReport = true
|
|
665
|
+
packets += Number(report.packetsReceived || 0) + Number(report.packetsSent || 0)
|
|
666
|
+
})
|
|
667
|
+
if (!hasRtpReport) return
|
|
668
|
+
let now = env.Date.now()
|
|
669
|
+
// A renegotiation may replace the RTP stream and reset counters. Any counter
|
|
670
|
+
// change, including a decrease, is therefore evidence of fresh media activity.
|
|
671
|
+
if (session.rtpPackets === undefined || packets != session.rtpPackets) session.lastRtpAt = now
|
|
672
|
+
if (session.rtpObservedAt === undefined) session.rtpObservedAt = now
|
|
673
|
+
session.rtpPackets = packets
|
|
674
|
+
|
|
675
|
+
let call = serverCalls[session.callid] || dialingRequest[session.callid] || {}
|
|
676
|
+
let isInvite =
|
|
677
|
+
!call.status ||
|
|
678
|
+
call.status == 'new' ||
|
|
679
|
+
call.status == 'dialing' ||
|
|
680
|
+
call.status == 'ringing' ||
|
|
681
|
+
call.status == 'pending'
|
|
682
|
+
let lastRtpAt = session.lastRtpAt || session.rtpObservedAt
|
|
683
|
+
if (!isInvite && session.everConnected && lastRtpAt && now - lastRtpAt > rtpStaleMs) {
|
|
684
|
+
expireTransportSession(session, 'webrtc_rtp_timeout')
|
|
685
|
+
}
|
|
686
|
+
})
|
|
687
|
+
.catch(() => {})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let checkCallLiveness = () => {
|
|
691
|
+
if (dead) return env.clearInterval(_livenessInterval)
|
|
692
|
+
let now = env.Date.now()
|
|
693
|
+
Object.keys(sessionsByCall).map((callid) => {
|
|
694
|
+
let session = sessionsByCall[callid]
|
|
695
|
+
if (!session || !session.peer) return
|
|
696
|
+
handlePeerStateChange(callid, session.connId, session.peer)
|
|
697
|
+
refreshRtpStats(session)
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
let calls = Object.assign({}, dialingRequest, serverCalls)
|
|
701
|
+
Object.keys(calls).map((callid) => {
|
|
702
|
+
let call = calls[callid]
|
|
703
|
+
if (!call || call.status == 'ended' || getSessionByCall(callid)) return
|
|
704
|
+
rememberCall(callid)
|
|
705
|
+
let isInvite =
|
|
706
|
+
!call.status ||
|
|
707
|
+
call.status == 'new' ||
|
|
708
|
+
call.status == 'dialing' ||
|
|
709
|
+
call.status == 'ringing' ||
|
|
710
|
+
call.status == 'pending'
|
|
711
|
+
if (isInvite && now - callSeenAt[callid] > inviteStaleMs) {
|
|
712
|
+
expireCall({call_id: callid, hangup_code: 'invite_timeout'})
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
let _livenessInterval = env.setInterval(checkCallLiveness, 5000)
|
|
717
|
+
|
|
718
|
+
publish = (ev) => {
|
|
177
719
|
if (ev.type == 'call_ended') {
|
|
178
720
|
let callid = ev.data && ev.data.call_info && ev.data.call_info.call_id
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
stopStream(streams[connId])
|
|
721
|
+
let callInfo = ev.data && ev.data.call_info
|
|
722
|
+
let serverCall = serverCalls[callid] || {}
|
|
723
|
+
let connId = getConnIdByCall(callid) || (callInfo && callInfo.device_id) || serverCall.device_id
|
|
724
|
+
markSessionEnded(callid)
|
|
725
|
+
cleanupCallConnection(callid, connId)
|
|
186
726
|
// do not clean up this now or some functions cannot work after the call ended
|
|
187
727
|
// DONOT: delete call2Connection[callid]
|
|
188
|
-
delete streams[connId]
|
|
189
728
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
arr.map((cb) => cb())
|
|
194
|
-
delete endcallCb[callid]
|
|
195
|
-
})
|
|
729
|
+
for (let op of operations) {
|
|
730
|
+
if (op.callid === callid) op.cancel('call_ended')
|
|
731
|
+
}
|
|
196
732
|
}
|
|
197
733
|
|
|
198
734
|
let direction = ev.data && ev.data.call_info && ev.data.call_info.direction
|
|
@@ -202,7 +738,17 @@ function WebRTCConn(options) {
|
|
|
202
738
|
}
|
|
203
739
|
|
|
204
740
|
this.shutdown = () => {
|
|
741
|
+
if (dead) return
|
|
205
742
|
dead = true
|
|
743
|
+
let callids = new Set(Object.keys(sessionsByCall))
|
|
744
|
+
env.clearInterval(_livenessInterval)
|
|
745
|
+
env.clearInterval(_endcalInterval)
|
|
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'})
|
|
206
752
|
}
|
|
207
753
|
|
|
208
754
|
this.matchCall = (callid) => {
|
|
@@ -227,16 +773,19 @@ function WebRTCConn(options) {
|
|
|
227
773
|
})
|
|
228
774
|
})
|
|
229
775
|
|
|
776
|
+
// copy-on-write: calls là shallow copy nên value vẫn trỏ vào object trong
|
|
777
|
+
// serverCalls/dialingRequest — mutate trực tiếp là sửa lén state của store
|
|
230
778
|
Object.keys(answerRequest).map((callid) => {
|
|
231
779
|
let call = calls[callid]
|
|
232
780
|
if (!call) return
|
|
233
|
-
call
|
|
781
|
+
calls[callid] = Object.assign({}, call, {answer_requested: answerRequest[callid]})
|
|
234
782
|
})
|
|
235
783
|
|
|
236
|
-
Object.
|
|
784
|
+
Object.keys(calls).map((callid) => {
|
|
785
|
+
let call = calls[callid]
|
|
237
786
|
let direction = call && call.direction
|
|
238
|
-
if (direction == 'incoming') call
|
|
239
|
-
if (direction == 'outgoing') call
|
|
787
|
+
if (direction == 'incoming') calls[callid] = Object.assign({}, call, {direction: 'inbound'})
|
|
788
|
+
if (direction == 'outgoing') calls[callid] = Object.assign({}, call, {direction: 'outbound'})
|
|
240
789
|
})
|
|
241
790
|
return calls
|
|
242
791
|
}
|
|
@@ -253,74 +802,175 @@ function WebRTCConn(options) {
|
|
|
253
802
|
}
|
|
254
803
|
if (answerRequest[callid]) call = Object.assign({}, call, {answer_requested: answerRequest[callid]})
|
|
255
804
|
let direction = call && call.direction
|
|
256
|
-
|
|
257
|
-
if (direction == '
|
|
805
|
+
// copy-on-write: call có thể vẫn là object gốc trong serverCalls/dialingRequest
|
|
806
|
+
if (direction == 'incoming') call = Object.assign({}, call, {direction: 'inbound'})
|
|
807
|
+
if (direction == 'outgoing') call = Object.assign({}, call, {direction: 'outbound'})
|
|
258
808
|
|
|
259
809
|
return call
|
|
260
810
|
}
|
|
261
811
|
|
|
812
|
+
// debug only, dump internal state so we can see why a call disappears from the store
|
|
813
|
+
this.debugState = () => {
|
|
814
|
+
let calls = {}
|
|
815
|
+
Object.keys(serverCalls).map((callid) => {
|
|
816
|
+
let call = serverCalls[callid] || {}
|
|
817
|
+
calls[callid] = {
|
|
818
|
+
status: call.status,
|
|
819
|
+
direction: call.direction,
|
|
820
|
+
device_id: call.device_id,
|
|
821
|
+
started: call.started,
|
|
822
|
+
answered: call.answered,
|
|
823
|
+
ended: call.ended,
|
|
824
|
+
hangup_code: call.hangup_code,
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
let sessions = {}
|
|
829
|
+
Object.keys(sessionsByCall).map((callid) => {
|
|
830
|
+
let ss = sessionsByCall[callid] || {}
|
|
831
|
+
let peer = ss.peer
|
|
832
|
+
sessions[callid] = {
|
|
833
|
+
conn_id: ss.connId,
|
|
834
|
+
status: ss.status,
|
|
835
|
+
cancelled: !!ss.cancelled,
|
|
836
|
+
peer: !!peer,
|
|
837
|
+
connection: peer && peer.connectionState,
|
|
838
|
+
ice: peer && peer.iceConnectionState,
|
|
839
|
+
signaling: peer && peer.signalingState,
|
|
840
|
+
ever_connected: !!ss.everConnected,
|
|
841
|
+
disconnected_at: ss.disconnectedAt,
|
|
842
|
+
last_transport_healthy_at: ss.lastTransportHealthyAt,
|
|
843
|
+
last_rtp_at: ss.lastRtpAt,
|
|
844
|
+
rtp_packets: ss.rtpPackets,
|
|
845
|
+
local_stream: !!ss.localStream,
|
|
846
|
+
remote_stream: !!ss.remoteStream,
|
|
847
|
+
muted: !!ss.muted,
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
api_url: apiUrl,
|
|
853
|
+
server_calls: calls,
|
|
854
|
+
dialing_requests: Object.keys(dialingRequest),
|
|
855
|
+
end_requests: Object.keys(endRequest),
|
|
856
|
+
answer_requests: Object.keys(answerRequest),
|
|
857
|
+
call2conn: Object.assign({}, call2Connection),
|
|
858
|
+
sessions,
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
this.expireCall = expireCall
|
|
863
|
+
|
|
262
864
|
realtime.onEvent((ev) => {
|
|
263
865
|
if (dead) return
|
|
264
866
|
if (!ev || !ev.type || !ev.data) return
|
|
265
|
-
if (env._is_webrtc_local && ev.user_id != '8') return // local
|
|
266
|
-
if (!env._is_webrtc_local && ev.user_id != '9') return // prod
|
|
267
867
|
|
|
268
868
|
if (ev.type == 'webrtc_candidate_added') {
|
|
269
869
|
let ice_candidate = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.ice_candidate
|
|
270
870
|
let connId = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.connection_id
|
|
271
871
|
if (!connId) return
|
|
272
872
|
if (!ice_candidate) return
|
|
273
|
-
let
|
|
873
|
+
let waitRemoteDescription = waitForRemoteDescription[connId]
|
|
874
|
+
let session = getSessionByConn(connId)
|
|
875
|
+
let peer = session && session.peer
|
|
274
876
|
|
|
275
|
-
|
|
877
|
+
if (!peer || !waitRemoteDescription || isCallEndedLocally(session && session.callid)) {
|
|
878
|
+
console.info('[webrtc]', 'IGNORE CANDIDATE FOR CLOSED CONNECTION', connId)
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return waitRemoteDescription
|
|
883
|
+
.then(() => {
|
|
884
|
+
let session = getSessionByConn(connId)
|
|
885
|
+
let peer = session && session.peer
|
|
886
|
+
if (!peer || isCallEndedLocally(session && session.callid)) return
|
|
887
|
+
let candidate = parseJSON(ice_candidate)
|
|
888
|
+
if (!candidate) return
|
|
889
|
+
return peer.addIceCandidate(candidate)
|
|
890
|
+
})
|
|
891
|
+
.catch((err) => {
|
|
892
|
+
// không catch thì thành unhandled rejection; candidate hỏng chỉ cần log
|
|
893
|
+
console.error('[webrtc]', 'ADD CANDIDATE FAILED', connId, err)
|
|
894
|
+
})
|
|
276
895
|
}
|
|
277
896
|
|
|
278
897
|
if (ev.type == 'webrtc_negotiation_offered') {
|
|
279
|
-
// console.log('RT/RECEIVE OFFERED NEGOTIATION', ev)
|
|
280
898
|
let jsonoffer = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.offer
|
|
281
899
|
let connId = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.connection_id
|
|
282
|
-
let
|
|
283
|
-
|
|
900
|
+
let session = getSessionByConn(connId)
|
|
901
|
+
let peer = session && session.peer
|
|
902
|
+
if (!peer || isCallEndedLocally(session && session.callid)) return
|
|
284
903
|
let negid = ev.id
|
|
285
904
|
// let negid = ev && ev.data && ev.data.webrtc_message && ev.data.webrtc_message.negotiation_id
|
|
286
905
|
let offer = parseJSON(jsonoffer)
|
|
287
906
|
if (!offer) return
|
|
907
|
+
// Xử lý TUẦN TỰ các offer trên cùng connection: 2 offer chồng nhau mà cùng
|
|
908
|
+
// setRemoteDescription là dính InvalidStateError (glare). Re-fetch session/
|
|
909
|
+
// peer bên trong vì đến lượt chạy thì connection có thể đã bị thay/đóng.
|
|
910
|
+
let handleOffer = () => {
|
|
911
|
+
let session = getSessionByConn(connId)
|
|
912
|
+
let peer = session && session.peer
|
|
913
|
+
if (!peer || isCallEndedLocally(session.callid)) return
|
|
914
|
+
return peer
|
|
915
|
+
.setRemoteDescription(offer)
|
|
916
|
+
.then(() => {
|
|
917
|
+
let resolveReadyToSendCandidate = readyToSendCandidateResolve[connId]
|
|
918
|
+
session = getSessionByConn(connId)
|
|
919
|
+
if (!session || isCallEndedLocally(session.callid) || !resolveReadyToSendCandidate) return
|
|
920
|
+
resolveReadyToSendCandidate()
|
|
921
|
+
peer = session.peer
|
|
922
|
+
let stream = session.localStream
|
|
923
|
+
if (!peer) return
|
|
924
|
+
stream &&
|
|
925
|
+
stream.getTracks().forEach((track) => {
|
|
926
|
+
let replaced = false
|
|
927
|
+
peer.getSenders().forEach((sender) => {
|
|
928
|
+
if (!sender.track) return
|
|
929
|
+
if (sender.track.kind != track.kind) return
|
|
930
|
+
sender.replaceTrack(track)
|
|
931
|
+
replaced = true
|
|
932
|
+
})
|
|
288
933
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
934
|
+
if (!replaced) {
|
|
935
|
+
peer.addTrack(track, stream)
|
|
936
|
+
}
|
|
937
|
+
})
|
|
938
|
+
return peer.createAnswer()
|
|
939
|
+
})
|
|
940
|
+
.then((answer) => {
|
|
941
|
+
if (!answer) return
|
|
942
|
+
session = getSessionByConn(connId)
|
|
943
|
+
peer = session && session.peer
|
|
944
|
+
if (!peer || isCallEndedLocally(session && session.callid)) return
|
|
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
|
|
953
|
+
// call_id: server dùng để log theo cuộc gọi
|
|
954
|
+
let path = `finish?x-access-token=${access_token}&connection_id=${connId}&call_id=${session.callid}&version=2&negotiation_id=${negid}&account_id=${accid}`
|
|
955
|
+
postPerCall(session.callid, path, jsonify(answer), function (body, code) {
|
|
956
|
+
if (code !== 200) {
|
|
957
|
+
return console.error('CANNOT FINISH', body)
|
|
958
|
+
}
|
|
959
|
+
return
|
|
302
960
|
})
|
|
303
|
-
if (!replaced) peer.addTrack(track, stream)
|
|
304
|
-
})
|
|
305
|
-
return peer.createAnswer()
|
|
306
|
-
})
|
|
307
|
-
.then((answer) =>
|
|
308
|
-
peer.setLocalDescription(answer).then(() => {
|
|
309
|
-
let url = `${apiUrl}finish?x-access-token=${access_token}&connection_id=${connId}&version=2&negotiation_id=${negid}&account_id=${accid}`
|
|
310
|
-
callAPI('post', url, jsonify(answer), function (body, code) {
|
|
311
|
-
if (code !== 200) {
|
|
312
|
-
logError(accid, agid, body, 'finish:295')
|
|
313
|
-
return console.error('CANNOT FINISH', body)
|
|
314
|
-
}
|
|
315
|
-
return
|
|
316
961
|
})
|
|
317
|
-
})
|
|
318
|
-
|
|
962
|
+
})
|
|
963
|
+
}
|
|
964
|
+
negotiationQueue[connId] = (negotiationQueue[connId] || env.Promise.resolve()).then(handleOffer).catch((err) => {
|
|
965
|
+
console.error('[webrtc]', 'NEGOTIATION FAILED', connId, err)
|
|
966
|
+
})
|
|
967
|
+
return
|
|
319
968
|
}
|
|
320
969
|
|
|
321
970
|
let call_info = ev.data.call_info || {}
|
|
322
971
|
let callid = call_info.call_id
|
|
323
972
|
if (!callid) return
|
|
973
|
+
rememberCall(callid)
|
|
324
974
|
let type = ev.type
|
|
325
975
|
if (type === 'call_invited') {
|
|
326
976
|
if (agid == call_info.from_number) return // ignore our own invite
|
|
@@ -329,181 +979,214 @@ function WebRTCConn(options) {
|
|
|
329
979
|
}
|
|
330
980
|
|
|
331
981
|
if (type == 'call_ended') {
|
|
982
|
+
let wasEnded = isCallEndedLocally(callid)
|
|
332
983
|
serverCalls[callid] = call_info
|
|
984
|
+
if (wasEnded) return // already ended and published locally, ignore duplicate server event
|
|
333
985
|
return publish(ev)
|
|
334
986
|
}
|
|
335
987
|
|
|
336
988
|
if (ev.type === 'call_invite_expired') {
|
|
337
|
-
if (
|
|
989
|
+
if (getSessionByConn(call_info.device_id)) return // this call invitation is expired by us => ignore
|
|
338
990
|
serverCalls[callid] = call_info
|
|
339
991
|
publish(Object.assign({}, ev, {type: 'call_ended'})) // fake ended call
|
|
340
992
|
return
|
|
341
993
|
}
|
|
342
994
|
|
|
343
995
|
let connId = call_info.device_id
|
|
344
|
-
|
|
996
|
+
let session = getSessionByConn(connId)
|
|
997
|
+
let peer = session && session.peer
|
|
998
|
+
if (!peer) return
|
|
345
999
|
if (type == 'call_ringing' || type == 'call_joined') {
|
|
1000
|
+
session = bindSessionConnection(callid, connId, peer)
|
|
1001
|
+
if (!session) return
|
|
1002
|
+
session.status = call_info.status
|
|
346
1003
|
serverCalls[callid] = call_info
|
|
347
1004
|
return publish(ev)
|
|
348
1005
|
}
|
|
349
1006
|
})
|
|
350
1007
|
|
|
351
|
-
this.makeCall = (number, fromnumber, streamPm, callid, campaignid, outboundcallentryid) => {
|
|
1008
|
+
this.makeCall = async (number, fromnumber, streamPm, callid, campaignid, outboundcallentryid, channel) => {
|
|
352
1009
|
if (!callid) callid = 'webcall-' + randomString(30)
|
|
1010
|
+
if (dead || isCallEndedLocally(callid)) {
|
|
1011
|
+
if (streamPm && streamPm.then) env.Promise.resolve(streamPm).then(stopStream, reportCallbackError)
|
|
1012
|
+
return dead ? {error: 'shutdown'} : getLocalEndedCall(callid)
|
|
1013
|
+
}
|
|
1014
|
+
if (!streamPm || !streamPm.then) return {error: 'no_stream'}
|
|
1015
|
+
let op = startOperation(callid)
|
|
353
1016
|
let now = env.Date.now()
|
|
1017
|
+
rememberCall(callid)
|
|
354
1018
|
let call = {
|
|
355
1019
|
account_id: accid,
|
|
356
1020
|
call_id: callid,
|
|
357
1021
|
direction: 'outbound',
|
|
1022
|
+
channel: channel || 'call',
|
|
358
1023
|
member_id: agid,
|
|
359
1024
|
started: now,
|
|
360
1025
|
status: 'dialing',
|
|
361
1026
|
to_number: number,
|
|
362
1027
|
from_number: fromnumber,
|
|
1028
|
+
mic: 'waiting',
|
|
363
1029
|
}
|
|
364
1030
|
dialingRequest[callid] = call
|
|
365
|
-
publish({created: now, type: 'call_ringing', data: {call_info: {call_id: callid}}})
|
|
366
|
-
|
|
367
|
-
streamPm
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
}
|
|
376
1065
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let call = this.matchCall(callid)
|
|
393
|
-
if (call && call.status == 'ended') return rs(call)
|
|
394
|
-
if (!endcallCb[callid]) endcallCb[callid] = []
|
|
395
|
-
endcallCb[callid].push(() => rs(this.matchCall(callid)))
|
|
396
|
-
})
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
.catch((err) => rs({error: err}))
|
|
400
|
-
})
|
|
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
|
+
}
|
|
401
1081
|
}
|
|
402
1082
|
|
|
403
1083
|
this.hangupCall = (callid) => {
|
|
404
|
-
if (!callid) return
|
|
1084
|
+
if (dead || !callid || isCallEndedLocally(callid)) return
|
|
405
1085
|
endRequest[callid] = env.Date.now()
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (!call) {
|
|
409
|
-
call = {call_id: callid}
|
|
410
|
-
serverCalls[callid] = call
|
|
411
|
-
}
|
|
1086
|
+
let call = serverCalls[callid] || {call_id: callid}
|
|
1087
|
+
serverCalls[callid] = call
|
|
412
1088
|
call.status = 'ended'
|
|
413
1089
|
call.hangup_code = 'cancel'
|
|
414
1090
|
call.ended = env.Date.now()
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.listenCall = (callid, cb) => {
|
|
420
|
-
let start = env.Date.now()
|
|
421
|
-
initSession((err, connId) => {
|
|
422
|
-
let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&account_id=${accid}`
|
|
423
|
-
callAPI('post', url, undefined, (body, code) => {
|
|
424
|
-
if (code != 200) logError(accid, agid, body, 'listenCall:399')
|
|
425
|
-
collect('listen_call', callid, env.Date.now() - start)
|
|
426
|
-
return code != 200 ? cb(body) : cb()
|
|
427
|
-
})
|
|
428
|
-
})
|
|
1091
|
+
let hangupBase = apiBaseFor(callid)
|
|
1092
|
+
publish({created: env.Date.now(), type: 'call_ended', data: {call_info: Object.assign({}, call)}})
|
|
1093
|
+
signalHangup(callid, hangupBase)
|
|
429
1094
|
}
|
|
430
1095
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
+
}
|
|
444
1115
|
}
|
|
445
1116
|
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
})
|
|
1117
|
+
this.listenCall = (callid, cb) => join(callid, undefined, cb, false)
|
|
1118
|
+
this.joinCall = (callid, stream, cb) => join(callid, stream, cb, true)
|
|
1119
|
+
|
|
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 reason === 'call_ended' ? {body: getLocalEndedCall(callid)} : {error: reason}
|
|
1142
|
+
} finally {
|
|
1143
|
+
op.dispose()
|
|
1144
|
+
}
|
|
475
1145
|
}
|
|
476
1146
|
|
|
477
1147
|
this.sendDtmf = (key, callid) =>
|
|
478
1148
|
new env.Promise((rs) => {
|
|
479
1149
|
if (!callid) return rs({body: {}})
|
|
480
|
-
let connId =
|
|
1150
|
+
let connId = getConnIdByCall(callid)
|
|
481
1151
|
if (!connId) return rs({body: {}})
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
)
|
|
1152
|
+
let path = `dtmf?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&key=${encodeURIComponent(
|
|
1153
|
+
key,
|
|
1154
|
+
)}&account_id=${accid}`
|
|
1155
|
+
postPerCall(callid, path, undefined, (body, code) => {
|
|
1156
|
+
body = parseJSON(body)
|
|
1157
|
+
return code != 200 ? rs({error: body}) : rs({body})
|
|
1158
|
+
})
|
|
490
1159
|
})
|
|
491
1160
|
|
|
492
1161
|
this.transferCall = (tonumber, callid) =>
|
|
493
1162
|
new env.Promise((rs) => {
|
|
494
1163
|
if (!callid) return rs({})
|
|
495
|
-
let connId =
|
|
1164
|
+
let connId = getConnIdByCall(callid)
|
|
496
1165
|
if (!connId) return rs({body: {}})
|
|
497
|
-
let
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
})
|
|
505
|
-
)
|
|
1166
|
+
let start = env.Date.now()
|
|
1167
|
+
let path = `refer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${encodeURIComponent(
|
|
1168
|
+
tonumber,
|
|
1169
|
+
)}&account_id=${accid}`
|
|
1170
|
+
postPerCall(callid, path, undefined, (body, code) => {
|
|
1171
|
+
collect('transfer_call', callid, env.Date.now() - start)
|
|
1172
|
+
body = parseJSON(body)
|
|
1173
|
+
return code != 200 ? rs({error: body}) : rs({body})
|
|
1174
|
+
})
|
|
506
1175
|
})
|
|
1176
|
+
|
|
1177
|
+
this.setCallMuted = (callid, muted) => {
|
|
1178
|
+
if (dead) return {error: 'shutdown'}
|
|
1179
|
+
let session = ensureSession(callid)
|
|
1180
|
+
if (!session) return {error: 'call_not_found'}
|
|
1181
|
+
session.muted = !!muted
|
|
1182
|
+
applySessionMuted(session)
|
|
1183
|
+
return {body: {muted: session.muted}}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
this.isCallMuted = (callid) => {
|
|
1187
|
+
let session = getSessionByCall(callid)
|
|
1188
|
+
return !!(session && session.muted)
|
|
1189
|
+
}
|
|
507
1190
|
}
|
|
508
1191
|
|
|
509
1192
|
module.exports = WebRTCConn
|