Written by: Marlon Colca
Posted on 12 Sep 2025 - 22 days ago
nextjs typescript clones
Persist and restore playback position, and reflect it in the catalog UI.
Goal: Persist and restore playback position, and reflect it in the catalog UI.
src/hooks/useResume.ts
: saves { t, d, u }
in localStorage
as vp:pos:<key>
.src/components/VideoPlayer.tsx
: calls useResume(videoEl, storageKey, true)
.src/components/MovieCard.tsx
: reads progress and renders a small bar.// src/hooks/useResume.ts
export function useResume(
video: HTMLVideoElement | null,
key: string,
enable: boolean
) {
useEffect(() => {
if (!video || !enable || !key) return;
const storageKey = `vp:pos:${key}`;
let lastSaved = 0;
const tryRestore = () => {
try {
const raw = localStorage.getItem(storageKey);
if (!raw) return;
const { t, d } = JSON.parse(raw) as { t: number; d?: number };
if (
Number.isFinite(t) &&
t > 1 &&
video.duration &&
t < video.duration - 2
)
video.currentTime = t;
} catch {}
};
const save = () => {
if (!video.duration || video.duration < 20) return;
const t = video.currentTime;
const now = Date.now();
if (now - lastSaved < 2000) return;
lastSaved = now;
if (t < 2 || t >= video.duration - 2) {
try {
localStorage.removeItem(storageKey);
} catch {}
return;
}
try {
localStorage.setItem(
storageKey,
JSON.stringify({ t, d: video.duration, u: now })
);
} catch {}
};
const onLoaded = () => tryRestore();
video.addEventListener("loadedmetadata", onLoaded);
video.addEventListener("timeupdate", save);
video.addEventListener("pause", save);
video.addEventListener("ended", () => {
try {
localStorage.removeItem(storageKey);
} catch {}
});
return () => {
video.removeEventListener("loadedmetadata", onLoaded);
video.removeEventListener("timeupdate", save);
video.removeEventListener("pause", save);
video.removeEventListener("ended", () => {});
};
}, [video, key, enable]);
}
// src/components/MovieCard.tsx
const raw = localStorage.getItem(`vp:pos:movie:${movie.id}`);
const data = raw ? (JSON.parse(raw) as { t?: number; d?: number }) : undefined;
const pct = data?.t && data?.d ? Math.min(100, (data.t / data.d) * 100) : 0;
{
pct > 0 && (
<div className="absolute inset-x-0 bottom-0 h-1 bg-white/20">
<div className="h-full bg-red-500" style={{ width: `${pct}%` }} />
</div>
);
}
storageKey
(movie:<id>
) keeps data scoped and makes it easy to migrate later.duration
is unknown (streaming edge cases), the hook won’t save until metadata loads.Improve ergonomics with autoplay, best‑effort fullscreen, keyboard controls, and an optional “unmute” overlay.
13 Sep 2025 - 21 days ago