mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-30 01:36:04 +00:00
- 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
107 lines
2.8 KiB
TypeScript
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 };
|
|
}
|