@elevenlabs/react-native 1.0.2 → 1.1.0

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.
@@ -0,0 +1,273 @@
1
+ import { NativeModules, NativeEventEmitter } from "react-native";
2
+ import { WebRTCConnection, type VolumeProvider } from "@elevenlabs/client";
3
+ import {
4
+ MIN_VOICE_FREQUENCY,
5
+ MAX_VOICE_FREQUENCY,
6
+ type VoiceSessionSetupResult,
7
+ } from "@elevenlabs/client/internal";
8
+
9
+ const LiveKitModule = NativeModules.LivekitReactNativeModule;
10
+
11
+ let emitter: NativeEventEmitter | null = null;
12
+ function getEmitter(): NativeEventEmitter {
13
+ emitter ??= new NativeEventEmitter(LiveKitModule);
14
+ return emitter;
15
+ }
16
+
17
+ interface VolumeEvent {
18
+ id: string;
19
+ volume: number;
20
+ }
21
+
22
+ interface MultibandEvent {
23
+ id: string;
24
+ magnitudes: number[];
25
+ }
26
+
27
+ const MULTIBAND_FREQUENCY_OPTIONS = {
28
+ minFrequency: MIN_VOICE_FREQUENCY,
29
+ maxFrequency: MAX_VOICE_FREQUENCY,
30
+ updateInterval: 40,
31
+ };
32
+
33
+ /**
34
+ * A VolumeProvider backed by native LiveKit audio processors.
35
+ *
36
+ * Both processors are created lazily: the RMS volume processor on the first
37
+ * `getVolume()` call, and the FFT multiband processor on the first
38
+ * `getByteFrequencyData()` call (with its band count matching the buffer
39
+ * length). If the buffer size changes, the multiband processor is recreated.
40
+ */
41
+ class NativeVolumeProvider implements VolumeProvider {
42
+ private volume = 0;
43
+ private magnitudes: number[] = [];
44
+
45
+ // Volume processor state (lazy)
46
+ private volumeTag: string | null = null;
47
+ private volumeSub: { remove: () => void } | null = null;
48
+
49
+ // Multiband processor state (lazy)
50
+ private multibandTag: string | null = null;
51
+ private multibandSub: { remove: () => void } | null = null;
52
+ private currentBands = 0;
53
+
54
+ constructor(
55
+ private pcId: number,
56
+ private trackId: string
57
+ ) {}
58
+
59
+ /**
60
+ * Rebinds this provider to a new track, cleaning up any existing native
61
+ * processors so they are lazily recreated against the new track.
62
+ */
63
+ updateTrack(pcId: number, trackId: string) {
64
+ this.cleanupProcessors();
65
+ this.pcId = pcId;
66
+ this.trackId = trackId;
67
+ this.volume = 0;
68
+ this.magnitudes = [];
69
+ }
70
+
71
+ private ensureVolumeProcessor() {
72
+ if (this.volumeTag) return;
73
+ this.volumeTag = LiveKitModule.createVolumeProcessor(
74
+ this.pcId,
75
+ this.trackId
76
+ );
77
+ this.volumeSub = getEmitter().addListener(
78
+ "LK_VOLUME_PROCESSED",
79
+ (event: VolumeEvent) => {
80
+ if (event.id === this.volumeTag) {
81
+ this.volume = event.volume;
82
+ }
83
+ }
84
+ );
85
+ }
86
+
87
+ private ensureMultibandProcessor(bands: number) {
88
+ if (bands === this.currentBands) return;
89
+
90
+ // Cleanup previous processor
91
+ if (this.multibandTag) {
92
+ LiveKitModule.deleteMultibandVolumeProcessor(
93
+ this.multibandTag,
94
+ this.pcId,
95
+ this.trackId
96
+ );
97
+ this.multibandSub?.remove();
98
+ }
99
+
100
+ this.currentBands = bands;
101
+ this.magnitudes = [];
102
+ this.multibandTag = LiveKitModule.createMultibandVolumeProcessor(
103
+ { ...MULTIBAND_FREQUENCY_OPTIONS, bands },
104
+ this.pcId,
105
+ this.trackId
106
+ );
107
+ this.multibandSub = getEmitter().addListener(
108
+ "LK_MULTIBAND_PROCESSED",
109
+ (event: MultibandEvent) => {
110
+ if (event.id === this.multibandTag) {
111
+ this.magnitudes = event.magnitudes;
112
+ }
113
+ }
114
+ );
115
+ }
116
+
117
+ getVolume(): number {
118
+ this.ensureVolumeProcessor();
119
+ return this.volume < 0 ? 0 : this.volume > 1 ? 1 : this.volume;
120
+ }
121
+
122
+ getByteFrequencyData(buffer: Uint8Array<ArrayBuffer>): void {
123
+ this.ensureMultibandProcessor(buffer.length);
124
+ if (this.magnitudes.length === 0) {
125
+ // No multiband data yet; fall back to uniform fill from RMS volume
126
+ this.ensureVolumeProcessor();
127
+ const clamped = this.volume < 0 ? 0 : this.volume > 1 ? 1 : this.volume;
128
+ buffer.fill(Math.round(clamped * 255));
129
+ return;
130
+ }
131
+ for (let i = 0; i < buffer.length; i++) {
132
+ const m = this.magnitudes[i] ?? 0;
133
+ buffer[i] = Math.round((m < 0 ? 0 : m > 1 ? 1 : m) * 255);
134
+ }
135
+ }
136
+
137
+ private cleanupProcessors() {
138
+ if (this.volumeTag) {
139
+ LiveKitModule.deleteVolumeProcessor(
140
+ this.volumeTag,
141
+ this.pcId,
142
+ this.trackId
143
+ );
144
+ this.volumeSub?.remove();
145
+ this.volumeTag = null;
146
+ this.volumeSub = null;
147
+ }
148
+ if (this.multibandTag) {
149
+ LiveKitModule.deleteMultibandVolumeProcessor(
150
+ this.multibandTag,
151
+ this.pcId,
152
+ this.trackId
153
+ );
154
+ this.multibandSub?.remove();
155
+ this.multibandTag = null;
156
+ this.multibandSub = null;
157
+ this.currentBands = 0;
158
+ }
159
+ }
160
+
161
+ cleanup() {
162
+ this.cleanupProcessors();
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Sets up lazy native volume providers for the WebRTC connection. No native
168
+ * processors are created until `getVolume()` or `getByteFrequencyData()` is
169
+ * actually called. Returns a cleanup function.
170
+ */
171
+ function setupNativeVolumeProcessors(connection: WebRTCConnection): () => void {
172
+ const room = connection.getRoom();
173
+ const providers: NativeVolumeProvider[] = [];
174
+
175
+ // --- Input (local mic track) ---
176
+ let inputProvider: NativeVolumeProvider | null = null;
177
+
178
+ function setupInputTrack(track: any) {
179
+ const mst = track.mediaStreamTrack as any;
180
+ const pcId: number = mst._peerConnectionId ?? -1;
181
+ if (inputProvider) {
182
+ inputProvider.updateTrack(pcId, mst.id);
183
+ } else {
184
+ inputProvider = new NativeVolumeProvider(pcId, mst.id);
185
+ providers.push(inputProvider);
186
+ connection.setInputVolumeProvider(inputProvider);
187
+ }
188
+ }
189
+
190
+ const micPub = room.localParticipant.audioTrackPublications.values().next();
191
+ const micTrack = micPub.done ? undefined : micPub.value?.track;
192
+ if (micTrack) {
193
+ setupInputTrack(micTrack);
194
+ }
195
+
196
+ // Re-bind the input provider when the local mic track is republished
197
+ // (e.g. after setAudioInputDevice)
198
+ const localTrackHandler = (publication: any) => {
199
+ if (publication.track?.kind === "audio") {
200
+ setupInputTrack(publication.track);
201
+ }
202
+ };
203
+ room.on("localTrackPublished", localTrackHandler);
204
+
205
+ // --- Output (remote agent track) ---
206
+ let outputProvider: NativeVolumeProvider | null = null;
207
+
208
+ function setupOutputTrack(track: any) {
209
+ const mst = track.mediaStreamTrack as any;
210
+ const pcId: number = mst._peerConnectionId ?? -1;
211
+ if (outputProvider) {
212
+ outputProvider.updateTrack(pcId, mst.id);
213
+ } else {
214
+ outputProvider = new NativeVolumeProvider(pcId, mst.id);
215
+ providers.push(outputProvider);
216
+ connection.setOutputVolumeProvider(outputProvider);
217
+ }
218
+ }
219
+
220
+ // Check for an existing remote audio track from the agent
221
+ for (const participant of room.remoteParticipants.values()) {
222
+ if (participant.identity?.includes("agent")) {
223
+ for (const pub of participant.audioTrackPublications.values()) {
224
+ if (pub.track) {
225
+ setupOutputTrack(pub.track);
226
+ break;
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ // Listen for future track subscriptions
233
+ const trackHandler = (track: any, _publication: any, participant: any) => {
234
+ if (track.kind === "audio" && participant?.identity?.includes("agent")) {
235
+ setupOutputTrack(track);
236
+ }
237
+ };
238
+ room.on("trackSubscribed", trackHandler);
239
+
240
+ return () => {
241
+ room.off("localTrackPublished", localTrackHandler);
242
+ room.off("trackSubscribed", trackHandler);
243
+ for (const provider of providers) {
244
+ provider.cleanup();
245
+ }
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Attaches native volume processors to the WebRTC connection so that
251
+ * `getInputVolume()` / `getOutputVolume()` return real values on
252
+ * React Native. Falls through unchanged for non-WebRTC connections;
253
+ * WebSocket volume on React Native is a known gap (MediaDeviceInput/
254
+ * MediaDeviceOutput depend on AudioContext which is unavailable in RN).
255
+ */
256
+ export function attachNativeVolume(
257
+ result: VoiceSessionSetupResult
258
+ ): VoiceSessionSetupResult {
259
+ if (!(result.connection instanceof WebRTCConnection)) {
260
+ return result;
261
+ }
262
+
263
+ const cleanup = setupNativeVolumeProcessors(result.connection);
264
+
265
+ const originalDetach = result.detach;
266
+ return {
267
+ ...result,
268
+ detach: () => {
269
+ cleanup();
270
+ originalDetach();
271
+ },
272
+ };
273
+ }
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated during build
2
- export const PACKAGE_VERSION = "1.0.2";
2
+ export const PACKAGE_VERSION = "1.1.0";
@@ -9,8 +9,8 @@
9
9
  "sourceMap": true,
10
10
  "target": "ES2022",
11
11
  "lib": ["ES2022", "DOM"],
12
- "module": "ES2022",
13
- "moduleResolution": "bundler",
12
+ "module": "nodenext",
13
+ "moduleResolution": "nodenext",
14
14
  "jsx": "react-jsx",
15
15
  "skipLibCheck": true
16
16
  },