summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/media/processing.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/media/processing.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/lib/media/processing.ts')
-rw-r--r--apps/web/src/lib/media/processing.ts296
1 files changed, 0 insertions, 296 deletions
diff --git a/apps/web/src/lib/media/processing.ts b/apps/web/src/lib/media/processing.ts
deleted file mode 100644
index 547e7ed8..00000000
--- a/apps/web/src/lib/media/processing.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny";
-import { toast } from "sonner";
-import { getMediaTypeFromFile } from "@/lib/media/media-utils";
-import { formatStorageBytes } from "@/services/storage/quota";
-import { storageService } from "@/services/storage/service";
-import type { MediaAsset } from "@/lib/media/types";
-import { getVideoInfo } from "./mediabunny";
-
-export interface ProcessedMediaAsset extends Omit<MediaAsset, "id"> {}
-
-const THUMBNAIL_MAX_WIDTH = 1280;
-const THUMBNAIL_MAX_HEIGHT = 720;
-
-const getStorageLimitDescription = ({
- fileSize,
- availableBytes,
-}: {
- fileSize: number;
- availableBytes: number | null;
-}): string => {
- const fileSizeLabel = formatStorageBytes({ bytes: fileSize });
-
- if (availableBytes === null) {
- return `File size is ${fileSizeLabel}.`;
- }
-
- return `File size is ${fileSizeLabel}, but only ${formatStorageBytes({
- bytes: availableBytes,
- })} is safely available in browser storage.`;
-};
-
-const getThumbnailSize = ({
- width,
- height,
-}: {
- width: number;
- height: number;
-}): { width: number; height: number } => {
- const aspectRatio = width / height;
- let targetWidth = width;
- let targetHeight = height;
-
- if (targetWidth > THUMBNAIL_MAX_WIDTH) {
- targetWidth = THUMBNAIL_MAX_WIDTH;
- targetHeight = Math.round(targetWidth / aspectRatio);
- }
- if (targetHeight > THUMBNAIL_MAX_HEIGHT) {
- targetHeight = THUMBNAIL_MAX_HEIGHT;
- targetWidth = Math.round(targetHeight * aspectRatio);
- }
-
- return { width: targetWidth, height: targetHeight };
-};
-
-const renderToThumbnailDataUrl = ({
- width,
- height,
- draw,
-}: {
- width: number;
- height: number;
- draw: ({
- context,
- width,
- height,
- }: {
- context: CanvasRenderingContext2D;
- width: number;
- height: number;
- }) => void;
-}): string => {
- const size = getThumbnailSize({ width, height });
- const canvas = document.createElement("canvas");
- canvas.width = size.width;
- canvas.height = size.height;
- const context = canvas.getContext("2d");
-
- if (!context) {
- throw new Error("Could not get canvas context");
- }
-
- draw({ context, width: size.width, height: size.height });
- return canvas.toDataURL("image/jpeg", 0.8);
-};
-
-async function generateThumbnail({
- videoFile,
- timeInSeconds,
-}: {
- videoFile: File;
- timeInSeconds: number;
-}): Promise<string> {
- const input = new Input({
- source: new BlobSource(videoFile),
- formats: ALL_FORMATS,
- });
-
- const videoTrack = await input.getPrimaryVideoTrack();
- if (!videoTrack) {
- throw new Error("No video track found in the file");
- }
-
- const canDecode = await videoTrack.canDecode();
- if (!canDecode) {
- throw new Error("Video codec not supported for decoding");
- }
-
- const sink = new VideoSampleSink(videoTrack);
-
- const frame = await sink.getSample(timeInSeconds);
-
- if (!frame) {
- throw new Error("Could not get frame at specified time");
- }
-
- try {
- return renderToThumbnailDataUrl({
- width: videoTrack.displayWidth,
- height: videoTrack.displayHeight,
- draw: ({ context, width, height }) => {
- frame.draw(context, 0, 0, width, height);
- },
- });
- } finally {
- frame.close();
- }
-}
-
-async function generateImageThumbnail({
- imageFile,
-}: {
- imageFile: File;
-}): Promise<{ thumbnailUrl: string; width: number; height: number }> {
- return new Promise((resolve, reject) => {
- const image = new window.Image();
- const objectUrl = URL.createObjectURL(imageFile);
-
- image.addEventListener("load", () => {
- try {
- const thumbnailUrl = renderToThumbnailDataUrl({
- width: image.naturalWidth,
- height: image.naturalHeight,
- draw: ({ context, width, height }) => {
- context.drawImage(image, 0, 0, width, height);
- },
- });
- resolve({
- thumbnailUrl,
- width: image.naturalWidth,
- height: image.naturalHeight,
- });
- } catch (error) {
- reject(
- error instanceof Error ? error : new Error("Could not render image"),
- );
- } finally {
- URL.revokeObjectURL(objectUrl);
- image.remove();
- }
- });
-
- image.addEventListener("error", () => {
- URL.revokeObjectURL(objectUrl);
- image.remove();
- reject(new Error("Could not load image"));
- });
-
- image.src = objectUrl;
- });
-}
-
-export async function processMediaAssets({
- files,
- onProgress,
-}: {
- files: FileList | File[];
- onProgress?: ({ progress }: { progress: number }) => void;
-}): Promise<ProcessedMediaAsset[]> {
- const fileArray = Array.from(files);
- const processedAssets: ProcessedMediaAsset[] = [];
-
- const total = fileArray.length;
- let completed = 0;
-
- for (const file of fileArray) {
- const fileType = getMediaTypeFromFile({ file });
-
- if (!fileType) {
- toast.error(`Unsupported file type: ${file.name}`);
- continue;
- }
-
- const storageCheck = await storageService.canStoreFile({
- size: file.size,
- });
-
- if (!storageCheck.canStore) {
- toast.error(`Not enough browser storage for ${file.name}`, {
- description: getStorageLimitDescription({
- fileSize: file.size,
- availableBytes: storageCheck.availableBytes,
- }),
- });
- continue;
- }
-
- const url = URL.createObjectURL(file);
- let thumbnailUrl: string | undefined;
- let duration: number | undefined;
- let width: number | undefined;
- let height: number | undefined;
- let fps: number | undefined;
- let hasAudio: boolean | undefined;
-
- try {
- if (fileType === "image") {
- const result = await generateImageThumbnail({ imageFile: file });
- thumbnailUrl = result.thumbnailUrl;
- width = result.width;
- height = result.height;
- } else if (fileType === "video") {
- try {
- const videoInfo = await getVideoInfo({ videoFile: file });
- duration = videoInfo.duration;
- width = videoInfo.width;
- height = videoInfo.height;
- fps = Number.isFinite(videoInfo.fps)
- ? Math.round(videoInfo.fps)
- : undefined;
- hasAudio = videoInfo.hasAudio;
-
- thumbnailUrl = await generateThumbnail({
- videoFile: file,
- timeInSeconds: 1,
- });
- } catch (error) {
- console.warn("Video processing failed", error);
- }
- } else if (fileType === "audio") {
- // For audio, we don't set width/height/fps (they'll be undefined)
- duration = await getMediaDuration({ file });
- }
-
- processedAssets.push({
- name: file.name,
- type: fileType,
- file,
- url,
- thumbnailUrl,
- duration,
- width,
- height,
- fps,
- hasAudio,
- });
-
- await new Promise((resolve) => setTimeout(resolve, 0));
-
- completed += 1;
- if (onProgress) {
- const percent = Math.round((completed / total) * 100);
- onProgress({ progress: percent });
- }
- } catch (error) {
- console.error("Error processing file:", file.name, error);
- toast.error(`Failed to process ${file.name}`);
- URL.revokeObjectURL(url); // Clean up on error
- }
- }
-
- return processedAssets;
-}
-
-const getMediaDuration = ({ file }: { file: File }): Promise<number> => {
- return new Promise((resolve, reject) => {
- const element = document.createElement(
- file.type.startsWith("video/") ? "video" : "audio",
- ) as HTMLVideoElement;
- const objectUrl = URL.createObjectURL(file);
-
- element.addEventListener("loadedmetadata", () => {
- resolve(element.duration);
- URL.revokeObjectURL(objectUrl);
- element.remove();
- });
-
- element.addEventListener("error", () => {
- reject(new Error("Could not load media"));
- URL.revokeObjectURL(objectUrl);
- element.remove();
- });
-
- element.src = objectUrl;
- element.load();
- });
-};