@edkimmel/expo-audio-stream 0.4.2 → 0.5.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 CHANGED
@@ -200,6 +200,43 @@ The pipeline transitions from `draining` to `idle`.
200
200
 
201
201
  ---
202
202
 
203
+ ### `PipelinePlaybackStopped`
204
+
205
+ Fired approximately `outputLatencyMs` after `PipelineDrained` for the same turn —
206
+ i.e., when the last sample physically leaves the speaker. Pairs symmetrically
207
+ with `PipelinePlaybackStarted` (start-of-emission ↔ end-of-emission).
208
+
209
+ **Distinct from `state: 'idle'`:** that's the pipeline-state-machine value
210
+ reported via `PipelineStateChanged`. `PipelinePlaybackStopped` is a
211
+ physical-world milestone about audible audio, not a state-machine transition.
212
+
213
+ | Field | Type | Notes |
214
+ |---|---|---|
215
+ | `turnId` | `string` | The turn whose last sample just stopped emitting |
216
+
217
+ **Cancellation:** the scheduled dispatch is cancelled if a new turn starts,
218
+ `invalidatePipelineTurn` is called, or the pipeline disconnects before the
219
+ delay elapses. Consumers will not receive a late-arriving event mid-stream.
220
+
221
+ **Approximation:** the delay uses `outputLatencyMs` captured at drain time.
222
+ On iOS the value reflects `AVAudioSession.outputLatency` (total HW output
223
+ latency). On Android it uses `AudioTrack.getTimestamp()` to compute frames
224
+ in-flight, falling back to a HAL-buffer estimate if the timestamp call fails.
225
+ A route change during the latency window (e.g. switch to Bluetooth mid-tail)
226
+ is *not* re-measured — the captured value is used. The error is typically
227
+ single-digit milliseconds and is acceptable for VAD-gating use cases.
228
+
229
+ **Platform:** Android, iOS
230
+
231
+ **JS response:**
232
+ - Use this — not `PipelineDrained` — when you need to know when **mic-side
233
+ echo from the speaker is over**. Useful for voice-agent pipelines that
234
+ gate server-side VAD on the agent's own playback ending.
235
+ - If you do not care about the physical-world emission boundary, prefer
236
+ `PipelineDrained` (cheaper, fires earlier).
237
+
238
+ ---
239
+
203
240
  ### `PipelineFrequencyBands`
204
241
 
205
242
  Fired at the interval configured by `frequencyBandIntervalMs` during pipeline
@@ -101,6 +101,7 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
101
101
  PipelineIntegration.EVENT_ZOMBIE_DETECTED,
102
102
  PipelineIntegration.EVENT_UNDERRUN,
103
103
  PipelineIntegration.EVENT_DRAINED,
104
+ PipelineIntegration.EVENT_PLAYBACK_STOPPED,
104
105
  PipelineIntegration.EVENT_AUDIO_FOCUS_LOST,
105
106
  PipelineIntegration.EVENT_AUDIO_FOCUS_RESUMED,
106
107
  PipelineIntegration.EVENT_FREQUENCY_BANDS
@@ -194,6 +195,10 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
194
195
  pipelineIntegration.getState()
195
196
  }
196
197
 
198
+ Function("getPipelineOutputLatencyMs") {
199
+ pipelineIntegration.outputLatencyMs()
200
+ }
201
+
197
202
  }
198
203
  private fun initializeManager() {
199
204
  val androidContext =
@@ -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)
@@ -214,6 +215,13 @@ class AudioPipeline(
214
215
  // ── Underrun debounce ───────────────────────────────────────────────
215
216
  private var lastReportedUnderrunCount = 0
216
217
 
218
+ /**
219
+ * Pending PlaybackStopped runnable scheduled on [mainHandler] — cancelled
220
+ * on new turn / invalidateTurn / disconnect. All mutations of this field
221
+ * MUST happen on the main thread to avoid races with the timer firing.
222
+ */
223
+ private var pendingPlaybackStoppedRunnable: Runnable? = null
224
+
217
225
  // ── Frequency band analysis ──────────────────────────────────────
218
226
  private var frequencyBandAnalyzer: FrequencyBandAnalyzer? = null
219
227
  private var frequencyBandExecutor: java.util.concurrent.ScheduledExecutorService? = null
@@ -227,6 +235,8 @@ class AudioPipeline(
227
235
  val totalPushCalls = AtomicLong(0)
228
236
  val totalPushBytes = AtomicLong(0)
229
237
  val totalWriteLoops = AtomicLong(0)
238
+ /** Frames successfully written to AudioTrack (one frame per sample-time across all channels). */
239
+ private val framesWritten = AtomicLong(0)
230
240
 
231
241
  // ════════════════════════════════════════════════════════════════════
232
242
  // Connect / Disconnect
@@ -309,8 +319,8 @@ class AudioPipeline(
309
319
  } catch (e: Exception) {
310
320
  Log.e(TAG, "connect() failed", e)
311
321
  setState(PipelineState.ERROR)
312
- listener.onError("CONNECT_FAILED", e.message ?: "Unknown error")
313
322
  disconnect()
323
+ throw e
314
324
  }
315
325
  }
316
326
 
@@ -321,6 +331,9 @@ class AudioPipeline(
321
331
  * `WRITE_BLOCKING` call, then joins the thread.
322
332
  */
323
333
  fun disconnect() {
334
+ // Cancel any pending PlaybackStopped dispatch before tearing down.
335
+ cancelPendingPlaybackStopped()
336
+
324
337
  running.set(false)
325
338
 
326
339
  // Stop zombie detection
@@ -407,6 +420,7 @@ class AudioPipeline(
407
420
  pendingFlush.set(true)
408
421
  setState(PipelineState.STREAMING)
409
422
  frequencyBandAnalyzer?.reset()
423
+ cancelPendingPlaybackStopped()
410
424
  }
411
425
 
412
426
  // ── Decode base64 → PCM shorts ──────────────────────────────
@@ -451,6 +465,7 @@ class AudioPipeline(
451
465
  lastReportedUnderrunCount = 0
452
466
  setState(PipelineState.IDLE)
453
467
  frequencyBandAnalyzer?.reset()
468
+ cancelPendingPlaybackStopped()
454
469
  }
455
470
  }
456
471
 
@@ -479,6 +494,37 @@ class AudioPipeline(
479
494
  return bundle
480
495
  }
481
496
 
497
+ /**
498
+ * Current platform output latency in milliseconds — i.e., how long after
499
+ * a sample is written before it physically leaves the speaker.
500
+ *
501
+ * Uses [AudioTrack.getTimestamp] to compute frames still in flight, then
502
+ * converts to ms. Falls back to a conservative HAL-buffer estimate when
503
+ * the timestamp call returns false (notably during initial buffering or
504
+ * on bad audio routes).
505
+ *
506
+ * Returns 0 if the pipeline is not connected.
507
+ */
508
+ fun outputLatencyMs(): Double {
509
+ val track = audioTrack ?: return 0.0
510
+ val ts = android.media.AudioTimestamp()
511
+ val ok = try {
512
+ track.getTimestamp(ts)
513
+ } catch (e: IllegalStateException) {
514
+ false
515
+ }
516
+ if (ok) {
517
+ val inFlight = framesWritten.get() - ts.framePosition
518
+ if (inFlight > 0) {
519
+ return (inFlight.toDouble() / sampleRate.toDouble()) * 1000.0
520
+ }
521
+ return 0.0
522
+ }
523
+ // Fallback: assume the HAL holds ~2× minBuffer worth of frames.
524
+ val fallbackFrames = (minBufferBytes / 2 / channelCount) * 2
525
+ return (fallbackFrames.toDouble() / sampleRate.toDouble()) * 1000.0
526
+ }
527
+
482
528
  // ════════════════════════════════════════════════════════════════════
483
529
  // Write loop (runs on MAX_PRIORITY thread)
484
530
  // ════════════════════════════════════════════════════════════════════
@@ -534,6 +580,9 @@ class AudioPipeline(
534
580
  listener.onError("WRITE_ERROR", "AudioTrack.write returned $errorName ($written)")
535
581
  break
536
582
  }
583
+ // Track frames written for output-latency computation.
584
+ // `written` is Int16-sample count; divide by channelCount for frames.
585
+ framesWritten.addAndGet((written / channelCount).toLong())
537
586
  } catch (e: IllegalStateException) {
538
587
  // Track was stopped/released — expected during disconnect
539
588
  if (running.get()) {
@@ -561,7 +610,10 @@ class AudioPipeline(
561
610
 
562
611
  // ── Drain detection ─────────────────────────────────────────
563
612
  if (buf.isDrained() && state == PipelineState.DRAINING) {
564
- currentTurnId?.let { listener.onDrained(it) }
613
+ currentTurnId?.let { tid ->
614
+ listener.onDrained(tid)
615
+ schedulePlaybackStopped(tid)
616
+ }
565
617
  setState(PipelineState.IDLE)
566
618
  }
567
619
  }
@@ -772,6 +824,43 @@ class AudioPipeline(
772
824
  // Internal helpers
773
825
  // ════════════════════════════════════════════════════════════════════
774
826
 
827
+ /**
828
+ * Schedule a `PlaybackStopped` callback approximately [outputLatencyMs]
829
+ * milliseconds after `Drained`. Cancels any previously pending dispatch.
830
+ *
831
+ * Called from the write thread; the actual cancellation/scheduling and
832
+ * invocation happen on the main thread so we never race ourselves.
833
+ */
834
+ private fun schedulePlaybackStopped(turnId: String) {
835
+ val latencyMs = outputLatencyMs().toLong()
836
+ mainHandler.post {
837
+ pendingPlaybackStoppedRunnable?.let { mainHandler.removeCallbacks(it) }
838
+
839
+ val runnable = Runnable {
840
+ pendingPlaybackStoppedRunnable = null
841
+ listener.onPlaybackStopped(turnId)
842
+ }
843
+ pendingPlaybackStoppedRunnable = runnable
844
+
845
+ if (latencyMs > 0) {
846
+ mainHandler.postDelayed(runnable, latencyMs)
847
+ } else {
848
+ mainHandler.post(runnable)
849
+ }
850
+ }
851
+ }
852
+
853
+ /**
854
+ * Cancel any pending PlaybackStopped dispatch. Posts to [mainHandler] so
855
+ * all mutations of [pendingPlaybackStoppedRunnable] are on one thread.
856
+ */
857
+ private fun cancelPendingPlaybackStopped() {
858
+ mainHandler.post {
859
+ pendingPlaybackStoppedRunnable?.let { mainHandler.removeCallbacks(it) }
860
+ pendingPlaybackStoppedRunnable = null
861
+ }
862
+ }
863
+
775
864
  private fun setState(newState: PipelineState) {
776
865
  if (state == newState) return
777
866
  state = newState
@@ -787,6 +876,7 @@ class AudioPipeline(
787
876
  totalPushCalls.set(0)
788
877
  totalPushBytes.set(0)
789
878
  totalWriteLoops.set(0)
879
+ framesWritten.set(0)
790
880
  jitterBuffer?.resetTelemetry()
791
881
  }
792
882
  }
@@ -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";
@@ -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;;;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 +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,iBAAiB,EAAE,mBAAmB;CACvC,CAAC;AAEF,MAAM,UAAU,qBAAqB,CACnC,QAAqD;IAErD,OAAQ,OAAe,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,QAAiD;IAEjD,OAAQ,OAAe,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// packages/expo-audio-stream/src/events.ts\n\nimport { EventEmitter, type EventSubscription } from \"expo-modules-core\";\n\n// Type alias for backwards compatibility\nexport type Subscription = EventSubscription;\n\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\nconst emitter = new EventEmitter(ExpoPlayAudioStreamModule);\n\nexport interface AudioEventPayload {\n encoded?: string;\n buffer?: Float32Array;\n fileUri: string;\n lastEmittedSize: number;\n position: number;\n deltaSize: number;\n totalSize: number;\n mimeType: string;\n streamUuid: string;\n soundLevel?: number;\n frequencyBands?: { low: number; mid: number; high: number };\n /** Set by native when a mid-recording error occurs (interruption, read failure).\n * When present, `encoded` is absent and the recording is no longer active. */\n error?: string;\n errorMessage?: string;\n}\n\nexport const DeviceReconnectedReasons = {\n newDeviceAvailable: \"newDeviceAvailable\",\n oldDeviceUnavailable: \"oldDeviceUnavailable\",\n unknown: \"unknown\",\n} as const;\n\nexport type DeviceReconnectedReason =\n (typeof DeviceReconnectedReasons)[keyof typeof DeviceReconnectedReasons];\n\nexport type DeviceReconnectedEventPayload = {\n reason: DeviceReconnectedReason;\n};\n\nexport const AudioEvents = {\n AudioData: \"AudioData\",\n DeviceReconnected: \"DeviceReconnected\",\n};\n\nexport function addAudioEventListener(\n listener: (event: AudioEventPayload) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(\"AudioData\", listener);\n}\n\nexport function subscribeToEvent<T extends unknown>(\n eventName: string,\n listener: (event: T | undefined) => Promise<void>\n): EventSubscription {\n return (emitter as any).addListener(eventName, listener);\n}\n"]}
package/build/index.d.ts CHANGED
@@ -73,5 +73,5 @@ export { AudioDataEvent, DeviceReconnectedReason, DeviceReconnectedEventPayload,
73
73
  export type { EventSubscription } from "expo-modules-core";
74
74
  export type { Subscription } from "./events";
75
75
  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";
76
+ export type { ConnectPipelineOptions, ConnectPipelineResult, PushPipelineAudioOptions, InvalidatePipelineTurnOptions, PipelineState, PipelineEventMap, PipelineEventName, PipelineBufferTelemetry, PipelineTelemetry, PipelineStateChangedEvent, PipelinePlaybackStartedEvent, PipelineErrorEvent, PipelineZombieDetectedEvent, PipelineUnderrunEvent, PipelineDrainedEvent, PipelinePlaybackStoppedEvent, PipelineAudioFocusLostEvent, PipelineAudioFocusResumedEvent, } from "./pipeline";
77
77
  //# 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":"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;IAuDF;;;;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,4BAA4B,EAC5B,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,YAAY,CAAC"}
package/build/index.js CHANGED
@@ -19,10 +19,15 @@ 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") {
22
+ const { onAudioStream, onError, ...options } = recordingConfig;
23
+ if ((onAudioStream && typeof onAudioStream == "function") ||
24
+ (onError && typeof onError == "function")) {
24
25
  subscription = addAudioEventListener(async (event) => {
25
- const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, } = event;
26
+ const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, error, errorMessage, } = event;
27
+ if (error) {
28
+ onError?.({ code: error, message: errorMessage ?? "" });
29
+ return;
30
+ }
26
31
  if (!encoded) {
27
32
  console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);
28
33
  throw new Error("Encoded audio data is missing");
@@ -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 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 if (\n (onAudioStream && typeof onAudioStream == \"function\") ||\n (onError && typeof onError == \"function\")\n ) {\n subscription = addAudioEventListener(\n async (event: AudioEventPayload) => {\n const {\n fileUri,\n deltaSize,\n totalSize,\n position,\n encoded,\n soundLevel,\n frequencyBands,\n error,\n errorMessage,\n } = event;\n if (error) {\n onError?.({ code: error, message: errorMessage ?? \"\" });\n return;\n }\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 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"]}
@@ -103,6 +103,19 @@ export interface PipelineUnderrunEvent {
103
103
  export interface PipelineDrainedEvent {
104
104
  turnId: string;
105
105
  }
106
+ /**
107
+ * Payload for `PipelinePlaybackStopped`.
108
+ *
109
+ * Fired when the last sample physically leaves the speaker, approximately
110
+ * `outputLatencyMs` after `PipelineDrained` for the same turn. Pairs with
111
+ * `PipelinePlaybackStarted` (start-of-emission ↔ end-of-emission).
112
+ *
113
+ * Note: this is a physical-world milestone, distinct from `state: 'idle'`
114
+ * (the pipeline-state-machine value reported via `PipelineStateChanged`).
115
+ */
116
+ export interface PipelinePlaybackStoppedEvent {
117
+ turnId: string;
118
+ }
106
119
  /** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */
107
120
  export type PipelineAudioFocusLostEvent = Record<string, never>;
108
121
  /** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */
@@ -121,6 +134,7 @@ export interface PipelineEventMap {
121
134
  PipelineZombieDetected: PipelineZombieDetectedEvent;
122
135
  PipelineUnderrun: PipelineUnderrunEvent;
123
136
  PipelineDrained: PipelineDrainedEvent;
137
+ PipelinePlaybackStopped: PipelinePlaybackStoppedEvent;
124
138
  PipelineAudioFocusLost: PipelineAudioFocusLostEvent;
125
139
  PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;
126
140
  PipelineFrequencyBands: PipelineFrequencyBandsEvent;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8CAA8C;AAC9C,+EAA+E","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Types\n// ────────────────────────────────────────────────────────────────────────────\n\nimport { PlaybackMode, FrequencyBandConfig, FrequencyBands } from \"../types\";\n\n// ── Connect ─────────────────────────────────────────────────────────────────\n\n/**\n * How the pipeline's playback should coexist with other audio on the device.\n *\n * - `'mixWithOthers'` (default): plays alongside other apps without\n * interrupting them. On Android no audio focus is requested. Best for\n * sound effects and short clips.\n * - `'duckOthers'`: requests audio focus with ducking. Other apps lower\n * their volume but keep playing.\n * - `'doNotMix'`: requests exclusive audio focus. Other apps pause.\n */\nexport type PipelineAudioMode = 'mixWithOthers' | 'duckOthers' | 'doNotMix';\n\n/** Options passed to `connectPipeline()`. */\nexport interface ConnectPipelineOptions {\n /** Sample rate in Hz (default 24000). */\n sampleRate?: number;\n /** Number of channels — 1 = mono, 2 = stereo (default 1). */\n channelCount?: number;\n /**\n * How many ms of audio to accumulate in the jitter buffer before the\n * priming gate opens and audio begins playing (default 80).\n */\n targetBufferMs?: number;\n /**\n * Playback mode hint for native optimizations. Affects thread priority and\n */\n playbackMode?: PlaybackMode;\n /** Interval in ms for PipelineFrequencyBands events (default 100). */\n frequencyBandIntervalMs?: number;\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n /**\n * How pipeline playback should coexist with other apps' audio.\n * Default is `'mixWithOthers'` (matches expo-audio).\n *\n * Note: this is a **behavior change** vs. prior versions of this library,\n * which effectively used `'doNotMix'`. Pass `'doNotMix'` explicitly to\n * preserve that old behavior.\n */\n audioMode?: PipelineAudioMode;\n}\n\n/** Result returned from a successful `connectPipeline()` call. */\nexport interface ConnectPipelineResult {\n sampleRate: number;\n channelCount: number;\n targetBufferMs: number;\n /**\n * Frame size in samples derived from the device HAL's\n * `AudioTrack.getMinBufferSize()`. Useful for understanding the write\n * granularity on the native side.\n */\n frameSizeSamples: number;\n}\n\n// ── Push Audio ──────────────────────────────────────────────────────────────\n\n/** Options passed to `pushPipelineAudio()` / `pushPipelineAudioSync()`. */\nexport interface PushPipelineAudioOptions {\n /** Base64-encoded PCM 16-bit signed LE audio data. */\n audio: string;\n /** Conversation turn identifier. */\n turnId: string;\n /** True if this is the first chunk of a new turn (resets jitter buffer). */\n isFirstChunk?: boolean;\n /** True if this is the final chunk of the current turn (marks end-of-stream). */\n isLastChunk?: boolean;\n}\n\n// ── Invalidate Turn ─────────────────────────────────────────────────────────\n\n/** Options passed to `invalidatePipelineTurn()`. */\nexport interface InvalidatePipelineTurnOptions {\n /** The new turn identifier — stale audio for the old turn is discarded. */\n turnId: string;\n}\n\n// ── State ───────────────────────────────────────────────────────────────────\n\n/**\n * Pipeline states reported via `PipelineStateChanged` events.\n *\n * - `idle` — connected but no audio flowing\n * - `connecting` — AudioTrack being created, focus being requested\n * - `streaming` — actively receiving and playing audio\n * - `draining` — end-of-stream marked, playing remaining buffer\n * - `error` — unrecoverable error (zombie, write failure, etc.)\n */\nexport type PipelineState =\n | 'idle'\n | 'connecting'\n | 'streaming'\n | 'draining'\n | 'error';\n\n// ── Events ──────────────────────────────────────────────────────────────────\n\n/** Payload for `PipelineStateChanged`. */\nexport interface PipelineStateChangedEvent {\n state: PipelineState;\n}\n\n/** Payload for `PipelinePlaybackStarted`. */\nexport interface PipelinePlaybackStartedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineError`. */\nexport interface PipelineErrorEvent {\n code: string;\n message: string;\n}\n\n/** Payload for `PipelineZombieDetected`. */\nexport interface PipelineZombieDetectedEvent {\n playbackHead: number;\n stalledMs: number;\n}\n\n/** Payload for `PipelineUnderrun`. */\nexport interface PipelineUnderrunEvent {\n count: number;\n}\n\n/** Payload for `PipelineDrained`. */\nexport interface PipelineDrainedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */\nexport type PipelineAudioFocusLostEvent = Record<string, never>;\n\n/** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */\nexport type PipelineAudioFocusResumedEvent = Record<string, never>;\n\n/** Payload for `PipelineFrequencyBands`. */\nexport interface PipelineFrequencyBandsEvent extends FrequencyBands {}\n\n/**\n * Map of all pipeline event names to their payload types.\n * Used with `Pipeline.subscribe<K>()` for type-safe event subscriptions.\n */\nexport interface PipelineEventMap {\n PipelineStateChanged: PipelineStateChangedEvent;\n PipelinePlaybackStarted: PipelinePlaybackStartedEvent;\n PipelineError: PipelineErrorEvent;\n PipelineZombieDetected: PipelineZombieDetectedEvent;\n PipelineUnderrun: PipelineUnderrunEvent;\n PipelineDrained: PipelineDrainedEvent;\n PipelineAudioFocusLost: PipelineAudioFocusLostEvent;\n PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;\n PipelineFrequencyBands: PipelineFrequencyBandsEvent;\n}\n\n/** Union of all pipeline event name strings. */\nexport type PipelineEventName = keyof PipelineEventMap;\n\n// ── Telemetry ───────────────────────────────────────────────────────────────\n\n/** Jitter buffer telemetry counters. */\nexport interface PipelineBufferTelemetry {\n /** Current buffer level in milliseconds. */\n bufferMs: number;\n /** Current buffer level in samples. */\n bufferSamples: number;\n /** Whether the priming gate has opened. */\n primed: boolean;\n /** Total samples written by the producer since last reset. */\n totalWritten: number;\n /** Total samples read by the consumer since last reset. */\n totalRead: number;\n /** Number of underrun events. */\n underrunCount: number;\n /** Peak buffer level in samples. */\n peakLevel: number;\n}\n\n/** Full pipeline telemetry snapshot. */\nexport interface PipelineTelemetry extends PipelineBufferTelemetry {\n /** Current pipeline state. */\n state: PipelineState;\n /** Total pushAudio/pushAudioSync calls since connect. */\n totalPushCalls: number;\n /** Total bytes pushed since connect. */\n totalPushBytes: number;\n /** Total write-loop iterations since connect. */\n totalWriteLoops: number;\n /** Current turn identifier. */\n turnId: string;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/pipeline/types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8CAA8C;AAC9C,+EAA+E","sourcesContent":["// ────────────────────────────────────────────────────────────────────────────\n// Native Audio Pipeline — V3 TypeScript Types\n// ────────────────────────────────────────────────────────────────────────────\n\nimport { PlaybackMode, FrequencyBandConfig, FrequencyBands } from \"../types\";\n\n// ── Connect ─────────────────────────────────────────────────────────────────\n\n/**\n * How the pipeline's playback should coexist with other audio on the device.\n *\n * - `'mixWithOthers'` (default): plays alongside other apps without\n * interrupting them. On Android no audio focus is requested. Best for\n * sound effects and short clips.\n * - `'duckOthers'`: requests audio focus with ducking. Other apps lower\n * their volume but keep playing.\n * - `'doNotMix'`: requests exclusive audio focus. Other apps pause.\n */\nexport type PipelineAudioMode = 'mixWithOthers' | 'duckOthers' | 'doNotMix';\n\n/** Options passed to `connectPipeline()`. */\nexport interface ConnectPipelineOptions {\n /** Sample rate in Hz (default 24000). */\n sampleRate?: number;\n /** Number of channels — 1 = mono, 2 = stereo (default 1). */\n channelCount?: number;\n /**\n * How many ms of audio to accumulate in the jitter buffer before the\n * priming gate opens and audio begins playing (default 80).\n */\n targetBufferMs?: number;\n /**\n * Playback mode hint for native optimizations. Affects thread priority and\n */\n playbackMode?: PlaybackMode;\n /** Interval in ms for PipelineFrequencyBands events (default 100). */\n frequencyBandIntervalMs?: number;\n /** Optional frequency band crossover configuration. */\n frequencyBandConfig?: FrequencyBandConfig;\n /**\n * How pipeline playback should coexist with other apps' audio.\n * Default is `'mixWithOthers'` (matches expo-audio).\n *\n * Note: this is a **behavior change** vs. prior versions of this library,\n * which effectively used `'doNotMix'`. Pass `'doNotMix'` explicitly to\n * preserve that old behavior.\n */\n audioMode?: PipelineAudioMode;\n}\n\n/** Result returned from a successful `connectPipeline()` call. */\nexport interface ConnectPipelineResult {\n sampleRate: number;\n channelCount: number;\n targetBufferMs: number;\n /**\n * Frame size in samples derived from the device HAL's\n * `AudioTrack.getMinBufferSize()`. Useful for understanding the write\n * granularity on the native side.\n */\n frameSizeSamples: number;\n}\n\n// ── Push Audio ──────────────────────────────────────────────────────────────\n\n/** Options passed to `pushPipelineAudio()` / `pushPipelineAudioSync()`. */\nexport interface PushPipelineAudioOptions {\n /** Base64-encoded PCM 16-bit signed LE audio data. */\n audio: string;\n /** Conversation turn identifier. */\n turnId: string;\n /** True if this is the first chunk of a new turn (resets jitter buffer). */\n isFirstChunk?: boolean;\n /** True if this is the final chunk of the current turn (marks end-of-stream). */\n isLastChunk?: boolean;\n}\n\n// ── Invalidate Turn ─────────────────────────────────────────────────────────\n\n/** Options passed to `invalidatePipelineTurn()`. */\nexport interface InvalidatePipelineTurnOptions {\n /** The new turn identifier — stale audio for the old turn is discarded. */\n turnId: string;\n}\n\n// ── State ───────────────────────────────────────────────────────────────────\n\n/**\n * Pipeline states reported via `PipelineStateChanged` events.\n *\n * - `idle` — connected but no audio flowing\n * - `connecting` — AudioTrack being created, focus being requested\n * - `streaming` — actively receiving and playing audio\n * - `draining` — end-of-stream marked, playing remaining buffer\n * - `error` — unrecoverable error (zombie, write failure, etc.)\n */\nexport type PipelineState =\n | 'idle'\n | 'connecting'\n | 'streaming'\n | 'draining'\n | 'error';\n\n// ── Events ──────────────────────────────────────────────────────────────────\n\n/** Payload for `PipelineStateChanged`. */\nexport interface PipelineStateChangedEvent {\n state: PipelineState;\n}\n\n/** Payload for `PipelinePlaybackStarted`. */\nexport interface PipelinePlaybackStartedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineError`. */\nexport interface PipelineErrorEvent {\n code: string;\n message: string;\n}\n\n/** Payload for `PipelineZombieDetected`. */\nexport interface PipelineZombieDetectedEvent {\n playbackHead: number;\n stalledMs: number;\n}\n\n/** Payload for `PipelineUnderrun`. */\nexport interface PipelineUnderrunEvent {\n count: number;\n}\n\n/** Payload for `PipelineDrained`. */\nexport interface PipelineDrainedEvent {\n turnId: string;\n}\n\n/**\n * Payload for `PipelinePlaybackStopped`.\n *\n * Fired when the last sample physically leaves the speaker, approximately\n * `outputLatencyMs` after `PipelineDrained` for the same turn. Pairs with\n * `PipelinePlaybackStarted` (start-of-emission ↔ end-of-emission).\n *\n * Note: this is a physical-world milestone, distinct from `state: 'idle'`\n * (the pipeline-state-machine value reported via `PipelineStateChanged`).\n */\nexport interface PipelinePlaybackStoppedEvent {\n turnId: string;\n}\n\n/** Payload for `PipelineAudioFocusLost` (empty — presence is the signal). */\nexport type PipelineAudioFocusLostEvent = Record<string, never>;\n\n/** Payload for `PipelineAudioFocusResumed` (empty — presence is the signal). */\nexport type PipelineAudioFocusResumedEvent = Record<string, never>;\n\n/** Payload for `PipelineFrequencyBands`. */\nexport interface PipelineFrequencyBandsEvent extends FrequencyBands {}\n\n/**\n * Map of all pipeline event names to their payload types.\n * Used with `Pipeline.subscribe<K>()` for type-safe event subscriptions.\n */\nexport interface PipelineEventMap {\n PipelineStateChanged: PipelineStateChangedEvent;\n PipelinePlaybackStarted: PipelinePlaybackStartedEvent;\n PipelineError: PipelineErrorEvent;\n PipelineZombieDetected: PipelineZombieDetectedEvent;\n PipelineUnderrun: PipelineUnderrunEvent;\n PipelineDrained: PipelineDrainedEvent;\n PipelinePlaybackStopped: PipelinePlaybackStoppedEvent;\n PipelineAudioFocusLost: PipelineAudioFocusLostEvent;\n PipelineAudioFocusResumed: PipelineAudioFocusResumedEvent;\n PipelineFrequencyBands: PipelineFrequencyBandsEvent;\n}\n\n/** Union of all pipeline event name strings. */\nexport type PipelineEventName = keyof PipelineEventMap;\n\n// ── Telemetry ───────────────────────────────────────────────────────────────\n\n/** Jitter buffer telemetry counters. */\nexport interface PipelineBufferTelemetry {\n /** Current buffer level in milliseconds. */\n bufferMs: number;\n /** Current buffer level in samples. */\n bufferSamples: number;\n /** Whether the priming gate has opened. */\n primed: boolean;\n /** Total samples written by the producer since last reset. */\n totalWritten: number;\n /** Total samples read by the consumer since last reset. */\n totalRead: number;\n /** Number of underrun events. */\n underrunCount: number;\n /** Peak buffer level in samples. */\n peakLevel: number;\n}\n\n/** Full pipeline telemetry snapshot. */\nexport interface PipelineTelemetry extends PipelineBufferTelemetry {\n /** Current pipeline state. */\n state: PipelineState;\n /** Total pushAudio/pushAudioSync calls since connect. */\n totalPushCalls: number;\n /** Total bytes pushed since connect. */\n totalPushBytes: number;\n /** Total write-loop iterations since connect. */\n totalWriteLoops: number;\n /** Current turn identifier. */\n turnId: string;\n}\n"]}