@defcomm/meetingsdk 0.6.9 → 0.7.1
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 +21 -5
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +195 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +195 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -386,7 +386,7 @@ var MeetingState = class {
|
|
|
386
386
|
const nextMedia = {
|
|
387
387
|
stream: patch.media?.stream ?? prevMedia.stream,
|
|
388
388
|
screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
|
|
389
|
-
cameraTrack: patch.media
|
|
389
|
+
cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
|
|
390
390
|
screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
|
|
391
391
|
audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
|
|
392
392
|
micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
|
|
@@ -647,6 +647,71 @@ var SFUClient = class {
|
|
|
647
647
|
setLocalStream(stream) {
|
|
648
648
|
this.localStream = stream;
|
|
649
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Attach or detach the local camera track without replacing the publisher
|
|
652
|
+
* PeerConnection. Detaching with replaceTrack(null) stops media transmission;
|
|
653
|
+
* VideoSDKCore separately calls MediaStreamTrack.stop() to release hardware.
|
|
654
|
+
*/
|
|
655
|
+
async setCameraTrack(track) {
|
|
656
|
+
if (!this.localStream) this.localStream = new MediaStream();
|
|
657
|
+
const currentTrack = this.localStream.getVideoTracks().find((item) => item !== track);
|
|
658
|
+
if (currentTrack) this.localStream.removeTrack(currentTrack);
|
|
659
|
+
if (track && !this.localStream.getTracks().includes(track)) {
|
|
660
|
+
this.localStream.addTrack(track);
|
|
661
|
+
}
|
|
662
|
+
if (!this.videoSender || !this.publisher || !this.cameraTransceiver) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const needsCameraNegotiation = Boolean(
|
|
666
|
+
track && this.cameraTransceiver.direction === "inactive"
|
|
667
|
+
);
|
|
668
|
+
if (needsCameraNegotiation && this.ws?.readyState !== WebSocket.OPEN) {
|
|
669
|
+
throw new Error("Cannot enable the camera while signaling is disconnected");
|
|
670
|
+
}
|
|
671
|
+
await this.videoSender.replaceTrack(track);
|
|
672
|
+
if (needsCameraNegotiation) {
|
|
673
|
+
await this.waitForPublisherStable();
|
|
674
|
+
this.cameraTransceiver.direction = "sendonly";
|
|
675
|
+
await this.sendPublisherOffer();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async waitForPublisherStable(timeoutMs = 5e3) {
|
|
679
|
+
const publisher = this.publisher;
|
|
680
|
+
if (!publisher) throw new Error("Publisher PeerConnection is not initialized");
|
|
681
|
+
const startedAt = Date.now();
|
|
682
|
+
while (publisher.signalingState !== "stable") {
|
|
683
|
+
if (publisher.signalingState === "closed") {
|
|
684
|
+
throw new Error("Publisher PeerConnection is closed");
|
|
685
|
+
}
|
|
686
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
687
|
+
throw new Error("Publisher negotiation did not become stable in time");
|
|
688
|
+
}
|
|
689
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async sendPublisherOffer() {
|
|
693
|
+
if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
|
|
694
|
+
const offer = await this.publisher.createOffer();
|
|
695
|
+
await this.publisher.setLocalDescription(offer);
|
|
696
|
+
const sent = this.send({
|
|
697
|
+
type: "publish_offer",
|
|
698
|
+
payload: {
|
|
699
|
+
revision: ++this.publisherRevision,
|
|
700
|
+
sdp: offer.sdp
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
if (!sent) {
|
|
704
|
+
try {
|
|
705
|
+
await this.publisher.setLocalDescription({ type: "rollback" });
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
if (this.cameraTransceiver) {
|
|
709
|
+
this.cameraTransceiver.direction = "inactive";
|
|
710
|
+
}
|
|
711
|
+
await this.videoSender?.replaceTrack(null);
|
|
712
|
+
throw new Error("Unable to negotiate camera publishing: signaling is disconnected");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
650
715
|
nextRequestId() {
|
|
651
716
|
return (++this.requestId).toString();
|
|
652
717
|
}
|
|
@@ -2324,6 +2389,8 @@ var VideoSDKCore = class {
|
|
|
2324
2389
|
this.screenStream = null;
|
|
2325
2390
|
this.isScreenSharing = false;
|
|
2326
2391
|
this.participantName = "";
|
|
2392
|
+
this.cameraEnabled = false;
|
|
2393
|
+
this.cameraToggleInProgress = false;
|
|
2327
2394
|
this.intentionalDisconnect = false;
|
|
2328
2395
|
this.remoteStreams = /* @__PURE__ */ new Map();
|
|
2329
2396
|
this.remoteScreenStreams = /* @__PURE__ */ new Map();
|
|
@@ -2357,6 +2424,7 @@ var VideoSDKCore = class {
|
|
|
2357
2424
|
this.participantName = name;
|
|
2358
2425
|
try {
|
|
2359
2426
|
this.localStream = await this.acquireOptionalLocalMedia();
|
|
2427
|
+
this.cameraEnabled = this.hasLiveCameraTrack();
|
|
2360
2428
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2361
2429
|
if (localAudioTrack) {
|
|
2362
2430
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
@@ -2368,17 +2436,47 @@ var VideoSDKCore = class {
|
|
|
2368
2436
|
throw err;
|
|
2369
2437
|
}
|
|
2370
2438
|
}
|
|
2371
|
-
|
|
2439
|
+
hasLiveCameraTrack() {
|
|
2440
|
+
return Boolean(
|
|
2441
|
+
this.localStream?.getVideoTracks().some((track) => track.readyState === "live")
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
getLiveCameraTrack() {
|
|
2445
|
+
return this.localStream?.getVideoTracks().find((track) => track.readyState === "live") ?? null;
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Stop every locally captured camera track and remove it from the stream.
|
|
2449
|
+
* Track.enabled=false only emits black frames; stop() releases camera capture.
|
|
2450
|
+
*/
|
|
2451
|
+
stopCameraCapture() {
|
|
2452
|
+
if (!this.localStream) {
|
|
2453
|
+
this.cameraEnabled = false;
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
for (const track of this.localStream.getVideoTracks()) {
|
|
2457
|
+
this.localStream.removeTrack(track);
|
|
2458
|
+
try {
|
|
2459
|
+
track.stop();
|
|
2460
|
+
} catch {
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
this.cameraEnabled = false;
|
|
2464
|
+
}
|
|
2465
|
+
updateLocalMedia() {
|
|
2372
2466
|
if (!this.localStream) return;
|
|
2467
|
+
const cameraTrack = this.getLiveCameraTrack();
|
|
2468
|
+
const audioTrack = this.localStream.getAudioTracks().find((track) => track.readyState === "live");
|
|
2469
|
+
this.cameraEnabled = Boolean(cameraTrack && cameraTrack.enabled);
|
|
2373
2470
|
this.state.updateLocalParticipant({
|
|
2374
2471
|
id: this.myId,
|
|
2375
2472
|
name: this.participantName,
|
|
2376
2473
|
media: {
|
|
2377
2474
|
stream: this.localStream,
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2475
|
+
// Explicit null clears a previously published track from React state.
|
|
2476
|
+
cameraTrack,
|
|
2477
|
+
audioTrack: audioTrack ?? void 0,
|
|
2478
|
+
micEnabled: audioTrack?.enabled ?? false,
|
|
2479
|
+
camEnabled: this.cameraEnabled,
|
|
2382
2480
|
isScreenSharing: this.isScreenSharing,
|
|
2383
2481
|
screenStream: this.screenStream,
|
|
2384
2482
|
screenTrack: this.screenStream?.getVideoTracks()[0]
|
|
@@ -2405,7 +2503,7 @@ var VideoSDKCore = class {
|
|
|
2405
2503
|
async getMeetingSummary(roomCode, sessionId) {
|
|
2406
2504
|
return this.sfuClient.getMeetingSummary(roomCode, sessionId);
|
|
2407
2505
|
}
|
|
2408
|
-
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
|
|
2506
|
+
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
|
|
2409
2507
|
const combined = new MediaStream();
|
|
2410
2508
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2411
2509
|
console.warn(
|
|
@@ -2413,13 +2511,13 @@ var VideoSDKCore = class {
|
|
|
2413
2511
|
);
|
|
2414
2512
|
return combined;
|
|
2415
2513
|
}
|
|
2416
|
-
const cameraPromise = navigator.mediaDevices.getUserMedia({
|
|
2514
|
+
const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
|
|
2417
2515
|
video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
|
|
2418
2516
|
width: { ideal: 1280 },
|
|
2419
2517
|
height: { ideal: 720 }
|
|
2420
2518
|
},
|
|
2421
2519
|
audio: false
|
|
2422
|
-
});
|
|
2520
|
+
}) : Promise.resolve(null);
|
|
2423
2521
|
const microphonePromise = navigator.mediaDevices.getUserMedia({
|
|
2424
2522
|
video: false,
|
|
2425
2523
|
audio: microphoneDeviceId ? {
|
|
@@ -2437,11 +2535,11 @@ var VideoSDKCore = class {
|
|
|
2437
2535
|
cameraPromise,
|
|
2438
2536
|
microphonePromise
|
|
2439
2537
|
]);
|
|
2440
|
-
if (cameraResult.status === "fulfilled") {
|
|
2538
|
+
if (cameraResult.status === "fulfilled" && cameraResult.value) {
|
|
2441
2539
|
for (const track of cameraResult.value.getVideoTracks()) {
|
|
2442
2540
|
combined.addTrack(track);
|
|
2443
2541
|
}
|
|
2444
|
-
} else {
|
|
2542
|
+
} else if (cameraResult.status === "rejected") {
|
|
2445
2543
|
console.warn("[SDK] Camera unavailable; continuing without video.", {
|
|
2446
2544
|
name: cameraResult.reason?.name,
|
|
2447
2545
|
message: cameraResult.reason?.message
|
|
@@ -2462,6 +2560,25 @@ var VideoSDKCore = class {
|
|
|
2462
2560
|
}
|
|
2463
2561
|
return combined;
|
|
2464
2562
|
}
|
|
2563
|
+
async acquireCameraTrack() {
|
|
2564
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2565
|
+
throw new Error("Camera access is not supported on this device");
|
|
2566
|
+
}
|
|
2567
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
2568
|
+
video: this.cameraDeviceId ? { deviceId: { exact: this.cameraDeviceId } } : {
|
|
2569
|
+
width: { ideal: 1280 },
|
|
2570
|
+
height: { ideal: 720 }
|
|
2571
|
+
},
|
|
2572
|
+
audio: false
|
|
2573
|
+
});
|
|
2574
|
+
const track = stream.getVideoTracks()[0];
|
|
2575
|
+
if (!track) {
|
|
2576
|
+
stream.getTracks().forEach((item) => item.stop());
|
|
2577
|
+
throw new Error("The selected camera did not provide a video track");
|
|
2578
|
+
}
|
|
2579
|
+
this.cameraDeviceId = track.getSettings().deviceId || this.cameraDeviceId;
|
|
2580
|
+
return track;
|
|
2581
|
+
}
|
|
2465
2582
|
/**
|
|
2466
2583
|
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
2467
2584
|
* whenever possible (Join, first click inside the meeting, etc.).
|
|
@@ -2511,10 +2628,12 @@ var VideoSDKCore = class {
|
|
|
2511
2628
|
this.participantName = name;
|
|
2512
2629
|
this.room.id = roomId;
|
|
2513
2630
|
this.room.name = name;
|
|
2631
|
+
this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
|
|
2514
2632
|
if (!this.localStream) {
|
|
2515
2633
|
this.localStream = await this.acquireOptionalLocalMedia(
|
|
2516
|
-
cameraDeviceId,
|
|
2517
|
-
microphoneDeviceId
|
|
2634
|
+
this.cameraDeviceId,
|
|
2635
|
+
microphoneDeviceId,
|
|
2636
|
+
!videoMuted
|
|
2518
2637
|
);
|
|
2519
2638
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2520
2639
|
if (localAudioTrack) {
|
|
@@ -2524,10 +2643,17 @@ var VideoSDKCore = class {
|
|
|
2524
2643
|
this.localStream.getAudioTracks().forEach((track) => {
|
|
2525
2644
|
track.enabled = !audioMuted;
|
|
2526
2645
|
});
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2646
|
+
if (videoMuted) {
|
|
2647
|
+
this.stopCameraCapture();
|
|
2648
|
+
} else {
|
|
2649
|
+
const initialCameraTrack = this.getLiveCameraTrack();
|
|
2650
|
+
this.cameraEnabled = Boolean(initialCameraTrack);
|
|
2651
|
+
if (initialCameraTrack) {
|
|
2652
|
+
this.cameraDeviceId = initialCameraTrack.getSettings().deviceId || this.cameraDeviceId;
|
|
2653
|
+
initialCameraTrack.enabled = true;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
this.updateLocalMedia();
|
|
2531
2657
|
const promise = this.connect(roomId, name);
|
|
2532
2658
|
this.joinPromise = promise;
|
|
2533
2659
|
try {
|
|
@@ -3154,7 +3280,12 @@ var VideoSDKCore = class {
|
|
|
3154
3280
|
if (!this.sfuClient) {
|
|
3155
3281
|
throw new Error("SFU client is not initialized");
|
|
3156
3282
|
}
|
|
3283
|
+
if (!this.cameraEnabled) {
|
|
3284
|
+
throw new Error("Your camera is off. Turn it on before switching devices.");
|
|
3285
|
+
}
|
|
3157
3286
|
const track = await this.sfuClient.selectCamera(deviceId);
|
|
3287
|
+
this.cameraDeviceId = deviceId;
|
|
3288
|
+
this.cameraEnabled = true;
|
|
3158
3289
|
if (this.localStream) {
|
|
3159
3290
|
this.state.localStream = this.localStream;
|
|
3160
3291
|
this.updateLocalMedia();
|
|
@@ -3196,24 +3327,52 @@ var VideoSDKCore = class {
|
|
|
3196
3327
|
this.sfuClient?.muteAudio(!nextEnabled);
|
|
3197
3328
|
this.events.onMicToggled?.(this.myId, nextEnabled);
|
|
3198
3329
|
}
|
|
3199
|
-
toggleCam() {
|
|
3200
|
-
|
|
3201
|
-
if (
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
camEnabled: nextEnabled
|
|
3330
|
+
async toggleCam() {
|
|
3331
|
+
if (!this.state.localParticipant?.media || !this.localStream) return;
|
|
3332
|
+
if (this.cameraToggleInProgress) return;
|
|
3333
|
+
this.cameraToggleInProgress = true;
|
|
3334
|
+
const nextEnabled = !this.cameraEnabled;
|
|
3335
|
+
try {
|
|
3336
|
+
if (!nextEnabled) {
|
|
3337
|
+
this.sfuClient?.muteVideo(true);
|
|
3338
|
+
this.stopCameraCapture();
|
|
3339
|
+
await this.sfuClient?.setCameraTrack(null);
|
|
3340
|
+
this.updateLocalMedia();
|
|
3341
|
+
this.events.onCamToggled?.(this.myId, false);
|
|
3342
|
+
return;
|
|
3213
3343
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3344
|
+
const track = await this.acquireCameraTrack();
|
|
3345
|
+
if (!this.localStream) this.localStream = new MediaStream();
|
|
3346
|
+
this.localStream.addTrack(track);
|
|
3347
|
+
try {
|
|
3348
|
+
await this.sfuClient?.setCameraTrack(track);
|
|
3349
|
+
this.cameraEnabled = true;
|
|
3350
|
+
this.sfuClient?.muteVideo(false);
|
|
3351
|
+
this.updateLocalMedia();
|
|
3352
|
+
this.events.onCamToggled?.(this.myId, true);
|
|
3353
|
+
} catch (error) {
|
|
3354
|
+
this.localStream.removeTrack(track);
|
|
3355
|
+
try {
|
|
3356
|
+
track.stop();
|
|
3357
|
+
} catch {
|
|
3358
|
+
}
|
|
3359
|
+
this.cameraEnabled = false;
|
|
3360
|
+
this.updateLocalMedia();
|
|
3361
|
+
throw error;
|
|
3362
|
+
}
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
this.cameraEnabled = false;
|
|
3365
|
+
this.updateLocalMedia();
|
|
3366
|
+
this.emitError(
|
|
3367
|
+
"CAMERA_TOGGLE_FAILED",
|
|
3368
|
+
error?.message || "Unable to change camera state",
|
|
3369
|
+
error,
|
|
3370
|
+
true
|
|
3371
|
+
);
|
|
3372
|
+
throw error;
|
|
3373
|
+
} finally {
|
|
3374
|
+
this.cameraToggleInProgress = false;
|
|
3375
|
+
}
|
|
3217
3376
|
}
|
|
3218
3377
|
// SCREEN SHARING
|
|
3219
3378
|
async startScreenShare() {
|
|
@@ -3371,6 +3530,8 @@ var VideoSDKCore = class {
|
|
|
3371
3530
|
this.localStream.getTracks().forEach((track) => track.stop());
|
|
3372
3531
|
this.localStream = null;
|
|
3373
3532
|
}
|
|
3533
|
+
this.cameraEnabled = false;
|
|
3534
|
+
this.cameraToggleInProgress = false;
|
|
3374
3535
|
if (this.screenStream) {
|
|
3375
3536
|
this.screenStream.getTracks().forEach((track) => track.stop());
|
|
3376
3537
|
this.screenStream = null;
|