@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.
- package/CHANGELOG.md +12 -1
- package/README.md +41 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +130 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1 -0
- package/android/src/main/java/net/siteed/audiostudio/Constants.kt +2 -1
- package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +5 -1
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/AudioStudio.web.js +125 -13
- package/build/cjs/AudioStudio.web.js.map +1 -1
- package/build/cjs/AudioStudioModule.js +6 -1
- package/build/cjs/AudioStudioModule.js.map +1 -1
- package/build/cjs/events.js +4 -0
- package/build/cjs/events.js.map +1 -1
- package/build/cjs/index.js +3 -1
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +187 -30
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/utils/nativeRecordingOptions.js +13 -0
- package/build/cjs/utils/nativeRecordingOptions.js.map +1 -0
- package/build/cjs/utils/nativeRecordingOptions.test.js +30 -0
- package/build/cjs/utils/nativeRecordingOptions.test.js.map +1 -0
- package/build/esm/AudioStudio.types.js.map +1 -1
- package/build/esm/AudioStudio.web.js +125 -13
- package/build/esm/AudioStudio.web.js.map +1 -1
- package/build/esm/AudioStudioModule.js +6 -1
- package/build/esm/AudioStudioModule.js.map +1 -1
- package/build/esm/events.js +3 -0
- package/build/esm/events.js.map +1 -1
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/useAudioRecorder.js +188 -31
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/utils/nativeRecordingOptions.js +10 -0
- package/build/esm/utils/nativeRecordingOptions.js.map +1 -0
- package/build/esm/utils/nativeRecordingOptions.test.js +28 -0
- package/build/esm/utils/nativeRecordingOptions.test.js.map +1 -0
- package/build/types/AudioStudio.types.d.ts +58 -1
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/AudioStudio.web.d.ts +17 -1
- package/build/types/AudioStudio.web.d.ts.map +1 -1
- package/build/types/AudioStudioModule.d.ts.map +1 -1
- package/build/types/events.d.ts +2 -1
- package/build/types/events.d.ts.map +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts +4 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/utils/nativeRecordingOptions.d.ts +28 -0
- package/build/types/utils/nativeRecordingOptions.d.ts.map +1 -0
- package/build/types/utils/nativeRecordingOptions.test.d.ts +2 -0
- package/build/types/utils/nativeRecordingOptions.test.d.ts.map +1 -0
- package/ios/AudioStreamManager.swift +103 -9
- package/ios/AudioStreamManagerDelegate.swift +1 -0
- package/ios/AudioStudioModule.swift +6 -0
- package/ios/RecordingSettings.swift +48 -43
- package/package.json +1 -1
- package/src/AudioStudio.types.ts +70 -1
- package/src/AudioStudio.web.ts +152 -13
- package/src/AudioStudioModule.ts +6 -1
- package/src/events.ts +13 -1
- package/src/index.ts +1 -0
- package/src/useAudioRecorder.tsx +260 -45
- package/src/utils/nativeRecordingOptions.test.ts +29 -0
- package/src/utils/nativeRecordingOptions.ts +20 -0
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -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 {
|
|
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' | '
|
|
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])
|
|
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
|
|
552
|
-
|
|
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
|
|
609
|
-
const cleanOptions =
|
|
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
|
-
|
|
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
|
+
}
|