@siteed/expo-audio-studio 2.11.0 → 2.12.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 +7 -1
- package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt +252 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +5 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +173 -18
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +6 -0
- package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +249 -0
- package/build/cjs/ExpoAudioStream.types.js.map +1 -1
- package/build/esm/ExpoAudioStream.types.js.map +1 -1
- package/build/types/ExpoAudioStream.types.d.ts +16 -0
- package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +49 -40
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +18 -0
|
@@ -114,8 +114,17 @@ class AudioRecorderManager(
|
|
|
114
114
|
private var telephonyManager: TelephonyManager? = null
|
|
115
115
|
get() {
|
|
116
116
|
if (field == null) {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
try {
|
|
118
|
+
field = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
|
119
|
+
if (field == null) {
|
|
120
|
+
LogUtils.w(CLASS_NAME, "TelephonyManager is null - device may not have telephony service (tablet/emulator)")
|
|
121
|
+
} else {
|
|
122
|
+
LogUtils.d(CLASS_NAME, "TelephonyManager initialization: successful")
|
|
123
|
+
}
|
|
124
|
+
} catch (e: Exception) {
|
|
125
|
+
LogUtils.w(CLASS_NAME, "Failed to initialize TelephonyManager: ${e.message}")
|
|
126
|
+
field = null
|
|
127
|
+
}
|
|
119
128
|
}
|
|
120
129
|
return field
|
|
121
130
|
}
|
|
@@ -409,8 +418,9 @@ class AudioRecorderManager(
|
|
|
409
418
|
}
|
|
410
419
|
TelephonyManager.CALL_STATE_IDLE -> {
|
|
411
420
|
if (_isRecording.get() && isPaused.get()) {
|
|
412
|
-
|
|
413
|
-
|
|
421
|
+
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
422
|
+
LogUtils.d(CLASS_NAME, "Call ended, handling auto-resume (enabled: $autoResume)")
|
|
423
|
+
if (autoResume) {
|
|
414
424
|
mainHandler.post {
|
|
415
425
|
resumeRecording(object : Promise {
|
|
416
426
|
override fun resolve(value: Any?) {
|
|
@@ -438,15 +448,18 @@ class AudioRecorderManager(
|
|
|
438
448
|
}
|
|
439
449
|
}
|
|
440
450
|
|
|
441
|
-
|
|
451
|
+
val localTelephonyManager = telephonyManager
|
|
452
|
+
if (localTelephonyManager != null) {
|
|
442
453
|
try {
|
|
443
|
-
|
|
454
|
+
localTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
|
|
444
455
|
LogUtils.d(CLASS_NAME, "Successfully registered phone state listener")
|
|
456
|
+
} catch (e: SecurityException) {
|
|
457
|
+
LogUtils.w(CLASS_NAME, "Missing permission for phone state listener: ${e.message}")
|
|
445
458
|
} catch (e: Exception) {
|
|
446
459
|
LogUtils.e(CLASS_NAME, "Failed to register phone state listener", e)
|
|
447
460
|
}
|
|
448
461
|
} else {
|
|
449
|
-
LogUtils.
|
|
462
|
+
LogUtils.w(CLASS_NAME, "TelephonyManager is null, phone call interruption handling disabled (device may not have telephony service)")
|
|
450
463
|
}
|
|
451
464
|
} else {
|
|
452
465
|
LogUtils.w(CLASS_NAME, "READ_PHONE_STATE permission not granted, phone call interruption handling disabled")
|
|
@@ -479,7 +492,8 @@ class AudioRecorderManager(
|
|
|
479
492
|
}
|
|
480
493
|
}
|
|
481
494
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
482
|
-
if (
|
|
495
|
+
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
496
|
+
if (_isRecording.get() && isPaused.get() && autoResume) {
|
|
483
497
|
mainHandler.post {
|
|
484
498
|
resumeRecording(object : Promise {
|
|
485
499
|
override fun resolve(value: Any?) {
|
|
@@ -608,7 +622,11 @@ class AudioRecorderManager(
|
|
|
608
622
|
|
|
609
623
|
} catch (e: Exception) {
|
|
610
624
|
releaseAudioFocus()
|
|
611
|
-
|
|
625
|
+
try {
|
|
626
|
+
telephonyManager?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
|
|
627
|
+
} catch (e: Exception) {
|
|
628
|
+
LogUtils.w(CLASS_NAME, "Failed to unregister phone state listener: ${e.message}")
|
|
629
|
+
}
|
|
612
630
|
promise.reject("UNEXPECTED_ERROR", "Unexpected error: ${e.message}", e)
|
|
613
631
|
}
|
|
614
632
|
}
|
|
@@ -985,15 +1003,30 @@ class AudioRecorderManager(
|
|
|
985
1003
|
val fileSize = audioFile?.length() ?: 0
|
|
986
1004
|
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes, Path: ${audioFile?.absolutePath}")
|
|
987
1005
|
|
|
988
|
-
|
|
989
|
-
val
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1006
|
+
// Calculate duration based on context - use actual recording time for streaming-only mode
|
|
1007
|
+
val duration = if (!recordingConfig.output.primary.enabled) {
|
|
1008
|
+
// For streaming-only mode, calculate duration from actual recording time
|
|
1009
|
+
val actualRecordingTime = if (recordingStartTime > 0) {
|
|
1010
|
+
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
1011
|
+
} else {
|
|
1012
|
+
0L
|
|
995
1013
|
}
|
|
996
|
-
|
|
1014
|
+
LogUtils.d(CLASS_NAME, "Streaming-only mode: Using actual recording time: ${actualRecordingTime}ms")
|
|
1015
|
+
actualRecordingTime
|
|
1016
|
+
} else {
|
|
1017
|
+
// For file-based recording, calculate duration from file size
|
|
1018
|
+
val dataFileSize = fileSize - 44 // Subtract header size
|
|
1019
|
+
val byteRate =
|
|
1020
|
+
recordingConfig.sampleRate * recordingConfig.channels * when (recordingConfig.encoding) {
|
|
1021
|
+
"pcm_8bit" -> 1
|
|
1022
|
+
"pcm_16bit" -> 2
|
|
1023
|
+
"pcm_32bit" -> 4
|
|
1024
|
+
else -> 2 // Default to 2 bytes per sample if the encoding is not recognized
|
|
1025
|
+
}
|
|
1026
|
+
val fileDuration = if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0
|
|
1027
|
+
LogUtils.d(CLASS_NAME, "File-based mode: Using file size duration: ${fileDuration}ms")
|
|
1028
|
+
fileDuration
|
|
1029
|
+
}
|
|
997
1030
|
|
|
998
1031
|
compressedRecorder?.apply {
|
|
999
1032
|
stop()
|
|
@@ -1687,6 +1720,55 @@ class AudioRecorderManager(
|
|
|
1687
1720
|
|
|
1688
1721
|
@SuppressLint("NewApi")
|
|
1689
1722
|
private fun requestAudioFocus(): Boolean {
|
|
1723
|
+
val strategy = getAudioFocusStrategy()
|
|
1724
|
+
|
|
1725
|
+
when (strategy) {
|
|
1726
|
+
"none" -> {
|
|
1727
|
+
LogUtils.d(CLASS_NAME, "Skipping audio focus request (strategy: none)")
|
|
1728
|
+
return true
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
"background" -> {
|
|
1732
|
+
LogUtils.d(CLASS_NAME, "Background recording - minimal audio focus")
|
|
1733
|
+
// For true background recording, we don't request audio focus
|
|
1734
|
+
// This allows recording to continue uninterrupted when users switch apps
|
|
1735
|
+
return true
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
"communication" -> {
|
|
1739
|
+
return requestCommunicationAudioFocus()
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
"interactive" -> {
|
|
1743
|
+
return requestInteractiveAudioFocus()
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
else -> {
|
|
1747
|
+
LogUtils.w(CLASS_NAME, "Unknown audio focus strategy: $strategy, using interactive")
|
|
1748
|
+
return requestInteractiveAudioFocus()
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private fun getAudioFocusStrategy(): String {
|
|
1754
|
+
// Use explicit strategy if provided
|
|
1755
|
+
if (::recordingConfig.isInitialized) {
|
|
1756
|
+
recordingConfig.audioFocusStrategy?.let { return it }
|
|
1757
|
+
|
|
1758
|
+
// Smart defaults based on other config
|
|
1759
|
+
return if (recordingConfig.keepAwake && enableBackgroundAudio) {
|
|
1760
|
+
"background"
|
|
1761
|
+
} else {
|
|
1762
|
+
"interactive"
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Default strategy if recordingConfig is not initialized
|
|
1767
|
+
return "interactive"
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
@SuppressLint("NewApi")
|
|
1771
|
+
private fun requestInteractiveAudioFocus(): Boolean {
|
|
1690
1772
|
audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
1691
1773
|
when (focusChange) {
|
|
1692
1774
|
AudioManager.AUDIOFOCUS_LOSS,
|
|
@@ -1709,7 +1791,8 @@ class AudioRecorderManager(
|
|
|
1709
1791
|
}
|
|
1710
1792
|
}
|
|
1711
1793
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1712
|
-
if (
|
|
1794
|
+
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
1795
|
+
if (_isRecording.get() && isPaused.get() && autoResume) {
|
|
1713
1796
|
mainHandler.post {
|
|
1714
1797
|
resumeRecording(object : Promise {
|
|
1715
1798
|
override fun resolve(value: Any?) {
|
|
@@ -1750,6 +1833,78 @@ class AudioRecorderManager(
|
|
|
1750
1833
|
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
1751
1834
|
}
|
|
1752
1835
|
|
|
1836
|
+
@SuppressLint("NewApi")
|
|
1837
|
+
private fun requestCommunicationAudioFocus(): Boolean {
|
|
1838
|
+
audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
1839
|
+
when (focusChange) {
|
|
1840
|
+
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
1841
|
+
// Only pause for permanent focus loss (like phone calls)
|
|
1842
|
+
if (_isRecording.get() && !isPaused.get()) {
|
|
1843
|
+
mainHandler.post {
|
|
1844
|
+
pauseRecording(object : Promise {
|
|
1845
|
+
override fun resolve(value: Any?) {
|
|
1846
|
+
isPaused.set(true)
|
|
1847
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1848
|
+
"reason" to "audioFocusLoss",
|
|
1849
|
+
"isPaused" to true
|
|
1850
|
+
))
|
|
1851
|
+
}
|
|
1852
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1853
|
+
LogUtils.e(CLASS_NAME, "Failed to pause recording on audio focus loss")
|
|
1854
|
+
}
|
|
1855
|
+
})
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
1860
|
+
// Don't pause for temporary loss in communication mode
|
|
1861
|
+
LogUtils.d(CLASS_NAME, "Ignoring transient audio focus loss in communication mode")
|
|
1862
|
+
}
|
|
1863
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
1864
|
+
val autoResume = if (::recordingConfig.isInitialized) recordingConfig.autoResumeAfterInterruption else false
|
|
1865
|
+
if (_isRecording.get() && isPaused.get() && autoResume) {
|
|
1866
|
+
mainHandler.post {
|
|
1867
|
+
resumeRecording(object : Promise {
|
|
1868
|
+
override fun resolve(value: Any?) {
|
|
1869
|
+
eventSender.sendExpoEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1870
|
+
"reason" to "audioFocusGain",
|
|
1871
|
+
"isPaused" to false
|
|
1872
|
+
))
|
|
1873
|
+
}
|
|
1874
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1875
|
+
LogUtils.e(CLASS_NAME, "Failed to resume recording on audio focus gain")
|
|
1876
|
+
}
|
|
1877
|
+
})
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1885
|
+
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
|
1886
|
+
.setAudioAttributes(AudioAttributes.Builder()
|
|
1887
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
|
1888
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
1889
|
+
.build())
|
|
1890
|
+
.setAcceptsDelayedFocusGain(false)
|
|
1891
|
+
.setWillPauseWhenDucked(false)
|
|
1892
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener!!)
|
|
1893
|
+
.build()
|
|
1894
|
+
audioFocusRequest = focusRequest
|
|
1895
|
+
audioManager.requestAudioFocus(focusRequest)
|
|
1896
|
+
} else {
|
|
1897
|
+
@Suppress("DEPRECATION")
|
|
1898
|
+
audioManager.requestAudioFocus(
|
|
1899
|
+
audioFocusChangeListener,
|
|
1900
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
1901
|
+
AudioManager.AUDIOFOCUS_GAIN
|
|
1902
|
+
)
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1753
1908
|
private fun releaseAudioFocus() {
|
|
1754
1909
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1755
1910
|
(audioFocusRequest as? AudioFocusRequest)?.let { request ->
|
|
@@ -64,6 +64,7 @@ data class RecordingConfig(
|
|
|
64
64
|
val filename: String? = null,
|
|
65
65
|
val deviceId: String? = null,
|
|
66
66
|
val deviceDisconnectionBehavior: String? = null,
|
|
67
|
+
val audioFocusStrategy: String? = null,
|
|
67
68
|
val bufferDurationSeconds: Double? = null,
|
|
68
69
|
) {
|
|
69
70
|
companion object {
|
|
@@ -125,6 +126,10 @@ data class RecordingConfig(
|
|
|
125
126
|
// Get device-related settings
|
|
126
127
|
val deviceId = options["deviceId"] as? String
|
|
127
128
|
val deviceDisconnectionBehavior = options["deviceDisconnectionBehavior"] as? String
|
|
129
|
+
|
|
130
|
+
// Get Android-specific settings
|
|
131
|
+
val androidConfig = options["android"] as? Map<String, Any>
|
|
132
|
+
val audioFocusStrategy = androidConfig?.get("audioFocusStrategy") as? String
|
|
128
133
|
|
|
129
134
|
// Initialize the recording configuration with cleaned directory path
|
|
130
135
|
val tempRecordingConfig = RecordingConfig(
|
|
@@ -151,6 +156,7 @@ data class RecordingConfig(
|
|
|
151
156
|
filename = options["filename"] as? String,
|
|
152
157
|
deviceId = deviceId,
|
|
153
158
|
deviceDisconnectionBehavior = deviceDisconnectionBehavior,
|
|
159
|
+
audioFocusStrategy = audioFocusStrategy,
|
|
154
160
|
bufferDurationSeconds = (options["bufferDurationSeconds"] as? Number)?.toDouble(),
|
|
155
161
|
)
|
|
156
162
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import org.junit.Test
|
|
4
|
+
import org.junit.Assert.*
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unit tests for audio focus strategy configuration and logic.
|
|
8
|
+
* These tests verify that the RecordingConfig correctly handles audioFocusStrategy
|
|
9
|
+
* parameter and that the smart defaults work as expected.
|
|
10
|
+
*/
|
|
11
|
+
class AudioFocusStrategyTest {
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun testRecordingConfigWithExplicitBackgroundStrategy() {
|
|
15
|
+
val options = mapOf(
|
|
16
|
+
"sampleRate" to 44100,
|
|
17
|
+
"channels" to 1,
|
|
18
|
+
"encoding" to "pcm_16bit",
|
|
19
|
+
"android" to mapOf(
|
|
20
|
+
"audioFocusStrategy" to "background"
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
val result = RecordingConfig.fromMap(options)
|
|
25
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
26
|
+
|
|
27
|
+
val (config, _) = result.getOrThrow()
|
|
28
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun testRecordingConfigWithExplicitInteractiveStrategy() {
|
|
33
|
+
val options = mapOf(
|
|
34
|
+
"sampleRate" to 44100,
|
|
35
|
+
"channels" to 1,
|
|
36
|
+
"encoding" to "pcm_16bit",
|
|
37
|
+
"android" to mapOf(
|
|
38
|
+
"audioFocusStrategy" to "interactive"
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
val result = RecordingConfig.fromMap(options)
|
|
43
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
44
|
+
|
|
45
|
+
val (config, _) = result.getOrThrow()
|
|
46
|
+
assertEquals("Audio focus strategy should be interactive", "interactive", config.audioFocusStrategy)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun testRecordingConfigWithExplicitCommunicationStrategy() {
|
|
51
|
+
val options = mapOf(
|
|
52
|
+
"sampleRate" to 44100,
|
|
53
|
+
"channels" to 1,
|
|
54
|
+
"encoding" to "pcm_16bit",
|
|
55
|
+
"android" to mapOf(
|
|
56
|
+
"audioFocusStrategy" to "communication"
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
val result = RecordingConfig.fromMap(options)
|
|
61
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
62
|
+
|
|
63
|
+
val (config, _) = result.getOrThrow()
|
|
64
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
fun testRecordingConfigWithExplicitNoneStrategy() {
|
|
69
|
+
val options = mapOf(
|
|
70
|
+
"sampleRate" to 44100,
|
|
71
|
+
"channels" to 1,
|
|
72
|
+
"encoding" to "pcm_16bit",
|
|
73
|
+
"android" to mapOf(
|
|
74
|
+
"audioFocusStrategy" to "none"
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
val result = RecordingConfig.fromMap(options)
|
|
79
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
80
|
+
|
|
81
|
+
val (config, _) = result.getOrThrow()
|
|
82
|
+
assertEquals("Audio focus strategy should be none", "none", config.audioFocusStrategy)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test
|
|
86
|
+
fun testRecordingConfigWithoutAudioFocusStrategy() {
|
|
87
|
+
val options = mapOf(
|
|
88
|
+
"sampleRate" to 44100,
|
|
89
|
+
"channels" to 1,
|
|
90
|
+
"encoding" to "pcm_16bit"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
val result = RecordingConfig.fromMap(options)
|
|
94
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
95
|
+
|
|
96
|
+
val (config, _) = result.getOrThrow()
|
|
97
|
+
assertNull("Audio focus strategy should be null when not specified", config.audioFocusStrategy)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Test
|
|
101
|
+
fun testRecordingConfigWithInvalidAudioFocusStrategy() {
|
|
102
|
+
val options = mapOf(
|
|
103
|
+
"sampleRate" to 44100,
|
|
104
|
+
"channels" to 1,
|
|
105
|
+
"encoding" to "pcm_16bit",
|
|
106
|
+
"android" to mapOf(
|
|
107
|
+
"audioFocusStrategy" to "invalid_strategy"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
val result = RecordingConfig.fromMap(options)
|
|
112
|
+
assertTrue("Config creation should succeed even with invalid strategy", result.isSuccess)
|
|
113
|
+
|
|
114
|
+
val (config, _) = result.getOrThrow()
|
|
115
|
+
assertEquals("Invalid audio focus strategy should be preserved", "invalid_strategy", config.audioFocusStrategy)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun testRecordingConfigWithNullAudioFocusStrategy() {
|
|
120
|
+
val options = mapOf(
|
|
121
|
+
"sampleRate" to 44100,
|
|
122
|
+
"channels" to 1,
|
|
123
|
+
"encoding" to "pcm_16bit",
|
|
124
|
+
"android" to mapOf(
|
|
125
|
+
"audioFocusStrategy" to null
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
val result = RecordingConfig.fromMap(options)
|
|
130
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
131
|
+
|
|
132
|
+
val (config, _) = result.getOrThrow()
|
|
133
|
+
assertNull("Audio focus strategy should be null", config.audioFocusStrategy)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
fun testRecordingConfigKeepAwakeAndBackgroundStrategy() {
|
|
138
|
+
val options = mapOf(
|
|
139
|
+
"sampleRate" to 44100,
|
|
140
|
+
"channels" to 1,
|
|
141
|
+
"encoding" to "pcm_16bit",
|
|
142
|
+
"keepAwake" to true,
|
|
143
|
+
"android" to mapOf(
|
|
144
|
+
"audioFocusStrategy" to "background"
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
val result = RecordingConfig.fromMap(options)
|
|
149
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
150
|
+
|
|
151
|
+
val (config, _) = result.getOrThrow()
|
|
152
|
+
assertTrue("keepAwake should be true", config.keepAwake)
|
|
153
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun testRecordingConfigKeepAwakeFalseAndInteractiveStrategy() {
|
|
158
|
+
val options = mapOf(
|
|
159
|
+
"sampleRate" to 44100,
|
|
160
|
+
"channels" to 1,
|
|
161
|
+
"encoding" to "pcm_16bit",
|
|
162
|
+
"keepAwake" to false,
|
|
163
|
+
"android" to mapOf(
|
|
164
|
+
"audioFocusStrategy" to "interactive"
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
val result = RecordingConfig.fromMap(options)
|
|
169
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
170
|
+
|
|
171
|
+
val (config, _) = result.getOrThrow()
|
|
172
|
+
assertFalse("keepAwake should be false", config.keepAwake)
|
|
173
|
+
assertEquals("Audio focus strategy should be interactive", "interactive", config.audioFocusStrategy)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
fun testRecordingConfigWithAutoResumeAndBackgroundStrategy() {
|
|
178
|
+
val options = mapOf(
|
|
179
|
+
"sampleRate" to 44100,
|
|
180
|
+
"channels" to 1,
|
|
181
|
+
"encoding" to "pcm_16bit",
|
|
182
|
+
"autoResumeAfterInterruption" to true,
|
|
183
|
+
"android" to mapOf(
|
|
184
|
+
"audioFocusStrategy" to "background"
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val result = RecordingConfig.fromMap(options)
|
|
189
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
190
|
+
|
|
191
|
+
val (config, _) = result.getOrThrow()
|
|
192
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
193
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Test
|
|
197
|
+
fun testRecordingConfigWithCommunicationStrategyAndSpeechSampleRate() {
|
|
198
|
+
val options = mapOf(
|
|
199
|
+
"sampleRate" to 16000, // Common speech sample rate
|
|
200
|
+
"channels" to 1,
|
|
201
|
+
"encoding" to "pcm_16bit",
|
|
202
|
+
"android" to mapOf(
|
|
203
|
+
"audioFocusStrategy" to "communication"
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
val result = RecordingConfig.fromMap(options)
|
|
208
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
209
|
+
|
|
210
|
+
val (config, _) = result.getOrThrow()
|
|
211
|
+
assertEquals("Sample rate should be 16000", 16000, config.sampleRate)
|
|
212
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Test
|
|
216
|
+
fun testDefaultRecordingConfigValues() {
|
|
217
|
+
val result = RecordingConfig.fromMap(null)
|
|
218
|
+
assertTrue("Config creation should succeed with null input", result.isSuccess)
|
|
219
|
+
|
|
220
|
+
val (config, _) = result.getOrThrow()
|
|
221
|
+
assertNull("Default audio focus strategy should be null", config.audioFocusStrategy)
|
|
222
|
+
assertTrue("Default keepAwake should be true", config.keepAwake)
|
|
223
|
+
assertFalse("Default autoResumeAfterInterruption should be false", config.autoResumeAfterInterruption)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Test
|
|
227
|
+
fun testRecordingConfigCompleteAudioFocusConfiguration() {
|
|
228
|
+
val options = mapOf(
|
|
229
|
+
"sampleRate" to 44100,
|
|
230
|
+
"channels" to 1,
|
|
231
|
+
"encoding" to "pcm_16bit",
|
|
232
|
+
"keepAwake" to true,
|
|
233
|
+
"autoResumeAfterInterruption" to true,
|
|
234
|
+
"showNotification" to true,
|
|
235
|
+
"android" to mapOf(
|
|
236
|
+
"audioFocusStrategy" to "background"
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
val result = RecordingConfig.fromMap(options)
|
|
241
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
242
|
+
|
|
243
|
+
val (config, _) = result.getOrThrow()
|
|
244
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
245
|
+
assertTrue("keepAwake should be true", config.keepAwake)
|
|
246
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
247
|
+
assertTrue("showNotification should be true", config.showNotification)
|
|
248
|
+
}
|
|
249
|
+
}
|