@subiz/wsclient 3.1.9 → 4.0.0

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.0",
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,302 @@
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
+ // import {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, mediaDevices} from 'react-native-webrtc'
8
+ // let connId = 'webrtc' + sb.randomString(20)
9
+ // env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, setInterval, jsonify, parseJSON}
10
+ // setting env._is_webrtc_local to true for local testing
11
+ function WebRTCConn(options) {
12
+ let {apiUrl, access_token, accid, agid, connId, realtime, callAPI, env, onTrack, onEvent} = options
13
+ if (!apiUrl) {
14
+ apiUrl = 'https://callcenter.subiz.com.vn/wc/'
15
+ if (env._is_webrtc_local) apiUrl = 'http://localhost:8008/'
16
+ }
17
+
18
+ callAPI = callAPI || xhrsend // allow hook
19
+
20
+ let parseJSON = (str) => {
21
+ try {
22
+ return env.JSON.parse(str)
23
+ } catch (e) {}
24
+ }
25
+ let jsonify = (str) => {
26
+ try {
27
+ return env.JSON.stringify(str)
28
+ } catch (e) {}
29
+ }
30
+ let currentStream
31
+ let pendingStream
32
+ let activeCalls = {}
33
+
34
+ let subscribe = realtime.subscribe([
35
+ `webrtc.account.${accid}.agent.${agid}`,
36
+ `webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`,
37
+ ])
38
+
39
+ let peer
40
+ // makeSure only returns when stream is added and negotiation is finished
41
+ // its safe to call this function multiple times at once
42
+ let makeSure = (cb) => {
43
+ initConnection()
44
+ subscribe.then(cb)
45
+ }
46
+
47
+ let checkIceConnected = (cb) =>
48
+ flow.loop(
49
+ () =>
50
+ new env.Promise((rs) => {
51
+ let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
52
+ if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping ultil connected
53
+ cb && cb()
54
+ rs(false)
55
+ }),
56
+ )
57
+
58
+ let readyToSendCandidateResolve
59
+ let readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
60
+
61
+ let initConnection = () => {
62
+ let needConnect =
63
+ !peer ||
64
+ peer.connectionState === 'disconnected' ||
65
+ peer.connectionState === 'closed' ||
66
+ peer.connectionState === 'failed'
67
+
68
+ if (!needConnect) return
69
+ peer = new env.RTCPeerConnection(servers)
70
+ let mypeer = peer
71
+ window.peer = peer
72
+
73
+ readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
74
+
75
+ peer.ontrack = (event) => {
76
+ if (peer != mypeer) return // outdated
77
+ onTrack && onTrack(event)
78
+ }
79
+
80
+ peer.onicecandidate = (event) => {
81
+ if (!event.candidate) return
82
+ if (peer != mypeer) return // outdated
83
+ let url = `${apiUrl}ice-candidates?x-access-token=${access_token}&connection_id=${connId}`
84
+ callAPI('post', url, jsonify(event.candidate))
85
+ }
86
+
87
+ peer.onnegotiationneeded = (ev) => console.log('NNNEED RE-NEGOTIATION', ev)
88
+ console.log('NEW PERR')
89
+ }
90
+
91
+ let stopMic = () => {
92
+ console.log('00000000STOP TRACK', currentStream)
93
+ return
94
+ currentStream &&
95
+ currentStream.getTracks().forEach((track) => {
96
+ track.enabled = false
97
+ })
98
+ }
99
+
100
+ // auto tear down when byte received is zero
101
+ // use to clear peer when there's no byte sent around 100 seconds
102
+ let receivedBytes = []
103
+ let check = () => {
104
+ let url = `${apiUrl}calls?x-access-token=${access_token}&connection_id=${connId}`
105
+ callAPI('get', url, undefined, function (body, code) {
106
+ if (code == 200) {
107
+ body = parseJSON(body) || []
108
+ activeCalls = {}
109
+ body.map && body.map((call) => (activeCalls[call.call_id] = call))
110
+ }
111
+
112
+ if (!peer) return
113
+ peer.getStats(null).then((stats) => {
114
+ let statsOutput = ''
115
+ let receivedByte = 0
116
+ stats.forEach((report) => {
117
+ if (report.type === 'inbound-rtp' && report.kind === 'audio') {
118
+ receivedByte += report.bytesReceived || 0
119
+ }
120
+ if (report.type === 'inbound-rtp' && report.kind === 'video') {
121
+ receivedByte += report.bytesReceived || 0
122
+ }
123
+ })
124
+ receivedBytes.push(receivedByte)
125
+ while (receivedBytes.length > 10) receivedBytes.shift()
126
+ let diff = receivedBytes.filter((n) => n != receivedByte)
127
+ if (diff.length == 0 && receivedBytes.length >= 10 && Object.keys(activeCalls).length == 0) {
128
+ if (Object.keys(activeCalls).length == 0) stopMic()
129
+ // could tear down
130
+ peer.close()
131
+ peer = undefined
132
+ }
133
+ })
134
+ })
135
+ }
136
+ check()
137
+ env.setInterval(check, 30000)
138
+
139
+ let publish = (ev) => onEvent && onEvent(ev)
140
+
141
+ this.matchCall = (callid) => (!callid ? activeCalls : activeCalls[callid])
142
+
143
+ realtime.onEvent((ev) => {
144
+ if (!ev || !ev.type || !ev.data) return
145
+ // if (ev.type.startsWith('call')) console.log('EEEEEEE', ev)
146
+ if (env._is_webrtc_local && ev.user_id != '8') return // local
147
+ if (!env._is_webrtc_local && ev.user_id != '9') return // prod
148
+
149
+ if (ev.type == 'webrtc_candidate_added') {
150
+ let ice_candidate = ev.data.ice_candidate
151
+ if (!ice_candidate) return
152
+ return readyToSendCandidate.then(() => peer && peer.addIceCandidate(parseJSON(ice_candidate)))
153
+ }
154
+
155
+ if (ev.type == 'webrtc_negotiation_offered') {
156
+ console.log('RT/RECEIVE OFFERED NEGOTIATION', ev)
157
+ if (!peer) return
158
+ let negid = ev.id
159
+ if (!ev.data.ice_candidate) return
160
+ let offer = parseJSON(ev.data.ice_candidate)
161
+
162
+ peer
163
+ .setRemoteDescription(offer)
164
+ .then(() => {
165
+ readyToSendCandidateResolve()
166
+ if (pendingStream && pendingStream != currentStream) {
167
+ currentStream = pendingStream
168
+ currentStream.getTracks().forEach((track) => peer.addTrack(track, currentStream))
169
+ }
170
+ pendingStream = undefined
171
+ return peer.createAnswer()
172
+ })
173
+ .then((answer) =>
174
+ peer.setLocalDescription(answer).then(() => {
175
+ let url = `${apiUrl}finish?x-access-token=${access_token}&connection_id=${connId}&version=2&negotiation_id=${negid}`
176
+ callAPI('post', url, jsonify(answer), function (body, code) {
177
+ if (code !== 200) return console.error('CANNOT FINISH', body)
178
+ return
179
+ })
180
+ }),
181
+ )
182
+ }
183
+
184
+ let call_info = ev.data.call_info || {}
185
+ let callid = call_info.call_id
186
+ if (!callid) return
187
+
188
+ if (ev.type === 'call_invite_expired') {
189
+ activeCalls[callid] = call_info
190
+ let device_id = call_info.device_id
191
+ if (device_id == connId) return // ignore since we have accepted the call
192
+
193
+ publish(Object.assign({}, ev, {type: 'call_ended'})) // fake ended call
194
+ delete activeCalls[callid]
195
+ }
196
+ let type = ev.type
197
+ if (type == 'call_ended' || type == 'call_ringing' || type == 'call_joined' || type === 'call_invited') {
198
+ if (ev.type === 'call_invited' && agid == call_info.from_number) return // ignore our own invite
199
+ activeCalls[callid] = ev.data.call_info
200
+ return publish(ev)
201
+ }
202
+ })
203
+
204
+ this.makeCall = (number, fromnumber, stream) =>
205
+ new env.Promise((rs) => {
206
+ let callid = 'webcall-' + randomString(20)
207
+ this.joinCall(callid, stream, (err) => {
208
+ if (err) return rs({error: err})
209
+ let url = `${apiUrl}call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${number}&from_number=${fromnumber}`
210
+ callAPI('post', url, undefined, (body, code) => {
211
+ if (code != 200) return rs({error: body})
212
+ body = parseJSON(body)
213
+ if (body) activeCalls[callid] = body
214
+ rs({body})
215
+ })
216
+ })
217
+ })
218
+
219
+ this.hangupCall = (callid) => {
220
+ if (!callid) return
221
+ let call = activeCalls[callid]
222
+ if (!call) {
223
+ call = {call_id: callid}
224
+ activeCalls[callid] = call
225
+ }
226
+ call.status = 'ended'
227
+ call.ended = env.Date.now()
228
+ callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`)
229
+ publish({type: 'call_ended', data: {call_info: call}})
230
+ }
231
+
232
+ this.listenCall = (callid, cb) => {
233
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
234
+ makeSure(() => callAPI('post', url, undefined, (body, code) => (code != 200 ? cb(body) : cb())))
235
+ }
236
+
237
+ this.joinCall = (callid, stream, cb) => {
238
+ console.time('makecall' + callid)
239
+ makeSure(() => {
240
+ pendingStream = stream
241
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
242
+ callAPI('post', url, undefined, (body, code) => {
243
+ if (code != 200) return cb(body)
244
+ let url = `${apiUrl}join?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
245
+ callAPI('post', url, undefined, (body, code) => {
246
+ console.timeEnd('makecall' + callid)
247
+ if (code != 200) return cb(body)
248
+ checkIceConnected(() => cb())
249
+ })
250
+ })
251
+ })
252
+ }
253
+
254
+ this.answerCall = (callid, stream) =>
255
+ new env.Promise((rs) => {
256
+ this.joinCall(callid, stream, (err) => {
257
+ if (err) return rs({error: err})
258
+ let url = `${apiUrl}answer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
259
+ callAPI('post', url, undefined, (body, code) => {
260
+ body = parseJSON(body)
261
+ return code != 200 ? rs({error: body}) : rs({body})
262
+ })
263
+ })
264
+ })
265
+
266
+ this.sendDtmf = (key, callid) =>
267
+ new env.Promise((rs) => {
268
+ if (!callid) return rs({body: {}})
269
+ let url = `${apiUrl}dtml?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&key={key}`
270
+ makeSure(() =>
271
+ callAPI('post', url, undefined, (body, bode) => {
272
+ body = parseJSON(body)
273
+ return code != 200 ? rs({error: body}) : rs({body})
274
+ }),
275
+ )
276
+ })
277
+
278
+ this.transferCall = (tonumber, callid) =>
279
+ new env.Promise((rs) => {
280
+ if (!callid) return rs({})
281
+ let url = `${apiUrl}refer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number={tonumber}`
282
+ makeSure(() =>
283
+ callAPI('post', url, undefined, (body, code) => {
284
+ body = parseJSON(body)
285
+ return code != 200 ? rs({error: body}) : rs({body})
286
+ }),
287
+ )
288
+ })
289
+ }
290
+
291
+ function randomString(len) {
292
+ var str = ''
293
+ if (!len || len < 1) len = 10
294
+ var asciiKey
295
+ for (var i = 0; i < len; i++) {
296
+ asciiKey = Math.floor(Math.random() * 25 + 97)
297
+ str += String.fromCharCode(asciiKey)
298
+ }
299
+ return str
300
+ }
301
+
302
+ module.exports = WebRTCConn
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./realtime.js')