@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/realtime.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
var flow = require('@subiz/flow')
|
|
2
1
|
var {xhrsend, parseJSON, Pubsub} = require('./common.js')
|
|
3
2
|
|
|
4
3
|
// Conn represents a realtime connection between an user and the realtime
|
|
@@ -18,145 +17,215 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
18
17
|
callAPI = callAPI || xhrsend // allow hook
|
|
19
18
|
credential = credential || {}
|
|
20
19
|
var lastToken = ''
|
|
20
|
+
var lastAccessToken = ''
|
|
21
21
|
if (!accid) accid = ''
|
|
22
22
|
var is_dead = false // tell if the connection is dead
|
|
23
23
|
var is_polling_stuck = false // tell if we've having trouble doing /poll
|
|
24
24
|
var is_sub_stuck = true // tell if we've having trouble doing /subscribe
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
// Server holds polls for at most 90s. Allow transport overhead, but recover
|
|
27
|
+
// before a request can hang forever. Custom transports may return an abort handle.
|
|
28
|
+
var POLL_TIMEOUT = 105000
|
|
29
|
+
var timers = new Set()
|
|
30
|
+
var requests = new Set()
|
|
31
|
+
var subscriptions = new Set()
|
|
32
|
+
function later(fn, delay) {
|
|
33
|
+
var timer = setTimeout(function () {
|
|
34
|
+
timers.delete(timer)
|
|
35
|
+
if (!is_dead) fn()
|
|
36
|
+
}, delay)
|
|
37
|
+
timers.add(timer)
|
|
38
|
+
return timer
|
|
39
|
+
}
|
|
40
|
+
function cancelTimer(timer) {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
timers.delete(timer)
|
|
43
|
+
}
|
|
44
|
+
function request(method, url, body, timeout, cb) {
|
|
45
|
+
var settled = false
|
|
46
|
+
var transport
|
|
47
|
+
function abort() {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof transport === 'function') transport()
|
|
50
|
+
else if (transport && typeof transport.abort === 'function') transport.abort()
|
|
51
|
+
} catch (err) {
|
|
52
|
+
reportCallbackError(err)
|
|
44
53
|
}
|
|
54
|
+
}
|
|
55
|
+
function cancel() {
|
|
56
|
+
if (settled) return
|
|
57
|
+
settled = true
|
|
58
|
+
cancelTimer(timer)
|
|
59
|
+
requests.delete(cancel)
|
|
60
|
+
abort()
|
|
61
|
+
}
|
|
62
|
+
function complete(body, code) {
|
|
63
|
+
if (settled || is_dead) return
|
|
64
|
+
settled = true
|
|
65
|
+
cancelTimer(timer)
|
|
66
|
+
requests.delete(cancel)
|
|
67
|
+
cb(body, code)
|
|
68
|
+
}
|
|
69
|
+
requests.add(cancel)
|
|
70
|
+
var timer = later(function () {
|
|
71
|
+
cancel()
|
|
72
|
+
if (!is_dead) cb(undefined, -1)
|
|
73
|
+
}, timeout)
|
|
74
|
+
try {
|
|
75
|
+
transport = callAPI(method, url, body, complete)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
complete(undefined, -1)
|
|
78
|
+
}
|
|
79
|
+
// A synchronous custom transport can stop the connection before returning.
|
|
80
|
+
if (is_dead) abort()
|
|
81
|
+
}
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
var MIN_POLL_INTERVAL = 250
|
|
84
|
+
var polling_started = false
|
|
85
|
+
function startPolling() {
|
|
86
|
+
if (polling_started) return
|
|
87
|
+
polling_started = true
|
|
88
|
+
polling(0)
|
|
89
|
+
}
|
|
90
|
+
function polling(backoff) {
|
|
91
|
+
if (is_dead) return
|
|
92
|
+
var started = Date.now()
|
|
93
|
+
request(
|
|
94
|
+
'get',
|
|
95
|
+
apiUrl + 'poll?token=' + encodeURIComponent(lastToken) + '&v=5&account-id=' + encodeURIComponent(accid),
|
|
96
|
+
undefined,
|
|
97
|
+
POLL_TIMEOUT,
|
|
98
|
+
function (body, code) {
|
|
99
|
+
var wait = Math.max(0, MIN_POLL_INTERVAL - (Date.now() - started))
|
|
100
|
+
if (retryable(code)) {
|
|
101
|
+
is_polling_stuck = true
|
|
102
|
+
return later(
|
|
103
|
+
function () {
|
|
104
|
+
polling(backoff + 1)
|
|
105
|
+
},
|
|
106
|
+
Math.max(calcNextBackoff(backoff), wait),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (code !== 200) {
|
|
110
|
+
is_polling_stuck = true
|
|
111
|
+
return kill('poll')
|
|
112
|
+
}
|
|
113
|
+
body = parseJSON(body) || {}
|
|
114
|
+
if (!body.sequential_token) return kill('poll')
|
|
115
|
+
is_polling_stuck = false
|
|
116
|
+
if (body.host) apiUrl = absUrl(body.host)
|
|
117
|
+
lastToken = body.sequential_token
|
|
118
|
+
onEvents(body.events || [])
|
|
119
|
+
if (!is_dead)
|
|
120
|
+
later(function () {
|
|
121
|
+
polling(0)
|
|
122
|
+
}, wait)
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
}
|
|
50
126
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
127
|
+
var pendingTopics = []
|
|
128
|
+
var batchTimer
|
|
129
|
+
var batchRunning = false
|
|
130
|
+
function scheduleBatch() {
|
|
131
|
+
if (is_dead || batchRunning || !pendingTopics.length) return
|
|
132
|
+
cancelTimer(batchTimer)
|
|
133
|
+
// Flush full queues early; smaller batches wait from their oldest topic.
|
|
134
|
+
var delay = pendingTopics.length > 50 ? 0 : Math.max(0, 100 - (Date.now() - pendingTopics[0].created))
|
|
135
|
+
batchTimer = later(flushBatch, delay)
|
|
136
|
+
}
|
|
137
|
+
function flushBatch() {
|
|
138
|
+
batchTimer = undefined
|
|
139
|
+
if (is_dead || batchRunning || !pendingTopics.length) return
|
|
140
|
+
batchRunning = true
|
|
141
|
+
var batch = pendingTopics.splice(0, 50)
|
|
142
|
+
function finish(results) {
|
|
143
|
+
batch.forEach(function (item, i) { item.resolve(results[i]) })
|
|
144
|
+
batchRunning = false
|
|
145
|
+
scheduleBatch()
|
|
146
|
+
}
|
|
147
|
+
subscribeBatch(batch.map(function (item) { return item.topic })).then(finish, function (err) {
|
|
148
|
+
reportCallbackError(err)
|
|
149
|
+
kill('subs')
|
|
150
|
+
finish(repeat('dead', batch.length))
|
|
57
151
|
})
|
|
58
152
|
}
|
|
59
|
-
|
|
60
|
-
var subQueue = flow.batch(50, 100, function (topics) {
|
|
153
|
+
function subscribeBatch(topics) {
|
|
61
154
|
if (is_dead) return Promise.resolve(repeat('dead', topics.length))
|
|
62
|
-
if (topics.length
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
99
|
-
else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
100
|
-
else if (access_token) query += '&access-token=' + access_token
|
|
101
|
-
|
|
102
|
-
var fullurl =
|
|
103
|
-
apiUrl + 'subs' + query + '&v=5&account-id=' + encodeURIComponent(accid || credential.account_id)
|
|
104
|
-
callAPI('post', fullurl, JSON.stringify({events: topics}), function (body, code) {
|
|
105
|
-
if (done) return
|
|
106
|
-
if (is_dead) {
|
|
107
|
-
resolve(repeat('dead', topics.length))
|
|
108
|
-
return rs(false) // break loop
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (retryable(code)) {
|
|
112
|
-
is_sub_stuck = true
|
|
113
|
-
return setTimeout(rs, 3000, true)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// unretryable error
|
|
117
|
-
if (code !== 200) {
|
|
118
|
-
// we are unable to start the communication with the realtime server.
|
|
119
|
-
// Must notify the user by killing the connection
|
|
120
|
-
kill('subs')
|
|
121
|
-
resolve(repeat('dead', topics.length))
|
|
122
|
-
return rs(false)
|
|
123
|
-
}
|
|
124
|
-
is_sub_stuck = false
|
|
125
|
-
done = true
|
|
126
|
-
if (lastToken) {
|
|
127
|
-
resolve(topics)
|
|
128
|
-
return rs(false)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// first time sub, should grab the initial token
|
|
132
|
-
body = parseJSON(body) || {}
|
|
133
|
-
var initialToken = body.initial_token
|
|
134
|
-
// the server returns a malform payload. We should end
|
|
135
|
-
if (!initialToken) {
|
|
136
|
-
kill('subs')
|
|
137
|
-
resolve(repeat('dead', topics.length))
|
|
138
|
-
return rs(false) // break loop
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// learn new host
|
|
142
|
-
if (body.host) apiUrl = absUrl(body.host)
|
|
143
|
-
|
|
144
|
-
lastToken = initialToken
|
|
145
|
-
polling(0)
|
|
146
|
-
resolve(topics)
|
|
147
|
-
return rs(false) // to break the loop
|
|
155
|
+
if (!topics.length) return Promise.resolve([])
|
|
156
|
+
return new Promise(function (resolve) {
|
|
157
|
+
var done = false
|
|
158
|
+
function finish(result) {
|
|
159
|
+
if (done) return
|
|
160
|
+
done = true
|
|
161
|
+
cancelTimer(stuckTimer)
|
|
162
|
+
cancelTimer(deadline)
|
|
163
|
+
cancelTimer(retryTimer)
|
|
164
|
+
subscriptions.delete(cancel)
|
|
165
|
+
resolve(result)
|
|
166
|
+
}
|
|
167
|
+
function cancel() {
|
|
168
|
+
finish(repeat('dead', topics.length))
|
|
169
|
+
}
|
|
170
|
+
subscriptions.add(cancel)
|
|
171
|
+
var stuckTimer = later(function () {
|
|
172
|
+
is_sub_stuck = true
|
|
173
|
+
}, 4000)
|
|
174
|
+
var deadline = later(function () {
|
|
175
|
+
is_sub_stuck = true
|
|
176
|
+
kill('subs')
|
|
177
|
+
}, 60000)
|
|
178
|
+
var retryTimer
|
|
179
|
+
function retry() {
|
|
180
|
+
if (done || is_dead) return
|
|
181
|
+
is_sub_stuck = true
|
|
182
|
+
retryTimer = later(attempt, 3000)
|
|
183
|
+
}
|
|
184
|
+
function attempt() {
|
|
185
|
+
if (done || is_dead) return
|
|
186
|
+
Promise.resolve()
|
|
187
|
+
.then(function () {
|
|
188
|
+
return credential.getAccessToken()
|
|
148
189
|
})
|
|
149
|
-
|
|
150
|
-
|
|
190
|
+
.then(function (accessToken) {
|
|
191
|
+
if (done || is_dead) return
|
|
192
|
+
lastAccessToken = accessToken || ''
|
|
193
|
+
var query = '?token=' + encodeURIComponent(lastToken)
|
|
194
|
+
if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
195
|
+
else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
196
|
+
else if (accessToken) query += '&access-token=' + encodeURIComponent(accessToken)
|
|
197
|
+
var url = apiUrl + 'subs' + query + '&v=5&account-id=' + encodeURIComponent(accid || credential.account_id)
|
|
198
|
+
request('post', url, JSON.stringify({events: topics}), 60000, function (body, code) {
|
|
199
|
+
if (done || is_dead) return
|
|
200
|
+
if (retryable(code)) return retry()
|
|
201
|
+
if (code !== 200) return kill('subs')
|
|
202
|
+
if (!lastToken) {
|
|
203
|
+
body = parseJSON(body) || {}
|
|
204
|
+
if (!body.initial_token) return kill('subs')
|
|
205
|
+
if (body.host) apiUrl = absUrl(body.host)
|
|
206
|
+
lastToken = body.initial_token
|
|
207
|
+
}
|
|
208
|
+
is_sub_stuck = false
|
|
209
|
+
finish(topics)
|
|
210
|
+
startPolling()
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
.catch(retry)
|
|
214
|
+
}
|
|
215
|
+
attempt()
|
|
151
216
|
})
|
|
152
|
-
|
|
153
|
-
return promise
|
|
154
|
-
})
|
|
217
|
+
}
|
|
155
218
|
|
|
156
219
|
// subscribe call /subs API to tell server that we are listening for those events
|
|
157
220
|
// this function may start polling loop if it hasn't been started yet
|
|
158
221
|
// its safe to call this function multiple times
|
|
159
|
-
this.subscribe =
|
|
222
|
+
this.subscribe = function (topic) {
|
|
223
|
+
if (is_dead) return Promise.resolve('dead')
|
|
224
|
+
return new Promise(function (resolve) {
|
|
225
|
+
pendingTopics.push({topic, resolve, created: Date.now()})
|
|
226
|
+
scheduleBatch()
|
|
227
|
+
})
|
|
228
|
+
}
|
|
160
229
|
|
|
161
230
|
this.getStatus = function () {
|
|
162
231
|
if (is_dead) return 'dead'
|
|
@@ -164,9 +233,38 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
164
233
|
return 'active'
|
|
165
234
|
}
|
|
166
235
|
|
|
236
|
+
// Page exit cannot wait for credential refresh or a normal XHR response.
|
|
237
|
+
this.cleanup = function (browserWindow) {
|
|
238
|
+
if (is_dead || !lastToken) return
|
|
239
|
+
var url = apiUrl + 'cleanup?token=' + encodeURIComponent(lastToken) + '&account-id=' + encodeURIComponent(accid)
|
|
240
|
+
// Also support hosts where /rt goes through the standard API auth wrapper.
|
|
241
|
+
if (credential.user_ref) url += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
242
|
+
else if (credential.user_mask) url += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
243
|
+
else if (lastAccessToken) url += '&access-token=' + encodeURIComponent(lastAccessToken)
|
|
244
|
+
try {
|
|
245
|
+
if (browserWindow.navigator && typeof browserWindow.navigator.sendBeacon === 'function') {
|
|
246
|
+
if (browserWindow.navigator.sendBeacon(url)) return
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {}
|
|
249
|
+
try {
|
|
250
|
+
if (typeof browserWindow.fetch === 'function') {
|
|
251
|
+
Promise.resolve(browserWindow.fetch(url, {method: 'POST', keepalive: true, credentials: 'include'})).catch(
|
|
252
|
+
function () {},
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
}
|
|
257
|
+
|
|
167
258
|
// kill force the connection to dead state
|
|
168
259
|
var kill = function (reason) {
|
|
260
|
+
if (is_dead) return
|
|
169
261
|
is_dead = true
|
|
262
|
+
for (var timer of timers) clearTimeout(timer)
|
|
263
|
+
timers.clear()
|
|
264
|
+
for (var cancel of requests) cancel()
|
|
265
|
+
for (var finish of subscriptions) finish()
|
|
266
|
+
for (var item of pendingTopics) item.resolve('dead')
|
|
267
|
+
pendingTopics = []
|
|
170
268
|
onDead(reason)
|
|
171
269
|
}
|
|
172
270
|
this.kill = kill
|
|
@@ -184,16 +282,47 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
184
282
|
}
|
|
185
283
|
}
|
|
186
284
|
if (!accid) accid = credential.account_id || ''
|
|
187
|
-
var pubsub = new Pubsub()
|
|
285
|
+
var pubsub = new Pubsub(reportCallbackError)
|
|
286
|
+
var reconnectTimer
|
|
287
|
+
var stopped = false
|
|
288
|
+
var leavingPage = false
|
|
289
|
+
var browserWindow =
|
|
290
|
+
typeof window !== 'undefined' &&
|
|
291
|
+
window.document &&
|
|
292
|
+
typeof window.addEventListener === 'function' &&
|
|
293
|
+
typeof window.removeEventListener === 'function'
|
|
294
|
+
? window
|
|
295
|
+
: undefined
|
|
296
|
+
|
|
297
|
+
var onPageHide = function (event) {
|
|
298
|
+
// A page saved in the back/forward cache may resume this connection.
|
|
299
|
+
if (event.persisted || leavingPage) return
|
|
300
|
+
leavingPage = true
|
|
301
|
+
clearTimeout(reconnectTimer)
|
|
302
|
+
reconnectTimer = undefined
|
|
303
|
+
browserWindow.removeEventListener('pagehide', onPageHide)
|
|
304
|
+
if (!conn) return
|
|
305
|
+
var oldconn = conn
|
|
306
|
+
conn = undefined
|
|
307
|
+
oldconn.cleanup(browserWindow)
|
|
308
|
+
oldconn.kill('pagehide')
|
|
309
|
+
}
|
|
188
310
|
|
|
189
311
|
// holds all topics that realtime must subscribed
|
|
190
312
|
var topics = {}
|
|
191
313
|
|
|
192
314
|
// stop the connection
|
|
193
315
|
this.stop = function () {
|
|
316
|
+
stopped = true
|
|
317
|
+
clearTimeout(reconnectTimer)
|
|
318
|
+
reconnectTimer = undefined
|
|
319
|
+
if (browserWindow) browserWindow.removeEventListener('pagehide', onPageHide)
|
|
194
320
|
if (!conn) return
|
|
195
|
-
|
|
321
|
+
// detach before killing so onDead knows this is intentional and
|
|
322
|
+
// doesn't schedule a reconnect
|
|
323
|
+
var oldconn = conn
|
|
196
324
|
conn = undefined
|
|
325
|
+
oldconn.kill()
|
|
197
326
|
pubsub.emit('interrupted')
|
|
198
327
|
}
|
|
199
328
|
|
|
@@ -236,8 +365,20 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
236
365
|
// if the Conn is dead, it recreate a new one
|
|
237
366
|
var conn
|
|
238
367
|
var reconnect = function () {
|
|
368
|
+
if (leavingPage) return Promise.resolve({error: 'dead'})
|
|
369
|
+
stopped = false
|
|
370
|
+
clearTimeout(reconnectTimer)
|
|
371
|
+
reconnectTimer = undefined
|
|
372
|
+
if (browserWindow) browserWindow.addEventListener('pagehide', onPageHide)
|
|
239
373
|
var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
|
|
240
|
-
if (conn)
|
|
374
|
+
if (conn) {
|
|
375
|
+
// detach before killing so the old conn's onDead doesn't
|
|
376
|
+
// schedule another reconnect (which would kill this new conn,
|
|
377
|
+
// schedule another one, and churn forever)
|
|
378
|
+
var oldconn = conn
|
|
379
|
+
conn = undefined
|
|
380
|
+
oldconn.kill()
|
|
381
|
+
}
|
|
241
382
|
var myconn = new Conn(
|
|
242
383
|
randomUrl,
|
|
243
384
|
credential,
|
|
@@ -245,7 +386,7 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
245
386
|
if (conn === undefined || myconn != conn) return // outdated
|
|
246
387
|
conn = undefined // prevent dupplicate dead fire
|
|
247
388
|
pubsub.emit('interrupted', code, body, status)
|
|
248
|
-
if (!skipautoreconnect) setTimeout(reconnect, 2000)
|
|
389
|
+
if (!skipautoreconnect && !stopped && !leavingPage) reconnectTimer = setTimeout(reconnect, 2000)
|
|
249
390
|
},
|
|
250
391
|
function (events) {
|
|
251
392
|
if (myconn != conn) return
|
|
@@ -298,4 +439,10 @@ function absUrl(url) {
|
|
|
298
439
|
return url
|
|
299
440
|
}
|
|
300
441
|
|
|
442
|
+
function reportCallbackError(err) {
|
|
443
|
+
try {
|
|
444
|
+
if (typeof console !== 'undefined' && console.error) console.error('Realtime callback failed:', err)
|
|
445
|
+
} catch (ignored) {}
|
|
446
|
+
}
|
|
447
|
+
|
|
301
448
|
module.exports = Realtime
|
package/webphone.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
var {xhrsend, parseJSON, Pubsub, randomString} = require('./common.js')
|
|
1
|
+
var {xhrsend, parseJSON, Pubsub, randomString, reportCallbackError} = require('./common.js')
|
|
2
2
|
const WebRTCConn = require('./webrtc.js')
|
|
3
3
|
var Realtime = require('./realtime.js')
|
|
4
4
|
|
|
5
5
|
function WebPhone(access_token, realtime) {
|
|
6
|
-
var pubsub = new Pubsub()
|
|
6
|
+
var pubsub = new Pubsub(reportCallbackError)
|
|
7
7
|
|
|
8
8
|
var $audio = null
|
|
9
9
|
// chuẩn bị thẻ audio để nghe được
|
|
@@ -28,13 +28,18 @@ function WebPhone(access_token, realtime) {
|
|
|
28
28
|
if (!agid.startsWith('ag')) throw 'invalid_access_token'
|
|
29
29
|
|
|
30
30
|
if (!realtime)
|
|
31
|
-
realtime = new Realtime(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
realtime = new Realtime(
|
|
32
|
+
'https://api.subiz.com.vn/rt/0/',
|
|
33
|
+
{
|
|
34
|
+
getAccessToken: function () {
|
|
35
|
+
return new Promise(function (rs) {
|
|
36
|
+
rs(access_token)
|
|
37
|
+
})
|
|
38
|
+
},
|
|
36
39
|
},
|
|
37
|
-
|
|
40
|
+
undefined,
|
|
41
|
+
accid,
|
|
42
|
+
)
|
|
38
43
|
|
|
39
44
|
var webrtcconn = new WebRTCConn({
|
|
40
45
|
accid: accid,
|
|
@@ -52,6 +57,7 @@ function WebPhone(access_token, realtime) {
|
|
|
52
57
|
},
|
|
53
58
|
onTrack: function (event) {
|
|
54
59
|
if (event.track.kind != 'audio') return
|
|
60
|
+
if (!$audio) return
|
|
55
61
|
$audio.srcObject = event.streams[0]
|
|
56
62
|
},
|
|
57
63
|
})
|
|
@@ -100,8 +106,8 @@ function WebPhone(access_token, realtime) {
|
|
|
100
106
|
|
|
101
107
|
var me = this
|
|
102
108
|
this.isMicAllowed = function () {
|
|
103
|
-
return me.checkMic().then(function (
|
|
104
|
-
return
|
|
109
|
+
return me.checkMic().result.then(function (stream) {
|
|
110
|
+
return !!stream
|
|
105
111
|
})
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -117,7 +123,7 @@ function WebPhone(access_token, realtime) {
|
|
|
117
123
|
var microPermission = getMicroPermissions()
|
|
118
124
|
return {
|
|
119
125
|
timeout: Promise.race([timeout, microPermission]),
|
|
120
|
-
result:
|
|
126
|
+
result: microPermission,
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
|
|
@@ -138,14 +144,9 @@ function WebPhone(access_token, realtime) {
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
this.answerWebCall = function (callid) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
webrtcconn.answerCall(callid, stream).then(function (out) {
|
|
145
|
-
if (out.error) return rs({error})
|
|
146
|
-
rs(body)
|
|
147
|
-
})
|
|
148
|
-
})
|
|
147
|
+
current_call_id = callid
|
|
148
|
+
return webrtcconn.answerCall(callid, getMicroPermissions()).then(function (out) {
|
|
149
|
+
return out.error ? {error: out.error} : out.body
|
|
149
150
|
})
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -158,12 +159,13 @@ function WebPhone(access_token, realtime) {
|
|
|
158
159
|
// makeCall(number, ['a','b','c'], streamPm) // fallback specific number
|
|
159
160
|
// makeCall(number, function() {}, streamPm) //
|
|
160
161
|
this.makeCall = function (number, fromnumbers, streamPm, _a, _b, _state) {
|
|
162
|
+
var ownsSource = !_state
|
|
161
163
|
if (!_state) _state = {}
|
|
162
164
|
if (!fromnumbers || (Array.isArray(fromnumbers) && fromnumbers.length == 0)) {
|
|
163
165
|
fromnumbers = _numbers
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
if (typeof fromnumbers === 'string' || fromnumbers instanceof String || typeof
|
|
168
|
+
if (typeof fromnumbers === 'string' || fromnumbers instanceof String || typeof fromnumbers === 'number') {
|
|
167
169
|
fromnumbers = [fromnumbers + '']
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -172,11 +174,9 @@ function WebPhone(access_token, realtime) {
|
|
|
172
174
|
callid += `-${randomString(4, base)}-${randomString(12, base)}`
|
|
173
175
|
current_call_id = callid
|
|
174
176
|
|
|
175
|
-
if (!streamPm) streamPm = getMicroPermissions()
|
|
176
|
-
|
|
177
177
|
let callnumber = chooseCallNumber(fromnumbers, _state.called)
|
|
178
178
|
if (!callnumber)
|
|
179
|
-
return (
|
|
179
|
+
return Promise.resolve(
|
|
180
180
|
_state.last_result || {
|
|
181
181
|
start: Date.now(),
|
|
182
182
|
ended: Date.now(),
|
|
@@ -187,9 +187,13 @@ function WebPhone(access_token, realtime) {
|
|
|
187
187
|
id: callid,
|
|
188
188
|
hangup_code: 'failure',
|
|
189
189
|
status: 'ended',
|
|
190
|
-
}
|
|
190
|
+
},
|
|
191
191
|
)
|
|
192
|
-
|
|
192
|
+
// Each attempt owns its clone. The source stays live across number fallback
|
|
193
|
+
// and is released once the complete dialing sequence finishes.
|
|
194
|
+
if (!_state.source) _state.source = Promise.resolve(streamPm || getMicroPermissions())
|
|
195
|
+
var attemptStream = _state.source.then((stream) => stream && stream.clone())
|
|
196
|
+
var result = webrtcconn.makeCall(number, callnumber, attemptStream, callid).then((out) => {
|
|
193
197
|
if (out.error) return out
|
|
194
198
|
if (out.hangup_code == 'cancel') return out
|
|
195
199
|
if (out.answered > 0) return out
|
|
@@ -198,10 +202,18 @@ function WebPhone(access_token, realtime) {
|
|
|
198
202
|
if (!_state.called) _state.called = {}
|
|
199
203
|
_state.called[callnumber] = true
|
|
200
204
|
_state.last_result = out
|
|
201
|
-
return
|
|
205
|
+
return me.makeCall(number, fromnumbers, streamPm, _a, _b, _state)
|
|
202
206
|
}
|
|
203
207
|
return out
|
|
204
208
|
})
|
|
209
|
+
if (!ownsSource) return result
|
|
210
|
+
return result.finally(() => {
|
|
211
|
+
_state.source
|
|
212
|
+
.then((stream) => {
|
|
213
|
+
if (stream) stream.getTracks().forEach((track) => track.stop())
|
|
214
|
+
}, reportCallbackError)
|
|
215
|
+
.catch(reportCallbackError)
|
|
216
|
+
})
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
var current_call_id = ''
|