@siteed/audio-studio 3.2.1-beta.1 → 3.2.1-beta.3
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 +22 -1
- package/README.md +12 -1
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +115 -93
- 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/useAudioRecorder.js +115 -93
- 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 +19 -5
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts +2 -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/package.json +2 -1
- package/plugin/build/index.cjs +79 -47
- package/plugin/build/index.d.cts +15 -0
- package/plugin/build/index.js +79 -47
- package/plugin/src/index.test.ts +78 -0
- package/plugin/src/index.ts +141 -59
- package/plugin/tsconfig.json +6 -1
- package/src/AudioStudio.types.ts +27 -5
- package/src/useAudioRecorder.tsx +161 -126
- package/src/utils/nativeRecordingOptions.test.ts +29 -0
- package/src/utils/nativeRecordingOptions.ts +20 -0
package/plugin/src/index.ts
CHANGED
|
@@ -10,6 +10,138 @@ const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'
|
|
|
10
10
|
const NOTIFICATION_USAGE = 'Show recording notifications and controls'
|
|
11
11
|
const LOG_PREFIX = '[@siteed/expo-audio-studio]'
|
|
12
12
|
|
|
13
|
+
const AUDIO_STUDIO_ANDROID_PACKAGE = 'net.siteed.audiostudio'
|
|
14
|
+
const RECORDING_ACTION_RECEIVER = `${AUDIO_STUDIO_ANDROID_PACKAGE}.RecordingActionReceiver`
|
|
15
|
+
const AUDIO_RECORDING_SERVICE = `${AUDIO_STUDIO_ANDROID_PACKAGE}.AudioRecordingService`
|
|
16
|
+
const LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER = '.RecordingActionReceiver'
|
|
17
|
+
const LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE = '.AudioRecordingService'
|
|
18
|
+
|
|
19
|
+
type AndroidComponent = {
|
|
20
|
+
$?: Record<string, string | boolean>
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type AndroidApplication = {
|
|
25
|
+
receiver?: AndroidComponent[]
|
|
26
|
+
service?: AndroidComponent[]
|
|
27
|
+
[key: string]: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function removeComponentsByName(
|
|
31
|
+
components: AndroidComponent[] | undefined,
|
|
32
|
+
names: string[]
|
|
33
|
+
): AndroidComponent[] | undefined {
|
|
34
|
+
if (!components) {
|
|
35
|
+
return components
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const filtered = components.filter(
|
|
39
|
+
(component) => !names.includes(String(component.$?.['android:name']))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return filtered.length > 0 ? filtered : undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function upsertComponent(
|
|
46
|
+
components: AndroidComponent[] | undefined,
|
|
47
|
+
componentConfig: AndroidComponent
|
|
48
|
+
): AndroidComponent[] {
|
|
49
|
+
const name = String(componentConfig.$?.['android:name'])
|
|
50
|
+
const nextComponents = (components || []).filter(
|
|
51
|
+
(component) => String(component.$?.['android:name']) !== name
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
nextComponents.push(componentConfig)
|
|
55
|
+
return nextComponents
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function addAndroidRemovalMarker(
|
|
59
|
+
components: AndroidComponent[] | undefined,
|
|
60
|
+
componentName: string
|
|
61
|
+
): AndroidComponent[] {
|
|
62
|
+
return upsertComponent(components, {
|
|
63
|
+
$: {
|
|
64
|
+
'android:name': componentName,
|
|
65
|
+
'tools:node': 'remove',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configureAndroidBackgroundRecordingComponents(
|
|
71
|
+
mainApplication: AndroidApplication,
|
|
72
|
+
enableBackgroundAudio: boolean | undefined
|
|
73
|
+
): void {
|
|
74
|
+
const receiverNames = [
|
|
75
|
+
LEGACY_RELATIVE_RECORDING_ACTION_RECEIVER,
|
|
76
|
+
RECORDING_ACTION_RECEIVER,
|
|
77
|
+
]
|
|
78
|
+
const serviceNames = [
|
|
79
|
+
LEGACY_RELATIVE_AUDIO_RECORDING_SERVICE,
|
|
80
|
+
AUDIO_RECORDING_SERVICE,
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
mainApplication.receiver = removeComponentsByName(
|
|
84
|
+
mainApplication.receiver,
|
|
85
|
+
receiverNames
|
|
86
|
+
)
|
|
87
|
+
mainApplication.service = removeComponentsByName(
|
|
88
|
+
mainApplication.service,
|
|
89
|
+
serviceNames
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (enableBackgroundAudio) {
|
|
93
|
+
const receiverConfig = {
|
|
94
|
+
$: {
|
|
95
|
+
'android:name': RECORDING_ACTION_RECEIVER,
|
|
96
|
+
'android:exported': 'false' as const,
|
|
97
|
+
},
|
|
98
|
+
'intent-filter': [
|
|
99
|
+
{
|
|
100
|
+
action: [
|
|
101
|
+
{ $: { 'android:name': 'PAUSE_RECORDING' } },
|
|
102
|
+
{ $: { 'android:name': 'RESUME_RECORDING' } },
|
|
103
|
+
{ $: { 'android:name': 'STOP_RECORDING' } },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const serviceConfig = {
|
|
110
|
+
$: {
|
|
111
|
+
'android:name': AUDIO_RECORDING_SERVICE,
|
|
112
|
+
'android:enabled': 'true' as const,
|
|
113
|
+
'android:exported': 'false' as const,
|
|
114
|
+
'android:foregroundServiceType': 'microphone',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
mainApplication.receiver = upsertComponent(
|
|
119
|
+
mainApplication.receiver,
|
|
120
|
+
receiverConfig
|
|
121
|
+
)
|
|
122
|
+
mainApplication.service = upsertComponent(
|
|
123
|
+
mainApplication.service,
|
|
124
|
+
serviceConfig
|
|
125
|
+
)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
mainApplication.receiver = addAndroidRemovalMarker(
|
|
130
|
+
mainApplication.receiver,
|
|
131
|
+
RECORDING_ACTION_RECEIVER
|
|
132
|
+
)
|
|
133
|
+
mainApplication.service = addAndroidRemovalMarker(
|
|
134
|
+
mainApplication.service,
|
|
135
|
+
AUDIO_RECORDING_SERVICE
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const __testing = {
|
|
140
|
+
AUDIO_RECORDING_SERVICE,
|
|
141
|
+
RECORDING_ACTION_RECEIVER,
|
|
142
|
+
configureAndroidBackgroundRecordingComponents,
|
|
143
|
+
}
|
|
144
|
+
|
|
13
145
|
function debugLog(message: string, ...args: unknown[]): void {
|
|
14
146
|
if (process.env.EXPO_DEBUG) {
|
|
15
147
|
console.log(`${LOG_PREFIX} ${message}`, ...args)
|
|
@@ -203,71 +335,21 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
|
|
|
203
335
|
)
|
|
204
336
|
})
|
|
205
337
|
|
|
338
|
+
AndroidConfig.Manifest.ensureToolsAvailable(config.modResults)
|
|
339
|
+
|
|
206
340
|
// Get the main application node
|
|
207
341
|
const mainApplication = config.modResults.manifest.application?.[0]
|
|
208
342
|
if (mainApplication) {
|
|
209
343
|
debugLog('📱 Configuring Android application components...')
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
mainApplication.receiver = []
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const receiverConfig = {
|
|
217
|
-
$: {
|
|
218
|
-
'android:name': '.RecordingActionReceiver',
|
|
219
|
-
'android:exported': 'false' as const,
|
|
220
|
-
},
|
|
221
|
-
'intent-filter': [
|
|
222
|
-
{
|
|
223
|
-
action: [
|
|
224
|
-
{ $: { 'android:name': 'PAUSE_RECORDING' } },
|
|
225
|
-
{ $: { 'android:name': 'RESUME_RECORDING' } },
|
|
226
|
-
{ $: { 'android:name': 'STOP_RECORDING' } },
|
|
227
|
-
],
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const receiverIndex = mainApplication.receiver.findIndex(
|
|
233
|
-
(receiver: any) =>
|
|
234
|
-
receiver.$?.['android:name'] === '.RecordingActionReceiver'
|
|
344
|
+
configureAndroidBackgroundRecordingComponents(
|
|
345
|
+
mainApplication,
|
|
346
|
+
enableBackgroundAudio
|
|
235
347
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mainApplication.receiver.push(receiverConfig)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
debugLog('✅ RecordingActionReceiver configured')
|
|
244
|
-
|
|
245
|
-
// Add AudioRecordingService
|
|
246
|
-
if (!mainApplication.service) {
|
|
247
|
-
mainApplication.service = []
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const serviceConfig = {
|
|
251
|
-
$: {
|
|
252
|
-
'android:name': '.AudioRecordingService',
|
|
253
|
-
'android:enabled': 'true' as const,
|
|
254
|
-
'android:exported': 'false' as const,
|
|
255
|
-
'android:foregroundServiceType': 'microphone',
|
|
256
|
-
},
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const serviceIndex = mainApplication.service.findIndex(
|
|
260
|
-
(service: any) =>
|
|
261
|
-
service.$?.['android:name'] === '.AudioRecordingService'
|
|
348
|
+
debugLog(
|
|
349
|
+
enableBackgroundAudio
|
|
350
|
+
? '✅ Android background recording components configured'
|
|
351
|
+
: '✅ Android background recording components disabled'
|
|
262
352
|
)
|
|
263
|
-
|
|
264
|
-
if (serviceIndex >= 0) {
|
|
265
|
-
mainApplication.service[serviceIndex] = serviceConfig
|
|
266
|
-
} else {
|
|
267
|
-
mainApplication.service.push(serviceConfig)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
debugLog('✅ AudioRecordingService configured')
|
|
271
353
|
} else {
|
|
272
354
|
console.error(
|
|
273
355
|
`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`
|
package/plugin/tsconfig.json
CHANGED
package/src/AudioStudio.types.ts
CHANGED
|
@@ -207,6 +207,8 @@ export interface MaxDurationReachedEvent {
|
|
|
207
207
|
autoStopped: boolean
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
export type RecordingStopReason = 'manual' | 'maxDuration'
|
|
211
|
+
|
|
210
212
|
export interface AudioSessionConfig {
|
|
211
213
|
/**
|
|
212
214
|
* Audio session category that defines the audio behavior
|
|
@@ -511,20 +513,33 @@ export interface RecordingConfig {
|
|
|
511
513
|
/**
|
|
512
514
|
* Stop recording automatically when maxDurationMs is reached.
|
|
513
515
|
*
|
|
514
|
-
* Defaults to false.
|
|
515
|
-
*
|
|
516
|
-
*
|
|
516
|
+
* Defaults to false. When used with `useAudioRecorder`, the
|
|
517
|
+
* MaxDurationReached event is emitted immediately, then the hook stops the
|
|
518
|
+
* recorder and exposes the final result through `onRecordingStopped`.
|
|
517
519
|
*/
|
|
518
520
|
autoStopOnMaxDuration?: boolean
|
|
519
521
|
|
|
520
522
|
/**
|
|
521
523
|
* Optional callback invoked when maxDurationMs is reached.
|
|
522
524
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
+
* This remains an immediate threshold callback. If
|
|
526
|
+
* autoStopOnMaxDuration is true, use `onRecordingStopped` for the full
|
|
527
|
+
* recording result after stop completes.
|
|
525
528
|
*/
|
|
526
529
|
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
527
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Optional callback invoked after a recording has fully stopped and the
|
|
533
|
+
* final `AudioRecording` result is available.
|
|
534
|
+
*
|
|
535
|
+
* The reason is `manual` when stopped through `stopRecording()` and
|
|
536
|
+
* `maxDuration` when stopped by `autoStopOnMaxDuration`.
|
|
537
|
+
*/
|
|
538
|
+
onRecordingStopped?: (
|
|
539
|
+
recording: AudioRecording,
|
|
540
|
+
reason: RecordingStopReason
|
|
541
|
+
) => void | Promise<void>
|
|
542
|
+
|
|
528
543
|
/** Optional directory path where output files will be saved */
|
|
529
544
|
outputDirectory?: string // If not provided, uses default app directory
|
|
530
545
|
/** Optional filename for the recording (uses UUID if not provided) */
|
|
@@ -757,10 +772,17 @@ export interface UseAudioRecorderState {
|
|
|
757
772
|
maxDurationReached?: boolean
|
|
758
773
|
/** Analysis data for the recording if processing was enabled */
|
|
759
774
|
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
775
|
+
/** Reason associated with the last completed recording */
|
|
776
|
+
lastRecordingReason?: RecordingStopReason
|
|
760
777
|
/** Optional callback to handle recording interruptions */
|
|
761
778
|
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
762
779
|
/** Optional callback invoked when maxDurationMs is reached */
|
|
763
780
|
onMaxDurationReached?: (_: MaxDurationReachedEvent) => void
|
|
781
|
+
/** Optional callback invoked when a recording fully stops */
|
|
782
|
+
onRecordingStopped?: (
|
|
783
|
+
recording: AudioRecording,
|
|
784
|
+
reason: RecordingStopReason
|
|
785
|
+
) => void | Promise<void>
|
|
764
786
|
}
|
|
765
787
|
|
|
766
788
|
/**
|