@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.
@@ -0,0 +1,9 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.callumvoice">
3
+
4
+ <uses-permission android:name="android.permission.INTERNET" />
5
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
6
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
7
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8
+
9
+ </manifest>
@@ -0,0 +1,289 @@
1
+ // Copyright Cloudfort. All Rights Reserved.
2
+
3
+ package com.callumvoice
4
+
5
+ import android.media.AudioFormat
6
+ import android.media.AudioManager
7
+ import android.media.AudioRecord
8
+ import android.media.AudioTrack
9
+ import android.media.MediaRecorder
10
+ import android.os.Build
11
+ import com.facebook.react.bridge.*
12
+ import com.facebook.react.modules.core.DeviceEventManagerModule
13
+
14
+ /**
15
+ * React Native native module for Android audio capture and playback.
16
+ *
17
+ * Handles microphone recording (AudioRecord) and audio playback (AudioTrack)
18
+ * using PCM 16-bit mono at 48000 Hz. Communicates audio data to/from the
19
+ * JavaScript layer via DeviceEventManagerModule events.
20
+ */
21
+ class CallumVoiceModule(reactContext: ReactApplicationContext)
22
+ : ReactContextBaseJavaModule(reactContext) {
23
+
24
+ companion object {
25
+ private const val MODULE_NAME = "CallumVoice"
26
+ private const val SAMPLE_RATE = 48000
27
+ private const val FRAME_SIZE = 960 // 20ms at 48kHz
28
+
29
+ // JS event names
30
+ private const val EVENT_AUDIO_CAPTURED = "onAudioCaptured"
31
+ private const val EVENT_AUDIO_ERROR = "onAudioError"
32
+ }
33
+
34
+ private var audioRecord: AudioRecord? = null
35
+ private var audioTrack: AudioTrack? = null
36
+ private var captureThread: Thread? = null
37
+ private var playbackThread: Thread? = null
38
+ private var captureRunning = false
39
+ private var playbackRunning = false
40
+
41
+ override fun getName(): String = MODULE_NAME
42
+
43
+ /**
44
+ * Exported constants accessible from JavaScript via NativeModules.CallumVoice.
45
+ */
46
+ override fun getConstants(): Map<String, Any> {
47
+ return hashMapOf(
48
+ "SAMPLE_RATE" to SAMPLE_RATE,
49
+ "FRAME_SIZE" to FRAME_SIZE
50
+ )
51
+ }
52
+
53
+ // ── Microphone Capture ─────────────────────────────────────────
54
+
55
+ @ReactMethod
56
+ fun startAudioCapture(promise: Promise) {
57
+ try {
58
+ val bufferSize = AudioRecord.getMinBufferSize(
59
+ SAMPLE_RATE,
60
+ AudioFormat.CHANNEL_IN_MONO,
61
+ AudioFormat.ENCODING_PCM_16BIT
62
+ )
63
+
64
+ if (bufferSize == AudioRecord.ERROR || bufferSize == AudioRecord.ERROR_BAD_VALUE) {
65
+ promise.reject("AUDIO_ERROR", "Cannot get minimum buffer size")
66
+ return
67
+ }
68
+
69
+ val audioSource = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
70
+ MediaRecorder.AudioSource.VOICE_COMMUNICATION
71
+ } else {
72
+ MediaRecorder.AudioSource.VOICE_COMMUNICATION
73
+ }
74
+
75
+ audioRecord = AudioRecord(
76
+ audioSource,
77
+ SAMPLE_RATE,
78
+ AudioFormat.CHANNEL_IN_MONO,
79
+ AudioFormat.ENCODING_PCM_16BIT,
80
+ bufferSize * 2
81
+ )
82
+
83
+ if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
84
+ promise.reject("AUDIO_ERROR", "AudioRecord initialization failed")
85
+ return
86
+ }
87
+
88
+ // Configure audio processing
89
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
90
+ val audioSessionId = audioRecord!!.audioSessionId
91
+ // TODO: Attach AcousticEchoCanceler, NoiseSuppressor, AutomaticGainController
92
+ }
93
+
94
+ audioRecord!!.startRecording()
95
+ captureRunning = true
96
+
97
+ captureThread = Thread {
98
+ val buffer = ShortArray(FRAME_SIZE)
99
+ while (captureRunning) {
100
+ val read = audioRecord?.read(buffer, 0, FRAME_SIZE) ?: -1
101
+ if (read > 0) {
102
+ // Send captured PCM samples to JavaScript
103
+ val writableArray = Arguments.createArray()
104
+ for (i in 0 until read) {
105
+ writableArray.pushInt(buffer[i].toInt())
106
+ }
107
+ reactApplicationContext
108
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
109
+ .emit(EVENT_AUDIO_CAPTURED, writableArray)
110
+ }
111
+ }
112
+ }.apply {
113
+ name = "CallumVoice-Capture"
114
+ start()
115
+ }
116
+
117
+ promise.resolve(null)
118
+ } catch (e: SecurityException) {
119
+ promise.reject("PERMISSION_DENIED", "Microphone permission not granted", e)
120
+ } catch (e: Exception) {
121
+ promise.reject("AUDIO_ERROR", "Failed to start audio capture: ${e.message}", e)
122
+ }
123
+ }
124
+
125
+ @ReactMethod
126
+ fun stopAudioCapture(promise: Promise) {
127
+ captureRunning = false
128
+ try {
129
+ captureThread?.join(1000)
130
+ audioRecord?.stop()
131
+ audioRecord?.release()
132
+ } catch (e: Exception) {
133
+ // ignore cleanup errors
134
+ }
135
+ audioRecord = null
136
+ captureThread = null
137
+ promise.resolve(null)
138
+ }
139
+
140
+ // ── Audio Playback ─────────────────────────────────────────────
141
+
142
+ @ReactMethod
143
+ fun startAudioPlayback(promise: Promise) {
144
+ try {
145
+ val bufferSize = AudioTrack.getMinBufferSize(
146
+ SAMPLE_RATE,
147
+ AudioFormat.CHANNEL_OUT_MONO,
148
+ AudioFormat.ENCODING_PCM_16BIT
149
+ )
150
+
151
+ if (bufferSize == AudioTrack.ERROR || bufferSize == AudioTrack.ERROR_BAD_VALUE) {
152
+ promise.reject("AUDIO_ERROR", "Cannot get minimum buffer size")
153
+ return
154
+ }
155
+
156
+ val streamType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
157
+ AudioManager.STREAM_VOICE_CALL
158
+ } else {
159
+ AudioManager.STREAM_VOICE_CALL
160
+ }
161
+
162
+ audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
163
+ AudioTrack.Builder()
164
+ .setAudioAttributes(
165
+ android.media.AudioAttributes.Builder()
166
+ .setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION)
167
+ .setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
168
+ .build()
169
+ )
170
+ .setAudioFormat(
171
+ AudioFormat.Builder()
172
+ .setSampleRate(SAMPLE_RATE)
173
+ .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
174
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
175
+ .build()
176
+ )
177
+ .setBufferSizeInBytes(bufferSize * 2)
178
+ .setTransferMode(AudioTrack.MODE_STREAM)
179
+ .build()
180
+ } else {
181
+ @Suppress("DEPRECATION")
182
+ AudioTrack(
183
+ streamType,
184
+ SAMPLE_RATE,
185
+ AudioFormat.CHANNEL_OUT_MONO,
186
+ AudioFormat.ENCODING_PCM_16BIT,
187
+ bufferSize * 2,
188
+ AudioTrack.MODE_STREAM
189
+ )
190
+ }
191
+
192
+ if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
193
+ promise.reject("AUDIO_ERROR", "AudioTrack initialization failed")
194
+ return
195
+ }
196
+
197
+ audioTrack!!.play()
198
+ playbackRunning = true
199
+
200
+ // TODO: Implement playback loop that receives audio from JS and writes to AudioTrack
201
+ // For now, audio playback is handled via enqueuePeerAudio method
202
+
203
+ promise.resolve(null)
204
+ } catch (e: Exception) {
205
+ promise.reject("AUDIO_ERROR", "Failed to start audio playback: ${e.message}", e)
206
+ }
207
+ }
208
+
209
+ @ReactMethod
210
+ fun stopAudioPlayback(promise: Promise) {
211
+ playbackRunning = false
212
+ try {
213
+ playbackThread?.join(1000)
214
+ audioTrack?.stop()
215
+ audioTrack?.release()
216
+ } catch (e: Exception) {
217
+ // ignore cleanup errors
218
+ }
219
+ audioTrack = null
220
+ playbackThread = null
221
+ promise.resolve(null)
222
+ }
223
+
224
+ /**
225
+ * Enqueue PCM audio samples for a specific peer.
226
+ * Called from JavaScript when binary audio is received from the server.
227
+ */
228
+ @ReactMethod
229
+ fun enqueuePeerAudio(peerId: String, samples: ReadableArray) {
230
+ if (audioTrack == null || !playbackRunning) return
231
+
232
+ val shortArray = ShortArray(samples.size())
233
+ for (i in 0 until samples.size()) {
234
+ shortArray[i] = samples.getInt(i).toShort()
235
+ }
236
+
237
+ audioTrack?.write(shortArray, 0, shortArray.size)
238
+ }
239
+
240
+ // ── Speaker Routing ────────────────────────────────────────────
241
+
242
+ @ReactMethod
243
+ fun setSpeakerphoneOn(on: Boolean, promise: Promise) {
244
+ try {
245
+ val audioManager = reactApplicationContext
246
+ .getSystemService(ReactApplicationContext.AUDIO_SERVICE) as AudioManager
247
+
248
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
249
+ audioManager.isSpeakerphoneOn = on
250
+
251
+ promise.resolve(null)
252
+ } catch (e: Exception) {
253
+ promise.reject("AUDIO_ERROR", "Failed to set speakerphone: ${e.message}", e)
254
+ }
255
+ }
256
+
257
+ // ── Audio Session ──────────────────────────────────────────────
258
+
259
+ @ReactMethod
260
+ fun configureAudioSession(promise: Promise) {
261
+ try {
262
+ val audioManager = reactApplicationContext
263
+ .getSystemService(ReactApplicationContext.AUDIO_SERVICE) as AudioManager
264
+
265
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
266
+ audioManager.isMusicActive = false
267
+
268
+ promise.resolve(null)
269
+ } catch (e: Exception) {
270
+ promise.reject("AUDIO_ERROR", "Failed to configure audio session: ${e.message}", e)
271
+ }
272
+ }
273
+
274
+ override fun onCatalystInstanceDestroy() {
275
+ super.onCatalystInstanceDestroy()
276
+ captureRunning = false
277
+ playbackRunning = false
278
+ try {
279
+ audioRecord?.stop()
280
+ audioRecord?.release()
281
+ } catch (e: Exception) { }
282
+ try {
283
+ audioTrack?.stop()
284
+ audioTrack?.release()
285
+ } catch (e: Exception) { }
286
+ audioRecord = null
287
+ audioTrack = null
288
+ }
289
+ }
@@ -0,0 +1,32 @@
1
+ // Copyright Cloudfort. All Rights Reserved.
2
+
3
+ package com.callumvoice
4
+
5
+ import com.facebook.react.ReactPackage
6
+ import com.facebook.react.bridge.NativeModule
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ /**
11
+ * React Native package registration for CallumVoice native module.
12
+ *
13
+ * Add this to your MainApplication.java/kt:
14
+ * ```
15
+ * override fun getPackages(): List<ReactPackage> {
16
+ * return listOf(
17
+ * MainReactPackage(),
18
+ * CallumVoicePackage()
19
+ * )
20
+ * }
21
+ * ```
22
+ */
23
+ class CallumVoicePackage : ReactPackage {
24
+
25
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
26
+ return listOf(CallumVoiceModule(reactContext))
27
+ }
28
+
29
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
30
+ return emptyList()
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ require_relative '../node_modules/react-native/scripts/react_native_pods'
2
+ require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
3
+
4
+ platform :ios, '13.0'
5
+
6
+ Pod::Spec.new do |s|
7
+ s.name = "callum-voice"
8
+ s.version = "1.0.0"
9
+ s.summary = "Real-time voice chat SDK for React Native"
10
+ s.homepage = "https://cloudfort.ir"
11
+ s.license = "MIT"
12
+ s.author = "Cloudfort"
13
+ s.source = { :git => "https://github.com/cloudfort/callum-voice.git", :tag => s.version.to_s }
14
+ s.platforms = { :ios => "13.0" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,swift}"
17
+
18
+ s.dependency "React-Core"
19
+
20
+ s.frameworks = "AVFoundation", "AudioToolbox"
21
+ end
@@ -0,0 +1,7 @@
1
+ // Copyright Cloudfort. All Rights Reserved.
2
+
3
+ #import "CallumVoiceModule-Bridging-Header.h"
4
+
5
+ // This file is needed for React Native to discover the native module.
6
+ // The actual implementation is in CallumVoiceModule.swift.
7
+ // The bridging header exposes the Swift class to Objective-C.
@@ -0,0 +1,7 @@
1
+ // Copyright Cloudfort. All Rights Reserved.
2
+
3
+ #import <React/RCTBridgeModule.h>
4
+ #import <React/RCTEventEmitter.h>
5
+
6
+ @interface CallumVoiceModule : RCTEventEmitter <RCTBridgeModule>
7
+ @end
@@ -0,0 +1,198 @@
1
+ // Copyright Cloudfort. All Rights Reserved.
2
+
3
+ import Foundation
4
+ import AVFoundation
5
+ import React
6
+
7
+ /// React Native native module for iOS audio capture and playback.
8
+ ///
9
+ /// Uses AVAudioEngine for microphone capture and audio playback.
10
+ /// Communicates audio data to/from the JavaScript layer via RCTEventEmitter.
11
+ @objc(CallumVoiceModule)
12
+ class CallumVoiceModule: RCTEventEmitter {
13
+
14
+ private let sampleRate: Double = 48000
15
+ private let frameSize: AVAudioFrameCount = 960 // 20ms at 48kHz
16
+
17
+ private var audioEngine: AVAudioEngine?
18
+ private var outputEngine: AVAudioEngine?
19
+ private var captureRunning = false
20
+ private var playbackRunning = false
21
+
22
+ override static func moduleName() -> String! {
23
+ return "CallumVoice"
24
+ }
25
+
26
+ override static func requiresMainQueueSetup() -> Bool {
27
+ return false
28
+ }
29
+
30
+ override func supportedEvents() -> [String]! {
31
+ return ["onAudioCaptured", "onAudioError"]
32
+ }
33
+
34
+ override func constantsToExport() -> [AnyHashable : Any]! {
35
+ return [
36
+ "SAMPLE_RATE": Int(sampleRate),
37
+ "FRAME_SIZE": Int(frameSize)
38
+ ]
39
+ }
40
+
41
+ // MARK: - Audio Capture
42
+
43
+ @objc
44
+ func startAudioCapture(_ resolve: @escaping RCTPromiseResolveBlock,
45
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
46
+ do {
47
+ let session = AVAudioSession.sharedInstance()
48
+ try session.setCategory(.playAndRecord,
49
+ mode: .voiceChat,
50
+ options: [.defaultToSpeaker, .allowBluetooth])
51
+ try session.setActive(true)
52
+ try session.setPreferredSampleRate(sampleRate)
53
+
54
+ let engine = AVAudioEngine()
55
+ let inputNode = engine.inputNode
56
+ let inputFormat = inputNode.outputFormat(forBus: 0)
57
+
58
+ // Install tap to capture PCM audio
59
+ inputNode.installTap(onBus: 0, bufferSize: frameSize, format: inputFormat) {
60
+ [weak self] buffer, time in
61
+ guard let self = self, self.captureRunning else { return }
62
+
63
+ // Convert to Int16 array and send to JS
64
+ let channelData = buffer.int16ChannelData![0]
65
+ let frameLength = Int(buffer.frameLength)
66
+ var samples = [Int16](repeating: 0, count: frameLength)
67
+ for i in 0..<frameLength {
68
+ samples[i] = channelData[i]
69
+ }
70
+
71
+ self.sendEvent(withName: "onAudioCaptured", body: samples)
72
+ }
73
+
74
+ engine.prepare()
75
+ try engine.start()
76
+
77
+ audioEngine = engine
78
+ captureRunning = true
79
+
80
+ resolve(nil)
81
+ } catch {
82
+ reject("AUDIO_ERROR", "Failed to start audio capture: \(error.localizedDescription)", error)
83
+ }
84
+ }
85
+
86
+ @objc
87
+ func stopAudioCapture(_ resolve: @escaping RCTPromiseResolveBlock,
88
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
89
+ captureRunning = false
90
+ audioEngine?.inputNode.removeTap(onBus: 0)
91
+ audioEngine?.stop()
92
+ audioEngine = nil
93
+ resolve(nil)
94
+ }
95
+
96
+ // MARK: - Audio Playback
97
+
98
+ @objc
99
+ func startAudioPlayback(_ resolve: @escaping RCTPromiseResolveBlock,
100
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
101
+ do {
102
+ let session = AVAudioSession.sharedInstance()
103
+ try session.setCategory(.playAndRecord,
104
+ mode: .voiceChat,
105
+ options: [.defaultToSpeaker, .allowBluetooth])
106
+ try session.setActive(true)
107
+
108
+ let engine = AVAudioEngine()
109
+ let outputNode = engine.outputNode
110
+ let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate,
111
+ channels: 1)
112
+
113
+ engine.prepare()
114
+ try engine.start()
115
+
116
+ outputEngine = engine
117
+ playbackRunning = true
118
+
119
+ resolve(nil)
120
+ } catch {
121
+ reject("AUDIO_ERROR", "Failed to start audio playback: \(error.localizedDescription)", error)
122
+ }
123
+ }
124
+
125
+ @objc
126
+ func stopAudioPlayback(_ resolve: @escaping RCTPromiseResolveBlock,
127
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
128
+ playbackRunning = false
129
+ outputEngine?.stop()
130
+ outputEngine = nil
131
+ resolve(nil)
132
+ }
133
+
134
+ /// Enqueue PCM audio samples for a specific peer.
135
+ @objc
136
+ func enqueuePeerAudio(_ peerId: String, samples: [Int16]) {
137
+ guard playbackRunning, let engine = outputEngine else { return }
138
+
139
+ let frameCount = AVAudioFrameCount(samples.count)
140
+ guard let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1),
141
+ let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
142
+ return
143
+ }
144
+
145
+ buffer.frameLength = frameCount
146
+ let channelData = buffer.int16ChannelData![0]
147
+ for i in 0..<samples.count {
148
+ channelData[i] = samples[i]
149
+ }
150
+
151
+ engine.outputNode.volume = 1.0
152
+ engine.outputNode.scheduleBuffer(buffer, at: nil, options: .interrupts)
153
+ }
154
+
155
+ // MARK: - Speaker Routing
156
+
157
+ @objc
158
+ func setSpeakerphoneOn(_ on: Bool,
159
+ resolver resolve: @escaping RCTPromiseResolveBlock,
160
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
161
+ do {
162
+ let session = AVAudioSession.sharedInstance()
163
+ if on {
164
+ try session.overrideOutputAudioPort(.speaker)
165
+ } else {
166
+ try session.overrideOutputAudioPort(.none)
167
+ }
168
+ resolve(nil)
169
+ } catch {
170
+ reject("AUDIO_ERROR", "Failed to set speaker: \(error.localizedDescription)", error)
171
+ }
172
+ }
173
+
174
+ // MARK: - Audio Session
175
+
176
+ @objc
177
+ func configureAudioSession(_ resolve: @escaping RCTPromiseResolveBlock,
178
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
179
+ do {
180
+ let session = AVAudioSession.sharedInstance()
181
+ try session.setCategory(.playAndRecord,
182
+ mode: .voiceChat,
183
+ options: [.defaultToSpeaker, .allowBluetooth])
184
+ try session.setActive(true)
185
+ resolve(nil)
186
+ } catch {
187
+ reject("AUDIO_ERROR", "Failed to configure audio session: \(error.localizedDescription)", error)
188
+ }
189
+ }
190
+
191
+ deinit {
192
+ captureRunning = false
193
+ playbackRunning = false
194
+ audioEngine?.inputNode.removeTap(onBus: 0)
195
+ audioEngine?.stop()
196
+ outputEngine?.stop()
197
+ }
198
+ }
@@ -0,0 +1,104 @@
1
+ import { VoiceClientState } from './VoiceClientState';
2
+ import { VoiceConfig } from './VoiceConfig';
3
+ import { VoiceEventListener } from './VoiceEventListener';
4
+ /**
5
+ * Cross-platform voice-chat client for React Native.
6
+ *
7
+ * Connects to the Go voice service over WebSocket, captures microphone audio
8
+ * via a native audio engine, and plays back remote peer audio.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const client = new VoiceClient(createVoiceConfig({
13
+ * server: 'callem.cloudfort.ir',
14
+ * apiKey: 'vc_live_...',
15
+ * peerId: 'user_123',
16
+ * }));
17
+ *
18
+ * client.setListener({
19
+ * onConnected: () => console.log('Connected!'),
20
+ * onPeerSpeaking: (id) => console.log(`${id} is speaking`),
21
+ * });
22
+ *
23
+ * await client.connect();
24
+ * await client.joinRoom('game_room_1');
25
+ * await client.enableMic();
26
+ * ```
27
+ */
28
+ export declare class VoiceClient {
29
+ private readonly config;
30
+ private listener;
31
+ private ws;
32
+ private _state;
33
+ private currentRoom;
34
+ private micEnabled;
35
+ private speakerEnabled;
36
+ private reconnectAttempts;
37
+ private prevRoom;
38
+ private prevMicEnabled;
39
+ private peers;
40
+ private speakingTimer;
41
+ private reconnectTimer;
42
+ constructor(config: VoiceConfig);
43
+ /** Current connection state */
44
+ get state(): VoiceClientState;
45
+ /** Whether connected to server */
46
+ get isConnected(): boolean;
47
+ /** Whether joined a room */
48
+ get isInRoom(): boolean;
49
+ /** Whether microphone is active */
50
+ get isMicEnabled(): boolean;
51
+ /** Whether speaker output is active */
52
+ get isSpeakerEnabled(): boolean;
53
+ /** Current room ID (null if not in room) */
54
+ get currentRoomId(): string | null;
55
+ /** List of peer IDs in the current room */
56
+ get peerIds(): string[];
57
+ /** Set the event listener */
58
+ setListener(listener: VoiceEventListener | null): void;
59
+ /** Connect to the voice server */
60
+ connect(): Promise<void>;
61
+ /** Disconnect from the server */
62
+ disconnect(): void;
63
+ /** Join a voice room */
64
+ joinRoom(roomId: string): Promise<void>;
65
+ /** Leave the current room */
66
+ leaveRoom(): Promise<void>;
67
+ /** Enable microphone (must be in a room) */
68
+ enableMic(): Promise<void>;
69
+ /** Disable microphone */
70
+ disableMic(): void;
71
+ /** Toggle microphone, returns new state */
72
+ toggleMic(): Promise<boolean>;
73
+ /** Enable speaker output */
74
+ enableSpeaker(): void;
75
+ /** Disable speaker output */
76
+ disableSpeaker(): void;
77
+ /** Toggle speaker, returns new state */
78
+ toggleSpeaker(): boolean;
79
+ /** Check if a specific peer is speaking */
80
+ isPeerSpeaking(peerId: string): boolean;
81
+ /** Get peer speaking state with RMS level */
82
+ getPeerSpeakingInfo(peerId: string): {
83
+ speaking: boolean;
84
+ rms: number;
85
+ };
86
+ /** Dispose and release all resources */
87
+ dispose(): void;
88
+ private handleWebSocketConnected;
89
+ private handleTextMessage;
90
+ private handleBinaryMessage;
91
+ private handlePeerAudio;
92
+ private checkSpeaking;
93
+ private handleClose;
94
+ private handleDisconnect;
95
+ private sendText;
96
+ /** Send captured audio to server (called from native audio engine) */
97
+ sendAudio(samples: Int16Array): void;
98
+ private cleanup;
99
+ private stopMic;
100
+ private resolveScheme;
101
+ private setState;
102
+ private raiseError;
103
+ }
104
+ //# sourceMappingURL=VoiceClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VoiceClient.d.ts","sourceRoot":"","sources":["../src/VoiceClient.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA+D1D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAmC;IAEnD,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAmD;IACjE,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,cAAc,CAA8C;gBAExD,MAAM,EAAE,WAAW;IAM/B,+BAA+B;IAC/B,IAAI,KAAK,IAAI,gBAAgB,CAE5B;IAED,kCAAkC;IAClC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,4BAA4B;IAC5B,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,mCAAmC;IACnC,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,uCAAuC;IACvC,IAAI,gBAAgB,IAAI,OAAO,CAE9B;IAED,4CAA4C;IAC5C,IAAI,aAAa,IAAI,MAAM,GAAG,IAAI,CAEjC;IAED,2CAA2C;IAC3C,IAAI,OAAO,IAAI,MAAM,EAAE,CAEtB;IAID,6BAA6B;IAC7B,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI;IAMtD,kCAAkC;IAC5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C9B,iCAAiC;IACjC,UAAU,IAAI,IAAI;IASlB,wBAAwB;IAClB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC7C,6BAA6B;IACvB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IA2BhC,4CAA4C;IACtC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAchC,yBAAyB;IACzB,UAAU,IAAI,IAAI;IAMlB,2CAA2C;IACrC,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAWnC,4BAA4B;IAC5B,aAAa,IAAI,IAAI;IAOrB,6BAA6B;IAC7B,cAAc,IAAI,IAAI;IAOtB,wCAAwC;IACxC,aAAa,IAAI,OAAO;IAWxB,2CAA2C;IAC3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIvC,6CAA6C;IAC7C,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAQvE,wCAAwC;IACxC,OAAO,IAAI,IAAI;IAOf,OAAO,CAAC,wBAAwB;IAsBhC,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,gBAAgB;IA8BxB,OAAO,CAAC,QAAQ;IAMhB,sEAAsE;IACtE,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IASpC,OAAO,CAAC,OAAO;IAgCf,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;CAGnB"}