@siteed/audio-studio 3.2.1-beta.0 → 3.2.1-beta.2

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 (64) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +41 -1
  3. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +130 -0
  4. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1 -0
  5. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
  6. package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
  7. package/build/cjs/AudioStudio.types.js.map +1 -1
  8. package/build/cjs/AudioStudio.web.js +125 -13
  9. package/build/cjs/AudioStudio.web.js.map +1 -1
  10. package/build/cjs/AudioStudioModule.js +6 -1
  11. package/build/cjs/AudioStudioModule.js.map +1 -1
  12. package/build/cjs/events.js +4 -0
  13. package/build/cjs/events.js.map +1 -1
  14. package/build/cjs/index.js +3 -1
  15. package/build/cjs/index.js.map +1 -1
  16. package/build/cjs/useAudioRecorder.js +187 -30
  17. package/build/cjs/useAudioRecorder.js.map +1 -1
  18. package/build/cjs/utils/nativeRecordingOptions.js +13 -0
  19. package/build/cjs/utils/nativeRecordingOptions.js.map +1 -0
  20. package/build/cjs/utils/nativeRecordingOptions.test.js +30 -0
  21. package/build/cjs/utils/nativeRecordingOptions.test.js.map +1 -0
  22. package/build/esm/AudioStudio.types.js.map +1 -1
  23. package/build/esm/AudioStudio.web.js +125 -13
  24. package/build/esm/AudioStudio.web.js.map +1 -1
  25. package/build/esm/AudioStudioModule.js +6 -1
  26. package/build/esm/AudioStudioModule.js.map +1 -1
  27. package/build/esm/events.js +3 -0
  28. package/build/esm/events.js.map +1 -1
  29. package/build/esm/index.js +1 -0
  30. package/build/esm/index.js.map +1 -1
  31. package/build/esm/useAudioRecorder.js +188 -31
  32. package/build/esm/useAudioRecorder.js.map +1 -1
  33. package/build/esm/utils/nativeRecordingOptions.js +10 -0
  34. package/build/esm/utils/nativeRecordingOptions.js.map +1 -0
  35. package/build/esm/utils/nativeRecordingOptions.test.js +28 -0
  36. package/build/esm/utils/nativeRecordingOptions.test.js.map +1 -0
  37. package/build/types/AudioStudio.types.d.ts +58 -1
  38. package/build/types/AudioStudio.types.d.ts.map +1 -1
  39. package/build/types/AudioStudio.web.d.ts +17 -1
  40. package/build/types/AudioStudio.web.d.ts.map +1 -1
  41. package/build/types/AudioStudioModule.d.ts.map +1 -1
  42. package/build/types/events.d.ts +2 -1
  43. package/build/types/events.d.ts.map +1 -1
  44. package/build/types/index.d.ts +1 -0
  45. package/build/types/index.d.ts.map +1 -1
  46. package/build/types/useAudioRecorder.d.ts +4 -1
  47. package/build/types/useAudioRecorder.d.ts.map +1 -1
  48. package/build/types/utils/nativeRecordingOptions.d.ts +28 -0
  49. package/build/types/utils/nativeRecordingOptions.d.ts.map +1 -0
  50. package/build/types/utils/nativeRecordingOptions.test.d.ts +2 -0
  51. package/build/types/utils/nativeRecordingOptions.test.d.ts.map +1 -0
  52. package/ios/AudioStreamManager.swift +103 -9
  53. package/ios/AudioStreamManagerDelegate.swift +1 -0
  54. package/ios/AudioStudioModule.swift +6 -0
  55. package/ios/RecordingSettings.swift +48 -43
  56. package/package.json +1 -1
  57. package/src/AudioStudio.types.ts +70 -1
  58. package/src/AudioStudio.web.ts +152 -13
  59. package/src/AudioStudioModule.ts +6 -1
  60. package/src/events.ts +13 -1
  61. package/src/index.ts +1 -0
  62. package/src/useAudioRecorder.tsx +260 -45
  63. package/src/utils/nativeRecordingOptions.test.ts +29 -0
  64. package/src/utils/nativeRecordingOptions.ts +20 -0
@@ -10,7 +10,9 @@ import {
10
10
  AudioStreamStatus,
11
11
  CompressionInfo,
12
12
  ConsoleLike,
13
+ MaxDurationReachedEvent,
13
14
  RecordingConfig,
15
+ RecordingStopReason,
14
16
  StartRecordingResult,
15
17
  } from './AudioStudio.types'
16
18
  import AudioStudioModule from './AudioStudioModule'
@@ -19,9 +21,10 @@ import {
19
21
  addAudioAnalysisListener,
20
22
  addAudioEventListener,
21
23
  AudioEventPayload,
24
+ addMaxDurationReachedListener,
22
25
  addRecordingInterruptionListener,
23
26
  } from './events'
24
- import { cleanNativeOptions } from './utils/cleanNativeOptions'
27
+ import { createNativeRecordingOptions } from './utils/nativeRecordingOptions'
25
28
 
26
29
  export interface UseAudioRecorderProps {
27
30
  logger?: ConsoleLike
@@ -41,6 +44,9 @@ export interface UseAudioRecorderState {
41
44
  size: number
42
45
  compression?: CompressionInfo
43
46
  analysisData?: AudioAnalysis
47
+ maxDurationMs?: number
48
+ maxDurationReached?: boolean
49
+ lastRecordingReason?: RecordingStopReason
44
50
  }
45
51
 
46
52
  interface RecorderReducerState {
@@ -50,10 +56,19 @@ interface RecorderReducerState {
50
56
  size: number
51
57
  compression?: CompressionInfo
52
58
  analysisData?: AudioAnalysis
59
+ maxDurationMs?: number
60
+ maxDurationReached?: boolean
61
+ lastRecordingReason?: RecordingStopReason
53
62
  }
54
63
 
55
64
  type RecorderAction =
56
- | { type: 'START' | 'STOP' | 'PAUSE' | 'RESUME' }
65
+ | { type: 'START' | 'PAUSE' | 'RESUME' }
66
+ | {
67
+ type: 'STOP'
68
+ payload: {
69
+ reason: RecordingStopReason
70
+ }
71
+ }
57
72
  | {
58
73
  type: 'UPDATE_RECORDING_STATE'
59
74
  payload: {
@@ -67,8 +82,14 @@ type RecorderAction =
67
82
  durationMs: number
68
83
  size: number
69
84
  compression?: CompressionInfo
85
+ maxDurationMs?: number
86
+ maxDurationReached?: boolean
70
87
  }
71
88
  }
89
+ | {
90
+ type: 'MAX_DURATION_REACHED'
91
+ payload: MaxDurationReachedEvent
92
+ }
72
93
  | { type: 'UPDATE_ANALYSIS'; payload: AudioAnalysis }
73
94
 
74
95
  const defaultAnalysis: AudioAnalysis = {
@@ -90,6 +111,42 @@ const defaultAnalysis: AudioAnalysis = {
90
111
  extractionTimeMs: 0,
91
112
  }
92
113
 
114
+ function finiteOrZero(value: number): number {
115
+ return Number.isFinite(value) ? value : 0
116
+ }
117
+
118
+ function sanitizeSerializableValue<T>(value: T): T {
119
+ if (typeof value === 'number') {
120
+ return finiteOrZero(value) as T
121
+ }
122
+
123
+ if (Array.isArray(value)) {
124
+ return value.map((item) => sanitizeSerializableValue(item)) as T
125
+ }
126
+
127
+ if (value && typeof value === 'object') {
128
+ const sanitized: Record<string, unknown> = {}
129
+
130
+ for (const [key, nestedValue] of Object.entries(
131
+ value as Record<string, unknown>
132
+ )) {
133
+ sanitized[key] = sanitizeSerializableValue(nestedValue)
134
+ }
135
+
136
+ return sanitized as T
137
+ }
138
+
139
+ return value
140
+ }
141
+
142
+ function createSerializableAnalysis(analysis: AudioAnalysis): AudioAnalysis {
143
+ return sanitizeSerializableValue(analysis)
144
+ }
145
+
146
+ function createRecordingSnapshot(recording: AudioRecording): AudioRecording {
147
+ return sanitizeSerializableValue(recording)
148
+ }
149
+
93
150
  function audioRecorderReducer(
94
151
  state: RecorderReducerState,
95
152
  action: RecorderAction
@@ -104,6 +161,9 @@ function audioRecorderReducer(
104
161
  size: 0,
105
162
  compression: undefined,
106
163
  analysisData: defaultAnalysis,
164
+ maxDurationMs: undefined,
165
+ maxDurationReached: false,
166
+ lastRecordingReason: undefined,
107
167
  }
108
168
  case 'STOP':
109
169
  return {
@@ -114,6 +174,9 @@ function audioRecorderReducer(
114
174
  size: 0,
115
175
  compression: undefined,
116
176
  analysisData: undefined,
177
+ lastRecordingReason: action.payload.reason,
178
+ // Preserve max-duration state after stop so UI and agentic
179
+ // validation can explain why recording ended. START resets it.
117
180
  }
118
181
  case 'PAUSE':
119
182
  return { ...state, isPaused: true, isRecording: false }
@@ -138,9 +201,17 @@ function audioRecorderReducer(
138
201
  format: action.payload.compression.format,
139
202
  }
140
203
  : undefined,
204
+ maxDurationMs: action.payload.maxDurationMs,
205
+ maxDurationReached: action.payload.maxDurationReached,
141
206
  }
142
207
  return newState
143
208
  }
209
+ case 'MAX_DURATION_REACHED':
210
+ return {
211
+ ...state,
212
+ maxDurationMs: action.payload.maxDurationMs,
213
+ maxDurationReached: true,
214
+ }
144
215
  case 'UPDATE_ANALYSIS':
145
216
  return {
146
217
  ...state,
@@ -176,6 +247,9 @@ export function useAudioRecorder({
176
247
  size: 0,
177
248
  compression: undefined,
178
249
  analysisData: undefined,
250
+ maxDurationMs: undefined,
251
+ maxDurationReached: false,
252
+ lastRecordingReason: undefined,
179
253
  })
180
254
 
181
255
  const startResultRef = useRef<StartRecordingResult | null>(null)
@@ -208,9 +282,13 @@ export function useAudioRecorder({
208
282
  durationMs: 0,
209
283
  size: 0,
210
284
  compression: undefined as CompressionInfo | undefined,
285
+ maxDurationMs: undefined as number | undefined,
286
+ maxDurationReached: false,
211
287
  })
212
288
 
213
289
  const recordingConfigRef = useRef<RecordingConfig | null>(null)
290
+ const maxDurationHandledRef = useRef(false)
291
+ const stopFinalizationRef = useRef<Promise<AudioRecording> | null>(null)
214
292
 
215
293
  // Generate unique instance ID for debugging
216
294
  const instanceId = useId().replace(/:/g, '').slice(0, 5)
@@ -442,6 +520,121 @@ export function useAudioRecorder({
442
520
  []
443
521
  )
444
522
 
523
+ const finalizeRecordingStop = useCallback(
524
+ async (reason: RecordingStopReason) => {
525
+ if (stopFinalizationRef.current) {
526
+ return stopFinalizationRef.current
527
+ }
528
+
529
+ const finalizePromise = (async () => {
530
+ const nativeStopResult: AudioRecording | null =
531
+ await audioStudio.stopRecording()
532
+
533
+ if (!nativeStopResult) {
534
+ throw new Error('Failed to stop recording')
535
+ }
536
+
537
+ const stopResult = createRecordingSnapshot(nativeStopResult)
538
+
539
+ if (shouldKeepFullAnalysis(recordingConfigRef.current)) {
540
+ stopResult.analysisData = createSerializableAnalysis(
541
+ fullAnalysisRef.current
542
+ )
543
+ } else {
544
+ // `keepFullAnalysis` is a hook-level retention policy. If a platform
545
+ // starts returning native analysisData in the future, keep opt-out
546
+ // semantics explicit and avoid leaking a full history here.
547
+ delete stopResult.analysisData
548
+ }
549
+
550
+ if (analysisListenerRef.current) {
551
+ analysisListenerRef.current.remove()
552
+ analysisListenerRef.current = null
553
+ }
554
+ onAudioStreamRef.current = null
555
+
556
+ stateRef.current.isRecording = false
557
+ stateRef.current.isPaused = false
558
+
559
+ // Note: We deliberately DON'T clear recordingConfigRef here to preserve callbacks.
560
+ logger?.debug(`recording stopped`, stopResult)
561
+ maxDurationHandledRef.current = false
562
+ dispatch({
563
+ type: 'STOP',
564
+ payload: { reason },
565
+ })
566
+
567
+ const stoppedCallback =
568
+ recordingConfigRef.current?.onRecordingStopped
569
+ if (stoppedCallback) {
570
+ try {
571
+ void Promise.resolve(
572
+ stoppedCallback(stopResult, reason)
573
+ ).catch((error) => {
574
+ logger?.error(
575
+ `Error in recording stopped callback:`,
576
+ error
577
+ )
578
+ })
579
+ } catch (error) {
580
+ logger?.error(
581
+ `Error in recording stopped callback:`,
582
+ error
583
+ )
584
+ }
585
+ }
586
+
587
+ return stopResult
588
+ })()
589
+
590
+ stopFinalizationRef.current = finalizePromise
591
+ try {
592
+ return await finalizePromise
593
+ } finally {
594
+ stopFinalizationRef.current = null
595
+ }
596
+ },
597
+ [audioStudio, dispatch, logger]
598
+ )
599
+
600
+ const handleMaxDurationReached = useCallback(
601
+ async (event: MaxDurationReachedEvent) => {
602
+ if (maxDurationHandledRef.current) {
603
+ return
604
+ }
605
+
606
+ maxDurationHandledRef.current = true
607
+ const config = recordingConfigRef.current
608
+ const callbackEvent: MaxDurationReachedEvent = {
609
+ ...event,
610
+ autoStopped:
611
+ event.autoStopped || !!config?.autoStopOnMaxDuration,
612
+ }
613
+
614
+ stateRef.current.maxDurationMs = callbackEvent.maxDurationMs
615
+ stateRef.current.maxDurationReached = true
616
+ dispatch({
617
+ type: 'MAX_DURATION_REACHED',
618
+ payload: callbackEvent,
619
+ })
620
+
621
+ try {
622
+ config?.onMaxDurationReached?.(callbackEvent)
623
+ } catch (error) {
624
+ logger?.error(`Error in max duration callback:`, error)
625
+ }
626
+
627
+ if (config?.autoStopOnMaxDuration && stateRef.current.isRecording) {
628
+ try {
629
+ await finalizeRecordingStop('maxDuration')
630
+ } catch (error) {
631
+ logger?.error(`Error auto-stopping on max duration:`, error)
632
+ }
633
+ }
634
+ },
635
+ [dispatch, finalizeRecordingStop, logger]
636
+ )
637
+
445
638
  const checkStatus = useCallback(async () => {
446
639
  try {
447
640
  const status: AudioStreamStatus = audioStudio.status()
@@ -450,6 +643,22 @@ export function useAudioRecorder({
450
643
  status.compression
451
644
  )
452
645
 
646
+ if (
647
+ status.maxDurationReached === true &&
648
+ status.maxDurationMs != null &&
649
+ !stateRef.current.maxDurationReached
650
+ ) {
651
+ await handleMaxDurationReached({
652
+ durationMs: status.durationMs,
653
+ maxDurationMs: status.maxDurationMs,
654
+ overrunMs: Math.max(
655
+ 0,
656
+ status.durationMs - status.maxDurationMs
657
+ ),
658
+ autoStopped: false,
659
+ })
660
+ }
661
+
453
662
  // Only dispatch if values actually changed
454
663
  if (
455
664
  status.isRecording !== stateRef.current.isRecording ||
@@ -466,26 +675,45 @@ export function useAudioRecorder({
466
675
  })
467
676
  }
468
677
 
678
+ const statusMaxDurationReached = status.maxDurationReached ?? false
679
+ const preserveStoppedMaxDuration =
680
+ !status.isRecording &&
681
+ !status.isPaused &&
682
+ stateRef.current.maxDurationReached &&
683
+ !statusMaxDurationReached
684
+ const nextMaxDurationMs = preserveStoppedMaxDuration
685
+ ? stateRef.current.maxDurationMs
686
+ : status.maxDurationMs
687
+ const nextMaxDurationReached = preserveStoppedMaxDuration
688
+ ? true
689
+ : statusMaxDurationReached
690
+
469
691
  if (
470
692
  status.durationMs !== stateRef.current.durationMs ||
471
- status.size !== stateRef.current.size
693
+ status.size !== stateRef.current.size ||
694
+ nextMaxDurationMs !== stateRef.current.maxDurationMs ||
695
+ nextMaxDurationReached !== stateRef.current.maxDurationReached
472
696
  ) {
473
697
  stateRef.current.durationMs = status.durationMs
474
698
  stateRef.current.size = status.size
475
699
  stateRef.current.compression = status.compression
700
+ stateRef.current.maxDurationMs = nextMaxDurationMs
701
+ stateRef.current.maxDurationReached = nextMaxDurationReached
476
702
  dispatch({
477
703
  type: 'UPDATE_STATUS',
478
704
  payload: {
479
705
  durationMs: status.durationMs,
480
706
  size: status.size,
481
707
  compression: status.compression,
708
+ maxDurationMs: nextMaxDurationMs,
709
+ maxDurationReached: nextMaxDurationReached,
482
710
  },
483
711
  })
484
712
  }
485
713
  } catch (error) {
486
714
  logger?.error(`Error getting status:`, error)
487
715
  }
488
- }, [audioStudio, logger]) // Only depend on audioStudio and logger
716
+ }, [audioStudio, handleMaxDurationReached, logger])
489
717
 
490
718
  // Update ref when state changes
491
719
  useEffect(() => {
@@ -495,6 +723,8 @@ export function useAudioRecorder({
495
723
  durationMs: state.durationMs,
496
724
  size: state.size,
497
725
  compression: state.compression,
726
+ maxDurationMs: state.maxDurationMs,
727
+ maxDurationReached: state.maxDurationReached ?? false,
498
728
  }
499
729
  }, [
500
730
  state.isRecording,
@@ -502,6 +732,8 @@ export function useAudioRecorder({
502
732
  state.durationMs,
503
733
  state.size,
504
734
  state.compression,
735
+ state.maxDurationMs,
736
+ state.maxDurationReached,
505
737
  ])
506
738
 
507
739
  const startRecording = useCallback(
@@ -525,6 +757,7 @@ export function useAudioRecorder({
525
757
  }
526
758
 
527
759
  recordingConfigRef.current = validatedOptions
760
+ maxDurationHandledRef.current = false
528
761
  logger?.debug(
529
762
  `start recording with validated config`,
530
763
  validatedOptions
@@ -532,14 +765,7 @@ export function useAudioRecorder({
532
765
 
533
766
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
534
767
  fullAnalysisRef.current = { ...defaultAnalysis }
535
- const {
536
- onAudioStream,
537
- onRecordingInterrupted,
538
- onAudioAnalysis,
539
- keepFullAnalysis: _keepFullAnalysis,
540
- ...options
541
- } = validatedOptions
542
- const { enableProcessing } = options
768
+ const { onAudioStream, enableProcessing } = validatedOptions
543
769
 
544
770
  const maxRecentDataDuration = 10000 // TODO compute maxRecentDataDuration based on screen dimensions
545
771
  if (typeof onAudioStream === 'function') {
@@ -548,8 +774,10 @@ export function useAudioRecorder({
548
774
  logger?.warn(`onAudioStream is not a function`, onAudioStream)
549
775
  onAudioStreamRef.current = null
550
776
  }
551
- // Strip undefined values and functions that can't cross the native bridge
552
- const cleanOptions = cleanNativeOptions(options)
777
+ // Strip hook-only values and undefineds that can't cross the native bridge.
778
+ // autoStopOnMaxDuration stays hook-owned so finalization can expose
779
+ // the same AudioRecording result as a manual stop.
780
+ const cleanOptions = createNativeRecordingOptions(validatedOptions)
553
781
  const startResult: StartRecordingResult =
554
782
  await audioStudio.startRecording(cleanOptions)
555
783
  dispatch({ type: 'START' })
@@ -589,13 +817,7 @@ export function useAudioRecorder({
589
817
 
590
818
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
591
819
  fullAnalysisRef.current = { ...defaultAnalysis }
592
- const {
593
- onAudioStream,
594
- onRecordingInterrupted,
595
- onAudioAnalysis,
596
- keepFullAnalysis: _keepFullAnalysis,
597
- ...options
598
- } = recordingOptions
820
+ const { onAudioStream } = recordingOptions
599
821
 
600
822
  // Store onAudioStream for later use when recording starts
601
823
  if (typeof onAudioStream === 'function') {
@@ -605,8 +827,8 @@ export function useAudioRecorder({
605
827
  onAudioStreamRef.current = null
606
828
  }
607
829
 
608
- // Strip undefined values and functions that can't cross the native bridge
609
- const cleanOptions = cleanNativeOptions(options)
830
+ // Strip hook-only values and undefineds that can't cross the native bridge.
831
+ const cleanOptions = createNativeRecordingOptions(recordingOptions)
610
832
  // Call the native prepareRecording method
611
833
  await audioStudio.prepareRecording(cleanOptions)
612
834
  logger?.debug(`recording prepared successfully`)
@@ -616,28 +838,8 @@ export function useAudioRecorder({
616
838
 
617
839
  const stopRecording = useCallback(async () => {
618
840
  logger?.debug(`stoping recording`)
619
-
620
- const stopResult: AudioRecording = await audioStudio.stopRecording()
621
- if (shouldKeepFullAnalysis(recordingConfigRef.current)) {
622
- stopResult.analysisData = fullAnalysisRef.current
623
- } else {
624
- // `keepFullAnalysis` is a hook-level retention policy. If a platform
625
- // starts returning native analysisData in the future, keep opt-out
626
- // semantics explicit and avoid leaking a full history here.
627
- delete stopResult.analysisData
628
- }
629
-
630
- if (analysisListenerRef.current) {
631
- analysisListenerRef.current.remove()
632
- analysisListenerRef.current = null
633
- }
634
- onAudioStreamRef.current = null
635
-
636
- // Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
637
- logger?.debug(`recording stopped`, stopResult)
638
- dispatch({ type: 'STOP' })
639
- return stopResult
640
- }, [dispatch])
841
+ return finalizeRecordingStop('manual')
842
+ }, [finalizeRecordingStop, logger])
641
843
 
642
844
  const pauseRecording = useCallback(async () => {
643
845
  logger?.debug(`pause recording`)
@@ -653,6 +855,16 @@ export function useAudioRecorder({
653
855
  return resumeResult
654
856
  }, [dispatch])
655
857
 
858
+ useEffect(() => {
859
+ const subscription = addMaxDurationReachedListener(async (event) => {
860
+ await handleMaxDurationReached(event)
861
+ })
862
+
863
+ return () => {
864
+ subscription.remove()
865
+ }
866
+ }, [handleMaxDurationReached])
867
+
656
868
  useEffect(() => {
657
869
  let intervalId: ReturnType<typeof setInterval> | undefined
658
870
 
@@ -802,5 +1014,8 @@ export function useAudioRecorder({
802
1014
  size: state.size,
803
1015
  compression: state.compression,
804
1016
  analysisData: state.analysisData,
1017
+ maxDurationMs: state.maxDurationMs,
1018
+ maxDurationReached: state.maxDurationReached,
1019
+ lastRecordingReason: state.lastRecordingReason,
805
1020
  }
806
1021
  }
@@ -0,0 +1,29 @@
1
+ import { createNativeRecordingOptions } from './nativeRecordingOptions'
2
+
3
+ describe('createNativeRecordingOptions', () => {
4
+ it('keeps maxDurationMs native but leaves auto-stop finalization hook-owned', () => {
5
+ const nativeOptions = createNativeRecordingOptions({
6
+ maxDurationMs: 1500,
7
+ autoStopOnMaxDuration: true,
8
+ onMaxDurationReached: jest.fn(),
9
+ onRecordingStopped: jest.fn(),
10
+ onRecordingInterrupted: jest.fn(),
11
+ onAudioAnalysis: jest.fn(),
12
+ onAudioStream: jest.fn(),
13
+ keepFullAnalysis: true,
14
+ sampleRate: 16000,
15
+ })
16
+
17
+ expect(nativeOptions).toMatchObject({
18
+ maxDurationMs: 1500,
19
+ sampleRate: 16000,
20
+ })
21
+ expect(nativeOptions).not.toHaveProperty('autoStopOnMaxDuration')
22
+ expect(nativeOptions).not.toHaveProperty('onMaxDurationReached')
23
+ expect(nativeOptions).not.toHaveProperty('onRecordingStopped')
24
+ expect(nativeOptions).not.toHaveProperty('onRecordingInterrupted')
25
+ expect(nativeOptions).not.toHaveProperty('onAudioAnalysis')
26
+ expect(nativeOptions).not.toHaveProperty('onAudioStream')
27
+ expect(nativeOptions).not.toHaveProperty('keepFullAnalysis')
28
+ })
29
+ })
@@ -0,0 +1,20 @@
1
+ import { RecordingConfig } from '../AudioStudio.types'
2
+ import { cleanNativeOptions } from './cleanNativeOptions'
3
+
4
+ export function createNativeRecordingOptions(recordingOptions: RecordingConfig) {
5
+ const {
6
+ onAudioStream,
7
+ onRecordingInterrupted,
8
+ onMaxDurationReached,
9
+ onRecordingStopped,
10
+ onAudioAnalysis,
11
+ keepFullAnalysis: _keepFullAnalysis,
12
+ // Keep hook-owned auto-stop out of the native bridge so the hook can
13
+ // reuse the same finalization path as manual stop and expose the
14
+ // resulting AudioRecording consistently.
15
+ autoStopOnMaxDuration: _autoStopOnMaxDuration,
16
+ ...options
17
+ } = recordingOptions
18
+
19
+ return cleanNativeOptions(options)
20
+ }