@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.
Files changed (23) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
  3. package/android/src/androidTest/java/net/siteed/audiostream/integration/DeviceDisconnectionFallbackTest.kt +218 -0
  4. package/android/src/androidTest/java/net/siteed/audiostream/integration/EventEmissionIntervalTest.kt +120 -0
  5. package/android/src/androidTest/java/net/siteed/audiostream/integration/M4aFormatTest.kt +345 -0
  6. package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt +252 -0
  7. package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +5 -0
  8. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +44 -32
  9. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +198 -22
  10. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +13 -4
  11. package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +249 -0
  12. package/android/src/test/java/net/siteed/audiostream/AudioFormatTest.kt +151 -0
  13. package/android/src/test/java/net/siteed/audiostream/DeviceDisconnectionFallbackUnitTest.kt +140 -0
  14. package/build/cjs/ExpoAudioStream.types.js.map +1 -1
  15. package/build/esm/ExpoAudioStream.types.js.map +1 -1
  16. package/build/types/ExpoAudioStream.types.d.ts +25 -2
  17. package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
  18. package/ios/AudioStreamManager.swift +55 -43
  19. package/ios/ExpoAudioStudioTests/EventEmissionIntervalTests.swift +105 -0
  20. package/ios/tests/README.md +41 -0
  21. package/ios/tests/opus_support_test_macos.swift +154 -0
  22. package/package.json +2 -2
  23. 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
- interval = options.getNumberOrDefault("interval", Constants.DEFAULT_INTERVAL),
134
- intervalAnalysis = options.getNumberOrDefault("intervalAnalysis", Constants.DEFAULT_INTERVAL_ANALYSIS),
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
+ }