@siteed/expo-audio-studio 2.10.6 → 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 +15 -1
- package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/DeviceDisconnectionFallbackTest.kt +218 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/EventEmissionIntervalTest.kt +120 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/M4aFormatTest.kt +345 -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/AudioProcessor.kt +44 -32
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +198 -22
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +13 -4
- package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +249 -0
- package/android/src/test/java/net/siteed/audiostream/AudioFormatTest.kt +151 -0
- package/android/src/test/java/net/siteed/audiostream/DeviceDisconnectionFallbackUnitTest.kt +140 -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 +25 -2
- package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +55 -43
- package/ios/ExpoAudioStudioTests/EventEmissionIntervalTests.swift +105 -0
- package/ios/tests/README.md +41 -0
- package/ios/tests/opus_support_test_macos.swift +154 -0
- package/package.json +2 -2
- package/src/ExpoAudioStream.types.ts +27 -2
|
@@ -17,7 +17,8 @@ data class OutputConfig(
|
|
|
17
17
|
data class CompressedOutput(
|
|
18
18
|
val enabled: Boolean = false,
|
|
19
19
|
val format: String = "aac",
|
|
20
|
-
val bitrate: Int = 128000
|
|
20
|
+
val bitrate: Int = 128000,
|
|
21
|
+
val preferRawStream: Boolean = false
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
companion object {
|
|
@@ -35,7 +36,8 @@ data class OutputConfig(
|
|
|
35
36
|
val compressed = CompressedOutput(
|
|
36
37
|
enabled = compressedMap.getBooleanOrDefault("enabled", false),
|
|
37
38
|
format = compressedMap.getStringOrDefault("format", "aac").lowercase(),
|
|
38
|
-
bitrate = compressedMap.getNumberOrDefault("bitrate", 128000)
|
|
39
|
+
bitrate = compressedMap.getNumberOrDefault("bitrate", 128000),
|
|
40
|
+
preferRawStream = compressedMap.getBooleanOrDefault("preferRawStream", false)
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
return OutputConfig(primary = primary, compressed = compressed)
|
|
@@ -62,6 +64,7 @@ data class RecordingConfig(
|
|
|
62
64
|
val filename: String? = null,
|
|
63
65
|
val deviceId: String? = null,
|
|
64
66
|
val deviceDisconnectionBehavior: String? = null,
|
|
67
|
+
val audioFocusStrategy: String? = null,
|
|
65
68
|
val bufferDurationSeconds: Double? = null,
|
|
66
69
|
) {
|
|
67
70
|
companion object {
|
|
@@ -123,6 +126,10 @@ data class RecordingConfig(
|
|
|
123
126
|
// Get device-related settings
|
|
124
127
|
val deviceId = options["deviceId"] as? String
|
|
125
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
|
|
126
133
|
|
|
127
134
|
// Initialize the recording configuration with cleaned directory path
|
|
128
135
|
val tempRecordingConfig = RecordingConfig(
|
|
@@ -130,8 +137,9 @@ data class RecordingConfig(
|
|
|
130
137
|
channels = options.getNumberOrDefault("channels", 1),
|
|
131
138
|
encoding = options.getStringOrDefault("encoding", "pcm_16bit"),
|
|
132
139
|
keepAwake = options.getBooleanOrDefault("keepAwake", true),
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
// Enforce minimum intervals to prevent excessive CPU usage
|
|
141
|
+
interval = maxOf(Constants.MIN_INTERVAL, options.getNumberOrDefault("interval", Constants.DEFAULT_INTERVAL)),
|
|
142
|
+
intervalAnalysis = maxOf(Constants.MIN_INTERVAL, options.getNumberOrDefault("intervalAnalysis", Constants.DEFAULT_INTERVAL_ANALYSIS)),
|
|
135
143
|
enableProcessing = options.getBooleanOrDefault("enableProcessing", false),
|
|
136
144
|
segmentDurationMs = options.getNumberOrDefault("segmentDurationMs", 100),
|
|
137
145
|
showNotification = options.getBooleanOrDefault("showNotification", false),
|
|
@@ -148,6 +156,7 @@ data class RecordingConfig(
|
|
|
148
156
|
filename = options["filename"] as? String,
|
|
149
157
|
deviceId = deviceId,
|
|
150
158
|
deviceDisconnectionBehavior = deviceDisconnectionBehavior,
|
|
159
|
+
audioFocusStrategy = audioFocusStrategy,
|
|
151
160
|
bufferDurationSeconds = (options["bufferDurationSeconds"] as? Number)?.toDouble(),
|
|
152
161
|
)
|
|
153
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
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import org.junit.Test
|
|
4
|
+
import org.junit.Assert.*
|
|
5
|
+
import org.junit.Before
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
class AudioFormatTest {
|
|
9
|
+
private lateinit var tempDir: File
|
|
10
|
+
|
|
11
|
+
@Before
|
|
12
|
+
fun setUp() {
|
|
13
|
+
tempDir = File(System.getProperty("java.io.tmpdir"), "audio_format_test_${System.currentTimeMillis()}")
|
|
14
|
+
tempDir.mkdirs()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun testGetFileExtension_aacDefaultsToM4a() {
|
|
19
|
+
// Given
|
|
20
|
+
val config = RecordingConfig(
|
|
21
|
+
output = OutputConfig(
|
|
22
|
+
compressed = OutputConfig.CompressedOutput(
|
|
23
|
+
enabled = true,
|
|
24
|
+
format = "aac",
|
|
25
|
+
preferRawStream = false
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// When
|
|
31
|
+
val file = createTestFile(config, isCompressed = true)
|
|
32
|
+
|
|
33
|
+
// Then
|
|
34
|
+
assertTrue("AAC without preferRawStream should produce .m4a", file.name.endsWith(".m4a"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun testGetFileExtension_aacWithPreferRawStreamProducesAac() {
|
|
39
|
+
// Given
|
|
40
|
+
val config = RecordingConfig(
|
|
41
|
+
output = OutputConfig(
|
|
42
|
+
compressed = OutputConfig.CompressedOutput(
|
|
43
|
+
enabled = true,
|
|
44
|
+
format = "aac",
|
|
45
|
+
preferRawStream = true
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// When
|
|
51
|
+
val file = createTestFile(config, isCompressed = true)
|
|
52
|
+
|
|
53
|
+
// Then
|
|
54
|
+
assertTrue("AAC with preferRawStream should produce .aac", file.name.endsWith(".aac"))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun testGetFileExtension_opusProducesOpus() {
|
|
59
|
+
// Given
|
|
60
|
+
val config = RecordingConfig(
|
|
61
|
+
output = OutputConfig(
|
|
62
|
+
compressed = OutputConfig.CompressedOutput(
|
|
63
|
+
enabled = true,
|
|
64
|
+
format = "opus"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// When
|
|
70
|
+
val file = createTestFile(config, isCompressed = true)
|
|
71
|
+
|
|
72
|
+
// Then
|
|
73
|
+
assertTrue("Opus should produce .opus", file.name.endsWith(".opus"))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun testGetFileExtension_wavForUncompressed() {
|
|
78
|
+
// Given
|
|
79
|
+
val config = RecordingConfig()
|
|
80
|
+
|
|
81
|
+
// When
|
|
82
|
+
val file = createTestFile(config, isCompressed = false)
|
|
83
|
+
|
|
84
|
+
// Then
|
|
85
|
+
assertTrue("Uncompressed should produce .wav", file.name.endsWith(".wav"))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
fun testCompressedOutput_parseFromMap() {
|
|
90
|
+
// Given
|
|
91
|
+
val map = mapOf(
|
|
92
|
+
"enabled" to true,
|
|
93
|
+
"format" to "aac",
|
|
94
|
+
"bitrate" to 192000,
|
|
95
|
+
"preferRawStream" to true
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// When
|
|
99
|
+
val compressed = OutputConfig.CompressedOutput(
|
|
100
|
+
enabled = map["enabled"] as Boolean,
|
|
101
|
+
format = map["format"] as String,
|
|
102
|
+
bitrate = map["bitrate"] as Int,
|
|
103
|
+
preferRawStream = map["preferRawStream"] as Boolean
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// Then
|
|
107
|
+
assertTrue("enabled should be true", compressed.enabled)
|
|
108
|
+
assertEquals("format should be aac", "aac", compressed.format)
|
|
109
|
+
assertEquals("bitrate should be 192000", 192000, compressed.bitrate)
|
|
110
|
+
assertTrue("preferRawStream should be true", compressed.preferRawStream)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun testCompressedOutput_defaultValues() {
|
|
115
|
+
// When
|
|
116
|
+
val compressed = OutputConfig.CompressedOutput()
|
|
117
|
+
|
|
118
|
+
// Then
|
|
119
|
+
assertFalse("enabled should default to false", compressed.enabled)
|
|
120
|
+
assertEquals("format should default to aac", "aac", compressed.format)
|
|
121
|
+
assertEquals("bitrate should default to 128000", 128000, compressed.bitrate)
|
|
122
|
+
assertFalse("preferRawStream should default to false", compressed.preferRawStream)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Helper function to simulate file creation logic from AudioRecorderManager
|
|
127
|
+
*/
|
|
128
|
+
private fun createTestFile(config: RecordingConfig, isCompressed: Boolean): File {
|
|
129
|
+
val baseFilename = config.filename?.let {
|
|
130
|
+
it.substringBeforeLast('.', it)
|
|
131
|
+
} ?: "test_recording"
|
|
132
|
+
|
|
133
|
+
val extension = if (isCompressed) {
|
|
134
|
+
when (config.output.compressed.format.lowercase()) {
|
|
135
|
+
"aac" -> {
|
|
136
|
+
if (config.output.compressed.preferRawStream) {
|
|
137
|
+
"aac" // Raw AAC stream
|
|
138
|
+
} else {
|
|
139
|
+
"m4a" // M4A container (new default)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
"opus" -> "opus" // Opus in OGG container
|
|
143
|
+
else -> config.output.compressed.format.lowercase()
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
"wav"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return File(tempDir, "$baseFilename.$extension")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.*
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unit test for Device Disconnection Fallback Behavior
|
|
8
|
+
*
|
|
9
|
+
* Tests the configuration and expected behavior for device disconnection scenarios.
|
|
10
|
+
*/
|
|
11
|
+
class DeviceDisconnectionFallbackUnitTest {
|
|
12
|
+
|
|
13
|
+
@Test
|
|
14
|
+
fun `test RecordingConfig stores deviceDisconnectionBehavior correctly`() {
|
|
15
|
+
// Test fallback behavior
|
|
16
|
+
val fallbackConfig = RecordingConfig(
|
|
17
|
+
sampleRate = 44100,
|
|
18
|
+
channels = 1,
|
|
19
|
+
encoding = "pcm_16bit",
|
|
20
|
+
deviceDisconnectionBehavior = "fallback"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assertEquals("Should store fallback behavior", "fallback", fallbackConfig.deviceDisconnectionBehavior)
|
|
24
|
+
|
|
25
|
+
// Test pause behavior
|
|
26
|
+
val pauseConfig = RecordingConfig(
|
|
27
|
+
sampleRate = 44100,
|
|
28
|
+
channels = 1,
|
|
29
|
+
encoding = "pcm_16bit",
|
|
30
|
+
deviceDisconnectionBehavior = "pause"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assertEquals("Should store pause behavior", "pause", pauseConfig.deviceDisconnectionBehavior)
|
|
34
|
+
|
|
35
|
+
// Test default behavior (should be null)
|
|
36
|
+
val defaultConfig = RecordingConfig(
|
|
37
|
+
sampleRate = 44100,
|
|
38
|
+
channels = 1,
|
|
39
|
+
encoding = "pcm_16bit"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assertNull("Default behavior should be null", defaultConfig.deviceDisconnectionBehavior)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun `test AudioRecorderManager stores deviceDisconnectionBehavior`() {
|
|
47
|
+
val config = RecordingConfig(
|
|
48
|
+
sampleRate = 44100,
|
|
49
|
+
channels = 1,
|
|
50
|
+
encoding = "pcm_16bit",
|
|
51
|
+
deviceDisconnectionBehavior = "fallback"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// Verify the config has the correct behavior
|
|
55
|
+
assertEquals("fallback", config.deviceDisconnectionBehavior)
|
|
56
|
+
|
|
57
|
+
// AudioRecorderManager should use this configuration
|
|
58
|
+
// The actual AudioRecorderManager.getDeviceDisconnectionBehavior()
|
|
59
|
+
// will return this value when recording is started with this config
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Test
|
|
63
|
+
fun `test device disconnection behavior values`() {
|
|
64
|
+
val validBehaviors = listOf("fallback", "pause")
|
|
65
|
+
|
|
66
|
+
for (behavior in validBehaviors) {
|
|
67
|
+
val config = RecordingConfig(
|
|
68
|
+
sampleRate = 44100,
|
|
69
|
+
channels = 1,
|
|
70
|
+
encoding = "pcm_16bit",
|
|
71
|
+
deviceDisconnectionBehavior = behavior
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assertEquals("Should accept $behavior behavior", behavior, config.deviceDisconnectionBehavior)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
fun `test interruption event reasons`() {
|
|
80
|
+
// Test expected event reasons for device disconnection scenarios
|
|
81
|
+
val expectedReasons = mapOf(
|
|
82
|
+
"fallback" to listOf("deviceFallback", "deviceSwitchFailed"),
|
|
83
|
+
"pause" to listOf("deviceDisconnected")
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Verify the expected reasons are valid strings
|
|
87
|
+
expectedReasons.forEach { (behavior, reasons) ->
|
|
88
|
+
assertNotNull("Behavior $behavior should have reasons", reasons)
|
|
89
|
+
assertTrue("Behavior $behavior should have at least one reason", reasons.isNotEmpty())
|
|
90
|
+
|
|
91
|
+
reasons.forEach { reason ->
|
|
92
|
+
assertNotNull("Reason should not be null", reason)
|
|
93
|
+
assertTrue("Reason should not be empty", reason.isNotEmpty())
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun `test fallback behavior logic`() {
|
|
100
|
+
// Test the logic for fallback behavior
|
|
101
|
+
val behavior = "fallback"
|
|
102
|
+
|
|
103
|
+
// When behavior is fallback:
|
|
104
|
+
// 1. Should attempt to get default device
|
|
105
|
+
// 2. If default device exists, should select it
|
|
106
|
+
// 3. If selection succeeds, should send "deviceFallback" event
|
|
107
|
+
// 4. If selection fails, should pause and send "deviceSwitchFailed" event
|
|
108
|
+
// 5. If no default device, should pause and send "deviceDisconnected" event
|
|
109
|
+
|
|
110
|
+
when (behavior) {
|
|
111
|
+
"fallback" -> {
|
|
112
|
+
// This branch should be taken
|
|
113
|
+
assertTrue("Should handle fallback behavior", true)
|
|
114
|
+
}
|
|
115
|
+
else -> {
|
|
116
|
+
fail("Should not reach default case for fallback behavior")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
fun `test pause behavior logic`() {
|
|
123
|
+
// Test the logic for pause behavior
|
|
124
|
+
val behavior = "pause"
|
|
125
|
+
|
|
126
|
+
// When behavior is pause:
|
|
127
|
+
// 1. Should pause recording immediately
|
|
128
|
+
// 2. Should send "deviceDisconnected" event
|
|
129
|
+
|
|
130
|
+
when (behavior) {
|
|
131
|
+
"fallback" -> {
|
|
132
|
+
fail("Should not handle as fallback")
|
|
133
|
+
}
|
|
134
|
+
else -> {
|
|
135
|
+
// This branch should be taken for pause
|
|
136
|
+
assertTrue("Should handle pause behavior", true)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|