dual_capture
Capture back and front camera in one tap. The two shots are composited into a single JPEG with a picture-in-picture overlay — you get the composited image plus both raw files.
Features
- One-call API:
DualCapture.capture(context) - Back → front sequential pipeline with automatic camera switching
- Picture-in-picture overlay at any corner (bottom-right, bottom-left, top-right, top-left)
- Configurable overlay size, margin, JPEG quality, and front-camera flip
- Optional rounded-corner border around the overlay
- Compositing runs in a background isolate (no UI jank)
- Embedded preview support via
DualCaptureController+DualCapturePreview
Platform setup
iOS
Add the following entries to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>dual_capture needs camera access to take photos.</string>
Android
Add to android/app/src/main/AndroidManifest.xml inside <manifest>:
<uses-permission android:name="android.permission.CAMERA" />
Getting started
dependencies:
dual_capture: ^0.1.0
Usage
One-shot capture
import 'package:dual_capture/dual_capture.dart';
final result = await DualCapture.capture(context);
if (result != null) {
// result.compositedFile — back + front composited JPEG
// result.backCameraFile — raw back-camera JPEG
// result.frontCameraFile — raw front-camera JPEG
// result.capturedAt — UTC DateTime
}
Custom options
final result = await DualCapture.capture(
context,
options: const DualCaptureOptions(
overlayPosition: OverlayPosition.topLeft,
overlayScale: 0.25,
jpegQuality: 90,
overlayBorder: OverlayBorder(
color: Color(0xFF6C63FF),
width: 3.0,
cornerRadius: 16.0,
),
),
);
Embedded preview (controller API)
class _MyState extends State<MyPage> {
late final DualCaptureController _ctrl;
@override
void initState() {
super.initState();
_ctrl = DualCaptureController();
_ctrl.initialize();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(child: DualCapturePreview(controller: _ctrl)),
ElevatedButton(
onPressed: _ctrl.state == DualCaptureState.readyBack
? () async {
final result = await _ctrl.capture();
// use result …
}
: null,
child: const Text('Capture'),
),
],
);
}
}
API
| Class / member | Description |
|---|---|
DualCapture.capture(context, {options}) |
Pushes capture UI, returns DualCaptureResult? |
DualCaptureController |
Manages the pipeline; extends ChangeNotifier |
DualCaptureController.initialize() |
Opens back camera |
DualCaptureController.capture() |
Runs full pipeline, returns result |
DualCaptureController.state |
Current DualCaptureState |
DualCapturePreview |
Widget that shows the live camera feed |
DualCaptureScreen |
Full-screen camera UI (used internally by DualCapture.capture) |
DualCaptureOptions |
All capture settings in one object |
DualCaptureResult |
Holds the three output files + timestamp |
OverlayPosition |
Enum: bottomRight, bottomLeft, topRight, topLeft |
OverlayBorder |
Optional border around the PiP overlay |
DualCaptureException |
Thrown when the pipeline encounters an error |
How it works
┌─────────────────────────────────────────────────────┐
│ 1. Open back camera → show preview │
│ 2. User taps shutter → takePicture() (back) │
│ 3. Dispose back controller (iOS race-condition fix) │
│ 4. Open front camera → warm-up delay → takePicture()│
│ 5. Dispose front controller │
│ 6. Background isolate: │
│ a. Decode both JPEGs │
│ b. Resize front to overlayScale × back width │
│ c. Optionally flip front horizontally │
│ d. Apply rounded corners + optional border │
│ e. Composite onto back image at chosen corner │
│ f. Encode result as JPEG │
│ 7. Return DualCaptureResult │
└─────────────────────────────────────────────────────┘
Contributing
PRs and issues welcome at the issue tracker.
Libraries
- dual_capture
- A Flutter package for dual camera capture.