chrysopedia/frontend/src/components/AudioWaveform.tsx
jlightner 76880d0477 feat: Created ChapterMarkers overlay component, added RegionsPlugin cha…
- "frontend/src/components/ChapterMarkers.tsx"
- "frontend/src/components/PlayerControls.tsx"
- "frontend/src/components/AudioWaveform.tsx"
- "frontend/src/pages/WatchPage.tsx"
- "frontend/src/App.css"

GSD-Task: S05/T03
2026-04-04 05:53:19 +00:00

81 lines
2.3 KiB
TypeScript

import { useEffect, useRef } from "react";
import WaveSurfer from "wavesurfer.js";
import RegionsPlugin from "wavesurfer.js/dist/plugins/regions.esm.js";
import type { MediaSyncState } from "../hooks/useMediaSync";
import type { Chapter } from "../api/videos";
interface AudioWaveformProps {
mediaSync: MediaSyncState;
src: string;
chapters?: Chapter[];
}
/**
* Audio-only waveform visualiser powered by wavesurfer.js.
* Renders a hidden <audio> element owned by useMediaSync and an
* interactive waveform. Used when no video URL is available.
* Optionally displays chapter regions via the RegionsPlugin.
*/
export default function AudioWaveform({ mediaSync, src, chapters }: AudioWaveformProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const wsRef = useRef<WaveSurfer | null>(null);
useEffect(() => {
const container = containerRef.current;
const audio = mediaSync.videoRef.current as HTMLAudioElement | null;
if (!container || !audio) return;
const regions = RegionsPlugin.create();
const ws = WaveSurfer.create({
container,
media: audio,
height: 128,
waveColor: "rgba(34, 211, 238, 0.4)",
progressColor: "rgba(34, 211, 238, 0.8)",
cursorColor: "#22d3ee",
barWidth: 2,
barGap: 1,
barRadius: 2,
backend: "MediaElement",
plugins: [regions],
});
wsRef.current = ws;
// Add chapter regions once waveform is ready
if (chapters && chapters.length > 0) {
ws.on("ready", () => {
for (const ch of chapters) {
regions.addRegion({
start: ch.start_time,
end: ch.end_time,
content: ch.title,
color: "rgba(0, 255, 209, 0.1)",
drag: false,
resize: false,
});
}
});
}
return () => {
ws.destroy();
wsRef.current = null;
};
// Re-create when src or chapters change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src, chapters]);
return (
<div className="audio-waveform">
<audio
ref={mediaSync.videoRef as React.RefObject<HTMLAudioElement>}
src={src}
preload="metadata"
style={{ display: "none" }}
/>
<div ref={containerRef} className="audio-waveform__canvas" />
</div>
);
}