@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
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.12.0] - 2025-06-07
|
|
12
|
+
### Changed
|
|
13
|
+
- fix(android): resolve PCM streaming duration calculation bug (Issue #263) (#265) ([a0c5500](https://github.com/deeeed/expo-audio-stream/commit/a0c550099fec9d6b0d486440819173d9d9275908))
|
|
14
|
+
- feat(expo-audio-studio): implement Android-only audioFocusStrategy (#264) ([cc77226](https://github.com/deeeed/expo-audio-stream/commit/cc7722605a5502a58b8236b610c9bdccf5f7f561))
|
|
15
|
+
- docs: fix comment formatting in OutputConfig interface for clarity ([07fac61](https://github.com/deeeed/expo-audio-stream/commit/07fac61245843c601709bb7576db6e48b2106cf7))
|
|
16
|
+
## [2.11.0] - 2025-06-05
|
|
17
|
+
### Changed
|
|
18
|
+
- refactor(expo-audio-studio): remove android/build.gradle and add device disconnection fallback tests ([36fe9a9](https://github.com/deeeed/expo-audio-stream/commit/36fe9a921505e136ea50406d4b664c597293ffd8))
|
|
19
|
+
- fix(expo-audio-studio): enforce 10ms minimum interval on both platforms (#262) ([035fc07](https://github.com/deeeed/expo-audio-stream/commit/035fc076334c169a2371527bad0ca60f222d10ee))
|
|
20
|
+
- fix(expo-audio-studio): add proper MediaCodec resource cleanup in AudioProcessor ([2b069b6](https://github.com/deeeed/expo-audio-stream/commit/2b069b6ae512f97fae80df1b8cb38bb3a14538e5))
|
|
21
|
+
- feat(expo-audio-studio): add audio format enhancement specification and tests ([ace22a2](https://github.com/deeeed/expo-audio-stream/commit/ace22a22cf6c94ec58ddd4f6cb44f77dd383d6bc))
|
|
22
|
+
- feat\!: Add M4A support with preferRawStream option (#261) ([c9faeb0](https://github.com/deeeed/expo-audio-stream/commit/c9faeb01cd5dcd7407f73a0f7e6d5822adb862a4))
|
|
11
23
|
## [2.10.6] - 2025-06-04
|
|
12
24
|
### Changed
|
|
13
25
|
- fix(expo-audio-studio): prevent durationMs returning 0 on iOS (#244) (#260) ([595e5d5](https://github.com/deeeed/expo-audio-stream/commit/595e5d56991c9fa88c2fa4e39efb197916cb8b84))
|
|
@@ -282,7 +294,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
282
294
|
- Feature: Audio features extraction during recording.
|
|
283
295
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
284
296
|
|
|
285
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
297
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.0...HEAD
|
|
298
|
+
[2.12.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.11.0...@siteed/expo-audio-studio@2.12.0
|
|
299
|
+
[2.11.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.6...@siteed/expo-audio-studio@2.11.0
|
|
286
300
|
[2.10.6]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.5...@siteed/expo-audio-studio@2.10.6
|
|
287
301
|
[2.10.5]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.4...@siteed/expo-audio-studio@2.10.5
|
|
288
302
|
[2.10.4]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.3...@siteed/expo-audio-studio@2.10.4
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
package net.siteed.audiostream.integration
|
|
2
|
+
|
|
3
|
+
import android.media.AudioManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
6
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.Assert.*
|
|
9
|
+
import org.junit.Before
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.runner.RunWith
|
|
12
|
+
import net.siteed.audiostream.RecordingConfig
|
|
13
|
+
import java.io.File
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration tests for audio focus strategy functionality.
|
|
17
|
+
* These tests run on actual Android devices/emulators to validate that
|
|
18
|
+
* audio focus strategies work correctly in real scenarios.
|
|
19
|
+
*/
|
|
20
|
+
@RunWith(AndroidJUnit4::class)
|
|
21
|
+
class AudioFocusStrategyIntegrationTest {
|
|
22
|
+
|
|
23
|
+
private lateinit var context: Context
|
|
24
|
+
private lateinit var audioManager: AudioManager
|
|
25
|
+
private lateinit var filesDir: File
|
|
26
|
+
|
|
27
|
+
@Before
|
|
28
|
+
fun setUp() {
|
|
29
|
+
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
30
|
+
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
31
|
+
filesDir = context.filesDir
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@After
|
|
35
|
+
fun tearDown() {
|
|
36
|
+
// Clean up any test files
|
|
37
|
+
val testFiles = filesDir.listFiles { _, name ->
|
|
38
|
+
name.startsWith("test_audio_focus_")
|
|
39
|
+
}
|
|
40
|
+
testFiles?.forEach { it.delete() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun testRecordingConfigWithBackgroundStrategy() {
|
|
45
|
+
val options = mapOf(
|
|
46
|
+
"sampleRate" to 44100,
|
|
47
|
+
"channels" to 1,
|
|
48
|
+
"encoding" to "pcm_16bit",
|
|
49
|
+
"keepAwake" to true,
|
|
50
|
+
"autoResumeAfterInterruption" to true,
|
|
51
|
+
"android" to mapOf(
|
|
52
|
+
"audioFocusStrategy" to "background"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
val result = RecordingConfig.fromMap(options)
|
|
57
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
58
|
+
|
|
59
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
60
|
+
|
|
61
|
+
// Verify audio focus strategy configuration
|
|
62
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
63
|
+
assertTrue("keepAwake should be true for background recording", config.keepAwake)
|
|
64
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
65
|
+
|
|
66
|
+
// Verify audio format is properly configured
|
|
67
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
68
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun testRecordingConfigWithInteractiveStrategy() {
|
|
73
|
+
val options = mapOf(
|
|
74
|
+
"sampleRate" to 44100,
|
|
75
|
+
"channels" to 1,
|
|
76
|
+
"encoding" to "pcm_16bit",
|
|
77
|
+
"keepAwake" to false,
|
|
78
|
+
"autoResumeAfterInterruption" to true,
|
|
79
|
+
"android" to mapOf(
|
|
80
|
+
"audioFocusStrategy" to "interactive"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
val result = RecordingConfig.fromMap(options)
|
|
85
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
86
|
+
|
|
87
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
88
|
+
|
|
89
|
+
// Verify audio focus strategy configuration
|
|
90
|
+
assertEquals("Audio focus strategy should be interactive", "interactive", config.audioFocusStrategy)
|
|
91
|
+
assertFalse("keepAwake should be false for interactive recording", config.keepAwake)
|
|
92
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
93
|
+
|
|
94
|
+
// Verify audio format is properly configured
|
|
95
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
96
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
fun testRecordingConfigWithCommunicationStrategy() {
|
|
101
|
+
val options = mapOf(
|
|
102
|
+
"sampleRate" to 16000, // Common speech sample rate
|
|
103
|
+
"channels" to 1,
|
|
104
|
+
"encoding" to "pcm_16bit",
|
|
105
|
+
"keepAwake" to false,
|
|
106
|
+
"autoResumeAfterInterruption" to true,
|
|
107
|
+
"android" to mapOf(
|
|
108
|
+
"audioFocusStrategy" to "communication"
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
val result = RecordingConfig.fromMap(options)
|
|
113
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
114
|
+
|
|
115
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
116
|
+
|
|
117
|
+
// Verify audio focus strategy configuration
|
|
118
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
119
|
+
assertEquals("Sample rate should be 16000 for speech", 16000, config.sampleRate)
|
|
120
|
+
assertFalse("keepAwake should be false", config.keepAwake)
|
|
121
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
122
|
+
|
|
123
|
+
// Verify audio format is properly configured
|
|
124
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
125
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun testRecordingConfigWithNoneStrategy() {
|
|
130
|
+
val options = mapOf(
|
|
131
|
+
"sampleRate" to 44100,
|
|
132
|
+
"channels" to 1,
|
|
133
|
+
"encoding" to "pcm_16bit",
|
|
134
|
+
"keepAwake" to false,
|
|
135
|
+
"android" to mapOf(
|
|
136
|
+
"audioFocusStrategy" to "none"
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
val result = RecordingConfig.fromMap(options)
|
|
141
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
142
|
+
|
|
143
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
144
|
+
|
|
145
|
+
// Verify audio focus strategy configuration
|
|
146
|
+
assertEquals("Audio focus strategy should be none", "none", config.audioFocusStrategy)
|
|
147
|
+
|
|
148
|
+
// Verify audio format is properly configured
|
|
149
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
150
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
fun testStrategyOverrideBehavior() {
|
|
155
|
+
val options = mapOf(
|
|
156
|
+
"sampleRate" to 44100,
|
|
157
|
+
"channels" to 1,
|
|
158
|
+
"encoding" to "pcm_16bit",
|
|
159
|
+
"keepAwake" to true, // This would normally default to background
|
|
160
|
+
"android" to mapOf(
|
|
161
|
+
"audioFocusStrategy" to "communication" // But we override to communication
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
val result = RecordingConfig.fromMap(options)
|
|
166
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
167
|
+
|
|
168
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
169
|
+
|
|
170
|
+
// Verify that explicit strategy overrides keepAwake defaults
|
|
171
|
+
assertEquals("Audio focus strategy should be communication (overriding keepAwake default)", "communication", config.audioFocusStrategy)
|
|
172
|
+
assertTrue("keepAwake should still be true", config.keepAwake)
|
|
173
|
+
|
|
174
|
+
// Verify audio format is properly configured
|
|
175
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
fun testAudioFocusStrategyWithCompression() {
|
|
180
|
+
val options = mapOf(
|
|
181
|
+
"sampleRate" to 44100,
|
|
182
|
+
"channels" to 1,
|
|
183
|
+
"encoding" to "pcm_16bit",
|
|
184
|
+
"android" to mapOf(
|
|
185
|
+
"audioFocusStrategy" to "background"
|
|
186
|
+
),
|
|
187
|
+
"output" to mapOf(
|
|
188
|
+
"compressed" to mapOf(
|
|
189
|
+
"enabled" to true,
|
|
190
|
+
"format" to "aac",
|
|
191
|
+
"bitrate" to 128000
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
val result = RecordingConfig.fromMap(options)
|
|
197
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
198
|
+
|
|
199
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
200
|
+
|
|
201
|
+
// Verify audio focus strategy works with compression
|
|
202
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
203
|
+
assertTrue("Compressed output should be enabled", config.output.compressed.enabled)
|
|
204
|
+
assertEquals("Compression format should be aac", "aac", config.output.compressed.format)
|
|
205
|
+
assertEquals("Bitrate should be 128000", 128000, config.output.compressed.bitrate)
|
|
206
|
+
|
|
207
|
+
// Verify audio format is properly configured
|
|
208
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
209
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@Test
|
|
213
|
+
fun testAudioFocusStrategyWithNotifications() {
|
|
214
|
+
val options = mapOf(
|
|
215
|
+
"sampleRate" to 44100,
|
|
216
|
+
"channels" to 1,
|
|
217
|
+
"encoding" to "pcm_16bit",
|
|
218
|
+
"showNotification" to true,
|
|
219
|
+
"showWaveformInNotification" to true,
|
|
220
|
+
"android" to mapOf(
|
|
221
|
+
"audioFocusStrategy" to "background"
|
|
222
|
+
),
|
|
223
|
+
"notification" to mapOf(
|
|
224
|
+
"title" to "Recording Audio",
|
|
225
|
+
"text" to "Background recording in progress",
|
|
226
|
+
"icon" to "ic_mic"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
val result = RecordingConfig.fromMap(options)
|
|
231
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
232
|
+
|
|
233
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
234
|
+
|
|
235
|
+
// Verify audio focus strategy works with notifications
|
|
236
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
237
|
+
assertTrue("Notifications should be enabled", config.showNotification)
|
|
238
|
+
assertTrue("Waveform in notification should be enabled", config.showWaveformInNotification)
|
|
239
|
+
assertEquals("Notification title should match", "Recording Audio", config.notification.title)
|
|
240
|
+
assertEquals("Notification text should match", "Background recording in progress", config.notification.text)
|
|
241
|
+
assertEquals("Notification icon should match", "ic_mic", config.notification.icon)
|
|
242
|
+
|
|
243
|
+
// Verify audio format is properly configured
|
|
244
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
245
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@Test
|
|
249
|
+
fun testAudioFocusStrategyValidation() {
|
|
250
|
+
// Test all valid strategies
|
|
251
|
+
val strategies = listOf("background", "interactive", "communication", "none")
|
|
252
|
+
|
|
253
|
+
for (strategy in strategies) {
|
|
254
|
+
val options = mapOf(
|
|
255
|
+
"sampleRate" to 44100,
|
|
256
|
+
"channels" to 1,
|
|
257
|
+
"encoding" to "pcm_16bit",
|
|
258
|
+
"android" to mapOf(
|
|
259
|
+
"audioFocusStrategy" to strategy
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
val result = RecordingConfig.fromMap(options)
|
|
264
|
+
assertTrue("Config creation should succeed for strategy: $strategy", result.isSuccess)
|
|
265
|
+
|
|
266
|
+
val (config, _) = result.getOrThrow()
|
|
267
|
+
assertEquals("Audio focus strategy should be $strategy", strategy, config.audioFocusStrategy)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Test
|
|
272
|
+
fun testInvalidAudioFocusStrategyHandling() {
|
|
273
|
+
val options = mapOf(
|
|
274
|
+
"sampleRate" to 44100,
|
|
275
|
+
"channels" to 1,
|
|
276
|
+
"encoding" to "pcm_16bit",
|
|
277
|
+
"android" to mapOf(
|
|
278
|
+
"audioFocusStrategy" to "invalid_strategy"
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
val result = RecordingConfig.fromMap(options)
|
|
283
|
+
assertTrue("Config creation should succeed even with invalid strategy", result.isSuccess)
|
|
284
|
+
|
|
285
|
+
val (config, _) = result.getOrThrow()
|
|
286
|
+
assertEquals("Invalid strategy should be preserved", "invalid_strategy", config.audioFocusStrategy)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Test
|
|
290
|
+
fun testCompleteAudioFocusConfiguration() {
|
|
291
|
+
val options = mapOf(
|
|
292
|
+
"sampleRate" to 44100,
|
|
293
|
+
"channels" to 1,
|
|
294
|
+
"encoding" to "pcm_16bit",
|
|
295
|
+
"keepAwake" to true,
|
|
296
|
+
"autoResumeAfterInterruption" to true,
|
|
297
|
+
"showNotification" to true,
|
|
298
|
+
"showWaveformInNotification" to false,
|
|
299
|
+
"enableProcessing" to false,
|
|
300
|
+
"android" to mapOf(
|
|
301
|
+
"audioFocusStrategy" to "communication"
|
|
302
|
+
),
|
|
303
|
+
"notification" to mapOf(
|
|
304
|
+
"title" to "Voice Call Recording",
|
|
305
|
+
"text" to "Call in progress",
|
|
306
|
+
"icon" to "ic_call"
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
val result = RecordingConfig.fromMap(options)
|
|
311
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
312
|
+
|
|
313
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
314
|
+
|
|
315
|
+
// Verify complete configuration
|
|
316
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
317
|
+
assertEquals("Sample rate should be 44100", 44100, config.sampleRate)
|
|
318
|
+
assertEquals("Channels should be 1", 1, config.channels)
|
|
319
|
+
assertEquals("Encoding should be pcm_16bit", "pcm_16bit", config.encoding)
|
|
320
|
+
assertTrue("keepAwake should be true", config.keepAwake)
|
|
321
|
+
assertFalse("showWaveformInNotification should be false", config.showWaveformInNotification)
|
|
322
|
+
assertTrue("showNotification should be true", config.showNotification)
|
|
323
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
324
|
+
assertFalse("enableProcessing should be false", config.enableProcessing)
|
|
325
|
+
assertEquals("Notification title should match", "Voice Call Recording", config.notification.title)
|
|
326
|
+
assertEquals("Notification text should match", "Call in progress", config.notification.text)
|
|
327
|
+
|
|
328
|
+
// Verify audio format is properly configured
|
|
329
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
330
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
package net.siteed.audiostream.integration
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
5
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
6
|
+
import net.siteed.audiostream.OutputConfig
|
|
7
|
+
import net.siteed.audiostream.RecordingConfig
|
|
8
|
+
import org.junit.After
|
|
9
|
+
import org.junit.Assert.*
|
|
10
|
+
import org.junit.Before
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
import org.junit.runner.RunWith
|
|
13
|
+
import java.io.File
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration test for device disconnection fallback behavior.
|
|
17
|
+
* Tests various scenarios when audio devices are disconnected during recording.
|
|
18
|
+
*/
|
|
19
|
+
@RunWith(AndroidJUnit4::class)
|
|
20
|
+
class DeviceDisconnectionFallbackTest {
|
|
21
|
+
|
|
22
|
+
private lateinit var context: Context
|
|
23
|
+
private lateinit var testDir: File
|
|
24
|
+
|
|
25
|
+
@Before
|
|
26
|
+
fun setup() {
|
|
27
|
+
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
28
|
+
|
|
29
|
+
// Create test directory
|
|
30
|
+
testDir = File(context.getExternalFilesDir(null), "fallback_test")
|
|
31
|
+
testDir.mkdirs()
|
|
32
|
+
|
|
33
|
+
// Clear any existing test files
|
|
34
|
+
testDir.listFiles()?.forEach { it.delete() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@After
|
|
38
|
+
fun tearDown() {
|
|
39
|
+
// Clean up test files
|
|
40
|
+
testDir.listFiles()?.forEach { it.delete() }
|
|
41
|
+
testDir.delete()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun testDeviceDisconnectionBehavior_Fallback() {
|
|
46
|
+
println("๐งช Test: Device Disconnection with Fallback Behavior")
|
|
47
|
+
println("--------------------------------------------------")
|
|
48
|
+
|
|
49
|
+
// Create recording config with fallback behavior
|
|
50
|
+
val config = RecordingConfig(
|
|
51
|
+
sampleRate = 44100,
|
|
52
|
+
channels = 1,
|
|
53
|
+
encoding = "pcm_16bit",
|
|
54
|
+
deviceDisconnectionBehavior = "fallback",
|
|
55
|
+
output = OutputConfig(
|
|
56
|
+
primary = OutputConfig.PrimaryOutput(
|
|
57
|
+
enabled = true,
|
|
58
|
+
format = "wav"
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
outputDirectory = testDir.absolutePath,
|
|
62
|
+
filename = "fallback_test.wav"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// Verify device disconnection behavior is set
|
|
66
|
+
assertEquals("Device disconnection behavior should be fallback",
|
|
67
|
+
"fallback", config.deviceDisconnectionBehavior)
|
|
68
|
+
|
|
69
|
+
println("โ
RecordingConfig correctly configured with fallback behavior")
|
|
70
|
+
|
|
71
|
+
// Note: Actual device disconnection simulation would require running the full
|
|
72
|
+
// ExpoAudioStreamModule with device connection/disconnection events
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun testDeviceDisconnectionBehavior_Pause() {
|
|
77
|
+
println("๐งช Test: Device Disconnection with Pause Behavior")
|
|
78
|
+
println("-----------------------------------------------")
|
|
79
|
+
|
|
80
|
+
val config = RecordingConfig(
|
|
81
|
+
sampleRate = 44100,
|
|
82
|
+
channels = 1,
|
|
83
|
+
encoding = "pcm_16bit",
|
|
84
|
+
deviceDisconnectionBehavior = "pause",
|
|
85
|
+
output = OutputConfig(
|
|
86
|
+
primary = OutputConfig.PrimaryOutput(
|
|
87
|
+
enabled = true,
|
|
88
|
+
format = "wav"
|
|
89
|
+
)
|
|
90
|
+
),
|
|
91
|
+
outputDirectory = testDir.absolutePath,
|
|
92
|
+
filename = "pause_test.wav"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// Verify device disconnection behavior is set to pause
|
|
96
|
+
assertEquals("Device disconnection behavior should be pause",
|
|
97
|
+
"pause", config.deviceDisconnectionBehavior)
|
|
98
|
+
|
|
99
|
+
println("โ
RecordingConfig correctly configured with pause behavior")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun testDefaultDeviceDisconnectionBehavior() {
|
|
104
|
+
println("๐งช Test: Default Device Disconnection Behavior")
|
|
105
|
+
println("--------------------------------------------")
|
|
106
|
+
|
|
107
|
+
// Create config without specifying deviceDisconnectionBehavior
|
|
108
|
+
val config = RecordingConfig(
|
|
109
|
+
sampleRate = 44100,
|
|
110
|
+
channels = 1,
|
|
111
|
+
encoding = "pcm_16bit",
|
|
112
|
+
output = OutputConfig(
|
|
113
|
+
primary = OutputConfig.PrimaryOutput(
|
|
114
|
+
enabled = true,
|
|
115
|
+
format = "wav"
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
outputDirectory = testDir.absolutePath,
|
|
119
|
+
filename = "default_test.wav"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Default behavior should be null (handled as pause in implementation)
|
|
123
|
+
assertNull("Default device disconnection behavior should be null",
|
|
124
|
+
config.deviceDisconnectionBehavior)
|
|
125
|
+
|
|
126
|
+
println("โ
Default device disconnection behavior is null (treated as 'pause')")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
fun testInterruptionEventReasons() {
|
|
131
|
+
println("๐งช Test: Interruption Event Reasons")
|
|
132
|
+
println("----------------------------------")
|
|
133
|
+
|
|
134
|
+
// Test that the expected event reasons are valid
|
|
135
|
+
val fallbackReasons = listOf("deviceFallback", "deviceSwitchFailed")
|
|
136
|
+
val pauseReasons = listOf("deviceDisconnected")
|
|
137
|
+
|
|
138
|
+
println("๐ Expected reasons for fallback behavior:")
|
|
139
|
+
fallbackReasons.forEach { reason ->
|
|
140
|
+
println(" - $reason")
|
|
141
|
+
assertNotNull("Reason should not be null", reason)
|
|
142
|
+
assertTrue("Reason should not be empty", reason.isNotEmpty())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
println("๐ Expected reasons for pause behavior:")
|
|
146
|
+
pauseReasons.forEach { reason ->
|
|
147
|
+
println(" - $reason")
|
|
148
|
+
assertNotNull("Reason should not be null", reason)
|
|
149
|
+
assertTrue("Reason should not be empty", reason.isNotEmpty())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
println("โ
All interruption event reasons are valid")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun testAllDeviceDisconnectionBehaviors() {
|
|
157
|
+
println("๐งช Test: All Device Disconnection Behaviors")
|
|
158
|
+
println("-----------------------------------------")
|
|
159
|
+
|
|
160
|
+
val behaviors = listOf("fallback", "pause")
|
|
161
|
+
|
|
162
|
+
behaviors.forEach { behavior ->
|
|
163
|
+
val config = RecordingConfig(
|
|
164
|
+
sampleRate = 44100,
|
|
165
|
+
channels = 1,
|
|
166
|
+
encoding = "pcm_16bit",
|
|
167
|
+
deviceDisconnectionBehavior = behavior,
|
|
168
|
+
output = OutputConfig(
|
|
169
|
+
primary = OutputConfig.PrimaryOutput(
|
|
170
|
+
enabled = true,
|
|
171
|
+
format = "wav"
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
outputDirectory = testDir.absolutePath,
|
|
175
|
+
filename = "${behavior}_test.wav"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// Verify configuration
|
|
179
|
+
assertEquals("Device disconnection behavior should be $behavior",
|
|
180
|
+
behavior, config.deviceDisconnectionBehavior)
|
|
181
|
+
|
|
182
|
+
println("โ
Behavior '$behavior' correctly configured")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Test
|
|
187
|
+
fun testDeviceDisconnectionWithCompressedOutput() {
|
|
188
|
+
println("๐งช Test: Device Disconnection with Compressed Output")
|
|
189
|
+
println("--------------------------------------------------")
|
|
190
|
+
|
|
191
|
+
// Test fallback behavior with compressed output enabled
|
|
192
|
+
val config = RecordingConfig(
|
|
193
|
+
sampleRate = 44100,
|
|
194
|
+
channels = 1,
|
|
195
|
+
encoding = "pcm_16bit",
|
|
196
|
+
deviceDisconnectionBehavior = "fallback",
|
|
197
|
+
output = OutputConfig(
|
|
198
|
+
primary = OutputConfig.PrimaryOutput(
|
|
199
|
+
enabled = false
|
|
200
|
+
),
|
|
201
|
+
compressed = OutputConfig.CompressedOutput(
|
|
202
|
+
enabled = true,
|
|
203
|
+
format = "aac",
|
|
204
|
+
bitrate = 128000
|
|
205
|
+
)
|
|
206
|
+
),
|
|
207
|
+
outputDirectory = testDir.absolutePath,
|
|
208
|
+
filename = "compressed_fallback_test"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assertEquals("Device disconnection behavior should be fallback",
|
|
212
|
+
"fallback", config.deviceDisconnectionBehavior)
|
|
213
|
+
assertTrue("Compressed output should be enabled", config.output.compressed.enabled)
|
|
214
|
+
assertFalse("Primary output should be disabled", config.output.primary.enabled)
|
|
215
|
+
|
|
216
|
+
println("โ
Fallback behavior configured with compressed-only output")
|
|
217
|
+
}
|
|
218
|
+
}
|