@siteed/expo-audio-stream 1.9.1 → 1.10.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 CHANGED
@@ -8,24 +8,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [1.10.0] - 2025-01-14
12
+ - add support for pausing and resuming compressed recordings ([bc3f629](https://github.com/deeeed/expo-audio-stream/commit/bc3f6295d060396325e0f008ff00b3be9c8722cd))
13
+ - optimize notification channel settings ([daa075e](https://github.com/deeeed/expo-audio-stream/commit/daa075e668f8faf0b8d2849e18c37384bdd293b8))
14
+
15
+ ## [1.9.2] - 2025-01-12
16
+ - ios bitrate verification to prevent invalid values ([035a180](https://github.com/deeeed/expo-audio-stream/commit/035a1800833264edcc59724aaa8a2e12d5c78dc2))
17
+
18
+
11
19
  ## [1.9.1] - 2025-01-12
12
20
  - ios potentially missing compressed file info ([88a628c](https://github.com/deeeed/expo-audio-stream/commit/88a628c35f2bfd626a2a5de1eb6950efd814619d))
13
21
 
22
+
23
+
14
24
  ## [1.9.0] - 2025-01-11
15
25
  - feat(web-audio): optimize memory usage and streaming performance for web audio recording (#75) ([7b93e12](https://github.com/deeeed/expo-audio-stream/commit/7b93e12aae4bc0599b06b48ca34a60f65587fc75))
16
26
 
17
27
 
28
+
29
+
18
30
  ## [1.8.0] - 2025-01-10
19
31
  - feat(audio): implement audio compression support ([ff4e060](https://github.com/deeeed/expo-audio-stream/commit/ff4e060fef1061804c1cc0126d4344d2d50daa9a))
20
32
 
21
33
 
22
34
 
35
+
36
+
23
37
  ## [1.7.2] - 2025-01-07
24
38
  - fix(audio-stream): correct WAV header handling in web audio recording ([9ba7de5](https://github.com/deeeed/expo-audio-stream/commit/9ba7de5b96ca4cc937dea261c80d3fda9c99e8f4))
25
39
 
26
40
 
27
41
 
28
42
 
43
+
44
+
29
45
  ## [1.7.1] - 2025-01-07
30
46
  - update notification to avoid triggering new alerts (#71) ([32dcfc5](https://github.com/deeeed/expo-audio-stream/commit/32dcfc55daf3236babefc17016f329c177d466fd))
31
47
 
@@ -33,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
49
 
34
50
 
35
51
 
52
+
53
+
36
54
  ## [1.7.0] - 2025-01-05
37
55
  - feat(playground): enhance app configuration and build setup for production deployment (#58) ([929d443](https://github.com/deeeed/expo-audio-stream/commit/929d443145378b1430d215db5c00b13758420e2b))
38
56
  - chore(expo-audio-stream): release @siteed/expo-audio-stream@1.6.1 ([084e8ad](https://github.com/deeeed/expo-audio-stream/commit/084e8adb91da7874c9e608b55d9c7b2ffd7a8327))
@@ -46,6 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
64
 
47
65
 
48
66
 
67
+
68
+
49
69
  ## [1.6.1] - 2024-12-11
50
70
  - chore(expo-audio-stream): remove git commit step from publish script ([4a772ce](https://github.com/deeeed/expo-audio-stream/commit/4a772ce93bb7405d9b8e981f46bdf8941a71ecfe))
51
71
  - chore: more publishing automation ([3693021](https://github.com/deeeed/expo-audio-stream/commit/369302107f9dca9dddd8ae68e6214481a39976ac))
@@ -63,6 +83,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63
83
 
64
84
 
65
85
 
86
+
87
+
66
88
  ## [1.5.0] - 2024-12-10
67
89
  - UNPUBLISHED because of a bug in the build system
68
90
 
@@ -73,6 +95,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
73
95
 
74
96
 
75
97
 
98
+
99
+
76
100
  ## [1.4.0] - 2024-12-05
77
101
  - chore: remove unusded dependencies ([ad81dd5](https://github.com/deeeed/expo-audio-stream/commit/ad81dd560c93dd1d04995a323a4ae72d4de20f3e))
78
102
 
@@ -83,6 +107,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
83
107
 
84
108
 
85
109
 
110
+
111
+
86
112
  ## [1.3.1] - 2024-12-05
87
113
  - feat(web): implement throttling and optimize event processing (#49) ([da28765](https://github.com/deeeed/expo-audio-stream/commit/da2876524c2c9d6e0a980fde40a0197b929d8a7f))
88
114
 
@@ -93,6 +119,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
93
119
 
94
120
 
95
121
 
122
+
123
+
96
124
  ## [1.3.0] - 2024-11-28
97
125
  ### Added
98
126
  - refactor(permissions): standardize permission status response structure across platforms (#44) ([7c9c800](https://github.com/deeeed/expo-audio-stream/commit/7c9c800d83b7cea3516643371484d5e1f3b99e4c))
@@ -108,6 +136,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
108
136
 
109
137
 
110
138
 
139
+
140
+
111
141
  ## [1.2.5] - 2024-11-12
112
142
  ### Added
113
143
  - docs(license): add MIT license to all packages (6 files changed)
@@ -120,6 +150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
120
150
 
121
151
 
122
152
 
153
+
154
+
123
155
  ## [1.2.4] - 2024-11-05
124
156
  ### Changed
125
157
  - Android minimum audio interval set to 10ms.
@@ -135,6 +167,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135
167
 
136
168
 
137
169
 
170
+
171
+
138
172
  ## [1.2.0] - 2024-10-24
139
173
  ### Added
140
174
  - Feature: Keep device awake during recording with `keepAwake` option
@@ -150,6 +184,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
150
184
 
151
185
 
152
186
 
187
+
188
+
153
189
  ## [1.1.17] - 2024-10-21
154
190
  ### Added
155
191
  - Support bluetooth headset on ios
@@ -162,6 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
162
198
 
163
199
 
164
200
 
201
+
202
+
165
203
  ## [1.0.0] - 2024-04-01
166
204
  ### Added
167
205
  - Initial release of @siteed/expo-audio-stream.
@@ -172,7 +210,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
172
210
  - Feature: Audio features extraction during recording.
173
211
  - Feature: Consistent WAV PCM recording format across all platforms.
174
212
 
175
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.1...HEAD
213
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.10.0...HEAD
214
+ [1.10.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.2...@siteed/expo-audio-stream@1.10.0
215
+ [1.9.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.1...@siteed/expo-audio-stream@1.9.2
176
216
  [1.9.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.9.0...@siteed/expo-audio-stream@1.9.1
177
217
  [1.9.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.8.0...@siteed/expo-audio-stream@1.9.0
178
218
  [1.8.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@1.7.2...@siteed/expo-audio-stream@1.8.0
@@ -18,6 +18,7 @@ import androidx.core.app.NotificationCompat
18
18
  import java.lang.ref.WeakReference
19
19
  import java.util.Locale
20
20
  import java.util.concurrent.atomic.AtomicBoolean
21
+ import java.util.Objects
21
22
 
22
23
  class AudioNotificationManager private constructor(context: Context) {
23
24
  private val contextRef = WeakReference(context.applicationContext)
@@ -40,6 +41,7 @@ class AudioNotificationManager private constructor(context: Context) {
40
41
  private var lastPauseTime: Long = 0
41
42
  private var lastWaveformUpdate: Long = 0
42
43
  private val waveformRenderer = WaveformRenderer()
44
+ private var lastNotificationHash: Int? = null
43
45
 
44
46
  companion object {
45
47
  private const val WAVEFORM_UPDATE_INTERVAL = 100L
@@ -66,21 +68,16 @@ class AudioNotificationManager private constructor(context: Context) {
66
68
  val channel = NotificationChannel(
67
69
  recordingConfig.notification.channelId,
68
70
  recordingConfig.notification.channelName,
69
- NotificationManager.IMPORTANCE_LOW // Lower importance means no sound by default
71
+ NotificationManager.IMPORTANCE_LOW
70
72
  ).apply {
71
73
  description = recordingConfig.notification.channelDescription
72
- enableLights(true)
73
- lightColor = Color.parseColor(recordingConfig.notification.lightColor)
74
- enableVibration(true)
75
- setShowBadge(true)
74
+ enableLights(false)
75
+ enableVibration(false)
76
+ setSound(null, null)
77
+ setShowBadge(false)
78
+ vibrationPattern = null
76
79
  }
77
80
 
78
- // Set description, disable lights, vibration, and sound.
79
- channel.enableLights(false);
80
- channel.enableVibration(false);
81
- channel.setSound(null, null);
82
- channel.setShowBadge(false);
83
-
84
81
  notificationManager.createNotificationChannel(channel)
85
82
  }
86
83
  }
@@ -134,6 +131,9 @@ class AudioNotificationManager private constructor(context: Context) {
134
131
  }
135
132
 
136
133
  private fun addNotificationActions(context: Context) {
134
+ // Clear existing actions first
135
+ notificationBuilder.clearActions()
136
+
137
137
  // Create pause action
138
138
  val pauseIntent = Intent(context, RecordingActionReceiver::class.java).apply {
139
139
  action = RecordingActionReceiver.ACTION_PAUSE_RECORDING
@@ -156,7 +156,7 @@ class AudioNotificationManager private constructor(context: Context) {
156
156
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
157
157
  )
158
158
 
159
- // Add pause or resume action based on current state
159
+ // Add only one pause/resume action based on current state
160
160
  if (isPaused.get()) {
161
161
  notificationBuilder.addAction(
162
162
  R.drawable.ic_play,
@@ -171,20 +171,23 @@ class AudioNotificationManager private constructor(context: Context) {
171
171
  )
172
172
  }
173
173
 
174
- // Add configured custom actions
174
+ // Add configured custom actions (only if they don't already exist)
175
+ val existingActions = mutableSetOf<String>()
175
176
  recordingConfig.notification.actions.forEach { action ->
176
- val intent = Intent(context, RecordingActionReceiver::class.java).apply {
177
- this.action = action.intentAction
177
+ if (existingActions.add(action.intentAction)) { // Only add if action is unique
178
+ val intent = Intent(context, RecordingActionReceiver::class.java).apply {
179
+ this.action = action.intentAction
180
+ }
181
+ val pendingIntent = PendingIntent.getBroadcast(
182
+ context,
183
+ action.intentAction.hashCode(),
184
+ intent,
185
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
186
+ )
187
+ val actionIconResId = action.icon?.let { getResourceIdByName(it) }
188
+ ?: R.drawable.ic_default_action_icon
189
+ notificationBuilder.addAction(actionIconResId, action.title, pendingIntent)
178
190
  }
179
- val pendingIntent = PendingIntent.getBroadcast(
180
- context,
181
- action.intentAction.hashCode(),
182
- intent,
183
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
184
- )
185
- val actionIconResId = action.icon?.let { getResourceIdByName(it) }
186
- ?: R.drawable.ic_default_action_icon
187
- notificationBuilder.addAction(actionIconResId, action.title, pendingIntent)
188
191
  }
189
192
  }
190
193
 
@@ -232,22 +235,43 @@ class AudioNotificationManager private constructor(context: Context) {
232
235
 
233
236
  try {
234
237
  val currentTime = SystemClock.elapsedRealtime()
238
+
239
+ // Calculate current notification state
240
+ val recordingDuration = if (isPaused.get()) {
241
+ lastPauseTime - recordingStartTime - pausedDuration
242
+ } else {
243
+ System.currentTimeMillis() - recordingStartTime - pausedDuration
244
+ }
245
+
246
+ // Create a hash of the current notification state
247
+ val currentHash = Objects.hash(
248
+ recordingConfig.notification.title,
249
+ recordingConfig.notification.text,
250
+ formatDuration(recordingDuration),
251
+ isPaused.get()
252
+ )
253
+
235
254
  val needsRemoteViewsRefresh = currentTime - lastRemoteViewsUpdate >= remoteViewsRefreshInterval ||
236
255
  consecutiveUpdateFailures >= maxUpdateFailures
237
256
 
257
+ // Only update if content changed or refresh needed
258
+ if (currentHash == lastNotificationHash && !needsRemoteViewsRefresh) {
259
+ // Update waveform only if needed
260
+ if (shouldUpdateWaveform(audioData, currentTime)) {
261
+ updateWaveformOnly(audioData)
262
+ }
263
+ return
264
+ }
265
+
266
+ lastNotificationHash = currentHash
267
+
268
+ // Only recreate RemoteViews periodically or after failures
238
269
  if (needsRemoteViewsRefresh) {
239
- // Only recreate RemoteViews periodically or after failures
240
270
  remoteViews = RemoteViews(context.packageName, R.layout.notification_recording)
241
271
  lastRemoteViewsUpdate = currentTime
242
272
  consecutiveUpdateFailures = 0
243
273
  }
244
274
 
245
- val recordingDuration = if (isPaused.get()) {
246
- lastPauseTime - recordingStartTime - pausedDuration
247
- } else {
248
- System.currentTimeMillis() - recordingStartTime - pausedDuration
249
- }
250
-
251
275
  // Update RemoteViews content
252
276
  remoteViews.apply {
253
277
  setTextViewText(R.id.notification_title, recordingConfig.notification.title)
@@ -280,11 +304,13 @@ class AudioNotificationManager private constructor(context: Context) {
280
304
  addNotificationActions(context)
281
305
  }
282
306
 
283
- // Update the notification without triggering a new alert
307
+ // Update the notification with disabled alerts
284
308
  notificationManager.notify(
285
309
  recordingConfig.notification.notificationId,
286
310
  notificationBuilder
287
311
  .setOnlyAlertOnce(true)
312
+ .setVibrate(null)
313
+ .setDefaults(0)
288
314
  .build()
289
315
  )
290
316
 
@@ -300,6 +326,37 @@ class AudioNotificationManager private constructor(context: Context) {
300
326
  }
301
327
  }
302
328
  }
329
+
330
+ private fun shouldUpdateWaveform(audioData: FloatArray?, currentTime: Long): Boolean {
331
+ return recordingConfig.showWaveformInNotification &&
332
+ audioData != null &&
333
+ audioData.isNotEmpty() &&
334
+ currentTime - lastWaveformUpdate >= WAVEFORM_UPDATE_INTERVAL
335
+ }
336
+
337
+ private fun updateWaveformOnly(audioData: FloatArray?) {
338
+ if (audioData == null) return
339
+
340
+ try {
341
+ val waveformBitmap = waveformRenderer.generateWaveform(audioData, recordingConfig.notification.waveform)
342
+ remoteViews.setImageViewBitmap(R.id.notification_waveform, waveformBitmap)
343
+ lastWaveformUpdate = SystemClock.elapsedRealtime()
344
+
345
+ notificationManager.notify(
346
+ recordingConfig.notification.notificationId,
347
+ notificationBuilder
348
+ .setCustomContentView(remoteViews)
349
+ .setCustomBigContentView(remoteViews)
350
+ .setOnlyAlertOnce(true)
351
+ .setVibrate(null)
352
+ .setDefaults(0)
353
+ .build()
354
+ )
355
+ } catch (e: Exception) {
356
+ Log.e(Constants.TAG, "Error updating waveform", e)
357
+ }
358
+ }
359
+
303
360
  private fun reinitializeNotification() {
304
361
  try {
305
362
  val context = contextRef.get() ?: return
@@ -322,7 +379,6 @@ class AudioNotificationManager private constructor(context: Context) {
322
379
  }
323
380
  }
324
381
 
325
-
326
382
  fun getNotification(): Notification = notificationBuilder.build()
327
383
 
328
384
  fun pauseUpdates() {
@@ -539,7 +539,11 @@ class AudioRecorderManager(
539
539
  acquireWakeLock()
540
540
  pausedDuration += System.currentTimeMillis() - lastPauseTime
541
541
  isPaused.set(false)
542
+
543
+ // Add these lines to resume both recordings
542
544
  audioRecord?.startRecording()
545
+ compressedRecorder?.resume()
546
+
543
547
  promise.resolve("Recording resumed")
544
548
  } catch (e: Exception) {
545
549
  releaseWakeLock()
@@ -549,7 +553,10 @@ class AudioRecorderManager(
549
553
 
550
554
  fun pauseRecording(promise: Promise) {
551
555
  if (isRecording.get() && !isPaused.get()) {
556
+ // Add these lines to pause both recordings
552
557
  audioRecord?.stop()
558
+ compressedRecorder?.pause()
559
+
553
560
  lastPauseTime = System.currentTimeMillis()
554
561
  isPaused.set(true)
555
562
 
@@ -410,13 +410,14 @@ class AudioStreamManager: NSObject {
410
410
  "interval": emissionInterval
411
411
  ]
412
412
 
413
- // Add compression info if enabled and file exists
413
+ // Add compression info if enabled
414
414
  if settings.enableCompressedOutput,
415
415
  let compressedURL = compressedFileURL,
416
416
  FileManager.default.fileExists(atPath: compressedURL.path) {
417
417
  do {
418
418
  let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
419
419
  if let compressedSize = compressedAttributes[.size] as? Int64 {
420
+ Logger.debug("Compressed file status - Size: \(compressedSize)")
420
421
  let compressionBundle: [String: Any] = [
421
422
  "fileUri": compressedURL.absoluteString,
422
423
  "mimeType": compressedFormat == "aac" ? "audio/aac" : "audio/opus",
@@ -558,34 +559,47 @@ class AudioStreamManager: NSObject {
558
559
  AVSampleRateKey: settings.sampleRate,
559
560
  AVNumberOfChannelsKey: settings.numberOfChannels,
560
561
  AVEncoderBitRateKey: settings.compressedBitRate,
561
- AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
562
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
563
+ AVEncoderBitDepthHintKey: 16
562
564
  ]
563
565
 
564
- // Create directory if it doesn't exist
566
+ Logger.debug("Initializing compressed recording with settings: \(compressedSettings)")
567
+
565
568
  let tempDirectory = FileManager.default.temporaryDirectory
566
569
  try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
567
570
 
568
- // Create compressed file URL and ensure file exists
569
- compressedFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
570
- .appendingPathExtension(settings.compressedFormat)
571
-
572
- if let url = compressedFileURL {
573
- // Create empty file first
574
- FileManager.default.createFile(atPath: url.path, contents: nil)
571
+ // Use the same UUID as the main recording
572
+ if let recordingUUID = recordingUUID {
573
+ compressedFileURL = tempDirectory.appendingPathComponent(recordingUUID.uuidString)
574
+ .appendingPathExtension(settings.compressedFormat)
575
575
 
576
- // Then initialize recorder
577
- compressedRecorder = try AVAudioRecorder(url: url, settings: compressedSettings)
578
- if let recorder = compressedRecorder {
579
- recorder.prepareToRecord()
580
- recorder.record()
581
- compressedFormat = settings.compressedFormat
582
- compressedBitRate = settings.compressedBitRate
583
- Logger.debug("Compressed recording initialized at: \(url.path)")
576
+ if let url = compressedFileURL {
577
+ // Create empty file first
578
+ if FileManager.default.createFile(atPath: url.path, contents: nil) {
579
+ Logger.debug("Created empty file at: \(url.path)")
580
+ } else {
581
+ Logger.debug("Failed to create empty file at: \(url.path)")
582
+ }
583
+
584
+ // Then initialize recorder
585
+ compressedRecorder = try AVAudioRecorder(url: url, settings: compressedSettings)
586
+ if let recorder = compressedRecorder {
587
+ let prepared = recorder.prepareToRecord()
588
+ Logger.debug("Recorder prepared: \(prepared)")
589
+
590
+ let started = recorder.record()
591
+ Logger.debug("Recorder started: \(started)")
592
+
593
+ Logger.debug("Recorder current time: \(recorder.currentTime)")
594
+
595
+ compressedFormat = settings.compressedFormat
596
+ compressedBitRate = settings.compressedBitRate
597
+ Logger.debug("Compressed recording initialized - Format: \(compressedFormat), Bitrate: \(compressedBitRate)")
598
+ }
584
599
  }
585
600
  }
586
601
  } catch {
587
602
  Logger.debug("Failed to setup compressed recording: \(error)")
588
- // Don't fail the entire recording if compression fails
589
603
  compressedFileURL = nil
590
604
  compressedRecorder = nil
591
605
  }
@@ -1213,32 +1227,50 @@ class AudioStreamManager: NSObject {
1213
1227
  var compressionInfo: [String: Any]? = nil
1214
1228
  if settings.enableCompressedOutput, let compressedURL = compressedFileURL {
1215
1229
  do {
1216
- let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
1217
- if let compressedSize = compressedAttributes[.size] as? Int64 {
1218
- let eventDataSize = compressedSize - lastEmittedCompressedSize
1219
-
1220
- // Read the new compressed data if there's new data
1221
- var compressedData: String? = nil
1222
- if eventDataSize > 0 {
1223
- let fileHandle = try FileHandle(forReadingFrom: compressedURL)
1224
- fileHandle.seek(toFileOffset: UInt64(lastEmittedCompressedSize))
1225
- let data = fileHandle.readData(ofLength: Int(eventDataSize))
1226
- compressedData = data.base64EncodedString()
1227
- fileHandle.closeFile()
1230
+ // Ensure file exists and has data
1231
+ if FileManager.default.fileExists(atPath: compressedURL.path) {
1232
+ let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
1233
+ if let compressedSize = compressedAttributes[.size] as? Int64 {
1234
+ let eventDataSize = compressedSize - lastEmittedCompressedSize
1235
+
1236
+ Logger.debug("Compressed file status - Total size: \(compressedSize), New data size: \(eventDataSize)")
1237
+
1238
+ // Read the new compressed data if there's new data
1239
+ var compressedData: String? = nil
1240
+ if eventDataSize > 0 {
1241
+ do {
1242
+ let fileHandle = try FileHandle(forReadingFrom: compressedURL)
1243
+ defer { fileHandle.closeFile() }
1244
+
1245
+ fileHandle.seek(toFileOffset: UInt64(lastEmittedCompressedSize))
1246
+ let data = fileHandle.readData(ofLength: Int(eventDataSize))
1247
+ compressedData = data.base64EncodedString()
1248
+
1249
+ Logger.debug("Read compressed data of size: \(data.count)")
1250
+ } catch {
1251
+ Logger.debug("Error reading compressed data: \(error)")
1252
+ }
1253
+ }
1254
+
1255
+ lastEmittedCompressedSize = compressedSize
1256
+
1257
+ compressionInfo = [
1258
+ "position": recordingTime * 1000, // Convert to milliseconds
1259
+ "fileUri": compressedURL.absoluteString,
1260
+ "eventDataSize": eventDataSize,
1261
+ "totalSize": compressedSize,
1262
+ "data": compressedData ?? ""
1263
+ ]
1264
+
1265
+ Logger.debug("Compression info prepared: \(String(describing: compressionInfo))")
1266
+ } else {
1267
+ Logger.debug("Could not get compressed file size")
1228
1268
  }
1229
-
1230
- lastEmittedCompressedSize = compressedSize
1231
-
1232
- compressionInfo = [
1233
- "position": recordingTime * 1000, // Convert to milliseconds
1234
- "fileUri": compressedURL.absoluteString,
1235
- "eventDataSize": eventDataSize,
1236
- "totalSize": compressedSize,
1237
- "data": compressedData ?? ""
1238
- ]
1269
+ } else {
1270
+ Logger.debug("Compressed file does not exist at path: \(compressedURL.path)")
1239
1271
  }
1240
1272
  } catch {
1241
- Logger.debug("Failed to read compressed data: \(error)")
1273
+ Logger.debug("Error preparing compression info: \(error)")
1242
1274
  }
1243
1275
  }
1244
1276
 
@@ -23,18 +23,25 @@ struct CompressedRecordingInfo {
23
23
  var bitrate: Int
24
24
  var format: String
25
25
 
26
- static func validate(format: String, bitrate: Int) -> Result<Void, Error> {
26
+ static func validate(format: String, bitrate: Int) -> Result<(String, Int), Error> {
27
27
  // Validate format
28
28
  guard ["aac", "opus"].contains(format.lowercased()) else {
29
29
  return .failure(RecordingError.unsupportedFormat(format))
30
30
  }
31
31
 
32
- // Validate bitrate
33
- guard (8000...960000).contains(bitrate) else {
34
- return .failure(RecordingError.invalidBitrate(bitrate))
32
+ // Adjust bitrate based on format
33
+ let adjustedBitrate: Int
34
+ if format.lowercased() == "aac" {
35
+ // Standard AAC bitrates (bps)
36
+ let standardAACBitrates = [32000, 48000, 64000, 96000, 128000, 160000, 192000, 256000, 320000]
37
+ adjustedBitrate = standardAACBitrates.min(by: { abs($0 - bitrate) < abs($1 - bitrate) }) ?? 128000
38
+ } else {
39
+ // For Opus, allow lower bitrates (especially good for voice)
40
+ // Typical Opus voice bitrates: 8-24 kbps, music: 32-128 kbps
41
+ adjustedBitrate = min(max(bitrate, 8000), 320000)
35
42
  }
36
43
 
37
- return .success(())
44
+ return .success((format, adjustedBitrate))
38
45
  }
39
46
  }
40
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-stream",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "stream audio crossplatform",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",