@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 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.10.0...HEAD
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
- Log.e(Constants.TAG, "Unexpected error in startRecording", e)
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(Constants.AUDIO_EVENT_NAME, Constants.AUDIO_ANALYSIS_EVENT_NAME)
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 interruptions.
111
- /// - Parameter notification: The notification object containing interruption information.
112
- @objc func handleAudioSessionInterruption(notification: Notification) {
113
- guard let info = notification.userInfo,
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
- Logger.debug("audio session interruption \(type)")
120
- if type == .began {
121
- disableWakeLock() // Disable wake lock when audio is interrupted
122
- } else if type == .ended {
123
- if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
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
- // Resume your audio recording
127
- Logger.debug("Resume audio recording \(recordingUUID!)")
128
- try? AVAudioSession.sharedInstance().setActive(true)
129
- enableWakeLock() // Re-enable wake lock when audio resumes
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([audioDataEvent, audioAnalysisEvent, "recordingStateChanged", "notificationStateChanged"])
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("recordingStateChanged", [
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("recordingStateChanged", [
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("notificationStateChanged", [
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.10.0",
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
  }
@@ -1,3 +1,8 @@
1
1
  import { ConfigPlugin } from '@expo/config-plugins';
2
- declare const withRecordingPermission: ConfigPlugin;
2
+ interface AudioStreamPluginOptions {
3
+ enablePhoneStateHandling?: boolean;
4
+ enableNotifications?: boolean;
5
+ enableBackgroundAudio?: boolean;
6
+ }
7
+ declare const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions>;
3
8
  export default withRecordingPermission;
@@ -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
- debugLog('📱 Configuring Recording Permissions Plugin...');
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
- debugLog('🍎 Configuring iOS permissions and capabilities...');
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
- // Add notification permissions
22
- config.modResults['NSUserNotificationsUsageDescription'] =
23
- NOTIFICATION_USAGE;
24
- // Add notification style
25
- config.modResults['NSUserNotificationAlertStyle'] = 'alert';
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 (!existingBackgroundModes.includes('audio')) {
34
+ if (enableBackgroundAudio &&
35
+ !existingBackgroundModes.includes('audio')) {
29
36
  existingBackgroundModes.push('audio');
30
37
  }
31
- if (!existingBackgroundModes.includes('remote-notification')) {
32
- existingBackgroundModes.push('remote-notification');
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
- debugLog('🤖 Configuring Android Manifest...');
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 = androidManifest.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
72
+ const existingPermission = config.modResults.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
68
73
  if (!existingPermission) {
69
- addPermission(androidManifest, permission);
74
+ addPermission(config.modResults, permission);
70
75
  }
71
76
  });
72
77
  // Get the main application node
73
- const mainApplication = androidManifest.manifest.application?.[0];
78
+ const mainApplication = config.modResults.manifest.application?.[0];
74
79
  if (mainApplication) {
75
80
  debugLog('📱 Configuring Android application components...');
76
81
  // Add RecordingActionReceiver
@@ -16,89 +16,112 @@ function debugLog(message: string, ...args: unknown[]): void {
16
16
  }
17
17
  }
18
18
 
19
- const withRecordingPermission: ConfigPlugin = (config: ExpoConfig) => {
20
- debugLog('📱 Configuring Recording Permissions Plugin...')
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
- debugLog('🍎 Configuring iOS permissions and capabilities...')
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
- // Add notification permissions
32
- config.modResults['NSUserNotificationsUsageDescription'] =
33
- NOTIFICATION_USAGE
34
-
35
- // Add notification style
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
- if (!existingBackgroundModes.includes('audio')) {
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
- debugLog('iOS Background Modes:', config.modResults.UIBackgroundModes)
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
- debugLog('🤖 Configuring Android Manifest...')
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 = androidManifest.manifest[
115
+ const existingPermission = config.modResults.manifest[
93
116
  'uses-permission'
94
117
  ]?.find((p) => p.$?.['android:name'] === permission)
95
118
  if (!existingPermission) {
96
- addPermission(androidManifest, permission)
119
+ addPermission(config.modResults, permission)
97
120
  }
98
121
  })
99
122
 
100
123
  // Get the main application node
101
- const mainApplication = androidManifest.manifest.application?.[0]
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(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`)
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
  }