@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/README.md +984 -637
- package/dist/index.d.mts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +231 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 {
|
|
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(
|
|
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,
|