V Video Compressor
A professional Flutter plugin for high-quality video compression with real-time progress tracking, thumbnail generation, and comprehensive configuration options.
โจ Key Features
- ๐ฌ Professional Video Compression - 5 quality levels with native platform APIs (Media3 for Android, AVFoundation for iOS)
- ๐ Real-Time Progress Tracking - Smooth progress updates with hybrid estimation algorithm
- ๐ Global Progress Stream - NEW! Typed global stream accessible from anywhere in your app
- ๐ง Advanced Configuration - 20+ compression parameters for professional control
- ๐ผ๏ธ Thumbnail Generation - Extract high-quality thumbnails at any timestamp
- ๐ฑ Cross-Platform - Full Android & iOS support with hardware acceleration
- ๐ Batch Processing - Compress multiple videos with overall progress tracking
- โ Cancellation Support - Cancel operations anytime with automatic cleanup
- ๐ Comprehensive Logging - Production-ready error tracking and debugging
- ๐งช Thoroughly Tested - 95%+ test coverage with complete mock implementations
- โก No ffmpeg - Uses native APIs for optimal performance and smaller app size
๐ฏ Philosophy
This plugin focuses exclusively on video compression and thumbnail generation. For video selection, use established plugins like image_picker
or file_picker
.
๐ฑ Platform Support
Platform | Support | Minimum Version | Notes |
---|---|---|---|
Android | โ Full Support | API 21+ (Android 5.0+) | Hardware acceleration available |
iOS | โ Full Support | iOS 11.0+ | Hardware acceleration available |
๐ Quick Start
1. Installation
Add to your pubspec.yaml
:
dependencies:
v_video_compressor: ^1.2.0
file_picker: ^8.0.0 # For video selection
# OR
image_picker: ^1.0.7 # Alternative for video selection
2. Platform Setup
Android Setup
Add permissions to android/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
iOS Setup
Add permissions to ios/Runner/Info.plist
:
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photo library to compress videos</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to save compressed videos to photo library</string>
3. Basic Usage
import 'package:v_video_compressor/v_video_compressor.dart';
import 'package:file_picker/file_picker.dart';
class VideoCompressionExample extends StatefulWidget {
@override
_VideoCompressionExampleState createState() => _VideoCompressionExampleState();
}
class _VideoCompressionExampleState extends State<VideoCompressionExample> {
final VVideoCompressor _compressor = VVideoCompressor();
double _progress = 0.0;
bool _isCompressing = false;
Future<void> _compressVideo() async {
// 1. Pick video using file_picker
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.video,
);
if (result == null || result.files.single.path == null) return;
final videoPath = result.files.single.path!;
setState(() {
_isCompressing = true;
_progress = 0.0;
});
try {
// 2. Compress with progress tracking
final compressionResult = await _compressor.compressVideo(
videoPath,
const VVideoCompressionConfig.medium(),
onProgress: (progress) {
setState(() => _progress = progress);
},
);
if (compressionResult != null) {
print('โ
Compression completed!');
print('Original: ${compressionResult.originalSizeFormatted}');
print('Compressed: ${compressionResult.compressedSizeFormatted}');
print('Space saved: ${compressionResult.spaceSavedFormatted}');
print('Output path: ${compressionResult.outputPath}');
}
} catch (e) {
print('โ Compression failed: $e');
} finally {
setState(() => _isCompressing = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Video Compressor')),
body: Center(
child: _isCompressing
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(value: _progress),
const SizedBox(height: 16),
Text('${(_progress * 100).toInt()}%'),
],
)
: ElevatedButton(
onPressed: _compressVideo,
child: const Text('Pick & Compress Video'),
),
),
);
}
}
๐ Global Progress Stream (NEW in v1.2.0)
Listen to compression progress from anywhere in your app with the new typed global stream:
Basic Global Stream Usage
import 'package:v_video_compressor/v_video_compressor.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
StreamSubscription<VVideoProgressEvent>? _progressSubscription;
VVideoProgressEvent? _currentProgress;
@override
void initState() {
super.initState();
_setupGlobalProgressListener();
}
void _setupGlobalProgressListener() {
// Listen to global progress stream from anywhere
_progressSubscription = VVideoCompressor.progressStream.listen(
(event) {
setState(() {
_currentProgress = event;
});
print('Progress: ${event.progressFormatted}');
if (event.isBatchOperation) {
print('Batch: ${event.batchProgressDescription}');
}
},
);
}
@override
void dispose() {
_progressSubscription?.cancel();
super.dispose();
}
}
Convenience Methods
// Method 1: Simple progress callback
VVideoCompressor.listenToProgress((progress) {
print('Progress: ${(progress * 100).toInt()}%');
});
// Method 2: Batch progress callback
VVideoCompressor.listenToBatchProgress((progress, currentIndex, total) {
print('Batch: Video ${currentIndex + 1}/$total - ${(progress * 100).toInt()}%');
});
// Method 3: Full event callback
VVideoCompressor.listen((event) {
print('Progress: ${event.progressFormatted}');
print('Video: ${event.videoPath}');
if (event.isBatchOperation) {
print('Batch: ${event.batchProgressDescription}');
}
});
Global Stream Benefits
- โ
Fully Typed: No more
Map
checking - properVVideoProgressEvent
type - โ Global Access: Listen from anywhere in your app (widgets, services, controllers)
- โ Automatic Management: Stream lifecycle handled automatically
- โ Multiple Listeners: Broadcast stream supports multiple listeners
- โ Batch Support: Built-in batch operation detection and progress
- โ Rich Information: Access to video path, progress, batch info, and formatted strings
Use Cases
In Services/Controllers:
class VideoCompressionService {
static StreamSubscription<VVideoProgressEvent>? _subscription;
static void startGlobalListener() {
_subscription = VVideoCompressor.progressStream.listen((event) {
// Update your state management, emit to other streams, etc.
print('Service: ${event.progressFormatted}');
});
}
static void stopGlobalListener() {
_subscription?.cancel();
}
}
With State Management:
class VideoCompressionNotifier extends ChangeNotifier {
VVideoProgressEvent? _currentProgress;
VVideoProgressEvent? get currentProgress => _currentProgress;
void startListening() {
VVideoCompressor.progressStream.listen((event) {
_currentProgress = event;
notifyListeners(); // Notify UI to rebuild
});
}
}
Multiple Widgets:
// Widget A
class ProgressIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<VVideoProgressEvent>(
stream: VVideoCompressor.progressStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return SizedBox();
return LinearProgressIndicator(value: snapshot.data!.progress);
},
);
}
}
// Widget B - completely separate
class ProgressText extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<VVideoProgressEvent>(
stream: VVideoCompressor.progressStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return Text('Ready');
return Text(snapshot.data!.progressFormatted);
},
);
}
}
๐ฏ Quality Levels
Choose the right quality for your use case:
Quality | Resolution | Typical Bitrate | File Size | Use Case |
---|---|---|---|---|
VVideoCompressQuality.high |
1080p HD | 3.5 Mbps | Larger | Professional, archival |
VVideoCompressQuality.medium |
720p | 1.8 Mbps | Balanced | General purpose, social media |
VVideoCompressQuality.low |
480p | 900 kbps | Smaller | Quick sharing, messaging |
VVideoCompressQuality.veryLow |
360p | 500 kbps | Very small | Bandwidth limited |
VVideoCompressQuality.ultraLow |
240p | 350 kbps | Minimal | Maximum compression |
๐ง Advanced Configuration
Custom Compression Settings
final advancedConfig = VVideoAdvancedConfig(
// Resolution & Quality
customWidth: 1280,
customHeight: 720,
videoBitrate: 2000000, // 2 Mbps
frameRate: 30.0, // 30 FPS
// Codec & Encoding
videoCodec: VVideoCodec.h265, // Better compression
audioCodec: VAudioCodec.aac,
encodingSpeed: VEncodingSpeed.medium,
crf: 25, // Quality factor (lower = better)
twoPassEncoding: true, // Better quality
hardwareAcceleration: true, // Use GPU
// Audio Settings
audioBitrate: 128000, // 128 kbps
audioSampleRate: 44100, // 44.1 kHz
audioChannels: 2, // Stereo
// Video Effects
brightness: 0.1, // Slight brightness boost
contrast: 0.05, // Slight contrast increase
saturation: 0.1, // Slight saturation increase
// Editing
trimStartMs: 2000, // Skip first 2 seconds
trimEndMs: 60000, // End at 1 minute
rotation: 90, // Rotate 90 degrees
);
final result = await _compressor.compressVideo(
videoPath,
VVideoCompressionConfig(
quality: VVideoCompressQuality.medium,
advanced: advancedConfig,
),
);
ID-Based Compression Tracking
Track compression operations with custom IDs for better monitoring:
// Compress with custom ID
final result = await _compressor.compressVideo(
videoPath,
const VVideoCompressionConfig.medium(),
onProgress: (progress) {
print('Compression progress: ${(progress * 100).toInt()}%');
},
id: 'my-video-compression-${DateTime.now().millisecondsSinceEpoch}',
);
// Or let the plugin auto-generate an ID
final result2 = await _compressor.compressVideo(
videoPath,
const VVideoCompressionConfig.medium(),
onProgress: (progress) {
print('Progress: ${(progress * 100).toInt()}%');
},
// No ID provided - will auto-generate one
);
Benefits of ID-based tracking:
- โ Better Logging: Each compression operation is logged with its unique ID
- โ Tracking: Monitor specific compression operations in your app
- โ Debugging: Easier to identify which compression operation failed
- โ Analytics: Track compression performance and success rates
Preset Configurations
// Maximum compression for smallest files
final maxCompression = VVideoAdvancedConfig.maximumCompression(
targetBitrate: 300000, // 300 kbps
keepAudio: false, // Remove audio
);
// Social media optimized
final socialMedia = VVideoAdvancedConfig.socialMediaOptimized();
// Mobile optimized
final mobile = VVideoAdvancedConfig.mobileOptimized();
final result = await _compressor.compressVideo(
videoPath,
VVideoCompressionConfig(
quality: VVideoCompressQuality.medium,
advanced: maxCompression, // Use preset
),
);
Advanced Configuration Options
The VVideoAdvancedConfig
class provides fine-grained control over the compression process:
๐ NEW: Automatic Orientation Correction
// Fix for vertical videos appearing horizontal after compression
VVideoAdvancedConfig(
autoCorrectOrientation: true, // Preserves original video orientation
videoBitrate: 1500000,
audioBitrate: 128000,
)
Key Features:
- โ Automatic Detection: Detects original video orientation metadata
- โ Vertical Video Fix: Prevents vertical videos from appearing horizontal
- โ Cross-Platform: Works on both Android and iOS
- โ No Quality Loss: Maintains video quality while preserving orientation
Other Advanced Options
VVideoAdvancedConfig(
videoBitrate: 1500000, // Custom video bitrate
audioBitrate: 128000, // Custom audio bitrate
customWidth: 1280, // Custom width (use with height)
customHeight: 720, // Custom height (use with width)
rotation: 90, // Manual rotation (0, 90, 180, 270)
frameRate: 30.0, // Target frame rate
removeAudio: false, // Remove audio track
brightness: 0.1, // Brightness adjustment (-1.0 to 1.0)
contrast: 0.1, // Contrast adjustment (-1.0 to 1.0)
autoCorrectOrientation: true, // NEW: Auto-correct orientation
// ... other options
)
๐ผ๏ธ Thumbnail Generation
Single Thumbnail
final thumbnail = await _compressor.getVideoThumbnail(
videoPath,
VVideoThumbnailConfig(
timeMs: 5000, // 5 seconds into video
maxWidth: 300,
maxHeight: 200,
format: VThumbnailFormat.jpeg,
quality: 85, // JPEG quality (0-100)
),
);
if (thumbnail != null) {
print('Thumbnail: ${thumbnail.thumbnailPath}');
print('Size: ${thumbnail.width}x${thumbnail.height}');
print('File size: ${thumbnail.fileSizeFormatted}');
}
Multiple Thumbnails
final thumbnails = await _compressor.getVideoThumbnails(
videoPath,
[
VVideoThumbnailConfig(timeMs: 1000, maxWidth: 150), // 1s
VVideoThumbnailConfig(timeMs: 5000, maxWidth: 150), // 5s
VVideoThumbnailConfig(timeMs: 10000, maxWidth: 150), // 10s
],
);
print('Generated ${thumbnails.length} thumbnails');
for (final thumbnail in thumbnails) {
print('${thumbnail.timeMs}ms: ${thumbnail.thumbnailPath}');
}
๐ Batch Processing
Compress multiple videos with overall progress tracking:
final results = await _compressor.compressVideos(
[videoPath1, videoPath2, videoPath3],
const VVideoCompressionConfig.medium(),
onProgress: (progress, currentIndex, total) {
print('Overall: ${(progress * 100).toInt()}% (${currentIndex + 1}/$total)');
},
);
print('Successfully compressed ${results.length} videos');
for (final result in results) {
print('${result.originalPath} โ ${result.outputPath}');
print('Space saved: ${result.spaceSavedFormatted}');
}
๐ Video Information & Compression Estimation
Get Video Information
final videoInfo = await _compressor.getVideoInfo(videoPath);
if (videoInfo != null) {
print('Duration: ${videoInfo.durationFormatted}');
print('Resolution: ${videoInfo.width}x${videoInfo.height}');
print('File size: ${videoInfo.fileSizeFormatted}');
}
Estimate Compression Size
final estimate = await _compressor.getCompressionEstimate(
videoPath,
VVideoCompressQuality.medium,
advanced: advancedConfig, // Optional
);
if (estimate != null) {
print('Estimated size: ${estimate.estimatedSizeFormatted}');
print('Compression ratio: ${(estimate.compressionRatio * 100).toInt()}%');
print('Expected bitrate: ${estimate.bitrateMbps.toStringAsFixed(1)} Mbps');
}
โ Cancellation Support
Cancel operations anytime:
// Cancel ongoing compression
await _compressor.cancelCompression();
// Check if compression is running
final isActive = await _compressor.isCompressing();
// Handle cancellation in UI
if (isActive) {
await _compressor.cancelCompression();
// Files are automatically cleaned up
}
๐งน Resource Management
Complete Cleanup
// Clean everything when app closes
@override
void dispose() {
_compressor.cleanup();
super.dispose();
}
Selective Cleanup
// Safe cleanup - keep compressed videos
await _compressor.cleanupFiles(
deleteThumbnails: true,
deleteCompressedVideos: false, // Keep compressed videos
clearCache: true,
);
// Full cleanup - โ ๏ธ removes all compressed videos
await _compressor.cleanupFiles(
deleteThumbnails: true,
deleteCompressedVideos: true, // โ ๏ธ This deletes your videos!
clearCache: true,
);
๐ Complete API Reference
Core Methods
// Video information
Future<VVideoInfo?> getVideoInfo(String videoPath);
// Compression estimation
Future<VVideoCompressionEstimate?> getCompressionEstimate(
String videoPath,
VVideoCompressQuality quality,
{VVideoAdvancedConfig? advanced}
);
// Single video compression
Future<VVideoCompressionResult?> compressVideo(
String videoPath,
VVideoCompressionConfig config,
{Function(double progress)? onProgress, String? id}
);
// Batch compression
Future<List<VVideoCompressionResult>> compressVideos(
List<String> videoPaths,
VVideoCompressionConfig config,
{Function(double progress, int currentIndex, int total)? onProgress}
);
// Control operations
Future<void> cancelCompression();
Future<bool> isCompressing();
Thumbnail Methods
// Single thumbnail
Future<VVideoThumbnailResult?> getVideoThumbnail(
String videoPath,
VVideoThumbnailConfig config
);
// Multiple thumbnails
Future<List<VVideoThumbnailResult>> getVideoThumbnails(
String videoPath,
List<VVideoThumbnailConfig> configs
);
Cleanup Methods
// Complete cleanup
Future<void> cleanup();
// Selective cleanup
Future<void> cleanupFiles({
bool deleteThumbnails = true,
bool deleteCompressedVideos = false,
bool clearCache = true,
});
๐ซ Error Handling
The plugin provides comprehensive error handling and logging:
try {
final result = await _compressor.compressVideo(videoPath, config);
if (result == null) {
print('Compression failed - check logs for details');
}
} catch (e, stackTrace) {
print('Error: $e');
print('Stack trace: $stackTrace');
// The plugin automatically logs detailed error information
// Check your development console for full context
}
๐ฑ Platform-Specific Notes
Android
- Minimum API: Android 5.0 (API 21)
- Hardware Acceleration: Available on most devices with Media3
- Permissions: Automatically handled for Android 13+
- Background: Full background compression support
iOS
- Minimum Version: iOS 11.0
- Hardware Acceleration: Available with AVFoundation
- Simulator: Limited acceleration (test on devices for best results)
- Background: May be limited by iOS background execution policies
๐ง Troubleshooting
Common Issues
1. Compression fails silently
- Check file permissions and paths
- Verify video file is not corrupted
- Check device storage space
2. Progress not updating
- Ensure you're calling
setState()
in progress callback - Check if compression is actually running with
isCompressing()
3. iOS simulator issues
- Hardware acceleration unavailable in simulator
- Test on physical iOS devices for accurate results
4. Large file handling
- Very large files (>4GB) may require more memory
- Consider using lower quality settings for large files
Debug Logging
The plugin provides comprehensive logging:
// Logs are automatically output to console with tag 'VVideoCompressor'
// Filter logs by tag to see only plugin-related messages
๐จ Example Projects
Check the example directory for complete sample applications:
- Basic compression with progress tracking
- Advanced configuration examples
- Thumbnail generation demos
- Batch processing implementation
- UI integration patterns
๐ Additional Resources
- API Documentation - Complete API reference
- Memory Optimization Guide - Critical for production apps
- Advanced Features Guide - Detailed feature matrix
- Android Quick Fix Guide - Android-specific issues
- iOS Quick Fix Guide - iOS-specific issues
โ ๏ธ Important Notes
Memory Management
Video compression is memory-intensive. For production apps, please read our Memory Optimization Guide to avoid OutOfMemoryError issues. Key recommendations:
- Always check available memory before compression
- Use lower quality settings for devices with < 2GB RAM
- Implement progressive quality fallback
- Process videos in batches for bulk operations
๐ค Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
git clone https://github.com/your-repo/v_video_compressor.git
cd v_video_compressor
flutter pub get
cd example && flutter pub get
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ฎ Roadmap
- 1.1.0: Enhanced progress algorithms and additional presets
- 1.2.0: Video filtering and advanced effects
- 1.3.0: Cloud storage integration helpers
- 2.0.0: Performance improvements with breaking changes
๐ Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: pub.dev
Made with โค๏ธ for the Flutter community
Libraries
- v_video_compressor
- V Video Compressor - A focused Flutter plugin for efficient video compression
- v_video_compressor_method_channel
- v_video_compressor_platform_interface