diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/lib/media/processing.ts | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-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.ts | 296 |
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(); - }); -}; |
