@waveform-playlist/recording 12.2.0 → 13.0.1
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/README.md +153 -139
- package/dist/index.d.mts +35 -10
- package/dist/index.d.ts +35 -10
- package/dist/index.js +104 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -57
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -30,10 +30,16 @@ function useRecording(stream, options = {}) {
|
|
|
30
30
|
const stopRecordingRef = useRef(null);
|
|
31
31
|
const stopAckResolveRef = useRef(null);
|
|
32
32
|
const isStoppingRef = useRef(false);
|
|
33
|
+
const stopPromiseRef = useRef(null);
|
|
34
|
+
const finishStopRecordingRef = useRef(null);
|
|
35
|
+
const isStartingRef = useRef(false);
|
|
36
|
+
const isUnmountedRef = useRef(false);
|
|
37
|
+
const durationRef = useRef(0);
|
|
33
38
|
const startDurationLoop = useCallback(() => {
|
|
34
39
|
const tick = () => {
|
|
35
40
|
if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {
|
|
36
41
|
const elapsed = (performance.now() - startTimeRef.current) / 1e3;
|
|
42
|
+
durationRef.current = elapsed;
|
|
37
43
|
setDuration(elapsed);
|
|
38
44
|
animationFrameRef.current = requestAnimationFrame(tick);
|
|
39
45
|
}
|
|
@@ -56,11 +62,12 @@ function useRecording(stream, options = {}) {
|
|
|
56
62
|
}
|
|
57
63
|
}, []);
|
|
58
64
|
const startRecording = useCallback(async () => {
|
|
59
|
-
if (isRecordingRef.current) return;
|
|
65
|
+
if (isRecordingRef.current || isStartingRef.current) return false;
|
|
60
66
|
if (!stream) {
|
|
61
67
|
setError(new Error("No microphone stream available"));
|
|
62
|
-
return;
|
|
68
|
+
return false;
|
|
63
69
|
}
|
|
70
|
+
isStartingRef.current = true;
|
|
64
71
|
try {
|
|
65
72
|
setError(null);
|
|
66
73
|
const context = getGlobalContext();
|
|
@@ -68,6 +75,7 @@ function useRecording(stream, options = {}) {
|
|
|
68
75
|
await context.resume();
|
|
69
76
|
}
|
|
70
77
|
await loadWorklet();
|
|
78
|
+
if (isUnmountedRef.current) return false;
|
|
71
79
|
const source = context.createMediaStreamSource(stream);
|
|
72
80
|
mediaStreamSourceRef.current = source;
|
|
73
81
|
const detectedChannelCount = stream.getAudioTracks()[0]?.getSettings().channelCount;
|
|
@@ -143,17 +151,29 @@ function useRecording(stream, options = {}) {
|
|
|
143
151
|
setIsPaused(false);
|
|
144
152
|
startTimeRef.current = performance.now();
|
|
145
153
|
startDurationLoop();
|
|
154
|
+
return true;
|
|
146
155
|
} catch (err) {
|
|
147
156
|
console.warn("[waveform-playlist] Failed to start recording:", String(err));
|
|
148
157
|
setError(err instanceof Error ? err : new Error("Failed to start recording"));
|
|
158
|
+
return false;
|
|
159
|
+
} finally {
|
|
160
|
+
isStartingRef.current = false;
|
|
149
161
|
}
|
|
150
162
|
}, [stream, channelCount, samplesPerPixel, bits, loadWorklet, startDurationLoop]);
|
|
151
163
|
const stopRecording = useCallback(async () => {
|
|
152
164
|
if (!isRecordingRef.current) {
|
|
153
165
|
return null;
|
|
154
166
|
}
|
|
167
|
+
if (isStoppingRef.current) {
|
|
168
|
+
return stopPromiseRef.current ?? null;
|
|
169
|
+
}
|
|
170
|
+
isStoppingRef.current = true;
|
|
171
|
+
const stopPromise = finishStopRecordingRef.current();
|
|
172
|
+
stopPromiseRef.current = stopPromise;
|
|
173
|
+
return stopPromise;
|
|
174
|
+
}, []);
|
|
175
|
+
const finishStopRecording = useCallback(async () => {
|
|
155
176
|
try {
|
|
156
|
-
isStoppingRef.current = true;
|
|
157
177
|
if (animationFrameRef.current !== null) {
|
|
158
178
|
cancelAnimationFrame(animationFrameRef.current);
|
|
159
179
|
animationFrameRef.current = null;
|
|
@@ -196,6 +216,8 @@ function useRecording(stream, options = {}) {
|
|
|
196
216
|
}
|
|
197
217
|
}
|
|
198
218
|
node.disconnect();
|
|
219
|
+
workletNodeRef.current = null;
|
|
220
|
+
mediaStreamSourceRef.current = null;
|
|
199
221
|
}
|
|
200
222
|
const context = getGlobalContext();
|
|
201
223
|
const rawContext = context.rawContext;
|
|
@@ -208,6 +230,7 @@ function useRecording(stream, options = {}) {
|
|
|
208
230
|
}
|
|
209
231
|
const buffer = createAudioBuffer(rawContext, channelData, rawContext.sampleRate, numChannels);
|
|
210
232
|
setAudioBuffer(buffer);
|
|
233
|
+
durationRef.current = buffer.duration;
|
|
211
234
|
setDuration(buffer.duration);
|
|
212
235
|
return buffer;
|
|
213
236
|
} catch (err) {
|
|
@@ -224,8 +247,9 @@ function useRecording(stream, options = {}) {
|
|
|
224
247
|
}
|
|
225
248
|
}, [channelCount]);
|
|
226
249
|
stopRecordingRef.current = stopRecording;
|
|
250
|
+
finishStopRecordingRef.current = finishStopRecording;
|
|
227
251
|
const pauseRecording = useCallback(() => {
|
|
228
|
-
if (
|
|
252
|
+
if (isRecordingRef.current && !isPausedRef.current && !isStoppingRef.current) {
|
|
229
253
|
workletNodeRef.current?.port.postMessage({ command: "pause" });
|
|
230
254
|
if (animationFrameRef.current !== null) {
|
|
231
255
|
cancelAnimationFrame(animationFrameRef.current);
|
|
@@ -234,18 +258,20 @@ function useRecording(stream, options = {}) {
|
|
|
234
258
|
isPausedRef.current = true;
|
|
235
259
|
setIsPaused(true);
|
|
236
260
|
}
|
|
237
|
-
}, [
|
|
261
|
+
}, []);
|
|
238
262
|
const resumeRecording = useCallback(() => {
|
|
239
|
-
if (
|
|
263
|
+
if (isRecordingRef.current && isPausedRef.current && !isStoppingRef.current) {
|
|
240
264
|
workletNodeRef.current?.port.postMessage({ command: "resume" });
|
|
241
265
|
isPausedRef.current = false;
|
|
242
266
|
setIsPaused(false);
|
|
243
|
-
startTimeRef.current = performance.now() -
|
|
267
|
+
startTimeRef.current = performance.now() - durationRef.current * 1e3;
|
|
244
268
|
startDurationLoop();
|
|
245
269
|
}
|
|
246
|
-
}, [
|
|
270
|
+
}, [startDurationLoop]);
|
|
247
271
|
useEffect(() => {
|
|
272
|
+
isUnmountedRef.current = false;
|
|
248
273
|
return () => {
|
|
274
|
+
isUnmountedRef.current = true;
|
|
249
275
|
if (audioTrackRef.current && onTrackEndedRef.current) {
|
|
250
276
|
audioTrackRef.current.removeEventListener("ended", onTrackEndedRef.current);
|
|
251
277
|
}
|
|
@@ -286,21 +312,20 @@ function useRecording(stream, options = {}) {
|
|
|
286
312
|
}
|
|
287
313
|
|
|
288
314
|
// src/hooks/useMicrophoneAccess.ts
|
|
289
|
-
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
315
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
316
|
+
import { enumerateMicrophones } from "@waveform-playlist/core";
|
|
290
317
|
function useMicrophoneAccess() {
|
|
291
318
|
const [stream, setStream] = useState2(null);
|
|
292
319
|
const [devices, setDevices] = useState2([]);
|
|
293
320
|
const [hasPermission, setHasPermission] = useState2(false);
|
|
294
321
|
const [isLoading, setIsLoading] = useState2(false);
|
|
295
322
|
const [error, setError] = useState2(null);
|
|
323
|
+
const streamRef = useRef2(null);
|
|
324
|
+
const requestGenerationRef = useRef2(0);
|
|
325
|
+
const isUnmountedRef = useRef2(false);
|
|
296
326
|
const enumerateDevices = useCallback2(async () => {
|
|
297
327
|
try {
|
|
298
|
-
const
|
|
299
|
-
const audioInputs = allDevices.filter((device) => device.kind === "audioinput").map((device) => ({
|
|
300
|
-
deviceId: device.deviceId,
|
|
301
|
-
label: device.label || `Microphone ${device.deviceId.slice(0, 8)}`,
|
|
302
|
-
groupId: device.groupId
|
|
303
|
-
}));
|
|
328
|
+
const { devices: audioInputs } = await enumerateMicrophones();
|
|
304
329
|
setDevices(audioInputs);
|
|
305
330
|
} catch (err) {
|
|
306
331
|
console.error("Failed to enumerate devices:", err);
|
|
@@ -309,11 +334,14 @@ function useMicrophoneAccess() {
|
|
|
309
334
|
}, []);
|
|
310
335
|
const requestAccess = useCallback2(
|
|
311
336
|
async (deviceId, audioConstraints) => {
|
|
337
|
+
const generation = ++requestGenerationRef.current;
|
|
312
338
|
setIsLoading(true);
|
|
313
339
|
setError(null);
|
|
314
340
|
try {
|
|
315
|
-
if (
|
|
316
|
-
|
|
341
|
+
if (streamRef.current) {
|
|
342
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
343
|
+
streamRef.current = null;
|
|
344
|
+
setStream(null);
|
|
317
345
|
}
|
|
318
346
|
const audio = {
|
|
319
347
|
// Recording-optimized defaults: prioritize raw audio quality and low latency
|
|
@@ -332,36 +360,50 @@ function useMicrophoneAccess() {
|
|
|
332
360
|
video: false
|
|
333
361
|
};
|
|
334
362
|
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
363
|
+
if (generation !== requestGenerationRef.current || isUnmountedRef.current) {
|
|
364
|
+
newStream.getTracks().forEach((track) => track.stop());
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
streamRef.current = newStream;
|
|
335
368
|
setStream(newStream);
|
|
336
369
|
setHasPermission(true);
|
|
337
370
|
await enumerateDevices();
|
|
338
371
|
} catch (err) {
|
|
339
372
|
console.error("Failed to access microphone:", err);
|
|
340
|
-
|
|
341
|
-
|
|
373
|
+
if (generation === requestGenerationRef.current) {
|
|
374
|
+
setError(err instanceof Error ? err : new Error("Failed to access microphone"));
|
|
375
|
+
setHasPermission(false);
|
|
376
|
+
}
|
|
342
377
|
} finally {
|
|
343
|
-
|
|
378
|
+
if (generation === requestGenerationRef.current) {
|
|
379
|
+
setIsLoading(false);
|
|
380
|
+
}
|
|
344
381
|
}
|
|
345
382
|
},
|
|
346
|
-
[
|
|
383
|
+
[enumerateDevices]
|
|
347
384
|
);
|
|
348
385
|
const stopStream = useCallback2(() => {
|
|
349
|
-
|
|
350
|
-
|
|
386
|
+
requestGenerationRef.current++;
|
|
387
|
+
if (streamRef.current) {
|
|
388
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
389
|
+
streamRef.current = null;
|
|
351
390
|
setStream(null);
|
|
352
391
|
setHasPermission(false);
|
|
353
392
|
}
|
|
354
|
-
}, [
|
|
393
|
+
}, []);
|
|
355
394
|
useEffect2(() => {
|
|
395
|
+
isUnmountedRef.current = false;
|
|
356
396
|
enumerateDevices();
|
|
357
397
|
navigator.mediaDevices.addEventListener("devicechange", enumerateDevices);
|
|
358
398
|
return () => {
|
|
359
399
|
navigator.mediaDevices.removeEventListener("devicechange", enumerateDevices);
|
|
360
|
-
|
|
361
|
-
|
|
400
|
+
isUnmountedRef.current = true;
|
|
401
|
+
if (streamRef.current) {
|
|
402
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
403
|
+
streamRef.current = null;
|
|
362
404
|
}
|
|
363
405
|
};
|
|
364
|
-
}, [enumerateDevices
|
|
406
|
+
}, [enumerateDevices]);
|
|
365
407
|
return {
|
|
366
408
|
stream,
|
|
367
409
|
devices,
|
|
@@ -374,7 +416,7 @@ function useMicrophoneAccess() {
|
|
|
374
416
|
}
|
|
375
417
|
|
|
376
418
|
// src/hooks/useMicrophoneLevel.ts
|
|
377
|
-
import { useEffect as useEffect3, useState as useState3, useRef as
|
|
419
|
+
import { useEffect as useEffect3, useState as useState3, useRef as useRef3, useCallback as useCallback3 } from "react";
|
|
378
420
|
import { getGlobalContext as getGlobalContext2 } from "@waveform-playlist/playout";
|
|
379
421
|
import { gainToNormalized } from "@waveform-playlist/core";
|
|
380
422
|
import { addMeterWorkletModule } from "@waveform-playlist/worklets";
|
|
@@ -385,9 +427,9 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
385
427
|
const [peakLevels, setPeakLevels] = useState3(() => new Array(channelCount).fill(0));
|
|
386
428
|
const [rmsLevels, setRmsLevels] = useState3(() => new Array(channelCount).fill(0));
|
|
387
429
|
const [meterError, setMeterError] = useState3(null);
|
|
388
|
-
const workletNodeRef =
|
|
389
|
-
const sourceRef =
|
|
390
|
-
const smoothedPeakRef =
|
|
430
|
+
const workletNodeRef = useRef3(null);
|
|
431
|
+
const sourceRef = useRef3(null);
|
|
432
|
+
const smoothedPeakRef = useRef3(new Array(channelCount).fill(0));
|
|
391
433
|
const resetPeak = useCallback3(
|
|
392
434
|
() => setPeakLevels(new Array(channelCount).fill(0)),
|
|
393
435
|
[channelCount]
|
|
@@ -431,21 +473,23 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
431
473
|
smoothedPeakRef.current = new Array(actualChannels).fill(0);
|
|
432
474
|
workletNode.port.onmessage = (event) => {
|
|
433
475
|
if (!isMounted) return;
|
|
434
|
-
const
|
|
476
|
+
const data = event.data;
|
|
477
|
+
const workletChannels = data.length / 2;
|
|
435
478
|
const smoothed = smoothedPeakRef.current;
|
|
436
479
|
const peakValues = [];
|
|
437
480
|
const rmsValues = [];
|
|
438
|
-
for (let ch = 0; ch <
|
|
439
|
-
smoothed[ch] = Math.max(
|
|
481
|
+
for (let ch = 0; ch < workletChannels; ch++) {
|
|
482
|
+
smoothed[ch] = Math.max(data[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);
|
|
440
483
|
peakValues.push(gainToNormalized(smoothed[ch]));
|
|
441
|
-
rmsValues.push(gainToNormalized(
|
|
484
|
+
rmsValues.push(gainToNormalized(data[workletChannels + ch]));
|
|
442
485
|
}
|
|
443
|
-
const mirroredPeaks =
|
|
444
|
-
const mirroredRms =
|
|
486
|
+
const mirroredPeaks = workletChannels < channelCount ? new Array(channelCount).fill(peakValues[0]) : peakValues;
|
|
487
|
+
const mirroredRms = workletChannels < channelCount ? new Array(channelCount).fill(rmsValues[0]) : rmsValues;
|
|
445
488
|
setLevels(mirroredPeaks);
|
|
446
489
|
setRmsLevels(mirroredRms);
|
|
447
490
|
setPeakLevels((prev) => mirroredPeaks.map((val, i) => Math.max(prev[i] ?? 0, val)));
|
|
448
491
|
};
|
|
492
|
+
setMeterError(null);
|
|
449
493
|
};
|
|
450
494
|
setupMonitoring().catch((err) => {
|
|
451
495
|
console.warn("[waveform-playlist] Failed to set up mic level monitoring:", String(err));
|
|
@@ -488,8 +532,9 @@ function useMicrophoneLevel(stream, options = {}) {
|
|
|
488
532
|
}
|
|
489
533
|
|
|
490
534
|
// src/hooks/useIntegratedRecording.ts
|
|
491
|
-
import { useState as useState4, useCallback as useCallback4, useEffect as useEffect4, useRef as
|
|
535
|
+
import { useState as useState4, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4 } from "react";
|
|
492
536
|
import {
|
|
537
|
+
carveClipRange,
|
|
493
538
|
resolveRecordingOffsetSamples
|
|
494
539
|
} from "@waveform-playlist/core";
|
|
495
540
|
import {
|
|
@@ -502,11 +547,13 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
502
547
|
const [isMonitoring, setIsMonitoring] = useState4(false);
|
|
503
548
|
const [selectedDevice, setSelectedDevice] = useState4(null);
|
|
504
549
|
const [hookError, setHookError] = useState4(null);
|
|
505
|
-
const recordingStartTimeRef =
|
|
506
|
-
const selectedTrackIdRef =
|
|
550
|
+
const recordingStartTimeRef = useRef4(0);
|
|
551
|
+
const selectedTrackIdRef = useRef4(selectedTrackId);
|
|
507
552
|
selectedTrackIdRef.current = selectedTrackId;
|
|
508
|
-
const currentTimeRef =
|
|
553
|
+
const currentTimeRef = useRef4(currentTime);
|
|
509
554
|
currentTimeRef.current = currentTime;
|
|
555
|
+
const tracksRef = useRef4(tracks);
|
|
556
|
+
tracksRef.current = tracks;
|
|
510
557
|
const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();
|
|
511
558
|
const {
|
|
512
559
|
level,
|
|
@@ -536,7 +583,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
536
583
|
setHookError(
|
|
537
584
|
new Error("Cannot start recording: no track selected. Select or create a track first.")
|
|
538
585
|
);
|
|
539
|
-
return;
|
|
586
|
+
return false;
|
|
540
587
|
}
|
|
541
588
|
try {
|
|
542
589
|
setHookError(null);
|
|
@@ -545,9 +592,10 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
545
592
|
setIsMonitoring(true);
|
|
546
593
|
}
|
|
547
594
|
recordingStartTimeRef.current = currentTimeRef.current;
|
|
548
|
-
await startRec();
|
|
595
|
+
return await startRec();
|
|
549
596
|
} catch (err) {
|
|
550
597
|
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
598
|
+
return false;
|
|
551
599
|
}
|
|
552
600
|
}, [isMonitoring, startRec]);
|
|
553
601
|
const stopRecording = useCallback4(async () => {
|
|
@@ -559,8 +607,9 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
559
607
|
return;
|
|
560
608
|
}
|
|
561
609
|
const trackId = selectedTrackIdRef.current;
|
|
610
|
+
const currentTracks = tracksRef.current;
|
|
562
611
|
if (buffer && trackId) {
|
|
563
|
-
const selectedTrackIndex =
|
|
612
|
+
const selectedTrackIndex = currentTracks.findIndex((t) => t.id === trackId);
|
|
564
613
|
if (selectedTrackIndex === -1) {
|
|
565
614
|
const err = new Error(
|
|
566
615
|
`Recording completed but track "${trackId}" no longer exists. The recorded audio could not be saved.`
|
|
@@ -569,16 +618,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
569
618
|
setHookError(err);
|
|
570
619
|
return;
|
|
571
620
|
}
|
|
572
|
-
const
|
|
573
|
-
const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);
|
|
574
|
-
let lastClipEndSample = 0;
|
|
575
|
-
if (selectedTrack.clips.length > 0) {
|
|
576
|
-
const endSamples = selectedTrack.clips.map(
|
|
577
|
-
(clip) => clip.startSample + clip.durationSamples
|
|
578
|
-
);
|
|
579
|
-
lastClipEndSample = Math.max(...endSamples);
|
|
580
|
-
}
|
|
581
|
-
const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);
|
|
621
|
+
const startSample = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);
|
|
582
622
|
const audioContext = getGlobalAudioContext();
|
|
583
623
|
const outputLatency = audioContext.outputLatency ?? 0;
|
|
584
624
|
const toneContext = getGlobalContext3();
|
|
@@ -608,18 +648,21 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
608
648
|
gain: 1,
|
|
609
649
|
name: `Recording ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`
|
|
610
650
|
};
|
|
611
|
-
const newTracks =
|
|
651
|
+
const newTracks = currentTracks.map((track, index) => {
|
|
612
652
|
if (index === selectedTrackIndex) {
|
|
613
653
|
return {
|
|
614
654
|
...track,
|
|
615
|
-
clips: [
|
|
655
|
+
clips: [
|
|
656
|
+
...carveClipRange(track.clips, startSample, startSample + effectiveDuration),
|
|
657
|
+
newClip
|
|
658
|
+
]
|
|
616
659
|
};
|
|
617
660
|
}
|
|
618
661
|
return track;
|
|
619
662
|
});
|
|
620
663
|
setTracks(newTracks);
|
|
621
664
|
}
|
|
622
|
-
}, [
|
|
665
|
+
}, [setTracks, stopRec, latencyOffset]);
|
|
623
666
|
useEffect4(() => {
|
|
624
667
|
if (!hasPermission || devices.length === 0) return;
|
|
625
668
|
if (selectedDevice === null) {
|
|
@@ -631,7 +674,13 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
631
674
|
requestAccess(fallbackId, audioConstraints);
|
|
632
675
|
}
|
|
633
676
|
}, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);
|
|
677
|
+
const isRecordingRef = useRef4(isRecording);
|
|
678
|
+
isRecordingRef.current = isRecording;
|
|
634
679
|
const requestMicAccess = useCallback4(async () => {
|
|
680
|
+
if (isRecordingRef.current) {
|
|
681
|
+
setHookError(new Error("Cannot request microphone access while recording. Stop first."));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
635
684
|
try {
|
|
636
685
|
setHookError(null);
|
|
637
686
|
await requestAccess(void 0, audioConstraints);
|
|
@@ -643,6 +692,10 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
643
692
|
}, [requestAccess, audioConstraints]);
|
|
644
693
|
const changeDevice = useCallback4(
|
|
645
694
|
async (deviceId) => {
|
|
695
|
+
if (isRecordingRef.current) {
|
|
696
|
+
setHookError(new Error("Cannot switch microphone while recording. Stop first."));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
646
699
|
try {
|
|
647
700
|
setHookError(null);
|
|
648
701
|
setSelectedDevice(deviceId);
|