@siteed/expo-audio-stream 1.10.0 → 1.11.1
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 +44 -1
- package/README.md +4 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +206 -1
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +5 -1
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +8 -0
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +3 -1
- package/build/ExpoAudioStream.types.d.ts +8 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/ios/AudioStreamManager.swift +55 -14
- package/ios/AudioStreamManagerDelegate.swift +1 -0
- package/ios/ExpoAudioStreamModule.swift +16 -4
- package/ios/RecordingSettings.swift +4 -1
- package/package.json +2 -1
- package/plugin/build/index.d.ts +6 -1
- package/plugin/build/index.js +43 -38
- package/plugin/src/index.ts +75 -50
- package/src/ExpoAudioStream.types.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,25 +8,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [1.11.1] - 2025-01-22
|
|
12
|
+
- chore: force deployment of 1.11.1
|
|
13
|
+
|
|
14
|
+
## [1.11.0] - 2025-01-22
|
|
15
|
+
- feat(audio): add intelligent call interruption handling & compression improvements ([f8f6187](https://github.com/deeeed/expo-audio-stream/pull/78))
|
|
16
|
+
|
|
17
|
+
|
|
11
18
|
## [1.10.0] - 2025-01-14
|
|
12
19
|
- add support for pausing and resuming compressed recordings ([bc3f629](https://github.com/deeeed/expo-audio-stream/commit/bc3f6295d060396325e0f008ff00b3be9c8722cd))
|
|
13
20
|
- optimize notification channel settings ([daa075e](https://github.com/deeeed/expo-audio-stream/commit/daa075e668f8faf0b8d2849e18c37384bdd293b8))
|
|
14
21
|
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
## [1.9.2] - 2025-01-12
|
|
16
25
|
- ios bitrate verification to prevent invalid values ([035a180](https://github.com/deeeed/expo-audio-stream/commit/035a1800833264edcc59724aaa8a2e12d5c78dc2))
|
|
17
26
|
|
|
18
27
|
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
## [1.9.1] - 2025-01-12
|
|
20
31
|
- ios potentially missing compressed file info ([88a628c](https://github.com/deeeed/expo-audio-stream/commit/88a628c35f2bfd626a2a5de1eb6950efd814619d))
|
|
21
32
|
|
|
22
33
|
|
|
23
34
|
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
## [1.9.0] - 2025-01-11
|
|
25
38
|
- feat(web-audio): optimize memory usage and streaming performance for web audio recording (#75) ([7b93e12](https://github.com/deeeed/expo-audio-stream/commit/7b93e12aae4bc0599b06b48ca34a60f65587fc75))
|
|
26
39
|
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
## [1.8.0] - 2025-01-10
|
|
31
46
|
- feat(audio): implement audio compression support ([ff4e060](https://github.com/deeeed/expo-audio-stream/commit/ff4e060fef1061804c1cc0126d4344d2d50daa9a))
|
|
32
47
|
|
|
@@ -34,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
34
49
|
|
|
35
50
|
|
|
36
51
|
|
|
52
|
+
|
|
53
|
+
|
|
37
54
|
## [1.7.2] - 2025-01-07
|
|
38
55
|
- fix(audio-stream): correct WAV header handling in web audio recording ([9ba7de5](https://github.com/deeeed/expo-audio-stream/commit/9ba7de5b96ca4cc937dea261c80d3fda9c99e8f4))
|
|
39
56
|
|
|
@@ -42,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
42
59
|
|
|
43
60
|
|
|
44
61
|
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
## [1.7.1] - 2025-01-07
|
|
46
65
|
- update notification to avoid triggering new alerts (#71) ([32dcfc5](https://github.com/deeeed/expo-audio-stream/commit/32dcfc55daf3236babefc17016f329c177d466fd))
|
|
47
66
|
|
|
@@ -51,6 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
51
70
|
|
|
52
71
|
|
|
53
72
|
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
## [1.7.0] - 2025-01-05
|
|
55
76
|
- feat(playground): enhance app configuration and build setup for production deployment (#58) ([929d443](https://github.com/deeeed/expo-audio-stream/commit/929d443145378b1430d215db5c00b13758420e2b))
|
|
56
77
|
- chore(expo-audio-stream): release @siteed/expo-audio-stream@1.6.1 ([084e8ad](https://github.com/deeeed/expo-audio-stream/commit/084e8adb91da7874c9e608b55d9c7b2ffd7a8327))
|
|
@@ -66,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
66
87
|
|
|
67
88
|
|
|
68
89
|
|
|
90
|
+
|
|
91
|
+
|
|
69
92
|
## [1.6.1] - 2024-12-11
|
|
70
93
|
- chore(expo-audio-stream): remove git commit step from publish script ([4a772ce](https://github.com/deeeed/expo-audio-stream/commit/4a772ce93bb7405d9b8e981f46bdf8941a71ecfe))
|
|
71
94
|
- chore: more publishing automation ([3693021](https://github.com/deeeed/expo-audio-stream/commit/369302107f9dca9dddd8ae68e6214481a39976ac))
|
|
@@ -85,6 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
85
108
|
|
|
86
109
|
|
|
87
110
|
|
|
111
|
+
|
|
112
|
+
|
|
88
113
|
## [1.5.0] - 2024-12-10
|
|
89
114
|
- UNPUBLISHED because of a bug in the build system
|
|
90
115
|
|
|
@@ -97,6 +122,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
97
122
|
|
|
98
123
|
|
|
99
124
|
|
|
125
|
+
|
|
126
|
+
|
|
100
127
|
## [1.4.0] - 2024-12-05
|
|
101
128
|
- chore: remove unusded dependencies ([ad81dd5](https://github.com/deeeed/expo-audio-stream/commit/ad81dd560c93dd1d04995a323a4ae72d4de20f3e))
|
|
102
129
|
|
|
@@ -109,6 +136,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
109
136
|
|
|
110
137
|
|
|
111
138
|
|
|
139
|
+
|
|
140
|
+
|
|
112
141
|
## [1.3.1] - 2024-12-05
|
|
113
142
|
- feat(web): implement throttling and optimize event processing (#49) ([da28765](https://github.com/deeeed/expo-audio-stream/commit/da2876524c2c9d6e0a980fde40a0197b929d8a7f))
|
|
114
143
|
|
|
@@ -121,6 +150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
121
150
|
|
|
122
151
|
|
|
123
152
|
|
|
153
|
+
|
|
154
|
+
|
|
124
155
|
## [1.3.0] - 2024-11-28
|
|
125
156
|
### Added
|
|
126
157
|
- refactor(permissions): standardize permission status response structure across platforms (#44) ([7c9c800](https://github.com/deeeed/expo-audio-stream/commit/7c9c800d83b7cea3516643371484d5e1f3b99e4c))
|
|
@@ -138,6 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
138
169
|
|
|
139
170
|
|
|
140
171
|
|
|
172
|
+
|
|
173
|
+
|
|
141
174
|
## [1.2.5] - 2024-11-12
|
|
142
175
|
### Added
|
|
143
176
|
- docs(license): add MIT license to all packages (6 files changed)
|
|
@@ -152,6 +185,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
152
185
|
|
|
153
186
|
|
|
154
187
|
|
|
188
|
+
|
|
189
|
+
|
|
155
190
|
## [1.2.4] - 2024-11-05
|
|
156
191
|
### Changed
|
|
157
192
|
- Android minimum audio interval set to 10ms.
|
|
@@ -169,6 +204,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
169
204
|
|
|
170
205
|
|
|
171
206
|
|
|
207
|
+
|
|
208
|
+
|
|
172
209
|
## [1.2.0] - 2024-10-24
|
|
173
210
|
### Added
|
|
174
211
|
- Feature: Keep device awake during recording with `keepAwake` option
|
|
@@ -186,6 +223,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
186
223
|
|
|
187
224
|
|
|
188
225
|
|
|
226
|
+
|
|
227
|
+
|
|
189
228
|
## [1.1.17] - 2024-10-21
|
|
190
229
|
### Added
|
|
191
230
|
- Support bluetooth headset on ios
|
|
@@ -200,6 +239,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
200
239
|
|
|
201
240
|
|
|
202
241
|
|
|
242
|
+
|
|
243
|
+
|
|
203
244
|
## [1.0.0] - 2024-04-01
|
|
204
245
|
### Added
|
|
205
246
|
- Initial release of @siteed/expo-audio-stream.
|
|
@@ -210,7 +251,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
210
251
|
- Feature: Audio features extraction during recording.
|
|
211
252
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
212
253
|
|
|
213
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.
|
|
254
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.11.1...HEAD
|
|
255
|
+
[1.11.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.11.0...@siteed/expo-audio-stream@1.11.1
|
|
256
|
+
[1.11.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.10.0...@siteed/expo-audio-stream@1.11.0
|
|
214
257
|
[1.10.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.2...@siteed/expo-audio-stream@1.10.0
|
|
215
258
|
[1.9.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.1...@siteed/expo-audio-stream@1.9.2
|
|
216
259
|
[1.9.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.0...@siteed/expo-audio-stream@1.9.1
|
package/README.md
CHANGED
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
- Compression formats: OPUS or AAC
|
|
38
38
|
- Configurable bitrate for compressed audio
|
|
39
39
|
- Optimized storage for both high-quality and compressed formats
|
|
40
|
+
- Intelligent interruption handling:
|
|
41
|
+
- Automatic pause/resume during phone calls
|
|
42
|
+
- Configurable automatic resumption
|
|
43
|
+
- Detailed interruption event callbacks
|
|
40
44
|
- Configurable intervals for audio buffer receipt.
|
|
41
45
|
- Automated microphone permissions setup in managed Expo projects.
|
|
42
46
|
- Background audio recording on iOS.
|
|
@@ -23,6 +23,11 @@ import android.os.PowerManager
|
|
|
23
23
|
import android.content.Context
|
|
24
24
|
import java.nio.ByteBuffer
|
|
25
25
|
import java.nio.ByteOrder
|
|
26
|
+
import android.media.AudioManager
|
|
27
|
+
import android.media.AudioAttributes
|
|
28
|
+
import android.media.AudioFocusRequest
|
|
29
|
+
import android.telephony.PhoneStateListener
|
|
30
|
+
import android.telephony.TelephonyManager
|
|
26
31
|
|
|
27
32
|
class AudioRecorderManager(
|
|
28
33
|
private val context: Context,
|
|
@@ -63,6 +68,55 @@ class AudioRecorderManager(
|
|
|
63
68
|
private var compressedRecorder: MediaRecorder? = null
|
|
64
69
|
private var compressedFile: File? = null
|
|
65
70
|
|
|
71
|
+
private var audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
72
|
+
private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
|
|
73
|
+
private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
|
|
74
|
+
private var phoneStateListener: PhoneStateListener? = null
|
|
75
|
+
private var telephonyManager: TelephonyManager? = null
|
|
76
|
+
|
|
77
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
78
|
+
private val audioFocusCallback = object : AudioManager.OnAudioFocusChangeListener {
|
|
79
|
+
override fun onAudioFocusChange(focusChange: Int) {
|
|
80
|
+
when (focusChange) {
|
|
81
|
+
AudioManager.AUDIOFOCUS_LOSS,
|
|
82
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
83
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
84
|
+
mainHandler.post {
|
|
85
|
+
pauseRecording(object : Promise {
|
|
86
|
+
override fun resolve(value: Any?) {
|
|
87
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
88
|
+
"reason" to "audioFocusLoss",
|
|
89
|
+
"isPaused" to true
|
|
90
|
+
))
|
|
91
|
+
}
|
|
92
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
93
|
+
Log.e(Constants.TAG, "Failed to pause recording on audio focus loss")
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
100
|
+
if (isRecording.get() && isPaused.get()) {
|
|
101
|
+
mainHandler.post {
|
|
102
|
+
resumeRecording(object : Promise {
|
|
103
|
+
override fun resolve(value: Any?) {
|
|
104
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
105
|
+
"reason" to "audioFocusGain",
|
|
106
|
+
"isPaused" to false
|
|
107
|
+
))
|
|
108
|
+
}
|
|
109
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
110
|
+
Log.e(Constants.TAG, "Failed to resume recording on audio focus gain")
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
66
120
|
companion object {
|
|
67
121
|
@SuppressLint("StaticFieldLeak")
|
|
68
122
|
@Volatile
|
|
@@ -88,6 +142,15 @@ class AudioRecorderManager(
|
|
|
88
142
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
89
143
|
fun startRecording(options: Map<String, Any?>, promise: Promise) {
|
|
90
144
|
try {
|
|
145
|
+
// Initialize phone state listener
|
|
146
|
+
initializePhoneStateListener()
|
|
147
|
+
|
|
148
|
+
// Request audio focus
|
|
149
|
+
if (!requestAudioFocus()) {
|
|
150
|
+
promise.reject("AUDIO_FOCUS_ERROR", "Failed to obtain audio focus", null)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
91
154
|
Log.d(Constants.TAG, "Starting recording with options: $options")
|
|
92
155
|
|
|
93
156
|
// Check permissions
|
|
@@ -158,7 +221,8 @@ class AudioRecorderManager(
|
|
|
158
221
|
promise.resolve(result)
|
|
159
222
|
|
|
160
223
|
} catch (e: Exception) {
|
|
161
|
-
|
|
224
|
+
releaseAudioFocus()
|
|
225
|
+
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
|
|
162
226
|
promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
|
|
163
227
|
}
|
|
164
228
|
}
|
|
@@ -906,4 +970,145 @@ class AudioRecorderManager(
|
|
|
906
970
|
return false
|
|
907
971
|
}
|
|
908
972
|
}
|
|
973
|
+
|
|
974
|
+
private fun initializePhoneStateListener() {
|
|
975
|
+
try {
|
|
976
|
+
// Check for READ_PHONE_STATE permission before initializing phone state listener
|
|
977
|
+
if (permissionUtils.checkPhoneStatePermission()) {
|
|
978
|
+
phoneStateListener = object : PhoneStateListener() {
|
|
979
|
+
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
|
|
980
|
+
when (state) {
|
|
981
|
+
TelephonyManager.CALL_STATE_RINGING,
|
|
982
|
+
TelephonyManager.CALL_STATE_OFFHOOK -> {
|
|
983
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
984
|
+
mainHandler.post {
|
|
985
|
+
pauseRecording(object : Promise {
|
|
986
|
+
override fun resolve(value: Any?) {
|
|
987
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
988
|
+
"reason" to "phoneCall",
|
|
989
|
+
"isPaused" to true
|
|
990
|
+
))
|
|
991
|
+
}
|
|
992
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
993
|
+
Log.e(Constants.TAG, "Failed to pause recording on phone call")
|
|
994
|
+
}
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
TelephonyManager.CALL_STATE_IDLE -> {
|
|
1000
|
+
if (isRecording.get() && isPaused.get()) {
|
|
1001
|
+
mainHandler.post {
|
|
1002
|
+
resumeRecording(object : Promise {
|
|
1003
|
+
override fun resolve(value: Any?) {
|
|
1004
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1005
|
+
"reason" to "phoneCallEnded",
|
|
1006
|
+
"isPaused" to false
|
|
1007
|
+
))
|
|
1008
|
+
}
|
|
1009
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1010
|
+
Log.e(Constants.TAG, "Failed to resume recording after phone call")
|
|
1011
|
+
}
|
|
1012
|
+
})
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
1021
|
+
if (telephonyManager != null) {
|
|
1022
|
+
try {
|
|
1023
|
+
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
|
1024
|
+
} catch (e: Exception) {
|
|
1025
|
+
Log.e(Constants.TAG, "Failed to register phone state listener", e)
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} else {
|
|
1029
|
+
Log.w(Constants.TAG, "READ_PHONE_STATE permission not granted, phone call interruption handling disabled")
|
|
1030
|
+
}
|
|
1031
|
+
} catch (e: Exception) {
|
|
1032
|
+
Log.e(Constants.TAG, "Failed to initialize phone state listener", e)
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@SuppressLint("NewApi")
|
|
1037
|
+
private fun requestAudioFocus(): Boolean {
|
|
1038
|
+
audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
1039
|
+
when (focusChange) {
|
|
1040
|
+
AudioManager.AUDIOFOCUS_LOSS,
|
|
1041
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1042
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
1043
|
+
mainHandler.post {
|
|
1044
|
+
pauseRecording(object : Promise {
|
|
1045
|
+
override fun resolve(value: Any?) {
|
|
1046
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1047
|
+
"reason" to "audioFocusLoss",
|
|
1048
|
+
"isPaused" to true
|
|
1049
|
+
))
|
|
1050
|
+
}
|
|
1051
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1052
|
+
Log.e(Constants.TAG, "Failed to pause recording on audio focus loss")
|
|
1053
|
+
}
|
|
1054
|
+
})
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1059
|
+
if (isRecording.get() && isPaused.get()) {
|
|
1060
|
+
mainHandler.post {
|
|
1061
|
+
resumeRecording(object : Promise {
|
|
1062
|
+
override fun resolve(value: Any?) {
|
|
1063
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1064
|
+
"reason" to "audioFocusGain",
|
|
1065
|
+
"isPaused" to false
|
|
1066
|
+
))
|
|
1067
|
+
}
|
|
1068
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1069
|
+
Log.e(Constants.TAG, "Failed to resume recording on audio focus gain")
|
|
1070
|
+
}
|
|
1071
|
+
})
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1079
|
+
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
|
1080
|
+
.setAudioAttributes(AudioAttributes.Builder()
|
|
1081
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
1082
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
1083
|
+
.build())
|
|
1084
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener!!)
|
|
1085
|
+
.build()
|
|
1086
|
+
audioFocusRequest = focusRequest
|
|
1087
|
+
audioManager.requestAudioFocus(focusRequest)
|
|
1088
|
+
} else {
|
|
1089
|
+
@Suppress("DEPRECATION")
|
|
1090
|
+
audioManager.requestAudioFocus(
|
|
1091
|
+
audioFocusChangeListener,
|
|
1092
|
+
AudioManager.STREAM_MUSIC,
|
|
1093
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private fun releaseAudioFocus() {
|
|
1101
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1102
|
+
(audioFocusRequest as? AudioFocusRequest)?.let { request ->
|
|
1103
|
+
audioManager.abandonAudioFocusRequest(request)
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
@Suppress("DEPRECATION")
|
|
1107
|
+
audioFocusChangeListener?.let { listener ->
|
|
1108
|
+
audioManager.abandonAudioFocus(listener)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
audioFocusRequest = null
|
|
1112
|
+
audioFocusChangeListener = null
|
|
1113
|
+
}
|
|
909
1114
|
}
|
|
@@ -3,6 +3,7 @@ package net.siteed.audiostream
|
|
|
3
3
|
object Constants {
|
|
4
4
|
const val AUDIO_EVENT_NAME = "AudioData"
|
|
5
5
|
const val AUDIO_ANALYSIS_EVENT_NAME = "AudioAnalysis"
|
|
6
|
+
const val RECORDING_INTERRUPTED_EVENT_NAME = "onRecordingInterrupted"
|
|
6
7
|
const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
|
|
7
8
|
const val DEFAULT_CHANNEL_CONFIG = 1 // Mono
|
|
8
9
|
const val DEFAULT_AUDIO_FORMAT = 16 // 16-bit PCM
|
|
@@ -20,7 +20,11 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
20
20
|
// The module will be accessible from `requireNativeModule('ExpoAudioStream')` in JavaScript.
|
|
21
21
|
Name("ExpoAudioStream")
|
|
22
22
|
|
|
23
|
-
Events(
|
|
23
|
+
Events(
|
|
24
|
+
Constants.AUDIO_EVENT_NAME,
|
|
25
|
+
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
26
|
+
Constants.RECORDING_INTERRUPTED_EVENT_NAME
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
// Initialize AudioRecorderManager
|
|
26
30
|
initializeManager()
|
|
@@ -48,4 +48,12 @@ class PermissionUtils(private val context: Context) {
|
|
|
48
48
|
}
|
|
49
49
|
return result
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
fun checkPhoneStatePermission(): Boolean {
|
|
53
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
54
|
+
context.checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
|
55
|
+
} else {
|
|
56
|
+
true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
51
59
|
}
|
|
@@ -19,6 +19,7 @@ data class RecordingConfig(
|
|
|
19
19
|
val enableCompressedOutput: Boolean = false,
|
|
20
20
|
val compressedFormat: String = "opus",
|
|
21
21
|
val compressedBitRate: Int = 24000,
|
|
22
|
+
val autoResumeAfterInterruption: Boolean = false,
|
|
22
23
|
) {
|
|
23
24
|
companion object {
|
|
24
25
|
fun fromMap(options: Map<String, Any?>?): Result<Pair<RecordingConfig, AudioFormatInfo>> {
|
|
@@ -73,7 +74,8 @@ data class RecordingConfig(
|
|
|
73
74
|
features = features,
|
|
74
75
|
enableCompressedOutput = enableCompressedOutput,
|
|
75
76
|
compressedFormat = compressedFormat,
|
|
76
|
-
compressedBitRate = compressedBitRate
|
|
77
|
+
compressedBitRate = compressedBitRate,
|
|
78
|
+
autoResumeAfterInterruption = options.getBooleanOrDefault("autoResumeAfterInterruption", false)
|
|
77
79
|
)
|
|
78
80
|
|
|
79
81
|
// Validate sample rate and channels
|
|
@@ -79,6 +79,11 @@ export interface AudioSessionConfig {
|
|
|
79
79
|
export interface IOSConfig {
|
|
80
80
|
audioSession?: AudioSessionConfig;
|
|
81
81
|
}
|
|
82
|
+
export type RecordingInterruptionReason = 'audioFocusLoss' | 'audioFocusGain' | 'phoneCall' | 'phoneCallEnded';
|
|
83
|
+
export interface RecordingInterruptionEvent {
|
|
84
|
+
reason: RecordingInterruptionReason;
|
|
85
|
+
isPaused: boolean;
|
|
86
|
+
}
|
|
82
87
|
export interface RecordingConfig {
|
|
83
88
|
sampleRate?: SampleRate;
|
|
84
89
|
channels?: 1 | 2;
|
|
@@ -100,6 +105,8 @@ export interface RecordingConfig {
|
|
|
100
105
|
format: 'aac' | 'opus' | 'mp3';
|
|
101
106
|
bitrate?: number;
|
|
102
107
|
};
|
|
108
|
+
autoResumeAfterInterruption?: boolean;
|
|
109
|
+
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void;
|
|
103
110
|
}
|
|
104
111
|
export interface NotificationConfig {
|
|
105
112
|
title?: string;
|
|
@@ -144,5 +151,6 @@ export interface UseAudioRecorderState {
|
|
|
144
151
|
size: number;
|
|
145
152
|
compression?: CompressionInfo;
|
|
146
153
|
analysisData?: AudioAnalysis;
|
|
154
|
+
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void;
|
|
147
155
|
}
|
|
148
156
|
//# sourceMappingURL=ExpoAudioStream.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"","sourcesContent":["// packages/expo-audio-stream/src/ExpoAudioStream.types.ts\nimport {\n AmplitudeAlgorithm,\n AudioAnalysis,\n AudioFeaturesOptions,\n} from './AudioAnalysis/AudioAnalysis.types'\nimport { AudioAnalysisEvent } from './events'\n\nexport interface CompressionInfo {\n size: number\n mimeType: string\n bitrate: number\n format: string\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean\n isPaused: boolean\n durationMs: number\n size: number\n interval: number\n mimeType: string\n compression?: CompressionInfo\n}\n\nexport interface AudioDataEvent {\n data: string | Float32Array\n position: number\n fileUri: string\n eventDataSize: number\n totalSize: number\n compression?: CompressionInfo & {\n data?: string | Blob // Base64 (native) or Float32Array (web) encoded compressed data chunk\n }\n}\n\nexport type EncodingType = 'pcm_32bit' | 'pcm_16bit' | 'pcm_8bit'\nexport type SampleRate = 16000 | 44100 | 48000\nexport type BitDepth = 8 | 16 | 32\n\nexport type ConsoleLike = {\n log: (message: string, ...args: unknown[]) => void\n debug: (message: string, ...args: unknown[]) => void\n warn: (message: string, ...args: unknown[]) => void\n error: (message: string, ...args: unknown[]) => void\n}\n\nexport interface Chunk {\n text: string\n timestamp: [number, number | null]\n}\n\nexport interface TranscriberData {\n id: string\n isBusy: boolean\n text: string\n startTime: number\n endTime: number\n chunks: Chunk[]\n}\n\nexport interface AudioRecording {\n fileUri: string\n filename: string\n durationMs: number\n size: number\n mimeType: string\n channels: number\n bitDepth: BitDepth\n sampleRate: SampleRate\n transcripts?: TranscriberData[]\n analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag\n compression?: CompressionInfo & {\n compressedFileUri: string\n }\n}\n\nexport interface StartRecordingResult {\n fileUri: string\n mimeType: string\n channels?: number\n bitDepth?: BitDepth\n sampleRate?: SampleRate\n compression?: CompressionInfo & {\n compressedFileUri: string\n }\n}\n\nexport interface AudioSessionConfig {\n category?:\n | 'Ambient'\n | 'SoloAmbient'\n | 'Playback'\n | 'Record'\n | 'PlayAndRecord'\n | 'MultiRoute'\n mode?:\n | 'Default'\n | 'VoiceChat'\n | 'VideoChat'\n | 'GameChat'\n | 'VideoRecording'\n | 'Measurement'\n | 'MoviePlayback'\n | 'SpokenAudio'\n categoryOptions?: (\n | 'MixWithOthers'\n | 'DuckOthers'\n | 'InterruptSpokenAudioAndMixWithOthers'\n | 'AllowBluetooth'\n | 'AllowBluetoothA2DP'\n | 'AllowAirPlay'\n | 'DefaultToSpeaker'\n )[]\n}\n\nexport interface IOSConfig {\n audioSession?: AudioSessionConfig\n}\n\nexport interface RecordingConfig {\n // Sample rate for recording (16000, 44100, or 48000 Hz)\n sampleRate?: SampleRate\n\n // Number of audio channels (1 for mono, 2 for stereo)\n channels?: 1 | 2\n\n // Encoding type for the recording (pcm_32bit, pcm_16bit, pcm_8bit)\n encoding?: EncodingType\n\n // Interval in milliseconds at which to emit recording data\n interval?: number\n\n // Keep the device awake while recording (default is false)\n keepAwake?: boolean\n\n // Show a notification during recording (default is false)\n showNotification?: boolean\n\n // Show waveform in the notification (Android only, when showNotification is true)\n showWaveformInNotification?: boolean\n\n // Configuration for the notification\n notification?: NotificationConfig\n\n // Enable audio processing (default is false)\n enableProcessing?: boolean\n\n // iOS-specific configuration\n ios?: IOSConfig\n\n // Number of data points to extract per second of audio (default is 1000)\n pointsPerSecond?: number\n\n // Algorithm to use for amplitude computation (default is \"rms\")\n algorithm?: AmplitudeAlgorithm\n\n // Feature options to extract (default is empty)\n features?: AudioFeaturesOptions\n\n // Callback function to handle audio stream\n onAudioStream?: (_: AudioDataEvent) => Promise<void>\n\n // Callback function to handle audio features extraction results\n onAudioAnalysis?: (_: AudioAnalysisEvent) => Promise<void>\n\n compression?: {\n enabled: boolean\n format: 'aac' | 'opus' | 'mp3'\n bitrate?: number\n }\n}\n\nexport interface NotificationConfig {\n // Title of the notification\n title?: string\n\n // Main text content of the notification\n text?: string\n\n // Icon to be displayed in the notification (resource name or URI)\n icon?: string\n\n // Android-specific notification configuration\n android?: {\n // Unique identifier for the notification channel\n channelId?: string\n\n // User-visible name of the notification channel\n channelName?: string\n\n // User-visible description of the notification channel\n channelDescription?: string\n\n // Unique identifier for this notification\n notificationId?: number\n\n // List of actions that can be performed from the notification\n actions?: NotificationAction[]\n\n // Configuration for the waveform visualization in the notification\n waveform?: WaveformConfig\n\n // Color of the notification LED (if device supports it)\n lightColor?: string\n\n // Priority of the notification (affects how it's displayed)\n priority?: 'min' | 'low' | 'default' | 'high' | 'max'\n\n // Accent color for the notification (used for the app icon and buttons)\n accentColor?: string\n }\n\n // iOS-specific notification configuration\n ios?: {\n // Identifier for the notification category (used for grouping similar notifications)\n categoryIdentifier?: string\n }\n}\n\nexport interface NotificationAction {\n // Display title for the action\n title: string\n\n // Unique identifier for the action\n identifier: string\n\n // Icon to be displayed for the action (Android only)\n icon?: string\n}\n\nexport interface WaveformConfig {\n color?: string // The color of the waveform (e.g., \"#FFFFFF\" for white)\n opacity?: number // Opacity of the waveform (0.0 - 1.0)\n strokeWidth?: number // Width of the waveform line (default: 1.5)\n style?: 'stroke' | 'fill' // Drawing style: \"stroke\" for outline, \"fill\" for solid\n mirror?: boolean // Whether to mirror the waveform (symmetrical display)\n height?: number // Height of the waveform view in dp (default: 64)\n}\n\nexport interface UseAudioRecorderState {\n startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>\n stopRecording: () => Promise<AudioRecording | null>\n pauseRecording: () => Promise<void>\n resumeRecording: () => Promise<void>\n isRecording: boolean\n isPaused: boolean\n durationMs: number // Duration of the recording\n size: number // Size in bytes of the recorded audio\n compression?: CompressionInfo\n analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"","sourcesContent":["// packages/expo-audio-stream/src/ExpoAudioStream.types.ts\nimport {\n AmplitudeAlgorithm,\n AudioAnalysis,\n AudioFeaturesOptions,\n} from './AudioAnalysis/AudioAnalysis.types'\nimport { AudioAnalysisEvent } from './events'\n\nexport interface CompressionInfo {\n size: number\n mimeType: string\n bitrate: number\n format: string\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean\n isPaused: boolean\n durationMs: number\n size: number\n interval: number\n mimeType: string\n compression?: CompressionInfo\n}\n\nexport interface AudioDataEvent {\n data: string | Float32Array\n position: number\n fileUri: string\n eventDataSize: number\n totalSize: number\n compression?: CompressionInfo & {\n data?: string | Blob // Base64 (native) or Float32Array (web) encoded compressed data chunk\n }\n}\n\nexport type EncodingType = 'pcm_32bit' | 'pcm_16bit' | 'pcm_8bit'\nexport type SampleRate = 16000 | 44100 | 48000\nexport type BitDepth = 8 | 16 | 32\n\nexport type ConsoleLike = {\n log: (message: string, ...args: unknown[]) => void\n debug: (message: string, ...args: unknown[]) => void\n warn: (message: string, ...args: unknown[]) => void\n error: (message: string, ...args: unknown[]) => void\n}\n\nexport interface Chunk {\n text: string\n timestamp: [number, number | null]\n}\n\nexport interface TranscriberData {\n id: string\n isBusy: boolean\n text: string\n startTime: number\n endTime: number\n chunks: Chunk[]\n}\n\nexport interface AudioRecording {\n fileUri: string\n filename: string\n durationMs: number\n size: number\n mimeType: string\n channels: number\n bitDepth: BitDepth\n sampleRate: SampleRate\n transcripts?: TranscriberData[]\n analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag\n compression?: CompressionInfo & {\n compressedFileUri: string\n }\n}\n\nexport interface StartRecordingResult {\n fileUri: string\n mimeType: string\n channels?: number\n bitDepth?: BitDepth\n sampleRate?: SampleRate\n compression?: CompressionInfo & {\n compressedFileUri: string\n }\n}\n\nexport interface AudioSessionConfig {\n category?:\n | 'Ambient'\n | 'SoloAmbient'\n | 'Playback'\n | 'Record'\n | 'PlayAndRecord'\n | 'MultiRoute'\n mode?:\n | 'Default'\n | 'VoiceChat'\n | 'VideoChat'\n | 'GameChat'\n | 'VideoRecording'\n | 'Measurement'\n | 'MoviePlayback'\n | 'SpokenAudio'\n categoryOptions?: (\n | 'MixWithOthers'\n | 'DuckOthers'\n | 'InterruptSpokenAudioAndMixWithOthers'\n | 'AllowBluetooth'\n | 'AllowBluetoothA2DP'\n | 'AllowAirPlay'\n | 'DefaultToSpeaker'\n )[]\n}\n\nexport interface IOSConfig {\n audioSession?: AudioSessionConfig\n}\n\n// Add new type for interruption reasons\nexport type RecordingInterruptionReason =\n | 'audioFocusLoss'\n | 'audioFocusGain'\n | 'phoneCall'\n | 'phoneCallEnded'\n\n// Add new interface for interruption events\nexport interface RecordingInterruptionEvent {\n reason: RecordingInterruptionReason\n isPaused: boolean\n}\n\nexport interface RecordingConfig {\n // Sample rate for recording (16000, 44100, or 48000 Hz)\n sampleRate?: SampleRate\n\n // Number of audio channels (1 for mono, 2 for stereo)\n channels?: 1 | 2\n\n // Encoding type for the recording (pcm_32bit, pcm_16bit, pcm_8bit)\n encoding?: EncodingType\n\n // Interval in milliseconds at which to emit recording data\n interval?: number\n\n // Keep the device awake while recording (default is false)\n keepAwake?: boolean\n\n // Show a notification during recording (default is false)\n showNotification?: boolean\n\n // Show waveform in the notification (Android only, when showNotification is true)\n showWaveformInNotification?: boolean\n\n // Configuration for the notification\n notification?: NotificationConfig\n\n // Enable audio processing (default is false)\n enableProcessing?: boolean\n\n // iOS-specific configuration\n ios?: IOSConfig\n\n // Number of data points to extract per second of audio (default is 1000)\n pointsPerSecond?: number\n\n // Algorithm to use for amplitude computation (default is \"rms\")\n algorithm?: AmplitudeAlgorithm\n\n // Feature options to extract (default is empty)\n features?: AudioFeaturesOptions\n\n // Callback function to handle audio stream\n onAudioStream?: (_: AudioDataEvent) => Promise<void>\n\n // Callback function to handle audio features extraction results\n onAudioAnalysis?: (_: AudioAnalysisEvent) => Promise<void>\n\n compression?: {\n enabled: boolean\n format: 'aac' | 'opus' | 'mp3'\n bitrate?: number\n }\n\n // Whether to automatically resume recording after an interruption (default is false)\n autoResumeAfterInterruption?: boolean\n\n // Optional callback to handle recording interruptions\n onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void\n}\n\nexport interface NotificationConfig {\n // Title of the notification\n title?: string\n\n // Main text content of the notification\n text?: string\n\n // Icon to be displayed in the notification (resource name or URI)\n icon?: string\n\n // Android-specific notification configuration\n android?: {\n // Unique identifier for the notification channel\n channelId?: string\n\n // User-visible name of the notification channel\n channelName?: string\n\n // User-visible description of the notification channel\n channelDescription?: string\n\n // Unique identifier for this notification\n notificationId?: number\n\n // List of actions that can be performed from the notification\n actions?: NotificationAction[]\n\n // Configuration for the waveform visualization in the notification\n waveform?: WaveformConfig\n\n // Color of the notification LED (if device supports it)\n lightColor?: string\n\n // Priority of the notification (affects how it's displayed)\n priority?: 'min' | 'low' | 'default' | 'high' | 'max'\n\n // Accent color for the notification (used for the app icon and buttons)\n accentColor?: string\n }\n\n // iOS-specific notification configuration\n ios?: {\n // Identifier for the notification category (used for grouping similar notifications)\n categoryIdentifier?: string\n }\n}\n\nexport interface NotificationAction {\n // Display title for the action\n title: string\n\n // Unique identifier for the action\n identifier: string\n\n // Icon to be displayed for the action (Android only)\n icon?: string\n}\n\nexport interface WaveformConfig {\n color?: string // The color of the waveform (e.g., \"#FFFFFF\" for white)\n opacity?: number // Opacity of the waveform (0.0 - 1.0)\n strokeWidth?: number // Width of the waveform line (default: 1.5)\n style?: 'stroke' | 'fill' // Drawing style: \"stroke\" for outline, \"fill\" for solid\n mirror?: boolean // Whether to mirror the waveform (symmetrical display)\n height?: number // Height of the waveform view in dp (default: 64)\n}\n\nexport interface UseAudioRecorderState {\n startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>\n stopRecording: () => Promise<AudioRecording | null>\n pauseRecording: () => Promise<void>\n resumeRecording: () => Promise<void>\n isRecording: boolean\n isPaused: boolean\n durationMs: number // Duration of the recording\n size: number // Size in bytes of the recorded audio\n compression?: CompressionInfo\n analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag\n onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void\n}\n"]}
|
|
@@ -11,6 +11,7 @@ import Accelerate
|
|
|
11
11
|
import UIKit
|
|
12
12
|
import MediaPlayer
|
|
13
13
|
import UserNotifications
|
|
14
|
+
import CallKit
|
|
14
15
|
|
|
15
16
|
// Helper to convert to little-endian byte array
|
|
16
17
|
extension UInt32 {
|
|
@@ -74,6 +75,9 @@ class AudioStreamManager: NSObject {
|
|
|
74
75
|
private var compressedFormat: String = "aac"
|
|
75
76
|
private var compressedBitRate: Int = 128000
|
|
76
77
|
|
|
78
|
+
// Add property to track auto-resume preference
|
|
79
|
+
private var autoResumeAfterInterruption: Bool = false
|
|
80
|
+
|
|
77
81
|
/// Initializes the AudioStreamManager
|
|
78
82
|
override init() {
|
|
79
83
|
super.init()
|
|
@@ -107,28 +111,62 @@ class AudioStreamManager: NSObject {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
/// Handles audio session
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
114
|
+
/// Handles an audio session interruption.
|
|
115
|
+
@objc private func handleAudioSessionInterruption(_ notification: Notification) {
|
|
116
|
+
guard let userInfo = notification.userInfo,
|
|
117
|
+
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
115
118
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
116
119
|
return
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
let wasSuspended = isPaused
|
|
123
|
+
|
|
124
|
+
switch type {
|
|
125
|
+
case .began:
|
|
126
|
+
Logger.debug("Audio session interruption began")
|
|
127
|
+
pauseRecording()
|
|
128
|
+
|
|
129
|
+
// Notify about the interruption
|
|
130
|
+
delegate?.audioStreamManager(
|
|
131
|
+
self,
|
|
132
|
+
didReceiveInterruption: [
|
|
133
|
+
"type": "began",
|
|
134
|
+
"wasSuspended": wasSuspended
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
case .ended:
|
|
139
|
+
Logger.debug("Audio session interruption ended")
|
|
140
|
+
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
124
141
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
125
142
|
if options.contains(.shouldResume) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
143
|
+
if autoResumeAfterInterruption && !wasSuspended {
|
|
144
|
+
resumeRecording()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Notify about the interruption
|
|
148
|
+
delegate?.audioStreamManager(
|
|
149
|
+
self,
|
|
150
|
+
didReceiveInterruption: [
|
|
151
|
+
"type": "ended",
|
|
152
|
+
"wasSuspended": wasSuspended,
|
|
153
|
+
"shouldResume": true
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
// Notify about the interruption without resume option
|
|
158
|
+
delegate?.audioStreamManager(
|
|
159
|
+
self,
|
|
160
|
+
didReceiveInterruption: [
|
|
161
|
+
"type": "ended",
|
|
162
|
+
"wasSuspended": wasSuspended,
|
|
163
|
+
"shouldResume": false
|
|
164
|
+
]
|
|
165
|
+
)
|
|
130
166
|
}
|
|
131
167
|
}
|
|
168
|
+
@unknown default:
|
|
169
|
+
break
|
|
132
170
|
}
|
|
133
171
|
}
|
|
134
172
|
|
|
@@ -441,6 +479,9 @@ class AudioStreamManager: NSObject {
|
|
|
441
479
|
/// - intervalMilliseconds: The interval in milliseconds for emitting audio data.
|
|
442
480
|
/// - Returns: A StartRecordingResult object if recording starts successfully, or nil otherwise.
|
|
443
481
|
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
|
|
482
|
+
// Update auto-resume preference from settings
|
|
483
|
+
autoResumeAfterInterruption = settings.autoResumeAfterInterruption
|
|
484
|
+
|
|
444
485
|
guard !isRecording else {
|
|
445
486
|
Logger.debug("Debug: Recording is already in progress.")
|
|
446
487
|
return nil
|
|
@@ -11,4 +11,5 @@ protocol AudioStreamManagerDelegate: AnyObject {
|
|
|
11
11
|
func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date)
|
|
12
12
|
func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date)
|
|
13
13
|
func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool)
|
|
14
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any])
|
|
14
15
|
}
|
|
@@ -3,6 +3,9 @@ import AVFoundation
|
|
|
3
3
|
|
|
4
4
|
let audioDataEvent: String = "AudioData"
|
|
5
5
|
let audioAnalysisEvent: String = "AudioAnalysis"
|
|
6
|
+
let recordingStateChangedEvent: String = "recordingStateChanged"
|
|
7
|
+
let notificationStateChangedEvent: String = "notificationStateChanged"
|
|
8
|
+
let recordingInterruptedEvent: String = "onRecordingInterrupted"
|
|
6
9
|
|
|
7
10
|
public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
8
11
|
private var streamManager = AudioStreamManager()
|
|
@@ -13,7 +16,13 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
13
16
|
Name("ExpoAudioStream")
|
|
14
17
|
|
|
15
18
|
// Defines event names that the module can send to JavaScript.
|
|
16
|
-
Events([
|
|
19
|
+
Events([
|
|
20
|
+
audioDataEvent,
|
|
21
|
+
audioAnalysisEvent,
|
|
22
|
+
recordingStateChangedEvent,
|
|
23
|
+
notificationStateChangedEvent,
|
|
24
|
+
recordingInterruptedEvent
|
|
25
|
+
])
|
|
17
26
|
|
|
18
27
|
OnCreate {
|
|
19
28
|
print("Setting streamManager delegate")
|
|
@@ -299,21 +308,21 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
|
|
302
|
-
sendEvent(
|
|
311
|
+
sendEvent(recordingStateChangedEvent, [
|
|
303
312
|
"state": "paused",
|
|
304
313
|
"timestamp": pauseTime.timeIntervalSince1970 * 1000
|
|
305
314
|
])
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
|
|
309
|
-
sendEvent(
|
|
318
|
+
sendEvent(recordingStateChangedEvent, [
|
|
310
319
|
"state": "recording",
|
|
311
320
|
"timestamp": resumeTime.timeIntervalSince1970 * 1000
|
|
312
321
|
])
|
|
313
322
|
}
|
|
314
323
|
|
|
315
324
|
func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
|
|
316
|
-
sendEvent(
|
|
325
|
+
sendEvent(notificationStateChangedEvent, [
|
|
317
326
|
"isPaused": isPaused
|
|
318
327
|
])
|
|
319
328
|
}
|
|
@@ -447,4 +456,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
447
456
|
}
|
|
448
457
|
}
|
|
449
458
|
|
|
459
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
|
|
460
|
+
sendEvent(recordingInterruptedEvent, info)
|
|
461
|
+
}
|
|
450
462
|
}
|
|
@@ -98,6 +98,8 @@ struct RecordingSettings {
|
|
|
98
98
|
let compressedFormat: String // "aac" or "opus"
|
|
99
99
|
let compressedBitRate: Int
|
|
100
100
|
|
|
101
|
+
let autoResumeAfterInterruption: Bool
|
|
102
|
+
|
|
101
103
|
static func fromDictionary(_ dict: [String: Any]) -> Result<RecordingSettings, Error> {
|
|
102
104
|
// Extract compression settings
|
|
103
105
|
let compression = dict["compression"] as? [String: Any]
|
|
@@ -122,7 +124,8 @@ struct RecordingSettings {
|
|
|
122
124
|
desiredSampleRate: dict["desiredSampleRate"] as? Double ?? 44100.0,
|
|
123
125
|
enableCompressedOutput: enableCompressedOutput,
|
|
124
126
|
compressedFormat: compressedFormat,
|
|
125
|
-
compressedBitRate: compressedBitRate
|
|
127
|
+
compressedBitRate: compressedBitRate,
|
|
128
|
+
autoResumeAfterInterruption: dict["autoResumeAfterInterruption"] as? Bool ?? false
|
|
126
129
|
)
|
|
127
130
|
|
|
128
131
|
// Parse core settings
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-stream",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "stream audio crossplatform",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -104,6 +104,7 @@
|
|
|
104
104
|
"registry": "https://registry.npmjs.org"
|
|
105
105
|
},
|
|
106
106
|
"dependencies": {
|
|
107
|
+
"@siteed/design-system": "^0.35.1",
|
|
107
108
|
"expo-modules-core": "^2.1.1"
|
|
108
109
|
}
|
|
109
110
|
}
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
-
|
|
2
|
+
interface AudioStreamPluginOptions {
|
|
3
|
+
enablePhoneStateHandling?: boolean;
|
|
4
|
+
enableNotifications?: boolean;
|
|
5
|
+
enableBackgroundAudio?: boolean;
|
|
6
|
+
}
|
|
7
|
+
declare const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions>;
|
|
3
8
|
export default withRecordingPermission;
|
package/plugin/build/index.js
CHANGED
|
@@ -9,68 +9,73 @@ function debugLog(message, ...args) {
|
|
|
9
9
|
console.log(`${LOG_PREFIX} ${message}`, ...args);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
const withRecordingPermission = (config) => {
|
|
13
|
-
|
|
12
|
+
const withRecordingPermission = (config, props) => {
|
|
13
|
+
// Default options if pluginOptions is undefined (void)
|
|
14
|
+
const options = {
|
|
15
|
+
enablePhoneStateHandling: true,
|
|
16
|
+
enableNotifications: true,
|
|
17
|
+
enableBackgroundAudio: true,
|
|
18
|
+
...(props || {}),
|
|
19
|
+
};
|
|
20
|
+
const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, } = options;
|
|
21
|
+
debugLog('📱 Configuring Recording Permissions Plugin...', options);
|
|
14
22
|
// iOS Configuration
|
|
15
23
|
config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
|
|
16
|
-
|
|
17
|
-
// Existing microphone permission
|
|
24
|
+
// Base microphone permission (always required)
|
|
18
25
|
config.modResults['NSMicrophoneUsageDescription'] =
|
|
19
26
|
config.modResults['NSMicrophoneUsageDescription'] ||
|
|
20
27
|
MICROPHONE_USAGE;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Background modes
|
|
28
|
+
if (enableNotifications) {
|
|
29
|
+
config.modResults['NSUserNotificationsUsageDescription'] =
|
|
30
|
+
NOTIFICATION_USAGE;
|
|
31
|
+
config.modResults['NSUserNotificationAlertStyle'] = 'alert';
|
|
32
|
+
}
|
|
27
33
|
const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
|
|
28
|
-
if (
|
|
34
|
+
if (enableBackgroundAudio &&
|
|
35
|
+
!existingBackgroundModes.includes('audio')) {
|
|
29
36
|
existingBackgroundModes.push('audio');
|
|
30
37
|
}
|
|
31
|
-
if (
|
|
32
|
-
existingBackgroundModes.
|
|
38
|
+
if (enablePhoneStateHandling) {
|
|
39
|
+
if (!existingBackgroundModes.includes('voip')) {
|
|
40
|
+
existingBackgroundModes.push('voip');
|
|
41
|
+
}
|
|
42
|
+
const existingCapabilities = (config.modResults
|
|
43
|
+
.UIRequiredDeviceCapabilities || []);
|
|
44
|
+
if (!existingCapabilities.includes('telephony')) {
|
|
45
|
+
existingCapabilities.push('telephony');
|
|
46
|
+
}
|
|
47
|
+
config.modResults.UIRequiredDeviceCapabilities =
|
|
48
|
+
existingCapabilities;
|
|
33
49
|
}
|
|
34
50
|
config.modResults.UIBackgroundModes = existingBackgroundModes;
|
|
35
|
-
debugLog('iOS Background Modes:', config.modResults.UIBackgroundModes);
|
|
36
51
|
return config;
|
|
37
52
|
});
|
|
38
53
|
// Android Configuration
|
|
39
54
|
config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
40
|
-
|
|
41
|
-
const androidManifest = config.modResults;
|
|
42
|
-
if (!androidManifest.manifest) {
|
|
43
|
-
console.error(`${LOG_PREFIX} ❌ Android Manifest is null - plugin cannot continue`);
|
|
44
|
-
return config;
|
|
45
|
-
}
|
|
46
|
-
// Add xmlns:android attribute to manifest
|
|
47
|
-
androidManifest.manifest.$ = {
|
|
48
|
-
...androidManifest.manifest.$,
|
|
49
|
-
'xmlns:android': 'http://schemas.android.com/apk/res/android',
|
|
50
|
-
};
|
|
51
|
-
// Ensure permissions array exists
|
|
52
|
-
if (!androidManifest.manifest['uses-permission']) {
|
|
53
|
-
androidManifest.manifest['uses-permission'] = [];
|
|
54
|
-
}
|
|
55
|
-
const { addPermission } = config_plugins_1.AndroidConfig.Permissions;
|
|
56
|
-
debugLog('📋 Existing Android permissions:', androidManifest.manifest['uses-permission']?.map(p => p.$?.['android:name']) || []);
|
|
57
|
-
const permissionsToAdd = [
|
|
55
|
+
const basePermissions = [
|
|
58
56
|
'android.permission.RECORD_AUDIO',
|
|
59
|
-
'android.permission.FOREGROUND_SERVICE',
|
|
60
|
-
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
|
|
61
57
|
'android.permission.WAKE_LOCK',
|
|
62
|
-
'android.permission.POST_NOTIFICATIONS',
|
|
63
58
|
];
|
|
59
|
+
const optionalPermissions = [
|
|
60
|
+
enableNotifications && 'android.permission.POST_NOTIFICATIONS',
|
|
61
|
+
enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE',
|
|
62
|
+
enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
|
|
63
|
+
enableBackgroundAudio &&
|
|
64
|
+
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
const permissionsToAdd = [...basePermissions, ...optionalPermissions];
|
|
67
|
+
debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
|
|
64
68
|
debugLog('➕ Adding Android permissions:', permissionsToAdd);
|
|
69
|
+
const { addPermission } = config_plugins_1.AndroidConfig.Permissions;
|
|
65
70
|
// Add each permission only if it doesn't exist
|
|
66
71
|
permissionsToAdd.forEach((permission) => {
|
|
67
|
-
const existingPermission =
|
|
72
|
+
const existingPermission = config.modResults.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
|
|
68
73
|
if (!existingPermission) {
|
|
69
|
-
addPermission(
|
|
74
|
+
addPermission(config.modResults, permission);
|
|
70
75
|
}
|
|
71
76
|
});
|
|
72
77
|
// Get the main application node
|
|
73
|
-
const mainApplication =
|
|
78
|
+
const mainApplication = config.modResults.manifest.application?.[0];
|
|
74
79
|
if (mainApplication) {
|
|
75
80
|
debugLog('📱 Configuring Android application components...');
|
|
76
81
|
// Add RecordingActionReceiver
|
package/plugin/src/index.ts
CHANGED
|
@@ -16,89 +16,112 @@ function debugLog(message: string, ...args: unknown[]): void {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
interface AudioStreamPluginOptions {
|
|
20
|
+
enablePhoneStateHandling?: boolean
|
|
21
|
+
enableNotifications?: boolean
|
|
22
|
+
enableBackgroundAudio?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
|
|
26
|
+
config: ExpoConfig,
|
|
27
|
+
props: AudioStreamPluginOptions | void
|
|
28
|
+
) => {
|
|
29
|
+
// Default options if pluginOptions is undefined (void)
|
|
30
|
+
const options: AudioStreamPluginOptions = {
|
|
31
|
+
enablePhoneStateHandling: true,
|
|
32
|
+
enableNotifications: true,
|
|
33
|
+
enableBackgroundAudio: true,
|
|
34
|
+
...(props || {}),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
enablePhoneStateHandling,
|
|
39
|
+
enableNotifications,
|
|
40
|
+
enableBackgroundAudio,
|
|
41
|
+
} = options
|
|
42
|
+
|
|
43
|
+
debugLog('📱 Configuring Recording Permissions Plugin...', options)
|
|
21
44
|
|
|
22
45
|
// iOS Configuration
|
|
23
46
|
config = withInfoPlist(config as any, (config) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Existing microphone permission
|
|
47
|
+
// Base microphone permission (always required)
|
|
27
48
|
config.modResults['NSMicrophoneUsageDescription'] =
|
|
28
49
|
config.modResults['NSMicrophoneUsageDescription'] ||
|
|
29
50
|
MICROPHONE_USAGE
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
config.modResults['NSUserNotificationAlertStyle'] = 'alert'
|
|
52
|
+
if (enableNotifications) {
|
|
53
|
+
config.modResults['NSUserNotificationsUsageDescription'] =
|
|
54
|
+
NOTIFICATION_USAGE
|
|
55
|
+
config.modResults['NSUserNotificationAlertStyle'] = 'alert'
|
|
56
|
+
}
|
|
37
57
|
|
|
38
|
-
// Background modes
|
|
39
58
|
const existingBackgroundModes =
|
|
40
59
|
config.modResults.UIBackgroundModes || []
|
|
41
|
-
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
enableBackgroundAudio &&
|
|
63
|
+
!existingBackgroundModes.includes('audio')
|
|
64
|
+
) {
|
|
42
65
|
existingBackgroundModes.push('audio')
|
|
43
66
|
}
|
|
44
|
-
if (!existingBackgroundModes.includes('remote-notification')) {
|
|
45
|
-
existingBackgroundModes.push('remote-notification')
|
|
46
|
-
}
|
|
47
|
-
config.modResults.UIBackgroundModes = existingBackgroundModes
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
if (enablePhoneStateHandling) {
|
|
69
|
+
if (!existingBackgroundModes.includes('voip')) {
|
|
70
|
+
existingBackgroundModes.push('voip')
|
|
71
|
+
}
|
|
72
|
+
const existingCapabilities = (config.modResults
|
|
73
|
+
.UIRequiredDeviceCapabilities || []) as string[]
|
|
74
|
+
if (!existingCapabilities.includes('telephony')) {
|
|
75
|
+
existingCapabilities.push('telephony')
|
|
76
|
+
}
|
|
77
|
+
config.modResults.UIRequiredDeviceCapabilities =
|
|
78
|
+
existingCapabilities
|
|
79
|
+
}
|
|
50
80
|
|
|
81
|
+
config.modResults.UIBackgroundModes = existingBackgroundModes
|
|
51
82
|
return config
|
|
52
83
|
})
|
|
53
84
|
|
|
54
85
|
// Android Configuration
|
|
55
86
|
config = withAndroidManifest(config as any, (config) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const androidManifest = config.modResults
|
|
59
|
-
if (!androidManifest.manifest) {
|
|
60
|
-
console.error(`${LOG_PREFIX} ❌ Android Manifest is null - plugin cannot continue`)
|
|
61
|
-
return config
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Add xmlns:android attribute to manifest
|
|
65
|
-
androidManifest.manifest.$ = {
|
|
66
|
-
...androidManifest.manifest.$,
|
|
67
|
-
'xmlns:android': 'http://schemas.android.com/apk/res/android',
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Ensure permissions array exists
|
|
71
|
-
if (!androidManifest.manifest['uses-permission']) {
|
|
72
|
-
androidManifest.manifest['uses-permission'] = []
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const { addPermission } = AndroidConfig.Permissions
|
|
76
|
-
|
|
77
|
-
debugLog('📋 Existing Android permissions:',
|
|
78
|
-
androidManifest.manifest['uses-permission']?.map(p => p.$?.['android:name']) || [])
|
|
79
|
-
|
|
80
|
-
const permissionsToAdd = [
|
|
87
|
+
const basePermissions = [
|
|
81
88
|
'android.permission.RECORD_AUDIO',
|
|
82
|
-
'android.permission.FOREGROUND_SERVICE',
|
|
83
|
-
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
|
|
84
89
|
'android.permission.WAKE_LOCK',
|
|
85
|
-
'android.permission.POST_NOTIFICATIONS',
|
|
86
90
|
]
|
|
87
91
|
|
|
92
|
+
const optionalPermissions = [
|
|
93
|
+
enableNotifications && 'android.permission.POST_NOTIFICATIONS',
|
|
94
|
+
enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE',
|
|
95
|
+
enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
|
|
96
|
+
enableBackgroundAudio &&
|
|
97
|
+
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
|
|
98
|
+
].filter(Boolean) as string[]
|
|
99
|
+
|
|
100
|
+
const permissionsToAdd = [...basePermissions, ...optionalPermissions]
|
|
101
|
+
|
|
102
|
+
debugLog(
|
|
103
|
+
'📋 Existing Android permissions:',
|
|
104
|
+
config.modResults.manifest['uses-permission']?.map(
|
|
105
|
+
(p) => p.$?.['android:name']
|
|
106
|
+
) || []
|
|
107
|
+
)
|
|
108
|
+
|
|
88
109
|
debugLog('➕ Adding Android permissions:', permissionsToAdd)
|
|
89
110
|
|
|
111
|
+
const { addPermission } = AndroidConfig.Permissions
|
|
112
|
+
|
|
90
113
|
// Add each permission only if it doesn't exist
|
|
91
114
|
permissionsToAdd.forEach((permission) => {
|
|
92
|
-
const existingPermission =
|
|
115
|
+
const existingPermission = config.modResults.manifest[
|
|
93
116
|
'uses-permission'
|
|
94
117
|
]?.find((p) => p.$?.['android:name'] === permission)
|
|
95
118
|
if (!existingPermission) {
|
|
96
|
-
addPermission(
|
|
119
|
+
addPermission(config.modResults, permission)
|
|
97
120
|
}
|
|
98
121
|
})
|
|
99
122
|
|
|
100
123
|
// Get the main application node
|
|
101
|
-
const mainApplication =
|
|
124
|
+
const mainApplication = config.modResults.manifest.application?.[0]
|
|
102
125
|
if (mainApplication) {
|
|
103
126
|
debugLog('📱 Configuring Android application components...')
|
|
104
127
|
|
|
@@ -163,7 +186,9 @@ const withRecordingPermission: ConfigPlugin = (config: ExpoConfig) => {
|
|
|
163
186
|
|
|
164
187
|
debugLog('✅ AudioRecordingService configured')
|
|
165
188
|
} else {
|
|
166
|
-
console.error(
|
|
189
|
+
console.error(
|
|
190
|
+
`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`
|
|
191
|
+
)
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
return config
|
|
@@ -118,6 +118,19 @@ export interface IOSConfig {
|
|
|
118
118
|
audioSession?: AudioSessionConfig
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Add new type for interruption reasons
|
|
122
|
+
export type RecordingInterruptionReason =
|
|
123
|
+
| 'audioFocusLoss'
|
|
124
|
+
| 'audioFocusGain'
|
|
125
|
+
| 'phoneCall'
|
|
126
|
+
| 'phoneCallEnded'
|
|
127
|
+
|
|
128
|
+
// Add new interface for interruption events
|
|
129
|
+
export interface RecordingInterruptionEvent {
|
|
130
|
+
reason: RecordingInterruptionReason
|
|
131
|
+
isPaused: boolean
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
export interface RecordingConfig {
|
|
122
135
|
// Sample rate for recording (16000, 44100, or 48000 Hz)
|
|
123
136
|
sampleRate?: SampleRate
|
|
@@ -169,6 +182,12 @@ export interface RecordingConfig {
|
|
|
169
182
|
format: 'aac' | 'opus' | 'mp3'
|
|
170
183
|
bitrate?: number
|
|
171
184
|
}
|
|
185
|
+
|
|
186
|
+
// Whether to automatically resume recording after an interruption (default is false)
|
|
187
|
+
autoResumeAfterInterruption?: boolean
|
|
188
|
+
|
|
189
|
+
// Optional callback to handle recording interruptions
|
|
190
|
+
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
export interface NotificationConfig {
|
|
@@ -249,4 +268,5 @@ export interface UseAudioRecorderState {
|
|
|
249
268
|
size: number // Size in bytes of the recorded audio
|
|
250
269
|
compression?: CompressionInfo
|
|
251
270
|
analysisData?: AudioAnalysis // Analysis data for the recording depending on enableProcessing flag
|
|
271
|
+
onRecordingInterrupted?: (_: RecordingInterruptionEvent) => void
|
|
252
272
|
}
|