@siteed/expo-audio-studio 2.12.3 → 2.13.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 (33) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/android/build.gradle +11 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
  6. package/app.plugin.js +3 -1
  7. package/build/cjs/AudioDeviceManager.js +225 -40
  8. package/build/cjs/AudioDeviceManager.js.map +1 -1
  9. package/build/cjs/hooks/useAudioDevices.js +30 -5
  10. package/build/cjs/hooks/useAudioDevices.js.map +1 -1
  11. package/build/cjs/useAudioRecorder.js +52 -8
  12. package/build/cjs/useAudioRecorder.js.map +1 -1
  13. package/build/esm/AudioDeviceManager.js +225 -40
  14. package/build/esm/AudioDeviceManager.js.map +1 -1
  15. package/build/esm/hooks/useAudioDevices.js +31 -6
  16. package/build/esm/hooks/useAudioDevices.js.map +1 -1
  17. package/build/esm/useAudioRecorder.js +53 -9
  18. package/build/esm/useAudioRecorder.js.map +1 -1
  19. package/build/types/AudioDeviceManager.d.ts +78 -2
  20. package/build/types/AudioDeviceManager.d.ts.map +1 -1
  21. package/build/types/hooks/useAudioDevices.d.ts +1 -0
  22. package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
  23. package/build/types/useAudioRecorder.d.ts.map +1 -1
  24. package/ios/AudioDeviceManager.swift +21 -9
  25. package/ios/ExpoAudioStreamModule.swift +33 -1
  26. package/package.json +8 -6
  27. package/plugin/build/index.cjs +194 -0
  28. package/plugin/build/index.d.cts +1 -0
  29. package/plugin/build/index.js +7 -6
  30. package/plugin/src/index.ts +8 -8
  31. package/src/AudioDeviceManager.ts +286 -59
  32. package/src/hooks/useAudioDevices.ts +39 -6
  33. package/src/useAudioRecorder.tsx +102 -9
@@ -1,8 +1,9 @@
1
1
  // src/useAudioRecorder.ts
2
2
  import { EventSubscription, Platform } from 'expo-modules-core'
3
- import { useCallback, useEffect, useReducer, useRef } from 'react'
3
+ import { useCallback, useEffect, useReducer, useRef, useId } from 'react'
4
4
 
5
5
  import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
6
+ import { audioDeviceManager } from './AudioDeviceManager'
6
7
  import {
7
8
  AudioDataEvent,
8
9
  AudioRecording,
@@ -157,6 +158,10 @@ export function useAudioRecorder({
157
158
  audioWorkletUrl,
158
159
  featuresExtratorUrl,
159
160
  }: UseAudioRecorderProps = {}): UseAudioRecorderState {
161
+ // Initialize AudioDeviceManager with logger (once)
162
+ if (logger) {
163
+ audioDeviceManager.setLogger(logger)
164
+ }
160
165
  const [state, dispatch] = useReducer(audioRecorderReducer, {
161
166
  isRecording: false,
162
167
  isPaused: false,
@@ -200,6 +205,9 @@ export function useAudioRecorder({
200
205
 
201
206
  const recordingConfigRef = useRef<RecordingConfig | null>(null)
202
207
 
208
+ // Generate unique instance ID for debugging
209
+ const instanceId = useId().replace(/:/g, '').slice(0, 5)
210
+
203
211
  const handleAudioAnalysis = useCallback(
204
212
  async ({
205
213
  analysis,
@@ -469,7 +477,12 @@ export function useAudioRecorder({
469
477
 
470
478
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
471
479
  fullAnalysisRef.current = { ...defaultAnalysis }
472
- const { onAudioStream, ...options } = recordingOptions
480
+ const {
481
+ onAudioStream,
482
+ onRecordingInterrupted,
483
+ onAudioAnalysis,
484
+ ...options
485
+ } = recordingOptions
473
486
  const { enableProcessing } = options
474
487
 
475
488
  const maxRecentDataDuration = 10000 // TODO compute maxRecentDataDuration based on screen dimensions
@@ -518,7 +531,12 @@ export function useAudioRecorder({
518
531
 
519
532
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
520
533
  fullAnalysisRef.current = { ...defaultAnalysis }
521
- const { onAudioStream, ...options } = recordingOptions
534
+ const {
535
+ onAudioStream,
536
+ onRecordingInterrupted,
537
+ onAudioAnalysis,
538
+ ...options
539
+ } = recordingOptions
522
540
 
523
541
  // Store onAudioStream for later use when recording starts
524
542
  if (typeof onAudioStream === 'function') {
@@ -546,6 +564,8 @@ export function useAudioRecorder({
546
564
  analysisListenerRef.current = null
547
565
  }
548
566
  onAudioStreamRef.current = null
567
+
568
+ // Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
549
569
  logger?.debug(`recording stopped`, stopResult)
550
570
  dispatch({ type: 'STOP' })
551
571
  return stopResult
@@ -603,31 +623,104 @@ export function useAudioRecorder({
603
623
 
604
624
  useEffect(() => {
605
625
  // Add event subscription for recording interruptions
606
- logger?.debug('Setting up recording interruption listener')
626
+ logger?.debug(
627
+ `Setting up recording interruption listener [${instanceId}]`
628
+ )
607
629
 
608
630
  const subscription = addRecordingInterruptionListener((event) => {
609
- logger?.debug('Received recording interruption event:', event)
631
+ logger?.debug(
632
+ `[${instanceId}] Received recording interruption event:`,
633
+ event
634
+ )
635
+
636
+ // Handle device disconnection for UI updates
637
+ if (event.reason === 'deviceDisconnected') {
638
+ logger?.debug(
639
+ `[${instanceId}] Device disconnected - temporarily hiding last device from UI`
640
+ )
641
+
642
+ // Get current device list before the native layer updates
643
+ const currentDevices = audioDeviceManager.getRawDevices()
644
+
645
+ // Wait a moment for native layer to update, then compare
646
+ setTimeout(async () => {
647
+ try {
648
+ // Get updated devices without notifying yet
649
+ const updatedDevices =
650
+ await audioDeviceManager.getAvailableDevices({
651
+ refresh: true,
652
+ })
653
+
654
+ // Find missing devices by comparing lists
655
+ const missingDevices = currentDevices.filter(
656
+ (oldDevice) =>
657
+ !updatedDevices.some(
658
+ (newDevice) => newDevice.id === oldDevice.id
659
+ )
660
+ )
661
+
662
+ if (missingDevices.length > 0) {
663
+ // Mark all missing devices as disconnected (silently)
664
+ missingDevices.forEach((missingDevice) => {
665
+ logger?.debug(
666
+ `[${instanceId}] Confirmed disconnected device: ${missingDevice.name} (${missingDevice.id})`
667
+ )
668
+ audioDeviceManager.markDeviceAsDisconnected(
669
+ missingDevice.id,
670
+ false
671
+ )
672
+ })
673
+ }
674
+
675
+ // Notify listeners once with the final filtered state
676
+ audioDeviceManager.notifyListeners()
677
+ } catch (error) {
678
+ logger?.warn(
679
+ `[${instanceId}] Error in delayed device disconnection handling:`,
680
+ error
681
+ )
682
+ }
683
+ }, 500) // 500ms delay to let native layer update
684
+ } else if (event.reason === 'deviceConnected') {
685
+ // Device reconnected - force refresh to show it immediately
686
+ logger?.debug(
687
+ `[${instanceId}] Device connected, forcing refresh`
688
+ )
689
+ audioDeviceManager.forceRefreshDevices()
690
+ }
610
691
 
611
692
  // Check if we have a callback configured
693
+ logger?.debug(
694
+ `[${instanceId}] recordingConfigRef.current exists:`,
695
+ !!recordingConfigRef.current
696
+ )
697
+
612
698
  if (recordingConfigRef.current?.onRecordingInterrupted) {
613
699
  try {
700
+ logger?.debug(
701
+ `[${instanceId}] Calling recording interruption callback`
702
+ )
614
703
  recordingConfigRef.current.onRecordingInterrupted(event)
615
704
  } catch (error) {
616
705
  logger?.error(
617
- 'Error in recording interruption callback:',
706
+ `[${instanceId}] Error in recording interruption callback:`,
618
707
  error
619
708
  )
620
709
  }
621
710
  } else {
622
- logger?.debug('No recording interruption callback configured')
711
+ logger?.debug(
712
+ `[${instanceId}] No recording interruption callback configured`
713
+ )
623
714
  }
624
715
  })
625
716
 
626
717
  return () => {
627
- logger?.debug('Removing recording interruption listener')
718
+ logger?.debug(
719
+ `[${instanceId}] Removing recording interruption listener`
720
+ )
628
721
  subscription.remove()
629
722
  }
630
- }, []) // Empty dependency array since we want this to run once
723
+ }, [instanceId, logger]) // Include instanceId and logger in dependencies
631
724
 
632
725
  return {
633
726
  prepareRecording,