From 5058afc980bae1388fe9ee7bab20a1131d7a57a3 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 1 Feb 2026 15:58:11 -0800 Subject: [PATCH] feat: prefer derived mp4 playback with fallback --- apps/web/app/components/MediaPanel.tsx | 40 ++++++++++++++++--- apps/web/app/lib/playback.ts | 22 ++++++++++ apps/web/src/__tests__/prefer-derived.test.ts | 18 +++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/lib/playback.ts create mode 100644 apps/web/src/__tests__/prefer-derived.test.ts diff --git a/apps/web/app/components/MediaPanel.tsx b/apps/web/app/components/MediaPanel.tsx index da39362..29a4c98 100644 --- a/apps/web/app/components/MediaPanel.tsx +++ b/apps/web/app/components/MediaPanel.tsx @@ -24,6 +24,7 @@ type SignedUrlResponse = { }; type PreviewUrlState = Record; +type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: 720 }; function startOfDayUtc(iso: string) { const d = new Date(iso); @@ -45,7 +46,7 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { const [viewer, setViewer] = useState<{ asset: Asset; url: string; - variant: "original" | "thumb_med" | "poster"; + variant: "original" | "thumb_med" | "poster" | "video_mp4"; } | null>(null); const [viewerError, setViewerError] = useState(null); @@ -103,16 +104,35 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { async function loadSignedUrl( assetId: string, - variant: "original" | "thumb_small" | "thumb_med" | "poster", + variant: + | "original" + | "thumb_small" + | "thumb_med" + | "poster" + | "video_mp4_720", ) { - const res = await fetch(`/api/assets/${assetId}/url?variant=${variant}`, { - cache: "no-store", - }); + const url = + variant === "video_mp4_720" + ? `/api/assets/${assetId}/url?kind=video_mp4&size=720` + : `/api/assets/${assetId}/url?variant=${variant}`; + const res = await fetch(url, { cache: "no-store" }); if (!res.ok) throw new Error(`presign_failed:${res.status}`); const json = (await res.json()) as SignedUrlResponse; return json.url; } + async function loadVideoPlaybackUrl( + assetId: string, + ): Promise<{ url: string; variant: VideoPlaybackVariant }> { + try { + const url = await loadSignedUrl(assetId, "video_mp4_720"); + return { url, variant: { kind: "video_mp4", size: 720 } }; + } catch { + const url = await loadSignedUrl(assetId, "original"); + return { url, variant: { kind: "original" } }; + } + } + async function openViewer(asset: Asset) { if (asset.status === "failed") { setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`); @@ -123,6 +143,16 @@ export function MediaPanel(props: { selectedDayIso: string | null }) { setViewerError(null); setVideoFallback(null); + if (asset.media_type === "video") { + const playback = await loadVideoPlaybackUrl(asset.id); + const variantLabel = + playback.variant.kind === "video_mp4" + ? "video_mp4" + : playback.variant.kind; + setViewer({ asset, url: playback.url, variant: variantLabel }); + return; + } + const variant: "original" | "thumb_med" | "poster" = "original"; const url = await loadSignedUrl(asset.id, variant); setViewer({ asset, url, variant }); diff --git a/apps/web/app/lib/playback.ts b/apps/web/app/lib/playback.ts new file mode 100644 index 0000000..2a9d812 --- /dev/null +++ b/apps/web/app/lib/playback.ts @@ -0,0 +1,22 @@ +type Variant = { + kind: "video_mp4"; + size: number; + key: string; +}; + +type PlaybackInput = { + originalMimeType: string | null | undefined; + variants: Variant[]; +}; + +export function pickVideoPlaybackVariant(input: PlaybackInput): + | { kind: "video_mp4"; size: number } + | { kind: "original" } { + const mp4Variant = input.variants.find( + (variant) => variant.kind === "video_mp4" && variant.size === 720, + ); + if (mp4Variant) { + return { kind: "video_mp4", size: mp4Variant.size }; + } + return { kind: "original" }; +} diff --git a/apps/web/src/__tests__/prefer-derived.test.ts b/apps/web/src/__tests__/prefer-derived.test.ts new file mode 100644 index 0000000..42df1ac --- /dev/null +++ b/apps/web/src/__tests__/prefer-derived.test.ts @@ -0,0 +1,18 @@ +import { test, expect } from "bun:test"; +import { pickVideoPlaybackVariant } from "../../app/lib/playback"; + +test("prefer mp4 derived over original", () => { + const picked = pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }], + }); + expect(picked).toEqual({ kind: "video_mp4", size: 720 }); +}); + +test("falls back to original when no derived", () => { + const picked = pickVideoPlaybackVariant({ + originalMimeType: "video/x-matroska", + variants: [], + }); + expect(picked).toEqual({ kind: "original" }); +});