feat: prefer derived mp4 playback with fallback
This commit is contained in:
@@ -24,6 +24,7 @@ type SignedUrlResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type PreviewUrlState = Record<string, string | undefined>;
|
type PreviewUrlState = Record<string, string | undefined>;
|
||||||
|
type VideoPlaybackVariant = { kind: "original" } | { kind: "video_mp4"; size: 720 };
|
||||||
|
|
||||||
function startOfDayUtc(iso: string) {
|
function startOfDayUtc(iso: string) {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
@@ -45,7 +46,7 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
|||||||
const [viewer, setViewer] = useState<{
|
const [viewer, setViewer] = useState<{
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
url: string;
|
url: string;
|
||||||
variant: "original" | "thumb_med" | "poster";
|
variant: "original" | "thumb_med" | "poster" | "video_mp4";
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [viewerError, setViewerError] = useState<string | null>(null);
|
const [viewerError, setViewerError] = useState<string | null>(null);
|
||||||
@@ -103,16 +104,35 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
|||||||
|
|
||||||
async function loadSignedUrl(
|
async function loadSignedUrl(
|
||||||
assetId: string,
|
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}`, {
|
const url =
|
||||||
cache: "no-store",
|
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}`);
|
if (!res.ok) throw new Error(`presign_failed:${res.status}`);
|
||||||
const json = (await res.json()) as SignedUrlResponse;
|
const json = (await res.json()) as SignedUrlResponse;
|
||||||
return json.url;
|
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) {
|
async function openViewer(asset: Asset) {
|
||||||
if (asset.status === "failed") {
|
if (asset.status === "failed") {
|
||||||
setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`);
|
setViewerError(`${asset.id}: ${asset.error_message ?? "asset_failed"}`);
|
||||||
@@ -123,6 +143,16 @@ export function MediaPanel(props: { selectedDayIso: string | null }) {
|
|||||||
setViewerError(null);
|
setViewerError(null);
|
||||||
setVideoFallback(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 variant: "original" | "thumb_med" | "poster" = "original";
|
||||||
const url = await loadSignedUrl(asset.id, variant);
|
const url = await loadSignedUrl(asset.id, variant);
|
||||||
setViewer({ asset, url, variant });
|
setViewer({ asset, url, variant });
|
||||||
|
|||||||
22
apps/web/app/lib/playback.ts
Normal file
22
apps/web/app/lib/playback.ts
Normal file
@@ -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" };
|
||||||
|
}
|
||||||
18
apps/web/src/__tests__/prefer-derived.test.ts
Normal file
18
apps/web/src/__tests__/prefer-derived.test.ts
Normal file
@@ -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" });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user