@siteed/expo-audio-studio 2.4.1 → 2.6.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 +576 -252
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +419 -155
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
- 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 +104 -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 +478 -62
- 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 +74 -11
- package/build/WebRecorder.web.d.ts.map +1 -1
- package/build/WebRecorder.web.js +390 -74
- 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/utils/writeWavHeader.d.ts +3 -18
- package/build/utils/writeWavHeader.d.ts.map +1 -1
- package/build/utils/writeWavHeader.js +19 -26
- package/build/utils/writeWavHeader.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 +113 -1
- package/src/ExpoAudioStream.web.ts +609 -69
- package/src/ExpoAudioStreamModule.ts +23 -0
- package/src/WebRecorder.web.ts +482 -92
- 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/utils/writeWavHeader.ts +26 -25
- 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,
|
|
@@ -41,9 +43,41 @@ class AudioRecorderManager(
|
|
|
41
43
|
private val enablePhoneStateHandling: Boolean = true,
|
|
42
44
|
private val enableBackgroundAudio: Boolean = true
|
|
43
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
|
+
|
|
44
78
|
private var audioRecord: AudioRecord? = null
|
|
45
79
|
private var bufferSizeInBytes = 0
|
|
46
|
-
private var
|
|
80
|
+
private var _isRecording = AtomicBoolean(false)
|
|
47
81
|
private val isPaused = AtomicBoolean(false)
|
|
48
82
|
private var streamUuid: String? = null
|
|
49
83
|
private var audioFile: File? = null
|
|
@@ -81,7 +115,7 @@ class AudioRecorderManager(
|
|
|
81
115
|
get() {
|
|
82
116
|
if (field == null) {
|
|
83
117
|
field = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
84
|
-
|
|
118
|
+
LogUtils.d(CLASS_NAME, "TelephonyManager initialization: ${if (field != null) "successful" else "failed"}")
|
|
85
119
|
}
|
|
86
120
|
return field
|
|
87
121
|
}
|
|
@@ -90,12 +124,253 @@ class AudioRecorderManager(
|
|
|
90
124
|
private val analysisBuffer = ByteArrayOutputStream()
|
|
91
125
|
private var isFirstAnalysis = true
|
|
92
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
|
+
|
|
93
368
|
private fun initializePhoneStateListener() {
|
|
94
369
|
try {
|
|
95
|
-
|
|
370
|
+
LogUtils.d(CLASS_NAME, "Initializing phone state listener...")
|
|
96
371
|
|
|
97
372
|
if (permissionUtils.checkPhoneStatePermission()) {
|
|
98
|
-
|
|
373
|
+
LogUtils.d(CLASS_NAME, "Phone state permission granted")
|
|
99
374
|
|
|
100
375
|
phoneStateListener = object : PhoneStateListener() {
|
|
101
376
|
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
|
|
@@ -105,49 +380,49 @@ class AudioRecorderManager(
|
|
|
105
380
|
TelephonyManager.CALL_STATE_IDLE -> "IDLE"
|
|
106
381
|
else -> "UNKNOWN"
|
|
107
382
|
}
|
|
108
|
-
|
|
383
|
+
LogUtils.d(CLASS_NAME, "Phone state changed to: $stateStr")
|
|
109
384
|
|
|
110
385
|
when (state) {
|
|
111
386
|
TelephonyManager.CALL_STATE_RINGING,
|
|
112
387
|
TelephonyManager.CALL_STATE_OFFHOOK -> {
|
|
113
|
-
if (
|
|
114
|
-
|
|
388
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
389
|
+
LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
|
|
115
390
|
mainHandler.post {
|
|
116
391
|
pauseRecording(object : Promise {
|
|
117
392
|
override fun resolve(value: Any?) {
|
|
118
|
-
|
|
393
|
+
LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
|
|
119
394
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
120
395
|
"reason" to "phoneCall",
|
|
121
396
|
"isPaused" to true
|
|
122
397
|
))
|
|
123
398
|
}
|
|
124
399
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
125
|
-
|
|
400
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on phone call", cause)
|
|
126
401
|
}
|
|
127
402
|
})
|
|
128
403
|
}
|
|
129
404
|
}
|
|
130
405
|
}
|
|
131
406
|
TelephonyManager.CALL_STATE_IDLE -> {
|
|
132
|
-
if (
|
|
133
|
-
|
|
407
|
+
if (_isRecording.get() && isPaused.get()) {
|
|
408
|
+
LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: ${recordingConfig.autoResumeAfterInterruption})")
|
|
134
409
|
if (recordingConfig.autoResumeAfterInterruption) {
|
|
135
410
|
mainHandler.post {
|
|
136
411
|
resumeRecording(object : Promise {
|
|
137
412
|
override fun resolve(value: Any?) {
|
|
138
|
-
|
|
413
|
+
LogUtils.d(CLASS_NAME, "Successfully resumed recording after call")
|
|
139
414
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
140
415
|
"reason" to "phoneCallEnded",
|
|
141
416
|
"isPaused" to false
|
|
142
417
|
))
|
|
143
418
|
}
|
|
144
419
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
145
|
-
|
|
420
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording after phone call", cause)
|
|
146
421
|
}
|
|
147
422
|
})
|
|
148
423
|
}
|
|
149
424
|
} else {
|
|
150
|
-
|
|
425
|
+
LogUtils.d(CLASS_NAME, "Auto-resume disabled, staying paused")
|
|
151
426
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
152
427
|
"reason" to "phoneCallEnded",
|
|
153
428
|
"isPaused" to true
|
|
@@ -162,18 +437,18 @@ class AudioRecorderManager(
|
|
|
162
437
|
if (telephonyManager != null) {
|
|
163
438
|
try {
|
|
164
439
|
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
|
165
|
-
|
|
440
|
+
LogUtils.d(CLASS_NAME, "Successfully registered phone state listener")
|
|
166
441
|
} catch (e: Exception) {
|
|
167
|
-
|
|
442
|
+
LogUtils.e(CLASS_NAME, "Failed to register phone state listener", e)
|
|
168
443
|
}
|
|
169
444
|
} else {
|
|
170
|
-
|
|
445
|
+
LogUtils.e(CLASS_NAME, "TelephonyManager is null, cannot register phone state listener")
|
|
171
446
|
}
|
|
172
447
|
} else {
|
|
173
|
-
|
|
448
|
+
LogUtils.w(CLASS_NAME, "READ_PHONE_STATE permission not granted, phone call interruption handling disabled")
|
|
174
449
|
}
|
|
175
450
|
} catch (e: Exception) {
|
|
176
|
-
|
|
451
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize phone state listener", e)
|
|
177
452
|
}
|
|
178
453
|
}
|
|
179
454
|
|
|
@@ -183,7 +458,7 @@ class AudioRecorderManager(
|
|
|
183
458
|
when (focusChange) {
|
|
184
459
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
185
460
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
186
|
-
if (
|
|
461
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
187
462
|
mainHandler.post {
|
|
188
463
|
pauseRecording(object : Promise {
|
|
189
464
|
override fun resolve(value: Any?) {
|
|
@@ -193,14 +468,14 @@ class AudioRecorderManager(
|
|
|
193
468
|
))
|
|
194
469
|
}
|
|
195
470
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
196
|
-
|
|
471
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on audio focus loss")
|
|
197
472
|
}
|
|
198
473
|
})
|
|
199
474
|
}
|
|
200
475
|
}
|
|
201
476
|
}
|
|
202
477
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
203
|
-
if (
|
|
478
|
+
if (_isRecording.get() && isPaused.get() && recordingConfig.autoResumeAfterInterruption) {
|
|
204
479
|
mainHandler.post {
|
|
205
480
|
resumeRecording(object : Promise {
|
|
206
481
|
override fun resolve(value: Any?) {
|
|
@@ -210,7 +485,7 @@ class AudioRecorderManager(
|
|
|
210
485
|
))
|
|
211
486
|
}
|
|
212
487
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
213
|
-
|
|
488
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording on audio focus gain")
|
|
214
489
|
}
|
|
215
490
|
})
|
|
216
491
|
}
|
|
@@ -220,191 +495,83 @@ class AudioRecorderManager(
|
|
|
220
495
|
}
|
|
221
496
|
}
|
|
222
497
|
|
|
223
|
-
companion object {
|
|
224
|
-
@SuppressLint("StaticFieldLeak")
|
|
225
|
-
@Volatile
|
|
226
|
-
private var instance: AudioRecorderManager? = null
|
|
227
|
-
|
|
228
|
-
fun getInstance(): AudioRecorderManager? = instance
|
|
229
|
-
|
|
230
|
-
fun initialize(
|
|
231
|
-
context: Context,
|
|
232
|
-
filesDir: File,
|
|
233
|
-
permissionUtils: PermissionUtils,
|
|
234
|
-
audioDataEncoder: AudioDataEncoder,
|
|
235
|
-
eventSender: EventSender,
|
|
236
|
-
enablePhoneStateHandling: Boolean = true,
|
|
237
|
-
enableBackgroundAudio: Boolean = true
|
|
238
|
-
): AudioRecorderManager {
|
|
239
|
-
return instance ?: synchronized(this) {
|
|
240
|
-
instance ?: AudioRecorderManager(
|
|
241
|
-
context, filesDir, permissionUtils, audioDataEncoder, eventSender,
|
|
242
|
-
enablePhoneStateHandling, enableBackgroundAudio
|
|
243
|
-
).also { instance = it }
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
fun destroy() {
|
|
248
|
-
instance?.cleanup()
|
|
249
|
-
instance = null
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
private fun isOngoingCall(): Boolean {
|
|
254
|
-
try {
|
|
255
|
-
if (!permissionUtils.checkPhoneStatePermission()) {
|
|
256
|
-
Log.w(Constants.TAG, "READ_PHONE_STATE permission not granted, cannot check call state")
|
|
257
|
-
return false
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
val tm = telephonyManager
|
|
261
|
-
if (tm == null) {
|
|
262
|
-
Log.e(Constants.TAG, "TelephonyManager is null")
|
|
263
|
-
return false
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Get audio manager state
|
|
267
|
-
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
268
|
-
val audioMode = audioManager.mode
|
|
269
|
-
val isMusicActive = audioManager.isMusicActive
|
|
270
|
-
|
|
271
|
-
// Get current audio device info
|
|
272
|
-
val currentRoute =
|
|
273
|
-
audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS).firstOrNull()?.type?.let { type ->
|
|
274
|
-
when (type) {
|
|
275
|
-
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "SPEAKER"
|
|
276
|
-
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "EARPIECE"
|
|
277
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "BLUETOOTH_SCO"
|
|
278
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "BLUETOOTH_A2DP"
|
|
279
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET -> "WIRED_HEADSET"
|
|
280
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "WIRED_HEADPHONES"
|
|
281
|
-
else -> "OTHER($type)"
|
|
282
|
-
}
|
|
283
|
-
} ?: "UNKNOWN"
|
|
284
|
-
|
|
285
|
-
// Get communication device info
|
|
286
|
-
val communicationDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
287
|
-
audioManager.communicationDevice?.type?.let { type ->
|
|
288
|
-
when (type) {
|
|
289
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "BLUETOOTH_SCO"
|
|
290
|
-
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "SPEAKER"
|
|
291
|
-
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "EARPIECE"
|
|
292
|
-
else -> "OTHER($type)"
|
|
293
|
-
}
|
|
294
|
-
} ?: "NONE"
|
|
295
|
-
} else {
|
|
296
|
-
@Suppress("DEPRECATION")
|
|
297
|
-
if (audioManager.isBluetoothScoOn) "BLUETOOTH_SCO" else "NONE"
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
Log.d(Constants.TAG, """
|
|
301
|
-
Audio State Check:
|
|
302
|
-
- Audio Mode: ${getAudioModeString(audioMode)}
|
|
303
|
-
- Music Active: $isMusicActive
|
|
304
|
-
- Current Audio Route: $currentRoute
|
|
305
|
-
- Communication Device: $communicationDevice
|
|
306
|
-
""".trimIndent())
|
|
307
|
-
|
|
308
|
-
// Check telephony state
|
|
309
|
-
val callState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
310
|
-
tm.callStateForSubscription
|
|
311
|
-
} else {
|
|
312
|
-
@Suppress("DEPRECATION")
|
|
313
|
-
tm.callState
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
val isVoipCall = audioMode == AudioManager.MODE_IN_COMMUNICATION
|
|
317
|
-
val isRegularCall = callState == TelephonyManager.CALL_STATE_OFFHOOK ||
|
|
318
|
-
callState == TelephonyManager.CALL_STATE_RINGING
|
|
319
|
-
|
|
320
|
-
Log.d(Constants.TAG, """
|
|
321
|
-
Call State Check:
|
|
322
|
-
- Telephony Call State: ${getCallStateString(callState)}
|
|
323
|
-
- VoIP Call Detected: $isVoipCall
|
|
324
|
-
- Regular Call Detected: $isRegularCall
|
|
325
|
-
""".trimIndent())
|
|
326
|
-
|
|
327
|
-
return isVoipCall || isRegularCall
|
|
328
|
-
|
|
329
|
-
} catch (e: SecurityException) {
|
|
330
|
-
Log.e(Constants.TAG, "SecurityException when checking call state", e)
|
|
331
|
-
return false
|
|
332
|
-
} catch (e: Exception) {
|
|
333
|
-
Log.e(Constants.TAG, "Error checking call state", e)
|
|
334
|
-
return false
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private fun getAudioModeString(mode: Int): String = when (mode) {
|
|
339
|
-
AudioManager.MODE_NORMAL -> "MODE_NORMAL"
|
|
340
|
-
AudioManager.MODE_RINGTONE -> "MODE_RINGTONE"
|
|
341
|
-
AudioManager.MODE_IN_CALL -> "MODE_IN_CALL"
|
|
342
|
-
AudioManager.MODE_IN_COMMUNICATION -> "MODE_IN_COMMUNICATION"
|
|
343
|
-
else -> "MODE_UNKNOWN($mode)"
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private fun getCallStateString(state: Int): String = when (state) {
|
|
347
|
-
TelephonyManager.CALL_STATE_IDLE -> "CALL_STATE_IDLE"
|
|
348
|
-
TelephonyManager.CALL_STATE_RINGING -> "CALL_STATE_RINGING"
|
|
349
|
-
TelephonyManager.CALL_STATE_OFFHOOK -> "CALL_STATE_OFFHOOK"
|
|
350
|
-
else -> "CALL_STATE_UNKNOWN($state)"
|
|
351
|
-
}
|
|
352
|
-
|
|
353
498
|
@RequiresApi(Build.VERSION_CODES.R)
|
|
354
499
|
fun startRecording(options: Map<String, Any?>, promise: Promise) {
|
|
355
500
|
try {
|
|
356
|
-
//
|
|
357
|
-
if (
|
|
358
|
-
|
|
501
|
+
// Check if already recording
|
|
502
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
503
|
+
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
504
|
+
return
|
|
359
505
|
}
|
|
360
|
-
|
|
361
|
-
// Request audio focus
|
|
506
|
+
|
|
507
|
+
// Request audio focus - always do this right before starting
|
|
362
508
|
if (!requestAudioFocus()) {
|
|
363
509
|
promise.reject("AUDIO_FOCUS_ERROR", "Failed to obtain audio focus", null)
|
|
364
510
|
return
|
|
365
511
|
}
|
|
366
512
|
|
|
367
|
-
|
|
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
|
+
}
|
|
368
521
|
|
|
369
|
-
|
|
370
|
-
if (!checkPermissions(options, promise)) return
|
|
522
|
+
LogUtils.d(CLASS_NAME, "Starting recording with options: $options")
|
|
371
523
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
375
|
-
return
|
|
376
|
-
}
|
|
524
|
+
// Check permissions
|
|
525
|
+
if (!checkPermissions(options, promise)) return
|
|
377
526
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
}
|
|
388
537
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
395
548
|
|
|
396
|
-
|
|
549
|
+
if (!initializeAudioFormat(promise)) return
|
|
397
550
|
|
|
398
|
-
|
|
551
|
+
if (!initializeBufferSize(promise)) return
|
|
399
552
|
|
|
400
|
-
|
|
553
|
+
if (!initializeAudioRecord(promise)) return
|
|
401
554
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
555
|
+
if (recordingConfig.enableCompressedOutput && !initializeCompressedRecorder(
|
|
556
|
+
if (recordingConfig.compressedFormat == "aac") "aac" else "opus",
|
|
557
|
+
promise
|
|
558
|
+
)) return
|
|
406
559
|
|
|
407
|
-
|
|
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
|
+
}
|
|
408
575
|
|
|
409
576
|
if (!startRecordingProcess(promise)) return
|
|
410
577
|
|
|
@@ -412,7 +579,7 @@ class AudioRecorderManager(
|
|
|
412
579
|
try {
|
|
413
580
|
compressedRecorder?.start()
|
|
414
581
|
} catch (e: Exception) {
|
|
415
|
-
|
|
582
|
+
LogUtils.e(CLASS_NAME, "Failed to start compressed recording", e)
|
|
416
583
|
cleanup()
|
|
417
584
|
promise.reject("COMPRESSED_START_FAILED", "Failed to start compressed recording", e)
|
|
418
585
|
return
|
|
@@ -490,7 +657,7 @@ class AudioRecorderManager(
|
|
|
490
657
|
|
|
491
658
|
// Only check phone state permission if enabled
|
|
492
659
|
if (enablePhoneStateHandling && !permissionUtils.checkPhoneStatePermission()) {
|
|
493
|
-
|
|
660
|
+
LogUtils.w(CLASS_NAME, "READ_PHONE_STATE permission not granted, phone call interruption handling will be disabled")
|
|
494
661
|
// Don't reject here, just log warning as this is optional
|
|
495
662
|
}
|
|
496
663
|
|
|
@@ -515,7 +682,7 @@ class AudioRecorderManager(
|
|
|
515
682
|
audioFormat
|
|
516
683
|
)
|
|
517
684
|
) {
|
|
518
|
-
|
|
685
|
+
LogUtils.e(CLASS_NAME, "Selected audio format not supported, falling back to 16-bit PCM")
|
|
519
686
|
audioFormat = AudioFormat.ENCODING_PCM_16BIT
|
|
520
687
|
|
|
521
688
|
if (!isAudioFormatSupported(
|
|
@@ -553,7 +720,7 @@ class AudioRecorderManager(
|
|
|
553
720
|
|
|
554
721
|
when {
|
|
555
722
|
bufferSizeInBytes == AudioRecord.ERROR -> {
|
|
556
|
-
|
|
723
|
+
LogUtils.e(CLASS_NAME, "Error getting minimum buffer size: ERROR")
|
|
557
724
|
promise.reject(
|
|
558
725
|
"BUFFER_SIZE_ERROR",
|
|
559
726
|
"Failed to get minimum buffer size: generic error",
|
|
@@ -562,7 +729,7 @@ class AudioRecorderManager(
|
|
|
562
729
|
return false
|
|
563
730
|
}
|
|
564
731
|
bufferSizeInBytes == AudioRecord.ERROR_BAD_VALUE -> {
|
|
565
|
-
|
|
732
|
+
LogUtils.e(CLASS_NAME, "Error getting minimum buffer size: BAD_VALUE")
|
|
566
733
|
promise.reject(
|
|
567
734
|
"BUFFER_SIZE_ERROR",
|
|
568
735
|
"Failed to get minimum buffer size: invalid parameters",
|
|
@@ -571,7 +738,7 @@ class AudioRecorderManager(
|
|
|
571
738
|
return false
|
|
572
739
|
}
|
|
573
740
|
bufferSizeInBytes <= 0 -> {
|
|
574
|
-
|
|
741
|
+
LogUtils.e(CLASS_NAME, "Invalid buffer size: $bufferSizeInBytes")
|
|
575
742
|
promise.reject(
|
|
576
743
|
"BUFFER_SIZE_ERROR",
|
|
577
744
|
"Failed to get valid buffer size",
|
|
@@ -580,12 +747,12 @@ class AudioRecorderManager(
|
|
|
580
747
|
return false
|
|
581
748
|
}
|
|
582
749
|
else -> {
|
|
583
|
-
|
|
750
|
+
LogUtils.d(CLASS_NAME, "AudioFormat: $audioFormat, BufferSize: $bufferSizeInBytes")
|
|
584
751
|
return true
|
|
585
752
|
}
|
|
586
753
|
}
|
|
587
754
|
} catch (e: Exception) {
|
|
588
|
-
|
|
755
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize buffer size", e)
|
|
589
756
|
promise.reject(
|
|
590
757
|
"BUFFER_SIZE_ERROR",
|
|
591
758
|
"Failed to initialize buffer size: ${e.message}",
|
|
@@ -608,7 +775,7 @@ class AudioRecorderManager(
|
|
|
608
775
|
|
|
609
776
|
try {
|
|
610
777
|
if (audioRecord == null || !isPaused.get()) {
|
|
611
|
-
|
|
778
|
+
LogUtils.d(CLASS_NAME, "Initializing AudioRecord with format: $audioFormat, BufferSize: $bufferSizeInBytes")
|
|
612
779
|
|
|
613
780
|
audioRecord = AudioRecord(
|
|
614
781
|
MediaRecorder.AudioSource.MIC,
|
|
@@ -630,7 +797,7 @@ class AudioRecorderManager(
|
|
|
630
797
|
return true
|
|
631
798
|
|
|
632
799
|
} catch (e: SecurityException) {
|
|
633
|
-
|
|
800
|
+
LogUtils.e(CLASS_NAME, "Security exception while initializing AudioRecord", e)
|
|
634
801
|
promise.reject(
|
|
635
802
|
"PERMISSION_DENIED",
|
|
636
803
|
"Recording permission denied: ${e.message}",
|
|
@@ -638,7 +805,7 @@ class AudioRecorderManager(
|
|
|
638
805
|
)
|
|
639
806
|
return false
|
|
640
807
|
} catch (e: Exception) {
|
|
641
|
-
|
|
808
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize AudioRecord", e)
|
|
642
809
|
promise.reject(
|
|
643
810
|
"INITIALIZATION_FAILED",
|
|
644
811
|
"Failed to initialize the audio recorder: ${e.message}",
|
|
@@ -679,7 +846,7 @@ class AudioRecorderManager(
|
|
|
679
846
|
return false
|
|
680
847
|
} catch (e: Exception) {
|
|
681
848
|
releaseWakeLock()
|
|
682
|
-
|
|
849
|
+
LogUtils.e(CLASS_NAME, "Unexpected error in startRecording", e)
|
|
683
850
|
promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
|
|
684
851
|
return false
|
|
685
852
|
}
|
|
@@ -688,7 +855,7 @@ class AudioRecorderManager(
|
|
|
688
855
|
private fun startRecordingProcess(promise: Promise): Boolean {
|
|
689
856
|
try {
|
|
690
857
|
// Add detailed logging of recording configuration
|
|
691
|
-
|
|
858
|
+
LogUtils.d(CLASS_NAME, """
|
|
692
859
|
Starting audio recording with configuration:
|
|
693
860
|
- Sample Rate: ${recordingConfig.sampleRate} Hz
|
|
694
861
|
- Channels: ${recordingConfig.channels}
|
|
@@ -712,7 +879,7 @@ class AudioRecorderManager(
|
|
|
712
879
|
|
|
713
880
|
audioRecord?.startRecording()
|
|
714
881
|
isPaused.set(false)
|
|
715
|
-
|
|
882
|
+
_isRecording.set(true)
|
|
716
883
|
isFirstChunk = true
|
|
717
884
|
|
|
718
885
|
if (!isPaused.get()) {
|
|
@@ -729,7 +896,7 @@ class AudioRecorderManager(
|
|
|
729
896
|
return true
|
|
730
897
|
|
|
731
898
|
} catch (e: Exception) {
|
|
732
|
-
|
|
899
|
+
LogUtils.e(CLASS_NAME, "Failed to start recording", e)
|
|
733
900
|
cleanup()
|
|
734
901
|
promise.reject("START_FAILED", "Failed to start recording: ${e.message}", e)
|
|
735
902
|
return false
|
|
@@ -738,8 +905,8 @@ class AudioRecorderManager(
|
|
|
738
905
|
|
|
739
906
|
fun stopRecording(promise: Promise) {
|
|
740
907
|
synchronized(audioRecordLock) {
|
|
741
|
-
if (!
|
|
742
|
-
|
|
908
|
+
if (!_isRecording.get()) {
|
|
909
|
+
LogUtils.e(CLASS_NAME, "Recording is not active")
|
|
743
910
|
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
744
911
|
return
|
|
745
912
|
}
|
|
@@ -758,25 +925,26 @@ class AudioRecorderManager(
|
|
|
758
925
|
AudioRecordingService.stopService(context)
|
|
759
926
|
}
|
|
760
927
|
|
|
761
|
-
|
|
928
|
+
_isRecording.set(false)
|
|
929
|
+
isPrepared = false // Reset preparation state
|
|
762
930
|
recordingThread?.join(1000)
|
|
763
931
|
|
|
764
932
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
765
933
|
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
766
|
-
|
|
934
|
+
LogUtils.d(CLASS_NAME, "Last Read $bytesRead bytes")
|
|
767
935
|
if (bytesRead > 0) {
|
|
768
936
|
emitAudioData(audioData.copyOfRange(0, bytesRead), bytesRead)
|
|
769
937
|
}
|
|
770
938
|
|
|
771
|
-
|
|
939
|
+
LogUtils.d(CLASS_NAME, "Stopping recording state = ${audioRecord?.state}")
|
|
772
940
|
if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
|
|
773
|
-
|
|
941
|
+
LogUtils.d(CLASS_NAME, "Stopping AudioRecord")
|
|
774
942
|
audioRecord!!.stop()
|
|
775
943
|
}
|
|
776
944
|
|
|
777
945
|
cleanup()
|
|
778
946
|
} catch (e: IllegalStateException) {
|
|
779
|
-
|
|
947
|
+
LogUtils.e(CLASS_NAME, "Error reading from AudioRecord", e)
|
|
780
948
|
} finally {
|
|
781
949
|
releaseWakeLock()
|
|
782
950
|
audioRecord?.release()
|
|
@@ -787,7 +955,7 @@ class AudioRecorderManager(
|
|
|
787
955
|
audioProcessor.resetCumulativeAmplitudeRange()
|
|
788
956
|
|
|
789
957
|
val fileSize = audioFile?.length() ?: 0
|
|
790
|
-
|
|
958
|
+
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes, Path: ${audioFile?.absolutePath}")
|
|
791
959
|
|
|
792
960
|
val dataFileSize = fileSize - 44 // Subtract header size
|
|
793
961
|
val byteRate =
|
|
@@ -808,7 +976,7 @@ class AudioRecorderManager(
|
|
|
808
976
|
// Log compressed file status if enabled
|
|
809
977
|
if (recordingConfig.enableCompressedOutput) {
|
|
810
978
|
val compressedSize = compressedFile?.length() ?: 0
|
|
811
|
-
|
|
979
|
+
LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
|
|
812
980
|
}
|
|
813
981
|
|
|
814
982
|
val result = bundleOf(
|
|
@@ -832,12 +1000,12 @@ class AudioRecorderManager(
|
|
|
832
1000
|
promise.resolve(result)
|
|
833
1001
|
|
|
834
1002
|
// Reset the timing variables
|
|
835
|
-
|
|
1003
|
+
_isRecording.set(false)
|
|
836
1004
|
isPaused.set(false)
|
|
837
1005
|
totalRecordedTime = 0
|
|
838
1006
|
pausedDuration = 0
|
|
839
1007
|
} catch (e: Exception) {
|
|
840
|
-
|
|
1008
|
+
LogUtils.e(CLASS_NAME, "Failed to stop recording: ${e.message}")
|
|
841
1009
|
promise.reject("STOP_FAILED", "Failed to stop recording", e)
|
|
842
1010
|
} finally {
|
|
843
1011
|
audioRecord = null
|
|
@@ -846,18 +1014,51 @@ class AudioRecorderManager(
|
|
|
846
1014
|
}
|
|
847
1015
|
|
|
848
1016
|
fun resumeRecording(promise: Promise) {
|
|
1017
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording method entered - isPaused=${isPaused.get()}, isRecording=${_isRecording.get()}")
|
|
849
1018
|
if (!isPaused.get()) {
|
|
1019
|
+
LogUtils.e(CLASS_NAME, "⏺️ Cannot resume recording: not paused")
|
|
850
1020
|
promise.reject("NOT_PAUSED", "Recording is not paused", null)
|
|
851
1021
|
return
|
|
852
1022
|
}
|
|
853
1023
|
|
|
854
1024
|
if (isOngoingCall()) {
|
|
1025
|
+
LogUtils.e(CLASS_NAME, "⏺️ Cannot resume recording: ongoing call detected")
|
|
855
1026
|
promise.reject("ONGOING_CALL", "Cannot resume recording during an ongoing call", null)
|
|
856
1027
|
return
|
|
857
1028
|
}
|
|
858
1029
|
|
|
859
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
|
+
|
|
860
1060
|
if (recordingConfig.showNotification) {
|
|
1061
|
+
LogUtils.d(CLASS_NAME, "⏺️ Resuming notification updates")
|
|
861
1062
|
notificationManager.resumeUpdates()
|
|
862
1063
|
}
|
|
863
1064
|
|
|
@@ -865,18 +1066,36 @@ class AudioRecorderManager(
|
|
|
865
1066
|
pausedDuration += System.currentTimeMillis() - lastPauseTime
|
|
866
1067
|
isPaused.set(false)
|
|
867
1068
|
|
|
868
|
-
|
|
869
|
-
|
|
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
|
+
}
|
|
870
1087
|
|
|
1088
|
+
LogUtils.d(CLASS_NAME, "⏺️ Recording resumed successfully")
|
|
871
1089
|
promise.resolve("Recording resumed")
|
|
872
1090
|
} catch (e: Exception) {
|
|
1091
|
+
LogUtils.e(CLASS_NAME, "⏺️ Failed to resume recording: ${e.message}", e)
|
|
873
1092
|
releaseWakeLock()
|
|
874
|
-
promise.reject("RESUME_FAILED", "Failed to resume recording", e)
|
|
1093
|
+
promise.reject("RESUME_FAILED", "Failed to resume recording: ${e.message}", e)
|
|
875
1094
|
}
|
|
876
1095
|
}
|
|
877
1096
|
|
|
878
1097
|
fun pauseRecording(promise: Promise) {
|
|
879
|
-
if (
|
|
1098
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
880
1099
|
audioRecord?.stop()
|
|
881
1100
|
compressedRecorder?.pause()
|
|
882
1101
|
|
|
@@ -908,14 +1127,14 @@ class AudioRecorderManager(
|
|
|
908
1127
|
} ?: false
|
|
909
1128
|
|
|
910
1129
|
// If service is running but we think we're not recording, clean up
|
|
911
|
-
if (isServiceRunning && !
|
|
912
|
-
|
|
1130
|
+
if (isServiceRunning && !_isRecording.get()) {
|
|
1131
|
+
LogUtils.d(CLASS_NAME, "Detected orphaned recording service, cleaning up...")
|
|
913
1132
|
cleanup()
|
|
914
1133
|
AudioRecordingService.stopService(context)
|
|
915
1134
|
}
|
|
916
1135
|
|
|
917
|
-
if (!
|
|
918
|
-
|
|
1136
|
+
if (!_isRecording.get()) {
|
|
1137
|
+
LogUtils.d(CLASS_NAME, "Not recording --- skip status with default values")
|
|
919
1138
|
return bundleOf(
|
|
920
1139
|
"isRecording" to false,
|
|
921
1140
|
"isPaused" to false,
|
|
@@ -947,7 +1166,7 @@ class AudioRecorderManager(
|
|
|
947
1166
|
|
|
948
1167
|
return bundleOf(
|
|
949
1168
|
"durationMs" to duration,
|
|
950
|
-
"isRecording" to
|
|
1169
|
+
"isRecording" to _isRecording.get(),
|
|
951
1170
|
"isPaused" to isPaused.get(),
|
|
952
1171
|
"mimeType" to mimeType,
|
|
953
1172
|
"size" to totalDataSize,
|
|
@@ -969,9 +1188,9 @@ class AudioRecorderManager(
|
|
|
969
1188
|
acquire()
|
|
970
1189
|
}
|
|
971
1190
|
wasWakeLockEnabled = true
|
|
972
|
-
|
|
1191
|
+
LogUtils.d(CLASS_NAME, "Wake lock acquired")
|
|
973
1192
|
} catch (e: Exception) {
|
|
974
|
-
|
|
1193
|
+
LogUtils.e(CLASS_NAME, "Failed to acquire wake lock", e)
|
|
975
1194
|
}
|
|
976
1195
|
}
|
|
977
1196
|
}
|
|
@@ -982,13 +1201,45 @@ class AudioRecorderManager(
|
|
|
982
1201
|
wakeLock?.let {
|
|
983
1202
|
if (it.isHeld) {
|
|
984
1203
|
it.release()
|
|
985
|
-
|
|
1204
|
+
LogUtils.d(CLASS_NAME, "Wake lock released")
|
|
986
1205
|
}
|
|
987
1206
|
wakeLock = null
|
|
988
1207
|
wasWakeLockEnabled = false
|
|
989
1208
|
}
|
|
990
1209
|
} catch (e: Exception) {
|
|
991
|
-
|
|
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
|
|
992
1243
|
}
|
|
993
1244
|
}
|
|
994
1245
|
|
|
@@ -1005,11 +1256,11 @@ class AudioRecorderManager(
|
|
|
1005
1256
|
|
|
1006
1257
|
private fun recordingProcess() {
|
|
1007
1258
|
try {
|
|
1008
|
-
|
|
1259
|
+
LogUtils.i(CLASS_NAME, "Starting recording process...")
|
|
1009
1260
|
FileOutputStream(audioFile, true).use { fos ->
|
|
1010
1261
|
// Write audio data directly to the file
|
|
1011
1262
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
1012
|
-
|
|
1263
|
+
LogUtils.d(CLASS_NAME, "Entering recording loop")
|
|
1013
1264
|
|
|
1014
1265
|
// Buffer to accumulate data
|
|
1015
1266
|
val accumulatedAudioData = ByteArrayOutputStream()
|
|
@@ -1033,7 +1284,7 @@ class AudioRecorderManager(
|
|
|
1033
1284
|
var shouldProcessAnalysis = false
|
|
1034
1285
|
|
|
1035
1286
|
// Debug log for intervals
|
|
1036
|
-
|
|
1287
|
+
LogUtils.d(CLASS_NAME, """
|
|
1037
1288
|
Recording process started with intervals:
|
|
1038
1289
|
- Data emission interval: ${recordingConfig.interval}ms
|
|
1039
1290
|
- Analysis interval: ${recordingConfig.intervalAnalysis}ms
|
|
@@ -1041,7 +1292,7 @@ class AudioRecorderManager(
|
|
|
1041
1292
|
""".trimIndent())
|
|
1042
1293
|
|
|
1043
1294
|
// Recording loop
|
|
1044
|
-
while (
|
|
1295
|
+
while (_isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
1045
1296
|
if (isPaused.get()) {
|
|
1046
1297
|
Thread.sleep(100) // Add small delay when paused
|
|
1047
1298
|
continue
|
|
@@ -1055,12 +1306,12 @@ class AudioRecorderManager(
|
|
|
1055
1306
|
val bytesRead = synchronized(audioRecordLock) {
|
|
1056
1307
|
audioRecord?.let {
|
|
1057
1308
|
if (it.state != AudioRecord.STATE_INITIALIZED) {
|
|
1058
|
-
|
|
1309
|
+
LogUtils.e(CLASS_NAME, "AudioRecord not initialized")
|
|
1059
1310
|
return@let -1
|
|
1060
1311
|
}
|
|
1061
1312
|
it.read(audioData, 0, bufferSizeInBytes).also { bytes ->
|
|
1062
1313
|
if (bytes < 0) {
|
|
1063
|
-
|
|
1314
|
+
LogUtils.e(CLASS_NAME, "AudioRecord read error: $bytes")
|
|
1064
1315
|
}
|
|
1065
1316
|
}
|
|
1066
1317
|
} ?: -1 // Handle null case
|
|
@@ -1085,7 +1336,7 @@ class AudioRecorderManager(
|
|
|
1085
1336
|
// Handle analysis emission separately
|
|
1086
1337
|
if (shouldProcessAnalysis) {
|
|
1087
1338
|
val analysisDataSize = accumulatedAnalysisData.size()
|
|
1088
|
-
|
|
1339
|
+
LogUtils.d(CLASS_NAME, """
|
|
1089
1340
|
Processing analysis data:
|
|
1090
1341
|
- Time since last: ${currentTime - lastEmissionTimeAnalysis}ms
|
|
1091
1342
|
- Configured interval: ${recordingConfig.intervalAnalysis}ms
|
|
@@ -1102,7 +1353,7 @@ class AudioRecorderManager(
|
|
|
1102
1353
|
recordingConfig
|
|
1103
1354
|
)
|
|
1104
1355
|
|
|
1105
|
-
|
|
1356
|
+
LogUtils.d(CLASS_NAME, """
|
|
1106
1357
|
Analysis data details:
|
|
1107
1358
|
- Raw data size: ${accumulatedAnalysisData.size()} bytes
|
|
1108
1359
|
""".trimIndent())
|
|
@@ -1114,7 +1365,7 @@ class AudioRecorderManager(
|
|
|
1114
1365
|
analysisData.toBundle()
|
|
1115
1366
|
)
|
|
1116
1367
|
} catch (e: Exception) {
|
|
1117
|
-
|
|
1368
|
+
LogUtils.e(CLASS_NAME, "Failed to send audio analysis event", e)
|
|
1118
1369
|
}
|
|
1119
1370
|
}
|
|
1120
1371
|
|
|
@@ -1137,7 +1388,7 @@ class AudioRecorderManager(
|
|
|
1137
1388
|
if (!isPaused.get()) {
|
|
1138
1389
|
releaseWakeLock()
|
|
1139
1390
|
}
|
|
1140
|
-
|
|
1391
|
+
LogUtils.e(CLASS_NAME, "Error in recording process", e)
|
|
1141
1392
|
}
|
|
1142
1393
|
}
|
|
1143
1394
|
|
|
@@ -1166,7 +1417,7 @@ class AudioRecorderManager(
|
|
|
1166
1417
|
audioDataEncoder.encodeToBase64(buffer)
|
|
1167
1418
|
}
|
|
1168
1419
|
} catch (e: Exception) {
|
|
1169
|
-
|
|
1420
|
+
LogUtils.e(CLASS_NAME, "Failed to read compressed data", e)
|
|
1170
1421
|
null
|
|
1171
1422
|
}
|
|
1172
1423
|
} else null
|
|
@@ -1198,7 +1449,7 @@ class AudioRecorderManager(
|
|
|
1198
1449
|
)
|
|
1199
1450
|
)
|
|
1200
1451
|
} catch (e: Exception) {
|
|
1201
|
-
|
|
1452
|
+
LogUtils.e(CLASS_NAME, "Failed to send event", e)
|
|
1202
1453
|
}
|
|
1203
1454
|
}
|
|
1204
1455
|
|
|
@@ -1268,14 +1519,15 @@ class AudioRecorderManager(
|
|
|
1268
1519
|
fun cleanup() {
|
|
1269
1520
|
synchronized(audioRecordLock) {
|
|
1270
1521
|
try {
|
|
1271
|
-
if (
|
|
1522
|
+
if (_isRecording.get()) {
|
|
1272
1523
|
audioRecord?.stop()
|
|
1273
1524
|
compressedRecorder?.stop()
|
|
1274
1525
|
compressedRecorder?.release()
|
|
1275
1526
|
}
|
|
1276
1527
|
|
|
1277
|
-
|
|
1528
|
+
_isRecording.set(false)
|
|
1278
1529
|
isPaused.set(false)
|
|
1530
|
+
isPrepared = false // Reset prepared state
|
|
1279
1531
|
|
|
1280
1532
|
if (recordingConfig.showNotification) {
|
|
1281
1533
|
notificationManager.stopUpdates()
|
|
@@ -1304,7 +1556,7 @@ class AudioRecorderManager(
|
|
|
1304
1556
|
"isPaused" to false
|
|
1305
1557
|
))
|
|
1306
1558
|
} catch (e: Exception) {
|
|
1307
|
-
|
|
1559
|
+
LogUtils.e(CLASS_NAME, "Error during cleanup", e)
|
|
1308
1560
|
}
|
|
1309
1561
|
}
|
|
1310
1562
|
}
|
|
@@ -1338,7 +1590,7 @@ class AudioRecorderManager(
|
|
|
1338
1590
|
}
|
|
1339
1591
|
return true
|
|
1340
1592
|
} catch (e: Exception) {
|
|
1341
|
-
|
|
1593
|
+
LogUtils.e(CLASS_NAME, "Failed to initialize compressed recorder", e)
|
|
1342
1594
|
promise.reject("COMPRESSED_INIT_FAILED", "Failed to initialize compressed recorder", e)
|
|
1343
1595
|
return false
|
|
1344
1596
|
}
|
|
@@ -1350,7 +1602,7 @@ class AudioRecorderManager(
|
|
|
1350
1602
|
when (focusChange) {
|
|
1351
1603
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
1352
1604
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1353
|
-
if (
|
|
1605
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
1354
1606
|
mainHandler.post {
|
|
1355
1607
|
pauseRecording(object : Promise {
|
|
1356
1608
|
override fun resolve(value: Any?) {
|
|
@@ -1361,14 +1613,14 @@ class AudioRecorderManager(
|
|
|
1361
1613
|
))
|
|
1362
1614
|
}
|
|
1363
1615
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1364
|
-
|
|
1616
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on audio focus loss")
|
|
1365
1617
|
}
|
|
1366
1618
|
})
|
|
1367
1619
|
}
|
|
1368
1620
|
}
|
|
1369
1621
|
}
|
|
1370
1622
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1371
|
-
if (
|
|
1623
|
+
if (_isRecording.get() && isPaused.get() && recordingConfig.autoResumeAfterInterruption) {
|
|
1372
1624
|
mainHandler.post {
|
|
1373
1625
|
resumeRecording(object : Promise {
|
|
1374
1626
|
override fun resolve(value: Any?) {
|
|
@@ -1378,7 +1630,7 @@ class AudioRecorderManager(
|
|
|
1378
1630
|
))
|
|
1379
1631
|
}
|
|
1380
1632
|
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1381
|
-
|
|
1633
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording on audio focus gain")
|
|
1382
1634
|
}
|
|
1383
1635
|
})
|
|
1384
1636
|
}
|
|
@@ -1446,4 +1698,76 @@ class AudioRecorderManager(
|
|
|
1446
1698
|
fun getKeepAwakeStatus(): Boolean {
|
|
1447
1699
|
return recordingConfig?.keepAwake ?: true
|
|
1448
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
|
+
}
|
|
1449
1773
|
}
|