media_reduce 0.1.0
media_reduce: ^0.1.0 copied to clipboard
Iteratively compresses images and videos toward a maximum byte budget on Android and iOS, so files fit API upload limits before sending.
media_reduce #
A Flutter plugin that iteratively shrinks images and videos on the device until they fit a maximum file size you specify (e.g. 4 MB), so the output can be uploaded to APIs with strict body-size limits.
The plugin runs a binary-search loop in Dart over native encoders:
- Android:
BitmapFactory(images) and Media3TransformerwithVideoEncoderSettings(videos). - iOS:
ImageIO/UIGraphicsImageRenderer(images) andAVAssetReader+AVAssetWriterwithAVVideoAverageBitRateKey(videos).
If the source is already under the target, it is returned untouched.
If the budget cannot be reached, the smallest valid output is returned
with achievedTarget == false.
Features #
- Single high-level API:
reduceToMaxBytes(ReductionRequest). - Works for both images and videos behind one entry point; type is detected from path / native metadata.
- Progress callback (
attempt,stage,currentBytes,targetMaxBytes). - Tolerance band so the search can stop early once "close enough".
- Clean architecture (domain / data split, port + adapter, dependency injection) so it is easy to mock and test.
Install #
dependencies:
media_reduce: ^0.1.0
Then:
flutter pub get
Platform requirements #
- Android:
minSdk24+, AGP 8+, Kotlin 1.9+, internet not required. - iOS:
iOS 13+. Add the standard photo / camera usage strings if your app picks media:
<key>NSPhotoLibraryUsageDescription</key>
<string>Select photos and videos to compress before upload.</string>
<key>NSCameraUsageDescription</key>
<string>Capture photos or videos to compress before upload.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record videos with audio to compress before upload.</string>
Usage #
import 'dart:io';
import 'package:media_reduce/media_reduce.dart';
final plugin = MediaReduce();
final result = await plugin.reduceToMaxBytes(
ReductionRequest(
sourcePath: pickedFile.path,
maxBytes: 4 * 1024 * 1024, // 4 MB cap
tolerancePercent: 5, // optional, default 5%
),
onProgress: (attempt, stage, bytes, target) {
debugPrint('attempt $attempt | $stage | $bytes / $target bytes');
},
);
if (result.success && result.outputPath != null) {
final upload = File(result.outputPath!);
// hand `upload` to your http multipart request...
}
result (ReductionResult) carries:
| Field | Meaning |
|---|---|
success |
A usable output exists |
outputPath |
Absolute path of the produced file |
originalBytes |
Source size in bytes |
outputBytes |
Compressed size in bytes |
kind |
MediaKind.image / video / unknown |
attempts |
How many native encode passes ran |
achievedTarget |
true if outputBytes <= maxAcceptableBytes |
stageLog |
Diagnostic log of each stage attempted |
message |
Failure or BEST_EFFORT marker |
Customising the search #
ReductionRequest(
sourcePath: file.path,
maxBytes: 4 * 1024 * 1024,
tolerancePercent: 8,
maxAttemptsPerStage: 18,
minImageQuality: 5,
maxImageQuality: 95,
minVideoBitrate: 200_000, // 200 kbps floor
maxVideoBitrate: 8_000_000, // 8 Mbps ceiling
);
Other helpers #
final info = await plugin.getMediaInfo(path); // sizeBytes, kind, w/h, durationMs
await plugin.clearCache(); // removes temp files this plugin created
How the search works #
Per kind:
- Read native
MediaInfo(size, dimensions, duration). - If
originalBytesis already inside the tolerance band, return it. - Walk through descending resolution stages (videos and images), and for each stage binary-search the native parameter (image quality or video bitrate) until an output fits the band, or the search converges.
- Keep the best result seen so far so a "best effort" output is always available.
Limitations #
- Android scaling for videos is not yet wired through Media3
Effects; resolution downscaling currently runs only on iOS. Bitrate-only transcode on Android is sufficient for typical "fit under N MB" use cases. - Audio bitrate is exposed end-to-end, but on Android the Transformer encoder factory is currently configured for video bitrate; audio uses Media3 defaults.
- Extremely aggressive ratios (e.g. 1 GB → 1 MB) will return a "best effort" result; the plugin does not lie about hitting the target when the encoder cannot.
Architecture #
lib/
media_reduce.dart # public API
src/
domain/
entities/ # ReductionRequest, ReductionResult, MediaInfo, MediaKind
repositories/native_media_gateway.dart # port (interface)
use_cases/reduce_media_to_max_bytes_use_case.dart
data/
method_channel_native_media_gateway.dart # adapter
android/ # Kotlin: ImageCompressor, VideoCompressor (Media3), MediaInfoReader
ios/ # Swift: ImageIO + AVAssetReader/Writer pipeline
License #
MIT