@origonai/web-chat-sdk 0.1.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/README.md +423 -0
- package/dist/origon-chat-sdk.js +584 -0
- package/dist/origon-chat-sdk.js.map +1 -0
- package/package.json +51 -0
- package/src/call.js +622 -0
- package/src/chat.js +376 -0
- package/src/constants.js +10 -0
- package/src/http.js +122 -0
- package/src/index.js +58 -0
- package/src/utils.js +84 -0
package/src/call.js
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket Service for Call SDK
|
|
3
|
+
* Handles WebRTC call functionality without depending on external state
|
|
4
|
+
* Uses callbacks to communicate state changes to the consumer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getCallServerEndpoint } from './utils.js'
|
|
8
|
+
import { getCredentials, getExternalId, updateSessionId } from './chat.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} CallCallbacks
|
|
12
|
+
* @property {(status: string) => void} [onCallStatus] - Called when call status changes
|
|
13
|
+
* @property {(error: string | null) => void} [onCallError] - Called when call error occurs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} CallSession
|
|
18
|
+
* @property {string} [sessionId]
|
|
19
|
+
* @property {WebSocket} [socket]
|
|
20
|
+
* @property {RTCPeerConnection} [peerConnection]
|
|
21
|
+
* @property {MediaStream} [localStream]
|
|
22
|
+
* @property {MediaStream} [remoteStream]
|
|
23
|
+
* @property {HTMLAudioElement} [remoteAudio]
|
|
24
|
+
* @property {boolean} isMuted
|
|
25
|
+
* @property {string} callStatus
|
|
26
|
+
* @property {NodeJS.Timeout} [pingInterval]
|
|
27
|
+
* @property {number} pingCount
|
|
28
|
+
* @property {number | null} lastPongTime
|
|
29
|
+
* @property {CallCallbacks} callbacks
|
|
30
|
+
* @property {string[]} localIceCandidates - Queued local ICE candidates to send after remote description is set
|
|
31
|
+
* @property {string[]} pendingRemoteIceCandidates - Queued remote ICE candidates to add after remote description is set
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new call session
|
|
36
|
+
* @param {CallCallbacks} [callbacks={}]
|
|
37
|
+
* @returns {CallSession}
|
|
38
|
+
*/
|
|
39
|
+
function createSession(callbacks = {}) {
|
|
40
|
+
return {
|
|
41
|
+
sessionId: undefined,
|
|
42
|
+
socket: null,
|
|
43
|
+
peerConnection: null,
|
|
44
|
+
localStream: null,
|
|
45
|
+
remoteStream: null,
|
|
46
|
+
remoteAudio: null,
|
|
47
|
+
isMuted: false,
|
|
48
|
+
callStatus: 'disconnected',
|
|
49
|
+
pingInterval: null,
|
|
50
|
+
pingCount: 0,
|
|
51
|
+
lastPongTime: null,
|
|
52
|
+
callbacks,
|
|
53
|
+
localIceCandidates: [],
|
|
54
|
+
pendingRemoteIceCandidates: []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @type {CallSession} */
|
|
59
|
+
let currentSession = createSession()
|
|
60
|
+
|
|
61
|
+
const rtcConfig = {
|
|
62
|
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set callbacks for the current session
|
|
67
|
+
* @param {CallCallbacks} callbacks
|
|
68
|
+
*/
|
|
69
|
+
export function setCallCallbacks(callbacks) {
|
|
70
|
+
currentSession.callbacks = { ...currentSession.callbacks, ...callbacks }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clean up the current session
|
|
75
|
+
*/
|
|
76
|
+
function cleanup() {
|
|
77
|
+
if (currentSession.peerConnection) {
|
|
78
|
+
currentSession.peerConnection.close()
|
|
79
|
+
currentSession.peerConnection = null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentSession.localStream) {
|
|
83
|
+
currentSession.localStream.getTracks().forEach((track) => track.stop())
|
|
84
|
+
currentSession.localStream = null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (currentSession.remoteStream) {
|
|
88
|
+
currentSession.remoteStream = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (currentSession.remoteAudio) {
|
|
92
|
+
currentSession.remoteAudio.srcObject = null
|
|
93
|
+
if (currentSession.remoteAudio.parentNode) {
|
|
94
|
+
currentSession.remoteAudio.parentNode.removeChild(currentSession.remoteAudio)
|
|
95
|
+
}
|
|
96
|
+
currentSession.remoteAudio = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentSession.socket) {
|
|
100
|
+
currentSession.socket.close()
|
|
101
|
+
currentSession.socket = null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stopPingInterval()
|
|
105
|
+
|
|
106
|
+
const callbacks = currentSession.callbacks
|
|
107
|
+
currentSession = createSession(callbacks)
|
|
108
|
+
|
|
109
|
+
console.log('Call session cleaned up')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update call status and notify callback
|
|
114
|
+
* @param {string} status
|
|
115
|
+
*/
|
|
116
|
+
function setCallStatus(status) {
|
|
117
|
+
currentSession.callStatus = status
|
|
118
|
+
currentSession.callbacks.onCallStatus?.(status)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Update call error and notify callback
|
|
123
|
+
* @param {string | null} error
|
|
124
|
+
*/
|
|
125
|
+
function setCallError(error) {
|
|
126
|
+
currentSession.callbacks.onCallError?.(error)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stop ping interval
|
|
131
|
+
*/
|
|
132
|
+
function stopPingInterval() {
|
|
133
|
+
if (currentSession.pingInterval) {
|
|
134
|
+
clearInterval(currentSession.pingInterval)
|
|
135
|
+
currentSession.pingInterval = null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Start ping interval
|
|
141
|
+
*/
|
|
142
|
+
function startPingInterval() {
|
|
143
|
+
stopPingInterval()
|
|
144
|
+
|
|
145
|
+
currentSession.pingInterval = setInterval(() => {
|
|
146
|
+
if (currentSession.socket && currentSession.socket.readyState === WebSocket.OPEN) {
|
|
147
|
+
currentSession.pingCount++
|
|
148
|
+
const pingMessage = {
|
|
149
|
+
type: 'ping',
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
count: currentSession.pingCount
|
|
152
|
+
}
|
|
153
|
+
sendEvent(pingMessage)
|
|
154
|
+
console.log(`Sending keep-alive ping #${currentSession.pingCount}`)
|
|
155
|
+
} else {
|
|
156
|
+
console.log('Socket not open, stopping ping interval')
|
|
157
|
+
stopPingInterval()
|
|
158
|
+
}
|
|
159
|
+
}, 10000)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle pong response
|
|
164
|
+
*/
|
|
165
|
+
function handlePong() {
|
|
166
|
+
currentSession.lastPongTime = Date.now()
|
|
167
|
+
console.log(`Received pong #${currentSession.pingCount}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Send event through socket
|
|
172
|
+
* @param {Object} payload
|
|
173
|
+
*/
|
|
174
|
+
function sendEvent(payload) {
|
|
175
|
+
if (!currentSession.socket) {
|
|
176
|
+
console.error('Failed to send event: no socket instance')
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
if (currentSession.socket.readyState !== WebSocket.OPEN) {
|
|
180
|
+
console.error('Failed to send event: socket state not open ', payload)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
currentSession.socket.send(JSON.stringify(payload))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get user media
|
|
189
|
+
*/
|
|
190
|
+
async function getUserMedia() {
|
|
191
|
+
try {
|
|
192
|
+
currentSession.localStream = await navigator.mediaDevices.getUserMedia({
|
|
193
|
+
audio: true,
|
|
194
|
+
video: false
|
|
195
|
+
})
|
|
196
|
+
console.log('Got audio media')
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`Failed to get audio media: ${error.message}`)
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Create peer connection
|
|
205
|
+
*/
|
|
206
|
+
function createPeerConnection() {
|
|
207
|
+
currentSession.peerConnection = new RTCPeerConnection(rtcConfig)
|
|
208
|
+
|
|
209
|
+
currentSession.peerConnection.onicecandidate = (event) => {
|
|
210
|
+
if (event.candidate) {
|
|
211
|
+
const candidateJson = JSON.stringify(event.candidate)
|
|
212
|
+
// Queue local ICE candidates until remote description is set
|
|
213
|
+
if (currentSession.peerConnection && currentSession.peerConnection.remoteDescription) {
|
|
214
|
+
sendEvent({
|
|
215
|
+
type: 'ice',
|
|
216
|
+
data: {
|
|
217
|
+
candidate: candidateJson
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
console.log('Sent ICE candidate immediately')
|
|
221
|
+
} else {
|
|
222
|
+
currentSession.localIceCandidates.push(candidateJson)
|
|
223
|
+
console.log('Queued local ICE candidate')
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
currentSession.peerConnection.ontrack = (event) => {
|
|
229
|
+
console.log('Received remote audio stream')
|
|
230
|
+
currentSession.remoteStream = event.streams[0]
|
|
231
|
+
|
|
232
|
+
if (!currentSession.remoteAudio) {
|
|
233
|
+
currentSession.remoteAudio = document.createElement('audio')
|
|
234
|
+
currentSession.remoteAudio.autoplay = true
|
|
235
|
+
currentSession.remoteAudio.controls = false
|
|
236
|
+
document.body.appendChild(currentSession.remoteAudio)
|
|
237
|
+
}
|
|
238
|
+
currentSession.remoteAudio.srcObject = currentSession.remoteStream
|
|
239
|
+
// explicitly kick off playback and catch any policy/gesture errors
|
|
240
|
+
currentSession.remoteAudio
|
|
241
|
+
.play()
|
|
242
|
+
.then(() => console.log('🔊 remote audio playing'))
|
|
243
|
+
.catch((err) => console.error('❌ playback error:', err))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
currentSession.peerConnection.onconnectionstatechange = () => {
|
|
247
|
+
const newState = currentSession.peerConnection.connectionState
|
|
248
|
+
console.log(`Connection state: ${newState}`)
|
|
249
|
+
|
|
250
|
+
if (newState === 'connected') {
|
|
251
|
+
setCallStatus('connected')
|
|
252
|
+
} else if (newState === 'disconnected' || newState === 'closed') {
|
|
253
|
+
setCallStatus('disconnected')
|
|
254
|
+
disconnectCall()
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
currentSession.peerConnection.oniceconnectionstatechange = () => {
|
|
259
|
+
console.log(`ICE connection state: ${currentSession.peerConnection.iceConnectionState}`)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Connect socket
|
|
265
|
+
* @param {{ sessionId?: string }} payload
|
|
266
|
+
*/
|
|
267
|
+
function connectSocket(payload) {
|
|
268
|
+
return new Promise((fulfill, reject) => {
|
|
269
|
+
if (
|
|
270
|
+
currentSession.socket &&
|
|
271
|
+
(currentSession.socket.readyState === WebSocket.CONNECTING ||
|
|
272
|
+
currentSession.socket.readyState === WebSocket.OPEN)
|
|
273
|
+
) {
|
|
274
|
+
console.log('Socket in connecting/open state, returning.')
|
|
275
|
+
fulfill(currentSession.socket.readyState === WebSocket.OPEN)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log('Initializing socket connection..')
|
|
280
|
+
const credentials = getCredentials()
|
|
281
|
+
if (!credentials || !credentials.endpoint) {
|
|
282
|
+
reject(new Error('SDK not initialized. Please initialize SDK first.'))
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Extract hostname from endpoint
|
|
287
|
+
const socketEndpoint = getCallServerEndpoint(credentials.endpoint)
|
|
288
|
+
if (!socketEndpoint) {
|
|
289
|
+
reject(
|
|
290
|
+
new Error(
|
|
291
|
+
'Invalid endpoint while initializing SDK. Please check the endpoint and try again.'
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const externalId = getExternalId()
|
|
298
|
+
const queryParams = new URLSearchParams({
|
|
299
|
+
externalId
|
|
300
|
+
})
|
|
301
|
+
if (payload.sessionId) {
|
|
302
|
+
queryParams.set('sessionId', payload.sessionId)
|
|
303
|
+
}
|
|
304
|
+
if (credentials.token) {
|
|
305
|
+
queryParams.set('token', credentials.token)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const socketUrl = `${socketEndpoint}?${queryParams.toString()}`
|
|
309
|
+
currentSession.socket = new WebSocket(socketUrl)
|
|
310
|
+
|
|
311
|
+
currentSession.socket.onopen = (event) => {
|
|
312
|
+
console.log('Socket connection established: ', event)
|
|
313
|
+
startPingInterval()
|
|
314
|
+
fulfill(true)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
currentSession.socket.onmessage = (event) => {
|
|
318
|
+
const data = JSON.parse(event.data)
|
|
319
|
+
handleCallServerEvent(data)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
currentSession.socket.onerror = (error) => {
|
|
323
|
+
console.error('Socket error: ', error)
|
|
324
|
+
setCallStatus('error')
|
|
325
|
+
setCallError(error.message || 'Unable to connect voice')
|
|
326
|
+
reject(error)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
currentSession.socket.onclose = (event) => {
|
|
330
|
+
console.log('Socket connection closed: ', event)
|
|
331
|
+
stopPingInterval()
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Handle call server event
|
|
338
|
+
* @param {Object} action
|
|
339
|
+
*/
|
|
340
|
+
function handleCallServerEvent(action) {
|
|
341
|
+
console.log('Handling socket server event: ', action)
|
|
342
|
+
|
|
343
|
+
switch (action.type) {
|
|
344
|
+
case 'pong':
|
|
345
|
+
handlePong()
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
case 'answer':
|
|
349
|
+
handleAnswer(action.data)
|
|
350
|
+
break
|
|
351
|
+
|
|
352
|
+
case 'ice':
|
|
353
|
+
handleIceCandidate(action.data)
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
case 'renegotiationOffer':
|
|
357
|
+
handleRenegotiationOffer(action.data)
|
|
358
|
+
break
|
|
359
|
+
case 'end':
|
|
360
|
+
disconnectCall()
|
|
361
|
+
break
|
|
362
|
+
case 'error':
|
|
363
|
+
setCallStatus('error')
|
|
364
|
+
setCallError(action.error || 'Unable to connect voice')
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
default:
|
|
368
|
+
console.log('Unknown call event type: ', action.type)
|
|
369
|
+
break
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Handle answer
|
|
375
|
+
* @param {Object} data
|
|
376
|
+
*/
|
|
377
|
+
async function handleAnswer(data) {
|
|
378
|
+
try {
|
|
379
|
+
console.log('Received answer')
|
|
380
|
+
|
|
381
|
+
currentSession.sessionId = data.sessionId
|
|
382
|
+
// Update chat session with the new sessionId and notify controller
|
|
383
|
+
updateSessionId(data.sessionId)
|
|
384
|
+
|
|
385
|
+
if (currentSession.peerConnection) {
|
|
386
|
+
const answer = new RTCSessionDescription({
|
|
387
|
+
type: 'answer',
|
|
388
|
+
sdp: data.sdp
|
|
389
|
+
})
|
|
390
|
+
console.log('Setting remote description answer: ', answer)
|
|
391
|
+
await currentSession.peerConnection.setRemoteDescription(answer)
|
|
392
|
+
console.log('Remote description set')
|
|
393
|
+
|
|
394
|
+
// Send all queued local ICE candidates
|
|
395
|
+
for (const candidateJson of currentSession.localIceCandidates) {
|
|
396
|
+
sendEvent({
|
|
397
|
+
type: 'ice',
|
|
398
|
+
data: {
|
|
399
|
+
candidate: candidateJson
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
console.log('Sent queued local ICE candidate')
|
|
403
|
+
}
|
|
404
|
+
currentSession.localIceCandidates = []
|
|
405
|
+
|
|
406
|
+
// Process any pending remote ICE candidates
|
|
407
|
+
for (const candidateJson of currentSession.pendingRemoteIceCandidates) {
|
|
408
|
+
try {
|
|
409
|
+
const candidate = new RTCIceCandidate(JSON.parse(candidateJson))
|
|
410
|
+
await currentSession.peerConnection.addIceCandidate(candidate)
|
|
411
|
+
console.log('Added pending remote ICE candidate')
|
|
412
|
+
} catch (err) {
|
|
413
|
+
console.error(`Failed to add pending ICE candidate: ${err.message}`)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
currentSession.pendingRemoteIceCandidates = []
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error(`Failed to handle answer: ${error.message}`)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Handle ICE candidate
|
|
425
|
+
* @param {Object} data
|
|
426
|
+
*/
|
|
427
|
+
async function handleIceCandidate(data) {
|
|
428
|
+
try {
|
|
429
|
+
if (currentSession.peerConnection) {
|
|
430
|
+
// Check if remote description is set
|
|
431
|
+
if (!currentSession.peerConnection.remoteDescription) {
|
|
432
|
+
// Queue the candidate until remote description is set
|
|
433
|
+
currentSession.pendingRemoteIceCandidates.push(data.candidate)
|
|
434
|
+
console.log('Queued remote ICE candidate - remote description not set')
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
const candidate = new RTCIceCandidate(JSON.parse(data.candidate))
|
|
438
|
+
await currentSession.peerConnection.addIceCandidate(candidate)
|
|
439
|
+
console.log('Added ICE candidate')
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error(`Failed to add ICE candidate: ${error.message}`)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Handle renegotiation offer
|
|
448
|
+
* @param {Object} data
|
|
449
|
+
*/
|
|
450
|
+
async function handleRenegotiationOffer(data) {
|
|
451
|
+
try {
|
|
452
|
+
console.log('Received renegotiation offer')
|
|
453
|
+
|
|
454
|
+
if (currentSession.peerConnection) {
|
|
455
|
+
const offer = new RTCSessionDescription({
|
|
456
|
+
type: 'offer',
|
|
457
|
+
sdp: data.sdp
|
|
458
|
+
})
|
|
459
|
+
console.log('Setting remote description offer: ', offer)
|
|
460
|
+
await currentSession.peerConnection.setRemoteDescription(offer)
|
|
461
|
+
console.log('Remote description set')
|
|
462
|
+
|
|
463
|
+
const answer = await currentSession.peerConnection.createAnswer()
|
|
464
|
+
await currentSession.peerConnection.setLocalDescription(answer)
|
|
465
|
+
|
|
466
|
+
sendEvent({
|
|
467
|
+
type: 'renegotiationAnswer',
|
|
468
|
+
data: {
|
|
469
|
+
sdp: answer.sdp
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(`Failed to handle renegotiation offer: ${error.message}`)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Start a call
|
|
480
|
+
* @param {{ sessionId?: string }} payload
|
|
481
|
+
*/
|
|
482
|
+
export async function startCall(payload = {}) {
|
|
483
|
+
try {
|
|
484
|
+
if (currentSession.callStatus === 'connecting' || currentSession.callStatus === 'connected') {
|
|
485
|
+
console.log(`Call already in ${currentSession.callStatus} state`)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
console.log('Starting audio call...')
|
|
490
|
+
setCallStatus('connecting')
|
|
491
|
+
setCallError(null)
|
|
492
|
+
|
|
493
|
+
currentSession.sessionId = payload.sessionId
|
|
494
|
+
|
|
495
|
+
await getUserMedia()
|
|
496
|
+
|
|
497
|
+
createPeerConnection()
|
|
498
|
+
|
|
499
|
+
currentSession.localStream.getTracks().forEach((track) => {
|
|
500
|
+
currentSession.peerConnection.addTrack(track, currentSession.localStream)
|
|
501
|
+
console.log(`Added ${track.kind} track`)
|
|
502
|
+
})
|
|
503
|
+
await connectSocket(payload)
|
|
504
|
+
const offer = await currentSession.peerConnection.createOffer()
|
|
505
|
+
await currentSession.peerConnection.setLocalDescription(offer)
|
|
506
|
+
|
|
507
|
+
sendEvent({
|
|
508
|
+
type: 'offer',
|
|
509
|
+
data: {
|
|
510
|
+
sdp: offer.sdp
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
console.log('Call initiated successfully')
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.log('error: ', error)
|
|
517
|
+
console.error(`Failed to start call: ${error.message}`)
|
|
518
|
+
setCallStatus('error')
|
|
519
|
+
setCallError(error.message || 'Unable to connect voice')
|
|
520
|
+
cleanup()
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Disconnect call
|
|
526
|
+
*/
|
|
527
|
+
export function disconnectCall() {
|
|
528
|
+
sendEvent({
|
|
529
|
+
type: 'end'
|
|
530
|
+
})
|
|
531
|
+
if (currentSession.socket) {
|
|
532
|
+
currentSession.socket.close()
|
|
533
|
+
currentSession.socket = null
|
|
534
|
+
}
|
|
535
|
+
setCallStatus('disconnected')
|
|
536
|
+
if (currentSession.peerConnection) {
|
|
537
|
+
currentSession.peerConnection.close()
|
|
538
|
+
currentSession.peerConnection = null
|
|
539
|
+
}
|
|
540
|
+
if (currentSession.localStream) {
|
|
541
|
+
currentSession.localStream.getTracks().forEach((track) => track.stop())
|
|
542
|
+
currentSession.localStream = null
|
|
543
|
+
}
|
|
544
|
+
cleanup()
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Toggle mute
|
|
549
|
+
* @returns {boolean}
|
|
550
|
+
*/
|
|
551
|
+
export function toggleMute() {
|
|
552
|
+
if (currentSession.localStream) {
|
|
553
|
+
const audioTrack = currentSession.localStream.getAudioTracks()[0]
|
|
554
|
+
if (audioTrack) {
|
|
555
|
+
audioTrack.enabled = !audioTrack.enabled
|
|
556
|
+
currentSession.isMuted = !audioTrack.enabled
|
|
557
|
+
console.log(`Audio ${currentSession.isMuted ? 'muted' : 'unmuted'}`)
|
|
558
|
+
return currentSession.isMuted
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return false
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get local stream
|
|
566
|
+
* @returns {MediaStream | null}
|
|
567
|
+
*/
|
|
568
|
+
export function getLocalStream() {
|
|
569
|
+
return currentSession.localStream
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Get inbound audio energy
|
|
574
|
+
* @returns {Promise<number>}
|
|
575
|
+
*/
|
|
576
|
+
export function getInboundAudioEnergy() {
|
|
577
|
+
return new Promise((resolve, reject) => {
|
|
578
|
+
if (!currentSession.peerConnection) {
|
|
579
|
+
reject(new Error('no peer connection'))
|
|
580
|
+
return
|
|
581
|
+
}
|
|
582
|
+
currentSession.peerConnection
|
|
583
|
+
.getStats()
|
|
584
|
+
.then((stats) => {
|
|
585
|
+
stats.forEach((report) => {
|
|
586
|
+
if (report.type == 'inbound-rtp') {
|
|
587
|
+
resolve(report.totalAudioEnergy)
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
reject(new Error('no inbound-rtp stats found'))
|
|
591
|
+
})
|
|
592
|
+
.catch((err) => {
|
|
593
|
+
reject(err)
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Get outbound audio energy (not implemented in original, but may be needed)
|
|
600
|
+
* @returns {Promise<number>}
|
|
601
|
+
*/
|
|
602
|
+
export function getOutboundAudioEnergy() {
|
|
603
|
+
return new Promise((resolve, reject) => {
|
|
604
|
+
if (!currentSession.peerConnection) {
|
|
605
|
+
reject(new Error('no peer connection'))
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
currentSession.peerConnection
|
|
609
|
+
.getStats()
|
|
610
|
+
.then((stats) => {
|
|
611
|
+
stats.forEach((report) => {
|
|
612
|
+
if (report.type == 'outbound-rtp') {
|
|
613
|
+
resolve(report.totalAudioEnergy)
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
reject(new Error('no outbound-rtp stats found'))
|
|
617
|
+
})
|
|
618
|
+
.catch((err) => {
|
|
619
|
+
reject(err)
|
|
620
|
+
})
|
|
621
|
+
})
|
|
622
|
+
}
|