@cloudfort/callum-voice 0.0.3
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 +672 -0
- package/android/build.gradle +38 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/callumvoice/CallumVoiceModule.kt +289 -0
- package/android/src/main/java/com/callumvoice/CallumVoicePackage.kt +32 -0
- package/callum-voice.podspec +21 -0
- package/ios/CallumVoiceBridge.m +7 -0
- package/ios/CallumVoiceModule-Bridging-Header.h +7 -0
- package/ios/CallumVoiceModule.swift +198 -0
- package/lib/VoiceClient.d.ts +104 -0
- package/lib/VoiceClient.d.ts.map +1 -0
- package/lib/VoiceClient.js +493 -0
- package/lib/VoiceClient.js.map +1 -0
- package/lib/VoiceClientState.d.ts +16 -0
- package/lib/VoiceClientState.d.ts.map +1 -0
- package/lib/VoiceClientState.js +21 -0
- package/lib/VoiceClientState.js.map +1 -0
- package/lib/VoiceConfig.d.ts +32 -0
- package/lib/VoiceConfig.d.ts.map +1 -0
- package/lib/VoiceConfig.js +11 -0
- package/lib/VoiceConfig.js.map +1 -0
- package/lib/VoiceEventListener.d.ts +49 -0
- package/lib/VoiceEventListener.d.ts.map +1 -0
- package/lib/VoiceEventListener.js +4 -0
- package/lib/VoiceEventListener.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/package.json +48 -0
- package/src/VoiceClient.ts +568 -0
- package/src/VoiceClientState.ts +21 -0
- package/src/VoiceConfig.ts +58 -0
- package/src/VoiceEventListener.ts +66 -0
- package/src/index.ts +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
# Callum.Voice React Native SDK — Real-Time Voice Chat Library
|
|
2
|
+
|
|
3
|
+
A **React Native plugin** for cross-platform real-time voice communication over a Go server (`phonil-opus`).
|
|
4
|
+
Works on **Android** and **iOS**.
|
|
5
|
+
All **microphone capture**, **audio playback**, **WebSocket connection**, **multi-peer audio mixing**, **speaking detection**, and **room management** logic is implemented in this plugin.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Architecture](#architecture)
|
|
12
|
+
- [File Structure](#file-structure)
|
|
13
|
+
- [Communication Protocol](#communication-protocol)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Complete Example](#complete-example)
|
|
17
|
+
- [API Reference](#api-reference)
|
|
18
|
+
- [VoiceConfig](#voiceconfig)
|
|
19
|
+
- [VoiceEventListener](#voiceeventlistener)
|
|
20
|
+
- [Native Modules](#native-modules)
|
|
21
|
+
- [Permissions](#permissions)
|
|
22
|
+
- [Important Notes](#important-notes)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ Callum.Voice React Native SDK │
|
|
31
|
+
│ │
|
|
32
|
+
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
|
|
33
|
+
│ │ VoiceClient (TS) │────▶│ Native Modules │ │
|
|
34
|
+
│ │ │◀────│ │ │
|
|
35
|
+
│ │ • WebSocket (native) │ │ Android: AudioRecord/ │ │
|
|
36
|
+
│ │ • Room Management │ │ AudioTrack │ │
|
|
37
|
+
│ │ • Peer Tracking │ │ iOS: AVAudioEngine │ │
|
|
38
|
+
│ │ • Speaking Detection │ └──────────────────────────┘ │
|
|
39
|
+
│ │ • Auto Reconnect │ ▲ │
|
|
40
|
+
│ └──────────┬───────────┘ │ │
|
|
41
|
+
│ │ WebSocket (PCM 16-bit mono) │ │
|
|
42
|
+
│ ▼ │ │
|
|
43
|
+
│ ┌─────────────────────┐ │ │
|
|
44
|
+
│ │ Go Server │── PCM Audio ─────┘ │
|
|
45
|
+
│ │ (phonil-opus) │ │
|
|
46
|
+
│ └─────────────────────┘ │
|
|
47
|
+
└──────────────────────────────────────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Data Flow
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Microphone ──▶ NativeModule ──(Int16[])──▶ JS VoiceClient ──(WebSocket)──▶ Server
|
|
54
|
+
Speaker ◀── NativeModule ◀──(Int16[])──◀ JS VoiceClient ◀──(WebSocket)──◀ Server
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## File Structure
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
callum-react-native/
|
|
63
|
+
├── src/
|
|
64
|
+
│ ├── index.ts # Library exports
|
|
65
|
+
│ ├── VoiceClientState.ts # Connection state enum
|
|
66
|
+
│ ├── VoiceConfig.ts # Configuration interface
|
|
67
|
+
│ ├── VoiceEventListener.ts # Event callback interface
|
|
68
|
+
│ └── VoiceClient.ts # Core client logic
|
|
69
|
+
├── android/
|
|
70
|
+
│ ├── build.gradle # Android build config
|
|
71
|
+
│ └── src/main/
|
|
72
|
+
│ ├── AndroidManifest.xml # Permissions
|
|
73
|
+
│ └── java/com/callumvoice/
|
|
74
|
+
│ ├── CallumVoiceModule.kt # Native audio module
|
|
75
|
+
│ └── CallumVoicePackage.kt # Package registration
|
|
76
|
+
├── ios/
|
|
77
|
+
│ ├── CallumVoiceModule.swift # Native audio module
|
|
78
|
+
│ ├── CallumVoiceModule-Bridging-Header.h # ObjC bridge
|
|
79
|
+
│ └── CallumVoiceBridge.m # Bridge implementation
|
|
80
|
+
├── callum-voice.podspec # iOS CocoaPods spec
|
|
81
|
+
├── package.json # npm package config
|
|
82
|
+
├── tsconfig.json # TypeScript config
|
|
83
|
+
└── README.md # This file
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Communication Protocol
|
|
89
|
+
|
|
90
|
+
### WebSocket Connection
|
|
91
|
+
```
|
|
92
|
+
ws(s)://{server}/ws?room=__lobby__&peer={peerId}&api_key={apiKey}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Binary Packet Format
|
|
96
|
+
```
|
|
97
|
+
┌──────────┬──────────┬──────────┬──────────┬──────────────┐
|
|
98
|
+
│ RoomLen │ RoomID │ PeerLen │ PeerID │ PCM Audio │
|
|
99
|
+
│ (1 byte) │ (N bytes)│ (1 byte) │ (N bytes)│ (variable) │
|
|
100
|
+
└──────────┴──────────┴──────────┴──────────┴──────────────┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Audio Format
|
|
104
|
+
- **PCM 16-bit signed integer**, mono, 48000 Hz
|
|
105
|
+
- ~960 samples per 20ms frame (~768 kbps)
|
|
106
|
+
|
|
107
|
+
### JSON Control Messages
|
|
108
|
+
```json
|
|
109
|
+
// Join room
|
|
110
|
+
{"type": "join", "room": "room_id", "peer": "peer_id", "sampleRate": 48000}
|
|
111
|
+
|
|
112
|
+
// UDP token (server → client)
|
|
113
|
+
{"type": "udp_token", "token": "..."}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Installation
|
|
119
|
+
|
|
120
|
+
### 1. Install Package
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm install callum-voice
|
|
124
|
+
# or
|
|
125
|
+
yarn add callum-voice
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. iOS — Install Pods
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
cd ios && pod install && cd ..
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3. Android — Register Package
|
|
135
|
+
|
|
136
|
+
In `MainApplication.kt`:
|
|
137
|
+
|
|
138
|
+
```kotlin
|
|
139
|
+
import com.callumvoice.CallumVoicePackage
|
|
140
|
+
|
|
141
|
+
override fun getPackages(): List<ReactPackage> {
|
|
142
|
+
return listOf(
|
|
143
|
+
MainReactPackage(),
|
|
144
|
+
CallumVoicePackage() // ← Add this
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 4. Platform Permissions
|
|
150
|
+
|
|
151
|
+
#### Android (`android/app/src/main/AndroidManifest.xml`)
|
|
152
|
+
|
|
153
|
+
```xml
|
|
154
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
155
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
156
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### iOS (`ios/Runner/Info.plist`)
|
|
160
|
+
|
|
161
|
+
```xml
|
|
162
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
163
|
+
<string>This app needs microphone access for voice chat</string>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Quick Start
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { VoiceClient, createVoiceConfig } from 'callum-voice';
|
|
172
|
+
|
|
173
|
+
// 1. Create config
|
|
174
|
+
const config = createVoiceConfig({
|
|
175
|
+
server: 'callem.cloudfort.ir',
|
|
176
|
+
apiKey: 'vc_live_0123456789abcdef0123456789abcdef',
|
|
177
|
+
peerId: 'user_123',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 2. Create client
|
|
181
|
+
const client = new VoiceClient(config);
|
|
182
|
+
|
|
183
|
+
// 3. Set listener
|
|
184
|
+
client.setListener({
|
|
185
|
+
onConnected: () => console.log('Connected!'),
|
|
186
|
+
onPeerSpeaking: (peerId) => console.log(`${peerId} is speaking`),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// 4. Connect
|
|
190
|
+
await client.connect();
|
|
191
|
+
|
|
192
|
+
// 5. Join a room
|
|
193
|
+
await client.joinRoom('game_room_1');
|
|
194
|
+
|
|
195
|
+
// 6. Enable microphone
|
|
196
|
+
await client.enableMic();
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Complete Example
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
205
|
+
import {
|
|
206
|
+
View,
|
|
207
|
+
Text,
|
|
208
|
+
FlatList,
|
|
209
|
+
TouchableOpacity,
|
|
210
|
+
StyleSheet,
|
|
211
|
+
Alert,
|
|
212
|
+
} from 'react-native';
|
|
213
|
+
import {
|
|
214
|
+
VoiceClient,
|
|
215
|
+
VoiceClientState,
|
|
216
|
+
VoiceEventListener,
|
|
217
|
+
createVoiceConfig,
|
|
218
|
+
} from 'callum-voice';
|
|
219
|
+
|
|
220
|
+
const STATE_LABELS: Record<VoiceClientState, string> = {
|
|
221
|
+
[VoiceClientState.Disconnected]: 'Disconnected',
|
|
222
|
+
[VoiceClientState.Connecting]: 'Connecting...',
|
|
223
|
+
[VoiceClientState.Connected]: 'Connected',
|
|
224
|
+
[VoiceClientState.InRoom]: 'In Room',
|
|
225
|
+
[VoiceClientState.Reconnecting]: 'Reconnecting...',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export default function VoiceChatScreen() {
|
|
229
|
+
const clientRef = useRef<VoiceClient | null>(null);
|
|
230
|
+
const [state, setState] = useState<VoiceClientState>(VoiceClientState.Disconnected);
|
|
231
|
+
const [peers, setPeers] = useState<string[]>([]);
|
|
232
|
+
const [speakingPeers, setSpeakingPeers] = useState<Set<string>>(new Set());
|
|
233
|
+
const [micEnabled, setMicEnabled] = useState(false);
|
|
234
|
+
const [speakerEnabled, setSpeakerEnabled] = useState(false);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const config = createVoiceConfig({
|
|
238
|
+
server: 'callem.cloudfort.ir',
|
|
239
|
+
apiKey: 'vc_live_0123456789abcdef0123456789abcdef',
|
|
240
|
+
peerId: `user_${Date.now()}`,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const client = new VoiceClient(config);
|
|
244
|
+
clientRef.current = client;
|
|
245
|
+
|
|
246
|
+
const listener: VoiceEventListener = {
|
|
247
|
+
onConnected: () => console.log('Connected'),
|
|
248
|
+
onDisconnected: () => {
|
|
249
|
+
setPeers([]);
|
|
250
|
+
setSpeakingPeers(new Set());
|
|
251
|
+
},
|
|
252
|
+
onReconnecting: (attempt) => console.log(`Reconnecting #${attempt}`),
|
|
253
|
+
onAuthFailed: (reason) => Alert.alert('Auth Failed', reason),
|
|
254
|
+
onRoomJoined: (roomId) => console.log(`Joined ${roomId}`),
|
|
255
|
+
onRoomLeft: () => {
|
|
256
|
+
setPeers([]);
|
|
257
|
+
setSpeakingPeers(new Set());
|
|
258
|
+
},
|
|
259
|
+
onPeerJoined: (peerId) => setPeers(prev => [...prev, peerId]),
|
|
260
|
+
onPeerLeft: (peerId) => {
|
|
261
|
+
setPeers(prev => prev.filter(p => p !== peerId));
|
|
262
|
+
setSpeakingPeers(prev => {
|
|
263
|
+
const next = new Set(prev);
|
|
264
|
+
next.delete(peerId);
|
|
265
|
+
return next;
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
onPeerSpeaking: (peerId) =>
|
|
269
|
+
setSpeakingPeers(prev => new Set(prev).add(peerId)),
|
|
270
|
+
onPeerStopped: (peerId) =>
|
|
271
|
+
setSpeakingPeers(prev => {
|
|
272
|
+
const next = new Set(prev);
|
|
273
|
+
next.delete(peerId);
|
|
274
|
+
return next;
|
|
275
|
+
}),
|
|
276
|
+
onMicEnabled: () => setMicEnabled(true),
|
|
277
|
+
onMicDisabled: () => setMicEnabled(false),
|
|
278
|
+
onSpeakerEnabled: () => setSpeakerEnabled(true),
|
|
279
|
+
onSpeakerDisabled: () => setSpeakerEnabled(false),
|
|
280
|
+
onError: (error) => Alert.alert('Voice Error', error.message),
|
|
281
|
+
onStateChanged: (newState) => setState(newState),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
client.setListener(listener);
|
|
285
|
+
client.connect().then(() => client.joinRoom('game_room_1'));
|
|
286
|
+
|
|
287
|
+
return () => {
|
|
288
|
+
client.dispose();
|
|
289
|
+
clientRef.current = null;
|
|
290
|
+
};
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
const handleToggleMic = useCallback(async () => {
|
|
294
|
+
const client = clientRef.current;
|
|
295
|
+
if (!client) return;
|
|
296
|
+
if (micEnabled) {
|
|
297
|
+
client.disableMic();
|
|
298
|
+
} else {
|
|
299
|
+
await client.enableMic();
|
|
300
|
+
}
|
|
301
|
+
}, [micEnabled]);
|
|
302
|
+
|
|
303
|
+
const handleToggleSpeaker = useCallback(() => {
|
|
304
|
+
const client = clientRef.current;
|
|
305
|
+
if (!client) return;
|
|
306
|
+
if (speakerEnabled) {
|
|
307
|
+
client.disableSpeaker();
|
|
308
|
+
} else {
|
|
309
|
+
client.enableSpeaker();
|
|
310
|
+
}
|
|
311
|
+
}, [speakerEnabled]);
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<View style={styles.container}>
|
|
315
|
+
{/* Status Bar */}
|
|
316
|
+
<View style={styles.statusBar}>
|
|
317
|
+
<Text style={styles.statusText}>{STATE_LABELS[state]}</Text>
|
|
318
|
+
</View>
|
|
319
|
+
|
|
320
|
+
{/* Controls */}
|
|
321
|
+
<View style={styles.controls}>
|
|
322
|
+
<TouchableOpacity
|
|
323
|
+
style={[styles.button, micEnabled && styles.buttonActive]}
|
|
324
|
+
onPress={handleToggleMic}
|
|
325
|
+
>
|
|
326
|
+
<Text style={styles.buttonText}>
|
|
327
|
+
{micEnabled ? '🎤 Mute' : '🎤 Unmute'}
|
|
328
|
+
</Text>
|
|
329
|
+
</TouchableOpacity>
|
|
330
|
+
|
|
331
|
+
<TouchableOpacity
|
|
332
|
+
style={[styles.button, speakerEnabled && styles.buttonActive]}
|
|
333
|
+
onPress={handleToggleSpeaker}
|
|
334
|
+
>
|
|
335
|
+
<Text style={styles.buttonText}>
|
|
336
|
+
{speakerEnabled ? '🔊 Speaker Off' : '🔇 Speaker On'}
|
|
337
|
+
</Text>
|
|
338
|
+
</TouchableOpacity>
|
|
339
|
+
</View>
|
|
340
|
+
|
|
341
|
+
{/* Peers List */}
|
|
342
|
+
<FlatList
|
|
343
|
+
data={peers}
|
|
344
|
+
keyExtractor={(item) => item}
|
|
345
|
+
renderItem={({ item }) => {
|
|
346
|
+
const isSpeaking = speakingPeers.has(item);
|
|
347
|
+
return (
|
|
348
|
+
<View style={styles.peerRow}>
|
|
349
|
+
<Text style={styles.peerIcon}>{isSpeaking ? '🟢' : '⚪'}</Text>
|
|
350
|
+
<View style={styles.peerInfo}>
|
|
351
|
+
<Text style={styles.peerName}>{item}</Text>
|
|
352
|
+
<Text style={styles.peerStatus}>
|
|
353
|
+
{isSpeaking ? 'Speaking...' : 'Silent'}
|
|
354
|
+
</Text>
|
|
355
|
+
</View>
|
|
356
|
+
</View>
|
|
357
|
+
);
|
|
358
|
+
}}
|
|
359
|
+
ListEmptyComponent={
|
|
360
|
+
<Text style={styles.emptyText}>No peers in room</Text>
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
</View>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const styles = StyleSheet.create({
|
|
368
|
+
container: { flex: 1, backgroundColor: '#1a1a2e' },
|
|
369
|
+
statusBar: {
|
|
370
|
+
padding: 16,
|
|
371
|
+
backgroundColor: '#16213e',
|
|
372
|
+
alignItems: 'center',
|
|
373
|
+
},
|
|
374
|
+
statusText: { color: '#e94560', fontSize: 18, fontWeight: 'bold' },
|
|
375
|
+
controls: {
|
|
376
|
+
flexDirection: 'row',
|
|
377
|
+
justifyContent: 'center',
|
|
378
|
+
padding: 16,
|
|
379
|
+
gap: 16,
|
|
380
|
+
},
|
|
381
|
+
button: {
|
|
382
|
+
paddingHorizontal: 24,
|
|
383
|
+
paddingVertical: 12,
|
|
384
|
+
backgroundColor: '#0f3460',
|
|
385
|
+
borderRadius: 8,
|
|
386
|
+
},
|
|
387
|
+
buttonActive: { backgroundColor: '#e94560' },
|
|
388
|
+
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
389
|
+
peerRow: {
|
|
390
|
+
flexDirection: 'row',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
paddingHorizontal: 16,
|
|
393
|
+
paddingVertical: 12,
|
|
394
|
+
borderBottomWidth: 1,
|
|
395
|
+
borderBottomColor: '#16213e',
|
|
396
|
+
},
|
|
397
|
+
peerIcon: { fontSize: 24, marginRight: 12 },
|
|
398
|
+
peerInfo: { flex: 1 },
|
|
399
|
+
peerName: { color: '#fff', fontSize: 16, fontWeight: '500' },
|
|
400
|
+
peerStatus: { color: '#888', fontSize: 13 },
|
|
401
|
+
emptyText: { color: '#888', textAlign: 'center', marginTop: 40, fontSize: 16 },
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## API Reference
|
|
408
|
+
|
|
409
|
+
### VoiceClient
|
|
410
|
+
|
|
411
|
+
The main class for managing voice connections.
|
|
412
|
+
|
|
413
|
+
#### Constructor
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
new VoiceClient(config: VoiceConfig)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
| Parameter | Type | Description |
|
|
420
|
+
|-----------|------|-------------|
|
|
421
|
+
| `config` | `VoiceConfig` | Connection and audio configuration |
|
|
422
|
+
|
|
423
|
+
#### Properties
|
|
424
|
+
|
|
425
|
+
| Property | Type | Description |
|
|
426
|
+
|----------|------|-------------|
|
|
427
|
+
| `state` | `VoiceClientState` | Current connection state (read-only) |
|
|
428
|
+
| `isConnected` | `boolean` | Whether connected to server |
|
|
429
|
+
| `isInRoom` | `boolean` | Whether joined a room |
|
|
430
|
+
| `isMicEnabled` | `boolean` | Whether microphone is active |
|
|
431
|
+
| `isSpeakerEnabled` | `boolean` | Whether speaker output is active |
|
|
432
|
+
| `currentRoomId` | `string \| null` | Current room ID |
|
|
433
|
+
| `peerIds` | `string[]` | List of peer IDs in the current room |
|
|
434
|
+
|
|
435
|
+
#### Methods
|
|
436
|
+
|
|
437
|
+
| Method | Returns | Description |
|
|
438
|
+
|--------|---------|-------------|
|
|
439
|
+
| `setListener(listener)` | `void` | Set the event listener |
|
|
440
|
+
| `connect()` | `Promise<void>` | Connect to the voice server |
|
|
441
|
+
| `disconnect()` | `void` | Disconnect from the server |
|
|
442
|
+
| `joinRoom(roomId)` | `Promise<void>` | Join a voice room |
|
|
443
|
+
| `leaveRoom()` | `Promise<void>` | Leave the current room |
|
|
444
|
+
| `enableMic()` | `Promise<void>` | Enable microphone (must be in room) |
|
|
445
|
+
| `disableMic()` | `void` | Disable microphone |
|
|
446
|
+
| `toggleMic()` | `Promise<boolean>` | Toggle mic, returns new state |
|
|
447
|
+
| `enableSpeaker()` | `void` | Enable speaker output |
|
|
448
|
+
| `disableSpeaker()` | `void` | Disable speaker output |
|
|
449
|
+
| `toggleSpeaker()` | `boolean` | Toggle speaker, returns new state |
|
|
450
|
+
| `isPeerSpeaking(peerId)` | `boolean` | Check if a specific peer is speaking |
|
|
451
|
+
| `getPeerSpeakingInfo(peerId)` | `{speaking, rms}` | Get peer speaking state with RMS level |
|
|
452
|
+
| `sendAudio(samples)` | `void` | Send captured audio to server (from native) |
|
|
453
|
+
| `dispose()` | `void` | Cleanup and release resources |
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## VoiceConfig
|
|
458
|
+
|
|
459
|
+
Configuration for connecting to the voice chat service.
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
interface VoiceConfig {
|
|
463
|
+
server: string;
|
|
464
|
+
apiKey: string;
|
|
465
|
+
peerId: string;
|
|
466
|
+
useTls?: boolean;
|
|
467
|
+
sampleRate: number;
|
|
468
|
+
autoReconnect: boolean;
|
|
469
|
+
maxReconnectAttempts: number;
|
|
470
|
+
reconnectDelayMs: number;
|
|
471
|
+
echoCancellation: boolean;
|
|
472
|
+
noiseSuppression: boolean;
|
|
473
|
+
autoGainControl: boolean;
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### createVoiceConfig Helper
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
function createVoiceConfig(overrides: Partial<VoiceConfig>): VoiceConfig
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Creates a `VoiceConfig` with sensible defaults, merged with your overrides.
|
|
484
|
+
|
|
485
|
+
| Property | Type | Default | Description |
|
|
486
|
+
|----------|------|---------|-------------|
|
|
487
|
+
| `server` | `string` | `''` | Server address (e.g. `"callem.cloudfort.ir"`). Do NOT include scheme or port. |
|
|
488
|
+
| `apiKey` | `string` | `''` | API key from account registration (format: `vc_live_...`) |
|
|
489
|
+
| `peerId` | `string` | `''` | Unique identifier for this peer/user |
|
|
490
|
+
| `useTls` | `boolean?` | `undefined` | Use WSS instead of WS. Auto-detected when undefined. |
|
|
491
|
+
| `sampleRate` | `number` | `48000` | Audio sample rate in Hz |
|
|
492
|
+
| `autoReconnect` | `boolean` | `true` | Automatically reconnect on disconnect |
|
|
493
|
+
| `maxReconnectAttempts` | `number` | `5` | Maximum reconnection attempts |
|
|
494
|
+
| `reconnectDelayMs` | `number` | `2000` | Delay between reconnection attempts (ms) |
|
|
495
|
+
| `echoCancellation` | `boolean` | `true` | Enable platform echo cancellation |
|
|
496
|
+
| `noiseSuppression` | `boolean` | `true` | Enable platform noise suppression |
|
|
497
|
+
| `autoGainControl` | `boolean` | `true` | Enable platform automatic gain control |
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## VoiceEventListener
|
|
502
|
+
|
|
503
|
+
Interface for receiving voice client events. All methods are optional.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
interface VoiceEventListener {
|
|
507
|
+
onConnected?(): void;
|
|
508
|
+
onDisconnected?(): void;
|
|
509
|
+
onReconnecting?(attempt: number): void;
|
|
510
|
+
onAuthFailed?(reason: string): void;
|
|
511
|
+
onRoomJoined?(roomId: string): void;
|
|
512
|
+
onRoomLeft?(roomId: string): void;
|
|
513
|
+
onPeerJoined?(peerId: string): void;
|
|
514
|
+
onPeerLeft?(peerId: string): void;
|
|
515
|
+
onPeerSpeaking?(peerId: string): void;
|
|
516
|
+
onPeerStopped?(peerId: string): void;
|
|
517
|
+
onMicEnabled?(): void;
|
|
518
|
+
onMicDisabled?(): void;
|
|
519
|
+
onSpeakerEnabled?(): void;
|
|
520
|
+
onSpeakerDisabled?(): void;
|
|
521
|
+
onError?(error: Error): void;
|
|
522
|
+
onStateChanged?(newState: VoiceClientState): void;
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Events
|
|
527
|
+
|
|
528
|
+
| Method | Parameters | Description |
|
|
529
|
+
|--------|-----------|-------------|
|
|
530
|
+
| `onConnected` | — | Connection established |
|
|
531
|
+
| `onDisconnected` | — | Connection lost |
|
|
532
|
+
| `onReconnecting` | `attempt` — current attempt number | Reconnection in progress |
|
|
533
|
+
| `onAuthFailed` | `reason` — failure description | Authentication failed |
|
|
534
|
+
| `onRoomJoined` | `roomId` — joined room ID | Successfully joined a room |
|
|
535
|
+
| `onRoomLeft` | `roomId` — left room ID | Left a room |
|
|
536
|
+
| `onPeerJoined` | `peerId` — new peer's ID | A peer joined the room |
|
|
537
|
+
| `onPeerLeft` | `peerId` — departing peer's ID | A peer left the room |
|
|
538
|
+
| `onPeerSpeaking` | `peerId` — speaking peer's ID | Peer started speaking |
|
|
539
|
+
| `onPeerStopped` | `peerId` — silent peer's ID | Peer stopped speaking |
|
|
540
|
+
| `onMicEnabled` | — | Microphone enabled |
|
|
541
|
+
| `onMicDisabled` | — | Microphone disabled |
|
|
542
|
+
| `onSpeakerEnabled` | — | Speaker output enabled |
|
|
543
|
+
| `onSpeakerDisabled` | — | Speaker output disabled |
|
|
544
|
+
| `onError` | `error` — Error object | An error occurred |
|
|
545
|
+
| `onStateChanged` | `newState` — VoiceClientState | Connection state changed |
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Native Modules
|
|
550
|
+
|
|
551
|
+
### Android (Kotlin)
|
|
552
|
+
|
|
553
|
+
The Android native module at `android/src/main/java/com/callumvoice/CallumVoiceModule.kt` provides:
|
|
554
|
+
|
|
555
|
+
| Method | Description |
|
|
556
|
+
|--------|-------------|
|
|
557
|
+
| `startAudioCapture()` | Start microphone recording via `AudioRecord` |
|
|
558
|
+
| `stopAudioCapture()` | Stop microphone recording |
|
|
559
|
+
| `startAudioPlayback()` | Start audio playback via `AudioTrack` |
|
|
560
|
+
| `stopAudioPlayback()` | Stop audio playback |
|
|
561
|
+
| `enqueuePeerAudio(peerId, samples)` | Write PCM samples to AudioTrack |
|
|
562
|
+
| `setSpeakerphoneOn(on)` | Route audio to speaker/earpiece |
|
|
563
|
+
| `configureAudioSession()` | Set `MODE_IN_COMMUNICATION` |
|
|
564
|
+
|
|
565
|
+
**Events sent to JavaScript:**
|
|
566
|
+
- `onAudioCaptured` — Int16 array of captured PCM samples
|
|
567
|
+
- `onAudioError` — Error message string
|
|
568
|
+
|
|
569
|
+
### iOS (Swift)
|
|
570
|
+
|
|
571
|
+
The iOS native module at `ios/CallumVoiceModule.swift` provides:
|
|
572
|
+
|
|
573
|
+
| Method | Description |
|
|
574
|
+
|--------|-------------|
|
|
575
|
+
| `startAudioCapture()` | Start microphone capture via `AVAudioEngine` |
|
|
576
|
+
| `stopAudioCapture()` | Stop microphone capture |
|
|
577
|
+
| `startAudioPlayback()` | Start audio playback via `AVAudioEngine` |
|
|
578
|
+
| `stopAudioPlayback()` | Stop audio playback |
|
|
579
|
+
| `enqueuePeerAudio(peerId, samples)` | Schedule PCM buffer for playback |
|
|
580
|
+
| `setSpeakerphoneOn(on)` | Override output audio port |
|
|
581
|
+
| `configureAudioSession()` | Configure `AVAudioSession` for voice chat |
|
|
582
|
+
|
|
583
|
+
**Events sent to JavaScript:**
|
|
584
|
+
- `onAudioCaptured` — Int16 array of captured PCM samples
|
|
585
|
+
- `onAudioError` — Error message string
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Permissions
|
|
590
|
+
|
|
591
|
+
### Android
|
|
592
|
+
|
|
593
|
+
| Permission | Required | Description |
|
|
594
|
+
|-----------|----------|-------------|
|
|
595
|
+
| `INTERNET` | Yes | WebSocket connection to server |
|
|
596
|
+
| `RECORD_AUDIO` | Yes | Microphone capture |
|
|
597
|
+
| `MODIFY_AUDIO_SETTINGS` | Yes | Audio routing control |
|
|
598
|
+
| `ACCESS_NETWORK_STATE` | Recommended | Network change detection |
|
|
599
|
+
|
|
600
|
+
Request at runtime:
|
|
601
|
+
```typescript
|
|
602
|
+
import { PermissionsAndroid } from 'react-native';
|
|
603
|
+
|
|
604
|
+
const granted = await PermissionsAndroid.request(
|
|
605
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
606
|
+
{ title: 'Microphone', message: 'Needed for voice chat' }
|
|
607
|
+
);
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### iOS
|
|
611
|
+
|
|
612
|
+
| Key | Required | Description |
|
|
613
|
+
|-----|----------|-------------|
|
|
614
|
+
| `NSMicrophoneUsageDescription` | Yes | Microphone access reason |
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## Important Notes
|
|
619
|
+
|
|
620
|
+
### Connection Flow
|
|
621
|
+
1. Create `VoiceConfig` with server, API key, and peer ID
|
|
622
|
+
2. Create `VoiceClient` with config
|
|
623
|
+
3. Set a `VoiceEventListener`
|
|
624
|
+
4. Call `connect()` — establishes WebSocket connection
|
|
625
|
+
5. Call `joinRoom(roomId)` — joins a voice room
|
|
626
|
+
6. Call `enableMic()` — starts sending audio
|
|
627
|
+
7. Call `enableSpeaker()` — starts receiving audio
|
|
628
|
+
|
|
629
|
+
### Audio Format
|
|
630
|
+
- **PCM 16-bit signed integer**, mono, 48000 Hz
|
|
631
|
+
- 960 samples per 20ms frame
|
|
632
|
+
- ~768 kbps bandwidth per peer
|
|
633
|
+
|
|
634
|
+
### Speaking Detection
|
|
635
|
+
- RMS threshold: **0.012** (normalized)
|
|
636
|
+
- Check interval: **80ms**
|
|
637
|
+
- Window: last **480 samples** (~10ms at 48kHz)
|
|
638
|
+
|
|
639
|
+
### Ring Buffer
|
|
640
|
+
- Capacity: **96,000 samples** (~2 seconds at 48kHz)
|
|
641
|
+
- Oldest data is overwritten when full
|
|
642
|
+
|
|
643
|
+
### Auto-Reconnect
|
|
644
|
+
- Saves current room and mic state before disconnect
|
|
645
|
+
- Attempts reconnection up to `maxReconnectAttempts` times
|
|
646
|
+
- Restores room and mic state on successful reconnection
|
|
647
|
+
- Delay between attempts: `reconnectDelayMs` (default 2000ms)
|
|
648
|
+
|
|
649
|
+
### Authentication
|
|
650
|
+
- API key format: `vc_live_` followed by 32 hex characters
|
|
651
|
+
- Sent as query parameter in WebSocket URL
|
|
652
|
+
- Auth failures return close code `1008` or `4001`
|
|
653
|
+
|
|
654
|
+
### Multi-Peer Audio Mixing
|
|
655
|
+
- Volume scaling: `min(1.0, 0.8 / peerCount)`
|
|
656
|
+
- Prevents clipping when multiple peers speak simultaneously
|
|
657
|
+
|
|
658
|
+
### Thread Safety
|
|
659
|
+
- WebSocket events handled on the JS thread
|
|
660
|
+
- Native audio capture/playback on dedicated threads
|
|
661
|
+
- Peer state managed in JS single-threaded context
|
|
662
|
+
|
|
663
|
+
### Cleanup
|
|
664
|
+
- Always call `dispose()` when done to release resources
|
|
665
|
+
- `disconnect()` disables auto-reconnect before cleaning up
|
|
666
|
+
- Native audio resources are released in `onCatalystInstanceDestroy` / `deinit`
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## License
|
|
671
|
+
|
|
672
|
+
Copyright © Cloudfort. All Rights Reserved.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = {prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
apply plugin: 'com.android.library'
|
|
8
|
+
apply plugin: 'kotlin-android'
|
|
9
|
+
|
|
10
|
+
android {
|
|
11
|
+
namespace "com.callumvoice"
|
|
12
|
+
compileSdkVersion safeExtGet('compileSdkVersion', 34)
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
16
|
+
targetSdkVersion safeExtGet('targetSdkVersion', 34)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
compileOptions {
|
|
20
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
21
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
kotlinOptions {
|
|
25
|
+
jvmTarget = "1.8"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
sourceSets {
|
|
29
|
+
main {
|
|
30
|
+
java.srcDirs = ['src/main/java']
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dependencies {
|
|
36
|
+
implementation "com.facebook.react:react-android:+"
|
|
37
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:+"
|
|
38
|
+
}
|