import connection, { calculateRoomTime, useRoomRunningAndReadyChanged, useTimelineSongFileChanged, } from "../lib/Connection"; import { SyntheticEvent, useEffect, useRef, useState } from "react"; import { Button, Slider } from "antd"; import { SoundFilled, SoundOutlined } from "@ant-design/icons"; import "../css/player.css"; import { Room } from "../types/types"; const Player = () => { const room = useRoomRunningAndReadyChanged(); const timeline = useTimelineSongFileChanged(); const player = useRef(null); const defaultVolume = parseInt(localStorage.getItem("volume") ?? "100"); const [volume, setVolume] = useState(defaultVolume); const [muted, setMuted] = useState(false); const [finishedLoading, setFinishedLoading] = useState(false); const [timesSeeked, setTimesSeeked] = useState(0); const [hadError, setHadError] = useState(false); // If our time synchronisation algorithm thing thinks the time is off by more // than this value, we seek the running player to correct it. const diffSecondsRequiredToSeekRunningPlayer = 0.2; // Hard cap we are allowed to seek this player. Some browsers are slow or inaccurate // and will always be off. To avoid endless skipping of the song this cap stops seeking the // player. const maxTimesSeekAllow = 25; useEffect(() => { // Need to use an effect since 'player' will only contain a reference after first render. if (!timeline) { throw new Error("Player without active timeline."); } if (!player.current) { throw new Error("No player after mount."); } player.current.src = timeline.songFile; }, [timeline]); function handlePlayerOnPlay(e: SyntheticEvent) { e.preventDefault(); // For when the user manually started the player for when autoplay is off. setHadError(false); } async function handlePlayerPause(e: SyntheticEvent) { if (!shouldPlay()) { // We should not be playing, pausing is fine. console.log("should not play, paused"); return; } e.preventDefault(); if (room) { setPlayerTime(room, true); } await player.current?.play(); } function handlePlayerCanPlayThrough() { if (!finishedLoading) { setFinishedLoading(true); connection.requestStart(); } } function shouldPlay() { return ( player.current && timeline && room && room.running && room.readyToParticipate && !player.current.ended ); } async function startPlaying(manual: boolean) { if (!player.current) { return; } if (player.current.paused && !hadError) { setPlayerVolume(volume); try { await player.current.play(); setHadError(false); } catch (e) { console.error("Error playing", e); setHadError(true); } } if (!hadError && room) { setPlayerTime(room, manual); } } function setPlayerTime(room: Room, manualAdjustment: boolean) { if (!player.current) { return; } // Player's currentTime is in seconds, not ms. const targetTime = calculateRoomTime() / 1000; const diff = Math.abs(player.current.currentTime - targetTime); // console.log('PLAYER DIFF', diff, // 'min req to seek: ', diffSecondsRequiredToSeekRunningPlayer, // `(${timesSeeked} / ${maxTimesSeekAllow})`); if (diff <= diffSecondsRequiredToSeekRunningPlayer) { return; } if (timesSeeked >= maxTimesSeekAllow && !manualAdjustment) { // If we are adjusting manually we always allow a seek. console.warn( "The running player is off, but we've changed the time " + "too often, skipping synchronizing the player." ); return; } player.current.currentTime = targetTime; player.current.playbackRate = Math.min(room.speedFactor, 5); if (!manualAdjustment) { setTimesSeeked(timesSeeked + 1); } console.log( `Player seeked: diff: ${diff}, target: ${targetTime}, (${timesSeeked} / ${maxTimesSeekAllow})` ); } function toggleMute() { if (player.current != null) { player.current.muted = !player.current.muted; setMuted(!muted); } } function changeVolume(sliderValue: number) { setVolume(sliderValue); localStorage["volume"] = sliderValue; setPlayerVolume(sliderValue); } function setPlayerVolume(value: number) { if (player.current) { player.current.volume = value === 0.0 ? 0.0 : Math.pow(10, (value / 100 - 1) * 2); } } if (shouldPlay()) { startPlaying(false) .then(() => { // }) .catch((e) => { console.error(e); }); } else { if (player.current) { player.current.pause(); } } return (
); }; export default Player;