Files
project-afterlife/apps/web/src/hooks/useAudioPlayer.ts
2026-02-22 04:07:22 +00:00

125 lines
3.0 KiB
TypeScript

"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,
};
}