@nethesis/phone-island 0.2.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/.eslintrc.json +41 -0
- package/.prettierrc +9 -0
- package/.storybook/main.js +20 -0
- package/.storybook/preview.js +11 -0
- package/README.md +64 -0
- package/decs.d.ts +1 -0
- package/package.json +95 -0
- package/postcss.config.js +5 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +18 -0
- package/rollup.config.ts +59 -0
- package/scripts/buildUtils.js +28 -0
- package/src/App.tsx +467 -0
- package/src/index.css +9 -0
- package/src/index.ts +2 -0
- package/src/index.widget.tsx +22 -0
- package/src/lib/janus.js +3661 -0
- package/src/stories/App.stories.tsx +36 -0
- package/tailwind.config.js +14 -0
- package/tsconfig.json +21 -0
- package/widget-example/index.html +20 -0
package/src/App.tsx
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef, FC } from 'react'
|
|
2
|
+
import adapter from 'webrtc-adapter'
|
|
3
|
+
import Janus from './lib/janus.js'
|
|
4
|
+
import { io } from 'socket.io-client'
|
|
5
|
+
|
|
6
|
+
interface PhoneIslandType {
|
|
7
|
+
dataConfig: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const PhoneIsland: FC<PhoneIslandType> = ({ dataConfig }) => {
|
|
11
|
+
const [calling, setCalling] = useState<boolean>(false)
|
|
12
|
+
const [sipcall, setSipCall] = useState<any>(null)
|
|
13
|
+
const [jsepGlobal, setJsepGlobal] = useState<object | null>(null)
|
|
14
|
+
const [accepted, setAccepted] = useState<boolean>(false)
|
|
15
|
+
const [currentCall, setCurrentCall] = useState<{ [index: string]: string | number }>({})
|
|
16
|
+
const localStream = useRef(null)
|
|
17
|
+
|
|
18
|
+
const CONFIG: string[] = atob(dataConfig).split(':')
|
|
19
|
+
const HOST_NAME: string = CONFIG[0]
|
|
20
|
+
const USERNAME: string = CONFIG[1]
|
|
21
|
+
const AUTH_TOKEN: string = CONFIG[2]
|
|
22
|
+
const SIP_EXTEN: string = CONFIG[3]
|
|
23
|
+
const SIP_SECRET: string = CONFIG[4]
|
|
24
|
+
|
|
25
|
+
let registered = false
|
|
26
|
+
|
|
27
|
+
const decline = () => {
|
|
28
|
+
sipcall.send({
|
|
29
|
+
message: {
|
|
30
|
+
request: 'decline',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hangup = () => {
|
|
36
|
+
sipcall.send({
|
|
37
|
+
message: {
|
|
38
|
+
request: 'hangup',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const answer = () => {
|
|
44
|
+
sipcall.createAnswer({
|
|
45
|
+
jsep: jsepGlobal,
|
|
46
|
+
media: {
|
|
47
|
+
audio: true,
|
|
48
|
+
videoSend: false,
|
|
49
|
+
videoRecv: false,
|
|
50
|
+
},
|
|
51
|
+
success: (jsep) => {
|
|
52
|
+
sipcall.send({
|
|
53
|
+
message: {
|
|
54
|
+
request: 'accept',
|
|
55
|
+
},
|
|
56
|
+
jsep: jsep,
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
error: (error) => {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
Janus.error('WebRTC error:', error)
|
|
62
|
+
sipcall.send({
|
|
63
|
+
message: {
|
|
64
|
+
request: 'decline',
|
|
65
|
+
code: 480,
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const register = (sipcall) => {
|
|
73
|
+
// Register after Janus initialization
|
|
74
|
+
sipcall.send({
|
|
75
|
+
message: {
|
|
76
|
+
request: 'register',
|
|
77
|
+
username: 'sip:' + SIP_EXTEN + '@' + '127.0.0.1',
|
|
78
|
+
display_name: 'Foo 1',
|
|
79
|
+
secret: SIP_SECRET,
|
|
80
|
+
proxy: 'sip:' + '127.0.0.1' + ':5060',
|
|
81
|
+
sips: false,
|
|
82
|
+
refresh: false,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ConvType {
|
|
88
|
+
[index: string]: string | number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const getDisplayName = (conv: ConvType): string => {
|
|
92
|
+
let dispName = ''
|
|
93
|
+
if (
|
|
94
|
+
conv &&
|
|
95
|
+
conv.counterpartName !== '<unknown>' &&
|
|
96
|
+
typeof conv.counterpartName === 'string' &&
|
|
97
|
+
conv.counterpartName.length > 0
|
|
98
|
+
) {
|
|
99
|
+
dispName = conv.counterpartName
|
|
100
|
+
} else if (
|
|
101
|
+
conv &&
|
|
102
|
+
conv.counterpartNum &&
|
|
103
|
+
typeof conv.counterpartNum === 'string' &&
|
|
104
|
+
conv.counterpartNum.length > 0
|
|
105
|
+
) {
|
|
106
|
+
dispName = conv.counterpartNum
|
|
107
|
+
} else {
|
|
108
|
+
dispName = 'Anonymous'
|
|
109
|
+
}
|
|
110
|
+
return dispName
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const handleCalls = (res: any) => {
|
|
115
|
+
// Initialize conversation
|
|
116
|
+
const conv: ConvType = res.conversations[Object.keys(res.conversations)[0]] || {}
|
|
117
|
+
|
|
118
|
+
// Check conversation isn't empty
|
|
119
|
+
if (Object.keys(conv).length > 0) {
|
|
120
|
+
const status: string = res.status
|
|
121
|
+
if (status) {
|
|
122
|
+
switch (status) {
|
|
123
|
+
case 'ringing':
|
|
124
|
+
setCurrentCall((state) => ({
|
|
125
|
+
...state,
|
|
126
|
+
displayName: getDisplayName(conv),
|
|
127
|
+
}))
|
|
128
|
+
break
|
|
129
|
+
default:
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const initWsConnection = () => {
|
|
137
|
+
const socket = io(HOST_NAME, {
|
|
138
|
+
upgrade: false,
|
|
139
|
+
transports: ['websocket'],
|
|
140
|
+
reconnection: true,
|
|
141
|
+
reconnectionDelay: 2000,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
socket.on('connect', () => {
|
|
145
|
+
console.log('Socket on: ' + HOST_NAME + ' is connected !')
|
|
146
|
+
|
|
147
|
+
socket.emit('login', {
|
|
148
|
+
accessKeyId: USERNAME,
|
|
149
|
+
token: AUTH_TOKEN,
|
|
150
|
+
uaType: 'desktop',
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
socket.on('authe_ok', () => {
|
|
155
|
+
console.log('AUTH OK')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
socket.on('extenUpdate', (res) => {
|
|
159
|
+
if (res.username === USERNAME) {
|
|
160
|
+
handleCalls(res)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
initWsConnection()
|
|
166
|
+
|
|
167
|
+
navigator.mediaDevices.getUserMedia({
|
|
168
|
+
video: true,
|
|
169
|
+
audio: true,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const setupDeps = () =>
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
Janus.useDefaultDependencies({
|
|
175
|
+
adapter,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
var evtObservers = {
|
|
179
|
+
registration_failed: [],
|
|
180
|
+
registered: [],
|
|
181
|
+
calling: [],
|
|
182
|
+
incomingcall: [],
|
|
183
|
+
accepted: [],
|
|
184
|
+
hangup: [],
|
|
185
|
+
gateway_down: [],
|
|
186
|
+
error: [],
|
|
187
|
+
progress: [],
|
|
188
|
+
destroyed: [],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const initWebRTC = () => {
|
|
192
|
+
// @ts-ignore
|
|
193
|
+
Janus.init({
|
|
194
|
+
debug: 'all',
|
|
195
|
+
dependencies: setupDeps(),
|
|
196
|
+
callback: function () {
|
|
197
|
+
// @ts-ignore
|
|
198
|
+
const janus = new Janus({
|
|
199
|
+
server: 'https://nv-seb/janus',
|
|
200
|
+
success: () => {
|
|
201
|
+
console.log('success')
|
|
202
|
+
// @ts-ignore
|
|
203
|
+
janus.attach({
|
|
204
|
+
plugin: 'janus.plugin.sip',
|
|
205
|
+
opaqueId: 'sebastian' + '_' + new Date().getTime(),
|
|
206
|
+
success: function (pluginHandle) {
|
|
207
|
+
setSipCall(pluginHandle)
|
|
208
|
+
register(pluginHandle)
|
|
209
|
+
if (pluginHandle) {
|
|
210
|
+
console.log(
|
|
211
|
+
'SIP plugin attached! (' + pluginHandle.getPlugin() + ', id = ' + ')',
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
// getSupportedDevices(function () {
|
|
215
|
+
// resolve()
|
|
216
|
+
// })
|
|
217
|
+
},
|
|
218
|
+
error: function (error) {
|
|
219
|
+
console.error(' -- Error attaching plugin...')
|
|
220
|
+
console.error(error)
|
|
221
|
+
// reject()
|
|
222
|
+
},
|
|
223
|
+
consentDialog: function (on) {
|
|
224
|
+
console.log(`janus consentDialog (on: ${on})`)
|
|
225
|
+
},
|
|
226
|
+
webrtcState: function (on) {
|
|
227
|
+
console.log(
|
|
228
|
+
'Janus says our WebRTC PeerConnection is ' + (on ? 'up' : 'down') + ' now',
|
|
229
|
+
)
|
|
230
|
+
},
|
|
231
|
+
iceState: function (newState) {
|
|
232
|
+
if (sipcall) {
|
|
233
|
+
console.log(
|
|
234
|
+
`ICE state of PeerConnection of handle has changed to "${newState}"`,
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
mediaState: function (medium, on) {
|
|
239
|
+
console.log('Janus ' + (on ? 'started' : 'stopped') + ' receiving our ' + medium)
|
|
240
|
+
},
|
|
241
|
+
slowLink: function (uplink, count) {
|
|
242
|
+
if (uplink) {
|
|
243
|
+
console.warn(`SLOW link: several missing packets from janus (${count})`)
|
|
244
|
+
} else {
|
|
245
|
+
console.warn(`SLOW link: janus is not receiving all your packets (${count})`)
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
onmessage: function (msg, jsep) {
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
Janus.debug(' ::: Got a message :::')
|
|
251
|
+
// @ts-ignore
|
|
252
|
+
Janus.debug(JSON.stringify(msg))
|
|
253
|
+
// Any error?
|
|
254
|
+
var error = msg['error']
|
|
255
|
+
if (error != null && error != undefined) {
|
|
256
|
+
if (!registered) {
|
|
257
|
+
// @ts-ignore
|
|
258
|
+
Janus.log('User is not registered')
|
|
259
|
+
} else {
|
|
260
|
+
// Reset status
|
|
261
|
+
sipcall.hangup()
|
|
262
|
+
}
|
|
263
|
+
for (var evt in evtObservers['error']) {
|
|
264
|
+
// @ts-ignore
|
|
265
|
+
evtObservers['error'][evt](msg, jsep)
|
|
266
|
+
}
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
var result = msg['result']
|
|
270
|
+
if (
|
|
271
|
+
result !== null &&
|
|
272
|
+
result !== undefined &&
|
|
273
|
+
result['event'] !== undefined &&
|
|
274
|
+
result['event'] !== null
|
|
275
|
+
) {
|
|
276
|
+
// get event
|
|
277
|
+
var event = result['event']
|
|
278
|
+
|
|
279
|
+
// call all evt registered
|
|
280
|
+
for (var evt in evtObservers[event]) {
|
|
281
|
+
evtObservers[event][evt](msg, jsep)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//switch event
|
|
285
|
+
switch (event) {
|
|
286
|
+
case 'registration_failed':
|
|
287
|
+
// @ts-ignore
|
|
288
|
+
Janus.error(
|
|
289
|
+
'Registration failed: ' + result['code'] + ' ' + result['reason'],
|
|
290
|
+
)
|
|
291
|
+
return
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
case 'unregistered':
|
|
295
|
+
// @ts-ignore
|
|
296
|
+
Janus.log('Successfully un-registered as ' + result['username'] + '!')
|
|
297
|
+
// registered = false
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
case 'registered':
|
|
301
|
+
// @ts-ignore
|
|
302
|
+
Janus.log('Successfully registered as ' + result['username'] + '!')
|
|
303
|
+
if (!registered) {
|
|
304
|
+
registered = true
|
|
305
|
+
}
|
|
306
|
+
// lastActivity = new Date().getTime()
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
case 'registering':
|
|
310
|
+
// @ts-ignore
|
|
311
|
+
Janus.log('janus registering')
|
|
312
|
+
break
|
|
313
|
+
|
|
314
|
+
case 'calling':
|
|
315
|
+
// @ts-ignore
|
|
316
|
+
Janus.log('Waiting for the peer to answer...')
|
|
317
|
+
// lastActivity = new Date().getTime()
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
case 'incomingcall':
|
|
321
|
+
setJsepGlobal(jsep)
|
|
322
|
+
setCalling(true)
|
|
323
|
+
|
|
324
|
+
// @ts-ignore
|
|
325
|
+
Janus.log('Incoming call from ' + result['username'] + '!')
|
|
326
|
+
// lastActivity = new Date().getTime()
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
case 'progress':
|
|
330
|
+
// @ts-ignore
|
|
331
|
+
Janus.log(
|
|
332
|
+
"There's early media from " +
|
|
333
|
+
result['username'] +
|
|
334
|
+
', wairing for the call!',
|
|
335
|
+
)
|
|
336
|
+
// if (jsep !== null && jsep !== undefined) {
|
|
337
|
+
// handleRemote(jsep)
|
|
338
|
+
// }
|
|
339
|
+
// lastActivity = new Date().getTime()
|
|
340
|
+
break
|
|
341
|
+
|
|
342
|
+
case 'accepted':
|
|
343
|
+
setAccepted(true)
|
|
344
|
+
|
|
345
|
+
// @ts-ignore
|
|
346
|
+
Janus.log(result['username'] + ' accepted the call!')
|
|
347
|
+
// if (jsep !== null && jsep !== undefined) {
|
|
348
|
+
// handleRemote(jsep)
|
|
349
|
+
// }
|
|
350
|
+
// lastActivity = new Date().getTime()
|
|
351
|
+
break
|
|
352
|
+
|
|
353
|
+
case 'hangup':
|
|
354
|
+
setCalling(false)
|
|
355
|
+
setAccepted(false)
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
result['code'] === 486 &&
|
|
359
|
+
result['event'] === 'hangup' &&
|
|
360
|
+
result['reason'] === 'Busy Here'
|
|
361
|
+
) {
|
|
362
|
+
// @ts-ignore
|
|
363
|
+
busyToneSound.play()
|
|
364
|
+
}
|
|
365
|
+
// @ts-ignore
|
|
366
|
+
Janus.log('Call hung up (' + result['code'] + ' ' + result['reason'] + ')!')
|
|
367
|
+
// @ts-ignore
|
|
368
|
+
if (incoming != null) {
|
|
369
|
+
// @ts-ignore
|
|
370
|
+
incoming = null
|
|
371
|
+
}
|
|
372
|
+
sipcall.hangup()
|
|
373
|
+
|
|
374
|
+
// lastActivity = new Date().getTime()
|
|
375
|
+
// stopScreenSharingI()
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
default:
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
onlocalstream: function (stream) {
|
|
384
|
+
// @ts-ignore
|
|
385
|
+
Janus.debug(' ::: Got a local stream :::')
|
|
386
|
+
// @ts-ignore
|
|
387
|
+
Janus.debug(stream)
|
|
388
|
+
// @ts-ignore
|
|
389
|
+
Janus.attachMediaStream(localStream.current, stream)
|
|
390
|
+
/* IS VIDEO ENABLED ? */
|
|
391
|
+
var videoTracks = stream.getVideoTracks()
|
|
392
|
+
/* */
|
|
393
|
+
},
|
|
394
|
+
onremotestream: function (stream) {
|
|
395
|
+
// @ts-ignore
|
|
396
|
+
Janus.debug(' ::: Got a remote stream :::')
|
|
397
|
+
// @ts-ignore
|
|
398
|
+
Janus.debug(stream)
|
|
399
|
+
// retrieve stream track
|
|
400
|
+
var audioTracks = stream.getAudioTracks()
|
|
401
|
+
var videoTracks = stream.getVideoTracks()
|
|
402
|
+
// @ts-ignore
|
|
403
|
+
// Janus.attachMediaStream(remoteStreamAudio, new MediaStream(audioTracks))
|
|
404
|
+
// @ts-ignore
|
|
405
|
+
// Janus.attachMediaStream(remoteStreamVideo, new MediaStream(videoTracks))
|
|
406
|
+
},
|
|
407
|
+
oncleanup: function () {
|
|
408
|
+
console.log(' ::: janus Got a cleanup notification :::')
|
|
409
|
+
},
|
|
410
|
+
detached: function () {
|
|
411
|
+
console.warn('SIP plugin handle detached from the plugin itself')
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
},
|
|
415
|
+
error: (err) => {
|
|
416
|
+
console.log('error', err)
|
|
417
|
+
},
|
|
418
|
+
})
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
initWebRTC()
|
|
424
|
+
|
|
425
|
+
return () => {
|
|
426
|
+
if (sipcall) {
|
|
427
|
+
sipcall.send({
|
|
428
|
+
message: {
|
|
429
|
+
request: 'unregister',
|
|
430
|
+
},
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}, [])
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<>
|
|
438
|
+
{calling && (
|
|
439
|
+
<>
|
|
440
|
+
<div className='bg-black px-10 py-8 rounded-3xl flex flex-col gap-5 text-white w-fit absolute bottom-6 left-20 font-sans'>
|
|
441
|
+
<div className='flex items-center'>
|
|
442
|
+
<span>{currentCall.displayName ? currentCall.displayName : '-'}</span>
|
|
443
|
+
{accepted && <span className='ml-5 w-3 h-3 bg-red-600 rounded-full'></span>}
|
|
444
|
+
</div>
|
|
445
|
+
<div className='flex gap-3'>
|
|
446
|
+
<button
|
|
447
|
+
onClick={answer}
|
|
448
|
+
className='flex content-center items-center justify-center font-medium tracking-wide transition-colors duration-200 transform focus:outline-none focus:ring-2 focus:z-20 focus:ring-offset-2 disabled:opacity-75 bg-green-600 text-white border border-transparent hover:bg-green-700 focus:ring-green-500 focus:ring-offset-black rounded-md px-3 py-2 text-sm leading-4'
|
|
449
|
+
>
|
|
450
|
+
Answer
|
|
451
|
+
</button>
|
|
452
|
+
<button
|
|
453
|
+
onClick={accepted ? hangup : decline}
|
|
454
|
+
className='flex content-center items-center justify-center font-medium tracking-wide transition-colors duration-200 transform focus:outline-none focus:ring-2 focus:z-20 focus:ring-offset-2 disabled:opacity-75 bg-red-600 text-white border border-transparent hover:bg-red-700 focus:ring-red-500 focus:ring-offset-black rounded-md px-3 py-2 text-sm leading-4'
|
|
455
|
+
>
|
|
456
|
+
Decline
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</>
|
|
461
|
+
)}
|
|
462
|
+
<video className='hidden' ref={localStream} muted autoPlay></video>
|
|
463
|
+
</>
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
PhoneIsland.displayName = 'PhoneIsland'
|
package/src/index.css
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
|
|
6
|
+
// Find all widget divs
|
|
7
|
+
const widgetDivs = document.querySelectorAll('.phone-island')
|
|
8
|
+
|
|
9
|
+
// Inject our React App into each element
|
|
10
|
+
widgetDivs.forEach((div) => {
|
|
11
|
+
const config: string = div.getAttribute('data-config') || ''
|
|
12
|
+
|
|
13
|
+
console.log("CONFIG")
|
|
14
|
+
console.log(config)
|
|
15
|
+
|
|
16
|
+
ReactDOM.render(
|
|
17
|
+
<React.StrictMode>
|
|
18
|
+
<App dataConfig={config} />
|
|
19
|
+
</React.StrictMode>,
|
|
20
|
+
div,
|
|
21
|
+
)
|
|
22
|
+
})
|