NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #3239 most downloaded on pub.dev
Native light video compression for Flutter (No FFmpeg) — single or batch, H.264/H.265, target size, trim, rotate and colour, with live progress and cancellation.
Last release 2 months ago
09 Aug 2026
Ships unpredictably
gaps range from 8 days to 1.3 years
Nearly every release is documented
notes for 14 of 14 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
14 releases · first in 2025
Several paths through the AVFoundation pull loop could end a compression with no result at all — no success, no failure, no crash — leaving the caller
Several paths through the AVFoundation pull loop could end a compression
with no result at all — no success, no failure, no crash — leaving the
caller waiting forever:
Every terminal path now returns, a failed reader reports a failure, and
the terminal reply and finishWriting each run exactly once behind
single-shot guards. These are latent defects found by inspection: a
physical iPhone 13 Pro Max (iOS 26.5.2) and macOS 15.7 both complete 4K
camera clips in either audio mode, and native probes show each terminal
branch running exactly once.
Also closes a coverage gap: the bundled sample clip has no audio track,
so the Apple audio mux (passthrough and AAC re-encode) was never
exercised — which is how the addInput crash fixed in 1.9.0 reached a
release. Adds an audio-bearing fixture, generated with the plugin's own
pipeline, and real coverage for both paths. Verified by reverting that
fix on a physical device: the new test crashes the app and passes with
the fix, where the previous one passed either way.
Bumps to 1.9.1.
One column per month.
Tap the foreground notification to reopen the app (Android, #16 ): the ongoing compression notification now carries a content intent, so tapping it br
FLAG_ACTIVITY_REORDER_TO_FRONT). With a task-reusing host activity (Flutter's default launchMode="singleTop") the existing Flutter state is kept; after a full process kill it cold-starts. Thanks @khlebobul (#18).AudioConfig (the default), the source audio is muxed through unchanged. The native audio writer input was created without the source format description, which made AVAssetWriter throw NSInvalidArgumentException ("provide a format hint") on a physical iOS device — reproducing 100% on .mov camera recordings (they always carry an audio track). The input now carries the source CMFormatDescription, so passthrough muxing to the .mp4 container succeeds. The simulator and macOS did not surface the crash, which is why it slipped past.BatchProgress.percent and overallPercent are now clamped to 0..100, matching single-video progress — a native value that rounds just over 100 (or a transient negative) no longer leaks into batch progress UIs.Wider compatibility: lowered the minimum toolchain to Flutter 3.24 / Dart 3.5 (from 3.41 / 3.11) — no code changes; the floor now matches the SwiftPM
compileSdk 34 (AGP 8.1.1+).Docs: refreshed the pub.dev package description. No code or API changes.
Opt-in debug logging — pass debugLogging: true to compressVideo / compressVideos to have the native side emit a couple of structured log lines per vid
debugLogging: true to compressVideo /compressVideos to have the native side emit a couple of structured log linesonProgressDetailStream<CompressionProgress>) reports, alongside the percentage, theetaMs), elapsed time (elapsedMs) and encodedbytesProcessed) for the single-video flow. TheBatchProgress (via onBatchUpdate). TheonProgressUpdated (Stream<double>) is unchanged — it stays theetaMs is a rough projection (annull until it becomes estimable.maxConcurrent to compressVideos>= 1) compresses strictly one-at-a-time (1) or tradescompressVideo.debugLogging: true to compressVideo /
compressVideos to have the native side emit a couple of structured log lines
per video (the resolved encode plan and the outcome). File paths are reduced to
their base names. Off by default; intended for diagnosing a single run.onProgressDetail
(Stream<CompressionProgress>) reports, alongside the percentage, the
estimated time remaining (etaMs), elapsed time (elapsedMs) and encoded
output bytes written so far (bytesProcessed) for the single-video flow. The
same fields are now also on BatchProgress (via onBatchUpdate). The
existing onProgressUpdated (Stream<double>) is unchanged — it stays the
simplest option for just the percentage. etaMs is a rough projection (an
indicator, not a guarantee) and is null until it becomes estimable.maxConcurrent to compressVideos
to cap how many videos transcode at the same time. Leaving it unset keeps each
platform's historic default (Android compresses up to 2 at once; Apple starts
them all); setting it (>= 1) compresses strictly one-at-a-time (1) or trades
memory and device heat for throughput at higher values. Honoured on Android,
iOS and macOS; has no effect on a single compressVideo.getMediaInfo now reports the correct fileSize for sources
larger than ~2 GB (it read the size as a 32-bit value, which wrapped). Affects
metadata only; compression was unaffected.AudioConfig) no longer holds the whole
encoded audio track in RAM — it spills to a temp file and streams into the
muxer, so memory stays flat regardless of audio duration. Output is unchanged.MediaMuxer's MP4 writer uses 32-bit
box offsets, so outputs approaching 4 GB may fail or truncate. Target a smaller
size (targetSizeMb) for very large/long sources.All additions are additive and fully backward compatible — existing APIs are unchanged.
Lightweight native editing — pass an optional VideoEdit as edit: to compressVideo / compressVideos to trim and/or rotate while compressing (still 100%
VideoEdit as edit: tocompressVideo / compressVideos to trim and/or rotate while compressingtrimStartMs / trimEndMs keep a time range; the output timeline0 and the reported duration reflects the trimmedrotationDegrees (0 / 90 / 180 / 270) applies abrightness (-1..1), contrast (0..2) andsaturation (0..2) tweak the picture (CIColorControls semantics; 0 / 11 = no change). Baked into the output pixels — Android via a GL shader,CIColorControls video composition. Exact cross-platform pixelAll additions are additive and fully backward compatible — existing APIs are
unchanged.
Target output size — pass targetSizeMb to compressVideo (on Video ) or to compressVideos to compress toward a maximum file size in megabytes. The comp
targetSizeMb to compressVideo (on Video) orcompressVideos to compress toward a maximum file size in megabytes. ThevideoBitrateInMbps. The new OnSuccess.targetSizeMet reports whether thefalse when the floor forced a larger output.twoPass: true (on Video / compressVideos,targetSizeMb) to land closer to the target size: the compressorOnSuccess.passesUsed reports how many passes ran (1 or 2). IgnoredtargetSizeMb.videoFps (on Video / compressVideos) toAudioConfig(bitrate:, sampleRate:) asaudio: to re-encode the audio track as AAC with a custom bitrate (and, onaudioSampleRate is applied on iOS/macOS; AndroidbitrateAll additions are additive and fully backward compatible — existing APIs are
unchanged.
getCompressionEstimate() — predict a compression's output (size, bitrate, output resolution, % reduction) without transcoding , via the new Compressio
getCompressionEstimate() — predict a compression's output (size, bitrate, output resolution, % reduction) without transcoding, via the new CompressionEstimate model. It reuses the same bitrate/resize math the compressor uses, so the figures track the real output (approximate — single-pass).getVideoThumbnails() — extract several frames in a single native round-trip, returning the JPEG paths in request order, via the new ThumbnailRequest model. More efficient than calling getVideoThumbnail repeatedly.isCompressing() — query whether a compression (single or batch) is currently running (e.g. to gate UI).EstimateException (extends LightCompressorException) for estimate failures.All additions are additive and fully backward compatible — existing APIs are unchanged.
H.265 / HEVC output — pass an optional videoFormat to compressVideo / compressVideos to choose the output codec ( VideoFormat.h264 — the default — or
videoFormat to compressVideo / compressVideos to choose the output codec (VideoFormat.h264 — the default — or VideoFormat.h265). HEVC produces noticeably smaller files at comparable quality. Omitting the parameter keeps the previous H.264 behaviour and is fully backwards compatible.
VideoFormat.h265 is used only when the device can encode HEVC in hardware (Android: a non-software video/hevc encoder; iOS/macOS: an advertised HEVC encoder). On devices without it, the compressor transparently falls back to H.264 instead of failing.OnSuccess.usedFormat — every successful result now reports the codec actually used, so you can tell whether an H.265 request was honoured or fell back to H.264.OnFailure.failureType — failures now carry a CompressionFailureType (permission, unsupported, notFound, unknown) so you can react to why a video failed — including per-item in a batch — without parsing message text. Defaults to unknown and OnFailure.message is unchanged, so this is fully backwards compatible.videoFormat to compressVideo / compressVideos to choose the output codec (VideoFormat.h264 — the default — or VideoFormat.h265). HEVC produces noticeably smaller files at comparable quality. Omitting the parameter keeps the previous H.264 behaviour and is fully backwards compatible.
VideoFormat.h265 is used only when the device can encode HEVC in hardware (Android: a non-software video/hevc encoder; iOS/macOS: an advertised HEVC encoder). On devices without it, the compressor transparently falls back to H.264 instead of failing.OnSuccess.usedFormat — every successful result now reports the codec actually used, so you can tell whether an H.265 request was honoured or fell back to H.264.OnFailure.failureType — failures now carry a CompressionFailureType (permission, unsupported, notFound, unknown) so you can react to why a video failed — including per-item in a batch — without parsing message text. Defaults to unknown and OnFailure.message is unchanged, so this is fully backwards compatible.MediaMuxer (native H.264 and H.265 support) instead of a bundled mp4 writer. This removes the third-party mp4parser / isoparser dependency.Background execution — pass an optional BackgroundConfig to compressVideo / compressVideos to keep a compression running while the app is backgrounded
BackgroundConfig to compressVideo / compressVideos to keep a compression running while the app is backgrounded or the screen is off. Omitting it (the default) preserves the previous behaviour and is fully backwards compatible. Behaviour is platform-specific:
2 / 5 (batch, since videos compress in parallel) 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 required.NSProcessInfo.beginActivity) so the process keeps full CPU while in the background. The notification fields are ignored.BackgroundConfig has no effect there; the compression pauses and resumes when the app returns to the foreground.Batch compression — compressVideos({required List paths, required List videoNames, ...}) compresses multiple videos with a shared set of options and r
compressVideos({required List<String> paths, required List<String> videoNames, ...}) compresses multiple videos with a shared set of options and returns Future<List<Result>> in the same order as the inputs (each entry an OnSuccess, OnFailure or OnCancelled). A single video failing does not stop the rest.onBatchUpdate — a Stream<BatchEvent> that emits BatchProgress (per-video and overall percent) and BatchItemCompleted (a video's result) as the batch runs, for building per-item UIs.compressVideo and its onProgressUpdated stream are unchanged — batch uses a separate compression/batch-stream channel, so existing code is unaffected.cancelCompression() now returns Future<void> instead of Future<Map<String, dynamic>?>. The old return type never carried a meaningful value; the cancellation outcome arrives as an OnCancelled result on the pending compressVideo / compressVideos call.onCancelled followed by onFailure) and replied twice on the same MethodChannel.Result, throwing IllegalStateException: Reply already submitted. Cancellation now yields exactly one onCancelled, and the single-video handler de-duplicates its reply like batch already did.cancelCompression() never completed. The Android, iOS and macOS handlers did not reply to the method call, so the returned Future hung forever. All three platforms now reply.FlutterResults or reply off the main thread.`getMediaInfo(path)` — returns a structured MediaInfo (width, height, duration, file size, bitrate, rotation, frame rate, MIME type) with rotation-awa
getMediaInfo(path) — returns a structured MediaInfo (width, height, duration, file size, bitrate, rotation, frame rate, MIME type) with rotation-aware displayWidth/displayHeight. On Android, duration/bitrate fall back to the MediaExtractor track format when the metadata retriever does not expose them.getVideoThumbnail(path, {positionInMs, quality}) — extracts a JPEG frame and returns its file path (Android MediaMetadataRetriever, iOS/macOS AVAssetImageGenerator).clearCache() — deletes temporary files generated during compression and thumbnail extraction (.mp4 and .jpg).OnSuccess now carries originalSize, compressedSize, duration and ratio (percentage reduction).PermissionDeniedException, UnsupportedVideoException, VideoNotFoundException, MediaInfoException, ThumbnailException, all extending LightCompressorException. Native failures are surfaced via stable error codes instead of message text.KEY_PROFILE with a supported KEY_LEVEL, so the hardware encoder no longer fails configure() with error -38 and silently downgrades to Baseline.MediaMetadataRetriever/MediaExtractor file descriptors open while reading and release the retriever (previously leaked).LightCompressor.swift.Added Swift Package Manager (SPM) support for iOS.
.gradle.kts).README.md).Forked from the original light_compressor package.
light_compressor package.kotlin-gradle-plugin to version 1.8.21.LightCompressor dependency to version 1.3.2.compileSdkVersion to 33.Your coding agent can read these notes before it upgrades. Set up the MCP server →