@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/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
@@ -0,0 +1,9 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ button {
7
+ @apply cursor-pointer
8
+ }
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export * from './App'
@@ -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
+ })