dragonflight/services/editor/apps/web/src/utils/media-recovery.ts
Zac b68f0c6aba feat(editor): integrate openreel-video as services/editor with MAM hooks
Vendored Augani/openreel-video (MIT) into services/editor and wired it to the MAM. Editor runs as its own container on port 47435. Library assets pull in via ?asset=<uuid>; render exports route back via POST /api/v1/upload/simple. Sidebar Editor link on every page; Edit button on every preview modal. See services/editor/INTEGRATION.md for the patch map.
2026-05-17 21:44:37 -04:00

100 lines
2.1 KiB
TypeScript

import type { MediaItem } from "@openreel/core";
export async function generateThumbnailFromBlob(
blob: Blob,
type: "video" | "audio" | "image",
): Promise<string | null> {
if (type === "audio") {
return null;
}
if (type === "image") {
return URL.createObjectURL(blob);
}
return new Promise((resolve) => {
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.preload = "metadata";
const cleanup = () => {
URL.revokeObjectURL(video.src);
video.remove();
};
video.onloadeddata = () => {
video.currentTime = 0.1;
};
video.onseeked = () => {
try {
const canvas = document.createElement("canvas");
canvas.width = Math.min(video.videoWidth, 320);
canvas.height = Math.min(
video.videoHeight,
(320 / video.videoWidth) * video.videoHeight,
);
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(
(thumbBlob) => {
cleanup();
if (thumbBlob) {
resolve(URL.createObjectURL(thumbBlob));
} else {
resolve(null);
}
},
"image/jpeg",
0.7,
);
} else {
cleanup();
resolve(null);
}
} catch {
cleanup();
resolve(null);
}
};
video.onerror = () => {
cleanup();
resolve(null);
};
setTimeout(() => {
cleanup();
resolve(null);
}, 5000);
video.src = URL.createObjectURL(blob);
});
}
export async function restoreMediaItem(
item: MediaItem,
storedBlob: Blob | undefined,
): Promise<MediaItem> {
const blob = storedBlob || item.blob;
if (!blob) {
return item;
}
let thumbnailUrl = item.thumbnailUrl;
if (!thumbnailUrl || thumbnailUrl.startsWith("blob:")) {
thumbnailUrl = await generateThumbnailFromBlob(blob, item.type);
}
return {
...item,
blob,
thumbnailUrl,
filmstripThumbnails: undefined,
};
}