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.
100 lines
2.1 KiB
TypeScript
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,
|
|
};
|
|
}
|