@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.
Files changed (48) hide show
  1. package/NATIVE_EVENTS.md +97 -6
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/src/main/java/expo/modules/audiostream/AudioEffectsManager.kt +6 -11
  11. package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +23 -6
  12. package/android/src/main/java/expo/modules/audiostream/CommunicationAudioManager.kt +155 -0
  13. package/android/src/main/java/expo/modules/audiostream/Constants.kt +1 -0
  14. package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +17 -3
  15. package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +104 -11
  16. package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +14 -0
  17. package/build/events.d.ts +26 -0
  18. package/build/events.d.ts.map +1 -1
  19. package/build/events.js +18 -0
  20. package/build/events.js.map +1 -1
  21. package/build/index.d.ts +3 -1
  22. package/build/index.d.ts.map +1 -1
  23. package/build/index.js +19 -7
  24. package/build/index.js.map +1 -1
  25. package/build/pipeline/index.d.ts +14 -1
  26. package/build/pipeline/index.d.ts.map +1 -1
  27. package/build/pipeline/index.js +15 -0
  28. package/build/pipeline/index.js.map +1 -1
  29. package/build/pipeline/types.d.ts +14 -0
  30. package/build/pipeline/types.d.ts.map +1 -1
  31. package/build/pipeline/types.js.map +1 -1
  32. package/build/types.d.ts +21 -0
  33. package/build/types.d.ts.map +1 -1
  34. package/build/types.js.map +1 -1
  35. package/ios/AudioPipeline.swift +67 -2
  36. package/ios/ExpoPlayAudioStreamModule.swift +43 -12
  37. package/ios/Microphone.swift +167 -120
  38. package/ios/MicrophoneDataDelegate.swift +10 -1
  39. package/ios/PipelineIntegration.swift +11 -1
  40. package/ios/SharedAudioEngine.swift +18 -0
  41. package/package.json +1 -2
  42. package/plugin/build/index.js +5 -0
  43. package/plugin/src/index.ts +5 -0
  44. package/src/events.ts +32 -0
  45. package/src/index.ts +27 -18
  46. package/src/pipeline/index.ts +17 -0
  47. package/src/pipeline/types.ts +15 -0
  48. 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 **USAGE_MEDIA + CONTENT_TYPE_SPEECH** (not
93
- * VOICE_COMMUNICATION avoids earpiece routing).
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 STREAM_MUSIC. */
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.USAGE_MEDIA)
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 { listener.onDrained(it) }
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.STREAM_MUSIC,
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.STREAM_MUSIC,
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.STREAM_MUSIC)
749
+ val current = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL)
695
750
  if (current < MIN_VOLUME_LEVEL) {
696
- Log.d(TAG, "VolumeGuard: raising STREAM_MUSIC from $current to $MIN_VOLUME_LEVEL")
751
+ Log.d(TAG, "VolumeGuard: raising STREAM_VOICE_CALL from $current to $MIN_VOLUME_LEVEL")
697
752
  try {
698
753
  audioManager.setStreamVolume(
699
- AudioManager.STREAM_MUSIC,
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
@@ -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;CAC7D;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;;;CAGvB,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GACpD,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"}
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
  }
@@ -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;AAgB5D,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}\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
- 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
@@ -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,EAGL,WAAW,EAEX,uBAAuB,EACvB,6BAA6B,EAC9B,MAAM,UAAU,CAAC;AAElB,qBAAa,mBAAmB;IAC9B;;;;OAIG;WACU,OAAO;IAIpB;;;;;OAKG;WACU,eAAe,CAAC,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC;QACtE,eAAe,EAAE,oBAAoB,CAAC;QACtC,YAAY,CAAC,EAAE,YAAY,CAAC;KAC7B,CAAC;IA8CF;;;;OAIG;WACU,cAAc,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAS7D;;;;OAIG;IACH,MAAM,CAAC,sBAAsB,CAC3B,kBAAkB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAC3D,YAAY;IAoBf;;;;;OAKG;IACH,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,OAAO,EAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAC/C,YAAY;IAIf;;;;OAIG;IACH,MAAM,CAAC,qBAAqB;IAI5B;;;;OAIG;IACH,MAAM,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO;IAItC;;;OAGG;WACU,uBAAuB,IAAI,OAAO,CAAC;QAC9C,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IASF;;;OAGG;WACU,mBAAmB,IAAI,OAAO,CAAC;QAC1C,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CAQH;AAED,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,6BAA6B,EAC7B,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,WAAW,EACX,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,GAClB,CAAC;AAGF,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,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,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,YAAY,CAAC"}
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
- if (onAudioStream && typeof onAudioStream == "function") {
24
- subscription = addAudioEventListener(async (event) => {
25
- const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, } = event;
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
@@ -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":"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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 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;IAQxC;;;;;;;;;;;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,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,SAAS,CAAC"}
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"}
@@ -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":"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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 // 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"]}