@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +1 -1
- package/.turbo/turbo-generate-version.log +1 -1
- package/.turbo/turbo-lint$colon$es.log +1 -1
- package/.turbo/turbo-lint$colon$prettier.log +1 -1
- package/CHANGELOG.md +27 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.react-native.d.ts +1 -1
- package/dist/index.react-native.d.ts.map +1 -1
- package/dist/index.react-native.js +5 -3
- package/dist/index.react-native.js.map +1 -1
- package/dist/nativeVolume.d.ts +10 -0
- package/dist/nativeVolume.d.ts.map +1 -0
- package/dist/nativeVolume.js +210 -0
- package/dist/nativeVolume.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/index.react-native.ts +5 -3
- package/src/index.ts +1 -1
- package/src/nativeVolume.ts +273 -0
- package/src/version.ts +1 -1
- package/tsconfig.build.json +2 -2
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -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
|
+
export const PACKAGE_VERSION = "1.1.0";
|
package/tsconfig.build.json
CHANGED