@siteed/expo-audio-studio 2.4.0 → 2.5.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 (81) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +25 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
  6. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
  7. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
  8. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +581 -255
  9. package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
  10. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +435 -158
  11. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
  12. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +14 -5
  13. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
  14. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  15. package/build/AudioDeviceManager.d.ts +107 -0
  16. package/build/AudioDeviceManager.d.ts.map +1 -0
  17. package/build/AudioDeviceManager.js +493 -0
  18. package/build/AudioDeviceManager.js.map +1 -0
  19. package/build/AudioRecorder.provider.d.ts.map +1 -1
  20. package/build/AudioRecorder.provider.js +3 -0
  21. package/build/AudioRecorder.provider.js.map +1 -1
  22. package/build/ExpoAudioStream.types.d.ts +90 -1
  23. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  24. package/build/ExpoAudioStream.types.js +7 -1
  25. package/build/ExpoAudioStream.types.js.map +1 -1
  26. package/build/ExpoAudioStream.web.d.ts +37 -0
  27. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  28. package/build/ExpoAudioStream.web.js +399 -54
  29. package/build/ExpoAudioStream.web.js.map +1 -1
  30. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  31. package/build/ExpoAudioStreamModule.js +20 -0
  32. package/build/ExpoAudioStreamModule.js.map +1 -1
  33. package/build/WebRecorder.web.d.ts +63 -10
  34. package/build/WebRecorder.web.d.ts.map +1 -1
  35. package/build/WebRecorder.web.js +277 -68
  36. package/build/WebRecorder.web.js.map +1 -1
  37. package/build/hooks/useAudioDevices.d.ts +14 -0
  38. package/build/hooks/useAudioDevices.d.ts.map +1 -0
  39. package/build/hooks/useAudioDevices.js +151 -0
  40. package/build/hooks/useAudioDevices.js.map +1 -0
  41. package/build/index.d.ts +2 -0
  42. package/build/index.d.ts.map +1 -1
  43. package/build/index.js +4 -0
  44. package/build/index.js.map +1 -1
  45. package/build/useAudioRecorder.d.ts +1 -0
  46. package/build/useAudioRecorder.d.ts.map +1 -1
  47. package/build/useAudioRecorder.js +20 -1
  48. package/build/useAudioRecorder.js.map +1 -1
  49. package/build/utils/BlobFix.d.ts.map +1 -1
  50. package/build/utils/BlobFix.js +2 -2
  51. package/build/utils/BlobFix.js.map +1 -1
  52. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  53. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  54. package/build/workers/InlineFeaturesExtractor.web.js +27 -26
  55. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  56. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  57. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  58. package/build/workers/inlineAudioWebWorker.web.js +25 -1
  59. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  60. package/ios/AudioDeviceManager.swift +654 -0
  61. package/ios/AudioStreamManager.swift +964 -760
  62. package/ios/ExpoAudioStreamModule.swift +174 -19
  63. package/ios/Features.swift +1 -1
  64. package/ios/ISSUE_IOS.md +45 -0
  65. package/ios/Logger.swift +13 -1
  66. package/ios/RecordingSettings.swift +12 -0
  67. package/package.json +2 -2
  68. package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
  69. package/src/AudioDeviceManager.ts +571 -0
  70. package/src/AudioRecorder.provider.tsx +3 -0
  71. package/src/ExpoAudioStream.types.ts +97 -1
  72. package/src/ExpoAudioStream.web.ts +513 -63
  73. package/src/ExpoAudioStreamModule.ts +23 -0
  74. package/src/WebRecorder.web.ts +346 -81
  75. package/src/hooks/useAudioDevices.ts +180 -0
  76. package/src/index.ts +6 -0
  77. package/src/types/crc-32.d.ts +6 -6
  78. package/src/useAudioRecorder.tsx +27 -1
  79. package/src/utils/BlobFix.ts +6 -4
  80. package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
  81. package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
@@ -27,15 +27,12 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
27
27
  latestPosition = 0;
28
28
  totalCompressedSize = 0;
29
29
  maxBufferSize;
30
+ eventCallback;
30
31
  constructor({ audioWorkletUrl, featuresExtratorUrl, logger, maxBufferSize = 100, // Default to storing last 100 chunks (1 chunk = 0.5 seconds)
31
32
  }) {
32
33
  const mockNativeModule = {
33
- addListener: () => {
34
- // Not used on web
35
- },
36
- removeListeners: () => {
37
- // Not used on web
38
- },
34
+ addListener: () => { },
35
+ removeListeners: () => { },
39
36
  };
40
37
  super(mockNativeModule); // Pass the mock native module to the parent class
41
38
  this.logger = logger;
@@ -70,14 +67,56 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
70
67
  throw error;
71
68
  }
72
69
  }
70
+ // Prepare recording with options
71
+ async prepareRecording(recordingConfig = {}) {
72
+ if (this.isRecording) {
73
+ this.logger?.warn('Cannot prepare: Recording is already in progress');
74
+ return false;
75
+ }
76
+ try {
77
+ // Check permissions and initialize basic settings
78
+ await this.getMediaStream().then((stream) => {
79
+ // Just verify we can access the microphone by getting a stream, then release it
80
+ stream.getTracks().forEach((track) => track.stop());
81
+ });
82
+ this.bitDepth = encodingToBitDepth({
83
+ encoding: recordingConfig.encoding ?? 'pcm_32bit',
84
+ });
85
+ // Store recording configuration for later use
86
+ this.recordingConfig = recordingConfig;
87
+ // Use custom filename if provided, otherwise fallback to timestamp
88
+ if (recordingConfig.filename) {
89
+ // Remove any existing extension from the filename
90
+ this.streamUuid = recordingConfig.filename.replace(/\.[^/.]+$/, '');
91
+ }
92
+ else {
93
+ this.streamUuid = Date.now().toString();
94
+ }
95
+ this.logger?.debug('Recording preparation completed successfully');
96
+ return true;
97
+ }
98
+ catch (error) {
99
+ this.logger?.error('Error preparing recording:', error);
100
+ return false;
101
+ }
102
+ }
73
103
  // Start recording with options
74
104
  async startRecording(recordingConfig = {}) {
75
105
  if (this.isRecording) {
76
106
  throw new Error('Recording is already in progress');
77
107
  }
78
- this.bitDepth = encodingToBitDepth({
79
- encoding: recordingConfig.encoding ?? 'pcm_32bit',
80
- });
108
+ // If we haven't prepared or have different settings, prepare now
109
+ if (!this.recordingConfig ||
110
+ this.recordingConfig.sampleRate !== recordingConfig.sampleRate ||
111
+ this.recordingConfig.channels !== recordingConfig.channels ||
112
+ this.recordingConfig.encoding !== recordingConfig.encoding) {
113
+ await this.prepareRecording(recordingConfig);
114
+ }
115
+ else {
116
+ this.logger?.debug('Using previously prepared recording configuration');
117
+ }
118
+ // Save recording config for reference
119
+ this.recordingConfig = recordingConfig;
81
120
  const audioContext = new (window.AudioContext ||
82
121
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
83
122
  // @ts-ignore - Allow webkitAudioContext for Safari
@@ -89,33 +128,13 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
89
128
  audioContext,
90
129
  source,
91
130
  recordingConfig,
92
- emitAudioEventCallback: ({ data, position, compression, }) => {
93
- // Keep only the latest chunks based on maxBufferSize
94
- this.audioChunks.push(new Float32Array(data));
95
- if (this.audioChunks.length > this.maxBufferSize) {
96
- this.audioChunks.shift(); // Remove oldest chunk
97
- }
98
- this.currentSize += data.byteLength;
99
- this.emitAudioEvent({ data, position, compression });
100
- this.lastEmittedTime = Date.now();
101
- this.lastEmittedSize = this.currentSize;
102
- this.lastEmittedCompressionSize = compression?.size ?? 0;
103
- },
104
- emitAudioAnalysisCallback: (audioAnalysisData) => {
105
- this.logger?.log(`Emitted AudioAnalysis:`, audioAnalysisData);
106
- this.emit('AudioAnalysis', audioAnalysisData);
107
- },
131
+ emitAudioEventCallback: this.customRecorderEventCallback.bind(this),
132
+ emitAudioAnalysisCallback: this.customRecorderAnalysisCallback.bind(this),
133
+ onInterruption: this.handleRecordingInterruption.bind(this),
108
134
  });
109
135
  await this.customRecorder.init();
110
136
  this.customRecorder.start();
111
- // // Set a timer to stop recording after 5 seconds
112
- // setTimeout(() => {
113
- // logger.log("AUTO Stopping recording");
114
- // this.customRecorder?.stopAndPlay();
115
- // this.isRecording = false;
116
- // }, 3000);
117
137
  this.isRecording = true;
118
- this.recordingConfig = recordingConfig;
119
138
  this.recordingStartTime = Date.now();
120
139
  this.pausedTime = 0;
121
140
  this.isPaused = false;
@@ -153,14 +172,96 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
153
172
  };
154
173
  return streamConfig;
155
174
  }
175
+ /**
176
+ * Centralized handler for recording interruptions
177
+ */
178
+ handleRecordingInterruption(event) {
179
+ this.logger?.debug(`Received recording interruption: ${event.reason}`);
180
+ // Update local state if the interruption should pause recording
181
+ if (event.isPaused) {
182
+ this.isPaused = true;
183
+ // If this is a device disconnection, handle according to behavior setting
184
+ if (event.reason === 'deviceDisconnected') {
185
+ this.pausedTime = Date.now();
186
+ // Check if we should try fallback to another device
187
+ if (this.recordingConfig?.deviceDisconnectionBehavior ===
188
+ 'fallback') {
189
+ this.logger?.debug('Device disconnected with fallback behavior - attempting to switch to default device');
190
+ // Try to restart with default device
191
+ this.handleDeviceFallback().catch((error) => {
192
+ // If fallback fails, emit warning
193
+ this.logger?.error('Device fallback failed:', error);
194
+ this.emit('onRecordingInterrupted', {
195
+ reason: 'deviceSwitchFailed',
196
+ isPaused: true,
197
+ timestamp: Date.now(),
198
+ message: 'Failed to switch to fallback device. Recording paused.',
199
+ });
200
+ });
201
+ }
202
+ else {
203
+ // Just warn about disconnection if fallback not enabled
204
+ this.logger?.warn('Device disconnected - recording paused automatically');
205
+ this.emit('onRecordingInterrupted', event);
206
+ }
207
+ }
208
+ else {
209
+ // For other interruption types, just emit the event
210
+ this.emit('onRecordingInterrupted', event);
211
+ }
212
+ }
213
+ else {
214
+ // If not causing a pause, just forward the event
215
+ this.emit('onRecordingInterrupted', event);
216
+ }
217
+ }
218
+ /**
219
+ * Handler for audio events from the WebRecorder
220
+ */
221
+ customRecorderEventCallback({ data, position, compression, }) {
222
+ // Keep only the latest chunks based on maxBufferSize
223
+ this.audioChunks.push(new Float32Array(data));
224
+ if (this.audioChunks.length > this.maxBufferSize) {
225
+ this.audioChunks.shift(); // Remove oldest chunk
226
+ }
227
+ this.currentSize += data.byteLength;
228
+ this.emitAudioEvent({ data, position, compression });
229
+ this.lastEmittedTime = Date.now();
230
+ this.lastEmittedSize = this.currentSize;
231
+ this.lastEmittedCompressionSize = compression?.size ?? 0;
232
+ }
233
+ /**
234
+ * Handler for audio analysis events from the WebRecorder
235
+ */
236
+ customRecorderAnalysisCallback(audioAnalysisData) {
237
+ this.emit('AudioAnalysis', audioAnalysisData);
238
+ }
239
+ // Get recording duration
240
+ getRecordingDuration() {
241
+ if (!this.isRecording) {
242
+ return 0;
243
+ }
244
+ return this.currentDurationMs;
245
+ }
156
246
  emitAudioEvent({ data, position, compression }) {
157
247
  const fileUri = `${this.streamUuid}.${this.extension}`;
158
248
  if (compression?.size) {
159
249
  this.lastEmittedCompressionSize = compression.size;
160
250
  this.totalCompressedSize = compression.totalSize;
161
251
  }
252
+ // Update latest position for tracking
162
253
  this.latestPosition = position;
163
- this.currentDurationMs = position * 1000; // Convert position (in seconds) to ms
254
+ // Calculate duration of this chunk in ms
255
+ const sampleRate = this.recordingConfig?.sampleRate || 44100;
256
+ const chunkDurationMs = (data.length / sampleRate) * 1000;
257
+ // Handle duration calculation
258
+ if (this.customRecorder?.isFirstChunkAfterSwitch) {
259
+ this.logger?.debug(`Processing first chunk after device switch, duration preserved at ${this.currentDurationMs}ms`);
260
+ this.customRecorder.isFirstChunkAfterSwitch = false;
261
+ }
262
+ else {
263
+ this.currentDurationMs += chunkDurationMs;
264
+ }
164
265
  const audioEventPayload = {
165
266
  fileUri,
166
267
  mimeType: `audio/${this.extension}`,
@@ -186,17 +287,15 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
186
287
  if (!this.customRecorder) {
187
288
  throw new Error('Recorder is not initialized');
188
289
  }
189
- this.logger?.debug('[Stop] Starting stop process');
190
- const startTime = performance.now();
290
+ this.logger?.debug('Starting stop process');
191
291
  try {
192
- this.logger?.debug('[Stop] Stopping recorder');
193
292
  const { compressedBlob } = await this.customRecorder.stop();
194
293
  this.isRecording = false;
195
294
  this.isPaused = false;
196
- this.currentDurationMs = Date.now() - this.recordingStartTime;
197
295
  let compression;
198
296
  let fileUri = `${this.streamUuid}.${this.extension}`;
199
297
  let mimeType = `audio/${this.extension}`;
298
+ // Process compressed audio if available
200
299
  if (compressedBlob && this.recordingConfig?.compression?.enabled) {
201
300
  const compressedUri = URL.createObjectURL(compressedBlob);
202
301
  compression = {
@@ -210,15 +309,11 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
210
309
  fileUri = compressedUri;
211
310
  mimeType = 'audio/webm';
212
311
  }
213
- this.logger?.debug(`[Stop] Completed stop process in ${performance.now() - startTime}ms`, {
214
- durationMs: this.currentDurationMs,
215
- compressedSize: compression?.size,
216
- });
217
- // Use the stored streamUuid (which contains our custom filename) for the final filename
312
+ // Use the stored streamUuid for the final filename
218
313
  const filename = `${this.streamUuid}.${this.extension}`;
219
314
  const result = {
220
315
  fileUri,
221
- filename, // This will now use our custom filename
316
+ filename,
222
317
  bitDepth: this.bitDepth,
223
318
  createdAt: this.recordingStartTime,
224
319
  channels: this.recordingConfig?.channels ?? 1,
@@ -233,38 +328,82 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
233
328
  return result;
234
329
  }
235
330
  catch (error) {
236
- this.logger?.error('[Stop] Error stopping recording:', error);
331
+ this.logger?.error('Error stopping recording:', error);
237
332
  throw error;
238
333
  }
239
334
  }
240
335
  // Pause recording
241
336
  async pauseRecording() {
242
- if (!this.isRecording || this.isPaused) {
243
- throw new Error('Recording is not active or already paused');
337
+ if (!this.isRecording) {
338
+ throw new Error('Recording is not active');
339
+ }
340
+ if (this.isPaused) {
341
+ this.logger?.debug('Recording already paused, skipping');
342
+ return;
343
+ }
344
+ try {
345
+ if (this.customRecorder) {
346
+ this.customRecorder.pause();
347
+ }
348
+ this.isPaused = true;
349
+ this.pausedTime = Date.now();
244
350
  }
245
- if (this.customRecorder) {
246
- this.customRecorder.pause();
351
+ catch (error) {
352
+ this.logger?.error('Error in pauseRecording', error);
353
+ // Even if the pause operation failed, make sure our state is consistent
354
+ this.isPaused = true;
355
+ this.pausedTime = Date.now();
247
356
  }
248
- this.isPaused = true;
249
- this.pausedTime = Date.now();
250
357
  }
251
358
  // Resume recording
252
359
  async resumeRecording() {
253
360
  if (!this.isPaused) {
254
361
  throw new Error('Recording is not paused');
255
362
  }
256
- if (this.customRecorder) {
363
+ this.logger?.debug('Resuming recording', {
364
+ deviceDisconnectionBehavior: this.recordingConfig?.deviceDisconnectionBehavior,
365
+ isDeviceDisconnected: this.customRecorder?.isDeviceDisconnected,
366
+ });
367
+ try {
368
+ // If we have no recorder, or if the device is disconnected, always attempt fallback
369
+ if (!this.customRecorder ||
370
+ this.customRecorder.isDeviceDisconnected) {
371
+ this.logger?.debug('No recorder exists or device disconnected - attempting fallback on resume');
372
+ await this.handleDeviceFallback();
373
+ // handleDeviceFallback will manage resuming if successful, or emit error if failed.
374
+ return;
375
+ }
376
+ // Normal resume path - device is still connected
257
377
  this.customRecorder.resume();
378
+ this.isPaused = false;
379
+ // Adjust the recording start time to account for the pause duration
380
+ const pauseDuration = Date.now() - this.pausedTime;
381
+ this.recordingStartTime += pauseDuration;
382
+ this.pausedTime = 0;
383
+ this.emit('onRecordingInterrupted', {
384
+ reason: 'userResumed',
385
+ isPaused: false,
386
+ timestamp: Date.now(),
387
+ });
388
+ }
389
+ catch (error) {
390
+ this.logger?.error('Resume failed:', error);
391
+ // Fallback to emitting a general failure if resume fails unexpectedly
392
+ this.emit('onRecordingInterrupted', {
393
+ reason: 'resumeFailed', // Use a more specific reason
394
+ isPaused: true, // Remain paused if resume fails
395
+ timestamp: Date.now(),
396
+ message: 'Failed to resume recording. Please stop and start again.',
397
+ });
258
398
  }
259
- this.isPaused = false;
260
- this.recordingStartTime += Date.now() - this.pausedTime;
261
399
  }
262
400
  // Get current status
263
401
  status() {
402
+ const durationMs = this.getRecordingDuration();
264
403
  const status = {
265
404
  isRecording: this.isRecording,
266
405
  isPaused: this.isPaused,
267
- durationMs: this.currentDurationMs,
406
+ durationMs,
268
407
  size: this.currentSize,
269
408
  interval: this.currentInterval,
270
409
  intervalAnalysis: this.currentIntervalAnalysis,
@@ -281,5 +420,211 @@ export class ExpoAudioStreamWeb extends LegacyEventEmitter {
281
420
  };
282
421
  return status;
283
422
  }
423
+ /**
424
+ * Handles device fallback when the current device is disconnected
425
+ */
426
+ async handleDeviceFallback() {
427
+ this.logger?.debug('Starting device fallback procedure');
428
+ if (!this.isRecording) {
429
+ return false;
430
+ }
431
+ try {
432
+ // Save important state before switching
433
+ const currentPosition = this.latestPosition;
434
+ const existingAudioChunks = [...this.audioChunks];
435
+ // Save compressed chunks if available
436
+ let compressedChunks = [];
437
+ if (this.customRecorder) {
438
+ try {
439
+ compressedChunks = this.customRecorder.getCompressedChunks();
440
+ }
441
+ catch (err) {
442
+ this.logger?.warn('Failed to get compressed chunks:', err);
443
+ }
444
+ }
445
+ // Save the current counter value for continuity
446
+ let currentDataPointCounter = 0;
447
+ if (this.customRecorder) {
448
+ currentDataPointCounter =
449
+ this.customRecorder.getDataPointCounter();
450
+ }
451
+ // Clean up existing recorder
452
+ if (this.customRecorder) {
453
+ try {
454
+ this.customRecorder.cleanup();
455
+ }
456
+ catch (cleanupError) {
457
+ this.logger?.warn('Error during cleanup:', cleanupError);
458
+ }
459
+ }
460
+ // Keep recording state true but mark as paused
461
+ this.isPaused = true;
462
+ this.pausedTime = Date.now();
463
+ // Store current size and other stats
464
+ const previousTotalSize = this.currentSize;
465
+ const previousLastEmittedSize = this.lastEmittedSize;
466
+ const previousCompressedSize = this.totalCompressedSize;
467
+ // Try to get a fallback device
468
+ const fallbackDeviceInfo = await this.getFallbackDevice();
469
+ if (!fallbackDeviceInfo) {
470
+ this.emit('onRecordingInterrupted', {
471
+ reason: 'deviceSwitchFailed',
472
+ isPaused: true,
473
+ timestamp: Date.now(),
474
+ message: 'Failed to switch to fallback device. Recording paused.',
475
+ });
476
+ return false;
477
+ }
478
+ // Start recording with the new device
479
+ try {
480
+ const stream = await this.requestPermissionsAndGetUserMedia(fallbackDeviceInfo.deviceId);
481
+ const audioContext = new (window.AudioContext ||
482
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
483
+ // @ts-ignore - Allow webkitAudioContext for Safari
484
+ window.webkitAudioContext)();
485
+ const source = audioContext.createMediaStreamSource(stream);
486
+ // Create a new recorder with the fallback device
487
+ this.customRecorder = new WebRecorder({
488
+ logger: this.logger,
489
+ audioContext,
490
+ source,
491
+ recordingConfig: this.recordingConfig || {},
492
+ emitAudioEventCallback: this.customRecorderEventCallback.bind(this),
493
+ emitAudioAnalysisCallback: this.customRecorderAnalysisCallback.bind(this),
494
+ onInterruption: this.handleRecordingInterruption.bind(this),
495
+ });
496
+ await this.customRecorder.init();
497
+ // Set the initial position to continue from the previous device
498
+ this.customRecorder.setPosition(currentPosition);
499
+ // Reset the data point counter to continue from where the previous device left off
500
+ if (currentDataPointCounter > 0) {
501
+ this.customRecorder.resetDataPointCounter(currentDataPointCounter);
502
+ }
503
+ // Prepare the recorder to handle the device switch properly
504
+ this.customRecorder.prepareForDeviceSwitch();
505
+ // Restore the existing audio chunks
506
+ if (existingAudioChunks.length > 0) {
507
+ this.audioChunks = existingAudioChunks;
508
+ }
509
+ // Restore compressed chunks if available
510
+ if (compressedChunks.length > 0) {
511
+ this.customRecorder.setCompressedChunks(compressedChunks);
512
+ }
513
+ // Start the new recorder while preserving counters
514
+ this.customRecorder.start(true);
515
+ // Update recording state
516
+ this.isPaused = false;
517
+ this.recordingStartTime = Date.now();
518
+ // Restore size counters to maintain continuity
519
+ this.currentSize = previousTotalSize;
520
+ this.lastEmittedSize = previousLastEmittedSize;
521
+ this.totalCompressedSize = previousCompressedSize;
522
+ // Notify that we switched to a fallback device
523
+ if (this.eventCallback) {
524
+ this.eventCallback({
525
+ type: 'deviceFallback',
526
+ device: fallbackDeviceInfo.deviceId,
527
+ timestamp: new Date(),
528
+ });
529
+ }
530
+ return true;
531
+ }
532
+ catch (error) {
533
+ this.logger?.error('Failed to start recording with fallback device', error);
534
+ this.isPaused = true;
535
+ this.emit('onRecordingInterrupted', {
536
+ reason: 'deviceSwitchFailed',
537
+ isPaused: true,
538
+ timestamp: Date.now(),
539
+ message: 'Failed to switch to fallback device. Recording paused.',
540
+ });
541
+ return false;
542
+ }
543
+ }
544
+ catch (error) {
545
+ this.logger?.error('Failed to use fallback device', error);
546
+ this.isPaused = true;
547
+ this.emit('onRecordingInterrupted', {
548
+ reason: 'deviceSwitchFailed',
549
+ isPaused: true,
550
+ timestamp: Date.now(),
551
+ message: 'Failed to switch to fallback device. Recording paused.',
552
+ });
553
+ return false;
554
+ }
555
+ }
556
+ /**
557
+ * Attempts to get a fallback audio device
558
+ */
559
+ async getFallbackDevice() {
560
+ try {
561
+ // Get list of available audio input devices
562
+ const devices = await navigator.mediaDevices.enumerateDevices();
563
+ const audioInputDevices = devices.filter((device) => device.kind === 'audioinput');
564
+ if (audioInputDevices.length === 0) {
565
+ return null;
566
+ }
567
+ // Try to find a device that's not the current one
568
+ if (this.customRecorder) {
569
+ try {
570
+ // Use mediaDevices.enumerateDevices to find the current active device
571
+ const tracks = navigator.mediaDevices
572
+ .getUserMedia({ audio: true })
573
+ .then((stream) => {
574
+ const track = stream.getAudioTracks()[0];
575
+ return track ? track.label : '';
576
+ })
577
+ .catch(() => '');
578
+ const currentTrackLabel = await tracks;
579
+ if (currentTrackLabel) {
580
+ // Find a device with a different label
581
+ const differentDevice = audioInputDevices.find((device) => device.label &&
582
+ device.label !== currentTrackLabel);
583
+ if (differentDevice) {
584
+ return differentDevice;
585
+ }
586
+ }
587
+ }
588
+ catch (err) {
589
+ this.logger?.warn('Error determining current device, using default');
590
+ }
591
+ }
592
+ // Return the first available device (default device)
593
+ return audioInputDevices[0];
594
+ }
595
+ catch (error) {
596
+ this.logger?.error('Error finding fallback device:', error);
597
+ return null;
598
+ }
599
+ }
600
+ /**
601
+ * Gets user media with specific device ID
602
+ */
603
+ async requestPermissionsAndGetUserMedia(deviceId) {
604
+ try {
605
+ // Request the specific device
606
+ return await navigator.mediaDevices.getUserMedia({
607
+ audio: {
608
+ deviceId: { exact: deviceId },
609
+ },
610
+ });
611
+ }
612
+ catch (error) {
613
+ this.logger?.error(`Failed to get media for device ${deviceId}`, error);
614
+ // Try with default constraints as fallback
615
+ return await navigator.mediaDevices.getUserMedia({ audio: true });
616
+ }
617
+ }
618
+ init(options) {
619
+ try {
620
+ this.logger = options?.logger;
621
+ this.eventCallback = options?.eventCallback;
622
+ return Promise.resolve();
623
+ }
624
+ catch (error) {
625
+ this.logger?.error('Error initializing ExpoAudioStream', error);
626
+ return Promise.reject(error);
627
+ }
628
+ }
284
629
  }
285
630
  //# sourceMappingURL=ExpoAudioStream.web.js.map