mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
PAP-10499: guard video thumbnail fallback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a0de45ec41
commit
32d38571ed
2 files changed
+63
-2
No files matched your search
@@ -130,6 +130,44 @@ describe("ArtifactCard", () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("reveals video previews if the browser does not report seek completion", () => {
|
||||
vi.useFakeTimers();
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
try {
|
||||
flushSync(() => {
|
||||
root.render(
|
||||
<ArtifactCard
|
||||
artifact={makeArtifact({ mediaKind: "video", contentType: "video/mp4", contentPath: "/files/clip.mp4" })}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const video = container.querySelector("video") as HTMLVideoElement;
|
||||
expect(video).not.toBeNull();
|
||||
expect(video.dataset.frameReady).toBe("false");
|
||||
|
||||
flushSync(() => {
|
||||
video.dispatchEvent(new Event("loadedmetadata", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(video.currentTime).toBe(0.05);
|
||||
expect(video.dataset.frameReady).toBe("false");
|
||||
|
||||
flushSync(() => {
|
||||
vi.advanceTimersByTime(3000);
|
||||
});
|
||||
|
||||
expect(video.dataset.frameReady).toBe("true");
|
||||
} finally {
|
||||
flushSync(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders a falling-back video placeholder when no content path exists", () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<ArtifactCard artifact={makeArtifact({ mediaKind: "video", contentPath: null })} />,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type SyntheticEvent, useRef, useState } from "react";
|
||||
import { type SyntheticEvent, useEffect, useRef, useState } from "react";
|
||||
import { Download, ExternalLink, Paperclip, Play } from "lucide-react";
|
||||
import type { CompanyArtifact } from "@/api/artifacts";
|
||||
import { Link } from "@/lib/router";
|
||||
@@ -54,6 +54,14 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
|
||||
const [errored, setErrored] = useState(false);
|
||||
const [frameReady, setFrameReady] = useState(false);
|
||||
const thumbnailSeekRequested = useRef(false);
|
||||
const frameReadyFallbackTimer = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (frameReadyFallbackTimer.current !== null) {
|
||||
window.clearTimeout(frameReadyFallbackTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
if (errored || !artifact.contentPath) {
|
||||
return (
|
||||
<PreviewFrame className="flex items-center justify-center bg-black/80">
|
||||
@@ -64,7 +72,19 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
|
||||
);
|
||||
}
|
||||
|
||||
const markFrameReady = () => setFrameReady(true);
|
||||
const markFrameReady = () => {
|
||||
if (frameReadyFallbackTimer.current !== null) {
|
||||
window.clearTimeout(frameReadyFallbackTimer.current);
|
||||
frameReadyFallbackTimer.current = null;
|
||||
}
|
||||
setFrameReady(true);
|
||||
};
|
||||
const scheduleFrameReadyFallback = () => {
|
||||
if (frameReadyFallbackTimer.current !== null) {
|
||||
window.clearTimeout(frameReadyFallbackTimer.current);
|
||||
}
|
||||
frameReadyFallbackTimer.current = window.setTimeout(markFrameReady, 3000);
|
||||
};
|
||||
const loadThumbnailFrame = (event: SyntheticEvent<HTMLVideoElement>) => {
|
||||
if (thumbnailSeekRequested.current) return;
|
||||
thumbnailSeekRequested.current = true;
|
||||
@@ -74,6 +94,9 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
|
||||
try {
|
||||
if (Math.abs(video.currentTime - seekTarget) > 0.001) {
|
||||
video.currentTime = seekTarget;
|
||||
scheduleFrameReadyFallback();
|
||||
} else {
|
||||
markFrameReady();
|
||||
}
|
||||
} catch {
|
||||
markFrameReady();
|
||||
|
||||
Reference in new issue
Block a user