@siteed/expo-audio-studio 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -1
- package/README.md +25 -0
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +581 -255
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +435 -158
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +14 -5
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioDeviceManager.d.ts +107 -0
- package/build/AudioDeviceManager.d.ts.map +1 -0
- package/build/AudioDeviceManager.js +493 -0
- package/build/AudioDeviceManager.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +3 -0
- package/build/AudioRecorder.provider.js.map +1 -1
- package/build/ExpoAudioStream.types.d.ts +90 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js +7 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +37 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/ExpoAudioStream.web.js +399 -54
- package/build/ExpoAudioStream.web.js.map +1 -1
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +20 -0
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +63 -10
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +277 -68
- package/build/WebRecorder.web.js.map +1 -1
- package/build/hooks/useAudioDevices.d.ts +14 -0
- package/build/hooks/useAudioDevices.d.ts.map +1 -0
- package/build/hooks/useAudioDevices.js +151 -0
- package/build/hooks/useAudioDevices.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.d.ts +1 -0
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +20 -1
- package/build/useAudioRecorder.js.map +1 -1
- package/build/utils/BlobFix.d.ts.map +1 -1
- package/build/utils/BlobFix.js +2 -2
- package/build/utils/BlobFix.js.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/build/workers/InlineFeaturesExtractor.web.js +27 -26
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
- package/build/workers/inlineAudioWebWorker.web.js +25 -1
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
- package/ios/AudioDeviceManager.swift +654 -0
- package/ios/AudioStreamManager.swift +964 -760
- package/ios/ExpoAudioStreamModule.swift +174 -19
- package/ios/Features.swift +1 -1
- package/ios/ISSUE_IOS.md +45 -0
- package/ios/Logger.swift +13 -1
- package/ios/RecordingSettings.swift +12 -0
- package/package.json +2 -2
- package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
- package/src/AudioDeviceManager.ts +571 -0
- package/src/AudioRecorder.provider.tsx +3 -0
- package/src/ExpoAudioStream.types.ts +97 -1
- package/src/ExpoAudioStream.web.ts +513 -63
- package/src/ExpoAudioStreamModule.ts +23 -0
- package/src/WebRecorder.web.ts +346 -81
- package/src/hooks/useAudioDevices.ts +180 -0
- package/src/index.ts +6 -0
- package/src/types/crc-32.d.ts +6 -6
- package/src/useAudioRecorder.tsx +27 -1
- package/src/utils/BlobFix.ts +6 -4
- package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
- package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// net/siteed/audiostream/AudioRecorderManager.kt
|
|
2
2
|
package net.siteed.audiostream
|
|
3
3
|
|
|
4
|
+
import android.Manifest
|
|
4
5
|
import android.annotation.SuppressLint
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.media.AudioDeviceInfo
|
|
5
8
|
import android.media.AudioFormat
|
|
6
9
|
import android.media.AudioRecord
|
|
7
10
|
import android.media.MediaRecorder
|
|
@@ -9,6 +12,7 @@ import android.os.Build
|
|
|
9
12
|
import android.os.Bundle
|
|
10
13
|
import android.os.Handler
|
|
11
14
|
import android.os.Looper
|
|
15
|
+
import android.os.PowerManager
|
|
12
16
|
import android.os.SystemClock
|
|
13
17
|
import android.util.Log
|
|
14
18
|
import androidx.annotation.RequiresApi
|
|
@@ -19,8 +23,6 @@ import java.io.File
|
|
|
19
23
|
import java.io.FileOutputStream
|
|
20
24
|
import java.io.IOException
|
|
21
25
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
22
|
-
import android.os.PowerManager
|
|
23
|
-
import android.content.Context
|
|
24
26
|
import java.nio.ByteBuffer
|
|
25
27
|
import java.nio.ByteOrder
|
|
26
28
|
import android.media.AudioManager
|
|
@@ -30,7 +32,7 @@ import android.telephony.PhoneStateListener
|
|
|
30
32
|
import android.telephony.TelephonyManager
|
|
31
33
|
import android.app.ActivityManager
|
|
32
34
|
import java.util.UUID
|
|
33
|
-
import
|
|
35
|
+
import net.siteed.audiostream.LogUtils
|
|
34
36
|
|
|
35
37
|
class AudioRecorderManager(
|
|
36
38
|
private val context: Context,
|
|
@@ -38,11 +40,44 @@ class AudioRecorderManager(
|
|
|
38
40
|
private val permissionUtils: PermissionUtils,
|
|
39
41
|
private val audioDataEncoder: AudioDataEncoder,
|
|
40
42
|
private val eventSender: EventSender,
|
|
41
|
-
private val enablePhoneStateHandling: Boolean = true
|
|
43
|
+
private val enablePhoneStateHandling: Boolean = true,
|
|
44
|
+
private val enableBackgroundAudio: Boolean = true
|
|
42
45
|
) {
|
|
46
|
+
companion object {
|
|
47
|
+
private const val CLASS_NAME = "AudioRecorderManager"
|
|
48
|
+
|
|
49
|
+
@SuppressLint("StaticFieldLeak")
|
|
50
|
+
@Volatile
|
|
51
|
+
private var instance: AudioRecorderManager? = null
|
|
52
|
+
|
|
53
|
+
fun getInstance(): AudioRecorderManager? = instance
|
|
54
|
+
|
|
55
|
+
fun initialize(
|
|
56
|
+
context: Context,
|
|
57
|
+
filesDir: File,
|
|
58
|
+
permissionUtils: PermissionUtils,
|
|
59
|
+
audioDataEncoder: AudioDataEncoder,
|
|
60
|
+
eventSender: EventSender,
|
|
61
|
+
enablePhoneStateHandling: Boolean = true,
|
|
62
|
+
enableBackgroundAudio: Boolean = true
|
|
63
|
+
): AudioRecorderManager {
|
|
64
|
+
return instance ?: synchronized(this) {
|
|
65
|
+
instance ?: AudioRecorderManager(
|
|
66
|
+
context, filesDir, permissionUtils, audioDataEncoder, eventSender,
|
|
67
|
+
enablePhoneStateHandling, enableBackgroundAudio
|
|
68
|
+
).also { instance = it }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fun destroy() {
|
|
73
|
+
instance?.cleanup()
|
|
74
|
+
instance = null
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
private var audioRecord: AudioRecord? = null
|
|
44
79
|
private var bufferSizeInBytes = 0
|
|
45
|
-
private var
|
|
80
|
+
private var _isRecording = AtomicBoolean(false)
|
|
46
81
|
private val isPaused = AtomicBoolean(false)
|
|
47
82
|
private var streamUuid: String? = null
|
|
48
83
|
private var audioFile: File? = null
|
|
@@ -80,7 +115,7 @@ class AudioRecorderManager(
|
|
|
80
115
|
get() {
|
|
81
116
|
if (field == null) {
|
|
82
117
|
field = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
83
|
-
|
|
118
|
+
LogUtils.d(CLASS_NAME, "TelephonyManager initialization: ${if (field != null) "successful" else "failed"}")
|
|
84
119
|
}
|
|
85
120
|
return field
|
|
86
121
|
}
|
|
@@ -89,12 +124,253 @@ class AudioRecorderManager(
|
|
|
89
124
|
private val analysisBuffer = ByteArrayOutputStream()
|
|
90
125
|
private var isFirstAnalysis = true
|
|
91
126
|
|
|
127
|
+
// Properties for device disconnection handling
|
|
128
|
+
var isPrepared = false
|
|
129
|
+
private var selectedDeviceId: String? = null
|
|
130
|
+
private var deviceDisconnectionBehavior: String? = null
|
|
131
|
+
|
|
132
|
+
// Add a method to handle device changes
|
|
133
|
+
fun handleDeviceChange() {
|
|
134
|
+
LogUtils.d(CLASS_NAME, "🔄 handleDeviceChange called - isRecording=${_isRecording.get()}, isPaused=${isPaused.get()}")
|
|
135
|
+
if (!_isRecording.get()) {
|
|
136
|
+
LogUtils.d(CLASS_NAME, "🔄 handleDeviceChange: Not recording, no action needed")
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isPaused.get()) {
|
|
141
|
+
LogUtils.d(CLASS_NAME, "🔄 handleDeviceChange: Recording is paused, marking for restart with new device when resumed")
|
|
142
|
+
|
|
143
|
+
// When paused after device disconnection, we need to release the existing AudioRecord
|
|
144
|
+
// so that it can be properly reinitialized when resumed
|
|
145
|
+
synchronized(audioRecordLock) {
|
|
146
|
+
if (audioRecord != null) {
|
|
147
|
+
LogUtils.d(CLASS_NAME, "🔄 Releasing current AudioRecord while paused to allow proper reinitialization")
|
|
148
|
+
audioRecord?.release()
|
|
149
|
+
audioRecord = null
|
|
150
|
+
LogUtils.d(CLASS_NAME, "🔄 AudioRecord released successfully")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
LogUtils.d(CLASS_NAME, "🔄 handleDeviceChange: Restarting recording with new device")
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Log current device configuration for debugging
|
|
161
|
+
val deviceInfo = getAudioDeviceInfo()
|
|
162
|
+
LogUtils.d(CLASS_NAME, "🔄 Current device info: ${deviceInfo["id"] ?: "unknown"} (${deviceInfo["type"] ?: "unknown"})")
|
|
163
|
+
|
|
164
|
+
// Make a copy of current recording settings
|
|
165
|
+
val currentSettings = recordingConfig
|
|
166
|
+
|
|
167
|
+
// Pause the current recording
|
|
168
|
+
synchronized(audioRecordLock) {
|
|
169
|
+
if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
|
|
170
|
+
LogUtils.d(CLASS_NAME, "🔄 Stopping current AudioRecord")
|
|
171
|
+
audioRecord!!.stop()
|
|
172
|
+
LogUtils.d(CLASS_NAME, "🔄 AudioRecord stopped")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (compressedRecorder != null) {
|
|
176
|
+
LogUtils.d(CLASS_NAME, "🔄 Pausing compressed recorder")
|
|
177
|
+
compressedRecorder!!.pause()
|
|
178
|
+
LogUtils.d(CLASS_NAME, "🔄 Compressed recorder paused")
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Release the current audio record resources
|
|
183
|
+
synchronized(audioRecordLock) {
|
|
184
|
+
LogUtils.d(CLASS_NAME, "🔄 Releasing current AudioRecord")
|
|
185
|
+
audioRecord?.release()
|
|
186
|
+
audioRecord = null
|
|
187
|
+
LogUtils.d(CLASS_NAME, "🔄 AudioRecord resources released")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Log available devices
|
|
191
|
+
logAvailableDevices()
|
|
192
|
+
|
|
193
|
+
// Give a small delay for the system to fully complete device transition
|
|
194
|
+
LogUtils.d(CLASS_NAME, "🔄 Waiting for device transition to complete")
|
|
195
|
+
Thread.sleep(200)
|
|
196
|
+
|
|
197
|
+
// Initialize a new audio record with the same settings
|
|
198
|
+
LogUtils.d(CLASS_NAME, "🔄 Reinitializing AudioRecord with new device")
|
|
199
|
+
if (!initializeAudioRecord(object : Promise {
|
|
200
|
+
override fun resolve(value: Any?) {
|
|
201
|
+
LogUtils.d(CLASS_NAME, "🔄 Successfully reinitialized AudioRecord with new device")
|
|
202
|
+
}
|
|
203
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
204
|
+
LogUtils.e(CLASS_NAME, "🔄 Failed to reinitialize AudioRecord: $message")
|
|
205
|
+
}
|
|
206
|
+
})) {
|
|
207
|
+
LogUtils.e(CLASS_NAME, "🔄 Failed to reinitialize audio record, stopping recording")
|
|
208
|
+
stopRecording(object : Promise {
|
|
209
|
+
override fun resolve(value: Any?) {
|
|
210
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
211
|
+
"reason" to "deviceSwitchFailed",
|
|
212
|
+
"isPaused" to true
|
|
213
|
+
))
|
|
214
|
+
}
|
|
215
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
216
|
+
})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Re-verify recording state
|
|
221
|
+
synchronized(audioRecordLock) {
|
|
222
|
+
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
|
|
223
|
+
LogUtils.e(CLASS_NAME, "🔄 AudioRecord not properly initialized after device change")
|
|
224
|
+
stopRecording(object : Promise {
|
|
225
|
+
override fun resolve(value: Any?) {
|
|
226
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
227
|
+
"reason" to "deviceSwitchFailed",
|
|
228
|
+
"isPaused" to true
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
232
|
+
})
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Restart the audio record
|
|
238
|
+
synchronized(audioRecordLock) {
|
|
239
|
+
LogUtils.d(CLASS_NAME, "🔄 Starting recording with new device")
|
|
240
|
+
audioRecord?.startRecording()
|
|
241
|
+
LogUtils.d(CLASS_NAME, "🔄 AudioRecord started recording")
|
|
242
|
+
|
|
243
|
+
// Resume compressed recorder if it was active
|
|
244
|
+
if (compressedRecorder != null) {
|
|
245
|
+
LogUtils.d(CLASS_NAME, "🔄 Resuming compressed recorder")
|
|
246
|
+
compressedRecorder!!.resume()
|
|
247
|
+
LogUtils.d(CLASS_NAME, "🔄 Compressed recorder resumed")
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Get new device info
|
|
252
|
+
val newDeviceInfo = getAudioDeviceInfo()
|
|
253
|
+
LogUtils.d(CLASS_NAME, "🔄 New device info: ${newDeviceInfo["id"] ?: "unknown"} (${newDeviceInfo["type"] ?: "unknown"})")
|
|
254
|
+
|
|
255
|
+
// Notify JavaScript
|
|
256
|
+
LogUtils.d(CLASS_NAME, "🔄 Sending device changed event to JavaScript")
|
|
257
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
258
|
+
"reason" to "deviceChanged",
|
|
259
|
+
"isPaused" to false,
|
|
260
|
+
"deviceInfo" to newDeviceInfo
|
|
261
|
+
))
|
|
262
|
+
LogUtils.d(CLASS_NAME, "🔄 Device change handling completed successfully")
|
|
263
|
+
|
|
264
|
+
} catch (e: Exception) {
|
|
265
|
+
LogUtils.e(CLASS_NAME, "🔄 Error handling device change: ${e.message}", e)
|
|
266
|
+
// If something went wrong, try to pause recording
|
|
267
|
+
pauseRecording(object : Promise {
|
|
268
|
+
override fun resolve(value: Any?) {
|
|
269
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
270
|
+
"reason" to "deviceSwitchFailed",
|
|
271
|
+
"isPaused" to true,
|
|
272
|
+
"error" to e.message
|
|
273
|
+
))
|
|
274
|
+
}
|
|
275
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Helper to get info about current audio device
|
|
281
|
+
private fun getAudioDeviceInfo(): Map<String, Any> {
|
|
282
|
+
return try {
|
|
283
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
284
|
+
|
|
285
|
+
// Check if using Bluetooth SCO
|
|
286
|
+
if (audioManager.isBluetoothScoOn) {
|
|
287
|
+
mapOf(
|
|
288
|
+
"id" to (selectedDeviceId ?: "unknown"),
|
|
289
|
+
"type" to "bluetooth",
|
|
290
|
+
"name" to "Bluetooth Headset",
|
|
291
|
+
"isDefault" to false
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
// Check if using wired headset
|
|
295
|
+
else if (audioManager.isWiredHeadsetOn) {
|
|
296
|
+
mapOf(
|
|
297
|
+
"id" to (selectedDeviceId ?: "unknown"),
|
|
298
|
+
"type" to "wired",
|
|
299
|
+
"name" to "Wired Headset",
|
|
300
|
+
"isDefault" to false
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
// Default to built-in mic
|
|
304
|
+
else {
|
|
305
|
+
mapOf(
|
|
306
|
+
"id" to (selectedDeviceId ?: "unknown"),
|
|
307
|
+
"type" to "builtin_mic",
|
|
308
|
+
"name" to "Built-in Microphone",
|
|
309
|
+
"isDefault" to true
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
} catch (e: Exception) {
|
|
313
|
+
LogUtils.e(CLASS_NAME, "Error getting audio device info: ${e.message}", e)
|
|
314
|
+
mapOf(
|
|
315
|
+
"id" to "unknown",
|
|
316
|
+
"type" to "unknown",
|
|
317
|
+
"name" to "Unknown Device",
|
|
318
|
+
"isDefault" to false
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Log available audio devices for debugging
|
|
324
|
+
private fun logAvailableDevices() {
|
|
325
|
+
try {
|
|
326
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
327
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
328
|
+
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
|
|
329
|
+
|
|
330
|
+
LogUtils.d(CLASS_NAME, "Available audio devices (${devices.size}):")
|
|
331
|
+
devices.forEachIndexed { index, device ->
|
|
332
|
+
val name = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
333
|
+
device.productName?.toString() ?: "Unknown"
|
|
334
|
+
} else {
|
|
335
|
+
when (device.type) {
|
|
336
|
+
AudioDeviceInfo.TYPE_BUILTIN_MIC -> "Built-in Microphone"
|
|
337
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth Headset"
|
|
338
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headset"
|
|
339
|
+
AudioDeviceInfo.TYPE_USB_DEVICE -> "USB Audio Device"
|
|
340
|
+
AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Headset"
|
|
341
|
+
else -> "Unknown Device Type (${device.type})"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
LogUtils.d(CLASS_NAME, "Device $index: $name (ID: ${device.id})")
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
349
|
+
LogUtils.d(CLASS_NAME, "Device info on pre-M Android:")
|
|
350
|
+
LogUtils.d(CLASS_NAME, "- Bluetooth SCO: ${audioManager.isBluetoothScoOn}")
|
|
351
|
+
LogUtils.d(CLASS_NAME, "- Wired Headset: ${audioManager.isWiredHeadsetOn}")
|
|
352
|
+
LogUtils.d(CLASS_NAME, "- Selected Device ID: $selectedDeviceId")
|
|
353
|
+
}
|
|
354
|
+
} catch (e: Exception) {
|
|
355
|
+
LogUtils.e(CLASS_NAME, "Error logging available devices: ${e.message}", e)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get the device disconnection behavior
|
|
360
|
+
fun getDeviceDisconnectionBehavior(): String {
|
|
361
|
+
return deviceDisconnectionBehavior ?: "pause" // Default to pause if not specified
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Add isRecording property accessor
|
|
365
|
+
val isRecording: Boolean
|
|
366
|
+
get() = _isRecording.get()
|
|
367
|
+
|
|
92
368
|
private fun initializePhoneStateListener() {
|
|
93
369
|
try {
|
|
94
|
-
|
|
370
|
+
LogUtils.d(CLASS_NAME, "Initializing phone state listener...")
|
|
95
371
|
|
|
96
372
|
if (permissionUtils.checkPhoneStatePermission()) {
|
|
97
|
-
|
|
373
|
+
LogUtils.d(CLASS_NAME, "Phone state permission granted")
|
|
98
374
|
|
|
99
375
|
phoneStateListener = object : PhoneStateListener() {
|
|
100
376
|
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
|
|
@@ -104,49 +380,49 @@ class AudioRecorderManager(
|
|
|
104
380
|
TelephonyManager.CALL_STATE_IDLE -> "IDLE"
|
|
105
381
|
else -> "UNKNOWN"
|
|
106
382
|
}
|
|
107
|
-
|
|
383
|
+
LogUtils.d(CLASS_NAME, "Phone state changed to: $stateStr")
|
|
108
384
|
|
|
109
385
|
when (state) {
|
|
110
386
|
TelephonyManager.CALL_STATE_RINGING,
|
|
111
387
|
TelephonyManager.CALL_STATE_OFFHOOK -> {
|
|
112
|
-
if (
|
|
113
|
-
|
|
388
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
389
|
+
LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
|
|
114
390
|
mainHandler.post {
|
|
115
391
|
pauseRecording(object : Promise {
|
|
116
392
|
override fun resolve(value: Any?) {
|
|
117
|
-
|
|
393
|
+
LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
|
|
118
394
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
119
395
|
"reason" to "phoneCall",
|
|
120
396
|
"isPaused" to true
|
|
121
397
|
))
|
|
122
398
|
}
|
|
123
399
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
124
|
-
|
|
400
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on phone call", cause)
|
|
125
401
|
}
|
|
126
402
|
})
|
|
127
403
|
}
|
|
128
404
|
}
|
|
129
405
|
}
|
|
130
406
|
TelephonyManager.CALL_STATE_IDLE -> {
|
|
131
|
-
if (
|
|
132
|
-
|
|
407
|
+
if (_isRecording.get() && isPaused.get()) {
|
|
408
|
+
LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: ${recordingConfig.autoResumeAfterInterruption})")
|
|
133
409
|
if (recordingConfig.autoResumeAfterInterruption) {
|
|
134
410
|
mainHandler.post {
|
|
135
411
|
resumeRecording(object : Promise {
|
|
136
412
|
override fun resolve(value: Any?) {
|
|
137
|
-
|
|
413
|
+
LogUtils.d(CLASS_NAME, "Successfully resumed recording after call")
|
|
138
414
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
139
415
|
"reason" to "phoneCallEnded",
|
|
140
416
|
"isPaused" to false
|
|
141
417
|
))
|
|
142
418
|
}
|
|
143
419
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
144
|
-
|
|
420
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording after phone call", cause)
|
|
145
421
|
}
|
|
146
422
|
})
|
|
147
423
|
}
|
|
148
424
|
} else {
|
|
149
|
-
|
|
425
|
+
LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
|
|
150
426
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
151
427
|
"reason" to "phoneCallEnded",
|
|
152
428
|
"isPaused" to true
|
|
@@ -161,18 +437,18 @@ class AudioRecorderManager(
|
|
|
161
437
|
if (telephonyManager != null) {
|
|
162
438
|
try {
|
|
163
439
|
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
|
164
|
-
|
|
440
|
+
LogUtils.d(CLASS_NAME, "Successfully registered phone state listener")
|
|
165
441
|
} catch (e: Exception) {
|
|
166
|
-
|
|
442
|
+
LogUtils.e(CLASS_NAME, "Failed to register phone state listener", e)
|
|
167
443
|
}
|
|
168
444
|
} else {
|
|
169
|
-
|
|
445
|
+
LogUtils.e(CLASS_NAME, "TelephonyManager is null, cannot register phone state listener")
|
|
170
446
|
}
|
|
171
447
|
} else {
|
|
172
|
-
|
|
448
|
+
LogUtils.w(CLASS_NAME, "READ_PHONE_STATE permission not granted, phone call interruption handling disabled")
|
|
173
449
|
}
|
|
174
450
|
} catch (e: Exception) {
|
|
175
|
-
|
|
451
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize phone state listener", e)
|
|
176
452
|
}
|
|
177
453
|
}
|
|
178
454
|
|
|
@@ -182,7 +458,7 @@ class AudioRecorderManager(
|
|
|
182
458
|
when (focusChange) {
|
|
183
459
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
184
460
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
185
|
-
if (
|
|
461
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
186
462
|
mainHandler.post {
|
|
187
463
|
pauseRecording(object : Promise {
|
|
188
464
|
override fun resolve(value: Any?) {
|
|
@@ -192,14 +468,14 @@ class AudioRecorderManager(
|
|
|
192
468
|
))
|
|
193
469
|
}
|
|
194
470
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
195
|
-
|
|
471
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on audio focus loss")
|
|
196
472
|
}
|
|
197
473
|
})
|
|
198
474
|
}
|
|
199
475
|
}
|
|
200
476
|
}
|
|
201
477
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
202
|
-
if (
|
|
478
|
+
if (_isRecording.get() && isPaused.get() && recordingConfig.autoResumeAfterInterruption) {
|
|
203
479
|
mainHandler.post {
|
|
204
480
|
resumeRecording(object : Promise {
|
|
205
481
|
override fun resolve(value: Any?) {
|
|
@@ -209,7 +485,7 @@ class AudioRecorderManager(
|
|
|
209
485
|
))
|
|
210
486
|
}
|
|
211
487
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
212
|
-
|
|
488
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording on audio focus gain")
|
|
213
489
|
}
|
|
214
490
|
})
|
|
215
491
|
}
|
|
@@ -219,190 +495,83 @@ class AudioRecorderManager(
|
|
|
219
495
|
}
|
|
220
496
|
}
|
|
221
497
|
|
|
222
|
-
companion object {
|
|
223
|
-
@SuppressLint("StaticFieldLeak")
|
|
224
|
-
@Volatile
|
|
225
|
-
private var instance: AudioRecorderManager? = null
|
|
226
|
-
|
|
227
|
-
fun getInstance(): AudioRecorderManager? = instance
|
|
228
|
-
|
|
229
|
-
fun initialize(
|
|
230
|
-
context: Context,
|
|
231
|
-
filesDir: File,
|
|
232
|
-
permissionUtils: PermissionUtils,
|
|
233
|
-
audioDataEncoder: AudioDataEncoder,
|
|
234
|
-
eventSender: EventSender,
|
|
235
|
-
enablePhoneStateHandling: Boolean = true
|
|
236
|
-
): AudioRecorderManager {
|
|
237
|
-
return instance ?: synchronized(this) {
|
|
238
|
-
instance ?: AudioRecorderManager(
|
|
239
|
-
context, filesDir, permissionUtils, audioDataEncoder, eventSender,
|
|
240
|
-
enablePhoneStateHandling
|
|
241
|
-
).also { instance = it }
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
fun destroy() {
|
|
246
|
-
instance?.cleanup()
|
|
247
|
-
instance = null
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private fun isOngoingCall(): Boolean {
|
|
252
|
-
try {
|
|
253
|
-
if (!permissionUtils.checkPhoneStatePermission()) {
|
|
254
|
-
Log.w(Constants.TAG, "READ_PHONE_STATE permission not granted, cannot check call state")
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
val tm = telephonyManager
|
|
259
|
-
if (tm == null) {
|
|
260
|
-
Log.e(Constants.TAG, "TelephonyManager is null")
|
|
261
|
-
return false
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Get audio manager state
|
|
265
|
-
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
266
|
-
val audioMode = audioManager.mode
|
|
267
|
-
val isMusicActive = audioManager.isMusicActive
|
|
268
|
-
|
|
269
|
-
// Get current audio device info
|
|
270
|
-
val currentRoute =
|
|
271
|
-
audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull()?.type?.let { type ->
|
|
272
|
-
when (type) {
|
|
273
|
-
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "SPEAKER"
|
|
274
|
-
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "EARPIECE"
|
|
275
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "BLUETOOTH_SCO"
|
|
276
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "BLUETOOTH_A2DP"
|
|
277
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET -> "WIRED_HEADSET"
|
|
278
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "WIRED_HEADPHONES"
|
|
279
|
-
else -> "OTHER($type)"
|
|
280
|
-
}
|
|
281
|
-
} ?: "UNKNOWN"
|
|
282
|
-
|
|
283
|
-
// Get communication device info
|
|
284
|
-
val communicationDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
285
|
-
audioManager.communicationDevice?.type?.let { type ->
|
|
286
|
-
when (type) {
|
|
287
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "BLUETOOTH_SCO"
|
|
288
|
-
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "SPEAKER"
|
|
289
|
-
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "EARPIECE"
|
|
290
|
-
else -> "OTHER($type)"
|
|
291
|
-
}
|
|
292
|
-
} ?: "NONE"
|
|
293
|
-
} else {
|
|
294
|
-
@Suppress("DEPRECATION")
|
|
295
|
-
if (audioManager.isBluetoothScoOn) "BLUETOOTH_SCO" else "NONE"
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
Log.d(Constants.TAG, """
|
|
299
|
-
Audio State Check:
|
|
300
|
-
- Audio Mode: ${getAudioModeString(audioMode)}
|
|
301
|
-
- Music Active: $isMusicActive
|
|
302
|
-
- Current Audio Route: $currentRoute
|
|
303
|
-
- Communication Device: $communicationDevice
|
|
304
|
-
""".trimIndent())
|
|
305
|
-
|
|
306
|
-
// Check telephony state
|
|
307
|
-
val callState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
308
|
-
tm.callStateForSubscription
|
|
309
|
-
} else {
|
|
310
|
-
@Suppress("DEPRECATION")
|
|
311
|
-
tm.callState
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
val isVoipCall = audioMode == AudioManager.MODE_IN_COMMUNICATION
|
|
315
|
-
val isRegularCall = callState == TelephonyManager.CALL_STATE_OFFHOOK ||
|
|
316
|
-
callState == TelephonyManager.CALL_STATE_RINGING
|
|
317
|
-
|
|
318
|
-
Log.d(Constants.TAG, """
|
|
319
|
-
Call State Check:
|
|
320
|
-
- Telephony Call State: ${getCallStateString(callState)}
|
|
321
|
-
- VoIP Call Detected: $isVoipCall
|
|
322
|
-
- Regular Call Detected: $isRegularCall
|
|
323
|
-
""".trimIndent())
|
|
324
|
-
|
|
325
|
-
return isVoipCall || isRegularCall
|
|
326
|
-
|
|
327
|
-
} catch (e: SecurityException) {
|
|
328
|
-
Log.e(Constants.TAG, "SecurityException when checking call state", e)
|
|
329
|
-
return false
|
|
330
|
-
} catch (e: Exception) {
|
|
331
|
-
Log.e(Constants.TAG, "Error checking call state", e)
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
private fun getAudioModeString(mode: Int): String = when (mode) {
|
|
337
|
-
AudioManager.MODE_NORMAL -> "MODE_NORMAL"
|
|
338
|
-
AudioManager.MODE_RINGTONE -> "MODE_RINGTONE"
|
|
339
|
-
AudioManager.MODE_IN_CALL -> "MODE_IN_CALL"
|
|
340
|
-
AudioManager.MODE_IN_COMMUNICATION -> "MODE_IN_COMMUNICATION"
|
|
341
|
-
else -> "MODE_UNKNOWN($mode)"
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
private fun getCallStateString(state: Int): String = when (state) {
|
|
345
|
-
TelephonyManager.CALL_STATE_IDLE -> "CALL_STATE_IDLE"
|
|
346
|
-
TelephonyManager.CALL_STATE_RINGING -> "CALL_STATE_RINGING"
|
|
347
|
-
TelephonyManager.CALL_STATE_OFFHOOK -> "CALL_STATE_OFFHOOK"
|
|
348
|
-
else -> "CALL_STATE_UNKNOWN($state)"
|
|
349
|
-
}
|
|
350
|
-
|
|
351
498
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
352
499
|
fun startRecording(options: Map<String, Any?>, promise: Promise) {
|
|
353
500
|
try {
|
|
354
|
-
//
|
|
355
|
-
if (
|
|
356
|
-
|
|
501
|
+
// Check if already recording
|
|
502
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
503
|
+
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
504
|
+
return
|
|
357
505
|
}
|
|
358
|
-
|
|
359
|
-
// Request audio focus
|
|
506
|
+
|
|
507
|
+
// Request audio focus - always do this right before starting
|
|
360
508
|
if (!requestAudioFocus()) {
|
|
361
509
|
promise.reject("AUDIO_FOCUS_ERROR", "Failed to obtain audio focus", null)
|
|
362
510
|
return
|
|
363
511
|
}
|
|
364
512
|
|
|
365
|
-
|
|
513
|
+
// If already prepared, we can skip initialization
|
|
514
|
+
if (!isPrepared) {
|
|
515
|
+
LogUtils.d(CLASS_NAME, "Not prepared, preparing recording first")
|
|
516
|
+
|
|
517
|
+
// Initialize phone state listener only if enabled
|
|
518
|
+
if (enablePhoneStateHandling) {
|
|
519
|
+
initializePhoneStateListener()
|
|
520
|
+
}
|
|
366
521
|
|
|
367
|
-
|
|
368
|
-
if (!checkPermissions(options, promise)) return
|
|
522
|
+
LogUtils.d(CLASS_NAME, "Starting recording with options: $options")
|
|
369
523
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
373
|
-
return
|
|
374
|
-
}
|
|
524
|
+
// Check permissions
|
|
525
|
+
if (!checkPermissions(options, promise)) return
|
|
375
526
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
527
|
+
// Parse recording configuration
|
|
528
|
+
val configResult = RecordingConfig.fromMap(options)
|
|
529
|
+
if (configResult.isFailure) {
|
|
530
|
+
promise.reject(
|
|
531
|
+
"INVALID_CONFIG",
|
|
532
|
+
configResult.exceptionOrNull()?.message ?: "Invalid configuration",
|
|
533
|
+
configResult.exceptionOrNull()
|
|
534
|
+
)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
386
537
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
538
|
+
val (tempRecordingConfig, audioFormatInfo) = configResult.getOrNull()!!
|
|
539
|
+
|
|
540
|
+
recordingConfig = tempRecordingConfig
|
|
541
|
+
|
|
542
|
+
// Store device-related settings
|
|
543
|
+
selectedDeviceId = recordingConfig.deviceId
|
|
544
|
+
deviceDisconnectionBehavior = recordingConfig.deviceDisconnectionBehavior ?: "pause"
|
|
545
|
+
|
|
546
|
+
audioFormat = audioFormatInfo.format
|
|
547
|
+
mimeType = audioFormatInfo.mimeType
|
|
393
548
|
|
|
394
|
-
|
|
549
|
+
if (!initializeAudioFormat(promise)) return
|
|
395
550
|
|
|
396
|
-
|
|
551
|
+
if (!initializeBufferSize(promise)) return
|
|
397
552
|
|
|
398
|
-
|
|
553
|
+
if (!initializeAudioRecord(promise)) return
|
|
399
554
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
555
|
+
if (recordingConfig.enableCompressedOutput && !initializeCompressedRecorder(
|
|
556
|
+
if (recordingConfig.compressedFormat == "aac") "aac" else "opus",
|
|
557
|
+
promise
|
|
558
|
+
)) return
|
|
404
559
|
|
|
405
|
-
|
|
560
|
+
if (!initializeRecordingResources(audioFormatInfo.fileExtension, promise)) return
|
|
561
|
+
} else {
|
|
562
|
+
LogUtils.d(CLASS_NAME, "Using prepared recording state")
|
|
563
|
+
|
|
564
|
+
// Even when prepared, update device settings from the new options
|
|
565
|
+
val configResult = RecordingConfig.fromMap(options)
|
|
566
|
+
if (configResult.isSuccess) {
|
|
567
|
+
val (tempRecordingConfig, _) = configResult.getOrNull()!!
|
|
568
|
+
// Update device-related settings
|
|
569
|
+
selectedDeviceId = tempRecordingConfig.deviceId ?: selectedDeviceId
|
|
570
|
+
deviceDisconnectionBehavior = tempRecordingConfig.deviceDisconnectionBehavior
|
|
571
|
+
?: deviceDisconnectionBehavior
|
|
572
|
+
?: "pause"
|
|
573
|
+
}
|
|
574
|
+
}
|
|
406
575
|
|
|
407
576
|
if (!startRecordingProcess(promise)) return
|
|
408
577
|
|
|
@@ -410,7 +579,7 @@ class AudioRecorderManager(
|
|
|
410
579
|
try {
|
|
411
580
|
compressedRecorder?.start()
|
|
412
581
|
} catch (e: Exception) {
|
|
413
|
-
|
|
582
|
+
LogUtils.e(CLASS_NAME, "Failed to start compressed recording", e)
|
|
414
583
|
cleanup()
|
|
415
584
|
promise.reject("COMPRESSED_START_FAILED", "Failed to start compressed recording", e)
|
|
416
585
|
return
|
|
@@ -441,7 +610,7 @@ class AudioRecorderManager(
|
|
|
441
610
|
}
|
|
442
611
|
|
|
443
612
|
private fun isAudioFormatSupported(sampleRate: Int, channels: Int, format: Int): Boolean {
|
|
444
|
-
if (!permissionUtils.checkRecordingPermission()) {
|
|
613
|
+
if (!permissionUtils.checkRecordingPermission(enableBackgroundAudio)) {
|
|
445
614
|
throw SecurityException("Recording permission has not been granted")
|
|
446
615
|
}
|
|
447
616
|
|
|
@@ -477,7 +646,7 @@ class AudioRecorderManager(
|
|
|
477
646
|
}
|
|
478
647
|
|
|
479
648
|
private fun checkPermissions(options: Map<String, Any?>, promise: Promise): Boolean {
|
|
480
|
-
if (!permissionUtils.checkRecordingPermission()) {
|
|
649
|
+
if (!permissionUtils.checkRecordingPermission(enableBackgroundAudio)) {
|
|
481
650
|
promise.reject(
|
|
482
651
|
"PERMISSION_DENIED",
|
|
483
652
|
"Recording permission has not been granted",
|
|
@@ -488,7 +657,7 @@ class AudioRecorderManager(
|
|
|
488
657
|
|
|
489
658
|
// Only check phone state permission if enabled
|
|
490
659
|
if (enablePhoneStateHandling && !permissionUtils.checkPhoneStatePermission()) {
|
|
491
|
-
|
|
660
|
+
LogUtils.w(CLASS_NAME, "READ_PHONE_STATE permission not granted, phone call interruption handling will be disabled")
|
|
492
661
|
// Don't reject here, just log warning as this is optional
|
|
493
662
|
}
|
|
494
663
|
|
|
@@ -513,7 +682,7 @@ class AudioRecorderManager(
|
|
|
513
682
|
audioFormat
|
|
514
683
|
)
|
|
515
684
|
) {
|
|
516
|
-
|
|
685
|
+
LogUtils.e(CLASS_NAME, "Selected audio format not supported, falling back to 16-bit PCM")
|
|
517
686
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT
|
|
518
687
|
|
|
519
688
|
if (!isAudioFormatSupported(
|
|
@@ -551,7 +720,7 @@ class AudioRecorderManager(
|
|
|
551
720
|
|
|
552
721
|
when {
|
|
553
722
|
bufferSizeInBytes == AudioRecord.ERROR -> {
|
|
554
|
-
|
|
723
|
+
LogUtils.e(CLASS_NAME, "Error getting minimum buffer size: ERROR")
|
|
555
724
|
promise.reject(
|
|
556
725
|
"BUFFER_SIZE_ERROR",
|
|
557
726
|
"Failed to get minimum buffer size: generic error",
|
|
@@ -560,7 +729,7 @@ class AudioRecorderManager(
|
|
|
560
729
|
return false
|
|
561
730
|
}
|
|
562
731
|
bufferSizeInBytes == AudioRecord.ERROR_BAD_VALUE -> {
|
|
563
|
-
|
|
732
|
+
LogUtils.e(CLASS_NAME, "Error getting minimum buffer size: BAD_VALUE")
|
|
564
733
|
promise.reject(
|
|
565
734
|
"BUFFER_SIZE_ERROR",
|
|
566
735
|
"Failed to get minimum buffer size: invalid parameters",
|
|
@@ -569,7 +738,7 @@ class AudioRecorderManager(
|
|
|
569
738
|
return false
|
|
570
739
|
}
|
|
571
740
|
bufferSizeInBytes <= 0 -> {
|
|
572
|
-
|
|
741
|
+
LogUtils.e(CLASS_NAME, "Invalid buffer size: $bufferSizeInBytes")
|
|
573
742
|
promise.reject(
|
|
574
743
|
"BUFFER_SIZE_ERROR",
|
|
575
744
|
"Failed to get valid buffer size",
|
|
@@ -578,12 +747,12 @@ class AudioRecorderManager(
|
|
|
578
747
|
return false
|
|
579
748
|
}
|
|
580
749
|
else -> {
|
|
581
|
-
|
|
750
|
+
LogUtils.d(CLASS_NAME, "AudioFormat: $audioFormat, BufferSize: $bufferSizeInBytes")
|
|
582
751
|
return true
|
|
583
752
|
}
|
|
584
753
|
}
|
|
585
754
|
} catch (e: Exception) {
|
|
586
|
-
|
|
755
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize buffer size", e)
|
|
587
756
|
promise.reject(
|
|
588
757
|
"BUFFER_SIZE_ERROR",
|
|
589
758
|
"Failed to initialize buffer size: ${e.message}",
|
|
@@ -595,7 +764,7 @@ class AudioRecorderManager(
|
|
|
595
764
|
|
|
596
765
|
|
|
597
766
|
private fun initializeAudioRecord(promise: Promise): Boolean {
|
|
598
|
-
if (!permissionUtils.checkRecordingPermission()) {
|
|
767
|
+
if (!permissionUtils.checkRecordingPermission(enableBackgroundAudio)) {
|
|
599
768
|
promise.reject(
|
|
600
769
|
"PERMISSION_DENIED",
|
|
601
770
|
"Recording permission has not been granted",
|
|
@@ -606,7 +775,7 @@ class AudioRecorderManager(
|
|
|
606
775
|
|
|
607
776
|
try {
|
|
608
777
|
if (audioRecord == null || !isPaused.get()) {
|
|
609
|
-
|
|
778
|
+
LogUtils.d(CLASS_NAME, "Initializing AudioRecord with format: $audioFormat, BufferSize: $bufferSizeInBytes")
|
|
610
779
|
|
|
611
780
|
audioRecord = AudioRecord(
|
|
612
781
|
MediaRecorder.AudioSource.MIC,
|
|
@@ -628,7 +797,7 @@ class AudioRecorderManager(
|
|
|
628
797
|
return true
|
|
629
798
|
|
|
630
799
|
} catch (e: SecurityException) {
|
|
631
|
-
|
|
800
|
+
LogUtils.e(CLASS_NAME, "Security exception while initializing AudioRecord", e)
|
|
632
801
|
promise.reject(
|
|
633
802
|
"PERMISSION_DENIED",
|
|
634
803
|
"Recording permission denied: ${e.message}",
|
|
@@ -636,7 +805,7 @@ class AudioRecorderManager(
|
|
|
636
805
|
)
|
|
637
806
|
return false
|
|
638
807
|
} catch (e: Exception) {
|
|
639
|
-
|
|
808
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize AudioRecord", e)
|
|
640
809
|
promise.reject(
|
|
641
810
|
"INITIALIZATION_FAILED",
|
|
642
811
|
"Failed to initialize the audio recorder: ${e.message}",
|
|
@@ -677,7 +846,7 @@ class AudioRecorderManager(
|
|
|
677
846
|
return false
|
|
678
847
|
} catch (e: Exception) {
|
|
679
848
|
releaseWakeLock()
|
|
680
|
-
|
|
849
|
+
LogUtils.e(CLASS_NAME, "Unexpected error in startRecording", e)
|
|
681
850
|
promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
|
|
682
851
|
return false
|
|
683
852
|
}
|
|
@@ -686,7 +855,7 @@ class AudioRecorderManager(
|
|
|
686
855
|
private fun startRecordingProcess(promise: Promise): Boolean {
|
|
687
856
|
try {
|
|
688
857
|
// Add detailed logging of recording configuration
|
|
689
|
-
|
|
858
|
+
LogUtils.d(CLASS_NAME, """
|
|
690
859
|
Starting audio recording with configuration:
|
|
691
860
|
- Sample Rate: ${recordingConfig.sampleRate} Hz
|
|
692
861
|
- Channels: ${recordingConfig.channels}
|
|
@@ -710,7 +879,7 @@ class AudioRecorderManager(
|
|
|
710
879
|
|
|
711
880
|
audioRecord?.startRecording()
|
|
712
881
|
isPaused.set(false)
|
|
713
|
-
|
|
882
|
+
_isRecording.set(true)
|
|
714
883
|
isFirstChunk = true
|
|
715
884
|
|
|
716
885
|
if (!isPaused.get()) {
|
|
@@ -727,7 +896,7 @@ class AudioRecorderManager(
|
|
|
727
896
|
return true
|
|
728
897
|
|
|
729
898
|
} catch (e: Exception) {
|
|
730
|
-
|
|
899
|
+
LogUtils.e(CLASS_NAME, "Failed to start recording", e)
|
|
731
900
|
cleanup()
|
|
732
901
|
promise.reject("START_FAILED", "Failed to start recording: ${e.message}", e)
|
|
733
902
|
return false
|
|
@@ -736,8 +905,8 @@ class AudioRecorderManager(
|
|
|
736
905
|
|
|
737
906
|
fun stopRecording(promise: Promise) {
|
|
738
907
|
synchronized(audioRecordLock) {
|
|
739
|
-
if (!
|
|
740
|
-
|
|
908
|
+
if (!_isRecording.get()) {
|
|
909
|
+
LogUtils.e(CLASS_NAME, "Recording is not active")
|
|
741
910
|
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
742
911
|
return
|
|
743
912
|
}
|
|
@@ -756,25 +925,26 @@ class AudioRecorderManager(
|
|
|
756
925
|
AudioRecordingService.stopService(context)
|
|
757
926
|
}
|
|
758
927
|
|
|
759
|
-
|
|
928
|
+
_isRecording.set(false)
|
|
929
|
+
isPrepared = false // Reset preparation state
|
|
760
930
|
recordingThread?.join(1000)
|
|
761
931
|
|
|
762
932
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
763
933
|
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
764
|
-
|
|
934
|
+
LogUtils.d(CLASS_NAME, "Last Read $bytesRead bytes")
|
|
765
935
|
if (bytesRead > 0) {
|
|
766
936
|
emitAudioData(audioData.copyOfRange(0, bytesRead), bytesRead)
|
|
767
937
|
}
|
|
768
938
|
|
|
769
|
-
|
|
939
|
+
LogUtils.d(CLASS_NAME, "Stopping recording state = ${audioRecord?.state}")
|
|
770
940
|
if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
|
|
771
|
-
|
|
941
|
+
LogUtils.d(CLASS_NAME, "Stopping AudioRecord")
|
|
772
942
|
audioRecord!!.stop()
|
|
773
943
|
}
|
|
774
944
|
|
|
775
945
|
cleanup()
|
|
776
946
|
} catch (e: IllegalStateException) {
|
|
777
|
-
|
|
947
|
+
LogUtils.e(CLASS_NAME, "Error reading from AudioRecord", e)
|
|
778
948
|
} finally {
|
|
779
949
|
releaseWakeLock()
|
|
780
950
|
audioRecord?.release()
|
|
@@ -785,7 +955,7 @@ class AudioRecorderManager(
|
|
|
785
955
|
audioProcessor.resetCumulativeAmplitudeRange()
|
|
786
956
|
|
|
787
957
|
val fileSize = audioFile?.length() ?: 0
|
|
788
|
-
|
|
958
|
+
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes, Path: ${audioFile?.absolutePath}")
|
|
789
959
|
|
|
790
960
|
val dataFileSize = fileSize - 44 // Subtract header size
|
|
791
961
|
val byteRate =
|
|
@@ -806,7 +976,7 @@ class AudioRecorderManager(
|
|
|
806
976
|
// Log compressed file status if enabled
|
|
807
977
|
if (recordingConfig.enableCompressedOutput) {
|
|
808
978
|
val compressedSize = compressedFile?.length() ?: 0
|
|
809
|
-
|
|
979
|
+
LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
|
|
810
980
|
}
|
|
811
981
|
|
|
812
982
|
val result = bundleOf(
|
|
@@ -830,12 +1000,12 @@ class AudioRecorderManager(
|
|
|
830
1000
|
promise.resolve(result)
|
|
831
1001
|
|
|
832
1002
|
// Reset the timing variables
|
|
833
|
-
|
|
1003
|
+
_isRecording.set(false)
|
|
834
1004
|
isPaused.set(false)
|
|
835
1005
|
totalRecordedTime = 0
|
|
836
1006
|
pausedDuration = 0
|
|
837
1007
|
} catch (e: Exception) {
|
|
838
|
-
|
|
1008
|
+
LogUtils.e(CLASS_NAME, "Failed to stop recording: ${e.message}")
|
|
839
1009
|
promise.reject("STOP_FAILED", "Failed to stop recording", e)
|
|
840
1010
|
} finally {
|
|
841
1011
|
audioRecord = null
|
|
@@ -844,18 +1014,51 @@ class AudioRecorderManager(
|
|
|
844
1014
|
}
|
|
845
1015
|
|
|
846
1016
|
fun resumeRecording(promise: Promise) {
|
|
1017
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording method entered - isPaused=${isPaused.get()}, isRecording=${_isRecording.get()}")
|
|
847
1018
|
if (!isPaused.get()) {
|
|
1019
|
+
LogUtils.e(CLASS_NAME, "⏺️ Cannot resume recording: not paused")
|
|
848
1020
|
promise.reject("NOT_PAUSED", "Recording is not paused", null)
|
|
849
1021
|
return
|
|
850
1022
|
}
|
|
851
1023
|
|
|
852
1024
|
if (isOngoingCall()) {
|
|
1025
|
+
LogUtils.e(CLASS_NAME, "⏺️ Cannot resume recording: ongoing call detected")
|
|
853
1026
|
promise.reject("ONGOING_CALL", "Cannot resume recording during an ongoing call", null)
|
|
854
1027
|
return
|
|
855
1028
|
}
|
|
856
1029
|
|
|
857
1030
|
try {
|
|
1031
|
+
// Check if audioRecord needs reinitializing
|
|
1032
|
+
var needsReinitialize = false
|
|
1033
|
+
synchronized(audioRecordLock) {
|
|
1034
|
+
LogUtils.d(CLASS_NAME, "⏺️ Checking audioRecord state: ${audioRecord?.state ?: "null"}")
|
|
1035
|
+
if (audioRecord == null || audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
|
|
1036
|
+
LogUtils.d(CLASS_NAME, "⏺️ AudioRecord is null or not properly initialized, will reinitialize")
|
|
1037
|
+
needsReinitialize = true
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Reinitialize audioRecord if needed (like after device disconnection)
|
|
1042
|
+
if (needsReinitialize) {
|
|
1043
|
+
LogUtils.d(CLASS_NAME, "⏺️ Starting reinitialization of AudioRecord for resumption after disconnection")
|
|
1044
|
+
if (!initializeAudioRecord(object : Promise {
|
|
1045
|
+
override fun resolve(value: Any?) {
|
|
1046
|
+
LogUtils.d(CLASS_NAME, "⏺️ Successfully reinitialized AudioRecord for resumption")
|
|
1047
|
+
}
|
|
1048
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1049
|
+
LogUtils.e(CLASS_NAME, "⏺️ Failed to reinitialize AudioRecord: $message")
|
|
1050
|
+
// We'll let the main try-catch handle this error
|
|
1051
|
+
throw IllegalStateException("Failed to reinitialize AudioRecord: $message")
|
|
1052
|
+
}
|
|
1053
|
+
})) {
|
|
1054
|
+
LogUtils.e(CLASS_NAME, "⏺️ Failed to reinitialize AudioRecord")
|
|
1055
|
+
throw IllegalStateException("Failed to reinitialize AudioRecord for resumption")
|
|
1056
|
+
}
|
|
1057
|
+
LogUtils.d(CLASS_NAME, "⏺️ Reinitialization completed successfully")
|
|
1058
|
+
}
|
|
1059
|
+
|
|
858
1060
|
if (recordingConfig.showNotification) {
|
|
1061
|
+
LogUtils.d(CLASS_NAME, "⏺️ Resuming notification updates")
|
|
859
1062
|
notificationManager.resumeUpdates()
|
|
860
1063
|
}
|
|
861
1064
|
|
|
@@ -863,18 +1066,36 @@ class AudioRecorderManager(
|
|
|
863
1066
|
pausedDuration += System.currentTimeMillis() - lastPauseTime
|
|
864
1067
|
isPaused.set(false)
|
|
865
1068
|
|
|
866
|
-
|
|
867
|
-
|
|
1069
|
+
synchronized(audioRecordLock) {
|
|
1070
|
+
// Double-check audioRecord is valid after potential reinitialization
|
|
1071
|
+
LogUtils.d(CLASS_NAME, "⏺️ Final check of audioRecord state: ${audioRecord?.state ?: "null"}")
|
|
1072
|
+
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
|
|
1073
|
+
LogUtils.e(CLASS_NAME, "⏺️ AudioRecord is not properly initialized")
|
|
1074
|
+
throw IllegalStateException("AudioRecord is not properly initialized")
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
LogUtils.d(CLASS_NAME, "⏺️ Starting AudioRecord recording")
|
|
1078
|
+
audioRecord?.startRecording()
|
|
1079
|
+
LogUtils.d(CLASS_NAME, "⏺️ AudioRecord.startRecording called")
|
|
1080
|
+
|
|
1081
|
+
if (compressedRecorder != null) {
|
|
1082
|
+
LogUtils.d(CLASS_NAME, "⏺️ Resuming compressed recorder")
|
|
1083
|
+
compressedRecorder?.resume()
|
|
1084
|
+
LogUtils.d(CLASS_NAME, "⏺️ Compressed recorder resumed")
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
868
1087
|
|
|
1088
|
+
LogUtils.d(CLASS_NAME, "⏺️ Recording resumed successfully")
|
|
869
1089
|
promise.resolve("Recording resumed")
|
|
870
1090
|
} catch (e: Exception) {
|
|
1091
|
+
LogUtils.e(CLASS_NAME, "⏺️ Failed to resume recording: ${e.message}", e)
|
|
871
1092
|
releaseWakeLock()
|
|
872
|
-
promise.reject("RESUME_FAILED", "Failed to resume recording", e)
|
|
1093
|
+
promise.reject("RESUME_FAILED", "Failed to resume recording: ${e.message}", e)
|
|
873
1094
|
}
|
|
874
1095
|
}
|
|
875
1096
|
|
|
876
1097
|
fun pauseRecording(promise: Promise) {
|
|
877
|
-
if (
|
|
1098
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
878
1099
|
audioRecord?.stop()
|
|
879
1100
|
compressedRecorder?.pause()
|
|
880
1101
|
|
|
@@ -906,14 +1127,14 @@ class AudioRecorderManager(
|
|
|
906
1127
|
} ?: false
|
|
907
1128
|
|
|
908
1129
|
// If service is running but we think we're not recording, clean up
|
|
909
|
-
if (isServiceRunning && !
|
|
910
|
-
|
|
1130
|
+
if (isServiceRunning && !_isRecording.get()) {
|
|
1131
|
+
LogUtils.d(CLASS_NAME, "Detected orphaned recording service, cleaning up...")
|
|
911
1132
|
cleanup()
|
|
912
1133
|
AudioRecordingService.stopService(context)
|
|
913
1134
|
}
|
|
914
1135
|
|
|
915
|
-
if (!
|
|
916
|
-
|
|
1136
|
+
if (!_isRecording.get()) {
|
|
1137
|
+
LogUtils.d(CLASS_NAME, "Not recording --- skip status with default values")
|
|
917
1138
|
return bundleOf(
|
|
918
1139
|
"isRecording" to false,
|
|
919
1140
|
"isPaused" to false,
|
|
@@ -945,7 +1166,7 @@ class AudioRecorderManager(
|
|
|
945
1166
|
|
|
946
1167
|
return bundleOf(
|
|
947
1168
|
"durationMs" to duration,
|
|
948
|
-
"isRecording" to
|
|
1169
|
+
"isRecording" to _isRecording.get(),
|
|
949
1170
|
"isPaused" to isPaused.get(),
|
|
950
1171
|
"mimeType" to mimeType,
|
|
951
1172
|
"size" to totalDataSize,
|
|
@@ -967,9 +1188,9 @@ class AudioRecorderManager(
|
|
|
967
1188
|
acquire()
|
|
968
1189
|
}
|
|
969
1190
|
wasWakeLockEnabled = true
|
|
970
|
-
|
|
1191
|
+
LogUtils.d(CLASS_NAME, "Wake lock acquired")
|
|
971
1192
|
} catch (e: Exception) {
|
|
972
|
-
|
|
1193
|
+
LogUtils.e(CLASS_NAME, "Failed to acquire wake lock", e)
|
|
973
1194
|
}
|
|
974
1195
|
}
|
|
975
1196
|
}
|
|
@@ -980,13 +1201,45 @@ class AudioRecorderManager(
|
|
|
980
1201
|
wakeLock?.let {
|
|
981
1202
|
if (it.isHeld) {
|
|
982
1203
|
it.release()
|
|
983
|
-
|
|
1204
|
+
LogUtils.d(CLASS_NAME, "Wake lock released")
|
|
984
1205
|
}
|
|
985
1206
|
wakeLock = null
|
|
986
1207
|
wasWakeLockEnabled = false
|
|
987
1208
|
}
|
|
988
1209
|
} catch (e: Exception) {
|
|
989
|
-
|
|
1210
|
+
LogUtils.e(CLASS_NAME, "Failed to release wake lock", e)
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Checks if there is an ongoing call that would interfere with recording
|
|
1216
|
+
*/
|
|
1217
|
+
private fun isOngoingCall(): Boolean {
|
|
1218
|
+
try {
|
|
1219
|
+
if (telephonyManager == null) return false
|
|
1220
|
+
|
|
1221
|
+
// Get phone call state directly from telephonyManager instead of
|
|
1222
|
+
// relying on audio manager state which could be misleading after device disconnection
|
|
1223
|
+
val callState = telephonyManager?.callState
|
|
1224
|
+
|
|
1225
|
+
LogUtils.d(CLASS_NAME, "Call state check: callState=${callState}, " +
|
|
1226
|
+
"audioManager.mode=${audioManager.mode}, " +
|
|
1227
|
+
"audioManager.isBluetoothScoOn=${audioManager.isBluetoothScoOn}")
|
|
1228
|
+
|
|
1229
|
+
// Trust phone state more than audio manager state
|
|
1230
|
+
if (callState == TelephonyManager.CALL_STATE_RINGING ||
|
|
1231
|
+
callState == TelephonyManager.CALL_STATE_OFFHOOK) {
|
|
1232
|
+
return true
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Only check audio manager mode as secondary indicator
|
|
1236
|
+
return audioManager.mode == AudioManager.MODE_IN_CALL ||
|
|
1237
|
+
audioManager.mode == AudioManager.MODE_IN_COMMUNICATION
|
|
1238
|
+
|
|
1239
|
+
// Remove audioManager.isBluetoothScoOn check as it can be erroneously true after disconnection
|
|
1240
|
+
} catch (e: Exception) {
|
|
1241
|
+
LogUtils.e(CLASS_NAME, "Error checking call state: ${e.message}")
|
|
1242
|
+
return false
|
|
990
1243
|
}
|
|
991
1244
|
}
|
|
992
1245
|
|
|
@@ -1003,11 +1256,11 @@ class AudioRecorderManager(
|
|
|
1003
1256
|
|
|
1004
1257
|
private fun recordingProcess() {
|
|
1005
1258
|
try {
|
|
1006
|
-
|
|
1259
|
+
LogUtils.i(CLASS_NAME, "Starting recording process...")
|
|
1007
1260
|
FileOutputStream(audioFile, true).use { fos ->
|
|
1008
1261
|
// Write audio data directly to the file
|
|
1009
1262
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
1010
|
-
|
|
1263
|
+
LogUtils.d(CLASS_NAME, "Entering recording loop")
|
|
1011
1264
|
|
|
1012
1265
|
// Buffer to accumulate data
|
|
1013
1266
|
val accumulatedAudioData = ByteArrayOutputStream()
|
|
@@ -1031,7 +1284,7 @@ class AudioRecorderManager(
|
|
|
1031
1284
|
var shouldProcessAnalysis = false
|
|
1032
1285
|
|
|
1033
1286
|
// Debug log for intervals
|
|
1034
|
-
|
|
1287
|
+
LogUtils.d(CLASS_NAME, """
|
|
1035
1288
|
Recording process started with intervals:
|
|
1036
1289
|
- Data emission interval: ${recordingConfig.interval}ms
|
|
1037
1290
|
- Analysis interval: ${recordingConfig.intervalAnalysis}ms
|
|
@@ -1039,7 +1292,7 @@ class AudioRecorderManager(
|
|
|
1039
1292
|
""".trimIndent())
|
|
1040
1293
|
|
|
1041
1294
|
// Recording loop
|
|
1042
|
-
while (
|
|
1295
|
+
while (_isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
1043
1296
|
if (isPaused.get()) {
|
|
1044
1297
|
Thread.sleep(100) // Add small delay when paused
|
|
1045
1298
|
continue
|
|
@@ -1053,12 +1306,12 @@ class AudioRecorderManager(
|
|
|
1053
1306
|
val bytesRead = synchronized(audioRecordLock) {
|
|
1054
1307
|
audioRecord?.let {
|
|
1055
1308
|
if (it.state != AudioRecord.STATE_INITIALIZED) {
|
|
1056
|
-
|
|
1309
|
+
LogUtils.e(CLASS_NAME, "AudioRecord not initialized")
|
|
1057
1310
|
return@let -1
|
|
1058
1311
|
}
|
|
1059
1312
|
it.read(audioData, 0, bufferSizeInBytes).also { bytes ->
|
|
1060
1313
|
if (bytes < 0) {
|
|
1061
|
-
|
|
1314
|
+
LogUtils.e(CLASS_NAME, "AudioRecord read error: $bytes")
|
|
1062
1315
|
}
|
|
1063
1316
|
}
|
|
1064
1317
|
} ?: -1 // Handle null case
|
|
@@ -1083,7 +1336,7 @@ class AudioRecorderManager(
|
|
|
1083
1336
|
// Handle analysis emission separately
|
|
1084
1337
|
if (shouldProcessAnalysis) {
|
|
1085
1338
|
val analysisDataSize = accumulatedAnalysisData.size()
|
|
1086
|
-
|
|
1339
|
+
LogUtils.d(CLASS_NAME, """
|
|
1087
1340
|
Processing analysis data:
|
|
1088
1341
|
- Time since last: ${currentTime - lastEmissionTimeAnalysis}ms
|
|
1089
1342
|
- Configured interval: ${recordingConfig.intervalAnalysis}ms
|
|
@@ -1100,7 +1353,7 @@ class AudioRecorderManager(
|
|
|
1100
1353
|
recordingConfig
|
|
1101
1354
|
)
|
|
1102
1355
|
|
|
1103
|
-
|
|
1356
|
+
LogUtils.d(CLASS_NAME, """
|
|
1104
1357
|
Analysis data details:
|
|
1105
1358
|
- Raw data size: ${accumulatedAnalysisData.size()} bytes
|
|
1106
1359
|
""".trimIndent())
|
|
@@ -1112,7 +1365,7 @@ class AudioRecorderManager(
|
|
|
1112
1365
|
analysisData.toBundle()
|
|
1113
1366
|
)
|
|
1114
1367
|
} catch (e: Exception) {
|
|
1115
|
-
|
|
1368
|
+
LogUtils.e(CLASS_NAME, "Failed to send audio analysis event", e)
|
|
1116
1369
|
}
|
|
1117
1370
|
}
|
|
1118
1371
|
|
|
@@ -1135,7 +1388,7 @@ class AudioRecorderManager(
|
|
|
1135
1388
|
if (!isPaused.get()) {
|
|
1136
1389
|
releaseWakeLock()
|
|
1137
1390
|
}
|
|
1138
|
-
|
|
1391
|
+
LogUtils.e(CLASS_NAME, "Error in recording process", e)
|
|
1139
1392
|
}
|
|
1140
1393
|
}
|
|
1141
1394
|
|
|
@@ -1164,7 +1417,7 @@ class AudioRecorderManager(
|
|
|
1164
1417
|
audioDataEncoder.encodeToBase64(buffer)
|
|
1165
1418
|
}
|
|
1166
1419
|
} catch (e: Exception) {
|
|
1167
|
-
|
|
1420
|
+
LogUtils.e(CLASS_NAME, "Failed to read compressed data", e)
|
|
1168
1421
|
null
|
|
1169
1422
|
}
|
|
1170
1423
|
} else null
|
|
@@ -1196,7 +1449,7 @@ class AudioRecorderManager(
|
|
|
1196
1449
|
)
|
|
1197
1450
|
)
|
|
1198
1451
|
} catch (e: Exception) {
|
|
1199
|
-
|
|
1452
|
+
LogUtils.e(CLASS_NAME, "Failed to send event", e)
|
|
1200
1453
|
}
|
|
1201
1454
|
}
|
|
1202
1455
|
|
|
@@ -1266,14 +1519,15 @@ class AudioRecorderManager(
|
|
|
1266
1519
|
fun cleanup() {
|
|
1267
1520
|
synchronized(audioRecordLock) {
|
|
1268
1521
|
try {
|
|
1269
|
-
if (
|
|
1522
|
+
if (_isRecording.get()) {
|
|
1270
1523
|
audioRecord?.stop()
|
|
1271
1524
|
compressedRecorder?.stop()
|
|
1272
1525
|
compressedRecorder?.release()
|
|
1273
1526
|
}
|
|
1274
1527
|
|
|
1275
|
-
|
|
1528
|
+
_isRecording.set(false)
|
|
1276
1529
|
isPaused.set(false)
|
|
1530
|
+
isPrepared = false // Reset prepared state
|
|
1277
1531
|
|
|
1278
1532
|
if (recordingConfig.showNotification) {
|
|
1279
1533
|
notificationManager.stopUpdates()
|
|
@@ -1302,7 +1556,7 @@ class AudioRecorderManager(
|
|
|
1302
1556
|
"isPaused" to false
|
|
1303
1557
|
))
|
|
1304
1558
|
} catch (e: Exception) {
|
|
1305
|
-
|
|
1559
|
+
LogUtils.e(CLASS_NAME, "Error during cleanup", e)
|
|
1306
1560
|
}
|
|
1307
1561
|
}
|
|
1308
1562
|
}
|
|
@@ -1336,7 +1590,7 @@ class AudioRecorderManager(
|
|
|
1336
1590
|
}
|
|
1337
1591
|
return true
|
|
1338
1592
|
} catch (e: Exception) {
|
|
1339
|
-
|
|
1593
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize compressed recorder", e)
|
|
1340
1594
|
promise.reject("COMPRESSED_INIT_FAILED", "Failed to initialize compressed recorder", e)
|
|
1341
1595
|
return false
|
|
1342
1596
|
}
|
|
@@ -1348,7 +1602,7 @@ class AudioRecorderManager(
|
|
|
1348
1602
|
when (focusChange) {
|
|
1349
1603
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
1350
1604
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1351
|
-
if (
|
|
1605
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
1352
1606
|
mainHandler.post {
|
|
1353
1607
|
pauseRecording(object : Promise {
|
|
1354
1608
|
override fun resolve(value: Any?) {
|
|
@@ -1359,14 +1613,14 @@ class AudioRecorderManager(
|
|
|
1359
1613
|
))
|
|
1360
1614
|
}
|
|
1361
1615
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1362
|
-
|
|
1616
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on audio focus loss")
|
|
1363
1617
|
}
|
|
1364
1618
|
})
|
|
1365
1619
|
}
|
|
1366
1620
|
}
|
|
1367
1621
|
}
|
|
1368
1622
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1369
|
-
if (
|
|
1623
|
+
if (_isRecording.get() && isPaused.get() && recordingConfig.autoResumeAfterInterruption) {
|
|
1370
1624
|
mainHandler.post {
|
|
1371
1625
|
resumeRecording(object : Promise {
|
|
1372
1626
|
override fun resolve(value: Any?) {
|
|
@@ -1376,7 +1630,7 @@ class AudioRecorderManager(
|
|
|
1376
1630
|
))
|
|
1377
1631
|
}
|
|
1378
1632
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1379
|
-
|
|
1633
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording on audio focus gain")
|
|
1380
1634
|
}
|
|
1381
1635
|
})
|
|
1382
1636
|
}
|
|
@@ -1444,4 +1698,76 @@ class AudioRecorderManager(
|
|
|
1444
1698
|
fun getKeepAwakeStatus(): Boolean {
|
|
1445
1699
|
return recordingConfig?.keepAwake ?: true
|
|
1446
1700
|
}
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* Prepares audio recording with all initial setup but without starting.
|
|
1704
|
+
* This reuses the existing validation and setup functions for compatibility.
|
|
1705
|
+
*/
|
|
1706
|
+
fun prepareRecording(options: Map<String, Any?>): Boolean {
|
|
1707
|
+
if (_isRecording.get()) {
|
|
1708
|
+
LogUtils.d(CLASS_NAME, "Cannot prepare recording - already recording")
|
|
1709
|
+
return false
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (isPrepared) {
|
|
1713
|
+
LogUtils.d(CLASS_NAME, "Already prepared")
|
|
1714
|
+
return true
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
try {
|
|
1718
|
+
// Initialize phone state listener only if enabled
|
|
1719
|
+
if (enablePhoneStateHandling) {
|
|
1720
|
+
initializePhoneStateListener()
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Check permissions - create a dummy promise to avoid rejections
|
|
1724
|
+
val dummyPromise = object : Promise {
|
|
1725
|
+
override fun resolve(value: Any?) {}
|
|
1726
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1727
|
+
LogUtils.e(CLASS_NAME, "Preparation error: $code - $message", cause)
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (!checkPermissions(options, dummyPromise)) return false
|
|
1732
|
+
|
|
1733
|
+
// Parse recording configuration - reuse existing code
|
|
1734
|
+
val configResult = RecordingConfig.fromMap(options)
|
|
1735
|
+
if (configResult.isFailure) {
|
|
1736
|
+
LogUtils.e(CLASS_NAME, "Invalid configuration: ${configResult.exceptionOrNull()?.message}")
|
|
1737
|
+
return false
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
val (tempRecordingConfig, audioFormatInfo) = configResult.getOrNull()!!
|
|
1741
|
+
recordingConfig = tempRecordingConfig
|
|
1742
|
+
|
|
1743
|
+
// Store device-related settings
|
|
1744
|
+
selectedDeviceId = recordingConfig.deviceId
|
|
1745
|
+
deviceDisconnectionBehavior = recordingConfig.deviceDisconnectionBehavior ?: "pause"
|
|
1746
|
+
|
|
1747
|
+
audioFormat = audioFormatInfo.format
|
|
1748
|
+
mimeType = audioFormatInfo.mimeType
|
|
1749
|
+
|
|
1750
|
+
// Use all the existing validation functions with our dummy promise
|
|
1751
|
+
if (!initializeAudioFormat(dummyPromise)) return false
|
|
1752
|
+
if (!initializeBufferSize(dummyPromise)) return false
|
|
1753
|
+
if (!initializeAudioRecord(dummyPromise)) return false
|
|
1754
|
+
|
|
1755
|
+
if (recordingConfig.enableCompressedOutput && !initializeCompressedRecorder(
|
|
1756
|
+
if (recordingConfig.compressedFormat == "aac") "aac" else "opus",
|
|
1757
|
+
dummyPromise
|
|
1758
|
+
)) return false
|
|
1759
|
+
|
|
1760
|
+
if (!initializeRecordingResources(audioFormatInfo.fileExtension, dummyPromise)) return false
|
|
1761
|
+
|
|
1762
|
+
// Everything is ready, mark as prepared
|
|
1763
|
+
isPrepared = true
|
|
1764
|
+
LogUtils.d(CLASS_NAME, "Recording prepared successfully")
|
|
1765
|
+
return true
|
|
1766
|
+
} catch (e: Exception) {
|
|
1767
|
+
LogUtils.e(CLASS_NAME, "Error during preparation: ${e.message}", e)
|
|
1768
|
+
cleanup()
|
|
1769
|
+
isPrepared = false
|
|
1770
|
+
return false
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1447
1773
|
}
|