@subiz/wsclient 3.1.9 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/common.js ADDED
@@ -0,0 +1,66 @@
1
+ function dofetch(method, url, body, cb) {
2
+ let headers = {}
3
+ if (body) headers['content-type'] = 'text/plain'
4
+ fetch(url, {method: method, headers: headers, body: body})
5
+ .then((res) => res.text().then((text) => cb && cb(text, res.status)))
6
+ .catch((err) => cb && cb(err, -1))
7
+ }
8
+
9
+ // xhrsend sends an HTTP request
10
+ var xhrsend = function (method, url, body, cb) {
11
+ if (typeof XMLHttpRequest === 'undefined') return dofetch(method, url, body, cb)
12
+ var request = new XMLHttpRequest()
13
+ request.onreadystatechange = function (e) {
14
+ if (request.readyState !== 4) return
15
+ cb && cb(request.responseText, request.status)
16
+ cb = undefined // dont call cb anymore
17
+ }
18
+
19
+ request.onerror = function () {
20
+ cb && cb(request.responseText, -1) // network error
21
+ cb = undefined // dont call cb anymore
22
+ }
23
+
24
+ request.open(method, url)
25
+ if (body) request.setRequestHeader('content-type', 'text/plain')
26
+ request.send(body)
27
+ }
28
+
29
+ // JSON.parse without exeption
30
+ // return undefined when parsing invalid JSON string
31
+ function parseJSON(str) {
32
+ try {
33
+ return JSON.parse(str)
34
+ } catch (e) {}
35
+ }
36
+
37
+ function Pubsub() {
38
+ // holds all subscriptions for all topics
39
+ // example {
40
+ // topic1: [cb1, cb2],
41
+ // topic2: [cb3, cb4],
42
+ // }
43
+ var listeners = {}
44
+
45
+ // emit notifies any callback functions that previously
46
+ // subscribed to the topic (by the on function)
47
+ this.emit = function (topic) {
48
+ // skip the first item in arguments, which is the topic
49
+ var args = []
50
+ for (var i = 1; i < arguments.length; i++) args.push(arguments[i])
51
+
52
+ var cbs = listeners[topic]
53
+ if (!cbs) return
54
+ for (var i = 0; i < cbs.length; i++) cbs[i].apply(undefined, args)
55
+ }
56
+
57
+ // register a callback function for a topic
58
+ // when something is sent to the topic (by the emit function),
59
+ // the callback function will be called
60
+ this.on = function (topic, cb) {
61
+ if (!listeners[topic]) listeners[topic] = []
62
+ listeners[topic].push(cb)
63
+ }
64
+ }
65
+
66
+ module.exports = {xhrsend, parseJSON, Pubsub}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "3.1.9",
4
- "main": "index.js",
3
+ "version": "4.0.1",
4
+ "main": "realtime.js",
5
5
  "scripts": {
6
6
  "test": "node realtime.test.js"
7
7
  },
package/realtime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  var flow = require('@subiz/flow')
2
+ var {xhrsend, parseJSON, Pubsub} = require('./common.js')
2
3
 
3
4
  // Conn represents a realtime connection between an user and the realtime
4
5
  // server (https://rt-0.subiz.com)
@@ -212,74 +213,6 @@ function Realtime(apiUrls, credential, callAPI) {
212
213
  reconnect()
213
214
  }
214
215
 
215
- var dofetch = function (method, url, body, cb) {
216
- let headers = {}
217
- if (body) headers['content-type'] = 'text/plain'
218
- fetch(url, {
219
- method: method,
220
- headers: headers,
221
- body: body,
222
- })
223
- .then((res) => res.text().then((text) => cb && cb(text, res.status)))
224
- .catch((err) => cb && cb(err, -1))
225
- }
226
- // xhrsend sends an HTTP request
227
- var xhrsend = function (method, url, body, cb) {
228
- if (typeof XMLHttpRequest === 'undefined') return dofetch(method, url, body, cb)
229
- var request = new XMLHttpRequest()
230
- request.onreadystatechange = function (e) {
231
- if (request.readyState !== 4) return
232
- cb && cb(request.responseText, request.status)
233
- cb = undefined // dont call cb anymore
234
- }
235
-
236
- request.onerror = function () {
237
- cb && cb(request.responseText, -1) // network error
238
- cb = undefined // dont call cb anymore
239
- }
240
-
241
- request.open(method, url)
242
- if (body) request.setRequestHeader('content-type', 'text/plain')
243
- request.send(body)
244
- }
245
-
246
- // JSON.parse without exeption
247
- // return undefined when parsing invalid JSON string
248
- function parseJSON(str) {
249
- try {
250
- return JSON.parse(str)
251
- } catch (e) {}
252
- }
253
-
254
- function Pubsub() {
255
- // holds all subscriptions for all topics
256
- // example {
257
- // topic1: [cb1, cb2],
258
- // topic2: [cb3, cb4],
259
- // }
260
- var listeners = {}
261
-
262
- // emit notifies any callback functions that previously
263
- // subscribed to the topic (by the on function)
264
- this.emit = function (topic) {
265
- // skip the first item in arguments, which is the topic
266
- var args = []
267
- for (var i = 1; i < arguments.length; i++) args.push(arguments[i])
268
-
269
- var cbs = listeners[topic]
270
- if (!cbs) return
271
- for (var i = 0; i < cbs.length; i++) cbs[i].apply(undefined, args)
272
- }
273
-
274
- // register a callback function for a topic
275
- // when something is sent to the topic (by the emit function),
276
- // the callback function will be called
277
- this.on = function (topic, cb) {
278
- if (!listeners[topic]) listeners[topic] = []
279
- listeners[topic].push(cb)
280
- }
281
- }
282
-
283
216
  // calcNextBackoff returns number of seconds we must wait
284
217
  // before sending a next request
285
218
  // the results is determind based on exponential backoff algorithm
package/webrtc.js ADDED
@@ -0,0 +1,353 @@
1
+ var flow = require('@subiz/flow')
2
+ var {xhrsend} = require('./common.js')
3
+ const servers = {
4
+ iceServers: [{urls: ['stun:stun.l.google.com:19302?transport=tcp']}],
5
+ }
6
+
7
+ // let connId = 'webrtc' + sb.randomString(20)
8
+ // env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, setInterval, jsonify, parseJSON}
9
+ // setting env._is_webrtc_local to true for local testing
10
+ function WebRTCConn(options) {
11
+ let {apiUrl, access_token, accid, agid, connId, realtime, callAPI, env, onTrack, onEvent} = options
12
+ if (!apiUrl) {
13
+ apiUrl = 'https://callcenter.subiz.com.vn/wc/'
14
+ if (env._is_webrtc_local) apiUrl = 'http://localhost:8008/'
15
+ }
16
+
17
+ callAPI = callAPI || xhrsend // allow hook
18
+
19
+ let parseJSON = (str) => {
20
+ try {
21
+ return env.JSON.parse(str)
22
+ } catch (e) {}
23
+ }
24
+ let jsonify = (str) => {
25
+ try {
26
+ return env.JSON.stringify(str)
27
+ } catch (e) {}
28
+ }
29
+ let currentStream
30
+ let pendingStream
31
+ let activeCalls = {}
32
+
33
+ let dialingRequest = {}
34
+ let endRequest = {}
35
+
36
+ let subscribe = realtime.subscribe([
37
+ `webrtc.account.${accid}.agent.${agid}`,
38
+ `webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`,
39
+ ])
40
+
41
+ let peer
42
+ // makeSure only returns when stream is added and negotiation is finished
43
+ // its safe to call this function multiple times at once
44
+ let makeSure = (cb) => {
45
+ initConnection()
46
+ subscribe.then(cb)
47
+ }
48
+
49
+ let checkIceConnected = (cb) =>
50
+ flow.loop(
51
+ () =>
52
+ new env.Promise((rs) => {
53
+ let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
54
+ if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping util connected
55
+ cb && cb()
56
+ rs(false)
57
+ }),
58
+ )
59
+
60
+ let readyToSendCandidateResolve
61
+ let readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
62
+
63
+ let initConnection = () => {
64
+ let needConnect =
65
+ !peer ||
66
+ peer.connectionState === 'disconnected' ||
67
+ peer.connectionState === 'closed' ||
68
+ peer.connectionState === 'failed'
69
+
70
+ if (!needConnect) return
71
+ peer = new env.RTCPeerConnection(servers)
72
+ let mypeer = peer
73
+ window.peer = peer
74
+
75
+ readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
76
+
77
+ peer.ontrack = (event) => {
78
+ if (peer != mypeer) return // outdated
79
+ onTrack && onTrack(event)
80
+ }
81
+
82
+ peer.onicecandidate = (event) => {
83
+ if (!event.candidate) return
84
+ if (peer != mypeer) return // outdated
85
+ let url = `${apiUrl}ice-candidates?x-access-token=${access_token}&connection_id=${connId}`
86
+ callAPI('post', url, jsonify(event.candidate))
87
+ }
88
+
89
+ peer.onnegotiationneeded = (ev) => console.log('NNNEED RE-NEGOTIATION', ev)
90
+ console.log('NEW PERR')
91
+ }
92
+
93
+ let stopMic = () => {
94
+ console.log('00000000STOP TRACK', currentStream)
95
+ return
96
+ currentStream &&
97
+ currentStream.getTracks().forEach((track) => {
98
+ track.enabled = false
99
+ })
100
+ }
101
+
102
+ // auto tear down when byte received is zero
103
+ // use to clear peer when there's no byte sent around 100 seconds
104
+ let receivedBytes = []
105
+ let check = () => {
106
+ let url = `${apiUrl}calls?x-access-token=${access_token}&connection_id=${connId}`
107
+ callAPI('get', url, undefined, function (body, code) {
108
+ if (code == 200) {
109
+ body = parseJSON(body) || []
110
+ activeCalls = {}
111
+ body.map && body.map((call) => (activeCalls[call.call_id] = call))
112
+ }
113
+
114
+ if (!peer) return
115
+ peer.getStats(null).then((stats) => {
116
+ let statsOutput = ''
117
+ let receivedByte = 0
118
+ stats.forEach((report) => {
119
+ if (report.type === 'inbound-rtp' && report.kind === 'audio') {
120
+ receivedByte += report.bytesReceived || 0
121
+ }
122
+ if (report.type === 'inbound-rtp' && report.kind === 'video') {
123
+ receivedByte += report.bytesReceived || 0
124
+ }
125
+ })
126
+ receivedBytes.push(receivedByte)
127
+ while (receivedBytes.length > 10) receivedBytes.shift()
128
+ let diff = receivedBytes.filter((n) => n != receivedByte)
129
+ if (diff.length == 0 && receivedBytes.length >= 10 && Object.keys(activeCalls).length == 0) {
130
+ if (Object.keys(activeCalls).length == 0) stopMic()
131
+ // could tear down
132
+ peer.close()
133
+ peer = undefined
134
+ }
135
+ })
136
+ })
137
+ }
138
+ check()
139
+ env.setInterval(check, 30000)
140
+
141
+ let publish = (ev) => onEvent && onEvent(ev)
142
+
143
+ this.matchCall = (callid) => {
144
+ if (!callid || callid == '*' || callid == '-') {
145
+ let calls = Object.assign({}, activeCalls)
146
+ Object.keys(dialingRequest).map((callid) => {
147
+ let call = activeCalls[callid]
148
+ if (!call && dialingRequest[callid]) {
149
+ calls[callid] = dialingRequest[callid]
150
+ }
151
+ })
152
+ return calls
153
+ }
154
+
155
+ let call = activeCalls[callid]
156
+ if (endRequest[callid] && call && call.status != 'ended') {
157
+ return Object.assign({}, call, {
158
+ call_id: callid,
159
+ ended: endRequest[callid],
160
+ status: 'ended',
161
+ hangup_code: 'cancel',
162
+ })
163
+ }
164
+ if (!call && dialingRequest[callid]) return dialingRequest[callid]
165
+ return call
166
+ }
167
+
168
+ realtime.onEvent((ev) => {
169
+ if (!ev || !ev.type || !ev.data) return
170
+ // if (ev.type.startsWith('call')) console.log('EEEEEEE', ev)
171
+ if (env._is_webrtc_local && ev.user_id != '8') return // local
172
+ if (!env._is_webrtc_local && ev.user_id != '9') return // prod
173
+
174
+ if (ev.type == 'webrtc_candidate_added') {
175
+ let ice_candidate = ev.data.ice_candidate
176
+ if (!ice_candidate) return
177
+ return readyToSendCandidate.then(() => peer && peer.addIceCandidate(parseJSON(ice_candidate)))
178
+ }
179
+
180
+ if (ev.type == 'webrtc_negotiation_offered') {
181
+ console.log('RT/RECEIVE OFFERED NEGOTIATION', ev)
182
+ if (!peer) return
183
+ let negid = ev.id
184
+ if (!ev.data.ice_candidate) return
185
+ let offer = parseJSON(ev.data.ice_candidate)
186
+
187
+ peer
188
+ .setRemoteDescription(offer)
189
+ .then(() => {
190
+ readyToSendCandidateResolve()
191
+ if (pendingStream && pendingStream != currentStream) {
192
+ currentStream = pendingStream
193
+ currentStream.getTracks().forEach((track) => peer.addTrack(track, currentStream))
194
+ }
195
+ pendingStream = undefined
196
+ return peer.createAnswer()
197
+ })
198
+ .then((answer) =>
199
+ peer.setLocalDescription(answer).then(() => {
200
+ let url = `${apiUrl}finish?x-access-token=${access_token}&connection_id=${connId}&version=2&negotiation_id=${negid}`
201
+ callAPI('post', url, jsonify(answer), function (body, code) {
202
+ if (code !== 200) return console.error('CANNOT FINISH', body)
203
+ return
204
+ })
205
+ }),
206
+ )
207
+ }
208
+
209
+ let call_info = ev.data.call_info || {}
210
+ let callid = call_info.call_id
211
+ if (!callid) return
212
+
213
+ if (ev.type === 'call_invite_expired') {
214
+ let device_id = call_info.device_id
215
+ if (device_id == connId) return // ignore since we have accepted the call
216
+ publish(Object.assign({}, ev, {type: 'call_ended'})) // fake ended call
217
+ delete activeCalls[callid]
218
+ }
219
+
220
+ let type = ev.type
221
+ if (type == 'call_ended' || type == 'call_ringing' || type == 'call_joined' || type === 'call_invited') {
222
+ if (ev.type === 'call_invited' && agid == call_info.from_number) return // ignore our own invite
223
+ activeCalls[callid] = call_info
224
+ return publish(ev)
225
+ }
226
+ })
227
+
228
+ this.makeCall = (number, fromnumber, stream) =>
229
+ new env.Promise((rs) => {
230
+ let callid = 'webcall-' + randomString(20)
231
+ let call = {
232
+ account_id: accid,
233
+ call_id: callid,
234
+ device_id: connId,
235
+ direction: 'outbound',
236
+ member_id: agid,
237
+ started: env.Date.now(),
238
+ status: 'dialing',
239
+ to_number: number,
240
+ }
241
+ dialingRequest[callid] = call
242
+ publish({created: env.Date.now(), type: 'call_ringing', data: {call_info: call}}) // fake dialing
243
+ this.joinCall(callid, stream, (err) => {
244
+ if (err) {
245
+ delete dialingRequest[callid]
246
+ endRequest[callid] = env.Date.now()
247
+ publish({
248
+ type: 'call_ended',
249
+ data: {call_info: {call_id: callid, ended: endRequest[callid], status: 'ended', hangup_code: 'error'}},
250
+ })
251
+ return rs({error: err})
252
+ }
253
+ let url = `${apiUrl}call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${number}&from_number=${fromnumber}`
254
+ callAPI('post', url, undefined, (body, code) => {
255
+ if (code != 200) {
256
+ delete dialingRequest[callid]
257
+ endRequest[callid] = env.Date.now()
258
+ publish({
259
+ type: 'call_ended',
260
+ data: {call_info: {call_id: callid, ended: endRequest[callid], status: 'ended', hangup_code: 'error'}},
261
+ })
262
+ return rs({error: err})
263
+ }
264
+ body = parseJSON(body)
265
+ if (body) activeCalls[callid] = body
266
+ rs({body})
267
+ })
268
+ })
269
+ })
270
+
271
+ this.hangupCall = (callid) => {
272
+ if (!callid) return
273
+ endRequest[callid] = env.Date.now()
274
+
275
+ let call = activeCalls[callid]
276
+ if (!call) {
277
+ call = {call_id: callid}
278
+ activeCalls[callid] = call
279
+ }
280
+ call.status = 'ended'
281
+ call.ended = env.Date.now()
282
+ callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`)
283
+ publish({type: 'call_ended', data: {call_info: call}})
284
+ }
285
+
286
+ this.listenCall = (callid, cb) => {
287
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
288
+ makeSure(() => callAPI('post', url, undefined, (body, code) => (code != 200 ? cb(body) : cb())))
289
+ }
290
+
291
+ this.joinCall = (callid, stream, cb) => {
292
+ console.time('makecall' + callid)
293
+ makeSure(() => {
294
+ pendingStream = stream
295
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&talk=true`
296
+ callAPI('post', url, undefined, (body, code) => {
297
+ if (code != 200) return cb(body)
298
+ console.timeEnd('makecall' + callid)
299
+ if (code != 200) return cb(body)
300
+ checkIceConnected(() => cb())
301
+ })
302
+ })
303
+ }
304
+
305
+ this.answerCall = (callid, stream) =>
306
+ new env.Promise((rs) => {
307
+ this.joinCall(callid, stream, (err) => {
308
+ if (err) return rs({error: err})
309
+ let url = `${apiUrl}answer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
310
+ callAPI('post', url, undefined, (body, code) => {
311
+ body = parseJSON(body)
312
+ return code != 200 ? rs({error: body}) : rs({body})
313
+ })
314
+ })
315
+ })
316
+
317
+ this.sendDtmf = (key, callid) =>
318
+ new env.Promise((rs) => {
319
+ if (!callid) return rs({body: {}})
320
+ let url = `${apiUrl}dtml?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&key={key}`
321
+ makeSure(() =>
322
+ callAPI('post', url, undefined, (body, bode) => {
323
+ body = parseJSON(body)
324
+ return code != 200 ? rs({error: body}) : rs({body})
325
+ }),
326
+ )
327
+ })
328
+
329
+ this.transferCall = (tonumber, callid) =>
330
+ new env.Promise((rs) => {
331
+ if (!callid) return rs({})
332
+ let url = `${apiUrl}refer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number={tonumber}`
333
+ makeSure(() =>
334
+ callAPI('post', url, undefined, (body, code) => {
335
+ body = parseJSON(body)
336
+ return code != 200 ? rs({error: body}) : rs({body})
337
+ }),
338
+ )
339
+ })
340
+ }
341
+
342
+ function randomString(len) {
343
+ var str = ''
344
+ if (!len || len < 1) len = 10
345
+ var asciiKey
346
+ for (var i = 0; i < len; i++) {
347
+ asciiKey = Math.floor(Math.random() * 25 + 97)
348
+ str += String.fromCharCode(asciiKey)
349
+ }
350
+ return str
351
+ }
352
+
353
+ module.exports = WebRTCConn
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./realtime.js')