@waveform-playlist/recording 12.1.1 → 13.0.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/README.md +153 -139
- package/dist/index.d.mts +44 -10
- package/dist/index.d.ts +44 -10
- package/dist/index.js +108 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -62
- 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,23 +532,28 @@ 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
|
|
492
|
-
import {
|
|
535
|
+
import { useState as useState4, useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4 } from "react";
|
|
536
|
+
import {
|
|
537
|
+
carveClipRange,
|
|
538
|
+
resolveRecordingOffsetSamples
|
|
539
|
+
} from "@waveform-playlist/core";
|
|
493
540
|
import {
|
|
494
541
|
resumeGlobalAudioContext,
|
|
495
542
|
getGlobalAudioContext,
|
|
496
543
|
getGlobalContext as getGlobalContext3
|
|
497
544
|
} from "@waveform-playlist/playout";
|
|
498
545
|
function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}) {
|
|
499
|
-
const { currentTime = 0, audioConstraints, ...recordingOptions } = options;
|
|
546
|
+
const { currentTime = 0, audioConstraints, latencyOffset, ...recordingOptions } = options;
|
|
500
547
|
const [isMonitoring, setIsMonitoring] = useState4(false);
|
|
501
548
|
const [selectedDevice, setSelectedDevice] = useState4(null);
|
|
502
549
|
const [hookError, setHookError] = useState4(null);
|
|
503
|
-
const recordingStartTimeRef =
|
|
504
|
-
const selectedTrackIdRef =
|
|
550
|
+
const recordingStartTimeRef = useRef4(0);
|
|
551
|
+
const selectedTrackIdRef = useRef4(selectedTrackId);
|
|
505
552
|
selectedTrackIdRef.current = selectedTrackId;
|
|
506
|
-
const currentTimeRef =
|
|
553
|
+
const currentTimeRef = useRef4(currentTime);
|
|
507
554
|
currentTimeRef.current = currentTime;
|
|
555
|
+
const tracksRef = useRef4(tracks);
|
|
556
|
+
tracksRef.current = tracks;
|
|
508
557
|
const { stream, devices, hasPermission, requestAccess, error: micError } = useMicrophoneAccess();
|
|
509
558
|
const {
|
|
510
559
|
level,
|
|
@@ -534,7 +583,7 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
534
583
|
setHookError(
|
|
535
584
|
new Error("Cannot start recording: no track selected. Select or create a track first.")
|
|
536
585
|
);
|
|
537
|
-
return;
|
|
586
|
+
return false;
|
|
538
587
|
}
|
|
539
588
|
try {
|
|
540
589
|
setHookError(null);
|
|
@@ -543,9 +592,10 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
543
592
|
setIsMonitoring(true);
|
|
544
593
|
}
|
|
545
594
|
recordingStartTimeRef.current = currentTimeRef.current;
|
|
546
|
-
await startRec();
|
|
595
|
+
return await startRec();
|
|
547
596
|
} catch (err) {
|
|
548
597
|
setHookError(err instanceof Error ? err : new Error(String(err)));
|
|
598
|
+
return false;
|
|
549
599
|
}
|
|
550
600
|
}, [isMonitoring, startRec]);
|
|
551
601
|
const stopRecording = useCallback4(async () => {
|
|
@@ -557,8 +607,9 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
557
607
|
return;
|
|
558
608
|
}
|
|
559
609
|
const trackId = selectedTrackIdRef.current;
|
|
610
|
+
const currentTracks = tracksRef.current;
|
|
560
611
|
if (buffer && trackId) {
|
|
561
|
-
const selectedTrackIndex =
|
|
612
|
+
const selectedTrackIndex = currentTracks.findIndex((t) => t.id === trackId);
|
|
562
613
|
if (selectedTrackIndex === -1) {
|
|
563
614
|
const err = new Error(
|
|
564
615
|
`Recording completed but track "${trackId}" no longer exists. The recorded audio could not be saved.`
|
|
@@ -567,25 +618,17 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
567
618
|
setHookError(err);
|
|
568
619
|
return;
|
|
569
620
|
}
|
|
570
|
-
const
|
|
571
|
-
const recordStartTimeSamples = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);
|
|
572
|
-
let lastClipEndSample = 0;
|
|
573
|
-
if (selectedTrack.clips.length > 0) {
|
|
574
|
-
const endSamples = selectedTrack.clips.map(
|
|
575
|
-
(clip) => clip.startSample + clip.durationSamples
|
|
576
|
-
);
|
|
577
|
-
lastClipEndSample = Math.max(...endSamples);
|
|
578
|
-
}
|
|
579
|
-
const startSample = Math.max(recordStartTimeSamples, lastClipEndSample);
|
|
621
|
+
const startSample = Math.floor(recordingStartTimeRef.current * buffer.sampleRate);
|
|
580
622
|
const audioContext = getGlobalAudioContext();
|
|
581
623
|
const outputLatency = audioContext.outputLatency ?? 0;
|
|
582
624
|
const toneContext = getGlobalContext3();
|
|
583
625
|
const lookAhead = toneContext.lookAhead ?? 0;
|
|
584
|
-
const latencyOffsetSamples =
|
|
626
|
+
const latencyOffsetSamples = resolveRecordingOffsetSamples({
|
|
627
|
+
overrideSeconds: latencyOffset,
|
|
585
628
|
outputLatency,
|
|
586
629
|
lookAhead,
|
|
587
|
-
buffer.sampleRate
|
|
588
|
-
);
|
|
630
|
+
sampleRate: buffer.sampleRate
|
|
631
|
+
});
|
|
589
632
|
const effectiveDuration = Math.max(0, buffer.length - latencyOffsetSamples);
|
|
590
633
|
if (effectiveDuration === 0) {
|
|
591
634
|
console.warn(
|
|
@@ -605,18 +648,21 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
605
648
|
gain: 1,
|
|
606
649
|
name: `Recording ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`
|
|
607
650
|
};
|
|
608
|
-
const newTracks =
|
|
651
|
+
const newTracks = currentTracks.map((track, index) => {
|
|
609
652
|
if (index === selectedTrackIndex) {
|
|
610
653
|
return {
|
|
611
654
|
...track,
|
|
612
|
-
clips: [
|
|
655
|
+
clips: [
|
|
656
|
+
...carveClipRange(track.clips, startSample, startSample + effectiveDuration),
|
|
657
|
+
newClip
|
|
658
|
+
]
|
|
613
659
|
};
|
|
614
660
|
}
|
|
615
661
|
return track;
|
|
616
662
|
});
|
|
617
663
|
setTracks(newTracks);
|
|
618
664
|
}
|
|
619
|
-
}, [
|
|
665
|
+
}, [setTracks, stopRec, latencyOffset]);
|
|
620
666
|
useEffect4(() => {
|
|
621
667
|
if (!hasPermission || devices.length === 0) return;
|
|
622
668
|
if (selectedDevice === null) {
|
|
@@ -628,7 +674,13 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
628
674
|
requestAccess(fallbackId, audioConstraints);
|
|
629
675
|
}
|
|
630
676
|
}, [hasPermission, devices, selectedDevice, resetPeak, requestAccess, audioConstraints]);
|
|
677
|
+
const isRecordingRef = useRef4(isRecording);
|
|
678
|
+
isRecordingRef.current = isRecording;
|
|
631
679
|
const requestMicAccess = useCallback4(async () => {
|
|
680
|
+
if (isRecordingRef.current) {
|
|
681
|
+
setHookError(new Error("Cannot request microphone access while recording. Stop first."));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
632
684
|
try {
|
|
633
685
|
setHookError(null);
|
|
634
686
|
await requestAccess(void 0, audioConstraints);
|
|
@@ -640,6 +692,10 @@ function useIntegratedRecording(tracks, setTracks, selectedTrackId, options = {}
|
|
|
640
692
|
}, [requestAccess, audioConstraints]);
|
|
641
693
|
const changeDevice = useCallback4(
|
|
642
694
|
async (deviceId) => {
|
|
695
|
+
if (isRecordingRef.current) {
|
|
696
|
+
setHookError(new Error("Cannot switch microphone while recording. Stop first."));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
643
699
|
try {
|
|
644
700
|
setHookError(null);
|
|
645
701
|
setSelectedDevice(deviceId);
|