@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +12 -1
  3. package/build/cjs/AudioStudio.types.js.map +1 -1
  4. package/build/cjs/useAudioRecorder.js +115 -93
  5. package/build/cjs/useAudioRecorder.js.map +1 -1
  6. package/build/cjs/utils/nativeRecordingOptions.js +13 -0
  7. package/build/cjs/utils/nativeRecordingOptions.js.map +1 -0
  8. package/build/cjs/utils/nativeRecordingOptions.test.js +30 -0
  9. package/build/cjs/utils/nativeRecordingOptions.test.js.map +1 -0
  10. package/build/esm/AudioStudio.types.js.map +1 -1
  11. package/build/esm/useAudioRecorder.js +115 -93
  12. package/build/esm/useAudioRecorder.js.map +1 -1
  13. package/build/esm/utils/nativeRecordingOptions.js +10 -0
  14. package/build/esm/utils/nativeRecordingOptions.js.map +1 -0
  15. package/build/esm/utils/nativeRecordingOptions.test.js +28 -0
  16. package/build/esm/utils/nativeRecordingOptions.test.js.map +1 -0
  17. package/build/types/AudioStudio.types.d.ts +19 -5
  18. package/build/types/AudioStudio.types.d.ts.map +1 -1
  19. package/build/types/useAudioRecorder.d.ts +2 -1
  20. package/build/types/useAudioRecorder.d.ts.map +1 -1
  21. package/build/types/utils/nativeRecordingOptions.d.ts +28 -0
  22. package/build/types/utils/nativeRecordingOptions.d.ts.map +1 -0
  23. package/build/types/utils/nativeRecordingOptions.test.d.ts +2 -0
  24. package/build/types/utils/nativeRecordingOptions.test.d.ts.map +1 -0
  25. package/package.json +2 -1
  26. package/plugin/build/index.cjs +79 -47
  27. package/plugin/build/index.d.cts +15 -0
  28. package/plugin/build/index.js +79 -47
  29. package/plugin/src/index.test.ts +78 -0
  30. package/plugin/src/index.ts +141 -59
  31. package/plugin/tsconfig.json +6 -1
  32. package/src/AudioStudio.types.ts +27 -5
  33. package/src/useAudioRecorder.tsx +161 -126
  34. package/src/utils/nativeRecordingOptions.test.ts +29 -0
  35. package/src/utils/nativeRecordingOptions.ts +20 -0
@@ -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
- // Add RecordingActionReceiver
212
- if (!mainApplication.receiver) {
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
- if (receiverIndex >= 0) {
238
- mainApplication.receiver[receiverIndex] = receiverConfig
239
- } else {
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`
@@ -12,5 +12,10 @@
12
12
  }
13
13
  },
14
14
  "include": ["./src"],
15
- "exclude": ["**/__mocks__/*", "**/__tests__/*"]
15
+ "exclude": [
16
+ "**/__mocks__/*",
17
+ "**/__tests__/*",
18
+ "**/*.test.ts",
19
+ "**/*.spec.ts"
20
+ ]
16
21
  }
@@ -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. The MaxDurationReached event is emitted before the stop request.
515
- * The automatic stop result is not returned to onMaxDurationReached; use the
516
- * event and stream callbacks for immediate UI updates.
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
- * If autoStopOnMaxDuration is true, this callback is invoked before the
524
- * recorder finishes stopping. The final stop result is not passed here.
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
  /**