How to Build a Lightweight Flutter Camera Plugin for Windows, Linux, and macOS
While Flutter supports six plaforms — Windows, Linux, macOS, Android, iOS and web — the official Flutter camera plugin is limited to just three: Android, iOS, and web. The development pace for the official plugin has been sluggish, with no clear roadmap for desktop support. As a result, creating a cross-platform barcode scanner application that encompasses all six platforms remains unfeasible. To solve this problem, we will design and implement a desktop Flutter camera plugin from scratch, utilizing our C++ litecam project.
This article is Part 6 in a 6-Part Series.
- Part 1 - How to Read Barcodes from a Linux Camera in C++ Without OpenCV
- Part 2 - C++ Windows Webcam Barcode Scanner Tutorial: Windows Media Foundation API and Dynamsoft SDK
- Part 3 - How to Build a macOS Camera Barcode Scanner in C++ Using AVFoundation
- Part 4 - How to Read Multiple Barcodes from a Camera Using Python and Dynamsoft Barcode Reader
- Part 5 - How to Build a Node.js Camera SDK and Barcode Scanner
- Part 6 - How to Build a Lightweight Flutter Camera Plugin for Windows, Linux, and macOS
Desktop Flutter Multi-Barcode Scanner Demo
Scaffolding a Flutter Plugin Project for Windows, Linux, and macOS
Run the following command to create a new Flutter plugin project named flutter_lite_camera. This project will include platform-specific folders and initial code for Windows, Linux, and macOS:
flutter create --org com.example --template=plugin --platforms=linux,windows,macos flutter_lite_camera
The supported programming languages for Windows and Linux are C++, while macOS uses Swift. The source code from the litecam project can be reused for Windows and Linux. For macOS, we need to adapt the Objective-C logic to Swift.
Defining the API for Desktop Camera Functionality
When using litecam, the camera feed is typically displayed in a system window. However, since Flutter has its own UI system, we need to render captured frames within a Flutter widget. To accomplish this, we define six essential functions — a pair of preview methods (startPreview() and stopPreview()) plus the four capture/control methods:
class FlutterLiteCamera {
Future<List<String>> getDeviceList() {
return FlutterLiteCameraPlatform.instance.getDeviceList();
}
Future<bool> open(int index) {
return FlutterLiteCameraPlatform.instance.open(index);
}
Future<int> startPreview() {
return FlutterLiteCameraPlatform.instance.startPreview();
}
Future<void> stopPreview() {
return FlutterLiteCameraPlatform.instance.stopPreview();
}
Future<Map<String, dynamic>> captureFrame() {
return FlutterLiteCameraPlatform.instance.captureFrame();
}
Future<void> release() {
return FlutterLiteCameraPlatform.instance.release();
}
}
Explanation:
getDeviceList(): Retrieves a list of available camera devices.open(int index): Opens the camera device with the specified index.startPreview(): Starts the native preview stream and returns a texture id that you can feed to a FlutterTexturewidget to render the live video feed.stopPreview(): Stops the preview stream and unregisters the texture.captureFrame(): Captures a frame as an RGB888 image. While a preview is running, the frame comes from a native cache, so grabbing a frame for image processing does not disturb the video stream.release(): Releases the camera device, freeing up resources.
Implementing the Native Interface for Desktop Platforms
The default target resolution is 640x480; each platform negotiates with the device and reports the actual frame width and height through the API. The plugin renders the live preview at the native layer using a Flutter texture, so no per-frame pixel data crosses the platform channel just to display the feed. When you need pixels for image processing (such as barcode decoding), captureFrame() returns a single RGB888 frame on demand without disturbing the preview stream.
Windows
- Copy the
Camera.handCameraWindows.cppfiles from thelitecamproject to thewindowsfolder. -
Update the
CMakelists.txtfile to include theCameraWindows.cppfile, thetexture_handlerfiles, and link the required libraries:... add_library(${PLUGIN_NAME} SHARED "include/flutter_lite_camera/flutter_lite_camera_plugin_c_api.h" "flutter_lite_camera_plugin_c_api.cpp" "CameraWindows.cpp" "texture_handler.cpp" "texture_handler.h" ${PLUGIN_SOURCES} ) ... target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) target_link_libraries(${PLUGIN_NAME} PRIVATE ole32 uuid mfplat mf mfreadwrite mfuuid) ...
The TextureHandler class wraps a flutter::PixelBufferTexture. The camera capture thread pushes RGBA frames with UpdateBuffer(), while the Flutter raster thread pulls them through the pixel-buffer callback:
```cpp
class TextureHandler
{
public:
explicit TextureHandler(flutter::TextureRegistrar *texture_registrar)
: texture_registrar_(texture_registrar) {}
int64_t RegisterTexture();
void UnregisterTexture();
// Called from the camera capture thread with RGBA8888 data.
void UpdateBuffer(const unsigned char *rgbaData, int width, int height);
private:
flutter::TextureRegistrar *texture_registrar_;
int64_t texture_id_ = -1;
std::unique_ptr<flutter::TextureVariant> texture_;
std::unique_ptr<FlutterDesktopPixelBuffer> flutter_desktop_pixel_buffer_;
std::mutex buffer_mutex_;
std::vector<unsigned char> source_buffer_; // RGBA8888
};
```
-
Implement the method channel logic in
flutter_lite_camera_plugin.cpp:#include "flutter_lite_camera_plugin.h" #include <windows.h> #include <VersionHelpers.h> #include <flutter/method_channel.h> #include <flutter/plugin_registrar_windows.h> #include <flutter/standard_method_codec.h> #include <memory> #include <sstream> #include <codecvt> namespace flutter_lite_camera { ... FlutterLiteCameraPlugin::FlutterLiteCameraPlugin(flutter::TextureRegistrar *texture_registrar) : texture_registrar_(texture_registrar) { camera = new Camera(); } FlutterLiteCameraPlugin::~FlutterLiteCameraPlugin() { StopPreview(); delete camera; } void FlutterLiteCameraPlugin::StopPreview() { camera->StopCaptureLoop(); if (texture_handler_) { texture_handler_->UnregisterTexture(); texture_handler_ = nullptr; } } void FlutterLiteCameraPlugin::HandleMethodCall( const flutter::MethodCall<flutter::EncodableValue> &method_call, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) { if (method_call.method_name().compare("getDeviceList") == 0) { std::vector<CaptureDeviceInfo> devices = ListCaptureDevices(); flutter::EncodableList deviceList; for (size_t i = 0; i < devices.size(); i++) { CaptureDeviceInfo &device = devices[i]; std::wstring wstr(device.friendlyName); int size_needed = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), NULL, 0, NULL, NULL); std::string utf8Str(size_needed, 0); WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), &utf8Str[0], size_needed, NULL, NULL); deviceList.push_back(flutter::EncodableValue(utf8Str)); } result->Success(flutter::EncodableValue(deviceList)); } else if (method_call.method_name().compare("open") == 0) { const auto *arguments = std::get_if<flutter::EncodableList>(method_call.arguments()); if (arguments && !arguments->empty()) { int index = std::get<int>((*arguments)[0]); StopPreview(); bool success = camera->Open(index); result->Success(flutter::EncodableValue(success)); } else { result->Error("InvalidArguments", "Expected camera index"); } } else if (method_call.method_name().compare("startPreview") == 0) { if (camera->IsStreaming() && texture_handler_ && texture_handler_->TextureRegistered()) { result->Success(flutter::EncodableValue(texture_handler_->texture_id())); return; } texture_handler_ = std::make_unique<TextureHandler>(texture_registrar_); int64_t texture_id = texture_handler_->RegisterTexture(); if (texture_id < 0) { texture_handler_ = nullptr; result->Error("TextureError", "Failed to register texture"); return; } TextureHandler *handler = texture_handler_.get(); bool started = camera->StartCaptureLoop( [handler](const unsigned char *rgbaData, int width, int height) { handler->UpdateBuffer(rgbaData, width, height); }); if (!started) { texture_handler_ = nullptr; result->Error("CameraError", "Failed to start preview. Is the camera open?"); return; } result->Success(flutter::EncodableValue(texture_id)); } else if (method_call.method_name().compare("stopPreview") == 0) { StopPreview(); result->Success(); } else if (method_call.method_name().compare("captureFrame") == 0) { FrameData frame = camera->CaptureFrame(); if (frame.rgbData) { flutter::EncodableMap frameMap; frameMap[flutter::EncodableValue("width")] = flutter::EncodableValue(frame.width); frameMap[flutter::EncodableValue("height")] = flutter::EncodableValue(frame.height); frameMap[flutter::EncodableValue("data")] = flutter::EncodableValue(std::vector<uint8_t>(frame.rgbData, frame.rgbData + frame.size)); ReleaseFrame(frame); result->Success(flutter::EncodableValue(frameMap)); } else { result->Error("CaptureFailed", "Failed to capture frame"); } } else if (method_call.method_name().compare("release") == 0) { StopPreview(); camera->Release(); result->Success(); } else { result->NotImplemented(); } } }
Linux
- Copy the
Camera.handCameraLinux.cppfiles from thelitecamproject to thelinuxfolder. -
Update the
CMakelists.txtfile to include theCameraLinux.cppfile and link the required libraries:... list(APPEND PLUGIN_SOURCES "flutter_lite_camera_plugin.cc" "camera_texture.cc" "camera_texture.h" ) add_library(${PLUGIN_NAME} SHARED "CameraLinux.cpp" ${PLUGIN_SOURCES} ) ... target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) ... -
Implement the method channel logic in
flutter_lite_camera_plugin.cc:#include "include/Camera.h" #include "include/flutter_lite_camera/flutter_lite_camera_plugin.h" #include <flutter_linux/flutter_linux.h> #include <gtk/gtk.h> #include <sys/utsname.h> #include <cstring> #include "flutter_lite_camera_plugin_private.h" #define FLUTTER_LITE_CAMERA_PLUGIN(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), flutter_lite_camera_plugin_get_type(), \ FlutterLiteCameraPlugin)) struct _FlutterLiteCameraPlugin { GObject parent_instance; Camera *camera; FlTextureRegistrar *texture_registrar; CameraTexture *texture; int64_t texture_id; gint mark_pending; }; G_DEFINE_TYPE(FlutterLiteCameraPlugin, flutter_lite_camera_plugin, g_object_get_type()) // Runs on the main thread; scheduled from the capture thread via g_idle_add. static gboolean mark_frame_available_cb(gpointer user_data) { FlutterLiteCameraPlugin *self = FLUTTER_LITE_CAMERA_PLUGIN(user_data); if (self->texture_registrar != nullptr && self->texture != nullptr) { fl_texture_registrar_mark_texture_frame_available(self->texture_registrar, FL_TEXTURE(self->texture)); } g_atomic_int_set(&self->mark_pending, 0); return G_SOURCE_REMOVE; } static void stop_preview(FlutterLiteCameraPlugin *self) { // Join the capture thread first so no frame callback can run while the // texture is being torn down. self->camera->StopCaptureLoop(); if (self->texture_registrar != nullptr && self->texture != nullptr) { fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture)); self->texture_id = -1; } g_clear_object(&self->texture); } static void flutter_lite_camera_plugin_handle_method_call( FlutterLiteCameraPlugin *self, FlMethodCall *method_call) { g_autoptr(FlMethodResponse) response = nullptr; const gchar *method = fl_method_call_get_name(method_call); if (strcmp(method, "getDeviceList") == 0) { std::vector<CaptureDeviceInfo> devices = ListCaptureDevices(); FlValue *deviceList = fl_value_new_list(); for (const auto &device : devices) { FlValue *deviceName = fl_value_new_string(device.friendlyName); fl_value_append_take(deviceList, deviceName); } response = FL_METHOD_RESPONSE(fl_method_success_response_new(deviceList)); } else if (strcmp(method, "open") == 0) { FlValue *args = fl_method_call_get_args(method_call); FlValue *index = fl_value_get_list_value(args, 0); if (index) { int index_int = fl_value_get_int(index); stop_preview(self); bool success = self->camera->Open(index_int); response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(success))); } else { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGUMENTS", "Expected camera index", nullptr)); } } else if (strcmp(method, "startPreview") == 0) { if (self->texture != nullptr && self->texture_id >= 0) { response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(self->texture_id))); } else { self->texture = camera_texture_new(); if (!fl_texture_registrar_register_texture(self->texture_registrar, FL_TEXTURE(self->texture))) { g_clear_object(&self->texture); response = FL_METHOD_RESPONSE(fl_method_error_response_new("TEXTURE_ERROR", "Failed to register texture", nullptr)); } else { self->texture_id = fl_texture_get_id(FL_TEXTURE(self->texture)); FlutterLiteCameraPlugin *plugin = self; bool started = self->camera->StartCaptureLoop( [plugin](const unsigned char *rgbaData, int width, int height) { camera_texture_update_frame(plugin->texture, rgbaData, static_cast<uint32_t>(width), static_cast<uint32_t>(height)); // fl_texture_registrar_* must be called on the main thread. if (g_atomic_int_compare_and_exchange(&plugin->mark_pending, 0, 1)) { g_idle_add(mark_frame_available_cb, plugin); } }); if (!started) { fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture)); g_clear_object(&self->texture); self->texture_id = -1; response = FL_METHOD_RESPONSE(fl_method_error_response_new("CAMERA_ERROR", "Failed to start preview. Is the camera open?", nullptr)); } else { response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(self->texture_id))); } } } } else if (strcmp(method, "stopPreview") == 0) { stop_preview(self); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else if (strcmp(method, "captureFrame") == 0) { FrameData frame = self->camera->CaptureFrame(); if (frame.rgbData == nullptr) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("CAPTURE_FAILED", "No frame data available", nullptr)); } else { FlValue *frameData = fl_value_new_map(); fl_value_set_take(frameData, fl_value_new_string("width"), fl_value_new_int(frame.width)); fl_value_set_take(frameData, fl_value_new_string("height"), fl_value_new_int(frame.height)); FlValue *rgbData = fl_value_new_uint8_list(frame.rgbData, frame.size); fl_value_set_take(frameData, fl_value_new_string("data"), rgbData); ReleaseFrame(frame); response = FL_METHOD_RESPONSE(fl_method_success_response_new(frameData)); } } else if (strcmp(method, "release") == 0) { stop_preview(self); self->camera->Release(); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } if (response == nullptr) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INTERNAL_ERROR", "Unexpected error", nullptr)); } fl_method_call_respond(method_call, response, nullptr); } static void flutter_lite_camera_plugin_dispose(GObject *object) { FlutterLiteCameraPlugin *self = FLUTTER_LITE_CAMERA_PLUGIN(object); stop_preview(self); delete self->camera; g_clear_object(&self->texture_registrar); G_OBJECT_CLASS(flutter_lite_camera_plugin_parent_class)->dispose(object); } static void flutter_lite_camera_plugin_init(FlutterLiteCameraPlugin *self) { self->camera = new Camera(); self->texture_registrar = nullptr; self->texture = nullptr; self->texture_id = -1; self->mark_pending = 0; } ...
macOS
-
Create a
CameraManager.swiftfile to implement the camera functionality:```swift import AVFoundation import FlutterMacOS import Foundation
class CameraManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { private var captureSession: AVCaptureSession? private var videoOutput: AVCaptureVideoDataOutput? private var captureDevice: AVCaptureDevice? private var frameWidth: Int = 640 private var frameHeight: Int = 480
/// Called on the capture queue for every incoming frame. The pixel buffer /// is BGRA and can be handed to a FlutterTexture directly. var onFrame: ((CVPixelBuffer) -> Void)?
private let bufferLock = NSLock() private var _latestPixelBuffer: CVPixelBuffer?
private var latestPixelBuffer: CVPixelBuffer? { get { bufferLock.lock() defer { bufferLock.unlock() } return _latestPixelBuffer } set { bufferLock.lock() _latestPixelBuffer = newValue bufferLock.unlock() } }
struct FrameData { var width: Int var height: Int var rgbData: Data }
override init() { super.init() }
func listDevices() -> [String] { let devices = AVCaptureDevice.devices() .filter { $0.hasMediaType(.video) } return devices.map { $0.localizedName } } func open(cameraIndex: Int) -> Bool { guard cameraIndex < AVCaptureDevice.devices(for: .video).count else { print("Camera index out of range.") return false } let devices = AVCaptureDevice.devices(for: .video) self.captureDevice = devices[cameraIndex] do { let input = try AVCaptureDeviceInput(device: self.captureDevice!) self.captureSession = AVCaptureSession() self.captureSession?.beginConfiguration() if self.captureSession?.canAddInput(input) == true { self.captureSession?.addInput(input) } else { print("Cannot add input to session.") return false } // Pick the format closest to the requested resolution if let format = self.captureDevice?.formats.min(by: { let d0 = CMVideoFormatDescriptionGetDimensions($0.formatDescription) let d1 = CMVideoFormatDescriptionGetDimensions($1.formatDescription) return abs(Int(d0.width) - self.frameWidth) + abs(Int(d0.height) - self.frameHeight) < abs(Int(d1.width) - self.frameWidth) + abs(Int(d1.height) - self.frameHeight) }) { try self.captureDevice?.lockForConfiguration() self.captureDevice?.activeFormat = format self.captureDevice?.unlockForConfiguration() let d = CMVideoFormatDescriptionGetDimensions(format.formatDescription) print("Resolution set to \(d.width)x\(d.height)") } else { print("\(self.frameWidth)x\(self.frameHeight) resolution not supported") } self.videoOutput = AVCaptureVideoDataOutput() self.videoOutput?.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ] self.videoOutput?.alwaysDiscardsLateVideoFrames = true if self.captureSession?.canAddOutput(self.videoOutput!) == true { self.captureSession?.addOutput(self.videoOutput!) self.videoOutput?.setSampleBufferDelegate( self, queue: DispatchQueue.global(qos: .userInteractive)) } else { print("Cannot add video output to session.") return false } self.captureSession?.commitConfiguration() self.captureSession?.startRunning() return true } catch { print("Error initializing camera: \(error.localizedDescription)") return false } }
/// Converts the latest BGRA frame to RGB888 on demand. This runs only when /// a frame is explicitly requested (e.g. for barcode decoding) and never /// touches the preview path. func captureFrame() -> FrameData? { guard let pixelBuffer = latestPixelBuffer else { return nil }
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
return nil
}
var rgbData = Data(count: width * height * 3)
rgbData.withUnsafeMutableBytes { dstPointer in
let dst = dstPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
let src = baseAddress.assumingMemoryBound(to: UInt8.self)
for y in 0..<height {
let srcRow = src + y * bytesPerRow
let dstRow = dst + y * width * 3
for x in 0..<width {
// BGRA -> RGB
dstRow[x * 3] = srcRow[x * 4 + 2]
dstRow[x * 3 + 1] = srcRow[x * 4 + 1]
dstRow[x * 3 + 2] = srcRow[x * 4]
}
}
}
return FrameData(width: width, height: height, rgbData: rgbData)
}
func getWidth() -> Int {
if let buffer = latestPixelBuffer {
return CVPixelBufferGetWidth(buffer)
}
return self.frameWidth
}
func getHeight() -> Int {
if let buffer = latestPixelBuffer {
return CVPixelBufferGetHeight(buffer)
}
return self.frameHeight
}
func release() {
self.onFrame = nil
self.captureSession?.stopRunning()
self.captureSession = nil
self.videoOutput = nil
self.captureDevice = nil
self.latestPixelBuffer = nil
}
func captureOutput(
_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get pixel buffer.")
return
}
// Retain the buffer for on-demand captureFrame() conversions, then
// forward it to the preview texture without any pixel processing.
self.latestPixelBuffer = pixelBuffer
self.onFrame?(pixelBuffer)
}
}
```
-
Edit
flutter_lite_camera.podspecto link the required frameworks:Pod::Spec.new do |s| ... s.frameworks = ['AVFoundation', 'CoreMedia', 'CoreVideo'] ... end -
Implement the method channel logic in
FlutterLiteCameraPlugin.swift:import Cocoa import FlutterMacOS /// FlutterTexture backed by the latest camera CVPixelBuffer. The buffer is /// BGRA, which Flutter supports natively, so preview rendering is zero-copy. class CameraTexture: NSObject, FlutterTexture { private var pixelBuffer: CVPixelBuffer? private let lock = NSLock() /// Called on the Flutter raster thread. The returned buffer must be /// retained; Flutter releases it when done. func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { lock.lock() defer { lock.unlock() } guard let buffer = pixelBuffer else { return nil } return Unmanaged.passRetained(buffer) } /// Called on the camera capture queue. func update(_ buffer: CVPixelBuffer) { lock.lock() pixelBuffer = buffer lock.unlock() } } public class FlutterLiteCameraPlugin: NSObject, FlutterPlugin { private let cameraManager = CameraManager() private var textureRegistry: FlutterTextureRegistry? private var cameraTexture: CameraTexture? private var textureId: Int64 = -1 public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "flutter_lite_camera", binaryMessenger: registrar.messenger) let instance = FlutterLiteCameraPlugin() instance.textureRegistry = registrar.textures registrar.addMethodCallDelegate(instance, channel: channel) } private func stopPreview() { cameraManager.onFrame = nil if textureId >= 0 { textureRegistry?.unregisterTexture(textureId) textureId = -1 } cameraTexture = nil } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getDeviceList": result(cameraManager.listDevices()) case "open": if let args = call.arguments as? [Int], let index = args.first { stopPreview() result(cameraManager.open(cameraIndex: index)) } else { result(FlutterError(code: "INVALID_ARGUMENT", message: "Index required", details: nil)) } case "startPreview": if textureId >= 0 { result(textureId) return } guard let registry = textureRegistry else { result(FlutterError(code: "TEXTURE_ERROR", message: "Texture registry unavailable", details: nil)) return } let texture = CameraTexture() let id = registry.register(texture) self.cameraTexture = texture self.textureId = id cameraManager.onFrame = { [weak self, weak texture] pixelBuffer in guard let self = self, let texture = texture else { return } texture.update(pixelBuffer) self.textureRegistry?.textureFrameAvailable(id) } result(id) case "stopPreview": stopPreview() result(nil) case "captureFrame": if let frame = cameraManager.captureFrame() { result([ "width": frame.width, "height": frame.height, "data": frame.rgbData, ]) } else { result(FlutterError(code: "CAPTURE_FAILED", message: "No frame available", details: nil)) } case "release": stopPreview() cameraManager.release() result(nil) default: result(FlutterMethodNotImplemented) } } }
Building a Flutter Application to Display Camera Feed
Because the plugin now renders the preview at the native layer, displaying the camera feed in Flutter is much simpler: you feed the texture id returned by startPreview() to a Texture widget, and the native layer draws every frame directly. No pixel data crosses the platform channel just to show the feed.
-
Open the camera and start the preview, then store the returned texture id:
final FlutterLiteCamera _camera = FlutterLiteCamera(); int _textureId = -1; int _width = 640; int _height = 480; bool _isCameraOpened = false; Future<void> _startCamera() async { try { List<String> devices = await _camera.getDeviceList(); if (devices.isNotEmpty) { bool opened = await _camera.open(0); if (opened) { // The native layer renders the video feed into this texture; no // frame data crosses into Dart for display purposes. int textureId = await _camera.startPreview(); setState(() { _isCameraOpened = true; _textureId = textureId; }); } } } catch (e) { // Handle the error. } } Future<void> _stopCamera() async { if (_isCameraOpened) { await _camera.stopPreview(); await _camera.release(); setState(() { _isCameraOpened = false; _textureId = -1; }); } } -
Display the live feed with a
Texturewidget and keep the aspect ratio with aLayoutBuilder:@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; final imageAspectRatio = _width / _height; 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: Texture(textureId: _textureId), ), ); }, ) else const Center(child: Text('Camera not initialized')), ], ), ); } -
On macOS, grant camera access by adding the following key to
DebugProfile.entitlements(andRelease.entitlements):<key>com.apple.security.device.camera</key> <true/>
Integrating Multi-Barcode Scanning into the Flutter Application
Now that the desktop Flutter camera application is complete, we can integrate an image processing SDK like Dynamsoft Barcode Reader to enable multi-barcode scanning.
-
Add the
flutter_barcode_sdkpackage to your project by running the following command:flutter pub add flutter_barcode_sdk -
Visit the Dynamsoft website to obtain a 30-day free trial license key. Initialize the barcode reader as follows:
import 'package:flutter_barcode_sdk/flutter_barcode_sdk.dart'; FlutterBarcodeSdk? _barcodeReader; Future<void> initBarcodeSDK() async { _barcodeReader = FlutterBarcodeSdk(); await _barcodeReader!.setLicense(licenseKey); await _barcodeReader!.init(); } -
Decode 1D/2D barcodes from the captured frame using the flutter_barcode_sdk library. Frames are pulled from the native cache with
captureFrame(), so decoding does not disturb the preview stream:bool _shouldDecode = false; bool isDecoding = false; List<BarcodeResult>? results; Future<void> _decodeFrames() async { if (!_isCameraOpened || !_shouldDecode) return; if (!isDecoding && _barcodeReader != null) { isDecoding = true; try { Map<String, dynamic> frame = await _camera.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); } } -
Draw the detection results on top of the preview. Create a
CustomPaintpainter that renders each barcode’s bounding box and text:class ResultPainter extends CustomPainter { final List<BarcodeResult> results; final double scale; ResultPainter(this.results, this.scale); @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 path = Path() ..moveTo(result.x1.toDouble() * scale, result.y1.toDouble() * scale) ..lineTo(result.x2.toDouble() * scale, result.y2.toDouble() * scale) ..lineTo(result.x3.toDouble() * scale, result.y3.toDouble() * scale) ..lineTo(result.x4.toDouble() * scale, result.y4.toDouble() * scale) ..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, Offset(result.x1.toDouble() * scale, result.y1.toDouble() * scale), ); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }Then stack the painter over the
Texturewidget so the bounding boxes and text are drawn on top of the live feed:Texture(textureId: _textureId), CustomPaint( painter: ResultPainter(results ?? [], drawWidth / _width), child: Container(), ),
Source Code
Get the complete sample project source code on GitHub: flutter_lite_camera.