@defcomm/meetingsdk 0.6.7 → 0.6.9
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 +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +219 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -11,6 +11,7 @@ type Events = {
|
|
|
11
11
|
onUserJoined?: (p: Participant) => void;
|
|
12
12
|
/** Fired when a remote participant permanently joins the meeting. */
|
|
13
13
|
onParticipantJoined?: (p: Participant) => void;
|
|
14
|
+
onParticipantPresence?: (participantId: string, presence: string) => void;
|
|
14
15
|
onEntryRequested?: (req: EntryRequest) => void;
|
|
15
16
|
onEntryResponded?: (payload: {
|
|
16
17
|
participantId: string;
|
|
@@ -285,6 +286,12 @@ declare class MeetingState {
|
|
|
285
286
|
clearRecording(): void;
|
|
286
287
|
setNetworkQuality(quality: NetworkQuality | null): void;
|
|
287
288
|
addParticipant(p: Participant): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Reconcile a participant received from a room snapshot without emitting a
|
|
291
|
+
* lifecycle/join event. Snapshot data is authoritative for current room
|
|
292
|
+
* membership, including after a reconnect/resume.
|
|
293
|
+
*/
|
|
294
|
+
upsertParticipantSnapshot(p: Participant): void;
|
|
288
295
|
removeParticipant(id: string): void;
|
|
289
296
|
updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
|
|
290
297
|
updateLocalParticipant(patch: LocalParticipantPatch): void;
|
|
@@ -332,6 +339,12 @@ declare class VideoSDKCore {
|
|
|
332
339
|
getMeetingSummary(roomCode: string, sessionId: string): Promise<{
|
|
333
340
|
summary: MeetingSummary | null;
|
|
334
341
|
}>;
|
|
342
|
+
private acquireOptionalLocalMedia;
|
|
343
|
+
/**
|
|
344
|
+
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
345
|
+
* whenever possible (Join, first click inside the meeting, etc.).
|
|
346
|
+
*/
|
|
347
|
+
resumeActiveSpeaker(): Promise<boolean>;
|
|
335
348
|
joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
|
|
336
349
|
private connect;
|
|
337
350
|
private createSFUClient;
|
|
@@ -420,6 +433,7 @@ type MeetingContextValue = {
|
|
|
420
433
|
}>;
|
|
421
434
|
toggleMic: () => void;
|
|
422
435
|
toggleCam: () => void;
|
|
436
|
+
resumeActiveSpeaker: () => Promise<boolean>;
|
|
423
437
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
424
438
|
stopScreenShare: () => void;
|
|
425
439
|
sendMessage: (input: ChatInput) => void;
|
|
@@ -468,6 +482,7 @@ type MeetingContextValue = {
|
|
|
468
482
|
onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
|
|
469
483
|
onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
|
|
470
484
|
onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
|
|
485
|
+
onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
|
|
471
486
|
onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
|
|
472
487
|
};
|
|
473
488
|
declare const MeetingProvider: ({ config, children, }: {
|
|
@@ -516,6 +531,7 @@ declare const useMeeting: (handlers?: {
|
|
|
516
531
|
}>;
|
|
517
532
|
toggleMic: () => void;
|
|
518
533
|
toggleCam: () => void;
|
|
534
|
+
resumeActiveSpeaker: () => Promise<boolean>;
|
|
519
535
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
520
536
|
stopScreenShare: () => void;
|
|
521
537
|
sendMessage: (input: ChatInput) => void;
|
|
@@ -564,6 +580,7 @@ declare const useMeeting: (handlers?: {
|
|
|
564
580
|
onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
|
|
565
581
|
onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
|
|
566
582
|
onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
|
|
583
|
+
onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
|
|
567
584
|
onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
|
|
568
585
|
};
|
|
569
586
|
|
|
@@ -621,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
|
|
|
621
638
|
releaseMs?: number;
|
|
622
639
|
onActiveSpeakersChanged?: (participantIds: string[]) => void;
|
|
623
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Detects currently speaking participants from the actual audio tracks that
|
|
643
|
+
* are being received/published by WebRTC.
|
|
644
|
+
*
|
|
645
|
+
* The AudioContext is explicitly resumable so an application can wake it from
|
|
646
|
+
* a real user gesture (for example, the Join button). This matters in browsers
|
|
647
|
+
* that create Web Audio contexts in the "suspended" state until the page has
|
|
648
|
+
* received user interaction.
|
|
649
|
+
*/
|
|
624
650
|
declare class ActiveSpeakerDetector {
|
|
625
651
|
private audioContext;
|
|
626
652
|
private speakers;
|
|
@@ -633,7 +659,12 @@ declare class ActiveSpeakerDetector {
|
|
|
633
659
|
private lastEmitted;
|
|
634
660
|
constructor(options?: ActiveSpeakerDetectorOptions);
|
|
635
661
|
private ensureAudioContext;
|
|
636
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Create/wake the Web Audio context. Call this from a user gesture when
|
|
664
|
+
* possible. Returns true when the context is running after the call.
|
|
665
|
+
*/
|
|
666
|
+
resume(): Promise<boolean>;
|
|
667
|
+
getAudioContextState(): AudioContextState | "unavailable";
|
|
637
668
|
addTrack(participantId: string, track: MediaStreamTrack): void;
|
|
638
669
|
removeParticipant(participantId: string): void;
|
|
639
670
|
removeTrack(track: MediaStreamTrack): void;
|
|
@@ -641,6 +672,7 @@ declare class ActiveSpeakerDetector {
|
|
|
641
672
|
start(): void;
|
|
642
673
|
stop(): void;
|
|
643
674
|
private sample;
|
|
675
|
+
private getActiveSpeakers;
|
|
644
676
|
private getActiveSpeakerIds;
|
|
645
677
|
private emitIfChanged;
|
|
646
678
|
destroy(): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ type Events = {
|
|
|
11
11
|
onUserJoined?: (p: Participant) => void;
|
|
12
12
|
/** Fired when a remote participant permanently joins the meeting. */
|
|
13
13
|
onParticipantJoined?: (p: Participant) => void;
|
|
14
|
+
onParticipantPresence?: (participantId: string, presence: string) => void;
|
|
14
15
|
onEntryRequested?: (req: EntryRequest) => void;
|
|
15
16
|
onEntryResponded?: (payload: {
|
|
16
17
|
participantId: string;
|
|
@@ -285,6 +286,12 @@ declare class MeetingState {
|
|
|
285
286
|
clearRecording(): void;
|
|
286
287
|
setNetworkQuality(quality: NetworkQuality | null): void;
|
|
287
288
|
addParticipant(p: Participant): boolean;
|
|
289
|
+
/**
|
|
290
|
+
* Reconcile a participant received from a room snapshot without emitting a
|
|
291
|
+
* lifecycle/join event. Snapshot data is authoritative for current room
|
|
292
|
+
* membership, including after a reconnect/resume.
|
|
293
|
+
*/
|
|
294
|
+
upsertParticipantSnapshot(p: Participant): void;
|
|
288
295
|
removeParticipant(id: string): void;
|
|
289
296
|
updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
|
|
290
297
|
updateLocalParticipant(patch: LocalParticipantPatch): void;
|
|
@@ -332,6 +339,12 @@ declare class VideoSDKCore {
|
|
|
332
339
|
getMeetingSummary(roomCode: string, sessionId: string): Promise<{
|
|
333
340
|
summary: MeetingSummary | null;
|
|
334
341
|
}>;
|
|
342
|
+
private acquireOptionalLocalMedia;
|
|
343
|
+
/**
|
|
344
|
+
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
345
|
+
* whenever possible (Join, first click inside the meeting, etc.).
|
|
346
|
+
*/
|
|
347
|
+
resumeActiveSpeaker(): Promise<boolean>;
|
|
335
348
|
joinMeeting(config: Omit<MeetingConfig, "participantId">): Promise<void>;
|
|
336
349
|
private connect;
|
|
337
350
|
private createSFUClient;
|
|
@@ -420,6 +433,7 @@ type MeetingContextValue = {
|
|
|
420
433
|
}>;
|
|
421
434
|
toggleMic: () => void;
|
|
422
435
|
toggleCam: () => void;
|
|
436
|
+
resumeActiveSpeaker: () => Promise<boolean>;
|
|
423
437
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
424
438
|
stopScreenShare: () => void;
|
|
425
439
|
sendMessage: (input: ChatInput) => void;
|
|
@@ -468,6 +482,7 @@ type MeetingContextValue = {
|
|
|
468
482
|
onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
|
|
469
483
|
onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
|
|
470
484
|
onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
|
|
485
|
+
onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
|
|
471
486
|
onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
|
|
472
487
|
};
|
|
473
488
|
declare const MeetingProvider: ({ config, children, }: {
|
|
@@ -516,6 +531,7 @@ declare const useMeeting: (handlers?: {
|
|
|
516
531
|
}>;
|
|
517
532
|
toggleMic: () => void;
|
|
518
533
|
toggleCam: () => void;
|
|
534
|
+
resumeActiveSpeaker: () => Promise<boolean>;
|
|
519
535
|
startScreenShare: () => Promise<MediaStream | null>;
|
|
520
536
|
stopScreenShare: () => void;
|
|
521
537
|
sendMessage: (input: ChatInput) => void;
|
|
@@ -564,6 +580,7 @@ declare const useMeeting: (handlers?: {
|
|
|
564
580
|
onNetworkQuality: (cb: (quality: NetworkQuality) => void) => () => void;
|
|
565
581
|
onChatMessage: (cb: (message: ChatMessage) => void) => () => void;
|
|
566
582
|
onParticipantJoined: (cb: (participant: Participant) => void) => () => void;
|
|
583
|
+
onParticipantPresence: (cb: (participantId: string, presence: string) => void) => () => void;
|
|
567
584
|
onParticipantLeft: (cb: (participant: Participant) => void) => () => void;
|
|
568
585
|
};
|
|
569
586
|
|
|
@@ -621,6 +638,15 @@ interface ActiveSpeakerDetectorOptions {
|
|
|
621
638
|
releaseMs?: number;
|
|
622
639
|
onActiveSpeakersChanged?: (participantIds: string[]) => void;
|
|
623
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Detects currently speaking participants from the actual audio tracks that
|
|
643
|
+
* are being received/published by WebRTC.
|
|
644
|
+
*
|
|
645
|
+
* The AudioContext is explicitly resumable so an application can wake it from
|
|
646
|
+
* a real user gesture (for example, the Join button). This matters in browsers
|
|
647
|
+
* that create Web Audio contexts in the "suspended" state until the page has
|
|
648
|
+
* received user interaction.
|
|
649
|
+
*/
|
|
624
650
|
declare class ActiveSpeakerDetector {
|
|
625
651
|
private audioContext;
|
|
626
652
|
private speakers;
|
|
@@ -633,7 +659,12 @@ declare class ActiveSpeakerDetector {
|
|
|
633
659
|
private lastEmitted;
|
|
634
660
|
constructor(options?: ActiveSpeakerDetectorOptions);
|
|
635
661
|
private ensureAudioContext;
|
|
636
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Create/wake the Web Audio context. Call this from a user gesture when
|
|
664
|
+
* possible. Returns true when the context is running after the call.
|
|
665
|
+
*/
|
|
666
|
+
resume(): Promise<boolean>;
|
|
667
|
+
getAudioContextState(): AudioContextState | "unavailable";
|
|
637
668
|
addTrack(participantId: string, track: MediaStreamTrack): void;
|
|
638
669
|
removeParticipant(participantId: string): void;
|
|
639
670
|
removeTrack(track: MediaStreamTrack): void;
|
|
@@ -641,6 +672,7 @@ declare class ActiveSpeakerDetector {
|
|
|
641
672
|
start(): void;
|
|
642
673
|
stop(): void;
|
|
643
674
|
private sample;
|
|
675
|
+
private getActiveSpeakers;
|
|
644
676
|
private getActiveSpeakerIds;
|
|
645
677
|
private emitIfChanged;
|
|
646
678
|
destroy(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -63,13 +63,16 @@ var ActiveSpeakerDetector = class {
|
|
|
63
63
|
this.timer = null;
|
|
64
64
|
this.lastEmitted = "";
|
|
65
65
|
this.intervalMs = options.intervalMs ?? 100;
|
|
66
|
-
this.threshold = options.threshold ?? 0.
|
|
67
|
-
this.releaseThreshold = options.releaseThreshold ?? 0.
|
|
68
|
-
this.releaseMs = options.releaseMs ??
|
|
66
|
+
this.threshold = options.threshold ?? 0.02;
|
|
67
|
+
this.releaseThreshold = options.releaseThreshold ?? 0.01;
|
|
68
|
+
this.releaseMs = options.releaseMs ?? 650;
|
|
69
69
|
this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
ensureAudioContext() {
|
|
73
|
+
if (typeof window === "undefined") {
|
|
74
|
+
throw new Error("AudioContext is only available in the browser");
|
|
75
|
+
}
|
|
73
76
|
if (!this.audioContext) {
|
|
74
77
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
75
78
|
if (!AudioContextClass) {
|
|
@@ -79,28 +82,52 @@ var ActiveSpeakerDetector = class {
|
|
|
79
82
|
}
|
|
80
83
|
return this.audioContext;
|
|
81
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Create/wake the Web Audio context. Call this from a user gesture when
|
|
87
|
+
* possible. Returns true when the context is running after the call.
|
|
88
|
+
*/
|
|
82
89
|
async resume() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
try {
|
|
91
|
+
const context = this.ensureAudioContext();
|
|
92
|
+
if (context.state === "suspended") {
|
|
93
|
+
try {
|
|
94
|
+
await context.resume();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn("[ActiveSpeaker] AudioContext resume failed", error);
|
|
97
|
+
}
|
|
88
98
|
}
|
|
99
|
+
return context.state === "running";
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn("[ActiveSpeaker] AudioContext unavailable", error);
|
|
102
|
+
return false;
|
|
89
103
|
}
|
|
90
104
|
}
|
|
105
|
+
getAudioContextState() {
|
|
106
|
+
if (!this.audioContext) return "unavailable";
|
|
107
|
+
return this.audioContext.state;
|
|
108
|
+
}
|
|
91
109
|
addTrack(participantId, track) {
|
|
92
|
-
if (!participantId) return;
|
|
93
|
-
if (track.kind !== "audio") return;
|
|
110
|
+
if (!participantId || track.kind !== "audio") return;
|
|
94
111
|
this.removeParticipant(participantId);
|
|
95
|
-
|
|
112
|
+
let context;
|
|
113
|
+
try {
|
|
114
|
+
context = this.ensureAudioContext();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn("[ActiveSpeaker] Could not create AudioContext", error);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
96
119
|
const stream = new MediaStream([track]);
|
|
97
120
|
const source = context.createMediaStreamSource(stream);
|
|
98
121
|
const analyser = context.createAnalyser();
|
|
99
122
|
analyser.fftSize = 512;
|
|
100
|
-
analyser.smoothingTimeConstant = 0.
|
|
123
|
+
analyser.smoothingTimeConstant = 0.35;
|
|
101
124
|
analyser.minDecibels = -90;
|
|
102
125
|
analyser.maxDecibels = -10;
|
|
103
126
|
source.connect(analyser);
|
|
127
|
+
const sink = context.createGain();
|
|
128
|
+
sink.gain.value = 0;
|
|
129
|
+
analyser.connect(sink);
|
|
130
|
+
sink.connect(context.destination);
|
|
104
131
|
const data = new Float32Array(analyser.fftSize);
|
|
105
132
|
const state = {
|
|
106
133
|
participantId,
|
|
@@ -108,15 +135,20 @@ var ActiveSpeakerDetector = class {
|
|
|
108
135
|
stream,
|
|
109
136
|
source,
|
|
110
137
|
analyser,
|
|
138
|
+
sink,
|
|
111
139
|
data,
|
|
112
140
|
active: false,
|
|
141
|
+
level: 0,
|
|
113
142
|
lastActiveAt: 0
|
|
114
143
|
};
|
|
115
144
|
this.speakers.set(participantId, state);
|
|
116
145
|
track.addEventListener(
|
|
117
146
|
"ended",
|
|
118
147
|
() => {
|
|
119
|
-
this.
|
|
148
|
+
const current = this.speakers.get(participantId);
|
|
149
|
+
if (current?.track === track) {
|
|
150
|
+
this.removeParticipant(participantId);
|
|
151
|
+
}
|
|
120
152
|
},
|
|
121
153
|
{ once: true }
|
|
122
154
|
);
|
|
@@ -127,15 +159,19 @@ var ActiveSpeakerDetector = class {
|
|
|
127
159
|
const state = this.speakers.get(participantId);
|
|
128
160
|
if (!state) return;
|
|
129
161
|
state.active = false;
|
|
162
|
+
state.level = 0;
|
|
130
163
|
try {
|
|
131
164
|
state.source.disconnect();
|
|
132
165
|
} catch {
|
|
133
166
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
167
|
+
try {
|
|
168
|
+
state.analyser.disconnect();
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
state.sink.disconnect();
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
139
175
|
this.speakers.delete(participantId);
|
|
140
176
|
this.emitIfChanged();
|
|
141
177
|
}
|
|
@@ -147,7 +183,7 @@ var ActiveSpeakerDetector = class {
|
|
|
147
183
|
}
|
|
148
184
|
}
|
|
149
185
|
clear() {
|
|
150
|
-
for (const participantId of this.speakers.keys()) {
|
|
186
|
+
for (const participantId of [...this.speakers.keys()]) {
|
|
151
187
|
this.removeParticipant(participantId);
|
|
152
188
|
}
|
|
153
189
|
this.speakers.clear();
|
|
@@ -167,6 +203,7 @@ var ActiveSpeakerDetector = class {
|
|
|
167
203
|
const now = performance.now();
|
|
168
204
|
for (const state of this.speakers.values()) {
|
|
169
205
|
if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
|
|
206
|
+
state.level = 0;
|
|
170
207
|
if (state.active) {
|
|
171
208
|
state.active = false;
|
|
172
209
|
}
|
|
@@ -179,30 +216,33 @@ var ActiveSpeakerDetector = class {
|
|
|
179
216
|
sumSquares += sample * sample;
|
|
180
217
|
}
|
|
181
218
|
const rms = Math.sqrt(sumSquares / state.data.length);
|
|
219
|
+
state.level = rms;
|
|
182
220
|
if (!state.active) {
|
|
183
221
|
if (rms >= this.threshold) {
|
|
184
222
|
state.active = true;
|
|
185
223
|
state.lastActiveAt = now;
|
|
186
224
|
}
|
|
187
|
-
} else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
state.active = false;
|
|
192
|
-
}
|
|
225
|
+
} else if (rms >= this.releaseThreshold) {
|
|
226
|
+
state.lastActiveAt = now;
|
|
227
|
+
} else if (now - state.lastActiveAt >= this.releaseMs) {
|
|
228
|
+
state.active = false;
|
|
193
229
|
}
|
|
194
230
|
}
|
|
195
231
|
this.emitIfChanged();
|
|
196
232
|
}
|
|
233
|
+
getActiveSpeakers() {
|
|
234
|
+
return Array.from(this.speakers.values()).filter((speaker) => speaker.active).sort((a, b) => {
|
|
235
|
+
if (b.level !== a.level) return b.level - a.level;
|
|
236
|
+
return a.participantId.localeCompare(b.participantId);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
197
239
|
getActiveSpeakerIds() {
|
|
198
|
-
return
|
|
240
|
+
return this.getActiveSpeakers().map((speaker) => speaker.participantId);
|
|
199
241
|
}
|
|
200
242
|
emitIfChanged(force = false) {
|
|
201
|
-
const ids = this.getActiveSpeakerIds()
|
|
243
|
+
const ids = this.getActiveSpeakerIds();
|
|
202
244
|
const key = ids.join("|");
|
|
203
|
-
if (!force && key === this.lastEmitted)
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
245
|
+
if (!force && key === this.lastEmitted) return;
|
|
206
246
|
this.lastEmitted = key;
|
|
207
247
|
this.onActiveSpeakersChanged(ids);
|
|
208
248
|
}
|
|
@@ -279,6 +319,36 @@ var MeetingState = class {
|
|
|
279
319
|
this.notify("participants");
|
|
280
320
|
return true;
|
|
281
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Reconcile a participant received from a room snapshot without emitting a
|
|
324
|
+
* lifecycle/join event. Snapshot data is authoritative for current room
|
|
325
|
+
* membership, including after a reconnect/resume.
|
|
326
|
+
*/
|
|
327
|
+
upsertParticipantSnapshot(p) {
|
|
328
|
+
if (!p?.id) return;
|
|
329
|
+
const current = this.participants.get(p.id);
|
|
330
|
+
const updated = {
|
|
331
|
+
...current ?? {},
|
|
332
|
+
...p,
|
|
333
|
+
media: {
|
|
334
|
+
stream: current?.media?.stream ?? null,
|
|
335
|
+
screenStream: current?.media?.screenStream,
|
|
336
|
+
cameraTrack: current?.media?.cameraTrack,
|
|
337
|
+
screenTrack: current?.media?.screenTrack,
|
|
338
|
+
audioTrack: current?.media?.audioTrack,
|
|
339
|
+
micEnabled: current?.media?.micEnabled ?? !(p.media?.micEnabled === false),
|
|
340
|
+
camEnabled: current?.media?.camEnabled ?? !(p.media?.camEnabled === false),
|
|
341
|
+
isScreenSharing: current?.media?.isScreenSharing ?? false,
|
|
342
|
+
...current?.media,
|
|
343
|
+
...p.media
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const next = new Map(this.participants);
|
|
347
|
+
next.set(p.id, updated);
|
|
348
|
+
this.participants = next;
|
|
349
|
+
this.notify(`participant:${p.id}`);
|
|
350
|
+
this.notify("participants");
|
|
351
|
+
}
|
|
282
352
|
removeParticipant(id) {
|
|
283
353
|
const next = new Map(this.participants);
|
|
284
354
|
next.delete(id);
|
|
@@ -1243,7 +1313,6 @@ var SFUClient = class {
|
|
|
1243
1313
|
}
|
|
1244
1314
|
for (const participant of this.participants.values()) {
|
|
1245
1315
|
if (participant.participant_id !== this.currentParticipantId) {
|
|
1246
|
-
this.onParticipantJoined(participant);
|
|
1247
1316
|
this.onParticipantPresence(
|
|
1248
1317
|
participant.participant_id,
|
|
1249
1318
|
String(participant.presence || "connected")
|
|
@@ -2318,26 +2387,11 @@ var VideoSDKCore = class {
|
|
|
2318
2387
|
async initLocal(video, name) {
|
|
2319
2388
|
this.participantName = name;
|
|
2320
2389
|
try {
|
|
2321
|
-
this.localStream = await
|
|
2322
|
-
video: {
|
|
2323
|
-
width: { ideal: 1280 },
|
|
2324
|
-
height: { ideal: 720 }
|
|
2325
|
-
},
|
|
2326
|
-
audio: {
|
|
2327
|
-
echoCancellation: true,
|
|
2328
|
-
noiseSuppression: true,
|
|
2329
|
-
autoGainControl: true
|
|
2330
|
-
}
|
|
2331
|
-
});
|
|
2390
|
+
this.localStream = await this.acquireOptionalLocalMedia();
|
|
2332
2391
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2333
2392
|
if (localAudioTrack) {
|
|
2334
2393
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
2335
2394
|
}
|
|
2336
|
-
const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
|
|
2337
|
-
const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
|
|
2338
|
-
if (!hasVideo || !hasAudio) {
|
|
2339
|
-
throw new Error(`Missing tracks: video=${hasVideo}, audio=${hasAudio}`);
|
|
2340
|
-
}
|
|
2341
2395
|
video.srcObject = this.localStream;
|
|
2342
2396
|
this.updateLocalMedia();
|
|
2343
2397
|
} catch (err) {
|
|
@@ -2354,8 +2408,8 @@ var VideoSDKCore = class {
|
|
|
2354
2408
|
stream: this.localStream,
|
|
2355
2409
|
cameraTrack: this.localStream.getVideoTracks()[0],
|
|
2356
2410
|
audioTrack: this.localStream.getAudioTracks()[0],
|
|
2357
|
-
micEnabled: this.localStream.getAudioTracks()[0]?.enabled ??
|
|
2358
|
-
camEnabled: this.localStream.getVideoTracks()[0]?.enabled ??
|
|
2411
|
+
micEnabled: this.localStream.getAudioTracks()[0]?.enabled ?? false,
|
|
2412
|
+
camEnabled: this.localStream.getVideoTracks()[0]?.enabled ?? false,
|
|
2359
2413
|
isScreenSharing: this.isScreenSharing,
|
|
2360
2414
|
screenStream: this.screenStream,
|
|
2361
2415
|
screenTrack: this.screenStream?.getVideoTracks()[0]
|
|
@@ -2382,6 +2436,70 @@ var VideoSDKCore = class {
|
|
|
2382
2436
|
async getMeetingSummary(roomCode, sessionId) {
|
|
2383
2437
|
return this.sfuClient.getMeetingSummary(roomCode, sessionId);
|
|
2384
2438
|
}
|
|
2439
|
+
async acquireOptionalLocalMedia(cameraDeviceId, microphoneDeviceId) {
|
|
2440
|
+
const combined = new MediaStream();
|
|
2441
|
+
if (!navigator.mediaDevices?.getUserMedia) {
|
|
2442
|
+
console.warn(
|
|
2443
|
+
"[SDK] Camera/microphone access is not supported; joining without local media."
|
|
2444
|
+
);
|
|
2445
|
+
return combined;
|
|
2446
|
+
}
|
|
2447
|
+
const cameraPromise = navigator.mediaDevices.getUserMedia({
|
|
2448
|
+
video: cameraDeviceId ? { deviceId: { exact: cameraDeviceId } } : {
|
|
2449
|
+
width: { ideal: 1280 },
|
|
2450
|
+
height: { ideal: 720 }
|
|
2451
|
+
},
|
|
2452
|
+
audio: false
|
|
2453
|
+
});
|
|
2454
|
+
const microphonePromise = navigator.mediaDevices.getUserMedia({
|
|
2455
|
+
video: false,
|
|
2456
|
+
audio: microphoneDeviceId ? {
|
|
2457
|
+
deviceId: { exact: microphoneDeviceId },
|
|
2458
|
+
echoCancellation: true,
|
|
2459
|
+
noiseSuppression: true,
|
|
2460
|
+
autoGainControl: true
|
|
2461
|
+
} : {
|
|
2462
|
+
echoCancellation: true,
|
|
2463
|
+
noiseSuppression: true,
|
|
2464
|
+
autoGainControl: true
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
const [cameraResult, microphoneResult] = await Promise.allSettled([
|
|
2468
|
+
cameraPromise,
|
|
2469
|
+
microphonePromise
|
|
2470
|
+
]);
|
|
2471
|
+
if (cameraResult.status === "fulfilled") {
|
|
2472
|
+
for (const track of cameraResult.value.getVideoTracks()) {
|
|
2473
|
+
combined.addTrack(track);
|
|
2474
|
+
}
|
|
2475
|
+
} else {
|
|
2476
|
+
console.warn("[SDK] Camera unavailable; continuing without video.", {
|
|
2477
|
+
name: cameraResult.reason?.name,
|
|
2478
|
+
message: cameraResult.reason?.message
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
if (microphoneResult.status === "fulfilled") {
|
|
2482
|
+
for (const track of microphoneResult.value.getAudioTracks()) {
|
|
2483
|
+
combined.addTrack(track);
|
|
2484
|
+
}
|
|
2485
|
+
} else {
|
|
2486
|
+
console.warn(
|
|
2487
|
+
"[SDK] Microphone unavailable; continuing without audio.",
|
|
2488
|
+
{
|
|
2489
|
+
name: microphoneResult.reason?.name,
|
|
2490
|
+
message: microphoneResult.reason?.message
|
|
2491
|
+
}
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
return combined;
|
|
2495
|
+
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Wakes the active-speaker Web Audio pipeline. Call this from a user gesture
|
|
2498
|
+
* whenever possible (Join, first click inside the meeting, etc.).
|
|
2499
|
+
*/
|
|
2500
|
+
async resumeActiveSpeaker() {
|
|
2501
|
+
return this.activeSpeakerDetector.resume();
|
|
2502
|
+
}
|
|
2385
2503
|
// JOIN
|
|
2386
2504
|
async joinMeeting(config) {
|
|
2387
2505
|
const {
|
|
@@ -2425,15 +2543,10 @@ var VideoSDKCore = class {
|
|
|
2425
2543
|
this.room.id = roomId;
|
|
2426
2544
|
this.room.name = name;
|
|
2427
2545
|
if (!this.localStream) {
|
|
2428
|
-
this.localStream = await
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
echoCancellation: true,
|
|
2433
|
-
noiseSuppression: true,
|
|
2434
|
-
autoGainControl: true
|
|
2435
|
-
} : true
|
|
2436
|
-
});
|
|
2546
|
+
this.localStream = await this.acquireOptionalLocalMedia(
|
|
2547
|
+
cameraDeviceId,
|
|
2548
|
+
microphoneDeviceId
|
|
2549
|
+
);
|
|
2437
2550
|
const localAudioTrack = this.localStream.getAudioTracks()[0];
|
|
2438
2551
|
if (localAudioTrack) {
|
|
2439
2552
|
this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
|
|
@@ -2478,8 +2591,8 @@ var VideoSDKCore = class {
|
|
|
2478
2591
|
await this.sfuClient.getToken(roomId, {
|
|
2479
2592
|
displayName: name,
|
|
2480
2593
|
participantId: this.myId,
|
|
2481
|
-
audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ??
|
|
2482
|
-
videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ??
|
|
2594
|
+
audioMuted: !(this.localStream?.getAudioTracks()[0]?.enabled ?? false),
|
|
2595
|
+
videoMuted: !(this.localStream?.getVideoTracks()[0]?.enabled ?? false)
|
|
2483
2596
|
});
|
|
2484
2597
|
}
|
|
2485
2598
|
createSFUClient(token) {
|
|
@@ -2528,6 +2641,36 @@ var VideoSDKCore = class {
|
|
|
2528
2641
|
},
|
|
2529
2642
|
onRoomUpdate: (payload) => {
|
|
2530
2643
|
const participants = payload?.participants || [];
|
|
2644
|
+
const snapshotParticipantIds = /* @__PURE__ */ new Set();
|
|
2645
|
+
for (const participant of participants) {
|
|
2646
|
+
const pid = participant?.participant_id;
|
|
2647
|
+
if (!pid || pid === this.myId) continue;
|
|
2648
|
+
snapshotParticipantIds.add(pid);
|
|
2649
|
+
const current = this.state.getParticipant(pid);
|
|
2650
|
+
const name = participant?.user_metadata?.name || participant?.display_name || current?.name || "Participant";
|
|
2651
|
+
this.state.upsertParticipantSnapshot({
|
|
2652
|
+
id: pid,
|
|
2653
|
+
name,
|
|
2654
|
+
isModerator: Boolean(participant?.permissions?.moderate),
|
|
2655
|
+
isHost: Boolean(
|
|
2656
|
+
participant?.is_host ?? participant?.isHost ?? participant?.permissions?.host
|
|
2657
|
+
),
|
|
2658
|
+
handRaised: Boolean(participant?.hand_raised),
|
|
2659
|
+
permissions: participant?.permissions,
|
|
2660
|
+
connectionState: participant?.presence === "reconnecting" ? "reconnecting" : participant?.presence === "disconnected" ? "disconnected" : "connected",
|
|
2661
|
+
media: {
|
|
2662
|
+
...current?.media || {},
|
|
2663
|
+
micEnabled: !Boolean(participant?.audio_muted),
|
|
2664
|
+
camEnabled: !Boolean(participant?.video_muted),
|
|
2665
|
+
isScreenSharing: Boolean(participant?.screen_sharing)
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
for (const [pid] of this.state.participants) {
|
|
2670
|
+
if (pid === this.myId || snapshotParticipantIds.has(pid)) continue;
|
|
2671
|
+
this.cleanupRemoteParticipant(pid);
|
|
2672
|
+
this.state.removeParticipant(pid);
|
|
2673
|
+
}
|
|
2531
2674
|
const local = participants.find(
|
|
2532
2675
|
(p) => p?.participant_id === this.myId
|
|
2533
2676
|
);
|
|
@@ -3341,6 +3484,9 @@ var MeetingProvider = ({
|
|
|
3341
3484
|
const participantJoinedListeners = (0, import_react2.useRef)(
|
|
3342
3485
|
/* @__PURE__ */ new Set()
|
|
3343
3486
|
);
|
|
3487
|
+
const participantPresenceListeners = (0, import_react2.useRef)(
|
|
3488
|
+
/* @__PURE__ */ new Set()
|
|
3489
|
+
);
|
|
3344
3490
|
const participantLeftListeners = (0, import_react2.useRef)(
|
|
3345
3491
|
/* @__PURE__ */ new Set()
|
|
3346
3492
|
);
|
|
@@ -3429,7 +3575,9 @@ var MeetingProvider = ({
|
|
|
3429
3575
|
pubSubListeners.current.get("REACTION")?.forEach((fn) => fn(message));
|
|
3430
3576
|
},
|
|
3431
3577
|
onHandRaised: (participantId, raised) => {
|
|
3432
|
-
handRaisedListeners.current.forEach(
|
|
3578
|
+
handRaisedListeners.current.forEach(
|
|
3579
|
+
(fn) => fn(participantId, raised)
|
|
3580
|
+
);
|
|
3433
3581
|
},
|
|
3434
3582
|
onNetworkQuality: (quality) => {
|
|
3435
3583
|
networkQualityListeners.current.forEach((fn) => fn(quality));
|
|
@@ -3442,6 +3590,11 @@ var MeetingProvider = ({
|
|
|
3442
3590
|
onParticipantJoined: (participant) => {
|
|
3443
3591
|
participantJoinedListeners.current.forEach((fn) => fn(participant));
|
|
3444
3592
|
},
|
|
3593
|
+
onParticipantPresence: (participantId, presence) => {
|
|
3594
|
+
participantPresenceListeners.current.forEach(
|
|
3595
|
+
(fn) => fn(participantId, presence)
|
|
3596
|
+
);
|
|
3597
|
+
},
|
|
3445
3598
|
onParticipantLeft: (participant) => {
|
|
3446
3599
|
participantLeftListeners.current.forEach((fn) => fn(participant));
|
|
3447
3600
|
}
|
|
@@ -3499,6 +3652,7 @@ var MeetingProvider = ({
|
|
|
3499
3652
|
getMeetingSummary: sdk.getMeetingSummary.bind(sdk),
|
|
3500
3653
|
toggleMic: sdk.toggleMic.bind(sdk),
|
|
3501
3654
|
toggleCam: sdk.toggleCam.bind(sdk),
|
|
3655
|
+
resumeActiveSpeaker: sdk.resumeActiveSpeaker.bind(sdk),
|
|
3502
3656
|
startScreenShare: sdk.startScreenShare.bind(sdk),
|
|
3503
3657
|
stopScreenShare: sdk.stopScreenShare.bind(sdk),
|
|
3504
3658
|
sendMessage: sdk.sendChatMessage.bind(sdk),
|
|
@@ -3654,6 +3808,10 @@ var MeetingProvider = ({
|
|
|
3654
3808
|
participantJoinedListeners.current.add(cb);
|
|
3655
3809
|
return () => participantJoinedListeners.current.delete(cb);
|
|
3656
3810
|
},
|
|
3811
|
+
onParticipantPresence: (cb) => {
|
|
3812
|
+
participantPresenceListeners.current.add(cb);
|
|
3813
|
+
return () => participantPresenceListeners.current.delete(cb);
|
|
3814
|
+
},
|
|
3657
3815
|
onChatMessage: (cb) => {
|
|
3658
3816
|
chatMessageListeners.current.add(cb);
|
|
3659
3817
|
return () => chatMessageListeners.current.delete(cb);
|