Introduce the ability to extend camera functionality - #3274
Conversation
…h as Barcode scanning
There was a problem hiding this comment.
Are we still supporting Tizen? it's not possible to build a MAUI app on tizen as it's stuck on Net6...
There was a problem hiding this comment.
Good point! I needed to patch this for the builds to pass but it won't do anything
There was a problem hiding this comment.
Pull request overview
This PR adds an extensibility mechanism to CameraView via attachable “scenarios” (e.g., barcode scanning), wires scenario handling into platform camera pipelines (Android/iOS), and provides a sample barcode-scanning page demonstrating the new capability.
Changes:
- Introduces
ICameraView.Scenarios/CameraView.Scenariosas a collection ofCameraScenarioinstances. - Adds
CameraManagerscenario management (add/remove/reset) plus platform hooks to integrate platform-specific outputs/use-cases. - Adds a new sample page and platform implementations for barcode scanning (including an Android MLKit package reference).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs | Adds Scenarios bindable collection to CameraView. |
| src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs | Exposes Scenarios on the camera view contract. |
| src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs | Maps Scenarios and reacts to collection changes to update camera pipeline. |
| src/CommunityToolkit.Maui.Camera/Primitives/CameraScenario.shared.cs | Introduces base scenario lifecycle API (Initialize, attach/detach hooks). |
| src/CommunityToolkit.Maui.Camera/Primitives/PlatformCameraScenario.shared.cs | Adds a platform-specialized scenario base type. |
| src/CommunityToolkit.Maui.Camera/Primitives/PlatformCameraScenario.android.cs | Declares Android-specific platform scenario surface (UseCase). |
| src/CommunityToolkit.Maui.Camera/Primitives/PlatformCameraScenario.macios.cs | Declares Apple-specific platform scenario surface (AVCaptureOutput). |
| src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs | Adds scenario list + add/remove/reset and preview refresh behavior. |
| src/CommunityToolkit.Maui.Camera/CameraManager.android.cs | Integrates additional UseCases into rebinding flow. |
| src/CommunityToolkit.Maui.Camera/CameraManager.macios.cs | Integrates scenario outputs into AVCaptureSession startup. |
| src/CommunityToolkit.Maui.Camera/CameraManager.net.cs | Adds scenario APIs for unsupported platforms (throws NotSupported). |
| src/CommunityToolkit.Maui.Camera/CameraManager.tizen.cs | Adjusts partial method signatures and adds scenario APIs (throws NotSupported). |
| samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs | Adds a sample gallery entry for barcode scanning. |
| samples/CommunityToolkit.Maui.Sample/ViewModels/Views/CameraView/BarcodeScanningViewModel.cs | New view model to display detected barcode text and refresh cameras. |
| samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/BarcodeScanningPage.xaml | New page wiring CameraView.Scenarios to a barcode-scanning scenario. |
| samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/BarcodeScanningPage.xaml.cs | New page code-behind to refresh camera list on appearing. |
| samples/CommunityToolkit.Maui.Sample/PlatformBarcodeScanningScenario.cs | Shared scenario surface (bindable Command) for barcode detection callbacks. |
| samples/CommunityToolkit.Maui.Sample/Platforms/PlatformBarcodeScanningScenario.cs | Adds a partial scenario file in Platforms folder. |
| samples/CommunityToolkit.Maui.Sample/Platforms/Android/PlatformBarcodeScanningScenario.cs | Android-specific scenario implementation using CameraX ImageAnalysis. |
| samples/CommunityToolkit.Maui.Sample/Platforms/Android/BarcodeAnalyzer.cs | Android barcode analyzer implementation using MLKit. |
| samples/CommunityToolkit.Maui.Sample/Platforms/iOS/PlatformBarcodeScanningScenario.cs | iOS-specific scenario using AVCaptureMetadataOutput. |
| samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/PlatformBarcodeScanningScenario.cs | Mac Catalyst-specific scenario using AVCaptureMetadataOutput. |
| samples/CommunityToolkit.Maui.Sample/MauiProgram.cs | Registers the new barcode scanning page + view model. |
| samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs | Adds shell mapping for barcode scanning page. |
| samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj | Adds Android-only MLKit barcode scanning package reference. |
| /// <param name="data">The raw frame data.</param> | ||
| /// <param name="width">The width of the frame.</param> | ||
| /// <param name="height">The height of the frame.</param> | ||
| public CameraFrame(byte[] data, int width, int height) |
There was a problem hiding this comment.
@bijington Is there any format that these bytes will conform to? Is it rgba etc? or will it be platform specific?
There was a problem hiding this comment.
I was wondering about this over the weekend. I think this will need to be more intelligent, I'll have a play this week
There was a problem hiding this comment.
One option can be to add an enum for the Format for the common native formats (YUV*, etc), set it appropriately for different platforms and leave it up to the handler to process as they wish, reduces any perf overhead.
There was a problem hiding this comment.
Yes I like this idea, or possibly provide a default converter and let the developer override, then we don't have to worry about support multiple formats?
| return; | ||
| } | ||
|
|
||
| var buffer = planes[0].Buffer; |
There was a problem hiding this comment.
@bijington I think this code will get only the Y plane (luminance) data, which will effectively just be grayscale. I think we should be getting 1 (U) and 2 (V) as well?
| return; | ||
| } | ||
|
|
||
| var data = new byte[buffer.Remaining()]; |
There was a problem hiding this comment.
@bijington Not sure if this is out of scope of this scenario, but allocating these byte arrays can end up in the LOH and cause GC slowdown with repeated allocations and releases (if it's anything like decoding video files on disk 😔).
Is it possible to augment FrameBasedCameraScenario to allow a consumer to optionally handle memory management?
For eg. FrameBasedCameraScenario can have a
protected virtual byte[] Allocate(int size) => new byte[size];
which can be plumbed through in FrameAnalyzer.Analyze rather than directly allocating?
That way, a consumer can override FrameBasedCameraScenario to use ArrayPool/provide some preallocated/reusable buffer, etc or any other kind of memory management for these bytes (by coordinating with the use of CameraFrame) to avoid GC slowdown.
Didn't want to re-implement the entire FrameBasedCamera scenario on all platforms just for memory management from PlatformCameraScenario when all the other pieces exist...
There was a problem hiding this comment.
For eg. FrameBasedCameraScenario can have a
protected virtual byte[] Allocate(int size) => new byte[size];
which can be plumbed through in FrameAnalyzer.Analyze rather than directly allocating?
I like the idea of giving extension points to our users. But it's better to have a good solution inside the library; most devs will shoot themselves in the foot trying to manage that. So we do provide a better solution, than new byte [size] and allow devs to implement their own if needed.
There was a problem hiding this comment.
@pictos What better solution do you suggest? Anything more than new byte[] will place constraints on the usage, lifetime of the byte array - which seems strange out of the box.
|
@peruchali it'll be great to hear your thoughts. I have made a couple of changes here based on your feedback:
I should add most of this is currently untested now but I am keen to iterate over the API and then we can get the underlying implementation working |
| /// </summary> | ||
| /// <param name="size">The size of the byte array to allocate.</param> | ||
| /// <returns>A new byte array of the specified size.</returns> | ||
| protected virtual byte[] Allocate(int size) => ArrayPool<byte>.Shared.Rent(size); |
There was a problem hiding this comment.
We can't do it, users may not know this is an ArrayPool array, and may not return it into the pool. So it can be worse than doing new byte[size];
There was a problem hiding this comment.
Also, arrays from ArrayPool have their length as a power of 2 base. So if you ask for 1000, it will give back an array with a length of 1024. And it can be with garbage (when you return or rent an array into the pool it isn't zeroed by default), so users can't foreach over it.
There was a problem hiding this comment.
Yes, and also the value of the required length of the byte[] is lost here and downstream (bytes.Length can be > than the required size). Having it out of the box will require adding a new member to CameraFrame containing the length, which makes it more complicated.
| /// Frees the specified byte array. | ||
| /// </summary> | ||
| /// <param name="data">The byte array to free.</param> | ||
| protected virtual void Free(byte[] data) => ArrayPool<byte>.Shared.Return(data); |
There was a problem hiding this comment.
this is unsafe, if the returned array isn't an array from ArrayPool, this will throw an exception. We can't control if the user will reasign this array or something like. For example:
var data = myFrame.Allocate(1024);
data = data.Where( x => x != 0).ToArray(); // this will allocate a new array
myFrame.Free(data); //this will throw, because the array doesn't belong into the pool| { | ||
| ImageAnalysis? imageAnalysis; | ||
|
|
||
| public override Task OnAttached(IExecutorService? cameraExecutor, ResolutionSelector? resolutionSelector) |
There was a problem hiding this comment.
I believe this should be ValueTask instead of Task
pictos
left a comment
There was a problem hiding this comment.
I've couple of more comments, but I'm not sure about this approach. Maybe I didn't fully understand it yet, but I feel the better would be to expose the CameraManager and let users customize the capabilities and handling there, like we have for MediaManager, this current approach adds so many levels of indirection that can be dangerous.
| /// <summary> | ||
| /// Base class for output based processing for camera related activities. | ||
| /// </summary> | ||
| public abstract class CameraScenario : BindableObject, IDisposable |
There was a problem hiding this comment.
Before moving forward with this, would be good to see if it's possible to work with more than one scenario at once. How the Camera control would behave, and so on
There was a problem hiding this comment.
I agree, I've got this on my list to try out
|
|
||
| var preferredFormatName = PreferredFormat switch | ||
| { | ||
| CameraFrameFormat.Nv12 => "NV12", |
There was a problem hiding this comment.
would be better to create a Constant file and put those strings there and reuse it?
| static readonly BindablePropertyKey isAvailablePropertyKey = | ||
| BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable); | ||
|
|
||
| internal static readonly BindablePropertyKey ScenariosPropertyKey = BindableProperty.CreateReadOnly(nameof(Scenarios), typeof(IList<CameraScenario>), typeof(CameraView), default(IList<CameraScenario>), |
There was a problem hiding this comment.
if users can't change the collection type, why not use the concrete type?
Thanks @pictos. I'm not stuck on this approach, just spiking it to see if it makes sense, so your feedback here is valuable! I'm also happy to go down the exposing of My current goal is to find a way to solve the following:
I think we are closing to having those abilities but then we can evaluate and simplify how we actually expose it |
|
@bijington for:
I'm not sure that the library out of the box can implement any efficient memory management (like ArrayPool) without at the same time introducing constraints on the usage and lifetime of the bytes of CameraFrame (for eg the user should be very careful with passing the bytes around rather than copying). The default should be To "soften" the blow of the "default" out-of-the-box experience just being the humble
Other requests:
|
|
@peruchali thanks. Yes I don't think I had fully considered my idea last night 😂. It might be safest to just revert to something very simple and let the user decide |
Current status
This it the remaining TODO list
Introduce the ability to extend camera functionality for concepts such as Barcode scanning.
Replaces the original PR attempt at #2841.
Description of Change
Camera Scenarios Usage Guide
The Community Toolkit for MAUI CameraView allows extending its functionality through "Scenarios". There are two primary ways to implement a scenario:
PlatformCameraScenarioandFrameBasedCameraScenario.PlatformCameraScenario
PlatformCameraScenariois designed for scenarios where you want to leverage platform-specific high-performance APIs (e.g., Google ML Kit on Android, Apple Vision framework on iOS, or Windows Media Capture APIs).When to use it
Implementation Example (Windows)
In the sample project,
PlatformBarcodeScanningScenarioon Windows uses theMediaFrameReaderto capture frames and decode them usingZXing.Netwithin the platform-specific code.FrameBasedCameraScenario
FrameBasedCameraScenariois a specialized version ofPlatformCameraScenariothat abstracts the frame capture process. It provides a platform-agnosticOnFrameReceivedmethod that gives you access to aCameraFramecontaining raw byte data.When to use it
ZXing.Net) that can process raw image data.Implementation Example (Shared)
In the sample project,
SharedBarcodeScanningScenariodemonstrates how to implement barcode scanning in shared code.Key Differences
ImageProxy,CMSampleBuffer)CameraFrame(Raw bytes)Usage in XAML
Both types of scenarios are used in the same way with the
CameraView.Linked Issues
PR Checklist
approved(bug) orChampioned(feature/proposal)mainat time of PRAdditional information
Example of it working on macOS
scanning.mov