summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/media-processing.ts
blob: 738621b7a992e29512b4b18942c116fa1480f94f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { toast } from "sonner";
import {
	getFileType,
	generateVideoThumbnail,
	getMediaDuration,
	getImageDimensions,
	type MediaItem,
} from "@/stores/media-store";
import { generateThumbnail, getVideoInfo } from "./ffmpeg-utils";

export interface ProcessedMediaItem extends Omit<MediaItem, "id"> {}

export async function processMediaFiles(
	files: FileList | File[],
	onProgress?: (progress: number) => void,
): Promise<ProcessedMediaItem[]> {
	const fileArray = Array.from(files);
	const processedItems: ProcessedMediaItem[] = [];

	const total = fileArray.length;
	let completed = 0;

	for (const file of fileArray) {
		const fileType = getFileType(file);

		if (!fileType) {
			toast.error(`Unsupported file type: ${file.name}`);
			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;

		try {
			if (fileType === "image") {
				// Get image dimensions
				const dimensions = await getImageDimensions(file);
				width = dimensions.width;
				height = dimensions.height;
			} else if (fileType === "video") {
				try {
					// Use FFmpeg for comprehensive video info extraction
					const videoInfo = await getVideoInfo(file);
					duration = videoInfo.duration;
					width = videoInfo.width;
					height = videoInfo.height;
					fps = videoInfo.fps;

					// Generate thumbnail using FFmpeg
					thumbnailUrl = await generateThumbnail(file, 1);
				} catch (error) {
					console.warn(
						"FFmpeg processing failed, falling back to basic processing:",
						error,
					);
					// Fallback to basic processing
					const videoResult = await generateVideoThumbnail(file);
					thumbnailUrl = videoResult.thumbnailUrl;
					width = videoResult.width;
					height = videoResult.height;
					duration = await getMediaDuration(file);
					// FPS will remain undefined for fallback
				}
			} else if (fileType === "audio") {
				// For audio, we don't set width/height/fps (they'll be undefined)
				duration = await getMediaDuration(file);
			}

			processedItems.push({
				name: file.name,
				type: fileType,
				file,
				url,
				thumbnailUrl,
				duration,
				width,
				height,
				fps,
			});

			// Yield back to the event loop to keep the UI responsive
			await new Promise((resolve) => setTimeout(resolve, 0));

			completed += 1;
			if (onProgress) {
				const percent = Math.round((completed / total) * 100);
				onProgress(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 processedItems;
}