import { useState, useRef, useEffect, useCallback } from 'react'; import type { StorySegmentDto } from '../../../types/api/story'; interface StoryPlayerProps { segment: StorySegmentDto; autoPlay?: boolean; onPlay?: () => void; onPause?: () => void; onEnded?: () => void; } /** * StoryPlayer component - Handles audio playback for story segments. * Uses the HTML5 audio element to play audio files served from the backend. */ export function StoryPlayer({ segment, autoPlay = false, onPlay, onPause, onEnded, }: StoryPlayerProps) { const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(0.8); // Construct audio URL from segment audioUrl // Backend serves files from wwwroot, so URLs are like /audio/story/level1-segment1.wav const audioUrl = segment.audioUrl ? `${import.meta.env.VITE_API_URL || 'http://localhost:5000'}${segment.audioUrl}` : null; // Load audio metadata when component mounts or segment changes useEffect(() => { if (audioUrl && audioRef.current) { const audio = audioRef.current; const handleLoadedMetadata = () => { setDuration(audio.duration); setIsLoading(false); }; const handleError = () => { setError('Failed to load audio file'); setIsLoading(false); }; audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('error', handleError); // Load the audio source audio.src = audioUrl; audio.load(); return () => { audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('error', handleError); }; } }, [audioUrl]); // Handle audio events useEffect(() => { if (!audioRef.current) return; const audio = audioRef.current; const handlePlay = () => { setIsPlaying(true); onPlay?.(); }; const handlePause = () => { setIsPlaying(false); onPause?.(); }; const handleEnded = () => { setIsPlaying(false); setCurrentTime(0); onEnded?.(); }; const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); }; audio.addEventListener('play', handlePlay); audio.addEventListener('pause', handlePause); audio.addEventListener('ended', handleEnded); audio.addEventListener('timeupdate', handleTimeUpdate); return () => { audio.removeEventListener('play', handlePlay); audio.removeEventListener('pause', handlePause); audio.removeEventListener('ended', handleEnded); audio.removeEventListener('timeupdate', handleTimeUpdate); }; }, [onPlay, onPause, onEnded]); // Auto-play effect useEffect(() => { if (autoPlay && audioRef.current && !isPlaying && !isLoading) { audioRef.current.play().catch(() => setError('Audio playback failed')); } }, [autoPlay, isPlaying, isLoading]); const togglePlayPause = useCallback(() => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); } else { if (audioUrl) { audioRef.current.play().catch(() => setError('Audio playback failed')); } } }, [isPlaying, audioUrl]); const handleSeek = useCallback((e: React.ChangeEvent) => { if (!audioRef.current) return; const seekTime = parseFloat(e.target.value); audioRef.current.currentTime = seekTime; setCurrentTime(seekTime); }, []); const handleVolumeChange = useCallback((e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (audioRef.current) { audioRef.current.volume = newVolume; } }, []); const handlePrevious = useCallback(() => { // Previous segment logic would be handled by parent onEnded?.(); }, [onEnded]); const handleNext = useCallback(() => { // Next segment logic would be handled by parent onEnded?.(); }, [onEnded]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; if (!audioUrl) { return (

🎧 No audio available for this segment

); } if (error) { return (

⚠️ {error}

); } return (
{formatTime(currentTime)} {formatTime(duration)}
🔊
{/* Hidden audio element */}
); } export default StoryPlayer;