@edkimmel/expo-audio-stream 0.5.0 → 0.6.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/NATIVE_EVENTS.md +60 -6
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/src/main/java/expo/modules/audiostream/AudioEffectsManager.kt +6 -11
- package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +23 -6
- package/android/src/main/java/expo/modules/audiostream/CommunicationAudioManager.kt +155 -0
- package/android/src/main/java/expo/modules/audiostream/Constants.kt +1 -0
- package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +12 -3
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +12 -9
- package/build/events.d.ts +22 -0
- package/build/events.d.ts.map +1 -1
- package/build/events.js +18 -0
- package/build/events.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +21 -10
- package/build/index.js.map +1 -1
- package/build/types.d.ts +13 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/ExpoPlayAudioStreamModule.swift +29 -12
- package/ios/Microphone.swift +153 -197
- package/ios/MicrophoneDataDelegate.swift +10 -1
- package/ios/SharedAudioEngine.swift +18 -0
- package/package.json +1 -1
- package/src/events.ts +28 -0
- package/src/index.ts +29 -26
- package/src/types.ts +13 -0
package/NATIVE_EVENTS.md
CHANGED
|
@@ -9,8 +9,7 @@ including payload shapes, trigger conditions, and recommended JS responses.
|
|
|
9
9
|
|
|
10
10
|
### `AudioData`
|
|
11
11
|
|
|
12
|
-
Emitted at the configured interval during microphone recording.
|
|
13
|
-
the recording error channel.
|
|
12
|
+
Emitted at the configured interval during microphone recording.
|
|
14
13
|
|
|
15
14
|
| Field | Type | Notes |
|
|
16
15
|
|---|---|---|
|
|
@@ -27,19 +26,21 @@ the recording error channel.
|
|
|
27
26
|
|
|
28
27
|
**Error variant** (same event name, different shape):
|
|
29
28
|
|
|
29
|
+
> Errors are also emitted on the richer `MicrophoneError` event (see below).
|
|
30
|
+
> This variant is kept for backward compatibility.
|
|
31
|
+
|
|
30
32
|
| Field | Type | Notes |
|
|
31
33
|
|---|---|---|
|
|
32
|
-
| `error` | `string` | Error code
|
|
34
|
+
| `error` | `string` | Error code — see `MicrophoneError` section for full table |
|
|
33
35
|
| `errorMessage` | `string` | Human-readable description |
|
|
34
|
-
| `streamUuid` | `string` | Stream
|
|
36
|
+
| `streamUuid` | `string` | Stream ID (Android only; `""` on iOS) |
|
|
35
37
|
|
|
36
38
|
**Platform:** Android, iOS
|
|
37
39
|
|
|
38
40
|
**JS response:**
|
|
39
41
|
- Forward the base64 PCM to your STT pipeline or WebSocket.
|
|
40
42
|
- Use `soundLevel` for VAD or UI visualisation.
|
|
41
|
-
-
|
|
42
|
-
On error, stop the conversation turn or retry.
|
|
43
|
+
- Prefer subscribing via `addMicrophoneErrorListener` — it receives the structured `MicrophoneError` event with `isFatal` and `autoResuming` fields.
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -62,6 +63,59 @@ wired headset/headphones, USB headset).
|
|
|
62
63
|
|
|
63
64
|
---
|
|
64
65
|
|
|
66
|
+
### `MicrophoneError`
|
|
67
|
+
|
|
68
|
+
Fired on every microphone error alongside the backward-compatible `AudioData`
|
|
69
|
+
error variant. Carries structured `isFatal` and `autoResuming` fields decided
|
|
70
|
+
by native code.
|
|
71
|
+
|
|
72
|
+
**OTA safe:** apps can subscribe to this event before the native binary
|
|
73
|
+
includes it — old native simply never emits it, so the listener is never
|
|
74
|
+
called. Apps using the `AudioData` error variant continue to work unchanged.
|
|
75
|
+
|
|
76
|
+
| Field | Type | Notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `code` | `string` | Error code (see table below) |
|
|
79
|
+
| `message` | `string` | Human-readable description including any underlying system error |
|
|
80
|
+
| `isFatal` | `boolean` | `true` = recording has stopped; caller must call `stopMicrophone()` and reconnect |
|
|
81
|
+
| `autoResuming` | `boolean` | `true` = library will reinstall the tap automatically (`INTERRUPTED` only) |
|
|
82
|
+
|
|
83
|
+
**Error code reference:**
|
|
84
|
+
|
|
85
|
+
| Code | Platform | `isFatal` | `autoResuming` | Meaning | Recommended action |
|
|
86
|
+
|------|----------|-----------|----------------|---------|-------------------|
|
|
87
|
+
| `INTERRUPTED` | iOS | false | true | System interrupted the audio session | Show paused UI; do NOT stop — library auto-resumes |
|
|
88
|
+
| `RESUME_FAILED` | iOS | true | false | System allowed resume but engine restart failed | `stopMicrophone()` then reconnect |
|
|
89
|
+
| `RESTART_FAILED` | iOS | true | false | Route-change or engine-rebuild recovery failed | `stopMicrophone()` then reconnect |
|
|
90
|
+
| `ENGINE_DIED` | iOS | true | false | SharedAudioEngine exhausted all recovery | `stopMicrophone()` + `disconnectPipeline()` then reconnect both |
|
|
91
|
+
| `READ_ERROR` | iOS | false | false | Single empty buffer — transient, recording continues | Log for diagnostics; no action |
|
|
92
|
+
| `READ_ERROR` | Android | true | false | `AudioRecord.read()` failed 10× consecutively | `stopMicrophone()` then reconnect |
|
|
93
|
+
| `RECORDING_CRASH` | Android | true | false | Recording thread threw an unexpected exception | `stopMicrophone()` then reconnect |
|
|
94
|
+
|
|
95
|
+
**Platform:** Android, iOS
|
|
96
|
+
|
|
97
|
+
**JS response:**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { addMicrophoneErrorListener } from "@edkimmel/expo-audio-stream"
|
|
101
|
+
|
|
102
|
+
const sub = addMicrophoneErrorListener((error) => {
|
|
103
|
+
if (error.isFatal) {
|
|
104
|
+
stopMicrophone()
|
|
105
|
+
if (error.code === "ENGINE_DIED") disconnectPipeline()
|
|
106
|
+
reconnect()
|
|
107
|
+
} else if (error.autoResuming) {
|
|
108
|
+
showPausedUI() // INTERRUPTED — library will reinstall tap automatically
|
|
109
|
+
}
|
|
110
|
+
// isFatal: false, autoResuming: false → READ_ERROR on iOS: log and ignore
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// cleanup
|
|
114
|
+
sub.remove()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
65
119
|
## Legacy Playback Events (AudioPlaybackManager)
|
|
66
120
|
|
|
67
121
|
### `SoundStarted`
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -9,18 +9,13 @@ import android.util.Log
|
|
|
9
9
|
/**
|
|
10
10
|
* Manages hardware audio effects for voice recording.
|
|
11
11
|
*
|
|
12
|
-
* We use
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* We use VOICE_COMMUNICATION as our audio source. This source enables
|
|
13
|
+
* platform-managed AEC at the HAL level automatically. The explicit
|
|
14
|
+
* AcousticEchoCanceler effect here is belt-and-suspenders for devices
|
|
15
|
+
* where the platform does not apply it automatically.
|
|
16
16
|
*
|
|
17
|
-
* NS and AGC
|
|
18
|
-
*
|
|
19
|
-
* and can cause low-volume capture on many OEMs.
|
|
20
|
-
*
|
|
21
|
-
* AEC is the one effect the CDD permits for VOICE_RECOGNITION ("expects a
|
|
22
|
-
* stream that has an echo cancellation effect if available"), so it is
|
|
23
|
-
* enabled by default.
|
|
17
|
+
* NS and AGC remain opt-in — VOICE_COMMUNICATION applies its own
|
|
18
|
+
* processing and additional effects can cause over-processing on some OEMs.
|
|
24
19
|
*/
|
|
25
20
|
class AudioEffectsManager(
|
|
26
21
|
/** Enable hardware noise suppressor. Default false — CDD 5.4 [C-1-2] prohibits it for VOICE_RECOGNITION. */
|
|
@@ -339,23 +339,29 @@ class AudioRecorderManager(
|
|
|
339
339
|
consecutiveErrors++
|
|
340
340
|
if (consecutiveErrors >= 10) {
|
|
341
341
|
Log.e(Constants.TAG, "Too many consecutive read errors ($consecutiveErrors), stopping")
|
|
342
|
-
emitRecordingError("READ_ERROR", "AudioRecord read failed after $consecutiveErrors consecutive errors")
|
|
342
|
+
emitRecordingError("READ_ERROR", "AudioRecord read failed after $consecutiveErrors consecutive errors", isFatal = true)
|
|
343
343
|
break
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
} catch (e: Exception) {
|
|
348
348
|
Log.e(Constants.TAG, "Recording thread crashed", e)
|
|
349
|
-
emitRecordingError("RECORDING_CRASH", e.message ?: "Recording thread unexpected error")
|
|
349
|
+
emitRecordingError("RECORDING_CRASH", e.message ?: "Recording thread unexpected error", isFatal = true)
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
354
|
* Sends a recording error event to JS so the caller can react.
|
|
355
355
|
*/
|
|
356
|
-
private fun emitRecordingError(
|
|
356
|
+
private fun emitRecordingError(
|
|
357
|
+
code: String,
|
|
358
|
+
message: String,
|
|
359
|
+
isFatal: Boolean,
|
|
360
|
+
autoResuming: Boolean = false
|
|
361
|
+
) {
|
|
357
362
|
mainHandler.post {
|
|
358
363
|
try {
|
|
364
|
+
// Backward-compat: keep the error variant on AudioData for existing consumers
|
|
359
365
|
eventSender.sendExpoEvent(
|
|
360
366
|
Constants.AUDIO_EVENT_NAME, bundleOf(
|
|
361
367
|
"error" to code,
|
|
@@ -363,6 +369,15 @@ class AudioRecorderManager(
|
|
|
363
369
|
"streamUuid" to streamUuid
|
|
364
370
|
)
|
|
365
371
|
)
|
|
372
|
+
// Rich structured channel for new consumers
|
|
373
|
+
eventSender.sendExpoEvent(
|
|
374
|
+
Constants.MICROPHONE_ERROR_EVENT_NAME, bundleOf(
|
|
375
|
+
"code" to code,
|
|
376
|
+
"message" to message,
|
|
377
|
+
"isFatal" to isFatal,
|
|
378
|
+
"autoResuming" to autoResuming
|
|
379
|
+
)
|
|
380
|
+
)
|
|
366
381
|
} catch (e: Exception) {
|
|
367
382
|
Log.e(Constants.TAG, "Failed to send error event", e)
|
|
368
383
|
}
|
|
@@ -479,9 +494,11 @@ class AudioRecorderManager(
|
|
|
479
494
|
return null
|
|
480
495
|
}
|
|
481
496
|
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
497
|
+
// VOICE_COMMUNICATION enables platform-managed AEC (echo cancellation happens at
|
|
498
|
+
// the HAL level, not just via the explicit AcousticEchoCanceler effect).
|
|
499
|
+
// VOICE_RECOGNITION intentionally bypasses platform AEC per the Android CDD,
|
|
500
|
+
// making hardware echo cancellation ineffective regardless of AudioEffectsManager.
|
|
501
|
+
val audioSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION
|
|
485
502
|
|
|
486
503
|
val record = AudioRecord(
|
|
487
504
|
audioSource,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.media.AudioDeviceInfo
|
|
5
|
+
import android.media.AudioManager
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Owns Android communication-mode lifecycle and output device routing for
|
|
12
|
+
* hands-free voice sessions (microphone + speaker playback with AEC).
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities:
|
|
15
|
+
* - Set MODE_IN_COMMUNICATION before AudioRecord starts so the HAL echo
|
|
16
|
+
* reference path is active when AcousticEchoCanceler initializes.
|
|
17
|
+
* - Route output to the best available device: Bluetooth HFP > wired headset
|
|
18
|
+
* > built-in speaker. Re-routes automatically when devices connect or
|
|
19
|
+
* disconnect.
|
|
20
|
+
* - Bind hardware volume buttons to STREAM_VOICE_CALL while a session is
|
|
21
|
+
* active so the user's volume buttons control playback volume.
|
|
22
|
+
* - Reset everything when all callers have stopped so the phone's audio
|
|
23
|
+
* state is clean.
|
|
24
|
+
*
|
|
25
|
+
* Reference-counted: mic and pipeline each call startSession/stopSession
|
|
26
|
+
* independently. Communication mode stays active until both have stopped.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* cam.startSession(activity) // call before AudioRecord.startRecording() or pipeline connect
|
|
30
|
+
* cam.onDeviceChanged() // call from AudioDeviceCallback
|
|
31
|
+
* cam.stopSession(activity) // call after AudioRecord.stop() or pipeline disconnect
|
|
32
|
+
*/
|
|
33
|
+
class CommunicationAudioManager(private val audioManager: AudioManager) {
|
|
34
|
+
|
|
35
|
+
private val refCount = AtomicInteger(0)
|
|
36
|
+
|
|
37
|
+
private val sessionActive get() = refCount.get() > 0
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Start (or join) a voice session. Safe to call from multiple owners —
|
|
41
|
+
* communication mode is activated on the first call.
|
|
42
|
+
* Pass the current Activity so volume buttons bind to STREAM_VOICE_CALL.
|
|
43
|
+
*/
|
|
44
|
+
fun startSession(activity: Activity? = null) {
|
|
45
|
+
if (refCount.getAndIncrement() == 0) {
|
|
46
|
+
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
47
|
+
activity?.volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
|
48
|
+
Log.d(TAG, "Session started — MODE_IN_COMMUNICATION")
|
|
49
|
+
}
|
|
50
|
+
applyBestRoute()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Release this owner's hold on the session. Communication mode is reset
|
|
55
|
+
* only when all owners have called stopSession.
|
|
56
|
+
* No-op if the session was already ended via forceReset.
|
|
57
|
+
*/
|
|
58
|
+
fun stopSession(activity: Activity? = null) {
|
|
59
|
+
// Decrement only if currently positive — prevents post-forceReset calls
|
|
60
|
+
// from re-triggering cleanup (getAndUpdate returns the previous value).
|
|
61
|
+
val prev = refCount.getAndUpdate { if (it > 0) it - 1 else 0 }
|
|
62
|
+
if (prev <= 0) {
|
|
63
|
+
Log.d(TAG, "stopSession — already stopped, no-op")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
val remaining = prev - 1
|
|
67
|
+
if (remaining == 0) {
|
|
68
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
69
|
+
audioManager.clearCommunicationDevice()
|
|
70
|
+
} else {
|
|
71
|
+
@Suppress("DEPRECATION")
|
|
72
|
+
audioManager.stopBluetoothSco()
|
|
73
|
+
@Suppress("DEPRECATION")
|
|
74
|
+
audioManager.isSpeakerphoneOn = false
|
|
75
|
+
}
|
|
76
|
+
audioManager.mode = AudioManager.MODE_NORMAL
|
|
77
|
+
activity?.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
78
|
+
Log.d(TAG, "Session ended — audio mode reset to NORMAL")
|
|
79
|
+
} else {
|
|
80
|
+
Log.d(TAG, "stopSession — $remaining owner(s) still active")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Re-evaluate and apply the best available output route.
|
|
86
|
+
* Call this from AudioDeviceCallback whenever devices are added or removed.
|
|
87
|
+
*/
|
|
88
|
+
fun onDeviceChanged() {
|
|
89
|
+
applyBestRoute()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unconditionally reset all communication audio state regardless of ref count.
|
|
94
|
+
* Use in OnDestroy and explicit destroy flows where normal lifecycle won't run.
|
|
95
|
+
*/
|
|
96
|
+
fun forceReset(activity: Activity? = null) {
|
|
97
|
+
refCount.set(0)
|
|
98
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
99
|
+
audioManager.clearCommunicationDevice()
|
|
100
|
+
} else {
|
|
101
|
+
@Suppress("DEPRECATION")
|
|
102
|
+
audioManager.stopBluetoothSco()
|
|
103
|
+
@Suppress("DEPRECATION")
|
|
104
|
+
audioManager.isSpeakerphoneOn = false
|
|
105
|
+
}
|
|
106
|
+
audioManager.mode = AudioManager.MODE_NORMAL
|
|
107
|
+
activity?.volumeControlStream = AudioManager.USE_DEFAULT_STREAM_TYPE
|
|
108
|
+
Log.d(TAG, "forceReset — audio mode reset to NORMAL")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
private fun applyBestRoute() {
|
|
114
|
+
if (!sessionActive) return
|
|
115
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
116
|
+
applyBestRouteApi31()
|
|
117
|
+
} else {
|
|
118
|
+
applyBestRouteLegacy()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun applyBestRouteApi31() {
|
|
123
|
+
val devices = audioManager.availableCommunicationDevices
|
|
124
|
+
// Priority: BT HFP > wired headset > built-in speaker
|
|
125
|
+
val preferred = devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
|
126
|
+
?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
|
|
127
|
+
?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
|
|
128
|
+
if (preferred != null) {
|
|
129
|
+
audioManager.setCommunicationDevice(preferred)
|
|
130
|
+
Log.d(TAG, "Route → ${preferred.productName} (type=${preferred.type})")
|
|
131
|
+
} else {
|
|
132
|
+
Log.w(TAG, "No suitable communication device found")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Suppress("DEPRECATION")
|
|
137
|
+
private fun applyBestRouteLegacy() {
|
|
138
|
+
// Detect a connected BT HFP device via the full device list (API 23+).
|
|
139
|
+
val allDevices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL)
|
|
140
|
+
val btSco = allDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
|
141
|
+
if (btSco != null) {
|
|
142
|
+
audioManager.startBluetoothSco()
|
|
143
|
+
audioManager.isSpeakerphoneOn = false
|
|
144
|
+
Log.d(TAG, "Route → Bluetooth SCO (${btSco.productName})")
|
|
145
|
+
} else {
|
|
146
|
+
audioManager.stopBluetoothSco()
|
|
147
|
+
audioManager.isSpeakerphoneOn = true
|
|
148
|
+
Log.d(TAG, "Route → built-in speaker")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
companion object {
|
|
153
|
+
private const val TAG = "CommunicationAudioMgr"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -2,6 +2,7 @@ package expo.modules.audiostream
|
|
|
2
2
|
|
|
3
3
|
object Constants {
|
|
4
4
|
const val AUDIO_EVENT_NAME = "AudioData"
|
|
5
|
+
const val MICROPHONE_ERROR_EVENT_NAME = "MicrophoneError"
|
|
5
6
|
const val AUDIO_ANALYSIS_EVENT_NAME = "AudioAnalysis"
|
|
6
7
|
const val DEVICE_RECONNECTED_EVENT_NAME = "DeviceReconnected"
|
|
7
8
|
const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
|
|
@@ -26,6 +26,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
26
26
|
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
27
27
|
private lateinit var audioManager: AudioManager
|
|
28
28
|
private lateinit var pipelineIntegration: PipelineIntegration
|
|
29
|
+
private lateinit var communicationAudioManager: CommunicationAudioManager
|
|
29
30
|
|
|
30
31
|
// Ensure callbacks are delivered on the main thread
|
|
31
32
|
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
|
@@ -62,6 +63,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
62
63
|
if (firstOfGroup?.isNotEmpty()==true) {
|
|
63
64
|
val matched = firstOfGroup.map { "${it.productName} (type=${it.type})" }
|
|
64
65
|
Log.d("ExpoAudioCallback", "AudioDeviceCallback ➜ ADDED (interesting): $matched")
|
|
66
|
+
communicationAudioManager.onDeviceChanged()
|
|
65
67
|
pipelineIntegration.logAudioTrackHealth("device_added")
|
|
66
68
|
val params = Bundle()
|
|
67
69
|
params.putString("reason", "newDeviceAvailable")
|
|
@@ -79,6 +81,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
79
81
|
if (lastOfGroup?.isNotEmpty() == true) {
|
|
80
82
|
val matched = lastOfGroup.map { "${it.productName} (type=${it.type})" }
|
|
81
83
|
Log.d("ExpoAudioCallback", "AudioDeviceCallback ➜ REMOVED (interesting): $matched")
|
|
84
|
+
communicationAudioManager.onDeviceChanged()
|
|
82
85
|
pipelineIntegration.logAudioTrackHealth("device_removed")
|
|
83
86
|
val params = Bundle()
|
|
84
87
|
params.putString("reason", "oldDeviceUnavailable")
|
|
@@ -94,6 +97,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
94
97
|
|
|
95
98
|
Events(
|
|
96
99
|
Constants.AUDIO_EVENT_NAME,
|
|
100
|
+
Constants.MICROPHONE_ERROR_EVENT_NAME,
|
|
97
101
|
Constants.DEVICE_RECONNECTED_EVENT_NAME,
|
|
98
102
|
PipelineIntegration.EVENT_STATE_CHANGED,
|
|
99
103
|
PipelineIntegration.EVENT_PLAYBACK_STARTED,
|
|
@@ -113,26 +117,27 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
113
117
|
|
|
114
118
|
OnCreate {
|
|
115
119
|
audioManager = appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
120
|
+
communicationAudioManager = CommunicationAudioManager(audioManager)
|
|
116
121
|
audioManager.registerAudioDeviceCallback(audioCallCallback, mainHandler)
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
OnDestroy {
|
|
120
125
|
reportedGroups.clear()
|
|
121
126
|
audioManager.unregisterAudioDeviceCallback(audioCallCallback)
|
|
122
|
-
// Module is being destroyed (app shutdown)
|
|
123
|
-
// Just clean up resources without reinitialization
|
|
124
127
|
pipelineIntegration.destroy()
|
|
125
128
|
audioRecorderManager.release()
|
|
129
|
+
communicationAudioManager.forceReset(appContext.currentActivity)
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
AsyncFunction("destroy") { promise: Promise ->
|
|
129
|
-
// User explicitly called destroy - clean up and reinitialize for reuse
|
|
130
133
|
pipelineIntegration.destroy()
|
|
131
134
|
audioRecorderManager.release()
|
|
135
|
+
communicationAudioManager.forceReset(appContext.currentActivity)
|
|
132
136
|
|
|
133
137
|
// Reinitialize all managers so the module can be used again
|
|
134
138
|
initializeManager()
|
|
135
139
|
initializePipeline()
|
|
140
|
+
communicationAudioManager = CommunicationAudioManager(audioManager)
|
|
136
141
|
promise.resolve(null)
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -153,11 +158,13 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
AsyncFunction("startMicrophone") { options: Map<String, Any?>, promise: Promise ->
|
|
161
|
+
communicationAudioManager.startSession(appContext.currentActivity)
|
|
156
162
|
audioRecorderManager.startRecording(options, promise)
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
AsyncFunction("stopMicrophone") { promise: Promise ->
|
|
160
166
|
audioRecorderManager.stopRecording(promise)
|
|
167
|
+
communicationAudioManager.stopSession(appContext.currentActivity)
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
Function("toggleSilence") { isSilent: Boolean ->
|
|
@@ -168,6 +175,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
168
175
|
// ── Native Audio Pipeline V3 ────────────────────────────────────
|
|
169
176
|
|
|
170
177
|
AsyncFunction("connectPipeline") { options: Map<String, Any?>, promise: Promise ->
|
|
178
|
+
communicationAudioManager.startSession(appContext.currentActivity)
|
|
171
179
|
pipelineIntegration.connect(options, promise)
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -181,6 +189,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
|
181
189
|
|
|
182
190
|
AsyncFunction("disconnectPipeline") { promise: Promise ->
|
|
183
191
|
pipelineIntegration.disconnect(promise)
|
|
192
|
+
communicationAudioManager.stopSession(appContext.currentActivity)
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
AsyncFunction("invalidatePipelineTurn") { options: Map<String, Any?>, promise: Promise ->
|
|
@@ -90,8 +90,9 @@ enum class AudioMode {
|
|
|
90
90
|
* **MAX_PRIORITY write thread** that loops `buffer.read() → track.write(BLOCKING)`.
|
|
91
91
|
*
|
|
92
92
|
* Key design points:
|
|
93
|
-
* - AudioTrack uses **
|
|
94
|
-
*
|
|
93
|
+
* - AudioTrack uses **USAGE_VOICE_COMMUNICATION + CONTENT_TYPE_SPEECH** so its
|
|
94
|
+
* output feeds the hardware AEC echo reference path. Earpiece routing is
|
|
95
|
+
* prevented by CommunicationAudioManager (setCommunicationDevice/isSpeakerphoneOn).
|
|
95
96
|
* - AudioTrack stays alive for the entire session, writing silence when idle.
|
|
96
97
|
* This avoids 50–100 ms restart latency.
|
|
97
98
|
* - Config is **immutable per session** — tear down and rebuild to change
|
|
@@ -129,7 +130,9 @@ class AudioPipeline(
|
|
|
129
130
|
/** If playback head hasn't moved for this long, declare zombie. */
|
|
130
131
|
private const val ZOMBIE_STALL_THRESHOLD_MS = 5000L
|
|
131
132
|
|
|
132
|
-
/** Minimum volume level (0–15) enforced by VolumeGuard on
|
|
133
|
+
/** Minimum volume level (0–15) enforced by VolumeGuard on STREAM_VOICE_CALL.
|
|
134
|
+
* USAGE_VOICE_COMMUNICATION is required so the output feeds the hardware AEC
|
|
135
|
+
* echo reference path — USAGE_MEDIA does not. */
|
|
133
136
|
private const val MIN_VOLUME_LEVEL = 1
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -263,7 +266,7 @@ class AudioPipeline(
|
|
|
263
266
|
|
|
264
267
|
// ── 2. AudioTrack ───────────────────────────────────────────
|
|
265
268
|
val audioAttributes = AudioAttributes.Builder()
|
|
266
|
-
.setUsage(AudioAttributes.
|
|
269
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
267
270
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
268
271
|
.build()
|
|
269
272
|
|
|
@@ -637,7 +640,7 @@ class AudioPipeline(
|
|
|
637
640
|
AudioMode.DUCK_OTHERS -> {
|
|
638
641
|
val result = audioManager.requestAudioFocus(
|
|
639
642
|
focusChangeListener,
|
|
640
|
-
AudioManager.
|
|
643
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
641
644
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
|
642
645
|
)
|
|
643
646
|
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
@@ -648,7 +651,7 @@ class AudioPipeline(
|
|
|
648
651
|
AudioMode.DO_NOT_MIX -> {
|
|
649
652
|
val result = audioManager.requestAudioFocus(
|
|
650
653
|
focusChangeListener,
|
|
651
|
-
AudioManager.
|
|
654
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
652
655
|
AudioManager.AUDIOFOCUS_GAIN
|
|
653
656
|
)
|
|
654
657
|
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
@@ -743,12 +746,12 @@ class AudioPipeline(
|
|
|
743
746
|
private fun installVolumeGuard() {
|
|
744
747
|
volumeObserver = object : ContentObserver(mainHandler) {
|
|
745
748
|
override fun onChange(selfChange: Boolean) {
|
|
746
|
-
val current = audioManager.getStreamVolume(AudioManager.
|
|
749
|
+
val current = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL)
|
|
747
750
|
if (current < MIN_VOLUME_LEVEL) {
|
|
748
|
-
Log.d(TAG, "VolumeGuard: raising
|
|
751
|
+
Log.d(TAG, "VolumeGuard: raising STREAM_VOICE_CALL from $current to $MIN_VOLUME_LEVEL")
|
|
749
752
|
try {
|
|
750
753
|
audioManager.setStreamVolume(
|
|
751
|
-
AudioManager.
|
|
754
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
752
755
|
MIN_VOLUME_LEVEL,
|
|
753
756
|
0 // no flags — silent raise
|
|
754
757
|
)
|
package/build/events.d.ts
CHANGED
|
@@ -32,8 +32,30 @@ export type DeviceReconnectedEventPayload = {
|
|
|
32
32
|
};
|
|
33
33
|
export declare const AudioEvents: {
|
|
34
34
|
AudioData: string;
|
|
35
|
+
MicrophoneError: string;
|
|
35
36
|
DeviceReconnected: string;
|
|
36
37
|
};
|
|
37
38
|
export declare function addAudioEventListener(listener: (event: AudioEventPayload) => Promise<void>): EventSubscription;
|
|
39
|
+
export interface MicrophoneErrorEventPayload {
|
|
40
|
+
code: string;
|
|
41
|
+
message: string;
|
|
42
|
+
isFatal: boolean;
|
|
43
|
+
autoResuming: boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Subscribe to the dedicated MicrophoneError native event.
|
|
47
|
+
*
|
|
48
|
+
* OTA safe: if the running native binary predates this feature, the event is
|
|
49
|
+
* never emitted and this listener is never called. Apps that also subscribe to
|
|
50
|
+
* AudioData errors via addAudioEventListener continue to work unchanged.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const sub = addMicrophoneErrorListener((e) => {
|
|
54
|
+
* if (e.isFatal) { stopMicrophone(); reconnect(); }
|
|
55
|
+
* else if (e.autoResuming) { showPausedUI(); }
|
|
56
|
+
* })
|
|
57
|
+
* // cleanup: sub.remove()
|
|
58
|
+
*/
|
|
59
|
+
export declare function addMicrophoneErrorListener(listener: (event: MicrophoneErrorEventPayload) => void): EventSubscription;
|
|
38
60
|
export declare function subscribeToEvent<T extends unknown>(eventName: string, listener: (event: T | undefined) => Promise<void>): EventSubscription;
|
|
39
61
|
//# sourceMappingURL=events.d.ts.map
|
package/build/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAM7C,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D;kFAC8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,wBAAwB;;;;CAI3B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GACjC,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAE3E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAM7C,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D;kFAC8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,wBAAwB;;;;CAI3B,CAAC;AAEX,MAAM,MAAM,uBAAuB,GACjC,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAE3E,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAIvB,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GACpD,iBAAiB,CAEnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,GACrD,iBAAiB,CAEnB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,EAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAChD,iBAAiB,CAEnB"}
|
package/build/events.js
CHANGED
|
@@ -9,11 +9,29 @@ export const DeviceReconnectedReasons = {
|
|
|
9
9
|
};
|
|
10
10
|
export const AudioEvents = {
|
|
11
11
|
AudioData: "AudioData",
|
|
12
|
+
MicrophoneError: "MicrophoneError",
|
|
12
13
|
DeviceReconnected: "DeviceReconnected",
|
|
13
14
|
};
|
|
14
15
|
export function addAudioEventListener(listener) {
|
|
15
16
|
return emitter.addListener("AudioData", listener);
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to the dedicated MicrophoneError native event.
|
|
20
|
+
*
|
|
21
|
+
* OTA safe: if the running native binary predates this feature, the event is
|
|
22
|
+
* never emitted and this listener is never called. Apps that also subscribe to
|
|
23
|
+
* AudioData errors via addAudioEventListener continue to work unchanged.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* const sub = addMicrophoneErrorListener((e) => {
|
|
27
|
+
* if (e.isFatal) { stopMicrophone(); reconnect(); }
|
|
28
|
+
* else if (e.autoResuming) { showPausedUI(); }
|
|
29
|
+
* })
|
|
30
|
+
* // cleanup: sub.remove()
|
|
31
|
+
*/
|
|
32
|
+
export function addMicrophoneErrorListener(listener) {
|
|
33
|
+
return emitter.addListener("MicrophoneError", listener);
|
|
34
|
+
}
|
|
17
35
|
export function subscribeToEvent(eventName, listener) {
|
|
18
36
|
return emitter.addListener(eventName, listener);
|
|
19
37
|
}
|
package/build/events.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AAKzE,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAEpE,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,yBAAyB,CAAC,CAAC;AAoB5D,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,kBAAkB,EAAE,oBAAoB;IACxC,oBAAoB,EAAE,sBAAsB;IAC5C,OAAO,EAAE,SAAS;CACV,CAAC;AASX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,SAAS,EAAE,WAAW;IACtB,iBAAiB,EAAE,mBAAmB;CACvC,CAAC;AAEF,MAAM,UAAU,qBAAqB,CACnC,QAAqD;IAErD,OAAQ,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,QAAiD;IAEjD,OAAQ,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// packages/expo-audio-stream/src/events.ts\n\nimport { EventEmitter, type EventSubscription } from \"expo-modules-core\";\n\n// Type alias for backwards compatibility\nexport type Subscription = EventSubscription;\n\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\nconst emitter = new EventEmitter(ExpoPlayAudioStreamModule);\n\nexport interface AudioEventPayload {\n encoded?: string;\n buffer?: Float32Array;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n soundLevel?: number;\n frequencyBands?: { low: number; mid: number; high: number };\n /** Set by native when a mid-recording error occurs (interruption, read failure).\n * When present, `encoded` is absent and the recording is no longer active. */\n error?: string;\n errorMessage?: string;\n}\n\nexport const DeviceReconnectedReasons = {\n newDeviceAvailable: \"newDeviceAvailable\",\n oldDeviceUnavailable: \"oldDeviceUnavailable\",\n unknown: \"unknown\",\n} as const;\n\nexport type DeviceReconnectedReason =\n (typeof DeviceReconnectedReasons)[keyof typeof DeviceReconnectedReasons];\n\nexport type DeviceReconnectedEventPayload = {\n reason: DeviceReconnectedReason;\n};\n\nexport const AudioEvents = {\n AudioData: \"AudioData\",\n DeviceReconnected: \"DeviceReconnected\",\n};\n\nexport function addAudioEventListener(\n listener: (event: AudioEventPayload) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(\"AudioData\", listener);\n}\n\nexport function subscribeToEvent<T extends unknown>(\n eventName: string,\n listener: (event: T | undefined) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(eventName, listener);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AAKzE,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAEpE,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,yBAAyB,CAAC,CAAC;AAoB5D,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,kBAAkB,EAAE,oBAAoB;IACxC,oBAAoB,EAAE,sBAAsB;IAC5C,OAAO,EAAE,SAAS;CACV,CAAC;AASX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,SAAS,EAAE,WAAW;IACtB,eAAe,EAAE,iBAAiB;IAClC,iBAAiB,EAAE,mBAAmB;CACvC,CAAC;AAEF,MAAM,UAAU,qBAAqB,CACnC,QAAqD;IAErD,OAAQ,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAsD;IAEtD,OAAQ,OAAe,CAAC,WAAW,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,QAAiD;IAEjD,OAAQ,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// packages/expo-audio-stream/src/events.ts\n\nimport { EventEmitter, type EventSubscription } from \"expo-modules-core\";\n\n// Type alias for backwards compatibility\nexport type Subscription = EventSubscription;\n\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\nconst emitter = new EventEmitter(ExpoPlayAudioStreamModule);\n\nexport interface AudioEventPayload {\n encoded?: string;\n buffer?: Float32Array;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n soundLevel?: number;\n frequencyBands?: { low: number; mid: number; high: number };\n /** Set by native when a mid-recording error occurs (interruption, read failure).\n * When present, `encoded` is absent and the recording is no longer active. */\n error?: string;\n errorMessage?: string;\n}\n\nexport const DeviceReconnectedReasons = {\n newDeviceAvailable: \"newDeviceAvailable\",\n oldDeviceUnavailable: \"oldDeviceUnavailable\",\n unknown: \"unknown\",\n} as const;\n\nexport type DeviceReconnectedReason =\n (typeof DeviceReconnectedReasons)[keyof typeof DeviceReconnectedReasons];\n\nexport type DeviceReconnectedEventPayload = {\n reason: DeviceReconnectedReason;\n};\n\nexport const AudioEvents = {\n AudioData: \"AudioData\",\n MicrophoneError: \"MicrophoneError\",\n DeviceReconnected: \"DeviceReconnected\",\n};\n\nexport function addAudioEventListener(\n listener: (event: AudioEventPayload) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(\"AudioData\", listener);\n}\n\nexport interface MicrophoneErrorEventPayload {\n code: string;\n message: string;\n isFatal: boolean;\n autoResuming: boolean;\n}\n\n/**\n * Subscribe to the dedicated MicrophoneError native event.\n *\n * OTA safe: if the running native binary predates this feature, the event is\n * never emitted and this listener is never called. Apps that also subscribe to\n * AudioData errors via addAudioEventListener continue to work unchanged.\n *\n * @example\n * const sub = addMicrophoneErrorListener((e) => {\n * if (e.isFatal) { stopMicrophone(); reconnect(); }\n * else if (e.autoResuming) { showPausedUI(); }\n * })\n * // cleanup: sub.remove()\n */\nexport function addMicrophoneErrorListener(\n listener: (event: MicrophoneErrorEventPayload) => void\n): EventSubscription {\n return (emitter as any).addListener(\"MicrophoneError\", listener);\n}\n\nexport function subscribeToEvent<T extends unknown>(\n eventName: string,\n listener: (event: T | undefined) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(eventName, listener);\n}\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export declare class ExpoPlayAudioStream {
|
|
|
72
72
|
export { AudioDataEvent, DeviceReconnectedReason, DeviceReconnectedEventPayload, AudioRecording, RecordingConfig, StartRecordingResult, AudioEvents, PlaybackMode, Encoding, EncodingTypes, FrequencyBands, PlaybackModes, IAudioBufferConfig, IAudioPlayPayload, IAudioFrame, BufferHealthState, IBufferHealthMetrics, IAudioBufferManager, IFrameProcessor, IQualityMonitor, BufferedStreamConfig, SmartBufferConfig, SmartBufferMode, NetworkConditions, };
|
|
73
73
|
export type { EventSubscription } from "expo-modules-core";
|
|
74
74
|
export type { Subscription } from "./events";
|
|
75
|
+
export { addMicrophoneErrorListener } from "./events";
|
|
76
|
+
export type { MicrophoneErrorEventPayload } from "./events";
|
|
75
77
|
export { Pipeline } from "./pipeline";
|
|
76
78
|
export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelinePlaybackStoppedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from "./pipeline";
|
|
77
79
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,KAAK,YAAY,GAAG,iBAAiB,CAAC;AACtC,OAAO,EACL,cAAc,EACd,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,cAAc,EACd,aAAa,EAEb,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|