@siteed/audio-studio 3.2.0 โ†’ 3.2.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +30 -1
  2. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +142 -12
  3. package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +1 -1
  4. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +5 -4
  5. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
  6. package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +1 -1
  7. package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
  8. package/build/cjs/AudioRecorder.provider.js +3 -37
  9. package/build/cjs/AudioRecorder.provider.js.map +1 -1
  10. package/build/cjs/AudioStudio.types.js.map +1 -1
  11. package/build/cjs/AudioStudio.web.js +125 -13
  12. package/build/cjs/AudioStudio.web.js.map +1 -1
  13. package/build/cjs/AudioStudioModule.js +6 -1
  14. package/build/cjs/AudioStudioModule.js.map +1 -1
  15. package/build/cjs/events.js +4 -0
  16. package/build/cjs/events.js.map +1 -1
  17. package/build/cjs/index.js +3 -1
  18. package/build/cjs/index.js.map +1 -1
  19. package/build/cjs/useAudioRecorder.js +139 -4
  20. package/build/cjs/useAudioRecorder.js.map +1 -1
  21. package/build/esm/AudioRecorder.provider.js +3 -4
  22. package/build/esm/AudioRecorder.provider.js.map +1 -1
  23. package/build/esm/AudioStudio.types.js.map +1 -1
  24. package/build/esm/AudioStudio.web.js +125 -13
  25. package/build/esm/AudioStudio.web.js.map +1 -1
  26. package/build/esm/AudioStudioModule.js +6 -1
  27. package/build/esm/AudioStudioModule.js.map +1 -1
  28. package/build/esm/events.js +3 -0
  29. package/build/esm/events.js.map +1 -1
  30. package/build/esm/index.js +1 -0
  31. package/build/esm/index.js.map +1 -1
  32. package/build/esm/useAudioRecorder.js +140 -5
  33. package/build/esm/useAudioRecorder.js.map +1 -1
  34. package/build/types/AudioStudio.types.d.ts +44 -1
  35. package/build/types/AudioStudio.types.d.ts.map +1 -1
  36. package/build/types/AudioStudio.web.d.ts +17 -1
  37. package/build/types/AudioStudio.web.d.ts.map +1 -1
  38. package/build/types/AudioStudioModule.d.ts.map +1 -1
  39. package/build/types/events.d.ts +2 -1
  40. package/build/types/events.d.ts.map +1 -1
  41. package/build/types/index.d.ts +1 -0
  42. package/build/types/index.d.ts.map +1 -1
  43. package/build/types/useAudioRecorder.d.ts +2 -0
  44. package/build/types/useAudioRecorder.d.ts.map +1 -1
  45. package/ios/AudioStreamManager.swift +103 -9
  46. package/ios/AudioStreamManagerDelegate.swift +1 -0
  47. package/ios/AudioStudio.podspec +1 -1
  48. package/ios/AudioStudioModule.swift +6 -0
  49. package/ios/RecordingSettings.swift +48 -43
  50. package/package.json +163 -163
  51. package/plugin/tsconfig.json +8 -2
  52. package/src/AudioStudio.types.ts +48 -1
  53. package/src/AudioStudio.web.ts +152 -13
  54. package/src/AudioStudioModule.ts +6 -1
  55. package/src/events.ts +13 -1
  56. package/src/index.ts +1 -0
  57. package/src/useAudioRecorder.tsx +182 -2
  58. package/scripts/README.md +0 -58
@@ -10,6 +10,7 @@ import {
10
10
  AudioStreamStatus,
11
11
  CompressionInfo,
12
12
  ConsoleLike,
13
+ MaxDurationReachedEvent,
13
14
  RecordingConfig,
14
15
  StartRecordingResult,
15
16
  } from './AudioStudio.types'
@@ -19,6 +20,7 @@ import {
19
20
  addAudioAnalysisListener,
20
21
  addAudioEventListener,
21
22
  AudioEventPayload,
23
+ addMaxDurationReachedListener,
22
24
  addRecordingInterruptionListener,
23
25
  } from './events'
24
26
  import { cleanNativeOptions } from './utils/cleanNativeOptions'
@@ -41,6 +43,8 @@ export interface UseAudioRecorderState {
41
43
  size: number
42
44
  compression?: CompressionInfo
43
45
  analysisData?: AudioAnalysis
46
+ maxDurationMs?: number
47
+ maxDurationReached?: boolean
44
48
  }
45
49
 
46
50
  interface RecorderReducerState {
@@ -50,6 +54,8 @@ interface RecorderReducerState {
50
54
  size: number
51
55
  compression?: CompressionInfo
52
56
  analysisData?: AudioAnalysis
57
+ maxDurationMs?: number
58
+ maxDurationReached?: boolean
53
59
  }
54
60
 
55
61
  type RecorderAction =
@@ -67,8 +73,14 @@ type RecorderAction =
67
73
  durationMs: number
68
74
  size: number
69
75
  compression?: CompressionInfo
76
+ maxDurationMs?: number
77
+ maxDurationReached?: boolean
70
78
  }
71
79
  }
80
+ | {
81
+ type: 'MAX_DURATION_REACHED'
82
+ payload: MaxDurationReachedEvent
83
+ }
72
84
  | { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
73
85
 
74
86
  const defaultAnalysis: AudioAnalysis = {
@@ -104,6 +116,8 @@ function audioRecorderReducer(
104
116
  size: 0,
105
117
  compression: undefined,
106
118
  analysisData: defaultAnalysis,
119
+ maxDurationMs: undefined,
120
+ maxDurationReached: false,
107
121
  }
108
122
  case 'STOP':
109
123
  return {
@@ -114,6 +128,8 @@ function audioRecorderReducer(
114
128
  size: 0,
115
129
  compression: undefined,
116
130
  analysisData: undefined,
131
+ // Preserve max-duration state after stop so UI and agentic
132
+ // validation can explain why recording ended. START resets it.
117
133
  }
118
134
  case 'PAUSE':
119
135
  return { ...state, isPaused: true, isRecording: false }
@@ -138,9 +154,17 @@ function audioRecorderReducer(
138
154
  format: action.payload.compression.format,
139
155
  }
140
156
  : undefined,
157
+ maxDurationMs: action.payload.maxDurationMs,
158
+ maxDurationReached: action.payload.maxDurationReached,
141
159
  }
142
160
  return newState
143
161
  }
162
+ case 'MAX_DURATION_REACHED':
163
+ return {
164
+ ...state,
165
+ maxDurationMs: action.payload.maxDurationMs,
166
+ maxDurationReached: true,
167
+ }
144
168
  case 'UPDATE_ANALYSIS':
145
169
  return {
146
170
  ...state,
@@ -176,6 +200,8 @@ export function useAudioRecorder({
176
200
  size: 0,
177
201
  compression: undefined,
178
202
  analysisData: undefined,
203
+ maxDurationMs: undefined,
204
+ maxDurationReached: false,
179
205
  })
180
206
 
181
207
  const startResultRef = useRef<StartRecordingResult | null>(null)
@@ -208,9 +234,12 @@ export function useAudioRecorder({
208
234
  durationMs: 0,
209
235
  size: 0,
210
236
  compression: undefined as CompressionInfo | undefined,
237
+ maxDurationMs: undefined as number | undefined,
238
+ maxDurationReached: false,
211
239
  })
212
240
 
213
241
  const recordingConfigRef = useRef<RecordingConfig | null>(null)
242
+ const maxDurationHandledRef = useRef(false)
214
243
 
215
244
  // Generate unique instance ID for debugging
216
245
  const instanceId = useId().replace(/:/g, '').slice(0, 5)
@@ -442,6 +471,113 @@ export function useAudioRecorder({
442
471
  []
443
472
  )
444
473
 
474
+ const handleMaxDurationReached = useCallback(
475
+ async (event: MaxDurationReachedEvent) => {
476
+ if (maxDurationHandledRef.current) {
477
+ return
478
+ }
479
+
480
+ maxDurationHandledRef.current = true
481
+ const config = recordingConfigRef.current
482
+ const callbackEvent: MaxDurationReachedEvent = {
483
+ ...event,
484
+ autoStopped:
485
+ event.autoStopped || !!config?.autoStopOnMaxDuration,
486
+ }
487
+
488
+ stateRef.current.maxDurationMs = callbackEvent.maxDurationMs
489
+ stateRef.current.maxDurationReached = true
490
+ dispatch({
491
+ type: 'MAX_DURATION_REACHED',
492
+ payload: callbackEvent,
493
+ })
494
+
495
+ try {
496
+ config?.onMaxDurationReached?.(callbackEvent)
497
+ } catch (error) {
498
+ logger?.error(`Error in max duration callback:`, error)
499
+ }
500
+
501
+ const finishStoppedState = () => {
502
+ if (analysisListenerRef.current) {
503
+ analysisListenerRef.current.remove()
504
+ analysisListenerRef.current = null
505
+ }
506
+ onAudioStreamRef.current = null
507
+ stateRef.current.isRecording = false
508
+ stateRef.current.isPaused = false
509
+ dispatch({ type: 'STOP' })
510
+ }
511
+
512
+ const waitForPlatformAutoStop = async () => {
513
+ const timeoutMs = 3000
514
+ const startedAt = Date.now()
515
+ let lastStatus: AudioStreamStatus | undefined
516
+
517
+ while (Date.now() - startedAt < timeoutMs) {
518
+ await new Promise((resolve) => setTimeout(resolve, 50))
519
+ try {
520
+ const currentStatus: AudioStreamStatus =
521
+ audioStudio.status()
522
+ lastStatus = currentStatus
523
+ if (
524
+ !currentStatus.isRecording &&
525
+ !currentStatus.isPaused
526
+ ) {
527
+ finishStoppedState()
528
+ return
529
+ }
530
+ } catch (error) {
531
+ logger?.warn(
532
+ `Error checking status after max duration auto-stop:`,
533
+ error
534
+ )
535
+ break
536
+ }
537
+ }
538
+
539
+ if (
540
+ lastStatus &&
541
+ (lastStatus.isRecording || lastStatus.isPaused)
542
+ ) {
543
+ try {
544
+ await audioStudio.stopRecording()
545
+ } catch (error) {
546
+ logger?.warn(
547
+ `Error completing max duration auto-stop fallback:`,
548
+ error
549
+ )
550
+ }
551
+ }
552
+ // At this point platform-owned auto-stop did not settle cleanly.
553
+ // Clear hook state so the UI does not stay stuck as recording.
554
+ finishStoppedState()
555
+ }
556
+
557
+ // Only the original event tells us whether the platform already
558
+ // owns auto-stop. Keep stream callbacks alive until status confirms
559
+ // stop completion so native final audio flushes can still reach JS.
560
+ if (event.autoStopped && stateRef.current.isRecording) {
561
+ await waitForPlatformAutoStop()
562
+ return
563
+ }
564
+
565
+ if (
566
+ config?.autoStopOnMaxDuration &&
567
+ !event.autoStopped &&
568
+ stateRef.current.isRecording
569
+ ) {
570
+ try {
571
+ await audioStudio.stopRecording()
572
+ finishStoppedState()
573
+ } catch (error) {
574
+ logger?.error(`Error auto-stopping on max duration:`, error)
575
+ }
576
+ }
577
+ },
578
+ [audioStudio, dispatch, logger]
579
+ )
580
+
445
581
  const checkStatus = useCallback(async () => {
446
582
  try {
447
583
  const status: AudioStreamStatus = audioStudio.status()
@@ -450,6 +586,22 @@ export function useAudioRecorder({
450
586
  status.compression
451
587
  )
452
588
 
589
+ if (
590
+ status.maxDurationReached === true &&
591
+ status.maxDurationMs != null &&
592
+ !stateRef.current.maxDurationReached
593
+ ) {
594
+ await handleMaxDurationReached({
595
+ durationMs: status.durationMs,
596
+ maxDurationMs: status.maxDurationMs,
597
+ overrunMs: Math.max(
598
+ 0,
599
+ status.durationMs - status.maxDurationMs
600
+ ),
601
+ autoStopped: false,
602
+ })
603
+ }
604
+
453
605
  // Only dispatch if values actually changed
454
606
  if (
455
607
  status.isRecording !== stateRef.current.isRecording ||
@@ -468,24 +620,32 @@ export function useAudioRecorder({
468
620
 
469
621
  if (
470
622
  status.durationMs !== stateRef.current.durationMs ||
471
- status.size !== stateRef.current.size
623
+ status.size !== stateRef.current.size ||
624
+ status.maxDurationMs !== stateRef.current.maxDurationMs ||
625
+ status.maxDurationReached !==
626
+ stateRef.current.maxDurationReached
472
627
  ) {
473
628
  stateRef.current.durationMs = status.durationMs
474
629
  stateRef.current.size = status.size
475
630
  stateRef.current.compression = status.compression
631
+ stateRef.current.maxDurationMs = status.maxDurationMs
632
+ stateRef.current.maxDurationReached =
633
+ status.maxDurationReached ?? false
476
634
  dispatch({
477
635
  type: 'UPDATE_STATUS',
478
636
  payload: {
479
637
  durationMs: status.durationMs,
480
638
  size: status.size,
481
639
  compression: status.compression,
640
+ maxDurationMs: status.maxDurationMs,
641
+ maxDurationReached: status.maxDurationReached,
482
642
  },
483
643
  })
484
644
  }
485
645
  } catch (error) {
486
646
  logger?.error(`Error getting status:`, error)
487
647
  }
488
- }, [audioStudio, logger]) // Only depend on audioStudio and logger
648
+ }, [audioStudio, handleMaxDurationReached, logger])
489
649
 
490
650
  // Update ref when state changes
491
651
  useEffect(() => {
@@ -495,6 +655,8 @@ export function useAudioRecorder({
495
655
  durationMs: state.durationMs,
496
656
  size: state.size,
497
657
  compression: state.compression,
658
+ maxDurationMs: state.maxDurationMs,
659
+ maxDurationReached: state.maxDurationReached ?? false,
498
660
  }
499
661
  }, [
500
662
  state.isRecording,
@@ -502,6 +664,8 @@ export function useAudioRecorder({
502
664
  state.durationMs,
503
665
  state.size,
504
666
  state.compression,
667
+ state.maxDurationMs,
668
+ state.maxDurationReached,
505
669
  ])
506
670
 
507
671
  const startRecording = useCallback(
@@ -525,6 +689,7 @@ export function useAudioRecorder({
525
689
  }
526
690
 
527
691
  recordingConfigRef.current = validatedOptions
692
+ maxDurationHandledRef.current = false
528
693
  logger?.debug(
529
694
  `start recording with validated config`,
530
695
  validatedOptions
@@ -535,6 +700,7 @@ export function useAudioRecorder({
535
700
  const {
536
701
  onAudioStream,
537
702
  onRecordingInterrupted,
703
+ onMaxDurationReached,
538
704
  onAudioAnalysis,
539
705
  keepFullAnalysis: _keepFullAnalysis,
540
706
  ...options
@@ -592,6 +758,7 @@ export function useAudioRecorder({
592
758
  const {
593
759
  onAudioStream,
594
760
  onRecordingInterrupted,
761
+ onMaxDurationReached,
595
762
  onAudioAnalysis,
596
763
  keepFullAnalysis: _keepFullAnalysis,
597
764
  ...options
@@ -635,6 +802,7 @@ export function useAudioRecorder({
635
802
 
636
803
  // Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
637
804
  logger?.debug(`recording stopped`, stopResult)
805
+ maxDurationHandledRef.current = false
638
806
  dispatch({ type: 'STOP' })
639
807
  return stopResult
640
808
  }, [dispatch])
@@ -653,6 +821,16 @@ export function useAudioRecorder({
653
821
  return resumeResult
654
822
  }, [dispatch])
655
823
 
824
+ useEffect(() => {
825
+ const subscription = addMaxDurationReachedListener(async (event) => {
826
+ await handleMaxDurationReached(event)
827
+ })
828
+
829
+ return () => {
830
+ subscription.remove()
831
+ }
832
+ }, [handleMaxDurationReached])
833
+
656
834
  useEffect(() => {
657
835
  let intervalId: ReturnType<typeof setInterval> | undefined
658
836
 
@@ -802,5 +980,7 @@ export function useAudioRecorder({
802
980
  size: state.size,
803
981
  compression: state.compression,
804
982
  analysisData: state.analysisData,
983
+ maxDurationMs: state.maxDurationMs,
984
+ maxDurationReached: state.maxDurationReached,
805
985
  }
806
986
  }
package/scripts/README.md DELETED
@@ -1,58 +0,0 @@
1
- # Test Scripts
2
-
3
- This directory contains unified test scripts for expo-audio-studio.
4
-
5
- ## run_tests.sh
6
-
7
- A unified test runner that can execute tests for both Android and iOS platforms.
8
-
9
- ### Usage
10
-
11
- ```bash
12
- ./scripts/run_tests.sh [platform] [type]
13
- ```
14
-
15
- ### Parameters
16
-
17
- - **platform**: Which platform to test
18
- - `all` (default) - Run tests for both platforms
19
- - `android` - Run Android tests only
20
- - `ios` - Run iOS tests only
21
-
22
- - **type**: Which type of tests to run
23
- - `all` (default) - Run all test types
24
- - `unit` - Run unit tests (Android only)
25
- - `instrumented` - Run instrumented tests (Android only)
26
- - `standalone` - Run standalone Swift tests (iOS only)
27
-
28
- ### Examples
29
-
30
- ```bash
31
- # Run all tests for both platforms
32
- ./scripts/run_tests.sh
33
-
34
- # Run Android tests only
35
- ./scripts/run_tests.sh android
36
-
37
- # Run Android unit tests only
38
- ./scripts/run_tests.sh android unit
39
-
40
- # Run iOS tests only
41
- ./scripts/run_tests.sh ios
42
-
43
- # Run all tests explicitly
44
- ./scripts/run_tests.sh all all
45
- ```
46
-
47
- ### Requirements
48
-
49
- - **Android**: Requires Android SDK and a connected device/emulator for instrumented tests
50
- - **iOS**: Requires Swift compiler (comes with Xcode)
51
-
52
- ### Output
53
-
54
- The script provides colored output:
55
- - ๐Ÿงช Test execution progress
56
- - โœ… Success messages in green
57
- - โŒ Failure messages in red
58
- - Summary of tests passed/failed