@defcomm/meetingsdk 0.6.9 → 0.7.2
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 +20 -3
- package/dist/index.d.mts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +203 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +203 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ Your backend
|
|
|
92
92
|
Do **not** put a server-only API secret in browser code. Your backend should handle server credentials and issue the short-lived/access-scoped token that the browser uses for the meeting.
|
|
93
93
|
|
|
94
94
|
Your application is responsible for deciding who the participant is and for obtaining the token securely.
|
|
95
|
+
The SDK uses the supplied `config.token` for REST and SFU authentication. It does not request or refresh tokens; reconnects reuse the same token.
|
|
95
96
|
|
|
96
97
|
---
|
|
97
98
|
|
|
@@ -421,10 +422,26 @@ Toggle the local camera:
|
|
|
421
422
|
```tsx
|
|
422
423
|
const { toggleCam } = useMeeting();
|
|
423
424
|
|
|
424
|
-
<button
|
|
425
|
+
<button
|
|
426
|
+
onClick={() =>
|
|
427
|
+
void toggleCam().catch((error) => {
|
|
428
|
+
console.error("Unable to toggle camera", error);
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
>
|
|
432
|
+
Toggle camera
|
|
433
|
+
</button>;
|
|
425
434
|
```
|
|
426
435
|
|
|
427
|
-
|
|
436
|
+
Camera-off is a privacy action: the SDK detaches and stops the active camera
|
|
437
|
+
`MediaStreamTrack`, releasing the browser's camera capture. It does not merely
|
|
438
|
+
set `track.enabled = false`. Turning the camera back on explicitly requests a
|
|
439
|
+
new camera track and may trigger the browser's permission prompt if access was
|
|
440
|
+
revoked. Joining with `videoMuted: true` does not request camera access at all.
|
|
441
|
+
|
|
442
|
+
The local participant state exposes `media.micEnabled` and `media.camEnabled`,
|
|
443
|
+
so the button can reflect the current state. `toggleCam()` returns a Promise;
|
|
444
|
+
handle errors if camera permission is denied or the device is unavailable.
|
|
428
445
|
|
|
429
446
|
---
|
|
430
447
|
|
|
@@ -811,7 +828,7 @@ export function Meeting() {
|
|
|
811
828
|
<div>
|
|
812
829
|
<button onClick={() => void join()}>Join</button>
|
|
813
830
|
<button onClick={() => toggleMic()}>Mic</button>
|
|
814
|
-
<button onClick={() => toggleCam()}>Camera</button>
|
|
831
|
+
<button onClick={() => void toggleCam()}>Camera</button>
|
|
815
832
|
<button onClick={() => void startScreenShare()}>
|
|
816
833
|
Share screen
|
|
817
834
|
</button>
|
package/dist/index.d.mts
CHANGED
|
@@ -112,7 +112,7 @@ type Participant = {
|
|
|
112
112
|
type ParticipantMedia = {
|
|
113
113
|
stream?: MediaStream | null;
|
|
114
114
|
screenStream?: MediaStream | null;
|
|
115
|
-
cameraTrack?: MediaStreamTrack;
|
|
115
|
+
cameraTrack?: MediaStreamTrack | null;
|
|
116
116
|
screenTrack?: MediaStreamTrack;
|
|
117
117
|
audioTrack?: MediaStreamTrack;
|
|
118
118
|
micEnabled: boolean;
|
|
@@ -315,6 +315,9 @@ declare class VideoSDKCore {
|
|
|
315
315
|
private screenStream;
|
|
316
316
|
private isScreenSharing;
|
|
317
317
|
private participantName;
|
|
318
|
+
private cameraDeviceId?;
|
|
319
|
+
private cameraEnabled;
|
|
320
|
+
private cameraToggleInProgress;
|
|
318
321
|
readonly state: MeetingState;
|
|
319
322
|
private intentionalDisconnect;
|
|
320
323
|
private remoteStreams;
|
|
@@ -325,6 +328,13 @@ declare class VideoSDKCore {
|
|
|
325
328
|
private emitError;
|
|
326
329
|
constructor(options: VideoSDKCoreOptions, events?: Events, apiBase?: string, wsBase?: string);
|
|
327
330
|
initLocal(video: HTMLVideoElement, name: string): Promise<void>;
|
|
331
|
+
private hasLiveCameraTrack;
|
|
332
|
+
private getLiveCameraTrack;
|
|
333
|
+
/**
|
|
334
|
+
* Stop every locally captured camera track and remove it from the stream.
|
|
335
|
+
* Track.enabled=false only emits black frames; stop() releases camera capture.
|
|
336
|
+
*/
|
|
337
|
+
private stopCameraCapture;
|
|
328
338
|
private updateLocalMedia;
|
|
329
339
|
createRoom(input: {
|
|
330
340
|
name: string;
|
|
@@ -340,6 +350,7 @@ declare class VideoSDKCore {
|
|
|
340
350
|
summary: MeetingSummary | null;
|
|
341
351
|
}>;
|
|
342
352
|
private acquireOptionalLocalMedia;
|
|
353
|
+
private acquireCameraTrack;
|
|
343
354
|
/**
|
|
344
355
|
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
345
356
|
* whenever possible (Join, first click inside the meeting, etc.).
|
|
@@ -367,7 +378,7 @@ declare class VideoSDKCore {
|
|
|
367
378
|
selectCamera(deviceId: string): Promise<MediaStreamTrack>;
|
|
368
379
|
selectMicrophone(deviceId: string): Promise<MediaStreamTrack>;
|
|
369
380
|
toggleMic(): void;
|
|
370
|
-
toggleCam(): void
|
|
381
|
+
toggleCam(): Promise<void>;
|
|
371
382
|
startScreenShare(): Promise<MediaStream | null>;
|
|
372
383
|
stopScreenShare(): Promise<void>;
|
|
373
384
|
startRecording(): Promise<void>;
|
|
@@ -432,7 +443,7 @@ type MeetingContextValue = {
|
|
|
432
443
|
summary: MeetingSummary | null;
|
|
433
444
|
}>;
|
|
434
445
|
toggleMic: () => void;
|
|
435
|
-
toggleCam: () => void
|
|
446
|
+
toggleCam: () => Promise<void>;
|
|
436
447
|
resumeActiveSpeaker: () => Promise<boolean>;
|
|
437
448
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
438
449
|
stopScreenShare: () => void;
|
|
@@ -530,7 +541,7 @@ declare const useMeeting: (handlers?: {
|
|
|
530
541
|
summary: MeetingSummary | null;
|
|
531
542
|
}>;
|
|
532
543
|
toggleMic: () => void;
|
|
533
|
-
toggleCam: () => void
|
|
544
|
+
toggleCam: () => Promise<void>;
|
|
534
545
|
resumeActiveSpeaker: () => Promise<boolean>;
|
|
535
546
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
536
547
|
stopScreenShare: () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ type Participant = {
|
|
|
112
112
|
type ParticipantMedia = {
|
|
113
113
|
stream?: MediaStream | null;
|
|
114
114
|
screenStream?: MediaStream | null;
|
|
115
|
-
cameraTrack?: MediaStreamTrack;
|
|
115
|
+
cameraTrack?: MediaStreamTrack | null;
|
|
116
116
|
screenTrack?: MediaStreamTrack;
|
|
117
117
|
audioTrack?: MediaStreamTrack;
|
|
118
118
|
micEnabled: boolean;
|
|
@@ -315,6 +315,9 @@ declare class VideoSDKCore {
|
|
|
315
315
|
private screenStream;
|
|
316
316
|
private isScreenSharing;
|
|
317
317
|
private participantName;
|
|
318
|
+
private cameraDeviceId?;
|
|
319
|
+
private cameraEnabled;
|
|
320
|
+
private cameraToggleInProgress;
|
|
318
321
|
readonly state: MeetingState;
|
|
319
322
|
private intentionalDisconnect;
|
|
320
323
|
private remoteStreams;
|
|
@@ -325,6 +328,13 @@ declare class VideoSDKCore {
|
|
|
325
328
|
private emitError;
|
|
326
329
|
constructor(options: VideoSDKCoreOptions, events?: Events, apiBase?: string, wsBase?: string);
|
|
327
330
|
initLocal(video: HTMLVideoElement, name: string): Promise<void>;
|
|
331
|
+
private hasLiveCameraTrack;
|
|
332
|
+
private getLiveCameraTrack;
|
|
333
|
+
/**
|
|
334
|
+
* Stop every locally captured camera track and remove it from the stream.
|
|
335
|
+
* Track.enabled=false only emits black frames; stop() releases camera capture.
|
|
336
|
+
*/
|
|
337
|
+
private stopCameraCapture;
|
|
328
338
|
private updateLocalMedia;
|
|
329
339
|
createRoom(input: {
|
|
330
340
|
name: string;
|
|
@@ -340,6 +350,7 @@ declare class VideoSDKCore {
|
|
|
340
350
|
summary: MeetingSummary | null;
|
|
341
351
|
}>;
|
|
342
352
|
private acquireOptionalLocalMedia;
|
|
353
|
+
private acquireCameraTrack;
|
|
343
354
|
/**
|
|
344
355
|
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
345
356
|
* whenever possible (Join, first click inside the meeting, etc.).
|
|
@@ -367,7 +378,7 @@ declare class VideoSDKCore {
|
|
|
367
378
|
selectCamera(deviceId: string): Promise<MediaStreamTrack>;
|
|
368
379
|
selectMicrophone(deviceId: string): Promise<MediaStreamTrack>;
|
|
369
380
|
toggleMic(): void;
|
|
370
|
-
toggleCam(): void
|
|
381
|
+
toggleCam(): Promise<void>;
|
|
371
382
|
startScreenShare(): Promise<MediaStream | null>;
|
|
372
383
|
stopScreenShare(): Promise<void>;
|
|
373
384
|
startRecording(): Promise<void>;
|
|
@@ -432,7 +443,7 @@ type MeetingContextValue = {
|
|
|
432
443
|
summary: MeetingSummary | null;
|
|
433
444
|
}>;
|
|
434
445
|
toggleMic: () => void;
|
|
435
|
-
toggleCam: () => void
|
|
446
|
+
toggleCam: () => Promise<void>;
|
|
436
447
|
resumeActiveSpeaker: () => Promise<boolean>;
|
|
437
448
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
438
449
|
stopScreenShare: () => void;
|
|
@@ -530,7 +541,7 @@ declare const useMeeting: (handlers?: {
|
|
|
530
541
|
summary: MeetingSummary | null;
|
|
531
542
|
}>;
|
|
532
543
|
toggleMic: () => void;
|
|
533
|
-
toggleCam: () => void
|
|
544
|
+
toggleCam: () => Promise<void>;
|
|
534
545
|
resumeActiveSpeaker: () => Promise<boolean>;
|
|
535
546
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
536
547
|
stopScreenShare: () => void;
|
package/dist/index.js
CHANGED
|
@@ -417,7 +417,7 @@ var MeetingState = class {
|
|
|
417
417
|
const nextMedia = {
|
|
418
418
|
stream: patch.media?.stream ?? prevMedia.stream,
|
|
419
419
|
screenStream: patch.media?.screenStream ?? prevMedia.screenStream,
|
|
420
|
-
cameraTrack: patch.media
|
|
420
|
+
cameraTrack: patch.media && Object.prototype.hasOwnProperty.call(patch.media, "cameraTrack") ? patch.media.cameraTrack ?? void 0 : prevMedia.cameraTrack,
|
|
421
421
|
screenTrack: patch.media?.screenTrack ?? prevMedia.screenTrack,
|
|
422
422
|
audioTrack: patch.media?.audioTrack ?? prevMedia.audioTrack,
|
|
423
423
|
micEnabled: patch.media?.micEnabled ?? prevMedia.micEnabled,
|
|
@@ -504,9 +504,9 @@ var SFUClient = class {
|
|
|
504
504
|
this.publisherRevision = 0;
|
|
505
505
|
this.screenShareGeneration = 0;
|
|
506
506
|
this.currentParticipantId = null;
|
|
507
|
-
//
|
|
507
|
+
// Token supplied by the application for REST and SFU authentication.
|
|
508
508
|
this.authToken = null;
|
|
509
|
-
//
|
|
509
|
+
// Token used for the current WebSocket session.
|
|
510
510
|
this.token = null;
|
|
511
511
|
this.room = null;
|
|
512
512
|
this.resumeToken = null;
|
|
@@ -678,6 +678,71 @@ var SFUClient = class {
|
|
|
678
678
|
setLocalStream(stream) {
|
|
679
679
|
this.localStream = stream;
|
|
680
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Attach or detach the local camera track without replacing the publisher
|
|
683
|
+
* PeerConnection. Detaching with replaceTrack(null) stops media transmission;
|
|
684
|
+
* VideoSDKCore separately calls MediaStreamTrack.stop() to release hardware.
|
|
685
|
+
*/
|
|
686
|
+
async setCameraTrack(track) {
|
|
687
|
+
if (!this.localStream) this.localStream = new MediaStream();
|
|
688
|
+
const currentTrack = this.localStream.getVideoTracks().find((item) => item !== track);
|
|
689
|
+
if (currentTrack) this.localStream.removeTrack(currentTrack);
|
|
690
|
+
if (track && !this.localStream.getTracks().includes(track)) {
|
|
691
|
+
this.localStream.addTrack(track);
|
|
692
|
+
}
|
|
693
|
+
if (!this.videoSender || !this.publisher || !this.cameraTransceiver) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const needsCameraNegotiation = Boolean(
|
|
697
|
+
track && this.cameraTransceiver.direction === "inactive"
|
|
698
|
+
);
|
|
699
|
+
if (needsCameraNegotiation && this.ws?.readyState !== WebSocket.OPEN) {
|
|
700
|
+
throw new Error("Cannot enable the camera while signaling is disconnected");
|
|
701
|
+
}
|
|
702
|
+
await this.videoSender.replaceTrack(track);
|
|
703
|
+
if (needsCameraNegotiation) {
|
|
704
|
+
await this.waitForPublisherStable();
|
|
705
|
+
this.cameraTransceiver.direction = "sendonly";
|
|
706
|
+
await this.sendPublisherOffer();
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async waitForPublisherStable(timeoutMs = 5e3) {
|
|
710
|
+
const publisher = this.publisher;
|
|
711
|
+
if (!publisher) throw new Error("Publisher PeerConnection is not initialized");
|
|
712
|
+
const startedAt = Date.now();
|
|
713
|
+
while (publisher.signalingState !== "stable") {
|
|
714
|
+
if (publisher.signalingState === "closed") {
|
|
715
|
+
throw new Error("Publisher PeerConnection is closed");
|
|
716
|
+
}
|
|
717
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
718
|
+
throw new Error("Publisher negotiation did not become stable in time");
|
|
719
|
+
}
|
|
720
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async sendPublisherOffer() {
|
|
724
|
+
if (!this.publisher) throw new Error("Publisher PeerConnection is not initialized");
|
|
725
|
+
const offer = await this.publisher.createOffer();
|
|
726
|
+
await this.publisher.setLocalDescription(offer);
|
|
727
|
+
const sent = this.send({
|
|
728
|
+
type: "publish_offer",
|
|
729
|
+
payload: {
|
|
730
|
+
revision: ++this.publisherRevision,
|
|
731
|
+
sdp: offer.sdp
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (!sent) {
|
|
735
|
+
try {
|
|
736
|
+
await this.publisher.setLocalDescription({ type: "rollback" });
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
if (this.cameraTransceiver) {
|
|
740
|
+
this.cameraTransceiver.direction = "inactive";
|
|
741
|
+
}
|
|
742
|
+
await this.videoSender?.replaceTrack(null);
|
|
743
|
+
throw new Error("Unable to negotiate camera publishing: signaling is disconnected");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
681
746
|
nextRequestId() {
|
|
682
747
|
return (++this.requestId).toString();
|
|
683
748
|
}
|
|
@@ -732,7 +797,8 @@ var SFUClient = class {
|
|
|
732
797
|
}
|
|
733
798
|
);
|
|
734
799
|
}
|
|
735
|
-
async
|
|
800
|
+
async join(roomCode, options = {}) {
|
|
801
|
+
if (!this.authToken) throw new Error("Authentication token is required");
|
|
736
802
|
const nextRoom = roomCode.trim();
|
|
737
803
|
if (this.room && this.room !== nextRoom) {
|
|
738
804
|
this.resumeToken = null;
|
|
@@ -750,33 +816,7 @@ var SFUClient = class {
|
|
|
750
816
|
if (!this.resumeToken) {
|
|
751
817
|
this.resumeToken = this.loadResumeToken(this.room);
|
|
752
818
|
}
|
|
753
|
-
|
|
754
|
-
method: "POST",
|
|
755
|
-
body: JSON.stringify({
|
|
756
|
-
room_code: this.room,
|
|
757
|
-
user_id: this.participantId,
|
|
758
|
-
display_name: options.displayName,
|
|
759
|
-
avatar_url: options.avatarUrl
|
|
760
|
-
})
|
|
761
|
-
});
|
|
762
|
-
this.token = data?.token;
|
|
763
|
-
if (!this.token) throw new Error("Token endpoint returned no token");
|
|
764
|
-
return this.connect(this.token, this.joinOptions);
|
|
765
|
-
}
|
|
766
|
-
async refreshAccessToken() {
|
|
767
|
-
if (!this.room) throw new Error("Cannot refresh token without a room");
|
|
768
|
-
const data = await this.request(`${this.apiBase}/token`, {
|
|
769
|
-
method: "POST",
|
|
770
|
-
body: JSON.stringify({
|
|
771
|
-
room_code: this.room,
|
|
772
|
-
user_id: this.participantId,
|
|
773
|
-
display_name: this.joinOptions.displayName,
|
|
774
|
-
avatar_url: this.joinOptions.avatarUrl
|
|
775
|
-
})
|
|
776
|
-
});
|
|
777
|
-
if (!data?.token) throw new Error("Token refresh returned no token");
|
|
778
|
-
this.token = data.token;
|
|
779
|
-
return this.token;
|
|
819
|
+
return this.connect(this.authToken, this.joinOptions);
|
|
780
820
|
}
|
|
781
821
|
connect(token, options = {}) {
|
|
782
822
|
this.token = token;
|
|
@@ -866,15 +906,9 @@ var SFUClient = class {
|
|
|
866
906
|
this.reconnectBaseMs * Math.pow(2, this.reconnectAttempt - 1)
|
|
867
907
|
);
|
|
868
908
|
this.onReconnecting(this.reconnectAttempt);
|
|
869
|
-
this.reconnectTimer = setTimeout(
|
|
909
|
+
this.reconnectTimer = setTimeout(() => {
|
|
870
910
|
this.reconnectTimer = null;
|
|
871
|
-
|
|
872
|
-
await this.refreshAccessToken();
|
|
873
|
-
this.openWebSocket(this.joinOptions);
|
|
874
|
-
} catch (error) {
|
|
875
|
-
this.onError(error);
|
|
876
|
-
this.scheduleReconnect();
|
|
877
|
-
}
|
|
911
|
+
this.openWebSocket(this.joinOptions);
|
|
878
912
|
}, delay);
|
|
879
913
|
}
|
|
880
914
|
failJoin(error) {
|
|
@@ -2355,6 +2389,8 @@ var VideoSDKCore = class {
|
|
|
2355
2389
|
this.screenStream = null;
|
|
2356
2390
|
this.isScreenSharing = false;
|
|
2357
2391
|
this.participantName = "";
|
|
2392
|
+
this.cameraEnabled = false;
|
|
2393
|
+
this.cameraToggleInProgress = false;
|
|
2358
2394
|
this.intentionalDisconnect = false;
|
|
2359
2395
|
this.remoteStreams = /* @__PURE__ */ new Map();
|
|
2360
2396
|
this.remoteScreenStreams = /* @__PURE__ */ new Map();
|
|
@@ -2388,6 +2424,7 @@ var VideoSDKCore = class {
|
|
|
2388
2424
|
this.participantName = name;
|
|
2389
2425
|
try {
|
|
2390
2426
|
this.localStream = await this.acquireOptionalLocalMedia();
|
|
2427
|
+
this.cameraEnabled = this.hasLiveCameraTrack();
|
|
2391
2428
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2392
2429
|
if (localAudioTrack) {
|
|
2393
2430
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
@@ -2399,17 +2436,47 @@ var VideoSDKCore = class {
|
|
|
2399
2436
|
throw err;
|
|
2400
2437
|
}
|
|
2401
2438
|
}
|
|
2402
|
-
|
|
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() {
|
|
2403
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);
|
|
2404
2470
|
this.state.updateLocalParticipant({
|
|
2405
2471
|
id: this.myId,
|
|
2406
2472
|
name: this.participantName,
|
|
2407
2473
|
media: {
|
|
2408
2474
|
stream: this.localStream,
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
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,
|
|
2413
2480
|
isScreenSharing: this.isScreenSharing,
|
|
2414
2481
|
screenStream: this.screenStream,
|
|
2415
2482
|
screenTrack: this.screenStream?.getVideoTracks()[0]
|
|
@@ -2436,7 +2503,7 @@ var VideoSDKCore = class {
|
|
|
2436
2503
|
async getMeetingSummary(roomCode, sessionId) {
|
|
2437
2504
|
return this.sfuClient.getMeetingSummary(roomCode, sessionId);
|
|
2438
2505
|
}
|
|
2439
|
-
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
|
|
2506
|
+
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId, includeVideo = true) {
|
|
2440
2507
|
const combined = new MediaStream();
|
|
2441
2508
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2442
2509
|
console.warn(
|
|
@@ -2444,13 +2511,13 @@ var VideoSDKCore = class {
|
|
|
2444
2511
|
);
|
|
2445
2512
|
return combined;
|
|
2446
2513
|
}
|
|
2447
|
-
const cameraPromise = navigator.mediaDevices.getUserMedia({
|
|
2514
|
+
const cameraPromise = includeVideo ? navigator.mediaDevices.getUserMedia({
|
|
2448
2515
|
video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
|
|
2449
2516
|
width: { ideal: 1280 },
|
|
2450
2517
|
height: { ideal: 720 }
|
|
2451
2518
|
},
|
|
2452
2519
|
audio: false
|
|
2453
|
-
});
|
|
2520
|
+
}) : Promise.resolve(null);
|
|
2454
2521
|
const microphonePromise = navigator.mediaDevices.getUserMedia({
|
|
2455
2522
|
video: false,
|
|
2456
2523
|
audio: microphoneDeviceId ? {
|
|
@@ -2468,11 +2535,11 @@ var VideoSDKCore = class {
|
|
|
2468
2535
|
cameraPromise,
|
|
2469
2536
|
microphonePromise
|
|
2470
2537
|
]);
|
|
2471
|
-
if (cameraResult.status === "fulfilled") {
|
|
2538
|
+
if (cameraResult.status === "fulfilled" && cameraResult.value) {
|
|
2472
2539
|
for (const track of cameraResult.value.getVideoTracks()) {
|
|
2473
2540
|
combined.addTrack(track);
|
|
2474
2541
|
}
|
|
2475
|
-
} else {
|
|
2542
|
+
} else if (cameraResult.status === "rejected") {
|
|
2476
2543
|
console.warn("[SDK] Camera unavailable; continuing without video.", {
|
|
2477
2544
|
name: cameraResult.reason?.name,
|
|
2478
2545
|
message: cameraResult.reason?.message
|
|
@@ -2493,6 +2560,25 @@ var VideoSDKCore = class {
|
|
|
2493
2560
|
}
|
|
2494
2561
|
return combined;
|
|
2495
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
|
+
}
|
|
2496
2582
|
/**
|
|
2497
2583
|
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
2498
2584
|
* whenever possible (Join, first click inside the meeting, etc.).
|
|
@@ -2542,10 +2628,12 @@ var VideoSDKCore = class {
|
|
|
2542
2628
|
this.participantName = name;
|
|
2543
2629
|
this.room.id = roomId;
|
|
2544
2630
|
this.room.name = name;
|
|
2631
|
+
this.cameraDeviceId = cameraDeviceId ?? this.cameraDeviceId;
|
|
2545
2632
|
if (!this.localStream) {
|
|
2546
2633
|
this.localStream = await this.acquireOptionalLocalMedia(
|
|
2547
|
-
cameraDeviceId,
|
|
2548
|
-
microphoneDeviceId
|
|
2634
|
+
this.cameraDeviceId,
|
|
2635
|
+
microphoneDeviceId,
|
|
2636
|
+
!videoMuted
|
|
2549
2637
|
);
|
|
2550
2638
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2551
2639
|
if (localAudioTrack) {
|
|
@@ -2555,10 +2643,17 @@ var VideoSDKCore = class {
|
|
|
2555
2643
|
this.localStream.getAudioTracks().forEach((track) => {
|
|
2556
2644
|
track.enabled = !audioMuted;
|
|
2557
2645
|
});
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
}
|
|
2561
|
-
|
|
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();
|
|
2562
2657
|
const promise = this.connect(roomId, name);
|
|
2563
2658
|
this.joinPromise = promise;
|
|
2564
2659
|
try {
|
|
@@ -2588,7 +2683,7 @@ var VideoSDKCore = class {
|
|
|
2588
2683
|
);
|
|
2589
2684
|
}
|
|
2590
2685
|
this.sfuClient.setLocalStream(this.localStream);
|
|
2591
|
-
await this.sfuClient.
|
|
2686
|
+
await this.sfuClient.join(roomId, {
|
|
2592
2687
|
displayName: name,
|
|
2593
2688
|
participantId: this.myId,
|
|
2594
2689
|
audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
|
|
@@ -3185,7 +3280,12 @@ var VideoSDKCore = class {
|
|
|
3185
3280
|
if (!this.sfuClient) {
|
|
3186
3281
|
throw new Error("SFU client is not initialized");
|
|
3187
3282
|
}
|
|
3283
|
+
if (!this.cameraEnabled) {
|
|
3284
|
+
throw new Error("Your camera is off. Turn it on before switching devices.");
|
|
3285
|
+
}
|
|
3188
3286
|
const track = await this.sfuClient.selectCamera(deviceId);
|
|
3287
|
+
this.cameraDeviceId = deviceId;
|
|
3288
|
+
this.cameraEnabled = true;
|
|
3189
3289
|
if (this.localStream) {
|
|
3190
3290
|
this.state.localStream = this.localStream;
|
|
3191
3291
|
this.updateLocalMedia();
|
|
@@ -3227,24 +3327,52 @@ var VideoSDKCore = class {
|
|
|
3227
3327
|
this.sfuClient?.muteAudio(!nextEnabled);
|
|
3228
3328
|
this.events.onMicToggled?.(this.myId, nextEnabled);
|
|
3229
3329
|
}
|
|
3230
|
-
toggleCam() {
|
|
3231
|
-
|
|
3232
|
-
if (
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
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;
|
|
3244
3343
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
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
|
+
}
|
|
3248
3376
|
}
|
|
3249
3377
|
// SCREEN SHARING
|
|
3250
3378
|
async startScreenShare() {
|
|
@@ -3402,6 +3530,8 @@ var VideoSDKCore = class {
|
|
|
3402
3530
|
this.localStream.getTracks().forEach((track) => track.stop());
|
|
3403
3531
|
this.localStream = null;
|
|
3404
3532
|
}
|
|
3533
|
+
this.cameraEnabled = false;
|
|
3534
|
+
this.cameraToggleInProgress = false;
|
|
3405
3535
|
if (this.screenStream) {
|
|
3406
3536
|
this.screenStream.getTracks().forEach((track) => track.stop());
|
|
3407
3537
|
this.screenStream = null;
|