Files
Recordly/src/hooks/useAudioLevelMeter.ts
T
webadderall 8cdf21e125 feat: overhaul timeline drag/resize, zoom playback, and recording hooks
- TimelineEditor: rewrite drag/resize with neighbour clamping — items cannot
  overlap; drag preserves original duration; resize clamps to adjacent item
  boundaries instead of rejecting; floating drag/resize tooltip via direct
  DOM (no re-renders)
- TimelineWrapper: new wrapper component for cleaner TimelineContext wiring
- Track: extract Track component with empty-row hint slot
- AudioWaveform, Row, Subrow, KeyframeMarkers, Timeline.module.css: dark
  theme timeline overhaul
- zoomSuggestionUtils: smarter auto-zoom region suggestion from cursor telemetry
- videoPlayback (zoom): fix zoomRegionUtils edge cases at timeline bounds;
  rework zoomTransform for cursor-follow camera
- videoPlayback (cursor): cursorFollowCamera, cursorSway, cursorLoopTelemetry,
  cursorViewport — new cursor motion and snapping pipeline
- videoPlayback (layout): layoutUtils, overlayUtils, focusUtils, motionSmoothing
- useScreenRecorder: hardened recording lifecycle, pause segment tracking,
  improved WGC and SCK integration
- useVideoDevices / useMicrophoneDevices / useAudioLevelMeter improvements
2026-04-16 17:53:51 +10:00

107 lines
2.8 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
export interface AudioLevelMeterOptions {
enabled: boolean;
deviceId?: string;
smoothingFactor?: number;
}
export function useAudioLevelMeter(options: AudioLevelMeterOptions) {
const [level, setLevel] = useState(0);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const animationFrameRef = useRef<number | null>(null);
useEffect(() => {
const cleanup = () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
if (audioContextRef.current) {
audioContextRef.current.close().catch(() => undefined);
audioContextRef.current = null;
}
analyserRef.current = null;
};
if (!options.enabled) {
cleanup();
setLevel(0);
return cleanup;
}
let mounted = true;
const startMonitoring = async () => {
try {
const constraints: MediaStreamConstraints = {
audio: options.deviceId ? { deviceId: { exact: options.deviceId } } : true,
video: false,
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
if (!mounted) {
stream.getTracks().forEach((track) => track.stop());
return;
}
streamRef.current = stream;
const audioContext = new AudioContext();
if (audioContext.state === "suspended") {
await audioContext.resume();
}
audioContextRef.current = audioContext;
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
analyser.smoothingTimeConstant = options.smoothingFactor ?? 0.8;
analyserRef.current = analyser;
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const updateLevel = () => {
if (!mounted || !analyserRef.current) return;
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for (let index = 0; index < dataArray.length; index++) {
sum += dataArray[index] * dataArray[index];
}
const rms = Math.sqrt(sum / dataArray.length);
const normalizedLevel = Math.min(100, (rms / 255) * 100 * 2);
setLevel(normalizedLevel);
animationFrameRef.current = requestAnimationFrame(updateLevel);
};
updateLevel();
} catch (error) {
console.error("Error starting audio level monitoring:", error);
if (mounted) {
setLevel(0);
}
}
};
void startMonitoring();
return () => {
mounted = false;
cleanup();
};
}, [options.deviceId, options.enabled, options.smoothingFactor]);
return { level };
}