Usage Guide

April 19, 2026 · View on GitHub

Table of Contents

#SectionDescription
1Declaration / SetupXAML and code-behind setup
2Essential PropertiesKey properties reference
3Camera Lifecycle ManagementIsOn, Start(), global instance management
4Flash ControlPreview torch and capture flash modes
5Capture Format SelectionQuality presets, manual format, preview sync
6Opening Files in GalleryOpenFileInGallery()
7Camera SelectionFront/back/manual camera switching
8Captured Photo ProcessingCaptureSuccess event, TakePicture()
9Real-Time Effects & Custom ShadersBuilt-in effects, SKSL shaders
10Zoom ControlManual zoom, pinch-to-zoom
11Camera State ManagementStateChanged event, HardwareState
12Live ProcessingDrawing overlays on preview and recorded video, NewPreviewSet for processed-preview analysis
13Raw Frame ML HookZero-overhead GPU-accelerated raw frame access for ML/AI inference
14Permission HandlingNeedPermissions flags, CheckPermissions(), async helpers
15Complete MVVM ExampleFull ViewModel + Page example

1. Declaration / Setup

For installation please see README. Then you would be able to consume camera in your app.

The Canvas that would contain SkiaCamera must have its property RenderingMode = RenderingModeType.Accelerated:

XAML

xmlns:draw="http://schemas.appomobi.com/drawnUi/2023/draw"
xmlns:camera="clr-namespace:DrawnUi.Camera;assembly=DrawnUi.Maui.Camera"
    <draw:Canvas
        HorizontalOptions="Fill"
        VerticalOptions="Fill"
        Gestures="Lock"
        RenderingMode = "Accelerated">

<camera:SkiaCamera
    x:Name="CameraControl"
    BackgroundColor="Black"
    PhotoQuality="Medium"
    Facing="Default"
    HorizontalOptions="Fill"
    VerticalOptions="Fill"
    ZoomLimitMax="10"
    ZoomLimitMin="1" />

    </draw:Canvas>

Container tips:

  • Keep the container stable: no Auto rows, no unset width/height without Fill.
  • For correct saved video orientation, lock the app or the camera page to portrait. The UI can still respond to landscape rotation by rotating icons/controls — DrawnUI provides device orientation info at runtime.

Code-Behind

Example for use inside an already created Canvas:

var camera = new SkiaCamera
{
    BackgroundColor = Colors.Black,
    PhotoQuality = CaptureQuality.Medium,
    Facing = CameraPosition.Default,
    HorizontalOptions = LayoutOptions.Fill,
    VerticalOptions = LayoutOptions.Fill,
    ZoomLimitMax = 10,
    ZoomLimitMin = 1
};

2. Essential Properties

PropertyTypeDefaultDescription
FacingCameraPositionDefaultCamera selection: Default (back), Selfie (front), Manual
CameraIndexint-1Manual camera selection index (when Facing = Manual)
IsOnboolfalseCamera power state - use this to start/stop camera
PhotoQualityCaptureQualityMaxPhoto quality: Max, Medium, Low, Preview, Manual
PhotoFormatIndexint0Format index for manual capture (when PhotoQuality = Manual)
FlashModeFlashModeOffPreview torch mode: Off, On, Strobe
CaptureFlashModeCaptureFlashModeAutoFlash mode for capture: Off, Auto, On
IsFlashSupportedbool-Whether flash is available (read-only)
IsAutoFlashSupportedbool-Whether auto flash is supported (read-only)
EffectSkiaImageEffectNoneReal-time effects: None, Sepia, BlackAndWhite, Pastel
Zoomdouble1.0Camera zoom level
ZoomLimitMin/Maxdouble1.0/10.0Zoom constraints
StateHardwareStateOffCurrent camera state (read-only)
IsBusyboolfalseWhether camera is processing (read-only)

3. Camera Lifecycle Management

// Use IsOn for lifecycle management
camera.IsOn = true;  // Start camera
camera.IsOn = false; // Stop camera

Important: IsOn vs Start() difference:

  • IsOn = true: Proper lifecycle management, handles permissions, app backgrounding
  • Start(): Direct method call, bypasses safety checks

App backgrounding: The camera automatically turns off when the app goes to background and restores its state when the app resumes — no additional coding needed.

Global Instance Management

SkiaCamera maintains a static list of all active instances to prevent resource conflicts, especially on Windows where hardware release can be slow.

// Access all tracked camera instances
var activeCameras = SkiaCamera.Instances;

// Stop all active cameras globally
await SkiaCamera.StopAllAsync();

Automatic Conflict Resolution: When a new camera starts, it automatically checks SkiaCamera.Instances for other active cameras. If another camera is found to be busy or stopping (common during Hot Reload or rapid page navigation), the new instance will wait until the hardware is fully released before attempting to initialize.

4. Flash Control

SkiaCamera provides comprehensive flash control for both preview torch and still image capture:

Preview Torch Control

camera.FlashMode = FlashMode.Off;   // Disable torch
camera.FlashMode = FlashMode.On;    // Enable torch
camera.FlashMode = FlashMode.Strobe; // Strobe mode (future feature)

var currentMode = camera.GetFlashMode();

Capture Flash Mode Control

camera.CaptureFlashMode = CaptureFlashMode.Off;   // No flash
camera.CaptureFlashMode = CaptureFlashMode.Auto;  // Auto flash based on lighting
camera.CaptureFlashMode = CaptureFlashMode.On;    // Always flash

if (camera.IsFlashSupported)
{
    if (camera.IsAutoFlashSupported)
    {
        camera.CaptureFlashMode = CaptureFlashMode.Auto;
    }
}

XAML Binding

<camera:SkiaCamera
    x:Name="CameraControl"
    FlashMode="Off"
    CaptureFlashMode="Auto"
    Facing="Default" />

Flash Mode Cycling Examples

// Preview torch cycling
private void OnTorchButtonClicked()
{
    var currentMode = camera.FlashMode;
    var nextMode = currentMode switch
    {
        FlashMode.Off => FlashMode.On,
        FlashMode.On => FlashMode.Off,
        FlashMode.Strobe => FlashMode.Off,
        _ => FlashMode.Off
    };
    camera.FlashMode = nextMode;
}

// Capture flash cycling
private void OnCaptureFlashButtonClicked()
{
    var currentMode = camera.CaptureFlashMode;
    var nextMode = currentMode switch
    {
        CaptureFlashMode.Off => CaptureFlashMode.Auto,
        CaptureFlashMode.Auto => CaptureFlashMode.On,
        CaptureFlashMode.On => CaptureFlashMode.Off,
        _ => CaptureFlashMode.Auto
    };
    camera.CaptureFlashMode = nextMode;
}

Important Notes:

  • FlashMode controls preview torch (live view)
  • CaptureFlashMode controls flash behavior during photo capture
  • These are independent - you can have torch off but capture flash on Auto
  • Flash capabilities vary by device and camera (front/back)

Flash Architecture

SkiaCamera implements a dual-channel flash system:

ChannelPropertyModesUse Case
Preview TorchFlashModeOff/On/StrobeIllumination while composing shots
Capture FlashCaptureFlashModeOff/Auto/OnOptimal lighting for still photos

Platform Implementation:

PlatformPreview TorchCapture FlashAuto Flash
AndroidFlashMode.TorchFlashMode.Single + ControlAEModeOnAutoFlash
iOS/macOSAVCaptureTorchModeAVCaptureFlashModeAuto mode
WindowsFlashControl.EnabledFlashControl.AutoAuto detection

5. Capture Format Selection

Quality Presets

camera.PhotoQuality = CaptureQuality.Max;     // Highest resolution
camera.PhotoQuality = CaptureQuality.Medium;  // Balanced quality/size
camera.PhotoQuality = CaptureQuality.Low;     // Fastest capture
camera.PhotoQuality = CaptureQuality.Preview; // Smallest usable size

Manual Format Selection

var formats = await camera.GetAvailableCaptureFormatsAsync();

var options = formats.Select((format, index) =>
    $"[{index}] {format.Width}x{format.Height}, {format.AspectRatioString}"
).ToArray();

var result = await DisplayActionSheet("Select Capture Format", "Cancel", null, options);

if (!string.IsNullOrEmpty(result))
{
    var selectedIndex = Array.IndexOf(options, result);
    if (selectedIndex >= 0)
    {
        camera.PhotoQuality = CaptureQuality.Manual;
        camera.PhotoFormatIndex = selectedIndex;
    }
}

Reading Current Format

var currentFormat = camera.CurrentStillCaptureFormat;
if (currentFormat != null)
{
    Debug.WriteLine($"Current capture format: {currentFormat.Description}");
    Debug.WriteLine($"Resolution: {currentFormat.Width}x{currentFormat.Height}");
    Debug.WriteLine($"Aspect ratio: {currentFormat.AspectRatioString}");
}

Automatic Preview Synchronization

When you change capture format, preview automatically adjusts to match the aspect ratio:

camera.PhotoQuality = CaptureQuality.Manual;
camera.PhotoFormatIndex = 2; // Select 4000x3000 (4:3)
// Preview automatically switches to 4:3 — true WYSIWYG

Format Caching

var formats = await camera.GetAvailableCaptureFormatsAsync(); // Fast - uses cache
await camera.RefreshAvailableCaptureFormatsAsync(); // Slower - re-detects

// Cache is automatically cleared when camera facing or index changes
private async void OnCaptureSuccess(object sender, CapturedImage captured)
{
    try
    {
        var fileName = $"photo_{DateTime.Now:yyyyMMdd_HHmmss}.jpg";
        var filePath = Path.Combine(FileSystem.Current.CacheDirectory, fileName);

        using var fileStream = File.Create(filePath);
        using var data = captured.Image.Encode(SKEncodedImageFormat.Jpeg, 90);
        data.SaveTo(fileStream);

        camera.OpenFileInGallery(filePath);
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error opening file in gallery: {ex.Message}");
    }
}

Platform Requirements:

  • Android: Requires FileProvider configuration (see README setup section)
  • iOS/macOS: Works out of the box
  • Windows: Opens with default photo viewer

7. Camera Selection

Automatic Selection

camera.Facing = CameraPosition.Default; // Back camera
camera.Facing = CameraPosition.Selfie;  // Front camera

private void SwitchCamera()
{
    if (camera.IsOn)
    {
        camera.Facing = camera.Facing == CameraPosition.Selfie
            ? CameraPosition.Default
            : CameraPosition.Selfie;
    }
}

Manual Camera Selection

var cameras = await camera.GetAvailableCamerasAsync();

foreach (var cam in cameras)
{
    Console.WriteLine($"Camera {cam.Index}: {cam.Name} ({cam.Position}) Flash: {cam.HasFlash}");
}

camera.Facing = CameraPosition.Manual;
camera.CameraIndex = 2; // Select third camera
camera.IsOn = true;

CameraInfo Class

public class CameraInfo
{
    public string Id { get; set; }           // Platform-specific camera ID
    public string Name { get; set; }         // Human-readable name
    public CameraPosition Position { get; set; } // Front/Back/Unknown
    public int Index { get; set; }           // Index for manual selection
    public bool HasFlash { get; set; }       // Flash capability
}

8. Captured Photo Processing

Basic Capture

camera.CaptureSuccess += OnCaptureSuccess;
camera.CaptureFailed += OnCaptureFailed;

private async void TakePicture()
{
    if (camera.State == HardwareState.On && !camera.IsBusy)
    {
        camera.FlashScreen(Color.Parse("#EEFFFFFF"));
        await camera.TakePicture().ConfigureAwait(false);
    }
}

private void OnCaptureSuccess(object sender, CapturedImage captured)
{
    var originalImage = captured.Image; // SKImage - full resolution
    var timestamp = captured.Time;
    var metadata = captured.Metadata;
    ProcessCapturedPhoto(originalImage, metadata);
}

Command-Based Capture (MVVM)

public ICommand CommandCapturePhoto => new Command(async () =>
{
    if (camera.State == HardwareState.On && !camera.IsBusy)
    {
        camera.FlashScreen(Color.Parse("#EEFFFFFF"));
        await camera.TakePicture().ConfigureAwait(false);
    }
});

9. Real-Time Effects & Custom Shaders

Built-in Effects

private void CycleEffects()
{
    var effects = new[]
    {
        SkiaImageEffect.None,
        SkiaImageEffect.Sepia,
        SkiaImageEffect.BlackAndWhite,
        SkiaImageEffect.Pastel,
        SkiaImageEffect.Custom
    };

    var currentIndex = Array.IndexOf(effects, camera.Effect);
    var nextIndex = (currentIndex + 1) % effects.Length;
    camera.Effect = effects[nextIndex];
}

Custom Shader Effects

public class CameraWithEffects : SkiaCamera
{
    private SkiaShaderEffect _shader;

    public void SetCustomShader(string shaderFilename)
    {
        if (_shader != null && VisualEffects.Contains(_shader))
            VisualEffects.Remove(_shader);

        _shader = new SkiaShaderEffect()
        {
            ShaderSource = shaderFilename,
            FilterMode = SKFilterMode.Linear
        };

        VisualEffects.Add(_shader);
        Effect = SkiaImageEffect.Custom;
    }

    public void ChangeShaderCode(string skslCode)
    {
        if (_shader != null)
            _shader.ShaderCode = skslCode; // Live shader editing!
    }
}

Apply Shader Effect to Captured Photo

Use RenderCapturedPhotoAsync to bake a shader effect into a still photo after capture:

private async void OnCaptureSuccess(object sender, CapturedImage captured)
{
    var imageWithEffect = await CameraControl.RenderCapturedPhotoAsync(
        captured,
        overlay: null,
        configureImage: image =>
        {
            var shaderEffect = new SkiaShaderEffect
            {
                ShaderSource = ShaderEffectHelper.GetFilename(CameraControl.VideoEffect),
            };
            image.VisualEffects.Add(shaderEffect);
        },
        useGpu: true);

    captured.Image.Dispose();
    captured.Image = imageWithEffect;
    SaveFinalPhotoInBackground(captured);
}

Use named arguments here because drawOverlay now sits before configureImage in the overload.

Shader Preview Grid (Like Instagram Filters)

<draw:SkiaLayout Type="Row" ItemsSource="{Binding ShaderItems}">
    <draw:SkiaLayout.ItemTemplate>
        <DataTemplate x:DataType="models:ShaderItem">
            <draw:SkiaShape WidthRequest="80" HeightRequest="80">
                <draw:SkiaImage
                    ImageBitmap="{Binding Source={x:Reference CameraControl}, Path=DisplayPreview}"
                    Aspect="AspectCover">
                    <draw:SkiaImage.VisualEffects>
                        <draw:SkiaShaderEffect ShaderSource="{Binding ShaderFilename}" />
                    </draw:SkiaImage.VisualEffects>
                </draw:SkiaImage>
            </draw:SkiaShape>
        </DataTemplate>
    </draw:SkiaLayout.ItemTemplate>
</draw:SkiaLayout>

10. Zoom Control

camera.Zoom = 2.0; // 2x zoom

private void ZoomIn()
{
    camera.Zoom = Math.Min(camera.Zoom + 0.2, camera.ZoomLimitMax);
}

private void ZoomOut()
{
    camera.Zoom = Math.Max(camera.Zoom - 0.2, camera.ZoomLimitMin);
}

// Pinch-to-zoom gesture
private void OnZoomed(object sender, ZoomEventArgs e)
{
    camera.Zoom = e.Value;
}

11. Camera State Management

camera.StateChanged += OnCameraStateChanged;

private void OnCameraStateChanged(object sender, HardwareState newState)
{
    switch (newState)
    {
        case HardwareState.Off:
            break;
        case HardwareState.On:
            break;
        case HardwareState.Error:
            break;
    }
}

if (camera.State == HardwareState.On)
{
    // Safe to perform camera operations
}

12. Live Processing: ProcessFrame & ProcessPreview

SkiaCamera provides two drawing callbacks for real-time overlay rendering, plus an event for read-only frame analysis:

Callback / EventTypeWhen It FiresUse Case
ProcessFrameAction<DrawableFrame>Each frame being encoded to videoWatermarks, telemetry, overlays baked into recorded video
ProcessPreviewAction<DrawableFrame>Each preview frame before displayShow overlays on live preview (e.g., gauges, guides)
NewPreviewSetEventHandler<LoadedImageSource>Each preview frame after displayRead-only analysis of displayed preview, UI validation, processed-preview scanning

Key Insight: ProcessFrame draws on what gets recorded. ProcessPreview draws on what the user sees. NewPreviewSet lets you read already processed preview frames. All three are independent.

ProcessFrame (Video Recording Overlay)

Draws on each frame being encoded to the video file. Requires UseRealtimeVideoProcessing = true. Scale is always 1.0 (full recording resolution).

camera.UseRealtimeVideoProcessing = true;
camera.ProcessFrame = (frame) =>
{
    using var paint = new SKPaint
    {
        Color = SKColors.White.WithAlpha(128),
        TextSize = 48 * frame.Scale,
        IsAntialias = true
    };
    frame.Canvas.DrawText($"REC {frame.Time:mm\\:ss}", 50, 100, paint);
};

ProcessPreview (Live Preview Overlay)

Draws on each preview frame before it is displayed to the user.

camera.ProcessPreview = (frame) =>
{
    using var paint = new SKPaint
    {
        Color = SKColors.LimeGreen,
        TextSize = 48 * frame.Scale,
        IsAntialias = true
    };
    frame.Canvas.DrawText("READY", 50 * frame.Scale, 100 * frame.Scale, paint);
};

Using Both Together

void DrawOverlay(DrawableFrame frame)
{
    var s = frame.Scale; // 1.0 for recording, PreviewScale for preview
    using var paint = new SKPaint
    {
        Color = SKColors.White,
        TextSize = 48 * s,
        IsAntialias = true
    };

    frame.Canvas.DrawText($"{frame.Time:mm\\:ss}", 50 * s, 100 * s, paint);

    if (!frame.IsPreview)
    {
        frame.Canvas.DrawText("WATERMARK", 50 * s, 160 * s, paint);
    }
}

camera.UseRealtimeVideoProcessing = true;
camera.ProcessFrame = DrawOverlay;
camera.ProcessPreview = DrawOverlay;

Reuse ProcessFrame / ProcessPreview on Captured Still Photo

RenderCapturedPhotoAsync can now run the same Action<DrawableFrame> callback over a captured still photo. drawOverlay runs after the captured image is rendered and before any optional SkiaLayout overlay is rendered.

private async void OnCaptureSuccess(object sender, CapturedImage captured)
{
    var imageWithOverlay = await CameraControl.RenderCapturedPhotoAsync(
        captured,
        overlay: null,
        drawOverlay: CameraControl.ProcessFrame,
        useGpu: true);

    captured.Image.Dispose();
    captured.Image = imageWithOverlay;
}

Notes:

  • The generated still-photo DrawableFrame uses callback-space dimensions for the replayed overlay viewport.
  • For rotated stills, drawOverlay is replayed using the captured device orientation so reused preview/recording overlay code sees the expected viewport orientation.
  • Scale defaults to 1.0 unless you pass a custom value.
  • IsPreview is currently false, so draw-overlay code follows the non-preview branch.

Add a Pre-Overlay Still Stage

Use the new composeBase overload when your live preview path depends on a canvas composition step before ProcessPreview.

private async void OnCaptureSuccess(object sender, CapturedImage captured)
{
    using var sepiaPaint = new SKPaint { ColorFilter = SKColorFilter.CreateColorMatrix(_sepiaMatrix) };

    var imageWithPreviewStyle = await CameraControl.RenderCapturedPhotoAsync(
        captured,
        composeBase: (canvas, frameImage) =>
        {
            canvas.DrawImage(frameImage, 0, 0, sepiaPaint);
        },
        drawOverlay: CameraControl.ProcessPreview,
        useGpu: true);

    captured.Image.Dispose();
    captured.Image = imageWithPreviewStyle;
}

Ordering for the full overload is:

  • configureImage configures the intermediate SkiaImage
  • composeBase composes the rendered still into the destination canvas
  • drawOverlay draws reusable DrawableFrame overlays in replayed callback space based on the captured device orientation
  • overlay.Render(...) draws any DrawnUI overlay tree

Performance note:

  • the extra preparation pass exists only when composeBase is supplied
  • existing overloads keep the direct-render path and do not spend time on the pre-overlay stage

DrawableFrame Properties

PropertyTypeDescription
CanvasSKCanvasSkiaSharp canvas for drawing on the frame
WidthintFrame width in pixels
HeightintFrame height in pixels
TimeTimeSpanElapsed time since recording started
IsPreviewbooltrue for preview frames, false for recording frames
Scalefloat1.0 for recording frames; PreviewScale for preview frames

For still-photo rendering via RenderCapturedPhotoAsync(..., drawOverlay: ...), DrawableFrame uses replayed callback-space dimensions, custom scale if provided, and currently sets IsPreview = false.

Camera Layout Coordinates

Two properties expose the camera's position on the canvas:

PropertyTypeDescription
DrawingRectSKRectFull bounding rect of the control on the canvas
DisplayRectSKRectActual image area when using Fit aspect — excludes letterbox bars

Use DisplayRect when mapping screen touch coordinates to camera frame coordinates, or when positioning overlays that must stay inside the actual video area.

NewPreviewSet Event (Read-Only Analysis)

Use NewPreviewSet when you want to inspect the displayed preview exactly as the user sees it.

What it sees:

  • Effect and preview shader output
  • ProcessPreview drawing
  • during recording, ProcessFrame overlays too when UseRecordingFramesForPreview = true

Use it for:

  • preview QA/inspection
  • scanning the final on-screen preview
  • workflows that intentionally depend on what the user sees

Do not use it when the model needs clean camera pixels independent from overlays/effects. For that use the raw-frame hook in section 13.

For AI/ML processing where you need to read preview frames without drawing on them:

camera.NewPreviewSet += OnNewPreviewFrame;

private void OnNewPreviewFrame(object sender, LoadedImageSource source)
{
    source.ProtectFromDispose = true;
    
    if (!_mlSemaphore.Wait(0))
    {
        source.ProtectFromDispose = false;
        return;
    }

    Task.Run(async () =>
    {
        try
        {
            await RunMLAsync(source.Image);
        }
        finally
        {
            source.ProtectFromDispose = false;
            source.Dispose();
            _mlSemaphore.Release();
        }
    });
}

13. Raw Frame ML Hook: OnRawFrameAvailable(RawCameraFrame) & TryGetRgba

When you need to run ML/AI inference on raw camera frames before any ProcessFrame overlay is composited, override OnRawFrameAvailable(RawCameraFrame frame) and call frame.TryGetRgba(...).

Why not NewPreviewSet? During recording with UseRecordingFramesForPreview = true (default), the preview already has ProcessFrame overlays baked in.

MemberKindDescription
OnRawFrameAvailable(RawCameraFrame frame)protected internal virtualCalled every frame with a temporary raw-frame context. Use frame.TryGetRgba(...) for portable AI/ML input.
frame.RawImageSKImage?Optional advanced access to the raw frame. May be null on zero-copy GPU paths. Valid only inside the callback.
frame.RotationintDegrees the caller must still rotate frame.RawImage by to reach display orientation. Ignore when you use frame.TryGetRgba(...).
frame.TryGetRgba(int targetWidth, int targetHeight, byte[] outputBuffer)boolGPU-accelerated scale + pixel readback into a pre-allocated RGBA8888 byte array.
frame.TryGetRgbaBytes(int targetWidth, int targetHeight, out byte[]? rgbaBytes)boolReturns an owned raw RGBA8888 buffer, useful for custom endpoints that accept raw pixels.
frame.TryGetJpeg(int targetWidth, int targetHeight, out byte[]? jpegBytes, int quality = 100)boolReturns a JPEG payload encoded from display-oriented frame pixels. Good fit for hosted multimodal APIs.
frame.TryGetPng(int targetWidth, int targetHeight, out byte[]? pngBytes)boolReturns a PNG payload encoded from display-oriented frame pixels when lossless upload is needed.

Platform Implementation

PlatformGPU mechanism
iOS / MacCatalystMetalPreviewScaler — Metal compute shader
Android (GPU)GlPreviewScalerglBlitFramebuffer + glReadPixels
Android (legacy)CPU SKSurface + DrawImage
WindowsGPU SKSurface backed by encoder's GRContext

Usage

public class MyCam : SkiaCamera
{
    private readonly byte[] _mlBuffer = new byte[224 * 224 * 4]; // RGBA8888
    private readonly SemaphoreSlim _mlSemaphore = new(1, 1);

    protected override void OnRawFrameAvailable(RawCameraFrame frame)
    {
        // MUST be called synchronously (Android GPU: EGL context is current)
        if (!frame.TryGetRgba(224, 224, _mlBuffer))
            return;

        if (!_mlSemaphore.Wait(0))
            return;

        var snapshot = _mlBuffer.ToArray();
        int rotation = frame.Rotation;

        Task.Run(() =>
        {
            try   { RunInference(snapshot, rotation); }
            finally { _mlSemaphore.Release(); }
        });
    }
}

Buffer Layout

outputBuffer is filled with raw RGBA8888 pixels, targetWidth * targetHeight * 4 bytes, top-to-bottom, no row padding.

Which export method to use

  • Use frame.TryGetRgba(...) when you already own a reusable buffer and want the fastest hot-loop path.
  • Use frame.TryGetRgbaBytes(...) when a custom backend expects raw RGBA8888 plus separate width/height metadata.
  • Use frame.TryGetJpeg(...) for hosted AI APIs that expect normal image payloads and do not need lossless data.
  • Use frame.TryGetPng(...) when the destination expects a standard image format but you want lossless encoding.

Comparison of raw-frame ML options

APIFires whenHas overlays?GPU path?Zero-alloc?
NewPreviewSetAfter preview displayYes (when recording)NoNo
ProcessPreview callbackPreview compositingPartialNoNo
OnRawFrameAvailable(RawCameraFrame) + frame.TryGetRgba(...)Before any compositingNeverYesYes

14. Permission Handling

All permission methods are static and operate on the main thread internally.

NeedPermissions flags enum

[Flags]
public enum NeedPermissions
{
    Camera     = 1,
    Gallery    = 2,
    Microphone = 4,
    Location   = 8
}

var flags = NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone;

CheckPermissions (shows system dialogs)

SkiaCamera.CheckPermissions(
    granted:    () => camera.IsOn = true,
    notGranted: () => ShowPermissionsError(),
    request:    NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

// Async wrapper
bool ok = await SkiaCamera.RequestPermissionsAsync(
    NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

CheckPermissionsGranted (silent, no dialogs)

SkiaCamera.CheckPermissionsGranted(
    granted:    () => camera.IsOn = true,
    notGranted: () => ShowOnboardingScreen(),
    request:    NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

// Async wrapper
bool alreadyGranted = await SkiaCamera.RequestPermissionsGrantedAsync(
    NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

NeedPermissionsSet (instance property)

Controls which permissions are checked automatically when the camera turns on via IsOn = true. Defaults to Camera | Gallery.

camera.NeedPermissionsSet = NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone;
camera.IsOn = true;

Typical Onboarding Flow

bool alreadyGranted = await SkiaCamera.RequestPermissionsGrantedAsync(
    NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

if (alreadyGranted)
{
    camera.IsOn = true;
}
else
{
    bool granted = await SkiaCamera.RequestPermissionsAsync(
        NeedPermissions.Camera | NeedPermissions.Gallery | NeedPermissions.Microphone);

    if (granted)
        camera.IsOn = true;
    else
        ShowOpenSettingsHint();
}

15. Complete MVVM Example

ViewModel

public class CameraViewModel : INotifyPropertyChanged, IDisposable
{
    private SkiaCamera _camera;

    public void AttachCamera(SkiaCamera camera)
    {
        if (_camera == null && camera != null)
        {
            _camera = camera;
            _camera.CaptureSuccess += OnCaptureSuccess;
            _camera.StateChanged += OnCameraStateChanged;
            _camera.NewPreviewSet += OnNewPreviewSet;
        }
    }

    public ICommand CommandCapturePhoto => new Command(async () =>
    {
        if (_camera?.State == HardwareState.On && !_camera.IsBusy)
        {
            _camera.FlashScreen(Color.Parse("#EEFFFFFF"));
            await _camera.TakePicture().ConfigureAwait(false);
        }
    });

    public ICommand CommandSwitchCamera => new Command(() =>
    {
        if (_camera?.IsOn == true)
        {
            _camera.Facing = _camera.Facing == CameraPosition.Selfie
                ? CameraPosition.Default
                : CameraPosition.Selfie;
        }
    });

    private void OnCaptureSuccess(object sender, CapturedImage captured)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            // Update UI with captured image
        });
    }

    public void Dispose()
    {
        if (_camera != null)
        {
            _camera.CaptureSuccess -= OnCaptureSuccess;
            _camera.StateChanged -= OnCameraStateChanged;
            _camera.NewPreviewSet -= OnNewPreviewSet;
            _camera = null;
        }
    }
}

Page Code-Behind

public partial class CameraPage : ContentPage
{
    private readonly CameraViewModel _viewModel;

    public CameraPage(CameraViewModel viewModel)
    {
        _viewModel = viewModel;
        BindingContext = _viewModel;
        InitializeComponent();
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        var camera = this.FindByName<SkiaCamera>("CameraControl");
        _viewModel.AttachCamera(camera);

        SkiaCamera.CheckPermissions(async (granted) =>
        {
            if (granted)
            {
                camera.IsOn = true;
            }
        });
    }
}