|
|
|
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<HTMLAudioElement>(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 (
|
|
|
|
<div id="audio">
|
|
|
|
<audio
|
|
|
|
ref={player}
|
|
|
|
className="player"
|
|
|
|
controls={true}
|
|
|
|
loop={false}
|
|
|
|
hidden={!hadError}
|
|
|
|
onPause={handlePlayerPause}
|
|
|
|
onPlay={handlePlayerOnPlay}
|
|
|
|
onCanPlayThrough={handlePlayerCanPlayThrough}
|
|
|
|
/>
|
|
|
|
<div id="volume-control">
|
|
|
|
<Button onClick={toggleMute} shape="circle">
|
|
|
|
{muted ? <SoundOutlined /> : <SoundFilled />}
|
|
|
|
</Button>
|
|
|
|
<Slider
|
|
|
|
id="volume-slider"
|
|
|
|
defaultValue={volume}
|
|
|
|
onChange={changeVolume}
|
|
|
|
trackStyle={{ backgroundColor: "var(--secondary-color)" }}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Player;
|