| import { getServerTime } from '@/lib/clockSync' |
| import { PLAYER_PLAY_DEDUP_MS } from '@/lib/constants' |
| import { storage } from '@/lib/storage' |
| import { useSocketContext } from '@/providers/socket-context' |
| import { usePlayerStore } from '@/stores/playerStore' |
| import { useRoomStore } from '@/stores/roomStore' |
| import type { ScheduledPlayState, Track } from '@music-together/shared' |
| import { EVENTS } from '@music-together/shared' |
| import { useCallback, useEffect, useRef } from 'react' |
| import { useHowl } from './useHowl' |
| import { useLyric } from './useLyric' |
| import { useMediaSession } from './useMediaSession' |
| import { usePlayerSync } from './usePlayerSync' |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function usePlayer() { |
| const { socket } = useSocketContext() |
| const loadingRef = useRef<{ trackId: string; ts: number; serverTimestamp: number } | null>(null) |
| |
| |
| |
| const recoveredTrackIdRef = useRef<string | null>(null) |
|
|
| const next = useCallback(() => socket.emit(EVENTS.PLAYER_NEXT), [socket]) |
|
|
| |
| |
| |
| const autoNext = useCallback(() => { |
| const { room } = useRoomStore.getState() |
| const myId = storage.getUserId() |
| if (room?.hostId === myId) { |
| socket.emit(EVENTS.PLAYER_NEXT) |
| } |
| }, [socket]) |
|
|
| const { |
| howlRef, |
| soundIdRef, |
| loadTrack, |
| schedulePlayback, |
| cancelScheduledPlayback, |
| pausePlayback, |
| stopPlayback, |
| setPlaybackTempo, |
| } = useHowl(autoNext) |
| const { fetchLyric } = useLyric() |
|
|
| |
| usePlayerSync(howlRef, soundIdRef, schedulePlayback, cancelScheduledPlayback, pausePlayback, setPlaybackTempo) |
|
|
| |
| useEffect(() => { |
| const onDisconnect = () => { |
| loadingRef.current = null |
| recoveredTrackIdRef.current = null |
| } |
| socket.on('disconnect', onDisconnect) |
| return () => { |
| socket.off('disconnect', onDisconnect) |
| } |
| }, [socket]) |
|
|
| |
| useEffect(() => { |
| const onPlayerPlay = (data: { track: Track; playState: ScheduledPlayState }) => { |
| |
| |
| |
| |
| const now = Date.now() |
| if ( |
| loadingRef.current?.trackId === data.track.id && |
| loadingRef.current.serverTimestamp === data.playState.serverTimestamp && |
| now - loadingRef.current.ts < PLAYER_PLAY_DEDUP_MS |
| ) { |
| return |
| } |
| |
| |
| |
| |
| |
| if (recoveredTrackIdRef.current === data.track.id) { |
| recoveredTrackIdRef.current = null |
| loadingRef.current = { trackId: data.track.id, ts: now, serverTimestamp: data.playState.serverTimestamp } |
| useRoomStore.getState().updateRoom({ |
| currentTrack: data.track, |
| playState: { |
| isPlaying: data.playState.isPlaying, |
| currentTime: data.playState.currentTime, |
| serverTimestamp: data.playState.serverTimestamp, |
| }, |
| }) |
| if (data.playState.isPlaying) { |
| schedulePlayback(data.playState.currentTime, data.playState.serverTimeToExecute, () => { |
| fetchLyric(data.track) |
| }) |
| } |
| return |
| } |
| loadingRef.current = { trackId: data.track.id, ts: now, serverTimestamp: data.playState.serverTimestamp } |
|
|
| |
| useRoomStore.getState().updateRoom({ |
| currentTrack: data.track, |
| playState: { |
| isPlaying: data.playState.isPlaying, |
| currentTime: data.playState.currentTime, |
| serverTimestamp: data.playState.serverTimestamp, |
| }, |
| }) |
|
|
| |
| |
| |
| loadTrack(data.track, data.playState.currentTime, data.playState.isPlaying) |
| if (data.playState.isPlaying) { |
| schedulePlayback(data.playState.currentTime, data.playState.serverTimeToExecute, () => { |
| fetchLyric(data.track) |
| }) |
| } else { |
| fetchLyric(data.track) |
| } |
| } |
|
|
| socket.on(EVENTS.PLAYER_PLAY, onPlayerPlay) |
|
|
| return () => { |
| socket.off(EVENTS.PLAYER_PLAY, onPlayerPlay) |
| } |
| }, [socket, loadTrack, fetchLyric, schedulePlayback]) |
|
|
| useEffect(() => { |
| const onTrackMetadataUpdated = (data: { track: Track }) => { |
| const currentTrack = usePlayerStore.getState().currentTrack |
| if (currentTrack?.id !== data.track.id) return |
| usePlayerStore.getState().setCurrentTrack(data.track) |
| useRoomStore.getState().updateRoom({ currentTrack: data.track }) |
| fetchLyric(data.track) |
| } |
|
|
| socket.on(EVENTS.PLAYER_TRACK_METADATA_UPDATED, onTrackMetadataUpdated) |
| return () => socket.off(EVENTS.PLAYER_TRACK_METADATA_UPDATED, onTrackMetadataUpdated) |
| }, [socket, fetchLyric]) |
|
|
| |
| |
| useEffect(() => { |
| let hasRecovered = false |
|
|
| const recover = () => { |
| const { room } = useRoomStore.getState() |
|
|
| |
| if (!room) { |
| hasRecovered = false |
| return |
| } |
|
|
| if (hasRecovered) return |
| const playerTrack = usePlayerStore.getState().currentTrack |
| const roomTrack = room.currentTrack |
|
|
| |
| if (!roomTrack && playerTrack) { |
| hasRecovered = true |
| stopPlayback() |
| usePlayerStore.getState().reset() |
| return |
| } |
|
|
| |
| if (roomTrack?.streamUrl && (!playerTrack || !howlRef.current)) { |
| |
| |
| |
| |
| |
| |
| if (loadingRef.current?.trackId === roomTrack.id && howlRef.current) return |
|
|
| hasRecovered = true |
| const ps = room.playState |
| const elapsed = ps.isPlaying ? (getServerTime() - ps.serverTimestamp) / 1000 : 0 |
| const recoveredTime = ps.currentTime + Math.max(0, elapsed) |
| recoveredTrackIdRef.current = roomTrack.id |
| loadTrack(roomTrack, recoveredTime, ps.isPlaying) |
| if (ps.isPlaying) { |
| schedulePlayback(recoveredTime, getServerTime(), () => fetchLyric(roomTrack)) |
| } else { |
| fetchLyric(roomTrack) |
| } |
| } |
| } |
|
|
| |
| recover() |
|
|
| |
| const unsubscribe = useRoomStore.subscribe(recover) |
| return unsubscribe |
| |
| |
| }, [loadTrack, fetchLyric, schedulePlayback, stopPlayback]) |
|
|
| |
| |
| |
| |
| const play = useCallback(() => { |
| socket.emit(EVENTS.PLAYER_PLAY) |
| }, [socket]) |
|
|
| const pause = useCallback(() => { |
| socket.emit(EVENTS.PLAYER_PAUSE) |
| }, [socket]) |
|
|
| const seek = useCallback( |
| (time: number) => { |
| |
| usePlayerStore.getState().setCurrentTime(time) |
| socket.emit(EVENTS.PLAYER_SEEK, { currentTime: time }) |
| }, |
| [socket], |
| ) |
|
|
| const prev = useCallback(() => socket.emit(EVENTS.PLAYER_PREV), [socket]) |
|
|
| |
| |
| useMediaSession({ play, pause, next, prev, seek }) |
|
|
| return { play, pause, seek, next, prev } |
| } |
|
|