@afosecure/meetingsdk 1.4.3 → 1.4.5
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/dist/index.d.mts +22 -25
- package/dist/index.d.ts +22 -25
- package/dist/index.js +395 -613
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +396 -614
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -164,10 +164,10 @@ var VideoSDKCore = class {
|
|
|
164
164
|
this.events = events;
|
|
165
165
|
this.url = url;
|
|
166
166
|
this.ws = null;
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
169
|
-
this.lastPong = Date.now();
|
|
167
|
+
this.pubPC = null;
|
|
168
|
+
this.subPC = null;
|
|
170
169
|
this.iceServers = [];
|
|
170
|
+
this.lastPong = Date.now();
|
|
171
171
|
this.intentionalDisconnect = false;
|
|
172
172
|
this.room = {
|
|
173
173
|
id: null,
|
|
@@ -175,14 +175,11 @@ var VideoSDKCore = class {
|
|
|
175
175
|
};
|
|
176
176
|
this.localStream = null;
|
|
177
177
|
this.screenStream = null;
|
|
178
|
+
this.screenSender = null;
|
|
178
179
|
this.isScreenSharing = false;
|
|
179
|
-
this.screenSenders = {};
|
|
180
180
|
this.pingInterval = null;
|
|
181
|
-
this.pendingIceCandidates = {};
|
|
182
|
-
this.pendingOffers = {};
|
|
183
181
|
this.reconnectAttempts = 0;
|
|
184
182
|
this.participantName = "";
|
|
185
|
-
// Track if we're in the waiting room (pending approval)
|
|
186
183
|
this.isWaitingForApproval = false;
|
|
187
184
|
this.pendingRequestId = null;
|
|
188
185
|
this.iceTransportPolicy = "all";
|
|
@@ -192,29 +189,12 @@ var VideoSDKCore = class {
|
|
|
192
189
|
this.myId = localStorage.getItem("vsdk_id") || crypto.randomUUID();
|
|
193
190
|
localStorage.setItem("vsdk_id", this.myId);
|
|
194
191
|
}
|
|
195
|
-
|
|
196
|
-
const err = {
|
|
197
|
-
code,
|
|
198
|
-
message,
|
|
199
|
-
raw,
|
|
200
|
-
roomId: this.room.id,
|
|
201
|
-
userId: this.myId,
|
|
202
|
-
recoverable
|
|
203
|
-
};
|
|
204
|
-
this.events.onError?.(err);
|
|
205
|
-
this.joinRejecter?.(err);
|
|
206
|
-
this.joinRejecter = void 0;
|
|
207
|
-
console.error("[MeetingSDK Error]", err);
|
|
208
|
-
}
|
|
209
|
-
// ---------------- STREAM ----------------
|
|
192
|
+
// ---------------- MEDIA SETUP ----------------
|
|
210
193
|
async initLocal(video, name) {
|
|
211
194
|
this.participantName = name;
|
|
212
195
|
try {
|
|
213
196
|
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
214
|
-
video: {
|
|
215
|
-
width: { ideal: 1280 },
|
|
216
|
-
height: { ideal: 720 }
|
|
217
|
-
},
|
|
197
|
+
video: { width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
218
198
|
audio: {
|
|
219
199
|
echoCancellation: true,
|
|
220
200
|
noiseSuppression: true,
|
|
@@ -243,7 +223,115 @@ var VideoSDKCore = class {
|
|
|
243
223
|
throw err;
|
|
244
224
|
}
|
|
245
225
|
}
|
|
246
|
-
|
|
226
|
+
async joinMeeting(config) {
|
|
227
|
+
const { roomId, name, audioMuted = false, videoMuted = false } = config;
|
|
228
|
+
if (!roomId || !name) {
|
|
229
|
+
throw new Error("roomId and name are required to join meeting");
|
|
230
|
+
}
|
|
231
|
+
this.participantName = name;
|
|
232
|
+
if (!this.localStream) {
|
|
233
|
+
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
234
|
+
video: true,
|
|
235
|
+
audio: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
this.localStream.getAudioTracks().forEach((t) => t.enabled = !audioMuted);
|
|
239
|
+
this.localStream.getVideoTracks().forEach((t) => t.enabled = !videoMuted);
|
|
240
|
+
this.state.updateLocalParticipant({
|
|
241
|
+
id: this.myId,
|
|
242
|
+
name: this.participantName,
|
|
243
|
+
media: {
|
|
244
|
+
stream: this.localStream,
|
|
245
|
+
micEnabled: !audioMuted,
|
|
246
|
+
camEnabled: !videoMuted,
|
|
247
|
+
isScreenSharing: false
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
this.state.localStream = this.localStream;
|
|
251
|
+
await this.connect(roomId, name);
|
|
252
|
+
}
|
|
253
|
+
// ---------------- SFU PEER CONNECTION CREATION ----------------
|
|
254
|
+
setupPublisherPC() {
|
|
255
|
+
if (!this.localStream) return;
|
|
256
|
+
this.pubPC = new RTCPeerConnection({
|
|
257
|
+
iceServers: this.iceServers,
|
|
258
|
+
iceTransportPolicy: this.iceTransportPolicy
|
|
259
|
+
});
|
|
260
|
+
this.localStream.getTracks().forEach((track) => {
|
|
261
|
+
this.pubPC?.addTrack(track, this.localStream);
|
|
262
|
+
});
|
|
263
|
+
this.pubPC.onicecandidate = (e) => {
|
|
264
|
+
if (e.candidate) {
|
|
265
|
+
this.send({
|
|
266
|
+
type: "PUB_ICE",
|
|
267
|
+
payload: JSON.stringify(e.candidate),
|
|
268
|
+
user_id: this.myId
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
this.pubPC.onconnectionstatechange = () => {
|
|
273
|
+
console.log(`[SFU Publisher PC State]`, this.pubPC?.connectionState);
|
|
274
|
+
if (this.pubPC?.connectionState === "failed") {
|
|
275
|
+
this.restartPublisherIce();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
setupSubscriberPC() {
|
|
280
|
+
this.subPC = new RTCPeerConnection({
|
|
281
|
+
iceServers: this.iceServers,
|
|
282
|
+
iceTransportPolicy: this.iceTransportPolicy
|
|
283
|
+
});
|
|
284
|
+
this.subPC.onicecandidate = (e) => {
|
|
285
|
+
if (e.candidate) {
|
|
286
|
+
this.send({
|
|
287
|
+
type: "SUB_ICE",
|
|
288
|
+
payload: JSON.stringify(e.candidate),
|
|
289
|
+
user_id: this.myId
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
this.subPC.ontrack = (event) => {
|
|
294
|
+
const incomingStream = event.streams[0] || new MediaStream([event.track]);
|
|
295
|
+
const streamId = incomingStream.id.replace(/[{}]/g, "");
|
|
296
|
+
let matchedParticipant;
|
|
297
|
+
for (const p of this.state.participants.values()) {
|
|
298
|
+
if (p.media?.cameraStreamId === streamId || p.media?.remoteScreenStreamId === streamId) {
|
|
299
|
+
matchedParticipant = p;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!matchedParticipant) {
|
|
304
|
+
console.warn(
|
|
305
|
+
`[SFU ontrack] Dynamic track received for stream ${streamId}`
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const pId = matchedParticipant.id;
|
|
310
|
+
const isScreen = streamId === matchedParticipant.media?.remoteScreenStreamId;
|
|
311
|
+
if (isScreen) {
|
|
312
|
+
this.state.updateParticipantMedia(pId, {
|
|
313
|
+
screenStream: incomingStream,
|
|
314
|
+
screenTrack: event.track,
|
|
315
|
+
isScreenSharing: true
|
|
316
|
+
});
|
|
317
|
+
if (!this.state.presenterId) {
|
|
318
|
+
this.state.setPresenterId(pId);
|
|
319
|
+
}
|
|
320
|
+
this.events.onScreenShareStarted?.(pId, incomingStream);
|
|
321
|
+
} else {
|
|
322
|
+
this.state.updateParticipantMedia(pId, {
|
|
323
|
+
stream: incomingStream,
|
|
324
|
+
cameraTrack: incomingStream.getVideoTracks()[0],
|
|
325
|
+
audioTrack: incomingStream.getAudioTracks()[0]
|
|
326
|
+
});
|
|
327
|
+
this.events.onTrack?.(incomingStream, pId);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
this.subPC.onconnectionstatechange = () => {
|
|
331
|
+
console.log(`[SFU Subscriber PC State]`, this.subPC?.connectionState);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// ---------------- WEBSOCKET CONNECTION & SIGNALING ----------------
|
|
247
335
|
async connect(roomId, name) {
|
|
248
336
|
this.room.id = roomId;
|
|
249
337
|
this.reset();
|
|
@@ -252,15 +340,17 @@ var VideoSDKCore = class {
|
|
|
252
340
|
this.joinRejecter = reject;
|
|
253
341
|
this.ws = new WebSocket(this.url);
|
|
254
342
|
this.ws.onopen = () => {
|
|
255
|
-
console.log("WebSocket connected, sending JOIN...");
|
|
343
|
+
console.log("WebSocket connected to SFU, sending JOIN...");
|
|
344
|
+
const micEnabled = !!this.state.localParticipant?.media?.micEnabled;
|
|
345
|
+
const camEnabled = !!this.state.localParticipant?.media?.camEnabled;
|
|
256
346
|
this.send({
|
|
257
347
|
type: "JOIN",
|
|
258
348
|
room_id: roomId,
|
|
259
349
|
user_id: this.myId,
|
|
260
350
|
sender_name: name,
|
|
261
351
|
camera_stream_id: this.localStream?.id.replace(/[{}]/g, ""),
|
|
262
|
-
audio_muted:
|
|
263
|
-
video_muted:
|
|
352
|
+
audio_muted: !micEnabled,
|
|
353
|
+
video_muted: !camEnabled
|
|
264
354
|
});
|
|
265
355
|
};
|
|
266
356
|
this.ws.onerror = (err) => {
|
|
@@ -273,13 +363,7 @@ var VideoSDKCore = class {
|
|
|
273
363
|
raw: e
|
|
274
364
|
});
|
|
275
365
|
this.joinRejecter = void 0;
|
|
276
|
-
if (this.intentionalDisconnect) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (e.code === 1e3 || e.code === 1001) {
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (this.isWaitingForApproval) {
|
|
366
|
+
if (this.intentionalDisconnect || e.code === 1e3 || e.code === 1001 || this.isWaitingForApproval) {
|
|
283
367
|
return;
|
|
284
368
|
}
|
|
285
369
|
this.scheduleReconnect();
|
|
@@ -289,203 +373,70 @@ var VideoSDKCore = class {
|
|
|
289
373
|
};
|
|
290
374
|
});
|
|
291
375
|
}
|
|
292
|
-
async joinMeeting(config) {
|
|
293
|
-
const { roomId, name, audioMuted = false, videoMuted = false } = config;
|
|
294
|
-
if (!roomId || !name) {
|
|
295
|
-
throw new Error("roomId and name are required to join meeting");
|
|
296
|
-
}
|
|
297
|
-
this.participantName = name;
|
|
298
|
-
if (!this.localStream) {
|
|
299
|
-
this.localStream = await navigator.mediaDevices.getUserMedia({
|
|
300
|
-
video: true,
|
|
301
|
-
audio: true
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
this.localStream.getAudioTracks().forEach((t) => {
|
|
305
|
-
t.enabled = !audioMuted;
|
|
306
|
-
});
|
|
307
|
-
this.localStream.getVideoTracks().forEach((t) => {
|
|
308
|
-
t.enabled = !videoMuted;
|
|
309
|
-
});
|
|
310
|
-
this.state.updateLocalParticipant({
|
|
311
|
-
id: this.myId,
|
|
312
|
-
name: this.participantName,
|
|
313
|
-
media: {
|
|
314
|
-
stream: this.localStream,
|
|
315
|
-
micEnabled: !audioMuted,
|
|
316
|
-
camEnabled: !videoMuted,
|
|
317
|
-
isScreenSharing: false
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
this.state.localStream = this.localStream;
|
|
321
|
-
await this.connect(roomId, name);
|
|
322
|
-
}
|
|
323
|
-
getMeeting() {
|
|
324
|
-
return this.room;
|
|
325
|
-
}
|
|
326
|
-
toggleMic() {
|
|
327
|
-
const mediaState = this.state.localParticipant?.media;
|
|
328
|
-
if (!mediaState) return;
|
|
329
|
-
const nextEnabled = !mediaState.micEnabled;
|
|
330
|
-
this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
|
|
331
|
-
this.state.updateLocalParticipant({
|
|
332
|
-
id: this.myId,
|
|
333
|
-
name: this.participantName,
|
|
334
|
-
media: {
|
|
335
|
-
...mediaState,
|
|
336
|
-
micEnabled: nextEnabled
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
this.send({
|
|
340
|
-
type: "MEDIA_STATE",
|
|
341
|
-
kind: "audio",
|
|
342
|
-
enabled: nextEnabled
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
toggleCam() {
|
|
346
|
-
const mediaState = this.state.localParticipant?.media;
|
|
347
|
-
if (!mediaState) return;
|
|
348
|
-
const nextEnabled = !mediaState.camEnabled;
|
|
349
|
-
this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
|
|
350
|
-
this.state.updateLocalParticipant({
|
|
351
|
-
id: this.myId,
|
|
352
|
-
name: this.participantName,
|
|
353
|
-
media: {
|
|
354
|
-
...mediaState,
|
|
355
|
-
camEnabled: nextEnabled
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
this.send({
|
|
359
|
-
type: "MEDIA_STATE",
|
|
360
|
-
kind: "video",
|
|
361
|
-
enabled: nextEnabled
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
scheduleReconnect() {
|
|
365
|
-
if (!this.room.id) return;
|
|
366
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
367
|
-
clearTimeout(this.reconnectTimer);
|
|
368
|
-
this.reconnectTimer = window.setTimeout(async () => {
|
|
369
|
-
try {
|
|
370
|
-
await this.connect(this.room.id, this.participantName);
|
|
371
|
-
this.reconnectAttempts = 0;
|
|
372
|
-
} catch {
|
|
373
|
-
this.reconnectAttempts++;
|
|
374
|
-
this.scheduleReconnect();
|
|
375
|
-
}
|
|
376
|
-
}, delay);
|
|
377
|
-
}
|
|
378
|
-
startHeartbeat() {
|
|
379
|
-
this.stopHeartbeat();
|
|
380
|
-
this.pingInterval = setInterval(() => {
|
|
381
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
382
|
-
this.send({
|
|
383
|
-
type: "PING",
|
|
384
|
-
client_ts: Date.now()
|
|
385
|
-
});
|
|
386
|
-
}, 2e4);
|
|
387
|
-
}
|
|
388
|
-
stopHeartbeat() {
|
|
389
|
-
if (this.pingInterval) {
|
|
390
|
-
clearInterval(this.pingInterval);
|
|
391
|
-
this.pingInterval = null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// ---------------- RESET ----------------
|
|
395
|
-
reset() {
|
|
396
|
-
Object.values(this.peers).forEach((pc) => pc.close());
|
|
397
|
-
this.peers = {};
|
|
398
|
-
this.initiators.clear();
|
|
399
|
-
this.pendingIceCandidates = {};
|
|
400
|
-
this.state.resetRemoteState();
|
|
401
|
-
}
|
|
402
|
-
async handleJoinApproved(msg) {
|
|
403
|
-
console.log("JOIN_APPROVED received, sending new JOIN...");
|
|
404
|
-
this.events.onEntryResponded?.({
|
|
405
|
-
participantId: msg.user_id,
|
|
406
|
-
decision: "approved"
|
|
407
|
-
});
|
|
408
|
-
this.isWaitingForApproval = false;
|
|
409
|
-
this.pendingRequestId = null;
|
|
410
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
411
|
-
this.send({
|
|
412
|
-
type: "JOIN",
|
|
413
|
-
room_id: this.room.id,
|
|
414
|
-
user_id: this.myId,
|
|
415
|
-
sender_name: this.participantName
|
|
416
|
-
});
|
|
417
|
-
console.log("Sent new JOIN after approval");
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
376
|
async handle(msg) {
|
|
421
|
-
var _a, _b, _c, _d;
|
|
422
377
|
if (msg.sender === this.myId) return;
|
|
423
378
|
switch (msg.type) {
|
|
424
379
|
case "PONG":
|
|
425
380
|
this.lastPong = Date.now();
|
|
426
381
|
break;
|
|
427
|
-
case "
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
});
|
|
431
|
-
await this.handleOffer(msg.payload, msg.sender);
|
|
432
|
-
break;
|
|
433
|
-
case "ANSWER": {
|
|
434
|
-
const pc = this.peers[msg.sender];
|
|
435
|
-
console.log("[Answer] Received from", msg.sender, {
|
|
436
|
-
signalingState: pc?.signalingState,
|
|
437
|
-
iceConnectionState: pc?.iceConnectionState,
|
|
438
|
-
connectionState: pc?.connectionState
|
|
439
|
-
});
|
|
440
|
-
if (!pc) return;
|
|
441
|
-
if (pc.signalingState !== "have-local-offer") {
|
|
442
|
-
console.warn(
|
|
443
|
-
`[Signaling] Unexpected ANSWER in state "${pc.signalingState}", ignoring`
|
|
444
|
-
);
|
|
445
|
-
return;
|
|
382
|
+
case "JOINED": {
|
|
383
|
+
if (msg.iceServers) {
|
|
384
|
+
this.iceServers = msg.iceServers;
|
|
446
385
|
}
|
|
447
|
-
|
|
448
|
-
|
|
386
|
+
this.room.name = msg.room_name;
|
|
387
|
+
this.isWaitingForApproval = false;
|
|
388
|
+
this.pendingRequestId = null;
|
|
389
|
+
this.intentionalDisconnect = false;
|
|
390
|
+
this.reconnectAttempts = 0;
|
|
391
|
+
this.setupPublisherPC();
|
|
392
|
+
this.setupSubscriberPC();
|
|
393
|
+
await this.createPublisherOffer();
|
|
394
|
+
this.startHeartbeat();
|
|
395
|
+
this.joinResolver?.();
|
|
396
|
+
this.joinResolver = void 0;
|
|
397
|
+
this.joinRejecter = void 0;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
case "SFU_PUB_ANSWER": {
|
|
401
|
+
if (this.pubPC) {
|
|
402
|
+
await this.pubPC.setRemoteDescription({
|
|
449
403
|
type: "answer",
|
|
450
404
|
sdp: msg.payload
|
|
451
405
|
});
|
|
452
|
-
await this.flushIce(msg.sender, pc);
|
|
453
|
-
} catch (err) {
|
|
454
|
-
console.error("[Signaling] Failed to apply answer:", err);
|
|
455
|
-
this.emitError(
|
|
456
|
-
"ANSWER_FAILED",
|
|
457
|
-
`Failed to apply answer from ${msg.sender}`,
|
|
458
|
-
err,
|
|
459
|
-
true
|
|
460
|
-
);
|
|
461
406
|
}
|
|
462
407
|
break;
|
|
463
408
|
}
|
|
464
|
-
case "
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
409
|
+
case "SFU_SUB_OFFER": {
|
|
410
|
+
if (this.subPC) {
|
|
411
|
+
await this.subPC.setRemoteDescription({
|
|
412
|
+
type: "offer",
|
|
413
|
+
sdp: msg.payload
|
|
414
|
+
});
|
|
415
|
+
const answer = await this.subPC.createAnswer();
|
|
416
|
+
await this.subPC.setLocalDescription(answer);
|
|
417
|
+
this.send({
|
|
418
|
+
type: "SUB_ANSWER",
|
|
419
|
+
payload: answer.sdp,
|
|
420
|
+
user_id: this.myId
|
|
421
|
+
});
|
|
471
422
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
case "PUB_ICE": {
|
|
426
|
+
if (this.pubPC && msg.payload) {
|
|
427
|
+
await this.pubPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
|
|
476
428
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "SUB_ICE": {
|
|
432
|
+
if (this.subPC && msg.payload) {
|
|
433
|
+
await this.subPC.addIceCandidate(JSON.parse(msg.payload)).catch(console.warn);
|
|
481
434
|
}
|
|
482
435
|
break;
|
|
483
436
|
}
|
|
484
|
-
case "EXISTING_USERS":
|
|
437
|
+
case "EXISTING_USERS": {
|
|
485
438
|
if (msg.presenterId) {
|
|
486
439
|
this.state.setPresenterId(msg.presenterId);
|
|
487
|
-
this.events.onScreenShareStarted?.(msg.presenterId, null);
|
|
488
|
-
this.state.setPresenterId(msg.presenterId);
|
|
489
440
|
}
|
|
490
441
|
for (const p of msg.participants || []) {
|
|
491
442
|
if (!p?.id || p.id === this.myId) continue;
|
|
@@ -497,9 +448,6 @@ var VideoSDKCore = class {
|
|
|
497
448
|
media: {
|
|
498
449
|
stream: null,
|
|
499
450
|
screenStream: void 0,
|
|
500
|
-
cameraTrack: void 0,
|
|
501
|
-
screenTrack: void 0,
|
|
502
|
-
audioTrack: void 0,
|
|
503
451
|
micEnabled: p.micEnabled ?? true,
|
|
504
452
|
camEnabled: p.camEnabled ?? true,
|
|
505
453
|
isScreenSharing: p.isScreenSharing ?? false,
|
|
@@ -509,118 +457,53 @@ var VideoSDKCore = class {
|
|
|
509
457
|
};
|
|
510
458
|
this.state.addParticipant(structuredParticipant);
|
|
511
459
|
this.events.onUserJoined?.(structuredParticipant);
|
|
512
|
-
if (p.isScreenSharing && p.remoteScreenStreamId) {
|
|
513
|
-
this.state.setPresenterId(p.id);
|
|
514
|
-
this.state.updateParticipantMedia(p.id, {
|
|
515
|
-
isScreenSharing: true,
|
|
516
|
-
remoteScreenStreamId: p.remoteScreenStreamId,
|
|
517
|
-
cameraStreamId: p.cameraId || null
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
if (this.shouldInitiate(p.id)) {
|
|
521
|
-
await this.createOffer(p.id);
|
|
522
|
-
}
|
|
523
460
|
}
|
|
524
461
|
break;
|
|
525
|
-
case "JOINED": {
|
|
526
|
-
if (msg.iceServers) {
|
|
527
|
-
this.iceServers = msg.iceServers;
|
|
528
|
-
}
|
|
529
|
-
this.room.name = msg.room_name;
|
|
530
|
-
this.isWaitingForApproval = false;
|
|
531
|
-
this.pendingRequestId = null;
|
|
532
|
-
this.intentionalDisconnect = false;
|
|
533
|
-
this.reconnectAttempts = 0;
|
|
534
|
-
if (Object.keys(this.pendingOffers).length > 0) {
|
|
535
|
-
console.log(
|
|
536
|
-
"Processing",
|
|
537
|
-
Object.keys(this.pendingOffers).length,
|
|
538
|
-
"pending offers"
|
|
539
|
-
);
|
|
540
|
-
for (const [peerId2, sdp] of Object.entries(this.pendingOffers)) {
|
|
541
|
-
await this.handleOffer(sdp, peerId2);
|
|
542
|
-
}
|
|
543
|
-
this.pendingOffers = {};
|
|
544
|
-
}
|
|
545
|
-
const media = this.state.localParticipant?.media;
|
|
546
|
-
if (media) {
|
|
547
|
-
this.send({
|
|
548
|
-
type: "MEDIA_STATE",
|
|
549
|
-
kind: "audio",
|
|
550
|
-
enabled: !!media.micEnabled
|
|
551
|
-
});
|
|
552
|
-
this.send({
|
|
553
|
-
type: "MEDIA_STATE",
|
|
554
|
-
kind: "video",
|
|
555
|
-
enabled: !!media.camEnabled
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
this.startHeartbeat();
|
|
559
|
-
this.joinResolver?.();
|
|
560
|
-
this.joinResolver = void 0;
|
|
561
|
-
this.joinRejecter = void 0;
|
|
562
|
-
break;
|
|
563
462
|
}
|
|
564
463
|
case "USER_JOINED": {
|
|
565
464
|
const p = msg.participant;
|
|
566
465
|
if (!p?.id || p.id === this.myId) return;
|
|
567
466
|
this.state.addParticipant(p);
|
|
568
467
|
this.events.onUserJoined?.(p);
|
|
569
|
-
if (this.shouldInitiate(p.id)) {
|
|
570
|
-
await this.createOffer(p.id);
|
|
571
|
-
}
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
case "JOIN_PENDING": {
|
|
575
|
-
const req = msg.request;
|
|
576
|
-
this.isWaitingForApproval = true;
|
|
577
|
-
this.pendingRequestId = req.request_id;
|
|
578
|
-
this.events.onEntryRequested?.({
|
|
579
|
-
requestId: req.request_id,
|
|
580
|
-
userId: req.user_id,
|
|
581
|
-
name: req.name
|
|
582
|
-
});
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "JOIN_REQUEST": {
|
|
586
|
-
const req = msg.request;
|
|
587
|
-
this.events.onEntryRequested?.({
|
|
588
|
-
requestId: req.id,
|
|
589
|
-
userId: req.user_id,
|
|
590
|
-
name: req.name
|
|
591
|
-
});
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
case "JOIN_APPROVED": {
|
|
595
|
-
await this.handleJoinApproved(msg);
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
598
|
-
case "JOIN_REJECTED": {
|
|
599
|
-
const decision = "rejected";
|
|
600
|
-
this.isWaitingForApproval = false;
|
|
601
|
-
this.pendingRequestId = null;
|
|
602
|
-
this.events.onEntryResponded?.({
|
|
603
|
-
participantId: msg.user_id,
|
|
604
|
-
decision
|
|
605
|
-
});
|
|
606
468
|
break;
|
|
607
469
|
}
|
|
608
|
-
case "USER_LEFT":
|
|
470
|
+
case "USER_LEFT": {
|
|
609
471
|
const peerId = msg.participant.id;
|
|
610
|
-
this.closePeer(peerId);
|
|
611
472
|
this.state.removeParticipant(peerId);
|
|
612
473
|
this.events.onUserLeft?.(peerId);
|
|
613
474
|
break;
|
|
475
|
+
}
|
|
614
476
|
case "MEDIA_STATE_CHANGE": {
|
|
615
|
-
const
|
|
477
|
+
const peerId = msg.peerId;
|
|
616
478
|
const { kind, enabled } = msg;
|
|
617
479
|
if (kind === "audio") {
|
|
618
|
-
this.state.updateParticipantMedia(
|
|
619
|
-
this.events.onMicToggled?.(
|
|
480
|
+
this.state.updateParticipantMedia(peerId, { micEnabled: enabled });
|
|
481
|
+
this.events.onMicToggled?.(peerId, enabled);
|
|
620
482
|
} else if (kind === "video") {
|
|
621
|
-
this.state.updateParticipantMedia(
|
|
622
|
-
this.events.onCamToggled?.(
|
|
483
|
+
this.state.updateParticipantMedia(peerId, { camEnabled: enabled });
|
|
484
|
+
this.events.onCamToggled?.(peerId, enabled);
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
case "SCREEN_SHARE_START": {
|
|
489
|
+
const peerId = msg.peerId;
|
|
490
|
+
this.state.updateParticipantMedia(peerId, {
|
|
491
|
+
isScreenSharing: true,
|
|
492
|
+
remoteScreenStreamId: msg.stream_id,
|
|
493
|
+
cameraStreamId: msg?.camera_stream_id
|
|
494
|
+
});
|
|
495
|
+
if (!this.state.presenterId) {
|
|
496
|
+
this.state.setPresenterId(peerId);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "SCREEN_SHARE_STOP": {
|
|
501
|
+
const peerId = msg.peerId;
|
|
502
|
+
this.state.updateParticipantMedia(peerId, { isScreenSharing: false });
|
|
503
|
+
if (this.state.presenterId === peerId) {
|
|
504
|
+
this.state.setPresenterId(null);
|
|
623
505
|
}
|
|
506
|
+
this.events.onScreenShareStopped?.(peerId);
|
|
624
507
|
break;
|
|
625
508
|
}
|
|
626
509
|
case "CHAT_MESSAGE": {
|
|
@@ -637,27 +520,37 @@ var VideoSDKCore = class {
|
|
|
637
520
|
this.events.onChatMessage?.(msg);
|
|
638
521
|
break;
|
|
639
522
|
}
|
|
640
|
-
case "
|
|
641
|
-
const
|
|
642
|
-
this.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
523
|
+
case "JOIN_PENDING": {
|
|
524
|
+
const req = msg.request;
|
|
525
|
+
this.isWaitingForApproval = true;
|
|
526
|
+
this.pendingRequestId = req.request_id;
|
|
527
|
+
this.events.onEntryRequested?.({
|
|
528
|
+
requestId: req.request_id,
|
|
529
|
+
userId: req.user_id,
|
|
530
|
+
name: req.name
|
|
646
531
|
});
|
|
647
|
-
if (!this.state.presenterId) {
|
|
648
|
-
this.state.setPresenterId(peerId2);
|
|
649
|
-
}
|
|
650
|
-
const screenStream = this.state.getParticipant(peerId2)?.media?.screenStream;
|
|
651
|
-
this.events.onScreenShareStarted?.(peerId2, screenStream || null);
|
|
652
532
|
break;
|
|
653
533
|
}
|
|
654
|
-
case "
|
|
655
|
-
|
|
656
|
-
this.
|
|
657
|
-
if (this.
|
|
658
|
-
this.
|
|
534
|
+
case "JOIN_APPROVED": {
|
|
535
|
+
this.isWaitingForApproval = false;
|
|
536
|
+
this.pendingRequestId = null;
|
|
537
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
538
|
+
this.send({
|
|
539
|
+
type: "JOIN",
|
|
540
|
+
room_id: this.room.id,
|
|
541
|
+
user_id: this.myId,
|
|
542
|
+
sender_name: this.participantName
|
|
543
|
+
});
|
|
659
544
|
}
|
|
660
|
-
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "JOIN_REJECTED": {
|
|
548
|
+
this.isWaitingForApproval = false;
|
|
549
|
+
this.pendingRequestId = null;
|
|
550
|
+
this.events.onEntryResponded?.({
|
|
551
|
+
participantId: msg.user_id,
|
|
552
|
+
decision: "rejected"
|
|
553
|
+
});
|
|
661
554
|
break;
|
|
662
555
|
}
|
|
663
556
|
case "ERROR": {
|
|
@@ -668,259 +561,78 @@ var VideoSDKCore = class {
|
|
|
668
561
|
msg,
|
|
669
562
|
!fatal
|
|
670
563
|
);
|
|
671
|
-
if (fatal)
|
|
672
|
-
this.disconnect();
|
|
673
|
-
}
|
|
564
|
+
if (fatal) this.disconnect();
|
|
674
565
|
return;
|
|
675
566
|
}
|
|
676
567
|
}
|
|
677
568
|
}
|
|
678
|
-
// ----------------
|
|
679
|
-
|
|
680
|
-
if (!this.
|
|
681
|
-
if (!this.iceServers || this.iceServers.length === 0) {
|
|
682
|
-
throw new Error(
|
|
683
|
-
"ICE Servers not configured. Backend must provide iceServers on JOIN."
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
const pc = new RTCPeerConnection({
|
|
687
|
-
iceServers: this.iceServers,
|
|
688
|
-
iceTransportPolicy: this.iceTransportPolicy
|
|
689
|
-
});
|
|
690
|
-
pc.ontrack = (event) => {
|
|
691
|
-
const incomingStream = event.streams?.[0] || new MediaStream([event.track]);
|
|
692
|
-
const streamId = incomingStream?.id?.replace(/[{}]/g, "");
|
|
693
|
-
const participant = this.state.getParticipant(id);
|
|
694
|
-
const isCameraStream = streamId && participant?.media?.cameraStreamId && streamId === participant?.media?.cameraStreamId;
|
|
695
|
-
const isScreenStream = streamId && participant?.media?.remoteScreenStreamId && streamId === participant?.media?.remoteScreenStreamId;
|
|
696
|
-
if (event.track.muted) {
|
|
697
|
-
event.track.onunmute = () => {
|
|
698
|
-
console.log(`${event.track.kind} track unmuted for ${id}`);
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
if (isScreenStream) {
|
|
702
|
-
const videoTrack = event.track.kind === "video" ? event.track : incomingStream.getVideoTracks()[0] || participant?.media?.screenTrack;
|
|
703
|
-
this.state.updateParticipantMedia(id, {
|
|
704
|
-
screenStream: incomingStream,
|
|
705
|
-
screenTrack: videoTrack,
|
|
706
|
-
remoteScreenStreamId: incomingStream.id,
|
|
707
|
-
isScreenSharing: true
|
|
708
|
-
});
|
|
709
|
-
if (!this.state.presenterId) {
|
|
710
|
-
this.state.setPresenterId(id);
|
|
711
|
-
}
|
|
712
|
-
this.events.onScreenShareStarted?.(id, incomingStream);
|
|
713
|
-
console.log(`Screen share stream detected for ${id}`);
|
|
714
|
-
} else if (isCameraStream) {
|
|
715
|
-
this.state.updateParticipantMedia(id, {
|
|
716
|
-
stream: incomingStream,
|
|
717
|
-
cameraTrack: incomingStream.getVideoTracks()[0],
|
|
718
|
-
audioTrack: incomingStream.getAudioTracks()[0]
|
|
719
|
-
});
|
|
720
|
-
this.events.onTrack?.(incomingStream, id);
|
|
721
|
-
} else {
|
|
722
|
-
this.state.updateParticipantMedia(id, {
|
|
723
|
-
stream: incomingStream,
|
|
724
|
-
cameraTrack: incomingStream.getVideoTracks()[0],
|
|
725
|
-
audioTrack: incomingStream.getAudioTracks()[0]
|
|
726
|
-
});
|
|
727
|
-
this.events.onTrack?.(incomingStream, id);
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
pc.onicecandidate = (e) => {
|
|
731
|
-
if (!e.candidate) return;
|
|
732
|
-
console.log("LOCAL ICE:", e.candidate.candidate);
|
|
733
|
-
this.send({
|
|
734
|
-
type: "ICE",
|
|
735
|
-
payload: JSON.stringify(e.candidate),
|
|
736
|
-
sender: this.myId,
|
|
737
|
-
target: id
|
|
738
|
-
});
|
|
739
|
-
};
|
|
740
|
-
pc.oniceconnectionstatechange = async () => {
|
|
741
|
-
console.log("================================");
|
|
742
|
-
console.log(`Peer: ${id}`);
|
|
743
|
-
console.log(`ICE: ${pc.iceConnectionState}`);
|
|
744
|
-
console.log(`Connection: ${pc.connectionState}`);
|
|
745
|
-
console.log(`Signaling: ${pc.signalingState}`);
|
|
746
|
-
if (pc.iceConnectionState === "disconnected") {
|
|
747
|
-
const stats = await pc.getStats();
|
|
748
|
-
stats.forEach((report) => {
|
|
749
|
-
if (report.type === "transport") {
|
|
750
|
-
console.log(JSON.stringify(report, null, 2));
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
pc.onconnectionstatechange = () => {
|
|
756
|
-
if (pc.connectionState === "failed") {
|
|
757
|
-
try {
|
|
758
|
-
pc.restartIce();
|
|
759
|
-
} catch {
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
};
|
|
763
|
-
this.localStream.getTracks().forEach((track) => {
|
|
764
|
-
pc.addTrack(track, this.localStream);
|
|
765
|
-
});
|
|
766
|
-
if (this.isScreenSharing && this.screenStream) {
|
|
767
|
-
this.screenSenders[id] = [];
|
|
768
|
-
this.screenStream.getTracks().forEach((track) => {
|
|
769
|
-
const sender = pc.addTrack(track, this.screenStream);
|
|
770
|
-
this.screenSenders[id].push(sender);
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
return pc;
|
|
774
|
-
}
|
|
775
|
-
// ---------------- OFFER ----------------
|
|
776
|
-
async createOffer(id, isRenegotiation = false) {
|
|
777
|
-
if (!isRenegotiation && !this.shouldInitiate(id)) {
|
|
778
|
-
console.debug(
|
|
779
|
-
`[Offer] ${id} should initiate (${id} > ${this.myId}), skipping`
|
|
780
|
-
);
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
if (!isRenegotiation && this.initiators.has(id)) {
|
|
784
|
-
console.debug(
|
|
785
|
-
`[Offer] Already initiating with ${id}, skipping duplicate`
|
|
786
|
-
);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (!isRenegotiation) {
|
|
790
|
-
this.initiators.add(id);
|
|
791
|
-
}
|
|
792
|
-
if (!this.peers[id]) {
|
|
793
|
-
this.peers[id] = this.createPeer(id);
|
|
794
|
-
}
|
|
795
|
-
const pc = this.peers[id];
|
|
569
|
+
// ---------------- PUBLISHER RENEGOTIATION ----------------
|
|
570
|
+
async createPublisherOffer() {
|
|
571
|
+
if (!this.pubPC) return;
|
|
796
572
|
try {
|
|
797
|
-
const offer = await
|
|
798
|
-
await
|
|
573
|
+
const offer = await this.pubPC.createOffer();
|
|
574
|
+
await this.pubPC.setLocalDescription(offer);
|
|
799
575
|
this.send({
|
|
800
|
-
type: "
|
|
576
|
+
type: "PUB_OFFER",
|
|
801
577
|
payload: offer.sdp,
|
|
802
|
-
|
|
803
|
-
|
|
578
|
+
user_id: this.myId,
|
|
579
|
+
room_id: this.room.id
|
|
804
580
|
});
|
|
805
|
-
console.debug(`[Offer] Sent to ${id}`);
|
|
806
581
|
} catch (err) {
|
|
807
|
-
console.error(
|
|
582
|
+
console.error("[SFU Publisher Offer Error]", err);
|
|
808
583
|
}
|
|
809
584
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
const pc = this.peers[id];
|
|
824
|
-
try {
|
|
825
|
-
if (pc.signalingState === "have-local-offer") {
|
|
826
|
-
if (this.shouldInitiate(id)) {
|
|
827
|
-
console.warn(
|
|
828
|
-
`[Glare] Both sent OFFERs, we win (${this.myId} < ${id}), keeping our OFFER`
|
|
829
|
-
);
|
|
830
|
-
return;
|
|
831
|
-
} else {
|
|
832
|
-
console.warn(
|
|
833
|
-
`[Glare] Both sent OFFERs, they win (${id} < ${this.myId}), rolling back`
|
|
834
|
-
);
|
|
835
|
-
pc.close();
|
|
836
|
-
delete this.peers[id];
|
|
837
|
-
this.initiators.delete(id);
|
|
838
|
-
this.peers[id] = this.createPeer(id);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (this.peers[id].signalingState !== "stable" && this.peers[id].signalingState !== "have-local-offer") {
|
|
842
|
-
console.warn(
|
|
843
|
-
`[Signaling] Cannot accept OFFER in state "${this.peers[id].signalingState}"`
|
|
844
|
-
);
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
await this.peers[id].setRemoteDescription({
|
|
848
|
-
type: "offer",
|
|
849
|
-
sdp
|
|
850
|
-
});
|
|
851
|
-
const pending = this.pendingIceCandidates[id] || [];
|
|
852
|
-
for (const candidate of pending) {
|
|
853
|
-
try {
|
|
854
|
-
await this.peers[id].addIceCandidate(candidate);
|
|
855
|
-
} catch (err) {
|
|
856
|
-
console.warn("[ICE] Failed to add candidate:", err);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
delete this.pendingIceCandidates[id];
|
|
860
|
-
const answer = await this.peers[id].createAnswer();
|
|
861
|
-
await this.peers[id].setLocalDescription(answer);
|
|
862
|
-
await this.flushIce(id, this.peers[id]);
|
|
863
|
-
this.send({
|
|
864
|
-
type: "ANSWER",
|
|
865
|
-
payload: answer.sdp,
|
|
866
|
-
sender: this.myId,
|
|
867
|
-
target: id
|
|
868
|
-
});
|
|
869
|
-
console.debug(`[Answer] Sent to ${id}`);
|
|
870
|
-
} catch (err) {
|
|
871
|
-
console.error(`[Signaling] Failed to handle OFFER from ${id}:`, err);
|
|
872
|
-
this.emitError(
|
|
873
|
-
"OFFER_HANDLING_FAILED",
|
|
874
|
-
`Failed to handle offer from ${id}`,
|
|
875
|
-
err,
|
|
876
|
-
true
|
|
877
|
-
);
|
|
878
|
-
}
|
|
585
|
+
// ---------------- MEDIA TOGGLES ----------------
|
|
586
|
+
toggleMic() {
|
|
587
|
+
const mediaState = this.state.localParticipant?.media;
|
|
588
|
+
if (!mediaState) return;
|
|
589
|
+
const nextEnabled = !mediaState.micEnabled;
|
|
590
|
+
this.localStream?.getAudioTracks().forEach((t) => t.enabled = nextEnabled);
|
|
591
|
+
this.state.updateLocalParticipant({
|
|
592
|
+
id: this.myId,
|
|
593
|
+
name: this.participantName,
|
|
594
|
+
media: { ...mediaState, micEnabled: nextEnabled }
|
|
595
|
+
});
|
|
596
|
+
this.send({ type: "MEDIA_STATE", kind: "audio", enabled: nextEnabled });
|
|
879
597
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
this.
|
|
598
|
+
toggleCam() {
|
|
599
|
+
const mediaState = this.state.localParticipant?.media;
|
|
600
|
+
if (!mediaState) return;
|
|
601
|
+
const nextEnabled = !mediaState.camEnabled;
|
|
602
|
+
this.localStream?.getVideoTracks().forEach((t) => t.enabled = nextEnabled);
|
|
603
|
+
this.state.updateLocalParticipant({
|
|
604
|
+
id: this.myId,
|
|
605
|
+
name: this.participantName,
|
|
606
|
+
media: { ...mediaState, camEnabled: nextEnabled }
|
|
607
|
+
});
|
|
608
|
+
this.send({ type: "MEDIA_STATE", kind: "video", enabled: nextEnabled });
|
|
891
609
|
}
|
|
610
|
+
// ---------------- SCREEN SHARING (SFU) ----------------
|
|
892
611
|
async startScreenShare() {
|
|
893
612
|
try {
|
|
894
613
|
if (this.state.presenterId && this.state.presenterId !== this.myId) {
|
|
895
614
|
throw new Error("Another user is already sharing their screen.");
|
|
896
615
|
}
|
|
897
|
-
if (!navigator.mediaDevices?.getDisplayMedia) {
|
|
898
|
-
throw new Error("Screen sharing not supported on this device");
|
|
899
|
-
}
|
|
900
616
|
this.screenStream = await navigator.mediaDevices.getDisplayMedia({
|
|
901
617
|
video: true
|
|
902
|
-
// audio: true,
|
|
903
618
|
});
|
|
904
619
|
this.isScreenSharing = true;
|
|
620
|
+
const screenTrack = this.screenStream.getVideoTracks()[0];
|
|
621
|
+
if (this.pubPC) {
|
|
622
|
+
this.screenSender = this.pubPC.addTrack(screenTrack, this.screenStream);
|
|
623
|
+
await this.createPublisherOffer();
|
|
624
|
+
}
|
|
905
625
|
this.state.updateLocalParticipant({
|
|
906
626
|
media: {
|
|
907
627
|
isScreenSharing: true,
|
|
908
628
|
screenStream: this.screenStream,
|
|
909
|
-
screenTrack
|
|
629
|
+
screenTrack
|
|
910
630
|
}
|
|
911
631
|
});
|
|
912
632
|
this.state.setPresenterId(this.myId);
|
|
913
|
-
|
|
633
|
+
screenTrack.onended = () => {
|
|
914
634
|
this.stopScreenShare();
|
|
915
635
|
};
|
|
916
|
-
Object.entries(this.peers).forEach(([peerId, pc]) => {
|
|
917
|
-
this.screenSenders[peerId] = [];
|
|
918
|
-
this.screenStream.getTracks().forEach((track) => {
|
|
919
|
-
const sender = pc.addTrack(track, this.screenStream);
|
|
920
|
-
this.screenSenders[peerId].push(sender);
|
|
921
|
-
});
|
|
922
|
-
this.createOffer(peerId, true);
|
|
923
|
-
});
|
|
924
636
|
this.send({
|
|
925
637
|
type: "SCREEN_SHARE_START",
|
|
926
638
|
sender: this.myId,
|
|
@@ -932,7 +644,7 @@ var VideoSDKCore = class {
|
|
|
932
644
|
} catch (err) {
|
|
933
645
|
this.emitError(
|
|
934
646
|
"SCREEN_SHARE_FAILED",
|
|
935
|
-
err?.message || "Failed
|
|
647
|
+
err?.message || "Failed screen share",
|
|
936
648
|
err,
|
|
937
649
|
true
|
|
938
650
|
);
|
|
@@ -944,18 +656,15 @@ var VideoSDKCore = class {
|
|
|
944
656
|
stopScreenShare() {
|
|
945
657
|
if (!this.screenStream) return;
|
|
946
658
|
this.screenStream.getTracks().forEach((t) => t.stop());
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
delete this.screenSenders[peerId];
|
|
957
|
-
this.createOffer(peerId, true);
|
|
958
|
-
});
|
|
659
|
+
if (this.pubPC && this.screenSender) {
|
|
660
|
+
try {
|
|
661
|
+
this.pubPC.removeTrack(this.screenSender);
|
|
662
|
+
this.createPublisherOffer();
|
|
663
|
+
} catch (e) {
|
|
664
|
+
console.warn("Failed removing screen sender", e);
|
|
665
|
+
}
|
|
666
|
+
this.screenSender = null;
|
|
667
|
+
}
|
|
959
668
|
this.screenStream = null;
|
|
960
669
|
this.isScreenSharing = false;
|
|
961
670
|
this.state.updateLocalParticipant({
|
|
@@ -974,15 +683,10 @@ var VideoSDKCore = class {
|
|
|
974
683
|
room_id: this.room.id
|
|
975
684
|
});
|
|
976
685
|
}
|
|
686
|
+
// ---------------- CHAT & RECONNECT ----------------
|
|
977
687
|
sendChatMessage(payload) {
|
|
978
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
979
|
-
console.warn("WS not connected");
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
if (!this.room.id) {
|
|
983
|
-
console.warn("No roomId set");
|
|
688
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.room.id)
|
|
984
689
|
return;
|
|
985
|
-
}
|
|
986
690
|
const isPrivate = !!payload?.target;
|
|
987
691
|
const senderName = this.state.localParticipant?.name || "Anonymous";
|
|
988
692
|
const msg = {
|
|
@@ -1006,12 +710,48 @@ var VideoSDKCore = class {
|
|
|
1006
710
|
client_ts: Date.now()
|
|
1007
711
|
});
|
|
1008
712
|
}
|
|
713
|
+
scheduleReconnect() {
|
|
714
|
+
if (!this.room.id) return;
|
|
715
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
716
|
+
window.clearTimeout(this.reconnectTimer);
|
|
717
|
+
this.reconnectTimer = window.setTimeout(async () => {
|
|
718
|
+
try {
|
|
719
|
+
await this.connect(this.room.id, this.participantName);
|
|
720
|
+
this.reconnectAttempts = 0;
|
|
721
|
+
} catch {
|
|
722
|
+
this.reconnectAttempts++;
|
|
723
|
+
this.scheduleReconnect();
|
|
724
|
+
}
|
|
725
|
+
}, delay);
|
|
726
|
+
}
|
|
727
|
+
startHeartbeat() {
|
|
728
|
+
this.stopHeartbeat();
|
|
729
|
+
this.pingInterval = setInterval(() => {
|
|
730
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
731
|
+
this.send({ type: "PING", client_ts: Date.now() });
|
|
732
|
+
}
|
|
733
|
+
}, 2e4);
|
|
734
|
+
}
|
|
735
|
+
stopHeartbeat() {
|
|
736
|
+
if (this.pingInterval) {
|
|
737
|
+
clearInterval(this.pingInterval);
|
|
738
|
+
this.pingInterval = null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
reset() {
|
|
742
|
+
this.pubPC?.close();
|
|
743
|
+
this.subPC?.close();
|
|
744
|
+
this.pubPC = null;
|
|
745
|
+
this.subPC = null;
|
|
746
|
+
this.state.resetRemoteState();
|
|
747
|
+
}
|
|
1009
748
|
disconnect() {
|
|
1010
749
|
this.intentionalDisconnect = true;
|
|
1011
750
|
this.stopScreenShare();
|
|
1012
|
-
|
|
1013
|
-
this.
|
|
1014
|
-
this.
|
|
751
|
+
this.pubPC?.close();
|
|
752
|
+
this.subPC?.close();
|
|
753
|
+
this.pubPC = null;
|
|
754
|
+
this.subPC = null;
|
|
1015
755
|
this.stopHeartbeat();
|
|
1016
756
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1017
757
|
this.send({
|
|
@@ -1038,32 +778,40 @@ var VideoSDKCore = class {
|
|
|
1038
778
|
this.state.clearChat();
|
|
1039
779
|
this.state.setPresenterId(null);
|
|
1040
780
|
}
|
|
1041
|
-
async
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
console.warn("ICE flush error", e);
|
|
1049
|
-
}
|
|
781
|
+
async restartPublisherIce() {
|
|
782
|
+
if (!this.pubPC) return;
|
|
783
|
+
try {
|
|
784
|
+
this.pubPC.restartIce();
|
|
785
|
+
await this.createPublisherOffer();
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.error("[Publisher ICE Restart Failed]", err);
|
|
1050
788
|
}
|
|
1051
|
-
|
|
789
|
+
}
|
|
790
|
+
emitError(code, message, raw, recoverable = true) {
|
|
791
|
+
const err = {
|
|
792
|
+
code,
|
|
793
|
+
message,
|
|
794
|
+
raw,
|
|
795
|
+
roomId: this.room.id,
|
|
796
|
+
userId: this.myId,
|
|
797
|
+
recoverable
|
|
798
|
+
};
|
|
799
|
+
this.events.onError?.(err);
|
|
800
|
+
this.joinRejecter?.(err);
|
|
801
|
+
this.joinRejecter = void 0;
|
|
802
|
+
console.error("[MeetingSDK Error]", err);
|
|
1052
803
|
}
|
|
1053
804
|
send(msg) {
|
|
1054
805
|
this.ws?.send(JSON.stringify(msg));
|
|
1055
806
|
}
|
|
1056
807
|
approveJoinRequest(requestId) {
|
|
1057
|
-
this.send({
|
|
1058
|
-
type: "JOIN_APPROVE",
|
|
1059
|
-
request_id: requestId
|
|
1060
|
-
});
|
|
808
|
+
this.send({ type: "JOIN_APPROVE", request_id: requestId });
|
|
1061
809
|
}
|
|
1062
810
|
rejectJoinRequest(requestId) {
|
|
1063
|
-
this.send({
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
811
|
+
this.send({ type: "JOIN_REJECT", request_id: requestId });
|
|
812
|
+
}
|
|
813
|
+
getMeeting() {
|
|
814
|
+
return this.room;
|
|
1067
815
|
}
|
|
1068
816
|
};
|
|
1069
817
|
|
|
@@ -1320,40 +1068,74 @@ var useRemoteMedia = (participantId) => {
|
|
|
1320
1068
|
};
|
|
1321
1069
|
|
|
1322
1070
|
// src/react/useMeetingPreview.ts
|
|
1323
|
-
import {
|
|
1071
|
+
import { useEffect as useEffect6, useState as useState5 } from "react";
|
|
1324
1072
|
function useMeetingPreview(roomId, userId) {
|
|
1325
1073
|
const [room, setRoom] = useState5(null);
|
|
1326
|
-
const [isLoading, setIsLoading] = useState5(true);
|
|
1327
1074
|
const [error, setError] = useState5(null);
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1075
|
+
const [isConnected, setIsConnected] = useState5(false);
|
|
1076
|
+
const [isLoading, setIsLoading] = useState5(true);
|
|
1077
|
+
useEffect6(() => {
|
|
1078
|
+
if (!roomId || !userId) {
|
|
1079
|
+
setIsLoading(false);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
let ws = null;
|
|
1083
|
+
const heartbeat = setInterval(() => {
|
|
1084
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1085
|
+
ws.send(
|
|
1086
|
+
JSON.stringify({
|
|
1087
|
+
type: "PING"
|
|
1088
|
+
})
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}, 2e4);
|
|
1092
|
+
ws = new WebSocket(`${SDK_CONFIG.wsUrl}/watch/${roomId}?user_id=${userId}`);
|
|
1093
|
+
ws.onopen = () => {
|
|
1094
|
+
setIsConnected(true);
|
|
1332
1095
|
setError(null);
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1096
|
+
console.log("[Preview] watcher connected");
|
|
1097
|
+
};
|
|
1098
|
+
ws.onmessage = (event) => {
|
|
1099
|
+
try {
|
|
1100
|
+
const msg = JSON.parse(event.data);
|
|
1101
|
+
if (msg.type !== "ROOM_PRESENCE_UPDATE") {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
setRoom({
|
|
1105
|
+
active: msg.active ?? false,
|
|
1106
|
+
count: msg.count ?? 0,
|
|
1107
|
+
canJoin: msg.canJoin ?? false,
|
|
1108
|
+
approved: msg.approved ?? false,
|
|
1109
|
+
isHost: msg.isHost ?? false,
|
|
1110
|
+
hasMoreParticipants: msg.hasMoreParticipants ?? false,
|
|
1111
|
+
participants: msg.participants ?? []
|
|
1112
|
+
});
|
|
1113
|
+
setIsLoading(false);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
console.error("Invalid room presence payload", err);
|
|
1116
|
+
setIsLoading(false);
|
|
1338
1117
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
1343
|
-
} finally {
|
|
1118
|
+
};
|
|
1119
|
+
ws.onerror = () => {
|
|
1120
|
+
setError("Failed to connect to room monitor");
|
|
1344
1121
|
setIsLoading(false);
|
|
1345
|
-
}
|
|
1122
|
+
};
|
|
1123
|
+
ws.onclose = () => {
|
|
1124
|
+
setIsConnected(false);
|
|
1125
|
+
console.log("[Preview] disconnected");
|
|
1126
|
+
};
|
|
1127
|
+
return () => {
|
|
1128
|
+
clearInterval(heartbeat);
|
|
1129
|
+
if (ws) {
|
|
1130
|
+
ws.close(1e3, "Leaving preview");
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1346
1133
|
}, [roomId, userId]);
|
|
1347
|
-
useEffect6(() => {
|
|
1348
|
-
load();
|
|
1349
|
-
const interval = setInterval(load, 5e3);
|
|
1350
|
-
return () => clearInterval(interval);
|
|
1351
|
-
}, [load]);
|
|
1352
1134
|
return {
|
|
1353
1135
|
room,
|
|
1136
|
+
isConnected,
|
|
1354
1137
|
isLoading,
|
|
1355
|
-
error
|
|
1356
|
-
refetch: load
|
|
1138
|
+
error
|
|
1357
1139
|
};
|
|
1358
1140
|
}
|
|
1359
1141
|
export {
|