import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react"; import { AlertCircle, LoaderCircle, Pause, Play } from "lucide-react"; import type { CanvasTheme } from "@/lib/canvas-theme"; import { useCanvasNodeActions } from "./canvas-node-action-context"; import { getCanvasAudioPlaybackSnapshot, seekCanvasAudio, stopCanvasAudio, subscribeCanvasAudioNode, toggleCanvasAudio } from "@/services/canvas-audio-playback"; import type { CanvasNodeData } from "@/types/canvas"; type CanvasAudioPlayerProps = { node: CanvasNodeData; theme: CanvasTheme; }; export function CanvasAudioPlayer({ node, theme }: CanvasAudioPlayerProps) { const { updateMetadata } = useCanvasNodeActions(); const subscribe = useCallback((listener: () => void) => subscribeCanvasAudioNode(node.id, listener), [node.id]); const getSnapshot = useCallback(() => getCanvasAudioPlaybackSnapshot(node.id), [node.id]); const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); const source = useMemo( () => ({ nodeId: node.id, content: node.metadata?.content || "", storageKey: node.metadata?.storageKey, mimeType: node.metadata?.mimeType, durationMs: node.metadata?.durationMs, }), [node.id, node.metadata?.content, node.metadata?.durationMs, node.metadata?.mimeType, node.metadata?.storageKey], ); const waveform = useMemo(() => createStableWaveform(node.id), [node.id]); const syncedDurationRef = useRef(0); const durationMs = snapshot.durationMs || node.metadata?.durationMs || 0; const progress = durationMs > 0 ? Math.min(1, Math.max(0, snapshot.currentTimeMs / durationMs)) : 0; useEffect(() => { if (!snapshot.durationMs || snapshot.durationMs === node.metadata?.durationMs || syncedDurationRef.current === snapshot.durationMs) return; syncedDurationRef.current = snapshot.durationMs; updateMetadata?.(node.id, { durationMs: snapshot.durationMs }); }, [node.id, node.metadata?.durationMs, snapshot.durationMs, updateMetadata]); useEffect(() => () => stopCanvasAudio(node.id), [node.id]); const stopCanvasEvent = (event: React.SyntheticEvent) => event.stopPropagation(); const togglePlayback = () => { void toggleCanvasAudio(source); }; const seek = (event: React.ChangeEvent) => { seekCanvasAudio(node.id, Number(event.target.value), source); }; const label = node.title || "音频"; const isPlaying = snapshot.phase === "playing"; const isLoading = snapshot.phase === "loading"; const isError = snapshot.phase === "error"; return (
{formatAudioTime(snapshot.currentTimeMs)}/{durationMs ? formatAudioTime(durationMs) : "--:--"}
{isError ? (
) : null}
); } function createStableWaveform(nodeId: string) { let seed = 2166136261; for (let index = 0; index < nodeId.length; index += 1) { seed ^= nodeId.charCodeAt(index); seed = Math.imul(seed, 16777619); } return Array.from({ length: 36 }, (_, index) => { seed = Math.imul(seed ^ index, 16777619); const normalized = ((seed >>> 0) % 100) / 100; return 0.28 + normalized * 0.62; }); } function formatAudioTime(timeMs: number) { const totalSeconds = Math.max(0, Math.floor(timeMs / 1000)); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes}:${seconds.toString().padStart(2, "0")}`; }