flutter_lite_camera 0.2.0
flutter_lite_camera: ^0.2.0 copied to clipboard
A lightweight Flutter camera plugin for Windows, Linux, macOS, iOS, Android, and web. Renders previews natively through Flutter textures (or a video element on web) and captures RGB frames on demand f [...]
example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_barcode_sdk/flutter_barcode_sdk.dart';
import 'package:flutter_lite_camera/flutter_lite_camera.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const CameraApp(),
);
}
}
class CameraApp extends StatefulWidget {
const CameraApp({super.key});
@override
State<CameraApp> createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> {
final FlutterLiteCamera _flutterLiteCameraPlugin = FlutterLiteCamera();
bool _isCameraOpened = false;
int _textureId = -1;
// Actual frame size negotiated with the device. Filled in after the camera
// opens (and after every resolution change) — never hard-coded.
int _width = 0;
int _height = 0;
// Clockwise degrees to rotate the preview/result so it appears upright.
// Phones report 90 (or 180/270) depending on how the device is held;
// desktop/web report 0.
int _rotation = 0;
ResolutionPreset _preset = ResolutionPreset.medium;
bool _shouldDecode = false;
FlutterBarcodeSdk? _barcodeReader;
// To read barcodes, get a 30-day FREEE trial license for Dynamsoft Barcode Reader https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform
String licenseKey =
'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ==';
bool isDecoding = false;
List<BarcodeResult>? results;
@override
void initState() {
super.initState();
_handleWindowClose();
if (licenseKey != '') {
initBarcodeSDK();
}
}
Future<void> initBarcodeSDK() async {
_barcodeReader = FlutterBarcodeSdk();
await _barcodeReader!.setLicense(licenseKey);
await _barcodeReader!.init();
}
Future<void> _startCamera() async {
try {
List<String> devices = await _flutterLiteCameraPlugin.getDeviceList();
if (devices.isEmpty) {
debugPrint('No camera devices found. On an emulator, enable a '
'webcam/virtual scene in the AVD settings.');
return;
}
debugPrint("Available Devices: $devices");
debugPrint("Opening camera 0");
bool opened = await _flutterLiteCameraPlugin.open(0);
if (!opened) {
// false = permission denied or the device is busy/unavailable.
debugPrint("Failed to open the camera.");
return;
}
// The native layer renders the video feed into this texture; no
// frame data crosses into Dart for display purposes.
int textureId = await _flutterLiteCameraPlugin.startPreview();
int rotation = 0;
try {
rotation = await _flutterLiteCameraPlugin.getRotation();
} catch (e) {
debugPrint("getRotation failed, assuming 0: $e");
}
// The plugin negotiates the actual frame size with the device; read it
// back instead of assuming a fixed resolution.
int width = await _flutterLiteCameraPlugin.getWidth();
int height = await _flutterLiteCameraPlugin.getHeight();
debugPrint("Negotiated frame size: ${width}x$height, rotation $rotation");
setState(() {
_isCameraOpened = true;
_textureId = textureId;
if (width > 0 && height > 0) {
_width = width;
_height = height;
}
_rotation = rotation;
_shouldDecode = true;
});
// Start pulling frames for barcode decoding only. This does not
// affect the preview stream.
_decodeFrames();
// Honor a preset chosen before the camera was opened (open()
// negotiates the medium target by default).
if (_preset != ResolutionPreset.medium) {
await _applyResolution(_preset);
}
} catch (e) {
debugPrint("Error initializing camera: $e");
}
}
Future<void> _applyResolution(ResolutionPreset preset) async {
setState(() => _preset = preset);
bool ok = await _flutterLiteCameraPlugin.setResolutionPreset(preset);
if (!ok) {
debugPrint("setResolutionPreset($preset) failed.");
return;
}
// The device may have fallen back to a different size; read the actual
// one so the preview aspect ratio and overlay stay correct.
int width = await _flutterLiteCameraPlugin.getWidth();
int height = await _flutterLiteCameraPlugin.getHeight();
debugPrint("Resolution after $preset: ${width}x$height");
if (width > 0 && height > 0) {
setState(() {
_width = width;
_height = height;
});
}
}
Future<void> _decodeFrames() async {
if (!_isCameraOpened || !_shouldDecode) return;
if (!isDecoding && _barcodeReader != null) {
isDecoding = true;
try {
Map<String, dynamic> frame =
await _flutterLiteCameraPlugin.captureFrame();
if (frame.containsKey('data')) {
_width = frame['width'];
_height = frame['height'];
Uint8List rgbBuffer = frame['data'];
final ret = await _barcodeReader!.decodeImageBuffer(
rgbBuffer,
_width,
_height,
_width * 3,
ImagePixelFormat.IPF_RGB_888.index,
ImageRotation.rotation0.value,
);
setState(() {
results = ret;
});
}
} catch (e) {
// No frame available yet.
}
isDecoding = false;
}
if (_shouldDecode) {
Future.delayed(const Duration(milliseconds: 30), _decodeFrames);
}
}
Future<void> _stopCamera() async {
_shouldDecode = false;
if (_isCameraOpened) {
await _flutterLiteCameraPlugin.stopPreview();
await _flutterLiteCameraPlugin.release();
setState(() {
_isCameraOpened = false;
_textureId = -1;
results = null;
});
}
}
void _handleWindowClose() {
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChannels.lifecycle.setMessageHandler((message) async {
if (message == AppLifecycleState.detached.toString()) {
await _stopCamera();
}
return null;
});
});
}
@override
void dispose() {
_stopCamera();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
if (_textureId >= 0)
LayoutBuilder(
builder: (context, constraints) {
final screenWidth = constraints.maxWidth;
final screenHeight = constraints.maxHeight;
// When the preview is rotated 90/270 degrees the displayed
// frame becomes portrait, so the aspect ratio flips. Fall
// back to 4:3 until the first frame reports its real size.
final frameWidth = _width > 0 ? _width : 4;
final frameHeight = _height > 0 ? _height : 3;
final rotated = _rotation % 180 == 90;
final imageAspectRatio = rotated
? frameHeight / frameWidth
: frameWidth / frameHeight;
final screenAspectRatio = screenWidth / screenHeight;
double drawWidth, drawHeight;
if (imageAspectRatio > screenAspectRatio) {
drawWidth = screenWidth;
drawHeight = screenWidth / imageAspectRatio;
} else {
drawHeight = screenHeight;
drawWidth = screenHeight * imageAspectRatio;
}
return Center(
child: SizedBox(
width: drawWidth,
height: drawHeight,
child: Stack(
children: [
// Rotate only the widget — never the native pixels —
// so the preview appears upright without any per-frame
// buffer processing.
RotatedBox(
quarterTurns: (_rotation ~/ 90) % 4,
child: _flutterLiteCameraPlugin
.buildPreview(_textureId),
),
CustomPaint(
painter: ResultPainter(
results ?? [],
srcWidth: frameWidth,
srcHeight: frameHeight,
drawWidth: drawWidth,
drawHeight: drawHeight,
rotation: _rotation,
),
child: Container(),
),
],
),
),
);
},
)
else
Center(
child: Text('Camera not initialized'),
),
Positioned(
bottom: 20,
left: 20,
right: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Start Button
ElevatedButton(
onPressed: _isCameraOpened ? null : () => _startCamera(),
child: const Text('Start'),
),
// Resolution preset selector
Expanded(
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<ResolutionPreset>(
value: _preset,
underline: const SizedBox.shrink(),
isExpanded: true,
onChanged: (preset) {
if (preset == null) return;
if (_isCameraOpened) {
_applyResolution(preset);
} else {
setState(() => _preset = preset);
}
},
items: ResolutionPreset.values
.map((p) => DropdownMenuItem(
value: p,
child: Text(
'${p.name} (${p.width}x${p.height})',
overflow: TextOverflow.ellipsis,
),
))
.toList(),
),
),
),
),
// Stop Button
ElevatedButton(
onPressed: !_isCameraOpened ? null : () => _stopCamera(),
child: const Text('Stop'),
),
],
),
),
],
),
);
}
}
class ResultPainter extends CustomPainter {
final List<BarcodeResult> results;
final int srcWidth;
final int srcHeight;
final double drawWidth;
final double drawHeight;
final int rotation;
ResultPainter(
this.results, {
required this.srcWidth,
required this.srcHeight,
required this.drawWidth,
required this.drawHeight,
required this.rotation,
});
/// Maps a point from source-frame coordinates to the displayed (possibly
/// rotated) canvas, so overlay boxes align with the upright preview even
/// when the texture is rotated by [rotation] degrees clockwise.
Offset _transform(double x, double y) {
final q = (rotation ~/ 90) % 4;
switch (q) {
case 1: // 90° clockwise
final nx = srcHeight - y;
final ny = x;
return Offset(nx * drawWidth / srcHeight, ny * drawHeight / srcWidth);
case 2: // 180°
final nx = srcWidth - x;
final ny = srcHeight - y;
return Offset(nx * drawWidth / srcWidth, ny * drawHeight / srcHeight);
case 3: // 270° clockwise (90° counter-clockwise)
final nx = y;
final ny = srcWidth - x;
return Offset(nx * drawWidth / srcHeight, ny * drawHeight / srcWidth);
default: // 0°
return Offset(x * drawWidth / srcWidth, y * drawHeight / srcHeight);
}
}
@override
void paint(Canvas canvas, Size size) {
if (results.isEmpty) return;
final textPaint = Paint()
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 2;
for (var result in results) {
final a = _transform(result.x1.toDouble(), result.y1.toDouble());
final b = _transform(result.x2.toDouble(), result.y2.toDouble());
final c = _transform(result.x3.toDouble(), result.y3.toDouble());
final d = _transform(result.x4.toDouble(), result.y4.toDouble());
final path = Path()
..moveTo(a.dx, a.dy)
..lineTo(b.dx, b.dy)
..lineTo(c.dx, c.dy)
..lineTo(d.dx, d.dy)
..close();
canvas.drawPath(path, textPaint);
final textPainter = TextPainter(
text: TextSpan(
text: result.text,
style: const TextStyle(
color: Colors.red,
fontSize: 16,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, a);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}