2026-02-25 10:06:50 +01:00
|
|
|
import type { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
|
|
|
import type { ConverterEngine, WASMModuleState } from '@/types/media';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WASM module loading state
|
|
|
|
|
*/
|
|
|
|
|
const moduleState: WASMModuleState = {
|
|
|
|
|
ffmpeg: false,
|
|
|
|
|
imagemagick: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cached WASM instances
|
|
|
|
|
*/
|
|
|
|
|
let ffmpegInstance: FFmpeg | null = null;
|
|
|
|
|
let imagemagickInstance: any = null;
|
|
|
|
|
|
2026-02-25 10:44:49 +01:00
|
|
|
/**
|
|
|
|
|
* Helper to fetch a file with local priority and CDN fallback
|
|
|
|
|
*/
|
|
|
|
|
async function fetchWithFallback(localPath: string, cdnUrl: string): Promise<ArrayBuffer> {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(localPath);
|
|
|
|
|
if (!response.ok) throw new Error(`Local fetch failed: ${response.status}`);
|
|
|
|
|
console.log(`[WASM] Loaded from local: ${localPath}`);
|
|
|
|
|
return await response.arrayBuffer();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn(`[WASM] Local load failed for ${localPath}, falling back to CDN:`, e);
|
|
|
|
|
const response = await fetch(cdnUrl);
|
|
|
|
|
if (!response.ok) throw new Error(`CDN fetch failed: ${response.status}`);
|
|
|
|
|
console.log(`[WASM] Loaded from CDN: ${cdnUrl}`);
|
|
|
|
|
return await response.arrayBuffer();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper to create a blob URL from a fallback fetch
|
|
|
|
|
*/
|
|
|
|
|
async function getBlobUrl(localPath: string, cdnUrl: string, mimeType: string): Promise<string> {
|
|
|
|
|
const buffer = await fetchWithFallback(localPath, cdnUrl);
|
|
|
|
|
const blob = new Blob([buffer], { type: mimeType });
|
|
|
|
|
return URL.createObjectURL(blob);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 10:06:50 +01:00
|
|
|
/**
|
|
|
|
|
* Load FFmpeg WASM module
|
|
|
|
|
*/
|
|
|
|
|
export async function loadFFmpeg(): Promise<FFmpeg> {
|
|
|
|
|
if (ffmpegInstance && moduleState.ffmpeg) {
|
|
|
|
|
return ffmpegInstance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { FFmpeg } = await import('@ffmpeg/ffmpeg');
|
|
|
|
|
ffmpegInstance = new FFmpeg();
|
|
|
|
|
|
|
|
|
|
ffmpegInstance.on('log', ({ message }) => {
|
|
|
|
|
console.log('[FFmpeg]', message);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-25 10:44:49 +01:00
|
|
|
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
|
|
|
|
|
|
|
|
|
|
// Load core and dependencies with local priority
|
|
|
|
|
const coreURL = await getBlobUrl('/wasm/ffmpeg-core.js', `${baseURL}/ffmpeg-core.js`, 'text/javascript');
|
|
|
|
|
const wasmURL = await getBlobUrl('/wasm/ffmpeg-core.wasm', `${baseURL}/ffmpeg-core.wasm`, 'application/wasm');
|
|
|
|
|
|
|
|
|
|
await ffmpegInstance.load({ coreURL, wasmURL });
|
2026-02-25 10:06:50 +01:00
|
|
|
|
|
|
|
|
moduleState.ffmpeg = true;
|
|
|
|
|
console.log('FFmpeg loaded successfully');
|
|
|
|
|
|
|
|
|
|
return ffmpegInstance;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to load FFmpeg:', error);
|
|
|
|
|
throw new Error('Failed to load FFmpeg WASM module');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load ImageMagick WASM module
|
|
|
|
|
*/
|
|
|
|
|
export async function loadImageMagick(): Promise<any> {
|
|
|
|
|
if (imagemagickInstance && moduleState.imagemagick) {
|
|
|
|
|
return imagemagickInstance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { initializeImageMagick } = await import('@imagemagick/magick-wasm');
|
|
|
|
|
|
2026-02-25 10:44:49 +01:00
|
|
|
const localWasmUrl = '/wasm/magick.wasm';
|
|
|
|
|
const cdnUrl = 'https://unpkg.com/@imagemagick/magick-wasm@0.0.38/dist/magick.wasm';
|
2026-02-25 10:06:50 +01:00
|
|
|
|
2026-02-25 10:44:49 +01:00
|
|
|
const arrayBuffer = await fetchWithFallback(localWasmUrl, cdnUrl);
|
|
|
|
|
console.log('[ImageMagick] WASM file size:', arrayBuffer.byteLength, 'bytes');
|
|
|
|
|
|
|
|
|
|
await initializeImageMagick(arrayBuffer);
|
2026-02-25 10:06:50 +01:00
|
|
|
|
|
|
|
|
const ImageMagick = await import('@imagemagick/magick-wasm');
|
|
|
|
|
imagemagickInstance = ImageMagick;
|
|
|
|
|
moduleState.imagemagick = true;
|
|
|
|
|
console.log('[ImageMagick] Loaded and initialized successfully');
|
|
|
|
|
|
|
|
|
|
return imagemagickInstance;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[ImageMagick] Failed to load:', error);
|
|
|
|
|
throw new Error(`Failed to load ImageMagick WASM module: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get loaded module state
|
|
|
|
|
*/
|
|
|
|
|
export function getModuleState(): WASMModuleState {
|
|
|
|
|
return { ...moduleState };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a specific module is loaded
|
|
|
|
|
*/
|
|
|
|
|
export function isModuleLoaded(engine: ConverterEngine): boolean {
|
|
|
|
|
return moduleState[engine];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load appropriate WASM module for converter engine
|
|
|
|
|
*/
|
|
|
|
|
export async function loadModule(engine: ConverterEngine): Promise<any> {
|
|
|
|
|
switch (engine) {
|
|
|
|
|
case 'ffmpeg':
|
|
|
|
|
return loadFFmpeg();
|
|
|
|
|
case 'imagemagick':
|
|
|
|
|
return loadImageMagick();
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unknown converter engine: ${engine}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unload all WASM modules and free memory
|
|
|
|
|
*/
|
|
|
|
|
export function unloadAll(): void {
|
|
|
|
|
if (ffmpegInstance) {
|
|
|
|
|
// FFmpeg doesn't have an explicit unload method
|
|
|
|
|
// Just null the instance
|
|
|
|
|
ffmpegInstance = null;
|
|
|
|
|
moduleState.ffmpeg = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (imagemagickInstance) {
|
|
|
|
|
imagemagickInstance = null;
|
|
|
|
|
moduleState.imagemagick = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('All WASM modules unloaded');
|
|
|
|
|
}
|