Files
OpenCut/apps/web/src/components/ui/audio-player.tsx
T

133 lines
3.5 KiB
TypeScript
Raw Normal View History

2025-11-26 08:47:03 +01:00
"use client";
2026-01-15 11:05:17 +01:00
import { useEditor } from "@/hooks/use-editor";
2025-11-26 08:47:03 +01:00
import { useRef, useEffect } from "react";
interface AudioPlayerProps {
src: string;
className?: string;
clipStartTime: number;
trimStart: number;
trimEnd: number;
clipDuration: number;
trackMuted?: boolean;
}
export function AudioPlayer({
src,
className = "",
clipStartTime,
trimStart,
trimEnd,
clipDuration,
trackMuted = false,
}: AudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null);
2026-01-15 11:05:17 +01:00
const editor = useEditor();
const currentTime = editor.playback.getCurrentTime();
const isPlaying = editor.playback.getIsPlaying();
const volume = editor.playback.getVolume();
const muted = editor.playback.isMuted();
const speed = editor.playback.getSpeed();
2025-11-26 08:47:03 +01:00
// Calculate if we're within this clip's timeline range
const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd);
const isInClipRange =
currentTime >= clipStartTime && currentTime < clipEndTime;
// Sync playback events
useEffect(() => {
const audio = audioRef.current;
if (!audio || !isInClipRange) return;
const handleSeekEvent = (e: CustomEvent) => {
// Always update audio time, even if outside clip range
const timelineTime = e.detail.time;
const audioTime = Math.max(
trimStart,
Math.min(
clipDuration - trimEnd,
2026-01-15 11:05:17 +01:00
timelineTime - clipStartTime + trimStart,
),
2025-11-26 08:47:03 +01:00
);
audio.currentTime = audioTime;
};
const handleUpdateEvent = (e: CustomEvent) => {
// Always update audio time, even if outside clip range
const timelineTime = e.detail.time;
const targetTime = Math.max(
trimStart,
Math.min(
clipDuration - trimEnd,
2026-01-15 11:05:17 +01:00
timelineTime - clipStartTime + trimStart,
),
2025-11-26 08:47:03 +01:00
);
if (Math.abs(audio.currentTime - targetTime) > 0.5) {
audio.currentTime = targetTime;
}
};
const handleSpeed = (e: CustomEvent) => {
audio.playbackRate = e.detail.speed;
};
window.addEventListener("playback-seek", handleSeekEvent as EventListener);
window.addEventListener(
"playback-update",
2026-01-15 11:05:17 +01:00
handleUpdateEvent as EventListener,
2025-11-26 08:47:03 +01:00
);
window.addEventListener("playback-speed", handleSpeed as EventListener);
return () => {
window.removeEventListener(
"playback-seek",
2026-01-15 11:05:17 +01:00
handleSeekEvent as EventListener,
2025-11-26 08:47:03 +01:00
);
window.removeEventListener(
"playback-update",
2026-01-15 11:05:17 +01:00
handleUpdateEvent as EventListener,
2025-11-26 08:47:03 +01:00
);
window.removeEventListener(
"playback-speed",
2026-01-15 11:05:17 +01:00
handleSpeed as EventListener,
2025-11-26 08:47:03 +01:00
);
};
}, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]);
// Sync playback state
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying && isInClipRange && !trackMuted) {
audio.play().catch(() => {});
} else {
audio.pause();
}
}, [isPlaying, isInClipRange, trackMuted]);
// Sync volume and speed
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
audio.volume = volume;
audio.muted = muted || trackMuted;
audio.playbackRate = speed;
}, [volume, speed, muted, trackMuted]);
return (
<audio
ref={audioRef}
src={src}
className={className}
preload="auto"
controls={false}
style={{ display: "none" }} // Audio elements don't need visual representation
onContextMenu={(e) => e.preventDefault()}
/>
);
}