@siteed/audio-studio 3.0.5 → 3.1.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 +10 -1
- package/README.md +20 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +74 -22
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +33 -9
- package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +6 -0
- package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +37 -0
- package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +28 -0
- package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +49 -0
- package/build/cjs/AudioStudio.types.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +36 -18
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/esm/AudioStudio.types.js.map +1 -1
- package/build/esm/useAudioRecorder.js +36 -18
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/types/AudioStudio.types.d.ts +14 -1
- package/build/types/AudioStudio.types.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +79 -15
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +41 -1
- package/package.json +1 -1
- package/src/AudioStudio.types.ts +15 -1
- package/src/useAudioRecorder.tsx +36 -14
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [3.1.0] - 2026-05-01
|
|
12
|
+
### Changed
|
|
13
|
+
- fix(audio-studio): preserve pause intent across interruptions (#375) ([2f0f731](https://github.com/deeeed/audiolab/commit/2f0f731e412f45fc81c4fe46bec2abd3f15b4824))
|
|
14
|
+
- fix(android): avoid recorder crashes from stale system callbacks (#374) ([34a9bc0](https://github.com/deeeed/audiolab/commit/34a9bc0c2f7c4e3e569862e8db43710abbde9043))
|
|
15
|
+
- Document when to avoid retaining live analysis history (#373) ([aa617b0](https://github.com/deeeed/audiolab/commit/aa617b048dd790218dda43ec2ed21e0abaf38daf))
|
|
16
|
+
- Let long-running analysis skip full history retention (#372) ([13c230c](https://github.com/deeeed/audiolab/commit/13c230cde655131a3a7c9472d294b2d432f79d50))
|
|
17
|
+
- Keep low-rate iOS AAC recordings from losing compressed output (#371) ([a689eb0](https://github.com/deeeed/audiolab/commit/a689eb03c7436429bd3bf997430f7f7c842b2e57))
|
|
18
|
+
- chore(audio-studio): release @siteed/audio-studio@3.0.5 ([9dff021](https://github.com/deeeed/audiolab/commit/9dff0219993803d29a03946cf81fe2bedb541cab))
|
|
11
19
|
## [3.0.5] - 2026-04-25
|
|
12
20
|
### Changed
|
|
13
21
|
- fix(audio-studio): don't start notification on prepareRecording (Android) (#364) ([5d40d7e](https://github.com/deeeed/audiolab/commit/5d40d7e730f2b74459d319979fd9c112891b10f2))
|
|
@@ -349,7 +357,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
349
357
|
- Audio features extraction during recording
|
|
350
358
|
- Consistent WAV PCM recording format across all platforms
|
|
351
359
|
|
|
352
|
-
[unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0
|
|
360
|
+
[unreleased]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.1.0...HEAD
|
|
361
|
+
[3.1.0]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.5...@siteed/audio-studio@3.1.0
|
|
353
362
|
[3.0.5]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.4...@siteed/audio-studio@3.0.5
|
|
354
363
|
[3.0.4]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.3...@siteed/audio-studio@3.0.4
|
|
355
364
|
[3.0.3]: https://github.com/deeeed/audiolab/compare/@siteed/audio-studio@3.0.2...@siteed/audio-studio@3.0.3
|
package/README.md
CHANGED
|
@@ -113,6 +113,26 @@ await startRecording({
|
|
|
113
113
|
|
|
114
114
|
## Audio Analysis
|
|
115
115
|
|
|
116
|
+
For live analysis during recording, `useAudioRecorder` keeps a recent analysis
|
|
117
|
+
window in `analysisData` for visualization and, by default, also retains the
|
|
118
|
+
full analysis history so `stopRecording().analysisData` can describe the whole
|
|
119
|
+
recording. This option only matters when `enableProcessing: true`. For
|
|
120
|
+
long-running sessions that only need live callbacks, disable the full-history
|
|
121
|
+
retention to avoid unbounded JS memory growth:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
await startRecording({
|
|
125
|
+
sampleRate: 16000,
|
|
126
|
+
channels: 1,
|
|
127
|
+
enableProcessing: true,
|
|
128
|
+
keepFullAnalysis: false,
|
|
129
|
+
onAudioAnalysis: async (analysis) => {
|
|
130
|
+
// Consume each analysis chunk without retaining the full recording history.
|
|
131
|
+
updateVoiceActivity(analysis.dataPoints);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
116
136
|
```typescript
|
|
117
137
|
import { extractAudioAnalysis, extractPreview, extractMelSpectrogram, trimAudio } from '@siteed/audio-studio';
|
|
118
138
|
|
|
@@ -118,6 +118,7 @@ class AudioRecorderManager(
|
|
|
118
118
|
private var audioFocusRequest: Any? = null // Type Any to handle both old and new APIs
|
|
119
119
|
private var phoneStateListener: PhoneStateListener? = null
|
|
120
120
|
private var telephonyCallback: Any? = null // TelephonyCallback for API 31+, typed as Any to avoid class verification issues on older APIs
|
|
121
|
+
private val pausedBySystemInterruption = AtomicBoolean(false)
|
|
121
122
|
private var telephonyManager: TelephonyManager? = null
|
|
122
123
|
get() {
|
|
123
124
|
if (field == null) {
|
|
@@ -408,7 +409,7 @@ class AudioRecorderManager(
|
|
|
408
409
|
if (_isRecording.get() && !isPaused.get()) {
|
|
409
410
|
LogUtils.d(CLASS_NAME, "Pausing recording due to incoming/ongoing call")
|
|
410
411
|
mainHandler.post {
|
|
411
|
-
|
|
412
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
412
413
|
override fun resolve(value: Any?) {
|
|
413
414
|
LogUtils.d(CLASS_NAME, "Successfully paused recording due to call")
|
|
414
415
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
@@ -426,8 +427,14 @@ class AudioRecorderManager(
|
|
|
426
427
|
TelephonyManager.CALL_STATE_IDLE -> {
|
|
427
428
|
if (_isRecording.get() && isPaused.get()) {
|
|
428
429
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
429
|
-
|
|
430
|
-
|
|
430
|
+
val shouldAutoResume = InterruptionAutoResumePolicy.shouldAutoResume(
|
|
431
|
+
autoResumeAfterInterruption = autoResume,
|
|
432
|
+
isRecording = _isRecording.get(),
|
|
433
|
+
isPaused = isPaused.get(),
|
|
434
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
435
|
+
)
|
|
436
|
+
LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume, pausedBySystemInterruption: ${pausedBySystemInterruption.get()})")
|
|
437
|
+
if (shouldAutoResume) {
|
|
431
438
|
mainHandler.post {
|
|
432
439
|
resumeRecording(object : Promise {
|
|
433
440
|
override fun resolve(value: Any?) {
|
|
@@ -443,7 +450,7 @@ class AudioRecorderManager(
|
|
|
443
450
|
})
|
|
444
451
|
}
|
|
445
452
|
} else {
|
|
446
|
-
LogUtils.d(CLASS_NAME, "Auto-resume
|
|
453
|
+
LogUtils.d(CLASS_NAME, "Auto-resume not permitted, staying paused")
|
|
447
454
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
448
455
|
"reason" to "phoneCallEnded",
|
|
449
456
|
"isPaused" to true
|
|
@@ -954,6 +961,7 @@ class AudioRecorderManager(
|
|
|
954
961
|
|
|
955
962
|
audioRecord?.startRecording()
|
|
956
963
|
isPaused.set(false)
|
|
964
|
+
pausedBySystemInterruption.set(false)
|
|
957
965
|
isFirstChunk = true
|
|
958
966
|
recordingStartTime = System.currentTimeMillis()
|
|
959
967
|
|
|
@@ -1174,6 +1182,7 @@ class AudioRecorderManager(
|
|
|
1174
1182
|
// Reset the timing variables
|
|
1175
1183
|
_isRecording.set(false)
|
|
1176
1184
|
isPaused.set(false)
|
|
1185
|
+
pausedBySystemInterruption.set(false)
|
|
1177
1186
|
totalRecordedTime = 0
|
|
1178
1187
|
pausedDuration = 0
|
|
1179
1188
|
} catch (e: Exception) {
|
|
@@ -1258,6 +1267,7 @@ class AudioRecorderManager(
|
|
|
1258
1267
|
}
|
|
1259
1268
|
|
|
1260
1269
|
LogUtils.d(CLASS_NAME, "⏺️ Recording resumed successfully")
|
|
1270
|
+
pausedBySystemInterruption.set(false)
|
|
1261
1271
|
promise.resolve("Recording resumed")
|
|
1262
1272
|
} catch (e: Exception) {
|
|
1263
1273
|
LogUtils.e(CLASS_NAME, "⏺️ Failed to resume recording: ${e.message}", e)
|
|
@@ -1267,12 +1277,21 @@ class AudioRecorderManager(
|
|
|
1267
1277
|
}
|
|
1268
1278
|
|
|
1269
1279
|
fun pauseRecording(promise: Promise) {
|
|
1280
|
+
pauseRecording(promise, isSystemInterruption = false)
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private fun pauseRecordingForSystemInterruption(promise: Promise) {
|
|
1284
|
+
pauseRecording(promise, isSystemInterruption = true)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private fun pauseRecording(promise: Promise, isSystemInterruption: Boolean) {
|
|
1270
1288
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1271
1289
|
audioRecord?.stop()
|
|
1272
1290
|
compressedRecorder?.pause()
|
|
1273
1291
|
|
|
1274
1292
|
lastPauseTime = System.currentTimeMillis()
|
|
1275
1293
|
isPaused.set(true)
|
|
1294
|
+
pausedBySystemInterruption.set(isSystemInterruption)
|
|
1276
1295
|
|
|
1277
1296
|
if (recordingConfig.showNotification) {
|
|
1278
1297
|
notificationManager.pauseUpdates()
|
|
@@ -1417,17 +1436,7 @@ class AudioRecorderManager(
|
|
|
1417
1436
|
"audioManager.mode=${audioManager.mode}, " +
|
|
1418
1437
|
"audioManager.isBluetoothScoOn=${audioManager.isBluetoothScoOn}")
|
|
1419
1438
|
|
|
1420
|
-
|
|
1421
|
-
if (callState == TelephonyManager.CALL_STATE_RINGING ||
|
|
1422
|
-
callState == TelephonyManager.CALL_STATE_OFFHOOK) {
|
|
1423
|
-
return true
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Only check audio manager mode as secondary indicator
|
|
1427
|
-
return audioManager.mode == AudioManager.MODE_IN_CALL ||
|
|
1428
|
-
audioManager.mode == AudioManager.MODE_IN_COMMUNICATION
|
|
1429
|
-
|
|
1430
|
-
// Remove audioManager.isBluetoothScoOn check as it can be erroneously true after disconnection
|
|
1439
|
+
return AndroidCallState.isOngoingCall(callState, audioManager.mode)
|
|
1431
1440
|
} catch (e: Exception) {
|
|
1432
1441
|
LogUtils.e(CLASS_NAME, "Error checking call state: ${e.message}")
|
|
1433
1442
|
return false
|
|
@@ -1757,6 +1766,7 @@ class AudioRecorderManager(
|
|
|
1757
1766
|
|
|
1758
1767
|
_isRecording.set(false)
|
|
1759
1768
|
isPaused.set(false)
|
|
1769
|
+
pausedBySystemInterruption.set(false)
|
|
1760
1770
|
isPrepared = false // Reset prepared state
|
|
1761
1771
|
|
|
1762
1772
|
if (::recordingConfig.isInitialized && recordingConfig.showNotification) {
|
|
@@ -1919,9 +1929,8 @@ class AudioRecorderManager(
|
|
|
1919
1929
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1920
1930
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1921
1931
|
mainHandler.post {
|
|
1922
|
-
|
|
1932
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
1923
1933
|
override fun resolve(value: Any?) {
|
|
1924
|
-
isPaused.set(true)
|
|
1925
1934
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1926
1935
|
"reason" to "audioFocusLoss",
|
|
1927
1936
|
"isPaused" to true
|
|
@@ -1936,7 +1945,12 @@ class AudioRecorderManager(
|
|
|
1936
1945
|
}
|
|
1937
1946
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1938
1947
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
1939
|
-
if (
|
|
1948
|
+
if (InterruptionAutoResumePolicy.shouldAutoResume(
|
|
1949
|
+
autoResumeAfterInterruption = autoResume,
|
|
1950
|
+
isRecording = _isRecording.get(),
|
|
1951
|
+
isPaused = isPaused.get(),
|
|
1952
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
1953
|
+
)) {
|
|
1940
1954
|
mainHandler.post {
|
|
1941
1955
|
resumeRecording(object : Promise {
|
|
1942
1956
|
override fun resolve(value: Any?) {
|
|
@@ -1985,9 +1999,8 @@ class AudioRecorderManager(
|
|
|
1985
1999
|
// Only pause for permanent focus loss (like phone calls)
|
|
1986
2000
|
if (_isRecording.get() && !isPaused.get()) {
|
|
1987
2001
|
mainHandler.post {
|
|
1988
|
-
|
|
2002
|
+
pauseRecordingForSystemInterruption(object : Promise {
|
|
1989
2003
|
override fun resolve(value: Any?) {
|
|
1990
|
-
isPaused.set(true)
|
|
1991
2004
|
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1992
2005
|
"reason" to "audioFocusLoss",
|
|
1993
2006
|
"isPaused" to true
|
|
@@ -2006,7 +2019,12 @@ class AudioRecorderManager(
|
|
|
2006
2019
|
}
|
|
2007
2020
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
2008
2021
|
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
2009
|
-
if (
|
|
2022
|
+
if (InterruptionAutoResumePolicy.shouldAutoResume(
|
|
2023
|
+
autoResumeAfterInterruption = autoResume,
|
|
2024
|
+
isRecording = _isRecording.get(),
|
|
2025
|
+
isPaused = isPaused.get(),
|
|
2026
|
+
pausedBySystemInterruption = pausedBySystemInterruption.get()
|
|
2027
|
+
)) {
|
|
2010
2028
|
mainHandler.post {
|
|
2011
2029
|
resumeRecording(object : Promise {
|
|
2012
2030
|
override fun resolve(value: Any?) {
|
|
@@ -2168,4 +2186,38 @@ class AudioRecorderManager(
|
|
|
2168
2186
|
return false
|
|
2169
2187
|
}
|
|
2170
2188
|
}
|
|
2171
|
-
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
internal object AndroidCallState {
|
|
2192
|
+
/**
|
|
2193
|
+
* Telephony call state wins when known. AudioManager mode is only a fallback
|
|
2194
|
+
* for unknown state because some Android devices leave it stale after calls.
|
|
2195
|
+
*/
|
|
2196
|
+
fun isOngoingCall(callState: Int?, audioMode: Int): Boolean {
|
|
2197
|
+
return when (callState) {
|
|
2198
|
+
TelephonyManager.CALL_STATE_RINGING,
|
|
2199
|
+
TelephonyManager.CALL_STATE_OFFHOOK -> true
|
|
2200
|
+
TelephonyManager.CALL_STATE_IDLE -> false
|
|
2201
|
+
else -> audioMode == AudioManager.MODE_IN_CALL ||
|
|
2202
|
+
audioMode == AudioManager.MODE_IN_COMMUNICATION
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
internal object InterruptionAutoResumePolicy {
|
|
2208
|
+
/**
|
|
2209
|
+
* Auto-resume is only allowed when the pause was caused by a system interruption.
|
|
2210
|
+
* User-initiated pauses must stay paused even after phone/audio focus interruptions end.
|
|
2211
|
+
*/
|
|
2212
|
+
fun shouldAutoResume(
|
|
2213
|
+
autoResumeAfterInterruption: Boolean,
|
|
2214
|
+
isRecording: Boolean,
|
|
2215
|
+
isPaused: Boolean,
|
|
2216
|
+
pausedBySystemInterruption: Boolean
|
|
2217
|
+
): Boolean {
|
|
2218
|
+
return autoResumeAfterInterruption &&
|
|
2219
|
+
isRecording &&
|
|
2220
|
+
isPaused &&
|
|
2221
|
+
pausedBySystemInterruption
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
@@ -413,7 +413,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
413
413
|
|
|
414
414
|
val progressListener = object : AudioTrimmer.ProgressListener {
|
|
415
415
|
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
416
|
-
|
|
416
|
+
safeSendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
|
|
417
417
|
"progress" to progress,
|
|
418
418
|
"bytesProcessed" to bytesProcessed,
|
|
419
419
|
"totalBytes" to totalBytes
|
|
@@ -989,7 +989,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
989
989
|
}
|
|
990
990
|
|
|
991
991
|
// Notify JS about the disconnection
|
|
992
|
-
|
|
992
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
993
993
|
"type" to "deviceDisconnected",
|
|
994
994
|
"deviceId" to deviceId
|
|
995
995
|
))
|
|
@@ -1004,7 +1004,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1004
1004
|
audioDeviceManager.onDeviceConnected = { deviceId ->
|
|
1005
1005
|
LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
|
|
1006
1006
|
// Notify JS about the connection
|
|
1007
|
-
|
|
1007
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
1008
1008
|
"type" to "deviceConnected",
|
|
1009
1009
|
"deviceId" to deviceId
|
|
1010
1010
|
))
|
|
@@ -1014,7 +1014,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1014
1014
|
audioDeviceManager.onDeviceDisconnected = { deviceId ->
|
|
1015
1015
|
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
1016
1016
|
// Notify JS about the disconnection
|
|
1017
|
-
|
|
1017
|
+
safeSendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
1018
1018
|
"type" to "deviceDisconnected",
|
|
1019
1019
|
"deviceId" to deviceId
|
|
1020
1020
|
))
|
|
@@ -1023,6 +1023,18 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1023
1023
|
audioProcessor = AudioProcessor(filesDir)
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
|
+
private fun safeSendEvent(eventName: String, params: Bundle) {
|
|
1027
|
+
AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
|
|
1028
|
+
sendEvent(eventName, params)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private fun safeSendEvent(eventName: String, params: Map<String, Any?>) {
|
|
1033
|
+
AndroidEventEmitter.safeSend(CLASS_NAME, eventName) {
|
|
1034
|
+
sendEvent(eventName, params)
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1026
1038
|
/**
|
|
1027
1039
|
* Handles audio device disconnection based on the recording configuration
|
|
1028
1040
|
*/
|
|
@@ -1055,7 +1067,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1055
1067
|
|
|
1056
1068
|
// Notify JS about fallback
|
|
1057
1069
|
LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
|
|
1058
|
-
|
|
1070
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1059
1071
|
"reason" to "deviceFallback",
|
|
1060
1072
|
"isPaused" to false,
|
|
1061
1073
|
"deviceId" to deviceId
|
|
@@ -1070,7 +1082,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1070
1082
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1071
1083
|
audioRecorderManager.handleDeviceChange()
|
|
1072
1084
|
|
|
1073
|
-
|
|
1085
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1074
1086
|
"reason" to "deviceSwitchFailed",
|
|
1075
1087
|
"isPaused" to true
|
|
1076
1088
|
))
|
|
@@ -1090,7 +1102,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1090
1102
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1091
1103
|
audioRecorderManager.handleDeviceChange()
|
|
1092
1104
|
|
|
1093
|
-
|
|
1105
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1094
1106
|
"reason" to "deviceDisconnected",
|
|
1095
1107
|
"isPaused" to true
|
|
1096
1108
|
))
|
|
@@ -1112,7 +1124,7 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1112
1124
|
// Notify AudioRecorderManager to handle device change while paused
|
|
1113
1125
|
audioRecorderManager.handleDeviceChange()
|
|
1114
1126
|
|
|
1115
|
-
|
|
1127
|
+
safeSendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1116
1128
|
"reason" to "deviceDisconnected",
|
|
1117
1129
|
"isPaused" to true
|
|
1118
1130
|
))
|
|
@@ -1128,6 +1140,18 @@ class AudioStudioModule : Module(), EventSender {
|
|
|
1128
1140
|
|
|
1129
1141
|
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
1130
1142
|
LogUtils.d(CLASS_NAME, "Sending event: $eventName")
|
|
1131
|
-
|
|
1143
|
+
safeSendEvent(eventName, params)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
internal object AndroidEventEmitter {
|
|
1148
|
+
fun safeSend(className: String, eventName: String, send: () -> Unit): Boolean {
|
|
1149
|
+
return try {
|
|
1150
|
+
send()
|
|
1151
|
+
true
|
|
1152
|
+
} catch (e: Exception) {
|
|
1153
|
+
LogUtils.e(className, "Failed to send event $eventName: ${e.message}", e)
|
|
1154
|
+
false
|
|
1155
|
+
}
|
|
1132
1156
|
}
|
|
1133
1157
|
}
|
|
@@ -3,5 +3,11 @@ package net.siteed.audiostudio
|
|
|
3
3
|
import android.os.Bundle
|
|
4
4
|
|
|
5
5
|
interface EventSender {
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort event delivery to JavaScript.
|
|
8
|
+
*
|
|
9
|
+
* Native recording/device callbacks must not crash if Expo rejects an event
|
|
10
|
+
* while the module is not ready to emit.
|
|
11
|
+
*/
|
|
6
12
|
fun sendExpoEvent(eventName: String, params: Bundle)
|
|
7
13
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package net.siteed.audiostudio
|
|
2
|
+
|
|
3
|
+
import android.media.AudioManager
|
|
4
|
+
import android.telephony.TelephonyManager
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class AndroidCallStateTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun idleCallStateWinsOverStaleInCallAudioMode() {
|
|
12
|
+
val result = AndroidCallState.isOngoingCall(
|
|
13
|
+
TelephonyManager.CALL_STATE_IDLE,
|
|
14
|
+
AudioManager.MODE_IN_CALL
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assertFalse(result)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun ringingAndOffhookAreOngoingCalls() {
|
|
22
|
+
assertTrue(AndroidCallState.isOngoingCall(
|
|
23
|
+
TelephonyManager.CALL_STATE_RINGING,
|
|
24
|
+
AudioManager.MODE_NORMAL
|
|
25
|
+
))
|
|
26
|
+
assertTrue(AndroidCallState.isOngoingCall(
|
|
27
|
+
TelephonyManager.CALL_STATE_OFFHOOK,
|
|
28
|
+
AudioManager.MODE_NORMAL
|
|
29
|
+
))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
fun unknownCallStateFallsBackToAudioMode() {
|
|
34
|
+
assertTrue(AndroidCallState.isOngoingCall(null, AudioManager.MODE_IN_COMMUNICATION))
|
|
35
|
+
assertFalse(AndroidCallState.isOngoingCall(null, AudioManager.MODE_NORMAL))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package net.siteed.audiostudio
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class AndroidEventEmitterTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun safeSendReturnsTrueWhenEventIsSent() {
|
|
10
|
+
var sent = false
|
|
11
|
+
|
|
12
|
+
val result = AndroidEventEmitter.safeSend("Test", "event") {
|
|
13
|
+
sent = true
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
assertTrue(result)
|
|
17
|
+
assertTrue(sent)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun safeSendCatchesEmitterExceptions() {
|
|
22
|
+
val result = AndroidEventEmitter.safeSend("Test", "event") {
|
|
23
|
+
throw IllegalArgumentException("module not ready")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
assertFalse(result)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package net.siteed.audiostudio
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertFalse
|
|
4
|
+
import org.junit.Assert.assertTrue
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class InterruptionAutoResumePolicyTest {
|
|
8
|
+
@Test
|
|
9
|
+
fun autoResumesOnlyWhenSystemInterruptionPausedRecording() {
|
|
10
|
+
assertTrue(InterruptionAutoResumePolicy.shouldAutoResume(
|
|
11
|
+
autoResumeAfterInterruption = true,
|
|
12
|
+
isRecording = true,
|
|
13
|
+
isPaused = true,
|
|
14
|
+
pausedBySystemInterruption = true
|
|
15
|
+
))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun doesNotAutoResumeUserPausedRecording() {
|
|
20
|
+
assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
|
|
21
|
+
autoResumeAfterInterruption = true,
|
|
22
|
+
isRecording = true,
|
|
23
|
+
isPaused = true,
|
|
24
|
+
pausedBySystemInterruption = false
|
|
25
|
+
))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test
|
|
29
|
+
fun requiresAutoResumeAndActivePausedRecording() {
|
|
30
|
+
assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
|
|
31
|
+
autoResumeAfterInterruption = false,
|
|
32
|
+
isRecording = true,
|
|
33
|
+
isPaused = true,
|
|
34
|
+
pausedBySystemInterruption = true
|
|
35
|
+
))
|
|
36
|
+
assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
|
|
37
|
+
autoResumeAfterInterruption = true,
|
|
38
|
+
isRecording = false,
|
|
39
|
+
isPaused = true,
|
|
40
|
+
pausedBySystemInterruption = true
|
|
41
|
+
))
|
|
42
|
+
assertFalse(InterruptionAutoResumePolicy.shouldAutoResume(
|
|
43
|
+
autoResumeAfterInterruption = true,
|
|
44
|
+
isRecording = true,
|
|
45
|
+
isPaused = false,
|
|
46
|
+
pausedBySystemInterruption = true
|
|
47
|
+
))
|
|
48
|
+
}
|
|
49
|
+
}
|