@edkimmel/expo-audio-stream 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NATIVE_EVENTS.md +97 -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 +17 -3
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +104 -11
- package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +14 -0
- package/build/events.d.ts +26 -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 +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +19 -7
- package/build/index.js.map +1 -1
- package/build/pipeline/index.d.ts +14 -1
- package/build/pipeline/index.d.ts.map +1 -1
- package/build/pipeline/index.js +15 -0
- package/build/pipeline/index.js.map +1 -1
- package/build/pipeline/types.d.ts +14 -0
- package/build/pipeline/types.d.ts.map +1 -1
- package/build/pipeline/types.js.map +1 -1
- package/build/types.d.ts +21 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/ios/AudioPipeline.swift +67 -2
- package/ios/ExpoPlayAudioStreamModule.swift +43 -12
- package/ios/Microphone.swift +167 -120
- package/ios/MicrophoneDataDelegate.swift +10 -1
- package/ios/PipelineIntegration.swift +11 -1
- package/ios/SharedAudioEngine.swift +18 -0
- package/package.json +1 -2
- package/plugin/build/index.js +5 -0
- package/plugin/src/index.ts +5 -0
- package/src/events.ts +32 -0
- package/src/index.ts +27 -18
- package/src/pipeline/index.ts +17 -0
- package/src/pipeline/types.ts +15 -0
- package/src/types.ts +22 -0
|
@@ -49,6 +49,7 @@ interface PipelineListener {
|
|
|
49
49
|
fun onZombieDetected(playbackHead: Long, stalledMs: Long)
|
|
50
50
|
fun onUnderrun(count: Int)
|
|
51
51
|
fun onDrained(turnId: String)
|
|
52
|
+
fun onPlaybackStopped(turnId: String)
|
|
52
53
|
fun onAudioFocusLost()
|
|
53
54
|
fun onAudioFocusResumed()
|
|
54
55
|
fun onFrequencyBands(low: Float, mid: Float, high: Float)
|
|
@@ -89,8 +90,9 @@ enum class AudioMode {
|
|
|
89
90
|
* **MAX_PRIORITY write thread** that loops `buffer.read() → track.write(BLOCKING)`.
|
|
90
91
|
*
|
|
91
92
|
* Key design points:
|
|
92
|
-
* - AudioTrack uses **
|
|
93
|
-
*
|
|
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).
|
|
94
96
|
* - AudioTrack stays alive for the entire session, writing silence when idle.
|
|
95
97
|
* This avoids 50–100 ms restart latency.
|
|
96
98
|
* - Config is **immutable per session** — tear down and rebuild to change
|
|
@@ -128,7 +130,9 @@ class AudioPipeline(
|
|
|
128
130
|
/** If playback head hasn't moved for this long, declare zombie. */
|
|
129
131
|
private const val ZOMBIE_STALL_THRESHOLD_MS = 5000L
|
|
130
132
|
|
|
131
|
-
/** 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. */
|
|
132
136
|
private const val MIN_VOLUME_LEVEL = 1
|
|
133
137
|
}
|
|
134
138
|
|
|
@@ -214,6 +218,13 @@ class AudioPipeline(
|
|
|
214
218
|
// ── Underrun debounce ───────────────────────────────────────────────
|
|
215
219
|
private var lastReportedUnderrunCount = 0
|
|
216
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Pending PlaybackStopped runnable scheduled on [mainHandler] — cancelled
|
|
223
|
+
* on new turn / invalidateTurn / disconnect. All mutations of this field
|
|
224
|
+
* MUST happen on the main thread to avoid races with the timer firing.
|
|
225
|
+
*/
|
|
226
|
+
private var pendingPlaybackStoppedRunnable: Runnable? = null
|
|
227
|
+
|
|
217
228
|
// ── Frequency band analysis ──────────────────────────────────────
|
|
218
229
|
private var frequencyBandAnalyzer: FrequencyBandAnalyzer? = null
|
|
219
230
|
private var frequencyBandExecutor: java.util.concurrent.ScheduledExecutorService? = null
|
|
@@ -227,6 +238,8 @@ class AudioPipeline(
|
|
|
227
238
|
val totalPushCalls = AtomicLong(0)
|
|
228
239
|
val totalPushBytes = AtomicLong(0)
|
|
229
240
|
val totalWriteLoops = AtomicLong(0)
|
|
241
|
+
/** Frames successfully written to AudioTrack (one frame per sample-time across all channels). */
|
|
242
|
+
private val framesWritten = AtomicLong(0)
|
|
230
243
|
|
|
231
244
|
// ════════════════════════════════════════════════════════════════════
|
|
232
245
|
// Connect / Disconnect
|
|
@@ -253,7 +266,7 @@ class AudioPipeline(
|
|
|
253
266
|
|
|
254
267
|
// ── 2. AudioTrack ───────────────────────────────────────────
|
|
255
268
|
val audioAttributes = AudioAttributes.Builder()
|
|
256
|
-
.setUsage(AudioAttributes.
|
|
269
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
257
270
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
258
271
|
.build()
|
|
259
272
|
|
|
@@ -309,8 +322,8 @@ class AudioPipeline(
|
|
|
309
322
|
} catch (e: Exception) {
|
|
310
323
|
Log.e(TAG, "connect() failed", e)
|
|
311
324
|
setState(PipelineState.ERROR)
|
|
312
|
-
listener.onError("CONNECT_FAILED", e.message ?: "Unknown error")
|
|
313
325
|
disconnect()
|
|
326
|
+
throw e
|
|
314
327
|
}
|
|
315
328
|
}
|
|
316
329
|
|
|
@@ -321,6 +334,9 @@ class AudioPipeline(
|
|
|
321
334
|
* `WRITE_BLOCKING` call, then joins the thread.
|
|
322
335
|
*/
|
|
323
336
|
fun disconnect() {
|
|
337
|
+
// Cancel any pending PlaybackStopped dispatch before tearing down.
|
|
338
|
+
cancelPendingPlaybackStopped()
|
|
339
|
+
|
|
324
340
|
running.set(false)
|
|
325
341
|
|
|
326
342
|
// Stop zombie detection
|
|
@@ -407,6 +423,7 @@ class AudioPipeline(
|
|
|
407
423
|
pendingFlush.set(true)
|
|
408
424
|
setState(PipelineState.STREAMING)
|
|
409
425
|
frequencyBandAnalyzer?.reset()
|
|
426
|
+
cancelPendingPlaybackStopped()
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
// ── Decode base64 → PCM shorts ──────────────────────────────
|
|
@@ -451,6 +468,7 @@ class AudioPipeline(
|
|
|
451
468
|
lastReportedUnderrunCount = 0
|
|
452
469
|
setState(PipelineState.IDLE)
|
|
453
470
|
frequencyBandAnalyzer?.reset()
|
|
471
|
+
cancelPendingPlaybackStopped()
|
|
454
472
|
}
|
|
455
473
|
}
|
|
456
474
|
|
|
@@ -479,6 +497,37 @@ class AudioPipeline(
|
|
|
479
497
|
return bundle
|
|
480
498
|
}
|
|
481
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Current platform output latency in milliseconds — i.e., how long after
|
|
502
|
+
* a sample is written before it physically leaves the speaker.
|
|
503
|
+
*
|
|
504
|
+
* Uses [AudioTrack.getTimestamp] to compute frames still in flight, then
|
|
505
|
+
* converts to ms. Falls back to a conservative HAL-buffer estimate when
|
|
506
|
+
* the timestamp call returns false (notably during initial buffering or
|
|
507
|
+
* on bad audio routes).
|
|
508
|
+
*
|
|
509
|
+
* Returns 0 if the pipeline is not connected.
|
|
510
|
+
*/
|
|
511
|
+
fun outputLatencyMs(): Double {
|
|
512
|
+
val track = audioTrack ?: return 0.0
|
|
513
|
+
val ts = android.media.AudioTimestamp()
|
|
514
|
+
val ok = try {
|
|
515
|
+
track.getTimestamp(ts)
|
|
516
|
+
} catch (e: IllegalStateException) {
|
|
517
|
+
false
|
|
518
|
+
}
|
|
519
|
+
if (ok) {
|
|
520
|
+
val inFlight = framesWritten.get() - ts.framePosition
|
|
521
|
+
if (inFlight > 0) {
|
|
522
|
+
return (inFlight.toDouble() / sampleRate.toDouble()) * 1000.0
|
|
523
|
+
}
|
|
524
|
+
return 0.0
|
|
525
|
+
}
|
|
526
|
+
// Fallback: assume the HAL holds ~2× minBuffer worth of frames.
|
|
527
|
+
val fallbackFrames = (minBufferBytes / 2 / channelCount) * 2
|
|
528
|
+
return (fallbackFrames.toDouble() / sampleRate.toDouble()) * 1000.0
|
|
529
|
+
}
|
|
530
|
+
|
|
482
531
|
// ════════════════════════════════════════════════════════════════════
|
|
483
532
|
// Write loop (runs on MAX_PRIORITY thread)
|
|
484
533
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -534,6 +583,9 @@ class AudioPipeline(
|
|
|
534
583
|
listener.onError("WRITE_ERROR", "AudioTrack.write returned $errorName ($written)")
|
|
535
584
|
break
|
|
536
585
|
}
|
|
586
|
+
// Track frames written for output-latency computation.
|
|
587
|
+
// `written` is Int16-sample count; divide by channelCount for frames.
|
|
588
|
+
framesWritten.addAndGet((written / channelCount).toLong())
|
|
537
589
|
} catch (e: IllegalStateException) {
|
|
538
590
|
// Track was stopped/released — expected during disconnect
|
|
539
591
|
if (running.get()) {
|
|
@@ -561,7 +613,10 @@ class AudioPipeline(
|
|
|
561
613
|
|
|
562
614
|
// ── Drain detection ─────────────────────────────────────────
|
|
563
615
|
if (buf.isDrained() && state == PipelineState.DRAINING) {
|
|
564
|
-
currentTurnId?.let {
|
|
616
|
+
currentTurnId?.let { tid ->
|
|
617
|
+
listener.onDrained(tid)
|
|
618
|
+
schedulePlaybackStopped(tid)
|
|
619
|
+
}
|
|
565
620
|
setState(PipelineState.IDLE)
|
|
566
621
|
}
|
|
567
622
|
}
|
|
@@ -585,7 +640,7 @@ class AudioPipeline(
|
|
|
585
640
|
AudioMode.DUCK_OTHERS -> {
|
|
586
641
|
val result = audioManager.requestAudioFocus(
|
|
587
642
|
focusChangeListener,
|
|
588
|
-
AudioManager.
|
|
643
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
589
644
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
|
|
590
645
|
)
|
|
591
646
|
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
@@ -596,7 +651,7 @@ class AudioPipeline(
|
|
|
596
651
|
AudioMode.DO_NOT_MIX -> {
|
|
597
652
|
val result = audioManager.requestAudioFocus(
|
|
598
653
|
focusChangeListener,
|
|
599
|
-
AudioManager.
|
|
654
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
600
655
|
AudioManager.AUDIOFOCUS_GAIN
|
|
601
656
|
)
|
|
602
657
|
hasAudioFocus.set(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
@@ -691,12 +746,12 @@ class AudioPipeline(
|
|
|
691
746
|
private fun installVolumeGuard() {
|
|
692
747
|
volumeObserver = object : ContentObserver(mainHandler) {
|
|
693
748
|
override fun onChange(selfChange: Boolean) {
|
|
694
|
-
val current = audioManager.getStreamVolume(AudioManager.
|
|
749
|
+
val current = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL)
|
|
695
750
|
if (current < MIN_VOLUME_LEVEL) {
|
|
696
|
-
Log.d(TAG, "VolumeGuard: raising
|
|
751
|
+
Log.d(TAG, "VolumeGuard: raising STREAM_VOICE_CALL from $current to $MIN_VOLUME_LEVEL")
|
|
697
752
|
try {
|
|
698
753
|
audioManager.setStreamVolume(
|
|
699
|
-
AudioManager.
|
|
754
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
700
755
|
MIN_VOLUME_LEVEL,
|
|
701
756
|
0 // no flags — silent raise
|
|
702
757
|
)
|
|
@@ -772,6 +827,43 @@ class AudioPipeline(
|
|
|
772
827
|
// Internal helpers
|
|
773
828
|
// ════════════════════════════════════════════════════════════════════
|
|
774
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Schedule a `PlaybackStopped` callback approximately [outputLatencyMs]
|
|
832
|
+
* milliseconds after `Drained`. Cancels any previously pending dispatch.
|
|
833
|
+
*
|
|
834
|
+
* Called from the write thread; the actual cancellation/scheduling and
|
|
835
|
+
* invocation happen on the main thread so we never race ourselves.
|
|
836
|
+
*/
|
|
837
|
+
private fun schedulePlaybackStopped(turnId: String) {
|
|
838
|
+
val latencyMs = outputLatencyMs().toLong()
|
|
839
|
+
mainHandler.post {
|
|
840
|
+
pendingPlaybackStoppedRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
841
|
+
|
|
842
|
+
val runnable = Runnable {
|
|
843
|
+
pendingPlaybackStoppedRunnable = null
|
|
844
|
+
listener.onPlaybackStopped(turnId)
|
|
845
|
+
}
|
|
846
|
+
pendingPlaybackStoppedRunnable = runnable
|
|
847
|
+
|
|
848
|
+
if (latencyMs > 0) {
|
|
849
|
+
mainHandler.postDelayed(runnable, latencyMs)
|
|
850
|
+
} else {
|
|
851
|
+
mainHandler.post(runnable)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Cancel any pending PlaybackStopped dispatch. Posts to [mainHandler] so
|
|
858
|
+
* all mutations of [pendingPlaybackStoppedRunnable] are on one thread.
|
|
859
|
+
*/
|
|
860
|
+
private fun cancelPendingPlaybackStopped() {
|
|
861
|
+
mainHandler.post {
|
|
862
|
+
pendingPlaybackStoppedRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
863
|
+
pendingPlaybackStoppedRunnable = null
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
775
867
|
private fun setState(newState: PipelineState) {
|
|
776
868
|
if (state == newState) return
|
|
777
869
|
state = newState
|
|
@@ -787,6 +879,7 @@ class AudioPipeline(
|
|
|
787
879
|
totalPushCalls.set(0)
|
|
788
880
|
totalPushBytes.set(0)
|
|
789
881
|
totalWriteLoops.set(0)
|
|
882
|
+
framesWritten.set(0)
|
|
790
883
|
jitterBuffer?.resetTelemetry()
|
|
791
884
|
}
|
|
792
885
|
}
|
|
@@ -90,6 +90,7 @@ class PipelineIntegration(
|
|
|
90
90
|
const val EVENT_ZOMBIE_DETECTED = "PipelineZombieDetected"
|
|
91
91
|
const val EVENT_UNDERRUN = "PipelineUnderrun"
|
|
92
92
|
const val EVENT_DRAINED = "PipelineDrained"
|
|
93
|
+
const val EVENT_PLAYBACK_STOPPED = "PipelinePlaybackStopped"
|
|
93
94
|
const val EVENT_AUDIO_FOCUS_LOST = "PipelineAudioFocusLost"
|
|
94
95
|
const val EVENT_AUDIO_FOCUS_RESUMED = "PipelineAudioFocusResumed"
|
|
95
96
|
const val EVENT_FREQUENCY_BANDS = "PipelineFrequencyBands"
|
|
@@ -247,6 +248,13 @@ class PipelineIntegration(
|
|
|
247
248
|
return pipeline?.getState()?.value ?: PipelineState.IDLE.value
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Current platform output latency in milliseconds. Returns 0 if not connected.
|
|
253
|
+
*/
|
|
254
|
+
fun outputLatencyMs(): Double {
|
|
255
|
+
return pipeline?.outputLatencyMs() ?: 0.0
|
|
256
|
+
}
|
|
257
|
+
|
|
250
258
|
/**
|
|
251
259
|
* Log AudioTrack health — called from the device callback to capture
|
|
252
260
|
* track state at the moment of a route change.
|
|
@@ -305,6 +313,12 @@ class PipelineIntegration(
|
|
|
305
313
|
})
|
|
306
314
|
}
|
|
307
315
|
|
|
316
|
+
override fun onPlaybackStopped(turnId: String) {
|
|
317
|
+
sendEvent(EVENT_PLAYBACK_STOPPED, Bundle().apply {
|
|
318
|
+
putString("turnId", turnId)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
|
|
308
322
|
override fun onAudioFocusLost() {
|
|
309
323
|
sendEvent(EVENT_AUDIO_FOCUS_LOST, Bundle())
|
|
310
324
|
}
|
package/build/events.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface AudioEventPayload {
|
|
|
16
16
|
mid: number;
|
|
17
17
|
high: number;
|
|
18
18
|
};
|
|
19
|
+
/** Set by native when a mid-recording error occurs (interruption, read failure).
|
|
20
|
+
* When present, `encoded` is absent and the recording is no longer active. */
|
|
21
|
+
error?: string;
|
|
22
|
+
errorMessage?: string;
|
|
19
23
|
}
|
|
20
24
|
export declare const DeviceReconnectedReasons: {
|
|
21
25
|
readonly newDeviceAvailable: "newDeviceAvailable";
|
|
@@ -28,8 +32,30 @@ export type DeviceReconnectedEventPayload = {
|
|
|
28
32
|
};
|
|
29
33
|
export declare const AudioEvents: {
|
|
30
34
|
AudioData: string;
|
|
35
|
+
MicrophoneError: string;
|
|
31
36
|
DeviceReconnected: string;
|
|
32
37
|
};
|
|
33
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;
|
|
34
60
|
export declare function subscribeToEvent<T extends unknown>(eventName: string, listener: (event: T | undefined) => Promise<void>): EventSubscription;
|
|
35
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;
|
|
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;
|
|
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
|
-
export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from "./pipeline";
|
|
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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"}
|
package/build/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ExpoPlayAudioStreamModule from "./ExpoPlayAudioStreamModule";
|
|
2
2
|
import { EncodingTypes, PlaybackModes, } from "./types";
|
|
3
|
-
import { addAudioEventListener, AudioEvents, subscribeToEvent, } from "./events";
|
|
3
|
+
import { addAudioEventListener, addMicrophoneErrorListener, AudioEvents, subscribeToEvent, } from "./events";
|
|
4
4
|
export class ExpoPlayAudioStream {
|
|
5
5
|
/**
|
|
6
6
|
* Destroys the audio stream module, cleaning up all resources.
|
|
@@ -19,15 +19,18 @@ export class ExpoPlayAudioStream {
|
|
|
19
19
|
static async startMicrophone(recordingConfig) {
|
|
20
20
|
let subscription;
|
|
21
21
|
try {
|
|
22
|
-
const { onAudioStream, ...options } = recordingConfig;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const { onAudioStream, onError, ...options } = recordingConfig;
|
|
23
|
+
const subscriptions = [];
|
|
24
|
+
if (onAudioStream && typeof onAudioStream === "function") {
|
|
25
|
+
subscriptions.push(addAudioEventListener(async (event) => {
|
|
26
|
+
const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, error } = event;
|
|
27
|
+
if (error)
|
|
28
|
+
return; // handled by MicrophoneError subscription; ignore here
|
|
26
29
|
if (!encoded) {
|
|
27
30
|
console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
|
|
28
31
|
throw new Error("Encoded audio data is missing");
|
|
29
32
|
}
|
|
30
|
-
onAudioStream
|
|
33
|
+
onAudioStream({
|
|
31
34
|
data: encoded,
|
|
32
35
|
position,
|
|
33
36
|
fileUri,
|
|
@@ -36,7 +39,15 @@ export class ExpoPlayAudioStream {
|
|
|
36
39
|
soundLevel,
|
|
37
40
|
frequencyBands,
|
|
38
41
|
});
|
|
39
|
-
});
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
if (onError && typeof onError === "function") {
|
|
45
|
+
subscriptions.push(addMicrophoneErrorListener((event) => {
|
|
46
|
+
onError(event);
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
if (subscriptions.length > 0) {
|
|
50
|
+
subscription = { remove: () => subscriptions.forEach((s) => s.remove()) };
|
|
40
51
|
}
|
|
41
52
|
const result = await ExpoPlayAudioStreamModule.startMicrophone(options);
|
|
42
53
|
return { recordingResult: result, subscription };
|
|
@@ -137,6 +148,7 @@ export class ExpoPlayAudioStream {
|
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
150
|
export { AudioEvents, EncodingTypes, PlaybackModes, };
|
|
151
|
+
export { addMicrophoneErrorListener } from "./events";
|
|
140
152
|
// Export native audio pipeline V3
|
|
141
153
|
export { Pipeline } from "./pipeline";
|
|
142
154
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\n// Type alias for backwards compatibility\ntype Subscription = EventSubscription;\nimport {\n AudioDataEvent,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n} from \"./types\";\n\nimport {\n addAudioEventListener,\n AudioEventPayload,\n AudioEvents,\n subscribeToEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n} from \"./events\";\n\nexport class ExpoPlayAudioStream {\n /**\n * Destroys the audio stream module, cleaning up all resources.\n * This should be called when the module is no longer needed.\n * It will reset all internal state and release audio resources.\n */\n static async destroy() {\n await ExpoPlayAudioStreamModule.destroy();\n }\n\n /**\n * Starts microphone streaming.\n * @param {RecordingConfig} recordingConfig - The recording configuration.\n * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.\n * @throws {Error} If the recording fails to start.\n */\n static async startMicrophone(recordingConfig: RecordingConfig): Promise<{\n recordingResult: StartRecordingResult;\n subscription?: Subscription;\n }> {\n let subscription: Subscription | undefined;\n try {\n const { onAudioStream, ...options } = recordingConfig;\n\n if (onAudioStream && typeof onAudioStream == \"function\") {\n subscription = addAudioEventListener(\n async (event: AudioEventPayload) => {\n const {\n fileUri,\n deltaSize,\n totalSize,\n position,\n encoded,\n soundLevel,\n frequencyBands,\n } = event;\n if (!encoded) {\n console.error(\n `[ExpoPlayAudioStream] Encoded audio data is missing`\n );\n throw new Error(\"Encoded audio data is missing\");\n }\n onAudioStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n }\n );\n }\n\n const result = await ExpoPlayAudioStreamModule.startMicrophone(options);\n\n return { recordingResult: result, subscription };\n } catch (error) {\n console.error(error);\n subscription?.remove();\n throw new Error(`Failed to start recording: ${error}`);\n }\n }\n\n /**\n * Stops the current microphone streaming.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone streaming fails to stop.\n */\n static async stopMicrophone(): Promise<AudioRecording | null> {\n try {\n return await ExpoPlayAudioStreamModule.stopMicrophone();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop mic stream: ${error}`);\n }\n }\n\n /**\n * Subscribes to audio events emitted during recording/streaming.\n * @param onMicrophoneStream - Callback function that will be called when audio data is received.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events\n */\n static subscribeToAudioEvents(\n onMicrophoneStream: (event: AudioDataEvent) => Promise<void>\n ): Subscription {\n return addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands } =\n event;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onMicrophoneStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n });\n }\n\n /**\n * Subscribes to events emitted by the audio stream module, for advanced use cases.\n * @param eventName - The name of the event to subscribe to.\n * @param onEvent - Callback function that will be called when the event is emitted.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribe<T extends unknown>(\n eventName: string,\n onEvent: (event: T | undefined) => Promise<void>\n ): Subscription {\n return subscribeToEvent(eventName, onEvent);\n }\n\n /**\n * Prompts the user to select the microphone mode.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone mode fails to prompt.\n */\n static promptMicrophoneModes() {\n ExpoPlayAudioStreamModule.promptMicrophoneModes();\n }\n\n /**\n * Toggles the silence state of the microphone.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone fails to toggle silence.\n */\n static toggleSilence(isSilent: boolean) {\n ExpoPlayAudioStreamModule.toggleSilence(isSilent);\n }\n\n /**\n * Requests microphone permission from the user.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.\n */\n static async requestPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.requestPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to request permissions: ${error}`);\n }\n }\n\n /**\n * Gets the current microphone permission status.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.\n */\n static async getPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.getPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to get permissions: ${error}`);\n }\n }\n}\n\nexport {\n AudioDataEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n AudioEvents,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n};\n\n// Re-export Subscription type for backwards compatibility\nexport type { EventSubscription } from \"expo-modules-core\";\nexport type { Subscription } from \"./events\";\n\n// Export native audio pipeline V3\nexport { Pipeline } from \"./pipeline\";\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from \"./pipeline\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,yBAAyB,MAAM,6BAA6B,CAAC;AAIpE,OAAO,EAOL,aAAa,EAEb,aAAa,GAcd,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAE1B,WAAW,EACX,gBAAgB,GAGjB,MAAM,UAAU,CAAC;AAElB,MAAM,OAAO,mBAAmB;IAC9B;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO;QAClB,MAAM,yBAAyB,CAAC,OAAO,EAAE,CAAC;IAC5C,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,eAAgC;QAI3D,IAAI,YAAsC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,GAAG,eAAe,CAAC;YAE/D,MAAM,aAAa,GAAwB,EAAE,CAAC;YAE9C,IAAI,aAAa,IAAI,OAAO,aAAa,KAAK,UAAU,EAAE,CAAC;gBACzD,aAAa,CAAC,IAAI,CAChB,qBAAqB,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;oBACvD,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;oBACtG,IAAI,KAAK;wBAAE,OAAO,CAAC,uDAAuD;oBAC1E,IAAI,CAAC,OAAO,EAAE,CAAC;wBACb,OAAO,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;wBACrE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;oBACnD,CAAC;oBACD,aAAa,CAAC;wBACZ,IAAI,EAAE,OAAO;wBACb,QAAQ;wBACR,OAAO;wBACP,aAAa,EAAE,SAAS;wBACxB,SAAS;wBACT,UAAU;wBACV,cAAc;qBACf,CAAC,CAAC;gBACL,CAAC,CAAC,CACH,CAAC;YACJ,CAAC;YAED,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC7C,aAAa,CAAC,IAAI,CAChB,0BAA0B,CAAC,CAAC,KAAK,EAAE,EAAE;oBACnC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC,CAAC,CACH,CAAC;YACJ,CAAC;YAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,YAAY,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAC5E,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,yBAAyB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAExE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACnD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,YAAY,EAAE,MAAM,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc;QACzB,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,cAAc,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,sBAAsB,CAC3B,kBAA4D;QAE5D,OAAO,qBAAqB,CAAC,KAAK,EAAE,KAAwB,EAAE,EAAE;YAC9D,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,GACpF,KAAK,CAAC;YACR,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,qDAAqD,CAAC,CAAC;gBACrE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,CAAC;YACD,kBAAkB,EAAE,CAAC;gBACnB,IAAI,EAAE,OAAO;gBACb,QAAQ;gBACR,OAAO;gBACP,aAAa,EAAE,SAAS;gBACxB,SAAS;gBACT,UAAU;gBACV,cAAc;aACf,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CACd,SAAiB,EACjB,OAAgD;QAEhD,OAAO,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,qBAAqB;QAC1B,yBAAyB,CAAC,qBAAqB,EAAE,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAiB;QACpC,yBAAyB,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,uBAAuB;QAKlC,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,uBAAuB,EAAE,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,mBAAmB;QAK9B,IAAI,CAAC;YACH,OAAO,MAAM,yBAAyB,CAAC,mBAAmB,EAAE,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;CACF;AAED,OAAO,EAOL,WAAW,EAGX,aAAa,EAEb,aAAa,GAcd,CAAC;AAKF,OAAO,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;AAGtD,kCAAkC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\n// Type alias for backwards compatibility\ntype Subscription = EventSubscription;\nimport {\n AudioDataEvent,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n} from \"./types\";\n\nimport {\n addAudioEventListener,\n addMicrophoneErrorListener,\n AudioEventPayload,\n AudioEvents,\n subscribeToEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n} from \"./events\";\n\nexport class ExpoPlayAudioStream {\n /**\n * Destroys the audio stream module, cleaning up all resources.\n * This should be called when the module is no longer needed.\n * It will reset all internal state and release audio resources.\n */\n static async destroy() {\n await ExpoPlayAudioStreamModule.destroy();\n }\n\n /**\n * Starts microphone streaming.\n * @param {RecordingConfig} recordingConfig - The recording configuration.\n * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.\n * @throws {Error} If the recording fails to start.\n */\n static async startMicrophone(recordingConfig: RecordingConfig): Promise<{\n recordingResult: StartRecordingResult;\n subscription?: Subscription;\n }> {\n let subscription: Subscription | undefined;\n try {\n const { onAudioStream, onError, ...options } = recordingConfig;\n\n const subscriptions: EventSubscription[] = [];\n\n if (onAudioStream && typeof onAudioStream === \"function\") {\n subscriptions.push(\n addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, error } = event;\n if (error) return; // handled by MicrophoneError subscription; ignore here\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onAudioStream({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n })\n );\n }\n\n if (onError && typeof onError === \"function\") {\n subscriptions.push(\n addMicrophoneErrorListener((event) => {\n onError(event);\n })\n );\n }\n\n if (subscriptions.length > 0) {\n subscription = { remove: () => subscriptions.forEach((s) => s.remove()) };\n }\n\n const result = await ExpoPlayAudioStreamModule.startMicrophone(options);\n\n return { recordingResult: result, subscription };\n } catch (error) {\n console.error(error);\n subscription?.remove();\n throw new Error(`Failed to start recording: ${error}`);\n }\n }\n\n /**\n * Stops the current microphone streaming.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone streaming fails to stop.\n */\n static async stopMicrophone(): Promise<AudioRecording | null> {\n try {\n return await ExpoPlayAudioStreamModule.stopMicrophone();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop mic stream: ${error}`);\n }\n }\n\n /**\n * Subscribes to audio events emitted during recording/streaming.\n * @param onMicrophoneStream - Callback function that will be called when audio data is received.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events\n */\n static subscribeToAudioEvents(\n onMicrophoneStream: (event: AudioDataEvent) => Promise<void>\n ): Subscription {\n return addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands } =\n event;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onMicrophoneStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n });\n }\n\n /**\n * Subscribes to events emitted by the audio stream module, for advanced use cases.\n * @param eventName - The name of the event to subscribe to.\n * @param onEvent - Callback function that will be called when the event is emitted.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribe<T extends unknown>(\n eventName: string,\n onEvent: (event: T | undefined) => Promise<void>\n ): Subscription {\n return subscribeToEvent(eventName, onEvent);\n }\n\n /**\n * Prompts the user to select the microphone mode.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone mode fails to prompt.\n */\n static promptMicrophoneModes() {\n ExpoPlayAudioStreamModule.promptMicrophoneModes();\n }\n\n /**\n * Toggles the silence state of the microphone.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone fails to toggle silence.\n */\n static toggleSilence(isSilent: boolean) {\n ExpoPlayAudioStreamModule.toggleSilence(isSilent);\n }\n\n /**\n * Requests microphone permission from the user.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.\n */\n static async requestPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.requestPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to request permissions: ${error}`);\n }\n }\n\n /**\n * Gets the current microphone permission status.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.\n */\n static async getPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.getPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to get permissions: ${error}`);\n }\n }\n}\n\nexport {\n AudioDataEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n AudioEvents,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n};\n\n// Re-export Subscription type for backwards compatibility\nexport type { EventSubscription } from \"expo-modules-core\";\nexport type { Subscription } from \"./events\";\nexport { addMicrophoneErrorListener } from \"./events\";\nexport type { MicrophoneErrorEventPayload } from \"./events\";\n\n// Export native audio pipeline V3\nexport { Pipeline } from \"./pipeline\";\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelinePlaybackStoppedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from \"./pipeline\";\n"]}
|
|
@@ -39,6 +39,19 @@ export declare class Pipeline {
|
|
|
39
39
|
static getState(): PipelineState;
|
|
40
40
|
/** Get a telemetry snapshot (buffer levels, counters, etc.). */
|
|
41
41
|
static getTelemetry(): PipelineTelemetry;
|
|
42
|
+
/**
|
|
43
|
+
* Query the platform's current output latency — i.e., how long after a
|
|
44
|
+
* sample is written to the native buffer before it actually leaves the
|
|
45
|
+
* speaker.
|
|
46
|
+
*
|
|
47
|
+
* Value can change mid-session, notably on audio route changes such as
|
|
48
|
+
* switching from built-in speaker to Bluetooth (Bluetooth typically adds
|
|
49
|
+
* 100+ ms). **Always query at the moment you care; do not cache.**
|
|
50
|
+
*
|
|
51
|
+
* Returns 0 if the pipeline is not connected or the platform cannot
|
|
52
|
+
* report a value.
|
|
53
|
+
*/
|
|
54
|
+
static getOutputLatencyMs(): number;
|
|
42
55
|
/**
|
|
43
56
|
* Subscribe to a specific pipeline event with full type safety.
|
|
44
57
|
*
|
|
@@ -77,5 +90,5 @@ export declare class Pipeline {
|
|
|
77
90
|
remove: () => void;
|
|
78
91
|
};
|
|
79
92
|
}
|
|
80
|
-
export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from './types';
|
|
93
|
+
export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelinePlaybackStoppedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from './types';
|
|
81
94
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,qBAAa,QAAQ;IAKnB;;;;;;OAMG;WACU,OAAO,CAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAIjC;;;OAGG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQxC;;;;;;OAMG;WACU,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO;IAQhE;;;OAGG;WACU,cAAc,CACzB,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC;IAQhB,oDAAoD;IACpD,MAAM,CAAC,QAAQ,IAAI,aAAa;IAIhC,gEAAgE;IAChE,MAAM,CAAC,YAAY,IAAI,iBAAiB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,qBAAa,QAAQ;IAKnB;;;;;;OAMG;WACU,OAAO,CAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAIjC;;;OAGG;WACU,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQxC;;;;;;OAMG;WACU,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO;IAQhE;;;OAGG;WACU,cAAc,CACzB,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC;IAQhB,oDAAoD;IACpD,MAAM,CAAC,QAAQ,IAAI,aAAa;IAIhC,gEAAgE;IAChE,MAAM,CAAC,YAAY,IAAI,iBAAiB;IAIxC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,kBAAkB,IAAI,MAAM;IAQnC;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAC1C,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC7D,iBAAiB;IAWpB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CACZ,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,GAC3D;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE;IAuBzB;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CACjB,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC9C;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE;CAmB1B;AAGD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,6BAA6B,EAC7B,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,iBAAiB,EACjB,yBAAyB,EACzB,4BAA4B,EAC5B,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,oBAAoB,EACpB,4BAA4B,EAC5B,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,SAAS,CAAC"}
|
package/build/pipeline/index.js
CHANGED
|
@@ -74,6 +74,21 @@ export class Pipeline {
|
|
|
74
74
|
static getTelemetry() {
|
|
75
75
|
return ExpoPlayAudioStreamModule.getPipelineTelemetry();
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Query the platform's current output latency — i.e., how long after a
|
|
79
|
+
* sample is written to the native buffer before it actually leaves the
|
|
80
|
+
* speaker.
|
|
81
|
+
*
|
|
82
|
+
* Value can change mid-session, notably on audio route changes such as
|
|
83
|
+
* switching from built-in speaker to Bluetooth (Bluetooth typically adds
|
|
84
|
+
* 100+ ms). **Always query at the moment you care; do not cache.**
|
|
85
|
+
*
|
|
86
|
+
* Returns 0 if the pipeline is not connected or the platform cannot
|
|
87
|
+
* report a value.
|
|
88
|
+
*/
|
|
89
|
+
static getOutputLatencyMs() {
|
|
90
|
+
return ExpoPlayAudioStreamModule.getPipelineOutputLatencyMs();
|
|
91
|
+
}
|
|
77
92
|
// ════════════════════════════════════════════════════════════════════════
|
|
78
93
|
// Event subscriptions
|
|
79
94
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,gDAAgD;AAChD,+EAA+E;AAC/E,EAAE;AACF,6EAA6E;AAC7E,uEAAuE;AACvE,EAAE;AACF,+EAA+E;AAC/E,yEAAyE;AAGzE,OAAO,yBAAyB,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAa7C,MAAM,OAAO,QAAQ;IACnB,2EAA2E;IAC3E,YAAY;IACZ,2EAA2E;IAE3E;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,CAClB,UAAkC,EAAE;QAEpC,OAAO,MAAM,yBAAyB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,UAAU;QACrB,OAAO,MAAM,yBAAyB,CAAC,kBAAkB,EAAE,CAAC;IAC9D,CAAC;IAED,2EAA2E;IAC3E,aAAa;IACb,2EAA2E;IAE3E;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,OAAiC;QACtD,OAAO,MAAM,yBAAyB,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACpE,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAAC,OAAiC;QACpD,OAAO,yBAAyB,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAClE,CAAC;IAED,2EAA2E;IAC3E,kBAAkB;IAClB,2EAA2E;IAE3E;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc,CACzB,OAAsC;QAEtC,OAAO,MAAM,yBAAyB,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACzE,CAAC;IAED,2EAA2E;IAC3E,oBAAoB;IACpB,2EAA2E;IAE3E,oDAAoD;IACpD,MAAM,CAAC,QAAQ;QACb,OAAO,yBAAyB,CAAC,gBAAgB,EAAmB,CAAC;IACvE,CAAC;IAED,gEAAgE;IAChE,MAAM,CAAC,YAAY;QACjB,OAAO,yBAAyB,CAAC,oBAAoB,EAAuB,CAAC;IAC/E,CAAC;IAED,2EAA2E;IAC3E,sBAAsB;IACtB,2EAA2E;IAE3E;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,SAAS,CACd,SAAY,EACZ,QAA8D;QAE9D,OAAO,gBAAgB,CACrB,SAAS,EACT,KAAK,EAAE,KAAK,EAAE,EAAE;YACd,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CACZ,QAA4D;QAE5D,MAAM,IAAI,GAAwB,EAAE,CAAC;QAErC,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YAC9C,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,wBAAwB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YACvD,QAAQ,CAAC;gBACP,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,0BAA0B,CAAC,CAAC,SAAS,cAAc,CAAC,CAAC,YAAY,EAAE;aAC7E,CAAC,CAAC;QACL,CAAC,CAAC,CACH,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC9C,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CACjB,QAA+C;QAE/C,MAAM,IAAI,GAAwB,EAAE,CAAC;QAErC,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,wBAAwB,EAAE,KAAK,IAAI,EAAE;YACtD,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/B,CAAC,CAAC,CACH,CAAC;QAEF,IAAI,CAAC,IAAI,CACP,QAAQ,CAAC,SAAS,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACzD,QAAQ,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CACH,CAAC;QAEF,OAAO;YACL,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC9C,CAAC;IACJ,CAAC;CACF","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Wrapper\n// ────────────────────────────────────────────────────────────────────────────\n//\n// Thin wrapper over the existing ExpoPlayAudioStreamModule (not a new native\n// module). Uses static methods matching the existing codebase pattern.\n//\n// Hot path: pushAudioSync() — synchronous Function call, no Promise overhead.\n// Cold path: pushAudio() — async with error propagation via Promise.\n\nimport type { EventSubscription } from 'expo-modules-core';\nimport ExpoPlayAudioStreamModule from '../ExpoPlayAudioStreamModule';\nimport { subscribeToEvent } from '../events';\n\nimport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineTelemetry,\n} from './types';\n\nexport class Pipeline {\n // ════════════════════════════════════════════════════════════════════════\n // Lifecycle\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Connect the native audio pipeline.\n *\n * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and\n * MAX_PRIORITY write thread. Config is immutable per session — disconnect\n * and reconnect to change sample rate.\n */\n static async connect(\n options: ConnectPipelineOptions = {}\n ): Promise<ConnectPipelineResult> {\n return await ExpoPlayAudioStreamModule.connectPipeline(options);\n }\n\n /**\n * Disconnect the pipeline. Tears down AudioTrack, write thread, audio\n * focus, volume guard, and zombie detection.\n */\n static async disconnect(): Promise<void> {\n return await ExpoPlayAudioStreamModule.disconnectPipeline();\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Push audio\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Push base64-encoded PCM16 audio into the jitter buffer (async).\n *\n * Use this when you need error propagation via Promise rejection.\n * For the hot path (e.g., inside a WebSocket message handler), prefer\n * [pushAudioSync] which avoids Promise overhead.\n */\n static async pushAudio(options: PushPipelineAudioOptions): Promise<void> {\n return await ExpoPlayAudioStreamModule.pushPipelineAudio(options);\n }\n\n /**\n * Push base64-encoded PCM16 audio synchronously (no Promise overhead).\n *\n * Designed for the hot path — call this from your WebSocket onmessage\n * handler for minimum latency. Returns `true` on success, `false` on\n * failure (errors are also reported via PipelineError events).\n */\n static pushAudioSync(options: PushPipelineAudioOptions): boolean {\n return ExpoPlayAudioStreamModule.pushPipelineAudioSync(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Turn management\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Invalidate the current turn. Resets the jitter buffer so stale audio\n * from the old turn is discarded immediately.\n */\n static async invalidateTurn(\n options: InvalidatePipelineTurnOptions\n ): Promise<void> {\n return await ExpoPlayAudioStreamModule.invalidatePipelineTurn(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // State & Telemetry\n // ════════════════════════════════════════════════════════════════════════\n\n /** Get the current pipeline state synchronously. */\n static getState(): PipelineState {\n return ExpoPlayAudioStreamModule.getPipelineState() as PipelineState;\n }\n\n /** Get a telemetry snapshot (buffer levels, counters, etc.). */\n static getTelemetry(): PipelineTelemetry {\n return ExpoPlayAudioStreamModule.getPipelineTelemetry() as PipelineTelemetry;\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Event subscriptions\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Subscribe to a specific pipeline event with full type safety.\n *\n * @example\n * ```ts\n * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {\n * console.log('State:', e.state);\n * });\n * // Later:\n * sub.remove();\n * ```\n */\n static subscribe<K extends PipelineEventName>(\n eventName: K,\n listener: (event: PipelineEventMap[K]) => Promise<void> | void\n ): EventSubscription {\n return subscribeToEvent<PipelineEventMap[K]>(\n eventName,\n async (event) => {\n if (event !== undefined) {\n await listener(event);\n }\n }\n );\n }\n\n /**\n * Convenience: subscribe to both PipelineError and PipelineZombieDetected.\n *\n * Useful for a single error handler that covers fatal and near-fatal\n * conditions. The callback receives a normalized `{ code, message }`.\n */\n static onError(\n listener: (error: { code: string; message: string }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineError', async (e) => {\n listener({ code: e.code, message: e.message });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineZombieDetected', async (e) => {\n listener({\n code: 'ZOMBIE_DETECTED',\n message: `AudioTrack stalled for ${e.stalledMs}ms at head=${e.playbackHead}`,\n });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n\n /**\n * Convenience: subscribe to audio focus loss and resumption events.\n *\n * During focus loss the pipeline writes silence instead of real audio.\n * The caller should typically invalidateTurn + re-request audio from the\n * AI backend on focus regain.\n */\n static onAudioFocus(\n listener: (event: { focused: boolean }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusLost', async () => {\n listener({ focused: false });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusResumed', async () => {\n listener({ focused: true });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n}\n\n// Re-export all types for consumer convenience\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from './types';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pipeline/index.ts"],"names":[],"mappings":"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","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Wrapper\n// ────────────────────────────────────────────────────────────────────────────\n//\n// Thin wrapper over the existing ExpoPlayAudioStreamModule (not a new native\n// module). Uses static methods matching the existing codebase pattern.\n//\n// Hot path: pushAudioSync() — synchronous Function call, no Promise overhead.\n// Cold path: pushAudio() — async with error propagation via Promise.\n\nimport type { EventSubscription } from 'expo-modules-core';\nimport ExpoPlayAudioStreamModule from '../ExpoPlayAudioStreamModule';\nimport { subscribeToEvent } from '../events';\n\nimport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineTelemetry,\n} from './types';\n\nexport class Pipeline {\n // ════════════════════════════════════════════════════════════════════════\n // Lifecycle\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Connect the native audio pipeline.\n *\n * Creates an AudioTrack (buffer size from device HAL), jitter buffer, and\n * MAX_PRIORITY write thread. Config is immutable per session — disconnect\n * and reconnect to change sample rate.\n */\n static async connect(\n options: ConnectPipelineOptions = {}\n ): Promise<ConnectPipelineResult> {\n return await ExpoPlayAudioStreamModule.connectPipeline(options);\n }\n\n /**\n * Disconnect the pipeline. Tears down AudioTrack, write thread, audio\n * focus, volume guard, and zombie detection.\n */\n static async disconnect(): Promise<void> {\n return await ExpoPlayAudioStreamModule.disconnectPipeline();\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Push audio\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Push base64-encoded PCM16 audio into the jitter buffer (async).\n *\n * Use this when you need error propagation via Promise rejection.\n * For the hot path (e.g., inside a WebSocket message handler), prefer\n * [pushAudioSync] which avoids Promise overhead.\n */\n static async pushAudio(options: PushPipelineAudioOptions): Promise<void> {\n return await ExpoPlayAudioStreamModule.pushPipelineAudio(options);\n }\n\n /**\n * Push base64-encoded PCM16 audio synchronously (no Promise overhead).\n *\n * Designed for the hot path — call this from your WebSocket onmessage\n * handler for minimum latency. Returns `true` on success, `false` on\n * failure (errors are also reported via PipelineError events).\n */\n static pushAudioSync(options: PushPipelineAudioOptions): boolean {\n return ExpoPlayAudioStreamModule.pushPipelineAudioSync(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Turn management\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Invalidate the current turn. Resets the jitter buffer so stale audio\n * from the old turn is discarded immediately.\n */\n static async invalidateTurn(\n options: InvalidatePipelineTurnOptions\n ): Promise<void> {\n return await ExpoPlayAudioStreamModule.invalidatePipelineTurn(options);\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // State & Telemetry\n // ════════════════════════════════════════════════════════════════════════\n\n /** Get the current pipeline state synchronously. */\n static getState(): PipelineState {\n return ExpoPlayAudioStreamModule.getPipelineState() as PipelineState;\n }\n\n /** Get a telemetry snapshot (buffer levels, counters, etc.). */\n static getTelemetry(): PipelineTelemetry {\n return ExpoPlayAudioStreamModule.getPipelineTelemetry() as PipelineTelemetry;\n }\n\n /**\n * Query the platform's current output latency — i.e., how long after a\n * sample is written to the native buffer before it actually leaves the\n * speaker.\n *\n * Value can change mid-session, notably on audio route changes such as\n * switching from built-in speaker to Bluetooth (Bluetooth typically adds\n * 100+ ms). **Always query at the moment you care; do not cache.**\n *\n * Returns 0 if the pipeline is not connected or the platform cannot\n * report a value.\n */\n static getOutputLatencyMs(): number {\n return ExpoPlayAudioStreamModule.getPipelineOutputLatencyMs() as number;\n }\n\n // ════════════════════════════════════════════════════════════════════════\n // Event subscriptions\n // ════════════════════════════════════════════════════════════════════════\n\n /**\n * Subscribe to a specific pipeline event with full type safety.\n *\n * @example\n * ```ts\n * const sub = Pipeline.subscribe('PipelineStateChanged', async (e) => {\n * console.log('State:', e.state);\n * });\n * // Later:\n * sub.remove();\n * ```\n */\n static subscribe<K extends PipelineEventName>(\n eventName: K,\n listener: (event: PipelineEventMap[K]) => Promise<void> | void\n ): EventSubscription {\n return subscribeToEvent<PipelineEventMap[K]>(\n eventName,\n async (event) => {\n if (event !== undefined) {\n await listener(event);\n }\n }\n );\n }\n\n /**\n * Convenience: subscribe to both PipelineError and PipelineZombieDetected.\n *\n * Useful for a single error handler that covers fatal and near-fatal\n * conditions. The callback receives a normalized `{ code, message }`.\n */\n static onError(\n listener: (error: { code: string; message: string }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineError', async (e) => {\n listener({ code: e.code, message: e.message });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineZombieDetected', async (e) => {\n listener({\n code: 'ZOMBIE_DETECTED',\n message: `AudioTrack stalled for ${e.stalledMs}ms at head=${e.playbackHead}`,\n });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n\n /**\n * Convenience: subscribe to audio focus loss and resumption events.\n *\n * During focus loss the pipeline writes silence instead of real audio.\n * The caller should typically invalidateTurn + re-request audio from the\n * AI backend on focus regain.\n */\n static onAudioFocus(\n listener: (event: { focused: boolean }) => void\n ): { remove: () => void } {\n const subs: EventSubscription[] = [];\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusLost', async () => {\n listener({ focused: false });\n })\n );\n\n subs.push(\n Pipeline.subscribe('PipelineAudioFocusResumed', async () => {\n listener({ focused: true });\n })\n );\n\n return {\n remove: () => subs.forEach((s) => s.remove()),\n };\n }\n}\n\n// Re-export all types for consumer convenience\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelinePlaybackStoppedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from './types';\n"]}
|