my_filter_camera
A Flutter plugin for real-time camera filters, filtered photo and video capture, exposure control, torch control, and front/back camera switching on Android and iOS.
Platform and requirements
| Requirement | Supported value |
|---|---|
| Flutter | >=3.47.1 |
| Dart | >=3.13.1 <4.0.0 |
| Android | API 24+ |
| iOS | 15.0+ |
| Web, desktop | Not supported |
Android uses CameraX 1.6.1, Media3, and GPUImage-compatible filters. iOS uses AVFoundation, Core Image, and AVAssetWriter without a third-party pod. Both CocoaPods and Swift Package Manager are supported.
Platform support
| Capability | Android | iOS |
|---|---|---|
| Live camera preview and 13 filters | ✅ | ✅ |
| Filtered JPEG capture | ✅ | ✅ |
| MP4 recording with optional audio | ✅ | ✅ |
| Pause and resume recording | ✅ | ✅ |
| Front/back camera and torch | ✅ | ✅ |
| Exposure, contrast, gamma, and RGB controls | ✅ | ✅ |
| Hardware zoom for preview, photos, and video | ✅ | ✅ |
| Tap focus and exposure-point metering | ✅ | ✅ |
| Preferred video quality with native fallback | ✅ | ✅ |
| Typed video recording state | ✅ | ✅ |
| Automatic app lifecycle handling | ✅ | ✅ |
| Filter-processed video recording | ✅ | ✅ |
Camera-dependent controls such as torch and exposure are applied only when the active device supports them. The selected filter and color adjustments are written into both JPEG captures and recorded MP4 video.
Features
- Real-time filters in an Android or iOS PlatformView.
- Capture the currently selected filter to a JPEG file.
- Record filter-processed MP4 video, with optional microphone audio.
- Pause, resume, and stop an active video recording.
- Switch front/back cameras and control the torch.
- Query the active camera zoom range and apply hardware zoom.
- Focus or meter exposure at normalized points; opt into tap and pinch gestures.
- Select a preferred recording quality with a safe native fallback.
- Adjust exposure compensation, contrast, gamma, and RGB values.
- Receive camera, torch, recording, zoom, exposure, and capture state through typed Dart APIs.
- Automatically stop/resume the camera with the application lifecycle.
Installation
Add the package to pubspec.yaml:
dependencies:
my_filter_camera: ^3.0.0
Android permissions
The plugin manifest declares camera and microphone permissions. Do not add legacy storage permissions: captured files use app-private storage and recordings use MediaStore.
If your application never records audio, remove the merged microphone permission in your application manifest:
<uses-permission
android:name="android.permission.RECORD_AUDIO"
tools:node="remove" />
When using tools:node, add xmlns:tools="http://schemas.android.com/tools" to the root <manifest> element.
iOS permissions
Add usage descriptions to the consuming application's ios/Runner/Info.plist. iOS terminates applications that access these capabilities without the matching description:
<key>NSCameraUsageDescription</key>
<string>Use the camera to preview filters and capture photos and videos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Use the microphone only when recording video with audio.</string>
NSMicrophoneUsageDescription is required only when the application calls startVideoRecording(enableAudio: true).
See the platform channel contract for native arguments, return types, events, and error codes.
Basic usage
Import the package and create one controller for one camera screen:
import 'package:my_filter_camera/my_filter_camera.dart';
final controller = CameraViewController(
facing: CameraFacing.back,
ratio: Ratio.ratio_16_9,
autoResume: true,
videoQuality: CameraVideoQuality.fullHd,
);
Display the preview. CameraView starts the controller automatically and reports startup errors through errorBuilder:
CameraView(
controller: controller,
enablePinchToZoom: true,
enableTapToFocus: true,
onDetect: (data, args) {
// Capture events sent by the native implementation.
},
errorBuilder: (context, error) {
return Center(child: Text('Camera error: $error'));
},
)
Dispose the controller when the owner of the controller is removed:
@override
void dispose() {
controller.dispose();
super.dispose();
}
If CameraView creates its own controller, it always releases that controller. For an injected controller, autoDispose: true (the default) lets the widget release it.
Start, stop, and camera state
When using the controller without CameraView:
final granted = await controller.checkPermission();
if (granted) {
await controller.start();
}
controller.isRunningState.addListener(() {
debugPrint('running: ${controller.isRunning}');
});
await controller.stop();
Only one native camera session should be active at a time. Stop or dispose the current controller before creating a camera screen with different settings.
Filters

await controller.updateFilter(
filterType: PluginFilterEnum.GRAYSCALE.code,
);
final samples = await controller.applyImageSample();
PluginFilterEnum retains its uppercase 1.x names for source compatibility.
Capture an image

final XFile image = await controller.capture();
debugPrint(image.path);
Images are written to an app-private Images directory. No storage runtime permission is required.
Record video
Record without audio:
await controller.startVideoRecording();
await controller.pauseVideoRecording();
await controller.resumeVideoRecording();
final XFile video = await controller.stopVideoRecording();
Build recording controls from typed state instead of maintaining a duplicate boolean in the application:
controller.recordingState.addListener(() {
switch (controller.recordingState.value) {
case CameraRecordingState.idle:
// Show the record button.
case CameraRecordingState.starting:
case CameraRecordingState.stopping:
// Show progress and temporarily disable controls.
case CameraRecordingState.recording:
// Show pause and stop.
case CameraRecordingState.paused:
// Show resume and stop.
}
});
Record with audio (Android and iOS request microphone permission when needed):
await controller.startVideoRecording(enableAudio: true);
final XFile video = await controller.stopVideoRecording();
The returned MP4 is written to the app-specific Videos directory using a unique filename. Android also creates a MediaStore recording. On iOS, pause/resume records filtered segments and merges them when recording stops.
The filter visible in the preview is baked into the video. Calling updateFilter, adjustContrast, adjustGamma, or adjustRGB during recording applies the new values to subsequent frames without restarting the recording.
Camera controls
await controller.switchCamera();
if (controller.hasTorch) {
await controller.setTorch(TorchState.on);
}
final camera = controller.args.value!;
final appliedExposure = await controller.setExposureOffset(2.0);
final zoom = await controller.setZoomLevel(2.0);
final focused = await controller.setFocusPoint(const Offset(0.5, 0.5));
final metered = await controller.setExposurePoint(const Offset(0.5, 0.5));
await controller.resetFocusAndExposure();
final contrast = await controller.adjustContrast(value: 1.2);
final gamma = await controller.adjustGamma(value: 0.9);
final rgb = await controller.adjustRGB(red: 1, green: 0.95, blue: 0.9);
The exposure value is clamped from camera.minExposureOffset to
camera.maxExposureOffset. Check supportsExposureOffset,
supportsFocusPoint, and supportsExposurePoint before displaying the
matching control. Focus and exposure points use normalized preview coordinates:
top-left is Offset.zero, bottom-right is Offset(1, 1).
Zoom is also clamped, so applications can build a slider directly from
controller.minZoomLevel to controller.maxZoomLevel. Listen to
controller.zoomLevelState when the UI needs the latest applied value. Hardware
zoom affects the preview, captured JPEG, and recorded MP4 together.
enableTapToFocus meters focus and exposure together. Gesture errors can be
observed with CameraView.onCameraControlError. Both gestures default to
false, so they do not intercept touches in existing applications.
Video quality
Choose lowest, sd, hd, fullHd, ultraHd, or highest when constructing
the controller. This is a preference rather than a guarantee: unsupported
qualities fall back to a compatible native CameraX or AVFoundation setting.
The setting is applied the next time the camera starts.
Events
controller.faces.listen((CameraData data) {
debugPrint('captured path: ${data.faceImage}');
});
controller.torchState.addListener(() {
debugPrint('torch: ${controller.torchState.value}');
});
controller.cameraRunningResponseStream.listen((event) {
debugPrint('running: ${event.isCameraRunning}');
});
Delete plugin-created media
removeDir() is destructive. It deletes the plugin's app-private Images and Videos directories. Android also deletes MediaStore videos whose display name starts with CameraX-recording-:
await controller.removeDir();
Error handling
Native failures are returned as PlatformException; calls after dispose() throw StateError.
try {
await controller.start();
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
}
See MIGRATION.md when upgrading from 1.x, SECURITY.md for privacy guidance, and OPERATIONS.md for validation/release commands.
Contribute
Issues and pull requests are welcome at GitHub. For project questions, contact Thao Doan or Duc Nguyen.