@siteed/expo-audio-studio 2.12.3 → 2.13.0

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 (33) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/android/build.gradle +11 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
  6. package/app.plugin.js +3 -1
  7. package/build/cjs/AudioDeviceManager.js +225 -40
  8. package/build/cjs/AudioDeviceManager.js.map +1 -1
  9. package/build/cjs/hooks/useAudioDevices.js +30 -5
  10. package/build/cjs/hooks/useAudioDevices.js.map +1 -1
  11. package/build/cjs/useAudioRecorder.js +52 -8
  12. package/build/cjs/useAudioRecorder.js.map +1 -1
  13. package/build/esm/AudioDeviceManager.js +225 -40
  14. package/build/esm/AudioDeviceManager.js.map +1 -1
  15. package/build/esm/hooks/useAudioDevices.js +31 -6
  16. package/build/esm/hooks/useAudioDevices.js.map +1 -1
  17. package/build/esm/useAudioRecorder.js +53 -9
  18. package/build/esm/useAudioRecorder.js.map +1 -1
  19. package/build/types/AudioDeviceManager.d.ts +78 -2
  20. package/build/types/AudioDeviceManager.d.ts.map +1 -1
  21. package/build/types/hooks/useAudioDevices.d.ts +1 -0
  22. package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
  23. package/build/types/useAudioRecorder.d.ts.map +1 -1
  24. package/ios/AudioDeviceManager.swift +21 -9
  25. package/ios/ExpoAudioStreamModule.swift +33 -1
  26. package/package.json +8 -6
  27. package/plugin/build/index.cjs +194 -0
  28. package/plugin/build/index.d.cts +1 -0
  29. package/plugin/build/index.js +7 -6
  30. package/plugin/src/index.ts +8 -8
  31. package/src/AudioDeviceManager.ts +286 -59
  32. package/src/hooks/useAudioDevices.ts +39 -6
  33. package/src/useAudioRecorder.tsx +102 -9
@@ -18,7 +18,7 @@ private let audioDeviceTypeWiredHeadphones = "wired_headphones"
18
18
  private let audioDeviceTypeSpeaker = "speaker"
19
19
  private let audioDeviceTypeUnknown = "unknown"
20
20
 
21
- public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
21
+ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDeviceManagerDelegate {
22
22
  private var streamManager = AudioStreamManager()
23
23
  private let notificationCenter = UNUserNotificationCenter.current()
24
24
  private let notificationIdentifier = "audio_recording_notification"
@@ -41,11 +41,30 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
41
41
  OnCreate {
42
42
  Logger.debug("ExpoAudioStreamModule", "Module created, setting delegate and starting device monitoring.")
43
43
  streamManager.delegate = self
44
+ // Set up device manager delegate to emit device change events
45
+ deviceManager.delegate = self
46
+
47
+ // Listen for device connection notifications (minimal addition)
48
+ NotificationCenter.default.addObserver(
49
+ forName: NSNotification.Name("DeviceConnected"),
50
+ object: nil,
51
+ queue: .main
52
+ ) { [weak self] notification in
53
+ if let deviceId = notification.userInfo?["deviceId"] as? String {
54
+ Logger.debug("ExpoAudioStreamModule", "Device connected: \(deviceId)")
55
+ self?.sendEvent(deviceChangedEvent, [
56
+ "type": "deviceConnected",
57
+ "deviceId": deviceId
58
+ ])
59
+ }
60
+ }
44
61
  }
45
62
 
46
63
  OnDestroy {
47
64
  Logger.debug("ExpoAudioStreamModule", "Module destroyed, stopping device monitoring.")
48
65
  _ = streamManager.stopRecording()
66
+ // Clear device manager delegate
67
+ deviceManager.delegate = nil
49
68
  }
50
69
 
51
70
  /// Extracts audio analysis data from an audio file.
@@ -980,4 +999,17 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
980
999
  Logger.error("ExpoAudioStreamModule", "Delegate: didFailWithError: \(error)")
981
1000
  sendEvent(errorEvent, [ "message": error ])
982
1001
  }
1002
+
1003
+ // MARK: - AudioDeviceManagerDelegate
1004
+
1005
+ /// Handles device disconnection events from the AudioDeviceManager
1006
+ func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String) {
1007
+ Logger.debug("ExpoAudioStreamModule", "Device disconnected: \(deviceId)")
1008
+
1009
+ // Emit device change event to match Android implementation
1010
+ sendEvent(deviceChangedEvent, [
1011
+ "type": "deviceDisconnected",
1012
+ "deviceId": deviceId
1013
+ ])
1014
+ }
983
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.12.3",
3
+ "version": "2.13.0",
4
4
  "description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -74,11 +74,12 @@
74
74
  "build:cjs": "tsc -p tsconfig.cjs.json",
75
75
  "build:esm": "tsc -p tsconfig.esm.json",
76
76
  "build:types": "tsc -p tsconfig.types.json",
77
- "build:plugin": "tsc --build plugin/tsconfig.json",
77
+ "build:plugin": "tsc --project plugin/tsconfig.json && cp plugin/build/index.js plugin/build/index.cjs",
78
78
  "build:plugin:dev": "expo-module build plugin",
79
79
  "build:dev": "expo-module build",
80
80
  "clean": "expo-module clean && rimraf build plugin/build",
81
81
  "lint": "expo-module lint",
82
+ "lint:fix": "expo-module lint --fix",
82
83
  "test": "expo-module test",
83
84
  "test:android": "yarn test:android:unit && yarn test:android:instrumented",
84
85
  "test:android:unit": "cd ../../apps/playground/android && ./gradlew :siteed-expo-audio-studio:test",
@@ -100,6 +101,9 @@
100
101
  "agent:test:integration": "yarn test:android:instrumented",
101
102
  "agent:compilation:check": "yarn typecheck && yarn build"
102
103
  },
104
+ "dependencies": {
105
+ "expo-modules-core": "~2.3.13"
106
+ },
103
107
  "devDependencies": {
104
108
  "@expo/config-plugins": "~10.0.0",
105
109
  "@expo/npm-proofread": "^1.0.1",
@@ -122,7 +126,8 @@
122
126
  "expo-module-scripts": "^4.1.7",
123
127
  "jest": "^29.7.0",
124
128
  "prettier": "^3.2.5",
125
- "react-native": "0.79.2",
129
+ "react": "19.0.0",
130
+ "react-native": "0.79.3",
126
131
  "rimraf": "^6.0.1",
127
132
  "size-limit": "^11.1.4",
128
133
  "ts-node": "^10.9.2",
@@ -138,8 +143,5 @@
138
143
  "publishConfig": {
139
144
  "access": "public",
140
145
  "registry": "https://registry.npmjs.org"
141
- },
142
- "dependencies": {
143
- "expo-modules-core": "~2.3.13"
144
146
  }
145
147
  }
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_plugins_1 = require("@expo/config-plugins");
4
+ const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
5
+ const NOTIFICATION_USAGE = 'Show recording notifications and controls';
6
+ const LOG_PREFIX = '[@siteed/expo-audio-studio]';
7
+ function debugLog(message, ...args) {
8
+ if (process.env.EXPO_DEBUG) {
9
+ console.log(`${LOG_PREFIX} ${message}`, ...args);
10
+ }
11
+ }
12
+ const withRecordingPermission = (config, props) => {
13
+ const options = {
14
+ enablePhoneStateHandling: true, // Default to true for backward compatibility
15
+ enableNotifications: true,
16
+ enableBackgroundAudio: true,
17
+ enableDeviceDetection: true, // Default to true for backward compatibility
18
+ iosBackgroundModes: {
19
+ useVoIP: false,
20
+ useAudio: false,
21
+ useProcessing: false,
22
+ useLocation: false,
23
+ useExternalAccessory: false,
24
+ },
25
+ iosConfig: {
26
+ microphoneUsageDescription: MICROPHONE_USAGE,
27
+ notificationUsageDescription: NOTIFICATION_USAGE,
28
+ },
29
+ ...(props || {}),
30
+ };
31
+ const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, enableDeviceDetection, } = options;
32
+ debugLog('📱 Configuring Recording Permissions Plugin...', options);
33
+ // iOS Configuration
34
+ config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
35
+ // Always set the microphone usage description from options first
36
+ config.modResults['NSMicrophoneUsageDescription'] =
37
+ options.iosConfig?.microphoneUsageDescription ||
38
+ config.modResults['NSMicrophoneUsageDescription'] ||
39
+ MICROPHONE_USAGE;
40
+ if (enableNotifications) {
41
+ config.modResults['NSUserNotificationsUsageDescription'] =
42
+ options.iosConfig?.notificationUsageDescription ||
43
+ config.modResults['NSUserNotificationsUsageDescription'] ||
44
+ NOTIFICATION_USAGE;
45
+ config.modResults['NSUserNotificationAlertStyle'] = 'alert';
46
+ }
47
+ const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
48
+ // If background audio is enabled with useAudio, add the audio background mode
49
+ if (options.iosBackgroundModes?.useAudio === true &&
50
+ enableBackgroundAudio === true &&
51
+ !existingBackgroundModes.includes('audio')) {
52
+ // Add 'audio' background mode - REQUIRED for background recording
53
+ existingBackgroundModes.push('audio');
54
+ debugLog('✅ Added audio background mode for iOS background recording');
55
+ // Also ensure processing mode is recommended
56
+ if (options.iosBackgroundModes?.useProcessing !== true) {
57
+ console.warn(`${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`);
58
+ }
59
+ }
60
+ if (options.iosBackgroundModes?.useVoIP === true &&
61
+ enablePhoneStateHandling === true) {
62
+ if (!existingBackgroundModes.includes('voip')) {
63
+ existingBackgroundModes.push('voip');
64
+ }
65
+ const existingCapabilities = (config.modResults
66
+ .UIRequiredDeviceCapabilities || []);
67
+ if (!existingCapabilities.includes('telephony')) {
68
+ existingCapabilities.push('telephony');
69
+ }
70
+ config.modResults.UIRequiredDeviceCapabilities =
71
+ existingCapabilities;
72
+ }
73
+ // Add additional background modes only if explicitly set to true
74
+ if (options.iosBackgroundModes?.useProcessing === true) {
75
+ if (!existingBackgroundModes.includes('processing')) {
76
+ existingBackgroundModes.push('processing');
77
+ }
78
+ // Add processing info if enabled
79
+ // Note: We keep the 'audiostream' namespace for native modules to maintain compatibility
80
+ config.modResults.BGTaskSchedulerPermittedIdentifiers = [
81
+ 'com.siteed.audiostream.processing',
82
+ ];
83
+ }
84
+ if (options.iosBackgroundModes?.useLocation === true) {
85
+ if (!existingBackgroundModes.includes('location')) {
86
+ existingBackgroundModes.push('location');
87
+ }
88
+ }
89
+ if (options.iosBackgroundModes?.useExternalAccessory === true) {
90
+ if (!existingBackgroundModes.includes('external-accessory')) {
91
+ existingBackgroundModes.push('external-accessory');
92
+ }
93
+ }
94
+ // Configure background processing info if enabled
95
+ if (options.iosConfig?.backgroundProcessingTitle) {
96
+ config.modResults.BGProcessingTaskTitle =
97
+ options.iosConfig.backgroundProcessingTitle;
98
+ }
99
+ // Configure audio session behavior
100
+ if (options.iosConfig?.allowBackgroundAudioControls) {
101
+ config.modResults.UIBackgroundModes = [
102
+ ...existingBackgroundModes,
103
+ 'remote-notification',
104
+ ];
105
+ config.modResults.MPNowPlayingInfoPropertyPlaybackRate = true;
106
+ }
107
+ config.modResults.UIBackgroundModes = existingBackgroundModes;
108
+ return config;
109
+ });
110
+ // Android Configuration
111
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
112
+ const basePermissions = [
113
+ 'android.permission.RECORD_AUDIO',
114
+ 'android.permission.WAKE_LOCK',
115
+ ];
116
+ const optionalPermissions = [
117
+ enableNotifications && 'android.permission.POST_NOTIFICATIONS',
118
+ enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
119
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
120
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
121
+ // Device detection permissions (only if enabled)
122
+ enableDeviceDetection && 'android.permission.BLUETOOTH',
123
+ enableDeviceDetection && 'android.permission.BLUETOOTH_CONNECT',
124
+ enableDeviceDetection && 'android.permission.USB_PERMISSION',
125
+ ].filter(Boolean);
126
+ const permissionsToAdd = [...basePermissions, ...optionalPermissions];
127
+ debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
128
+ debugLog('➕ Adding Android permissions:', permissionsToAdd);
129
+ // Add each permission only if it doesn't exist
130
+ permissionsToAdd.forEach((permission) => {
131
+ config_plugins_1.AndroidConfig.Permissions.addPermission(config.modResults, permission);
132
+ });
133
+ // Get the main application node
134
+ const mainApplication = config.modResults.manifest.application?.[0];
135
+ if (mainApplication) {
136
+ debugLog('📱 Configuring Android application components...');
137
+ // Add RecordingActionReceiver
138
+ if (!mainApplication.receiver) {
139
+ mainApplication.receiver = [];
140
+ }
141
+ const receiverConfig = {
142
+ $: {
143
+ 'android:name': '.RecordingActionReceiver',
144
+ 'android:exported': 'false',
145
+ },
146
+ 'intent-filter': [
147
+ {
148
+ action: [
149
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
150
+ { $: { 'android:name': 'RESUME_RECORDING' } },
151
+ { $: { 'android:name': 'STOP_RECORDING' } },
152
+ ],
153
+ },
154
+ ],
155
+ };
156
+ const receiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$?.['android:name'] === '.RecordingActionReceiver');
157
+ if (receiverIndex >= 0) {
158
+ mainApplication.receiver[receiverIndex] = receiverConfig;
159
+ }
160
+ else {
161
+ mainApplication.receiver.push(receiverConfig);
162
+ }
163
+ debugLog('✅ RecordingActionReceiver configured');
164
+ // Add AudioRecordingService
165
+ if (!mainApplication.service) {
166
+ mainApplication.service = [];
167
+ }
168
+ const serviceConfig = {
169
+ $: {
170
+ 'android:name': '.AudioRecordingService',
171
+ 'android:enabled': 'true',
172
+ 'android:exported': 'false',
173
+ 'android:foregroundServiceType': 'microphone',
174
+ },
175
+ };
176
+ const serviceIndex = mainApplication.service.findIndex((service) => service.$?.['android:name'] === '.AudioRecordingService');
177
+ if (serviceIndex >= 0) {
178
+ mainApplication.service[serviceIndex] = serviceConfig;
179
+ }
180
+ else {
181
+ mainApplication.service.push(serviceConfig);
182
+ }
183
+ debugLog('✅ AudioRecordingService configured');
184
+ }
185
+ else {
186
+ console.error(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`);
187
+ }
188
+ return config;
189
+ });
190
+ debugLog('✨ Recording Permissions Plugin configuration completed');
191
+ return config;
192
+ };
193
+ // Export as default
194
+ exports.default = withRecordingPermission;
@@ -3,6 +3,7 @@ interface AudioStreamPluginOptions {
3
3
  enablePhoneStateHandling?: boolean;
4
4
  enableNotifications?: boolean;
5
5
  enableBackgroundAudio?: boolean;
6
+ enableDeviceDetection?: boolean;
6
7
  iosBackgroundModes?: {
7
8
  useVoIP?: boolean;
8
9
  useAudio?: boolean;
@@ -14,6 +14,7 @@ const withRecordingPermission = (config, props) => {
14
14
  enablePhoneStateHandling: true, // Default to true for backward compatibility
15
15
  enableNotifications: true,
16
16
  enableBackgroundAudio: true,
17
+ enableDeviceDetection: true, // Default to true for backward compatibility
17
18
  iosBackgroundModes: {
18
19
  useVoIP: false,
19
20
  useAudio: false,
@@ -27,7 +28,7 @@ const withRecordingPermission = (config, props) => {
27
28
  },
28
29
  ...(props || {}),
29
30
  };
30
- const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, } = options;
31
+ const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, enableDeviceDetection, } = options;
31
32
  debugLog('📱 Configuring Recording Permissions Plugin...', options);
32
33
  // iOS Configuration
33
34
  config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
@@ -117,17 +118,17 @@ const withRecordingPermission = (config, props) => {
117
118
  enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
118
119
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
119
120
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
121
+ // Device detection permissions (only if enabled)
122
+ enableDeviceDetection && 'android.permission.BLUETOOTH',
123
+ enableDeviceDetection && 'android.permission.BLUETOOTH_CONNECT',
124
+ enableDeviceDetection && 'android.permission.USB_PERMISSION',
120
125
  ].filter(Boolean);
121
126
  const permissionsToAdd = [...basePermissions, ...optionalPermissions];
122
127
  debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
123
128
  debugLog('➕ Adding Android permissions:', permissionsToAdd);
124
- const { addPermission } = config_plugins_1.AndroidConfig.Permissions;
125
129
  // Add each permission only if it doesn't exist
126
130
  permissionsToAdd.forEach((permission) => {
127
- const existingPermission = config.modResults.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
128
- if (!existingPermission) {
129
- addPermission(config.modResults, permission);
130
- }
131
+ config_plugins_1.AndroidConfig.Permissions.addPermission(config.modResults, permission);
131
132
  });
132
133
  // Get the main application node
133
134
  const mainApplication = config.modResults.manifest.application?.[0];
@@ -20,6 +20,7 @@ interface AudioStreamPluginOptions {
20
20
  enablePhoneStateHandling?: boolean // Controls READ_PHONE_STATE permission
21
21
  enableNotifications?: boolean
22
22
  enableBackgroundAudio?: boolean
23
+ enableDeviceDetection?: boolean // Controls Bluetooth and USB permissions for device change detection
23
24
  iosBackgroundModes?: {
24
25
  useVoIP?: boolean
25
26
  useAudio?: boolean
@@ -43,6 +44,7 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
43
44
  enablePhoneStateHandling: true, // Default to true for backward compatibility
44
45
  enableNotifications: true,
45
46
  enableBackgroundAudio: true,
47
+ enableDeviceDetection: true, // Default to true for backward compatibility
46
48
  iosBackgroundModes: {
47
49
  useVoIP: false,
48
50
  useAudio: false,
@@ -61,6 +63,7 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
61
63
  enablePhoneStateHandling,
62
64
  enableNotifications,
63
65
  enableBackgroundAudio,
66
+ enableDeviceDetection,
64
67
  } = options
65
68
 
66
69
  debugLog('📱 Configuring Recording Permissions Plugin...', options)
@@ -175,6 +178,10 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
175
178
  enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
176
179
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
177
180
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
181
+ // Device detection permissions (only if enabled)
182
+ enableDeviceDetection && 'android.permission.BLUETOOTH',
183
+ enableDeviceDetection && 'android.permission.BLUETOOTH_CONNECT',
184
+ enableDeviceDetection && 'android.permission.USB_PERMISSION',
178
185
  ].filter(Boolean) as string[]
179
186
 
180
187
  const permissionsToAdd = [...basePermissions, ...optionalPermissions]
@@ -188,16 +195,9 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
188
195
 
189
196
  debugLog('➕ Adding Android permissions:', permissionsToAdd)
190
197
 
191
- const { addPermission } = AndroidConfig.Permissions
192
-
193
198
  // Add each permission only if it doesn't exist
194
199
  permissionsToAdd.forEach((permission) => {
195
- const existingPermission = config.modResults.manifest[
196
- 'uses-permission'
197
- ]?.find((p) => p.$?.['android:name'] === permission)
198
- if (!existingPermission) {
199
- addPermission(config.modResults, permission)
200
- }
200
+ AndroidConfig.Permissions.addPermission(config.modResults, permission)
201
201
  })
202
202
 
203
203
  // Get the main application node