From 06fdfcc447da13288a37dd5495a6dbb4f24a384d Mon Sep 17 00:00:00 2001 From: static Date: Mon, 12 Oct 2026 01:00:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=B0=8D=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=8F=99=EC=98=81=EC=83=81=EC=9D=98=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=EC=9D=84=20=EC=83=9D=EC=84=B1=ED=95=A0=20?= =?UTF-8?q?=EB=95=8C=20=ED=94=84=EB=A0=88=EC=9E=84=EC=9D=B4=20=EC=A4=80?= =?UTF-8?q?=EB=B9=84=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9C=BC=EB=A9=B4=20?= =?UTF-8?q?=EB=AC=B4=ED=95=9C=ED=9E=88=20=EB=8C=80=EA=B8=B0=ED=95=98?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/lib/client/media/thumbnail.ts | 41 ++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lib/client/media/thumbnail.ts b/src/lib/client/media/thumbnail.ts index 7b409a9..07d3ce8 100644 --- a/src/lib/client/media/thumbnail.ts +++ b/src/lib/client/media/thumbnail.ts @@ -60,24 +60,53 @@ export const captureVideoThumbnail = (video: HTMLVideoElement) => { }); }; +const VIDEO_THUMBNAIL_TIMEOUT_MS = 30_000; + const generateVideoThumbnail = (videoUrl: string, time = 0) => { return new Promise((resolve, reject) => { const video = document.createElement("video"); + let isCaptured = false; + + const cleanup = () => { + clearTimeout(timeout); + video.onloadedmetadata = video.onloadeddata = video.onseeked = video.onerror = null; + video.removeAttribute("src"); + video.load(); // Stops loading the video + }; + const fail = (reason: unknown) => { + cleanup(); + reject(reason); + }; + const captureFrame = () => { + if (isCaptured || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return; + isCaptured = true; + captureVideoThumbnail(video).then(resolve, reject).finally(cleanup); + }; + + // Streamed videos may stall without any error + const timeout = setTimeout( + () => fail(new Error("Timed out while loading the video")), + VIDEO_THUMBNAIL_TIMEOUT_MS, + ); + video.onloadedmetadata = () => { if (video.videoWidth === 0 || video.videoHeight === 0) { - return reject(); + return fail(new Error("Invalid video size")); } - const callbackId = video.requestVideoFrameCallback(() => { - captureVideoThumbnail(video).then(resolve).catch(reject); - video.cancelVideoFrameCallback(callbackId); - }); + // Frame callbacks aren't guaranteed for videos not in the document, so also wait for seeking + video.requestVideoFrameCallback(captureFrame); + video.onseeked = captureFrame; + if (time === 0) { + video.onloadeddata = captureFrame; + } video.currentTime = Math.min(time, video.duration); }; - video.onerror = reject; + video.onerror = () => fail(video.error); video.muted = true; video.playsInline = true; + video.preload = "auto"; video.src = videoUrl; }); };