import type { Dispatch } from 'react'; import type ReactPlayer from 'react-player'; import { useCallback, useRef, useState } from 'react'; import { OnProgressProps } from 'react-player/base'; import { WebPlayerEngine, WebPlayerEngineHandle } from './engine/web-player-engine'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/listener/use-player-events'; import { usePlayerActions, usePlayerData, usePlayerMuted, usePlayerProperties, usePlayerVolume, } from '/@/renderer/store'; import { PlayerStatus, PlayerStyle } from '/@/shared/types/types'; const PLAY_PAUSE_FADE_DURATION = 300; const PLAY_PAUSE_FADE_INTERVAL = 10; export function WebPlayer() { const playerRef = useRef(null); const { player, player1, player2 } = usePlayerData(); const { mediaAutoNext, setProgress } = usePlayerActions(); const { crossfadeDuration, speed, transitionType } = usePlayerProperties(); const isMuted = usePlayerMuted(); const volume = usePlayerVolume(); const [localPlayerStatus, setLocalPlayerStatus] = useState(player.status); const [isTransitioning, setIsTransitioning] = useState(false); const fadeAndSetStatus = useCallback( async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => { if (isTransitioning) { return setLocalPlayerStatus(status); } const steps = duration / PLAY_PAUSE_FADE_INTERVAL; const volumeStep = (endVolume - startVolume) / steps; let currentStep = 0; const promise = new Promise((resolve) => { const interval = setInterval(() => { currentStep++; const newVolume = startVolume + volumeStep * currentStep; playerRef.current?.setVolume(newVolume); if (currentStep >= steps) { clearInterval(interval); setIsTransitioning(false); resolve(true); } }, PLAY_PAUSE_FADE_INTERVAL); }); if (status === PlayerStatus.PAUSED) { await promise; setLocalPlayerStatus(status); } else if (status === PlayerStatus.PLAYING) { setLocalPlayerStatus(status); await promise; } }, [isTransitioning], ); const onProgressPlayer1 = useCallback( (e: OnProgressProps) => { if (transitionType === 'crossfade' && player.playerNum === 1) { setProgress(Number(e.playedSeconds.toFixed(0))); } else if (transitionType === 'gapless') { setProgress(Number(e.playedSeconds.toFixed(0))); } if (!playerRef.current?.player1()) { return; } switch (transitionType) { case PlayerStyle.CROSSFADE: crossfadeHandler({ crossfadeDuration: crossfadeDuration, currentPlayer: playerRef.current.player1(), currentPlayerNum: player.playerNum, currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player1().ref), isTransitioning, nextPlayer: playerRef.current.player2(), playerNum: 1, setIsTransitioning, volume, }); break; case PlayerStyle.GAPLESS: gaplessHandler({ currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player1().ref), isFlac: false, isTransitioning, nextPlayer: playerRef.current.player2(), setIsTransitioning, }); break; } }, [crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume], ); const onProgressPlayer2 = useCallback( (e: OnProgressProps) => { if (transitionType === PlayerStyle.CROSSFADE && player.playerNum === 2) { setProgress(Number(e.playedSeconds.toFixed(0))); } else if (transitionType === PlayerStyle.GAPLESS) { setProgress(Number(e.playedSeconds.toFixed(0))); } if (!playerRef.current?.player2()) { return; } switch (transitionType) { case PlayerStyle.CROSSFADE: crossfadeHandler({ crossfadeDuration: crossfadeDuration, currentPlayer: playerRef.current.player2(), currentPlayerNum: player.playerNum, currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player2().ref), isTransitioning, nextPlayer: playerRef.current.player1(), playerNum: 2, setIsTransitioning, volume, }); break; case PlayerStyle.GAPLESS: gaplessHandler({ currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player2().ref), isFlac: false, isTransitioning, nextPlayer: playerRef.current.player1(), setIsTransitioning, }); break; } }, [crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume], ); const handleOnEndedPlayer1 = useCallback(() => { const promise = new Promise((resolve) => { mediaAutoNext(); resolve(true); }); promise.then(() => { playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); playerRef.current?.setVolume(volume); setIsTransitioning(false); }); }, [mediaAutoNext, volume]); const handleOnEndedPlayer2 = useCallback(() => { const promise = new Promise((resolve) => { mediaAutoNext(); resolve(true); }); promise.then(() => { playerRef.current?.player2()?.ref?.getInternalPlayer().pause(); playerRef.current?.setVolume(volume); setIsTransitioning(false); }); }, [mediaAutoNext, volume]); usePlayerEvents( { onPlayerSeekToTimestamp: (properties) => { const timestamp = properties.timestamp; if (player.playerNum === 1) { playerRef.current?.player1()?.ref?.seekTo(timestamp); } else { playerRef.current?.player2()?.ref?.seekTo(timestamp); } }, onPlayerStatus: async (properties) => { const status = properties.status; if (status === PlayerStatus.PAUSED) { fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED); } else if (status === PlayerStatus.PLAYING) { fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING); } }, onPlayerVolume: (properties) => { const volume = properties.volume; playerRef.current?.setVolume(volume); }, }, [volume, player.playerNum, isTransitioning], ); return ( ); } function crossfadeHandler(args: { crossfadeDuration: number; currentPlayer: { ref: null | ReactPlayer; setVolume: (volume: number) => void; }; currentPlayerNum: number; currentTime: number; duration: number; isTransitioning: boolean | string; nextPlayer: { ref: null | ReactPlayer; setVolume: (volume: number) => void; }; playerNum: number; setIsTransitioning: Dispatch; volume: number; }) { const { crossfadeDuration, currentPlayer, currentPlayerNum, currentTime, duration, isTransitioning, nextPlayer, playerNum, setIsTransitioning, volume, } = args; const player = `player${playerNum}`; if (!isTransitioning) { if (currentTime > duration - crossfadeDuration) { nextPlayer.setVolume(0); nextPlayer.ref?.getInternalPlayer().play(); return setIsTransitioning(player); } return; } if (isTransitioning !== player && currentPlayerNum !== playerNum) { return; } const timeLeft = duration - currentTime; // Calculate the volume levels based on time remaining const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume; const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume; // Set volumes for both players currentPlayer.setVolume(currentPlayerVolume); nextPlayer.setVolume(nextPlayerVolume); } function gaplessHandler(args: { currentTime: number; duration: number; isFlac: boolean; isTransitioning: boolean | string; nextPlayer: { ref: null | ReactPlayer; setVolume: (volume: number) => void; }; setIsTransitioning: Dispatch; }) { const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args; if (!isTransitioning) { if (currentTime > duration - 2) { return setIsTransitioning(true); } return null; } const durationPadding = getDurationPadding(isFlac); if (currentTime + durationPadding >= duration) { return nextPlayer.ref ?.getInternalPlayer() ?.play() .catch(() => {}); } return null; } function getDuration(ref: null | ReactPlayer | undefined) { return ref?.getInternalPlayer()?.duration || 0; } function getDurationPadding(isFlac: boolean) { switch (isFlac) { case false: return 0.116; case true: return 0.065; } }