monolens 0.4.0
monolens: ^0.4.0 copied to clipboard
Headless camera capture and on-device media editing for Flutter: crop, rotate, trim, blur regions, burn in text, stickers and drawings, with undo. No ffmpeg.
Changelog #
All notable changes to monolens are documented here. The format follows Keep a Changelog, and the project follows semantic versioning.
0.4.0 is the first version published to pub.dev. 0.1.0 through 0.3.0 were development iterations, kept here because what changed in them is still the history of this API.
0.4.0 #
Value equality, capture timing, and a reworked example.
Added #
- Value equality on
ImageEdit,VideoEditand everyAnnotation. These are value types and were compared by identity, which meantEditHistory's no-op guard never fired for them:copyWithnever returns the receiver, so a drag that emitted an unchanged sample still cost an undo press. Hosts can now compare edits directly too.
Changed #
- The example app is rebuilt as a reference editor rather than a control gallery. The three stacked rows of mode, tool and panel buttons collapse into one tool rail with a contextual tray; the media goes full-bleed on monokit's media register with chrome floating over it; and the editing glyphs -- crop, flip, blur, a pen nib -- are drawn rather than substituted from the nearest available icon. A blur region now previews as a real backdrop blur instead of a white wash, so what the author sees matches what the exporter burns in.
- The example's blur tool exposes
BlurAnnotation.strength, which the API has always carried and the old UI never surfaced. - Play/pause moved out of the example's trim tool and onto the media itself, so it is available while annotating rather than one tool away. Playback loops the trimmed range rather than the whole source, and drives the trim playhead.
Fixed #
- The example's tool rail held its own copy of the selected tool, so placing an annotation -- which moves the controller to the select tool so the new annotation can be dragged -- left the rail highlighting a tool that was no longer active. It derives from the controller now.
- Pinch and twist did nothing to a selected annotation in the example. The canvas carried a pan recognizer, which never reports a second finger, so a two-finger gesture was invisible to it. It uses a scale recognizer now, with a single pointer meaning drag.
- The example's corner resize handle did nothing. A
Stackstops hit-testing at the topmost child that reports a hit andRenderCustomPaint.hitTestSelfreturns true by default, so the glyph inside the handle swallowed the drag before it could reach the canvas detector underneath. Each handle carries its own recognizer now, on a 48pt target. - Scaling a stroke in the example changed only its line weight, leaving the drawn shape the size it was.
- A capped recording left its ticker running after the cap fired, so the reported duration ran past the cap and a host that did not collect the take immediately leaked a periodic timer.
recordedDurationaccumulated the tick's nominal period rather than reading a clock, so it drifted from the real take under load — precisely where it is being read against a cap. It comes off aStopwatchnow and holds at the cap.MonolensCameraSession.disposeandFakeCameraSession.disposewere not idempotent, and disposing aChangeNotifiertwice throws. A host that disposes from both a lifecycle callback andState.disposeis doing something reasonable.- The example's selection chrome ignored rotation: the outline stayed square around a turned annotation, the handles sat at the unturned corners, and the tappable region was a different shape from the drawn one. Outline, handles and hit test now all read from one oriented box.
0.3.0 #
Annotations, video crop, and undo.
Added #
- Annotations, burned into stills and into every frame of a clip:
TextAnnotation(emoji included — an emoji is a glyph, soAnnotationFactories.emojiis a convenience over the same primitive),StickerAnnotation,BlurAnnotation(rectangle or oval), andStrokeAnnotationfor coloured freehand lines. Geometry is normalized against the output frame, so an annotation survives a change of crop. VideoEdit— crop, rotation, horizontal flip, trim, mute and annotations for clips, so a clip is edited through the same declarative shape a still is. The flip mirrors the rotated frame on both platforms, matching stills.EditHistory— undo/redo over any edit value, with gesture coalescing so a drag is one step rather than four hundred. It implementsValueListenable, so it binds to any UI library. Undo is cheap precisely because an edit is a value: there is nothing to invert, and blur has no inverse.- Pixel-level integration tests for strokes, blur, text, stickers (placement, the stretch-to-rect semantic, and opacity) and annotations-under-crop, plus a video flip test — all on both platforms. The sticker tests run against a generated magenta fixture, since a sticker needs a real file on disk.
Fixed #
- Sticker opacity was ignored on iOS.
UIImage.draw(in:)is defined asdraw(in:blendMode:alpha:)with alpha 1, so it overwrote the context alpha and a translucent sticker exported fully opaque. Caught by the new test.
Changed — breaking #
MediaEditor.startTrimtakes aVideoEditrather than aVideoTrim.VideoTrimis now purely a range;muteAudiomoved toVideoEdit.ImageEditgainedannotations, andisIdentityaccounts for them.
Native #
Regional video blur on Android is a custom Media3 GlEffect. Media3's stock
GaussianBlur blurs the whole frame with no mask, and no chain of stock effects
can recover the sharp part, because an overlay draws onto a frame and can never
sample it. The mask is rasterized once by the same Canvas code the still path
uses and uploaded as a texture — rather than passed as array uniforms, which
GLES reports under the name uName[0] and which therefore fail to bind by the
name written in the shader source.
Media3's ExportException reports only the top of its cause chain, which for
anything in the frame pipeline is the useless "Video frame processing error".
Failures now carry the flattened chain instead.
0.2.0 #
Headless, and a good deal faster.
Changed — breaking #
- The package ships no widgets.
MonolensCameraView,MonolensCropViewandMonolensTrimView, with their controllers and style objects, are gone. Monolens is an API; the UI is the host's. CameraSession.buildPreview(BuildContext)is replaced byCameraSession.preview, aPreviewTexturecarrying the texture id, the stream size, the sensor orientation and the active lens. Render it withTexture(textureId: …)— see the README for the few lines involved.ImageEditRequest.cropis aNormalizedRectrather than aPixelRect, which is now removed along withCropRect.resolve. The platform resolves the fractions, soapplyImageEditno longer probes the source first: an edit is one channel call instead of two.- A flip now mirrors the rotated frame on both platforms. iOS previously
mirrored before rotating and Android after, so
rotation + flipHorizontalcombinations disagreed between them. Covered now by pixel-level tests.
Performance #
- Image edits derive their output size from bounds metadata, then decode only
that many pixels (
kCGImageSourceThumbnailMaxPixelSize/inSampleSize), and fold crop, rotate, mirror and scale into one composite pass. Previously this was a full-resolution decode followed by up to three full-size redraws. - Encoding on iOS goes straight to disk through ImageIO, with no
UIImageround trip and no intermediateData. - Filmstrips: iOS batches every timestamp through
generateCGImagesAsynchronouslyrather than re-seeking per frame; Android decodes directly to thumbnail size viagetScaledFrameAtTimewhere available.
Added #
- Pixel-level integration tests that assert which region a crop kept and which way up it came out, on both platforms — dimensions alone cannot catch a transform composed in the wrong order.
- Camera integration tests covering preview, still capture and the recording auto-stop, which run wherever a camera exists and skip where one does not.
0.1.0 #
First cut. Capture and on-device editing behind interfaces, with real native exporters on both platforms.
Added #
- Capture.
CameraSession(a live viewfinder with lens flip, flash cycle and a recording cap enforced at the shutter) andMediaPicker(gallery import), both interfaces with acamera/image_pickerimplementation and a test double inpackage:monolens/testing.dart. - Editing.
MediaEditor—applyImageEditfor crop/rotate/flip/downscale,startTrimfor a cancellable video export with progress,filmstripfor scrubbing frames, andprobefor dimensions and duration. - Native exporters. Video trim runs on
AVAssetExportSession(iOS) andandroidx.media3.transformer.Transformer(Android). No ffmpeg, no bundled native binary, no per-ABI size cost. Cuts are frame-accurate: both sides force a re-encode rather than snapping the in-point back to a keyframe. - Widgets.
MonolensCameraView,MonolensCropViewandMonolensTrimView, built onpackage:flutter/widgets.dartonly — no Material dependency, so they drop into aWidgetsApphost. Each takes a style object, and the camera view takes acontrolsBuilderfor hosts that supply their own chrome. - Typed bridge. All platform traffic goes through Pigeon
(
pigeons/monolens_api.dart), with generated Dart, Swift and Kotlin committed.
Notes #
- Value types are declarative, not command logs: an
ImageEditre-applies to the original, so stacking edits costs no generation loss and "revert" is just dropping back toImageEdit.none. CropRectis normalized, resolving to pixels only at export, so a crop survives rotation and preview resizing.- The plugin carries no
path_providerdependency — it asks the platform for its own cache directory.