light_compressor_v2
A powerful, easy-to-use video compression plugin for Flutter โ for single videos and batches.
It generates a compressed MP4 with reduced width, height, and bitrate while keeping good visual quality, and also exposes media metadata, thumbnail extraction, and cache cleanup.
๐ ๏ธ How it Works
Extreme high bitrates are reduced while maintaining good video quality, resulting in a much smaller file size.
- Quality presets: choose between 5 compression qualities โ
very_low,low,medium,high,very_high. The plugin automatically computes the target bitrate for the output. - Minimum bitrate guard: with
isMinBitrateCheckEnabled(default 2 Mbps threshold), the plugin skips compression for low-bitrate or already-compressed videos, avoiding cumulative quality degradation.
โจ Features
- Single & batch compression โ compress one video, or many in a single call with per-item results and progress.
- Five quality presets โ the plugin calculates the optimal bitrate automatically.
- H.264 & H.265 (HEVC) โ pick the output codec via
videoFormat; H.265 produces smaller files and automatically falls back to H.264 when the device can't encode it.OnSuccess.usedFormatreports the codec actually used. - Custom resolution & bitrate โ override width, height, and bitrate when presets aren't enough.
- Structured success result โ
OnSuccesscarriesoriginalSize,compressedSize,duration, andratio(percentage reduction). - Media info โ read width, height, duration, bitrate, rotation, frame rate, and MIME type via
getMediaInfo. - Thumbnail extraction โ grab a JPEG frame at any timecode via
getVideoThumbnail. - Progress streams โ real-time percentage for single (
onProgressUpdated) and per-item + overall for batch (onBatchUpdate). - Cancellation โ cancel any in-progress compression with a single call.
- Background execution โ keep compressing while the app is backgrounded or the screen is off via
BackgroundConfig(Android foreground service; macOS App Nap suppression; not supported on iOS). - Typed exceptions โ
PermissionDeniedException,UnsupportedVideoException,VideoNotFoundException, and more โ react programmatically instead of parsing strings. - Minimum bitrate guard โ optionally skip compression for already-low-bitrate videos.
- Disable audio โ generate silent videos when audio isn't needed.
- Cache cleanup โ remove temporary files with
clearCache. - iOS / macOS: Swift Package Manager (SPM) support alongside CocoaPods.
- Android: fully Kotlin native layer, Gradle KTS build script.
๐ธ Demo
๐ฑ Platform Support
| iOS | Android | macOS |
|---|---|---|
| โ | โ | โ |
Minimum versions: iOS 11 ยท Android API 24 ยท macOS 10.15
๐ฆ Installation
Add the dependency to your pubspec.yaml:
dependencies:
light_compressor_v2: ^1.3.0
Then run:
flutter pub get
iOS / macOS โ Podfile
No extra Podfile configuration is required. The plugin ships with both a .podspec (CocoaPods) and a Package.swift (SPM); Flutter picks the appropriate integration automatically.
Android โ minSdk
The plugin requires minSdk 24. If your app targets a lower SDK, update your android/app/build.gradle:
android {
defaultConfig {
minSdk = 24
}
}
๐ Usage
import 'package:light_compressor_v2/light_compressor_v2.dart';
final compressor = LightCompressor();
Compress a single video
final Result result = await compressor.compressVideo(
path: '/path/to/source.mp4',
videoQuality: VideoQuality.medium,
isMinBitrateCheckEnabled: false,
video: Video(videoName: 'compressed.mp4'),
android: AndroidConfig(isSharedStorage: true, saveAt: SaveAt.Movies),
ios: IOSConfig(saveInGallery: true),
);
if (result is OnSuccess) {
print('Saved to ${result.destinationPath}');
print('Reduced by ${result.ratio.toStringAsFixed(1)}% '
'(${result.originalSize} โ ${result.compressedSize} bytes)');
} else if (result is OnFailure) {
print('Failed: ${result.message}');
} else if (result is OnCancelled) {
print('Cancelled');
}
Compress a batch of videos
A single failing video does not stop the others โ its slot in the returned
list becomes an OnFailure. Results are returned in the same order as paths.
final List<Result> results = await compressor.compressVideos(
paths: ['/path/a.mp4', '/path/b.mp4'],
videoNames: ['a_compressed.mp4', 'b_compressed.mp4'],
videoQuality: VideoQuality.medium,
android: AndroidConfig(saveAt: SaveAt.Movies),
ios: IOSConfig(saveInGallery: false),
);
for (final (int i, Result r) in results.indexed) {
if (r is OnSuccess) print('Video $i โ ${r.destinationPath}');
}
Run in the background
Pass a BackgroundConfig to keep a compression running while the app is
backgrounded or the screen turns off. Works for both compressVideo and
compressVideos:
final result = await compressor.compressVideo(
path: '/path/to/source.mp4',
videoQuality: VideoQuality.medium,
video: Video(videoName: 'compressed.mp4'),
android: AndroidConfig(saveAt: SaveAt.Movies),
ios: IOSConfig(saveInGallery: true),
background: const BackgroundConfig(
notificationTitle: 'Compressing video',
),
);
Platform behaviour differs significantly:
- Android โ runs under a foreground service. The ongoing notification shows
live progress (bar + %), elapsed time, the current file (single) or
a
done / totalcount (batch) and a Cancel action. The title comes fromBackgroundConfig. The plugin declares the service + receiver and requestsPOST_NOTIFICATIONS(Android 13+) automatically โ no host-app manifest changes are needed. - macOS โ suppresses App Nap so the process keeps full CPU in the background. The notification fields are ignored.
- iOS โ not supported. iOS suspends backgrounded apps within seconds
and offers no sanctioned way to keep video transcoding running, so passing a
BackgroundConfighas no effect; the compression pauses and resumes when the app returns to the foreground.
Choose the output codec (H.265 / HEVC)
By default the output is H.264 (AVC). Pass videoFormat: VideoFormat.h265 to
request HEVC, which yields smaller files at comparable quality. It applies to
both compressVideo and compressVideos:
final result = await compressor.compressVideo(
path: '/path/to/source.mp4',
videoQuality: VideoQuality.medium,
videoFormat: VideoFormat.h265,
video: Video(videoName: 'compressed.mp4'),
android: AndroidConfig(saveAt: SaveAt.Movies),
ios: IOSConfig(saveInGallery: true),
);
if (result is OnSuccess) {
// Tells you whether H.265 was honoured or fell back to H.264.
print('Encoded with ${result.usedFormat.name}');
}
VideoFormat.h265 is used only when the device has a hardware HEVC encoder
(Android excludes software-only encoders; iOS/macOS check the platform's
advertised HEVC support). When it isn't available the compressor silently
falls back to H.264 rather than failing โ always read OnSuccess.usedFormat
to know what you got.
Listen to progress
Single video โ a Stream<double> from 0 to 100:
StreamBuilder<double>(
stream: compressor.onProgressUpdated,
builder: (context, snapshot) {
final percent = snapshot.data ?? 0;
return Text('${percent.toStringAsFixed(0)}%');
},
);
Batch โ per-video and overall progress, plus a completion event per item:
compressor.onBatchUpdate.listen((BatchEvent event) {
switch (event) {
case BatchProgress(:final index, :final overallPercent):
print('Video $index โ overall ${overallPercent.toStringAsFixed(0)}%');
case BatchItemCompleted(:final index, :final result):
print('Video $index finished: $result');
}
});
Cancel compression
await compressor.cancelCompression();
The cancelled job is reported as an OnCancelled result on the pending
compressVideo / compressVideos call.
Read media info
final MediaInfo info = await compressor.getMediaInfo('/path/to/video.mp4');
print('${info.displayWidth} ร ${info.displayHeight}');
print('Duration: ${info.duration}, bitrate: ${info.bitrate} bps');
Extract a thumbnail
final String thumbnailPath = await compressor.getVideoThumbnail(
'/path/to/video.mp4',
positionInMs: 2000, // grab the frame at 2s
quality: 80,
);
Clear cached files
await compressor.clearCache();
Handle errors
Recognized native failures are thrown as typed exceptions; unclassified
failures are returned as OnFailure instead.
try {
final info = await compressor.getMediaInfo('/path/to/video.mp4');
// use info...
} on VideoNotFoundException catch (e) {
print(e.message);
} on PermissionDeniedException catch (e) {
print(e.message);
} on LightCompressorException catch (e) {
print(e.message); // base type โ catches any of the above
}
๐ API Reference
compressVideo() โ Future<Result>
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
path |
String |
โ | โ | Absolute path to the source video file. |
videoQuality |
VideoQuality |
โ | โ | Quality preset: very_low, low, medium, high, very_high. |
android |
AndroidConfig |
โ | โ | Android-specific storage configuration. |
ios |
IOSConfig |
โ | โ | iOS/macOS-specific storage configuration. |
video |
Video |
โ | โ | Output video configuration (name, resolution, bitrate). |
isMinBitrateCheckEnabled |
bool |
true |
Skip compression when source bitrate is below 2 Mbps. | |
disableAudio |
bool? |
false |
Strip the audio track from the output. | |
videoFormat |
VideoFormat |
h264 |
Output codec: h264 or h265 (HEVC). Falls back to H.264 when HEVC isn't supported. See VideoFormat. |
|
background |
BackgroundConfig? |
null |
Keep running while the app is backgrounded. See BackgroundConfig. |
compressVideos() โ Future<List<Result>>
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
paths |
List<String> |
โ | โ | Source video paths. |
videoNames |
List<String> |
โ | โ | Output file names; must match paths length. |
videoQuality |
VideoQuality |
โ | โ | Quality preset, shared by every video. |
android |
AndroidConfig |
โ | โ | Android-specific storage configuration. |
ios |
IOSConfig |
โ | โ | iOS/macOS-specific storage configuration. |
keepOriginalResolution |
bool |
false |
Keep source dimensions instead of downscaling. | |
videoWidth / videoHeight |
int? |
null |
Custom output size (set both together). | |
videoBitrateInMbps |
int? |
null |
Custom bitrate in Mbps (overrides the preset). | |
disableAudio |
bool |
false |
Strip the audio track from every output. | |
isMinBitrateCheckEnabled |
bool |
true |
Skip compression when source bitrate is below 2 Mbps. | |
videoFormat |
VideoFormat |
h264 |
Output codec for every video: h264 or h265 (HEVC). See VideoFormat. |
|
background |
BackgroundConfig? |
null |
Keep the whole batch running while backgrounded. See BackgroundConfig. |
Video
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
videoName |
String |
โ | โ | Output filename (.mp4 appended automatically if missing). |
keepOriginalResolution |
bool? |
false |
Keep source dimensions instead of downscaling. | |
videoBitrateInMbps |
int? |
null |
Custom bitrate in Mbps (overrides the quality preset). | |
videoHeight |
int? |
null |
Custom height in pixels. Must be set with videoWidth. |
|
videoWidth |
int? |
null |
Custom width in pixels. Must be set with videoHeight. |
AndroidConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
isSharedStorage |
bool |
true |
true = shared storage (MediaStore); false = app-specific directory. |
saveAt |
SaveAt |
Movies |
Target collection: Pictures, Movies, or Downloads. Ignored when isSharedStorage is false. |
IOSConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
saveInGallery |
bool |
true |
Save the compressed video to the photo library. |
BackgroundConfig
Opt into background execution. notificationTitle is the Android foreground-service notification title; iOS and macOS ignore it.
| Parameter | Type | Default | Description |
|---|---|---|---|
notificationTitle |
String |
'Compressing video' |
Title of the Android foreground-service notification. |
VideoFormat
Output codec, written into an MP4/QuickTime container.
| Value | Description |
|---|---|
h264 |
H.264 / AVC. The widely compatible default. |
h265 |
H.265 / HEVC. Smaller files at comparable quality; requires a hardware HEVC encoder and automatically falls back to h264 otherwise. Check OnSuccess.usedFormat for the codec actually used. |
Result types
| Type | Properties | Description |
|---|---|---|
OnSuccess |
destinationPath: String, originalSize: int, compressedSize: int, duration: double, ratio: double, usedFormat: VideoFormat |
Output path, byte sizes, duration (seconds), percentage size reduction and the codec actually used. |
OnFailure |
message: String, failureType: CompressionFailureType |
A failure: a human-readable message plus a CompressionFailureType category for reacting in code without parsing text. |
OnCancelled |
isCancelled: bool |
Compression was cancelled via cancelCompression(). |
CompressionFailureType
The category carried by OnFailure.failureType, for reacting to why a video failed (including per-item in a batch) without parsing message. Defaults to unknown.
| Value | Description |
|---|---|
permission |
A required permission (e.g. storage) was denied. |
unsupported |
The source could not be processed โ e.g. no decodable video track or an unsupported format. |
notFound |
The source file could not be found or opened. |
unknown |
Any other or unclassified failure. |
BatchEvent (from onBatchUpdate)
| Type | Properties | Description |
|---|---|---|
BatchProgress |
index: int, percent: double, overallPercent: double |
Progress of one video and the batch average. |
BatchItemCompleted |
index: int, result: Result |
A video finished; result is OnSuccess / OnFailure / OnCancelled. |
MediaInfo (from getMediaInfo)
All fields are nullable โ a container/device may not expose every value.
| Property | Type | Description |
|---|---|---|
width / height |
int? |
Encoded dimensions in pixels (before rotation). |
displayWidth / displayHeight |
int? |
Dimensions as displayed (rotation-aware). |
duration |
Duration? |
Total duration. |
fileSize |
int? |
File size in bytes. |
bitrate |
int? |
Bitrate in bits per second. |
rotation |
int? |
Rotation in degrees (0, 90, 180, 270). |
frameRate |
double? |
Frames per second. |
mimeType |
String? |
Container MIME type. |
Exceptions
All extend LightCompressorException (catch the base type to handle any):
| Exception | Thrown when |
|---|---|
PermissionDeniedException |
Missing read/write permission. |
UnsupportedVideoException |
Unsupported format/codec or missing track. |
VideoNotFoundException |
The source video was not found. |
MediaInfoException |
Metadata could not be read (getMediaInfo). |
ThumbnailException |
A frame could not be extracted (getVideoThumbnail). |
Other members
| Member | Signature | Description |
|---|---|---|
onProgressUpdated |
Stream<double> |
Single-video progress, 0โ100. |
onBatchUpdate |
Stream<BatchEvent> |
Per-video + overall progress and completion events during compressVideos. |
getMediaInfo() |
Future<MediaInfo> |
Read video metadata. |
getVideoThumbnail() |
Future<String> |
Extract a JPEG frame; returns its file path. |
clearCache() |
Future<void> |
Delete temporary .mp4 files created during compression. |
cancelCompression() |
Future<void> |
Cancel any running compression. |
โ๏ธ Configuration
iOS / macOS
SPM vs CocoaPods โ the plugin includes both Package.swift and .podspec. Flutter โฅ 3.24 uses SPM by default; older versions fall back to CocoaPods automatically.
Info.plist โ if you use IOSConfig(saveInGallery: true), add the photo library usage description:
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to save compressed videos.</string>
Android
Permissions โ add the appropriate permissions to AndroidManifest.xml based on your target API level:
<!-- API < 29 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />
<!-- API 29โ32 -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- API โฅ 33 -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
Background execution โ when you pass a BackgroundConfig, no manifest changes are required on your side. The plugin already declares the foreground service and the FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC and POST_NOTIFICATIONS permissions, which merge into your app automatically; the POST_NOTIFICATIONS runtime prompt (Android 13+) is requested for you.
ProGuard โ no special ProGuard or R8 rules are required.
๐งช Testing
The plugin ships with two layers of tests:
-
Unit tests (
test/) cover the Dart surface: argument forwarding over the method channel, result/event parsing, batch ordering and failure typing, progress-stream coercion, and the typed-exception mapping. They need no device:flutter test -
Integration tests (
example/integration_test/) exercise the real native pipeline (AndroidMediaCodec/MediaMuxer, AppleAVFoundation) on a device, emulator, or simulator โ metadata, thumbnails, compression options, progress streams, cancellation, batch resilience, and H.264 / H.265 codec selection with automatic fallback:cd example flutter test integration_test/plugin_integration_test.dart -d <deviceId> flutter test integration_test/hevc_compression_test.dart -d <deviceId>A short sample clip is bundled at
example/integration_test/assets/sample.mp4; the tests skip cleanly when it is absent.
flutter analyze, formatting, and the unit tests run in CI on every push and pull request. Integration tests are run manually, since they need a device.
๐ค Contributing
Contributions are welcome! To get started:
- Fork the repository: github.com/Farid023/light_compressor_v2
- Create a feature branch:
git checkout -b feat/my-feature - Make your changes and run the example app to verify:
cd example flutter run - Open a Pull Request with a clear description of the change.
Please report bugs via GitHub Issues. Include the device name, OS version, and whether the issue reproduces in the example app.
๐ License
Released under the MIT License โ see LICENSE for the full text.
MIT ยฉ 2025 Farid Gurbanov