Quick Compression is an open-source, modular file compression and conversion tool built with Next.js, TypeScript, and FFmpeg. It supports audio, video, and image compression with format conversion capabilities, all processed locally in your browser for privacy—no files are uploaded to a server. The app features real-time progress tracking, detailed error handling, and a modern UI for selecting compression and conversion options.
- Type Conversion Support: Added format conversion for audio (AAC, MP3, Opus/Vorbis) and video (WebM) with limited, optimized options for better compatibility.
- Fixed Conversion Issues: Resolved codec mapping and argument handling issues for reliable format conversion.
- Updated Compression Settings UI: Redesigned settings interface for better user experience with conversion options.
- Audio Format Selector Fix: The audio format selector now correctly maps codecs to output formats and MIME types (e.g., Opus uses OGG).
- Removed PDF Support: PDF functionality has been removed as it only provided basic optimization rather than true compression.
- Improved FFmpeg Argument Handling: Audio and video compression now use robust argument construction for better compatibility and output reliability.
- Detailed Logging & Error Handling: All compression hooks provide detailed logs and user-friendly error messages for easier debugging.
- UI Updates for Image/Video Settings: Resolution selection for images and videos now uses presets (e.g., 720p, 1080p) with support for custom values, improving usability and state synchronization.
- Architecture Overview
- Project Structure
- Compression and Conversion Flow
- Adding New File Types
- Debugging Guide
- Development Workflow
- Performance Considerations
- Troubleshooting
- Modular Architecture: Each compression type has its own specialized hook
- Shared State Management: Single FFmpeg instance with global state using Zustand
- Type Safety: Comprehensive TypeScript coverage with centralized types
- Separation of Concerns: Clear separation between UI, business logic, and utilities
- Scalability: Easy to add new compression types without touching existing code
- Local-Only Processing: All compression is performed in-browser for privacy—no uploads.
The compression system follows a modular architecture where each file type has its own specialized hook, promoting code reusability and maintainability:
useCompression (Main Orchestrator - 96 lines)
├── useFFmpeg (Shared FFmpeg Instance - 140 lines)
│ ├── Manages FFmpeg.wasm loading and instance lifecycle
│ ├── Provides global progress tracking and error handling
│ └── Handles virtual file system operations
├── useAudioCompression (67 lines) - FFmpeg-based
│ ├── Handles audio compression and format conversion (AAC, MP3, Opus/Vorbis)
│ ├── Includes codec-to-format mapping and detailed logging
│ └── Supports multiple audio formats with optimized settings
├── useVideoCompression (69 lines) - FFmpeg-based
│ ├── Manages video compression and conversion to WebM format
│ ├── Provides resolution and bitrate optimization
│ └── Includes comprehensive error handling and logging
├── useImageCompression (72 lines) - Canvas API-based
│ ├── Performs client-side image resizing and format conversion
│ ├── Uses HTML5 Canvas for efficient processing
│ └── Handles various image formats (JPEG, PNG, WebP)
Each compression hook is self-contained, receives options from Zustand stores, and integrates seamlessly with the shared FFmpeg instance for consistent state management.
The application uses Zustand for global state management with three specialized stores, each handling separated concerns for better modularity and maintainability:
Zustand Stores:
├── file-store.ts - File management and results
│ ├── Manages uploaded files, compression results, and file metadata
│ ├── Tracks file processing status and download URLs
│ └── Handles file queue and batch operations
├── settings-store.ts - Compression settings and options
│ ├── Stores user-configurable compression parameters (quality, format, resolution)
│ ├── Provides preset configurations for different use cases
│ ├── Syncs settings across UI components using reactive state
│ └── Validates and persists user preferences
└── compression-store.ts - Compression state and progress
├── Tracks real-time compression progress and status
├── Manages FFmpeg instance state and error handling
├── Coordinates between compression hooks and UI updates
└── Handles global compression lifecycle events
Each store follows the single responsibility principle, ensuring clean separation of concerns and easy testing.
src/
├── app/
│ ├── page.tsx # Main application page (404 lines)
│ ├── layout.tsx # Root layout with providers
│ └── globals.css # Global styles
├── components/
│ ├── ui/ # Shadcn/ui components
│ ├── FileDropZone.tsx # File upload interface
│ ├── CompressionSettings.tsx # Settings panel with conversion options
│ ├── ProgressDisplay.tsx # Progress and stats
│ ├── FileListResults.tsx # Results display
│ ├── settings/
│ │ ├── AudioSettings.tsx # Audio settings with format conversion
│ │ ├── ImageSettings.tsx # Image settings with resolution presets
│ │ └── VideoSettings.tsx # Video settings with resolution presets
├── lib/ # Core compression hooks
│ ├── useFFmpeg.ts # Shared FFmpeg instance
│ ├── useCompression.tsx # Main orchestrator
│ ├── useAudioCompression.ts # Audio-specific logic (with codec-to-format mapping, detailed logging)
│ ├── useVideoCompression.ts # Video-specific logic (improved argument handling, logging)
│ ├── useImageCompression.ts # Image-specific logic (canvas-based, error handling)
├── store/ # Zustand state stores
│ ├── file-store.ts # File management
│ ├── settings-store.ts # Settings state
│ └── compression-store.ts # Compression state
├── types/
│ └── index.ts # Centralized TypeScript types
└── utils/ # Utility functions
├── compression-helpers.ts # Core compression utilities
├── compression-defaults.ts # Smart default settings
├── ffmpeg-args-builder.ts # FFmpeg argument construction
├── file-compression.ts # File type detection
├── file-size-formatter.ts # Size formatting
├── file-name-generator.ts # Name generation
└── download-helper.ts # Download utilities
graph TD
A[User drops files] --> B[FileDropZone validates files]
B --> C[Files added to file-store]
C --> D[UI updates with file list]
D --> E[User clicks compress]
graph TD
A[compressFile called] --> B[getCompressionType detects file type]
B --> C{File Type?}
C -->|Audio| D[useAudioCompression: compress & convert (AAC/MP3/Opus)]
C -->|Video| E[useVideoCompression: compress & convert to WebM]
C -->|Image| F[useImageCompression: compress & convert (JPEG/PNG/WebP)]
D --> H[FFmpeg processes with conversion]
E --> H
F --> I[Canvas API processes with conversion]
H --> K[Progress updates via global state]
I --> K
K --> L[Converted/compressed blob returned]
L --> M[File store updated with results]
graph TD
A[useFFmpeg - Global State] --> B[Progress Updates]
A --> C[Error Handling]
A --> D[FFmpeg Instance]
B --> E[All compression hooks]
C --> E
D --> E
E --> F[UI Components]
graph TD
A[User adjusts settings] --> B[UI components update settings-store]
B --> C[settings-store persists options]
C --> D[useCompression retrieves options]
D --> E[Options passed to specific compression hooks]
E --> F[Hooks apply settings during compression]
F --> G[Results reflect user preferences]
Options are centrally managed in Zustand stores, ensuring consistent configuration across all compression operations and UI components.
// types/index.ts
export interface CompressionOptions {
// Add your new compression options
newFileTypeQuality?: number;
newFileTypeFormat?: string;
// ... other options
}// lib/useNewFileTypeCompression.ts
"use client"
import { useCallback } from 'react';
import { CompressionOptions } from '@/types';
import { useFFmpeg } from './useFFmpeg';
import { useSettingsStore } from '@/store/settings-store';
export const useNewFileTypeCompression = () => {
const { setCompressionProgress, clearError } = useFFmpeg();
const newFileTypeOptions = useSettingsStore(state => state.newFileTypeOptions);
const compressNewFileType = useCallback(async (
file: File
): Promise<Blob> => {
clearError();
setCompressionProgress(0);
try {
// Retrieve options from Zustand store
const options = { ...newFileTypeOptions };
// Your compression logic here
setCompressionProgress(50);
// Process the file using options from store
const result = await processFile(file, options);
setCompressionProgress(100);
return result;
} catch (err) {
throw new Error(`Compression failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
setTimeout(() => setCompressionProgress(null), 1000);
}
}, [setCompressionProgress, clearError]);
return { compressNewFileType };
};// lib/useCompression.tsx
import { useNewFileTypeCompression } from './useNewFileTypeCompression';
export const useCompression = () => {
// Add the new hook (options are retrieved internally from Zustand)
const { compressNewFileType } = useNewFileTypeCompression();
// Add wrapper function
const handleNewFileTypeCompression = async (file: File): Promise<Blob> => {
setIsCompressing(true);
try {
return await compressNewFileType(file);
} finally {
setIsCompressing(false);
}
};
return {
// Add to exports
compressNewFileType: handleNewFileTypeCompression,
// ... other exports
};
};// utils/file-compression.ts
export function getCompressionType(file: File): 'audio' | 'video' | 'image' | 'newFileType' | null {
// Add your file type detection logic
if (file.type === 'application/your-type' || file.name.toLowerCase().endsWith('.ext')) {
return 'newFileType';
}
// ... existing logic
}// app/page.tsx
const { compressNewFileType } = useCompression(compressionOptions);
// In the compression logic:
if (compressionType === 'newFileType') {
compressedBlob = await compressNewFileType(file, compressionOptions);
}// utils/compression-defaults.ts
export function getSmartDefaults(file: File, type: string): CompressionOptions {
switch (type) {
case 'newFileType':
return {
newFileTypeQuality: fileSizeMB > 10 ? 0.7 : 0.9,
newFileTypeFormat: 'optimized'
};
// ... existing cases
}
}// Enable FFmpeg logging
ffmpegInstance.on('log', ({ message }) => {
console.log('FFmpeg Log:', message);
});// Check global state updates
console.log('Global progress:', globalCompressionProgress);
console.log('Listeners count:', listeners.size);// In getCompressionType function
console.log('File type:', file.type);
console.log('File name:', file.name);
console.log('Detected type:', detectedType);// In compression hooks
console.log('Compression options:', options);
console.log('FFmpeg args:', args);- Format Conversion: Audio supports conversion to AAC, MP3, and Opus/Vorbis formats. Video converts to WebM. Check codec compatibility for source files.
- Codec-to-Format Mapping: Audio compression uses mapping to ensure correct output format and MIME type (e.g., Opus → OGG).
- FFmpeg Argument Validation: Arguments are validated and logged before running FFmpeg. Check browser console for detailed logs if conversion fails.
- Error Handling: All hooks throw user-friendly errors and log technical details for easier troubleshooting.
- Resolution Presets: Image and video settings use select menus for common resolutions (720p, 1080p, etc.), with custom value support. State is synchronized using Zustand.
- State Synchronization: Check Zustand store updates for settings changes. Use browser dev tools to inspect store state.
// Debug settings store
import { useSettingsStore } from '@/store/settings-store';
const settings = useSettingsStore.getState();
console.log('Current settings:', settings);
// Debug file store
import { useFileStore } from '@/store/file-store';
const files = useFileStore.getState();
console.log('Current files:', files);
// Debug compression store
import { useCompressionStore } from '@/store/compression-store';
const compressionState = useCompressionStore.getState();
console.log('Compression state:', compressionState);- FFmpeg not loading: Check browser console for WASM errors
- Progress not updating: Verify useFFmpeg singleton pattern
- File type not detected: Check MIME types and file extensions
- Compression failing: Enable FFmpeg logging to see detailed errors
# Development mode
pnpm dev
# Build for production
pnpm build
pnpm start
# Type checking
pnpm type-check
# Linting
pnpm lint# Add new compression library
pnpm add new-compression-lib
# Add development dependency
pnpm add -D @types/new-lib- Small files first: Test with small files to debug logic
- Different formats: Test various file formats for your type
- Edge cases: Test corrupted files, very large files, unsupported formats
- Progress tracking: Ensure progress updates correctly
- Error handling: Test error scenarios
- FFmpeg operations can use significant memory
- Clean up virtual files after compression
- Consider implementing file size limits
- Large files may cause browser to freeze
- Consider Web Workers for heavy processing
- Implement chunked processing for very large files
// Optimize FFmpeg arguments for speed
const args = ['-preset', 'ultrafast', '-crf', '28'];
// Clean up immediately after use
await ffmpegInstance.deleteFile(inputFileName);
await ffmpegInstance.deleteFile(outputFileName);
// Use appropriate quality settings
const quality = fileSizeMB > 100 ? 'high' : 'medium';# Check if FFmpeg resources are accessible
curl https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.wasm# Regenerate types
pnpm type-check
# Clear Next.js cache
rm -rf .next
pnpm dev- Check if multiple useFFmpeg instances are created
- Verify singleton pattern is working
- Check listener registration
- Check import paths are correct
- Verify all types are properly exported
- Ensure dependencies are installed
lib/useFFmpeg.ts: Singleton pattern, global state managementlib/useCompression.tsx: Main orchestrator, how hooks are combinedutils/ffmpeg-args-builder.ts: How FFmpeg arguments are constructedutils/compression-defaults.ts: Smart defaults based on file sizestore/file-store.ts: File state management with Zustand
- Choose Appropriate Output Formats: Use the limited conversion options (WebM for video, AAC/MP3/Opus for audio) for best compatibility and performance.
- Use Preset Selectors for Resolution: Prefer using the built-in resolution presets for images and videos to ensure optimal results and avoid invalid input.
- Check Codec/Format Mapping: When compressing audio, verify the codec-to-format mapping to ensure correct output.
- Enable Logging for Debugging: Use browser console logs to trace FFmpeg arguments and error messages.
- Test UI State Sync: When updating settings, verify that the UI reflects the current state, especially for custom resolution values.
- Leverage Zustand for State Management: Use the separated store logics (settings, files, compression) for clean state management and avoid prop drilling.
- Keep hooks under 100 lines when possible
- Always handle errors gracefully with user-friendly messages
- Update progress regularly for good UX
- Clean up resources after compression
- Use TypeScript strictly - avoid
anytypes - Test with real files of various sizes and formats
- Document new compression parameters in types
- Follow the established patterns when adding new features
Happy coding! The modular architecture makes it easy to add new compression types while maintaining clean, maintainable code.