- "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
81 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|