Skip to content

Commit bbb82f0

Browse files
committed
feat(studio): export useThumbnailStill so a host's own pictures go through the thumbnail scheduler
1 parent b3c5207 commit bbb82f0

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// @vitest-environment happy-dom
2+
3+
import React, { act } from "react";
4+
import { createRoot, type Root } from "react-dom/client";
5+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6+
import { thumbnailScheduler } from "../player/lib/thumbnailScheduler";
7+
import { useThumbnailStill } from "./useThumbnailStill";
8+
9+
Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { configurable: true, value: true });
10+
11+
class MockImage {
12+
onload: (() => void) | null = null;
13+
onerror: (() => void) | null = null;
14+
naturalWidth = 320;
15+
naturalHeight = 180;
16+
set src(_value: string) {
17+
queueMicrotask(() => this.onload?.());
18+
}
19+
}
20+
21+
const URL_ONE = "http://localhost:3000/api/projects/demo/thumbnail/index.html?t=1.00&revision=1";
22+
const originalImage = globalThis.Image;
23+
const originalFetch = globalThis.fetch;
24+
const originalCreate = URL.createObjectURL;
25+
const originalRevoke = URL.revokeObjectURL;
26+
let host: HTMLDivElement;
27+
let root: Root | null = null;
28+
let shown: string | null = null;
29+
30+
function Still({ url }: { url: string | null }) {
31+
shown = useThumbnailStill(url, "demo");
32+
return null;
33+
}
34+
35+
const mount = (url: string | null) =>
36+
act(async () => {
37+
root ??= createRoot(host);
38+
root.render(React.createElement(Still, { url }));
39+
await new Promise((resolve) => setTimeout(resolve, 0));
40+
});
41+
42+
beforeEach(() => {
43+
globalThis.Image = MockImage as unknown as typeof Image;
44+
globalThis.fetch = vi.fn(async () => new Response(new Blob(["still"]), { status: 200 }));
45+
URL.createObjectURL = vi.fn(() => "blob:still");
46+
URL.revokeObjectURL = vi.fn();
47+
shown = null;
48+
host = document.createElement("div");
49+
document.body.append(host);
50+
});
51+
52+
afterEach(() => {
53+
act(() => root?.unmount());
54+
root = null;
55+
thumbnailScheduler.setPreviewReloading(false);
56+
thumbnailScheduler.invalidateProject("demo");
57+
globalThis.Image = originalImage;
58+
globalThis.fetch = originalFetch;
59+
URL.createObjectURL = originalCreate;
60+
URL.revokeObjectURL = originalRevoke;
61+
document.body.replaceChildren();
62+
});
63+
64+
describe("useThumbnailStill", () => {
65+
it("hands back the still's object URL once the scheduler has loaded it", async () => {
66+
await mount(URL_ONE);
67+
expect(globalThis.fetch).toHaveBeenCalledWith(URL_ONE, expect.anything());
68+
expect(shown).toBe("blob:still");
69+
});
70+
71+
it("asks for nothing without a url", async () => {
72+
await mount(null);
73+
expect(globalThis.fetch).not.toHaveBeenCalled();
74+
expect(shown).toBeNull();
75+
});
76+
77+
it("holds the fetch while a preview reloads, and runs it once the reload is over", async () => {
78+
thumbnailScheduler.setPreviewReloading(true);
79+
await mount(URL_ONE);
80+
expect(globalThis.fetch).not.toHaveBeenCalled();
81+
expect(shown).toBeNull();
82+
thumbnailScheduler.setPreviewReloading(false);
83+
await act(async () => {
84+
await new Promise((resolve) => setTimeout(resolve, 0));
85+
});
86+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
87+
expect(shown).toBe("blob:still");
88+
});
89+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useMemo } from "react";
2+
import { compositionThumbnailRequest } from "../player/components/CompositionThumbnail";
3+
import type { ThumbnailPriority } from "../player/lib/thumbnailScheduler";
4+
import { useThumbnailLease } from "./useThumbnailLease";
5+
6+
export interface UseThumbnailStillOptions {
7+
sessionEpoch?: number;
8+
priority?: ThumbnailPriority;
9+
}
10+
11+
/** A host picture's server still through the thumbnail scheduler, so it yields to a preview reload. Null until ready. */
12+
export function useThumbnailStill(
13+
url: string | null,
14+
projectId: string,
15+
{ sessionEpoch = 0, priority = "visible" }: UseThumbnailStillOptions = {},
16+
): string | null {
17+
const request = useMemo(
18+
() => (url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null),
19+
[priority, projectId, sessionEpoch, url],
20+
);
21+
const snapshot = useThumbnailLease(request);
22+
return snapshot.status === "ready" && snapshot.value.kind === "image" ? snapshot.value.url : null;
23+
}

‎packages/studio/src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export type { AudioWaveformProps } from "./player/components/AudioWaveform";
7676
export { ImageThumbnail } from "./player/components/ImageThumbnail";
7777
export type { ImageThumbnailProps } from "./player/components/ImageThumbnail";
7878
export { useRenderClipContent } from "./hooks/useRenderClipContent";
79+
export { useThumbnailStill } from "./hooks/useThumbnailStill";
80+
export type { UseThumbnailStillOptions } from "./hooks/useThumbnailStill";
7981
export type { UseRenderClipContentOptions } from "./hooks/useRenderClipContent";
8082
export type { ThumbnailPriority } from "./player/lib/thumbnailScheduler";
8183
export type {

0 commit comments

Comments
 (0)