Skip to content

Commit

Permalink
basic looping
Browse files Browse the repository at this point in the history
  • Loading branch information
samcarton committed Jul 28, 2024
1 parent 85dd81b commit d460e16
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 4 deletions.
44 changes: 43 additions & 1 deletion src/components/VideoPlayer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

/* width: calc(100% - 2.25rem); */
width: 100%;
margin: 1rem 0 0 0;
margin: 0;

/* Removes default focus */
&:focus {
Expand Down Expand Up @@ -101,3 +101,45 @@
min-width: 0;
}
}

.loopGroup {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
gap: 6px;

& button {
text-decoration: none;
display: inline-block;
outline: 0;
border: 0;
cursor: pointer;
background: rgb(var(--accent-light));
color: #ffffff;
border-radius: 8px;
padding: 12px 12px;
font-size: 16px;
font-weight: 700;
line-height: 1;
transition:
transform 200ms,
background 200ms;
&:hover {
transform: translateY(-2px);
}
}
}

.loopDisplay {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
gap: 0.5rem;
}

h3 {
margin: 0;
}
104 changes: 102 additions & 2 deletions src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useRef, useState } from "react";
import ReactPlayer from "react-player";
import classes from "./VideoPlayer.module.css";

Expand Down Expand Up @@ -53,28 +53,101 @@ const tryGetVideoUrl = (urlInput: string | null) => {
};

export const VideoPlayer = () => {
const playerRef = useRef<ReactPlayer | null>(null);
const urlParams = new URLSearchParams(window.location.search);
const videoUrl = tryGetVideoUrl(urlParams.get("v"));

const [duration, setDuration] = useState(99999);
const handleDuration = (duration: number) => {
setDuration(duration);
setLoopTo(duration);
};

// #region Speed
const defaultSpeed = tryParseDefaultSpeed(urlParams.get("s"));
const [speed, setSpeed] = useState(defaultSpeed);

const handleSpeedChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSpeed(parseFloat(e.target.value));
replaceSpeedHistory(e.target.value);
};

// #endregion

// #region loop
const [isLooping, setIsLooping] = useState(false);
const [loopFrom, setLoopFrom] = useState<number | null>(null);
const [loopTo, setLoopTo] = useState<number | null>(null);
const minLoopSeconds = 2;
const hasValidLoopingValues = loopFrom !== null && loopTo !== null;

const handleProgress = (state: { played: number; playedSeconds: number }) => {
if (!isLooping || loopFrom === null || loopTo === null) {
return;
}

if (state.playedSeconds > loopTo) {
playerRef.current?.seekTo(loopFrom);
// playing=true needs to be set on the player otherwise it pauses after seeking
}
};

const handleSetLoopLast = (xSeconds: number) => {
const to = Math.max(
playerRef.current?.getCurrentTime() || 0,
minLoopSeconds,
);
setLoopTo(to);
setLoopFrom(Math.max(to - xSeconds, 0));
setIsLooping(true);
};

const handleSetLoopNext = (xSeconds: number) => {
const from = playerRef.current?.getCurrentTime() || 0;
setLoopFrom(from);
setLoopTo(from + xSeconds);
setIsLooping(true);
};

// tap-twice - start here, end here
const handleSetStartLoopHere = () => {
setLoopFrom(playerRef.current?.getCurrentTime() || 0);
};

const handleSetEndLoopHere = () => {
setLoopTo(
Math.max(playerRef.current?.getCurrentTime() || 0, minLoopSeconds),
);
if (loopFrom !== null) {
setIsLooping(true);
}
};

const handleClearLoop = () => {
setLoopFrom(null);
setLoopTo(null);
setIsLooping(false);
};

// #endregion

return (
<div className={classes.container}>
<div className={classes.responsiveWrapper}>
<ReactPlayer
playing
ref={playerRef}
url={videoUrl}
width={"100%"}
height={"100%"}
controls
playbackRate={speed}
className={classes.player}
onProgress={handleProgress}
progressInterval={100}
onDuration={handleDuration}
/>
</div>
<h3>Speed</h3>
<input
type="range"
min="0.5"
Expand All @@ -99,6 +172,33 @@ export const VideoPlayer = () => {
<div></div>
<div>100%</div>
</div>
<h3>Looping</h3>
<div className={classes.loopDisplay}>
<div>Loop: {isLooping ? "Enabled" : "Disabled"}</div>
{hasValidLoopingValues && (
<div>
from: {loopFrom.toFixed(2)}s to {loopTo.toFixed(2)}s
</div>
)}
</div>
<div className={classes.loopGroup}>
<button onClick={() => handleSetLoopLast(20)}>Last 20s</button>
<button onClick={() => handleSetLoopLast(10)}>Last 10s</button>
<button onClick={() => handleSetLoopLast(5)}>Last 5s</button>
<button onClick={() => handleSetLoopNext(5)}>Next 5s</button>
<button onClick={() => handleSetLoopNext(10)}>Next 10s</button>
<button onClick={() => handleSetLoopNext(20)}>Next 20s</button>
</div>
<div className={classes.loopGroup}>
<button onClick={handleSetStartLoopHere}>Start here</button>
<button onClick={handleSetEndLoopHere}>End here</button>
</div>
<div className={classes.loopGroup}>
<button onClick={() => setIsLooping((x) => !x)}>
{isLooping ? "Disable" : "Enable"} loop
</button>
<button onClick={handleClearLoop}>Clear loop</button>
</div>
</div>
);
};
3 changes: 2 additions & 1 deletion src/pages/play.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import Footer from "../components/Footer.astro";
<SmallHeader />
<VideoPlayer client:only="react" />
<div class="instructions">
<p>☝️ Adjust playback speed using the slider above.</p>
<p>☝️ Adjust playback speed using the slider.</p>
<p>🔁 Adjust looping using the loop controls.</p>
<p>
🔖 Bookmark this page to come back to the same video at the same speed.
</p>
Expand Down

0 comments on commit d460e16

Please sign in to comment.