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
JavaScript
'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