@@ -33,6 +33,8 @@ export function GroupMusicProvider({ children }) {
3333 const [ currentSong , setCurrentSong ] = useState ( null )
3434 const [ volume , setVolume ] = useState ( 0.7 )
3535 const [ isLoading , setIsLoading ] = useState ( false )
36+ const [ isSyncing , setIsSyncing ] = useState ( false )
37+ const [ syncCountdown , setSyncCountdown ] = useState ( 0 )
3638
3739 // Search state
3840 const [ searchResults , setSearchResults ] = useState ( [ ] )
@@ -60,6 +62,17 @@ export function GroupMusicProvider({ children }) {
6062 const periodicSyncRef = useRef ( null )
6163 const hasAttemptedRejoinRef = useRef ( false )
6264 const audioRef = useRef ( null )
65+ const syncTimerRef = useRef ( null )
66+
67+ const isPlayingRef = useRef ( isPlaying )
68+ const currentSongRef = useRef ( currentSong )
69+ const currentGroupRef = useRef ( currentGroup )
70+ const serverTimeOffsetRef = useRef ( serverTimeOffset )
71+
72+ useEffect ( ( ) => { isPlayingRef . current = isPlaying } , [ isPlaying ] )
73+ useEffect ( ( ) => { currentSongRef . current = currentSong } , [ currentSong ] )
74+ useEffect ( ( ) => { currentGroupRef . current = currentGroup } , [ currentGroup ] )
75+ useEffect ( ( ) => { serverTimeOffsetRef . current = serverTimeOffset } , [ serverTimeOffset ] )
6376
6477 // Derived state
6578 const currentQueueItem = useMemo ( ( ) => {
@@ -438,12 +451,21 @@ export function GroupMusicProvider({ children }) {
438451
439452 if ( audioRef . current ) {
440453 audioRef . current . pause ( )
441- audioRef . current . currentTime = 0
454+ audioRef . current . removeAttribute ( 'src' )
455+ audioRef . current . load ( )
456+ }
457+
458+ if ( syncTimerRef . current ) {
459+ clearInterval ( syncTimerRef . current )
460+ syncTimerRef . current = null
442461 }
443462
444463 setCurrentGroup ( null )
464+ currentGroupRef . current = null
445465 setCurrentSong ( null )
446466 setIsPlaying ( false )
467+ setIsSyncing ( false )
468+ setSyncCountdown ( 0 )
447469 setMessages ( [ ] )
448470 setGroupMembers ( [ ] )
449471 setQueue ( [ ] )
@@ -556,7 +578,7 @@ export function GroupMusicProvider({ children }) {
556578 }
557579
558580 // If we're playing a different song or no song, load the correct one
559- if ( ! currentSong || currentSong . id !== serverTrack . id ) {
581+ if ( ! currentSongRef . current || currentSongRef . current . id !== serverTrack . id ) {
560582 console . log ( "Out of sync - loading correct song" )
561583 setCurrentSong ( serverTrack )
562584 const url = serverTrack . download_url ?. find ( ( u ) => u . quality === "320kbps" ) ?. link
@@ -568,7 +590,7 @@ export function GroupMusicProvider({ children }) {
568590 if ( ! audioRef . current ) return
569591
570592 // Drift correction
571- const serverNow = Date . now ( ) + serverTimeOffset
593+ const serverNow = Date . now ( ) + serverTimeOffsetRef . current
572594 const timePassed = ( serverNow - playbackState . lastUpdate ) / 1000
573595 const expectedTime = playbackState . currentTime + ( playbackState . isPlaying ? timePassed : 0 )
574596 const actualTime = audioRef . current . currentTime
@@ -643,23 +665,65 @@ export function GroupMusicProvider({ children }) {
643665 setLastSync ( serverNow )
644666 } )
645667
646- socket . on ( "music-update" , async ( { song, currentTime, queueItem, autoPlay } ) => {
668+ socket . on ( "music-update" , async ( { song, currentTime, queueItem, autoPlay, scheduledPlayTime } ) => {
669+ if ( syncTimerRef . current ) clearInterval ( syncTimerRef . current )
670+
647671 setCurrentSong ( song )
648- const url = song . download_url . find ( ( url ) => url . quality === "320kbps" ) ?. link
672+ currentSongRef . current = song
673+ const url = song . download_url . find ( ( u ) => u . quality === "320kbps" ) ?. link
674+
675+ if ( scheduledPlayTime ) {
676+ setIsSyncing ( true )
677+ const serverNow = Date . now ( ) + serverTimeOffsetRef . current
678+ const totalDelay = Math . max ( 0 , scheduledPlayTime - serverNow )
679+ setSyncCountdown ( Math . ceil ( totalDelay / 1000 ) )
680+
681+ syncTimerRef . current = setInterval ( ( ) => {
682+ const remaining = Math . max ( 0 , scheduledPlayTime - ( Date . now ( ) + serverTimeOffsetRef . current ) )
683+ const secs = Math . ceil ( remaining / 1000 )
684+ setSyncCountdown ( secs )
685+ if ( secs <= 0 ) {
686+ clearInterval ( syncTimerRef . current )
687+ syncTimerRef . current = null
688+ }
689+ } , 200 )
649690
650- if ( url ) {
651- await loadAudio ( url , queueItem ?. id )
691+ if ( url ) {
692+ await loadAudio ( url , queueItem ?. id )
693+ }
652694
653- if ( audioRef . current ) {
654- audioRef . current . currentTime = currentTime
655- if ( autoPlay || isPlaying ) {
695+ const nowAfterLoad = Date . now ( ) + serverTimeOffsetRef . current
696+ const remainingDelay = Math . max ( 0 , scheduledPlayTime - nowAfterLoad )
697+
698+ setTimeout ( async ( ) => {
699+ if ( ! currentGroupRef . current ) return ; // Prevent playing if user left
700+
701+ setIsSyncing ( false )
702+ setSyncCountdown ( 0 )
703+ if ( audioRef . current && audioRef . current . src ) {
704+ audioRef . current . currentTime = currentTime || 0
656705 try {
657706 await audioRef . current . play ( )
658707 setIsPlaying ( true )
659708 } catch ( err ) {
660709 console . error ( "Autoplay blocked:" , err )
661710 }
662711 }
712+ } , remainingDelay )
713+ } else {
714+ if ( url ) {
715+ await loadAudio ( url , queueItem ?. id )
716+ if ( audioRef . current ) {
717+ audioRef . current . currentTime = currentTime
718+ if ( autoPlay || isPlayingRef . current ) {
719+ try {
720+ await audioRef . current . play ( )
721+ setIsPlaying ( true )
722+ } catch ( err ) {
723+ console . error ( "Autoplay blocked:" , err )
724+ }
725+ }
726+ }
663727 }
664728 }
665729
@@ -810,9 +874,22 @@ export function GroupMusicProvider({ children }) {
810874 } )
811875
812876 socket . on ( "group-disbanded" , ( ) => {
877+ if ( audioRef . current ) {
878+ audioRef . current . pause ( )
879+ audioRef . current . removeAttribute ( 'src' )
880+ audioRef . current . load ( )
881+ }
882+ if ( syncTimerRef . current ) {
883+ clearInterval ( syncTimerRef . current )
884+ syncTimerRef . current = null
885+ }
886+
813887 setCurrentGroup ( null )
888+ currentGroupRef . current = null
814889 setCurrentSong ( null )
815890 setIsPlaying ( false )
891+ setIsSyncing ( false )
892+ setSyncCountdown ( 0 )
816893 setMessages ( [ ] )
817894 setGroupMembers ( [ ] )
818895 setQueue ( [ ] )
@@ -859,16 +936,7 @@ export function GroupMusicProvider({ children }) {
859936 socket . off ( "group-full" )
860937 socket . off ( "feature-locked" )
861938 }
862- } , [
863- socket ,
864- isPlaying ,
865- serverTimeOffset ,
866- user ,
867- loadAudio ,
868- saveSession ,
869- clearSession ,
870- getServerTime ,
871- ] )
939+ } , [ socket , user , loadAudio , saveSession , clearSession , getServerTime ] )
872940
873941 const contextValue = {
874942 // Group state
@@ -901,6 +969,8 @@ export function GroupMusicProvider({ children }) {
901969 setVolume,
902970 isLoading,
903971 setIsLoading,
972+ isSyncing,
973+ syncCountdown,
904974
905975 // Search state
906976 searchResults,
0 commit comments