feat: build interactive documentary page with audio player and chapter navigation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
124
apps/web/src/hooks/useAudioPlayer.ts
Normal file
124
apps/web/src/hooks/useAudioPlayer.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { Howl } from "howler";
|
||||
|
||||
interface AudioTrack {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export function useAudioPlayer() {
|
||||
const [tracks, setTracks] = useState<AudioTrack[]>([]);
|
||||
const [currentTrackIndex, setCurrentTrackIndex] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [continuousMode, setContinuousMode] = useState(false);
|
||||
const howlRef = useRef<Howl | null>(null);
|
||||
const animFrameRef = useRef<number>(0);
|
||||
|
||||
const currentTrack = tracks[currentTrackIndex] ?? null;
|
||||
|
||||
const destroyHowl = useCallback(() => {
|
||||
if (howlRef.current) {
|
||||
howlRef.current.unload();
|
||||
howlRef.current = null;
|
||||
}
|
||||
cancelAnimationFrame(animFrameRef.current);
|
||||
}, []);
|
||||
|
||||
const loadTrack = useCallback(
|
||||
(index: number) => {
|
||||
if (!tracks[index]) return;
|
||||
destroyHowl();
|
||||
|
||||
const howl = new Howl({
|
||||
src: [tracks[index].url],
|
||||
html5: true,
|
||||
rate: playbackRate,
|
||||
onplay: () => {
|
||||
setIsPlaying(true);
|
||||
const updateProgress = () => {
|
||||
if (howl.playing()) {
|
||||
setProgress(howl.seek() as number);
|
||||
animFrameRef.current = requestAnimationFrame(updateProgress);
|
||||
}
|
||||
};
|
||||
animFrameRef.current = requestAnimationFrame(updateProgress);
|
||||
},
|
||||
onpause: () => setIsPlaying(false),
|
||||
onstop: () => setIsPlaying(false),
|
||||
onend: () => {
|
||||
setIsPlaying(false);
|
||||
if (continuousMode && index < tracks.length - 1) {
|
||||
setCurrentTrackIndex(index + 1);
|
||||
}
|
||||
},
|
||||
onload: () => {
|
||||
setDuration(howl.duration());
|
||||
},
|
||||
});
|
||||
|
||||
howlRef.current = howl;
|
||||
setCurrentTrackIndex(index);
|
||||
setProgress(0);
|
||||
},
|
||||
[tracks, playbackRate, continuousMode, destroyHowl]
|
||||
);
|
||||
|
||||
const play = useCallback(() => howlRef.current?.play(), []);
|
||||
const pause = useCallback(() => howlRef.current?.pause(), []);
|
||||
const toggle = useCallback(() => {
|
||||
if (isPlaying) pause();
|
||||
else play();
|
||||
}, [isPlaying, play, pause]);
|
||||
|
||||
const seek = useCallback((seconds: number) => {
|
||||
howlRef.current?.seek(seconds);
|
||||
setProgress(seconds);
|
||||
}, []);
|
||||
|
||||
const changeRate = useCallback(
|
||||
(rate: number) => {
|
||||
setPlaybackRate(rate);
|
||||
howlRef.current?.rate(rate);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const goToTrack = useCallback(
|
||||
(index: number) => {
|
||||
loadTrack(index);
|
||||
setTimeout(() => howlRef.current?.play(), 100);
|
||||
},
|
||||
[loadTrack]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => destroyHowl();
|
||||
}, [destroyHowl]);
|
||||
|
||||
return {
|
||||
tracks,
|
||||
setTracks,
|
||||
currentTrack,
|
||||
currentTrackIndex,
|
||||
isPlaying,
|
||||
progress,
|
||||
duration,
|
||||
playbackRate,
|
||||
continuousMode,
|
||||
setContinuousMode,
|
||||
loadTrack,
|
||||
play,
|
||||
pause,
|
||||
toggle,
|
||||
seek,
|
||||
changeRate,
|
||||
goToTrack,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user