light_compressor_v2

Pub Version Pub Platforms Pub Likes License: MIT

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.
  • Custom resolution & bitrate โ€” override width, height, and bitrate when presets aren't enough.
  • Structured success result โ€” OnSuccess carries originalSize, compressedSize, duration, and ratio (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

Demo GIF


๐Ÿ“ฑ 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 / total count (batch) and a Cancel action. The title comes from BackgroundConfig. The plugin declares the service + receiver and requests POST_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 BackgroundConfig has no effect; the compression pauses and resumes when the app returns to the foreground.

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.
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.
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.

Result types

Type Properties Description
OnSuccess destinationPath: String, originalSize: int, compressedSize: int, duration: double, ratio: double Output path, byte sizes, duration (seconds) and percentage size reduction.
OnFailure message: String Unclassified failure with an error message.
OnCancelled isCancelled: bool Compression was cancelled via cancelCompression().

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.


๐Ÿค Contributing

Contributions are welcome! To get started:

  1. Fork the repository: github.com/Farid023/light_compressor_v2
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Make your changes and run the example app to verify:
    cd example
    flutter run
    
  4. 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

License: MIT

Released under the MIT License โ€” see LICENSE for the full text.

MIT ยฉ 2025 Farid Gurbanov