fix(studio): route rooted timeline media through preview (#3061)

This commit is contained in:
Miguel Ángel
2026-08-05 22:14:28 -07:00
committed by GitHub
parent 96861cbafc
commit 88853f170f
7 changed files with 223 additions and 42 deletions
@@ -1,15 +1,24 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getMediaProbeDiagnostics, probeMediaUrl, resetMediaProbeRegistry } from "./mediaProbe";
import {
applyCachedSourceDurations,
getMediaProbeDiagnostics,
probeMediaUrl,
probeMissingSourceDurations,
resetMediaProbeRegistry,
} from "./mediaProbe";
const dispose = vi.fn();
const getDurationFromMetadata = vi.fn(async () => 5);
const requestedSources: string[] = [];
vi.mock("mediabunny", () => ({
ALL_FORMATS: {},
UrlSource: class {
constructor(readonly url: string) {}
constructor(readonly url: string) {
requestedSources.push(url);
}
},
Input: class {
getDurationFromMetadata = getDurationFromMetadata;
@@ -22,6 +31,7 @@ vi.mock("mediabunny", () => ({
beforeEach(() => {
resetMediaProbeRegistry();
vi.clearAllMocks();
requestedSources.length = 0;
getDurationFromMetadata.mockResolvedValue(5);
});
@@ -78,4 +88,56 @@ describe("media probe registry", () => {
await expect(probeMediaUrl("/bad.mp4")).resolves.toBeNull();
expect(getDurationFromMetadata).toHaveBeenCalledTimes(2);
});
it("probes same-origin rooted media through the active project preview", async () => {
const apply = vi.fn();
await probeMissingSourceDurations(
[
{
id: "clip",
tag: "video",
src: `${window.location.origin}/assets/clip.mp4`,
},
],
"project-a",
apply,
);
expect(requestedSources).toEqual([
`${window.location.origin}/api/projects/project-a/preview/assets/clip.mp4`,
]);
expect(apply).toHaveBeenCalledWith("clip", 5);
expect(
applyCachedSourceDurations(
[
{
id: "clip",
tag: "video",
src: `${window.location.origin}/assets/clip.mp4`,
},
],
"project-a",
),
).toEqual([
{
id: "clip",
tag: "video",
src: `${window.location.origin}/assets/clip.mp4`,
sourceDuration: 5,
},
]);
await probeMissingSourceDurations(
[
{
id: "clip",
tag: "video",
src: `${window.location.origin}/assets/clip.mp4`,
},
],
"project-a",
apply,
);
expect(requestedSources).toHaveLength(1);
});
});
+26 -14
View File
@@ -1,3 +1,4 @@
import { resolveMediaPreviewUrl } from "../components/thumbnailUtils";
import { TIMELINE_VIEWPORT_BUDGETS } from "./timelineViewportBudgets";
export interface MediaProbeResult {
@@ -85,6 +86,10 @@ function getCachedProbe(url: string): MediaProbeResult | undefined {
return cached?.result;
}
function resolveProbeSource(src: string, projectId: string | null): string {
return projectId ? resolveMediaPreviewUrl(src, projectId, window.location.origin) : src;
}
function evictMetadataOverflow(): void {
const overflow = cache.size + failed.size - TIMELINE_VIEWPORT_BUDGETS.metadataRegistryEntries;
if (overflow <= 0) return;
@@ -106,11 +111,11 @@ function evictMetadataOverflow(): void {
*/
export function applyCachedSourceDurations<
T extends { src?: string; tag: string; sourceDuration?: number },
>(elements: T[]): T[] {
>(elements: T[], projectId: string | null): T[] {
return elements.map((el) => {
const tag = el.tag.toLowerCase();
if (!el.src || el.sourceDuration != null || (tag !== "audio" && tag !== "video")) return el;
const cached = getCachedProbe(el.src);
const cached = getCachedProbe(resolveProbeSource(el.src, projectId));
return cached?.duration && cached.duration > 0
? { ...el, sourceDuration: cached.duration }
: el;
@@ -124,20 +129,27 @@ export function applyCachedSourceDurations<
*/
export async function probeMissingSourceDurations<
T extends { src?: string; tag: string; sourceDuration?: number; key?: string; id: string },
>(elements: T[], apply: (key: string, durationSeconds: number) => void): Promise<void> {
const needs = elements.filter(
(el) =>
el.src &&
el.sourceDuration == null &&
["video", "audio"].includes(el.tag.toLowerCase()) &&
!getCachedProbe(el.src) &&
!hasFreshFailure(normalizeUrl(el.src)),
);
>(
elements: T[],
projectId: string | null,
apply: (key: string, durationSeconds: number) => void,
): Promise<void> {
const needs = elements.flatMap((el) => {
if (
!el.src ||
el.sourceDuration != null ||
!["video", "audio"].includes(el.tag.toLowerCase())
) {
return [];
}
const source = resolveProbeSource(el.src, projectId);
return !getCachedProbe(source) && !hasFreshFailure(normalizeUrl(source))
? [{ el, source }]
: [];
});
if (needs.length === 0) return;
await Promise.allSettled(
needs.map(async (el) => {
const source = el.src;
if (!source) return;
needs.map(async ({ el, source }) => {
const result = await probeMediaUrl(source);
if (result) apply(el.key ?? el.id, result.duration);
}),