aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-06-07 03:19:34 -0700
committerPinapelz <yukais@pinapelz.com>2026-06-07 03:19:34 -0700
commit52f008d7eb638143e1319d140d7058bfa1766674 (patch)
tree9521032230f49d63b1d8ba4991a0a5038acbccfc
parent38f01714c551f3d97406703904f9782d42012fe9 (diff)
make unlimited mode start at random point in video
-rw-r--r--README.md4
-rw-r--r--src/components/MiniYouTubePlayer/index.tsx1
-rw-r--r--src/components/YTPlayer/index.tsx149
3 files changed, 124 insertions, 30 deletions
diff --git a/README.md b/README.md
index 61b3c6c..af50ee2 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,10 @@ The state of the daily game is kept locally and a signature is generated to prev
# Unlimited Mode
- Unlimited play
- Uses a minimized YouTube miniplayer for audio
-- Song segments always start at the beginning of the video
+- A random segment of audio from YouTube video
- Guesses are verified locally
-> Its recommended to play in fullscrene to prevent any media modules from leaking the solutions
+> Its recommended to play in fullscreen to prevent any media modules from leaking the solutions. Extra audio sometimes plays due to the nature of YouTube's IFrame API.
# Local Development
diff --git a/src/components/MiniYouTubePlayer/index.tsx b/src/components/MiniYouTubePlayer/index.tsx
index 5aa2d57..7c8e86a 100644
--- a/src/components/MiniYouTubePlayer/index.tsx
+++ b/src/components/MiniYouTubePlayer/index.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { default as YouTubePlayer } from "react-youtube";
interface Props {
diff --git a/src/components/YTPlayer/index.tsx b/src/components/YTPlayer/index.tsx
index 1aac9ac..9910351 100644
--- a/src/components/YTPlayer/index.tsx
+++ b/src/components/YTPlayer/index.tsx
@@ -9,81 +9,160 @@ interface Props {
currentTry: number;
}
+const DEFAULT_VOLUME = 0.7;
+
+const loadVolume = () => {
+ try {
+ const storedVolume = localStorage.getItem("playerVolume");
+ if (storedVolume === null) return DEFAULT_VOLUME;
+
+ const parsedVolume = Number(storedVolume);
+ if (!Number.isFinite(parsedVolume)) return DEFAULT_VOLUME;
+
+ return Math.max(0, Math.min(1, parsedVolume));
+ } catch {
+ return DEFAULT_VOLUME;
+ }
+};
+
export function Player({ id, currentTry }: Props) {
const opts = {
width: "0",
height: "0",
};
- // react-youtube doesn't export types for this
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const playerRef = React.useRef<any>(null);
+ const MIN_REMAINING_SEC = 17;
const currentPlayTime = playTimes[currentTry];
+ const totalPlayTime = playTimes[playTimes.length - 1];
+ const progressDurationSec = totalPlayTime / 1000;
- const [play, setPlay] = React.useState<boolean>(false);
-
- const [currentTime, setCurrentTime] = React.useState<number>(0);
+ const [play, setPlay] = React.useState(false);
+ const [startTime, setStartTime] = React.useState(0);
+ const [currentTime, setCurrentTime] = React.useState(0);
+ const [isReady, setIsReady] = React.useState(false);
+ const [volume, setVolume] = React.useState(loadVolume);
- const [isReady, setIsReady] = React.useState<boolean>(false);
+ const progressValue = Math.min(
+ progressDurationSec,
+ Math.max(0, currentTime - startTime),
+ );
React.useEffect(() => {
- setInterval(() => {
+ const interval = setInterval(() => {
playerRef.current?.internalPlayer
- .getCurrentTime()
- .then((time: number) => {
- setCurrentTime(time);
- });
+ ?.getCurrentTime()
+ .then((time: number) => setCurrentTime(time));
}, 250);
+
+ return () => clearInterval(interval);
}, []);
React.useEffect(() => {
- if (play) {
- if (currentTime * 1000 >= currentPlayTime) {
- playerRef.current?.internalPlayer.pauseVideo();
- playerRef.current?.internalPlayer.seekTo(0);
- setPlay(false);
- }
+ if (!play) return;
+
+ const elapsedMs = Math.max(0, (currentTime - startTime) * 1000);
+
+ if (elapsedMs >= currentPlayTime) {
+ playerRef.current?.internalPlayer.pauseVideo();
+ playerRef.current?.internalPlayer.seekTo(startTime, true);
+ setPlay(false);
}
- }, [play, currentTime]);
+ }, [play, currentTime, startTime, currentPlayTime]);
- // don't call play video each time currentTime changes
const startPlayback = React.useCallback(() => {
- playerRef.current?.internalPlayer.playVideo();
+ const player = playerRef.current?.internalPlayer;
+ if (!player) return;
+
+ player.seekTo(startTime, true);
+ player.playVideo();
setPlay(true);
- }, []);
+ }, [startTime]);
+
+ const updateVolume = React.useCallback(
+ (event: React.ChangeEvent<HTMLInputElement>) => {
+ setVolume(Number(event.target.value));
+ },
+ [],
+ );
+
+ const setReady = React.useCallback(async () => {
- const setReady = React.useCallback(() => {
+ const player = playerRef.current?.internalPlayer;
+ if (!player) return;
+
+ const duration = await player.getDuration();
+
+ const maxStart = Math.max(0, duration - MIN_REMAINING_SEC);
+ const randomStart = Math.random() * maxStart;
+
+ setStartTime(randomStart);
+ player.setVolume(volume * 100);
+ player.seekTo(randomStart, true);
setIsReady(true);
- }, []);
+ }, [volume]);
+
+ React.useEffect(() => {
+ if (!isReady) return;
+
+ const player = playerRef.current?.internalPlayer;
+ if (!player) return;
+
+ player.getDuration().then((duration: number) => {
+ const maxStart = Math.max(0, duration - MIN_REMAINING_SEC);
+ const randomStart = Math.random() * maxStart;
+
+ setStartTime(randomStart);
+ setPlay(false);
+ setCurrentTime(0);
+
+ player.seekTo(randomStart, true);
+ });
+ }, [id, isReady]);
+
+ React.useEffect(() => {
+ if (!isReady) return;
+
+ playerRef.current?.internalPlayer?.setVolume(volume * 100);
+
+ try {
+ localStorage.setItem("playerVolume", String(volume));
+ } catch {
+ }
+ }, [isReady, volume]);
return (
<>
<YouTube opts={opts} videoId={id} onReady={setReady} ref={playerRef} />
+
{isReady ? (
<>
<Styled.ProgressBackground>
- {currentTime !== 0 && <Styled.Progress value={currentTime} />}
+ <Styled.Progress value={progressValue} />
+
{playTimes.map((playTime) => (
<Styled.Separator
- style={{ left: `${(playTime / 16000) * 100}%` }}
key={playTime}
+ style={{ left: `${(playTime / totalPlayTime) * 100}%` }}
/>
))}
</Styled.ProgressBackground>
+
<Styled.TimeStamps>
<Styled.TimeStamp>1s</Styled.TimeStamp>
<Styled.TimeStamp>16s</Styled.TimeStamp>
</Styled.TimeStamps>
- {!play && (
+
+ {!play ? (
<IoPlay
style={{ cursor: "pointer" }}
size={36}
color="var(--cl-green-6)"
onClick={startPlayback}
/>
- )}
- {play && (
+ ) : (
<IoPause
style={{ cursor: "pointer" }}
size={36}
@@ -91,6 +170,22 @@ export function Player({ id, currentTry }: Props) {
onClick={startPlayback}
/>
)}
+
+ <Styled.VolumeControl>
+ <Styled.VolumeLabel htmlFor="youtube-player-volume">
+ Volume {Math.round(volume * 100)}%
+ </Styled.VolumeLabel>
+ <Styled.VolumeSlider
+ id="youtube-player-volume"
+ type="range"
+ min="0"
+ max="1"
+ step="0.01"
+ value={volume}
+ onChange={updateVolume}
+ aria-label="Volume"
+ />
+ </Styled.VolumeControl>
</>
) : (
<p>Loading player...</p>
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage