@siteed/expo-audio-studio 2.13.1 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderPerformanceInstrumentedTest.kt +234 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +52 -17
- package/build/cjs/AudioDeviceManager.js.map +1 -1
- package/build/cjs/index.js.map +1 -1
- package/build/esm/AudioDeviceManager.js.map +1 -1
- package/build/esm/index.js.map +1 -1
- package/build/types/AudioDeviceManager.d.ts.map +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +141 -180
- package/package.json +1 -1
- package/src/AudioDeviceManager.ts +2 -1
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.14.0] - 2025-06-11
|
|
12
|
+
### Changed
|
|
13
|
+
- feat(expo-audio-studio): comprehensive cross-platform stop recording performance optimization ([b3ed474](https://github.com/deeeed/expo-audio-stream/commit/b3ed474d91994698fe082354621adc98e758557e))
|
|
14
|
+
## [2.13.2] - 2025-06-10
|
|
15
|
+
### Changed
|
|
16
|
+
- fix: invalid type exports ([18340ea](https://github.com/deeeed/expo-audio-stream/commit/18340eac9cfef39691637400fc6af811d5b004df))
|
|
17
|
+
- chore(expo-audio-studio): release @siteed/expo-audio-studio@2.13.1 ([9ccce85](https://github.com/deeeed/expo-audio-stream/commit/9ccce858174254387aac44d30853c908707d8254))
|
|
11
18
|
## [2.13.1] - 2025-06-09
|
|
12
19
|
### Changed
|
|
13
20
|
- feat(investigation): resolve Issue #251 - comprehensive sub-100ms audio events analysis (#270) ([4813f1e](https://github.com/deeeed/expo-audio-stream/commit/4813f1ef05f3856b58ec8fde95b7b8909feb513d))
|
|
@@ -312,7 +319,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
312
319
|
- Feature: Audio features extraction during recording.
|
|
313
320
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
314
321
|
|
|
315
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
322
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.0...HEAD
|
|
323
|
+
[2.14.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.2...@siteed/expo-audio-studio@2.14.0
|
|
324
|
+
[2.13.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.1...@siteed/expo-audio-studio@2.13.2
|
|
316
325
|
[2.13.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.0...@siteed/expo-audio-studio@2.13.1
|
|
317
326
|
[2.13.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.3...@siteed/expo-audio-studio@2.13.0
|
|
318
327
|
[2.12.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.2...@siteed/expo-audio-studio@2.12.3
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
7
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
8
|
+
import androidx.test.rule.GrantPermissionRule
|
|
9
|
+
import expo.modules.kotlin.Promise
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.Assert.*
|
|
12
|
+
import org.junit.Before
|
|
13
|
+
import org.junit.Rule
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import java.io.File
|
|
17
|
+
import java.util.concurrent.CountDownLatch
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
import kotlin.system.measureTimeMillis
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Performance tests for measuring stop recording times.
|
|
23
|
+
*/
|
|
24
|
+
@RunWith(AndroidJUnit4::class)
|
|
25
|
+
class AudioRecorderPerformanceInstrumentedTest {
|
|
26
|
+
|
|
27
|
+
@get:Rule
|
|
28
|
+
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
|
29
|
+
Manifest.permission.RECORD_AUDIO
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
private lateinit var context: Context
|
|
33
|
+
private lateinit var filesDir: File
|
|
34
|
+
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
35
|
+
private lateinit var testEventSender: TestEventSender
|
|
36
|
+
private lateinit var permissionUtils: PermissionUtils
|
|
37
|
+
private lateinit var audioDataEncoder: AudioDataEncoder
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
private const val TAG = "PerformanceTest"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Test event sender to capture events
|
|
44
|
+
private class TestEventSender : EventSender {
|
|
45
|
+
override fun sendExpoEvent(eventName: String, params: android.os.Bundle) {
|
|
46
|
+
// No-op for performance tests
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Before
|
|
51
|
+
fun setUp() {
|
|
52
|
+
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
53
|
+
filesDir = context.filesDir
|
|
54
|
+
testEventSender = TestEventSender()
|
|
55
|
+
permissionUtils = PermissionUtils(context)
|
|
56
|
+
audioDataEncoder = AudioDataEncoder()
|
|
57
|
+
|
|
58
|
+
// Initialize AudioRecorderManager
|
|
59
|
+
audioRecorderManager = AudioRecorderManager.initialize(
|
|
60
|
+
context = context,
|
|
61
|
+
filesDir = filesDir,
|
|
62
|
+
permissionUtils = permissionUtils,
|
|
63
|
+
audioDataEncoder = audioDataEncoder,
|
|
64
|
+
eventSender = testEventSender,
|
|
65
|
+
enablePhoneStateHandling = false,
|
|
66
|
+
enableBackgroundAudio = false
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Clean up any existing audio files
|
|
70
|
+
cleanupAudioFiles()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@After
|
|
74
|
+
fun tearDown() {
|
|
75
|
+
// Stop any ongoing recording
|
|
76
|
+
if (audioRecorderManager.isRecording) {
|
|
77
|
+
val promise = object : Promise {
|
|
78
|
+
override fun resolve(value: Any?) {}
|
|
79
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
80
|
+
}
|
|
81
|
+
audioRecorderManager.stopRecording(promise)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clean up
|
|
85
|
+
AudioRecorderManager.destroy()
|
|
86
|
+
cleanupAudioFiles()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun cleanupAudioFiles() {
|
|
90
|
+
filesDir.listFiles()?.forEach { file ->
|
|
91
|
+
if (file.name.endsWith(".wav") || file.name.endsWith(".aac") || file.name.endsWith(".opus")) {
|
|
92
|
+
file.delete()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun measureStopTime_5seconds() {
|
|
99
|
+
runPerformanceTest(5_000L, "5 second recording")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun measureStopTime_30seconds() {
|
|
104
|
+
runPerformanceTest(30_000L, "30 second recording")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
fun measureStopTime_1minute() {
|
|
109
|
+
runPerformanceTest(60_000L, "1 minute recording")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun measureStopTime_2minutes() {
|
|
114
|
+
runPerformanceTest(120_000L, "2 minute recording")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun measureStopTime_5minutes() {
|
|
119
|
+
runPerformanceTest(300_000L, "5 minute recording")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Test
|
|
123
|
+
fun measureStopTime_10minutes() {
|
|
124
|
+
runPerformanceTest(600_000L, "10 minute recording")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun measureStopTime_15minutes() {
|
|
129
|
+
runPerformanceTest(900_000L, "15 minute recording")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun runPerformanceTest(recordingDurationMs: Long, testName: String) {
|
|
133
|
+
val recordingOptions = mapOf(
|
|
134
|
+
"sampleRate" to 44100,
|
|
135
|
+
"channels" to 1,
|
|
136
|
+
"encoding" to "pcm_16bit",
|
|
137
|
+
"interval" to 1000,
|
|
138
|
+
"enableProcessing" to false,
|
|
139
|
+
"showNotification" to false,
|
|
140
|
+
"output" to mapOf(
|
|
141
|
+
"primary" to mapOf("enabled" to true),
|
|
142
|
+
"compressed" to mapOf("enabled" to false)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Start recording
|
|
147
|
+
val startLatch = CountDownLatch(1)
|
|
148
|
+
audioRecorderManager.startRecording(recordingOptions, object : Promise {
|
|
149
|
+
override fun resolve(value: Any?) {
|
|
150
|
+
startLatch.countDown()
|
|
151
|
+
}
|
|
152
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
153
|
+
fail("Start recording failed: $message")
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
assertTrue("Recording should start", startLatch.await(5, TimeUnit.SECONDS))
|
|
158
|
+
assertTrue("Recording should be active", audioRecorderManager.isRecording)
|
|
159
|
+
|
|
160
|
+
// Record for specified duration
|
|
161
|
+
Thread.sleep(recordingDurationMs)
|
|
162
|
+
|
|
163
|
+
// Measure stop time
|
|
164
|
+
val stopLatch = CountDownLatch(1)
|
|
165
|
+
var fileSize = 0L
|
|
166
|
+
var stopResult: Map<String, Any>? = null
|
|
167
|
+
|
|
168
|
+
val stopDuration = measureTimeMillis {
|
|
169
|
+
audioRecorderManager.stopRecording(object : Promise {
|
|
170
|
+
override fun resolve(value: Any?) {
|
|
171
|
+
when (value) {
|
|
172
|
+
is android.os.Bundle -> {
|
|
173
|
+
fileSize = value.getLong("size", 0)
|
|
174
|
+
stopResult = bundleToMap(value)
|
|
175
|
+
}
|
|
176
|
+
is Map<*, *> -> {
|
|
177
|
+
@Suppress("UNCHECKED_CAST")
|
|
178
|
+
stopResult = value as? Map<String, Any>
|
|
179
|
+
fileSize = (stopResult?.get("size") as? Long) ?: 0
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
stopLatch.countDown()
|
|
183
|
+
}
|
|
184
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
185
|
+
fail("Stop recording failed: $message")
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
assertTrue("Stop should complete", stopLatch.await(10, TimeUnit.SECONDS))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Log results
|
|
193
|
+
val fileSizeMB = fileSize / (1024.0 * 1024.0)
|
|
194
|
+
Log.i(TAG, """
|
|
195
|
+
Performance Test: $testName
|
|
196
|
+
- Recording Duration: ${recordingDurationMs}ms
|
|
197
|
+
- Stop Duration: ${stopDuration}ms
|
|
198
|
+
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
199
|
+
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
200
|
+
""".trimIndent())
|
|
201
|
+
|
|
202
|
+
println("""
|
|
203
|
+
Performance Test: $testName
|
|
204
|
+
- Recording Duration: ${recordingDurationMs}ms
|
|
205
|
+
- Stop Duration: ${stopDuration}ms
|
|
206
|
+
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
207
|
+
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
208
|
+
""".trimIndent())
|
|
209
|
+
|
|
210
|
+
assertFalse("Recording should not be active", audioRecorderManager.isRecording)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun getTargetTime(recordingDurationMs: Long): Long {
|
|
214
|
+
return when {
|
|
215
|
+
recordingDurationMs <= 5_000 -> 100
|
|
216
|
+
recordingDurationMs <= 30_000 -> 150
|
|
217
|
+
recordingDurationMs <= 60_000 -> 200
|
|
218
|
+
recordingDurationMs <= 300_000 -> 500
|
|
219
|
+
else -> 750
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun bundleToMap(bundle: android.os.Bundle): Map<String, Any> {
|
|
224
|
+
val map = mutableMapOf<String, Any>()
|
|
225
|
+
for (key in bundle.keySet()) {
|
|
226
|
+
val value = bundle.get(key)
|
|
227
|
+
when (value) {
|
|
228
|
+
is android.os.Bundle -> map[key] = bundleToMap(value)
|
|
229
|
+
else -> value?.let { map[key] = it }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return map
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -137,6 +137,10 @@ class AudioRecorderManager(
|
|
|
137
137
|
var isPrepared = false
|
|
138
138
|
private var selectedDeviceId: String? = null
|
|
139
139
|
private var deviceDisconnectionBehavior: String? = null
|
|
140
|
+
|
|
141
|
+
// Cache file sizes to avoid file system calls during stop
|
|
142
|
+
private var cachedPrimaryFileSize: Long = 44L // Start with WAV header size
|
|
143
|
+
private var cachedCompressedFileSize: Long = 0L
|
|
140
144
|
|
|
141
145
|
// Add a method to handle device changes
|
|
142
146
|
fun handleDeviceChange() {
|
|
@@ -819,6 +823,10 @@ class AudioRecorderManager(
|
|
|
819
823
|
streamUuid = java.util.UUID.randomUUID().toString()
|
|
820
824
|
totalDataSize = 0
|
|
821
825
|
|
|
826
|
+
// Reset cached file sizes
|
|
827
|
+
cachedPrimaryFileSize = 44L // WAV header size
|
|
828
|
+
cachedCompressedFileSize = 0L
|
|
829
|
+
|
|
822
830
|
// Only create file if primary output is enabled
|
|
823
831
|
if (recordingConfig.output.primary.enabled) {
|
|
824
832
|
audioFile = createRecordingFile(recordingConfig)
|
|
@@ -936,7 +944,19 @@ class AudioRecorderManager(
|
|
|
936
944
|
|
|
937
945
|
_isRecording.set(false)
|
|
938
946
|
isPrepared = false // Reset preparation state
|
|
939
|
-
|
|
947
|
+
|
|
948
|
+
// Calculate adaptive timeout based on estimated file size
|
|
949
|
+
// Assume ~5MB per minute at 44.1kHz, 16-bit, mono
|
|
950
|
+
val recordingDurationMs = if (recordingStartTime > 0) {
|
|
951
|
+
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
952
|
+
} else {
|
|
953
|
+
0L
|
|
954
|
+
}
|
|
955
|
+
val estimatedFileSizeMB = (recordingDurationMs / 60000.0) * 5.0
|
|
956
|
+
val timeoutMs = maxOf(2000L, (estimatedFileSizeMB * 100).toLong()) // 100ms per MB, min 2 seconds
|
|
957
|
+
|
|
958
|
+
LogUtils.d(CLASS_NAME, "Waiting for recording thread to complete with timeout: ${timeoutMs}ms (estimated size: ${estimatedFileSizeMB}MB)")
|
|
959
|
+
recordingThread?.join(timeoutMs)
|
|
940
960
|
|
|
941
961
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
942
962
|
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
@@ -963,8 +983,9 @@ class AudioRecorderManager(
|
|
|
963
983
|
AudioProcessor.resetUniqueIdCounter()
|
|
964
984
|
audioProcessor.resetCumulativeAmplitudeRange()
|
|
965
985
|
|
|
966
|
-
|
|
967
|
-
|
|
986
|
+
// Use cached file size to avoid file system call
|
|
987
|
+
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
988
|
+
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes (cached), Path: ${audioFile?.absolutePath}")
|
|
968
989
|
|
|
969
990
|
// Calculate duration based on context - use actual recording time for streaming-only mode
|
|
970
991
|
val duration = if (!recordingConfig.output.primary.enabled) {
|
|
@@ -997,9 +1018,12 @@ class AudioRecorderManager(
|
|
|
997
1018
|
}
|
|
998
1019
|
compressedRecorder = null
|
|
999
1020
|
|
|
1000
|
-
// Log compressed file status if enabled
|
|
1021
|
+
// Log compressed file status if enabled - use actual file size for validation
|
|
1001
1022
|
if (recordingConfig.output.compressed.enabled) {
|
|
1023
|
+
// Note: For compressed files, we need to get actual size as MediaRecorder handles the writing
|
|
1024
|
+
// Use actual file size here for validation purposes only
|
|
1002
1025
|
val compressedSize = compressedFile?.length() ?: 0
|
|
1026
|
+
cachedCompressedFileSize = compressedSize // Update cache with final size
|
|
1003
1027
|
LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
|
|
1004
1028
|
}
|
|
1005
1029
|
|
|
@@ -1008,7 +1032,7 @@ class AudioRecorderManager(
|
|
|
1008
1032
|
val localCompressedFile = compressedFile // Create local copy to avoid smart cast issues
|
|
1009
1033
|
val compressionBundle = if (recordingConfig.output.compressed.enabled && localCompressedFile != null) {
|
|
1010
1034
|
bundleOf(
|
|
1011
|
-
"size" to
|
|
1035
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1012
1036
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1013
1037
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1014
1038
|
"format" to recordingConfig.output.compressed.format,
|
|
@@ -1040,7 +1064,7 @@ class AudioRecorderManager(
|
|
|
1040
1064
|
"mimeType" to mimeType,
|
|
1041
1065
|
"createdAt" to System.currentTimeMillis(),
|
|
1042
1066
|
"compression" to if (compressedFile != null) bundleOf(
|
|
1043
|
-
"size" to
|
|
1067
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1044
1068
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1045
1069
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1046
1070
|
"format" to recordingConfig.output.compressed.format,
|
|
@@ -1195,7 +1219,8 @@ class AudioRecorderManager(
|
|
|
1195
1219
|
)
|
|
1196
1220
|
}
|
|
1197
1221
|
|
|
1198
|
-
|
|
1222
|
+
// Use cached file size instead of file system call
|
|
1223
|
+
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
1199
1224
|
val duration = when (mimeType) {
|
|
1200
1225
|
"audio/wav" -> {
|
|
1201
1226
|
val dataFileSize = fileSize - Constants.WAV_HEADER_SIZE
|
|
@@ -1208,7 +1233,7 @@ class AudioRecorderManager(
|
|
|
1208
1233
|
|
|
1209
1234
|
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1210
1235
|
bundleOf(
|
|
1211
|
-
"size" to
|
|
1236
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1212
1237
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1213
1238
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1214
1239
|
"format" to recordingConfig.output.compressed.format
|
|
@@ -1380,6 +1405,7 @@ class AudioRecorderManager(
|
|
|
1380
1405
|
// Only write to file if primary output is enabled
|
|
1381
1406
|
if (fos != null) {
|
|
1382
1407
|
fos.write(audioData, 0, bytesRead)
|
|
1408
|
+
cachedPrimaryFileSize += bytesRead // Update cached file size
|
|
1383
1409
|
}
|
|
1384
1410
|
totalDataSize += bytesRead
|
|
1385
1411
|
|
|
@@ -1440,16 +1466,17 @@ class AudioRecorderManager(
|
|
|
1440
1466
|
}
|
|
1441
1467
|
}
|
|
1442
1468
|
} finally {
|
|
1443
|
-
//
|
|
1469
|
+
// Flush and close the file output stream if it was opened
|
|
1470
|
+
try {
|
|
1471
|
+
fos?.flush()
|
|
1472
|
+
LogUtils.d(CLASS_NAME, "FileOutputStream flushed successfully")
|
|
1473
|
+
} catch (e: Exception) {
|
|
1474
|
+
LogUtils.e(CLASS_NAME, "Error flushing FileOutputStream", e)
|
|
1475
|
+
}
|
|
1444
1476
|
fos?.close()
|
|
1445
1477
|
}
|
|
1446
1478
|
|
|
1447
|
-
//
|
|
1448
|
-
if (recordingConfig.output.primary.enabled) {
|
|
1449
|
-
audioFile?.let { file ->
|
|
1450
|
-
audioFileHandler.updateWavHeader(file)
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1479
|
+
// WAV header update is already handled in cleanup(), no need to duplicate here
|
|
1453
1480
|
|
|
1454
1481
|
} catch (e: Exception) {
|
|
1455
1482
|
// Ensure wake lock is released if the thread is interrupted
|
|
@@ -1463,7 +1490,8 @@ class AudioRecorderManager(
|
|
|
1463
1490
|
private fun emitAudioData(audioData: ByteArray, length: Int) {
|
|
1464
1491
|
val encodedBuffer = audioDataEncoder.encodeToBase64(audioData)
|
|
1465
1492
|
|
|
1466
|
-
|
|
1493
|
+
// Use cached file size instead of file system call
|
|
1494
|
+
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
1467
1495
|
val from = lastEmittedSize
|
|
1468
1496
|
lastEmittedSize = fileSize
|
|
1469
1497
|
|
|
@@ -1472,7 +1500,14 @@ class AudioRecorderManager(
|
|
|
1472
1500
|
(from * 1000) / (recordingConfig.sampleRate * recordingConfig.channels * (if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8)
|
|
1473
1501
|
|
|
1474
1502
|
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1475
|
-
|
|
1503
|
+
// For compressed files, we need to get actual size as MediaRecorder handles the writing
|
|
1504
|
+
// Only update cache periodically to avoid frequent file system calls
|
|
1505
|
+
val currentTime = System.currentTimeMillis()
|
|
1506
|
+
if (cachedCompressedFileSize == 0L || (currentTime - lastEmittedCompressedSize) > 5000) {
|
|
1507
|
+
cachedCompressedFileSize = compressedFile?.length() ?: 0
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
val compressedSize = cachedCompressedFileSize
|
|
1476
1511
|
val eventDataSize = compressedSize - lastEmittedCompressedSize
|
|
1477
1512
|
|
|
1478
1513
|
// Read the new compressed data
|