@@ -785,12 +785,10 @@ func (s *Server) handleReconnect(c *Client, payload []byte) {
785785 }
786786
787787 // Calculate live position for reconnect state
788+ nowMs := time .Now ().UnixMilli ()
788789 liveState := * room .State
789- if liveState .IsPlaying {
790- elapsed := time .Now ().UnixMilli () - liveState .LastUpdate
791- liveState .Position += elapsed
792- }
793- liveState .LastUpdate = time .Now ().UnixMilli ()
790+ liveState .Position = livePlaybackPosition (room .State , nowMs )
791+ liveState .LastUpdate = nowMs
794792
795793 isHost := room .Host == c
796794
@@ -886,6 +884,30 @@ func sanitizeString(s string, maxLen int) string {
886884 return s
887885}
888886
887+ func livePlaybackPosition (state * RoomState , nowMs int64 ) int64 {
888+ if state == nil {
889+ return 0
890+ }
891+
892+ position := state .Position
893+ if position < 0 {
894+ position = 0
895+ }
896+
897+ if state .IsPlaying && state .LastUpdate > 0 {
898+ elapsed := nowMs - state .LastUpdate
899+ if elapsed > 0 {
900+ position += elapsed
901+ }
902+ }
903+
904+ if state .CurrentTrack != nil && state .CurrentTrack .Duration > 0 && position > state .CurrentTrack .Duration {
905+ return state .CurrentTrack .Duration
906+ }
907+
908+ return position
909+ }
910+
889911func (s * Server ) handleMessage (c * Client , data []byte ) {
890912 // Decode message using protobuf codec
891913 msgType , payloadBytes , err := c .codec .Decode (data )
@@ -1362,15 +1384,16 @@ func (s *Server) handleApproveJoin(c *Client, payload []byte) {
13621384
13631385 // If there is a current track, immediately send buffer-complete + seek (+ play if host is playing)
13641386 if room .State .CurrentTrack != nil {
1387+ syncPosition := livePlaybackPosition (room .State , time .Now ().UnixMilli ())
13651388 joiningClient .sendMessage (s .logger , MsgTypeBufferComplete , BufferCompletePayload {TrackID : room .State .CurrentTrack .ID })
13661389 joiningClient .sendMessage (s .logger , MsgTypeSyncPlayback , PlaybackActionPayload {
13671390 Action : ActionSeek ,
1368- Position : room . State . Position ,
1391+ Position : syncPosition ,
13691392 })
13701393 if room .State .IsPlaying {
13711394 joiningClient .sendMessage (s .logger , MsgTypeSyncPlayback , PlaybackActionPayload {
13721395 Action : ActionPlay ,
1373- Position : room . State . Position ,
1396+ Position : syncPosition ,
13741397 })
13751398 }
13761399 }
@@ -1470,10 +1493,6 @@ func (s *Server) handlePlaybackAction(c *Client, payload []byte) {
14701493 return
14711494 }
14721495
1473- // Update room state based on action
1474- prevLastUpdate := room .State .LastUpdate
1475- room .State .LastUpdate = time .Now ().UnixMilli ()
1476-
14771496 switch p .Action {
14781497 case ActionPlay :
14791498 // Block play if no track is set
@@ -1482,21 +1501,44 @@ func (s *Server) handlePlaybackAction(c *Client, payload []byte) {
14821501 c .sendError (s .logger , "no_track" , "Cannot play without a track" )
14831502 return
14841503 }
1504+ if p .Position < 0 {
1505+ c .sendError (s .logger , "invalid_position" , "Position cannot be negative" )
1506+ return
1507+ }
1508+ nowMs := time .Now ().UnixMilli ()
1509+ if room .State .CurrentTrack != nil && room .State .CurrentTrack .Duration > 0 && p .Position > room .State .CurrentTrack .Duration {
1510+ p .Position = room .State .CurrentTrack .Duration
1511+ }
14851512 room .State .IsPlaying = true
14861513 room .State .Position = p .Position
1487- p .ServerTime = time .Now ().UnixMilli ()
1514+ room .State .LastUpdate = nowMs
1515+ p .ServerTime = nowMs
14881516
14891517 case ActionPause :
14901518 // Pause is always allowed
1519+ if p .Position < 0 {
1520+ c .sendError (s .logger , "invalid_position" , "Position cannot be negative" )
1521+ return
1522+ }
1523+ nowMs := time .Now ().UnixMilli ()
1524+ if room .State .CurrentTrack != nil && room .State .CurrentTrack .Duration > 0 && p .Position > room .State .CurrentTrack .Duration {
1525+ p .Position = room .State .CurrentTrack .Duration
1526+ }
14911527 room .State .IsPlaying = false
14921528 room .State .Position = p .Position
1529+ room .State .LastUpdate = nowMs
14931530
14941531 case ActionSeek :
14951532 if p .Position < 0 {
14961533 c .sendError (s .logger , "invalid_position" , "Position cannot be negative" )
14971534 return
14981535 }
1536+ nowMs := time .Now ().UnixMilli ()
1537+ if room .State .CurrentTrack != nil && room .State .CurrentTrack .Duration > 0 && p .Position > room .State .CurrentTrack .Duration {
1538+ p .Position = room .State .CurrentTrack .Duration
1539+ }
14991540 room .State .Position = p .Position
1541+ room .State .LastUpdate = nowMs
15001542
15011543 case ActionChangeTrack :
15021544 if p .TrackInfo == nil {
@@ -1525,6 +1567,7 @@ func (s *Server) handlePlaybackAction(c *Client, payload []byte) {
15251567 room .State .CurrentTrack = p .TrackInfo
15261568 room .State .Position = 0
15271569 room .State .IsPlaying = false
1570+ room .State .LastUpdate = time .Now ().UnixMilli ()
15281571
15291572 // For new tracks, always start at position 0
15301573 room .HostStartPosition = 0
@@ -1570,6 +1613,7 @@ func (s *Server) handlePlaybackAction(c *Client, payload []byte) {
15701613
15711614 case ActionSkipNext , ActionSkipPrev :
15721615 room .State .Position = 0
1616+ room .State .LastUpdate = time .Now ().UnixMilli ()
15731617
15741618 case ActionQueueAdd :
15751619 if p .TrackInfo == nil {
@@ -1660,7 +1704,6 @@ func (s *Server) handlePlaybackAction(c *Client, payload []byte) {
16601704 return
16611705 }
16621706 room .State .Volume = p .Volume
1663- room .State .LastUpdate = prevLastUpdate
16641707
16651708 default :
16661709 c .sendError (s .logger , "unknown_action" , fmt .Sprintf ("Unknown action: %s" , p .Action ))
@@ -1712,16 +1755,17 @@ func (s *Server) handleBufferReady(c *Client, payload []byte) {
17121755 // If buffering is disabled for this room, respond per-client so late buffer_ready still receives SEEK/PLAY
17131756 if room .BufferingUsers == nil {
17141757 s .logger .Debug ("Buffering disabled for room - per-client ACK" , zap .String ("room_code" , room .Code ), zap .String ("user_id" , c .ID ))
1758+ syncPosition := livePlaybackPosition (room .State , time .Now ().UnixMilli ())
17151759 // Send buffer-complete and sync to this specific client so they will apply seek/play
17161760 c .sendMessage (s .logger , MsgTypeBufferComplete , BufferCompletePayload {TrackID : p .TrackID })
17171761 c .sendMessage (s .logger , MsgTypeSyncPlayback , PlaybackActionPayload {
17181762 Action : ActionSeek ,
1719- Position : room . State . Position ,
1763+ Position : syncPosition ,
17201764 })
17211765 if room .State .IsPlaying {
17221766 c .sendMessage (s .logger , MsgTypeSyncPlayback , PlaybackActionPayload {
17231767 Action : ActionPlay ,
1724- Position : room . State . Position ,
1768+ Position : syncPosition ,
17251769 })
17261770 }
17271771 return
@@ -1964,10 +2008,11 @@ func (s *Server) handleRequestSync(c *Client) {
19642008 defer room .mu .RUnlock ()
19652009
19662010 // Calculate live position
1967- currentPosition := room .State .Position
1968- elapsed := time .Now ().UnixMilli () - room .State .LastUpdate
1969- if room .State .IsPlaying {
1970- currentPosition += elapsed
2011+ nowMs := time .Now ().UnixMilli ()
2012+ currentPosition := livePlaybackPosition (room .State , nowMs )
2013+ elapsed := int64 (0 )
2014+ if room .State .IsPlaying && currentPosition > room .State .Position {
2015+ elapsed = currentPosition - room .State .Position
19712016 }
19722017
19732018 responsePlaying := room .State .IsPlaying
@@ -1985,7 +2030,7 @@ func (s *Server) handleRequestSync(c *Client) {
19852030 CurrentTrack : room .State .CurrentTrack ,
19862031 IsPlaying : responsePlaying ,
19872032 Position : currentPosition ,
1988- LastUpdate : time . Now (). UnixMilli () ,
2033+ LastUpdate : nowMs ,
19892034 Volume : room .State .Volume ,
19902035 })
19912036}
0 commit comments