UNPKG

compresswave

Version:

🌊 Powerful file compression library that runs entirely in the browser. No uploads, no servers, complete privacy.

472 lines (465 loc) • 16.9 kB
'use strict'; var ffmpeg = require('@ffmpeg/ffmpeg'); var util = require('@ffmpeg/util'); var imageCompression = require('browser-image-compression'); const COMPRESSION_PRESETS = { web: { name: 'Web Optimized', video: { codec: 'h264', crf: 28, resolution: { width: 1280, height: 720 }, audio: { codec: 'aac', bitrate: '128k' } }, image: { quality: 0.8, maxWidth: 1920, format: 'auto', progressive: true } }, mobile: { name: 'Mobile Friendly', video: { codec: 'h264', crf: 32, resolution: { width: 854, height: 480 }, audio: { codec: 'aac', bitrate: '96k' } }, image: { quality: 0.7, maxWidth: 1280, format: 'auto' } }, social: { name: 'Social Media', video: { codec: 'h264', crf: 30, resolution: { width: 1080, height: 1080 }, audio: { codec: 'aac', bitrate: '128k' } }, image: { quality: 0.75, maxWidth: 1080, format: 'auto' } }, email: { name: 'Email Attachment', video: { codec: 'h264', crf: 35, resolution: { width: 640, height: 360 }, audio: { codec: 'aac', bitrate: '64k' } }, image: { quality: 0.6, maxWidth: 800, format: 'auto' } } }; function getPreset(presetName) { return COMPRESSION_PRESETS[presetName]; } function listPresets() { return Object.keys(COMPRESSION_PRESETS); } class VideoCompressor { constructor(options = {}) { this.ffmpeg = new ffmpeg.FFmpeg(); this.options = { preset: 'web', codec: 'h264', crf: 28, ...options }; // Apply preset settings if specified if (this.options.preset && this.options.preset !== 'custom') { const preset = getPreset(this.options.preset); if (preset === null || preset === void 0 ? void 0 : preset.video) { this.options = { ...preset.video, ...this.options }; } } this.setupFFmpeg(); } async setupFFmpeg() { var _a, _b; try { // Setup progress listener this.ffmpeg.on('progress', ({ progress }) => { var _a, _b; const percentage = Math.round(progress * 100); (_b = (_a = this.options).onProgress) === null || _b === void 0 ? void 0 : _b.call(_a, percentage); }); this.ffmpeg.on('log', ({ message }) => { console.log('[FFmpeg]', message); }); // Load FFmpeg const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm'; await this.ffmpeg.load({ coreURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); } catch (error) { console.error('Failed to load FFmpeg:', error); (_b = (_a = this.options).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error); } } async compress(file) { var _a, _b, _c, _d; const startTime = performance.now(); try { // Ensure FFmpeg is loaded if (!this.ffmpeg.loaded) { await this.setupFFmpeg(); } // Write input file await this.ffmpeg.writeFile(file.name, await util.fetchFile(file)); // Build FFmpeg command const outputName = `compressed_${file.name.replace(/\.[^/.]+$/, '')}.mp4`; const command = this.buildFFmpegCommand(file.name, outputName); // Execute compression await this.ffmpeg.exec(command); // Read output file const data = await this.ffmpeg.readFile(outputName); const blob = new Blob([data.buffer], { type: 'video/mp4' }); const url = URL.createObjectURL(blob); // Calculate metrics const originalSize = file.size; const compressedSize = blob.size; const compressionRatio = Math.round(((originalSize - compressedSize) / originalSize) * 100); const processingTime = performance.now() - startTime; const result = { originalSize, compressedSize, compressionRatio, processingTime, blob, url }; (_b = (_a = this.options).onComplete) === null || _b === void 0 ? void 0 : _b.call(_a, result); return result; } catch (error) { (_d = (_c = this.options).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error); throw error; } } buildFFmpegCommand(inputName, outputName) { const command = ['-i', inputName]; // Video codec command.push('-c:v', this.options.codec || 'h264'); // CRF (quality) if (this.options.crf !== undefined) { command.push('-crf', this.options.crf.toString()); } // Resolution if (this.options.resolution) { const { width, height } = this.options.resolution; command.push('-vf', `scale=${width}:${height}`); } // Framerate if (this.options.framerate) { command.push('-r', this.options.framerate.toString()); } // Video bitrate if (this.options.bitrate) { command.push('-b:v', this.options.bitrate); } // Audio settings if (this.options.audio) { if (this.options.audio.codec) { command.push('-c:a', this.options.audio.codec); } if (this.options.audio.bitrate) { command.push('-b:a', this.options.audio.bitrate); } if (this.options.audio.sampleRate) { command.push('-ar', this.options.audio.sampleRate.toString()); } } // Output settings command.push('-preset', 'medium'); command.push('-movflags', '+faststart'); command.push('-y'); // Overwrite output file command.push(outputName); return command; } async terminate() { await this.ffmpeg.terminate(); } } class ImageOptimizer { constructor(options = {}) { this.options = { quality: 0.8, maxWidth: 1920, format: 'auto', progressive: true, preserveExif: false, ...options }; // Apply preset settings if specified if (this.options.preset && this.options.preset !== 'custom') { const preset = getPreset(this.options.preset); if (preset === null || preset === void 0 ? void 0 : preset.image) { this.options = { ...preset.image, ...this.options }; } } } async process(file) { var _a, _b, _c, _d; const startTime = performance.now(); try { // Determine output format const outputFormat = this.getOutputFormat(file); // Configure compression options const compressionOptions = { maxSizeMB: undefined, // We'll use quality instead maxWidthOrHeight: Math.max(this.options.maxWidth || 1920, this.options.maxHeight || 1920), useWebWorker: true, fileType: outputFormat, initialQuality: this.options.quality || 0.8, alwaysKeepResolution: false, preserveExif: this.options.preserveExif || false, onProgress: (progress) => { var _a, _b; (_b = (_a = this.options).onProgress) === null || _b === void 0 ? void 0 : _b.call(_a, Math.round(progress * 100)); } }; // Compress the image const compressedFile = await imageCompression(file, compressionOptions); // Convert to blob if needed let blob; if (compressedFile instanceof Blob) { blob = compressedFile; } else { blob = new Blob([compressedFile], { type: outputFormat }); } // Create URL for the compressed image const url = URL.createObjectURL(blob); // Calculate metrics const originalSize = file.size; const compressedSize = blob.size; const compressionRatio = Math.round(((originalSize - compressedSize) / originalSize) * 100); const processingTime = performance.now() - startTime; const result = { originalSize, compressedSize, compressionRatio, processingTime, blob, url }; (_b = (_a = this.options).onComplete) === null || _b === void 0 ? void 0 : _b.call(_a, result); return result; } catch (error) { (_d = (_c = this.options).onError) === null || _d === void 0 ? void 0 : _d.call(_c, error); throw error; } } getOutputFormat(file) { if (this.options.format && this.options.format !== 'auto') { return this.getMimeType(this.options.format); } // Auto-detect best format based on input const inputType = file.type.toLowerCase(); // For PNG with transparency, keep as PNG or convert to WebP if (inputType.includes('png')) { return 'image/webp'; // WebP supports transparency and better compression } // For JPEG, use WebP for better compression if (inputType.includes('jpeg') || inputType.includes('jpg')) { return 'image/webp'; } // For other formats, default to WebP return 'image/webp'; } getMimeType(format) { const mimeTypes = { 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'png': 'image/png', 'webp': 'image/webp', 'avif': 'image/avif' }; return mimeTypes[format.toLowerCase()] || 'image/jpeg'; } async processMultiple(files) { var _a, _b; const results = []; for (let i = 0; i < files.length; i++) { try { const result = await this.process(files[i]); results.push(result); } catch (error) { console.error(`Failed to process file ${files[i].name}:`, error); (_b = (_a = this.options).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error); } } return results; } // Helper method to resize image to specific dimensions async resize(file, width, height) { const originalOptions = { ...this.options }; this.options.maxWidth = width; this.options.maxHeight = height; try { const result = await this.process(file); this.options = originalOptions; // Restore original options return result; } catch (error) { this.options = originalOptions; // Restore original options throw error; } } // Helper method to convert format async convertFormat(file, format) { const originalOptions = { ...this.options }; this.options.format = format; try { const result = await this.process(file); this.options = originalOptions; // Restore original options return result; } catch (error) { this.options = originalOptions; // Restore original options throw error; } } } class BatchProcessor { constructor(options = {}) { this.options = { concurrency: 2, ...options }; } async compressFiles(files, videoOptions, imageOptions) { var _a, _b; const results = []; // Initialize compressors with options this.videoCompressor = new VideoCompressor(videoOptions); this.imageOptimizer = new ImageOptimizer(imageOptions); // Process files in batches based on concurrency const concurrency = this.options.concurrency || 2; const batches = this.createBatches(files, concurrency); for (const batch of batches) { const batchPromises = batch.map(async ({ file, index }) => { var _a, _b, _c, _d, _e, _f; try { (_b = (_a = this.options).onFileStart) === null || _b === void 0 ? void 0 : _b.call(_a, file, index); const result = await this.processFile(file); results[index] = result; (_d = (_c = this.options).onFileComplete) === null || _d === void 0 ? void 0 : _d.call(_c, file, result, index); return result; } catch (error) { const err = error; (_f = (_e = this.options).onFileError) === null || _f === void 0 ? void 0 : _f.call(_e, file, err, index); throw error; } }); // Wait for current batch to complete before processing next batch await Promise.allSettled(batchPromises); } // Clean up compressors await this.cleanup(); // Call completion callback (_b = (_a = this.options).onAllComplete) === null || _b === void 0 ? void 0 : _b.call(_a, results.filter(Boolean)); return results.filter(Boolean); } async processFile(file) { const fileType = file.type.toLowerCase(); if (fileType.startsWith('video/')) { if (!this.videoCompressor) { throw new Error('Video compressor not initialized'); } return await this.videoCompressor.compress(file); } else if (fileType.startsWith('image/')) { if (!this.imageOptimizer) { throw new Error('Image optimizer not initialized'); } return await this.imageOptimizer.process(file); } else { throw new Error(`Unsupported file type: ${fileType}`); } } createBatches(items, batchSize) { const batches = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize).map((file, batchIndex) => ({ file, index: i + batchIndex })); batches.push(batch); } return batches; } async cleanup() { try { if (this.videoCompressor) { await this.videoCompressor.terminate(); } } catch (error) { console.warn('Error cleaning up video compressor:', error); } } // Helper method to process only video files async compressVideos(files, options) { const videoFiles = files.filter(file => file.type.startsWith('video/')); return this.compressFiles(videoFiles, options); } // Helper method to process only image files async optimizeImages(files, options) { const imageFiles = files.filter(file => file.type.startsWith('image/')); return this.compressFiles(imageFiles, undefined, options); } // Static method to analyze files and return statistics static analyzeFiles(files) { let totalSize = 0; let videoCount = 0; let imageCount = 0; let otherCount = 0; const types = {}; files.forEach(file => { totalSize += file.size; const fileType = file.type.toLowerCase(); types[fileType] = (types[fileType] || 0) + 1; if (fileType.startsWith('video/')) { videoCount++; } else if (fileType.startsWith('image/')) { imageCount++; } else { otherCount++; } }); return { totalSize, videoCount, imageCount, otherCount, types }; } } exports.BatchProcessor = BatchProcessor; exports.COMPRESSION_PRESETS = COMPRESSION_PRESETS; exports.ImageOptimizer = ImageOptimizer; exports.VideoCompressor = VideoCompressor; exports.getPreset = getPreset; exports.listPresets = listPresets; //# sourceMappingURL=index.js.map