light_compressor_v2
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.
1.9.1
13K downloads/mo
#2622 most downloaded on pub.dev
Farid023/light_compressor_v2
What this package is like to depend on
Last release 15 days 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
1 years old
14 releases · first in 2025
13 releases in the last 12 months
see the full history below
Release timeline
14 releases · Feb 2025 to Aug 2026Releases
latest 14-
1.9.109 Aug 2026Release notes
Open source →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:- The terminal branches did not return, so the loop could re-enter, find
the audio reader already reading, take the no-audio branch and call
finishWriting while the audio input was still unfinished. A writer in
that state never completes. - A failed reader returned quietly through a bare guard.
- The audio loop had the same missing return, so a second
markAsFinished/finishWriting was reachable. - The audio phase began with a redundant second startSession.
- The audio phase never checked for cancellation.
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.
Release notes
Open source →Fixed
- iOS/macOS: a compression could end with no result at all — no success, no failure, no crash — leaving the caller waiting forever. The encode loop now always ends a pass exactly once: a drained or failed reader is handled explicitly, the file is closed only once, and cancellation is honoured while the audio track is being written.
Internal
- Audio is now covered by the integration tests. The bundled sample clip had no audio track, so the Apple audio path (passthrough and AAC re-encode) was never exercised. Added a clip with audio and tests for both.
- The terminal branches did not return, so the loop could re-enter, find
-
1.9.020 Jul 2026Release notes
Open source →New
- Tap the foreground notification to reopen the app (Android, #16): the ongoing compression notification now carries a content intent, so tapping it brings the running app forward (
FLAG_ACTIVITY_REORDER_TO_FRONT). With a task-reusing host activity (Flutter's defaultlaunchMode="singleTop") the existing Flutter state is kept; after a full process kill it cold-starts. Thanks @khlebobul (#18).
Fixed
- iOS/macOS crash when compressing videos that have audio (#17): with no
AudioConfig(the default), the source audio is muxed through unchanged. The native audio writer input was created without the source format description, which madeAVAssetWriterthrowNSInvalidArgumentException("provide a format hint") on a physical iOS device — reproducing 100% on.movcamera recordings (they always carry an audio track). The input now carries the sourceCMFormatDescription, so passthrough muxing to the.mp4container succeeds. The simulator and macOS did not surface the crash, which is why it slipped past. - Batch progress bounds:
BatchProgress.percentandoverallPercentare now clamped to0..100, matching single-video progress — a native value that rounds just over100(or a transient negative) no longer leaks into batch progress UIs.
Release notes
Open source →New
- Tap the foreground notification to reopen the app (Android, #16): the ongoing compression notification now carries a content intent, so tapping it brings the running app forward (
FLAG_ACTIVITY_REORDER_TO_FRONT). With a task-reusing host activity (Flutter's defaultlaunchMode="singleTop") the existing Flutter state is kept; after a full process kill it cold-starts. Thanks @khlebobul (#18).
Fixed
- iOS/macOS crash when compressing videos that have audio (#17): with no
AudioConfig(the default), the source audio is muxed through unchanged. The native audio writer input was created without the source format description, which madeAVAssetWriterthrowNSInvalidArgumentException("provide a format hint") on a physical iOS device — reproducing 100% on.movcamera recordings (they always carry an audio track). The input now carries the sourceCMFormatDescription, so passthrough muxing to the.mp4container succeeds. The simulator and macOS did not surface the crash, which is why it slipped past. - Batch progress bounds:
BatchProgress.percentandoverallPercentare now clamped to0..100, matching single-video progress — a native value that rounds just over100(or a transient negative) no longer leaks into batch progress UIs.
- Tap the foreground notification to reopen the app (Android, #16): the ongoing compression notification now carries a content intent, so tapping it brings the running app forward (
-
1.8.206 Jul 2026Release notes
Open source →Changed
- 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 baseline and Android
compileSdk 34(AGP 8.1.1+). - Docs: refreshed the pub.dev package description. No code or API changes.
Release notes
Open source →Changed
- 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 baseline and Android
compileSdk 34(AGP 8.1.1+).
- 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 baseline and Android
-
1.8.102 Jul 2026Release notes
Open source →Changed
- Docs: refreshed the pub.dev package description. No code or API changes.
-
1.8.002 Jul 2026Release notes
Open source →New
- Opt-in debug logging — pass
debugLogging: truetocompressVideo/
compressVideosto 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. - Progress detail (ETA + bytes) — a new
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 onBatchProgress(viaonBatchUpdate). The
existingonProgressUpdated(Stream<double>) is unchanged — it stays the
simplest option for just the percentage.etaMsis a rough projection (an
indicator, not a guarantee) and isnulluntil it becomes estimable. - Configurable batch concurrency — pass
maxConcurrenttocompressVideos
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 singlecompressVideo. - Example app gains a "Max concurrent" (Auto / 1 / 2 / 3) selector in the
batch flow.
Release notes
Open source →New
- Opt-in debug logging — pass
debugLogging: truetocompressVideo/compressVideosto 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. - Progress detail (ETA + bytes) — a new
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 onBatchProgress(viaonBatchUpdate). The existingonProgressUpdated(Stream<double>) is unchanged — it stays the simplest option for just the percentage.etaMsis a rough projection (an indicator, not a guarantee) and isnulluntil it becomes estimable. - Configurable batch concurrency — pass
maxConcurrenttocompressVideosto 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 singlecompressVideo. - Example app gains a "Max concurrent" (Auto / 1 / 2 / 3) selector in the batch flow.
Fixed
- iOS/macOS:
getMediaInfonow reports the correctfileSizefor sources larger than ~2 GB (it read the size as a 32-bit value, which wrapped). Affects metadata only; compression was unaffected. - Android: the AAC audio re-encode (
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.
Known limitations (large files)
- ~4 GB MP4 output ceiling (Android).
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.
- Opt-in debug logging — pass
-
1.7.002 Jul 2026Release notes
Open source →New
- Lightweight native editing — pass an optional
VideoEditasedit:to
compressVideo/compressVideosto trim and/or rotate while compressing
(still 100% native — no ffmpeg):- Trim —
trimStartMs/trimEndMskeep a time range; the output timeline
is rebased to start at0and the reporteddurationreflects the trimmed
length. Frame-accurate (the clip is re-encoded). Either bound is optional. - Rotate —
rotationDegrees(0/90/180/270) applies a
quarter-turn on top of the source orientation. This is a cheap
container-metadata rotation (no extra pixel pass) that players honour — a 90°
turn swaps the displayed dimensions. - Colour adjust —
brightness(-1..1),contrast(0..2) and
saturation(0..2) tweak the picture (CIColorControls semantics;0/1
/1= no change). Baked into the output pixels — Android via a GL shader,
Apple via aCIColorControlsvideo composition. Exact cross-platform pixel
parity is not guaranteed.
- Trim —
- Example app gains trim start/end (ms) fields, a 0/90/180/270 rotate selector and
brightness/contrast/saturation sliders in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are
unchanged.Release notes
Open source →New
- Lightweight native editing — pass an optional
VideoEditasedit:tocompressVideo/compressVideosto trim and/or rotate while compressing (still 100% native — no ffmpeg):- Trim —
trimStartMs/trimEndMskeep a time range; the output timeline is rebased to start at0and the reporteddurationreflects the trimmed length. Frame-accurate (the clip is re-encoded). Either bound is optional. - Rotate —
rotationDegrees(0/90/180/270) applies a quarter-turn on top of the source orientation. This is a cheap container-metadata rotation (no extra pixel pass) that players honour — a 90° turn swaps the displayed dimensions. - Colour adjust —
brightness(-1..1),contrast(0..2) andsaturation(0..2) tweak the picture (CIColorControls semantics;0/1/1= no change). Baked into the output pixels — Android via a GL shader, Apple via aCIColorControlsvideo composition. Exact cross-platform pixel parity is not guaranteed.
- Trim —
- Example app gains trim start/end (ms) fields, a 0/90/180/270 rotate selector and brightness/contrast/saturation sliders in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are unchanged.
- Lightweight native editing — pass an optional
-
1.6.001 Jul 2026Release notes
Open source →New
- Target output size — pass
targetSizeMbtocompressVideo(onVideo) or
tocompressVideosto compress toward a maximum file size in megabytes. The
compressor solves for the video bitrate that lands the output at or below the
target (reserving room for audio + ~3% container overhead), clamped to a 2 Mbps
quality floor and never above the source bitrate. Mutually exclusive with
videoBitrateInMbps. The newOnSuccess.targetSizeMetreports whether the
target was achievable —falsewhen the floor forced a larger output.
Single-pass and approximate (typically within ~10–15%). - Two-pass encoding — pass
twoPass: true(onVideo/compressVideos,
alongsidetargetSizeMb) to land closer to the target size: the compressor
encodes once, and only if the output overshot the target does it re-encode a
second time at a corrected (lower) bitrate. An undershoot is kept as-is, so it
re-encodes only when needed (roughly doubling the time on overshooting clips).
The newOnSuccess.passesUsedreports how many passes ran (1 or 2). Ignored
without atargetSizeMb. - Frame-rate control — pass
videoFps(onVideo/compressVideos) to
downsample the output frame rate (e.g. 30 → 24). Downsample-only: a value at or
above the source rate leaves it unchanged (frames are never duplicated). - Audio re-encoding — pass an
AudioConfig(bitrate:, sampleRate:)as
audio:to re-encode the audio track as AAC with a custom bitrate (and, on
Apple, sample rate). Omitting it copies the source audio through untouched.- Platform note:
audioSampleRateis applied on iOS/macOS; Android
re-encodes at the source sample rate (no resampler), so onlybitrate
takes effect there.
- Platform note:
- Example app gains "max output size (MB)", a "Two-pass (precise size)" toggle,
"output FPS" and "audio bitrate (kbps)" fields in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are
unchanged.Release notes
Open source →New
- Target output size — pass
targetSizeMbtocompressVideo(onVideo) or tocompressVideosto compress toward a maximum file size in megabytes. The compressor solves for the video bitrate that lands the output at or below the target (reserving room for audio + ~3% container overhead), clamped to a 2 Mbps quality floor and never above the source bitrate. Mutually exclusive withvideoBitrateInMbps. The newOnSuccess.targetSizeMetreports whether the target was achievable —falsewhen the floor forced a larger output. Single-pass and approximate (typically within ~10–15%). - Two-pass encoding — pass
twoPass: true(onVideo/compressVideos, alongsidetargetSizeMb) to land closer to the target size: the compressor encodes once, and only if the output overshot the target does it re-encode a second time at a corrected (lower) bitrate. An undershoot is kept as-is, so it re-encodes only when needed (roughly doubling the time on overshooting clips). The newOnSuccess.passesUsedreports how many passes ran (1 or 2). Ignored without atargetSizeMb. - Frame-rate control — pass
videoFps(onVideo/compressVideos) to downsample the output frame rate (e.g. 30 → 24). Downsample-only: a value at or above the source rate leaves it unchanged (frames are never duplicated). - Audio re-encoding — pass an
AudioConfig(bitrate:, sampleRate:)asaudio:to re-encode the audio track as AAC with a custom bitrate (and, on Apple, sample rate). Omitting it copies the source audio through untouched.- Platform note:
audioSampleRateis applied on iOS/macOS; Android re-encodes at the source sample rate (no resampler), so onlybitratetakes effect there.
- Platform note:
- Example app gains "max output size (MB)", a "Two-pass (precise size)" toggle, "output FPS" and "audio bitrate (kbps)" fields in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are unchanged.
- Target output size — pass
-
1.5.030 Jun 2026Release notes
Open source →New
getCompressionEstimate()— predict a compression's output (size, bitrate, output resolution, % reduction) without transcoding, via the newCompressionEstimatemodel. 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 newThumbnailRequestmodel. More efficient than callinggetVideoThumbnailrepeatedly.isCompressing()— query whether a compression (single or batch) is currently running (e.g. to gate UI).- New
EstimateException(extendsLightCompressorException) for estimate failures. - Example app gains a pre-flight estimate card and a multi-thumbnail filmstrip in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are unchanged.
Release notes
Open source →New
getCompressionEstimate()— predict a compression's output (size, bitrate, output resolution, % reduction) without transcoding, via the newCompressionEstimatemodel. 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 newThumbnailRequestmodel. More efficient than callinggetVideoThumbnailrepeatedly.isCompressing()— query whether a compression (single or batch) is currently running (e.g. to gate UI).- New
EstimateException(extendsLightCompressorException) for estimate failures. - Example app gains a pre-flight estimate card and a multi-thumbnail filmstrip in the single-video flow.
All additions are additive and fully backward compatible — existing APIs are unchanged.
-
1.4.025 Jun 2026Release notes
Open source →New
- H.265 / HEVC output — pass an optional
videoFormattocompressVideo/compressVideosto choose the output codec (VideoFormat.h264— the default — orVideoFormat.h265). HEVC produces noticeably smaller files at comparable quality. Omitting the parameter keeps the previous H.264 behaviour and is fully backwards compatible.- Automatic fallback —
VideoFormat.h265is used only when the device can encode HEVC in hardware (Android: a non-softwarevideo/hevcencoder; 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.
- Automatic fallback —
OnFailure.failureType— failures now carry aCompressionFailureType(permission,unsupported,notFound,unknown) so you can react to why a video failed — including per-item in a batch — without parsing message text. Defaults tounknownandOnFailure.messageis unchanged, so this is fully backwards compatible.- Example app gains a “Use H.265 (HEVC)” toggle in both the single and batch flows, and the single-video result now shows the codec used.
Release notes
Open source →New
- H.265 / HEVC output — pass an optional
videoFormattocompressVideo/compressVideosto choose the output codec (VideoFormat.h264— the default — orVideoFormat.h265). HEVC produces noticeably smaller files at comparable quality. Omitting the parameter keeps the previous H.264 behaviour and is fully backwards compatible.- Automatic fallback —
VideoFormat.h265is used only when the device can encode HEVC in hardware (Android: a non-softwarevideo/hevcencoder; 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.
- Automatic fallback —
OnFailure.failureType— failures now carry aCompressionFailureType(permission,unsupported,notFound,unknown) so you can react to why a video failed — including per-item in a batch — without parsing message text. Defaults tounknownandOnFailure.messageis unchanged, so this is fully backwards compatible.- Example app gains a “Use H.265 (HEVC)” toggle in both the single and batch flows, and the single-video result now shows the codec used.
Changed
- Android: video muxing now uses the platform
MediaMuxer(native H.264 and H.265 support) instead of a bundled mp4 writer. This removes the third-partymp4parser/isoparserdependency.
- H.265 / HEVC output — pass an optional
-
1.3.023 Jun 2026Release notes
Open source →New
- Background execution — pass an optional
BackgroundConfigtocompressVideo/compressVideosto 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:- Android — runs under a foreground service. Its ongoing notification shows live progress (bar + %), an elapsed-time timer, the current file name (single) or a done/total count like
2 / 5(batch, since videos compress in parallel) 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 required. - macOS — suppresses App Nap (
NSProcessInfo.beginActivity) so the process keeps full CPU while 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 there; the compression pauses and resumes when the app returns to the foreground.
- Android — runs under a foreground service. Its ongoing notification shows live progress (bar + %), an elapsed-time timer, the current file name (single) or a done/total count like
- Example app gains a “Run in background” toggle in both the single and batch flows.
- Background execution — pass an optional
-
1.2.017 Jun 2026Release notes
Open source →New
- Batch compression —
compressVideos({required List<String> paths, required List<String> videoNames, ...})compresses multiple videos with a shared set of options and returnsFuture<List<Result>>in the same order as the inputs (each entry anOnSuccess,OnFailureorOnCancelled). A single video failing does not stop the rest. onBatchUpdate— aStream<BatchEvent>that emitsBatchProgress(per-video and overall percent) andBatchItemCompleted(a video's result) as the batch runs, for building per-item UIs.- The single-video
compressVideoand itsonProgressUpdatedstream are unchanged — batch uses a separatecompression/batch-streamchannel, so existing code is unaffected.
Changed
cancelCompression()now returnsFuture<void>instead ofFuture<Map<String, dynamic>?>. The old return type never carried a meaningful value; the cancellation outcome arrives as anOnCancelledresult on the pendingcompressVideo/compressVideoscall.
Fixed
- Cancelling a single compression crashed the app on Android. Cancellation delivered two terminal callbacks for one video (
onCancelledfollowed byonFailure) and replied twice on the sameMethodChannel.Result, throwingIllegalStateException: Reply already submitted. Cancellation now yields exactly oneonCancelled, 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 returnedFuturehung forever. All three platforms now reply.- iOS / macOS: the single-video handler funnels every terminal reply through one main-thread reply, so a cancel/finish race can no longer deliver two
FlutterResults or reply off the main thread.
- Batch compression —
-
1.1.012 Jun 2026Release notes
Open source →New
getMediaInfo(path)— returns a structuredMediaInfo(width, height, duration, file size, bitrate, rotation, frame rate, MIME type) with rotation-awaredisplayWidth/displayHeight. On Android, duration/bitrate fall back to theMediaExtractortrack format when the metadata retriever does not expose them.getVideoThumbnail(path, {positionInMs, quality})— extracts a JPEG frame and returns its file path (AndroidMediaMetadataRetriever, iOS/macOSAVAssetImageGenerator).clearCache()— deletes temporary files generated during compression and thumbnail extraction (.mp4and.jpg).- Structured success result —
OnSuccessnow carriesoriginalSize,compressedSize,durationandratio(percentage reduction). - Typed exceptions —
PermissionDeniedException,UnsupportedVideoException,VideoNotFoundException,MediaInfoException,ThumbnailException, all extendingLightCompressorException. Native failures are surfaced via stable error codes instead of message text. - Example app demonstrates metadata display, thumbnail preview, and a Clear Cache action.
Fixed
- Android H.264 encoder — pair
KEY_PROFILEwith a supportedKEY_LEVEL, so the hardware encoder no longer failsconfigure()with error-38and silently downgrades to Baseline. - Reported duration — use the exact duration measured during transcoding instead of a file-size/bitrate estimate (previously could report wildly wrong values for files without duration metadata).
- Over-compression — when a source has no duration/bitrate metadata, estimate the bitrate from the resolution instead of collapsing to the minimum bitrate.
- Resource handling (Android) — keep
MediaMetadataRetriever/MediaExtractorfile descriptors open while reading and release the retriever (previously leaked). - macOS — fixed a build failure caused by an out-of-sync
LightCompressor.swift.
-
1.0.101 Jun 2026Release notes
Open source →- Added Swift Package Manager (SPM) support for iOS.
- Fully migrated the Android native layer to Kotlin, including core compression algorithms, MP4 builder, and video/texture renderers.
- Integrated the native compression library sources directly into the plugin codebase.
- Upgraded the Android build system and configurations (converted build scripts to Kotlin DSL
.gradle.kts). - Added comprehensive production-ready documentation (
README.md). - Updated example project and video player.
- Optimized codebase and improved performance.
- Fixed minor issues.
-
1.0.028 Feb 2025Release notes
Open source →- Forked from the original
light_compressorpackage. - Updated
kotlin-gradle-pluginto version 1.8.21. - Upgraded the
LightCompressordependency to version 1.3.2. - Increased
compileSdkVersionto 33. - Fixed various bugs and improved performance.
- Forked from the original