diff --git a/ui/src/components/artifacts/ArtifactCard.test.tsx b/ui/src/components/artifacts/ArtifactCard.test.tsx
index 5e206763f1a..9ea18a61894 100644
--- a/ui/src/components/artifacts/ArtifactCard.test.tsx
+++ b/ui/src/components/artifacts/ArtifactCard.test.tsx
@@ -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(
+ ,
+ );
+ });
+
+ 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(
,
diff --git a/ui/src/components/artifacts/ArtifactCard.tsx b/ui/src/components/artifacts/ArtifactCard.tsx
index 61c52b1e800..bc4a4819093 100644
--- a/ui/src/components/artifacts/ArtifactCard.tsx
+++ b/ui/src/components/artifacts/ArtifactCard.tsx
@@ -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(null);
+ useEffect(() => {
+ return () => {
+ if (frameReadyFallbackTimer.current !== null) {
+ window.clearTimeout(frameReadyFallbackTimer.current);
+ }
+ };
+ }, []);
if (errored || !artifact.contentPath) {
return (
@@ -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) => {
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();