From ae3187804ea43a8a9096b3072c324bc27456be90 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 2 Mar 2026 14:28:20 +0800 Subject: [PATCH] Notes slider musik belum berfungsi --- src/app/darmasaba/(pages)/musik/lib/seek.ts | 14 +- .../(pages)/musik/lib/useAudioProgress.ts | 146 ++++++++++++ .../(pages)/musik/musik-desa/page.tsx | 217 ++++++++++++++---- 3 files changed, 329 insertions(+), 48 deletions(-) create mode 100644 src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts diff --git a/src/app/darmasaba/(pages)/musik/lib/seek.ts b/src/app/darmasaba/(pages)/musik/lib/seek.ts index d0852f55..e49f19d5 100644 --- a/src/app/darmasaba/(pages)/musik/lib/seek.ts +++ b/src/app/darmasaba/(pages)/musik/lib/seek.ts @@ -1,15 +1,19 @@ export function seekTo( - audioRef: React.RefObject, + audioRef: React.RefObject, time: number, setCurrentTime?: (v: number) => void ) { if (!audioRef.current) return; - + + // Validasi: jangan seek melebihi durasi atau negatif + const duration = audioRef.current.duration || 0; + const safeTime = Math.min(Math.max(0, time), duration); + // Set waktu audio - audioRef.current.currentTime = time; - + audioRef.current.currentTime = safeTime; + // Update state jika provided if (setCurrentTime) { - setCurrentTime(Math.round(time)); + setCurrentTime(Math.floor(safeTime)); } } diff --git a/src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts b/src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts new file mode 100644 index 00000000..28fde576 --- /dev/null +++ b/src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts @@ -0,0 +1,146 @@ +import { useRef, useEffect, useCallback } from 'react'; + +/** + * Custom hook untuk smooth audio progress update menggunakan requestAnimationFrame + * Lebih smooth dan reliable dibanding onTimeUpdate event + */ +export function useAudioProgress( + audioRef: React.RefObject, + isPlaying: boolean, + setCurrentTime: (time: number) => void, + isSeekingRef: React.RefObject, + lastSeekTimeRef?: React.RefObject +) { + const rafRef = useRef(null); + const lastTimeRef = useRef(0); + + const updateProgress = useCallback(() => { + if (!audioRef.current || audioRef.current.paused || isSeekingRef.current) { + rafRef.current = requestAnimationFrame(updateProgress); + return; + } + + const audio = audioRef.current; + const currentTime = Math.floor(audio.currentTime); + + // Hanya update state jika waktu berubah + if (currentTime !== lastTimeRef.current) { + lastTimeRef.current = currentTime; + setCurrentTime(currentTime); + } + + rafRef.current = requestAnimationFrame(updateProgress); + }, [audioRef, setCurrentTime, isSeekingRef]); + + useEffect(() => { + if (isPlaying) { + rafRef.current = requestAnimationFrame(updateProgress); + } else if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + return () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [isPlaying, updateProgress]); + + return rafRef; +} + +// 'use client' +// import { useEffect, useRef, useState, useCallback } from 'react'; + +// export function useAudioEngine() { +// const audioRef = useRef(null); +// const rafRef = useRef(null); +// const isSeekingRef = useRef(false); + +// const [isPlaying, setIsPlaying] = useState(false); +// const [currentTime, setCurrentTime] = useState(0); +// const [duration, setDuration] = useState(0); + +// const load = useCallback((src: string) => { +// if (!audioRef.current) return; +// audioRef.current.src = src; +// audioRef.current.load(); +// setCurrentTime(0); +// }, []); + +// const play = async () => { +// if (!audioRef.current) return; +// await audioRef.current.play(); +// setIsPlaying(true); +// }; + +// const pause = () => { +// if (!audioRef.current) return; +// audioRef.current.pause(); +// setIsPlaying(false); +// }; + +// const toggle = () => { +// if (!audioRef.current) return; +// audioRef.current.paused ? play() : pause(); +// }; + +// const seek = (time: number) => { +// if (!audioRef.current) return; +// isSeekingRef.current = true; +// audioRef.current.currentTime = time; +// setCurrentTime(time); +// requestAnimationFrame(() => { +// isSeekingRef.current = false; +// }); +// }; + +// useEffect(() => { +// if (!audioRef.current) return; +// const audio = audioRef.current; + +// const onLoaded = () => { +// setDuration(Math.floor(audio.duration)); +// }; + +// const onEnded = () => { +// setIsPlaying(false); +// setCurrentTime(0); +// }; + +// audio.addEventListener('loadedmetadata', onLoaded); +// audio.addEventListener('ended', onEnded); + +// return () => { +// audio.removeEventListener('loadedmetadata', onLoaded); +// audio.removeEventListener('ended', onEnded); +// }; +// }, []); + +// useEffect(() => { +// const loop = () => { +// if ( +// audioRef.current && +// !audioRef.current.paused && +// !isSeekingRef.current +// ) { +// setCurrentTime(Math.floor(audioRef.current.currentTime)); +// } +// rafRef.current = requestAnimationFrame(loop); +// }; +// rafRef.current = requestAnimationFrame(loop); +// return () => { +// if (rafRef.current) cancelAnimationFrame(rafRef.current); +// }; +// }, []); + +// return { +// audioRef, +// isPlaying, +// currentTime, +// duration, +// load, +// toggle, +// seek, +// }; +// } diff --git a/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx b/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx index 84ab18d3..cbee32c7 100644 --- a/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx +++ b/src/app/darmasaba/(pages)/musik/musik-desa/page.tsx @@ -6,9 +6,9 @@ import BackButton from '../../desa/layanan/_com/BackButto'; import { togglePlayPause } from '../lib/playPause'; import { getNextIndex, getPrevIndex } from '../lib/nextPrev'; import { handleRepeatOrNext } from '../lib/repeat'; -import { seekTo } from '../lib/seek'; import { toggleShuffle } from '../lib/shuffle'; import { setAudioVolume, toggleMute as toggleMuteUtil } from '../lib/volume'; +import { useAudioProgress } from '../lib/useAudioProgress'; interface MusicFile { id: string; @@ -44,8 +44,13 @@ const MusicPlayer = () => { const [musikData, setMusikData] = useState([]); const [loading, setLoading] = useState(true); const [currentSongIndex, setCurrentSongIndex] = useState(-1); - const [isSeeking, setIsSeeking] = useState(false); const audioRef = useRef(null); + const isSeekingRef = useRef(false); + const lastPlayedSongIdRef = useRef(null); + const lastSeekTimeRef = useRef(0); // Track last seek time + + // Smooth progress update dengan requestAnimationFrame + useAudioProgress(audioRef as React.RefObject, isPlaying, setCurrentTime, isSeekingRef, lastSeekTimeRef); // Fetch musik data from API useEffect(() => { @@ -102,26 +107,57 @@ const MusicPlayer = () => { // }; // }, [isPlaying]); - // Update duration when song changes + // Update duration when song changes (HANYA saat ganti lagu, bukan saat isPlaying berubah) + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (currentSong && audioRef.current) { - // Gunakan durasi dari database sebagai acuan utama - const durationParts = currentSong.durasi.split(':'); - const durationInSeconds = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]); - setDuration(durationInSeconds); - - // Reset audio currentTime ke 0 hanya untuk lagu baru - audioRef.current.currentTime = 0; - setCurrentTime(0); - - if (isPlaying) { - audioRef.current.play().catch(err => { - console.error('Error playing audio:', err); - setIsPlaying(false); - }); + // Cek apakah ini benar-benar lagu baru + const isNewSong = lastPlayedSongIdRef.current !== currentSong.id; + + if (isNewSong) { + // Gunakan durasi dari database sebagai acuan utama + const durationParts = currentSong.durasi.split(':'); + const durationInSeconds = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]); + setDuration(durationInSeconds); + + // Reset audio currentTime ke 0 untuk lagu baru + audioRef.current.currentTime = 0; + setCurrentTime(0); + + // Update ref + lastPlayedSongIdRef.current = currentSong.id; + + if (isPlaying) { + audioRef.current.play().catch(err => { + console.error('Error playing audio:', err); + setIsPlaying(false); + }); + } } + // Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0) } - }, [currentSong?.id, currentSongIndex]); + }, [currentSong?.id]); + + // Sync duration dari audio element jika berbeda signifikan (> 1 detik) + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + const audio = audioRef.current; + if (!audio || !currentSong) return; + + const handleLoadedMetadata = () => { + const audioDuration = Math.floor(audio.duration); + const durationParts = currentSong.durasi.split(':'); + const dbDuration = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]); + + // Jika perbedaan > 2 detik, gunakan audio duration (lebih akurat) + if (Math.abs(audioDuration - dbDuration) > 2) { + setDuration(audioDuration); + } + }; + + audio.addEventListener('loadedmetadata', handleLoadedMetadata); + return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata); + }, [currentSong?.id]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); @@ -202,24 +238,9 @@ const MusicPlayer = () => { {/* Hidden audio element - gunakan key yang stabil untuk mencegah remount */} {currentSong?.audioFile && (