@defcomm/meetingsdk 0.1.0 → 0.1.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/dist/index.d.mts CHANGED
@@ -14,6 +14,7 @@ type Events = {
14
14
  participantId: string;
15
15
  decision: EntryDecision;
16
16
  } | string, decision?: EntryDecision) => void;
17
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
17
18
  onJoinApproved?: (requestId: string) => void;
18
19
  onJoinRejected?: (requestId: string) => void;
19
20
  onConnectionStateChanged?: (state: string) => void;
@@ -82,6 +83,7 @@ type ChatInput = {
82
83
  };
83
84
  type MeetingConfig = {
84
85
  roomId: string;
86
+ userId?: string;
85
87
  name: string;
86
88
  audioMuted?: boolean;
87
89
  videoMuted?: boolean;
@@ -94,7 +96,7 @@ type RecordingInfo = {
94
96
  stopped_at?: string;
95
97
  active?: boolean;
96
98
  };
97
- type StateScope = "participants" | "localParticipant" | "chat" | "presenter" | `participant:${string}`;
99
+ type StateScope = "participants" | "localParticipant" | "chat" | "presenter" | "activeSpeakers" | `participant:${string}`;
98
100
  type SDKError = {
99
101
  code: string;
100
102
  message: string;
@@ -115,6 +117,10 @@ interface TrackDescriptor {
115
117
  source: "camera" | "screen" | "audio";
116
118
  mid?: string;
117
119
  }
120
+ interface Recording {
121
+ started_at?: string;
122
+ recording_id?: string;
123
+ }
118
124
 
119
125
  declare const useLocalParticipant: () => {
120
126
  participant: Participant | null;
@@ -132,10 +138,12 @@ declare class MeetingState {
132
138
  localStream: MediaStream | null;
133
139
  chatMessages: Map<string, ChatMessage>;
134
140
  presenterId: string | null;
141
+ activeSpeakerIds: Set<string>;
135
142
  private listeners;
136
143
  subscribe(scope: StateScope, fn: Listener): () => void;
137
144
  notify(scope: StateScope): void;
138
145
  setPresenterId(id: string | null): void;
146
+ setActiveSpeakers(ids: string[]): void;
139
147
  addParticipant(p: Participant): boolean;
140
148
  removeParticipant(id: string): void;
141
149
  updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
@@ -163,6 +171,7 @@ declare class VideoSDKCore {
163
171
  private intentionalDisconnect;
164
172
  private remoteStreams;
165
173
  private remoteScreenStreams;
174
+ private activeSpeakerDetector;
166
175
  private emitError;
167
176
  constructor(events?: Events, apiBase?: string, wsBase?: string);
168
177
  initLocal(video: HTMLVideoElement, name: string): Promise<void>;
@@ -426,6 +435,7 @@ type MeetingContextValue = {
426
435
  participants: Map<string, Participant>;
427
436
  messages: ChatMessage[];
428
437
  presenterId: string | null;
438
+ activeSpeakerIds: string[];
429
439
  usePubSub: (topic: PubSubTopic) => PubSubHandle;
430
440
  approveJoinRequest: (requestId: string) => void;
431
441
  rejectJoinRequest: (requestId: string) => void;
@@ -437,8 +447,9 @@ type MeetingContextValue = {
437
447
  onEntryResponded: (cb: (payload: any, decision?: any) => void) => () => void;
438
448
  onMeetingLeft: (cb: () => void) => () => void;
439
449
  onTrackStateChanged: (cb: (track: any) => void) => () => void;
440
- onRecordingStarted: (cb: (recording: any) => void) => () => void;
450
+ onRecordingStarted: (cb: (recording: Recording) => void) => () => void;
441
451
  onRecordingStopped: (cb: (recording: any) => void) => () => void;
452
+ onActiveSpeakersChanged: (cb: (participantIds: string[]) => void) => () => void;
442
453
  };
443
454
  declare const MeetingProvider: ({ config, children, }: {
444
455
  config: MeetingConfig;
@@ -454,6 +465,7 @@ declare const useMeeting: (handlers?: {
454
465
  onTrackStateChanged?: (track: any) => void;
455
466
  onRecordingStarted?: (recording: any) => void;
456
467
  onRecordingStopped?: (recording: any) => void;
468
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
457
469
  }) => {
458
470
  join: (config?: MeetingConfig) => Promise<void>;
459
471
  leave: () => void;
@@ -478,6 +490,7 @@ declare const useMeeting: (handlers?: {
478
490
  participants: Map<string, Participant>;
479
491
  messages: ChatMessage[];
480
492
  presenterId: string | null;
493
+ activeSpeakerIds: string[];
481
494
  usePubSub: (topic: PubSubTopic) => {
482
495
  messages: ChatMessage[];
483
496
  publish: (input: ChatInput) => void;
@@ -492,8 +505,9 @@ declare const useMeeting: (handlers?: {
492
505
  onEntryResponded: (cb: (payload: any, decision?: any) => void) => () => void;
493
506
  onMeetingLeft: (cb: () => void) => () => void;
494
507
  onTrackStateChanged: (cb: (track: any) => void) => () => void;
495
- onRecordingStarted: (cb: (recording: any) => void) => () => void;
508
+ onRecordingStarted: (cb: (recording: Recording) => void) => () => void;
496
509
  onRecordingStopped: (cb: (recording: any) => void) => () => void;
510
+ onActiveSpeakersChanged: (cb: (participantIds: string[]) => void) => () => void;
497
511
  };
498
512
 
499
513
  declare const useParticipants: () => Participant[];
@@ -540,4 +554,36 @@ declare function useMeetingPreview(roomId: string, userId: string): {
540
554
  error: string | null;
541
555
  };
542
556
 
543
- export { type ChatInput, type MeetingConfig, MeetingProvider, MeetingState, type Participant, type ParticipantInfo, type RecordingInfo, SFUClient, type SFUConnectionState, type TrackInfo, VideoSDKCore, useLocalParticipant, useMeeting, useMeetingContext, useMeetingPreview, useParticipants, useRemoteMedia };
557
+ interface ActiveSpeakerDetectorOptions {
558
+ intervalMs?: number;
559
+ threshold?: number;
560
+ releaseThreshold?: number;
561
+ releaseMs?: number;
562
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
563
+ }
564
+ declare class ActiveSpeakerDetector {
565
+ private audioContext;
566
+ private speakers;
567
+ private timer;
568
+ private readonly intervalMs;
569
+ private readonly threshold;
570
+ private readonly releaseThreshold;
571
+ private readonly releaseMs;
572
+ private onActiveSpeakersChanged;
573
+ private lastEmitted;
574
+ constructor(options?: ActiveSpeakerDetectorOptions);
575
+ private ensureAudioContext;
576
+ resume(): Promise<void>;
577
+ addTrack(participantId: string, track: MediaStreamTrack): void;
578
+ removeParticipant(participantId: string): void;
579
+ removeTrack(track: MediaStreamTrack): void;
580
+ clear(): void;
581
+ start(): void;
582
+ stop(): void;
583
+ private sample;
584
+ private getActiveSpeakerIds;
585
+ private emitIfChanged;
586
+ destroy(): Promise<void>;
587
+ }
588
+
589
+ export { ActiveSpeakerDetector, type ChatInput, type MeetingConfig, MeetingProvider, MeetingState, type Participant, type ParticipantInfo, type RecordingInfo, SFUClient, type SFUConnectionState, type TrackInfo, VideoSDKCore, useLocalParticipant, useMeeting, useMeetingContext, useMeetingPreview, useParticipants, useRemoteMedia };
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ type Events = {
14
14
  participantId: string;
15
15
  decision: EntryDecision;
16
16
  } | string, decision?: EntryDecision) => void;
17
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
17
18
  onJoinApproved?: (requestId: string) => void;
18
19
  onJoinRejected?: (requestId: string) => void;
19
20
  onConnectionStateChanged?: (state: string) => void;
@@ -82,6 +83,7 @@ type ChatInput = {
82
83
  };
83
84
  type MeetingConfig = {
84
85
  roomId: string;
86
+ userId?: string;
85
87
  name: string;
86
88
  audioMuted?: boolean;
87
89
  videoMuted?: boolean;
@@ -94,7 +96,7 @@ type RecordingInfo = {
94
96
  stopped_at?: string;
95
97
  active?: boolean;
96
98
  };
97
- type StateScope = "participants" | "localParticipant" | "chat" | "presenter" | `participant:${string}`;
99
+ type StateScope = "participants" | "localParticipant" | "chat" | "presenter" | "activeSpeakers" | `participant:${string}`;
98
100
  type SDKError = {
99
101
  code: string;
100
102
  message: string;
@@ -115,6 +117,10 @@ interface TrackDescriptor {
115
117
  source: "camera" | "screen" | "audio";
116
118
  mid?: string;
117
119
  }
120
+ interface Recording {
121
+ started_at?: string;
122
+ recording_id?: string;
123
+ }
118
124
 
119
125
  declare const useLocalParticipant: () => {
120
126
  participant: Participant | null;
@@ -132,10 +138,12 @@ declare class MeetingState {
132
138
  localStream: MediaStream | null;
133
139
  chatMessages: Map<string, ChatMessage>;
134
140
  presenterId: string | null;
141
+ activeSpeakerIds: Set<string>;
135
142
  private listeners;
136
143
  subscribe(scope: StateScope, fn: Listener): () => void;
137
144
  notify(scope: StateScope): void;
138
145
  setPresenterId(id: string | null): void;
146
+ setActiveSpeakers(ids: string[]): void;
139
147
  addParticipant(p: Participant): boolean;
140
148
  removeParticipant(id: string): void;
141
149
  updateParticipantMedia(id: string, patch: Partial<NonNullable<Participant["media"]>>): void;
@@ -163,6 +171,7 @@ declare class VideoSDKCore {
163
171
  private intentionalDisconnect;
164
172
  private remoteStreams;
165
173
  private remoteScreenStreams;
174
+ private activeSpeakerDetector;
166
175
  private emitError;
167
176
  constructor(events?: Events, apiBase?: string, wsBase?: string);
168
177
  initLocal(video: HTMLVideoElement, name: string): Promise<void>;
@@ -426,6 +435,7 @@ type MeetingContextValue = {
426
435
  participants: Map<string, Participant>;
427
436
  messages: ChatMessage[];
428
437
  presenterId: string | null;
438
+ activeSpeakerIds: string[];
429
439
  usePubSub: (topic: PubSubTopic) => PubSubHandle;
430
440
  approveJoinRequest: (requestId: string) => void;
431
441
  rejectJoinRequest: (requestId: string) => void;
@@ -437,8 +447,9 @@ type MeetingContextValue = {
437
447
  onEntryResponded: (cb: (payload: any, decision?: any) => void) => () => void;
438
448
  onMeetingLeft: (cb: () => void) => () => void;
439
449
  onTrackStateChanged: (cb: (track: any) => void) => () => void;
440
- onRecordingStarted: (cb: (recording: any) => void) => () => void;
450
+ onRecordingStarted: (cb: (recording: Recording) => void) => () => void;
441
451
  onRecordingStopped: (cb: (recording: any) => void) => () => void;
452
+ onActiveSpeakersChanged: (cb: (participantIds: string[]) => void) => () => void;
442
453
  };
443
454
  declare const MeetingProvider: ({ config, children, }: {
444
455
  config: MeetingConfig;
@@ -454,6 +465,7 @@ declare const useMeeting: (handlers?: {
454
465
  onTrackStateChanged?: (track: any) => void;
455
466
  onRecordingStarted?: (recording: any) => void;
456
467
  onRecordingStopped?: (recording: any) => void;
468
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
457
469
  }) => {
458
470
  join: (config?: MeetingConfig) => Promise<void>;
459
471
  leave: () => void;
@@ -478,6 +490,7 @@ declare const useMeeting: (handlers?: {
478
490
  participants: Map<string, Participant>;
479
491
  messages: ChatMessage[];
480
492
  presenterId: string | null;
493
+ activeSpeakerIds: string[];
481
494
  usePubSub: (topic: PubSubTopic) => {
482
495
  messages: ChatMessage[];
483
496
  publish: (input: ChatInput) => void;
@@ -492,8 +505,9 @@ declare const useMeeting: (handlers?: {
492
505
  onEntryResponded: (cb: (payload: any, decision?: any) => void) => () => void;
493
506
  onMeetingLeft: (cb: () => void) => () => void;
494
507
  onTrackStateChanged: (cb: (track: any) => void) => () => void;
495
- onRecordingStarted: (cb: (recording: any) => void) => () => void;
508
+ onRecordingStarted: (cb: (recording: Recording) => void) => () => void;
496
509
  onRecordingStopped: (cb: (recording: any) => void) => () => void;
510
+ onActiveSpeakersChanged: (cb: (participantIds: string[]) => void) => () => void;
497
511
  };
498
512
 
499
513
  declare const useParticipants: () => Participant[];
@@ -540,4 +554,36 @@ declare function useMeetingPreview(roomId: string, userId: string): {
540
554
  error: string | null;
541
555
  };
542
556
 
543
- export { type ChatInput, type MeetingConfig, MeetingProvider, MeetingState, type Participant, type ParticipantInfo, type RecordingInfo, SFUClient, type SFUConnectionState, type TrackInfo, VideoSDKCore, useLocalParticipant, useMeeting, useMeetingContext, useMeetingPreview, useParticipants, useRemoteMedia };
557
+ interface ActiveSpeakerDetectorOptions {
558
+ intervalMs?: number;
559
+ threshold?: number;
560
+ releaseThreshold?: number;
561
+ releaseMs?: number;
562
+ onActiveSpeakersChanged?: (participantIds: string[]) => void;
563
+ }
564
+ declare class ActiveSpeakerDetector {
565
+ private audioContext;
566
+ private speakers;
567
+ private timer;
568
+ private readonly intervalMs;
569
+ private readonly threshold;
570
+ private readonly releaseThreshold;
571
+ private readonly releaseMs;
572
+ private onActiveSpeakersChanged;
573
+ private lastEmitted;
574
+ constructor(options?: ActiveSpeakerDetectorOptions);
575
+ private ensureAudioContext;
576
+ resume(): Promise<void>;
577
+ addTrack(participantId: string, track: MediaStreamTrack): void;
578
+ removeParticipant(participantId: string): void;
579
+ removeTrack(track: MediaStreamTrack): void;
580
+ clear(): void;
581
+ start(): void;
582
+ stop(): void;
583
+ private sample;
584
+ private getActiveSpeakerIds;
585
+ private emitIfChanged;
586
+ destroy(): Promise<void>;
587
+ }
588
+
589
+ export { ActiveSpeakerDetector, type ChatInput, type MeetingConfig, MeetingProvider, MeetingState, type Participant, type ParticipantInfo, type RecordingInfo, SFUClient, type SFUConnectionState, type TrackInfo, VideoSDKCore, useLocalParticipant, useMeeting, useMeetingContext, useMeetingPreview, useParticipants, useRemoteMedia };
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ActiveSpeakerDetector: () => ActiveSpeakerDetector,
23
24
  MeetingProvider: () => MeetingProvider,
24
25
  MeetingState: () => MeetingState,
25
26
  SFUClient: () => SFUClient,
@@ -41,9 +42,7 @@ var import_react2 = require("react");
41
42
 
42
43
  // src/config/ws.ts
43
44
  var SDK_CONFIG = {
44
- // SFU API endpoint for token generation
45
45
  apiBase: "https://sfu-server.duckdns.org/api",
46
- // SFU WebSocket endpoint for signaling
47
46
  wsBase: "wss://sfu-server.duckdns.org/ws",
48
47
  // Public STUN/TURN servers (fallback)
49
48
  iceServers: [
@@ -52,10 +51,170 @@ var SDK_CONFIG = {
52
51
  ],
53
52
  // Connection and reconnection settings
54
53
  connectionTimeout: 1e4,
55
- maxReconnectAttempts: 5,
56
- // Legacy WebSocket URL (if using the old mesh architecture)
57
- wsUrl: "wss://sfu-server.duckdns.org/ws"
58
- // kept for backward compatibility
54
+ maxReconnectAttempts: 5
55
+ };
56
+
57
+ // src/core/ActiveSpeakerDetector.ts
58
+ var ActiveSpeakerDetector = class {
59
+ constructor(options = {}) {
60
+ this.audioContext = null;
61
+ this.speakers = /* @__PURE__ */ new Map();
62
+ this.timer = null;
63
+ this.lastEmitted = "";
64
+ this.intervalMs = options.intervalMs ?? 100;
65
+ this.threshold = options.threshold ?? 0.025;
66
+ this.releaseThreshold = options.releaseThreshold ?? 0.015;
67
+ this.releaseMs = options.releaseMs ?? 600;
68
+ this.onActiveSpeakersChanged = options.onActiveSpeakersChanged ?? (() => {
69
+ });
70
+ }
71
+ ensureAudioContext() {
72
+ if (!this.audioContext) {
73
+ const AudioContextClass = window.AudioContext || window.webkitAudioContext;
74
+ if (!AudioContextClass) {
75
+ throw new Error("Web Audio API is not supported by this browser");
76
+ }
77
+ this.audioContext = new AudioContextClass();
78
+ }
79
+ return this.audioContext;
80
+ }
81
+ async resume() {
82
+ if (!this.audioContext) return;
83
+ if (this.audioContext.state === "suspended") {
84
+ try {
85
+ await this.audioContext.resume();
86
+ } catch {
87
+ }
88
+ }
89
+ }
90
+ addTrack(participantId, track) {
91
+ if (!participantId) return;
92
+ if (track.kind !== "audio") return;
93
+ this.removeParticipant(participantId);
94
+ const context = this.ensureAudioContext();
95
+ const stream = new MediaStream([track]);
96
+ const source = context.createMediaStreamSource(stream);
97
+ const analyser = context.createAnalyser();
98
+ analyser.fftSize = 512;
99
+ analyser.smoothingTimeConstant = 0.65;
100
+ analyser.minDecibels = -90;
101
+ analyser.maxDecibels = -10;
102
+ source.connect(analyser);
103
+ const data = new Float32Array(analyser.fftSize);
104
+ const state = {
105
+ participantId,
106
+ track,
107
+ stream,
108
+ source,
109
+ analyser,
110
+ data,
111
+ active: false,
112
+ lastActiveAt: 0
113
+ };
114
+ this.speakers.set(participantId, state);
115
+ track.addEventListener(
116
+ "ended",
117
+ () => {
118
+ this.removeParticipant(participantId);
119
+ },
120
+ { once: true }
121
+ );
122
+ this.start();
123
+ void this.resume();
124
+ }
125
+ removeParticipant(participantId) {
126
+ const state = this.speakers.get(participantId);
127
+ if (!state) return;
128
+ state.active = false;
129
+ try {
130
+ state.source.disconnect();
131
+ } catch {
132
+ }
133
+ state.stream.getTracks().forEach((track) => {
134
+ if (track !== state.track) {
135
+ track.stop();
136
+ }
137
+ });
138
+ this.speakers.delete(participantId);
139
+ this.emitIfChanged();
140
+ }
141
+ removeTrack(track) {
142
+ for (const [participantId, state] of this.speakers) {
143
+ if (state.track === track) {
144
+ this.removeParticipant(participantId);
145
+ }
146
+ }
147
+ }
148
+ clear() {
149
+ for (const participantId of this.speakers.keys()) {
150
+ this.removeParticipant(participantId);
151
+ }
152
+ this.speakers.clear();
153
+ this.stop();
154
+ this.emitIfChanged(true);
155
+ }
156
+ start() {
157
+ if (this.timer) return;
158
+ this.timer = setInterval(() => this.sample(), this.intervalMs);
159
+ }
160
+ stop() {
161
+ if (!this.timer) return;
162
+ clearInterval(this.timer);
163
+ this.timer = null;
164
+ }
165
+ sample() {
166
+ const now = performance.now();
167
+ for (const state of this.speakers.values()) {
168
+ if (state.track.readyState !== "live" || state.track.muted || !state.track.enabled) {
169
+ if (state.active) {
170
+ state.active = false;
171
+ }
172
+ continue;
173
+ }
174
+ state.analyser.getFloatTimeDomainData(state.data);
175
+ let sumSquares = 0;
176
+ for (let i = 0; i < state.data.length; i++) {
177
+ const sample = state.data[i];
178
+ sumSquares += sample * sample;
179
+ }
180
+ const rms = Math.sqrt(sumSquares / state.data.length);
181
+ if (!state.active) {
182
+ if (rms >= this.threshold) {
183
+ state.active = true;
184
+ state.lastActiveAt = now;
185
+ }
186
+ } else {
187
+ if (rms >= this.releaseThreshold) {
188
+ state.lastActiveAt = now;
189
+ } else if (now - state.lastActiveAt >= this.releaseMs) {
190
+ state.active = false;
191
+ }
192
+ }
193
+ }
194
+ this.emitIfChanged();
195
+ }
196
+ getActiveSpeakerIds() {
197
+ return Array.from(this.speakers.values()).filter((speaker) => speaker.active).map((speaker) => speaker.participantId);
198
+ }
199
+ emitIfChanged(force = false) {
200
+ const ids = this.getActiveSpeakerIds().sort();
201
+ const key = ids.join("|");
202
+ if (!force && key === this.lastEmitted) {
203
+ return;
204
+ }
205
+ this.lastEmitted = key;
206
+ this.onActiveSpeakersChanged(ids);
207
+ }
208
+ async destroy() {
209
+ this.clear();
210
+ if (this.audioContext) {
211
+ try {
212
+ await this.audioContext.close();
213
+ } catch {
214
+ }
215
+ this.audioContext = null;
216
+ }
217
+ }
59
218
  };
60
219
 
61
220
  // src/core/MeetingState.ts
@@ -66,6 +225,7 @@ var MeetingState = class {
66
225
  this.localStream = null;
67
226
  this.chatMessages = /* @__PURE__ */ new Map();
68
227
  this.presenterId = null;
228
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
69
229
  this.listeners = /* @__PURE__ */ new Map();
70
230
  }
71
231
  // ---- reactive system ----
@@ -87,6 +247,14 @@ var MeetingState = class {
87
247
  this.notify("presenter");
88
248
  this.notify("participants");
89
249
  }
250
+ setActiveSpeakers(ids) {
251
+ const next = new Set(ids);
252
+ if (next.size === this.activeSpeakerIds.size && [...next].every((id) => this.activeSpeakerIds.has(id))) {
253
+ return;
254
+ }
255
+ this.activeSpeakerIds = next;
256
+ this.notify("activeSpeakers");
257
+ }
90
258
  // ---- participants ----
91
259
  addParticipant(p) {
92
260
  if (this.participants.has(p.id)) return false;
@@ -200,9 +368,11 @@ var MeetingState = class {
200
368
  this.participants.clear();
201
369
  this.chatMessages.clear();
202
370
  this.presenterId = null;
371
+ this.activeSpeakerIds = /* @__PURE__ */ new Set();
203
372
  this.notify("participants");
204
373
  this.notify("chat");
205
374
  this.notify("presenter");
375
+ this.notify("activeSpeakers");
206
376
  }
207
377
  };
208
378
 
@@ -1348,6 +1518,12 @@ var VideoSDKCore = class {
1348
1518
  this.remoteScreenStreams = /* @__PURE__ */ new Map();
1349
1519
  this.state = new MeetingState();
1350
1520
  this.events = events;
1521
+ this.activeSpeakerDetector = new ActiveSpeakerDetector({
1522
+ onActiveSpeakersChanged: (participantIds) => {
1523
+ this.state.setActiveSpeakers(participantIds);
1524
+ this.events.onActiveSpeakersChanged?.(participantIds);
1525
+ }
1526
+ });
1351
1527
  let storedId = null;
1352
1528
  try {
1353
1529
  storedId = typeof localStorage !== "undefined" ? localStorage.getItem("defcomm:participant_id") : null;
@@ -1388,6 +1564,10 @@ var VideoSDKCore = class {
1388
1564
  autoGainControl: true
1389
1565
  }
1390
1566
  });
1567
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1568
+ if (localAudioTrack) {
1569
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1570
+ }
1391
1571
  const hasVideo = this.localStream.getVideoTracks().some((t) => t.readyState === "live");
1392
1572
  const hasAudio = this.localStream.getAudioTracks().some((t) => t.readyState === "live");
1393
1573
  if (!hasVideo || !hasAudio) {
@@ -1444,10 +1624,19 @@ var VideoSDKCore = class {
1444
1624
  return client.deleteRoom(roomCode);
1445
1625
  }
1446
1626
  async joinMeeting(config) {
1447
- const { roomId, name, audioMuted = false, videoMuted = false } = config;
1627
+ const {
1628
+ roomId,
1629
+ name,
1630
+ audioMuted = false,
1631
+ videoMuted = false,
1632
+ userId
1633
+ } = config;
1448
1634
  if (!roomId || !name) {
1449
1635
  throw new Error("roomId and name are required to join meeting");
1450
1636
  }
1637
+ if (userId) {
1638
+ this.myId = userId;
1639
+ }
1451
1640
  this.intentionalDisconnect = false;
1452
1641
  this.participantName = name;
1453
1642
  this.room.id = roomId;
@@ -1457,6 +1646,10 @@ var VideoSDKCore = class {
1457
1646
  video: true,
1458
1647
  audio: true
1459
1648
  });
1649
+ const localAudioTrack = this.localStream.getAudioTracks()[0];
1650
+ if (localAudioTrack) {
1651
+ this.activeSpeakerDetector.addTrack(this.myId, localAudioTrack);
1652
+ }
1460
1653
  }
1461
1654
  this.localStream.getAudioTracks().forEach((track) => {
1462
1655
  track.enabled = !audioMuted;
@@ -1479,10 +1672,7 @@ var VideoSDKCore = class {
1479
1672
  autoSubscribe: true,
1480
1673
  onStateChange: (state) => this.handleSFUState(state),
1481
1674
  onConnected: (payload, resumed) => {
1482
- console.log(
1483
- `[SFU] ${resumed ? "Resumed" : "Joined"} room`,
1484
- payload
1485
- );
1675
+ console.log(`[SFU] ${resumed ? "Resumed" : "Joined"} room`, payload);
1486
1676
  this.state.updateLocalParticipant({
1487
1677
  id: this.myId,
1488
1678
  name: this.participantName
@@ -1661,7 +1851,12 @@ var VideoSDKCore = class {
1661
1851
  },
1662
1852
  onError: (error) => {
1663
1853
  console.error("[SFU] Error:", error);
1664
- this.emitError("SFU_ERROR", error?.message || String(error), error, true);
1854
+ this.emitError(
1855
+ "SFU_ERROR",
1856
+ error?.message || String(error),
1857
+ error,
1858
+ true
1859
+ );
1665
1860
  }
1666
1861
  });
1667
1862
  this.sfuClient = client;
@@ -1736,6 +1931,9 @@ var VideoSDKCore = class {
1736
1931
  cameraTrack: track.kind === "video" ? track : current?.media?.cameraTrack,
1737
1932
  audioTrack: track.kind === "audio" ? track : current?.media?.audioTrack
1738
1933
  });
1934
+ if (track.kind === "audio") {
1935
+ this.activeSpeakerDetector.addTrack(participantId, track);
1936
+ }
1739
1937
  this.events.onTrack?.(remoteStream, participantId);
1740
1938
  track.onended = () => {
1741
1939
  if (track.kind === "video") {
@@ -1881,6 +2079,7 @@ var VideoSDKCore = class {
1881
2079
  }
1882
2080
  // ============ CLEANUP ============
1883
2081
  cleanupRemoteParticipant(participantId) {
2082
+ this.activeSpeakerDetector.removeParticipant(participantId);
1884
2083
  const stream = this.remoteStreams.get(participantId);
1885
2084
  stream?.getTracks().forEach((track) => track.stop());
1886
2085
  this.remoteStreams.delete(participantId);
@@ -1946,6 +2145,9 @@ var MeetingProvider = ({
1946
2145
  const trackStateListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1947
2146
  const recordingStartedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
1948
2147
  const recordingStoppedListeners = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
2148
+ const activeSpeakerListeners = (0, import_react2.useRef)(
2149
+ /* @__PURE__ */ new Set()
2150
+ );
1949
2151
  if (!sdkRef.current) {
1950
2152
  sdkRef.current = new VideoSDKCore({
1951
2153
  onError: (err) => errorListeners.current.forEach((fn) => fn(err)),
@@ -1958,6 +2160,11 @@ var MeetingProvider = ({
1958
2160
  });
1959
2161
  }
1960
2162
  const sdk = sdkRef.current;
2163
+ const activeSpeakerIds = useMeetingStore(
2164
+ sdk.state,
2165
+ "activeSpeakers",
2166
+ (s) => Array.from(s.activeSpeakerIds)
2167
+ );
1961
2168
  const presenterId = useMeetingStore(
1962
2169
  sdk.state,
1963
2170
  "presenter",
@@ -2006,6 +2213,7 @@ var MeetingProvider = ({
2006
2213
  participants,
2007
2214
  messages,
2008
2215
  presenterId,
2216
+ activeSpeakerIds,
2009
2217
  usePubSub: (topic) => {
2010
2218
  if (topic !== "SECURE_CHAT") {
2011
2219
  throw new Error(`Unsupported PubSub argument: "${topic}"`);
@@ -2055,6 +2263,12 @@ var MeetingProvider = ({
2055
2263
  onRecordingStopped: (cb) => {
2056
2264
  recordingStoppedListeners.current.add(cb);
2057
2265
  return () => recordingStoppedListeners.current.delete(cb);
2266
+ },
2267
+ onActiveSpeakersChanged: (cb) => {
2268
+ activeSpeakerListeners.current.add(cb);
2269
+ return () => {
2270
+ activeSpeakerListeners.current.delete(cb);
2271
+ };
2058
2272
  }
2059
2273
  };
2060
2274
  }, [config, sdk, localParticipant, participants, messages, presenterId]);
@@ -2118,6 +2332,10 @@ var useMeeting = (handlers) => {
2118
2332
  if (!handlers?.onError) return;
2119
2333
  return ctx.onError(handlers.onError);
2120
2334
  }, [handlers?.onError]);
2335
+ (0, import_react4.useEffect)(() => {
2336
+ if (!handlers?.onActiveSpeakersChanged) return;
2337
+ return ctx.onActiveSpeakersChanged(handlers.onActiveSpeakersChanged);
2338
+ }, [handlers?.onActiveSpeakersChanged]);
2121
2339
  (0, import_react4.useEffect)(() => {
2122
2340
  if (!handlers?.onEntryRequested) return;
2123
2341
  return ctx.onEntryRequested(handlers.onEntryRequested);
@@ -2275,6 +2493,7 @@ function useMeetingPreview(roomId, userId) {
2275
2493
  }
2276
2494
  // Annotate the CommonJS export names for ESM import in node:
2277
2495
  0 && (module.exports = {
2496
+ ActiveSpeakerDetector,
2278
2497
  MeetingProvider,
2279
2498
  MeetingState,
2280
2499
  SFUClient,