How to Capture Photos, Pick Files, and Handle Media in .NET MAUI 10

Learn how to capture photos, pick files, and handle media in .NET MAUI 10. Covers MediaPicker with multi-select and compression, FilePicker for documents, and CameraView for custom camera experiences — with full code examples.

Why Media Handling Matters in Mobile Apps

Let's be honest — almost every mobile app eventually needs to deal with photos, videos, or documents. Whether you're building a social media feed, a document scanner, an expense tracker with receipt uploads, or a chat app, you need solid APIs for capturing images from the camera, selecting files from the gallery, and processing media before shipping it off to a server.

.NET MAUI 10 makes this a lot easier than it used to be. The built-in MediaPicker now supports multi-file selection and on-the-fly image compression, the FilePicker handles documents and custom file types across platforms, and the Community Toolkit's CameraView gives you a fully embeddable camera preview with zoom, flash, and recording controls. That's a pretty significant upgrade from what we had before.

This guide walks you through every major media scenario with working code examples, platform permission setup, and best practices for production apps targeting Android, iOS, macOS, and Windows.

Platform Permissions Setup

Before any media API will work, you've got to declare the correct permissions on each platform. Getting this wrong is (by far) the number-one source of runtime crashes in media-related code. I've seen teams spend hours debugging what turned out to be a missing manifest entry.

Android (Platforms/Android/AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Camera access -->
    <uses-permission android:name="android.permission.CAMERA" />

    <!-- Video recording -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

    <!-- Android 12 and below -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                     android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="32" />

    <!-- Android 13+ granular media permissions -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
</manifest>

Android 13 (API 33) replaced the blanket READ_EXTERNAL_STORAGE permission with granular media permissions. Always set android:maxSdkVersion="32" on the legacy permission so newer devices skip it entirely.

iOS and Mac Catalyst (Platforms/iOS/Info.plist)

<key>NSCameraUsageDescription</key>
<string>This app needs camera access to take photos and record videos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for video recording.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to select images and videos.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app saves captured photos to your photo library.</string>

Each description string gets shown to the user in the system permission dialog. Make them descriptive and specific to your app's purpose — vague messages are a fast track to App Store rejections.

Windows

Good news here: no additional manifest changes are required on Windows. The MediaPicker and FilePicker work out of the box with WinUI 3.

Capturing Photos and Videos with MediaPicker

The IMediaPicker interface lives in the Microsoft.Maui.Media namespace and gives you four core methods:

  • CapturePhotoAsync — Opens the device camera to take a photo
  • CaptureVideoAsync — Opens the device camera to record a video
  • PickPhotoAsync / PickPhotosAsync — Opens the gallery to select one or more photos
  • PickVideoAsync / PickVideosAsync — Opens the gallery to select one or more videos

Taking a Photo from the Camera

public async Task<string?> CapturePhotoAsync()
{
    if (!MediaPicker.Default.IsCaptureSupported)
        return null;

    var photo = await MediaPicker.Default.CapturePhotoAsync(
        new MediaPickerOptions { Title = "Take a Photo" });

    if (photo is null)
        return null;

    // Save to app cache directory
    var localPath = Path.Combine(FileSystem.CacheDirectory, photo.FileName);

    using var sourceStream = await photo.OpenReadAsync();
    using var destinationStream = File.Create(localPath);
    await sourceStream.CopyToAsync(destinationStream);

    return localPath;
}

Always check IsCaptureSupported before calling capture methods. Desktop environments and emulators without a camera will return false, and you don't want your app crashing on a user's laptop because of that.

Picking a Single Photo from the Gallery

public async Task<ImageSource?> PickPhotoAsync()
{
    var photo = await MediaPicker.Default.PickPhotoAsync();

    if (photo is null)
        return null;

    var stream = await photo.OpenReadAsync();
    return ImageSource.FromStream(() => stream);
}

Multi-Select and Compression: New in .NET MAUI 10

.NET MAUI 10 introduces two of the most requested MediaPicker features: multi-file selection and built-in image compression. Honestly, these were long overdue. Before .NET 10, both required platform-specific workarounds or third-party plugins that added complexity and maintenance burden.

Picking Multiple Photos with Processing Options

public async Task<List<string>> PickMultiplePhotosAsync()
{
    var results = await MediaPicker.Default.PickPhotosAsync(
        new MediaPickerOptions
        {
            Title = "Select Photos",
            SelectionLimit = 10,       // 0 = unlimited
            MaximumWidth = 1920,       // Resize wider images
            MaximumHeight = 1080,
            CompressionQuality = 80,   // 0-100, where 100 = best quality
            RotateImage = true,        // Apply EXIF rotation
            PreserveMetaData = true    // Keep GPS, date, camera info
        });

    var savedPaths = new List<string>();

    foreach (var file in results)
    {
        var localPath = Path.Combine(FileSystem.CacheDirectory, file.FileName);

        using var source = await file.OpenReadAsync();
        using var destination = File.Create(localPath);
        await source.CopyToAsync(destination);

        savedPaths.Add(localPath);
    }

    return savedPaths;
}

Understanding CompressionQuality

The CompressionQuality property accepts values from 0 to 100, but its behavior isn't quite what you'd expect:

  • JPEG images: The value directly controls lossy compression quality. A value of 80 strikes a nice balance between file size and visual quality.
  • PNG images: This one's a bit tricky. Values below 90 cause the image to be converted to JPEG for better compression. Values 90–99 scale down the PNG. A value of 100 preserves the original PNG format untouched.
  • Performance note: Compression runs on the device itself. On lower-end hardware, compressing many large images at once can cause noticeable delays. Consider processing images in batches or showing a progress indicator so users don't think the app froze.

Platform-Specific Caveats for Multi-Select

There are a few edge cases worth knowing about:

  • Android: Some custom gallery apps may not enforce SelectionLimit. Validate the count in your code and notify the user if they go over the limit.
  • iOS: When SelectionLimit is 0 (unlimited), certain iOS versions may cap the result set. This is tracked as a known issue in .NET 10 SR6.
  • Windows: SelectionLimit isn't supported at the OS level. Users can select as many files as they want.
  • Cancellation: When the user cancels a multi-select operation, the returned collection is empty — not null. A small detail, but it matters for your null checks.

Picking Documents and Custom File Types with FilePicker

While MediaPicker is designed for photos and videos, FilePicker handles everything else — PDFs, spreadsheets, JSON files, or whatever custom format your app needs.

Picking a Single File

public async Task<FileResult?> PickDocumentAsync()
{
    var result = await FilePicker.Default.PickAsync(new PickOptions
    {
        PickerTitle = "Select a Document",
        FileTypes = FilePickerFileType.Pdf
    });

    if (result is null)
        return null;

    // Use OpenReadAsync — FullPath may not always be a real file path
    using var stream = await result.OpenReadAsync();

    // Process the document stream...
    return result;
}

Picking Multiple Files

public async Task<IEnumerable<FileResult>?> PickMultipleDocumentsAsync()
{
    var results = await FilePicker.Default.PickMultipleAsync(new PickOptions
    {
        PickerTitle = "Select Files",
        FileTypes = FilePickerFileType.Images
    });

    return results;
}

Defining Custom File Types Per Platform

Here's where things get a little messy. Each platform uses a different format to identify file types — Android uses MIME types, iOS and macOS use UTType identifiers, and Windows uses plain file extensions. You end up with a dictionary like this:

var customFileTypes = new FilePickerFileType(
    new Dictionary<DevicePlatform, IEnumerable<string>>
    {
        { DevicePlatform.iOS, new[]
            {
                "com.microsoft.word.doc",
                "org.openxmlformats.wordprocessingml.document",
                "com.microsoft.excel.xls",
                "org.openxmlformats.spreadsheetml.sheet"
            }},
        { DevicePlatform.Android, new[]
            {
                "application/msword",
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                "application/vnd.ms-excel",
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            }},
        { DevicePlatform.WinUI, new[] { ".doc", ".docx", ".xls", ".xlsx" } },
        { DevicePlatform.macOS, new[] { "doc", "docx", "xls", "xlsx" } }
    });

var options = new PickOptions
{
    PickerTitle = "Select Office Documents",
    FileTypes = customFileTypes
};

var result = await FilePicker.Default.PickAsync(options);

It's verbose, sure, but it gives you precise control over what users can select on every platform.

Using FilePicker with Dependency Injection

For better testability, register IFilePicker in your DI container:

// In MauiProgram.cs
builder.Services.AddSingleton<IFilePicker>(FilePicker.Default);

Then inject it into your ViewModel or service class instead of calling FilePicker.Default directly. Your future self (and your unit tests) will thank you.

Building a Custom Camera Experience with CameraView

When the system camera app just isn't enough — maybe you need an in-app barcode scanner, a document capture frame, or a live camera preview with custom overlays — the Community Toolkit's CameraView gives you full control.

Installation

First, install the separate NuGet package:

dotnet add package CommunityToolkit.Maui.Camera

Then register it in MauiProgram.cs:

var builder = MauiApp.CreateBuilder();
builder
    .UseMauiApp<App>()
    .UseMauiCommunityToolkitCamera();  // Add this line

XAML Layout with Zoom and Flash Controls

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Class="MyApp.CameraPage">

    <Grid RowDefinitions="*, Auto" Padding="0">

        <!-- Camera preview fills the screen -->
        <toolkit:CameraView x:Name="cameraView"
                            Grid.Row="0"
                            CameraFlashMode="{Binding FlashMode}"
                            ZoomFactor="{Binding CurrentZoom}"
                            MediaCaptured="OnMediaCaptured" />

        <!-- Controls bar -->
        <HorizontalStackLayout Grid.Row="1"
                               HorizontalOptions="Center"
                               Spacing="20"
                               Padding="10">
            <Button Text="📸 Capture"
                    Clicked="OnCaptureClicked" />
            <Slider WidthRequest="150"
                    Value="{Binding CurrentZoom}"
                    Minimum="{Binding Source={x:Reference cameraView},
                              Path=SelectedCamera.MinimumZoomFactor,
                              FallbackValue=1}"
                    Maximum="{Binding Source={x:Reference cameraView},
                              Path=SelectedCamera.MaximumZoomFactor,
                              FallbackValue=5}" />
            <Picker Title="Flash"
                    ItemsSource="{Binding FlashModes}"
                    SelectedItem="{Binding FlashMode}" />
        </HorizontalStackLayout>

    </Grid>
</ContentPage>

Code-Behind for Capture

public partial class CameraPage : ContentPage
{
    public CameraPage()
    {
        InitializeComponent();
    }

    private async void OnCaptureClicked(object? sender, EventArgs e)
    {
        var fileName = $"photo_{DateTime.UtcNow:yyyyMMdd_HHmmss}.jpg";
        var filePath = Path.Combine(FileSystem.CacheDirectory, fileName);

        // CaptureImage saves directly to a file
        await cameraView.CaptureImage(
            CancellationToken.None);
    }

    private void OnMediaCaptured(object? sender,
        CommunityToolkit.Maui.Views.MediaCapturedEventArgs e)
    {
        // e.Media contains the captured image stream
        MainThread.BeginInvokeOnMainThread(async () =>
        {
            if (e.Media is not null)
            {
                var filePath = Path.Combine(
                    FileSystem.CacheDirectory,
                    $"capture_{DateTime.UtcNow:yyyyMMdd_HHmmss}.jpg");

                using var fileStream = File.Create(filePath);
                await e.Media.CopyToAsync(fileStream);

                await DisplayAlert("Saved", $"Photo saved to {filePath}", "OK");
            }
        });
    }
}

Switching Between Front and Rear Cameras

// Get all available cameras
var cameras = cameraView.AvailableCameras;

// Switch to front camera
var frontCamera = cameras?.FirstOrDefault(
    c => c.Position == CameraPosition.Front);

if (frontCamera is not null)
    cameraView.SelectedCamera = frontCamera;

Image Compression and Optimization for Uploads

Even with .NET MAUI 10's built-in CompressionQuality on MediaPicker, you'll sometimes need more control over image processing — especially when preparing images for API uploads where file size limits or bandwidth constraints come into play.

Resizing Images with Microsoft.Maui.Graphics

public static async Task<byte[]> ResizeImageAsync(
    Stream imageStream,
    int maxWidth,
    int maxHeight,
    float quality = 0.8f)
{
    var image = Microsoft.Maui.Graphics.Platform.PlatformImage
        .FromStream(imageStream);

    // Calculate proportional dimensions
    var ratioX = (double)maxWidth / image.Width;
    var ratioY = (double)maxHeight / image.Height;
    var ratio = Math.Min(ratioX, ratioY);

    var newWidth = (int)(image.Width * ratio);
    var newHeight = (int)(image.Height * ratio);

    // Resize the image
    var resized = image.Resize(newWidth, newHeight,
        ResizeMode.Fit);

    // Convert to JPEG bytes
    using var ms = new MemoryStream();
    await Task.Run(() =>
        resized.Save(ms, ImageFormat.Jpeg, quality));

    return ms.ToArray();
}

Platform-Specific Compression (When You Need Full Control)

For maximum control, you can drop down to platform-specific APIs through partial classes or conditional compilation. It's more work, but sometimes it's exactly what you need:

// Android: Bitmap.Compress
#if ANDROID
public static byte[] CompressAndroid(Stream imageStream, int quality)
{
    var bitmap = Android.Graphics.BitmapFactory.DecodeStream(imageStream);
    using var output = new MemoryStream();
    bitmap!.Compress(Android.Graphics.Bitmap.CompressFormat.Jpeg!, quality, output);
    bitmap.Dispose();
    return output.ToArray();
}
#endif

// iOS: UIImage.AsJPEG
#if IOS || MACCATALYST
public static byte[] CompressIos(Stream imageStream)
{
    var data = Foundation.NSData.FromStream(imageStream)!;
    var uiImage = UIKit.UIImage.LoadFromData(data)!;
    var compressed = uiImage.AsJPEG(0.75f); // 0.0 = max compression, 1.0 = min
    return compressed!.ToArray();
}
#endif

Recommended Compression Settings by Use Case

Here's a quick reference table I keep coming back to when tuning image sizes for different scenarios:

Use CaseMax WidthMax HeightQualityTypical Size
Profile avatar4004007030–60 KB
Chat image1024102475100–250 KB
Product photo1920108085300–600 KB
Document scan2480350890500 KB–1.5 MB
Full-resolution archiveOriginalOriginal1002–8 MB

Uploading Media to an API

Once you've captured or picked your media files, you'll typically need to upload them to a backend server. Here's a complete example using HttpClient with MultipartFormDataContent:

public async Task<bool> UploadMediaAsync(
    string apiUrl,
    IEnumerable<string> filePaths)
{
    using var httpClient = new HttpClient();
    using var content = new MultipartFormDataContent();

    foreach (var filePath in filePaths)
    {
        var fileBytes = await File.ReadAllBytesAsync(filePath);
        var fileContent = new ByteArrayContent(fileBytes);

        var fileName = Path.GetFileName(filePath);
        var mimeType = fileName.EndsWith(".png",
            StringComparison.OrdinalIgnoreCase) ? "image/png" : "image/jpeg";

        fileContent.Headers.ContentType =
            new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType);

        content.Add(fileContent, "files", fileName);
    }

    var response = await httpClient.PostAsync(apiUrl, content);
    return response.IsSuccessStatusCode;
}

Putting It All Together: A Complete Media Service

So, let's bring everything together. Here's a reusable service class that wraps all the media operations we've covered. Register it in your DI container and inject it wherever you need media functionality:

public interface IMediaService
{
    Task<string?> CapturePhotoAsync();
    Task<List<string>> PickPhotosAsync(int maxCount = 10);
    Task<FileResult?> PickDocumentAsync();
}

public class MediaService : IMediaService
{
    public async Task<string?> CapturePhotoAsync()
    {
        if (!MediaPicker.Default.IsCaptureSupported)
            return null;

        var photo = await MediaPicker.Default.CapturePhotoAsync();
        if (photo is null) return null;

        return await SaveToLocalAsync(photo);
    }

    public async Task<List<string>> PickPhotosAsync(int maxCount = 10)
    {
        var options = new MediaPickerOptions
        {
            SelectionLimit = maxCount,
            MaximumWidth = 1920,
            MaximumHeight = 1080,
            CompressionQuality = 80,
            RotateImage = true
        };

        var results = await MediaPicker.Default.PickPhotosAsync(options);
        var paths = new List<string>();

        foreach (var file in results)
        {
            var path = await SaveToLocalAsync(file);
            if (path is not null)
                paths.Add(path);
        }

        return paths;
    }

    public async Task<FileResult?> PickDocumentAsync()
    {
        return await FilePicker.Default.PickAsync(new PickOptions
        {
            PickerTitle = "Select a Document",
            FileTypes = FilePickerFileType.Pdf
        });
    }

    private static async Task<string?> SaveToLocalAsync(FileResult file)
    {
        var localPath = Path.Combine(FileSystem.CacheDirectory, file.FileName);

        using var source = await file.OpenReadAsync();
        using var destination = File.Create(localPath);
        await source.CopyToAsync(destination);

        return localPath;
    }
}

// Registration in MauiProgram.cs
builder.Services.AddSingleton<IMediaService, MediaService>();

Troubleshooting Common Issues

SecurityException on Android

If you get a Java.Lang.SecurityException when calling CapturePhotoAsync, you likely forgot to add the CAMERA permission to AndroidManifest.xml. Also verify that you're calling the method on the UI thread — this trips up more people than you'd think.

Massive File Size Difference on iOS (Capture vs. Pick)

This one catches a lot of developers off guard. Captured photos on iOS can be significantly larger (25+ MB) compared to picked photos (~5 MB) from the same camera. Why? Because CapturePhotoAsync returns the raw camera output, while the photo library stores optimized versions. Use the CompressionQuality and MaximumWidth/MaximumHeight options in .NET MAUI 10 to tame those file sizes, or apply manual compression after capture.

FilePicker Returns Null FullPath

On some platforms, FileResult.FullPath doesn't return a usable file system path. The fix is simple: always use OpenReadAsync() to access the file contents instead of reading from FullPath directly.

CameraView Shows Black Screen

This is usually one of two things. Either you forgot to call UseMauiCommunityToolkitCamera() in MauiProgram.cs, or the correct platform permissions aren't declared. On Android, also confirm that the CAMERA permission has actually been granted at runtime (not just declared in the manifest).

Frequently Asked Questions

How do I access the front camera in .NET MAUI?

The built-in MediaPicker uses the rear camera by default and doesn't expose a camera-selection option. To use the front camera, you'll need the Community Toolkit's CameraView instead. Query cameraView.AvailableCameras and set SelectedCamera to the one with Position == CameraPosition.Front.

Can I pick both photos and documents in a single picker dialog?

Not with a single API call, unfortunately. MediaPicker only handles images and videos, while FilePicker handles arbitrary file types. You can work around this by defining a custom FilePickerFileType that includes image MIME types alongside document types and using FilePicker as a unified picker — though you'll lose the nice gallery-style UI that MediaPicker provides.

What is the maximum number of files I can select with PickPhotosAsync?

Set SelectionLimit = 0 for unlimited selection. That said, Android and iOS may enforce their own limits depending on the gallery app and OS version. Always validate the returned count in your code rather than relying solely on SelectionLimit.

How do I reduce image file size before uploading to a server?

In .NET MAUI 10, the easiest approach is setting CompressionQuality, MaximumWidth, and MaximumHeight on MediaPickerOptions when picking or capturing images. For more granular control, use Microsoft.Maui.Graphics to resize and re-encode images after capture. A compression quality of 75–85 typically reduces file size by 60–80% with minimal visible quality loss.

Does MediaPicker work on Windows desktop apps?

Yes, it does. PickPhotoAsync and PickVideoAsync open the standard Windows file picker. CapturePhotoAsync and CaptureVideoAsync also work if the device has a webcam, but IsCaptureSupported will return false on machines without a camera.

About the Author Editorial Team

Our team of expert writers and editors.